@codilore/llm 1.15.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +321 -0
- package/README.md +131 -0
- package/example/call-sites.md +591 -0
- package/example/tutorial.ts +255 -0
- package/package.json +50 -0
- package/script/recording-cost-report.ts +250 -0
- package/script/setup-recording-env.ts +542 -0
- package/src/cache-policy.ts +111 -0
- package/src/index.ts +32 -0
- package/src/llm.ts +186 -0
- package/src/protocols/anthropic-messages.ts +841 -0
- package/src/protocols/bedrock-converse.ts +649 -0
- package/src/protocols/bedrock-event-stream.ts +87 -0
- package/src/protocols/gemini.ts +465 -0
- package/src/protocols/index.ts +6 -0
- package/src/protocols/openai-chat.ts +431 -0
- package/src/protocols/openai-compatible-chat.ts +24 -0
- package/src/protocols/openai-responses.ts +987 -0
- package/src/protocols/shared.ts +283 -0
- package/src/protocols/utils/bedrock-auth.ts +70 -0
- package/src/protocols/utils/bedrock-cache.ts +37 -0
- package/src/protocols/utils/bedrock-media.ts +80 -0
- package/src/protocols/utils/cache.ts +16 -0
- package/src/protocols/utils/gemini-tool-schema.ts +101 -0
- package/src/protocols/utils/lifecycle.ts +102 -0
- package/src/protocols/utils/openai-options.ts +84 -0
- package/src/protocols/utils/tool-stream.ts +218 -0
- package/src/provider.ts +37 -0
- package/src/providers/amazon-bedrock.ts +43 -0
- package/src/providers/anthropic.ts +35 -0
- package/src/providers/azure.ts +110 -0
- package/src/providers/cloudflare.ts +127 -0
- package/src/providers/github-copilot.ts +66 -0
- package/src/providers/google.ts +35 -0
- package/src/providers/index.ts +11 -0
- package/src/providers/openai-compatible-profile.ts +20 -0
- package/src/providers/openai-compatible.ts +65 -0
- package/src/providers/openai-options.ts +81 -0
- package/src/providers/openai.ts +63 -0
- package/src/providers/openrouter.ts +98 -0
- package/src/providers/xai.ts +56 -0
- package/src/route/auth-options.ts +57 -0
- package/src/route/auth.ts +156 -0
- package/src/route/client.ts +434 -0
- package/src/route/endpoint.ts +53 -0
- package/src/route/executor.ts +374 -0
- package/src/route/framing.ts +27 -0
- package/src/route/index.ts +25 -0
- package/src/route/protocol.ts +84 -0
- package/src/route/transport/http.ts +108 -0
- package/src/route/transport/index.ts +33 -0
- package/src/route/transport/websocket.ts +280 -0
- package/src/schema/errors.ts +203 -0
- package/src/schema/events.ts +370 -0
- package/src/schema/ids.ts +43 -0
- package/src/schema/index.ts +5 -0
- package/src/schema/messages.ts +404 -0
- package/src/schema/options.ts +221 -0
- package/src/tool-runtime.ts +78 -0
- package/src/tool.ts +241 -0
- package/src/utils/record.ts +3 -0
- package/sst-env.d.ts +10 -0
- package/test/adapter.test.ts +164 -0
- package/test/auth-options.types.ts +168 -0
- package/test/auth.test.ts +103 -0
- package/test/cache-policy.test.ts +262 -0
- package/test/continuation-scenarios.ts +104 -0
- package/test/endpoint.test.ts +58 -0
- package/test/executor.test.ts +418 -0
- package/test/exports.test.ts +62 -0
- package/test/fixtures/media/restroom.png +0 -0
- package/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +43 -0
- package/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json +56 -0
- package/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/streams-text.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/streams-tool-call.json +29 -0
- package/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +48 -0
- package/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json +55 -0
- package/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json +29 -0
- package/test/fixtures/recordings/bedrock-converse/streams-text.json +29 -0
- package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
- package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json +32 -0
- package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
- package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json +32 -0
- package/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json +32 -0
- package/test/fixtures/recordings/gemini/streams-text.json +28 -0
- package/test/fixtures/recordings/gemini/streams-tool-call.json +28 -0
- package/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json +46 -0
- package/test/fixtures/recordings/openai-chat/continues-after-tool-result.json +28 -0
- package/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json +46 -0
- package/test/fixtures/recordings/openai-chat/streams-text.json +28 -0
- package/test/fixtures/recordings/openai-chat/streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json +53 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json +53 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json +42 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +58 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json +32 -0
- package/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +46 -0
- package/test/generate-object.test.ts +184 -0
- package/test/lib/effect.ts +50 -0
- package/test/lib/http.ts +98 -0
- package/test/lib/openai-chunks.ts +27 -0
- package/test/lib/sse.ts +17 -0
- package/test/lib/tool-runtime.ts +146 -0
- package/test/llm.test.ts +167 -0
- package/test/provider/anthropic-messages-cache.recorded.test.ts +54 -0
- package/test/provider/anthropic-messages.recorded.test.ts +46 -0
- package/test/provider/anthropic-messages.test.ts +829 -0
- package/test/provider/bedrock-converse-cache.recorded.test.ts +54 -0
- package/test/provider/bedrock-converse.test.ts +707 -0
- package/test/provider/cloudflare.test.ts +230 -0
- package/test/provider/gemini-cache.recorded.test.ts +48 -0
- package/test/provider/gemini.test.ts +476 -0
- package/test/provider/golden.recorded.test.ts +219 -0
- package/test/provider/openai-chat.test.ts +446 -0
- package/test/provider/openai-compatible-chat.test.ts +238 -0
- package/test/provider/openai-responses-cache.recorded.test.ts +46 -0
- package/test/provider/openai-responses.test.ts +1322 -0
- package/test/provider/openrouter.test.ts +56 -0
- package/test/provider.types.ts +41 -0
- package/test/recorded-golden.ts +97 -0
- package/test/recorded-runner.ts +100 -0
- package/test/recorded-scenarios.ts +531 -0
- package/test/recorded-test.ts +74 -0
- package/test/recorded-utils.ts +56 -0
- package/test/recorded-websocket.ts +26 -0
- package/test/route.test.ts +43 -0
- package/test/schema.test.ts +97 -0
- package/test/tool-runtime.test.ts +802 -0
- package/test/tool-stream.test.ts +99 -0
- package/test/tool.types.ts +40 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
|
|
2
|
+
import { Headers } from "effect/unstable/http"
|
|
3
|
+
import { LLMError, TransportReason } from "../../schema"
|
|
4
|
+
import * as HttpTransport from "./http"
|
|
5
|
+
import type { Transport } from "./index"
|
|
6
|
+
|
|
7
|
+
export interface WebSocketRequest {
|
|
8
|
+
readonly url: string
|
|
9
|
+
readonly headers: Headers.Headers
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WebSocketConnection {
|
|
13
|
+
readonly sendText: (message: string) => Effect.Effect<void, LLMError>
|
|
14
|
+
readonly messages: Stream.Stream<string | Uint8Array, LLMError>
|
|
15
|
+
readonly close: Effect.Effect<void, never>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Interface {
|
|
19
|
+
readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, LLMError>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type WebSocketConstructorWithHeaders = new (
|
|
23
|
+
url: string,
|
|
24
|
+
options?: { readonly headers?: Headers.Headers },
|
|
25
|
+
) => globalThis.WebSocket
|
|
26
|
+
|
|
27
|
+
export class Service extends Context.Service<Service, Interface>()("@Codilore/LLM/WebSocketExecutor") {}
|
|
28
|
+
|
|
29
|
+
const transportError = (
|
|
30
|
+
method: string,
|
|
31
|
+
message: string,
|
|
32
|
+
input: { readonly url?: string; readonly kind?: string } = {},
|
|
33
|
+
) =>
|
|
34
|
+
new LLMError({
|
|
35
|
+
module: "WebSocketExecutor",
|
|
36
|
+
method,
|
|
37
|
+
reason: new TransportReason({ message, url: input.url, kind: input.kind }),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const eventMessage = (event: Event) => {
|
|
41
|
+
if ("message" in event && typeof event.message === "string") return event.message
|
|
42
|
+
return event.type
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const binaryMessage = (data: unknown) => {
|
|
46
|
+
if (data instanceof Uint8Array) return data
|
|
47
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data)
|
|
48
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
|
|
53
|
+
if (ws.readyState === globalThis.WebSocket.OPEN) return Effect.void
|
|
54
|
+
if (ws.readyState === globalThis.WebSocket.CLOSING || ws.readyState === globalThis.WebSocket.CLOSED) {
|
|
55
|
+
return Effect.fail(
|
|
56
|
+
transportError("open", `WebSocket closed before opening (state ${ws.readyState})`, {
|
|
57
|
+
url: input.url,
|
|
58
|
+
kind: "open",
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return Effect.callback<void, LLMError>((resume, signal) => {
|
|
63
|
+
const cleanup = () => {
|
|
64
|
+
ws.removeEventListener("open", onOpen)
|
|
65
|
+
ws.removeEventListener("error", onError)
|
|
66
|
+
ws.removeEventListener("close", onClose)
|
|
67
|
+
signal.removeEventListener("abort", onAbort)
|
|
68
|
+
}
|
|
69
|
+
const onAbort = () => {
|
|
70
|
+
cleanup()
|
|
71
|
+
if (ws.readyState !== globalThis.WebSocket.CLOSED && ws.readyState !== globalThis.WebSocket.CLOSING)
|
|
72
|
+
ws.close(1000)
|
|
73
|
+
}
|
|
74
|
+
const onOpen = () => {
|
|
75
|
+
cleanup()
|
|
76
|
+
resume(Effect.void)
|
|
77
|
+
}
|
|
78
|
+
const onError = (event: Event) => {
|
|
79
|
+
cleanup()
|
|
80
|
+
resume(
|
|
81
|
+
Effect.fail(
|
|
82
|
+
transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, { url: input.url, kind: "open" }),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
const onClose = (event: CloseEvent) => {
|
|
87
|
+
cleanup()
|
|
88
|
+
resume(
|
|
89
|
+
Effect.fail(
|
|
90
|
+
transportError("open", `WebSocket closed before opening with code ${event.code}`, {
|
|
91
|
+
url: input.url,
|
|
92
|
+
kind: "open",
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
ws.addEventListener("open", onOpen, { once: true })
|
|
98
|
+
ws.addEventListener("error", onError, { once: true })
|
|
99
|
+
ws.addEventListener("close", onClose, { once: true })
|
|
100
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const webSocketUrl = (value: string) =>
|
|
105
|
+
Effect.try({
|
|
106
|
+
try: () => {
|
|
107
|
+
const url = new URL(value)
|
|
108
|
+
if (url.protocol === "https:") {
|
|
109
|
+
url.protocol = "wss:"
|
|
110
|
+
return url.toString()
|
|
111
|
+
}
|
|
112
|
+
if (url.protocol === "http:") {
|
|
113
|
+
url.protocol = "ws:"
|
|
114
|
+
return url.toString()
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Unsupported WebSocket URL protocol ${url.protocol}`)
|
|
117
|
+
},
|
|
118
|
+
catch: (error) =>
|
|
119
|
+
transportError("prepare", error instanceof Error ? error.message : "Invalid WebSocket URL", {
|
|
120
|
+
url: value,
|
|
121
|
+
kind: "websocket",
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
export const open = (input: WebSocketRequest) =>
|
|
126
|
+
Effect.try({
|
|
127
|
+
try: () =>
|
|
128
|
+
new (globalThis.WebSocket as unknown as WebSocketConstructorWithHeaders)(input.url, { headers: input.headers }),
|
|
129
|
+
catch: (error) =>
|
|
130
|
+
transportError("open", error instanceof Error ? error.message : "Failed to construct WebSocket", {
|
|
131
|
+
url: input.url,
|
|
132
|
+
kind: "open",
|
|
133
|
+
}),
|
|
134
|
+
}).pipe(Effect.flatMap((ws) => fromWebSocket(ws, input)))
|
|
135
|
+
|
|
136
|
+
export const layer: Layer.Layer<Service> = Layer.succeed(Service, Service.of({ open }))
|
|
137
|
+
|
|
138
|
+
export const fromWebSocket = (
|
|
139
|
+
ws: globalThis.WebSocket,
|
|
140
|
+
input: WebSocketRequest,
|
|
141
|
+
): Effect.Effect<WebSocketConnection, LLMError> =>
|
|
142
|
+
Effect.gen(function* () {
|
|
143
|
+
yield* waitOpen(ws, input)
|
|
144
|
+
const messages = yield* Queue.bounded<string | Uint8Array, LLMError | Cause.Done<void>>(128)
|
|
145
|
+
|
|
146
|
+
const onMessage = (event: MessageEvent) => {
|
|
147
|
+
if (typeof event.data === "string") return Queue.offerUnsafe(messages, event.data)
|
|
148
|
+
const binary = binaryMessage(event.data)
|
|
149
|
+
if (binary) return Queue.offerUnsafe(messages, binary)
|
|
150
|
+
Queue.failCauseUnsafe(
|
|
151
|
+
messages,
|
|
152
|
+
Cause.fail(
|
|
153
|
+
transportError("message", "Unsupported WebSocket message payload", { url: input.url, kind: "message" }),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
const onError = (event: Event) => {
|
|
158
|
+
Queue.failCauseUnsafe(
|
|
159
|
+
messages,
|
|
160
|
+
Cause.fail(
|
|
161
|
+
transportError("message", `WebSocket error: ${eventMessage(event)}`, { url: input.url, kind: "message" }),
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
const onClose = (event: CloseEvent) => {
|
|
166
|
+
if (event.code === 1000 || event.code === 1005) return Queue.endUnsafe(messages)
|
|
167
|
+
Queue.failCauseUnsafe(
|
|
168
|
+
messages,
|
|
169
|
+
Cause.fail(
|
|
170
|
+
transportError("message", `WebSocket closed with code ${event.code}`, { url: input.url, kind: "close" }),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
const cleanup = Effect.sync(() => {
|
|
175
|
+
ws.removeEventListener("message", onMessage)
|
|
176
|
+
ws.removeEventListener("error", onError)
|
|
177
|
+
ws.removeEventListener("close", onClose)
|
|
178
|
+
}).pipe(Effect.andThen(Queue.shutdown(messages)))
|
|
179
|
+
|
|
180
|
+
ws.addEventListener("message", onMessage)
|
|
181
|
+
ws.addEventListener("error", onError)
|
|
182
|
+
ws.addEventListener("close", onClose)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
sendText: (message) =>
|
|
186
|
+
Effect.try({
|
|
187
|
+
try: () => ws.send(message),
|
|
188
|
+
catch: (error) =>
|
|
189
|
+
transportError("sendText", error instanceof Error ? error.message : "Failed to send WebSocket message", {
|
|
190
|
+
url: input.url,
|
|
191
|
+
kind: "write",
|
|
192
|
+
}),
|
|
193
|
+
}),
|
|
194
|
+
messages: Stream.fromQueue(messages),
|
|
195
|
+
close: cleanup.pipe(
|
|
196
|
+
Effect.andThen(
|
|
197
|
+
Effect.sync(() => {
|
|
198
|
+
if (ws.readyState === globalThis.WebSocket.CLOSED || ws.readyState === globalThis.WebSocket.CLOSING) return
|
|
199
|
+
ws.close(1000)
|
|
200
|
+
}),
|
|
201
|
+
),
|
|
202
|
+
),
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
export const messageText = (message: string | Uint8Array, decoder: TextDecoder) =>
|
|
207
|
+
typeof message === "string" ? message : decoder.decode(message)
|
|
208
|
+
|
|
209
|
+
export interface JsonPrepared {
|
|
210
|
+
readonly url: string
|
|
211
|
+
readonly headers: Headers.Headers
|
|
212
|
+
readonly message: string
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface JsonInput<Body, Message> {
|
|
216
|
+
readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, LLMError>
|
|
217
|
+
readonly encodeMessage: (message: Message) => string
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type JsonPatch<Body, Message> = Partial<JsonInput<Body, Message>>
|
|
221
|
+
|
|
222
|
+
export interface JsonTransport<Body, Message> extends Transport<Body, JsonPrepared, string> {
|
|
223
|
+
readonly with: (patch: JsonPatch<Body, Message>) => JsonTransport<Body, Message>
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const json = <Body, Message>(input: JsonInput<Body, Message>): JsonTransport<Body, Message> => ({
|
|
227
|
+
id: "websocket-json",
|
|
228
|
+
with: (patch) => json({ ...input, ...patch }),
|
|
229
|
+
prepare: (prepareInput) =>
|
|
230
|
+
Effect.gen(function* () {
|
|
231
|
+
const parts = yield* HttpTransport.jsonRequestParts({
|
|
232
|
+
...prepareInput,
|
|
233
|
+
})
|
|
234
|
+
return {
|
|
235
|
+
url: yield* webSocketUrl(parts.url),
|
|
236
|
+
headers: parts.headers,
|
|
237
|
+
message: input.encodeMessage(yield* input.toMessage(parts.jsonBody)),
|
|
238
|
+
}
|
|
239
|
+
}),
|
|
240
|
+
frames: (prepared, _request, runtime) => {
|
|
241
|
+
const webSocket = runtime.webSocket
|
|
242
|
+
if (!webSocket) {
|
|
243
|
+
return Stream.fail(
|
|
244
|
+
transportError("json", "WebSocket JSON transport requires WebSocketExecutor.Service", {
|
|
245
|
+
url: prepared.url,
|
|
246
|
+
kind: "websocket",
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
const decoder = new TextDecoder()
|
|
251
|
+
return Stream.unwrap(
|
|
252
|
+
Effect.gen(function* () {
|
|
253
|
+
const connection = yield* Effect.acquireRelease(
|
|
254
|
+
webSocket.open({ url: prepared.url, headers: prepared.headers }),
|
|
255
|
+
(connection) => connection.close,
|
|
256
|
+
)
|
|
257
|
+
yield* connection.sendText(prepared.message)
|
|
258
|
+
return connection.messages.pipe(Stream.map((message) => messageText(message, decoder)))
|
|
259
|
+
}),
|
|
260
|
+
)
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
export const jsonTransport = {
|
|
265
|
+
id: "websocket-json",
|
|
266
|
+
with: json,
|
|
267
|
+
} as const
|
|
268
|
+
|
|
269
|
+
export const WebSocketExecutor = {
|
|
270
|
+
Service,
|
|
271
|
+
layer,
|
|
272
|
+
open,
|
|
273
|
+
fromWebSocket,
|
|
274
|
+
messageText,
|
|
275
|
+
} as const
|
|
276
|
+
|
|
277
|
+
export const WebSocketTransport = {
|
|
278
|
+
json,
|
|
279
|
+
jsonTransport,
|
|
280
|
+
} as const
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids"
|
|
3
|
+
|
|
4
|
+
export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("LLM.HttpRequestDetails")({
|
|
5
|
+
method: Schema.String,
|
|
6
|
+
url: Schema.String,
|
|
7
|
+
headers: Schema.Record(Schema.String, Schema.String),
|
|
8
|
+
}) {}
|
|
9
|
+
|
|
10
|
+
export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("LLM.HttpResponseDetails")({
|
|
11
|
+
status: Schema.Number,
|
|
12
|
+
headers: Schema.Record(Schema.String, Schema.String),
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("LLM.HttpRateLimitDetails")({
|
|
16
|
+
retryAfterMs: Schema.optional(Schema.Number),
|
|
17
|
+
limit: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
18
|
+
remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
19
|
+
reset: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
|
|
23
|
+
request: HttpRequestDetails,
|
|
24
|
+
response: Schema.optional(HttpResponseDetails),
|
|
25
|
+
body: Schema.optional(Schema.String),
|
|
26
|
+
bodyTruncated: Schema.optional(Schema.Boolean),
|
|
27
|
+
requestId: Schema.optional(Schema.String),
|
|
28
|
+
rateLimit: Schema.optional(HttpRateLimitDetails),
|
|
29
|
+
}) {}
|
|
30
|
+
|
|
31
|
+
export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LLM.Error.InvalidRequest")({
|
|
32
|
+
_tag: Schema.tag("InvalidRequest"),
|
|
33
|
+
message: Schema.String,
|
|
34
|
+
parameter: Schema.optional(Schema.String),
|
|
35
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
36
|
+
http: Schema.optional(HttpContext),
|
|
37
|
+
}) {
|
|
38
|
+
get retryable() {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class NoRouteReason extends Schema.Class<NoRouteReason>("LLM.Error.NoRoute")({
|
|
44
|
+
_tag: Schema.tag("NoRoute"),
|
|
45
|
+
route: RouteID,
|
|
46
|
+
provider: ProviderID,
|
|
47
|
+
model: ModelID,
|
|
48
|
+
}) {
|
|
49
|
+
get retryable() {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get message() {
|
|
54
|
+
return `No LLM route for ${this.provider}/${this.model} using ${this.route}`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LLM.Error.Authentication")({
|
|
59
|
+
_tag: Schema.tag("Authentication"),
|
|
60
|
+
message: Schema.String,
|
|
61
|
+
kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
|
|
62
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
63
|
+
http: Schema.optional(HttpContext),
|
|
64
|
+
}) {
|
|
65
|
+
get retryable() {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.RateLimit")({
|
|
71
|
+
_tag: Schema.tag("RateLimit"),
|
|
72
|
+
message: Schema.String,
|
|
73
|
+
retryAfterMs: Schema.optional(Schema.Number),
|
|
74
|
+
rateLimit: Schema.optional(HttpRateLimitDetails),
|
|
75
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
76
|
+
http: Schema.optional(HttpContext),
|
|
77
|
+
}) {
|
|
78
|
+
get retryable() {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("LLM.Error.QuotaExceeded")({
|
|
84
|
+
_tag: Schema.tag("QuotaExceeded"),
|
|
85
|
+
message: Schema.String,
|
|
86
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
87
|
+
http: Schema.optional(HttpContext),
|
|
88
|
+
}) {
|
|
89
|
+
get retryable() {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("LLM.Error.ContentPolicy")({
|
|
95
|
+
_tag: Schema.tag("ContentPolicy"),
|
|
96
|
+
message: Schema.String,
|
|
97
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
98
|
+
http: Schema.optional(HttpContext),
|
|
99
|
+
}) {
|
|
100
|
+
get retryable() {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("LLM.Error.ProviderInternal")({
|
|
106
|
+
_tag: Schema.tag("ProviderInternal"),
|
|
107
|
+
message: Schema.String,
|
|
108
|
+
status: Schema.Number,
|
|
109
|
+
retryAfterMs: Schema.optional(Schema.Number),
|
|
110
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
111
|
+
http: Schema.optional(HttpContext),
|
|
112
|
+
}) {
|
|
113
|
+
get retryable() {
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Transport")({
|
|
119
|
+
_tag: Schema.tag("Transport"),
|
|
120
|
+
message: Schema.String,
|
|
121
|
+
kind: Schema.optional(Schema.String),
|
|
122
|
+
url: Schema.optional(Schema.String),
|
|
123
|
+
http: Schema.optional(HttpContext),
|
|
124
|
+
}) {
|
|
125
|
+
get retryable() {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOutputReason>(
|
|
131
|
+
"LLM.Error.InvalidProviderOutput",
|
|
132
|
+
)({
|
|
133
|
+
_tag: Schema.tag("InvalidProviderOutput"),
|
|
134
|
+
message: Schema.String,
|
|
135
|
+
route: Schema.optional(Schema.String),
|
|
136
|
+
raw: Schema.optional(Schema.String),
|
|
137
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
138
|
+
}) {
|
|
139
|
+
get retryable() {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("LLM.Error.UnknownProvider")({
|
|
145
|
+
_tag: Schema.tag("UnknownProvider"),
|
|
146
|
+
message: Schema.String,
|
|
147
|
+
status: Schema.optional(Schema.Number),
|
|
148
|
+
providerMetadata: Schema.optional(ProviderMetadata),
|
|
149
|
+
http: Schema.optional(HttpContext),
|
|
150
|
+
}) {
|
|
151
|
+
get retryable() {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const LLMErrorReason = Schema.Union([
|
|
157
|
+
InvalidRequestReason,
|
|
158
|
+
NoRouteReason,
|
|
159
|
+
AuthenticationReason,
|
|
160
|
+
RateLimitReason,
|
|
161
|
+
QuotaExceededReason,
|
|
162
|
+
ContentPolicyReason,
|
|
163
|
+
ProviderInternalReason,
|
|
164
|
+
TransportReason,
|
|
165
|
+
InvalidProviderOutputReason,
|
|
166
|
+
UnknownProviderReason,
|
|
167
|
+
]).pipe(Schema.toTaggedUnion("_tag"))
|
|
168
|
+
export type LLMErrorReason = Schema.Schema.Type<typeof LLMErrorReason>
|
|
169
|
+
|
|
170
|
+
export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
|
|
171
|
+
module: Schema.String,
|
|
172
|
+
method: Schema.String,
|
|
173
|
+
reason: LLMErrorReason,
|
|
174
|
+
}) {
|
|
175
|
+
override readonly cause = this.reason
|
|
176
|
+
|
|
177
|
+
get retryable() {
|
|
178
|
+
return this.reason.retryable
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
get retryAfterMs() {
|
|
182
|
+
return "retryAfterMs" in this.reason ? this.reason.retryAfterMs : undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
override get message() {
|
|
186
|
+
return `${this.module}.${this.method}: ${this.reason.message}`
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Failure type for tool execute handlers. Handlers must map their internal
|
|
192
|
+
* errors to this shape; the runtime catches `ToolFailure`s and surfaces them
|
|
193
|
+
* as `tool-error` events plus a `tool-result` of `type: "error"` so the model
|
|
194
|
+
* can self-correct.
|
|
195
|
+
*
|
|
196
|
+
* Anything thrown or yielded by a handler that is not a `ToolFailure` is
|
|
197
|
+
* treated as a defect and fails the stream.
|
|
198
|
+
*/
|
|
199
|
+
export class ToolFailure extends Schema.TaggedErrorClass<ToolFailure>()("LLM.ToolFailure", {
|
|
200
|
+
message: Schema.String,
|
|
201
|
+
error: Schema.optional(Schema.Defect),
|
|
202
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
203
|
+
}) {}
|