index.js
9.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* @drama/llm —— 多厂商 LLM 网关公共面(唯一对外入口)。
*
* 只导出契约 §3 声明的符号:createLLM / createStaticKeyProvider / LLMError / registeredProviders。
* 内部实现(base/providers/ports/retry/map-error)不外泄;深导入由 ESLint 拦截。
*
* 设计红线(见 docs/architecture/README.md §2):包内不读 process.env、不建顶层连接、不写盘;
* apiKey/baseUrl 经注入的 keyProvider 提供;成本、审计、结算端口由 composition root 成组注入。
*
* **无假 provider、无 fetch 注入缝**:网关只会对着真实 socket 说话。E2E 打的是 `.env` 里的真厂商
* (`pnpm llm:e2e`),不是本地假上游——假上游一旦存在,"代表性的那条成功调用"就会被它顶掉。
*/
export { createLLM, createStaticKeyProvider } from './llm.js'
export { LLMError } from './errors.js'
// 适配器名单是网关的知识:`model_provider_capability.adapter` 的合法取值即注册表键集。装配 / seed 侧据此在
// 启动期校验配置(未注册的 adapter 不得起服务),不再各自抄一份「厂商名 → 协议」的猜测映射。
export { registeredProviders } from './registry.js'
/* ===== 端口与数据形态契约(JSDoc @typedef,checkJs 友好,0 个 .ts 文件) ===== */
/**
* KeyProvider 端口(由 @drama/keyrouter 或 createStaticKeyProvider 提供,包内只调)。
* @typedef {object} KeyProvider
* @property {(q:{ vendor:string, model?:string, affinity_key?:string, lease_ttl_ms?:number }) => Promise<KeyLease>} acquire
* @property {(key_id:string, usage:object) => void} report_usage
* @property {(key_id:string, retry_after_ms?:number) => void} report_rate_limited
* @property {(key_id:string, kind:string) => void} report_error
*/
/**
* @typedef {object} KeyLease
* @property {string} api_key
* @property {string|null} account_id
* @property {string|null} base_url
* @property {string|null} key_id
* 网关只把它原样透传给 keyProvider 的回报方法与 AuditPort,不解释它的来源。
* @property {() => void} release 释放并发槽;务必 finally 调用。
*/
/**
* @typedef {object} ModelCostPort
* @property {(input:{provider:string, model:string, usage:object}) => Promise<{pricing_snapshot:object, supplier_cost_micros:number, currency:'CNY', breakdown:object}>} calculate
*/
/**
* @typedef {object} BillingPort
* @property {(input:{call_id:string,user_id?:string, call_ref:{type:string,id:string}, supplier_cost:{supplier_cost_micros:number,currency:'CNY'}, policy:object, idempotency_key:string}) => Promise<{billing_mode:string,billing_policy_snapshot:object,charged_points:number,ledger_id:string|null}>} settle
*/
/**
* @typedef {object} NotifyPort
* @property {(event:object, ctx?:object) => Promise<void>} notify
*/
/**
* AuditPort 端口(由 @drama/llm-audit 提供)。失败记录异步入队;供应商成功事实与结算结果必须 await 落库。
* @typedef {object} AuditPort
* @property {(entry: AuditEntry) => void} record
* @property {(entry: AuditEntry) => Promise<void>} recordProviderSuccess
*/
/**
* 一次 chat 的审计条目——在 chat 出口一次性拼齐的完整单一形态(含 null 显式化)。
* 三出口(成功 / 失败 / abort)都拼齐全部字段:**失败也有 messages 与 key_id**——认证失败正是最需要
* 知道"用户问了什么、哪把 key 挂了"的时候。
* @typedef {object} AuditEntry
* @property {string} provider
* @property {string} model
* @property {'ok'|'error'|'aborted'} status
* @property {number|null} ttft_ms
* @property {number} duration_ms
* @property {ChatUsage} usage
* @property {string} request_id
* @property {string} billing_operation_id 业务操作幂等标识;同一业务重试保持不变,和 request_id 分离。
* @property {string|null} session_id
* @property {string|null} user_id 计费主体(← billing_ctx.user_id);缺省 null。
* @property {string|null} provider_request_id
* @property {string|null} service_tier
* @property {object|null} pricing_snapshot
* @property {number|null} supplier_cost_micros
* @property {'CNY'|null} currency
* @property {null} billing_mode provider 成功落库时尚未结算,固定为 null。
* @property {null} billing_policy_snapshot provider 成功落库时尚未结算,固定为 null。
* @property {null} charged_points
* @property {null} ledger_id
* @property {string|null} error_reason 映射后的 LLMError.reason(成功为 null)。
* @property {string|null} error_message 可读错误消息(成功为 null)。
* @property {Record<string, any>|null} error_detail 第三方边界构造的结构化错误详情(成功为 null)。
* @property {Date} created_at
* @property {ChatMessage[]} messages 发出去的消息(valibot 校验后的请求),三出口都有。
* @property {string|null} response_text 回来的正文(← ChatResult.content);失败/abort 为 null。
* @property {string|null} reasoning_text 回来的推理;无推理或失败为 null。
* @property {string|null} key_id 服务这次调用的 key(← KeyLease.key_id = llm_key.id);static provider 为 null。
* @property {string|null} mode 场景名(← ChatOpts.mode,由 mode-router 透传);直接 chat 时 null。
* @property {string|null} correlation_id 同一次 forMode 调用的所有 attempt 共用(← ChatOpts.correlation_id)。
* @property {number|null} attempt_index fallback 链上第几步,0 = 主力(← ChatOpts.attempt_index)。
*/
/**
* @typedef {object} Span
* @property {(k:string, v:any) => void} setAttr
* @property {(n:string, d?:object) => void} addEvent
* @property {() => void} end
*/
/**
* @typedef {object} TracePort
* @property {(name:string) => Span} startSpan
*/
/**
* @typedef {object} ChatMessage
* @property {'system'|'user'|'assistant'|'tool'} role
* @property {any} content
* @property {boolean} [prompt_cache_breakpoint] OpenAI Responses 静态前缀终点。
*/
/**
* @typedef {object} ChatRequest
* @property {ChatMessage[]} messages
* @property {string} [model]
* @property {number} [temperature]
* @property {number} [max_tokens]
* @property {{ type:'json_object' } | { type?:'json_schema', name?:string, schema:object }} [response_format]
* 网关规范化 structured output:两种归一形态——json_object(要求合法 JSON,无 schema)
* 与 json_schema(带 schema,name 可选)。入口一次归一,各 provider 按支持度映射/拒绝。
* @property {Array<object>} [tools]
* @property {object} [thinking]
* @property {Array<Buffer|string>} [images]
* @property {Array<Buffer|string>} [videos]
* @property {{ mode:'implicit'|'explicit', ttl?:'30m' }} [prompt_cache_options]
*/
/**
* @typedef {object} ChatOpts
* @property {boolean} [cache] 请求级 prompt-caching(支持的厂商生效,否则忽略)。
* @property {number} [timeout_ms] 每次 attempt 独立计时的内部超时毫秒(timeout 可重试)。
* @property {(chunk:string) => void} [on_content]
* @property {(chunk:string) => void} [on_thinking]
* @property {(calls:Array<object>) => void} [on_tool_call]
* @property {(frame:object) => void} [on_raw_frame] 真实上游 SSE JSON 帧;诊断脚本用于核对协议解析。
* @property {AbortSignal} [signal]
* @property {{ user_id?:string, idempotency_key?:string, policy?:object }} [billing_ctx]
* `policy` 由 composition root 注入;付费模式下业务方提供 `user_id`,需要跨请求防重时提供稳定的
* `idempotency_key`。provider、model 和 request_id 均由网关自己的调用事实确定。
* @property {{ session_id?:string, [k:string]:any }} [trace_ctx]
* @property {string} [prompt_cache_key] OpenAI Prompt Caching 路由键;与 trace session 和 key affinity 独立。
* @property {string} [mode] 场景名(`mode_model_config.mode`),由 `@drama/llm-router` 的 mode-router 透传;原样落审计。
* @property {string} [correlation_id] 一次 forMode 调用的所有 attempt 共用的 id,由 `withFallback` 生成;原样落审计。
* @property {number} [attempt_index] fallback 链上第几步(0 = 主力),由 `withFallback` 给出;原样落审计。
* @property {(key_id: string|null) => void} [on_key_lease]
* **网关内部注入,调用方不要设置(设了也会被覆盖)**:base 每次 attempt acquire 到 key 后回调,
* 把 `KeyLease.key_id` 交回 createLLM 出口写进 AuditEntry.key_id。
*/
/**
* @typedef {object} ChatUsage
* @property {number} prompt_tokens
* @property {number} completion_tokens
* @property {number} total_tokens
* @property {number} cached_tokens
* @property {number} cache_write_tokens
* @property {number} reasoning_tokens
*/
/**
* @typedef {object} ChatResult
* @property {string} content
* @property {string} [reasoning]
* @property {Array<object>} [tool_calls]
* @property {ChatUsage} usage
* @property {string} provider
* @property {string} model
* @property {{ ttft_ms:number|null, duration_ms:number }} [timings] 成功路径的计时(透传自网关)。
* @property {string} [request_id] 本次真实厂商调用的全局唯一标识(= llm_call_log.request_id),对账/回查用。
* @property {string} [billing_operation_id] 稳定业务操作标识;同一业务重试保持不变。
* @property {ChatUsage & {kind:'token'}} metered_usage
* @property {string|null} provider_request_id
* @property {string|null} service_tier
* @property {{ call_ref:{type:string,id:string}, usage:object, pricing_snapshot:object, supplier_cost_micros:number, currency:'CNY', billing_mode:string, charged_points:number, ledger_id:string|null }} [accounting]
*/
/**
* @typedef {object} Llm
* @property {(req:ChatRequest, opts?:ChatOpts) => Promise<ChatResult>} chat
*/