OpenAI 兼容接口

这一页说明怎么接一个 OpenAI 兼容的 Chat Completions 接口:每个函数收到什么、要返回什么,以及这个协议要注意的地方。

结论

  • 本适配器消费 Chat Completions 流,不接 OpenAI Responses API;最小配置只给 run。
  • 停止、续接、插话、本文的历史和会话结构属于 SDK 接入扩展,要使用时由后端按后端约定另外提供。
  • SDK 不把 chunk.id 当作轮次编号;需要远端停止、续接和历史时,后端应保存能力函数收到的 runId。
  • 请求内容里没有模型名,由接入方在函数里补。
  • 上下文由运行时当前持有的消息编码,不会自动获取尚未加载的历史;systemPrompt 可额外加入系统消息。
能力本 SDK 的接入方式给函数之前要确认
run有—
regenerate / edit见下文后端不存会话内容时直接复用 run;存的话要能作废旧内容
abort接入方扩展后端提供了按轮次编号停止的接口
resume接入方扩展后端存了每一轮的事件,流里带事件编号
steer接入方扩展后端提供了给进行中的一轮追加消息的接口
history接入方扩展后端按约定的格式返回历史

最简接入

只给 run,直连模型服务:

adapter.ts
import { createOpenAiAdapter } from '@volcengine/chatkit/adapters/openai';

// 直连模型服务,只用于本地开发:密钥从本机的环境变量读,不要提交、不要上线
const BASE = 'https://llm.example.com/v1';
const KEY = import.meta.env.VITE_LLM_KEY;
const MODEL = 'your-model-id';

export const adapter = createOpenAiAdapter({
  run: ({ body }) =>
    fetch(`${BASE}/chat/completions`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${KEY}` },
      // 请求内容里没有模型名,在这里补
      body: JSON.stringify({ model: MODEL, ...body }),
    }),
});
有什么没有什么
多轮对话(上下文由前端带)会话列表
流式显示、思考过程历史:刷新后界面清空
停止(需配置 abort)重新生成、编辑后重发
密钥

直连的写法会把密钥放进浏览器,任何打开页面的人都能拿到。只用于本地开发,上线前换成下面的写法。

走自己的代理

生产环境的请求发给自己的服务:

adapter.ts
import { createOpenAiAdapter } from '@volcengine/chatkit/adapters/openai';

// 请求发给自己的服务。模型名、密钥由它在服务端补,浏览器里没有
export const adapter = createOpenAiAdapter({
  run: ({ sessionId, runId, body }) =>
    fetch('/api/chat/completions', {
      method: 'POST',
      credentials: 'include', // 带上登录态,服务端据此知道用户是谁
      headers: { 'Content-Type': 'application/json' },
      // 会话编号和轮次编号带给服务端,它要存历史、支持停止时用得上
      body: JSON.stringify({ session_id: sessionId, run_id: runId, ...body }),
    }),
});

代理要做的事:

要点说明
补上模型名和密钥由代理决定服务密钥与允许的模型;示例也可在能力函数中传模型名
用户是谁从登录态取不要让前端传用户 ID
事件流原样透传不要缓冲
连接断开要传到上游用户点停止时前端会断开请求,代理要跟着断开和模型服务的连接,是否停止远端执行仍需上游取消能力确认
限制请求的内容代理可以检查 messages 的长度、tools 的内容,不要把前端的请求原样放大给模型

要支持历史、停止、续接时,这一层还要存东西,见后端不符合约定时 · 加自己的服务层。

全部能力

adapter.ts
import { createOpenAiAdapter } from '@volcengine/chatkit/adapters/openai';

// 地址都是自己后端的,下面只是一种排法
const BASE = '/api/chat';
const send = (method: string, path: string, body?: unknown) =>
  fetch(`${BASE}${path}`, {
    method,
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
const query = (cursor?: string) => (cursor ? `?cursor=${encodeURIComponent(cursor)}` : '');

export const adapter = createOpenAiAdapter({
  // 协议没有「一轮」的编号:把 runId 带给后端,后端存下来,停止、续接、历史里都用它
  run: ({ sessionId, runId, body }) => send('POST', `/sessions/${sessionId}/runs`, { run_id: runId, ...body }),
  regenerate: ({ sessionId, runId, targetRunId, body }) =>
    send('POST', `/sessions/${sessionId}/runs`, { run_id: runId, replace_from: targetRunId, ...body }),
  edit: ({ sessionId, runId, targetRunId, body }) =>
    send('POST', `/sessions/${sessionId}/runs`, { run_id: runId, replace_from: targetRunId, ...body }),
  // 最后收到的事件编号放进请求头,后端只发它之后的事件
  resume: ({ sessionId, runId, eventId }) =>
    fetch(`${BASE}/sessions/${sessionId}/runs/${runId}/events`, {
      credentials: 'include',
      headers: { 'Last-Event-ID': eventId },
    }),
  abort: ({ sessionId, runId }) => send('POST', `/sessions/${sessionId}/runs/${runId}/cancel`, {}),
  steer: ({ sessionId, runId, body }) => send('POST', `/sessions/${sessionId}/runs/${runId}/messages`, body),
  history: ({ sessionId, cursor }) => send('GET', `/sessions/${sessionId}/messages${query(cursor)}`),
});

地址和字段名只是一种排法,以和后端团队商定的为准。

每个函数收到什么、要返回什么

表中“事件流”指未配置 subscribe 时的 Response 或异步事件序列。配置 subscribe 后,run / resume / regenerate / edit 只返回受理 Response,输出全部走订阅;其余能力仍返回 Response。

函数请求上的字段请求内容 body要返回的响应
runsessionId、runId、body{ messages, stream: true, stream_options, tools? }流,每块是 chat.completion.chunk
regenerate / edit同 run,多一个 targetRunId同 run。messages 里是目标轮之前的内容加上输入同 run
resumesessionId、runId、eventId没有流,格式同 run
steersessionId、runId、body{ messages: [追加的那条 user 消息] }只看状态码
abortsessionId、runId没有只看状态码
historysessionId、cursor?没有{ messages, runs?, cursor?, has_more? }
要点说明
body 里没有模型名在函数里补:{ model, ...body }。temperature 这类采样参数同理
默认请求用量统计body 里带 stream_options: { include_usage: true }。后端不认时在 run 里去掉这个字段
一轮里可能调用多次 run有工具调用时,每请求一次模型调用一次 run,runId 不变
重做只有第一次走重做的函数重新生成、编辑后重发只有第一次请求走 regenerate / edit,工具调用之后的请求走 run

响应的格式和例子见后端约定 · OpenAI 兼容接口。

编号

编号取自说明
会话编号sessionId协议里没有会话,要不要带给后端由接入方决定
轮次编号前端发起时生成的编号所有函数收到的 runId、targetRunId 都是它。流块的 id 标识的是一次模型调用,不用它
事件编号SSE 的 id:流里没有 id: 时没有事件编号,续接不可用

业务状态与内容扩展

聊天里的业务字段通过 transformOutput 转成内容变更。独立业务状态由应用维护,再交给插槽组件;适配器不提供业务状态通知或累积状态 getter。

输入怎么放进请求

输入里的放在哪
正文 text最后一条 user 消息
附件只支持图片,放进 user 消息的图片内容
引用不进请求,只挂在界面上的用户消息上。正文里已经有 @名称
metadata不进请求,只挂在界面上的用户消息上

后端认别的字段时用 transformInput,返回完整请求体,覆盖默认编码后的请求内容:

adapter.ts
import { createOpenAiAdapter } from '@volcengine/chatkit/adapters/openai';

export const adapter = createOpenAiAdapter({
  run: ({ body }) =>
    fetch('/api/chat/completions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }),
  // 默认协议编码完成后,返回完整请求体。
  transformInput: (input, { defaultInput }) => ({
    ...defaultInput,
    references: input?.content.filter(part => part.type === 'reference') ?? [],
  }),
});

输入里带了图片以外的附件时不发请求,这一轮以 UNSUPPORTED_ATTACHMENT 失败。

注意点

重新生成与编辑后重发

后端怎么做
不存会话内容请求携带目标轮之前已加载的上下文和本轮输入,可复用 run;不自动补取未加载历史
存会话内容在函数里把 targetRunId 带给后端,后端作废那一轮和它之后的内容
adapter.ts
import { createOpenAiAdapter, type OpenAiAdapterOptions } from '@volcengine/chatkit/adapters/openai';

const run: OpenAiAdapterOptions['run'] = ({ body }) =>
  fetch('/api/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });

// 后端不存会话内容:每次请求都带着完整的上下文,重新生成和编辑后重发就是再发起一轮
export const adapter = createOpenAiAdapter({ run, regenerate: run, edit: run });

刷新后续接接不上工具循环

工具在浏览器里执行时,一轮里发给模型的上下文只在页面内存里。刷新后续接回来的流如果要求执行工具, 这一轮以 TOOL_LOOP_NOT_RESUMABLE 失败。未刷新且该轮内存状态仍在时可以继续,但后端仍须保证重放无重复、无缺失。

思考过程

后端用的字段支持
delta.reasoning_content是
delta.reasoning是

流中途断开

连接状态与执行结果分开:断线或缺少终态只表示结果未知。给了 history 时刷新消息与轮次结论;有已收到的游标与 resume 时续流。恢复不重新发起用户问题;续接后进入工具循环时仍可能调用 run 请求下一模型步骤。停止后仍接收输出,远端受理取消不等于已经停止。详见断线恢复与停止确认。

错误

情况错误码能不能重试
返回非 2xx,响应内容是 { error }error.code,没有则 error.type408、409、429、5xx 标记 retryable;不自动重发
返回非 2xx,响应内容不是 JSONHTTP_{状态码}同上
流里发了 { error }code / type,缺省 STREAM_ERROR—
连接中断或请求抛错执行结果未知可查询或续流,不重新发起用户问题
刷新后续接回来的回答要求执行工具TOOL_LOOP_NOT_RESUMABLE—
请求模型的次数超限MAX_STEPS_EXCEEDED—
输入里有不支持的附件UNSUPPORTED_ATTACHMENT—

会话业务

会话目录、创建、删除、改名和 fork 由应用调用业务 API,取得真实 sessionId 后传给 useChatSessions。Adapter 不提供 sessions 配置。历史仍由 history 能力函数转换。详见会话契约。

模型选项与工具循环

openai-tools.ts
import { createOpenAiAdapter } from '@volcengine/chatkit/adapters/openai';

export const adapter = createOpenAiAdapter({
  run: ({ body }) =>
    fetch('/api/chat/completions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }),
  systemPrompt: '你是一个天气助手',
  // 工具在浏览器里执行:模型要求调用时按名字执行,结果带回去再请求一次
  tools: {
    weather: {
      description: '查询城市当前的天气',
      parameters: {
        type: 'object',
        properties: { city: { type: 'string', description: '城市名' } },
        required: ['city'],
      },
      execute: async (args, { signal }) => {
        const { city } = args as { city: string };
        const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}`, { signal });
        return response.json();
      },
    },
  },
  // 一轮里最多请求模型几次
  maxSteps: 4,
});
  • systemPrompt 放在每次编码的上下文最前面;tools 按名字提供 description、parameters 和 execute(args, { toolCallId })。工具顺序执行,结果回传模型;工具抛错或不存在时,工具失败信息也会回传,未必立即结束整轮。
  • maxSteps 默认 8,限制一轮模型请求次数。transformInput 对每次模型请求都执行;重新生成或编辑后的后续工具步骤以 capability: run 调用。
  • finish_reason 表示当前模型步骤有结论,tool_calls 仍会继续工具循环。直接 HTTP 流也识别 [DONE];只收到 finish_reason 时读取器仍等流结束,才能处理剩余用量信息。异步序列使用带 finish_reason 的 chunk 后结束序列,不传字符串 [DONE]。
  • reasoning_content / reasoning 是本 SDK 兼容的增量字段,不承诺每个 Chat Completions 服务都返回思考内容。
  • 直接流没有按 SSE id 去重,后端 resume 必须排除已交付的增量。多个 choices 的内容会依次写到同一回答,当前不提供多候选分栏,应让后端只返回一个 choice。
  • 历史需要 run_id 才能归并思考、工具及同轮多条助手消息;无 run_id 的助手消息仅保留普通正文。run_id 的存在不等于有执行结论,还需 runs。