usage/req.bend
cswap-bend-cli@0.1.0.0 · 190 lines · 28 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/http.bend as Http
3import cswap-bend-cli@0.1.0.0/json.bend as Json
4import cswap-bend-cli@0.1.0.0/usage/text.bend as UText
5
6
7
8
9
10
11type Err is Data:
12 Err{kind: String, retry: Maybe<&2, U32>, body: String}
13
14def retry.int(ps: List<&2, String>) -> Maybe<&2, U32>:
15 match ps:
16 case Con{i, t}:
17 U32.read(i)
18 case Nil{}:
19 None{}
20
21def retry.norm(m: Maybe<&2, String>) -> Maybe<&2, U32>:
22 match m:
23 case None{}:
24 None{}
25 case Some{n}:
26 retry.int(String.split(n, '.'))
27
28def retry.zero(m: Maybe<&2, String>) -> Maybe<&2, U32>:
29 match m:
30 case None{}:
31 None{}
32 case Some{n}:
33 Some{0}
34
35def retry.neg(neg: Bool, +raw: String) -> Maybe<&2, U32>:
36 match neg:
37 case True{}:
38 retry.zero(UText.float(String.drop(raw, 1n)))
39 case False{}:
40 retry.norm(UText.float(raw))
41
42def retry.of(m: Maybe<&2, String>) -> Maybe<&2, U32>:
43 match m:
44 case None{}:
45 None{}
46 case Some{+raw}:
47 retry.neg(String.starts_with(raw, "-"), raw)
48
49
50
51def retry_after(+head: String) -> Maybe<&2, U32>:
52 retry.of(Http.header("retry-after:", head))
53
54def http.err(+r: Http.Reply) -> Err:
55 match r:
56 case Http.Reply{st, head, body}:
57 Err{"http-" ++ U32.show(st), retry_after(head), body}
58
59def ok2xx(+st: U32) -> Bool:
60 U32.is_ge(st, 200) && U32.is_lt(st, 300)
61
62
63def scope_error(e: Err) -> Bool:
64 match e:
65 case Err{kind, retry, +body}:
66 String.eq(kind, "http-403") && (String.contains(body, "oauth_scope_insufficient") || String.contains(body, "scope requirement"))
67
68def is_429(e: Err) -> Bool:
69 match e:
70 case Err{kind, retry, body}:
71 String.eq(kind, "http-429")
72
73def bad(kind: String) -> Err:
74 Err{kind, None{}, ""}
75
76
77
78def probe.pick(data: Maybe<&2, Json.Json>, ok: Bool, e: Err) -> Result<&2, &2, Err, Json.Json>:
79 match data ok:
80 case Some{d} _:
81 Done{d}
82 case None{} True{}:
83 Fail{bad("bad-response")}
84 case None{} False{}:
85 Fail{e}
86
87def probe.reply(+r: Http.Reply) -> Result<&2, &2, Err, Json.Json>:
88 match r:
89 case Http.Reply{st, head, body}:
90 probe.pick(UText.usage_from_ratelimit_headers(head), ok2xx(st), http.err(r))
91
92def probe.result(r: Result<&2, &2, String, Http.Reply>) -> Result<&2, &2, Err, Json.Json>:
93 match r:
94 case Fail{k}:
95 Fail{bad(k)}
96 case Done{rep}:
97 probe.reply(rep)
98
99
100
101def probe(token: String) -> IO(Result<&2, &2, Err, Json.Json>):
102 IO.bind(Result<&2, &2, String, Http.Reply>, Result<&2, &2, Err, Json.Json>,
103 Http.request("POST", UText.messages_url(),
104 [("Authorization", "Bearer " ++ token), ("anthropic-version", "2023-06-01"),
105 ("anthropic-beta", "claude-code-20250219,oauth-2025-04-20"), ("content-type", "application/json"),
106 ("User-Agent", UText.user_agent())],
107 UText.probe.body(), 20),
108 r => IO.pure(Result<&2, &2, Err, Json.Json>, probe.result(r)))
109
110
111def body.obj(j: Json.Json) -> Result<&2, &2, Err, Json.Json>:
112 match j:
113 case Json.JObj{f}:
114 Done{j}
115 case _:
116 Fail{bad("AttributeError")}
117
118def body.json(ok: Result<&2, &2, String, Json.Json>) -> Result<&2, &2, Err, Json.Json>:
119 match ok:
120 case Fail{m}:
121 Fail{bad("bad-response")}
122 case Done{j}:
123 body.obj(j)
124
125def usage.pick(ok: Bool, +body: String, e: Err) -> Result<&2, &2, Err, Json.Json>:
126 match ok:
127 case True{}:
128 body.json(Json.parse(body))
129 case False{}:
130 Fail{e}
131
132def usage.reply(+r: Http.Reply) -> Result<&2, &2, Err, Json.Json>:
133 match r:
134 case Http.Reply{st, head, body}:
135 usage.pick(ok2xx(st), body, http.err(r))
136
137def usage.result(r: Result<&2, &2, String, Http.Reply>) -> Result<&2, &2, Err, Json.Json>:
138 match r:
139 case Fail{k}:
140 Fail{bad(k)}
141 case Done{rep}:
142 usage.reply(rep)
143
144def usage.get(token: String) -> IO(Result<&2, &2, Err, Json.Json>):
145 IO.bind(Result<&2, &2, String, Http.Reply>, Result<&2, &2, Err, Json.Json>,
146 Http.request("GET", UText.usage_url(),
147 [("Authorization", "Bearer " ++ token), ("anthropic-beta", "oauth-2025-04-20"), ("User-Agent", UText.user_agent())],
148 "", 5),
149 r => IO.pure(Result<&2, &2, Err, Json.Json>, usage.result(r)))
150
151
152def fallback.pick(p: Result<&2, &2, Err, Json.Json>, orig: Err) -> Result<&2, &2, Err, Json.Json>:
153 match p:
154 case Done{d}:
155 Done{d}
156 case Fail{e}:
157 Fail{orig}
158
159def fallback(+token: String, +orig: Err) -> IO(Result<&2, &2, Err, Json.Json>):
160 IO.bind(Result<&2, &2, Err, Json.Json>, Result<&2, &2, Err, Json.Json>, probe(token),
161 p => IO.pure(Result<&2, &2, Err, Json.Json>, fallback.pick(p, orig)))
162
163def route(scope: Bool, rl: Bool, e: Err, token: String) -> IO(Result<&2, &2, Err, Json.Json>):
164 match scope rl:
165 case True{} _:
166 probe(token)
167 case False{} True{}:
168 fallback(token, e)
169 case False{} False{}:
170 IO.pure(Result<&2, &2, Err, Json.Json>, Fail{e})
171
172def usage.next(r: Result<&2, &2, Err, Json.Json>, token: String) -> IO(Result<&2, &2, Err, Json.Json>):
173 match r:
174 case Done{d}:
175 IO.pure(Result<&2, &2, Err, Json.Json>, Done{d})
176 case Fail{+e}:
177 route(scope_error(e), is_429(e), e, token)
178
179def request_data.go(+token: String, inference_only: Bool) -> IO(Result<&2, &2, Err, Json.Json>):
180 match inference_only:
181 case True{}:
182 probe(token)
183 case False{}:
184 IO.bind(Result<&2, &2, Err, Json.Json>, Result<&2, &2, Err, Json.Json>, usage.get(token), r => usage.next(r, token))
185
186
187
188
189def request_usage_data(token: String, inference_only: Bool) -> IO(Result<&2, &2, Err, Json.Json>):
190 request_data.go(token, inference_only)