# @dm-pkg/xllm — 面向大模型 / Agent 的使用说明 本文档供 **大模型、代码助手、自动化 Agent** 在编写或修改调用代码时作为单一事实来源。API 以当前包导出为准(入口:`packages/xllm/src/index.ts`)。 --- ## 1. 包与运行时 - **包名**:`@dm-pkg/xllm` - **模块**:ESM(`"type": "module"`) - **运行环境**:需存在全局 `fetch`(现代 Node、浏览器、Bun 等)。无 `fetch` 时抛 `XllmError`(`NETWORK_ERROR`)。 - **安装(monorepo)**:在依赖包中 `bun add @dm-pkg/xllm@workspace:*` 或等价 workspace 引用。 --- ## 2. 入口与导出 ### 2.1 运行时导出 ```ts import { createXllm, XllmError, registerAdapter, getRegisteredProviderNames } from "@dm-pkg/xllm"; ``` ### 2.2 类型导出(`export type`) `XClientOptions`, `XRequest`, `XResponse`, `XStreamEvent`, `XMessage`, `XContentPart`, `XProviderName`, `XThinkingMode`, `XReasoningEffortInput`, `XToolDefinition`, `XToolCall`, `XToolChoice`, `XToolExecutor`, `XToolExecutorMap`, `XToolErrorStrategy`, `XChatWithToolsOptions`, `XChatWithToolsResult`, `XUsage`, `ProviderAdapter`, `ProviderHttpRequest`, `StreamState`, `SSEFrame` > **注意**:`XThinkingMode` 与 `XReasoningEffortInput` 仍作为类型导出供参考,但不再是 `XRequest` 的直接字段——请通过 `providerExtras` 传入(见 §5.1)。 --- ## 3. 客户端:`createXllm` 与 `XllmClient` ```ts const xllm = createXllm(options?: XClientOptions): XllmClient; ``` ### 3.1 `XllmClient` 方法 | 方法 | 签名 | 用途 | |------|------|------| | `generate` | `(request: XRequest) => Promise` | 非流式,一次返回完整结构 | | `stream` | `(request: XRequest) => AsyncIterable` | 流式 SSE,用 `for await` 消费 | | `chatWithTools` | `(request: XChatWithToolsOptions, executors: XToolExecutorMap) => Promise` | 非流式 + 自动工具闭环 | | `streamWithTools` | 同上,返回 `AsyncIterable` | 流式 + 自动工具闭环 | | `with` | `(overrides: Partial) => XllmClient` | 复制客户端并合并默认配置 | --- ## 4. 配置合并与环境变量 ### 4.1 优先级(从高到低) 1. **单次请求** `XRequest` / `XChatWithToolsOptions` 上的字段(`provider`, `model`, `apiKey`, `baseURL` 等) 2. **`createXllm` 传入的 `XClientOptions`** 3. **环境变量**(仅 Node 等存在 `process.env` 时) 实现位置:`packages/xllm/src/runtime/config.ts`。 ### 4.2 环境变量 | 变量 | 作用 | |------|------| | `XLLM_API_KEY` | 通用 API Key 兜底 | | `OPENAI_API_KEY` | `provider === "openai-compatible"` 时兜底 | | `DEEPSEEK_API_KEY` | `provider === "deepseek"` 时兜底 | | `XLLM_BASE_URL` | 通用 base URL 兜底 | ### 4.3 默认 `provider` / `model` / `baseURL` 未指定时: - `provider` 默认为 `"openai-compatible"` - `model`:`openai-compatible` → `gpt-4o-mini`,`deepseek` → `deepseek-chat` - `baseURL`:`https://api.openai.com/v1` 或 `https://api.deepseek.com` --- ## 5. 供应商(`XProviderName`) `XProviderName` 的类型为 `string`(不再为字面量联合),以支持通过 `registerAdapter()` 动态注册任意供应商。 当前内置: - `"openai-compatible"`:OpenAI Chat Completions 兼容 HTTP 形态(`/chat/completions`) - `"deepseek"`:在兼容层之上设置 DeepSeek 默认 `baseURL`,其余映射与 openai-compatible 一致 **接入其它「OpenAI 兼容」网关**:不必新增枚举;使用 `provider: "openai-compatible"`,并设置 `baseURL`(及可选 `headers`)指向该网关。 **非兼容协议**:可通过 `registerAdapter()` 在运行时注册自定义 `ProviderAdapter`(见 §11),无需修改库源码。 ### 5.0 运行时注册 API ```ts import { registerAdapter, getRegisteredProviderNames } from "@dm-pkg/xllm"; ``` | 函数 | 签名 | 用途 | |------|------|------| | `registerAdapter` | `(adapter: ProviderAdapter) => void` | 注册或覆盖一个供应商适配器;后续 `generate`/`stream` 等调用即可使用该 `adapter.name` 作为 `provider` | | `getRegisteredProviderNames` | `() => string[]` | 返回当前已注册的所有供应商名称(含内置) | **示例**: ```ts import { registerAdapter, createXllm } from "@dm-pkg/xllm"; import { myCustomAdapter } from "./my-custom-adapter"; registerAdapter(myCustomAdapter); const xllm = createXllm({ provider: myCustomAdapter.name }); const res = await xllm.generate({ messages: [...] }); ``` --- ## 5.1 思考模式(Thinking / CoT)— **非全供应商通用** `thinking` / `reasoning_effort` **不是**抽象意义上的「全市场通用配置」:它们对应 **OpenAI Chat Completions 兼容 JSON** 里部分厂商采用的字段名(DeepSeek 官方文档中的 OpenAI 形态即如此)。**其它供应商若使用不同字段名、不同嵌套路径或不同 API**,有两种做法: 1. **`providerExtras`**(推荐先做):在 `XRequest.providerExtras` 里写入任意键值,库会在构建完标准字段后 **`Object.assign` 合并进请求体根对象**(**后合并者可覆盖**同名标准字段)。用于传入厂商文档要求的专有参数,而无需立刻新增适配器。 2. **新 `ProviderAdapter`**:协议差异大(路径、鉴权、流式格式都不同)时,通过 `registerAdapter()` 注册自定义适配器(见 §11),无需修改库源码。 > **迁移说明**:`thinking` 和 `reasoningEffort` **不再是 `XRequest` 的直接字段**。请改用 `providerExtras` 传入。`XThinkingMode` 和 `XReasoningEffortInput` 类型仍导出供参考,但仅用于构造 `providerExtras` 的值。 与 DeepSeek 文档一致的字段映射如下(兼容体): | `providerExtras` 字段 | 请求体字段 | 说明 | |-----------------------|------------|------| | `thinking: { type: "enabled" \| "disabled" }` | `thinking` | 思考开关;不传则由服务端默认(一般为 enabled) | | `reasoning_effort: "low" \| "medium" \| "high" \| "max" \| "xhigh"` | `reasoning_effort` | 仅发送 **`high`** 或 **`max`**:`low` / `medium` → `high`,`xhigh` → `max`。注意 `providerExtras` 中使用 **snake_case**(`reasoning_effort`),因为 `providerExtras` 直接映射到供应商请求体 | **使用示例**: ```ts // 旧写法(已移除): // { messages, thinking: { type: "enabled" }, reasoningEffort: "high" } // 新写法: const res = await xllm.generate({ messages, providerExtras: { thinking: { type: "enabled" }, reasoning_effort: "high", }, }); ``` **流式**:若供应商在 SSE 的 `choices[].delta` 中返回 `reasoning_content`(或 `reasoning` 字符串),库会发出 `XStreamEvent`:`{ type: "reasoning.delta", text: string }`,与 `{ type: "text.delta", ... }` 区分。 **非流式**:若 `message` 中含 `reasoning_content` 或 `reasoning`,会填入 `XResponse.reasoning`;正文仍在 `XResponse.text`。 `provider: "deepseek"` 与 `provider: "openai-compatible"` 均走同一适配器请求体逻辑;其它 **同形态** 网关若支持相同字段,亦可使用 `providerExtras` 传入 `thinking` / `reasoning_effort`。 --- ## 6. 请求与消息模型 ### 6.1 `XRequest`(核心字段) - `messages: XMessage[]`(必填) - `tools?: XToolDefinition[]` - `toolChoice?: "auto" | "none" | { name: string }` - `temperature?`, `topP?`, `maxTokens?`, `metadata?` - 可覆盖:`provider?`, `model?`, `apiKey?`, `baseURL?` - `providerExtras?: Record`:合并进 Chat Completions 请求 JSON 根对象(见 §5.1)。思考模式参数 `thinking` / `reasoning_effort` 等供应商专有字段均通过此字段传入 - `stream?`:由 `generate`/`stream` 内部控制,调用方一般不必依赖此字段语义 > **已移除**:`thinking` 和 `reasoningEffort` 不再是 `XRequest` 的直接字段。请使用 `providerExtras: { thinking: ..., reasoning_effort: ... }` 代替。 ### 6.2 `XMessage` - `system` | `user` | `assistant`:`content: XContentPart[] | string`(纯文本场景可直接传 `string`,等价于 `[{ type: "text", text: "..." }]`);`assistant` 可选 `toolCalls?: XToolCall[]`(**协议要求**:在 `role: "tool"` 之前,assistant 需带对应 `tool_calls`;使用 `chatWithTools` / `streamWithTools` 时由库自动写入);`assistant` 还可选 **`reasoningContent?: string`**,序列化为 **`reasoning_content`**(思考模式 + 工具调用时,部分供应商要求后续轮完整回传) - `tool`:**必须**包含 `toolCallId: string`,且与上一轮 assistant 的 `tool_calls[].id` 对应;`content` 同样接受 `XContentPart[] | string` ### 6.3 `XContentPart` - `{ type: "text"; text: string }` - `{ type: "image"; image: { url: string } }`(http(s) 或 data URL;**模型是否支持多模态由供应商决定**,库只做字段映射) ### 6.4 `XToolDefinition` - `name`, 可选 `description`, `parameters`(通常为 JSON Schema 风格对象,会原样进入供应商请求体) --- ## 7. 响应与流式事件 ### 7.1 `XResponse` - `text`, `toolCalls`, `usage?`, `provider`, `model`, `finishReason?`, `raw?` ### 7.2 `XStreamEvent`(判别联合,按 `event.type` 收窄) | `type` | 含义 | |--------|------| | `response.start` | 流开始,含 `provider`, `model`, `requestId?` | | `text.delta` | 文本增量 | | `reasoning.delta` | 思考链增量(如 `reasoning_content`) | | `tool_call.delta` | 工具参数增量 | | `tool_call.done` | 单个工具调用片段结束 | | `response.usage` | 用量 | | `response.done` | 本轮流结束;`finishReason` 可能为空(例如仅 `[DONE]` 收尾时) | **消费建议**:直出场景主要处理 `text.delta`;工具场景监听 `tool_call.done`;结束可用 `response.done`(注意可能与适配器在 `finish_reason` 与 `[DONE]` 路径上的去重逻辑配合,避免重复收尾业务状态)。 ### 7.3 `SSEFrame`(底层 SSE 帧) ```ts interface SSEFrame { event?: string; data: string; } ``` `SSEFrame` 是库内部 SSE 解析器产出的原始帧类型。每个 SSE 事件被解析为一个 `SSEFrame`: - `event`:SSE 事件类型(如 `message`、`error` 等);若服务端未发送 `event:` 行则为 `undefined` - `data`:SSE `data:` 字段的完整内容(多行 `data:` 会以 `\n` 拼接) 此类型通过 `parseSSE()` 生成器产出,供 `ProviderAdapter.fromProviderStreamChunk` 消费。大多数使用者无需直接操作 `SSEFrame`,但它在自定义适配器中可用于解析非标准 SSE 格式。 --- ## 8. 工具调用 ### 8.1 仅单次模型调用(手动处理工具) 1. `generate` / `stream` 带 `tools` + `toolChoice: "auto"` 2. 从 `XResponse.toolCalls` 或流式 `tool_call.done` 取 `id`, `name`, `arguments`(`arguments` 为 JSON 字符串) 3. 本地执行后,追加 `role: "tool"` 且 `toolCallId` 对齐,再发下一轮请求;assistant 侧需带 `toolCalls`(**推荐直接用 `chatWithTools` / `streamWithTools` 避免协议错误**) ### 8.2 `chatWithTools`(非流式闭环) - 额外选项:`maxRounds?`(默认 `5`),`toolErrorStrategy?`(默认 `"throw"`) - 返回:`{ response, rounds, toolCallsExecuted }` - 行为:每轮 `generate`;若有 `toolCalls`,执行 `executors[name]`,将结果写入 tool 消息并追加历史,直到无工具调用或超出 `maxRounds` - **思考模式 + 工具调用(DeepSeek)**:若本轮 `XResponse.reasoning` 有值(来自 `message.reasoning_content`),库会把同一全文写入历史 assistant 的 `reasoningContent`,下一轮请求序列化为 `reasoning_content`,满足「后续请求须完整回传」的要求。 ### 8.3 `streamWithTools`(流式闭环) - 每轮消费完整 `stream`;收集 `tool_call.done` 与拼接的 `assistantText`;**同时拼接本轮全部 `reasoning.delta` 为 `reasoningContent`**;执行工具后追加 `assistant`(含 `toolCalls` 与可选 `reasoningContent`)与 `tool` 消息,再进入下一轮 - 事件仍原样 `yield` 给调用方 ### 8.4 `XToolExecutor` 与 `XToolExecutorMap` ```ts type XToolExecutor = (args: unknown, toolCall: XToolCall) => Promise | unknown; type XToolExecutorMap = Record; ``` - `args` 由库对 `toolCall.arguments` 做 `JSON.parse` 失败时回退为 `{ raw: string }` ### 8.5 `toolErrorStrategy` | 值 | 行为 | |----|------| | `throw` | 工具不存在或执行抛错 → 抛 `XllmError` | | `return_tool_error_message` | 将错误封装为 tool 消息内容(JSON),让模型继续 | | `skip` | 跳过该工具,不追加 tool 消息 | --- ## 9. 错误:`XllmError` - `code`: `AUTH_ERROR` | `RATE_LIMIT` | `NETWORK_ERROR` | `INVALID_REQUEST` | `PROVIDER_ERROR` | `TIMEOUT` | `CANCELLED` - `provider`, `message`, 可选 `statusCode`, `requestId`, `raw` - `responseHeaders?: Record`:HTTP 响应头(非 2xx 响应时可用,便于读取 `x-request-id` 等诊断信息) - HTTP 非 2xx 时由适配器 `normalizeError` 映射 ### 9.1 错误码说明 | 错误码 | 触发场景 | |--------|----------| | `AUTH_ERROR` | API Key 无效或缺失(HTTP 401/403) | | `RATE_LIMIT` | 请求频率超限(HTTP 429) | | `NETWORK_ERROR` | 网络不可达、DNS 失败等(无 `fetch` 时同样抛此码) | | `INVALID_REQUEST` | 请求参数不合法(HTTP 400) | | `PROVIDER_ERROR` | 供应商侧 5xx 或其它未归类错误 | | `TIMEOUT` | 请求超时(由调用方或适配器 AbortController 触发) | | `CANCELLED` | 请求被主动取消(AbortSignal 已触发) | --- ## 10. 自定义 `fetch` 与测试 ```ts const xllm = createXllm({ apiKey: "test", fetch: mockFetch as typeof fetch, }); ``` 用于单测或代理;`generate`/`stream` 使用合并后的 `fetchImpl`。 --- ## 11. 自定义供应商适配器 ### 11.1 运行时注册(无需改库) 通过 `registerAdapter()` 可在运行时注册自定义 `ProviderAdapter`,无需修改库源码: ```ts import { registerAdapter, getRegisteredProviderNames, createXllm } from "@dm-pkg/xllm"; import type { ProviderAdapter } from "@dm-pkg/xllm"; const myAdapter: ProviderAdapter = { name: "my-provider", toProviderRequest(input, config, stream) { /* ... */ }, fromProviderResponse(raw, provider) { /* ... */ }, fromProviderStreamChunk(chunkData, state) { /* ... */ }, normalizeError(error) { /* ... */ }, }; registerAdapter(myAdapter); // getRegisteredProviderNames() → [..., "my-provider"] const xllm = createXllm({ provider: "my-provider" }); ``` ### 11.2 `ProviderAdapter` 接口 ```ts interface ProviderAdapter { /** 适配器名称,即 `XProviderName` 值 */ name: XProviderName; /** 将 XRequest 转换为供应商 HTTP 请求 */ toProviderRequest( input: XRequest, config: ResolvedConfig, stream: boolean, ): ProviderHttpRequest; /** 将供应商原始响应转换为 XResponse */ fromProviderResponse(raw: unknown, provider: XProviderName): XResponse; /** 将供应商 SSE chunk 转换为 XStreamEvent[](一个 chunk 可产出多个事件) */ fromProviderStreamChunk(chunkData: unknown, state: StreamState): XStreamEvent[]; /** 将供应商错误转换为 XllmError */ normalizeError(error: unknown): XllmError; /** 请求发送前的钩子,可修改请求(如签名、注入 header)。可选。 */ onBeforeRequest?( request: ProviderHttpRequest, config: ResolvedConfig, ): ProviderHttpRequest; /** 收到响应后的钩子,可用于日志、指标采集。返回值可替换响应。可选。 */ onAfterResponse?( response: Response, config: ResolvedConfig, ): Response | Promise; } ``` ### 11.3 `ProviderHttpRequest` ```ts interface ProviderHttpRequest { url: string; method: "POST"; headers: Record; body: Record; } ``` ### 11.4 `StreamState` ```ts interface StreamState { started: boolean; doneEmitted: boolean; toolCallsByIndex: Map; } ``` 适配器在 `fromProviderStreamChunk` 中通过 `state` 跟踪流状态:`started` 标记是否已发出 `response.start`,`doneEmitted` 防止重复发出 `response.done`,`toolCallsByIndex` 用于拼接跨 chunk 的工具调用参数。 ### 11.5 钩子:`onBeforeRequest` / `onAfterResponse` - **`onBeforeRequest`**:在 `fetch` 调用前触发,接收即将发送的 `ProviderHttpRequest`,返回修改后的请求。典型用途:请求签名、注入认证 header、添加追踪 ID。 - **`onAfterResponse`**:在收到 HTTP `Response` 后触发,可读取状态码/头信息做日志或指标采集,也可返回一个新的 `Response` 替换原始响应。支持异步(`Promise`)。 ### 11.6 新增内置供应商(需改库) 若需将适配器作为库的内置支持,步骤如下: 1. 实现 `ProviderAdapter`(`packages/xllm/src/providers/` 下新增文件) 2. 在 `registry.ts` 中注册内置实例 3. 在 `runtime/config.ts` 补充默认 `model` / `baseURL` / `resolveApiKey` 分支 4. 增加 `index.test.ts` 用 mock `fetch` 覆盖 **OpenAI 兼容第三方**:通常只需 `openai-compatible` + `baseURL`,无需新适配器。 --- ## 12. 实现文件索引(供维护者 / Agent 定位) | 职责 | 路径 | |------|------| | 对外导出 | `src/index.ts` | | 客户端 | `src/client/create-xllm.ts`, `generate.ts`, `stream.ts`, `chat-with-tools.ts`, `stream-with-tools.ts`, `tool-loop-shared.ts` | | 类型 | `src/core/types.ts`, `src/core/errors.ts` | | 配置 | `src/runtime/config.ts`, `http.ts`, `sse.ts` | | 适配器 | `src/providers/openai-compatible.adapter.ts`, `deepseek.adapter.ts`, `registry.ts`, `types.ts` | --- ## 13. 生成代码时的检查清单(给大模型) - [ ] 是否设置了 `apiKey` 或对应环境变量? - [ ] 兼容网关是否使用 `openai-compatible` + 正确 `baseURL`? - [ ] 流式是否用 `for await` 遍历 `XStreamEvent` 并收窄 `type`? - [ ] 工具闭环是否优先使用 `chatWithTools` / `streamWithTools`,避免手写 `tool_calls` 顺序错误? - [ ] 多模态是否确认目标模型支持,否则仅发 `text`? - [ ] 工具失败策略是否为生产环境显式选择 `toolErrorStrategy`? - [ ] 思考类参数(`thinking` / `reasoning_effort`)是否通过 `providerExtras` 传入,而非作为 `XRequest` 直接字段? - [ ] `providerExtras` 中的键名是否使用 **snake_case**(如 `reasoning_effort`),因为它们直接映射到供应商请求体? - [ ] 自定义供应商是否通过 `registerAdapter()` 注册,而非修改库源码? - [ ] 是否处理了 `TIMEOUT` 和 `CANCELLED` 错误码(如使用 AbortController 时)? --- *文档与 `packages/xllm` 源码同步维护;若行为与代码不一致,以源码为准。*