Appearance
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_call | tool 调用及参数 |
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() 适合需要实时展示流式进度或处理多类型输出的场景。