OpenClaw @openclaw/ai 最小消费者示例:一个隔离 LLM 运行时、内置 Provider 与流式补全
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
本文基于 OpenClaw 仓库中 examples/ai-chat 的官方最小示例,讲解如何仅使用已发布的@openclaw/ai包(不依赖任何 OpenClaw 应用代码)创建隔离的 LLM 运行时、注册 8 个内置 API Provider,并以纯 Node.js ESM 脚本流式消费一次模型补全。读完后你将掌握该包的公开入口(createLlmRuntime、registerBuiltInApiProviders)、模型对象各字段的含义、三种 Provider(Anthropic / OpenAI / Ollama)的实际运行命令,以及底层延迟注册与事件流机制的源码实现。
示例定位:为什么这个示例有价值
@openclaw/ai是 OpenClaw 对外发布的可复用模型适配包,提供 Provider 中立的契约、Provider 适配器与流式原语。包文档(packages/ai/README.md)明确了它的能力边界:
- 支持隔离的运行时实例:导入该包不会在包级别全局注册任何 Provider;
- Provider 中立的契约、校验、诊断与事件流可从包根及
@openclaw/ai/event-stream、@openclaw/ai/transports、@openclaw/ai/validation等子路径导入; - Provider 凭据、模型目录、重试与故障转移属于应用层关注点,OpenClaw 应用在这个包之上提供这些策略;
- 主机策略(自定义 fetch 防护、密钥脱敏、strict-tool 默认值、诊断日志)可通过
configureAiTransportHost注入,默认值是不活跃的(inert); @openclaw/ai/internal/*系列子路径仅供 OpenClaw 应用自身使用,无 semver 保证,外部不应依赖。
examples/ai-chat 就是这个包"最小外部消费者"的活体证明:它只依赖@openclaw/ai的公开面,不引入任何 OpenClaw 应用代码。package.json(examples/ai-chat/package.json)声明为私有 ESM 包:
{ "name": "@openclaw/example-ai-chat", "version": "0.0.0-private", "private": true, "description": "Minimal external-consumer example for @openclaw/ai", "type": "module", "scripts": { "start": "node index.mjs" }, "dependencies": { "@openclaw/ai": "workspace:*" } }这里有一个关键的运行前提:在仓库内部,@openclaw/ai的 workspace 链接解析到该包的已构建dist产物——也就是 npm 消费者实际安装的同一份产物。因此必须先执行pnpm build,否则示例无法解析到包内容。
完整示例代码解析(index.mjs)
整个示例只有约 80 行(examples/ai-chat/index.mjs),结构分四段:模型表、参数解析、运行时创建、流式消费。
1. 模型声明表:三个 Provider 的完整配置
const MODELS = { anthropic: { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", api: "anthropic-messages", provider: "anthropic", baseUrl: "https://api.anthropic.com", reasoning: true, input: ["text"], cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, contextWindow: 200_000, maxTokens: 8192, }, openai: { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", api: "openai-responses", provider: "openai", baseUrl: "https://api.openai.com/v1", reasoning: true, input: ["text"], cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }, contextWindow: 1_050_000, maxTokens: 128_000, }, // Local Ollama server; no API key required. ollama: { id: process.env.OLLAMA_MODEL || "llama3.2:latest", name: "Ollama", api: "openai-completions", provider: "ollama", baseUrl: "http://localhost:11434/v1", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32_000, maxTokens: 4096, }, };模型对象的每个字段对应@openclaw/ai的模型契约:
| 字段 | 作用 | 示例中的取值要点 |
|---|---|---|
id/name | 模型标识与展示名 | Ollama 的id由OLLAMA_MODEL环境变量覆盖,默认llama3.2:latest |
api | 决定走哪个内置传输(transport) | 三个取值分别对应anthropic-messages、openai-responses、openai-completions三种注册好的协议适配器 |
provider | Provider 标识 | anthropic/openai/ollama |
baseUrl | API 端点 | Ollama 指向本机http://localhost:11434/v1(OpenAI 兼容端点) |
reasoning | 是否具备推理(extended thinking / reasoning tokens)能力 | Anthropic、OpenAI 为true,Ollama 为false |
input | 支持的输入模态 | 均为["text"] |
cost | 每百万 token 的计价参数 | 本地 Ollama 全为 0 |
contextWindow | 上下文窗口大小 | Ollama 32K,OpenAI 1.05M |
maxTokens | 单次最大输出 token 数 | Ollama 4096,OpenAI 128K |
注意一个设计细节:Ollama 通过api: "openai-completions"复用 OpenAI 兼容传输——本地模型服务器只要暴露 OpenAI 兼容的/v1接口,就无需单独适配。
2. 命令行参数解析
const args = process.argv.slice(2); const providerFlag = args.indexOf("--provider"); const provider = providerFlag === -1 ? "anthropic" : args[providerFlag + 1]; const prompt = args.filter((_, i) => i !== providerFlag && i !== providerFlag + 1).join(" ") || "Reply with one short sentence: what is @openclaw/ai?"; const model = MODELS[provider]; if (!model) { console.error(`Unknown provider "${provider}". Use one of: ${Object.keys(MODELS).join(", ")}`); process.exit(1); }--provider缺省时默认anthropic;- 提示词 = 除
--provider及其取值外的所有剩余参数拼接(保留空格分隔),再整体作为单条 user 消息; - 无提示词时使用内置默认提示;
- 未识别的 provider 直接打印可用列表并以退出码 1 终止。
3. 运行时创建与 Provider 注册:整个包最核心的两步
const runtime = createLlmRuntime(); registerBuiltInApiProviders(runtime.registry);对照 packages/ai/src/stream.ts 的实现,createLlmRuntime会创建一个隔离的运行时:
export function createLlmRuntime(registry: ApiRegistry = createApiRegistry()) { function resolveApiProvider(api: Api) { const provider = registry.getApiProvider(api); if (!provider) { throw new Error(`No API provider registered for api: ${api}`); } return provider; } // ... return { registry, stream, complete, streamSimple, completeSimple }; }从源码结构看,运行时暴露了 5 个成员:
registry:Provider 注册表(ApiRegistry),模型请求按model.api在此解析出对应的stream/streamSimple函数;未注册则抛No API provider registered for api: ...;stream(model, context, options?):走完整Context契约的流式调用,返回AssistantMessageEventStreamContract;complete(...):stream(...).result()的便捷封装,等待流结束返回最终AssistantMessage;streamSimple(model, context, options?):走简化的SimpleStreamOptions(如{ messages }),示例使用的正是这一档;completeSimple(...):streamSimple(...).result()的便捷封装。
registerBuiltInApiProviders实现见 packages/ai/src/providers/register-builtins.ts。它一次性注册8 个内置传输,与 README 描述一一对应:
| api 标识 | 对应协议 | 说明 |
|---|---|---|
anthropic-messages | Anthropic Messages | 示例 anthropic 配置所用 |
openai-completions | OpenAI Chat Completions 兼容 | Ollama 走此传输 |
openai-responses | OpenAI Responses API | 示例 openai 配置所用 |
azure-openai-responses | Azure OpenAI | — |
openai-chatgpt-responses | ChatGPT (Codex) Responses | — |
mistral-conversations | Mistral Conversations | — |
google-generative-ai | — | |
google-vertex | Vertex | — |
注册采用惰性加载:createLazyRegistration为每个 api 注册一个包装函数,调用时才动态import()对应 Provider 模块(如import("./anthropic.js")),且加载结果被缓存(streamsPromise ??= importModule().then(select))。因此"Provider SDK 模块在首次使用时才加载"——注册本身是零成本的,示例进程只加载了实际请求命中的那个适配器。同时createLazyStream保证调用方同步拿到一个流对象,底层模块加载完成后通过forwardStream把事件逐条转发进这个外层流;加载失败时则推送一条error事件并以零用量结果收尾(projectProviderError会统一错误投影)。
4. 流式消费:stdout 与 stderr 的分流
const stream = runtime.streamSimple( model, { messages: [{ role: "user", content: prompt, timestamp: Date.now() }] }, // Ollama ignores credentials but the OpenAI-compatible transport requires one. provider === "ollama" ? { apiKey: "ollama" } : undefined, ); for await (const event of stream) { if (event.type === "text_delta") { process.stdout.write(event.delta); } } const result = await stream.result(); process.stdout.write("\n"); if (result.stopReason === "error" || result.stopReason === "aborted") { console.error(`error: ${result.errorMessage ?? result.stopReason}`); process.exit(1); } const { input, output } = result.usage; console.error(`[${model.id}] stop=${result.stopReason} tokens in=${input} out=${output}`);三个值得注意的点:
- 输出分流:文本增量(
text_delta事件)逐块写到 stdout;停止原因与 token 用量走 stderr。这使得模型输出可以被管道消费,而诊断信息不污染管道内容; - Ollama 的占位 key:Ollama 本身不需要凭据,但 OpenAI 兼容传输要求存在
apiKey,所以传入字面量"ollama"占位; - 结果收尾:
stream.result()在流结束后返回最终AssistantMessage,包含stopReason、errorMessage(错误投影时)与usage(input/outputtoken 数及cost明细)。stopReason为error或aborted时以退出码 1 终止。
运行方式:完整命令清单
POSIX Shell(Ollama 一条在 PowerShell 中同样可用)
ANTHROPIC_API_KEY=example-anthropic-key-not-real node index.mjs "Say hello" OPENAI_API_KEY=example-openai-key-not-real node index.mjs --provider openai "Say hello" # keyless, against a local Ollama server (OLLAMA_MODEL overrides the model id) node index.mjs --provider ollama "Say hello"PowerShell
$env:ANTHROPIC_API_KEY = "example-anthropic-key-not-real" node index.mjs "Say hello" $env:OPENAI_API_KEY = "example-openai-key-not-real" node index.mjs --provider openai "Say hello" # Clear the session-scoped keys. $env:ANTHROPIC_API_KEY = $null $env:OPENAI_API_KEY = $null两种写法的差异与限制需要注意:
- POSIX 中
KEY=... node index.mjs的形式,环境变量只作用于单条命令,执行完即消失; - PowerShell 的
$env:赋值在会话内持续存在,直到手动清空(置$null)或关闭 shell——README 特意提示了这一点,避免密钥残留; - Ollama 分支完全无 key,但要求本机有一个运行中的 Ollama 服务器(
http://localhost:11434),可用OLLAMA_MODEL环境变量覆盖默认的llama3.2:latest模型 id。
运行前请确认已执行pnpm build(workspace 链接指向构建产物),并将占位 key 替换为真实 API key。
面向库使用者的四条要点
README 末尾给出的四条 library consumer notes,逐条对应到源码事实:
createLlmRuntime()给你隔离注册表:导入包本身不产生任何全局注册,每个createLlmRuntime()实例拥有独立的ApiRegistry(packages/ai/src/stream.ts 中默认参数createApiRegistry()即为每实例新建)。registerBuiltInApiProviders(runtime.registry)一次性获得 8 个内置传输:Anthropic、OpenAI Completions/Responses、Azure OpenAI、ChatGPT Responses、Google、Vertex、Mistral;Provider SDK 模块在首次使用时惰性加载(见上文createLazyRegistration分析)。- 主机策略可注入:自定义 fetch、密钥脱敏、strict-tool 默认值、诊断日志通过
configureAiTransportHost注入,默认实现是 inert 的——也就是说开箱即用时不会拦截或改写任何请求行为。 - 完整 TypeScript 类型随包发布:类型声明位于
dist/*.d.mts(见 packages/ai/package.json 的exports字段,根及 7 个公共子路径均带types条件);示例刻意使用纯 ESM 以便在裸node环境下直接运行,无需 TS 工具链。
延伸阅读
- 包文档:packages/ai/README.md
- 运行时实现:packages/ai/src/stream.ts
- 内置 Provider 注册与惰性加载:packages/ai/src/providers/register-builtins.ts
- 示例源码:examples/ai-chat/index.mjs
这个示例展示了@openclaw/ai作为独立库的最小接入形态:隔离运行时 + 内置注册 + 一次流式补全。如果你的应用需要多 Provider 故障转移、凭据管理或模型目录,则应在该包之上自行实现这些应用层策略——这正是@openclaw/ai刻意不内置的边界。
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考