npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codehz/ai

v0.9.1

Published

统一流式 AI 客户端,提供一套 canonical API,对接真实模型后端与面向测试的回调驱动 `MockAdapter`(`responses` / `messages` / `chat-completions` / `ollama` / `gemini` / `mock`)。

Readme

@codehz/ai

统一流式 AI 客户端,提供一套 canonical API,对接真实模型后端与面向测试的回调驱动 MockAdapterresponses / messages / chat-completions / ollama / gemini / mock)。

0.6.0 迁移说明

小 breaking:warnings 形状、tool_call 校验与 Chat Completions opaque 入站。

warnings 结构化

AIResponse.warnings / response.warning / response.completed.warningsstring[] 改为:

type StreamWarning = { message: string; code?: WarningCode };
// AIResponse.warnings?: StreamWarning[]

迁移:原先 for (const w of response.warnings ?? []) console.log(w) 改为读取 w.message(及可选 w.code)。

tool_call.argumentsText 校验

运行时 validateRequest 不再tool_call.argumentsTextJSON.parse;只校验为 string。object-wire adapter(ollama / messages / gemini)在入站映射(parseToolArguments / parseJsonStrictObject)时若参数不是合法 JSON object,抛 AIRecoverableErrorTOOL_CALL_ARGUMENTS_INVALID):AdapterBase soft-complete 为 response.warning + response.completedstopReason: "error"),不向调用方抛错、也不发 provider HTTP。chat-completions / responses 仍按字符串透传。

Chat Completions opaque

入站 opaque 仅接受 messages 形({ messages: ChatMessage[] })。单条 { role, content }deprecate(有效 envelope 下未识别 shape 会被跳过,不再当完整 assistant turn 还原)。出站仍写 messages: [assistantReplayMessage]

opaque replay 只检查 object 和 JSON 可序列化性;客户端不再强制 1MiB/8MiB 大小或嵌套深度上限,也不会因这些通用限制丢弃 replay。实际请求大小和 provider 可接受的 payload 由 provider / transport 决定。

其余行为(0.6 一并落地,多为兼容增强)

  • HTTP adapter 错误码与 incomplete 流完成路径收敛;Chat Completions 支持 arguments-before-id 与 function_call/tool_calls 互斥 warning。
  • Aggregator 在 finalize 时从 output 派生 text / toolCalls / serverTool*argumentsText 分块累积。
  • 四家厚 adapter 拆为 map-request / map-stream(对齐 responses);opaque 恒尾置。

请求校验边界

normalizeRequestcreateAIClient 不再自动运行通用 validateRequest。客户端只做默认值合并、include 归一化和 request id 生成;temperaturemaxOutputTokens、tool 名称及其他 provider/model policy 交给目标 provider。adapter 仍保留构造合法 wire request 所需的 capability、shape 和 JSON object 检查。

通用校验器不再从 runtime 入口导出;应用如果需要严格输入检查,应在自己的信任边界显式实现。

0.5.0 迁移说明(摘要)

0.5.0 收紧了根入口公开面:AdapterBasecreateEventFactoryaggregateEventsnormalizeRequest、transport / syntheticStream不再@codehz/ai 根导出(内部模块)。

仍从根导出: createAIClientcollectStream、错误类型与 WarningCode、全部 adapters 与 Mock 夹具、canonical 构造、REASONING_LEVELS,以及 canonical 类型。自定义 adapter 请实现 BackendAdapter

错误通道语义

  • AIRequestError / AIProviderError / AIStreamError:致命,同步或在异步迭代中抛出,不伪造 response.completed
  • AIMappingError:由内部 AdapterBase 捕获后降级为 response.warningWarningCode.MAPPING_ERROR)+ 空 output 的 response.completed(无 stopReason);生产 adapter 原则上不抛。
  • AIRecoverableError:由内部 AdapterBase 捕获后 soft-complete 为 response.warning(code 取自错误)+ 空 replay 的 response.completedstopReason 默认 "error")。用于可清理历史后重试的回合失败(如 object-wire 入站非法 tool_call.argumentsText)。
  • 非致命差异走 response.warningWarningCode(0.6 起为结构化对象)。

安装

bun add @codehz/ai

依赖:Bun(内置 fetchcrypto),无需额外运行时依赖。

快速开始

import { createAIClient, ResponsesAdapter } from "@codehz/ai";

const client = createAIClient({
  adapter: new ResponsesAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
  model: "gpt-4o",
});

const stream = client.stream({
  input: [{ type: "message", role: "user", content: [{ type: "text", text: "What's the weather in Hangzhou?" }] }],
});

for await (const event of stream) {
  if (event.type === "message.delta") {
    process.stdout.write(event.delta.text);
  }
}

核心概念

统一请求模型

所有 adapter 接受同一形状的 AIRequest

type AIRequest = {
  instructions?: string | InstructionBlock[]; // 系统级指令
  input: InputItem[]; // 输入 items
  tools?: ToolDefinition[]; // 客户端函数工具(由调用方执行)
  serverTools?: ServerToolDefinition[]; // Provider 托管工具(web_search / code_execution / mcp)
  toolChoice?: ToolChoice; // 客户端工具选择策略
  temperature?: number; // provider/model-defined generation parameter
  maxOutputTokens?: number; // 最大输出 token
  reasoningLevel?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; // 可移植思考力度
  include?: {
    usage?: "off" | "best_effort" | "required";
    billing?: "off" | "best_effort" | "required";
    providerMetadata?: "off" | "best_effort";
  };
};

toolsserverTools 可在同一请求中共存。客户端 tool_call / 手动 tool-loop 语义不变;服务端工具由 provider 在请求内执行,不会stopReason 设为 tool_call

reasoningLevel 是 portable 枚举,由各 adapter 映射到 provider 原生字段;未设置时不写相关 wire 字段。adapter 无法映射的 level(如 Ollama 的 minimal / xhigh / max)会抛 AIRequestErrorUNSUPPORTED_REASONING_LEVEL)。需要 budget / summary 等特化参数时,仍可用构造期 extraBody 覆盖同名顶层键。

| Adapter | 映射 | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | ResponsesAdapter | reasoning: { effort } | | ChatCompletionsAdapter | 顶层 reasoning_effort | | MessagesAdapter | thinking: { type: "disabled" }{ type: "enabled", budget_tokens }(由 maxOutputTokens 按比例推导,默认 4096) | | OllamaAdapter | think: false \| "low" \| "medium" \| "high" | | GeminiAdapter | generationConfig.thinkingConfignone 关闭 thoughts;minimal/low/medium/highthinkingLevelxhigh/max 不支持) | | MockAdapter | 透传到 MockHandlerContext.reasoningLevel |

input 是 item 数组,每个 item 可以是:

| Item 类型 | 用途 | | ----------------------- | ----------------------------------- | | message | 用户 / 助手消息(可带 citations) | | reasoning | 思维链(输入侧 replay) | | tool_call | 客户端工具调用(输入侧 replay) | | tool_result | 客户端工具执行结果 | | server_tool_call | Provider 托管工具调用 | | server_tool_result | Provider 托管工具结果 | | server_tool_discovery | MCP 等远端工具发现列表 | | opaque | Provider 私有续接材料 |

统一事件流

所有 adapter 产出 AsyncIterable<AIStreamEvent>,事件语义一致:

response.started → (item.started → item.delta* → item.completed)* → response.completed

事件类型:

| 事件 | 含义 | | --------------------------------------- | ------------------------------------------- | | response.started | 响应开始 | | message.{started,delta,completed} | 消息输出(completed 可带 citations) | | reasoning.{started,delta,completed} | 思维链 | | tool_call.{started,delta,completed} | 客户端工具调用 | | server_tool.{started,delta,completed} | 服务端工具调用 | | server_tool_result.completed | 服务端工具结果(原子) | | server_tool_discovery.completed | MCP 工具发现(原子) | | response.warning | 非致命警告 | | response.auxiliary | usage / billing 辅助信息 | | response.completed | 响应结束,携带 replay、终止原因及最终元数据 |

统一终结结果

AIResponse 的完整形态以 collectStream() / 事件聚合器为唯一真相源。 流式消费者读 AIStreamEventresponse.completed 只携带 replay、终止原因、usage/billing 等元数据,携带完整 output/text/toolCalls 账本。

流结束后可通过 collectStream() 聚合为 AIResponse

import { collectStream } from "@codehz/ai";

const response = await collectStream(client.stream({ input }));
console.log(response.text); // 全部文本
console.log(response.toolCalls); // 工具调用列表
console.log(response.usage); // token 统计
console.log(response.replay); // 续接材料

AIResponse 包含:

| 字段 | 类型 | 说明 | | ------------------- | ------------------------ | ---------------------------------- | | output | OutputItem[] | 当前轮输出 | | replay | ReplayItem[] | 续接材料(下次请求带回) | | text | string | 全部文本拼接 | | toolCalls | ToolCallItem[] | 客户端工具调用 | | serverToolCalls | ServerToolCallItem[] | 服务端工具调用 | | serverToolResults | ServerToolResultItem[] | 服务端工具结果 | | stopReason | StopReason? | 终止原因(可选) | | usage | Usage? | token 统计(可选) | | billing | BillingInfo? | 计费信息(可选) | | auxiliary | AuxiliaryInfo? | Provider 辅助信息(可选) | | warnings | StreamWarning[]? | 非致命警告({ message; code? }) | | backend | BackendTrace | 调用链路元数据 |

流式 message.delta / reasoning.delta 保持后端分片粒度;完成态 output 中的 message / reasoning 会合并相邻 text content blocks(直接拼接且不添加分隔符), 非文本 block 仍保留原有边界。

后端 Adapter

| Adapter | 类 | 说明 | | ----------------------- | ------------------------ | ------------------------------ | | OpenAI Responses API | ResponsesAdapter | OpenAI Responses 端点 | | Anthropic Messages API | MessagesAdapter | Anthropic Messages 端点 | | OpenAI Chat Completions | ChatCompletionsAdapter | Chat Completions 端点 | | Ollama Chat API | OllamaAdapter | 本地或自托管 Ollama | | Google Gemini API | GeminiAdapter | Gemini streamGenerateContent | | Scripted Test Backend | MockAdapter | 脚本化测试夹具 |

import {
  ResponsesAdapter,
  MessagesAdapter,
  ChatCompletionsAdapter,
  OllamaAdapter,
  GeminiAdapter,
  MockAdapter,
  withMockStreaming,
} from "@codehz/ai";

// OpenAI Responses API
const responses = new ResponsesAdapter({
  apiKey: "sk-...",
  // 可选:自定义请求头 / body 额外顶层字段(构造期静态,后写覆盖内置鉴权头与同名 body 键)
  headers: { "OpenAI-Organization": "org-..." },
  extraBody: { top_p: 0.9 },
});

// Anthropic Messages API
const messages = new MessagesAdapter({
  apiKey: "sk-ant-...",
  headers: { "anthropic-beta": "..." },
  extraBody: { top_p: 0.9 },
});

// OpenAI Chat Completions
const chat = new ChatCompletionsAdapter({
  apiKey: "sk-...",
  headers: { "OpenAI-Organization": "org-..." },
  extraBody: { top_p: 0.9 },
});

// Ollama
const ollama = new OllamaAdapter({
  baseUrl: "http://localhost:11434",
  headers: { "X-Custom": "..." },
  extraBody: { keep_alive: "10m" },
});

// Google Gemini Developer API(原生 generateContent 流,非 OpenAI 兼容层)
const gemini = new GeminiAdapter({
  apiKey: process.env.GEMINI_API_KEY!,
  // 可选:代理 / Vertex 兼容端点
  // baseUrl: "https://generativelanguage.googleapis.com/v1beta",
  headers: { "X-Custom": "..." },
  extraBody: { safetySettings: [] },
});

// 面向测试的回调驱动 mock backend
const mock = new MockAdapter({
  handler: withMockStreaming(
    async function* () {
      yield { type: "message", content: "我先调用天气工具。" };
      yield {
        type: "tool_call",
        id: "mock-call-weather",
        name: "get_weather",
        argumentsText: '{"city":"Hangzhou"}',
      };
    },
    {
      charsPerSecond: 24,
      chunkSize: 1,
    },
  ),
});

公开 adapter 接口暴露稳定标识和流来源:

adapter.kind; // "responses" | "messages" | "chat-completions" | ...
adapter.isSyntheticStream;

响应级 backend.isSyntheticStream 使用同一标记;具体响应内容仍应从 本次事件流、warning 和 replay 判断。

上下文压缩(可选能力)

部分 adapter 原生支持上下文压缩,通过独立接口暴露,挂在 BackendAdapter / AIClient 上:

| Adapter | supportsContextCompress | 说明 | | ------------------ | ------------------------- | ----------------------------------------------------------------------- | | ResponsesAdapter | 是 | POST /responses/compact;结果为 opaque compacted_window | | MockAdapter | 是 | 构造期可选 compressHandler;未配置则抛 MOCK_COMPRESS_NOT_CONFIGURED | | 其余内置 adapter | 否 | 本期无客户端摘要 fallback |

import { ResponsesAdapter, supportsContextCompress, createAIClient, collectStream } from "@codehz/ai";
import type { InputItem } from "@codehz/ai";

const adapter = new ResponsesAdapter({ apiKey: process.env.OPENAI_API_KEY! });
const client = createAIClient({ adapter, model: "gpt-5.1" });

let transcript: InputItem[] = [/* 多轮累积 */];

if (supportsContextCompress(adapter)) {
  // 用 replay 替换旧 transcript(不要 append 全文)
  const { replay } = await adapter.compress({
    model: "gpt-5.1",
    input: transcript,
  });
  transcript = [...replay];
}

transcript.push({
  type: "message",
  role: "user",
  content: [{ type: "text", text: "继续上一任务" }],
});

const response = await collectStream(client.stream({ input: transcript }));

边界:

  • 本期仅覆盖 独立 compress();不支持在 stream 请求里自动 context_management / compact_threshold
  • Anthropic Messages 的请求内 compaction 未接入(无独立端点,另期)。
  • 调用方负责何时压缩与 transcript 替换;库不托管会话状态。

Mock 后端

MockAdapter 是一个面向测试的回调驱动 adapter,用来验证长流程工具调用、replay 续接和异常路径。

如果你要调试前端逐字渲染效果,可以用 withMockStreaming() 给非流式 handler 包一层分片输出:

const handler = withMockStreaming(
  async function* () {
    yield { type: "message", content: "Streaming preview for the frontend." };
  },
  {
    charsPerSecond: 20, // 每秒约 20 个字符
    chunkSize: 1, // 默认 1,即逐字输出
    initialDelayMs: 150, // 可选:首字前停顿
  },
);

默认会发出单个完整 message.delta。只有经 withMockStreaming() 注入默认流速后,message / reasoning / tool_call 参数才会被拆成多个 delta。单个 step 也可用 stream: false 关闭包装器的默认流速配置。

核心思路是每轮请求执行一次 handler:

  • handler 会拿到 requestcontext
  • context 内建 previousReplaypendingToolCallshistory
  • handler 可脚本化发出 message / reasoning / tool_call / server_tool_*
  • message step 可附带 citations
  • 可注入 warningcontent_filter、transport interruption、provider-style error
  • 可用 assertMockRequest() 验证 replay / tool_result / serverTools 等期望
import { assertMockRequest, createAIClient, MockAdapter } from "@codehz/ai";

const client = createAIClient({
  adapter: new MockAdapter({
    handler: async function* (request, context) {
      if (context.turnIndex === 0) {
        assertMockRequest(
          request,
          {
            items: [{ type: "message", role: "user", textIncludes: "weather" }],
            tools: "present",
            toolChoice: "present",
          },
          context,
        );

        yield { type: "message", content: "Checking weather now." };
        yield {
          type: "tool_call",
          id: "mock-call-weather",
          name: "get_weather",
          argumentsText: '{"city":"Hangzhou"}',
        };
        return;
      }

      assertMockRequest(
        request,
        {
          requireReplayFromPreviousTurn: true,
          requireToolResultsForPendingCalls: true,
        },
        context,
      );

      yield { type: "message", content: "Hangzhou is 28C and sunny." };
    },
  }),
  model: "mock-model",
});

核心类型:

type MockHandler = (request: NormalizedRequest, context: MockHandlerContext) => AsyncIterable<MockStep>;

type MockHandlerContext = {
  turnIndex: number;
  previousReplay: ReplayItem[];
  pendingToolCalls: readonly ToolCallItem[];
  history: readonly MockHistoryRecord[];
};

测试工具循环时,第二轮通常会要求:

  • requireReplayFromPreviousTurn: true
  • requireToolResultsForPendingCalls: true

畸形路径示例:

yield { type: "message", content: "partial answer" };
yield { type: "interrupt" }; // 不发 response.completed,collectStream() 应失败
yield { type: "warning", message: "content filtered by policy", code: "CONTENT_FILTER" };
yield { type: "complete", stopReason: "content_filter" };

多轮对话

库不托管会话状态。调用方自行保留 response.replay 并在下一轮带回:

const transcript: InputItem[] = [{ type: "message", role: "user", content: [{ type: "text", text: "Hello" }] }];

const r1 = await collectStream(client.stream({ input: transcript }));
transcript.push(...r1.replay);
transcript.push({ type: "message", role: "user", content: [{ type: "text", text: "Continue" }] });

const r2 = await collectStream(client.stream({ input: transcript }));

详细示例见 examples/multi-turn.ts

服务端工具(serverTools

Provider 托管工具(不进客户端 tool loop)。首版由 ResponsesAdapter 落地:

| Canonical serverTools | Responses wire | 说明 | | ----------------------- | -------------------------- | ---------------------------------------------- | | web_search | type: "web_search" | 域名过滤、userLocationsearchContextSize | | code_execution | type: "code_interpreter" | 仅 auto container(memoryLimit / fileIds) | | mcp | type: "mcp" | 远程 MCP; requireApproval: "never" |

const stream = client.stream({
  input: [{ type: "message", role: "user", content: [{ type: "text", text: "杭州今天天气?" }] }],
  serverTools: [
    {
      type: "web_search",
      allowedDomains: ["example.com"],
      searchContextSize: "low",
    },
    {
      type: "code_execution",
      container: { type: "auto", memoryLimit: "4g" },
    },
    {
      type: "mcp",
      serverLabel: "dmcp",
      serverUrl: "https://dmcp-server.example/mcp",
      requireApproval: "never",
      // authorization 每请求由调用方重传;不会写入 opaque 回放
      authorization: process.env.MCP_TOKEN,
    },
  ],
});

const result = await collectStream(stream);
console.log(result.serverToolCalls);
console.log(result.serverToolResults);
// 消息 citations(url / container_file)挂在 MessageItem.citations

支持矩阵:

| Adapter | serverTools | | ---------------------------------------------------- | ---------------------------------------------------------------------- | | ResponsesAdapter | 请求映射 + SSE 解析 | | MockAdapter | 可脚本化产出 server_tool_* 事件与 citations | | ChatCompletions / Messages / Ollama / Gemini | 传入非空 serverToolsAIRequestErrorUNSUPPORTED_SERVER_TOOL) |

范围说明(刻意不做):

  • 客户端自动 tool-loop(仍由调用方编排)
  • computer_use / shell 托管
  • Chat Completions 搜索专用模型
  • MCP approval 交互回路(出现 mcp_approval_requestresponse.warning
  • Containers REST 管理 API

多轮续写推荐用 Responses 的 previous_response_id opaque replay,无需把 server tool result 当客户端 tool_result 回传。Mock 演示见 examples/server-tools.ts

手动工具循环

模型返回 tool_call → 调用方执行工具 → 下一轮带入 tool_result

const r1 = await collectStream(client.stream({ input, tools }));

for (const call of r1.toolCalls) {
  const result = await myTool(call);
  input.push(...r1.replay);
  input.push({
    type: "tool_result",
    callId: call.id,
    toolName: call.name,
    outcome: "success",
    content: [{ type: "json", json: result }],
  });
}

const r2 = await collectStream(client.stream({ input, tools }));

详细示例见 examples/tool-loop.ts

模拟流式

真实 adapter 在原生流不可用时,库内部会用 synthetic 路径包装为规范事件流。应用层一般只需消费 client.stream() / collectStream()0.5.0syntheticStream 不再从根入口导出(test-first helper,不展开 server_tool_*)。

若只需前端逐字预览效果,请优先使用 MockAdapter + withMockStreaming()(见上文 Mock 后端)。

辅助信息采集

usage / billing / providerMetadata 由 adapter 在流结束时经 response.auxiliaryAIResponse 字段交付。AuxiliaryCollector 是 provider 内部实现细节,0.5.0 起不再从根入口导出。lookup / postprocessBilling 为 experimental 扩展点,库内 HTTP adapter 默认未接线。

usagebillinginclude 支持三种模式:off 表示不采集;best_effort(默认)表示尽可能采集,缺失时静默;required 表示缺失时发出 USAGE_MISSINGBILLING_MISSING warning。估算 billing 仍会发出 BILLING_ESTIMATED warning。

开发命令

examples/ 下的三个示例默认都基于 MockAdapter,可直接运行,无需配置真实模型或 API key。

bun run typecheck    # TypeScript 类型检查
bun run test         # 运行全部测试
bun run example:basic
bun run example:multi-turn
bun run example:tool-loop

License

MIT