@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,370 @@
1
+ import { Schema } from "effect"
2
+ import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids"
3
+ import { ModelSchema } from "./options"
4
+ import { ToolOutput, ToolResultValue } from "./messages"
5
+
6
+ /**
7
+ * Token usage reported by an LLM provider.
8
+ *
9
+ * **Inclusive totals** (match AI SDK / OpenAI / LangChain convention — a
10
+ * reader from any of those ecosystems sees the number they expect):
11
+ *
12
+ * - `inputTokens` — total prompt tokens, *including* cached reads/writes.
13
+ * - `outputTokens` — total output tokens, *including* reasoning.
14
+ * - `totalTokens` — provider-supplied total, or `inputTokens + outputTokens`.
15
+ *
16
+ * **Non-overlapping breakdown** (every field is independently meaningful;
17
+ * consumers never have to subtract):
18
+ *
19
+ * - `nonCachedInputTokens` — the "fresh" portion of the prompt.
20
+ * - `cacheReadInputTokens` — input tokens served from cache.
21
+ * - `cacheWriteInputTokens` — input tokens written to cache.
22
+ * - `reasoningTokens` — subset of `outputTokens` spent on hidden reasoning.
23
+ *
24
+ * **Invariant**: `nonCachedInputTokens + cacheReadInputTokens +
25
+ * cacheWriteInputTokens = inputTokens`, and `reasoningTokens ≤ outputTokens`.
26
+ * Each protocol mapper computes whichever side it doesn't get natively,
27
+ * with `Math.max(0, …)` clamping for defense against provider bugs. Because
28
+ * every breakdown field is stored independently, downstream consumers can
29
+ * read whatever they need (cost-by-category, context-pressure, AI-SDK-style
30
+ * inclusive total) without ever subtracting — eliminating the underflow
31
+ * class of bug where a clamped difference would silently store the wrong
32
+ * value.
33
+ *
34
+ * **Semantics by provider**:
35
+ *
36
+ * - OpenAI Chat / Responses / Gemini / Bedrock: provider reports inclusive
37
+ * `inputTokens` and an inclusive `outputTokens`; mapper subtracts to
38
+ * derive the breakdown.
39
+ * - Anthropic: provider reports the breakdown natively (`input_tokens` is
40
+ * non-cached only); mapper sums to derive the inclusive `inputTokens`.
41
+ * Anthropic does *not* break extended-thinking out of `output_tokens`, so
42
+ * `reasoningTokens` is `undefined` and `outputTokens` carries the
43
+ * combined total — a documented limitation of the Anthropic API.
44
+ *
45
+ * `providerMetadata` always carries the provider's raw usage payload —
46
+ * keyed by provider name (`{ openai: ... }`, `{ anthropic: ... }`, etc.)
47
+ * — for fields we don't normalize and for billing-level audit trails.
48
+ * Matches the same escape-hatch field on `LLMEvent`.
49
+ */
50
+ export class Usage extends Schema.Class<Usage>("LLM.Usage")({
51
+ inputTokens: Schema.optional(Schema.Number),
52
+ outputTokens: Schema.optional(Schema.Number),
53
+ nonCachedInputTokens: Schema.optional(Schema.Number),
54
+ cacheReadInputTokens: Schema.optional(Schema.Number),
55
+ cacheWriteInputTokens: Schema.optional(Schema.Number),
56
+ reasoningTokens: Schema.optional(Schema.Number),
57
+ totalTokens: Schema.optional(Schema.Number),
58
+ providerMetadata: Schema.optional(ProviderMetadata),
59
+ }) {
60
+ /**
61
+ * Visible output tokens — `outputTokens` minus `reasoningTokens`, clamped
62
+ * to zero. The one place subtraction happens in this contract; the clamp
63
+ * means a provider reporting `reasoningTokens > outputTokens` produces a
64
+ * harmless zero rather than a negative that crashes downstream schemas.
65
+ */
66
+ get visibleOutputTokens() {
67
+ return Math.max(0, (this.outputTokens ?? 0) - (this.reasoningTokens ?? 0))
68
+ }
69
+
70
+ static from(input: UsageInput) {
71
+ return input instanceof Usage ? input : new Usage(input)
72
+ }
73
+ }
74
+
75
+ export type UsageInput = Usage | ConstructorParameters<typeof Usage>[0]
76
+
77
+ export const StepStart = Schema.Struct({
78
+ type: Schema.tag("step-start"),
79
+ index: Schema.Number,
80
+ }).annotate({ identifier: "LLM.Event.StepStart" })
81
+ export type StepStart = Schema.Schema.Type<typeof StepStart>
82
+
83
+ export const TextStart = Schema.Struct({
84
+ type: Schema.tag("text-start"),
85
+ id: ContentBlockID,
86
+ providerMetadata: Schema.optional(ProviderMetadata),
87
+ }).annotate({ identifier: "LLM.Event.TextStart" })
88
+ export type TextStart = Schema.Schema.Type<typeof TextStart>
89
+
90
+ export const TextDelta = Schema.Struct({
91
+ type: Schema.tag("text-delta"),
92
+ id: ContentBlockID,
93
+ text: Schema.String,
94
+ providerMetadata: Schema.optional(ProviderMetadata),
95
+ }).annotate({ identifier: "LLM.Event.TextDelta" })
96
+ export type TextDelta = Schema.Schema.Type<typeof TextDelta>
97
+
98
+ export const TextEnd = Schema.Struct({
99
+ type: Schema.tag("text-end"),
100
+ id: ContentBlockID,
101
+ providerMetadata: Schema.optional(ProviderMetadata),
102
+ }).annotate({ identifier: "LLM.Event.TextEnd" })
103
+ export type TextEnd = Schema.Schema.Type<typeof TextEnd>
104
+
105
+ export const ReasoningStart = Schema.Struct({
106
+ type: Schema.tag("reasoning-start"),
107
+ id: ContentBlockID,
108
+ providerMetadata: Schema.optional(ProviderMetadata),
109
+ }).annotate({ identifier: "LLM.Event.ReasoningStart" })
110
+ export type ReasoningStart = Schema.Schema.Type<typeof ReasoningStart>
111
+
112
+ export const ReasoningDelta = Schema.Struct({
113
+ type: Schema.tag("reasoning-delta"),
114
+ id: ContentBlockID,
115
+ text: Schema.String,
116
+ providerMetadata: Schema.optional(ProviderMetadata),
117
+ }).annotate({ identifier: "LLM.Event.ReasoningDelta" })
118
+ export type ReasoningDelta = Schema.Schema.Type<typeof ReasoningDelta>
119
+
120
+ export const ReasoningEnd = Schema.Struct({
121
+ type: Schema.tag("reasoning-end"),
122
+ id: ContentBlockID,
123
+ providerMetadata: Schema.optional(ProviderMetadata),
124
+ }).annotate({ identifier: "LLM.Event.ReasoningEnd" })
125
+ export type ReasoningEnd = Schema.Schema.Type<typeof ReasoningEnd>
126
+
127
+ export const ToolInputStart = Schema.Struct({
128
+ type: Schema.tag("tool-input-start"),
129
+ id: ToolCallID,
130
+ name: Schema.String,
131
+ providerMetadata: Schema.optional(ProviderMetadata),
132
+ }).annotate({ identifier: "LLM.Event.ToolInputStart" })
133
+ export type ToolInputStart = Schema.Schema.Type<typeof ToolInputStart>
134
+
135
+ export const ToolInputDelta = Schema.Struct({
136
+ type: Schema.tag("tool-input-delta"),
137
+ id: ToolCallID,
138
+ name: Schema.String,
139
+ text: Schema.String,
140
+ }).annotate({ identifier: "LLM.Event.ToolInputDelta" })
141
+ export type ToolInputDelta = Schema.Schema.Type<typeof ToolInputDelta>
142
+
143
+ export const ToolInputEnd = Schema.Struct({
144
+ type: Schema.tag("tool-input-end"),
145
+ id: ToolCallID,
146
+ name: Schema.String,
147
+ providerMetadata: Schema.optional(ProviderMetadata),
148
+ }).annotate({ identifier: "LLM.Event.ToolInputEnd" })
149
+ export type ToolInputEnd = Schema.Schema.Type<typeof ToolInputEnd>
150
+
151
+ export const ToolCall = Schema.Struct({
152
+ type: Schema.tag("tool-call"),
153
+ id: ToolCallID,
154
+ name: Schema.String,
155
+ input: Schema.Unknown,
156
+ providerExecuted: Schema.optional(Schema.Boolean),
157
+ providerMetadata: Schema.optional(ProviderMetadata),
158
+ }).annotate({ identifier: "LLM.Event.ToolCall" })
159
+ export type ToolCall = Schema.Schema.Type<typeof ToolCall>
160
+
161
+ export const ToolResult = Schema.Struct({
162
+ type: Schema.tag("tool-result"),
163
+ id: ToolCallID,
164
+ name: Schema.String,
165
+ result: ToolResultValue,
166
+ output: Schema.optional(ToolOutput),
167
+ providerExecuted: Schema.optional(Schema.Boolean),
168
+ providerMetadata: Schema.optional(ProviderMetadata),
169
+ }).annotate({ identifier: "LLM.Event.ToolResult" })
170
+ export type ToolResult = Schema.Schema.Type<typeof ToolResult>
171
+
172
+ export const ToolError = Schema.Struct({
173
+ type: Schema.tag("tool-error"),
174
+ id: ToolCallID,
175
+ name: Schema.String,
176
+ message: Schema.String,
177
+ error: Schema.optional(Schema.Defect),
178
+ providerMetadata: Schema.optional(ProviderMetadata),
179
+ }).annotate({ identifier: "LLM.Event.ToolError" })
180
+ export type ToolError = Schema.Schema.Type<typeof ToolError>
181
+
182
+ export const StepFinish = Schema.Struct({
183
+ type: Schema.tag("step-finish"),
184
+ index: Schema.Number,
185
+ reason: FinishReason,
186
+ usage: Schema.optional(Usage),
187
+ providerMetadata: Schema.optional(ProviderMetadata),
188
+ }).annotate({ identifier: "LLM.Event.StepFinish" })
189
+ export type StepFinish = Schema.Schema.Type<typeof StepFinish>
190
+
191
+ export const Finish = Schema.Struct({
192
+ type: Schema.tag("finish"),
193
+ reason: FinishReason,
194
+ usage: Schema.optional(Usage),
195
+ providerMetadata: Schema.optional(ProviderMetadata),
196
+ }).annotate({ identifier: "LLM.Event.Finish" })
197
+ export type Finish = Schema.Schema.Type<typeof Finish>
198
+
199
+ export const ProviderErrorEvent = Schema.Struct({
200
+ type: Schema.tag("provider-error"),
201
+ message: Schema.String,
202
+ retryable: Schema.optional(Schema.Boolean),
203
+ providerMetadata: Schema.optional(ProviderMetadata),
204
+ }).annotate({ identifier: "LLM.Event.ProviderError" })
205
+ export type ProviderErrorEvent = Schema.Schema.Type<typeof ProviderErrorEvent>
206
+
207
+ const llmEventTagged = Schema.Union([
208
+ StepStart,
209
+ TextStart,
210
+ TextDelta,
211
+ TextEnd,
212
+ ReasoningStart,
213
+ ReasoningDelta,
214
+ ReasoningEnd,
215
+ ToolInputStart,
216
+ ToolInputDelta,
217
+ ToolInputEnd,
218
+ ToolCall,
219
+ ToolResult,
220
+ ToolError,
221
+ StepFinish,
222
+ Finish,
223
+ ProviderErrorEvent,
224
+ ]).pipe(Schema.toTaggedUnion("type"))
225
+
226
+ type WithID<Event extends { readonly id: unknown }, ID> = Omit<Event, "type" | "id"> & { readonly id: ID | string }
227
+ type WithUsage<Event extends { readonly usage?: Usage }> = Omit<Event, "type" | "usage"> & {
228
+ readonly usage?: UsageInput
229
+ }
230
+
231
+ const contentBlockID = (value: ContentBlockID | string) => ContentBlockID.make(value)
232
+ const toolCallID = (value: ToolCallID | string) => ToolCallID.make(value)
233
+
234
+ /**
235
+ * camelCase aliases for `LLMEvent.guards` (provided by `Schema.toTaggedUnion`).
236
+ * Lets consumers write `events.filter(LLMEvent.is.toolCall)` instead of
237
+ * `events.filter(LLMEvent.guards["tool-call"])`.
238
+ */
239
+ export const LLMEvent = Object.assign(llmEventTagged, {
240
+ stepStart: StepStart.make,
241
+ textStart: (input: WithID<TextStart, ContentBlockID>) => TextStart.make({ ...input, id: contentBlockID(input.id) }),
242
+ textDelta: (input: WithID<TextDelta, ContentBlockID>) => TextDelta.make({ ...input, id: contentBlockID(input.id) }),
243
+ textEnd: (input: WithID<TextEnd, ContentBlockID>) => TextEnd.make({ ...input, id: contentBlockID(input.id) }),
244
+ reasoningStart: (input: WithID<ReasoningStart, ContentBlockID>) =>
245
+ ReasoningStart.make({ ...input, id: contentBlockID(input.id) }),
246
+ reasoningDelta: (input: WithID<ReasoningDelta, ContentBlockID>) =>
247
+ ReasoningDelta.make({ ...input, id: contentBlockID(input.id) }),
248
+ reasoningEnd: (input: WithID<ReasoningEnd, ContentBlockID>) =>
249
+ ReasoningEnd.make({ ...input, id: contentBlockID(input.id) }),
250
+ toolInputStart: (input: WithID<ToolInputStart, ToolCallID>) =>
251
+ ToolInputStart.make({ ...input, id: toolCallID(input.id) }),
252
+ toolInputDelta: (input: WithID<ToolInputDelta, ToolCallID>) =>
253
+ ToolInputDelta.make({ ...input, id: toolCallID(input.id) }),
254
+ toolInputEnd: (input: WithID<ToolInputEnd, ToolCallID>) => ToolInputEnd.make({ ...input, id: toolCallID(input.id) }),
255
+ toolCall: (input: WithID<ToolCall, ToolCallID>) => ToolCall.make({ ...input, id: toolCallID(input.id) }),
256
+ toolResult: (input: WithID<ToolResult, ToolCallID>) =>
257
+ ToolResult.make({
258
+ ...input,
259
+ id: toolCallID(input.id),
260
+ output: input.output === undefined ? undefined : ToolOutput.make(input.output.structured, input.output.content),
261
+ }),
262
+ toolError: (input: WithID<ToolError, ToolCallID>) => ToolError.make({ ...input, id: toolCallID(input.id) }),
263
+ stepFinish: (input: WithUsage<StepFinish>) =>
264
+ StepFinish.make({
265
+ ...input,
266
+ usage: input.usage === undefined ? undefined : Usage.from(input.usage),
267
+ }),
268
+ finish: (input: WithUsage<Finish>) =>
269
+ Finish.make({
270
+ ...input,
271
+ usage: input.usage === undefined ? undefined : Usage.from(input.usage),
272
+ }),
273
+ providerError: ProviderErrorEvent.make,
274
+ is: {
275
+ stepStart: llmEventTagged.guards["step-start"],
276
+ textStart: llmEventTagged.guards["text-start"],
277
+ textDelta: llmEventTagged.guards["text-delta"],
278
+ textEnd: llmEventTagged.guards["text-end"],
279
+ reasoningStart: llmEventTagged.guards["reasoning-start"],
280
+ reasoningDelta: llmEventTagged.guards["reasoning-delta"],
281
+ reasoningEnd: llmEventTagged.guards["reasoning-end"],
282
+ toolInputStart: llmEventTagged.guards["tool-input-start"],
283
+ toolInputDelta: llmEventTagged.guards["tool-input-delta"],
284
+ toolInputEnd: llmEventTagged.guards["tool-input-end"],
285
+ toolCall: llmEventTagged.guards["tool-call"],
286
+ toolResult: llmEventTagged.guards["tool-result"],
287
+ toolError: llmEventTagged.guards["tool-error"],
288
+ stepFinish: llmEventTagged.guards["step-finish"],
289
+ finish: llmEventTagged.guards.finish,
290
+ providerError: llmEventTagged.guards["provider-error"],
291
+ },
292
+ })
293
+ export type LLMEvent = Schema.Schema.Type<typeof llmEventTagged>
294
+
295
+ export class PreparedRequest extends Schema.Class<PreparedRequest>("LLM.PreparedRequest")({
296
+ id: Schema.String,
297
+ route: RouteID,
298
+ protocol: ProtocolID,
299
+ model: ModelSchema,
300
+ body: Schema.Unknown,
301
+ metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
302
+ }) {}
303
+
304
+ /**
305
+ * A `PreparedRequest` whose `body` is typed as `Body`. Use with the generic
306
+ * on `LLMClient.prepare<Body>(...)` when the caller knows which route their
307
+ * request will resolve to and wants its native shape statically exposed
308
+ * (debug UIs, request previews, plan rendering).
309
+ *
310
+ * The runtime body is identical — the route still emits `body: unknown` — so
311
+ * this is a type-level assertion the caller makes about what they expect to
312
+ * find. The prepare runtime does not validate the assertion.
313
+ */
314
+ export type PreparedRequestOf<Body> = Omit<PreparedRequest, "body"> & {
315
+ readonly body: Body
316
+ }
317
+
318
+ const responseText = (events: ReadonlyArray<LLMEvent>) =>
319
+ events
320
+ .filter(LLMEvent.is.textDelta)
321
+ .map((event) => event.text)
322
+ .join("")
323
+
324
+ const responseReasoning = (events: ReadonlyArray<LLMEvent>) =>
325
+ events
326
+ .filter(LLMEvent.is.reasoningDelta)
327
+ .map((event) => event.text)
328
+ .join("")
329
+
330
+ const responseUsage = (events: ReadonlyArray<LLMEvent>) =>
331
+ events.reduce<Usage | undefined>(
332
+ (usage, event) => ("usage" in event && event.usage !== undefined ? event.usage : usage),
333
+ undefined,
334
+ )
335
+
336
+ export class LLMResponse extends Schema.Class<LLMResponse>("LLM.Response")({
337
+ events: Schema.Array(LLMEvent),
338
+ usage: Schema.optional(Usage),
339
+ }) {
340
+ /** Concatenated assistant text assembled from streamed `text-delta` events. */
341
+ get text() {
342
+ return responseText(this.events)
343
+ }
344
+
345
+ /** Concatenated reasoning text assembled from streamed `reasoning-delta` events. */
346
+ get reasoning() {
347
+ return responseReasoning(this.events)
348
+ }
349
+
350
+ /** Completed tool calls emitted by the provider. */
351
+ get toolCalls() {
352
+ return this.events.filter(LLMEvent.is.toolCall)
353
+ }
354
+ }
355
+
356
+ export namespace LLMResponse {
357
+ export type Output = LLMResponse | { readonly events: ReadonlyArray<LLMEvent>; readonly usage?: Usage }
358
+
359
+ /** Concatenate assistant text from a response or collected event list. */
360
+ export const text = (response: Output) => responseText(response.events)
361
+
362
+ /** Return response usage, falling back to the latest usage-bearing event. */
363
+ export const usage = (response: Output) => response.usage ?? responseUsage(response.events)
364
+
365
+ /** Return completed tool calls from a response or collected event list. */
366
+ export const toolCalls = (response: Output) => response.events.filter(LLMEvent.is.toolCall)
367
+
368
+ /** Concatenate reasoning text from a response or collected event list. */
369
+ export const reasoning = (response: Output) => responseReasoning(response.events)
370
+ }
@@ -0,0 +1,43 @@
1
+ import { Schema } from "effect"
2
+
3
+ /** Stable string identifier for a protocol implementation. */
4
+ export const ProtocolID = Schema.String
5
+ export type ProtocolID = Schema.Schema.Type<typeof ProtocolID>
6
+
7
+ /** Stable string identifier for the runnable route. */
8
+ export const RouteID = Schema.String
9
+ export type RouteID = Schema.Schema.Type<typeof RouteID>
10
+
11
+ export const ModelID = Schema.String.pipe(Schema.brand("LLM.ModelID"))
12
+ export type ModelID = typeof ModelID.Type
13
+
14
+ export const ProviderID = Schema.String.pipe(Schema.brand("LLM.ProviderID"))
15
+ export type ProviderID = typeof ProviderID.Type
16
+
17
+ export const ResponseID = Schema.String
18
+ export type ResponseID = Schema.Schema.Type<typeof ResponseID>
19
+
20
+ export const ContentBlockID = Schema.String
21
+ export type ContentBlockID = Schema.Schema.Type<typeof ContentBlockID>
22
+
23
+ export const ToolCallID = Schema.String
24
+ export type ToolCallID = Schema.Schema.Type<typeof ToolCallID>
25
+
26
+ export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
27
+ export const ReasoningEffort = Schema.Literals(ReasoningEfforts)
28
+ export type ReasoningEffort = Schema.Schema.Type<typeof ReasoningEffort>
29
+
30
+ export const TextVerbosity = Schema.Literals(["low", "medium", "high"])
31
+ export type TextVerbosity = Schema.Schema.Type<typeof TextVerbosity>
32
+
33
+ export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
34
+ export type MessageRole = Schema.Schema.Type<typeof MessageRole>
35
+
36
+ export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"])
37
+ export type FinishReason = Schema.Schema.Type<typeof FinishReason>
38
+
39
+ export const JsonSchema = Schema.Record(Schema.String, Schema.Unknown)
40
+ export type JsonSchema = Schema.Schema.Type<typeof JsonSchema>
41
+
42
+ export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))
43
+ export type ProviderMetadata = Schema.Schema.Type<typeof ProviderMetadata>
@@ -0,0 +1,5 @@
1
+ export * from "./ids"
2
+ export * from "./options"
3
+ export * from "./messages"
4
+ export * from "./events"
5
+ export * from "./errors"