/** * @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} 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} notify */ /** * AuditPort 端口(由 @drama/llm-audit 提供)。失败记录异步入队;供应商成功事实与结算结果必须 await 落库。 * @typedef {object} AuditPort * @property {(entry: AuditEntry) => void} record * @property {(entry: AuditEntry) => Promise} 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|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} [tools] * @property {object} [thinking] * @property {Array} [images] * @property {Array} [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) => 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} [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} chat */