Skip to content

callModel 基于 OpenRouter Responses API 的 items 模型,而不是传统的消息-chunks 累积模式。核心区别在于:相同 ID 的 item 会被多次 emit,每次都是完整内容(而非增量),你只需按 ID 替换整条记录。这种设计天然契合 React 状态更新——无需手动拼接文本,直接用 Map 管理 item 状态即可。本页详细介绍 item 类型、React 集成示例,以及从旧版 getNewMessagesStream() 的迁移方法。

Items 范式与传统消息模式的对比

callModel 底层基于 OpenRouter Responses API,采用 items-based 模型,而不是 OpenAI Chat 或 Vercel AI SDK 那种消息+chunks 模式。

核心理念:items 会以相同 ID 被多次 emit,但内容逐步完整。你要做的是按 ID 替换整条 item,而不是累积 chunks。

传统方式(OpenAI Chat、Vercel AI)callModel(Items 原生)
接收 chunk,累积文本接收 item,按 ID 替换
单一 message 类型多种 item 类型
结束时重建完整内容每次 emit 都是完整数据
手动状态管理天然契合 React state

Item 类型

getItemsStream() 会 yield 以下类型的 item:

类型描述
message助手文本响应
function_calltool 调用及参数
reasoning模型推理过程(extended thinking)
web_search_call网页搜索操作
file_search_call文件搜索操作
image_generation_call图像生成操作
function_call_output已执行 tool 的返回结果

流式工作原理

每次迭代 yield 的是相同 ID 但内容已更新的完整 item

typescript
// 迭代 1
{ id: "msg_123", type: "message", content: [{ type: "output_text", text: "Hello" }] }

// 迭代 2
{ id: "msg_123", type: "message", content: [{ type: "output_text", text: "Hello world" }] }

// 迭代 3
{ id: "msg_123", type: "message", content: [{ type: "output_text", text: "Hello world!" }] }

function call 的参数也遵循相同规律——每次 emit 都包含到目前为止的完整(部分)JSON。

React 集成

Items 范式消除了手动 chunk 累积的需要。用以 item ID 为 key 的 Map 管理状态,让 React reconciliation 自动处理更新:

typescript
import { useState } from 'react';
import type { StreamableOutputItem } from '@openrouter/agent';
import { OpenRouter } from '@openrouter/agent';

const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });

function Chat() {
  const [items, setItems] = useState<Map<string, StreamableOutputItem>>(new Map());

  async function handleSubmit(input: string) {
    const result = client.callModel({
      model: 'anthropic/claude-sonnet-4',
      input,
    });

    for await (const item of result.getItemsStream()) {
      // 按 ID 替换整条 item,React 自动触发重渲染
      setItems((prev) => new Map(prev).set(item.id, item));
    }
  }

  return (
    <div>
      <div>
        {[...items.values()].map((item) => (
          <ItemRenderer key={item.id} item={item} />
        ))}
      </div>
    </div>
  );
}

function ItemRenderer({ item }: { item: StreamableOutputItem }) {
  switch (item.type) {
    case 'message':
      return <MessageItem message={item} />;
    case 'function_call':
      return <ToolCallItem call={item} />;
    case 'reasoning':
      return <ReasoningItem reasoning={item} />;
    default:
      return null;
  }
}

优势

  • 无需 chunk 累积 — 每次 item emit 都是完整数据
  • 天然 React 更新 — setState 触发自动重渲染
  • 并发 item 处理 — function call 和 message 可同时流式输出
  • 兼容 React 18+ — 支持并发特性和 Suspense
  • 完整类型安全 — 所有 item 类型都有 TypeScript 推断

与 Chunk 累积方式的对比

typescript
// 传统方式 - 手动累积
const [text, setText] = useState('');

for await (const chunk of result.getTextStream()) {
  setText((prev) => prev + chunk); // 必须手动拼接
}

// Items 方式 - 按 ID 替换
for await (const item of result.getItemsStream()) {
  setItems((prev) => new Map(prev).set(item.id, item)); // 完整替换
}

Items 方式在模型同时产出多个输出时尤为强大(例如:thinking + tool calls + 文本并发)。

从 getNewMessagesStream() 迁移

getNewMessagesStream() 已废弃,请改用 getItemsStream()

typescript
// 旧版(已废弃)
for await (const message of result.getNewMessagesStream()) {
  if (message.type === 'message') {
    console.log(message.content);
  }
}

// 新版
for await (const item of result.getItemsStream()) {
  if (item.type === 'message') {
    console.log(item.content);
  }
}

主要区别:getItemsStream() 包含所有 item 类型(reasoning、function calls 等),而不仅仅是 messages。

常见问题

Q: 为什么不直接用 getTextStream() 就好了?

A: getTextStream() 只返回纯文本增量,无法区分 reasoning、tool call 等不同类型的输出。当模型同时进行推理和工具调用时,getItemsStream() 才能让你正确展示每个环节的进度。

Q: item 的 ID 是否稳定,可以作为 React key?

A: 是的。同一条 item 的 ID 在整个流式过程中保持不变,可以安全地用作 React key 和 Map key。

Q: 如果只关心最终文本,还需要用 getItemsStream() 吗?

A: 不需要。如果只需要最终文本,直接用 await result.getText() 即可,更简洁。getItemsStream() 适合需要实时展示流式进度或处理多类型输出的场景。