@x-otto/plugin-otto-wire-protocols 0.1.0-alpha.14

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.
@@ -0,0 +1,967 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import Anthropic from '@anthropic-ai/sdk'
3
+ import { ProviderError } from '@x-otto/shared'
4
+ import { normalizeImageData, toAnthropicImageMime } from '@x-otto/interchange'
5
+ import {
6
+ buildRequestHeaders,
7
+ sanitizeToolRoundtrips,
8
+ assertSystemNotificationPlacement,
9
+ degradedSystemNotificationText,
10
+ mergeConsecutiveSystemNotifications,
11
+ assembleAssistantMessage,
12
+ blocksContent,
13
+ initStreamUsage,
14
+ toolCallFromParsed,
15
+ recoverLiteralInvokeToolCalls,
16
+ registeredToolNames,
17
+ clampThinkingLevel,
18
+ toJsonSchema,
19
+ } from '@x-otto/provider'
20
+ import { extractProviderUsageFromHeaders } from './anthropic-usage'
21
+ import type {
22
+ AssistantMessage,
23
+ Message,
24
+ Model,
25
+ ResolvedAuth,
26
+ StopReason,
27
+ StreamContext,
28
+ StreamEvent,
29
+ StreamOptions,
30
+ SystemPromptSegment,
31
+ CacheRetention,
32
+ ProviderStream,
33
+ StreamBlock,
34
+ StreamUsage,
35
+ TokenDerivedHeader,
36
+ } from '@x-otto/provider'
37
+
38
+ /**
39
+ * RFC-185 D3:日志改走宿主注入端口(`AnthropicProviderOptions.logger`),插件源码禁止
40
+ * `createLogger` 自建——esbuild bundle 会复制出宿主管不着的第二份 pino 单例,trace 级
41
+ * 直写 stderr 破坏 TUI(findings-backlog「TUI 残影回归」根因环 1/2)。缺省 = no-op
42
+ * (红线:绝不 fallback 到 createLogger)。
43
+ */
44
+ import type { WireProtocolLogger } from '@x-otto/provider'
45
+ import { noopWireProtocolLogger, resolveProviderBaseUrl } from '@x-otto/provider'
46
+
47
+ export type AnthropicCredentialResolver = () => Promise<string | undefined>
48
+ export type AnthropicAuthResolver = () => Promise<ResolvedAuth | undefined>
49
+ export type AnthropicBaseUrlResolver = () => Promise<string | undefined>
50
+
51
+ export function toBudgetTokens(level: string, maxOutputTokens: number): number {
52
+ // RFC-235:Anthropic 是 token 预算型(非 effort 关键字)。otto 档位 → maxOutput 百分比预算。
53
+ // 词表对齐 Claude Code(去 minimal 加 max):low 25% / medium 50% / high 75% / xhigh 90% / max 95%。
54
+ let budget: number
55
+ switch (level) {
56
+ case 'low':
57
+ budget = Math.floor(maxOutputTokens * 0.25)
58
+ break
59
+ case 'high':
60
+ budget = Math.floor(maxOutputTokens * 0.75)
61
+ break
62
+ case 'xhigh':
63
+ budget = Math.floor(maxOutputTokens * 0.9)
64
+ break
65
+ case 'max':
66
+ budget = Math.floor(maxOutputTokens * 0.95)
67
+ break
68
+ default:
69
+ budget = Math.floor(maxOutputTokens * 0.5)
70
+ }
71
+ return Math.min(Math.max(budget, 1024), maxOutputTokens - 1)
72
+ }
73
+
74
+ /** thinking 配置的两种输出形态(由 model.thinkingMode 声明决定)。 */
75
+ export type ThinkingConfig =
76
+ | { type: 'enabled'; budget_tokens: number }
77
+ | { type: 'adaptive' }
78
+
79
+ export function buildThinkingConfig(
80
+ level: string | undefined,
81
+ reasoning: boolean,
82
+ maxOutputTokens: number,
83
+ thinkingMode?: 'enabled' | 'adaptive',
84
+ ): ThinkingConfig | undefined {
85
+ if (!reasoning || !level) {
86
+ return undefined
87
+ }
88
+ if (thinkingMode === 'adaptive') {
89
+ return { type: 'adaptive' }
90
+ }
91
+ if (maxOutputTokens <= 1024) {
92
+ return undefined
93
+ }
94
+ return { type: 'enabled', budget_tokens: toBudgetTokens(level, maxOutputTokens) }
95
+ }
96
+
97
+ /** adaptive 模式的 effort 值映射(output_config.effort)。 */
98
+ function toAdaptiveEffort(level: string): 'low' | 'medium' | 'high' {
99
+ switch (level) {
100
+ case 'low': return 'low'
101
+ case 'medium': return 'medium'
102
+ case 'high': return 'high'
103
+ case 'xhigh': return 'high'
104
+ case 'max': return 'high'
105
+ default: return 'medium'
106
+ }
107
+ }
108
+
109
+ const CACHE_CONTROL = { type: 'ephemeral' as const }
110
+ const CACHE_CONTROL_1H = { type: 'ephemeral' as const, ttl: '1h' as const }
111
+
112
+ /**
113
+ * cacheRetention 选档 → ephemeral 断点(含 TTL):
114
+ * - 'none' → null(不放任何断点,调用方据此跳过);
115
+ * - 'short' / 缺省 → 5 分钟 TTL;
116
+ * - 'long' → 1 小时 TTL(SDK 0.88 原生 ttl,GA、无需 beta 头)。
117
+ * 返回 null(而非 undefined)以便与「参数省略走 5m 默认」区分。
118
+ */
119
+ export function cacheControlFor(
120
+ retention?: CacheRetention,
121
+ ): Anthropic.CacheControlEphemeral | null {
122
+ if (retention === 'none') {
123
+ return null
124
+ }
125
+ return retention === 'long' ? CACHE_CONTROL_1H : CACHE_CONTROL
126
+ }
127
+
128
+ /**
129
+ * 断点③——会话前缀尾:最后一条消息的最后一个 content block 挂 cache_control。
130
+ * 仅对安全的块类型(text/tool_result/image)标记;string content 先升格为 block 数组。
131
+ * cacheControl=null(retention 'none')→ 不放断点。参数省略默认 5m(保留旧直调行为)。
132
+ */
133
+ export function withMessageCacheBreakpoint(
134
+ messages: Anthropic.MessageParam[],
135
+ cacheControl: Anthropic.CacheControlEphemeral | null = CACHE_CONTROL,
136
+ ): Anthropic.MessageParam[] {
137
+ const last = messages[messages.length - 1]
138
+ if (!last || !cacheControl) {
139
+ return messages
140
+ }
141
+
142
+ if (typeof last.content === 'string') {
143
+ if (last.content.length === 0) {
144
+ return messages
145
+ }
146
+ last.content = [{ type: 'text', text: last.content, cache_control: cacheControl }]
147
+ return messages
148
+ }
149
+
150
+ const lastBlock = last.content[last.content.length - 1]
151
+ if (
152
+ lastBlock &&
153
+ (lastBlock.type === 'text' || lastBlock.type === 'tool_result' || lastBlock.type === 'image') &&
154
+ !(lastBlock.type === 'text' && lastBlock.text.length === 0)
155
+ ) {
156
+ lastBlock.cache_control = cacheControl
157
+ }
158
+ return messages
159
+ }
160
+
161
+ /**
162
+ * 断点④——system 稳定前缀尾(动态):把 system 转为 text block 数组,
163
+ * 在「最后一个 stable 段」之后挂 cache_control。volatile 段(如 phase context)
164
+ * 照发但落在断点之后,不进缓存前缀——因此其逐轮变化不会击穿 system / messages 缓存。
165
+ *
166
+ * - segments 缺省(仅 string):整串视作单一 stable 段,断点落在尾部 == 旧行为。
167
+ * - 全 volatile(无 stable 段)/ cacheControl=null(retention 'none'):不挂断点。
168
+ * - 空内容:返回 undefined,调用方省略 system 字段。
169
+ * 参数省略默认 5m(保留旧直调行为)。
170
+ */
171
+ export function buildSystemBlocks(
172
+ systemPrompt: string,
173
+ segments?: SystemPromptSegment[],
174
+ cacheControl: Anthropic.CacheControlEphemeral | null = CACHE_CONTROL,
175
+ ): Anthropic.TextBlockParam[] | undefined {
176
+ const source: SystemPromptSegment[] =
177
+ segments && segments.length > 0 ? segments : systemPrompt ? [{ text: systemPrompt }] : []
178
+
179
+ const blocks = source.filter((s) => s.text.length > 0)
180
+ if (blocks.length === 0) {
181
+ return undefined
182
+ }
183
+
184
+ let breakpoint = -1
185
+ if (cacheControl) {
186
+ for (const [i, seg] of blocks.entries()) {
187
+ if ((seg.cache ?? 'stable') === 'stable') {
188
+ breakpoint = i
189
+ }
190
+ }
191
+ }
192
+
193
+ return blocks.map((seg, i) => ({
194
+ type: 'text' as const,
195
+ text: seg.text,
196
+ ...(i === breakpoint && cacheControl ? { cache_control: cacheControl } : {}),
197
+ }))
198
+ }
199
+
200
+ /**
201
+ * RFC-144 M2:mid-conversation system message 模型能力检测——据 Anthropic 官方文档
202
+ * (docs.anthropic.com/en/docs/build-with-claude/mid-conversation-system-messages,
203
+ * 2026-07 核实)"This feature is available on Claude Opus 4.8 only",且不支持 Amazon
204
+ * Bedrock / Google Cloud(本函数只判模型本身,不判部署平台——otto 当前只走 Claude API
205
+ * 直连,平台判定不在范围内)。M0 已用真实 API 调用验证 `anthropic/claude-opus-4-8`
206
+ * 支持该特性(见 RFC-144 milestone M0 执行记录)。
207
+ *
208
+ * 不支持的模型走降级路径(决策 1 已述:`role:'user'` + 显式文本前缀),由调用方
209
+ * (`buildAnthropicMessages` 的 system_notification 分支)据此结果切换序列化形态。
210
+ */
211
+ export function supportsMidConversationSystemMessage(model: Model): boolean {
212
+ return model.id === 'anthropic/claude-opus-4-8'
213
+ }
214
+
215
+ /**
216
+ * Anthropic 要求 `tool_use.id` / `tool_result.tool_use_id` 匹配 `^[a-zA-Z0-9_-]+$`
217
+ * (见 400 报错 `String should match pattern '^[a-zA-Z0-9_-]+$'`)。会话历史里的
218
+ * tool_call id 可能由其它 provider/网关生成,含点号、冒号、斜杠或非 ASCII 字符(例如跨
219
+ * provider 续聊、代理层重写的 id),直接透传就会被 Anthropic 拒绝整轮请求。
220
+ *
221
+ * 这里在 wire 边界做**确定性**清洗:合法 id 原样返回;非法 id 用其 sha256 摘要派生一个
222
+ * 稳定的合规 id。关键性质是纯函数——同一输入永远映射到同一输出,因此 assistant 的
223
+ * `tool_use.id` 与其配对的 `tool_result.tool_use_id` 各自独立清洗后仍然相等,roundtrip
224
+ * 配对不被破坏(不需要跨消息共享映射表)。
225
+ */
226
+ const VALID_TOOL_USE_ID = /^[a-zA-Z0-9_-]+$/
227
+
228
+ export function sanitizeToolUseId(id: string): string {
229
+ if (id.length > 0 && VALID_TOOL_USE_ID.test(id)) {
230
+ return id
231
+ }
232
+ return `toolu_x_${createHash('sha256').update(id).digest('hex').slice(0, 32)}`
233
+ }
234
+
235
+ /**
236
+ * `Message[]` → Anthropic `MessageParam[]`。
237
+ *
238
+ * `model` 可选(缺省走降级路径,见 `supportsMidConversationSystemMessage`)——保持向后
239
+ * 兼容:既有调用方(如未来新增的测试/工具)不传 model 时不会因新增必填参数而破坏编译。
240
+ *
241
+ * SDK 类型滞后说明(RFC-144 M2):`@anthropic-ai/sdk@0.88.0` 的 `MessageParam.role` 类型声明
242
+ * 仅 `'user' | 'assistant'`,但 Anthropic 官方文档明确 API 本身接受 `{role:'system', ...}`
243
+ * 出现在 messages 数组中(M0 已用真实 API 调用验证)——这是 SDK 类型声明滞后于实际 API
244
+ * 能力的已知情况,用 `as Anthropic.MessageParam` 类型断言绕过,不代表真实类型契约。
245
+ */
246
+ export function buildAnthropicMessages(messages: Message[], model?: Model): Anthropic.MessageParam[] {
247
+ const result: Anthropic.MessageParam[] = []
248
+ const nativeSystemMessageSupported = model ? supportsMidConversationSystemMessage(model) : false
249
+
250
+ // 2026-07 修正:Anthropic 官方约束"Consecutive system messages are not allowed"——原生
251
+ // 路径必须先把相邻的 system_notification 合并为一条,否则多个 lifecycleAsync 工具同轮
252
+ // 完成时会产出连续 system 消息,被 API 拒绝(400)。降级路径(role:'user')不受此限制,
253
+ // 不合并(各自独立 user 消息,Anthropic 对连续 user 无限制)。
254
+ const normalized = nativeSystemMessageSupported
255
+ ? mergeConsecutiveSystemNotifications(messages)
256
+ : messages
257
+
258
+ // RFC-144 M2:放置规则断言——只有原生走 mid-conversation system message 的路径才受
259
+ // Anthropic 官方放置约束限制(降级路径 role:'user' 无此约束,断言在此处跑纯粹是
260
+ // 因为该模型能力已知支持,检查值得做;不支持的模型走断言意义不大但跑一次无害)。
261
+ // 对合并后的序列断言——合并已消除"连续 system_notification"这一类曾经的假阳性源。
262
+ assertSystemNotificationPlacement(normalized)
263
+
264
+ for (const msg of sanitizeToolRoundtrips(normalized)) {
265
+ if (msg.role === 'user') {
266
+ if (typeof msg.content === 'string') {
267
+ result.push({ role: 'user', content: msg.content })
268
+ } else {
269
+ const content: Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam> = []
270
+
271
+ for (const part of msg.content) {
272
+ if (part.type === 'text') {
273
+ if (part.text.length > 0) {
274
+ content.push({ type: 'text', text: part.text })
275
+ }
276
+ } else if (part.type === 'image') {
277
+ const { base64, mediaType, valid } = normalizeImageData(part.source, part.mime)
278
+ if (valid) {
279
+ content.push({
280
+ type: 'image',
281
+ source: {
282
+ type: 'base64',
283
+ media_type: toAnthropicImageMime(mediaType, base64),
284
+ data: base64,
285
+ },
286
+ })
287
+ } else {
288
+ content.push({ type: 'text', text: '[image omitted: corrupted or empty data]' })
289
+ }
290
+ }
291
+ }
292
+
293
+ if (content.length > 0) {
294
+ result.push({ role: 'user', content })
295
+ }
296
+ }
297
+ } else if (msg.role === 'assistant') {
298
+ const content: Anthropic.ContentBlockParam[] = []
299
+
300
+ for (const part of msg.content) {
301
+ if (part.type === 'thinking') {
302
+ // RFC-446 观察项真机复现修复:`signature` 是 Anthropic 自己签发、绑定到具体会话
303
+ // 前缀的防篡改令牌(仅由本文件的 signature_delta 累积产生,见下方 line ~500)。
304
+ // 跨模型切换后历史里混入非 Anthropic 来源(如 DeepSeek/GLM)生成的 thinking block
305
+ // ——其 `signature` 天然为 undefined,此前用 `?? ''` 填空串发给 Anthropic,被新版
306
+ // 签名校验拒绝:400 "Invalid `signature` in `thinking` block. The block is bound to
307
+ // a different conversation."(官方 troubleshooting 文档 fix:无有效签名的 block 应
308
+ // 整体剥离,而非带假签名重放)。故此处丢弃整个 block,只保留同轮的 text/tool_use。
309
+ if (part.signature) {
310
+ content.push({
311
+ type: 'thinking',
312
+ thinking: part.text,
313
+ signature: part.signature,
314
+ } as Anthropic.ThinkingBlockParam)
315
+ }
316
+ } else if (part.type === 'text') {
317
+ if (part.text.trim()) {
318
+ content.push({ type: 'text', text: part.text })
319
+ }
320
+ } else if (part.type === 'tool_call') {
321
+ content.push({
322
+ type: 'tool_use',
323
+ id: sanitizeToolUseId(part.id),
324
+ name: part.name,
325
+ input: part.arguments,
326
+ })
327
+ }
328
+ }
329
+
330
+ if (content.length > 0) {
331
+ result.push({ role: 'assistant', content })
332
+ }
333
+ } else if (msg.role === 'tool_result') {
334
+ const toolContent: Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam> = []
335
+
336
+ for (const c of msg.content) {
337
+ if (c.type === 'text') {
338
+ if (c.text.length > 0) {
339
+ toolContent.push({ type: 'text', text: c.text })
340
+ }
341
+ } else if (c.type === 'image') {
342
+ const { base64, mediaType, valid } = normalizeImageData(c.source, c.mime)
343
+ if (valid) {
344
+ toolContent.push({
345
+ type: 'image',
346
+ source: {
347
+ type: 'base64',
348
+ media_type: toAnthropicImageMime(mediaType, base64),
349
+ data: base64,
350
+ },
351
+ })
352
+ } else {
353
+ toolContent.push({ type: 'text', text: '[image omitted: corrupted or empty data]' })
354
+ }
355
+ }
356
+ }
357
+
358
+ result.push({
359
+ role: 'user',
360
+ content: [
361
+ {
362
+ type: 'tool_result',
363
+ tool_use_id: sanitizeToolUseId(msg.toolCallId),
364
+ content: toolContent,
365
+ ...(msg.isError ? { is_error: true } : {}),
366
+ },
367
+ ],
368
+ })
369
+ } else if (msg.role === 'system_notification') {
370
+ // RFC-144 M2:lifecycleAsync 工具完成通知——支持原生特性的模型走 mid-conversation
371
+ // system message(结构化保留"运营方权威"语义);不支持的模型降级为 role:'user' +
372
+ // 显式前缀(放置约束见函数头注释;追加时机天然满足"紧跟 tool_result 之后",因为
373
+ // work-loop.ts 的落库时机固定在下一次 runTurn 之前的静默期,见 RFC-144 硬约束 5)。
374
+ if (nativeSystemMessageSupported) {
375
+ result.push({ role: 'system', content: msg.content } as unknown as Anthropic.MessageParam)
376
+ } else {
377
+ result.push({ role: 'user', content: degradedSystemNotificationText(msg.content) })
378
+ }
379
+ }
380
+ }
381
+
382
+ // RFC-144 M2 补充修复(2026-07):Anthropic mid-conversation system message 放置约束
383
+ // 的第二半(后继约束)——system 消息必须是数组末尾或紧接一个 assistant turn。
384
+ // assertSystemNotificationPlacement 在内部层面已检测到此违规并 warn,但 fail-soft 不阻断——
385
+ // 原生支持 mid-conversation system message 的模型仍可能在 wire 格式中产生
386
+ // [...assistant, system, user, ...] 这类违规序列(当 lifecycleAsync 工具在上一轮模型
387
+ // 响应后才完成、紧接着用户发送了新消息时),被 Anthropic 400 拒绝。
388
+ // 修复:对 wire 结果做一次后扫描,将放置违规的 system 消息降级为 role:'user' + 前缀
389
+ // (与此前非原生模型的降级路径同一序列化形态,不引入新格式)。
390
+ if (nativeSystemMessageSupported) {
391
+ for (let i = 0; i < result.length; i++) {
392
+ // SDK 的 MessageParam.role 类型声明仅 'user'|'assistant',但实际 wire 中可能有
393
+ // 'system'(Anthropic SDK 类型滞后于 API 能力,同 buildAnthropicMessages 内
394
+ // push 时的 as unknown as MessageParam 注释)。
395
+ if ((result[i] as unknown as { role: string }).role === 'system') {
396
+ const next = result[i + 1]
397
+ if (next && next.role !== 'assistant') {
398
+ const text = typeof result[i]!.content === 'string' ? (result[i]!.content as string) : ''
399
+ result[i] = { role: 'user', content: degradedSystemNotificationText(text) }
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ // 2026-09 修正(真机复现:Claude Fable 5.1 / Opus·Sonnet 4.6+ 系列):这批模型撤回了
406
+ // "assistant message prefill"能力——请求 `messages` 数组若以 role:'assistant' 收尾,
407
+ // 一律 400 "This model does not support assistant message prefill. The conversation
408
+ // must end with a user message."(4.5 及更早模型仍接受,无此限制)。
409
+ //
410
+ // otto 主对话链路(turn-loop/tool-turn-runner/pipeline 的 seal 系列)已保证正常
411
+ // 场景下请求以 user/tool_result 收尾,但压缩前缀重放、多 agent handoff、外部直接
412
+ // 复用 buildAnthropicMessages 等旁路仍可能喂入"以 assistant 收尾"的历史。序列化是
413
+ // provider-agnostic 请求构造的最后一道关口,在此兜底:末尾若是 assistant,追加一条
414
+ // 最小续写触发 user 消息(与 work-loop-turn-outcome.ts 的 <engine_truncation> 同款
415
+ // 模式),而非让请求直接被 API 拒绝——宁可多花一次往返续写,也不能让整轮对话硬崩。
416
+ const last = result[result.length - 1]
417
+ if (last && (last as unknown as { role: string }).role === 'assistant') {
418
+ result.push({
419
+ role: 'user',
420
+ content: 'Continue.',
421
+ })
422
+ }
423
+
424
+ return result
425
+ }
426
+
427
+ function mapStopReason(reason: string | null | undefined): StopReason {
428
+ switch (reason) {
429
+ case 'end_turn':
430
+ return 'end_turn'
431
+ case 'max_tokens':
432
+ return 'max_tokens'
433
+ case 'tool_use':
434
+ return 'tool_use'
435
+ case 'stop_sequence':
436
+ return 'stop_sequence'
437
+ case 'refusal':
438
+ return 'refusal'
439
+ default:
440
+ return 'end_turn'
441
+ }
442
+ }
443
+
444
+ interface AnthropicStreamState extends StreamUsage {
445
+ model: string
446
+ blocks: Map<number, StreamBlock>
447
+ stopReason: StopReason
448
+ }
449
+
450
+ function initStreamState(modelId: string): AnthropicStreamState {
451
+ return {
452
+ ...initStreamUsage(),
453
+ model: modelId,
454
+ blocks: new Map(),
455
+ stopReason: 'end_turn',
456
+ }
457
+ }
458
+
459
+ function buildAssistantMessage(state: AnthropicStreamState, final = false): AssistantMessage {
460
+ return assembleAssistantMessage(
461
+ { api: 'anthropic-messages', provider: 'anthropic', model: state.model },
462
+ blocksContent(state.blocks, final),
463
+ state,
464
+ state.stopReason,
465
+ )
466
+ }
467
+
468
+ function dispatchStreamEvent(
469
+ event: Anthropic.MessageStreamEvent,
470
+ state: AnthropicStreamState,
471
+ ): StreamEvent[] {
472
+ switch (event.type) {
473
+ case 'message_start': {
474
+ // usage 缺省容错:兼容网关(OpenAI 上游转译成 anthropic-messages 下行)可能整段省略
475
+ // usage;这是纯统计字段,缺了按 0 起算即可,不能让整轮对话因取字段抛 TypeError。
476
+ const u = event.message.usage ?? { input_tokens: 0, output_tokens: 0 }
477
+ state.inputTokens = u.input_tokens ?? 0
478
+ state.outputTokens = u.output_tokens ?? 0
479
+ if (u.cache_read_input_tokens != null) {
480
+ state.cacheReadTokens = u.cache_read_input_tokens
481
+ }
482
+ if (u.cache_creation_input_tokens != null) {
483
+ state.cacheWriteTokens = u.cache_creation_input_tokens
484
+ }
485
+ return []
486
+ }
487
+
488
+ case 'content_block_start': {
489
+ const index = event.index
490
+ const block = event.content_block
491
+
492
+ if (block.type === 'text') {
493
+ state.blocks.set(index, { kind: 'text', text: '' })
494
+ return [{ type: 'text_start', index, partial: buildAssistantMessage(state) }]
495
+ } else if (block.type === 'thinking') {
496
+ state.blocks.set(index, { kind: 'thinking', text: '', signature: '' })
497
+ return [{ type: 'thinking_start', index, partial: buildAssistantMessage(state) }]
498
+ } else if (block.type === 'tool_use') {
499
+ state.blocks.set(index, {
500
+ kind: 'tool_call',
501
+ id: block.id,
502
+ name: block.name,
503
+ argumentsJson: '',
504
+ })
505
+ return [{ type: 'tool_call_start', index, partial: buildAssistantMessage(state) }]
506
+ }
507
+ return []
508
+ }
509
+
510
+ case 'content_block_delta': {
511
+ const index = event.index
512
+ const delta = event.delta
513
+ const block = state.blocks.get(index)
514
+
515
+ if (delta.type === 'text_delta' && block?.kind === 'text') {
516
+ block.text += delta.text
517
+ return [{
518
+ type: 'text_delta',
519
+ index,
520
+ delta: delta.text,
521
+ partial: buildAssistantMessage(state),
522
+ }]
523
+ } else if (delta.type === 'thinking_delta' && block?.kind === 'thinking') {
524
+ block.text += delta.thinking
525
+ return [{
526
+ type: 'thinking_delta',
527
+ index,
528
+ delta: delta.thinking,
529
+ partial: buildAssistantMessage(state),
530
+ }]
531
+ } else if (delta.type === 'signature_delta' && block?.kind === 'thinking') {
532
+ block.signature = (block.signature ?? '') + delta.signature
533
+ return []
534
+ } else if (delta.type === 'input_json_delta' && block?.kind === 'tool_call') {
535
+ block.argumentsJson += delta.partial_json
536
+ return [{
537
+ type: 'tool_call_delta',
538
+ index,
539
+ delta: delta.partial_json,
540
+ partial: buildAssistantMessage(state),
541
+ }]
542
+ }
543
+ return []
544
+ }
545
+
546
+ case 'content_block_stop': {
547
+ const index = event.index
548
+ const block = state.blocks.get(index)
549
+
550
+ if (block?.kind === 'text') {
551
+ return [{
552
+ type: 'text_end',
553
+ index,
554
+ content: block.text,
555
+ partial: buildAssistantMessage(state),
556
+ }]
557
+ } else if (block?.kind === 'thinking') {
558
+ return [{
559
+ type: 'thinking_end',
560
+ index,
561
+ content: block.text,
562
+ partial: buildAssistantMessage(state),
563
+ }]
564
+ } else if (block?.kind === 'tool_call') {
565
+ return [{
566
+ type: 'tool_call_end',
567
+ index,
568
+ toolCall: toolCallFromParsed(block.id, block.name, block.argumentsJson),
569
+ partial: buildAssistantMessage(state),
570
+ }]
571
+ }
572
+ return []
573
+ }
574
+
575
+ case 'message_delta': {
576
+ state.outputTokens = event.usage.output_tokens
577
+ // RFC-019 修复:message_delta 的 usage 是累积快照,input/cache 字段在服务端工具
578
+ // (web_search/code_execution 等)产出后追加到 prompt 时会重新下发且与 message_start
579
+ // 不同(官方 streaming 文档 web_search 示例:input_tokens 2679→10682)。
580
+ // 官方 SDK 自身的 MessageStream 累积器(accumulateMessage)对这三个字段做同款的
581
+ // "非 null 才覆盖"合并——此前仅取 message_start 快照会在服务端工具场景下把
582
+ // inputTokens/cacheReadTokens/cacheWriteTokens 钉死在过时值,连带 R1 命中率算错。
583
+ if (event.usage.input_tokens != null) {
584
+ state.inputTokens = event.usage.input_tokens
585
+ }
586
+ if (event.usage.cache_creation_input_tokens != null) {
587
+ state.cacheWriteTokens = event.usage.cache_creation_input_tokens
588
+ }
589
+ if (event.usage.cache_read_input_tokens != null) {
590
+ state.cacheReadTokens = event.usage.cache_read_input_tokens
591
+ }
592
+ state.stopReason = mapStopReason(event.delta.stop_reason)
593
+ return []
594
+ }
595
+
596
+ case 'message_stop':
597
+ return []
598
+ }
599
+ }
600
+
601
+ /**
602
+ * 请求定制点入参(RFC-140 D-copilot-spoof-split:厂商专属行为下沉插件)。
603
+ * `auth` 供定制器判断鉴权模式(如"仅 OAuth 模式才定制");`sessionId` 是本
604
+ * provider 实例进程内稳定的会话标识(同一实例每次请求复用同一个值)。
605
+ */
606
+ export interface AnthropicRequestCustomizeInput {
607
+ auth: ResolvedAuth
608
+ sessionId: string
609
+ }
610
+
611
+ /**
612
+ * system 文本块的最小结构类型(有意不用 `Anthropic.TextBlockParam` SDK 具体类型):
613
+ * 定制器的宿主实现方(如 `packages/coding/src/plugin/`)不该被迫依赖 `@anthropic-ai/sdk`
614
+ * 只为了满足一个类型签名——两者结构兼容(本类型字段是 `Anthropic.TextBlockParam` 的子集),
615
+ * 调用方传入/接收 SDK 真实类型的值时无需转换,TypeScript 结构化类型系统天然兼容。
616
+ */
617
+ export interface AnthropicSystemTextBlock {
618
+ type: 'text'
619
+ text: string
620
+ cache_control?: unknown
621
+ }
622
+
623
+ /** 定制点可覆盖的三个请求维度——与真实 SDK 调用形状对齐,均可选、缺省不改变默认行为。 */
624
+ export interface AnthropicRequestCustomization {
625
+ /** 附加/覆盖的 SDK 客户端默认请求头(如身份伪装头)。 */
626
+ defaultHeaders?: Record<string, string>
627
+ /** system 数组变换(如插入身份前缀块);接收当前 blocks,返回替换后的 blocks。 */
628
+ transformSystemBlocks?: (
629
+ blocks: AnthropicSystemTextBlock[] | undefined,
630
+ ) => AnthropicSystemTextBlock[] | undefined
631
+ /** 附加的 body.metadata 字段(如 user_id 结构)。 */
632
+ metadata?: Record<string, unknown>
633
+ }
634
+
635
+ export interface AnthropicProviderOptions {
636
+ resolveKey?: AnthropicCredentialResolver
637
+ resolveAuth?: AnthropicAuthResolver
638
+ resolveBaseUrl?: AnthropicBaseUrlResolver
639
+ preferResolvedBaseUrl?: boolean
640
+ /**
641
+ * 请求定制点(RFC-140 D-copilot-spoof-split,取代此前的 `enableClaudeCodeSpoof` 布尔开关):
642
+ * `createAnthropicProvider` 是**任意**声明 `wireApi: 'anthropic-messages'` 的插件共用的
643
+ * 通用协议工厂(`provider-loader.ts` `buildFactory`)——本文件不认识"Claude Code"这个
644
+ * 具体厂商概念,只暴露一个通用回调,由调用方(插件)决定要不要、如何定制请求(伪装
645
+ * 身份头/system 前缀/metadata,或其他厂商特定需求)。
646
+ *
647
+ * `extensions/plugin-anthropic` 通过此参数注入 Claude Code 客户端伪装逻辑(绕 Anthropic
648
+ * 对订阅 OAuth token 的客户端分类器限流)——伪装成官方 Claude Code CLI 这一行为只对
649
+ * "连接 api.anthropic.com、用 Claude Pro/Max 订阅 OAuth token"这一具体场景成立且必要。
650
+ * 第三方 `anthropic-messages` 兼容端点(代理网关、企业自建实例等)不传本参数,默认零定制、
651
+ * 零伪装——不再需要显式传 `false` 来"关闭"(本就没有内置的厂商专属行为可关)。
652
+ */
653
+ customizeRequest?: (input: AnthropicRequestCustomizeInput) => AnthropicRequestCustomization
654
+ /**
655
+ * RFC-270 缺陷 1(P0):静态额外 headers(如 manifest 声明的企业网关路由标识头)。
656
+ * 此前本 Options **完全没有这个字段**——`plugin.ts` 的 adapter 也因此未透传
657
+ * `input.headers`,声明式 provider 给本协议写的 headers 被静默丢弃且不报错。该缺陷长期
658
+ * 未被发现,是因为唯一的真实用例 `anthropic-version` 恰好被 @anthropic-ai/sdk 自身硬编码
659
+ * 补上(`client.js` buildRequest),掩盖了丢弃。
660
+ */
661
+ headers?: Record<string, string>
662
+ /** RFC-122 D3:JWT claim 派生 headers(同缺陷 1,此前本协议未支持)。 */
663
+ tokenDerivedHeaders?: TokenDerivedHeader[]
664
+ /** RFC-270 D3:manifest 显式声明后,允许自定义头覆盖鉴权头(企业网关换鉴权)。 */
665
+ allowAuthHeaderOverride?: boolean
666
+ /** RFC-185 D3:宿主注入的日志端口,缺省 no-op(`noopWireProtocolLogger`,@x-otto/provider 单例)。 */
667
+ logger?: WireProtocolLogger
668
+ }
669
+
670
+ class AnthropicStream implements ProviderStream {
671
+ readonly id = 'anthropic-messages'
672
+ readonly displayName = 'Anthropic'
673
+
674
+ /**
675
+ * 进程内稳定的会话 id——供 `customizeRequest` 定制器使用(如订阅 OAuth 形状匹配的
676
+ * X-Claude-Code-Session-Id / metadata)。本文件不解读其含义,只透传给定制器。
677
+ */
678
+ private readonly requestSessionId = randomUUID()
679
+
680
+ constructor(
681
+ private readonly resolveCredential?: AnthropicCredentialResolver,
682
+ private readonly baseUrlResolver?: AnthropicBaseUrlResolver,
683
+ private readonly preferResolvedBaseUrl = false,
684
+ private readonly authResolver?: AnthropicAuthResolver,
685
+ /** 见 `AnthropicProviderOptions.customizeRequest` 注释。缺省 = 零定制。 */
686
+ private readonly customizeRequest?: (input: AnthropicRequestCustomizeInput) => AnthropicRequestCustomization,
687
+ private readonly logger: WireProtocolLogger = noopWireProtocolLogger,
688
+ /** 见 `AnthropicProviderOptions.headers`(RFC-270 缺陷 1 修复)。 */
689
+ private readonly staticHeaders?: Record<string, string>,
690
+ private readonly tokenDerivedHeaders?: TokenDerivedHeader[],
691
+ private readonly allowAuthHeaderOverride?: boolean,
692
+ ) {}
693
+
694
+ /**
695
+ * 统一鉴权解析(RFC-140 D5,`ProviderStream.resolveAuth`):优先 resolveAuth(含
696
+ * mode/beta);回退到旧的字符串 resolveKey(视为 api_key);最后回退 ANTHROPIC_API_KEY env。
697
+ */
698
+ async resolveAuth(_model: Model): Promise<ResolvedAuth> {
699
+ if (this.authResolver) {
700
+ const auth = await this.authResolver()
701
+ if (auth?.token) {
702
+ return auth
703
+ }
704
+ }
705
+
706
+ if (this.resolveCredential) {
707
+ const key = await this.resolveCredential()
708
+ if (key) {
709
+ return { token: key, mode: 'api_key' }
710
+ }
711
+ }
712
+
713
+ const envKey = process.env['ANTHROPIC_API_KEY']
714
+ if (envKey) {
715
+ return { token: envKey, mode: 'api_key' }
716
+ }
717
+
718
+ throw new ProviderError(
719
+ 'Missing Anthropic API key. Set ANTHROPIC_API_KEY or configure via auth file.',
720
+ { code: 'PROVIDER_AUTH_MISSING' },
721
+ )
722
+ }
723
+
724
+ private async buildRequestParams(
725
+ model: Model,
726
+ context: StreamContext,
727
+ auth: ResolvedAuth,
728
+ // 出站直接走 create({ stream: true }) 的流式重载(不再经 MessageStream),故类型是
729
+ // MessageCreateParamsStreaming 而非 MessageStreamParams。
730
+ ): Promise<{ client: Anthropic; requestParams: Anthropic.MessageCreateParamsStreaming; baseURL: string }> {
731
+ const baseURL = await resolveProviderBaseUrl(
732
+ 'ANTHROPIC_BASE_URL',
733
+ this.baseUrlResolver,
734
+ model.baseUrl,
735
+ this.preferResolvedBaseUrl,
736
+ )
737
+ // RFC-140 D-copilot-spoof-split:本文件不再判断"是否 oauth 才伪装"——那是具体厂商
738
+ // (如 plugin-anthropic 的 Claude Code 伪装)的业务逻辑,交给 customizeRequest 定制器
739
+ // 自行按 input.auth.mode 判断要不要返回定制内容。未提供定制器 = 零调用零开销。
740
+ const customization = this.customizeRequest?.({ auth, sessionId: this.requestSessionId })
741
+
742
+ /*
743
+ * RFC-270 缺陷 1(P0)+ D1:统一头管线,**filter-output 形态**。
744
+ *
745
+ * 本协议经 @anthropic-ai/sdk 出站,鉴权由 SDK 构造参数(apiKey/authToken)承担、不进
746
+ * 管线——因此不传 `auth` 档。鉴权保护必须靠"交给 SDK 前剥离"而非"最后写入":SDK 内部
747
+ * 合并链是 `内部默认 < authHeaders < defaultHeaders < options.headers`(后写覆盖,见
748
+ * @anthropic-ai/sdk internal/headers.js),otto 交给它的 defaultHeaders 排在 SDK 鉴权头
749
+ * **之后**、能覆盖它,write-last 在这条路径物理不成立。管线在 `auth` 缺省时照常执行
750
+ * ②③④ 档的鉴权族剥离,正是为此。
751
+ *
752
+ * 两个 auth 分支统一走同一份头(此前仅 oauth 分支传 defaultHeaders,api_key 分支完全
753
+ * 不传——即使定制器返回了头也会丢,是缺陷 1 的次生项)。
754
+ */
755
+ const defaultHeaders = buildRequestHeaders({
756
+ staticHeaders: this.staticHeaders,
757
+ tokenDerived: this.tokenDerivedHeaders?.length
758
+ ? { headers: this.tokenDerivedHeaders, token: auth.token }
759
+ : undefined,
760
+ customized: customization?.defaultHeaders,
761
+ allowAuthHeaderOverride: this.allowAuthHeaderOverride,
762
+ logger: this.logger,
763
+ diagnosticLabel: `anthropic-messages:${model.provider}`,
764
+ })
765
+ const headerOptions = Object.keys(defaultHeaders).length > 0 ? { defaultHeaders } : {}
766
+
767
+ // 代理接线:**刻意不在这里传 `fetchOptions.dispatcher`**(RFC-288 D0a 修订,重要事项规则 11)。
768
+ //
769
+ // 本仓的代理由 `installGlobalProxyDispatcher`(`@x-otto/shared`)经 undici 全局 dispatcher
770
+ // 统一接管,Anthropic SDK 内部的 `fetch` 自动经代理,无需此处显式传参——已在真实 Node
771
+ // 运行时实测确认(非仅测试环境)。
772
+ //
773
+ // 为什么不能传:Node 22 内置 fetch 用的是 Node 自带的 undici **6.23**
774
+ // (`process.versions.undici`),而 `@x-otto/shared` 依赖的是 undici **8.7**。
775
+ // `createDispatcher()` 造出的是 8.7 的 `ProxyAgent` 实例,把它交给内置 fetch 会抛
776
+ // `UND_ERR_INVALID_ARG: invalid onRequestStart method`——**配了代理的用户会直接请求失败**,
777
+ // 比"不走代理"更糟。`setGlobalDispatcher` 之所以能跨版本工作,是因为它写的是 Node 的
778
+ // `globalThis` 共享槽;per-request 的 `dispatcher` 走的却是严格实例校验,两者不同路。
779
+ //
780
+ // 若将来要恢复 per-client 接线,必须先让 shared 的 undici 与 Node 内置版本对齐,
781
+ // 并补一条**真发请求**的回归(只断言构造参数抓不到这类失败——这正是本缺陷曾溜过 M0 的原因)。
782
+ const client =
783
+ auth.mode === 'oauth'
784
+ ? new Anthropic({
785
+ apiKey: null,
786
+ authToken: auth.token,
787
+ baseURL,
788
+ maxRetries: 0,
789
+ ...headerOptions,
790
+ })
791
+ : new Anthropic({
792
+ apiKey: auth.token,
793
+ baseURL,
794
+ maxRetries: 0,
795
+ ...headerOptions,
796
+ })
797
+
798
+ const cacheControl = cacheControlFor(context.cacheRetention)
799
+
800
+ const tools: Anthropic.Tool[] = context.tools
801
+ ? context.tools.map((t) => ({
802
+ name: t.name,
803
+ description: t.description,
804
+ // 经公共 toJsonSchema 走双形态(zod / 纯 JSON Schema),与 openai 侧同源。
805
+ input_schema: toJsonSchema(t.parameters) as Anthropic.Tool['input_schema'],
806
+ }))
807
+ : []
808
+ const lastTool = tools[tools.length - 1]
809
+ if (lastTool && cacheControl) {
810
+ lastTool.cache_control = cacheControl
811
+ }
812
+
813
+ const effectiveMaxTokens = context.maxTokens ?? model.maxOutputTokens
814
+ const thinkingConfig = buildThinkingConfig(
815
+ // RFC-235:按模型声明的 thinkingLevels 降级后再算预算(clamp 单一真源)。
816
+ context.thinkingLevel ? clampThinkingLevel(context.thinkingLevel, model.thinkingLevels) : undefined,
817
+ model.reasoning,
818
+ effectiveMaxTokens,
819
+ model.thinkingMode,
820
+ )
821
+
822
+ let systemBlocks = buildSystemBlocks(
823
+ context.systemPrompt,
824
+ context.systemPromptSegments,
825
+ cacheControl,
826
+ )
827
+ if (customization?.transformSystemBlocks) {
828
+ // 定制点契约刻意用 AnthropicSystemTextBlock(SDK 无关最小结构)而非 Anthropic.TextBlockParam,
829
+ // 使宿主实现方(如 coding/plugin/)不必依赖 @anthropic-ai/sdk。两者结构兼容(前者是后者的
830
+ // 字段子集),此处是唯一需要接回 SDK 具体类型的边界点。
831
+ systemBlocks = customization.transformSystemBlocks(systemBlocks) as Anthropic.TextBlockParam[] | undefined
832
+ }
833
+
834
+ const requestParams: Anthropic.MessageCreateParamsStreaming = {
835
+ model: model.name,
836
+ stream: true,
837
+ max_tokens: effectiveMaxTokens,
838
+ ...(systemBlocks ? { system: systemBlocks } : {}),
839
+ messages: withMessageCacheBreakpoint(buildAnthropicMessages(context.messages, model), cacheControl),
840
+ ...(tools.length > 0 ? { tools } : {}),
841
+ ...(context.temperature !== undefined && !model.reasoning
842
+ ? { temperature: context.temperature }
843
+ : {}),
844
+ ...(thinkingConfig ? { thinking: thinkingConfig } : {}),
845
+ // adaptive 模式:网关要求顶层 output_config.effort 控制推理投入(与 thinking.type.adaptive 配对)。
846
+ ...(thinkingConfig?.type === 'adaptive' && context.thinkingLevel
847
+ ? { output_config: { effort: toAdaptiveEffort(clampThinkingLevel(context.thinkingLevel, model.thinkingLevels)) } }
848
+ : {}),
849
+ ...(customization?.metadata ? { metadata: customization.metadata } : {}),
850
+ }
851
+
852
+ return { client, requestParams, baseURL }
853
+ }
854
+
855
+ async *converse(
856
+ model: Model,
857
+ context: StreamContext,
858
+ options: StreamOptions,
859
+ signal: AbortSignal,
860
+ ): AsyncIterable<StreamEvent> {
861
+ const auth = await this.resolveAuth(model)
862
+ const { client, requestParams, baseURL } = await this.buildRequestParams(model, context, auth)
863
+
864
+ const state = initStreamState(model.id)
865
+ let started = false
866
+
867
+ this.logger.debug({ model: model.id, baseURL }, 'Starting Anthropic stream')
868
+
869
+ try {
870
+ /*
871
+ * 刻意用 `messages.create({ stream: true })` 的**原始 SSE 流**,而不是 `messages.stream()`。
872
+ *
873
+ * `messages.stream()` 返回的 MessageStream 会在转发每个事件的同时,用 SDK 自己的
874
+ * `accumulateMessage` 再累积一份 Message 快照——而 otto 本就有 `dispatchStreamEvent` +
875
+ * `state.blocks` 这份唯一权威的流状态机,SDK 那份快照的产物我们一个字节都不用。
876
+ * 它带来的只有**额外的流形状硬约束**,且这些约束严于 Anthropic 官方 SSE 契约:
877
+ * - `message_start.message.content` 缺省(不是 `[]`)→ 下一个 content_block_start 里
878
+ * `snapshot.content.push(...)` 直接 `TypeError: Cannot read properties of undefined
879
+ * (reading 'push')`,整轮对话失败;
880
+ * - 任何事件先于 message_start 到达 → 抛 `Unexpected event order`。
881
+ * 兼容网关(OpenAI chat_completions 上游转译成 anthropic-messages 下行)很容易漏掉
882
+ * `content: []` 这类"反正下游会补"的字段,真机上就是这么炸的(GLM 系模型 100% 复现)。
883
+ *
884
+ * 原始 Stream 只做 SSE 解析与 `event: error` → APIError 的映射(错误语义与 MessageStream
885
+ * 一致,不吞错),把"事件怎么拼成消息"完全交回给 otto——这才是本层该有的职责边界。
886
+ */
887
+ const { data: stream, response } = await client.messages
888
+ .create(requestParams, {
889
+ signal,
890
+ ...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
891
+ })
892
+ .withResponse()
893
+
894
+ for await (const event of stream) {
895
+ if (!started) {
896
+ started = true
897
+ yield { type: 'start', partial: buildAssistantMessage(state) }
898
+ }
899
+ yield* dispatchStreamEvent(event, state)
900
+ }
901
+
902
+ // 取消语义对齐 MessageStream:原始 Stream 在 abort 时是**静默结束迭代**(内部
903
+ // `if (isAbortError(e)) return`),若不显式还原,用户取消会被当成正常 done 上报、
904
+ // 带出一条半截的 assistant 消息。这里补回与此前完全同型的 APIUserAbortError
905
+ // (走下方 APIError 分支 → ProviderError PROVIDER_API_ERROR),上层分类逻辑不变。
906
+ if (signal.aborted) {
907
+ throw new Anthropic.APIUserAbortError()
908
+ }
909
+
910
+ // 成功路径 usage 快照:仅 Anthropic OAuth 订阅计划的响应带 anthropic-ratelimit-unified-*
911
+ // 头族(API key 走标准 RPM/TPM,headers 里没有该头族 → extractProviderUsageFromHeaders
912
+ // 返回 null,天然实现"API key 不显示")。
913
+ const providerUsage = extractProviderUsageFromHeaders(response?.headers)
914
+ const doneMessage = buildAssistantMessage(state, true)
915
+ yield {
916
+ type: 'done',
917
+ reason: doneMessage.stopReason,
918
+ // RFC-232(终局修正):跨 wire 协议救回"模型把工具调用写成字面文本 XML"违约响应
919
+ // (见 stream-state.ts recoverLiteralInvokeToolCalls 头注释)。判据内部短路,
920
+ // 无违约态时零开销(引用相等,不构造新对象)。
921
+ message: recoverLiteralInvokeToolCalls(doneMessage, registeredToolNames(context.tools)),
922
+ providerUsage,
923
+ }
924
+ } catch (error) {
925
+ if (error instanceof Anthropic.APIError) {
926
+ yield {
927
+ type: 'error',
928
+ error: new ProviderError(`Anthropic API error ${error.status}: ${error.message}`, {
929
+ code: 'PROVIDER_API_ERROR',
930
+ cause: error,
931
+ // 把 SDK 的 HTTP status/headers 转成 ProviderError 的 typed 字段
932
+ // (CodedError.status/headers),使 classifyStreamError 直接读 typed、不必沿 cause 链下挖。
933
+ status: error.status,
934
+ headers: error.headers,
935
+ // SDK 解析响应体 `error.error.type`(如 "authentication_error"/"permission_error")
936
+ // 到 APIError.type——机器可读的失效根因分类,供 stream.auth-failed 记录"为什么失效"
937
+ // (而不只是"401 unauthorized"这种笼统文案,见 CodedError.errorType 注释)。
938
+ errorType: error.type ?? undefined,
939
+ }),
940
+ }
941
+ } else if (error instanceof Error) {
942
+ yield { type: 'error', error }
943
+ } else {
944
+ yield {
945
+ type: 'error',
946
+ error: new ProviderError('Unknown Anthropic stream error', {
947
+ code: 'PROVIDER_STREAM_ERROR',
948
+ }),
949
+ }
950
+ }
951
+ }
952
+ }
953
+ }
954
+
955
+ export function createAnthropicProvider(options: AnthropicProviderOptions = {}): ProviderStream {
956
+ return new AnthropicStream(
957
+ options.resolveKey,
958
+ options.resolveBaseUrl,
959
+ options.preferResolvedBaseUrl,
960
+ options.resolveAuth,
961
+ options.customizeRequest,
962
+ options.logger,
963
+ options.headers,
964
+ options.tokenDerivedHeaders,
965
+ options.allowAuthHeaderOverride,
966
+ )
967
+ }