@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,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>
|