@deepwhale/llm 1.0.0
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/dist/anthropic-client.d.ts +147 -0
- package/dist/anthropic-client.d.ts.map +1 -0
- package/dist/anthropic-client.js +443 -0
- package/dist/anthropic-client.js.map +1 -0
- package/dist/canonicalize-schema.d.ts +40 -0
- package/dist/canonicalize-schema.d.ts.map +1 -0
- package/dist/canonicalize-schema.js +93 -0
- package/dist/canonicalize-schema.js.map +1 -0
- package/dist/deepseek-client.d.ts +106 -0
- package/dist/deepseek-client.d.ts.map +1 -0
- package/dist/deepseek-client.js +395 -0
- package/dist/deepseek-client.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/parse.d.ts +53 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +285 -0
- package/dist/parse.js.map +1 -0
- package/dist/pricing-config.d.ts +104 -0
- package/dist/pricing-config.d.ts.map +1 -0
- package/dist/pricing-config.js +186 -0
- package/dist/pricing-config.js.map +1 -0
- package/dist/pricing.default.toml +46 -0
- package/dist/types.d.ts +255 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +70 -0
- package/dist/types.js.map +1 -0
- package/package.json +28 -0
- package/src/pricing.default.toml +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepwhale/llm — OpenAI 兼容 LLM 客户端
|
|
3
|
+
*
|
|
4
|
+
* Sprint 0.3 落地:
|
|
5
|
+
* - DeepSeek OpenAI 兼容 HTTP 客户端(chat/非流式/abort/timeout)
|
|
6
|
+
* - 4 种 LLMError 子类(APIKeyMissing / RateLimit / Auth / Network / Unknown)
|
|
7
|
+
* - fetch 注入 → 单测 100% mock
|
|
8
|
+
*
|
|
9
|
+
* Sprint 1a 扩展:流式 + tool_calls + retry/backoff + usage + 6 个 LLMError 子类
|
|
10
|
+
* Sprint 1b 再加:cache_hit_rate、canonical schema
|
|
11
|
+
* Sprint 1b.5: pricing config.toml 化 — CostBreakdown / computeCostBreakdown
|
|
12
|
+
* 从 types.js 整体迁移到 pricing-config.js (per-model currency, R7 缺失定价中间路径).
|
|
13
|
+
*/
|
|
14
|
+
export const DEEPWHALE_LLM_VERSION = '0.1.0';
|
|
15
|
+
export { DeepSeekClient, DEEPSEEK_BASE_URL, DEEPSEEK_DEFAULT_MODEL } from './deepseek-client.js';
|
|
16
|
+
// Sprint 1b.5 Step 2: Anthropic provider (走 /anthropic endpoint, DeepSeek shim)
|
|
17
|
+
// - D1 拍板: 用官方 @anthropic-ai/sdk 实例发请求, 手写 parseAnthropic* 翻译
|
|
18
|
+
// - X3 拍板: Step 2 不接真, 测试用 mock fetch
|
|
19
|
+
export { AnthropicClient, DEEPSEEK_ANTHROPIC_BASE_URL, ANTHROPIC_DEFAULT_MODEL } from './anthropic-client.js';
|
|
20
|
+
export { canonicalizeSchema } from './canonicalize-schema.js';
|
|
21
|
+
// Sprint 1b.5 Step 2: 抽出来给 AnthropicClient 复用. parseOai* 处理 OAI-shape 协议
|
|
22
|
+
// (DeepSeek 直接返 + DeepSeek shim 接 /anthropic 路径时也返 OAI-shape).
|
|
23
|
+
export { isSseDoneSentinel, parseOaiChatCompletion, parseSseEvent, parseSseUsageField } from './parse.js';
|
|
24
|
+
export { parsePricingConfig, loadPricingConfig, computeCost, PricingConfigParseError, } from './pricing-config.js';
|
|
25
|
+
export { isLLMError } from './types.js';
|
|
26
|
+
export { APIKeyMissingError, LLMRateLimitError, LLMAuthError, LLMNetworkError, LLMUnknownError, LLMStreamError, } from './types.js';
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEjG,gFAAgF;AAChF,8DAA8D;AAC9D,sCAAsC;AACtC,OAAO,EAAE,eAAe,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAE9G,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,yEAAyE;AACzE,+DAA+D;AAC/D,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC1G,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAoB7B,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC"}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepwhale/llm — OpenAI-shape 协议解析层
|
|
3
|
+
*
|
|
4
|
+
* Sprint 1b.5 Step 2 抽出来: 之前这些 module-level fn 在 deepseek-client.ts 里,
|
|
5
|
+
* Step 2 起 AnthropicClient (走 /anthropic endpoint, DeepSeek shim 返 OpenAI-shape)
|
|
6
|
+
* 也需要复用, 抽到 parse.ts 当共享解析层.
|
|
7
|
+
*
|
|
8
|
+
* 设计原则:
|
|
9
|
+
* - 纯函数, 零副作用 (无 console / 无 I/O)
|
|
10
|
+
* - pricing/model 都是 optional, 缺时走 R7 中间路径 (base 2 字段, cost 字段 absent)
|
|
11
|
+
* - 不假设 caller 是 deepseek / anthropic, 任何返 OpenAI-shape JSON 的协议都走这层
|
|
12
|
+
*
|
|
13
|
+
* 不在这层的:
|
|
14
|
+
* - SSE 协议层 (data:/event:/id:/retry: 协议) — 留在 deepseek-client.ts:isSseDoneSentinel
|
|
15
|
+
* - HTTP / fetch / retry — caller 负责
|
|
16
|
+
* - request 编组 (ChatMessage → wire JSON) — 留 caller (toWireMessage 是 deepseek 私货)
|
|
17
|
+
*/
|
|
18
|
+
import type { ChatResult, ChatChunk, ModelId, Usage } from './types.js';
|
|
19
|
+
import type { PricingConfig } from './pricing-config.js';
|
|
20
|
+
/**
|
|
21
|
+
* 解析 OAI chat completion 完整响应 JSON 为 ChatResult.
|
|
22
|
+
* 返 null 表示响应结构无效 (缺 choices[0].message), caller 决定抛什么错.
|
|
23
|
+
*
|
|
24
|
+
* @param fallbackModel - 响应里没 model 字段时用的 fallback (避免 "unknown model" 传播)
|
|
25
|
+
* @param pricing - 可选, 用于算 cost_turn + cost_currency. undefined 走 R7 中间路径.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseOaiChatCompletion(json: unknown, fallbackModel: ModelId, pricing?: PricingConfig): ChatResult | null;
|
|
28
|
+
/**
|
|
29
|
+
* 判断 SSE event raw text 是否是 OAI 协议的 [DONE] 终止 marker.
|
|
30
|
+
* 在 parseSseEvent 之前调用, 确保 [DONE] sentinel 不被算作 parse failure
|
|
31
|
+
* (P2-D follow-up: 之前会被静默归入 sseParseFailures++, 正常流也刷 warn).
|
|
32
|
+
*
|
|
33
|
+
* 容忍: data: [DONE] / data:[DONE] / 多个 data: 行 / 前后空白 / CRLF.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isSseDoneSentinel(eventRaw: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 解析单个 SSE event (`data: {...}\n`).
|
|
38
|
+
* 返回 null 表示跳过该 event (heartbeat / comment / 解析失败).
|
|
39
|
+
*
|
|
40
|
+
* 注意:[DONE] sentinel 由 isSseDoneSentinel 在调用方提前拦截,
|
|
41
|
+
* 本函数不再处理,避免重复路径让 caller 误算 parse failure.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseSseEvent(eventRaw: string, pricing?: PricingConfig, model?: ModelId): ChatChunk | null;
|
|
44
|
+
/**
|
|
45
|
+
* 从 SSE event JSON 顶层 usage 字段解析出标准化的 Usage 结构,
|
|
46
|
+
* 顺手算 Sprint 1b 的 cache_hit_rate / cost_turn / tokens_uncached.
|
|
47
|
+
*
|
|
48
|
+
* 返回 undefined 表示该 event 不带 usage (OAI 标准: 只在 stream 末尾出现).
|
|
49
|
+
*
|
|
50
|
+
* P1 fix (2026-06-03): 抽出避免在 choices=[] 和 choices=[...] 两条路径上重复解析。
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseSseUsageField(obj: Record<string, unknown>, pricing?: PricingConfig, model?: ModelId): Usage | undefined;
|
|
53
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAY,KAAK,EAAE,MAAM,YAAY,CAAC;AAElF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,OAAO,EACb,aAAa,EAAE,OAAO,EACtB,OAAO,CAAC,EAAE,aAAa,GACtB,UAAU,GAAG,IAAI,CAkFnB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAc3D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,aAAa,EACvB,KAAK,CAAC,EAAE,OAAO,GACd,SAAS,GAAG,IAAI,CA2FlB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,OAAO,CAAC,EAAE,aAAa,EACvB,KAAK,CAAC,EAAE,OAAO,GACd,KAAK,GAAG,SAAS,CA+BnB"}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepwhale/llm — OpenAI-shape 协议解析层
|
|
3
|
+
*
|
|
4
|
+
* Sprint 1b.5 Step 2 抽出来: 之前这些 module-level fn 在 deepseek-client.ts 里,
|
|
5
|
+
* Step 2 起 AnthropicClient (走 /anthropic endpoint, DeepSeek shim 返 OpenAI-shape)
|
|
6
|
+
* 也需要复用, 抽到 parse.ts 当共享解析层.
|
|
7
|
+
*
|
|
8
|
+
* 设计原则:
|
|
9
|
+
* - 纯函数, 零副作用 (无 console / 无 I/O)
|
|
10
|
+
* - pricing/model 都是 optional, 缺时走 R7 中间路径 (base 2 字段, cost 字段 absent)
|
|
11
|
+
* - 不假设 caller 是 deepseek / anthropic, 任何返 OpenAI-shape JSON 的协议都走这层
|
|
12
|
+
*
|
|
13
|
+
* 不在这层的:
|
|
14
|
+
* - SSE 协议层 (data:/event:/id:/retry: 协议) — 留在 deepseek-client.ts:isSseDoneSentinel
|
|
15
|
+
* - HTTP / fetch / retry — caller 负责
|
|
16
|
+
* - request 编组 (ChatMessage → wire JSON) — 留 caller (toWireMessage 是 deepseek 私货)
|
|
17
|
+
*/
|
|
18
|
+
import { computeCost } from './pricing-config.js';
|
|
19
|
+
/**
|
|
20
|
+
* 解析 OAI chat completion 完整响应 JSON 为 ChatResult.
|
|
21
|
+
* 返 null 表示响应结构无效 (缺 choices[0].message), caller 决定抛什么错.
|
|
22
|
+
*
|
|
23
|
+
* @param fallbackModel - 响应里没 model 字段时用的 fallback (避免 "unknown model" 传播)
|
|
24
|
+
* @param pricing - 可选, 用于算 cost_turn + cost_currency. undefined 走 R7 中间路径.
|
|
25
|
+
*/
|
|
26
|
+
export function parseOaiChatCompletion(json, fallbackModel, pricing) {
|
|
27
|
+
if (typeof json !== 'object' || json === null)
|
|
28
|
+
return null;
|
|
29
|
+
const obj = json;
|
|
30
|
+
const choices = obj['choices'];
|
|
31
|
+
if (!Array.isArray(choices) || choices.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const first = choices[0];
|
|
34
|
+
if (typeof first !== 'object' || first === null)
|
|
35
|
+
return null;
|
|
36
|
+
const firstObj = first;
|
|
37
|
+
const message = firstObj['message'];
|
|
38
|
+
if (typeof message !== 'object' || message === null)
|
|
39
|
+
return null;
|
|
40
|
+
const msg = message;
|
|
41
|
+
// 机制 3:reasoning_content 不暴露给 caller(session 内部如果要保留,sprint 1b 再加)
|
|
42
|
+
// 这里直接忽略 reasoning_content 字段,只取 content
|
|
43
|
+
const content = typeof msg['content'] === 'string' ? msg['content'] : '';
|
|
44
|
+
// 解析 tool_calls
|
|
45
|
+
let toolCalls;
|
|
46
|
+
const rawTc = msg['tool_calls'];
|
|
47
|
+
if (Array.isArray(rawTc) && rawTc.length > 0) {
|
|
48
|
+
toolCalls = [];
|
|
49
|
+
for (const tc of rawTc) {
|
|
50
|
+
if (typeof tc !== 'object' || tc === null)
|
|
51
|
+
continue;
|
|
52
|
+
const tcObj = tc;
|
|
53
|
+
const fn = tcObj['function'];
|
|
54
|
+
if (typeof fn !== 'object' || fn === null)
|
|
55
|
+
continue;
|
|
56
|
+
const fnObj = fn;
|
|
57
|
+
const name = typeof fnObj['name'] === 'string' ? fnObj['name'] : '';
|
|
58
|
+
const argsStr = typeof fnObj['arguments'] === 'string' ? fnObj['arguments'] : '{}';
|
|
59
|
+
let args = {};
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(argsStr);
|
|
62
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
63
|
+
args = parsed;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// args 解析失败,留空对象。caller 看到 args={} 通常意味着 LLM 输出格式错误。
|
|
68
|
+
}
|
|
69
|
+
const id = typeof tcObj['id'] === 'string' ? tcObj['id'] : '';
|
|
70
|
+
toolCalls.push({ id, name, args });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// usage
|
|
74
|
+
let usage;
|
|
75
|
+
const rawUsage = obj['usage'];
|
|
76
|
+
if (typeof rawUsage === 'object' && rawUsage !== null) {
|
|
77
|
+
const u = rawUsage;
|
|
78
|
+
const prompt = typeof u['prompt_tokens'] === 'number' ? u['prompt_tokens'] : 0;
|
|
79
|
+
const completion = typeof u['completion_tokens'] === 'number' ? u['completion_tokens'] : 0;
|
|
80
|
+
const total = typeof u['total_tokens'] === 'number' ? u['total_tokens'] : prompt + completion;
|
|
81
|
+
const cached = typeof u['prompt_cache_hit_tokens'] === 'number' ? u['prompt_cache_hit_tokens'] : undefined;
|
|
82
|
+
usage = { prompt_tokens: prompt, completion_tokens: completion, total_tokens: total };
|
|
83
|
+
if (cached !== undefined)
|
|
84
|
+
usage.cached_tokens = cached;
|
|
85
|
+
// Sprint 1b.5: 改用 pricing-config.computeCost, 接 PricingConfig + ModelId,
|
|
86
|
+
// 返 CostBreakdownResult (undefined / base 2 字段 / 完整 4 字段 3 种).
|
|
87
|
+
// 公式不变 (V4-Flash pricing 在 default.toml 跟旧 hardcode 一致).
|
|
88
|
+
// parseOaiChatCompletion 是 module-level, 从参数接 pricing + fallbackModel.
|
|
89
|
+
const breakdown = computeCost(pricing, fallbackModel, prompt, completion, cached);
|
|
90
|
+
if (breakdown !== undefined) {
|
|
91
|
+
usage.cache_hit_rate = breakdown.cache_hit_rate;
|
|
92
|
+
if (breakdown.cost_turn !== undefined)
|
|
93
|
+
usage.cost_turn = breakdown.cost_turn;
|
|
94
|
+
if (breakdown.cost_currency !== undefined)
|
|
95
|
+
usage.cost_currency = breakdown.cost_currency;
|
|
96
|
+
usage.tokens_uncached = breakdown.tokens_uncached;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// finish_reason
|
|
100
|
+
const rawFr = firstObj['finish_reason'];
|
|
101
|
+
const finishReason = rawFr === 'stop' || rawFr === 'tool_calls' || rawFr === 'length' || rawFr === 'content_filter'
|
|
102
|
+
? rawFr
|
|
103
|
+
: undefined;
|
|
104
|
+
const modelRaw = obj['model'];
|
|
105
|
+
const model = typeof modelRaw === 'string' ? modelRaw : fallbackModel;
|
|
106
|
+
const result = { model, content };
|
|
107
|
+
if (toolCalls)
|
|
108
|
+
result.tool_calls = toolCalls;
|
|
109
|
+
if (usage)
|
|
110
|
+
result.usage = usage;
|
|
111
|
+
if (finishReason)
|
|
112
|
+
result.finish_reason = finishReason;
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 判断 SSE event raw text 是否是 OAI 协议的 [DONE] 终止 marker.
|
|
117
|
+
* 在 parseSseEvent 之前调用, 确保 [DONE] sentinel 不被算作 parse failure
|
|
118
|
+
* (P2-D follow-up: 之前会被静默归入 sseParseFailures++, 正常流也刷 warn).
|
|
119
|
+
*
|
|
120
|
+
* 容忍: data: [DONE] / data:[DONE] / 多个 data: 行 / 前后空白 / CRLF.
|
|
121
|
+
*/
|
|
122
|
+
export function isSseDoneSentinel(eventRaw) {
|
|
123
|
+
// 扫所有 data: 行,看是否有且只有 [DONE]
|
|
124
|
+
let sawDataLine = false;
|
|
125
|
+
for (const ln of eventRaw.split('\n')) {
|
|
126
|
+
if (!ln.startsWith('data:'))
|
|
127
|
+
continue;
|
|
128
|
+
const payload = ln.slice(5).trimStart();
|
|
129
|
+
if (payload === '[DONE]') {
|
|
130
|
+
sawDataLine = true;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// [DONE] event 不应混入别的 data
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return sawDataLine;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 解析单个 SSE event (`data: {...}\n`).
|
|
141
|
+
* 返回 null 表示跳过该 event (heartbeat / comment / 解析失败).
|
|
142
|
+
*
|
|
143
|
+
* 注意:[DONE] sentinel 由 isSseDoneSentinel 在调用方提前拦截,
|
|
144
|
+
* 本函数不再处理,避免重复路径让 caller 误算 parse failure.
|
|
145
|
+
*/
|
|
146
|
+
export function parseSseEvent(eventRaw, pricing, model) {
|
|
147
|
+
const lines = eventRaw.split('\n');
|
|
148
|
+
const dataLines = [];
|
|
149
|
+
for (const ln of lines) {
|
|
150
|
+
if (ln.startsWith('data:')) {
|
|
151
|
+
dataLines.push(ln.slice(5).trimStart());
|
|
152
|
+
}
|
|
153
|
+
// event:/id:/retry: 忽略
|
|
154
|
+
}
|
|
155
|
+
if (dataLines.length === 0)
|
|
156
|
+
return null;
|
|
157
|
+
const data = dataLines.join('\n');
|
|
158
|
+
// [DONE] sentinel 已被 isSseDoneSentinel 提前拦截,这里不再判断。
|
|
159
|
+
let json;
|
|
160
|
+
try {
|
|
161
|
+
json = JSON.parse(data);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null; // 解析失败静默跳过(Sprint 1a 简化,Sprint 1b 加重试日志)
|
|
165
|
+
}
|
|
166
|
+
if (typeof json !== 'object' || json === null)
|
|
167
|
+
return null;
|
|
168
|
+
const obj = json;
|
|
169
|
+
// P1 fix (2026-06-03): 之前 usage 解析只在 choices=[] 分支走, 但 OAI/DeepSeek
|
|
170
|
+
// stream 协议允许最后一个 chunk 同时带 choices (e.g. {delta:{}, finish_reason:"stop"})
|
|
171
|
+
// 和顶层 usage, 此时 usage 会被丢弃 → 状态栏拿不到 cache/cost 数据。
|
|
172
|
+
// 改成先解析顶层 usage, 任何 chunk 类型都挂上。
|
|
173
|
+
// usage-only chunk (choices=[]) 路径保留作为 fallback, 服务端历史兼容。
|
|
174
|
+
// Sprint 1b.5: 把 pricing + model 透传给 parseSseUsageField.
|
|
175
|
+
const topLevelUsage = parseSseUsageField(obj, pricing, model);
|
|
176
|
+
const choices = obj['choices'];
|
|
177
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
178
|
+
if (topLevelUsage !== undefined) {
|
|
179
|
+
return { delta: {}, usage: topLevelUsage };
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const first = choices[0];
|
|
184
|
+
if (typeof first !== 'object' || first === null)
|
|
185
|
+
return null;
|
|
186
|
+
const firstObj = first;
|
|
187
|
+
const rawDelta = firstObj['delta'];
|
|
188
|
+
if (typeof rawDelta !== 'object' || rawDelta === null)
|
|
189
|
+
return null;
|
|
190
|
+
const deltaObj = rawDelta;
|
|
191
|
+
// 机制 3:reasoning_content 不暴露
|
|
192
|
+
const content = typeof deltaObj['content'] === 'string' ? deltaObj['content'] : undefined;
|
|
193
|
+
// tool_calls 增量(DeepSeek V4 stream 一次性给完整,这里当 full 处理)
|
|
194
|
+
let toolCalls;
|
|
195
|
+
const rawTc = deltaObj['tool_calls'];
|
|
196
|
+
if (Array.isArray(rawTc) && rawTc.length > 0) {
|
|
197
|
+
toolCalls = [];
|
|
198
|
+
for (const tc of rawTc) {
|
|
199
|
+
if (typeof tc !== 'object' || tc === null)
|
|
200
|
+
continue;
|
|
201
|
+
const tcObj = tc;
|
|
202
|
+
const fn = tcObj['function'];
|
|
203
|
+
if (typeof fn !== 'object' || fn === null)
|
|
204
|
+
continue;
|
|
205
|
+
const fnObj = fn;
|
|
206
|
+
const name = typeof fnObj['name'] === 'string' ? fnObj['name'] : '';
|
|
207
|
+
const argsStr = typeof fnObj['arguments'] === 'string' ? fnObj['arguments'] : '{}';
|
|
208
|
+
let args = {};
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(argsStr);
|
|
211
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
212
|
+
args = parsed;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
/* leave {} */
|
|
217
|
+
}
|
|
218
|
+
const id = typeof tcObj['id'] === 'string' ? tcObj['id'] : '';
|
|
219
|
+
const idx = typeof tcObj['index'] === 'number' ? tcObj['index'] : 0;
|
|
220
|
+
// Sprint 1a 简化:按 index 收集,最终输出合并
|
|
221
|
+
toolCalls.push({ id: id || `${idx}`, name, args });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// finish_reason
|
|
225
|
+
const rawFr = firstObj['finish_reason'];
|
|
226
|
+
const finishReason = rawFr === 'stop' || rawFr === 'tool_calls' || rawFr === 'length' || rawFr === 'content_filter'
|
|
227
|
+
? rawFr
|
|
228
|
+
: undefined;
|
|
229
|
+
const delta = {};
|
|
230
|
+
if (content !== undefined)
|
|
231
|
+
delta.content = content;
|
|
232
|
+
if (toolCalls !== undefined)
|
|
233
|
+
delta.tool_calls = toolCalls;
|
|
234
|
+
const chunk = { delta };
|
|
235
|
+
if (finishReason)
|
|
236
|
+
chunk.finish_reason = finishReason;
|
|
237
|
+
// P1 fix (2026-06-03): 顶层 usage 在 choices 路径同样挂上,
|
|
238
|
+
// 让 final chunk (带 finish_reason="stop" + 顶层 usage) 能把 usage 透出。
|
|
239
|
+
if (topLevelUsage !== undefined)
|
|
240
|
+
chunk.usage = topLevelUsage;
|
|
241
|
+
return chunk;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 从 SSE event JSON 顶层 usage 字段解析出标准化的 Usage 结构,
|
|
245
|
+
* 顺手算 Sprint 1b 的 cache_hit_rate / cost_turn / tokens_uncached.
|
|
246
|
+
*
|
|
247
|
+
* 返回 undefined 表示该 event 不带 usage (OAI 标准: 只在 stream 末尾出现).
|
|
248
|
+
*
|
|
249
|
+
* P1 fix (2026-06-03): 抽出避免在 choices=[] 和 choices=[...] 两条路径上重复解析。
|
|
250
|
+
*/
|
|
251
|
+
export function parseSseUsageField(obj, pricing, model) {
|
|
252
|
+
const rawUsage = obj['usage'];
|
|
253
|
+
if (typeof rawUsage !== 'object' || rawUsage === null)
|
|
254
|
+
return undefined;
|
|
255
|
+
const u = rawUsage;
|
|
256
|
+
const prompt = typeof u['prompt_tokens'] === 'number' ? u['prompt_tokens'] : 0;
|
|
257
|
+
const completion = typeof u['completion_tokens'] === 'number' ? u['completion_tokens'] : 0;
|
|
258
|
+
const total = typeof u['total_tokens'] === 'number' ? u['total_tokens'] : prompt + completion;
|
|
259
|
+
const cached = typeof u['prompt_cache_hit_tokens'] === 'number'
|
|
260
|
+
? u['prompt_cache_hit_tokens']
|
|
261
|
+
: undefined;
|
|
262
|
+
const usage = {
|
|
263
|
+
prompt_tokens: prompt,
|
|
264
|
+
completion_tokens: completion,
|
|
265
|
+
total_tokens: total,
|
|
266
|
+
};
|
|
267
|
+
if (cached !== undefined)
|
|
268
|
+
usage.cached_tokens = cached;
|
|
269
|
+
// Sprint 1b.5: 改用 pricing-config.computeCost, 公式不变 (default.toml V4-Flash).
|
|
270
|
+
// parseSseUsageField 是 module-level fn, 不持有 this.pricing. pricing/model 可选
|
|
271
|
+
// — undefined 时走 R7 中间路径 (base 2 字段, cost 字段 absent, 不静默 fallback).
|
|
272
|
+
// model undefined 时也安全: computeCost 内部 pricing.models[model] 返 undefined
|
|
273
|
+
// → 走 base 2 字段分支, cost 字段 absent. 不需要 throw.
|
|
274
|
+
const breakdown = computeCost(pricing, model, prompt, completion, cached);
|
|
275
|
+
if (breakdown !== undefined) {
|
|
276
|
+
usage.cache_hit_rate = breakdown.cache_hit_rate;
|
|
277
|
+
if (breakdown.cost_turn !== undefined)
|
|
278
|
+
usage.cost_turn = breakdown.cost_turn;
|
|
279
|
+
if (breakdown.cost_currency !== undefined)
|
|
280
|
+
usage.cost_currency = breakdown.cost_currency;
|
|
281
|
+
usage.tokens_uncached = breakdown.tokens_uncached;
|
|
282
|
+
}
|
|
283
|
+
return usage;
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,IAAa,EACb,aAAsB,EACtB,OAAuB;IAEvB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,GAAG,GAAG,IAA+B,CAAC;IAC5C,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACjE,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,QAAQ,GAAG,KAAgC,CAAC;IAClD,MAAM,OAAO,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjE,MAAM,GAAG,GAAG,OAAkC,CAAC;IAC/C,mEAAmE;IACnE,yCAAyC;IACzC,MAAM,OAAO,GAAG,OAAO,GAAG,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEzE,gBAAgB;IAChB,IAAI,SAAiC,CAAC;IACtC,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7C,SAAS,GAAG,EAAE,CAAC;QACf,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;YACvB,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,IAAI;gBAAE,SAAS;YACpD,MAAM,KAAK,GAAG,EAA6B,CAAC;YAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;YAC7B,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,IAAI;gBAAE,SAAS;YACpD,MAAM,KAAK,GAAG,EAA6B,CAAC;YAC5C,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACpE,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,WAAW,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACnF,IAAI,IAAI,GAA4B,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACnC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC5E,IAAI,GAAG,MAAiC,CAAC;gBAC3C,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,qDAAqD;YACvD,CAAC;YACD,MAAM,EAAE,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9D,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,QAAQ;IACR,IAAI,KAAwB,CAAC;IAC7B,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtD,MAAM,CAAC,GAAG,QAAmC,CAAC;QAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,eAAe,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/E,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,mBAAmB,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3F,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,cAAc,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,UAAU,CAAC;QAC9F,MAAM,MAAM,GACV,OAAO,CAAC,CAAC,yBAAyB,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9F,KAAK,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;QACtF,IAAI,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC;QACvD,yEAAyE;QACzE,+DAA+D;QAC/D,yDAAyD;QACzD,uEAAuE;QACvE,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;QAClF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,KAAK,CAAC,cAAc,GAAG,SAAS,CAAC,cAAc,CAAC;YAChD,IAAI,SAAS,CAAC,SAAS,KAAK,SAAS;gBAAE,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC;YAC7E,IAAI,SAAS,CAAC,aAAa,KAAK,SAAS;gBAAE,KAAK,CAAC,aAAa,GAAG,SAAS,CAAC,aAAa,CAAC;YACzF,KAAK,CAAC,eAAe,GAAG,SAAS,CAAC,eAAe,CAAC;QACpD,CAAC;IACH,CAAC;IAED,gBAAgB;IAChB,MAAM,KAAK,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,YAAY,GAChB,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,YAAY,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,gBAAgB;QAC5F,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,SAAS,CAAC;IAEhB,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,KAAK,GAAY,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAE,QAAoB,CAAC,CAAC,CAAC,aAAa,CAAC;IAE5F,MAAM,MAAM,GAAe,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAC9C,IAAI,SAAS;QAAE,MAAM,CAAC,UAAU,GAAG,SAAS,CAAC;IAC7C,IAAI,KAAK;QAAE,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC;IAChC,IAAI,YAAY;QAAE,MAAM,CAAC,aAAa,GAAG,YAAY,CAAC;IACtD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,6BAA6B;IAC7B,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,KAAK,MAAM,EAAE,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,SAAS;QACtC,MAAM,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;QACxC,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,WAAW,GAAG,IAAI,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,2BAA2B;YAC3B,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAgB,EAChB,OAAuB,EACvB,KAAe;IAEf,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,uBAAuB;IACzB,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,oDAAoD;IACpD,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,yCAAyC;IACxD,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,GAAG,GAAG,IAA+B,CAAC;IAE5C,oEAAoE;IACpE,4EAA4E;IAC5E,mDAAmD;IACnD,iCAAiC;IACjC,0DAA0D;IAC1D,yDAAyD;IACzD,MAAM,aAAa,GAAG,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAE9D,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;QAC7C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,QAAQ,GAAG,KAAgC,CAAC;IAClD,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnE,MAAM,QAAQ,GAAG,QAAmC,CAAC;IAErD,6BAA6B;IAC7B,MAAM,OAAO,GAAG,OAAO,QAAQ,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1F,uDAAuD;IACvD,IAAI,SAAiC,CAAC;IACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7C,SAAS,GAAG,EAAE,CAAC;QACf,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;YACvB,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,IAAI;gBAAE,SAAS;YACpD,MAAM,KAAK,GAAG,EAA6B,CAAC;YAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;YAC7B,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,IAAI;gBAAE,SAAS;YACpD,MAAM,KAAK,GAAG,EAA6B,CAAC;YAC5C,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACpE,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,WAAW,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACnF,IAAI,IAAI,GAA4B,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACnC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC5E,IAAI,GAAG,MAAiC,CAAC;gBAC3C,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,cAAc;YAChB,CAAC;YACD,MAAM,EAAE,GAAG,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9D,MAAM,GAAG,GAAG,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACpE,iCAAiC;YACjC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,GAAG,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,gBAAgB;IAChB,MAAM,KAAK,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,YAAY,GAChB,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,YAAY,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,gBAAgB;QAC5F,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,SAAS,CAAC;IAEhB,MAAM,KAAK,GAA2D,EAAE,CAAC;IACzE,IAAI,OAAO,KAAK,SAAS;QAAE,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;IACnD,IAAI,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC;IAC1D,MAAM,KAAK,GAAc,EAAE,KAAK,EAAE,CAAC;IACnC,IAAI,YAAY;QAAE,KAAK,CAAC,aAAa,GAAG,YAAY,CAAC;IACrD,kDAAkD;IAClD,iEAAiE;IACjE,IAAI,aAAa,KAAK,SAAS;QAAE,KAAK,CAAC,KAAK,GAAG,aAAa,CAAC;IAC7D,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAA4B,EAC5B,OAAuB,EACvB,KAAe;IAEf,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACxE,MAAM,CAAC,GAAG,QAAmC,CAAC;IAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,eAAe,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,mBAAmB,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3F,MAAM,KAAK,GACT,OAAO,CAAC,CAAC,cAAc,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,UAAU,CAAC;IAClF,MAAM,MAAM,GACV,OAAO,CAAC,CAAC,yBAAyB,CAAC,KAAK,QAAQ;QAC9C,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC;QAC9B,CAAC,CAAC,SAAS,CAAC;IAChB,MAAM,KAAK,GAAU;QACnB,aAAa,EAAE,MAAM;QACrB,iBAAiB,EAAE,UAAU;QAC7B,YAAY,EAAE,KAAK;KACpB,CAAC;IACF,IAAI,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC;IACvD,4EAA4E;IAC5E,2EAA2E;IAC3E,oEAAoE;IACpE,yEAAyE;IACzE,8CAA8C;IAC9C,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAC1E,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,KAAK,CAAC,cAAc,GAAG,SAAS,CAAC,cAAc,CAAC;QAChD,IAAI,SAAS,CAAC,SAAS,KAAK,SAAS;YAAE,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC;QAC7E,IAAI,SAAS,CAAC,aAAa,KAAK,SAAS;YAAE,KAAK,CAAC,aAAa,GAAG,SAAS,CAAC,aAAa,CAAC;QACzF,KAAK,CAAC,eAAe,GAAG,SAAS,CAAC,eAAe,CAAC;IACpD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepwhale/llm — Pricing 配置 (Sprint 1b.5 Step 1)
|
|
3
|
+
*
|
|
4
|
+
* 把原本 hardcode 在 `types.ts:computeCostBreakdown` 的 V4-Flash pricing 抽到
|
|
5
|
+
* ship-in + 用户可覆盖的 TOML 配置。Per-model currency (DeepSeek CNY, Anthropic USD),
|
|
6
|
+
* 不在 UI 层做汇率换算 — UI 从 `cost_currency` 字段读 symbol (¥ / $)。
|
|
7
|
+
*
|
|
8
|
+
* ## 设计原则 (R7 / Step 0 拍板, 2026-06-03)
|
|
9
|
+
*
|
|
10
|
+
* 1. `loadPricingConfig(userPath)`:
|
|
11
|
+
* - 显式传 `userPath` 但文件不存在或非法 → **抛 PricingConfigParseError** (不静默)
|
|
12
|
+
* - 不传 → 加载 ship-in `pricing.default.toml`
|
|
13
|
+
*
|
|
14
|
+
* 2. `computeCost(pricing, model, ...)` 是**纯函数**: 无 console / 无 logger / 无 IO。
|
|
15
|
+
* 3 种返回路径 (`CostBreakdownResult` 联合类型):
|
|
16
|
+
* - 整体 `undefined`: 没 `cached_tokens` → 4 字段全 absent
|
|
17
|
+
* - `{ base, no cost }`: 有 cache 但 pricing 找不到 model → 有 cache_hit_rate / tokens_uncached, 缺 cost
|
|
18
|
+
* - 完整 4 字段: 找到
|
|
19
|
+
*
|
|
20
|
+
* 3. Warning 责任在 caller (client/mode 边界), 纯函数零副作用。
|
|
21
|
+
*
|
|
22
|
+
* 4. UI/RPC: `cost_turn` absent 时字段不显示 (absent 不是 null), 读 `cost_currency` 决 symbol。
|
|
23
|
+
*/
|
|
24
|
+
import type { ModelId } from './types.js';
|
|
25
|
+
/**
|
|
26
|
+
* 单个模型的定价。Currency 跟价格数字绑定, UI 层不做汇率换算。
|
|
27
|
+
*/
|
|
28
|
+
export interface ModelPricing {
|
|
29
|
+
/** 缓存未命中 input 价格, /M tokens. */
|
|
30
|
+
cache_miss_per_m: number;
|
|
31
|
+
/** 缓存命中 input 价格, /M tokens. */
|
|
32
|
+
cache_hit_per_m: number;
|
|
33
|
+
/** 输出价格, /M tokens. */
|
|
34
|
+
completion_per_m: number;
|
|
35
|
+
/** Currency 跟价格数字绑定, 不在 UI 层做汇率换算. */
|
|
36
|
+
currency: 'CNY' | 'USD';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 完整的 pricing 配置 (TOML 解析后).
|
|
40
|
+
*
|
|
41
|
+
* `models` 必须非空. 找不到 model 时**不静默 fallback** (见 R7).
|
|
42
|
+
*/
|
|
43
|
+
export interface PricingConfig {
|
|
44
|
+
models: Record<string, ModelPricing>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `computeCost` 的 3 种返回结果 (联合类型).
|
|
48
|
+
*
|
|
49
|
+
* - `undefined`: 没 `cached_tokens` (Provider 也不支持 cache) → 4 字段全 absent
|
|
50
|
+
* - `{ base, no cost }`: 有 cache 但 pricing 找不到 model 或 pricing 本身 undefined
|
|
51
|
+
* → 有 cache_hit_rate + tokens_uncached, 缺 cost_turn + cost_currency
|
|
52
|
+
* - 完整 4 字段: 找到 model → cost_turn 和 cost_currency 同步存在 (不变量)
|
|
53
|
+
*/
|
|
54
|
+
export type CostBreakdownResult = undefined | {
|
|
55
|
+
cache_hit_rate: number;
|
|
56
|
+
tokens_uncached: number;
|
|
57
|
+
/** cost_turn 存在时 cost_currency 必须也存在 (类型不变量). */
|
|
58
|
+
cost_turn?: number;
|
|
59
|
+
cost_currency?: 'CNY' | 'USD';
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* pricing TOML 解析/加载错误. `userPath` 显式传但文件不存在/非法时抛.
|
|
63
|
+
*/
|
|
64
|
+
export declare class PricingConfigParseError extends Error {
|
|
65
|
+
readonly userPath: string;
|
|
66
|
+
readonly name: "PricingConfigParseError";
|
|
67
|
+
constructor(message: string, userPath: string, options?: {
|
|
68
|
+
cause?: unknown;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 解析 TOML string → PricingConfig. 纯函数, 无 IO.
|
|
73
|
+
*
|
|
74
|
+
* 校验:
|
|
75
|
+
* - 必须有 `[models.xxx]` 块 (至少 1 个)
|
|
76
|
+
* - 每个 model 4 字段全必填 (cache_miss_per_m / cache_hit_per_m / completion_per_m / currency)
|
|
77
|
+
* - currency 必须在 'CNY' | 'USD' enum
|
|
78
|
+
* - 价格数字必须非负有限
|
|
79
|
+
*
|
|
80
|
+
* @throws PricingConfigParseError — malformed / 缺字段 / currency 错 / 数字非有限
|
|
81
|
+
*/
|
|
82
|
+
export declare function parsePricingConfig(toml: string): PricingConfig;
|
|
83
|
+
/**
|
|
84
|
+
* 纯函数, 无 console / 无 logger / 无 IO. 3 种返回路径见 `CostBreakdownResult`.
|
|
85
|
+
*
|
|
86
|
+
* 公式: tokens_uncached = max(0, prompt - cached)
|
|
87
|
+
* cache_hit_rate = cached / prompt (prompt=0 时 0, 避免除零)
|
|
88
|
+
* cost_turn = tokens_uncached * cache_miss_per_m/1e6
|
|
89
|
+
* + cached * cache_hit_per_m/1e6
|
|
90
|
+
* + completion * completion_per_m/1e6
|
|
91
|
+
*
|
|
92
|
+
* 把 /M 转 /token 必须除 1e6, 不要直接当 /token 用 (会大 1000×).
|
|
93
|
+
*/
|
|
94
|
+
export declare function computeCost(pricing: PricingConfig | undefined, model: ModelId | undefined, promptTokens: number, completionTokens: number, cachedTokens: number | undefined): CostBreakdownResult;
|
|
95
|
+
/**
|
|
96
|
+
* 加载 pricing config. 三种来源优先级 (高→低):
|
|
97
|
+
* 1. `userPath` 显式指定且存在 → 解析用户文件
|
|
98
|
+
* 2. `userPath` 显式指定但不存在或非法 → **抛 PricingConfigParseError** (不静默 fallback)
|
|
99
|
+
* 3. `userPath` 未传 → 加载 ship-in `pricing.default.toml`
|
|
100
|
+
*
|
|
101
|
+
* hot path 不应调 (走 client.pricing 字段). Step 2 的 AnthropicClient 启动时调一次.
|
|
102
|
+
*/
|
|
103
|
+
export declare function loadPricingConfig(userPath?: string): Promise<PricingConfig>;
|
|
104
|
+
//# sourceMappingURL=pricing-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing-config.d.ts","sourceRoot":"","sources":["../src/pricing-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,gCAAgC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,QAAQ,EAAE,KAAK,GAAG,KAAK,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT;IACE,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CAC/B,CAAC;AAEN;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAI9C,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAH3B,SAAkB,IAAI,EAAG,yBAAyB,CAAU;gBAE1D,OAAO,EAAE,MAAM,EACN,QAAQ,EAAE,MAAM,EACzB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAOhC;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAmE9D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,KAAK,EAAE,OAAO,GAAG,SAAS,EAC1B,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,mBAAmB,CA6BrB;AAID;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CASjF"}
|