mono项目开发模板,内置cli管理构建
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

18 KiB

@dm-pkg/xllm — 面向大模型 / Agent 的使用说明

本文档供 大模型、代码助手、自动化 Agent 在编写或修改调用代码时作为单一事实来源。API 以当前包导出为准(入口:packages/xllm/src/index.ts)。


1. 包与运行时

  • 包名@dm-pkg/xllm
  • 模块:ESM("type": "module"
  • 运行环境:需存在全局 fetch(现代 Node、浏览器、Bun 等)。无 fetch 时抛 XllmErrorNETWORK_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

注意XThinkingModeXReasoningEffortInput 仍作为类型导出供参考,但不再是 XRequest 的直接字段——请通过 providerExtras 传入(见 §5.1)。


3. 客户端:createXllmXllmClient

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 优先级(从高到低)

  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"
  • modelopenai-compatiblegpt-4o-minideepseekdeepseek-chat
  • baseURLhttps://api.openai.com/v1https://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,有两种做法:

  1. providerExtras(推荐先做):在 XRequest.providerExtras 里写入任意键值,库会在构建完标准字段后 Object.assign 合并进请求体根对象后合并者可覆盖同名标准字段)。用于传入厂商文档要求的专有参数,而无需立刻新增适配器。
  2. ProviderAdapter:协议差异大(路径、鉴权、流式格式都不同)时,通过 registerAdapter() 注册自定义适配器(见 §11),无需修改库源码。

迁移说明thinkingreasoningEffort 不再是 XRequest 的直接字段。请改用 providerExtras 传入。XThinkingModeXReasoningEffortInput 类型仍导出供参考,但仅用于构造 providerExtras 的值。

与 DeepSeek 文档一致的字段映射如下(兼容体):

providerExtras 字段 请求体字段 说明
thinking: { type: "enabled" | "disabled" } thinking 思考开关;不传则由服务端默认(一般为 enabled)
reasoning_effort: "low" | "medium" | "high" | "max" | "xhigh" reasoning_effort 仅发送 highmaxlow / mediumhighxhighmax。注意 providerExtras 中使用 snake_casereasoning_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_contentreasoning,会填入 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 内部控制,调用方一般不必依赖此字段语义

已移除thinkingreasoningEffort 不再是 XRequest 的直接字段。请使用 providerExtras: { thinking: ..., reasoning_effort: ... } 代替。

6.2 XMessage

  • system | user | assistantcontent: 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 事件类型(如 messageerror 等);若服务端未发送 event: 行则为 undefined
  • data:SSE data: 字段的完整内容(多行 data: 会以 \n 拼接)

此类型通过 parseSSE() 生成器产出,供 ProviderAdapter.fromProviderStreamChunk 消费。大多数使用者无需直接操作 SSEFrame,但它在自定义适配器中可用于解析非标准 SSE 格式。


8. 工具调用

8.1 仅单次模型调用(手动处理工具)

  1. generate / streamtools + toolChoice: "auto"
  2. XResponse.toolCalls 或流式 tool_call.doneid, name, argumentsarguments 为 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.deltareasoningContent;执行工具后追加 assistant(含 toolCalls 与可选 reasoningContent)与 tool 消息,再进入下一轮
  • 事件仍原样 yield 给调用方

8.4 XToolExecutorXToolExecutorMap

type XToolExecutor = (args: unknown, toolCall: XToolCall) => Promise<unknown> | unknown;
type XToolExecutorMap = Record<string, XToolExecutor>;
  • args 由库对 toolCall.argumentsJSON.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<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.startdoneEmitted 防止重复发出 response.donetoolCallsByIndex 用于拼接跨 chunk 的工具调用参数。

11.5 钩子:onBeforeRequest / onAfterResponse

  • onBeforeRequest:在 fetch 调用前触发,接收即将发送的 ProviderHttpRequest,返回修改后的请求。典型用途:请求签名、注入认证 header、添加追踪 ID。
  • onAfterResponse:在收到 HTTP Response 后触发,可读取状态码/头信息做日志或指标采集,也可返回一个新的 Response 替换原始响应。支持异步(Promise<Response>)。

11.6 新增内置供应商(需改库)

若需将适配器作为库的内置支持,步骤如下:

  1. 实现 ProviderAdapterpackages/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() 注册,而非修改库源码?
  • 是否处理了 TIMEOUTCANCELLED 错误码(如使用 AbortController 时)?

文档与 packages/xllm 源码同步维护;若行为与代码不一致,以源码为准。