@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,841 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
|
+
import { Route } from "../route/client"
|
|
3
|
+
import { Auth } from "../route/auth"
|
|
4
|
+
import { Endpoint } from "../route/endpoint"
|
|
5
|
+
import { Framing } from "../route/framing"
|
|
6
|
+
import { Protocol } from "../route/protocol"
|
|
7
|
+
import {
|
|
8
|
+
LLMEvent,
|
|
9
|
+
Usage,
|
|
10
|
+
type CacheHint,
|
|
11
|
+
type FinishReason,
|
|
12
|
+
type LLMRequest,
|
|
13
|
+
type MediaPart,
|
|
14
|
+
type ProviderMetadata,
|
|
15
|
+
type ToolCallPart,
|
|
16
|
+
type ToolDefinition,
|
|
17
|
+
type ToolResultContentPart,
|
|
18
|
+
type ToolResultPart,
|
|
19
|
+
} from "../schema"
|
|
20
|
+
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
|
|
21
|
+
import * as Cache from "./utils/cache"
|
|
22
|
+
import { Lifecycle } from "./utils/lifecycle"
|
|
23
|
+
import { ToolStream } from "./utils/tool-stream"
|
|
24
|
+
|
|
25
|
+
const ADAPTER = "anthropic-messages"
|
|
26
|
+
export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
|
|
27
|
+
export const PATH = "/messages"
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Request Body Schema
|
|
31
|
+
// =============================================================================
|
|
32
|
+
const AnthropicCacheControl = Schema.Struct({
|
|
33
|
+
type: Schema.tag("ephemeral"),
|
|
34
|
+
ttl: Schema.optional(Schema.Literals(["5m", "1h"])),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const AnthropicTextBlock = Schema.Struct({
|
|
38
|
+
type: Schema.tag("text"),
|
|
39
|
+
text: Schema.String,
|
|
40
|
+
cache_control: Schema.optional(AnthropicCacheControl),
|
|
41
|
+
})
|
|
42
|
+
type AnthropicTextBlock = Schema.Schema.Type<typeof AnthropicTextBlock>
|
|
43
|
+
|
|
44
|
+
const AnthropicImageBlock = Schema.Struct({
|
|
45
|
+
type: Schema.tag("image"),
|
|
46
|
+
source: Schema.Struct({
|
|
47
|
+
type: Schema.tag("base64"),
|
|
48
|
+
media_type: Schema.String,
|
|
49
|
+
data: Schema.String,
|
|
50
|
+
}),
|
|
51
|
+
cache_control: Schema.optional(AnthropicCacheControl),
|
|
52
|
+
})
|
|
53
|
+
type AnthropicImageBlock = Schema.Schema.Type<typeof AnthropicImageBlock>
|
|
54
|
+
|
|
55
|
+
const AnthropicThinkingBlock = Schema.Struct({
|
|
56
|
+
type: Schema.tag("thinking"),
|
|
57
|
+
thinking: Schema.String,
|
|
58
|
+
signature: Schema.optional(Schema.String),
|
|
59
|
+
cache_control: Schema.optional(AnthropicCacheControl),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const AnthropicToolUseBlock = Schema.Struct({
|
|
63
|
+
type: Schema.tag("tool_use"),
|
|
64
|
+
id: Schema.String,
|
|
65
|
+
name: Schema.String,
|
|
66
|
+
input: Schema.Unknown,
|
|
67
|
+
cache_control: Schema.optional(AnthropicCacheControl),
|
|
68
|
+
})
|
|
69
|
+
type AnthropicToolUseBlock = Schema.Schema.Type<typeof AnthropicToolUseBlock>
|
|
70
|
+
|
|
71
|
+
const AnthropicServerToolUseBlock = Schema.Struct({
|
|
72
|
+
type: Schema.tag("server_tool_use"),
|
|
73
|
+
id: Schema.String,
|
|
74
|
+
name: Schema.String,
|
|
75
|
+
input: Schema.Unknown,
|
|
76
|
+
cache_control: Schema.optional(AnthropicCacheControl),
|
|
77
|
+
})
|
|
78
|
+
type AnthropicServerToolUseBlock = Schema.Schema.Type<typeof AnthropicServerToolUseBlock>
|
|
79
|
+
|
|
80
|
+
// Server tool result blocks: web_search_tool_result, code_execution_tool_result,
|
|
81
|
+
// and web_fetch_tool_result. The provider executes the tool and inlines the
|
|
82
|
+
// structured result into the assistant turn — there is no client tool_result
|
|
83
|
+
// round-trip. We round-trip the structured `content` payload as opaque JSON so
|
|
84
|
+
// the next request can echo it back when continuing the conversation.
|
|
85
|
+
const AnthropicServerToolResultType = Schema.Literals([
|
|
86
|
+
"web_search_tool_result",
|
|
87
|
+
"code_execution_tool_result",
|
|
88
|
+
"web_fetch_tool_result",
|
|
89
|
+
])
|
|
90
|
+
type AnthropicServerToolResultType = Schema.Schema.Type<typeof AnthropicServerToolResultType>
|
|
91
|
+
|
|
92
|
+
const AnthropicServerToolResultBlock = Schema.Struct({
|
|
93
|
+
type: AnthropicServerToolResultType,
|
|
94
|
+
tool_use_id: Schema.String,
|
|
95
|
+
content: Schema.Unknown,
|
|
96
|
+
cache_control: Schema.optional(AnthropicCacheControl),
|
|
97
|
+
})
|
|
98
|
+
type AnthropicServerToolResultBlock = Schema.Schema.Type<typeof AnthropicServerToolResultBlock>
|
|
99
|
+
|
|
100
|
+
// Anthropic accepts either a plain string or an ordered array of text/image
|
|
101
|
+
// blocks inside `tool_result.content`. The array form is required when a tool
|
|
102
|
+
// returns image bytes (screenshot, image search, etc.) so they can be passed
|
|
103
|
+
// to the model as proper image inputs instead of being JSON-stringified into
|
|
104
|
+
// the prompt — which silently inflates context by megabytes and can push the
|
|
105
|
+
// conversation over the model's token limit.
|
|
106
|
+
const AnthropicToolResultContent = Schema.Union([AnthropicTextBlock, AnthropicImageBlock])
|
|
107
|
+
|
|
108
|
+
const AnthropicToolResultBlock = Schema.Struct({
|
|
109
|
+
type: Schema.tag("tool_result"),
|
|
110
|
+
tool_use_id: Schema.String,
|
|
111
|
+
content: Schema.Union([Schema.String, Schema.Array(AnthropicToolResultContent)]),
|
|
112
|
+
is_error: Schema.optional(Schema.Boolean),
|
|
113
|
+
cache_control: Schema.optional(AnthropicCacheControl),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const AnthropicUserBlock = Schema.Union([AnthropicTextBlock, AnthropicImageBlock, AnthropicToolResultBlock])
|
|
117
|
+
type AnthropicUserBlock = Schema.Schema.Type<typeof AnthropicUserBlock>
|
|
118
|
+
const AnthropicAssistantBlock = Schema.Union([
|
|
119
|
+
AnthropicTextBlock,
|
|
120
|
+
AnthropicThinkingBlock,
|
|
121
|
+
AnthropicToolUseBlock,
|
|
122
|
+
AnthropicServerToolUseBlock,
|
|
123
|
+
AnthropicServerToolResultBlock,
|
|
124
|
+
])
|
|
125
|
+
type AnthropicAssistantBlock = Schema.Schema.Type<typeof AnthropicAssistantBlock>
|
|
126
|
+
type AnthropicToolResultBlock = Schema.Schema.Type<typeof AnthropicToolResultBlock>
|
|
127
|
+
|
|
128
|
+
const AnthropicMessage = Schema.Union([
|
|
129
|
+
Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(AnthropicUserBlock) }),
|
|
130
|
+
Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(AnthropicAssistantBlock) }),
|
|
131
|
+
Schema.Struct({ role: Schema.Literal("system"), content: Schema.Array(AnthropicTextBlock) }),
|
|
132
|
+
]).pipe(Schema.toTaggedUnion("role"))
|
|
133
|
+
type AnthropicMessage = Schema.Schema.Type<typeof AnthropicMessage>
|
|
134
|
+
|
|
135
|
+
const AnthropicTool = Schema.Struct({
|
|
136
|
+
name: Schema.String,
|
|
137
|
+
description: Schema.String,
|
|
138
|
+
input_schema: JsonObject,
|
|
139
|
+
cache_control: Schema.optional(AnthropicCacheControl),
|
|
140
|
+
})
|
|
141
|
+
type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool>
|
|
142
|
+
|
|
143
|
+
const AnthropicToolChoice = Schema.Union([
|
|
144
|
+
Schema.Struct({ type: Schema.Literals(["auto", "any"]) }),
|
|
145
|
+
Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
const AnthropicThinking = Schema.Struct({
|
|
149
|
+
type: Schema.tag("enabled"),
|
|
150
|
+
budget_tokens: Schema.Number,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const AnthropicBodyFields = {
|
|
154
|
+
model: Schema.String,
|
|
155
|
+
system: optionalArray(AnthropicTextBlock),
|
|
156
|
+
messages: Schema.Array(AnthropicMessage),
|
|
157
|
+
tools: optionalArray(AnthropicTool),
|
|
158
|
+
tool_choice: Schema.optional(AnthropicToolChoice),
|
|
159
|
+
stream: Schema.Literal(true),
|
|
160
|
+
max_tokens: Schema.Number,
|
|
161
|
+
temperature: Schema.optional(Schema.Number),
|
|
162
|
+
top_p: Schema.optional(Schema.Number),
|
|
163
|
+
top_k: Schema.optional(Schema.Number),
|
|
164
|
+
stop_sequences: optionalArray(Schema.String),
|
|
165
|
+
thinking: Schema.optional(AnthropicThinking),
|
|
166
|
+
}
|
|
167
|
+
const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
|
|
168
|
+
export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody>
|
|
169
|
+
|
|
170
|
+
const AnthropicUsage = Schema.Struct({
|
|
171
|
+
input_tokens: Schema.optional(Schema.Number),
|
|
172
|
+
output_tokens: Schema.optional(Schema.Number),
|
|
173
|
+
cache_creation_input_tokens: optionalNull(Schema.Number),
|
|
174
|
+
cache_read_input_tokens: optionalNull(Schema.Number),
|
|
175
|
+
})
|
|
176
|
+
type AnthropicUsage = Schema.Schema.Type<typeof AnthropicUsage>
|
|
177
|
+
|
|
178
|
+
const AnthropicStreamBlock = Schema.Struct({
|
|
179
|
+
type: Schema.String,
|
|
180
|
+
id: Schema.optional(Schema.String),
|
|
181
|
+
name: Schema.optional(Schema.String),
|
|
182
|
+
text: Schema.optional(Schema.String),
|
|
183
|
+
thinking: Schema.optional(Schema.String),
|
|
184
|
+
signature: Schema.optional(Schema.String),
|
|
185
|
+
input: Schema.optional(Schema.Unknown),
|
|
186
|
+
// *_tool_result blocks arrive whole as content_block_start (no streaming
|
|
187
|
+
// delta) with the structured payload in `content` and the originating
|
|
188
|
+
// server_tool_use id in `tool_use_id`.
|
|
189
|
+
tool_use_id: Schema.optional(Schema.String),
|
|
190
|
+
content: Schema.optional(Schema.Unknown),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const AnthropicStreamDelta = Schema.Struct({
|
|
194
|
+
type: Schema.optional(Schema.String),
|
|
195
|
+
text: Schema.optional(Schema.String),
|
|
196
|
+
thinking: Schema.optional(Schema.String),
|
|
197
|
+
partial_json: Schema.optional(Schema.String),
|
|
198
|
+
signature: Schema.optional(Schema.String),
|
|
199
|
+
stop_reason: optionalNull(Schema.String),
|
|
200
|
+
stop_sequence: optionalNull(Schema.String),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const AnthropicEvent = Schema.Struct({
|
|
204
|
+
type: Schema.String,
|
|
205
|
+
index: Schema.optional(Schema.Number),
|
|
206
|
+
message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })),
|
|
207
|
+
content_block: Schema.optional(AnthropicStreamBlock),
|
|
208
|
+
delta: Schema.optional(AnthropicStreamDelta),
|
|
209
|
+
usage: Schema.optional(AnthropicUsage),
|
|
210
|
+
// `type` and `message` are both required per Anthropic's spec, but
|
|
211
|
+
// OpenAI-compatible proxies and gateway translations occasionally drop one
|
|
212
|
+
// or the other; mark them optional so a partial payload still parses and
|
|
213
|
+
// the parser can fall back to whichever field is populated.
|
|
214
|
+
error: Schema.optional(
|
|
215
|
+
Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }),
|
|
216
|
+
),
|
|
217
|
+
})
|
|
218
|
+
type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
|
|
219
|
+
|
|
220
|
+
interface ParserState {
|
|
221
|
+
readonly tools: ToolStream.State<number>
|
|
222
|
+
readonly usage?: Usage
|
|
223
|
+
readonly lifecycle: Lifecycle.State
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const invalid = ProviderShared.invalidRequest
|
|
227
|
+
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// Request Lowering
|
|
230
|
+
// =============================================================================
|
|
231
|
+
// Anthropic accepts at most 4 explicit cache_control breakpoints per request,
|
|
232
|
+
// across `tools`, `system`, and `messages`. Beyond the cap the API returns a
|
|
233
|
+
// 400 — so the lowering layer counts emitted markers and silently drops any
|
|
234
|
+
// that exceed it.
|
|
235
|
+
const ANTHROPIC_BREAKPOINT_CAP = 4
|
|
236
|
+
|
|
237
|
+
const EPHEMERAL_5M = { type: "ephemeral" as const }
|
|
238
|
+
const EPHEMERAL_1H = { type: "ephemeral" as const, ttl: "1h" as const }
|
|
239
|
+
|
|
240
|
+
const cacheControl = (breakpoints: Cache.Breakpoints, cache: CacheHint | undefined) => {
|
|
241
|
+
if (cache?.type !== "ephemeral" && cache?.type !== "persistent") return undefined
|
|
242
|
+
if (breakpoints.remaining <= 0) {
|
|
243
|
+
breakpoints.dropped += 1
|
|
244
|
+
return undefined
|
|
245
|
+
}
|
|
246
|
+
breakpoints.remaining -= 1
|
|
247
|
+
return Cache.ttlBucket(cache.ttlSeconds) === "1h" ? EPHEMERAL_1H : EPHEMERAL_5M
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const anthropicMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ anthropic: metadata })
|
|
251
|
+
|
|
252
|
+
const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string | undefined => {
|
|
253
|
+
const anthropic = metadata?.anthropic
|
|
254
|
+
if (!ProviderShared.isRecord(anthropic)) return undefined
|
|
255
|
+
return typeof anthropic.signature === "string" ? anthropic.signature : undefined
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition): AnthropicTool => ({
|
|
259
|
+
name: tool.name,
|
|
260
|
+
description: tool.description,
|
|
261
|
+
input_schema: tool.inputSchema,
|
|
262
|
+
cache_control: cacheControl(breakpoints, tool.cache),
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
|
266
|
+
ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, {
|
|
267
|
+
auto: () => ({ type: "auto" as const }),
|
|
268
|
+
none: () => undefined,
|
|
269
|
+
required: () => ({ type: "any" as const }),
|
|
270
|
+
tool: (name) => ({ type: "tool" as const, name }),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const lowerToolCall = (part: ToolCallPart): AnthropicToolUseBlock => ({
|
|
274
|
+
type: "tool_use",
|
|
275
|
+
id: part.id,
|
|
276
|
+
name: part.name,
|
|
277
|
+
input: part.input,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const lowerServerToolCall = (part: ToolCallPart): AnthropicServerToolUseBlock => ({
|
|
281
|
+
type: "server_tool_use",
|
|
282
|
+
id: part.id,
|
|
283
|
+
name: part.name,
|
|
284
|
+
input: part.input,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// Server tool result blocks are typed by name. Anthropic ships three today;
|
|
288
|
+
// extend this list when new server tools land. The block content is the
|
|
289
|
+
// structured payload returned by the provider, which we round-trip as-is.
|
|
290
|
+
const serverToolResultType = (name: string): AnthropicServerToolResultType | undefined => {
|
|
291
|
+
if (name === "web_search") return "web_search_tool_result"
|
|
292
|
+
if (name === "code_execution") return "code_execution_tool_result"
|
|
293
|
+
if (name === "web_fetch") return "web_fetch_tool_result"
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (part: ToolResultPart) {
|
|
298
|
+
const wireType = serverToolResultType(part.name)
|
|
299
|
+
if (!wireType)
|
|
300
|
+
return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
|
|
301
|
+
return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: MediaPart) {
|
|
305
|
+
if (!part.mediaType.startsWith("image/"))
|
|
306
|
+
return yield* invalid(`Anthropic Messages user media content only supports images`)
|
|
307
|
+
return {
|
|
308
|
+
type: "image" as const,
|
|
309
|
+
source: {
|
|
310
|
+
type: "base64" as const,
|
|
311
|
+
media_type: part.mediaType,
|
|
312
|
+
data: ProviderShared.mediaBase64(part),
|
|
313
|
+
},
|
|
314
|
+
} satisfies AnthropicImageBlock
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// Tool results may carry structured text/images. Keep media as provider-native
|
|
318
|
+
// content instead of JSON-stringifying base64 into a prompt string.
|
|
319
|
+
const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
|
|
320
|
+
item: ToolResultContentPart,
|
|
321
|
+
) {
|
|
322
|
+
if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
|
|
323
|
+
if (item.mediaType.startsWith("image/"))
|
|
324
|
+
return {
|
|
325
|
+
type: "image" as const,
|
|
326
|
+
source: {
|
|
327
|
+
type: "base64" as const,
|
|
328
|
+
media_type: item.mediaType,
|
|
329
|
+
data: ProviderShared.mediaBase64(item),
|
|
330
|
+
},
|
|
331
|
+
} satisfies AnthropicImageBlock
|
|
332
|
+
return yield* invalid(`Anthropic Messages tool-result media content only supports images, got ${item.mediaType}`)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) {
|
|
336
|
+
// Text / json / error results stay as a string for backward compatibility
|
|
337
|
+
// with existing cassettes and provider expectations.
|
|
338
|
+
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
|
339
|
+
// Preserve the narrowed array element type when compiled through a consumer package.
|
|
340
|
+
const content: ReadonlyArray<ToolResultContentPart> = part.result.value
|
|
341
|
+
return yield* Effect.forEach(content, lowerToolResultContentItem)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// Mid-conversation system messages are a native Claude API feature only for
|
|
345
|
+
// Opus 4.8. Other Anthropic models intentionally use the same visible wrapped-
|
|
346
|
+
// user fallback as non-Anthropic routes rather than sending a role they reject.
|
|
347
|
+
const supportsNativeSystemUpdates = (request: LLMRequest) => String(request.model.id) === "claude-opus-4-8"
|
|
348
|
+
|
|
349
|
+
const endsInServerToolUse = (message: LLMRequest["messages"][number]) => {
|
|
350
|
+
const last = message.content.at(-1)
|
|
351
|
+
return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted === true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const endsInLocalToolUse = (message: LLMRequest["messages"][number]) => {
|
|
355
|
+
const last = message.content.at(-1)
|
|
356
|
+
return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted !== true
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const validateNativeSystemUpdate = Effect.fn("AnthropicMessages.validateNativeSystemUpdate")(function* (
|
|
360
|
+
messages: LLMRequest["messages"],
|
|
361
|
+
index: number,
|
|
362
|
+
) {
|
|
363
|
+
const previous = messages[index - 1]
|
|
364
|
+
const next = messages[index + 1]
|
|
365
|
+
if (!previous)
|
|
366
|
+
return yield* invalid(
|
|
367
|
+
"Anthropic Messages chronological system updates cannot be the first message; use LLMRequest.system",
|
|
368
|
+
)
|
|
369
|
+
if (previous.role === "system")
|
|
370
|
+
return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
|
|
371
|
+
if (endsInLocalToolUse(previous))
|
|
372
|
+
return yield* invalid(
|
|
373
|
+
"Anthropic Messages chronological system updates cannot appear between a local tool call and its tool result",
|
|
374
|
+
)
|
|
375
|
+
if (previous.role !== "user" && previous.role !== "tool" && !endsInServerToolUse(previous))
|
|
376
|
+
return yield* invalid(
|
|
377
|
+
"Anthropic Messages chronological system updates must follow a user message, tool result, or assistant server tool use",
|
|
378
|
+
)
|
|
379
|
+
if (next?.role === "system")
|
|
380
|
+
return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
|
|
381
|
+
if (next && next.role !== "assistant")
|
|
382
|
+
return yield* invalid(
|
|
383
|
+
"Anthropic Messages chronological system updates must end the messages array or immediately precede an assistant message",
|
|
384
|
+
)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* (
|
|
388
|
+
message: LLMRequest["messages"][number],
|
|
389
|
+
breakpoints: Cache.Breakpoints,
|
|
390
|
+
) {
|
|
391
|
+
const content = yield* ProviderShared.systemUpdateText("Anthropic Messages", message)
|
|
392
|
+
return {
|
|
393
|
+
role: "system" as const,
|
|
394
|
+
content: content.map((part) => ({
|
|
395
|
+
type: "text" as const,
|
|
396
|
+
text: part.text,
|
|
397
|
+
cache_control: cacheControl(breakpoints, part.cache),
|
|
398
|
+
})),
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
|
403
|
+
request: LLMRequest,
|
|
404
|
+
breakpoints: Cache.Breakpoints,
|
|
405
|
+
) {
|
|
406
|
+
const messages: AnthropicMessage[] = []
|
|
407
|
+
|
|
408
|
+
for (const [index, message] of request.messages.entries()) {
|
|
409
|
+
if (message.role === "system") {
|
|
410
|
+
if (supportsNativeSystemUpdates(request)) {
|
|
411
|
+
yield* validateNativeSystemUpdate(request.messages, index)
|
|
412
|
+
messages.push(yield* lowerNativeSystemUpdate(message, breakpoints))
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
|
|
416
|
+
const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
|
|
417
|
+
const previous = messages.at(-1)
|
|
418
|
+
if (previous?.role === "user")
|
|
419
|
+
messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
|
|
420
|
+
else messages.push({ role: "user", content: [block] })
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (message.role === "user") {
|
|
425
|
+
const content: AnthropicUserBlock[] = []
|
|
426
|
+
for (const part of message.content) {
|
|
427
|
+
if (part.type === "text") {
|
|
428
|
+
content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
if (part.type === "media") {
|
|
432
|
+
content.push(yield* lowerImage(part))
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text", "media"])
|
|
436
|
+
}
|
|
437
|
+
messages.push({ role: "user", content })
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (message.role === "assistant") {
|
|
442
|
+
const content: AnthropicAssistantBlock[] = []
|
|
443
|
+
for (const part of message.content) {
|
|
444
|
+
if (part.type === "text") {
|
|
445
|
+
content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
|
|
446
|
+
continue
|
|
447
|
+
}
|
|
448
|
+
if (part.type === "reasoning") {
|
|
449
|
+
content.push({
|
|
450
|
+
type: "thinking",
|
|
451
|
+
thinking: part.text,
|
|
452
|
+
signature: part.encrypted ?? signatureFromMetadata(part.providerMetadata),
|
|
453
|
+
})
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
if (part.type === "tool-call") {
|
|
457
|
+
content.push(part.providerExecuted ? lowerServerToolCall(part) : lowerToolCall(part))
|
|
458
|
+
continue
|
|
459
|
+
}
|
|
460
|
+
if (part.type === "tool-result" && part.providerExecuted) {
|
|
461
|
+
content.push(yield* lowerServerToolResult(part))
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
return yield* invalid(
|
|
465
|
+
`Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`,
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
messages.push({ role: "assistant", content })
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const content: AnthropicToolResultBlock[] = []
|
|
473
|
+
for (const part of message.content) {
|
|
474
|
+
if (!ProviderShared.supportsContent(part, ["tool-result"]))
|
|
475
|
+
return yield* ProviderShared.unsupportedContent("Anthropic Messages", "tool", ["tool-result"])
|
|
476
|
+
content.push({
|
|
477
|
+
type: "tool_result",
|
|
478
|
+
tool_use_id: part.id,
|
|
479
|
+
content: yield* lowerToolResultContent(part),
|
|
480
|
+
is_error: part.result.type === "error" ? true : undefined,
|
|
481
|
+
cache_control: cacheControl(breakpoints, part.cache),
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
messages.push({ role: "user", content })
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return messages
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic
|
|
491
|
+
|
|
492
|
+
const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
|
|
493
|
+
const thinking = anthropicOptions(request)?.thinking
|
|
494
|
+
if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined
|
|
495
|
+
const budget =
|
|
496
|
+
typeof thinking.budgetTokens === "number"
|
|
497
|
+
? thinking.budgetTokens
|
|
498
|
+
: typeof thinking.budget_tokens === "number"
|
|
499
|
+
? thinking.budget_tokens
|
|
500
|
+
: undefined
|
|
501
|
+
if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens")
|
|
502
|
+
return { type: "enabled" as const, budget_tokens: budget }
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
|
|
506
|
+
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
|
|
507
|
+
const generation = request.generation
|
|
508
|
+
// Allocate the 4-breakpoint budget in invalidation order: tools → system →
|
|
509
|
+
// messages. Tools live highest in the cache hierarchy, so when callers
|
|
510
|
+
// over-mark we keep their tool hints and shed the message-tail ones first.
|
|
511
|
+
const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
|
|
512
|
+
const tools =
|
|
513
|
+
request.tools.length === 0 || request.toolChoice?.type === "none"
|
|
514
|
+
? undefined
|
|
515
|
+
: request.tools.map((tool) => lowerTool(breakpoints, tool))
|
|
516
|
+
const system =
|
|
517
|
+
request.system.length === 0
|
|
518
|
+
? undefined
|
|
519
|
+
: request.system.map((part) => ({
|
|
520
|
+
type: "text" as const,
|
|
521
|
+
text: part.text,
|
|
522
|
+
cache_control: cacheControl(breakpoints, part.cache),
|
|
523
|
+
}))
|
|
524
|
+
const messages = yield* lowerMessages(request, breakpoints)
|
|
525
|
+
if (breakpoints.dropped > 0) {
|
|
526
|
+
yield* Effect.logWarning(
|
|
527
|
+
`Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`,
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
model: request.model.id,
|
|
532
|
+
system,
|
|
533
|
+
messages,
|
|
534
|
+
tools,
|
|
535
|
+
tool_choice: toolChoice,
|
|
536
|
+
stream: true as const,
|
|
537
|
+
max_tokens: generation?.maxTokens ?? request.model.route.defaults.limits?.output ?? 4096,
|
|
538
|
+
temperature: generation?.temperature,
|
|
539
|
+
top_p: generation?.topP,
|
|
540
|
+
top_k: generation?.topK,
|
|
541
|
+
stop_sequences: generation?.stop,
|
|
542
|
+
thinking: yield* lowerThinking(request),
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
// =============================================================================
|
|
547
|
+
// Stream Parsing
|
|
548
|
+
// =============================================================================
|
|
549
|
+
const mapFinishReason = (reason: string | null | undefined): FinishReason => {
|
|
550
|
+
if (reason === "end_turn" || reason === "stop_sequence" || reason === "pause_turn") return "stop"
|
|
551
|
+
if (reason === "max_tokens") return "length"
|
|
552
|
+
if (reason === "tool_use") return "tool-calls"
|
|
553
|
+
if (reason === "refusal") return "content-filter"
|
|
554
|
+
return "unknown"
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Anthropic reports the non-overlapping breakdown natively — its
|
|
558
|
+
// `input_tokens` is the *non-cached* count per the Messages API docs, with
|
|
559
|
+
// cache reads and writes as separate fields. We sum them to derive the
|
|
560
|
+
// inclusive `inputTokens` the rest of the contract expects. Extended
|
|
561
|
+
// thinking tokens are *not* broken out by Anthropic — they're billed as
|
|
562
|
+
// part of `output_tokens`, so `reasoningTokens` stays `undefined` and
|
|
563
|
+
// `outputTokens` carries the combined total.
|
|
564
|
+
const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => {
|
|
565
|
+
if (!usage) return undefined
|
|
566
|
+
const nonCached = usage.input_tokens
|
|
567
|
+
const cacheRead = usage.cache_read_input_tokens ?? undefined
|
|
568
|
+
const cacheWrite = usage.cache_creation_input_tokens ?? undefined
|
|
569
|
+
const inputTokens = ProviderShared.sumTokens(nonCached, cacheRead, cacheWrite)
|
|
570
|
+
return new Usage({
|
|
571
|
+
inputTokens,
|
|
572
|
+
outputTokens: usage.output_tokens,
|
|
573
|
+
nonCachedInputTokens: nonCached,
|
|
574
|
+
cacheReadInputTokens: cacheRead,
|
|
575
|
+
cacheWriteInputTokens: cacheWrite,
|
|
576
|
+
totalTokens: ProviderShared.totalTokens(inputTokens, usage.output_tokens, undefined),
|
|
577
|
+
providerMetadata: { anthropic: usage },
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Anthropic emits usage on `message_start` and again on `message_delta` — the
|
|
582
|
+
// final delta carries the authoritative totals. Right-biased merge: each
|
|
583
|
+
// field prefers `right` when defined, falls back to `left`. `inputTokens` is
|
|
584
|
+
// recomputed from the merged breakdown so the inclusive total stays
|
|
585
|
+
// consistent with `nonCached + cacheRead + cacheWrite`.
|
|
586
|
+
const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => {
|
|
587
|
+
if (!left) return right
|
|
588
|
+
if (!right) return left
|
|
589
|
+
const nonCachedInputTokens = right.nonCachedInputTokens ?? left.nonCachedInputTokens
|
|
590
|
+
const cacheReadInputTokens = right.cacheReadInputTokens ?? left.cacheReadInputTokens
|
|
591
|
+
const cacheWriteInputTokens = right.cacheWriteInputTokens ?? left.cacheWriteInputTokens
|
|
592
|
+
const inputTokens = ProviderShared.sumTokens(nonCachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens)
|
|
593
|
+
const outputTokens = right.outputTokens ?? left.outputTokens
|
|
594
|
+
return new Usage({
|
|
595
|
+
inputTokens,
|
|
596
|
+
outputTokens,
|
|
597
|
+
nonCachedInputTokens,
|
|
598
|
+
cacheReadInputTokens,
|
|
599
|
+
cacheWriteInputTokens,
|
|
600
|
+
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
|
|
601
|
+
providerMetadata: {
|
|
602
|
+
anthropic: {
|
|
603
|
+
...left.providerMetadata?.["anthropic"],
|
|
604
|
+
...right.providerMetadata?.["anthropic"],
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Server tool result blocks come whole in `content_block_start` (no streaming
|
|
611
|
+
// delta sequence). We convert the payload to a `tool-result` event with
|
|
612
|
+
// `providerExecuted: true`. The runtime appends it to the assistant message
|
|
613
|
+
// for round-trip; downstream consumers can inspect `result.value` for the
|
|
614
|
+
// structured payload.
|
|
615
|
+
const SERVER_TOOL_RESULT_NAMES: Record<AnthropicServerToolResultType, string> = {
|
|
616
|
+
web_search_tool_result: "web_search",
|
|
617
|
+
code_execution_tool_result: "code_execution",
|
|
618
|
+
web_fetch_tool_result: "web_fetch",
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const isServerToolResultType = (type: string): type is AnthropicServerToolResultType => type in SERVER_TOOL_RESULT_NAMES
|
|
622
|
+
|
|
623
|
+
const serverToolResultEvent = (block: NonNullable<AnthropicEvent["content_block"]>): LLMEvent | undefined => {
|
|
624
|
+
if (!block.type || !isServerToolResultType(block.type)) return undefined
|
|
625
|
+
const errorPayload =
|
|
626
|
+
typeof block.content === "object" && block.content !== null && "type" in block.content
|
|
627
|
+
? String((block.content as Record<string, unknown>).type)
|
|
628
|
+
: ""
|
|
629
|
+
const isError = errorPayload.endsWith("_tool_result_error")
|
|
630
|
+
return LLMEvent.toolResult({
|
|
631
|
+
id: block.tool_use_id ?? "",
|
|
632
|
+
name: SERVER_TOOL_RESULT_NAMES[block.type],
|
|
633
|
+
result: isError ? { type: "error", value: block.content } : { type: "json", value: block.content },
|
|
634
|
+
providerExecuted: true,
|
|
635
|
+
providerMetadata: anthropicMetadata({ blockType: block.type }),
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
|
|
640
|
+
|
|
641
|
+
const NO_EVENTS: StepResult["1"] = []
|
|
642
|
+
|
|
643
|
+
const onMessageStart = (state: ParserState, event: AnthropicEvent): StepResult => {
|
|
644
|
+
const usage = mapUsage(event.message?.usage)
|
|
645
|
+
return [usage ? { ...state, usage: mergeUsage(state.usage, usage) } : state, NO_EVENTS]
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepResult => {
|
|
649
|
+
const block = event.content_block
|
|
650
|
+
if (!block) return [state, NO_EVENTS]
|
|
651
|
+
|
|
652
|
+
if ((block.type === "tool_use" || block.type === "server_tool_use") && event.index !== undefined) {
|
|
653
|
+
const events: LLMEvent[] = []
|
|
654
|
+
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
|
|
655
|
+
return [
|
|
656
|
+
{
|
|
657
|
+
...state,
|
|
658
|
+
lifecycle,
|
|
659
|
+
tools: ToolStream.start(state.tools, event.index, {
|
|
660
|
+
id: block.id ?? String(event.index),
|
|
661
|
+
name: block.name ?? "",
|
|
662
|
+
providerExecuted: block.type === "server_tool_use",
|
|
663
|
+
}),
|
|
664
|
+
},
|
|
665
|
+
[...events, LLMEvent.toolInputStart({ id: block.id ?? String(event.index), name: block.name ?? "" })],
|
|
666
|
+
]
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (block.type === "text" && block.text) {
|
|
670
|
+
const events: LLMEvent[] = []
|
|
671
|
+
return [
|
|
672
|
+
{ ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, block.text) },
|
|
673
|
+
events,
|
|
674
|
+
]
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (block.type === "thinking" && block.thinking) {
|
|
678
|
+
const events: LLMEvent[] = []
|
|
679
|
+
return [
|
|
680
|
+
{
|
|
681
|
+
...state,
|
|
682
|
+
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, block.thinking),
|
|
683
|
+
},
|
|
684
|
+
events,
|
|
685
|
+
]
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const result = serverToolResultEvent(block)
|
|
689
|
+
if (!result) return [state, NO_EVENTS]
|
|
690
|
+
const events: LLMEvent[] = []
|
|
691
|
+
return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]]
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(function* (
|
|
695
|
+
state: ParserState,
|
|
696
|
+
event: AnthropicEvent,
|
|
697
|
+
) {
|
|
698
|
+
const delta = event.delta
|
|
699
|
+
|
|
700
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
701
|
+
const events: LLMEvent[] = []
|
|
702
|
+
return [
|
|
703
|
+
{ ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, delta.text) },
|
|
704
|
+
events,
|
|
705
|
+
] satisfies StepResult
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
709
|
+
const events: LLMEvent[] = []
|
|
710
|
+
return [
|
|
711
|
+
{
|
|
712
|
+
...state,
|
|
713
|
+
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, delta.thinking),
|
|
714
|
+
},
|
|
715
|
+
events,
|
|
716
|
+
] satisfies StepResult
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (delta?.type === "signature_delta" && delta.signature) {
|
|
720
|
+
const events: LLMEvent[] = []
|
|
721
|
+
return [
|
|
722
|
+
{
|
|
723
|
+
...state,
|
|
724
|
+
lifecycle: Lifecycle.reasoningEnd(
|
|
725
|
+
state.lifecycle,
|
|
726
|
+
events,
|
|
727
|
+
`reasoning-${event.index ?? 0}`,
|
|
728
|
+
anthropicMetadata({ signature: delta.signature }),
|
|
729
|
+
),
|
|
730
|
+
},
|
|
731
|
+
events,
|
|
732
|
+
] satisfies StepResult
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (delta?.type === "input_json_delta" && event.index !== undefined) {
|
|
736
|
+
if (!delta.partial_json) return [state, NO_EVENTS] satisfies StepResult
|
|
737
|
+
const result = ToolStream.appendExisting(
|
|
738
|
+
ADAPTER,
|
|
739
|
+
state.tools,
|
|
740
|
+
event.index,
|
|
741
|
+
delta.partial_json,
|
|
742
|
+
"Anthropic Messages tool argument delta is missing its tool call",
|
|
743
|
+
)
|
|
744
|
+
if (ToolStream.isError(result)) return yield* result
|
|
745
|
+
const events: LLMEvent[] = []
|
|
746
|
+
const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
|
|
747
|
+
events.push(...result.events)
|
|
748
|
+
return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return [state, NO_EVENTS] satisfies StepResult
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(function* (
|
|
755
|
+
state: ParserState,
|
|
756
|
+
event: AnthropicEvent,
|
|
757
|
+
) {
|
|
758
|
+
if (event.index === undefined) return [state, NO_EVENTS] satisfies StepResult
|
|
759
|
+
const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index)
|
|
760
|
+
const events: LLMEvent[] = []
|
|
761
|
+
const resultEvents = result.events ?? []
|
|
762
|
+
const lifecycle = resultEvents.length
|
|
763
|
+
? Lifecycle.stepStart(state.lifecycle, events)
|
|
764
|
+
: Lifecycle.reasoningEnd(
|
|
765
|
+
Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
|
|
766
|
+
events,
|
|
767
|
+
`reasoning-${event.index}`,
|
|
768
|
+
)
|
|
769
|
+
events.push(...resultEvents)
|
|
770
|
+
return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => {
|
|
774
|
+
const usage = mergeUsage(state.usage, mapUsage(event.usage))
|
|
775
|
+
const events: LLMEvent[] = []
|
|
776
|
+
const lifecycle = Lifecycle.finish(state.lifecycle, events, {
|
|
777
|
+
reason: mapFinishReason(event.delta?.stop_reason),
|
|
778
|
+
usage,
|
|
779
|
+
providerMetadata: event.delta?.stop_sequence
|
|
780
|
+
? anthropicMetadata({ stopSequence: event.delta.stop_sequence })
|
|
781
|
+
: undefined,
|
|
782
|
+
})
|
|
783
|
+
return [{ ...state, lifecycle, usage }, events]
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Prefix `error.type` so overloads, rate limits, and quota errors are visible
|
|
787
|
+
// even when the provider message is generic or empty.
|
|
788
|
+
const providerErrorMessage = (event: AnthropicEvent): string => {
|
|
789
|
+
const type = event.error?.type
|
|
790
|
+
const message = event.error?.message
|
|
791
|
+
if (type && message) return `${type}: ${message}`
|
|
792
|
+
return message || type || "Anthropic Messages stream error"
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const onError = (state: ParserState, event: AnthropicEvent): StepResult => [
|
|
796
|
+
state,
|
|
797
|
+
[LLMEvent.providerError({ message: providerErrorMessage(event) })],
|
|
798
|
+
]
|
|
799
|
+
|
|
800
|
+
const step = (state: ParserState, event: AnthropicEvent) => {
|
|
801
|
+
if (event.type === "message_start") return Effect.succeed(onMessageStart(state, event))
|
|
802
|
+
if (event.type === "content_block_start") return Effect.succeed(onContentBlockStart(state, event))
|
|
803
|
+
if (event.type === "content_block_delta") return onContentBlockDelta(state, event)
|
|
804
|
+
if (event.type === "content_block_stop") return onContentBlockStop(state, event)
|
|
805
|
+
if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event))
|
|
806
|
+
if (event.type === "error") return Effect.succeed(onError(state, event))
|
|
807
|
+
return Effect.succeed<StepResult>([state, NO_EVENTS])
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// =============================================================================
|
|
811
|
+
// Protocol And Anthropic Route
|
|
812
|
+
// =============================================================================
|
|
813
|
+
/**
|
|
814
|
+
* The Anthropic Messages protocol — request body construction, body schema,
|
|
815
|
+
* and the streaming-event state machine. Used by native Anthropic Cloud and
|
|
816
|
+
* (once registered) Vertex Anthropic / Bedrock-hosted Anthropic passthrough.
|
|
817
|
+
*/
|
|
818
|
+
export const protocol = Protocol.make({
|
|
819
|
+
id: ADAPTER,
|
|
820
|
+
body: {
|
|
821
|
+
schema: AnthropicMessagesBody,
|
|
822
|
+
from: fromRequest,
|
|
823
|
+
},
|
|
824
|
+
stream: {
|
|
825
|
+
event: Protocol.jsonEvent(AnthropicEvent),
|
|
826
|
+
initial: () => ({ tools: ToolStream.empty<number>(), lifecycle: Lifecycle.initial() }),
|
|
827
|
+
step,
|
|
828
|
+
},
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
export const route = Route.make({
|
|
832
|
+
id: ADAPTER,
|
|
833
|
+
provider: "anthropic",
|
|
834
|
+
protocol,
|
|
835
|
+
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
|
836
|
+
auth: Auth.none,
|
|
837
|
+
framing: Framing.sse,
|
|
838
|
+
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
export * as AnthropicMessages from "./anthropic-messages"
|