@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.
Files changed (145) hide show
  1. package/AGENTS.md +321 -0
  2. package/README.md +131 -0
  3. package/example/call-sites.md +591 -0
  4. package/example/tutorial.ts +255 -0
  5. package/package.json +50 -0
  6. package/script/recording-cost-report.ts +250 -0
  7. package/script/setup-recording-env.ts +542 -0
  8. package/src/cache-policy.ts +111 -0
  9. package/src/index.ts +32 -0
  10. package/src/llm.ts +186 -0
  11. package/src/protocols/anthropic-messages.ts +841 -0
  12. package/src/protocols/bedrock-converse.ts +649 -0
  13. package/src/protocols/bedrock-event-stream.ts +87 -0
  14. package/src/protocols/gemini.ts +465 -0
  15. package/src/protocols/index.ts +6 -0
  16. package/src/protocols/openai-chat.ts +431 -0
  17. package/src/protocols/openai-compatible-chat.ts +24 -0
  18. package/src/protocols/openai-responses.ts +987 -0
  19. package/src/protocols/shared.ts +283 -0
  20. package/src/protocols/utils/bedrock-auth.ts +70 -0
  21. package/src/protocols/utils/bedrock-cache.ts +37 -0
  22. package/src/protocols/utils/bedrock-media.ts +80 -0
  23. package/src/protocols/utils/cache.ts +16 -0
  24. package/src/protocols/utils/gemini-tool-schema.ts +101 -0
  25. package/src/protocols/utils/lifecycle.ts +102 -0
  26. package/src/protocols/utils/openai-options.ts +84 -0
  27. package/src/protocols/utils/tool-stream.ts +218 -0
  28. package/src/provider.ts +37 -0
  29. package/src/providers/amazon-bedrock.ts +43 -0
  30. package/src/providers/anthropic.ts +35 -0
  31. package/src/providers/azure.ts +110 -0
  32. package/src/providers/cloudflare.ts +127 -0
  33. package/src/providers/github-copilot.ts +66 -0
  34. package/src/providers/google.ts +35 -0
  35. package/src/providers/index.ts +11 -0
  36. package/src/providers/openai-compatible-profile.ts +20 -0
  37. package/src/providers/openai-compatible.ts +65 -0
  38. package/src/providers/openai-options.ts +81 -0
  39. package/src/providers/openai.ts +63 -0
  40. package/src/providers/openrouter.ts +98 -0
  41. package/src/providers/xai.ts +56 -0
  42. package/src/route/auth-options.ts +57 -0
  43. package/src/route/auth.ts +156 -0
  44. package/src/route/client.ts +434 -0
  45. package/src/route/endpoint.ts +53 -0
  46. package/src/route/executor.ts +374 -0
  47. package/src/route/framing.ts +27 -0
  48. package/src/route/index.ts +25 -0
  49. package/src/route/protocol.ts +84 -0
  50. package/src/route/transport/http.ts +108 -0
  51. package/src/route/transport/index.ts +33 -0
  52. package/src/route/transport/websocket.ts +280 -0
  53. package/src/schema/errors.ts +203 -0
  54. package/src/schema/events.ts +370 -0
  55. package/src/schema/ids.ts +43 -0
  56. package/src/schema/index.ts +5 -0
  57. package/src/schema/messages.ts +404 -0
  58. package/src/schema/options.ts +221 -0
  59. package/src/tool-runtime.ts +78 -0
  60. package/src/tool.ts +241 -0
  61. package/src/utils/record.ts +3 -0
  62. package/sst-env.d.ts +10 -0
  63. package/test/adapter.test.ts +164 -0
  64. package/test/auth-options.types.ts +168 -0
  65. package/test/auth.test.ts +103 -0
  66. package/test/cache-policy.test.ts +262 -0
  67. package/test/continuation-scenarios.ts +104 -0
  68. package/test/endpoint.test.ts +58 -0
  69. package/test/executor.test.ts +418 -0
  70. package/test/exports.test.ts +62 -0
  71. package/test/fixtures/media/restroom.png +0 -0
  72. package/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json +29 -0
  73. package/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +43 -0
  74. package/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json +56 -0
  75. package/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json +29 -0
  76. package/test/fixtures/recordings/anthropic-messages/streams-text.json +29 -0
  77. package/test/fixtures/recordings/anthropic-messages/streams-tool-call.json +29 -0
  78. package/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +48 -0
  79. package/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json +55 -0
  80. package/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json +29 -0
  81. package/test/fixtures/recordings/bedrock-converse/streams-text.json +29 -0
  82. package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
  83. package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json +32 -0
  84. package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
  85. package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json +32 -0
  86. package/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json +32 -0
  87. package/test/fixtures/recordings/gemini/streams-text.json +28 -0
  88. package/test/fixtures/recordings/gemini/streams-tool-call.json +28 -0
  89. package/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json +46 -0
  90. package/test/fixtures/recordings/openai-chat/continues-after-tool-result.json +28 -0
  91. package/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json +46 -0
  92. package/test/fixtures/recordings/openai-chat/streams-text.json +28 -0
  93. package/test/fixtures/recordings/openai-chat/streams-tool-call.json +28 -0
  94. package/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json +28 -0
  95. package/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json +53 -0
  96. package/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json +28 -0
  97. package/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json +28 -0
  98. package/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json +54 -0
  99. package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json +53 -0
  100. package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json +54 -0
  101. package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json +28 -0
  102. package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json +28 -0
  103. package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json +28 -0
  104. package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json +28 -0
  105. package/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json +54 -0
  106. package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json +28 -0
  107. package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json +28 -0
  108. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json +42 -0
  109. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +58 -0
  110. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json +32 -0
  111. package/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +46 -0
  112. package/test/generate-object.test.ts +184 -0
  113. package/test/lib/effect.ts +50 -0
  114. package/test/lib/http.ts +98 -0
  115. package/test/lib/openai-chunks.ts +27 -0
  116. package/test/lib/sse.ts +17 -0
  117. package/test/lib/tool-runtime.ts +146 -0
  118. package/test/llm.test.ts +167 -0
  119. package/test/provider/anthropic-messages-cache.recorded.test.ts +54 -0
  120. package/test/provider/anthropic-messages.recorded.test.ts +46 -0
  121. package/test/provider/anthropic-messages.test.ts +829 -0
  122. package/test/provider/bedrock-converse-cache.recorded.test.ts +54 -0
  123. package/test/provider/bedrock-converse.test.ts +707 -0
  124. package/test/provider/cloudflare.test.ts +230 -0
  125. package/test/provider/gemini-cache.recorded.test.ts +48 -0
  126. package/test/provider/gemini.test.ts +476 -0
  127. package/test/provider/golden.recorded.test.ts +219 -0
  128. package/test/provider/openai-chat.test.ts +446 -0
  129. package/test/provider/openai-compatible-chat.test.ts +238 -0
  130. package/test/provider/openai-responses-cache.recorded.test.ts +46 -0
  131. package/test/provider/openai-responses.test.ts +1322 -0
  132. package/test/provider/openrouter.test.ts +56 -0
  133. package/test/provider.types.ts +41 -0
  134. package/test/recorded-golden.ts +97 -0
  135. package/test/recorded-runner.ts +100 -0
  136. package/test/recorded-scenarios.ts +531 -0
  137. package/test/recorded-test.ts +74 -0
  138. package/test/recorded-utils.ts +56 -0
  139. package/test/recorded-websocket.ts +26 -0
  140. package/test/route.test.ts +43 -0
  141. package/test/schema.test.ts +97 -0
  142. package/test/tool-runtime.test.ts +802 -0
  143. package/test/tool-stream.test.ts +99 -0
  144. package/test/tool.types.ts +40 -0
  145. 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
+ }) {}