llm.bend
bend-kit-llm@0.7.0.0 · 608 lines · 0 definitions
1# LLM client for the Anthropic Messages and OpenAI Chat Completions APIs, with SSE streaming. Source: https://github.com/paymog/bend-kit/tree/main/llm
2import Base
3import bend-kit-llm@0.7.0.0/sse.bend as Sse
4import bend-kit-hairpin@0.7.0.0/hairpin.bend as Hairpin
5import bend-kit-retry@0.1.0.0/retry.bend as Retry
6import bend-kit-http@0.34.0.0/http.bend as Http
7import bend-kit-json@0.6.0.0/json.bend as Json
8import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes
9import bend-kit-encoding@0.4.0.0/encoding.bend as Enc
10
11# Anthropic: POST {base}/v1/messages. OpenAI: POST {base}/chat/completions, which many other providers serve too.
12type Api is Data:
13 Anthropic{}
14 OpenAI{}
15
16type Role is Data:
17 User{}
18 Assistant{}
19
20type Msg is Data:
21 Msg{role: Role, text: String}
22
23# system: "" for none. max: the most tokens to write.
24type Req is Data:
25 Req{model: String, system: String, msgs: List<&2, Msg>, max: U32}
26
27# stop: the API's reason, as "end_turn" or "stop". input and output count tokens.
28type Reply is Data:
29 Reply{id: String, model: String, text: String, stop: String, input: U32, output: U32}
30
31# ErrNet: the request did not complete, or Hairpin's breaker admitted no attempt. ErrApi: the API refused, with
32# its status and error text; an error event in a stream has status 200. ErrReply: a reply that is not the API's JSON, with its text.
33type Err is Data:
34 ErrNet{e: Hairpin.Err}
35 ErrApi{status: U32, body: String}
36 ErrReply{body: String}
37
38# headers go on every request. ms: the step timeout. retries: extra tries after a retryable failure.
39type Client is Type:
40 Client{api: Api, base: String, headers: Map<&2, List<&2, String>>, ms: U32, retries: U32, http: Hairpin.Client}
41
42# ---- JSON
43
44def str(s: String) -> Json.Val:
45 Json.Str{Json.utf8(s)}
46
47def kv(k: String, v: Json.Val) -> Bytes.Bytes & Json.Val:
48 (Json.utf8(k), v)
49
50def num(+n: U32) -> Json.Val:
51 Json.Num{Bytes.from_string(U32.show(n))}
52
53def obj(kvs: List<&1, Bytes.Bytes & Json.Val>) -> Json.Val:
54 Json.Obj{kvs}
55
56def when(on: Bool, x: Bytes.Bytes & Json.Val, xs: List<&1, Bytes.Bytes & Json.Val>) -> List<&1, Bytes.Bytes & Json.Val>:
57 match on:
58 case True{}:
59 Con{x, xs}
60 case False{}:
61 xs
62
63def role(r: Role) -> String:
64 match r:
65 case User{}:
66 "user"
67 case Assistant{}:
68 "assistant"
69
70def msg(+r: String, t: String) -> Json.Val:
71 obj([kv("role", str(r)), kv("content", str(t))])
72
73def msgs(xs: List<&2, Msg>) -> List<&1, Json.Val>:
74 match xs:
75 case Nil{}:
76 Nil{}
77 case Con{Msg{r, t}, rest}:
78 Con{msg(role(r), t), msgs(rest)}
79
80def sys(on: Bool, s: String, xs: List<&1, Json.Val>) -> List<&1, Json.Val>:
81 match on:
82 case True{}:
83 Con{msg("system", s), xs}
84 case False{}:
85 xs
86
87# The request body for api. stream asks for SSE.
88def body(api: Api, req: Req, stream: Bool) -> Json.Val:
89 match api:
90 case Anthropic{}:
91 Req{model, +system, ms, max} = req
92 obj(Con{kv("model", str(model)), Con{kv("max_tokens", num(max)),
93 when(Bool.not(String.is_empty(system)), kv("system", str(system)),
94 Con{kv("messages", Json.Arr{msgs(ms)}), when(stream, kv("stream", Json.Flag{True{}}), Nil{})})}})
95 case OpenAI{}:
96 Req{model, +system, ms, max} = req
97 obj(Con{kv("model", str(model)), Con{kv("max_completion_tokens", num(max)),
98 Con{kv("messages", Json.Arr{sys(Bool.not(String.is_empty(system)), system, msgs(ms))}),
99 when(stream, kv("stream", Json.Flag{True{}}), Nil{})}}})
100
101# A JSON leaf: its path, as "content.0.text", and its text.
102type Leaf is Data:
103 Leaf{p: String, v: String}
104
105def path(+p: String, +k: String) -> String:
106 Bool.pick(String, String.is_empty(p), k, p ++ "." ++ k)
107
108def push.arr(xs: List<&1, Json.Val>, +p: String, +i: U32, t: List<&1, String & Json.Val>) -> List<&1, String & Json.Val>:
109 match xs:
110 case Nil{}:
111 t
112 case Con{x, rest}:
113 Con{(path(p, U32.show(i)), x), push.arr(rest, p, (i + 1 : U32), t)}
114
115def push.obj(kvs: List<&1, Bytes.Bytes & Json.Val>, +p: String, t: List<&1, String & Json.Val>) -> List<&1, String & Json.Val>:
116 match kvs:
117 case Nil{}:
118 t
119 case Con{(k, x), rest}:
120 Con{(path(p, Enc.utf8.decode(k)), x), push.obj(rest, p, t)}
121
122# The leaves under the stack's values, last first. f: at least the values left.
123def flat(f: Nat, stack: List<&1, String & Json.Val>, acc: List<&2, Leaf>) -> List<&2, Leaf>:
124 match f:
125 case 0n:
126 acc
127 case 1n+g:
128 match stack:
129 case Nil{}:
130 acc
131 case Con{(p, v), t}:
132 match v:
133 case Json.Null{}:
134 flat(g, t, acc)
135 case Json.Flag{on}:
136 flat(g, t, Con{Leaf{p, Bool.pick(String, on, "true", "false")}, acc})
137 case Json.Num{s}:
138 flat(g, t, Con{Leaf{p, Bytes.to_string(s)}, acc})
139 case Json.Str{s}:
140 flat(g, t, Con{Leaf{p, Enc.utf8.decode(s)}, acc})
141 case Json.Arr{xs}:
142 flat(g, push.arr(xs, p, 0, t), acc)
143 case Json.Obj{kvs}:
144 flat(g, push.obj(kvs, p, t), acc)
145
146def doc.of(+n: U32, m: Maybe<&1, Json.Val>) -> Maybe<&2, List<&2, Leaf>>:
147 match m:
148 case None{}:
149 None{}
150 case Some{v}:
151 Some{List.reverse(&2, Leaf, flat(Nat.add(U32.to_nat(n), 1n), [("", v)], Nil{}))}
152
153# Every non-null leaf of a JSON text in document order, keyed by its path, as ("content.0.text", "Hi").
154# Strings are decoded; numbers keep their text. None when b is not JSON. Each value takes a byte, so n + 1 steps suffice.
155def doc(b: Bytes.Bytes) -> Maybe<&2, List<&2, Leaf>>:
156 Bytes.Bytes{+n, buf} = b
157 doc.of(n, Json.parse.bytes(Bytes.Bytes{n, buf}))
158
159# The first value at path k, or "".
160def look(d: List<&2, Leaf>, +k: String) -> String:
161 match d:
162 case Nil{}:
163 ""
164 case Con{Leaf{p, v}, t}:
165 Bool.pick(String, String.eq(p, k), v, look(t, k))
166
167def count(s: String) -> U32:
168 Maybe.default(&2, U32, Json.u32.num(s), 0)
169
170def dots(s: String, +n: U32) -> U32:
171 match s:
172 case SNil{}:
173 n
174 case SCon{Chr{+c}, t}:
175 dots(t, Bool.pick(U32, U32.is_eq(c, 46), (n + 1 : U32), n))
176
177# Is p the text of a content block, "content.<i>.text"?
178def block(+p: String) -> Bool:
179 Bool.and(Bool.and(String.starts_with(p, "content."), String.ends_with(p, ".text")), U32.is_eq(dots(p, 0), 2))
180
181# The text blocks of an Anthropic message, joined.
182def blocks(d: List<&2, Leaf>) -> String:
183 match d:
184 case Nil{}:
185 ""
186 case Con{Leaf{+p, v}, t}:
187 Bool.pick(String, block(p), v, "") ++ blocks(t)
188
189def reply.of(api: Api, +d: List<&2, Leaf>) -> Reply:
190 match api:
191 case Anthropic{}:
192 Reply{look(d, "id"), look(d, "model"), blocks(d), look(d, "stop_reason"),
193 count(look(d, "usage.input_tokens")), count(look(d, "usage.output_tokens"))}
194 case OpenAI{}:
195 Reply{look(d, "id"), look(d, "model"), look(d, "choices.0.message.content"), look(d, "choices.0.finish_reason"),
196 count(look(d, "usage.prompt_tokens")), count(look(d, "usage.completion_tokens"))}
197
198def reply.got(api: Api, b: Bytes.Bytes, m: Maybe<&2, List<&2, Leaf>>) -> Result<&1, &1, Err, Reply>:
199 match m:
200 case None{}:
201 Fail{ErrReply{Enc.utf8.decode(b)}}
202 case Some{d}:
203 Done{reply.of(api, d)}
204
205def reply.copy(api: Api, r: Bytes.Bytes & Bytes.Bytes) -> Result<&1, &1, Err, Reply>:
206 (b, c) = r
207 reply.got(api, b, doc(c))
208
209# A 2xx response body as a Reply.
210def reply(api: Api, b: Bytes.Bytes) -> Result<&1, &1, Err, Reply>:
211 reply.copy(api, Bytes.slice(b, 0, 4294967295))
212
213# ---- stream events
214
215# A text piece, the end, an error, or an event with no text.
216type Ev is Data:
217 EText{s: String}
218 EEnd{}
219 EFail{e: Err}
220 ESkip{}
221
222def ev.text(+s: String) -> Ev:
223 Bool.pick(Ev, String.is_empty(s), ESkip{}, EText{s})
224
225def ev.doc(m: Maybe<&2, List<&2, Leaf>>) -> List<&2, Leaf>:
226 Maybe.default(&2, List<&2, Leaf>, m, Nil{})
227
228def ev.anthropic(+name: String, data: Bytes.Bytes) -> Ev:
229 +d = ev.doc(doc(data))
230 Bool.pick(Ev, String.eq(name, "content_block_delta"), ev.text(look(d, "delta.text")),
231 Bool.pick(Ev, String.eq(name, "message_stop"), EEnd{},
232 Bool.pick(Ev, String.eq(name, "error"), EFail{ErrApi{200, look(d, "error.message")}}, ESkip{})))
233
234def ev.openai(+s: String) -> Ev:
235 +d = ev.doc(doc(Bytes.from_string(s)))
236 +why = look(d, "error.message")
237 Bool.pick(Ev, String.eq(s, "[DONE]"), EEnd{},
238 Bool.pick(Ev, String.is_empty(why), ev.text(look(d, "choices.0.delta.content")), EFail{ErrApi{200, why}}))
239
240# What one SSE event of api means.
241def ev(api: Api, e: Sse.Message) -> Ev:
242 match api:
243 case Anthropic{}:
244 Sse.Message{name, data, id} = e
245 ev.anthropic(name, data)
246 case OpenAI{}:
247 Sse.Message{name, data, id} = e
248 ev.openai(Bytes.to_string(data))
249
250# ---- requests
251
252def ok(+s: U32) -> Bool:
253 Bool.and(U32.is_le(200, s), U32.is_lt(s, 300))
254
255# Statuses a later try may clear: 408, 409, 429, and 5xx, which holds Anthropic's 529.
256def again.status(+s: U32) -> Bool:
257 Bool.or(Bool.or(U32.is_eq(s, 408), U32.is_eq(s, 409)), Bool.or(U32.is_eq(s, 429), U32.is_le(500, s)))
258
259# A refused connect or a timeout may clear.
260def again.err(+e: Http.Err) -> Maybe<&2, String>:
261 match e:
262 case Http.ErrConnect{code, why}:
263 Some{""}
264 case Http.ErrTimeout{}:
265 Some{""}
266 case other:
267 None{}
268
269def again.of(+status: U32, +h: Map<&2, List<&2, String>>) -> Maybe<&2, String>:
270 Bool.pick(Maybe<&2, String>, again.status(status), Some{Http.header(h, "retry-after")}, None{})
271
272# The result, with Some{Retry-After} when another try may help. Hairpin's judge for every call.
273def again(r: Result<&1, &1, Http.Err, Http.Res>) -> Result<&1, &1, Http.Err, Http.Res> & Maybe<&2, String>:
274 match r:
275 case Fail{+e}:
276 (Fail{e}, again.err(e))
277 case Done{res}:
278 Http.Res{+status, +h, b} = res
279 (Done{Http.Res{status, h, b}}, again.of(status, h))
280
281def body.ok(good: Bool, +status: U32, b: Bytes.Bytes) -> Result<&1, &1, Err, Bytes.Bytes>:
282 match good:
283 case True{}:
284 Done{b}
285 case False{}:
286 Fail{ErrApi{status, Enc.utf8.decode(b)}}
287
288# The 2xx body, or why there is none.
289def body.of(r: Result<&1, &1, Hairpin.Err, Http.Res>) -> Result<&1, &1, Err, Bytes.Bytes>:
290 match r:
291 case Fail{e}:
292 Fail{ErrNet{e}}
293 case Done{res}:
294 Http.Res{+status, h, b} = res
295 body.ok(ok(status), status, b)
296
297def url(api: Api, +base: String) -> String:
298 match api:
299 case Anthropic{}:
300 base ++ "/v1/messages"
301 case OpenAI{}:
302 base ++ "/chat/completions"
303
304# ---- the client
305
306def new(+api: Api, base: String, headers: Map<&2, List<&2, String>>) -> Client:
307 Client{api, base, headers, 600000, 2, Hairpin.timeout(Hairpin.new(), 600000)}
308
309def json(h: Map<&2, List<&2, String>>) -> Map<&2, List<&2, String>>:
310 Http.set(h, "content-type", "application/json")
311
312# base https://api.anthropic.com, 10 min steps, 2 retries.
313def anthropic(key: String) -> Client:
314 new(Anthropic{}, "https://api.anthropic.com", json(Http.set(Http.set(Http.empty(), "x-api-key", key), "anthropic-version", "2023-06-01")))
315
316# base https://api.openai.com/v1, 10 min steps, 2 retries.
317def openai(key: String) -> Client:
318 new(OpenAI{}, "https://api.openai.com/v1", json(Http.set(Http.empty(), "authorization", "Bearer " ++ key)))
319
320# Another base URL, as for a provider that serves the same API.
321def base(c: Client, url: String) -> Client:
322 Client{api, old, h, ms, n, http} = c
323 Client{api, url, h, ms, n, http}
324
325# A header on every request, as anthropic-beta. It replaces one of the same name.
326def header(c: Client, k: String, v: String) -> Client:
327 Client{api, b, h, ms, n, http} = c
328 Client{api, b, Http.set(h, String.to_lower(k), v), ms, n, http}
329
330def timeout(c: Client, +ms: U32) -> Client:
331 Client{api, b, h, old, n, http} = c
332 Client{api, b, h, ms, n, Hairpin.timeout(http, ms)}
333
334def retry(c: Client, n: U32) -> Client:
335 Client{api, b, h, ms, old, http} = c
336 Client{api, b, h, ms, n, http}
337
338def close(c: Client) -> IO(Unit):
339 Client{api, b, h, ms, n, http} = c
340 Hairpin.close(http)
341
342def call.done(+api: Api, base: String, headers: Map<&2, List<&2, String>>, +ms: U32, +n: U32,
343 x: Hairpin.Client & Retry.Budget & Result<&1, &1, Hairpin.Err, Http.Res>) -> Client & Result<&1, &1, Err, Bytes.Bytes>:
344 (h, b, r) = x
345 (Client{api, base, headers, ms, n, h}, body.of(r))
346
347def call.budget(+api: Api, +base: String, +headers: Map<&2, List<&2, String>>, body: Bytes.Bytes, y: Hairpin.Client & Retry.Budget) ->
348 IO(Hairpin.Client & Retry.Budget & Result<&1, &1, Hairpin.Err, Http.Res>):
349 (h, b) = y
350 do IO<Hairpin.Client & Retry.Budget & Result<&1, &1, Hairpin.Err, Http.Res>>:
351 enc : String <- Http.codings()
352 Hairpin.request.as(~again, h, b, True{}, enc, "POST", url(api, base), headers, body)
353
354# POST body to the API with n retries on a budget of n + 1; the 2xx body comes back.
355# These POSTs are safe to repeat, so Hairpin retries them as again judges.
356def call(c: Client, body: Bytes.Bytes) -> IO(Client & Result<&1, &1, Err, Bytes.Bytes>):
357 Client{+api, +base, +headers, +ms, +n, h} = c
358 do IO<Client & Result<&1, &1, Err, Bytes.Bytes>>:
359 y : Hairpin.Client & Retry.Budget <- Hairpin.budget(Hairpin.retry(h, n))
360 x : Hairpin.Client & Retry.Budget & Result<&1, &1, Hairpin.Err, Http.Res> <- call.budget(api, base, headers, body, y)
361 return call.done(api, base, headers, ms, n, x)
362
363def raw.got(b: Bytes.Bytes, m: Maybe<&1, Json.Val>) -> Result<&1, &1, Err, Json.Val>:
364 match m:
365 case None{}:
366 Fail{ErrReply{Enc.utf8.decode(b)}}
367 case Some{v}:
368 Done{v}
369
370def raw.copy(r: Bytes.Bytes & Bytes.Bytes) -> Result<&1, &1, Err, Json.Val>:
371 (b, c) = r
372 raw.got(b, Json.parse.bytes(c))
373
374def raw.of(r: Result<&1, &1, Err, Bytes.Bytes>) -> Result<&1, &1, Err, Json.Val>:
375 match r:
376 case Fail{e}:
377 Fail{e}
378 case Done{b}:
379 raw.copy(Bytes.slice(b, 0, 4294967295))
380
381def raw.done(x: Client & Result<&1, &1, Err, Bytes.Bytes>) -> Client & Result<&1, &1, Err, Json.Val>:
382 (c, r) = x
383 (c, raw.of(r))
384
385# Any request body, as for tools or images, and the reply as JSON.
386def send.raw(c: Client, v: Json.Val) -> IO(Client & Result<&1, &1, Err, Json.Val>):
387 do IO<Client & Result<&1, &1, Err, Json.Val>>:
388 x : Client & Result<&1, &1, Err, Bytes.Bytes> <- call(c, Json.encode.bytes(v))
389 return raw.done(x)
390
391def send.of(r: Result<&1, &1, Err, Bytes.Bytes>, +api: Api) -> Result<&1, &1, Err, Reply>:
392 match r:
393 case Fail{e}:
394 Fail{e}
395 case Done{b}:
396 reply(api, b)
397
398def send.done(+api: Api, x: Client & Result<&1, &1, Err, Bytes.Bytes>) -> Client & Result<&1, &1, Err, Reply>:
399 (c, r) = x
400 (c, send.of(r, api))
401
402# One whole reply.
403def send(c: Client, req: Req) -> IO(Client & Result<&1, &1, Err, Reply>):
404 Client{+api, b, h, ms, n, http} = c
405 do IO<Client & Result<&1, &1, Err, Reply>>:
406 x : Client & Result<&1, &1, Err, Bytes.Bytes> <- call(Client{api, b, h, ms, n, http}, Json.encode.bytes(body(api, req, False{})))
407 return send.done(api, x)
408
409# ---- streams
410
411type Stream is Type:
412 Stream{api: Api, h: Http.Stream, rd: Sse.Reader}
413
414def open.res(x: Http.Stream & Http.Res) -> Result<&1, &1, Http.Err, Http.Stream> & Maybe<&2, String>:
415 (st, res) = x
416 Http.Res{+status, +h, b} = res
417 (Done{st}, again.of(status, h))
418
419def open.judge(r: Result<&1, &1, Http.Err, Http.Stream>) -> Result<&1, &1, Http.Err, Http.Stream> & Maybe<&2, String>:
420 match r:
421 case Fail{+e}:
422 (Fail{e}, again.err(e))
423 case Done{st}:
424 open.res(Http.stream.res(st))
425
426# A refused stream's error body, at most f more pieces, then the stream closed.
427def drain(f: Nat, x: Http.Stream & Result<&1, &1, Http.Err, Maybe<&1, Bytes.Bytes>>, acc: Bytes.Bytes) -> IO(Bytes.Bytes):
428 match f:
429 case 0n:
430 (h, r) = x
431 do IO<Bytes.Bytes>:
432 Http.stream.close(h)
433 return acc
434 case 1n+g:
435 (h, r) = x
436 match r:
437 case Fail{e}:
438 do IO<Bytes.Bytes>:
439 Http.stream.close(h)
440 return acc
441 case Done{m}:
442 match m:
443 case None{}:
444 do IO<Bytes.Bytes>:
445 Http.stream.close(h)
446 return acc
447 case Some{b}:
448 do IO<Bytes.Bytes>:
449 y : Http.Stream & Result<&1, &1, Http.Err, Maybe<&1, Bytes.Bytes>> <- Http.stream.read(h)
450 drain(g, y, Bytes.append(acc, b))
451
452def start.refused(+status: U32, h: Http.Stream) -> IO(Result<&1, &1, Err, Http.Stream>):
453 do IO<Result<&1, &1, Err, Http.Stream>>:
454 y : Http.Stream & Result<&1, &1, Http.Err, Maybe<&1, Bytes.Bytes>> <- Http.stream.read(h)
455 b : Bytes.Bytes <- drain(64n, y, Bytes.new(0))
456 return Fail{ErrApi{status, Enc.utf8.decode(b)}}
457
458def start.ok(good: Bool, +status: U32, h: Http.Stream) -> IO(Result<&1, &1, Err, Http.Stream>):
459 match good:
460 case True{}:
461 IO.pure(Result<&1, &1, Err, Http.Stream>, Done{h})
462 case False{}:
463 start.refused(status, h)
464
465def start.res(x: Http.Stream & Http.Res) -> IO(Result<&1, &1, Err, Http.Stream>):
466 (h, res) = x
467 Http.Res{+status, hd, b} = res
468 start.ok(ok(status), status, h)
469
470def start(r: Result<&1, &1, Hairpin.Err, Http.Stream>) -> IO(Result<&1, &1, Err, Http.Stream>):
471 match r:
472 case Fail{e}:
473 IO.pure(Result<&1, &1, Err, Http.Stream>, Fail{ErrNet{e}})
474 case Done{h}:
475 start.res(Http.stream.res(h))
476
477def stream.of(r: Result<&1, &1, Err, Http.Stream>, +api: Api) -> Result<&1, &1, Err, Stream>:
478 match r:
479 case Fail{e}:
480 Fail{e}
481 case Done{h}:
482 Done{Stream{api, h, Sse.reader()}}
483
484def stream.done(+api: Api, base: String, headers: Map<&2, List<&2, String>>, +ms: U32, +n: U32,
485 x: Hairpin.Client & Retry.Budget & Result<&1, &1, Hairpin.Err, Http.Stream>) -> IO(Client & Result<&1, &1, Err, Stream>):
486 (h, b, r) = x
487 do IO<Client & Result<&1, &1, Err, Stream>>:
488 s : Result<&1, &1, Err, Http.Stream> <- start(r)
489 return (Client{api, base, headers, ms, n, h}, stream.of(s, api))
490
491def open.budget(+api: Api, +base: String, +headers: Map<&2, List<&2, String>>, body: Bytes.Bytes, y: Hairpin.Client & Retry.Budget) ->
492 IO(Hairpin.Client & Retry.Budget & Result<&1, &1, Hairpin.Err, Http.Stream>):
493 (h, b) = y
494 do IO<Hairpin.Client & Retry.Budget & Result<&1, &1, Hairpin.Err, Http.Stream>>:
495 enc : String <- Http.codings()
496 Hairpin.open.as(~open.judge, h, b, True{}, enc, "POST", url(api, base), headers, body)
497
498# Any request body that asks for SSE, as for tools. The open retries as call does, on a budget of n + 1.
499# A stream opens its own connection, outside the pool.
500def stream.raw(c: Client, v: Json.Val) -> IO(Client & Result<&1, &1, Err, Stream>):
501 Client{+api, +base, +headers, +ms, +n, h} = c
502 do IO<Client & Result<&1, &1, Err, Stream>>:
503 y : Hairpin.Client & Retry.Budget <- Hairpin.budget(Hairpin.retry(h, n))
504 x : Hairpin.Client & Retry.Budget & Result<&1, &1, Hairpin.Err, Http.Stream> <- open.budget(api, base, headers, Json.encode.bytes(v), y)
505 stream.done(api, base, headers, ms, n, x)
506
507# The reply as it is written: read it with next.
508def stream(c: Client, req: Req) -> IO(Client & Result<&1, &1, Err, Stream>):
509 Client{+api, b, h, ms, n, http} = c
510 stream.raw(Client{api, b, h, ms, n, http}, body(api, req, True{}))
511
512def stream.close(s: Stream) -> IO(Unit):
513 Stream{api, h, rd} = s
514 Http.stream.close(h)
515
516# The reader after the next event, or the stream's result.
517type Rn is Type:
518 RnNext{h: Http.Stream, r: Sse.Reader & Sse.Next}
519 RnOut{h: Http.Stream, rd: Sse.Reader, r: Result<&1, &1, Err, Maybe<&1, Sse.Message>>}
520
521def event.read(rd: Sse.Reader, x: Http.Stream & Result<&1, &1, Http.Err, Maybe<&1, Bytes.Bytes>>) -> Rn:
522 (h, r) = x
523 match r:
524 case Fail{e}:
525 RnOut{h, rd, Fail{ErrNet{Hairpin.ErrHttp{e}}}}
526 case Done{m}:
527 match m:
528 case None{}:
529 RnOut{h, rd, Done{None{}}}
530 case Some{b}:
531 RnNext{h, Sse.next(Sse.feed(rd, b))}
532
533# f: pieces left to read before the stream counts as stalled.
534def event.loop(f: Nat, st: Rn) -> IO(Http.Stream & Sse.Reader & Result<&1, &1, Err, Maybe<&1, Sse.Message>>):
535 match f:
536 case 0n:
537 match st:
538 case RnNext{h, r}:
539 (rd, n) = r
540 IO.pure(Http.Stream & Sse.Reader & Result<&1, &1, Err, Maybe<&1, Sse.Message>>, (h, rd, Fail{ErrReply{"too many pieces without an event"}}))
541 case RnOut{h, rd, r}:
542 IO.pure(Http.Stream & Sse.Reader & Result<&1, &1, Err, Maybe<&1, Sse.Message>>, (h, rd, r))
543 case 1n+g:
544 match st:
545 case RnNext{h, r}:
546 (rd, n) = r
547 match n:
548 case Sse.Got{e}:
549 IO.pure(Http.Stream & Sse.Reader & Result<&1, &1, Err, Maybe<&1, Sse.Message>>, (h, rd, Done{Some{e}}))
550 case Sse.Want{}:
551 do IO<Http.Stream & Sse.Reader & Result<&1, &1, Err, Maybe<&1, Sse.Message>>>:
552 x : Http.Stream & Result<&1, &1, Http.Err, Maybe<&1, Bytes.Bytes>> <- Http.stream.read(h)
553 event.loop(g, event.read(rd, x))
554 case RnOut{h, rd, r}:
555 IO.pure(Http.Stream & Sse.Reader & Result<&1, &1, Err, Maybe<&1, Sse.Message>>, (h, rd, r))
556
557def event.done(+api: Api, x: Http.Stream & Sse.Reader & Result<&1, &1, Err, Maybe<&1, Sse.Message>>) -> Stream & Result<&1, &1, Err, Maybe<&1, Sse.Message>>:
558 (h, rd, r) = x
559 (Stream{api, h, rd}, r)
560
561# The next SSE event, or None when the stream ends. An event the end cuts off is dropped.
562def event(s: Stream) -> IO(Stream & Result<&1, &1, Err, Maybe<&1, Sse.Message>>):
563 Stream{+api, h, rd} = s
564 do IO<Stream & Result<&1, &1, Err, Maybe<&1, Sse.Message>>>:
565 x : Http.Stream & Sse.Reader & Result<&1, &1, Err, Maybe<&1, Sse.Message>> <- event.loop(Nat.mul(65536n, 256n), RnNext{h, Sse.next(rd)})
566 return event.done(api, x)
567
568def next.ev(r: Result<&1, &1, Err, Maybe<&1, Sse.Message>>, +api: Api) -> Ev:
569 match r:
570 case Fail{e}:
571 EFail{e}
572 case Done{m}:
573 match m:
574 case None{}:
575 EEnd{}
576 case Some{e}:
577 ev(api, e)
578
579def next.of(x: Stream & Result<&1, &1, Err, Maybe<&1, Sse.Message>>) -> Stream & Ev:
580 (s, r) = x
581 Stream{+api, h, rd} = s
582 (Stream{api, h, rd}, next.ev(r, api))
583
584# f: events left to skip.
585def next.loop(f: Nat, x: Stream & Ev) -> IO(Stream & Result<&1, &1, Err, Maybe<&2, String>>):
586 match f:
587 case 0n:
588 (s, e) = x
589 IO.pure(Stream & Result<&1, &1, Err, Maybe<&2, String>>, (s, Fail{ErrReply{"too many events without text"}}))
590 case 1n+g:
591 (s, e) = x
592 match e:
593 case EText{t}:
594 IO.pure(Stream & Result<&1, &1, Err, Maybe<&2, String>>, (s, Done{Some{t}}))
595 case EEnd{}:
596 IO.pure(Stream & Result<&1, &1, Err, Maybe<&2, String>>, (s, Done{None{}}))
597 case EFail{err}:
598 IO.pure(Stream & Result<&1, &1, Err, Maybe<&2, String>>, (s, Fail{err}))
599 case ESkip{}:
600 do IO<Stream & Result<&1, &1, Err, Maybe<&2, String>>>:
601 y : Stream & Result<&1, &1, Err, Maybe<&1, Sse.Message>> <- event(s)
602 next.loop(g, next.of(y))
603
604# The next piece of reply text, or None at the end.
605def next(s: Stream) -> IO(Stream & Result<&1, &1, Err, Maybe<&2, String>>):
606 do IO<Stream & Result<&1, &1, Err, Maybe<&2, String>>>:
607 y : Stream & Result<&1, &1, Err, Maybe<&1, Sse.Message>> <- event(s)
608 next.loop(Nat.mul(65536n, 256n), next.of(y))