@wudao-org/providers 0.0.0-stage → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/anthropic.d.ts +19 -0
  5. package/dist/anthropic.d.ts.map +1 -0
  6. package/dist/anthropic.js +621 -0
  7. package/dist/anthropic.js.map +1 -0
  8. package/dist/balance.d.ts +28 -0
  9. package/dist/balance.d.ts.map +1 -0
  10. package/dist/balance.js +105 -0
  11. package/dist/balance.js.map +1 -0
  12. package/dist/config.d.ts +328 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +623 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/dispatcher.d.ts +5 -0
  17. package/dist/dispatcher.d.ts.map +1 -0
  18. package/dist/dispatcher.js +21 -0
  19. package/dist/dispatcher.js.map +1 -0
  20. package/dist/effort-catalog.d.ts +17 -0
  21. package/dist/effort-catalog.d.ts.map +1 -0
  22. package/dist/effort-catalog.js +73 -0
  23. package/dist/effort-catalog.js.map +1 -0
  24. package/dist/error-body.d.ts +32 -0
  25. package/dist/error-body.d.ts.map +1 -0
  26. package/dist/error-body.js +74 -0
  27. package/dist/error-body.js.map +1 -0
  28. package/dist/header-validation.d.ts +57 -0
  29. package/dist/header-validation.d.ts.map +1 -0
  30. package/dist/header-validation.js +130 -0
  31. package/dist/header-validation.js.map +1 -0
  32. package/dist/index.d.ts +42 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +110 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/json-arguments.d.ts +17 -0
  37. package/dist/json-arguments.d.ts.map +1 -0
  38. package/dist/json-arguments.js +227 -0
  39. package/dist/json-arguments.js.map +1 -0
  40. package/dist/key-pool.d.ts +66 -0
  41. package/dist/key-pool.d.ts.map +1 -0
  42. package/dist/key-pool.js +207 -0
  43. package/dist/key-pool.js.map +1 -0
  44. package/dist/missing-reasoning-circuit.d.ts +17 -0
  45. package/dist/missing-reasoning-circuit.d.ts.map +1 -0
  46. package/dist/missing-reasoning-circuit.js +38 -0
  47. package/dist/missing-reasoning-circuit.js.map +1 -0
  48. package/dist/model-failure.d.ts +156 -0
  49. package/dist/model-failure.d.ts.map +1 -0
  50. package/dist/model-failure.js +253 -0
  51. package/dist/model-failure.js.map +1 -0
  52. package/dist/model-fallback.d.ts +18 -0
  53. package/dist/model-fallback.d.ts.map +1 -0
  54. package/dist/model-fallback.js +87 -0
  55. package/dist/model-fallback.js.map +1 -0
  56. package/dist/models.d.ts +20 -0
  57. package/dist/models.d.ts.map +1 -0
  58. package/dist/models.js +55 -0
  59. package/dist/models.js.map +1 -0
  60. package/dist/openai-compatible.d.ts +87 -0
  61. package/dist/openai-compatible.d.ts.map +1 -0
  62. package/dist/openai-compatible.js +368 -0
  63. package/dist/openai-compatible.js.map +1 -0
  64. package/dist/openai-engine.d.ts +18 -0
  65. package/dist/openai-engine.d.ts.map +1 -0
  66. package/dist/openai-engine.js +515 -0
  67. package/dist/openai-engine.js.map +1 -0
  68. package/dist/openai-images.d.ts +38 -0
  69. package/dist/openai-images.d.ts.map +1 -0
  70. package/dist/openai-images.js +301 -0
  71. package/dist/openai-images.js.map +1 -0
  72. package/dist/openai-messages.d.ts +40 -0
  73. package/dist/openai-messages.d.ts.map +1 -0
  74. package/dist/openai-messages.js +135 -0
  75. package/dist/openai-messages.js.map +1 -0
  76. package/dist/openai-wire.d.ts +93 -0
  77. package/dist/openai-wire.d.ts.map +1 -0
  78. package/dist/openai-wire.js +151 -0
  79. package/dist/openai-wire.js.map +1 -0
  80. package/dist/persona/apply.d.ts +19 -0
  81. package/dist/persona/apply.d.ts.map +1 -0
  82. package/dist/persona/apply.js +98 -0
  83. package/dist/persona/apply.js.map +1 -0
  84. package/dist/persona/builtins.d.ts +3 -0
  85. package/dist/persona/builtins.d.ts.map +1 -0
  86. package/dist/persona/builtins.js +180 -0
  87. package/dist/persona/builtins.js.map +1 -0
  88. package/dist/persona/decorator.d.ts +4 -0
  89. package/dist/persona/decorator.d.ts.map +1 -0
  90. package/dist/persona/decorator.js +112 -0
  91. package/dist/persona/decorator.js.map +1 -0
  92. package/dist/persona/files.d.ts +20 -0
  93. package/dist/persona/files.d.ts.map +1 -0
  94. package/dist/persona/files.js +84 -0
  95. package/dist/persona/files.js.map +1 -0
  96. package/dist/persona/ids.d.ts +9 -0
  97. package/dist/persona/ids.d.ts.map +1 -0
  98. package/dist/persona/ids.js +11 -0
  99. package/dist/persona/ids.js.map +1 -0
  100. package/dist/persona/registry.d.ts +12 -0
  101. package/dist/persona/registry.d.ts.map +1 -0
  102. package/dist/persona/registry.js +54 -0
  103. package/dist/persona/registry.js.map +1 -0
  104. package/dist/persona/runtime.d.ts +23 -0
  105. package/dist/persona/runtime.d.ts.map +1 -0
  106. package/dist/persona/runtime.js +32 -0
  107. package/dist/persona/runtime.js.map +1 -0
  108. package/dist/persona/schema.d.ts +27 -0
  109. package/dist/persona/schema.d.ts.map +1 -0
  110. package/dist/persona/schema.js +165 -0
  111. package/dist/persona/schema.js.map +1 -0
  112. package/dist/persona/types.d.ts +38 -0
  113. package/dist/persona/types.d.ts.map +1 -0
  114. package/dist/persona/types.js +2 -0
  115. package/dist/persona/types.js.map +1 -0
  116. package/dist/pricing.d.ts +40 -0
  117. package/dist/pricing.d.ts.map +1 -0
  118. package/dist/pricing.js +70 -0
  119. package/dist/pricing.js.map +1 -0
  120. package/dist/quota-park.d.ts +53 -0
  121. package/dist/quota-park.d.ts.map +1 -0
  122. package/dist/quota-park.js +135 -0
  123. package/dist/quota-park.js.map +1 -0
  124. package/dist/rate-pause.d.ts +28 -0
  125. package/dist/rate-pause.d.ts.map +1 -0
  126. package/dist/rate-pause.js +72 -0
  127. package/dist/rate-pause.js.map +1 -0
  128. package/dist/registry.d.ts +142 -0
  129. package/dist/registry.d.ts.map +1 -0
  130. package/dist/registry.js +471 -0
  131. package/dist/registry.js.map +1 -0
  132. package/dist/responses.d.ts +5 -0
  133. package/dist/responses.d.ts.map +1 -0
  134. package/dist/responses.js +290 -0
  135. package/dist/responses.js.map +1 -0
  136. package/dist/retry-after.d.ts +13 -0
  137. package/dist/retry-after.d.ts.map +1 -0
  138. package/dist/retry-after.js +64 -0
  139. package/dist/retry-after.js.map +1 -0
  140. package/dist/retry-ladder.d.ts +176 -0
  141. package/dist/retry-ladder.d.ts.map +1 -0
  142. package/dist/retry-ladder.js +664 -0
  143. package/dist/retry-ladder.js.map +1 -0
  144. package/dist/split-inband-thinking.d.ts +35 -0
  145. package/dist/split-inband-thinking.d.ts.map +1 -0
  146. package/dist/split-inband-thinking.js +117 -0
  147. package/dist/split-inband-thinking.js.map +1 -0
  148. package/dist/stream-idle.d.ts +97 -0
  149. package/dist/stream-idle.d.ts.map +1 -0
  150. package/dist/stream-idle.js +127 -0
  151. package/dist/stream-idle.js.map +1 -0
  152. package/dist/tool-call-ids.d.ts +16 -0
  153. package/dist/tool-call-ids.d.ts.map +1 -0
  154. package/dist/tool-call-ids.js +36 -0
  155. package/dist/tool-call-ids.js.map +1 -0
  156. package/package.json +32 -3
@@ -0,0 +1,621 @@
1
+ import { UnsupportedContentError, offloadRequestFiles, probeModelClient, truncateSafe } from "@wudao-org/core";
2
+ import { createKeyPool, cooldownClassForStatus } from "./key-pool.js";
3
+ import { createRatePauseWindow, DEFAULT_RATE_PAUSE_MS } from "./rate-pause.js";
4
+ import { validatedInputEffort, effectiveReasoningEffort } from "./config.js";
5
+ import { applyWirePersona, mergeBodyOverlay } from "./persona/apply.js";
6
+ import { gatewayExtraHeaders } from "./header-validation.js";
7
+ import { ProviderHttpError, isContextLengthFailure, parseContextLimitBody } from "./openai-compatible.js";
8
+ // OOM 上限(r1a task 2):与 openai 引擎同一常量、同一截断方向(逐字照抄其用法,
9
+ // 不自创第二语义)——只截断响应侧本地解析缓冲,永不触碰请求构造/序列化。
10
+ import { MAX_ACCUM_CHARS, TRUNCATED_MARK, capAccumulated } from "./openai-compatible.js";
11
+ import { ContextLengthError } from "./openai-compatible.js";
12
+ // 加固移植(r8 §4.1):共享梯子 + M58 §B idle 看门狗 —— 与 openai 引擎同一套部件,
13
+ // 零新分类逻辑(本适配器抛的 ProviderHttpError 消息本就匹配梯子的 regex)。
14
+ // ω99-315:看门狗双档化(soft warn / hard abort)的共享部件在 stream-idle.ts。
15
+ import { withRetry, withRetryStream, StreamOverflowError, TruncatedStreamError, } from "./openai-compatible.js";
16
+ import { createStreamIdleGuard, resolveStreamIdleBudget } from "./stream-idle.js";
17
+ // r1a Task 3:共享的 M62 §7 有序模型 fallback 链装饰器(同一链、同一语义)。
18
+ import { withModelFallback } from "./model-fallback.js";
19
+ // r1a Task 4:429 Retry-After 解析(只读响应头,与 openai 引擎同一解析器)。
20
+ import { parseRetryAfterMs } from "./retry-after.js";
21
+ import { tryQuotaParkError } from "./quota-park.js";
22
+ import { readErrorBody } from "./error-body.js";
23
+ // ω-1a:name-as-id 会话历史出站时剥离 `#n` 去重后缀(真 toolu_* id 恒为 no-op)。
24
+ import { restoreToolCallIds, restoreToolCallId } from "./tool-call-ids.js";
25
+ // wire-r2 follow-up ①:显式 maxTokens 与 thinking budget 冲突的类型化错误
26
+ // (定义在包内类型化错误的家 model-failure.ts —— 零包内依赖,无环)。
27
+ import { MaxTokensThinkingBudgetConflictError } from "./model-failure.js";
28
+ /** 0.3.26 coding-plan gateways (bearerAuth profile flag): dual-send
29
+ * `authorization: Bearer` alongside x-api-key — Kimi/MiniMax official docs
30
+ * auth via ANTHROPIC_AUTH_TOKEN, zhipu accepts x-api-key; both headers
31
+ * together are the widest compatibility face. Without the flag the header
32
+ * set is byte-identical to the historical one (real anthropic builtin). */
33
+ function anthropicAuthHeaders(config, apiKey) {
34
+ return {
35
+ "x-api-key": apiKey,
36
+ "anthropic-version": "2023-06-01",
37
+ ...(config.bearerAuth === true ? { authorization: `Bearer ${apiKey}` } : {}),
38
+ };
39
+ }
40
+ function messagesUrl(baseURL) {
41
+ return baseURL.endsWith("/v1") ? `${baseURL}/messages` : `${baseURL}/v1/messages`;
42
+ }
43
+ /** 同角色相邻块合并(anthropic 交替约束);tool 结果挂 user 角色 tool_result 块。 */
44
+ function toAnthropicMessages(messages) {
45
+ const out = [];
46
+ const push = (role, blocks) => {
47
+ if (blocks.length === 0)
48
+ return;
49
+ const last = out[out.length - 1];
50
+ if (last !== undefined && last.role === role)
51
+ last.content.push(...blocks);
52
+ else
53
+ out.push({ role, content: [...blocks] });
54
+ };
55
+ for (const m of messages) {
56
+ if (m.role === "user")
57
+ push("user", [{ type: "text", text: m.content }]);
58
+ else if (m.role === "assistant") {
59
+ const blocks = [];
60
+ if (m.content !== "")
61
+ blocks.push({ type: "text", text: m.content });
62
+ // ω-1a:出站还原 provider 原生 id(name-as-id 会话历史的 `#n` 后缀)。
63
+ for (const tc of restoreToolCallIds(m.toolCalls ?? []))
64
+ blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.arguments });
65
+ push("assistant", blocks);
66
+ }
67
+ else {
68
+ // ω-1a:tool_result 的 tool_use_id 与上方 tool_use 同步剥离。
69
+ push("user", [{ type: "tool_result", tool_use_id: restoreToolCallId(m.toolCallId, m.name), content: m.content, is_error: m.isError === true }]);
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+ // R2 T1 — fail-closed images gate. toAnthropicMessages emits text/tool_use/
75
+ // tool_result blocks ONLY: an AgentMessage.images block riding the input
76
+ // would be silently DROPPED by the converter (live data loss, not a
77
+ // degradation). Same M79 typed family as the openai engine's
78
+ // assertImagesSupported, with a structural reason: switching MODELS can
79
+ // never fix it (the wire itself carries no image blocks), so the model-
80
+ // fallback chain correctly leaves it alone. Fires in the request builder —
81
+ // BEFORE any network call, before a single image byte is read, generate and
82
+ // stream alike (both paths build through here). Never retried (see
83
+ // isRetryableProviderError's UnsupportedContentError arm).
84
+ function assertNoImages(wire, input) {
85
+ for (const m of input.messages) {
86
+ const imgs = m.images;
87
+ if (Array.isArray(imgs) && imgs.length > 0) {
88
+ throw new UnsupportedContentError("wire-no-images", `the "${wire}" wire does not carry image blocks — switch wire (openai-chat) or drop the images (images are never silently dropped)`);
89
+ }
90
+ }
91
+ }
92
+ // R2 T2 — effort → thinking budget_tokens on the anthropic messages wire.
93
+ // The floor 1024 is the API's documented minimum; max 32768 is the
94
+ // conservative cross-model ceiling documented for budget-capable models
95
+ // (32K). "none" maps to NO thinking field: the anthropic way to disable
96
+ // thinking is omission. The key appears ONLY when an effort is explicit
97
+ // (prefix-cache red line: absent effort = byte-identical body).
98
+ const THINKING_BUDGET_TOKENS = {
99
+ low: 1024,
100
+ medium: 2048,
101
+ high: 8192,
102
+ max: 32768,
103
+ };
104
+ // wire-r2 follow-up ① — the fail-closed narrowing of the T2 raise above:
105
+ // an EXPLICIT cap (input.maxTokens or config.maxTokens — either is a value
106
+ // the caller chose) sitting at/below the thinking budget is no longer
107
+ // silently rewritten to budget+1024; it throws the typed conflict instead.
108
+ // Only the adapter's own implicit default cap keeps the historic raise.
109
+ // Runs BEFORE the retry ladder (validatedInputEffort's spot — a
110
+ // deterministic config conflict never sleeps, never hits the wire) on both
111
+ // request paths; isModelFallbackEligible keeps it off the model-fallback
112
+ // chain (a sibling model re-hits the identical conflict).
113
+ function assertExplicitMaxTokensAboveBudget(config, input) {
114
+ const effort = effectiveReasoningEffort(input, config);
115
+ if (effort === undefined || effort === "none")
116
+ return;
117
+ const budget = THINKING_BUDGET_TOKENS[effort];
118
+ const explicit = input.maxTokens ?? config.maxTokens;
119
+ if (explicit !== undefined && explicit <= budget) {
120
+ // conflict-origin: `explicit` resolves input-first (input.maxTokens ??
121
+ // config.maxTokens — a caller-chosen per-request cap wins), so the
122
+ // marker asks ONE question: does the PERSISTENT config cap ALSO sit
123
+ // at/below the budget (config alone re-conflicts)? Two marked shapes:
124
+ // (a) no input cap — explicit IS config; nothing to drop, every resend
125
+ // re-conflicts. (b) input cap ≤ budget with config ≤ budget too —
126
+ // dropping the input clip exposes the conflicting config cap; a
127
+ // clip-dropped compact resend resolves back to config.maxTokens and
128
+ // re-conflicts before the wire. In both, no core recovery helps, so the
129
+ // conflict carries the marker and core skips the futile compact ladder
130
+ // (the one clip-retained corner where compact could physically serve is
131
+ // ruled fail-closed BY POLICY at the loop's routing comments — masking a
132
+ // deterministic config error is worse than the speculative recovery).
133
+ // Unmarked (key absent): only the input cap conflicts (config absent or
134
+ // above budget) — dropping the clip leaves a servable cap, so core may
135
+ // route its ladder. Request bytes are untouched (error path only; the
136
+ // fail-closed gate itself is byte-identical).
137
+ const configCapAlsoConflicts = config.maxTokens !== undefined && config.maxTokens <= budget;
138
+ throw new MaxTokensThinkingBudgetConflictError(explicit, budget, configCapAlsoConflicts);
139
+ }
140
+ }
141
+ function buildAnthropicRequestBody(config, input) {
142
+ assertNoImages("anthropic", input);
143
+ // ω99-55: FILE blocks project to handle text before the wire converter
144
+ // (same request-build point the images gate sits at — no provider receives
145
+ // file bytes; text rides every wire, so unlike images there is nothing to
146
+ // refuse). Identity when nothing rides files: byte-identical bodies.
147
+ const messages = offloadRequestFiles(input.messages);
148
+ const effort = effectiveReasoningEffort(input, config);
149
+ const budget = effort !== undefined && effort !== "none" ? THINKING_BUDGET_TOKENS[effort] : undefined;
150
+ let maxTokens = input.maxTokens ?? config.maxTokens ?? 4096;
151
+ // The API structurally requires max_tokens > thinking.budget_tokens — a
152
+ // cap at/below the budget can never be served (guaranteed 400), so it is
153
+ // raised to budget + 1024 headroom. Fires only when an effort is explicit
154
+ // (same gate as the thinking key itself; absent effort = untouched cap).
155
+ // wire-r2 follow-up ①: EXPLICIT caps at/below the budget were already
156
+ // rejected before the ladder (assertExplicitMaxTokensAboveBudget), so in
157
+ // practice this raise now fires only for the adapter's implicit default —
158
+ // kept as the builder's own safety net, byte-identical to the status quo.
159
+ if (budget !== undefined && maxTokens <= budget)
160
+ maxTokens = budget + 1024;
161
+ return {
162
+ model: config.model,
163
+ max_tokens: maxTokens,
164
+ system: input.systemPrompt,
165
+ messages: toAnthropicMessages(messages),
166
+ tools: input.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters })),
167
+ ...(budget !== undefined ? { thinking: { type: "enabled", budget_tokens: budget } } : {}),
168
+ };
169
+ }
170
+ const STOP_REASON_MAP = { end_turn: "stop", tool_use: "tool_calls", max_tokens: "length", stop_sequence: "stop" };
171
+ function mapStopReason(stop) {
172
+ return typeof stop === "string" ? (STOP_REASON_MAP[stop] ?? stop) : undefined;
173
+ }
174
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
+ function fromAnthropicResponse(payload) {
176
+ const blocks = Array.isArray(payload?.content) ? payload.content : [];
177
+ // OOM fix:块拼接是本路径的累积点 —— 完成态累积器走 capAccumulated(同一常量、
178
+ // 同一 in-band 标记)。tool_use 的 input 在本协议里到达时已是解析后的对象,
179
+ // 无字符串累积点可套(见任务报告的证伪说明)。
180
+ const text = capAccumulated(blocks.filter((b) => b?.type === "text").map((b) => b.text ?? "").join(""));
181
+ const reasoning = capAccumulated(blocks.filter((b) => b?.type === "thinking").map((b) => b.thinking ?? "").join(""));
182
+ const toolCalls = blocks
183
+ .filter((b) => b?.type === "tool_use")
184
+ .map((b) => ({ id: String(b.id), name: String(b.name), arguments: (b.input ?? {}) }));
185
+ return {
186
+ role: "assistant",
187
+ content: text,
188
+ ...(reasoning !== "" ? { reasoning } : {}),
189
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
190
+ ...(mapStopReason(payload?.stop_reason) !== undefined ? { stopReason: mapStopReason(payload?.stop_reason) } : {}),
191
+ };
192
+ }
193
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
194
+ function anthropicUsage(payload, durationMs) {
195
+ // 基准×探针弧修平:anthropic 协议的 usage 三分(input_tokens +
196
+ // cache_creation_input_tokens + cache_read_input_tokens)。此前漏
197
+ // cache_creation —— 网关把大头 prompt 走 cache 写入时报账严重偏低
198
+ // (GLM 实证 12 调用只记 878 入向)。口径:promptTokens=三分总和(整张
199
+ // 提示词账单);cacheHit=read;miss=总-read(cache 写入轮=M24 语义的
200
+ // 冷转,计 miss)。
201
+ const inputTokens = typeof payload?.usage?.input_tokens === "number" ? payload.usage.input_tokens : 0;
202
+ const cacheCreate = typeof payload?.usage?.cache_creation_input_tokens === "number" ? payload.usage.cache_creation_input_tokens : 0;
203
+ const cacheRead = typeof payload?.usage?.cache_read_input_tokens === "number" ? payload.usage.cache_read_input_tokens : 0;
204
+ const outputTokens = typeof payload?.usage?.output_tokens === "number" ? payload.usage.output_tokens : 0;
205
+ const promptTotal = inputTokens + cacheCreate + cacheRead;
206
+ return {
207
+ promptTokens: promptTotal, cacheHitTokens: cacheRead, cacheMissTokens: Math.max(0, promptTotal - cacheRead),
208
+ completionTokens: outputTokens, durationMs,
209
+ };
210
+ }
211
+ async function generateAnthropicOnce(config, input, doFetch, keyPool, ratePause, persona) {
212
+ const wire = applyWirePersona(persona, "anthropic", config.provider);
213
+ const base = buildAnthropicRequestBody(config, input);
214
+ const body = wire?.bodyOverlay !== undefined ? mergeBodyOverlay(base, wire.bodyOverlay) : base;
215
+ const apiKey = keyPool.select();
216
+ const startedAt = Date.now();
217
+ const res = await doFetch(messagesUrl(config.baseURL), {
218
+ method: "POST",
219
+ headers: {
220
+ "content-type": "application/json",
221
+ ...anthropicAuthHeaders(config, apiKey),
222
+ // ω99-64: registry card headers (per-provider baseline) spread after
223
+ // adapter auth and before persona wire.headers — adapter auth >
224
+ // persona > registry. gatewayExtraHeaders skips the full
225
+ // FORBIDDEN_HEADER_KEYS set (14 keys — single source
226
+ // header-validation.ts, ω99-69) so auth stays structurally the key
227
+ // pool's (auth lands BEFORE these spreads; an unfiltered key would
228
+ // silently replace it).
229
+ ...gatewayExtraHeaders(config.extraHeaders),
230
+ ...(wire?.headers ?? {}),
231
+ },
232
+ body: JSON.stringify(body),
233
+ signal: input.signal,
234
+ });
235
+ if (!res.ok) {
236
+ // 夜移 007: status-classed cooldowns — auth (401/403) cools LONG, rate
237
+ // (429) keeps 60s, provider-side statuses never touch the key.
238
+ const cooldownClass = cooldownClassForStatus(res.status);
239
+ if (cooldownClass !== null)
240
+ keyPool.markCooldown(apiKey, cooldownClass);
241
+ const text = await readErrorBody(res); // 波次 8 #42: 有界错误体读取
242
+ // M58 §A: classify context-length failures (413/prompt_too_long) into ContextLengthError
243
+ // so loop's compact-once-retry can trigger via isContextLengthError (err.name === "ContextLengthError")
244
+ if (isContextLengthFailure(res.status, text)) {
245
+ throw new ContextLengthError(res.status, text, parseContextLimitBody(text, input.maxTokens ?? config.maxTokens));
246
+ }
247
+ // 与 openai-compatible generateOnce 的错误路径同构:ProviderHttpError + 状态与响应体
248
+ // (构造器实参以真实签名为准:which/status/body;overload 标志由构造器自算)。
249
+ // r1a Task 4:响应头的 Retry-After 随错误携带(只读响应头)。
250
+ // 波次 7 #37:同一 deadline 也设置 provider 级暂停窗(仅当键池无备键可换时)。
251
+ const retryAfterMs = parseRetryAfterMs(res.headers);
252
+ if (res.status === 429 && keyPool.snapshot().available === 0) {
253
+ ratePause.trip(retryAfterMs ?? DEFAULT_RATE_PAUSE_MS);
254
+ }
255
+ // 113-B3: 账户级配额 429(带可解析重置时刻)在通用错误之前分类——类型化
256
+ // QuotaParkError(与 openai 引擎同位同门,见 generateOnce)。
257
+ const quotaPark = tryQuotaParkError("request", res.status, text, retryAfterMs, { provider: config.provider, model: config.model });
258
+ if (quotaPark !== undefined)
259
+ throw quotaPark;
260
+ throw new ProviderHttpError("request", res.status, text, retryAfterMs, { provider: config.provider, model: config.model }); // v7 γ4: 失败腿的 provider/model 身份(结构化字段+消息尾)
261
+ }
262
+ // 波次 7 #37:2xx 证明暂停已过期(同 openai-compatible generateOnce)。
263
+ ratePause.clear();
264
+ const payload = await res.json();
265
+ input.onUsage?.(anthropicUsage(payload, Date.now() - startedAt));
266
+ return fromAnthropicResponse(payload);
267
+ }
268
+ /** SSE data: 行流(跨 chunk 缓冲,事件以空行分隔)。
269
+ * 加固移植(r8 §4.1):M58 §B idle 看门狗与 openai 引擎同形。ω99-315 双档化:
270
+ * 共享 guard(stream-idle.ts)——只在读挂起(活跃流式)时武装,每次 read 解决
271
+ * 即重置计时器;soft 档超时只发一次 onSoftWarning(不取消、不产生模型请求);
272
+ * hard 档超时先 reject 后 cancel(Node 在 reader.cancel() 内同步解决挂起的
273
+ * read 为 done:true,race 必须采纳类型化错误而非 cancel 的 done)。抛出的
274
+ * StreamIdleError 走调用方的重试梯子(重试字节相同,前缀缓存不受影响);
275
+ * 用户 abort 仍以 AbortError 浮出(永不重试)。idle 预算缺省/为 0 = 不武装。
276
+ * r1b(B 组):行缓冲上限 —— 持续发字节但永不出现事件边界的活跃流会让 buf
277
+ * 无界增长(idle 看门狗管不住:read 一直有数据)。buf 超过 MAX_ACCUM_CHARS 时
278
+ * 抛 StreamOverflowError,fail-loud 不截断(SSE 行中段截断会制造垃圾事件);
279
+ * 梯子把它按可重试传输类分类(照 StreamIdleError 的接线)。 */
280
+ export async function* sseDataLines(res, signal, idle) {
281
+ const reader = res.body.getReader();
282
+ const decoder = new TextDecoder();
283
+ let buf = "";
284
+ // M58 §B / ω99-315 — the dual-tier stream idle watchdog (stream-idle.ts),
285
+ // shape-mirroring the openai engine: soft warns once per silent gap, hard
286
+ // aborts with the enriched typed error and races the pending read.
287
+ const idleGuard = createStreamIdleGuard(idle ?? { softMs: 0, hardMs: 0 }, {
288
+ ...(idle?.onSoftWarning !== undefined ? { onSoftWarning: idle.onSoftWarning } : {}),
289
+ cancel: () => void reader.cancel().catch(() => { }),
290
+ });
291
+ // 124b 项 1(codex instant_interrupt):显式 abort 面。undici 在 fetch
292
+ // signal 触发时会拒绝挂起的 body read(既有中止语义依赖于此),但代理
293
+ // 可以握住连接越过 abort——本地 reader.cancel() 让挂起读【立刻】解阻塞
294
+ // (cancel 语义把挂起 read 解决为 done:true)。标志位随后把这次「EOF」
295
+ // 翻译回 AbortError:不做这一步的话 cancel 形态会被当 EOF → 无终止语义
296
+ // → TruncatedStreamError → 梯子重试一个用户刚要求中断的请求。signal 已
297
+ // 触发时 reclassifyTransportAbort 原样放行 AbortError(永不重试)。
298
+ let signalAborted = signal?.aborted === true;
299
+ const onAbort = () => {
300
+ signalAborted = true;
301
+ void reader.cancel().catch(() => { });
302
+ };
303
+ if (signal !== undefined && !signal.aborted)
304
+ signal.addEventListener("abort", onAbort, { once: true });
305
+ try {
306
+ for (;;) {
307
+ idleGuard.arm();
308
+ const { done, value } = await idleGuard.race(reader.read());
309
+ if (signalAborted) {
310
+ throw Object.assign(new Error("This operation was aborted"), { name: "AbortError" });
311
+ }
312
+ if (done) {
313
+ // ω99-44 (pi 64eeb82a4): EOF is a frame terminator — flush the decoder
314
+ // (multi-byte tails), then terminate a non-blank residual with the
315
+ // event separator so the FINAL event missing its trailing "\n\n" is
316
+ // still emitted (it was previously left in buf and silently dropped).
317
+ // 残尾判界: the residual must still be a complete JSON data line — a
318
+ // half-JSON tail falls to the consumer's JSON.parse discard; an EOF
319
+ // that never saw terminal semantics throws there (TruncatedStreamError).
320
+ buf += decoder.decode();
321
+ buf = buf.trim() !== "" ? buf + "\n\n" : "";
322
+ }
323
+ else {
324
+ buf += decoder.decode(value, { stream: true });
325
+ // ω99-315: payload arrived — the silent gap is over; both tiers reset.
326
+ idleGuard.chunk(value.byteLength);
327
+ }
328
+ let idx = buf.indexOf("\n\n");
329
+ while (idx !== -1) {
330
+ const chunk = buf.slice(0, idx);
331
+ buf = buf.slice(idx + 2);
332
+ for (const line of chunk.split("\n")) {
333
+ // 0.3.35: SSE spec — after the field colon ONE optional leading space
334
+ // is stripped. Real Anthropic sends `data: {…}`; some anthropic-wire
335
+ // gateways (kimi api.kimi.com/coding) send `data:{…}`. The strict
336
+ // `"data: "` prefix matched only the former and silently dropped
337
+ // every event of the latter — a completed-but-empty stream.
338
+ if (line.startsWith("data:"))
339
+ yield line.slice("data:".length).replace(/^ /, "");
340
+ }
341
+ idx = buf.indexOf("\n\n");
342
+ }
343
+ // ω99-44: the done-flush above drained the buffer through the normal
344
+ // event loop — exit only AFTER processing (previously `if (done) break`
345
+ // sat before the decode, so the residual event never got this far).
346
+ if (done)
347
+ break;
348
+ // r1b: cap the residual (unterminated) event prefix — a live stream
349
+ // that never emits an event boundary ("\n\n") would grow `buf` without
350
+ // bound, past any idle watchdog (every read resolves on time). Checked
351
+ // AFTER this read's complete events are consumed, so an abort that
352
+ // follows delivered deltas is never replayed (first-delta rule). Fail
353
+ // LOUD, never truncate (a sliced SSE event would fabricate garbage
354
+ // downstream events); the ladder classifies it exactly like
355
+ // StreamIdleError — retryable transport, byte-identical replay.
356
+ if (buf.length > MAX_ACCUM_CHARS) {
357
+ void reader.cancel().catch(() => { });
358
+ throw new StreamOverflowError(MAX_ACCUM_CHARS);
359
+ }
360
+ }
361
+ }
362
+ finally {
363
+ if (signal !== undefined)
364
+ signal.removeEventListener("abort", onAbort);
365
+ idleGuard.dispose();
366
+ reader.releaseLock();
367
+ }
368
+ }
369
+ /** ω99-44: the anthropic wire's terminal semantics — a message_stop event, or
370
+ * a message_delta carrying a stop_reason (the stop_reason-bearing delta is
371
+ * itself the finish signal: a proxy cutting right after it loses nothing the
372
+ * loop keys on, so it must NOT count as a truncation). Per-adapter on
373
+ * purpose — the openai/responses predicates live next to their own
374
+ * consumers; no shared one-size-fits-all. */
375
+ function isAnthropicStreamTerminal(ev) {
376
+ return ev.type === "message_stop" || (ev.type === "message_delta" && ev.delta?.stop_reason !== undefined);
377
+ }
378
+ async function* streamAnthropicOnce(config, input, doFetch, keyPool, ratePause, persona, streamIdle) {
379
+ const wire = applyWirePersona(persona, "anthropic", config.provider);
380
+ const base = { ...buildAnthropicRequestBody(config, input), stream: true };
381
+ const body = wire?.bodyOverlay !== undefined ? mergeBodyOverlay(base, wire.bodyOverlay) : base;
382
+ const apiKey = keyPool.select();
383
+ const startedAt = Date.now();
384
+ const res = await doFetch(messagesUrl(config.baseURL), {
385
+ method: "POST",
386
+ headers: {
387
+ "content-type": "application/json",
388
+ ...anthropicAuthHeaders(config, apiKey),
389
+ // ω99-64: registry card headers (per-provider baseline) spread after
390
+ // adapter auth and before persona wire.headers — adapter auth >
391
+ // persona > registry. gatewayExtraHeaders skips the full
392
+ // FORBIDDEN_HEADER_KEYS set (14 keys — single source
393
+ // header-validation.ts, ω99-69) so auth stays structurally the key
394
+ // pool's (auth lands BEFORE these spreads; an unfiltered key would
395
+ // silently replace it).
396
+ ...gatewayExtraHeaders(config.extraHeaders),
397
+ ...(wire?.headers ?? {}),
398
+ },
399
+ body: JSON.stringify(body),
400
+ signal: input.signal,
401
+ });
402
+ if (!res.ok) {
403
+ // 夜移 007: status-classed cooldowns — auth (401/403) cools LONG, rate
404
+ // (429) keeps 60s, provider-side statuses never touch the key.
405
+ const cooldownClass = cooldownClassForStatus(res.status);
406
+ if (cooldownClass !== null)
407
+ keyPool.markCooldown(apiKey, cooldownClass);
408
+ const text = await readErrorBody(res); // 波次 8 #42: 有界错误体读取
409
+ // M58 §A: classify context-length failures (413/prompt_too_long) into ContextLengthError
410
+ // so loop's compact-once-retry can trigger via isContextLengthError (err.name === "ContextLengthError")
411
+ if (isContextLengthFailure(res.status, text)) {
412
+ throw new ContextLengthError(res.status, text, parseContextLimitBody(text, input.maxTokens ?? config.maxTokens));
413
+ }
414
+ // 波次 7 #37:同 generateAnthropicOnce 的 Retry-After + 暂停窗接线。
415
+ const retryAfterMs = parseRetryAfterMs(res.headers);
416
+ if (res.status === 429 && keyPool.snapshot().available === 0) {
417
+ ratePause.trip(retryAfterMs ?? DEFAULT_RATE_PAUSE_MS);
418
+ }
419
+ // 113-B3: 流式路径同款配额分类(见上方 generate 位点)。
420
+ const quotaPark = tryQuotaParkError("request", res.status, text, retryAfterMs, { provider: config.provider, model: config.model });
421
+ if (quotaPark !== undefined)
422
+ throw quotaPark;
423
+ throw new ProviderHttpError("request", res.status, text, retryAfterMs, { provider: config.provider, model: config.model }); // v7 γ4: 失败腿的 provider/model 身份(结构化字段+消息尾)
424
+ }
425
+ // 波次 7 #37:2xx 证明暂停已过期。
426
+ ratePause.clear();
427
+ let text = "";
428
+ let reasoning = "";
429
+ let usageIn = 0, usageCacheCreate = 0, usageCacheRead = 0, usageOut = 0;
430
+ let stop;
431
+ // ω99-44: terminal tracking (message_stop | stop_reason-bearing delta) and
432
+ // attribution chars — EOF with sawTerminal=false is a cut, not a completion.
433
+ let sawTerminal = false;
434
+ let sawChars = 0;
435
+ const blocks = new Map();
436
+ // OOM fix(openai-compatible 同语义):累积器一旦越过上限即冻结 —— 该通道后续
437
+ // delta 直接丢弃(下游消费者从 yield 出的 delta 自行累积,只封最终消息封不住
438
+ // 它们),最终消息携带截断标记。
439
+ let textCapped = false;
440
+ let reasoningCapped = false;
441
+ const cappedJson = new Set();
442
+ for await (const data of sseDataLines(res, input.signal, streamIdle)) {
443
+ sawChars += data.length;
444
+ let ev;
445
+ try {
446
+ ev = JSON.parse(data);
447
+ }
448
+ catch {
449
+ continue;
450
+ }
451
+ if (isAnthropicStreamTerminal(ev))
452
+ sawTerminal = true;
453
+ if (ev?.type === "message_start") {
454
+ usageIn = ev.message?.usage?.input_tokens ?? 0;
455
+ // 基准×探针弧修平:三分记账(见 anthropicUsage 注释)——cache 写入
456
+ // 此前被整块丢弃,流式路径同样报账偏低。
457
+ usageCacheCreate = ev.message?.usage?.cache_creation_input_tokens ?? 0;
458
+ usageCacheRead = ev.message?.usage?.cache_read_input_tokens ?? 0;
459
+ }
460
+ else if (ev?.type === "content_block_start") {
461
+ const cb = ev.content_block ?? {};
462
+ blocks.set(ev.index, { kind: cb.type === "tool_use" ? "tool_use" : cb.type === "thinking" ? "thinking" : "text", ...(cb.type === "tool_use" ? { tool: { id: String(cb.id), name: String(cb.name), json: "" } } : {}) });
463
+ }
464
+ else if (ev?.type === "content_block_delta") {
465
+ const d = ev.delta ?? {};
466
+ const blk = blocks.get(ev.index);
467
+ if (d.type === "text_delta") {
468
+ if (!textCapped) {
469
+ text += d.text ?? "";
470
+ if (text.length > MAX_ACCUM_CHARS) {
471
+ text = truncateSafe(text, MAX_ACCUM_CHARS); // 113-A4:累积帽代理对安全
472
+ textCapped = true;
473
+ }
474
+ yield { type: "text_delta", text: String(d.text ?? "") };
475
+ }
476
+ }
477
+ else if (d.type === "thinking_delta") {
478
+ if (!reasoningCapped) {
479
+ reasoning += d.thinking ?? "";
480
+ if (reasoning.length > MAX_ACCUM_CHARS) {
481
+ reasoning = truncateSafe(reasoning, MAX_ACCUM_CHARS); // 113-A4
482
+ reasoningCapped = true;
483
+ }
484
+ yield { type: "reasoning_delta", text: String(d.thinking ?? "") };
485
+ }
486
+ }
487
+ else if (d.type === "input_json_delta" && blk?.tool) {
488
+ if (!cappedJson.has(ev.index)) {
489
+ blk.tool.json += d.partial_json ?? "";
490
+ if (blk.tool.json.length > MAX_ACCUM_CHARS) {
491
+ blk.tool.json = truncateSafe(blk.tool.json, MAX_ACCUM_CHARS); // 113-A4
492
+ cappedJson.add(ev.index);
493
+ }
494
+ }
495
+ }
496
+ }
497
+ else if (ev?.type === "message_delta") {
498
+ if (ev.delta?.stop_reason !== undefined)
499
+ stop = ev.delta.stop_reason;
500
+ if (typeof ev.usage?.output_tokens === "number")
501
+ usageOut = ev.usage.output_tokens;
502
+ }
503
+ else if (ev?.type === "message_stop") {
504
+ break;
505
+ }
506
+ }
507
+ // ω99-44 (pi ebc374490): EOF without terminal semantics = the stream was
508
+ // cut. No synthetic done, no fabricated usage — the typed error walks the
509
+ // ladder as retryable transport (pre-first-delta replays byte-identically;
510
+ // a post-delta cut propagates to the loop untouched).
511
+ if (!sawTerminal)
512
+ throw new TruncatedStreamError("anthropic", sawChars);
513
+ input.onUsage?.({
514
+ promptTokens: usageIn + usageCacheCreate + usageCacheRead,
515
+ cacheHitTokens: usageCacheRead,
516
+ cacheMissTokens: Math.max(0, usageIn + usageCacheCreate),
517
+ completionTokens: usageOut, durationMs: Date.now() - startedAt,
518
+ });
519
+ const toolCalls = [...blocks.entries()].filter(([, b]) => b.tool).map(([idx, b]) => {
520
+ const t = b.tool;
521
+ // 冻结的工具参数在解析前拼上标记(照抄 openai 引擎的 cappedArgs 用法);
522
+ // 截断+标记不再是合法 JSON → 落进本适配器既有的 _unparsed 兜底(有界、带标记)。
523
+ const json = cappedJson.has(idx) ? t.json + TRUNCATED_MARK : t.json;
524
+ let args = {};
525
+ try {
526
+ args = json === "" ? {} : JSON.parse(json);
527
+ }
528
+ catch {
529
+ args = { _unparsed: json };
530
+ }
531
+ return { id: t.id, name: t.name, arguments: args };
532
+ });
533
+ const message = {
534
+ role: "assistant",
535
+ content: textCapped ? text + TRUNCATED_MARK : text,
536
+ ...(reasoning !== "" ? { reasoning: reasoningCapped ? reasoning + TRUNCATED_MARK : reasoning } : {}),
537
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
538
+ ...(mapStopReason(stop) !== undefined ? { stopReason: mapStopReason(stop) } : {}),
539
+ };
540
+ yield { type: "done", message };
541
+ }
542
+ /** 基准×探针弧:传输层 abort 归一。用户中止(input.signal 已触发)原样
543
+ * 透传 AbortError(梯子永不重试——语义不变);反之(信号未触发却收到
544
+ * AbortError)= 网关/连接层切断的传输形态(GLM 实证:建连后 ~4s 零字节
545
+ * 断开,12/79 次、恰非 429——无状态码无错误体)——包成
546
+ * TruncatedStreamError 归可重试运输类:首 delta 前重放=字节同一(M24 暖
547
+ * 前缀),梯子自愈,不再仰赖 loop 级恢复臂兜底。 */
548
+ function reclassifyTransportAbort(err, input) {
549
+ if (err instanceof Error && err.name === "AbortError" && input.signal?.aborted !== true) {
550
+ return new TruncatedStreamError("anthropic", 0);
551
+ }
552
+ return err;
553
+ }
554
+ async function* reclassifyAbortStream(gen, input) {
555
+ try {
556
+ for await (const ev of gen)
557
+ yield ev;
558
+ }
559
+ catch (err) {
560
+ throw reclassifyTransportAbort(err, input);
561
+ }
562
+ }
563
+ export function createAnthropicProvider(config, options = {}) {
564
+ const doFetch = options.fetch ?? fetch;
565
+ const keyPool = options.keyPool ?? createKeyPool(config.apiKeys ?? [config.apiKey]);
566
+ // 波次 7 #37 (CodeWhale C1):provider 级 429 暂停窗(同 openai-compatible 引擎)。
567
+ const ratePause = options.ratePauseWindow ?? createRatePauseWindow();
568
+ const persona = options.personaRuntime;
569
+ // 加固移植(r8 §4.1):梯子与看门狗选项此前被忽略 —— 现在与 openai 引擎同解析
570
+ // (工厂期一次):onRetry 合并进梯子选项。ω99-315:idle 预算双档化
571
+ // (soft 90s warn / hard 300s abort;legacy 选项与 env 仍是 hard 档覆盖),
572
+ // 与 openai 引擎同一 resolveStreamIdleBudget。
573
+ const retry = {
574
+ ...options.retry,
575
+ ...(options.onRetry !== undefined ? { onRetry: options.onRetry } : {}),
576
+ // 121 项 1:容量观察者同 onRetry 一并合并进梯子选项。
577
+ ...(options.onOverload !== undefined ? { onOverload: options.onOverload } : {}),
578
+ };
579
+ const streamIdle = {
580
+ ...resolveStreamIdleBudget({
581
+ softMs: options.streamIdleSoftMs,
582
+ hardMs: options.streamIdleHardMs,
583
+ legacyTimeoutMs: options.streamIdleTimeoutMs,
584
+ }),
585
+ ...(options.onStreamIdleWarning !== undefined ? { onSoftWarning: options.onStreamIdleWarning } : {}),
586
+ };
587
+ // r1a Task 3:anthropic 适配器经共享装饰器获得模型 fallback 能力 —— 与
588
+ // openai 引擎同一链、同一语义(config.fallbackModels,过滤与主模型相同的
589
+ // 项;sticky streak=2;key-class/abort/上下文溢出永不 fallback)。本工厂
590
+ // 只提供「一个模型的一条腿」:共享梯子在腿内,请求构造/序列化零改动。
591
+ const legClient = (cfg) => ({
592
+ modelId: cfg.model,
593
+ // 共享梯子包两条路径(M26 P11);querySource 从 ModelInput 穿进梯子选项
594
+ // (M70 §3:后台来源不重试 overload,照抄 openai-compatible 引擎的接法)。
595
+ async generate(input) {
596
+ // R2 T2 (M58 §C, engine parity): junk per-input effort fails fast
597
+ // BEFORE the ladder — parse errors are never retried, never on the wire.
598
+ const validated = validatedInputEffort(input);
599
+ // wire-r2 follow-up ①: an explicit maxTokens conflicting with the
600
+ // thinking budget fails closed BEFORE the ladder, same spot.
601
+ assertExplicitMaxTokensAboveBudget(cfg, validated);
602
+ // 波次 7 #37:腿入口门 —— 并发请求在此等暂停窗;发起者的梯子重试在腿内,不门。
603
+ await ratePause.wait(input.signal);
604
+ return withRetry(() => generateAnthropicOnce(cfg, validated, doFetch, keyPool, ratePause, persona)
605
+ .catch((err) => { throw reclassifyTransportAbort(err, validated); }), { ...retry, querySource: input.querySource });
606
+ },
607
+ async *stream(input) {
608
+ const validated = validatedInputEffort(input);
609
+ assertExplicitMaxTokensAboveBudget(cfg, validated);
610
+ // 波次 7 #37:同 generate 的入口门(首 delta 之前)。
611
+ await ratePause.wait(input.signal);
612
+ yield* withRetryStream(() => reclassifyAbortStream(streamAnthropicOnce(cfg, validated, doFetch, keyPool, ratePause, persona, streamIdle), validated), { ...retry, querySource: input.querySource });
613
+ },
614
+ });
615
+ // PB-1 探针缝(模型面,基准×探针弧修平):anthropic 线与 openai 工厂同款
616
+ // ——工厂返回处装饰记 model.call(tokens/缓存/重试/时延)。此前只挂了
617
+ // openai-compatible,anthropic 线(glm/kimi/minimax 等 wire:"anthropic")
618
+ // 模型遥测整面缺席(基准跑分器实证 model.call 零事件)。探针关=原对象。
619
+ return probeModelClient(withModelFallback(legClient, config));
620
+ }
621
+ //# sourceMappingURL=anthropic.js.map