@deepwhale/llm 1.0.10 → 1.0.12

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 yysf1949
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yysf1949
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepwhale/llm",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "LLM client factory: DeepSeek + Anthropic providers with token pricing",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,147 +0,0 @@
1
- /**
2
- * @deepwhale/llm — AnthropicClient
3
- *
4
- * Sprint 1b.5 Step 2: Anthropic provider, 走 /anthropic endpoint 薄包装方案 (D1 拍板).
5
- *
6
- * 关键设计 (R-D 拍板 2026-06-03):
7
- * - 用官方 @anthropic-ai/sdk 实例发请求, 不手写 fetch
8
- * - SDK opts.fetch 注入是设计意图内的 escape hatch (Cloudflare Workers / Deno
9
- * proxy), 我们用同 pattern 注入 mock fetch 用于测试, 真实部署走 SDK 实际 fetch
10
- * - baseURL 落 https://api.deepseek.com/anthropic, authToken 复用 DEEPSEEK_API_KEY
11
- * (DeepSeek shim 接 /anthropic 路径, 同 key 验证) — **不**直连 api.anthropic.com
12
- * - 响应是 Anthropic-shape Message, 写 parseAnthropicMessage 翻译成 ChatResult
13
- * - SSE 走 SDK MessageStream, 写 parseAnthropicSseEvent 翻译 RawMessageStreamEvent
14
- * → ChatChunk. 不复用 parseOai* (那是 OAI shape, 协议不同)
15
- * - X3 拍板: Step 2 不接真 API, 测试用 mock fetch, 不碰 key
16
- *
17
- * Cache 字段 (B1 拍板): Anthropic 有 cache_creation_input_tokens (新建) +
18
- * cache_read_input_tokens (命中) 两个**独立**字段. 我们合并到 cached_tokens:
19
- * cached_tokens = (cache_creation ?? 0) + (cache_read ?? 0)
20
- * cache_creation 详细拆解留 sprint 2 改进 (跟 cache_hit_rate / cost_turn 一起).
21
- *
22
- * StopReason 翻译 (Anthropic → OAI-shape finish_reason):
23
- * 'end_turn' → 'stop'
24
- * 'stop_sequence' → 'stop'
25
- * 'max_tokens' → 'length'
26
- * 'tool_use' → 'tool_calls'
27
- * null (in-flight) → undefined
28
- */
29
- import type { Message as AnthropicMessage, MessageDeltaUsage, RawMessageStreamEvent, Usage as AnthropicUsage } from '@anthropic-ai/sdk/resources/messages/messages.js';
30
- import type { ChatChunk, ChatMessage, ChatResult, LLMClient, LLMToolSchema, ModelId, Usage } from './types.js';
31
- import type { PricingConfig } from './pricing-config.js';
32
- /** DeepSeek shim 提供的 /anthropic 兼容端点 (相对 OAI v1 端点). */
33
- export declare const DEEPSEEK_ANTHROPIC_BASE_URL = "https://api.deepseek.com/anthropic";
34
- /** Anthropic 默认 model (走 DeepSeek shim 时, 服务端映射到 Claude Sonnet 4.5). */
35
- export declare const ANTHROPIC_DEFAULT_MODEL = "claude-sonnet-4-5";
36
- export interface AnthropicClientOptions {
37
- /** API key。优先于 process.env.ANTHROPIC_AUTH_TOKEN / DEEPSEEK_API_KEY。 */
38
- apiKey?: string;
39
- /** 模型 ID,默认 claude-sonnet-4-5。 */
40
- model?: string;
41
- /**
42
- * Base URL。
43
- *
44
- * 拍板 1C (2026-06-04): **DeepSeek-first + Anthropic SDK 协议兼容 + 多 Provider Adapter**。
45
- * 默认 `https://api.deepseek.com/anthropic` (DeepSeek 提供的 /anthropic 兼容端点, 单 key 走两家),
46
- * 但 caller 可显式指定其他 Anthropic-兼容 provider:
47
- * - `https://api.anthropic.com` — 真 Anthropic API (需 ANTHROPIC_AUTH_TOKEN 真 key, 不走 DEEPSEEK_API_KEY 退路)
48
- * - `https://openrouter.ai/api/v1/anthropic` — OpenRouter Anthropic Route
49
- * - 任何自定义 proxy / 兼容层
50
- *
51
- * Sprint 1d.5-B 揭示: DeepSeek /anthropic 端点**实际** routing 兜底到 OAI flash (server.model=deepseek-v4-flash),
52
- * 行为稳定但 server 协议声明 mis-labeled. 1C 拍板**不**解决这个 routing 问题 — 现状保留, caller
53
- * 自选 baseUrl 决定走哪条路.
54
- */
55
- baseUrl?: string;
56
- /** 自定义 fetch (注入 mock 用于测试). 默认走 SDK 内部 fetch. */
57
- fetchImpl?: typeof fetch;
58
- /** 单次 HTTP 调用的超时毫秒,默认 60s。 */
59
- timeoutMs?: number;
60
- /**
61
- * pricing config override. 不传 → 走 ship-in default.toml.
62
- * Sprint 1b.5 pricing 抽象层 (per-model currency). 详见 pricing-config.ts.
63
- */
64
- pricing?: PricingConfig;
65
- }
66
- /**
67
- * AnthropicClient implements LLMClient.
68
- *
69
- * 内部包一个 @anthropic-ai/sdk 实例, .messages.create() / .messages.stream()
70
- * 走 SDK 真实 HTTP 路径. 我们负责:
71
- * - ChatMessage → Anthropic.MessageParam 转换 (Sprint 1c 再加 tool_use schema)
72
- * - Anthropic.Message → ChatResult 转换 (parseAnthropicMessage)
73
- * - RawMessageStreamEvent → ChatChunk 转换 (parseAnthropicSseEvent, 含 usage 翻译)
74
- *
75
- * 不**在**这里写 HTTP / SSE 解析 (SDK 负责), 不**在**这里做重试 (SDK 自带 maxRetries=2,
76
- * 我们接受默认). Sprint 1c 集成测真接 shim 时可调整 SDK timeout / maxRetries.
77
- */
78
- export declare class AnthropicClient implements LLMClient {
79
- readonly model: ModelId;
80
- private readonly apiKey;
81
- private readonly baseUrl;
82
- private readonly timeoutMs;
83
- private readonly pricing;
84
- private readonly sdk;
85
- constructor(options?: AnthropicClientOptions);
86
- chat(messages: ChatMessage[], options?: {
87
- signal?: AbortSignal;
88
- tools?: ReadonlyArray<LLMToolSchema>;
89
- tool_choice?: 'auto' | 'none' | 'required';
90
- }): Promise<ChatResult>;
91
- stream(messages: ChatMessage[], options: {
92
- signal?: AbortSignal;
93
- tools?: ReadonlyArray<LLMToolSchema>;
94
- tool_choice?: 'auto' | 'none' | 'required';
95
- onChunk: (chunk: ChatChunk) => void;
96
- }): Promise<ChatResult>;
97
- }
98
- /**
99
- * 把 Anthropic SDK 的 Message 翻译成 ChatResult.
100
- *
101
- * - content: 拼 text block, 跳过 tool_use (1c 实施) / thinking (1b.5 不暴露)
102
- * - stop_reason → finish_reason 翻译
103
- * - usage: cache_creation + cache_read → cached_tokens (B1 拍板). 算 cache_hit_rate
104
- * + cost_turn + tokens_uncached 跟 OAI 路径一致.
105
- */
106
- export declare function parseAnthropicMessage(message: AnthropicMessage, fallbackModel: ModelId, pricing?: PricingConfig): ChatResult;
107
- /**
108
- * 把 Anthropic Usage 翻译成标准化 Usage 结构 (带 cache/cost 字段).
109
- *
110
- * B1 拍板 (Sprint 1b.5 Step 2): cached_tokens = (cache_creation ?? 0) + (cache_read ?? 0).
111
- *
112
- * ⚠️ 重要语义修正 (F4 拍板 2026-06-03, review 找到):
113
- * Anthropic 官方 prompt caching 文档: total input tokens = input_tokens + cache_creation_input_tokens
114
- * + cache_read_input_tokens. 之前 Step 2 写时把 input_tokens 当"总 prompt" 是错的, 漏算 cache
115
- * 字段对应的 token 总量. 修法:
116
- *
117
- * - total_prompt = input_tokens + cache_creation + cache_read
118
- * - cached_tokens = cache_creation + cache_read (跟 Step 2 一致)
119
- * - tokens_uncached = total_prompt - cached_tokens = input_tokens (不变量)
120
- * - cache_hit_rate = cached_tokens / total_prompt (cache 命中率, 包括 write + read)
121
- *
122
- * **Cost 1b.5 保守策略** (F4 拍板): cache_creation 跟 cache_read 价格不同 (Sonnet 1h TTL write
123
- * \$3.75/M, read \$0.30/M, 比例 12.5×), 1b.5 pricing 模型**不**拆 cache_write vs cache_read 字段.
124
- * 为避免假装知道 cache_creation 价格, **保守**: cache_creation OR cache_read 任一非零 →
125
- * cost_turn/cost_currency 字段 absent. 留 sprint 2 加 `cache_write_per_m` 字段.
126
- * - 注意: tokens_uncached 仍**可**算 (是 input_tokens, 跟 cache 字段无关), 不受 cost 限制
127
- *
128
- * 接受 Usage | MessageDeltaUsage (后者无 cache_creation / cache_read, 视为 0).
129
- * Sprint 1b.5 Step 2: 流末尾 message_delta event.usage 是 MessageDeltaUsage, 走 delta 路径
130
- * 不算 cost, 真实生产靠 stream.finalMessage() 拿完整 Usage.
131
- */
132
- export declare function parseAnthropicUsage(usage: AnthropicUsage | MessageDeltaUsage, fallbackModel: ModelId, pricing?: PricingConfig): Usage | undefined;
133
- /**
134
- * 把单个 RawMessageStreamEvent 翻译成 ChatChunk.
135
- * 返 null 表示跳过 (heartbeat / ping / 不该透出的 event).
136
- *
137
- * Event 类型 (来自 SDK type):
138
- * - message_start: 携带 message metadata (model, id, usage.input_tokens=0) — 不透出
139
- * - content_block_start: 新的 text/tool_use/thinking block 起点 — 不透出 (避免 0 token 块)
140
- * - content_block_delta: text 增量 (delta.text) 或 input_json_delta (tool_use) — 透出 content
141
- * - content_block_stop: block 结束 — 不透出
142
- * - message_delta: 顶层 delta (stop_reason + usage 更新) — 透出 finish_reason + 最终 usage
143
- * - message_stop: 流结束 — 不透出 (caller 走 stream.finalMessage 拿 final Message)
144
- * - ping: heartbeat — 不透出
145
- */
146
- export declare function parseAnthropicSseEvent(event: RawMessageStreamEvent, fallbackModel: ModelId, pricing?: PricingConfig): ChatChunk | null;
147
- //# sourceMappingURL=anthropic-client.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"anthropic-client.d.ts","sourceRoot":"","sources":["../src/anthropic-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EACV,OAAO,IAAI,gBAAgB,EAE3B,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,IAAI,cAAc,EACxB,MAAM,kDAAkD,CAAC;AAM1D,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,SAAS,EACT,aAAa,EACb,OAAO,EAEP,KAAK,EACN,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,wDAAwD;AACxD,eAAO,MAAM,2BAA2B,uCAAuC,CAAC;AAEhF,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAI3D,MAAM,WAAW,sBAAsB;IACrC,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAC/C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;gBAEpB,OAAO,GAAE,sBAA2B;IAwB1C,IAAI,CACR,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,KAAK,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;QACrC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;KAC5C,GACA,OAAO,CAAC,UAAU,CAAC;IA0BhB,MAAM,CACV,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,EAAE;QACP,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,KAAK,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;QACrC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;QAC3C,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;KACrC,GACA,OAAO,CAAC,UAAU,CAAC;CAsCvB;AAqID;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,gBAAgB,EACzB,aAAa,EAAE,OAAO,EACtB,OAAO,CAAC,EAAE,aAAa,GACtB,UAAU,CA8BZ;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,cAAc,GAAG,iBAAiB,EACzC,aAAa,EAAE,OAAO,EACtB,OAAO,CAAC,EAAE,aAAa,GACtB,KAAK,GAAG,SAAS,CA6CnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,qBAAqB,EAC5B,aAAa,EAAE,OAAO,EACtB,OAAO,CAAC,EAAE,aAAa,GACtB,SAAS,GAAG,IAAI,CA2BlB"}
@@ -1,443 +0,0 @@
1
- /**
2
- * @deepwhale/llm — AnthropicClient
3
- *
4
- * Sprint 1b.5 Step 2: Anthropic provider, 走 /anthropic endpoint 薄包装方案 (D1 拍板).
5
- *
6
- * 关键设计 (R-D 拍板 2026-06-03):
7
- * - 用官方 @anthropic-ai/sdk 实例发请求, 不手写 fetch
8
- * - SDK opts.fetch 注入是设计意图内的 escape hatch (Cloudflare Workers / Deno
9
- * proxy), 我们用同 pattern 注入 mock fetch 用于测试, 真实部署走 SDK 实际 fetch
10
- * - baseURL 落 https://api.deepseek.com/anthropic, authToken 复用 DEEPSEEK_API_KEY
11
- * (DeepSeek shim 接 /anthropic 路径, 同 key 验证) — **不**直连 api.anthropic.com
12
- * - 响应是 Anthropic-shape Message, 写 parseAnthropicMessage 翻译成 ChatResult
13
- * - SSE 走 SDK MessageStream, 写 parseAnthropicSseEvent 翻译 RawMessageStreamEvent
14
- * → ChatChunk. 不复用 parseOai* (那是 OAI shape, 协议不同)
15
- * - X3 拍板: Step 2 不接真 API, 测试用 mock fetch, 不碰 key
16
- *
17
- * Cache 字段 (B1 拍板): Anthropic 有 cache_creation_input_tokens (新建) +
18
- * cache_read_input_tokens (命中) 两个**独立**字段. 我们合并到 cached_tokens:
19
- * cached_tokens = (cache_creation ?? 0) + (cache_read ?? 0)
20
- * cache_creation 详细拆解留 sprint 2 改进 (跟 cache_hit_rate / cost_turn 一起).
21
- *
22
- * StopReason 翻译 (Anthropic → OAI-shape finish_reason):
23
- * 'end_turn' → 'stop'
24
- * 'stop_sequence' → 'stop'
25
- * 'max_tokens' → 'length'
26
- * 'tool_use' → 'tool_calls'
27
- * null (in-flight) → undefined
28
- */
29
- import Anthropic from '@anthropic-ai/sdk';
30
- import { readFileSync } from 'node:fs';
31
- import { fileURLToPath } from 'node:url';
32
- import { dirname, resolve } from 'node:path';
33
- import process from 'node:process';
34
- import { APIKeyMissingError, LLMUnknownError } from './types.js';
35
- import { computeCost, parsePricingConfig } from './pricing-config.js';
36
- /** DeepSeek shim 提供的 /anthropic 兼容端点 (相对 OAI v1 端点). */
37
- export const DEEPSEEK_ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic';
38
- /** Anthropic 默认 model (走 DeepSeek shim 时, 服务端映射到 Claude Sonnet 4.5). */
39
- export const ANTHROPIC_DEFAULT_MODEL = 'claude-sonnet-4-5';
40
- const DEFAULT_TIMEOUT_MS = 60_000;
41
- /**
42
- * AnthropicClient implements LLMClient.
43
- *
44
- * 内部包一个 @anthropic-ai/sdk 实例, .messages.create() / .messages.stream()
45
- * 走 SDK 真实 HTTP 路径. 我们负责:
46
- * - ChatMessage → Anthropic.MessageParam 转换 (Sprint 1c 再加 tool_use schema)
47
- * - Anthropic.Message → ChatResult 转换 (parseAnthropicMessage)
48
- * - RawMessageStreamEvent → ChatChunk 转换 (parseAnthropicSseEvent, 含 usage 翻译)
49
- *
50
- * 不**在**这里写 HTTP / SSE 解析 (SDK 负责), 不**在**这里做重试 (SDK 自带 maxRetries=2,
51
- * 我们接受默认). Sprint 1c 集成测真接 shim 时可调整 SDK timeout / maxRetries.
52
- */
53
- export class AnthropicClient {
54
- model;
55
- apiKey;
56
- baseUrl;
57
- timeoutMs;
58
- pricing;
59
- sdk;
60
- constructor(options = {}) {
61
- this.apiKey = options.apiKey ?? resolveApiKey();
62
- if (this.apiKey === '') {
63
- throw new APIKeyMissingError('Anthropic API key not set. Set ANTHROPIC_AUTH_TOKEN or DEEPSEEK_API_KEY env var, ' +
64
- 'or pass apiKey option.');
65
- }
66
- this.model = (options.model ?? ANTHROPIC_DEFAULT_MODEL);
67
- this.baseUrl = options.baseUrl ?? DEEPSEEK_ANTHROPIC_BASE_URL;
68
- this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
69
- this.pricing = options.pricing ?? loadDefaultPricing();
70
- // SDK opts.fetch 注入是设计意图内的 escape hatch (测试 mock + Cloudflare
71
- // Workers / Deno proxy). 真实生产不传, SDK 走全局 fetch.
72
- const sdkOptions = {
73
- authToken: this.apiKey,
74
- baseURL: this.baseUrl,
75
- timeout: this.timeoutMs,
76
- };
77
- if (options.fetchImpl !== undefined)
78
- sdkOptions.fetch = options.fetchImpl;
79
- this.sdk = new Anthropic(sdkOptions);
80
- }
81
- async chat(messages, options) {
82
- // Sprint 1c.5 拍板 (1c-revive-2-B-1, 2026-06-04): tool schema 转换 (OAI {parameters} → Anthropic
83
- // {input_schema}), 跟 DeepSeekClient 同 LLMClient 契约 (5-7 行 production 改).
84
- // 跟 pi-agent 4-layer 模式: model layer (AnthropicClient) 不知道 tool registry 细节, 只做协议转换.
85
- const body = toAnthropicMessages(messages, options?.tools);
86
- const createParams = {
87
- model: this.model,
88
- messages: body.messages,
89
- max_tokens: 4096, // Anthropic API 必填, 4096 是合理默认 (Sprint 1c 让 caller 传)
90
- ...(body.system !== undefined ? { system: body.system } : {}),
91
- ...(body.tools !== undefined ? { tools: body.tools } : {}),
92
- ...(options?.tool_choice !== undefined ? { tool_choice: mapToolChoice(options.tool_choice) } : {}),
93
- };
94
- const sdkOptions = {
95
- ...(options?.signal !== undefined ? { signal: options.signal } : {}),
96
- };
97
- let response;
98
- try {
99
- response = await this.sdk.messages.create(createParams, sdkOptions);
100
- }
101
- catch (e) {
102
- throw mapSdkError(e);
103
- }
104
- return parseAnthropicMessage(response, this.model, this.pricing);
105
- }
106
- async stream(messages, options) {
107
- const body = toAnthropicMessages(messages, options.tools);
108
- const streamParams = {
109
- model: this.model,
110
- messages: body.messages,
111
- max_tokens: 4096,
112
- ...(body.system !== undefined ? { system: body.system } : {}),
113
- ...(body.tools !== undefined ? { tools: body.tools } : {}),
114
- ...(options.tool_choice !== undefined ? { tool_choice: mapToolChoice(options.tool_choice) } : {}),
115
- };
116
- const sdkOptions = {
117
- ...(options.signal !== undefined ? { signal: options.signal } : {}),
118
- };
119
- // SDK 的 messages.stream() 返 MessageStream (异步可迭代 + EventEmitter).
120
- // 跟 for await 一起用, 跟 OAI SSE 处理模式不同 — 这里是 SDK 自家 stream,
121
- // 不是 SSE 字节流. parseAnthropicSseEvent 负责把每个 RawMessageStreamEvent
122
- // 翻译成 ChatChunk.
123
- const stream = this.sdk.messages.stream(streamParams, sdkOptions);
124
- let finalMessage;
125
- for await (const event of stream) {
126
- const chunk = parseAnthropicSseEvent(event, this.model, this.pricing);
127
- if (chunk !== null) {
128
- options.onChunk(chunk);
129
- // 收到 message_stop 时, SDK 也提供 finalMessage(), 但我们靠 event 流本身
130
- // 拼出 stop_reason + 完整 usage. 留 1c 集成测时优化 (现 8 tests 覆盖 main path).
131
- }
132
- // SDK 的 message_stop event 不携带 final message, 需调 stream.finalMessage()
133
- // 才能拿到. 简化: 收完流后从 stream 实例读 final message.
134
- if (event.type === 'message_stop') {
135
- finalMessage = await stream.finalMessage();
136
- }
137
- }
138
- if (finalMessage === undefined) {
139
- throw new LLMUnknownError('Anthropic stream ended without message_stop event');
140
- }
141
- return parseAnthropicMessage(finalMessage, this.model, this.pricing);
142
- }
143
- }
144
- // ---- 私有 helper ----
145
- function resolveApiKey() {
146
- // 优先 ANTHROPIC_AUTH_TOKEN (Anthropic SDK 标准), 退到 DEEPSEEK_API_KEY (Sprint 1b.5 shim).
147
- const anthropic = process.env['ANTHROPIC_AUTH_TOKEN'];
148
- if (anthropic !== undefined && anthropic !== '')
149
- return anthropic;
150
- const deepseek = process.env['DEEPSEEK_API_KEY'];
151
- if (deepseek !== undefined && deepseek !== '')
152
- return deepseek;
153
- return '';
154
- }
155
- function loadDefaultPricing() {
156
- try {
157
- // 跟 DeepSeekClient 走同 pattern: readFileSync pricing.default.toml relative to dist/.
158
- // Sprint 1c 集成测时检查这条路径在 ESM 打包后是否仍 OK. Step 2 单测用 mock fetch,
159
- // 不走 loadDefaultPricing (test fixture 显式传 pricing).
160
- const here = dirname(fileURLToPath(import.meta.url));
161
- const defaultPath = resolve(here, 'pricing.default.toml');
162
- const tomlText = readFileSync(defaultPath, 'utf-8');
163
- return parsePricingConfig(tomlText);
164
- }
165
- catch {
166
- return undefined;
167
- }
168
- }
169
- function mapSdkError(e) {
170
- // SDK 错误分类: APIConnectionError / APIConnectionTimeoutError / RateLimitError /
171
- // AuthenticationError / BadRequestError / InternalServerError 等. 简化: 透传
172
- // SDK Error name + message 到 LLMUnknownError. Sprint 1c 集成测时细化 1:1 映射
173
- // 到 LLMRateLimitError / LLMAuthError / LLMNetworkError (跟 DeepSeekClient 一致).
174
- if (e instanceof Error)
175
- return new LLMUnknownError(`Anthropic SDK: ${e.message}`, { cause: e });
176
- return new LLMUnknownError(`Anthropic SDK: ${String(e)}`);
177
- }
178
- /** 拆分 system / 非 system messages (Anthropic 协议 system 是顶层字段). */
179
- function toAnthropicMessages(messages, tools) {
180
- const out = [];
181
- let system;
182
- // Sprint 1c-revive-2-D-4-1 (P38, 2026-06-04): 合并连续 tool 消息到 1 个 user 消息
183
- // (Anthropic 协议要求 N 个 tool_use 紧跟 1 个 user 消息含 N 个 tool_result blocks).
184
- // 1c.5 拍板时 1-tool-call 路径碰巧合法 (N=1 时独立 user 消息仍可), 多 tool_calls 揭示.
185
- let pendingToolResults;
186
- const flushToolResults = () => {
187
- if (pendingToolResults !== undefined && pendingToolResults.length > 0) {
188
- out.push({
189
- role: 'user',
190
- content: pendingToolResults,
191
- });
192
- }
193
- pendingToolResults = undefined;
194
- };
195
- for (const m of messages) {
196
- if (m.role === 'system') {
197
- // 多条 system 合并 (Anthropic system 是单 string, 重复 system 罕见)
198
- system = system === undefined ? m.content : `${system}\n\n${m.content}`;
199
- continue;
200
- }
201
- if (m.role === 'tool') {
202
- // 拍板 (D-4-1): 累积 tool_result 进 pendingToolResults (跟下一个 tool 消息合并).
203
- // flush 时机: 1) 遇到非 tool 角色, 2) loop 结束.
204
- if (pendingToolResults === undefined) {
205
- pendingToolResults = [];
206
- }
207
- pendingToolResults.push({
208
- type: 'tool_result',
209
- tool_use_id: m.tool_call_id ?? '',
210
- content: m.content,
211
- });
212
- continue;
213
- }
214
- // 非 tool 角色: 先 flush pending tool_results (如果有)
215
- flushToolResults();
216
- if (m.role === 'user') {
217
- out.push({ role: 'user', content: m.content });
218
- continue;
219
- }
220
- // assistant: OAI tool_calls → Anthropic content blocks (text + tool_use)
221
- if (m.role === 'assistant') {
222
- if (m.tool_calls !== undefined && m.tool_calls.length > 0) {
223
- const blocks = m.tool_calls.map((tc) => ({
224
- type: 'tool_use',
225
- id: tc.id,
226
- name: tc.name,
227
- input: tc.args,
228
- }));
229
- out.push({
230
- role: 'assistant',
231
- // ToolUseBlockParam[] 在 SDK 类型上是 ContentBlockParam[] 的子集, 但 TS 4.x 推断不到
232
- // (SDK 用 union 反推, 编译期会失配). 显式 cast: 真实运行时 server 接受.
233
- content: blocks,
234
- });
235
- continue;
236
- }
237
- out.push({ role: 'assistant', content: m.content });
238
- continue;
239
- }
240
- }
241
- // loop 结束: flush 最后一批 tool_results (避免末尾独立 tool 消息丢失)
242
- flushToolResults();
243
- // tool schema: OAI {name, description, parameters} → Anthropic {name, description, input_schema}
244
- // 1c.5 拍板: 走 Tool 类型 (跟 SDK 对齐), 不拆 ToolUnion (Bash20250124 等 built-in 工具暂不用).
245
- let anthropicTools;
246
- if (tools !== undefined && tools.length > 0) {
247
- anthropicTools = tools.map((t) => ({
248
- name: t.name,
249
- description: t.description,
250
- input_schema: t.parameters,
251
- }));
252
- }
253
- const out2 = { system, messages: out };
254
- if (anthropicTools !== undefined)
255
- out2.tools = anthropicTools;
256
- return out2;
257
- }
258
- /** Map LLMClient 通用 tool_choice (OAI 风格) → Anthropic ToolChoice. */
259
- function mapToolChoice(choice) {
260
- switch (choice) {
261
- case 'auto':
262
- return { type: 'auto' };
263
- case 'none':
264
- return { type: 'none' };
265
- case 'required':
266
- return { type: 'any' }; // Anthropic 协议 'any' 强制至少调 1 个, 跟 OAI 'required' 等价
267
- }
268
- }
269
- // ---- 解析层: Anthropic.Message / RawMessageStreamEvent → ChatResult / ChatChunk ----
270
- /**
271
- * 把 Anthropic SDK 的 Message 翻译成 ChatResult.
272
- *
273
- * - content: 拼 text block, 跳过 tool_use (1c 实施) / thinking (1b.5 不暴露)
274
- * - stop_reason → finish_reason 翻译
275
- * - usage: cache_creation + cache_read → cached_tokens (B1 拍板). 算 cache_hit_rate
276
- * + cost_turn + tokens_uncached 跟 OAI 路径一致.
277
- */
278
- export function parseAnthropicMessage(message, fallbackModel, pricing) {
279
- // 拼 text + 提取 tool_use (1c.5 实施, 1b.5 留空)
280
- let content = '';
281
- const toolCalls = [];
282
- for (const block of message.content) {
283
- if (block.type === 'text') {
284
- content = content === '' ? block.text : `${content}${block.text}`;
285
- }
286
- else if (block.type === 'tool_use') {
287
- // Sprint 1c.5 (1c-revive-2-B-1): tool_use block → OAI-style ToolCall (跟 DeepSeek shape 对齐)
288
- // Anthropic SDK 给 input: unknown, 我们假设是 parsed object (runToolLoop 给 args object)
289
- const input = block.input;
290
- const args = typeof input === 'object' && input !== null
291
- ? input
292
- : {};
293
- toolCalls.push({ id: block.id, name: block.name, args });
294
- }
295
- // thinking / redacted_thinking 跳过 (跟 1b.5 范围一致)
296
- }
297
- const finishReason = mapStopReason(message.stop_reason);
298
- const usage = parseAnthropicUsage(message.usage, fallbackModel, pricing);
299
- const model = message.model ?? fallbackModel;
300
- const result = { model, content };
301
- if (toolCalls.length > 0)
302
- result.tool_calls = toolCalls;
303
- if (usage !== undefined)
304
- result.usage = usage;
305
- if (finishReason !== undefined)
306
- result.finish_reason = finishReason;
307
- return result;
308
- }
309
- /** 翻译 Anthropic stop_reason → ChatResult['finish_reason']. */
310
- function mapStopReason(stopReason) {
311
- if (stopReason === null)
312
- return undefined;
313
- switch (stopReason) {
314
- case 'end_turn':
315
- return 'stop';
316
- case 'stop_sequence':
317
- return 'stop';
318
- case 'max_tokens':
319
- return 'length';
320
- case 'tool_use':
321
- return 'tool_calls';
322
- }
323
- }
324
- /**
325
- * 把 Anthropic Usage 翻译成标准化 Usage 结构 (带 cache/cost 字段).
326
- *
327
- * B1 拍板 (Sprint 1b.5 Step 2): cached_tokens = (cache_creation ?? 0) + (cache_read ?? 0).
328
- *
329
- * ⚠️ 重要语义修正 (F4 拍板 2026-06-03, review 找到):
330
- * Anthropic 官方 prompt caching 文档: total input tokens = input_tokens + cache_creation_input_tokens
331
- * + cache_read_input_tokens. 之前 Step 2 写时把 input_tokens 当"总 prompt" 是错的, 漏算 cache
332
- * 字段对应的 token 总量. 修法:
333
- *
334
- * - total_prompt = input_tokens + cache_creation + cache_read
335
- * - cached_tokens = cache_creation + cache_read (跟 Step 2 一致)
336
- * - tokens_uncached = total_prompt - cached_tokens = input_tokens (不变量)
337
- * - cache_hit_rate = cached_tokens / total_prompt (cache 命中率, 包括 write + read)
338
- *
339
- * **Cost 1b.5 保守策略** (F4 拍板): cache_creation 跟 cache_read 价格不同 (Sonnet 1h TTL write
340
- * \$3.75/M, read \$0.30/M, 比例 12.5×), 1b.5 pricing 模型**不**拆 cache_write vs cache_read 字段.
341
- * 为避免假装知道 cache_creation 价格, **保守**: cache_creation OR cache_read 任一非零 →
342
- * cost_turn/cost_currency 字段 absent. 留 sprint 2 加 `cache_write_per_m` 字段.
343
- * - 注意: tokens_uncached 仍**可**算 (是 input_tokens, 跟 cache 字段无关), 不受 cost 限制
344
- *
345
- * 接受 Usage | MessageDeltaUsage (后者无 cache_creation / cache_read, 视为 0).
346
- * Sprint 1b.5 Step 2: 流末尾 message_delta event.usage 是 MessageDeltaUsage, 走 delta 路径
347
- * 不算 cost, 真实生产靠 stream.finalMessage() 拿完整 Usage.
348
- */
349
- export function parseAnthropicUsage(usage, fallbackModel, pricing) {
350
- // MessageDeltaUsage (流末尾 message_delta 事件): 只有 output_tokens, 拿不到 input/cache.
351
- // → 视为 delta 增量, 不算完整 cost. 真实生产靠 stream.finalMessage() 拿完整 Usage.
352
- if (!('input_tokens' in usage)) {
353
- return {
354
- prompt_tokens: 0,
355
- completion_tokens: usage.output_tokens,
356
- total_tokens: usage.output_tokens,
357
- };
358
- }
359
- // 此后 usage 一定是 AnthropicUsage (有 input_tokens + cache_creation + cache_read)
360
- const full = usage;
361
- // F4 修正: total_prompt = input + cache_creation + cache_read (官方文档)
362
- const cacheCreation = full.cache_creation_input_tokens ?? 0;
363
- const cacheRead = full.cache_read_input_tokens ?? 0;
364
- const cached = cacheCreation + cacheRead;
365
- const totalPrompt = full.input_tokens + cached;
366
- const completion = full.output_tokens;
367
- const total = totalPrompt + completion;
368
- const out = {
369
- prompt_tokens: totalPrompt, // 跟 DeepSeek OAI shape 字段对齐 (prompt = 全部输入, 含 cache)
370
- completion_tokens: completion,
371
- total_tokens: total,
372
- };
373
- if (cached > 0)
374
- out.cached_tokens = cached;
375
- // tokens_uncached 仍算 (跟 cache 字段无关, 跟 computeCost 内部算的 uncached 一致)
376
- if (cached > 0)
377
- out.tokens_uncached = full.input_tokens; // = totalPrompt - cached
378
- // F4 保守: cache_creation OR cache_read 非零 → cost_turn 字段 absent. 留 sprint 2 加 cache_write_per_m 字段.
379
- // 1b.5 pricing 模型只有 cache_miss / cache_hit / completion, 不能区分 cache_creation 跟
380
- // cache_read 的不同价. 假装按 cache_hit 价算 cache_creation 会**低估** Sonnet 12.5×.
381
- if (cached === 0) {
382
- // 跟 parseOaiSseUsageField 一致, 透传 pricing + model 给 computeCost
383
- // cached=0 (LLM 显式说 0 cache hit) → 走完整 4 字段路径, 跟 OAI shape 对齐
384
- const breakdown = computeCost(pricing, fallbackModel, totalPrompt, completion, 0);
385
- if (breakdown !== undefined) {
386
- out.cache_hit_rate = breakdown.cache_hit_rate;
387
- if (breakdown.cost_turn !== undefined)
388
- out.cost_turn = breakdown.cost_turn;
389
- if (breakdown.cost_currency !== undefined)
390
- out.cost_currency = breakdown.cost_currency;
391
- out.tokens_uncached = breakdown.tokens_uncached;
392
- }
393
- }
394
- else {
395
- // cache_creation OR cache_read 非零: cost 字段 absent, 但 cache_hit_rate 仍算 (观测)
396
- out.cache_hit_rate = totalPrompt > 0 ? cached / totalPrompt : 0;
397
- }
398
- return out;
399
- }
400
- /**
401
- * 把单个 RawMessageStreamEvent 翻译成 ChatChunk.
402
- * 返 null 表示跳过 (heartbeat / ping / 不该透出的 event).
403
- *
404
- * Event 类型 (来自 SDK type):
405
- * - message_start: 携带 message metadata (model, id, usage.input_tokens=0) — 不透出
406
- * - content_block_start: 新的 text/tool_use/thinking block 起点 — 不透出 (避免 0 token 块)
407
- * - content_block_delta: text 增量 (delta.text) 或 input_json_delta (tool_use) — 透出 content
408
- * - content_block_stop: block 结束 — 不透出
409
- * - message_delta: 顶层 delta (stop_reason + usage 更新) — 透出 finish_reason + 最终 usage
410
- * - message_stop: 流结束 — 不透出 (caller 走 stream.finalMessage 拿 final Message)
411
- * - ping: heartbeat — 不透出
412
- */
413
- export function parseAnthropicSseEvent(event, fallbackModel, pricing) {
414
- switch (event.type) {
415
- case 'content_block_delta': {
416
- const delta = event.delta;
417
- if (delta.type === 'text_delta') {
418
- return { delta: { content: delta.text } };
419
- }
420
- // input_json_delta (tool_use) / thinking_delta / signature_delta: 1b.5 跳过
421
- return null;
422
- }
423
- case 'message_delta': {
424
- // 顶层 delta: stop_reason + usage. 这是流末尾的最终 usage 更新.
425
- const finishReason = mapStopReason(event.delta.stop_reason);
426
- const usage = parseAnthropicUsage(event.usage, fallbackModel, pricing);
427
- const chunk = { delta: {} };
428
- if (finishReason !== undefined)
429
- chunk.finish_reason = finishReason;
430
- if (usage !== undefined)
431
- chunk.usage = usage;
432
- // 1b.5 简化: 即使 chunk 是空 delta, 也透出 (caller 看 finish_reason 收尾)
433
- return chunk;
434
- }
435
- // 其他 event 类型 (start/stop) 不透出. ping event 不在 union (SDK 内部 filter)
436
- case 'message_start':
437
- case 'content_block_start':
438
- case 'content_block_stop':
439
- case 'message_stop':
440
- return null;
441
- }
442
- }
443
- //# sourceMappingURL=anthropic-client.js.map