18 KiB
@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 运行时导出
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
const xllm = createXllm(options?: XClientOptions): XllmClient;
3.1 XllmClient 方法
| 方法 | 签名 | 用途 |
|---|---|---|
generate |
(request: XRequest) => Promise<XResponse> |
非流式,一次返回完整结构 |
stream |
(request: XRequest) => AsyncIterable<XStreamEvent> |
流式 SSE,用 for await 消费 |
chatWithTools |
(request: XChatWithToolsOptions, executors: XToolExecutorMap) => Promise<XChatWithToolsResult> |
非流式 + 自动工具闭环 |
streamWithTools |
同上,返回 AsyncIterable<XStreamEvent> |
流式 + 自动工具闭环 |
with |
(overrides: Partial<XClientOptions>) => XllmClient |
复制客户端并合并默认配置 |
4. 配置合并与环境变量
4.1 优先级(从高到低)
- 单次请求
XRequest/XChatWithToolsOptions上的字段(provider,model,apiKey,baseURL等) createXllm传入的XClientOptions- 环境变量(仅 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-chatbaseURL: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
import { registerAdapter, getRegisteredProviderNames } from "@dm-pkg/xllm";
| 函数 | 签名 | 用途 |
|---|---|---|
registerAdapter |
(adapter: ProviderAdapter) => void |
注册或覆盖一个供应商适配器;后续 generate/stream 等调用即可使用该 adapter.name 作为 provider |
getRegisteredProviderNames |
() => string[] |
返回当前已注册的所有供应商名称(含内置) |
示例:
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,有两种做法:
providerExtras(推荐先做):在XRequest.providerExtras里写入任意键值,库会在构建完标准字段后Object.assign合并进请求体根对象(后合并者可覆盖同名标准字段)。用于传入厂商文档要求的专有参数,而无需立刻新增适配器。- 新
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 直接映射到供应商请求体 |
使用示例:
// 旧写法(已移除):
// { 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<string, unknown>:合并进 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 帧)
interface SSEFrame {
event?: string;
data: string;
}
SSEFrame 是库内部 SSE 解析器产出的原始帧类型。每个 SSE 事件被解析为一个 SSEFrame:
event:SSE 事件类型(如message、error等);若服务端未发送event:行则为undefineddata:SSEdata:字段的完整内容(多行data:会以\n拼接)
此类型通过 parseSSE() 生成器产出,供 ProviderAdapter.fromProviderStreamChunk 消费。大多数使用者无需直接操作 SSEFrame,但它在自定义适配器中可用于解析非标准 SSE 格式。
8. 工具调用
8.1 仅单次模型调用(手动处理工具)
generate/stream带tools+toolChoice: "auto"- 从
XResponse.toolCalls或流式tool_call.done取id,name,arguments(arguments为 JSON 字符串) - 本地执行后,追加
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
type XToolExecutor = (args: unknown, toolCall: XToolCall) => Promise<unknown> | unknown;
type XToolExecutorMap = Record<string, XToolExecutor>;
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|CANCELLEDprovider,message, 可选statusCode,requestId,rawresponseHeaders?: Record<string, string>: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 与测试
const xllm = createXllm({
apiKey: "test",
fetch: mockFetch as typeof fetch,
});
用于单测或代理;generate/stream 使用合并后的 fetchImpl。
11. 自定义供应商适配器
11.1 运行时注册(无需改库)
通过 registerAdapter() 可在运行时注册自定义 ProviderAdapter,无需修改库源码:
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 接口
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<Response>;
}
11.3 ProviderHttpRequest
interface ProviderHttpRequest {
url: string;
method: "POST";
headers: Record<string, string>;
body: Record<string, unknown>;
}
11.4 StreamState
interface StreamState {
started: boolean;
doneEmitted: boolean;
toolCallsByIndex: Map<number, XToolCall>;
}
适配器在 fromProviderStreamChunk 中通过 state 跟踪流状态:started 标记是否已发出 response.start,doneEmitted 防止重复发出 response.done,toolCallsByIndex 用于拼接跨 chunk 的工具调用参数。
11.5 钩子:onBeforeRequest / onAfterResponse
onBeforeRequest:在fetch调用前触发,接收即将发送的ProviderHttpRequest,返回修改后的请求。典型用途:请求签名、注入认证 header、添加追踪 ID。onAfterResponse:在收到 HTTPResponse后触发,可读取状态码/头信息做日志或指标采集,也可返回一个新的Response替换原始响应。支持异步(Promise<Response>)。
11.6 新增内置供应商(需改库)
若需将适配器作为库的内置支持,步骤如下:
- 实现
ProviderAdapter(packages/xllm/src/providers/下新增文件) - 在
registry.ts中注册内置实例 - 在
runtime/config.ts补充默认model/baseURL/resolveApiKey分支 - 增加
index.test.ts用 mockfetch覆盖
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 源码同步维护;若行为与代码不一致,以源码为准。