@neurocode-ai/llm 1.18.8

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 (152) hide show
  1. package/AGENTS.md +321 -0
  2. package/DESIGN.md +1114 -0
  3. package/README.md +131 -0
  4. package/example/call-sites.md +591 -0
  5. package/example/tutorial.ts +255 -0
  6. package/package.json +51 -0
  7. package/script/recording-cost-report.ts +250 -0
  8. package/script/setup-recording-env.ts +542 -0
  9. package/src/cache-policy.ts +111 -0
  10. package/src/index.ts +33 -0
  11. package/src/llm.ts +186 -0
  12. package/src/protocols/anthropic-messages.ts +855 -0
  13. package/src/protocols/bedrock-converse.ts +674 -0
  14. package/src/protocols/bedrock-event-stream.ts +87 -0
  15. package/src/protocols/gemini.ts +512 -0
  16. package/src/protocols/index.ts +6 -0
  17. package/src/protocols/openai-chat.ts +506 -0
  18. package/src/protocols/openai-compatible-chat.ts +24 -0
  19. package/src/protocols/openai-responses.ts +1022 -0
  20. package/src/protocols/shared.ts +326 -0
  21. package/src/protocols/utils/bedrock-auth.ts +70 -0
  22. package/src/protocols/utils/bedrock-cache.ts +37 -0
  23. package/src/protocols/utils/bedrock-media.ts +90 -0
  24. package/src/protocols/utils/cache.ts +16 -0
  25. package/src/protocols/utils/gemini-tool-schema.ts +99 -0
  26. package/src/protocols/utils/lifecycle.ts +102 -0
  27. package/src/protocols/utils/openai-options.ts +93 -0
  28. package/src/protocols/utils/tool-schema.ts +86 -0
  29. package/src/protocols/utils/tool-stream.ts +218 -0
  30. package/src/provider-error.ts +43 -0
  31. package/src/provider.ts +36 -0
  32. package/src/providers/amazon-bedrock.ts +43 -0
  33. package/src/providers/anthropic.ts +35 -0
  34. package/src/providers/azure.ts +110 -0
  35. package/src/providers/cloudflare.ts +127 -0
  36. package/src/providers/github-copilot.ts +69 -0
  37. package/src/providers/google.ts +35 -0
  38. package/src/providers/index.ts +11 -0
  39. package/src/providers/openai-compatible-profile.ts +20 -0
  40. package/src/providers/openai-compatible.ts +65 -0
  41. package/src/providers/openai-options.ts +83 -0
  42. package/src/providers/openai.ts +63 -0
  43. package/src/providers/openrouter.ts +98 -0
  44. package/src/providers/xai.ts +56 -0
  45. package/src/route/auth-options.ts +57 -0
  46. package/src/route/auth.ts +156 -0
  47. package/src/route/client.ts +436 -0
  48. package/src/route/endpoint.ts +53 -0
  49. package/src/route/executor.ts +385 -0
  50. package/src/route/framing.ts +27 -0
  51. package/src/route/index.ts +25 -0
  52. package/src/route/protocol.ts +84 -0
  53. package/src/route/transport/http.ts +155 -0
  54. package/src/route/transport/index.ts +33 -0
  55. package/src/route/transport/websocket.ts +280 -0
  56. package/src/schema/errors.ts +207 -0
  57. package/src/schema/events.ts +618 -0
  58. package/src/schema/ids.ts +43 -0
  59. package/src/schema/index.ts +5 -0
  60. package/src/schema/messages.ts +312 -0
  61. package/src/schema/options.ts +276 -0
  62. package/src/tool-runtime.ts +78 -0
  63. package/src/tool.ts +253 -0
  64. package/src/utils/record.ts +3 -0
  65. package/sst-env.d.ts +10 -0
  66. package/test/adapter.test.ts +171 -0
  67. package/test/auth-options.types.ts +168 -0
  68. package/test/auth.test.ts +103 -0
  69. package/test/cache-policy.test.ts +262 -0
  70. package/test/continuation-scenarios.ts +104 -0
  71. package/test/endpoint.test.ts +58 -0
  72. package/test/executor.test.ts +458 -0
  73. package/test/exports.test.ts +76 -0
  74. package/test/fixtures/media/restroom.png +0 -0
  75. package/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json +29 -0
  76. package/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +43 -0
  77. package/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json +56 -0
  78. package/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json +29 -0
  79. package/test/fixtures/recordings/anthropic-messages/streams-text.json +29 -0
  80. package/test/fixtures/recordings/anthropic-messages/streams-tool-call.json +29 -0
  81. package/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +48 -0
  82. package/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json +55 -0
  83. package/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json +29 -0
  84. package/test/fixtures/recordings/bedrock-converse/streams-text.json +29 -0
  85. package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
  86. package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json +32 -0
  87. package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
  88. package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json +32 -0
  89. package/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json +32 -0
  90. package/test/fixtures/recordings/gemini/streams-text.json +28 -0
  91. package/test/fixtures/recordings/gemini/streams-tool-call.json +28 -0
  92. package/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json +46 -0
  93. package/test/fixtures/recordings/openai-chat/continues-after-tool-result.json +28 -0
  94. package/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json +46 -0
  95. package/test/fixtures/recordings/openai-chat/streams-text.json +28 -0
  96. package/test/fixtures/recordings/openai-chat/streams-tool-call.json +28 -0
  97. package/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json +28 -0
  98. package/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json +53 -0
  99. package/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json +28 -0
  100. package/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json +28 -0
  101. package/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json +54 -0
  102. package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json +53 -0
  103. package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json +54 -0
  104. package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json +28 -0
  105. package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json +28 -0
  106. package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json +28 -0
  107. package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json +28 -0
  108. package/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json +54 -0
  109. package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json +28 -0
  110. package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json +28 -0
  111. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json +42 -0
  112. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +58 -0
  113. package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json +32 -0
  114. package/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +46 -0
  115. package/test/generate-object.test.ts +184 -0
  116. package/test/lib/effect.ts +50 -0
  117. package/test/lib/http.ts +98 -0
  118. package/test/lib/openai-chunks.ts +27 -0
  119. package/test/lib/sse.ts +17 -0
  120. package/test/lib/tool-runtime.ts +146 -0
  121. package/test/llm.test.ts +199 -0
  122. package/test/prepare.test.ts +178 -0
  123. package/test/provider/anthropic-messages-cache.recorded.test.ts +53 -0
  124. package/test/provider/anthropic-messages.recorded.test.ts +45 -0
  125. package/test/provider/anthropic-messages.test.ts +895 -0
  126. package/test/provider/bedrock-converse-cache.recorded.test.ts +54 -0
  127. package/test/provider/bedrock-converse.test.ts +744 -0
  128. package/test/provider/cloudflare.test.ts +230 -0
  129. package/test/provider/gemini-cache.recorded.test.ts +48 -0
  130. package/test/provider/gemini.test.ts +584 -0
  131. package/test/provider/golden.recorded.test.ts +223 -0
  132. package/test/provider/openai-chat.test.ts +674 -0
  133. package/test/provider/openai-compatible-chat.test.ts +238 -0
  134. package/test/provider/openai-responses-cache.recorded.test.ts +46 -0
  135. package/test/provider/openai-responses.test.ts +1472 -0
  136. package/test/provider/openrouter.test.ts +56 -0
  137. package/test/provider-error.test.ts +30 -0
  138. package/test/provider.types.ts +41 -0
  139. package/test/recorded-golden.ts +97 -0
  140. package/test/recorded-runner.ts +100 -0
  141. package/test/recorded-scenarios.ts +531 -0
  142. package/test/recorded-test.ts +94 -0
  143. package/test/recorded-utils.ts +56 -0
  144. package/test/recorded-websocket.ts +26 -0
  145. package/test/response.test.ts +98 -0
  146. package/test/route.test.ts +43 -0
  147. package/test/schema.test.ts +86 -0
  148. package/test/tool-runtime.test.ts +818 -0
  149. package/test/tool-schema-projection.test.ts +117 -0
  150. package/test/tool-stream.test.ts +99 -0
  151. package/test/tool.types.ts +40 -0
  152. package/tsconfig.json +15 -0
@@ -0,0 +1,33 @@
1
+ import type { Effect, Stream } from "effect"
2
+ import type { Endpoint } from "../endpoint"
3
+ import type { Auth } from "../auth"
4
+ import type { Interface as RequestExecutorInterface } from "../executor"
5
+ import type { Interface as WebSocketExecutorInterface } from "./websocket"
6
+ import type { LLMError, LLMRequest } from "../../schema"
7
+
8
+ export interface TransportRuntime {
9
+ readonly http: RequestExecutorInterface
10
+ readonly webSocket?: WebSocketExecutorInterface
11
+ }
12
+
13
+ export interface Transport<Body, Prepared, Frame> {
14
+ readonly id: string
15
+ readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, LLMError>
16
+ readonly frames: (
17
+ prepared: Prepared,
18
+ request: LLMRequest,
19
+ runtime: TransportRuntime,
20
+ ) => Stream.Stream<Frame, LLMError>
21
+ }
22
+
23
+ export interface TransportPrepareInput<Body> {
24
+ readonly body: Body
25
+ readonly request: LLMRequest
26
+ readonly endpoint: Endpoint<Body>
27
+ readonly auth: Auth
28
+ readonly encodeBody: (body: Body) => string
29
+ readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
30
+ }
31
+
32
+ export * as HttpTransport from "./http"
33
+ export { WebSocketExecutor, WebSocketTransport } from "./websocket"
@@ -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>()("@neurocode/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,207 @@
1
+ import { Schema } from "effect"
2
+ import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids"
3
+
4
+ export const ProviderFailureClassification = Schema.Literal("context-overflow")
5
+ export type ProviderFailureClassification = typeof ProviderFailureClassification.Type
6
+
7
+ export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("LLM.HttpRequestDetails")({
8
+ method: Schema.String,
9
+ url: Schema.String,
10
+ headers: Schema.Record(Schema.String, Schema.String),
11
+ }) {}
12
+
13
+ export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("LLM.HttpResponseDetails")({
14
+ status: Schema.Number,
15
+ headers: Schema.Record(Schema.String, Schema.String),
16
+ }) {}
17
+
18
+ export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("LLM.HttpRateLimitDetails")({
19
+ retryAfterMs: Schema.optional(Schema.Number),
20
+ limit: Schema.optional(Schema.Record(Schema.String, Schema.String)),
21
+ remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)),
22
+ reset: Schema.optional(Schema.Record(Schema.String, Schema.String)),
23
+ }) {}
24
+
25
+ export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
26
+ request: HttpRequestDetails,
27
+ response: Schema.optional(HttpResponseDetails),
28
+ body: Schema.optional(Schema.String),
29
+ bodyTruncated: Schema.optional(Schema.Boolean),
30
+ requestId: Schema.optional(Schema.String),
31
+ rateLimit: Schema.optional(HttpRateLimitDetails),
32
+ }) {}
33
+
34
+ export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LLM.Error.InvalidRequest")({
35
+ _tag: Schema.tag("InvalidRequest"),
36
+ message: Schema.String,
37
+ parameter: Schema.optional(Schema.String),
38
+ classification: Schema.optional(ProviderFailureClassification),
39
+ providerMetadata: Schema.optional(ProviderMetadata),
40
+ http: Schema.optional(HttpContext),
41
+ }) {
42
+ get retryable() {
43
+ return false
44
+ }
45
+ }
46
+
47
+ export class NoRouteReason extends Schema.Class<NoRouteReason>("LLM.Error.NoRoute")({
48
+ _tag: Schema.tag("NoRoute"),
49
+ route: RouteID,
50
+ provider: ProviderID,
51
+ model: ModelID,
52
+ }) {
53
+ get retryable() {
54
+ return false
55
+ }
56
+
57
+ get message() {
58
+ return `No LLM route for ${this.provider}/${this.model} using ${this.route}`
59
+ }
60
+ }
61
+
62
+ export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LLM.Error.Authentication")({
63
+ _tag: Schema.tag("Authentication"),
64
+ message: Schema.String,
65
+ kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
66
+ providerMetadata: Schema.optional(ProviderMetadata),
67
+ http: Schema.optional(HttpContext),
68
+ }) {
69
+ get retryable() {
70
+ return false
71
+ }
72
+ }
73
+
74
+ export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.RateLimit")({
75
+ _tag: Schema.tag("RateLimit"),
76
+ message: Schema.String,
77
+ retryAfterMs: Schema.optional(Schema.Number),
78
+ rateLimit: Schema.optional(HttpRateLimitDetails),
79
+ providerMetadata: Schema.optional(ProviderMetadata),
80
+ http: Schema.optional(HttpContext),
81
+ }) {
82
+ get retryable() {
83
+ return true
84
+ }
85
+ }
86
+
87
+ export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("LLM.Error.QuotaExceeded")({
88
+ _tag: Schema.tag("QuotaExceeded"),
89
+ message: Schema.String,
90
+ providerMetadata: Schema.optional(ProviderMetadata),
91
+ http: Schema.optional(HttpContext),
92
+ }) {
93
+ get retryable() {
94
+ return false
95
+ }
96
+ }
97
+
98
+ export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("LLM.Error.ContentPolicy")({
99
+ _tag: Schema.tag("ContentPolicy"),
100
+ message: Schema.String,
101
+ providerMetadata: Schema.optional(ProviderMetadata),
102
+ http: Schema.optional(HttpContext),
103
+ }) {
104
+ get retryable() {
105
+ return false
106
+ }
107
+ }
108
+
109
+ export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("LLM.Error.ProviderInternal")({
110
+ _tag: Schema.tag("ProviderInternal"),
111
+ message: Schema.String,
112
+ status: Schema.Number,
113
+ retryAfterMs: Schema.optional(Schema.Number),
114
+ providerMetadata: Schema.optional(ProviderMetadata),
115
+ http: Schema.optional(HttpContext),
116
+ }) {
117
+ get retryable() {
118
+ return true
119
+ }
120
+ }
121
+
122
+ export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Transport")({
123
+ _tag: Schema.tag("Transport"),
124
+ message: Schema.String,
125
+ kind: Schema.optional(Schema.String),
126
+ url: Schema.optional(Schema.String),
127
+ http: Schema.optional(HttpContext),
128
+ }) {
129
+ get retryable() {
130
+ return false
131
+ }
132
+ }
133
+
134
+ export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOutputReason>(
135
+ "LLM.Error.InvalidProviderOutput",
136
+ )({
137
+ _tag: Schema.tag("InvalidProviderOutput"),
138
+ message: Schema.String,
139
+ route: Schema.optional(Schema.String),
140
+ raw: Schema.optional(Schema.String),
141
+ providerMetadata: Schema.optional(ProviderMetadata),
142
+ }) {
143
+ get retryable() {
144
+ return false
145
+ }
146
+ }
147
+
148
+ export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("LLM.Error.UnknownProvider")({
149
+ _tag: Schema.tag("UnknownProvider"),
150
+ message: Schema.String,
151
+ status: Schema.optional(Schema.Number),
152
+ providerMetadata: Schema.optional(ProviderMetadata),
153
+ http: Schema.optional(HttpContext),
154
+ }) {
155
+ get retryable() {
156
+ return false
157
+ }
158
+ }
159
+
160
+ export const LLMErrorReason = Schema.Union([
161
+ InvalidRequestReason,
162
+ NoRouteReason,
163
+ AuthenticationReason,
164
+ RateLimitReason,
165
+ QuotaExceededReason,
166
+ ContentPolicyReason,
167
+ ProviderInternalReason,
168
+ TransportReason,
169
+ InvalidProviderOutputReason,
170
+ UnknownProviderReason,
171
+ ]).pipe(Schema.toTaggedUnion("_tag"))
172
+ export type LLMErrorReason = Schema.Schema.Type<typeof LLMErrorReason>
173
+
174
+ export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
175
+ module: Schema.String,
176
+ method: Schema.String,
177
+ reason: LLMErrorReason,
178
+ }) {
179
+ override readonly cause = this.reason
180
+
181
+ get retryable() {
182
+ return this.reason.retryable
183
+ }
184
+
185
+ get retryAfterMs() {
186
+ return "retryAfterMs" in this.reason ? this.reason.retryAfterMs : undefined
187
+ }
188
+
189
+ override get message() {
190
+ return `${this.module}.${this.method}: ${this.reason.message}`
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Failure type for tool execute handlers. Handlers must map their internal
196
+ * errors to this shape; the runtime catches `ToolFailure`s and surfaces them
197
+ * as `tool-error` events plus a `tool-result` of `type: "error"` so the model
198
+ * can self-correct.
199
+ *
200
+ * Anything thrown or yielded by a handler that is not a `ToolFailure` is
201
+ * treated as a defect and fails the stream.
202
+ */
203
+ export class ToolFailure extends Schema.TaggedErrorClass<ToolFailure>()("LLM.ToolFailure", {
204
+ message: Schema.String,
205
+ error: Schema.optional(Schema.Defect()),
206
+ metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
207
+ }) {}