chatBridgeTranslate.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. /**
  2. * Responses API ↔ Chat Completions 的翻译层(纯函数 + SSE 状态机,零 IO)。
  3. *
  4. * Codex 0.155+ 只会发 Responses API;chat-only 端点(老 Ollama / vLLM / Xinference)
  5. * 由 chatBridgeService 在本地起代理,请求与响应的格式转换全部集中在这里。
  6. * 转换是有损的,丢弃项一律走 onDiagnostic 留痕,绝不静默。
  7. */
  8. // ── 结构类型(只声明用到的字段,其余原样忽略) ─────────────────────────────
  9. export interface ResponsesInputItem {
  10. type?: string;
  11. role?: string;
  12. content?: unknown;
  13. call_id?: string;
  14. name?: string;
  15. arguments?: string;
  16. output?: unknown;
  17. [key: string]: unknown;
  18. }
  19. export interface ResponsesTool {
  20. type?: string;
  21. name?: string;
  22. description?: string;
  23. parameters?: unknown;
  24. [key: string]: unknown;
  25. }
  26. export interface ResponsesCreateParams {
  27. model?: string;
  28. instructions?: string | null;
  29. input?: string | ResponsesInputItem[];
  30. tools?: ResponsesTool[];
  31. tool_choice?: unknown;
  32. max_output_tokens?: number | null;
  33. temperature?: number | null;
  34. top_p?: number | null;
  35. parallel_tool_calls?: boolean;
  36. stream?: boolean;
  37. [key: string]: unknown;
  38. }
  39. export interface ChatContentPart {
  40. type: string;
  41. text?: string;
  42. image_url?: { url: string };
  43. }
  44. export interface ChatToolCall {
  45. id: string;
  46. type: 'function';
  47. function: { name: string; arguments: string };
  48. }
  49. export interface ChatMessage {
  50. role: string;
  51. content?: string | ChatContentPart[] | null;
  52. tool_calls?: ChatToolCall[];
  53. tool_call_id?: string;
  54. }
  55. export interface ChatCompletionRequest {
  56. model?: string;
  57. messages: ChatMessage[];
  58. tools?: Array<{ type: 'function'; function: { name?: string; description?: string; parameters?: unknown } }>;
  59. tool_choice?: unknown;
  60. max_tokens?: number | null;
  61. temperature?: number | null;
  62. top_p?: number | null;
  63. parallel_tool_calls?: boolean;
  64. stream?: boolean;
  65. stream_options?: { include_usage?: boolean };
  66. }
  67. export interface ChatUsage {
  68. prompt_tokens?: number;
  69. completion_tokens?: number;
  70. total_tokens?: number;
  71. }
  72. export interface ResponsesUsage {
  73. input_tokens: number;
  74. output_tokens: number;
  75. total_tokens: number;
  76. }
  77. export interface ChatCompletionLike {
  78. id?: string;
  79. created?: number;
  80. model?: string;
  81. choices?: Array<{
  82. finish_reason?: string | null;
  83. message?: {
  84. content?: string | null;
  85. tool_calls?: ChatToolCall[];
  86. reasoning_content?: string;
  87. };
  88. }>;
  89. usage?: ChatUsage;
  90. }
  91. export interface BridgeSseEvent {
  92. event: string;
  93. data: Record<string, unknown>;
  94. }
  95. export type DiagnosticFn = (line: string) => void;
  96. // ── 小工具 ────────────────────────────────────────────────────────────────
  97. let idCounter = 0;
  98. function newId(prefix: string): string {
  99. idCounter += 1;
  100. return `${prefix}_${Date.now().toString(36)}${idCounter.toString(36)}${Math.random().toString(36).slice(2, 8)}`;
  101. }
  102. function mapUsage(usage: ChatUsage | null | undefined): ResponsesUsage {
  103. return {
  104. input_tokens: usage?.prompt_tokens ?? 0,
  105. output_tokens: usage?.completion_tokens ?? 0,
  106. total_tokens: usage?.total_tokens ?? 0,
  107. };
  108. }
  109. // ── 请求方向:Responses → Chat ─────────────────────────────────────────────
  110. function convertMessageItem(item: ResponsesInputItem, onDiagnostic?: DiagnosticFn): ChatMessage | null {
  111. const role = typeof item.role === 'string' && item.role ? item.role : 'user';
  112. const content = item.content;
  113. if (typeof content === 'string') return { role, content };
  114. if (Array.isArray(content)) {
  115. const parts: ChatContentPart[] = [];
  116. for (const part of content) {
  117. if (!part || typeof part !== 'object') continue;
  118. const record = part as { type?: string; text?: unknown; image_url?: unknown };
  119. if ((record.type === 'input_text' || record.type === 'output_text') && typeof record.text === 'string') {
  120. parts.push({ type: 'text', text: record.text });
  121. } else if (record.type === 'input_image' && typeof record.image_url === 'string') {
  122. parts.push({ type: 'image_url', image_url: { url: record.image_url } });
  123. } else {
  124. onDiagnostic?.(`chat-bridge: 丢弃不支持的 content part(${record.type ?? '未知类型'})`);
  125. }
  126. }
  127. if (!parts.length) return null;
  128. // 单文本压成 string,兼容性最好
  129. if (parts.length === 1 && parts[0].type === 'text') return { role, content: parts[0].text ?? '' };
  130. return { role, content: parts };
  131. }
  132. return null;
  133. }
  134. function convertToolChoice(choice: unknown): unknown {
  135. if (choice === 'auto' || choice === 'none' || choice === 'required') return choice;
  136. if (choice && typeof choice === 'object') {
  137. const record = choice as { type?: string; name?: unknown };
  138. if (record.type === 'function' && typeof record.name === 'string') {
  139. return { type: 'function', function: { name: record.name } };
  140. }
  141. }
  142. return undefined;
  143. }
  144. export function responsesRequestToChat(req: ResponsesCreateParams, onDiagnostic?: DiagnosticFn): ChatCompletionRequest {
  145. const messages: ChatMessage[] = [];
  146. if (typeof req.instructions === 'string' && req.instructions) {
  147. messages.push({ role: 'developer', content: req.instructions });
  148. }
  149. const input = req.input;
  150. if (typeof input === 'string') {
  151. if (input) messages.push({ role: 'user', content: input });
  152. } else if (Array.isArray(input)) {
  153. for (const item of input) {
  154. if (!item || typeof item !== 'object') continue;
  155. // easy-input-message 形式({role, content} 无 type 字段)按 message 处理
  156. const type = typeof item.type === 'string' ? item.type : item.role ? 'message' : null;
  157. if (type === 'message') {
  158. const converted = convertMessageItem(item, onDiagnostic);
  159. if (converted) messages.push(converted);
  160. } else if (type === 'function_call' || type === 'custom_tool_call') {
  161. messages.push({
  162. role: 'assistant',
  163. content: null,
  164. tool_calls: [
  165. {
  166. id: item.call_id ?? '',
  167. type: 'function',
  168. function: {
  169. name: item.name ?? '',
  170. arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? ''),
  171. },
  172. },
  173. ],
  174. });
  175. } else if (type === 'function_call_output' || type === 'custom_tool_call_output') {
  176. messages.push({
  177. role: 'tool',
  178. tool_call_id: item.call_id ?? '',
  179. content: typeof item.output === 'string' ? item.output : JSON.stringify(item.output ?? ''),
  180. });
  181. } else {
  182. // reasoning / item_reference / web_search_call 等:chat 协议没有对应物
  183. onDiagnostic?.(`chat-bridge: 丢弃不支持的 input item(${type ?? '未知类型'})`);
  184. }
  185. }
  186. }
  187. const request: ChatCompletionRequest = { messages };
  188. if (req.model) request.model = req.model;
  189. if (Array.isArray(req.tools)) {
  190. const tools = req.tools
  191. .filter((tool) => tool && tool.type === 'function')
  192. .map((tool) => ({
  193. type: 'function' as const,
  194. function: { name: tool.name, description: tool.description, parameters: tool.parameters },
  195. }));
  196. if (tools.length) request.tools = tools;
  197. }
  198. const toolChoice = convertToolChoice(req.tool_choice);
  199. if (toolChoice !== undefined) request.tool_choice = toolChoice;
  200. if (typeof req.max_output_tokens === 'number') request.max_tokens = req.max_output_tokens;
  201. if (typeof req.temperature === 'number') request.temperature = req.temperature;
  202. if (typeof req.top_p === 'number') request.top_p = req.top_p;
  203. if (typeof req.parallel_tool_calls === 'boolean') request.parallel_tool_calls = req.parallel_tool_calls;
  204. if (req.stream) {
  205. request.stream = true;
  206. // 让上游在最后一个 chunk 带 usage;不支持的端点会忽略,下游有全零兜底
  207. request.stream_options = { include_usage: true };
  208. }
  209. return request;
  210. }
  211. // ── 非流式响应方向:Chat → Responses ───────────────────────────────────────
  212. export function chatResponseToResponses(chat: ChatCompletionLike, req: ResponsesCreateParams): Record<string, unknown> {
  213. const choice = chat.choices?.[0];
  214. const message = choice?.message ?? {};
  215. const output: Array<Record<string, unknown>> = [];
  216. if (typeof message.reasoning_content === 'string' && message.reasoning_content) {
  217. output.push({
  218. id: newId('rs'),
  219. type: 'reasoning',
  220. summary: [{ type: 'summary_text', text: message.reasoning_content }],
  221. });
  222. }
  223. if (typeof message.content === 'string' && message.content) {
  224. output.push({
  225. id: newId('msg'),
  226. type: 'message',
  227. role: 'assistant',
  228. status: 'completed',
  229. content: [{ type: 'output_text', text: message.content, annotations: [] }],
  230. });
  231. }
  232. if (Array.isArray(message.tool_calls)) {
  233. for (const call of message.tool_calls) {
  234. output.push({
  235. id: newId('fc'),
  236. type: 'function_call',
  237. call_id: call.id ?? '',
  238. name: call.function?.name ?? '',
  239. arguments: call.function?.arguments ?? '',
  240. status: 'completed',
  241. });
  242. }
  243. }
  244. const incomplete = choice?.finish_reason === 'length';
  245. return {
  246. id: chat.id || newId('resp'),
  247. object: 'response',
  248. created_at: chat.created ?? Math.floor(Date.now() / 1000),
  249. model: chat.model ?? req.model ?? '',
  250. status: incomplete ? 'incomplete' : 'completed',
  251. ...(incomplete ? { incomplete_details: { reason: 'max_output_tokens' } } : {}),
  252. output,
  253. parallel_tool_calls: req.parallel_tool_calls ?? true,
  254. tool_choice: req.tool_choice ?? 'auto',
  255. tools: req.tools ?? [],
  256. usage: mapUsage(chat.usage),
  257. };
  258. }
  259. // ── SSE 流式状态机:chat.completion.chunk → Responses 语义事件 ─────────────
  260. interface TextItemState {
  261. itemId: string;
  262. outputIndex: number;
  263. open: boolean;
  264. text: string;
  265. }
  266. interface ReasoningItemState {
  267. itemId: string;
  268. outputIndex: number;
  269. open: boolean;
  270. text: string;
  271. }
  272. interface ToolCallState {
  273. itemId: string;
  274. outputIndex: number;
  275. callId: string;
  276. name: string;
  277. argsBuffer: string;
  278. open: boolean;
  279. }
  280. export class ResponsesSseTranslator {
  281. private readonly model: string;
  282. private readonly onDiagnostic?: DiagnosticFn;
  283. private readonly responseId = newId('resp');
  284. private readonly createdAt = Math.floor(Date.now() / 1000);
  285. private lineBuffer = '';
  286. private started = false;
  287. private completed = false;
  288. private nextOutputIndex = 0;
  289. private textItem: TextItemState | null = null;
  290. private reasoningItem: ReasoningItemState | null = null;
  291. private readonly toolCalls = new Map<number, ToolCallState>();
  292. private finishReason: string | null = null;
  293. private usage: ResponsesUsage | null = null;
  294. constructor(req: ResponsesCreateParams, onDiagnostic?: DiagnosticFn) {
  295. this.model = req.model ?? '';
  296. this.onDiagnostic = onDiagnostic;
  297. }
  298. /** 喂入上游 SSE 文本片段,吐出 0..n 个待发送的 Responses SSE 事件 */
  299. push(chunk: string): BridgeSseEvent[] {
  300. const events: BridgeSseEvent[] = [];
  301. this.lineBuffer += chunk;
  302. const lines = this.lineBuffer.split(/\r?\n/u);
  303. this.lineBuffer = lines.pop() ?? '';
  304. for (const line of lines) {
  305. const trimmed = line.trim();
  306. if (!trimmed || !trimmed.startsWith('data:')) continue; // event:/id:/注释行忽略
  307. const payload = trimmed.slice(5).trim();
  308. if (payload === '[DONE]') {
  309. events.push(...this.finish());
  310. continue;
  311. }
  312. let parsed: unknown;
  313. try {
  314. parsed = JSON.parse(payload);
  315. } catch {
  316. this.onDiagnostic?.('chat-bridge: 跳过无法解析的上游 SSE 行');
  317. continue;
  318. }
  319. events.push(...this.processChunk(parsed as Record<string, unknown>));
  320. }
  321. return events;
  322. }
  323. /** 上游流结束(可能没收到 [DONE]):闭合未闭 item 并补 response.completed */
  324. finish(): BridgeSseEvent[] {
  325. if (this.completed) return [];
  326. this.completed = true;
  327. const events: BridgeSseEvent[] = [];
  328. if (!this.started) {
  329. this.started = true;
  330. events.push(...this.openResponse());
  331. }
  332. events.push(...this.closeAllItems());
  333. const incomplete = this.finishReason === 'length';
  334. const response: Record<string, unknown> = {
  335. ...this.responseSkeleton(incomplete ? 'incomplete' : 'completed'),
  336. output: this.buildOutput(),
  337. usage: this.usage ?? mapUsage(null),
  338. };
  339. if (incomplete) response.incomplete_details = { reason: 'max_output_tokens' };
  340. events.push(this.event('response.completed', { response }));
  341. return events;
  342. }
  343. /** 流中途出错:补一个 response.failed 再交给 server 关闭 */
  344. fail(message: string): BridgeSseEvent[] {
  345. if (this.completed) return [];
  346. this.completed = true;
  347. const events: BridgeSseEvent[] = [];
  348. if (!this.started) {
  349. this.started = true;
  350. events.push(...this.openResponse());
  351. }
  352. const response: Record<string, unknown> = {
  353. ...this.responseSkeleton('failed'),
  354. output: this.buildOutput(),
  355. error: { code: 'upstream_error', message },
  356. usage: this.usage ?? mapUsage(null),
  357. };
  358. events.push(this.event('response.failed', { response }));
  359. return events;
  360. }
  361. // ── 内部 ──
  362. private event(type: string, data: Record<string, unknown>): BridgeSseEvent {
  363. return { event: type, data: { type, ...data } };
  364. }
  365. private responseSkeleton(status: string): Record<string, unknown> {
  366. return {
  367. id: this.responseId,
  368. object: 'response',
  369. created_at: this.createdAt,
  370. model: this.model,
  371. status,
  372. output: [],
  373. parallel_tool_calls: true,
  374. tool_choice: 'auto',
  375. tools: [],
  376. usage: null,
  377. };
  378. }
  379. private openResponse(): BridgeSseEvent[] {
  380. const response = this.responseSkeleton('in_progress');
  381. return [this.event('response.created', { response }), this.event('response.in_progress', { response })];
  382. }
  383. private ensureStarted(): BridgeSseEvent[] {
  384. if (this.started) return [];
  385. this.started = true;
  386. return this.openResponse();
  387. }
  388. private processChunk(chunk: Record<string, unknown>): BridgeSseEvent[] {
  389. const events: BridgeSseEvent[] = [];
  390. const usage = chunk.usage as ChatUsage | undefined;
  391. if (usage) this.usage = mapUsage(usage);
  392. const choices = chunk.choices as Array<Record<string, unknown>> | undefined;
  393. const choice = choices?.[0];
  394. if (!choice) return events; // usage-only chunk
  395. events.push(...this.ensureStarted());
  396. const delta = (choice.delta ?? {}) as {
  397. content?: unknown;
  398. reasoning_content?: unknown;
  399. tool_calls?: Array<{ index?: number; id?: string; function?: { name?: string; arguments?: string } }>;
  400. };
  401. if (typeof delta.reasoning_content === 'string' && delta.reasoning_content) {
  402. events.push(...this.pushReasoningDelta(delta.reasoning_content));
  403. }
  404. if (typeof delta.content === 'string' && delta.content) {
  405. events.push(...this.pushTextDelta(delta.content));
  406. }
  407. if (Array.isArray(delta.tool_calls)) {
  408. for (const call of delta.tool_calls) {
  409. events.push(...this.pushToolCallDelta(call));
  410. }
  411. }
  412. if (typeof choice.finish_reason === 'string' && choice.finish_reason) {
  413. this.finishReason = choice.finish_reason;
  414. events.push(...this.closeAllItems());
  415. }
  416. return events;
  417. }
  418. private closeReasoningIfOpen(exceptOpen = false): BridgeSseEvent[] {
  419. const state = this.reasoningItem;
  420. if (!state || !state.open || exceptOpen) return [];
  421. state.open = false;
  422. return [
  423. this.event('response.reasoning_summary_text.done', {
  424. item_id: state.itemId,
  425. output_index: state.outputIndex,
  426. summary_index: 0,
  427. text: state.text,
  428. }),
  429. this.event('response.reasoning_summary_part.done', {
  430. item_id: state.itemId,
  431. output_index: state.outputIndex,
  432. summary_index: 0,
  433. part: { type: 'summary_text', text: state.text },
  434. }),
  435. this.event('response.output_item.done', {
  436. output_index: state.outputIndex,
  437. item: {
  438. id: state.itemId,
  439. type: 'reasoning',
  440. summary: state.text ? [{ type: 'summary_text', text: state.text }] : [],
  441. },
  442. }),
  443. ];
  444. }
  445. private closeTextIfOpen(): BridgeSseEvent[] {
  446. const state = this.textItem;
  447. if (!state || !state.open) return [];
  448. state.open = false;
  449. const part = { type: 'output_text', text: state.text, annotations: [] as unknown[] };
  450. return [
  451. this.event('response.output_text.done', {
  452. item_id: state.itemId,
  453. output_index: state.outputIndex,
  454. content_index: 0,
  455. text: state.text,
  456. }),
  457. this.event('response.content_part.done', {
  458. item_id: state.itemId,
  459. output_index: state.outputIndex,
  460. content_index: 0,
  461. part,
  462. }),
  463. this.event('response.output_item.done', {
  464. output_index: state.outputIndex,
  465. item: { id: state.itemId, type: 'message', role: 'assistant', status: 'completed', content: [part] },
  466. }),
  467. ];
  468. }
  469. private closeToolCallIfOpen(state: ToolCallState): BridgeSseEvent[] {
  470. if (!state.open) return [];
  471. state.open = false;
  472. return [
  473. this.event('response.function_call_arguments.done', {
  474. item_id: state.itemId,
  475. output_index: state.outputIndex,
  476. arguments: state.argsBuffer,
  477. }),
  478. this.event('response.output_item.done', {
  479. output_index: state.outputIndex,
  480. item: {
  481. id: state.itemId,
  482. type: 'function_call',
  483. call_id: state.callId,
  484. name: state.name,
  485. arguments: state.argsBuffer,
  486. status: 'completed',
  487. },
  488. }),
  489. ];
  490. }
  491. private closeAllItems(): BridgeSseEvent[] {
  492. const events: BridgeSseEvent[] = [];
  493. events.push(...this.closeReasoningIfOpen());
  494. events.push(...this.closeTextIfOpen());
  495. for (const state of this.toolCalls.values()) {
  496. events.push(...this.closeToolCallIfOpen(state));
  497. }
  498. return events;
  499. }
  500. private pushReasoningDelta(text: string): BridgeSseEvent[] {
  501. const events: BridgeSseEvent[] = [];
  502. if (!this.reasoningItem) {
  503. this.reasoningItem = { itemId: newId('rs'), outputIndex: this.nextOutputIndex, open: true, text: '' };
  504. this.nextOutputIndex += 1;
  505. events.push(
  506. this.event('response.output_item.added', {
  507. output_index: this.reasoningItem.outputIndex,
  508. item: { id: this.reasoningItem.itemId, type: 'reasoning', summary: [] },
  509. }),
  510. this.event('response.reasoning_summary_part.added', {
  511. item_id: this.reasoningItem.itemId,
  512. output_index: this.reasoningItem.outputIndex,
  513. summary_index: 0,
  514. part: { type: 'summary_text', text: '' },
  515. }),
  516. );
  517. }
  518. const state = this.reasoningItem;
  519. state.text += text;
  520. events.push(
  521. this.event('response.reasoning_summary_text.delta', {
  522. item_id: state.itemId,
  523. output_index: state.outputIndex,
  524. summary_index: 0,
  525. delta: text,
  526. }),
  527. );
  528. return events;
  529. }
  530. private pushTextDelta(text: string): BridgeSseEvent[] {
  531. const events: BridgeSseEvent[] = [];
  532. events.push(...this.closeReasoningIfOpen());
  533. if (!this.textItem) {
  534. this.textItem = { itemId: newId('msg'), outputIndex: this.nextOutputIndex, open: true, text: '' };
  535. this.nextOutputIndex += 1;
  536. events.push(
  537. this.event('response.output_item.added', {
  538. output_index: this.textItem.outputIndex,
  539. item: { id: this.textItem.itemId, type: 'message', role: 'assistant', status: 'in_progress', content: [] },
  540. }),
  541. this.event('response.content_part.added', {
  542. item_id: this.textItem.itemId,
  543. output_index: this.textItem.outputIndex,
  544. content_index: 0,
  545. part: { type: 'output_text', text: '', annotations: [] },
  546. }),
  547. );
  548. }
  549. const state = this.textItem;
  550. state.text += text;
  551. events.push(
  552. this.event('response.output_text.delta', {
  553. item_id: state.itemId,
  554. output_index: state.outputIndex,
  555. content_index: 0,
  556. delta: text,
  557. }),
  558. );
  559. return events;
  560. }
  561. private pushToolCallDelta(call: { index?: number; id?: string; function?: { name?: string; arguments?: string } }): BridgeSseEvent[] {
  562. const events: BridgeSseEvent[] = [];
  563. const index = typeof call.index === 'number' ? call.index : 0;
  564. let state = this.toolCalls.get(index);
  565. if (!state) {
  566. // chat 流里 content 总在 tool_calls 之前;保守起见仍先闭合文本与 reasoning
  567. events.push(...this.closeReasoningIfOpen());
  568. events.push(...this.closeTextIfOpen());
  569. state = {
  570. itemId: newId('fc'),
  571. outputIndex: this.nextOutputIndex,
  572. callId: call.id ?? '',
  573. name: call.function?.name ?? '',
  574. argsBuffer: '',
  575. open: true,
  576. };
  577. this.nextOutputIndex += 1;
  578. this.toolCalls.set(index, state);
  579. events.push(
  580. this.event('response.output_item.added', {
  581. output_index: state.outputIndex,
  582. item: { id: state.itemId, type: 'function_call', call_id: state.callId, name: state.name, arguments: '', status: 'in_progress' },
  583. }),
  584. );
  585. } else {
  586. // 后续 chunk 可能补齐 id / name(多数上游只在首帧带)
  587. if (call.id && !state.callId) state.callId = call.id;
  588. if (call.function?.name && !state.name) state.name = call.function.name;
  589. }
  590. const argsDelta = call.function?.arguments;
  591. if (typeof argsDelta === 'string' && argsDelta) {
  592. state.argsBuffer += argsDelta;
  593. events.push(
  594. this.event('response.function_call_arguments.delta', {
  595. item_id: state.itemId,
  596. output_index: state.outputIndex,
  597. delta: argsDelta,
  598. }),
  599. );
  600. }
  601. return events;
  602. }
  603. /** 按 output_index 顺序组装最终 output(response.completed / failed 用) */
  604. private buildOutput(): Array<Record<string, unknown>> {
  605. const items: Array<{ outputIndex: number; item: Record<string, unknown> }> = [];
  606. if (this.reasoningItem) {
  607. items.push({
  608. outputIndex: this.reasoningItem.outputIndex,
  609. item: {
  610. id: this.reasoningItem.itemId,
  611. type: 'reasoning',
  612. summary: this.reasoningItem.text ? [{ type: 'summary_text', text: this.reasoningItem.text }] : [],
  613. },
  614. });
  615. }
  616. if (this.textItem) {
  617. items.push({
  618. outputIndex: this.textItem.outputIndex,
  619. item: {
  620. id: this.textItem.itemId,
  621. type: 'message',
  622. role: 'assistant',
  623. status: 'completed',
  624. content: [{ type: 'output_text', text: this.textItem.text, annotations: [] }],
  625. },
  626. });
  627. }
  628. for (const state of this.toolCalls.values()) {
  629. items.push({
  630. outputIndex: state.outputIndex,
  631. item: {
  632. id: state.itemId,
  633. type: 'function_call',
  634. call_id: state.callId,
  635. name: state.name,
  636. arguments: state.argsBuffer,
  637. status: 'completed',
  638. },
  639. });
  640. }
  641. items.sort((a, b) => a.outputIndex - b.outputIndex);
  642. return items.map((entry) => entry.item);
  643. }
  644. }