openai.bend
bend-openai-sdk@0.1.0.1 · 231 lines · 0 definitions
1# An unofficial OpenAI SDK for Bend, with typed requests, streaming, and tool calling.
2
3import Base
4import bend-openai-sdk@0.1.0.1/json.bend as J
5import bend-openai-sdk@0.1.0.1/transport.bend as T
6import bend-openai-sdk@0.1.0.1/docs.bend as Docs
7
8def repository() -> String:
9 Docs.repository()
10
11def version() -> String:
12 Docs.version()
13
14type Client is Data:
15 Client{port: U32, token: String}
16
17type Request is Data:
18 Request{model: String, input: J.Json, options: List<&2, J.Member<&2, J.Json>>}
19
20type Error is Data:
21 Error{code: String, message: String, status: String, request_id: String, retry_after: String}
22
23type Stream is Type:
24 Stream{connection: T.Connection, id: String, terminal: Bool}
25
26type Step is Type:
27 Event{stream: Stream, kind: String, data: J.Json, request_id: String}
28 Finished{}
29 Failed{error: Error}
30
31def text_request(model: String, input: String) -> Request:
32 Request{model, J.Text{input}, Nil{}}
33
34def params(request: Request) -> J.Json:
35 match request:
36 case Request{model, input, options}:
37 J.Object{Con{J.Field{"model", J.Text{model}}, Con{J.Field{"input", input}, options}}}
38
39def envelope(client: Client, id: String, stream: Bool, body: J.Json, operation: String) -> J.Json:
40 match client:
41 case Client{port, token}:
42 J.Object{[J.Field{"version", J.Number{"1"}}, J.Field{"token", J.Text{token}}, J.Field{"id", J.Text{id}}, J.Field{"operation", J.Text{operation}}, J.Field{"stream", J.Boolean{stream}}, J.Field{"params", body}]}
43
44def opened(result: Result<&1, &1, String, T.Connection>, id: String) -> Result<&1, &1, Error, Stream>:
45 match result:
46 case Fail{message}: Fail{Error{"transport_error", message, "0", "", ""}}
47 case Done{connection}: Done{Stream{connection, id, False{}}}
48
49def open_raw(+client: Client, +id: String, stream: Bool, body: J.Json) -> IO(Result<&1, &1, Error, Stream>):
50 match client:
51 case Client{port, token}:
52 do IO<Result<&1, &1, Error, Stream>>:
53 result : Result<&1, &1, String, T.Connection> <- T.connect(port, J.stringify(envelope(client, id, stream, body, "responses.create")))
54 return opened(result, id)
55
56def create(client: Client, id: String, request: Request) -> IO(Result<&1, &1, Error, Stream>):
57 open_raw(client, id, False{}, params(request))
58
59def stream(client: Client, id: String, request: Request) -> IO(Result<&1, &1, Error, Stream>):
60 open_raw(client, id, True{}, params(request))
61
62def close(stream: Stream) -> IO(Unit):
63 match stream:
64 case Stream{connection, id, terminal}: T.close(connection)
65
66def field(+frame: J.Json, key: String) -> String:
67 J.text(J.get(frame, key))
68
69def value_or_null(value: Maybe<&2, J.Json>) -> J.Json:
70 match value:
71 case Some{v}: v
72 case None{}: J.Null{}
73
74def json_field(frame: J.Json, key: String) -> J.Json:
75 value_or_null(J.get(frame, key))
76
77def fail(connection: T.Connection, code: String, message: String) -> IO(Step):
78 do IO<Step>:
79 T.close(connection)
80 return Failed{Error{code, message, "0", "", ""}}
81
82def finish(ok: Bool, connection: T.Connection) -> IO(Step):
83 match ok:
84 case True{}:
85 do IO<Step>:
86 T.close(connection)
87 return Finished{}
88 case False{}: fail(connection, "truncated_stream", "No result or terminal response event")
89
90def terminal_type(+name: String) -> Bool:
91 String.eq(name, "response.completed") || String.eq(name, "response.failed") || String.eq(name, "response.incomplete") || String.eq(name, "error")
92
93def status(value: J.Json) -> String:
94 match value:
95 case J.Number{s}: s
96 case _: "0"
97
98type FrameKind is Data:
99 EndFrame{}
100 ErrorFrame{}
101 ResultFrame{}
102 MetadataFrame{}
103 EventFrame{}
104 UnknownFrame{}
105
106def select_kind(ok: Bool, yes: FrameKind, no: FrameKind) -> FrameKind:
107 match ok:
108 case True{}: yes
109 case False{}: no
110
111def frame_kind(+kind: String) -> FrameKind:
112 select_kind(String.eq(kind, "end"), EndFrame{}, select_kind(String.eq(kind, "error"), ErrorFrame{},
113 select_kind(String.eq(kind, "result"), ResultFrame{}, select_kind(String.eq(kind, "metadata"), MetadataFrame{},
114 select_kind(String.eq(kind, "event"), EventFrame{}, UnknownFrame{})))))
115
116def step_kind(kind: FrameKind, connection: T.Connection, id: String, terminal: Bool, +frame: J.Json) -> IO(Step):
117 match kind:
118 case EndFrame{}: finish(terminal, connection)
119 case ErrorFrame{}:
120 do IO<Step>:
121 T.close(connection)
122 return Failed{Error{field(frame, "code"), field(frame, "message"), status(json_field(frame, "status")), field(frame, "request_id"), field(frame, "retry_after")}}
123 case ResultFrame{}: IO.pure(Step, Event{Stream{connection, id, True{}}, "result", json_field(frame, "data"), field(frame, "request_id")})
124 case MetadataFrame{}: IO.pure(Step, Event{Stream{connection, id, terminal}, "metadata", frame, field(frame, "request_id")})
125 case EventFrame{}:
126 +data = json_field(frame, "data")
127 +name = field(data, "type")
128 IO.pure(Step, Event{Stream{connection, id, terminal || terminal_type(name)}, name, data, ""})
129 case _: fail(connection, "protocol", "Unknown bridge frame kind")
130
131def step_id(ok: Bool, connection: T.Connection, id: String, terminal: Bool, +frame: J.Json) -> IO(Step):
132 match ok:
133 case True{}: step_kind(frame_kind(field(frame, "kind")), connection, id, terminal, frame)
134 case False{}: fail(connection, "protocol", "Mismatched bridge request id")
135
136def received(result: T.Read, +id: String, terminal: Bool) -> IO(Step):
137 match result:
138 case T.Broken{message}: IO.pure(Step, Failed{Error{"transport_error", message, "0", "", ""}})
139 case T.Frame{connection, +frame}: step_id(String.eq(field(frame, "id"), id), connection, id, terminal, frame)
140
141def next(stream: Stream) -> IO(Step):
142 match stream:
143 case Stream{connection, id, terminal}:
144 do IO<Step>:
145 result : T.Read <- T.read(connection)
146 received(result, id, terminal)
147
148def cancelled(kind: Bool, value: J.Json) -> Result<&1, &1, String, Bool>:
149 match kind value:
150 case True{} J.Boolean{found}: Done{found}
151 case _ _: Fail{"Cancellation rejected by bridge"}
152
153def cancel_read(result: T.Read) -> IO(Result<&1, &1, String, Bool>):
154 match result:
155 case T.Broken{message}: IO.pure(Result<&1, &1, String, Bool>, Fail{message})
156 case T.Frame{connection, +frame}:
157 do IO<Result<&1, &1, String, Bool>>:
158 T.close(connection)
159 return cancelled(String.eq(field(frame, "kind"), "cancelled"), json_field(frame, "found"))
160
161def cancel_open(result: Result<&1, &1, String, T.Connection>) -> IO(Result<&1, &1, String, Bool>):
162 match result:
163 case Fail{e}: IO.pure(Result<&1, &1, String, Bool>, Fail{e})
164 case Done{connection}:
165 do IO<Result<&1, &1, String, Bool>>:
166 result : T.Read <- T.read(connection)
167 cancel_read(result)
168
169def cancel(+client: Client, id: String) -> IO(Result<&1, &1, String, Bool>):
170 match client:
171 case Client{port, token}:
172 do IO<Result<&1, &1, String, Bool>>:
173 result : Result<&1, &1, String, T.Connection> <- T.connect(port, J.stringify(envelope(client, id, False{}, J.Null{}, "cancel")))
174 cancel_open(result)
175
176def function_tool(name: String, description: String, schema: J.Json) -> J.Json:
177 J.Object{[J.Field{"type", J.Text{"function"}}, J.Field{"name", J.Text{name}}, J.Field{"description", J.Text{description}}, J.Field{"parameters", schema}, J.Field{"strict", J.Boolean{True{}}}]}
178
179def function_output(call_id: String, output: String) -> J.Json:
180 J.Object{[J.Field{"type", J.Text{"function_call_output"}}, J.Field{"call_id", J.Text{call_id}}, J.Field{"output", J.Text{output}}]}
181
182def json_schema(name: String, schema: J.Json) -> J.Json:
183 J.Object{[J.Field{"format", J.Object{[J.Field{"type", J.Text{"json_schema"}}, J.Field{"name", J.Text{name}}, J.Field{"strict", J.Boolean{True{}}}, J.Field{"schema", schema}]}}]}
184
185type Usage is Data:
186 Usage{input_tokens: String, output_tokens: String, total_tokens: String}
187
188type FunctionCall is Data:
189 FunctionCall{id: String, call_id: String, name: String, arguments: String, raw: J.Json}
190
191type Response is Data:
192 Response{id: String, status: String, text: String, calls: List<&2, FunctionCall>, usage: Maybe<&2, Usage>, raw: J.Json}
193
194def array(value: J.Json) -> List<&2, J.Json>:
195 match value:
196 case J.Array{items}: items
197 case _: Nil{}
198
199def content_piece(kind: Bool, item: J.Json) -> String:
200 match kind:
201 case True{}: field(item, "text")
202 case _: ""
203
204def content_text(items: List<&2, J.Json>) -> String:
205 match items:
206 case Nil{}: ""
207 case Con{+item, rest}: content_piece(String.eq(field(item, "type"), "output_text"), item) ++ content_text(rest)
208
209def output_text(items: List<&2, J.Json>) -> String:
210 match items:
211 case Nil{}: ""
212 case Con{item, rest}: content_text(array(json_field(item, "content"))) ++ output_text(rest)
213
214def call_piece(kind: Bool, +item: J.Json, rest: List<&2, FunctionCall>) -> List<&2, FunctionCall>:
215 match kind:
216 case True{}: Con{FunctionCall{field(item, "id"), field(item, "call_id"), field(item, "name"), field(item, "arguments"), item}, rest}
217 case _: rest
218
219def function_calls(items: List<&2, J.Json>) -> List<&2, FunctionCall>:
220 match items:
221 case Nil{}: Nil{}
222 case Con{+item, rest}: call_piece(String.eq(field(item, "type"), "function_call"), item, function_calls(rest))
223
224def usage(value: J.Json) -> Maybe<&2, Usage>:
225 match value:
226 case J.Null{}: None{}
227 case J.Object{+fields}: Some{Usage{status(value_or_null(J.get_fields(fields, "input_tokens"))), status(value_or_null(J.get_fields(fields, "output_tokens"))), status(value_or_null(J.get_fields(fields, "total_tokens")))}}
228 case _: None{}
229
230def response(+raw: J.Json) -> Response:
231 Response{field(raw, "id"), field(raw, "status"), output_text(array(json_field(raw, "output"))), function_calls(array(json_field(raw, "output"))), usage(json_field(raw, "usage")), raw}