@strav/brain 1.0.0-alpha.22 → 1.0.0-alpha.24
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/package.json +3 -3
- package/src/agent_runner.ts +1 -1
- package/src/{provider.ts → brain_driver.ts} +11 -10
- package/src/brain_error.ts +86 -10
- package/src/brain_manager.ts +30 -7
- package/src/brain_provider.ts +16 -16
- package/src/drivers/anthropic/anthropic_brain_driver.ts +641 -0
- package/src/drivers/anthropic/anthropic_helpers.ts +65 -0
- package/src/drivers/anthropic/anthropic_message_builder.ts +258 -0
- package/src/drivers/anthropic/anthropic_response_mapper.ts +123 -0
- package/src/drivers/anthropic/anthropic_tool_loop.ts +246 -0
- package/src/drivers/anthropic/index.ts +1 -0
- package/src/{providers/deepseek_provider.ts → drivers/deepseek/deepseek_brain_driver.ts} +10 -10
- package/src/drivers/deepseek/index.ts +1 -0
- package/src/{providers/gemini_provider.ts → drivers/gemini/gemini_brain_driver.ts} +21 -21
- package/src/drivers/gemini/index.ts +1 -0
- package/src/drivers/ollama/index.ts +1 -0
- package/src/{providers/ollama_provider.ts → drivers/ollama/ollama_brain_driver.ts} +5 -5
- package/src/drivers/openai/index.ts +1 -0
- package/src/{providers/openai_provider.ts → drivers/openai/openai_brain_driver.ts} +152 -591
- package/src/drivers/openai/openai_helpers.ts +58 -0
- package/src/drivers/openai/openai_message_builder.ts +187 -0
- package/src/drivers/openai/openai_response_mapper.ts +70 -0
- package/src/drivers/openai/openai_tool_dispatch.ts +127 -0
- package/src/drivers/openai/openai_tool_loop.ts +191 -0
- package/src/drivers/openai_compat/index.ts +1 -0
- package/src/{providers/openai_compat_provider.ts → drivers/openai_compat/openai_compat_brain_driver.ts} +16 -16
- package/src/drivers/openai_responses/index.ts +1 -0
- package/src/{providers/openai_responses_provider.ts → drivers/openai_responses/openai_responses_brain_driver.ts} +24 -24
- package/src/index.ts +18 -12
- package/src/mcp/pool.ts +1 -1
- package/src/persistence/brain_message.ts +1 -1
- package/src/persistence/brain_message_repository.ts +3 -11
- package/src/persistence/brain_suspended_run.ts +1 -1
- package/src/persistence/brain_suspended_run_repository.ts +2 -11
- package/src/persistence/brain_thread.ts +1 -1
- package/src/persistence/brain_thread_repository.ts +2 -11
- package/src/persistence/index.ts +1 -1
- package/src/tool_runner.ts +1 -1
- package/src/types.ts +2 -2
- package/src/providers/anthropic_provider.ts +0 -1194
- /package/src/persistence/{schema → schemas}/brain_message_schema.ts +0 -0
- /package/src/persistence/{schema → schemas}/brain_suspended_run_schema.ts +0 -0
- /package/src/persistence/{schema → schemas}/brain_thread_schema.ts +0 -0
- /package/src/persistence/{schema → schemas}/index.ts +0 -0
|
@@ -1,1194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `AnthropicProvider` — implementation of `Provider` backed by the
|
|
3
|
-
* official `@anthropic-ai/sdk`.
|
|
4
|
-
*
|
|
5
|
-
* Responsibilities:
|
|
6
|
-
* 1. Hold a singleton `Anthropic` client instance for the
|
|
7
|
-
* configured API key + base URL.
|
|
8
|
-
* 2. Translate the framework's `ChatOptions` / `Message` shapes
|
|
9
|
-
* into Anthropic's `MessageCreateParams` (system as `TextBlock[]`
|
|
10
|
-
* with `cache_control` when requested; messages with per-block
|
|
11
|
-
* cache flags translated likewise; `thinking` mapped to
|
|
12
|
-
* `ThinkingConfigParam`; `effort` placed under `output_config`).
|
|
13
|
-
* 3. Translate the response back to `ChatResult` — flatten the
|
|
14
|
-
* content blocks into a single `text` string, surface usage with
|
|
15
|
-
* cache-hit counters, and pass the raw `Message` through on `.raw`.
|
|
16
|
-
* 4. Stream via `client.messages.stream()` and yield the framework
|
|
17
|
-
* `StreamEvent` union — `text` deltas plus a terminal `stop`
|
|
18
|
-
* event with usage + stop reason.
|
|
19
|
-
*
|
|
20
|
-
* Errors from the SDK propagate; apps that want provider-specific
|
|
21
|
-
* recovery can `instanceof Anthropic.RateLimitError` etc. The brain
|
|
22
|
-
* facade wraps the call site in `BrainError` only for invariants the
|
|
23
|
-
* facade owns (e.g. "no provider configured").
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import Anthropic from '@anthropic-ai/sdk'
|
|
27
|
-
import type { AgentResult } from '../agent_result.ts'
|
|
28
|
-
import type { AnthropicProviderConfig } from '../brain_config.ts'
|
|
29
|
-
import { DEFAULT_MODEL } from '../brain_config.ts'
|
|
30
|
-
import { BrainError } from '../brain_error.ts'
|
|
31
|
-
import type {
|
|
32
|
-
Provider,
|
|
33
|
-
RunWithToolsOptions,
|
|
34
|
-
RunWithToolsOptionsWithSuspend,
|
|
35
|
-
} from '../provider.ts'
|
|
36
|
-
import type { SuspendedRun } from '../suspended_run.ts'
|
|
37
|
-
import type { Tool } from '../tool.ts'
|
|
38
|
-
import type {
|
|
39
|
-
ChatOptions,
|
|
40
|
-
ChatResult,
|
|
41
|
-
ChatUsage,
|
|
42
|
-
CompactionBlock,
|
|
43
|
-
ContentBlock,
|
|
44
|
-
GenerateResult,
|
|
45
|
-
MCPToolResultBlock,
|
|
46
|
-
MCPToolUseBlock,
|
|
47
|
-
Message,
|
|
48
|
-
ServerTool,
|
|
49
|
-
StreamEvent,
|
|
50
|
-
SystemPrompt,
|
|
51
|
-
TextBlock,
|
|
52
|
-
ToolResultBlock,
|
|
53
|
-
ToolUseBlock,
|
|
54
|
-
} from '../types.ts'
|
|
55
|
-
import type { AgentGenerateResult } from '../agent_generate_result.ts'
|
|
56
|
-
import type { AgentStreamEvent } from '../agent_stream_event.ts'
|
|
57
|
-
import { parseGenerated, type OutputSchema } from '../output_schema.ts'
|
|
58
|
-
import { runToolWithRecovery } from '../tool_runner.ts'
|
|
59
|
-
|
|
60
|
-
const EPHEMERAL_CACHE = { type: 'ephemeral' } as const
|
|
61
|
-
|
|
62
|
-
export class AnthropicProvider implements Provider {
|
|
63
|
-
readonly name: string
|
|
64
|
-
private readonly client: Anthropic
|
|
65
|
-
private readonly defaultModel: string
|
|
66
|
-
private readonly defaultMaxTokens: number
|
|
67
|
-
private readonly betas: readonly string[]
|
|
68
|
-
|
|
69
|
-
constructor(
|
|
70
|
-
name: string,
|
|
71
|
-
config: AnthropicProviderConfig,
|
|
72
|
-
options: { client?: Anthropic } = {},
|
|
73
|
-
) {
|
|
74
|
-
this.name = name
|
|
75
|
-
this.defaultModel = config.defaultModel ?? DEFAULT_MODEL
|
|
76
|
-
this.defaultMaxTokens = config.defaultMaxTokens ?? 4096
|
|
77
|
-
this.betas = config.betas ?? []
|
|
78
|
-
// `client` injection point — tests pass a stub; apps that want a
|
|
79
|
-
// pre-configured SDK instance (custom retry, fetch transport, etc.)
|
|
80
|
-
// build their own and hand it over here.
|
|
81
|
-
this.client =
|
|
82
|
-
options.client ??
|
|
83
|
-
new Anthropic({
|
|
84
|
-
apiKey: config.apiKey,
|
|
85
|
-
...(config.baseUrl !== undefined ? { baseURL: config.baseUrl } : {}),
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async chat(messages: readonly Message[], options: ChatOptions = {}): Promise<ChatResult> {
|
|
90
|
-
const params = this.buildParams(messages, options)
|
|
91
|
-
const useBeta = needsBetaRouting(params)
|
|
92
|
-
const response = useBeta
|
|
93
|
-
? ((await this.client.beta.messages.create(
|
|
94
|
-
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
95
|
-
reqOpts(options),
|
|
96
|
-
)) as unknown as Anthropic.Message)
|
|
97
|
-
: await this.client.messages.create(params, reqOpts(options))
|
|
98
|
-
return this.toChatResult(response)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async *stream(
|
|
102
|
-
messages: readonly Message[],
|
|
103
|
-
options: ChatOptions = {},
|
|
104
|
-
): AsyncIterable<StreamEvent> {
|
|
105
|
-
const params = this.buildParams(messages, options)
|
|
106
|
-
const stream = needsBetaRouting(params)
|
|
107
|
-
? this.client.beta.messages.stream(
|
|
108
|
-
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsStreaming,
|
|
109
|
-
reqOpts(options),
|
|
110
|
-
)
|
|
111
|
-
: this.client.messages.stream(params, reqOpts(options))
|
|
112
|
-
for await (const event of stream) {
|
|
113
|
-
if (
|
|
114
|
-
event.type === 'content_block_delta' &&
|
|
115
|
-
event.delta.type === 'text_delta'
|
|
116
|
-
) {
|
|
117
|
-
yield { type: 'text', delta: event.delta.text }
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const final = await stream.finalMessage()
|
|
121
|
-
yield {
|
|
122
|
-
type: 'stop',
|
|
123
|
-
stopReason: final.stop_reason,
|
|
124
|
-
usage: toUsage(final.usage),
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async countTokens(
|
|
129
|
-
messages: readonly Message[],
|
|
130
|
-
options: ChatOptions = {},
|
|
131
|
-
): Promise<number> {
|
|
132
|
-
const base = this.buildParams(messages, options)
|
|
133
|
-
// count_tokens only accepts a subset of MessageCreateParams; build
|
|
134
|
-
// a focused payload that matches what apps actually need to budget.
|
|
135
|
-
const result = await this.client.messages.countTokens(
|
|
136
|
-
{
|
|
137
|
-
model: base.model,
|
|
138
|
-
messages: base.messages,
|
|
139
|
-
...(base.system !== undefined ? { system: base.system } : {}),
|
|
140
|
-
...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
|
|
141
|
-
},
|
|
142
|
-
reqOpts(options),
|
|
143
|
-
)
|
|
144
|
-
return result.input_tokens
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Agentic loop. Send → detect tool_use blocks → execute → append
|
|
149
|
-
* tool_result → re-send, until the model returns `end_turn` or
|
|
150
|
-
* the iteration ceiling is hit.
|
|
151
|
-
*
|
|
152
|
-
* Tools are passed once on every call — Anthropic doesn't carry
|
|
153
|
-
* tool state across requests; the model rediscovers them from the
|
|
154
|
-
* `tools` array each turn. Apps that care about cache hits keep
|
|
155
|
-
* the tool list stable across runs.
|
|
156
|
-
*/
|
|
157
|
-
runWithTools(
|
|
158
|
-
messages: readonly Message[],
|
|
159
|
-
tools: readonly Tool[],
|
|
160
|
-
options: RunWithToolsOptionsWithSuspend,
|
|
161
|
-
): Promise<AgentResult | SuspendedRun>
|
|
162
|
-
runWithTools(
|
|
163
|
-
messages: readonly Message[],
|
|
164
|
-
tools: readonly Tool[],
|
|
165
|
-
options?: RunWithToolsOptions,
|
|
166
|
-
): Promise<AgentResult>
|
|
167
|
-
async runWithTools(
|
|
168
|
-
messages: readonly Message[],
|
|
169
|
-
tools: readonly Tool[],
|
|
170
|
-
options: RunWithToolsOptions = {},
|
|
171
|
-
): Promise<AgentResult | SuspendedRun> {
|
|
172
|
-
const maxIterations = options.maxIterations ?? 10
|
|
173
|
-
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
174
|
-
const workingMessages: Message[] = [...messages]
|
|
175
|
-
const aggregated: ChatUsage = {
|
|
176
|
-
inputTokens: 0,
|
|
177
|
-
outputTokens: 0,
|
|
178
|
-
cacheReadTokens: 0,
|
|
179
|
-
cacheCreationTokens: 0,
|
|
180
|
-
}
|
|
181
|
-
let iterations = 0
|
|
182
|
-
let lastStopReason: string | null = null
|
|
183
|
-
|
|
184
|
-
const mcpServers = options.mcpServers ?? []
|
|
185
|
-
const useMcpBeta = mcpServers.length > 0
|
|
186
|
-
|
|
187
|
-
while (true) {
|
|
188
|
-
checkAborted(options.signal)
|
|
189
|
-
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
190
|
-
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
191
|
-
}
|
|
192
|
-
params.tools = [
|
|
193
|
-
// Server tools placed first when present (from buildParams).
|
|
194
|
-
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
195
|
-
...tools.map((t) => ({
|
|
196
|
-
name: t.name,
|
|
197
|
-
description: t.description,
|
|
198
|
-
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
199
|
-
})),
|
|
200
|
-
// MCP toolsets — one per declared server. The model sees the
|
|
201
|
-
// server's tools via Anthropic's connector, not via our local
|
|
202
|
-
// `tools` list.
|
|
203
|
-
...mcpServers
|
|
204
|
-
.filter((s) => s.tools?.enabled !== false)
|
|
205
|
-
.map((s) => ({
|
|
206
|
-
type: 'mcp_toolset' as const,
|
|
207
|
-
mcp_server_name: s.name,
|
|
208
|
-
...(s.tools?.allowedTools
|
|
209
|
-
? { allowed_tools: [...s.tools.allowedTools] }
|
|
210
|
-
: {}),
|
|
211
|
-
})),
|
|
212
|
-
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
213
|
-
|
|
214
|
-
// Declare MCP servers + flip to the beta surface when in use.
|
|
215
|
-
// Anthropic's MCP connector requires `mcp-client-2025-11-20`.
|
|
216
|
-
if (useMcpBeta) {
|
|
217
|
-
params.mcp_servers = mcpServers.map((s) => {
|
|
218
|
-
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
219
|
-
type: 'url',
|
|
220
|
-
name: s.name,
|
|
221
|
-
url: s.url,
|
|
222
|
-
}
|
|
223
|
-
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
224
|
-
return def
|
|
225
|
-
})
|
|
226
|
-
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
227
|
-
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
228
|
-
? [...baseBetas]
|
|
229
|
-
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
230
|
-
}
|
|
231
|
-
// Route via beta when either MCP servers OR compaction are in
|
|
232
|
-
// play — both live on the beta surface.
|
|
233
|
-
const response: Anthropic.Message = needsBetaRouting(params)
|
|
234
|
-
? ((await this.client.beta.messages.create(
|
|
235
|
-
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
236
|
-
reqOpts(options),
|
|
237
|
-
)) as unknown as Anthropic.Message)
|
|
238
|
-
: await this.client.messages.create(params, reqOpts(options))
|
|
239
|
-
addUsage(aggregated, response.usage)
|
|
240
|
-
lastStopReason = response.stop_reason ?? null
|
|
241
|
-
|
|
242
|
-
// Append the assistant turn verbatim from the SDK shape so
|
|
243
|
-
// tool_use blocks survive to the next request unchanged.
|
|
244
|
-
workingMessages.push({
|
|
245
|
-
role: 'assistant',
|
|
246
|
-
content: fromAnthropicContent(response.content),
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
if (response.stop_reason !== 'tool_use') {
|
|
250
|
-
return {
|
|
251
|
-
text: collectText(response.content),
|
|
252
|
-
messages: workingMessages,
|
|
253
|
-
iterations,
|
|
254
|
-
stopReason: lastStopReason ?? 'end_turn',
|
|
255
|
-
usage: aggregated,
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Execute every tool_use block in the response and append the
|
|
260
|
-
// results in a single user-role turn. The SDK's API expects all
|
|
261
|
-
// tool_result blocks for a given assistant turn to land in the
|
|
262
|
-
// same user message.
|
|
263
|
-
const toolUseBlocks = response.content.filter(
|
|
264
|
-
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
265
|
-
)
|
|
266
|
-
const resultBlocks: ContentBlock[] = []
|
|
267
|
-
for (let i = 0; i < toolUseBlocks.length; i++) {
|
|
268
|
-
const block = toolUseBlocks[i]!
|
|
269
|
-
if (options.shouldSuspend) {
|
|
270
|
-
const frameworkCall: ToolUseBlock = {
|
|
271
|
-
type: 'tool_use',
|
|
272
|
-
id: block.id,
|
|
273
|
-
name: block.name,
|
|
274
|
-
input: block.input as Record<string, unknown>,
|
|
275
|
-
}
|
|
276
|
-
if (await options.shouldSuspend(frameworkCall, options.context)) {
|
|
277
|
-
return {
|
|
278
|
-
status: 'suspended',
|
|
279
|
-
pendingToolCalls: toolUseBlocks.slice(i).map((b) => ({
|
|
280
|
-
type: 'tool_use',
|
|
281
|
-
id: b.id,
|
|
282
|
-
name: b.name,
|
|
283
|
-
input: b.input as Record<string, unknown>,
|
|
284
|
-
})),
|
|
285
|
-
state: { messages: workingMessages, iterations, usage: aggregated },
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const { content, isError } = await runToolWithRecovery(
|
|
290
|
-
toolMap.get(block.name),
|
|
291
|
-
block.name,
|
|
292
|
-
block.id,
|
|
293
|
-
block.input,
|
|
294
|
-
options,
|
|
295
|
-
)
|
|
296
|
-
const resultBlock: ToolResultBlock = {
|
|
297
|
-
type: 'tool_result',
|
|
298
|
-
toolUseId: block.id,
|
|
299
|
-
content,
|
|
300
|
-
...(isError ? { isError: true } : {}),
|
|
301
|
-
}
|
|
302
|
-
resultBlocks.push(resultBlock)
|
|
303
|
-
}
|
|
304
|
-
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
305
|
-
|
|
306
|
-
iterations++
|
|
307
|
-
if (iterations >= maxIterations) {
|
|
308
|
-
return {
|
|
309
|
-
text: collectText(response.content),
|
|
310
|
-
messages: workingMessages,
|
|
311
|
-
iterations,
|
|
312
|
-
stopReason: 'max_iterations',
|
|
313
|
-
usage: aggregated,
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async runWithToolsAndSchema<T>(
|
|
320
|
-
messages: readonly Message[],
|
|
321
|
-
tools: readonly Tool[],
|
|
322
|
-
schema: OutputSchema<T>,
|
|
323
|
-
options: RunWithToolsOptions = {},
|
|
324
|
-
): Promise<AgentGenerateResult<T>> {
|
|
325
|
-
const maxIterations = options.maxIterations ?? 10
|
|
326
|
-
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
327
|
-
const workingMessages: Message[] = [...messages]
|
|
328
|
-
const aggregated: ChatUsage = {
|
|
329
|
-
inputTokens: 0,
|
|
330
|
-
outputTokens: 0,
|
|
331
|
-
cacheReadTokens: 0,
|
|
332
|
-
cacheCreationTokens: 0,
|
|
333
|
-
}
|
|
334
|
-
let iterations = 0
|
|
335
|
-
let lastStopReason: string | null = null
|
|
336
|
-
|
|
337
|
-
const mcpServers = options.mcpServers ?? []
|
|
338
|
-
const useMcpBeta = mcpServers.length > 0
|
|
339
|
-
|
|
340
|
-
while (true) {
|
|
341
|
-
checkAborted(options.signal)
|
|
342
|
-
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
343
|
-
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
344
|
-
}
|
|
345
|
-
params.tools = [
|
|
346
|
-
// Server tools placed first when present (from buildParams).
|
|
347
|
-
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
348
|
-
...tools.map((t) => ({
|
|
349
|
-
name: t.name,
|
|
350
|
-
description: t.description,
|
|
351
|
-
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
352
|
-
})),
|
|
353
|
-
...mcpServers
|
|
354
|
-
.filter((s) => s.tools?.enabled !== false)
|
|
355
|
-
.map((s) => ({
|
|
356
|
-
type: 'mcp_toolset' as const,
|
|
357
|
-
mcp_server_name: s.name,
|
|
358
|
-
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
359
|
-
})),
|
|
360
|
-
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
361
|
-
params.output_config = {
|
|
362
|
-
...(params.output_config ?? {}),
|
|
363
|
-
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (useMcpBeta) {
|
|
367
|
-
params.mcp_servers = mcpServers.map((s) => {
|
|
368
|
-
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
369
|
-
type: 'url',
|
|
370
|
-
name: s.name,
|
|
371
|
-
url: s.url,
|
|
372
|
-
}
|
|
373
|
-
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
374
|
-
return def
|
|
375
|
-
})
|
|
376
|
-
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
377
|
-
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
378
|
-
? [...baseBetas]
|
|
379
|
-
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
380
|
-
}
|
|
381
|
-
const response: Anthropic.Message = needsBetaRouting(params)
|
|
382
|
-
? ((await this.client.beta.messages.create(
|
|
383
|
-
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsNonStreaming,
|
|
384
|
-
reqOpts(options),
|
|
385
|
-
)) as unknown as Anthropic.Message)
|
|
386
|
-
: await this.client.messages.create(params, reqOpts(options))
|
|
387
|
-
addUsage(aggregated, response.usage)
|
|
388
|
-
lastStopReason = response.stop_reason ?? null
|
|
389
|
-
|
|
390
|
-
workingMessages.push({
|
|
391
|
-
role: 'assistant',
|
|
392
|
-
content: fromAnthropicContent(response.content),
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
if (response.stop_reason !== 'tool_use') {
|
|
396
|
-
const text = collectText(response.content)
|
|
397
|
-
return {
|
|
398
|
-
value: parseGenerated(text, schema),
|
|
399
|
-
text,
|
|
400
|
-
messages: workingMessages,
|
|
401
|
-
iterations,
|
|
402
|
-
stopReason: lastStopReason ?? 'end_turn',
|
|
403
|
-
usage: aggregated,
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const toolUseBlocks = response.content.filter(
|
|
408
|
-
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
409
|
-
)
|
|
410
|
-
const resultBlocks: ContentBlock[] = []
|
|
411
|
-
for (const block of toolUseBlocks) {
|
|
412
|
-
const { content, isError } = await runToolWithRecovery(
|
|
413
|
-
toolMap.get(block.name),
|
|
414
|
-
block.name,
|
|
415
|
-
block.id,
|
|
416
|
-
block.input,
|
|
417
|
-
options,
|
|
418
|
-
)
|
|
419
|
-
const resultBlock: ToolResultBlock = {
|
|
420
|
-
type: 'tool_result',
|
|
421
|
-
toolUseId: block.id,
|
|
422
|
-
content,
|
|
423
|
-
...(isError ? { isError: true } : {}),
|
|
424
|
-
}
|
|
425
|
-
resultBlocks.push(resultBlock)
|
|
426
|
-
}
|
|
427
|
-
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
428
|
-
|
|
429
|
-
iterations++
|
|
430
|
-
if (iterations >= maxIterations) {
|
|
431
|
-
const text = collectText(response.content)
|
|
432
|
-
// Last turn was a tool_use response, so text may be empty —
|
|
433
|
-
// surface what we have but the value will likely fail parse.
|
|
434
|
-
return {
|
|
435
|
-
value: parseGenerated(text, schema),
|
|
436
|
-
text,
|
|
437
|
-
messages: workingMessages,
|
|
438
|
-
iterations,
|
|
439
|
-
stopReason: 'max_iterations',
|
|
440
|
-
usage: aggregated,
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async *streamWithTools(
|
|
447
|
-
messages: readonly Message[],
|
|
448
|
-
tools: readonly Tool[],
|
|
449
|
-
options: RunWithToolsOptions = {},
|
|
450
|
-
): AsyncIterable<AgentStreamEvent> {
|
|
451
|
-
const maxIterations = options.maxIterations ?? 10
|
|
452
|
-
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
453
|
-
const workingMessages: Message[] = [...messages]
|
|
454
|
-
const aggregated: ChatUsage = {
|
|
455
|
-
inputTokens: 0,
|
|
456
|
-
outputTokens: 0,
|
|
457
|
-
cacheReadTokens: 0,
|
|
458
|
-
cacheCreationTokens: 0,
|
|
459
|
-
}
|
|
460
|
-
let iterations = 0
|
|
461
|
-
|
|
462
|
-
const mcpServers = options.mcpServers ?? []
|
|
463
|
-
const useMcpBeta = mcpServers.length > 0
|
|
464
|
-
|
|
465
|
-
while (true) {
|
|
466
|
-
checkAborted(options.signal)
|
|
467
|
-
yield { type: 'iteration_start', iteration: iterations }
|
|
468
|
-
|
|
469
|
-
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
470
|
-
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
471
|
-
}
|
|
472
|
-
params.tools = [
|
|
473
|
-
// Server tools placed first when present (from buildParams).
|
|
474
|
-
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
475
|
-
...tools.map((t) => ({
|
|
476
|
-
name: t.name,
|
|
477
|
-
description: t.description,
|
|
478
|
-
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
479
|
-
})),
|
|
480
|
-
...mcpServers
|
|
481
|
-
.filter((s) => s.tools?.enabled !== false)
|
|
482
|
-
.map((s) => ({
|
|
483
|
-
type: 'mcp_toolset' as const,
|
|
484
|
-
mcp_server_name: s.name,
|
|
485
|
-
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
486
|
-
})),
|
|
487
|
-
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
488
|
-
|
|
489
|
-
if (useMcpBeta) {
|
|
490
|
-
params.mcp_servers = mcpServers.map((s) => {
|
|
491
|
-
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
492
|
-
type: 'url',
|
|
493
|
-
name: s.name,
|
|
494
|
-
url: s.url,
|
|
495
|
-
}
|
|
496
|
-
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
497
|
-
return def
|
|
498
|
-
})
|
|
499
|
-
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
500
|
-
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
501
|
-
? [...baseBetas]
|
|
502
|
-
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const stream = needsBetaRouting(params)
|
|
506
|
-
? this.client.beta.messages.stream(
|
|
507
|
-
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsStreaming,
|
|
508
|
-
reqOpts(options),
|
|
509
|
-
)
|
|
510
|
-
: this.client.messages.stream(params, reqOpts(options))
|
|
511
|
-
|
|
512
|
-
// Track tool_use content blocks by their stream index so
|
|
513
|
-
// `input_json_delta` events can be paired with the correct id.
|
|
514
|
-
// Anthropic's streaming protocol issues a `content_block_start`
|
|
515
|
-
// carrying the tool's id + name, then a sequence of
|
|
516
|
-
// `input_json_delta`s with `partial_json` chunks, then a
|
|
517
|
-
// `content_block_stop`.
|
|
518
|
-
const toolBlockIdByIndex = new Map<number, string>()
|
|
519
|
-
for await (const event of stream) {
|
|
520
|
-
if (
|
|
521
|
-
event.type === 'content_block_start' &&
|
|
522
|
-
event.content_block.type === 'tool_use'
|
|
523
|
-
) {
|
|
524
|
-
toolBlockIdByIndex.set(event.index, event.content_block.id)
|
|
525
|
-
yield {
|
|
526
|
-
type: 'tool_use_start',
|
|
527
|
-
id: event.content_block.id,
|
|
528
|
-
name: event.content_block.name,
|
|
529
|
-
}
|
|
530
|
-
} else if (event.type === 'content_block_delta') {
|
|
531
|
-
if (event.delta.type === 'text_delta' && event.delta.text.length > 0) {
|
|
532
|
-
yield { type: 'text', delta: event.delta.text }
|
|
533
|
-
} else if (event.delta.type === 'input_json_delta') {
|
|
534
|
-
const id = toolBlockIdByIndex.get(event.index)
|
|
535
|
-
if (id !== undefined && event.delta.partial_json.length > 0) {
|
|
536
|
-
yield { type: 'tool_use_delta', id, argsDelta: event.delta.partial_json }
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
const final = (await stream.finalMessage()) as unknown as Anthropic.Message
|
|
542
|
-
addUsage(aggregated, final.usage)
|
|
543
|
-
const finishReason: string | null = final.stop_reason ?? null
|
|
544
|
-
|
|
545
|
-
yield { type: 'iteration_end', iteration: iterations, stopReason: finishReason }
|
|
546
|
-
|
|
547
|
-
workingMessages.push({
|
|
548
|
-
role: 'assistant',
|
|
549
|
-
content: fromAnthropicContent(final.content),
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
if (final.stop_reason !== 'tool_use') {
|
|
553
|
-
yield {
|
|
554
|
-
type: 'stop',
|
|
555
|
-
stopReason: finishReason ?? 'end_turn',
|
|
556
|
-
iterations,
|
|
557
|
-
usage: aggregated,
|
|
558
|
-
messages: workingMessages,
|
|
559
|
-
}
|
|
560
|
-
return
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const toolUseBlocks = final.content.filter(
|
|
564
|
-
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
565
|
-
)
|
|
566
|
-
const resultBlocks: ContentBlock[] = []
|
|
567
|
-
for (const block of toolUseBlocks) {
|
|
568
|
-
yield { type: 'tool_use', id: block.id, name: block.name, input: block.input }
|
|
569
|
-
const { content, isError } = await runToolWithRecovery(
|
|
570
|
-
toolMap.get(block.name),
|
|
571
|
-
block.name,
|
|
572
|
-
block.id,
|
|
573
|
-
block.input,
|
|
574
|
-
options,
|
|
575
|
-
)
|
|
576
|
-
resultBlocks.push({
|
|
577
|
-
type: 'tool_result',
|
|
578
|
-
toolUseId: block.id,
|
|
579
|
-
content,
|
|
580
|
-
...(isError ? { isError: true } : {}),
|
|
581
|
-
} satisfies ToolResultBlock)
|
|
582
|
-
yield {
|
|
583
|
-
type: 'tool_result',
|
|
584
|
-
id: block.id,
|
|
585
|
-
name: block.name,
|
|
586
|
-
content,
|
|
587
|
-
isError,
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
591
|
-
|
|
592
|
-
iterations++
|
|
593
|
-
if (iterations >= maxIterations) {
|
|
594
|
-
yield {
|
|
595
|
-
type: 'stop',
|
|
596
|
-
stopReason: 'max_iterations',
|
|
597
|
-
iterations,
|
|
598
|
-
usage: aggregated,
|
|
599
|
-
messages: workingMessages,
|
|
600
|
-
}
|
|
601
|
-
return
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async *streamWithToolsAndSchema<T>(
|
|
607
|
-
messages: readonly Message[],
|
|
608
|
-
tools: readonly Tool[],
|
|
609
|
-
schema: OutputSchema<T>,
|
|
610
|
-
options: RunWithToolsOptions = {},
|
|
611
|
-
): AsyncIterable<AgentStreamEvent<T>> {
|
|
612
|
-
const maxIterations = options.maxIterations ?? 10
|
|
613
|
-
const toolMap = new Map<string, Tool>(tools.map((t) => [t.name, t]))
|
|
614
|
-
const workingMessages: Message[] = [...messages]
|
|
615
|
-
const aggregated: ChatUsage = {
|
|
616
|
-
inputTokens: 0,
|
|
617
|
-
outputTokens: 0,
|
|
618
|
-
cacheReadTokens: 0,
|
|
619
|
-
cacheCreationTokens: 0,
|
|
620
|
-
}
|
|
621
|
-
let iterations = 0
|
|
622
|
-
|
|
623
|
-
const mcpServers = options.mcpServers ?? []
|
|
624
|
-
const useMcpBeta = mcpServers.length > 0
|
|
625
|
-
|
|
626
|
-
while (true) {
|
|
627
|
-
checkAborted(options.signal)
|
|
628
|
-
yield { type: 'iteration_start', iteration: iterations }
|
|
629
|
-
|
|
630
|
-
const params = this.buildParams(workingMessages, options) as Anthropic.MessageCreateParamsNonStreaming & {
|
|
631
|
-
mcp_servers?: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition[]
|
|
632
|
-
}
|
|
633
|
-
params.tools = [
|
|
634
|
-
// Server tools placed first when present (from buildParams).
|
|
635
|
-
...((params.tools ?? []) as Anthropic.ToolUnion[]),
|
|
636
|
-
...tools.map((t) => ({
|
|
637
|
-
name: t.name,
|
|
638
|
-
description: t.description,
|
|
639
|
-
input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
|
|
640
|
-
})),
|
|
641
|
-
...mcpServers
|
|
642
|
-
.filter((s) => s.tools?.enabled !== false)
|
|
643
|
-
.map((s) => ({
|
|
644
|
-
type: 'mcp_toolset' as const,
|
|
645
|
-
mcp_server_name: s.name,
|
|
646
|
-
...(s.tools?.allowedTools ? { allowed_tools: [...s.tools.allowedTools] } : {}),
|
|
647
|
-
})),
|
|
648
|
-
] as unknown as Anthropic.MessageCreateParams['tools']
|
|
649
|
-
params.output_config = {
|
|
650
|
-
...(params.output_config ?? {}),
|
|
651
|
-
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
if (useMcpBeta) {
|
|
655
|
-
params.mcp_servers = mcpServers.map((s) => {
|
|
656
|
-
const def: Anthropic.Beta.Messages.BetaRequestMCPServerURLDefinition = {
|
|
657
|
-
type: 'url',
|
|
658
|
-
name: s.name,
|
|
659
|
-
url: s.url,
|
|
660
|
-
}
|
|
661
|
-
if (s.authorizationToken !== undefined) def.authorization_token = s.authorizationToken
|
|
662
|
-
return def
|
|
663
|
-
})
|
|
664
|
-
const baseBetas = (params as { betas?: readonly string[] }).betas ?? []
|
|
665
|
-
;(params as { betas?: string[] }).betas = baseBetas.includes('mcp-client-2025-11-20')
|
|
666
|
-
? [...baseBetas]
|
|
667
|
-
: [...baseBetas, 'mcp-client-2025-11-20']
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const stream = needsBetaRouting(params)
|
|
671
|
-
? this.client.beta.messages.stream(
|
|
672
|
-
params as unknown as Anthropic.Beta.Messages.MessageCreateParamsStreaming,
|
|
673
|
-
reqOpts(options),
|
|
674
|
-
)
|
|
675
|
-
: this.client.messages.stream(params, reqOpts(options))
|
|
676
|
-
|
|
677
|
-
// Track tool_use content blocks by their stream index so
|
|
678
|
-
// `input_json_delta` events can be paired with the correct id.
|
|
679
|
-
// Anthropic's streaming protocol issues a `content_block_start`
|
|
680
|
-
// carrying the tool's id + name, then a sequence of
|
|
681
|
-
// `input_json_delta`s with `partial_json` chunks, then a
|
|
682
|
-
// `content_block_stop`.
|
|
683
|
-
const toolBlockIdByIndex = new Map<number, string>()
|
|
684
|
-
for await (const event of stream) {
|
|
685
|
-
if (
|
|
686
|
-
event.type === 'content_block_start' &&
|
|
687
|
-
event.content_block.type === 'tool_use'
|
|
688
|
-
) {
|
|
689
|
-
toolBlockIdByIndex.set(event.index, event.content_block.id)
|
|
690
|
-
yield {
|
|
691
|
-
type: 'tool_use_start',
|
|
692
|
-
id: event.content_block.id,
|
|
693
|
-
name: event.content_block.name,
|
|
694
|
-
}
|
|
695
|
-
} else if (event.type === 'content_block_delta') {
|
|
696
|
-
if (event.delta.type === 'text_delta' && event.delta.text.length > 0) {
|
|
697
|
-
yield { type: 'text', delta: event.delta.text }
|
|
698
|
-
} else if (event.delta.type === 'input_json_delta') {
|
|
699
|
-
const id = toolBlockIdByIndex.get(event.index)
|
|
700
|
-
if (id !== undefined && event.delta.partial_json.length > 0) {
|
|
701
|
-
yield { type: 'tool_use_delta', id, argsDelta: event.delta.partial_json }
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
const final = (await stream.finalMessage()) as unknown as Anthropic.Message
|
|
707
|
-
addUsage(aggregated, final.usage)
|
|
708
|
-
const finishReason: string | null = final.stop_reason ?? null
|
|
709
|
-
yield { type: 'iteration_end', iteration: iterations, stopReason: finishReason }
|
|
710
|
-
|
|
711
|
-
workingMessages.push({
|
|
712
|
-
role: 'assistant',
|
|
713
|
-
content: fromAnthropicContent(final.content),
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
if (final.stop_reason !== 'tool_use') {
|
|
717
|
-
const text = collectText(final.content)
|
|
718
|
-
const value = parseGenerated(text, schema)
|
|
719
|
-
yield {
|
|
720
|
-
type: 'stop',
|
|
721
|
-
stopReason: finishReason ?? 'end_turn',
|
|
722
|
-
iterations,
|
|
723
|
-
usage: aggregated,
|
|
724
|
-
messages: workingMessages,
|
|
725
|
-
value,
|
|
726
|
-
text,
|
|
727
|
-
} as AgentStreamEvent<T>
|
|
728
|
-
return
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const toolUseBlocks = final.content.filter(
|
|
732
|
-
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
|
|
733
|
-
)
|
|
734
|
-
const resultBlocks: ContentBlock[] = []
|
|
735
|
-
for (const block of toolUseBlocks) {
|
|
736
|
-
yield { type: 'tool_use', id: block.id, name: block.name, input: block.input }
|
|
737
|
-
const { content, isError } = await runToolWithRecovery(
|
|
738
|
-
toolMap.get(block.name),
|
|
739
|
-
block.name,
|
|
740
|
-
block.id,
|
|
741
|
-
block.input,
|
|
742
|
-
options,
|
|
743
|
-
)
|
|
744
|
-
resultBlocks.push({
|
|
745
|
-
type: 'tool_result',
|
|
746
|
-
toolUseId: block.id,
|
|
747
|
-
content,
|
|
748
|
-
...(isError ? { isError: true } : {}),
|
|
749
|
-
} satisfies ToolResultBlock)
|
|
750
|
-
yield {
|
|
751
|
-
type: 'tool_result',
|
|
752
|
-
id: block.id,
|
|
753
|
-
name: block.name,
|
|
754
|
-
content,
|
|
755
|
-
isError,
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
workingMessages.push({ role: 'user', content: resultBlocks })
|
|
759
|
-
|
|
760
|
-
iterations++
|
|
761
|
-
if (iterations >= maxIterations) {
|
|
762
|
-
const text = collectText(final.content)
|
|
763
|
-
const value = parseGenerated(text, schema)
|
|
764
|
-
yield {
|
|
765
|
-
type: 'stop',
|
|
766
|
-
stopReason: 'max_iterations',
|
|
767
|
-
iterations,
|
|
768
|
-
usage: aggregated,
|
|
769
|
-
messages: workingMessages,
|
|
770
|
-
value,
|
|
771
|
-
text,
|
|
772
|
-
} as AgentStreamEvent<T>
|
|
773
|
-
return
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
async generate<T>(
|
|
779
|
-
messages: readonly Message[],
|
|
780
|
-
schema: OutputSchema<T>,
|
|
781
|
-
options: ChatOptions = {},
|
|
782
|
-
): Promise<GenerateResult<T>> {
|
|
783
|
-
const params = this.buildParams(messages, options) as Anthropic.MessageCreateParamsNonStreaming
|
|
784
|
-
params.output_config = {
|
|
785
|
-
...(params.output_config ?? {}),
|
|
786
|
-
format: { type: 'json_schema', schema: schema.jsonSchema },
|
|
787
|
-
}
|
|
788
|
-
const response = await this.client.messages.create(params, reqOpts(options))
|
|
789
|
-
const text = collectText(response.content)
|
|
790
|
-
const value = parseGenerated(text, schema)
|
|
791
|
-
return {
|
|
792
|
-
value,
|
|
793
|
-
text,
|
|
794
|
-
model: response.model,
|
|
795
|
-
stopReason: response.stop_reason,
|
|
796
|
-
usage: toUsage(response.usage),
|
|
797
|
-
raw: response,
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// ─── Param translation ──────────────────────────────────────────────────
|
|
802
|
-
|
|
803
|
-
private buildParams(
|
|
804
|
-
messages: readonly Message[],
|
|
805
|
-
options: ChatOptions,
|
|
806
|
-
): Anthropic.MessageCreateParamsNonStreaming {
|
|
807
|
-
const model = options.model ?? this.defaultModel
|
|
808
|
-
const params: Anthropic.MessageCreateParamsNonStreaming = {
|
|
809
|
-
model,
|
|
810
|
-
max_tokens: options.maxTokens ?? this.defaultMaxTokens,
|
|
811
|
-
messages: messages.map(toMessageParam),
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const system = toSystemParam(options.system)
|
|
815
|
-
if (system !== undefined) params.system = system
|
|
816
|
-
|
|
817
|
-
if (options.thinking === 'adaptive') {
|
|
818
|
-
params.thinking = { type: 'adaptive' }
|
|
819
|
-
} else if (options.thinking === 'disabled') {
|
|
820
|
-
params.thinking = { type: 'disabled' }
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (options.effort !== undefined) {
|
|
824
|
-
params.output_config = { effort: options.effort }
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if (options.cache === true) {
|
|
828
|
-
// Top-level auto-cache the last cacheable block. Maps to the
|
|
829
|
-
// SDK's `cache_control` shorthand on the request body.
|
|
830
|
-
;(params as { cache_control?: { type: 'ephemeral' } }).cache_control = EPHEMERAL_CACHE
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// Compaction — emits the beta `edits` entry + flips the
|
|
834
|
-
// `compact-2026-01-12` beta header so the request goes through
|
|
835
|
-
// the SDK's beta surface (same routing as MCP).
|
|
836
|
-
const baseBetas = mergeBetas(this.betas, options.betas)
|
|
837
|
-
const betas = options.compact !== undefined
|
|
838
|
-
? mergeBetas(baseBetas, [COMPACT_BETA])
|
|
839
|
-
: baseBetas
|
|
840
|
-
if (options.compact !== undefined) {
|
|
841
|
-
const edit: Record<string, unknown> = { type: COMPACT_EDIT_TYPE }
|
|
842
|
-
if (options.compact.trigger !== undefined) {
|
|
843
|
-
edit.trigger = { type: 'input_tokens', value: options.compact.trigger }
|
|
844
|
-
}
|
|
845
|
-
if (options.compact.instructions !== undefined) {
|
|
846
|
-
edit.instructions = options.compact.instructions
|
|
847
|
-
}
|
|
848
|
-
if (options.compact.pauseAfterCompaction !== undefined) {
|
|
849
|
-
edit.pause_after_compaction = options.compact.pauseAfterCompaction
|
|
850
|
-
}
|
|
851
|
-
;(params as { edits?: unknown[] }).edits = [edit]
|
|
852
|
-
}
|
|
853
|
-
if (betas.length > 0) {
|
|
854
|
-
;(params as { betas?: readonly string[] }).betas = betas
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
if (options.serverTools && options.serverTools.length > 0) {
|
|
858
|
-
params.tools = anthropicServerTools(options.serverTools)
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
return params
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
private toChatResult(message: Anthropic.Message): ChatResult<Anthropic.Message> {
|
|
865
|
-
const text = message.content
|
|
866
|
-
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
|
|
867
|
-
.map((b) => b.text)
|
|
868
|
-
.join('')
|
|
869
|
-
const result: ChatResult<Anthropic.Message> = {
|
|
870
|
-
text,
|
|
871
|
-
model: message.model,
|
|
872
|
-
stopReason: message.stop_reason,
|
|
873
|
-
usage: toUsage(message.usage),
|
|
874
|
-
raw: message,
|
|
875
|
-
}
|
|
876
|
-
// Surface structured content when the turn carries blocks
|
|
877
|
-
// beyond plain text (compaction today; reasoning blocks in a
|
|
878
|
-
// future slice). Apps that persist conversations push this
|
|
879
|
-
// onto the message history so round-trippable blocks survive
|
|
880
|
-
// subsequent requests.
|
|
881
|
-
const blocks = fromAnthropicContent(message.content)
|
|
882
|
-
if (blocks.some((b) => b.type !== 'text')) {
|
|
883
|
-
result.content = blocks
|
|
884
|
-
}
|
|
885
|
-
return result
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// ─── Shape converters ─────────────────────────────────────────────────────
|
|
890
|
-
|
|
891
|
-
/** Compaction beta — required header + `edits[].type` for `compact-2026-01-12`. */
|
|
892
|
-
const COMPACT_BETA = 'compact-2026-01-12'
|
|
893
|
-
const COMPACT_EDIT_TYPE = 'compact_20260112'
|
|
894
|
-
|
|
895
|
-
/**
|
|
896
|
-
* Whether the request needs to flow through `client.beta.messages.create`
|
|
897
|
-
* instead of the stable surface. Triggered by:
|
|
898
|
-
*
|
|
899
|
-
* - `edits[]` (compaction).
|
|
900
|
-
* - `mcp_servers[]` (server-side MCP).
|
|
901
|
-
*
|
|
902
|
-
* Tests typically stub `client.messages.create`; the beta path uses the
|
|
903
|
-
* stub that lives at `client.beta.messages.create`.
|
|
904
|
-
*/
|
|
905
|
-
function needsBetaRouting(params: Anthropic.MessageCreateParamsNonStreaming): boolean {
|
|
906
|
-
const p = params as { edits?: unknown[]; mcp_servers?: unknown[] }
|
|
907
|
-
return (p.edits !== undefined && p.edits.length > 0)
|
|
908
|
-
|| (p.mcp_servers !== undefined && p.mcp_servers.length > 0)
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
/** Build the request-options bag forwarded to the SDK. Only `signal` for now. */
|
|
912
|
-
function reqOpts(options: { signal?: AbortSignal }): { signal?: AbortSignal } | undefined {
|
|
913
|
-
return options.signal !== undefined ? { signal: options.signal } : undefined
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
/** Throw a DOMException-shaped abort error if the signal has fired. */
|
|
917
|
-
function checkAborted(signal: AbortSignal | undefined): void {
|
|
918
|
-
if (signal?.aborted) {
|
|
919
|
-
throw signal.reason ?? new DOMException('Aborted', 'AbortError')
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function toUsage(u: Anthropic.Usage): ChatUsage {
|
|
924
|
-
return {
|
|
925
|
-
inputTokens: u.input_tokens,
|
|
926
|
-
outputTokens: u.output_tokens,
|
|
927
|
-
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
928
|
-
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function toMessageParam(message: Message): Anthropic.MessageParam {
|
|
933
|
-
if (typeof message.content === 'string') {
|
|
934
|
-
return { role: message.role, content: message.content }
|
|
935
|
-
}
|
|
936
|
-
return {
|
|
937
|
-
role: message.role,
|
|
938
|
-
content: message.content
|
|
939
|
-
// MCP blocks are inbound-only — Anthropic produces them, we
|
|
940
|
-
// surface them on `result.messages` for observability, but we
|
|
941
|
-
// never echo them back to the model. The backend tracks MCP
|
|
942
|
-
// tool state on its side.
|
|
943
|
-
.filter(
|
|
944
|
-
(b): b is Exclude<ContentBlock, MCPToolUseBlock | MCPToolResultBlock> =>
|
|
945
|
-
b.type !== 'mcp_tool_use' && b.type !== 'mcp_tool_result',
|
|
946
|
-
)
|
|
947
|
-
.map((block): Anthropic.ContentBlockParam => {
|
|
948
|
-
if (block.type === 'tool_use') {
|
|
949
|
-
return {
|
|
950
|
-
type: 'tool_use',
|
|
951
|
-
id: block.id,
|
|
952
|
-
name: block.name,
|
|
953
|
-
input: block.input as Record<string, unknown>,
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
if (block.type === 'tool_result') {
|
|
957
|
-
const param: Anthropic.ToolResultBlockParam = {
|
|
958
|
-
type: 'tool_result',
|
|
959
|
-
tool_use_id: block.toolUseId,
|
|
960
|
-
content:
|
|
961
|
-
typeof block.content === 'string'
|
|
962
|
-
? block.content
|
|
963
|
-
: block.content.map(
|
|
964
|
-
(b) => ({ type: 'text', text: b.text }) as Anthropic.TextBlockParam,
|
|
965
|
-
),
|
|
966
|
-
}
|
|
967
|
-
if (block.isError) param.is_error = true
|
|
968
|
-
return param
|
|
969
|
-
}
|
|
970
|
-
if (block.type === 'image') {
|
|
971
|
-
return {
|
|
972
|
-
type: 'image',
|
|
973
|
-
source:
|
|
974
|
-
block.source.type === 'base64'
|
|
975
|
-
? {
|
|
976
|
-
type: 'base64',
|
|
977
|
-
media_type:
|
|
978
|
-
block.source.mediaType as Anthropic.Base64ImageSource['media_type'],
|
|
979
|
-
data: block.source.data,
|
|
980
|
-
}
|
|
981
|
-
: { type: 'url', url: block.source.url },
|
|
982
|
-
} satisfies Anthropic.ImageBlockParam
|
|
983
|
-
}
|
|
984
|
-
if (block.type === 'document') {
|
|
985
|
-
const documentParam: Anthropic.DocumentBlockParam = {
|
|
986
|
-
type: 'document',
|
|
987
|
-
source:
|
|
988
|
-
block.source.type === 'base64'
|
|
989
|
-
? {
|
|
990
|
-
type: 'base64',
|
|
991
|
-
media_type: 'application/pdf',
|
|
992
|
-
data: block.source.data,
|
|
993
|
-
}
|
|
994
|
-
: { type: 'url', url: block.source.url },
|
|
995
|
-
}
|
|
996
|
-
if (block.title !== undefined) documentParam.title = block.title
|
|
997
|
-
return documentParam
|
|
998
|
-
}
|
|
999
|
-
if (block.type === 'audio') {
|
|
1000
|
-
throw new BrainError(
|
|
1001
|
-
"AnthropicProvider: audio blocks are not supported. Anthropic's SDK does not expose an audio block type for chat messages. Route audio workloads to Gemini, or transcribe upstream and pass the text.",
|
|
1002
|
-
{ context: { provider: 'anthropic' } },
|
|
1003
|
-
)
|
|
1004
|
-
}
|
|
1005
|
-
if (block.type === 'compaction') {
|
|
1006
|
-
// Round-trip the compaction block verbatim — the server uses
|
|
1007
|
-
// the opaque `encrypted_content` to stitch prior compactions
|
|
1008
|
-
// together; mutating either field would invalidate the
|
|
1009
|
-
// history. Untyped on the stable SDK surface; cast through
|
|
1010
|
-
// the beta type shape.
|
|
1011
|
-
const param: Record<string, unknown> = { type: 'compaction' }
|
|
1012
|
-
if (block.content !== null) param.content = block.content
|
|
1013
|
-
if (block.encryptedContent !== null) {
|
|
1014
|
-
param.encrypted_content = block.encryptedContent
|
|
1015
|
-
}
|
|
1016
|
-
return param as unknown as Anthropic.ContentBlockParam
|
|
1017
|
-
}
|
|
1018
|
-
const text: Anthropic.TextBlockParam = { type: 'text', text: block.text }
|
|
1019
|
-
if (block.cache) text.cache_control = EPHEMERAL_CACHE
|
|
1020
|
-
return text
|
|
1021
|
-
}),
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function toSystemParam(
|
|
1026
|
-
system: SystemPrompt | undefined,
|
|
1027
|
-
): string | Anthropic.TextBlockParam[] | undefined {
|
|
1028
|
-
if (system === undefined) return undefined
|
|
1029
|
-
if (typeof system === 'string') return system
|
|
1030
|
-
if (Array.isArray(system)) {
|
|
1031
|
-
return system.map((block) => {
|
|
1032
|
-
const param: Anthropic.TextBlockParam = { type: 'text', text: block.text }
|
|
1033
|
-
if (block.cache) param.cache_control = EPHEMERAL_CACHE
|
|
1034
|
-
return param
|
|
1035
|
-
})
|
|
1036
|
-
}
|
|
1037
|
-
const param: Anthropic.TextBlockParam = { type: 'text', text: system.text }
|
|
1038
|
-
if (system.cache) param.cache_control = EPHEMERAL_CACHE
|
|
1039
|
-
return [param]
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Translate framework `ServerTool[]` into Anthropic's typed
|
|
1044
|
-
* server-tool entries. Uses the latest SDK-known versions; the
|
|
1045
|
-
* Anthropic backend is backward-compatible to older clients
|
|
1046
|
-
* pinning earlier dates, but we standardize on current. Web fetch
|
|
1047
|
-
* is Anthropic-only; `url_context` is rejected (Gemini-only).
|
|
1048
|
-
*/
|
|
1049
|
-
function anthropicServerTools(serverTools: readonly ServerTool[]): Anthropic.ToolUnion[] {
|
|
1050
|
-
const out: Anthropic.ToolUnion[] = []
|
|
1051
|
-
for (const t of serverTools) {
|
|
1052
|
-
if (t.type === 'web_search') {
|
|
1053
|
-
const tool: Anthropic.WebSearchTool20260209 = {
|
|
1054
|
-
type: 'web_search_20260209',
|
|
1055
|
-
name: 'web_search',
|
|
1056
|
-
}
|
|
1057
|
-
if (t.maxUses !== undefined) {
|
|
1058
|
-
;(tool as { max_uses?: number }).max_uses = t.maxUses
|
|
1059
|
-
}
|
|
1060
|
-
if (t.allowedDomains !== undefined) {
|
|
1061
|
-
tool.allowed_domains = [...t.allowedDomains]
|
|
1062
|
-
}
|
|
1063
|
-
if (t.blockedDomains !== undefined) {
|
|
1064
|
-
tool.blocked_domains = [...t.blockedDomains]
|
|
1065
|
-
}
|
|
1066
|
-
out.push(tool)
|
|
1067
|
-
} else if (t.type === 'code_execution') {
|
|
1068
|
-
out.push({
|
|
1069
|
-
type: 'code_execution_20260120',
|
|
1070
|
-
name: 'code_execution',
|
|
1071
|
-
} satisfies Anthropic.CodeExecutionTool20260120)
|
|
1072
|
-
} else if (t.type === 'web_fetch') {
|
|
1073
|
-
const tool: Anthropic.WebFetchTool20260309 = {
|
|
1074
|
-
type: 'web_fetch_20260309',
|
|
1075
|
-
name: 'web_fetch',
|
|
1076
|
-
}
|
|
1077
|
-
if (t.maxUses !== undefined) {
|
|
1078
|
-
;(tool as { max_uses?: number }).max_uses = t.maxUses
|
|
1079
|
-
}
|
|
1080
|
-
if (t.allowedDomains !== undefined) {
|
|
1081
|
-
tool.allowed_domains = [...t.allowedDomains]
|
|
1082
|
-
}
|
|
1083
|
-
if (t.blockedDomains !== undefined) {
|
|
1084
|
-
tool.blocked_domains = [...t.blockedDomains]
|
|
1085
|
-
}
|
|
1086
|
-
out.push(tool)
|
|
1087
|
-
} else if (t.type === 'url_context') {
|
|
1088
|
-
throw new BrainError(
|
|
1089
|
-
'AnthropicProvider: server tool `url_context` is Gemini-only. Use `web_fetch` for Anthropic or route the call to Gemini.',
|
|
1090
|
-
{ context: { provider: 'anthropic' } },
|
|
1091
|
-
)
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
return out
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
function mergeBetas(
|
|
1098
|
-
providerBetas: readonly string[],
|
|
1099
|
-
callBetas: readonly string[] | undefined,
|
|
1100
|
-
): readonly string[] {
|
|
1101
|
-
if (!callBetas || callBetas.length === 0) return providerBetas
|
|
1102
|
-
const seen = new Set<string>()
|
|
1103
|
-
const out: string[] = []
|
|
1104
|
-
for (const b of providerBetas) {
|
|
1105
|
-
if (seen.has(b)) continue
|
|
1106
|
-
seen.add(b)
|
|
1107
|
-
out.push(b)
|
|
1108
|
-
}
|
|
1109
|
-
for (const b of callBetas) {
|
|
1110
|
-
if (seen.has(b)) continue
|
|
1111
|
-
seen.add(b)
|
|
1112
|
-
out.push(b)
|
|
1113
|
-
}
|
|
1114
|
-
return out
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
function addUsage(acc: ChatUsage, u: Anthropic.Usage): void {
|
|
1118
|
-
acc.inputTokens += u.input_tokens
|
|
1119
|
-
acc.outputTokens += u.output_tokens
|
|
1120
|
-
acc.cacheReadTokens += u.cache_read_input_tokens ?? 0
|
|
1121
|
-
acc.cacheCreationTokens += u.cache_creation_input_tokens ?? 0
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
function collectText(content: Anthropic.ContentBlock[]): string {
|
|
1125
|
-
return content
|
|
1126
|
-
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
|
|
1127
|
-
.map((b) => b.text)
|
|
1128
|
-
.join('')
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* Translate the SDK's response content blocks back into framework
|
|
1133
|
-
* `ContentBlock`s for storage in `workingMessages`. We preserve
|
|
1134
|
-
* `text` and `tool_use` blocks verbatim; other server-side block
|
|
1135
|
-
* types (thinking, server tool blocks) are dropped — V1 doesn't
|
|
1136
|
-
* surface them, and re-sending them as part of the assistant turn
|
|
1137
|
-
* could confuse the model.
|
|
1138
|
-
*/
|
|
1139
|
-
function fromAnthropicContent(
|
|
1140
|
-
content: ReadonlyArray<Anthropic.ContentBlock | { type: string; [k: string]: unknown }>,
|
|
1141
|
-
): ContentBlock[] {
|
|
1142
|
-
const out: ContentBlock[] = []
|
|
1143
|
-
for (const block of content) {
|
|
1144
|
-
if (block.type === 'text') {
|
|
1145
|
-
out.push({ type: 'text', text: (block as { text: string }).text } satisfies TextBlock)
|
|
1146
|
-
} else if (block.type === 'tool_use') {
|
|
1147
|
-
const u = block as { id: string; name: string; input: unknown }
|
|
1148
|
-
out.push({
|
|
1149
|
-
type: 'tool_use',
|
|
1150
|
-
id: u.id,
|
|
1151
|
-
name: u.name,
|
|
1152
|
-
input: u.input,
|
|
1153
|
-
} satisfies ToolUseBlock)
|
|
1154
|
-
} else if (block.type === 'mcp_tool_use') {
|
|
1155
|
-
const m = block as unknown as {
|
|
1156
|
-
id: string
|
|
1157
|
-
server_name: string
|
|
1158
|
-
name: string
|
|
1159
|
-
input: unknown
|
|
1160
|
-
}
|
|
1161
|
-
out.push({
|
|
1162
|
-
type: 'mcp_tool_use',
|
|
1163
|
-
id: m.id,
|
|
1164
|
-
serverName: m.server_name,
|
|
1165
|
-
name: m.name,
|
|
1166
|
-
input: m.input,
|
|
1167
|
-
} satisfies MCPToolUseBlock)
|
|
1168
|
-
} else if (block.type === 'mcp_tool_result') {
|
|
1169
|
-
const r = block as unknown as {
|
|
1170
|
-
tool_use_id: string
|
|
1171
|
-
content: string | Array<{ type: 'text'; text: string }>
|
|
1172
|
-
is_error?: boolean
|
|
1173
|
-
}
|
|
1174
|
-
const result: MCPToolResultBlock = {
|
|
1175
|
-
type: 'mcp_tool_result',
|
|
1176
|
-
toolUseId: r.tool_use_id,
|
|
1177
|
-
content:
|
|
1178
|
-
typeof r.content === 'string'
|
|
1179
|
-
? r.content
|
|
1180
|
-
: r.content.map((c) => ({ type: 'text', text: c.text }) satisfies TextBlock),
|
|
1181
|
-
}
|
|
1182
|
-
if (r.is_error) result.isError = true
|
|
1183
|
-
out.push(result)
|
|
1184
|
-
} else if (block.type === 'compaction') {
|
|
1185
|
-
const c = block as { content?: string | null; encrypted_content?: string | null }
|
|
1186
|
-
out.push({
|
|
1187
|
-
type: 'compaction',
|
|
1188
|
-
content: c.content ?? null,
|
|
1189
|
-
encryptedContent: c.encrypted_content ?? null,
|
|
1190
|
-
} satisfies CompactionBlock)
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
return out
|
|
1194
|
-
}
|