@xiaozhiclaw/provider-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/aliyun-oss-file-upload-adapter.d.ts +44 -0
- package/dist/adapters/aliyun-oss-file-upload-adapter.js +96 -0
- package/dist/adapters/gemini-file-upload-adapter.d.ts +26 -0
- package/dist/adapters/gemini-file-upload-adapter.js +92 -0
- package/dist/adapters/hub-oss-file-upload-adapter.d.ts +29 -0
- package/dist/adapters/hub-oss-file-upload-adapter.js +53 -0
- package/dist/adapters/index.d.ts +10 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/openai-file-upload-adapter.d.ts +38 -0
- package/dist/adapters/openai-file-upload-adapter.js +56 -0
- package/dist/adapters/volcengine-file-upload-adapter.d.ts +24 -0
- package/dist/adapters/volcengine-file-upload-adapter.js +45 -0
- package/dist/builtin-providers.d.ts +8 -0
- package/dist/builtin-providers.js +2237 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/credentials.d.ts +1 -0
- package/dist/credentials.js +8 -0
- package/dist/debug-transport.d.ts +12 -0
- package/dist/debug-transport.js +99 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +12 -0
- package/dist/events.d.ts +48 -0
- package/dist/events.js +1 -0
- package/dist/file-upload-service.d.ts +68 -0
- package/dist/file-upload-service.js +110 -0
- package/dist/gemini-schema-utils.d.ts +17 -0
- package/dist/gemini-schema-utils.js +76 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +33 -0
- package/dist/llm-client.d.ts +43 -0
- package/dist/llm-client.js +217 -0
- package/dist/media-client.d.ts +42 -0
- package/dist/media-client.js +174 -0
- package/dist/media-transport.d.ts +176 -0
- package/dist/media-transport.js +16 -0
- package/dist/media.d.ts +2 -0
- package/dist/media.js +1 -0
- package/dist/model-detection.d.ts +22 -0
- package/dist/model-detection.js +28 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +11 -0
- package/dist/provider-def.d.ts +220 -0
- package/dist/provider-def.js +9 -0
- package/dist/provider-registry.d.ts +51 -0
- package/dist/provider-registry.js +130 -0
- package/dist/provider-tool-api.d.ts +44 -0
- package/dist/provider-tool-api.js +9 -0
- package/dist/provider-variant-resolver.d.ts +35 -0
- package/dist/provider-variant-resolver.js +174 -0
- package/dist/retry.d.ts +37 -0
- package/dist/retry.js +71 -0
- package/dist/transport.d.ts +281 -0
- package/dist/transport.js +27 -0
- package/dist/transports/anthropic-messages.d.ts +65 -0
- package/dist/transports/anthropic-messages.js +1004 -0
- package/dist/transports/gemini-cache-api.d.ts +86 -0
- package/dist/transports/gemini-cache-api.js +141 -0
- package/dist/transports/gemini-file-api.d.ts +90 -0
- package/dist/transports/gemini-file-api.js +164 -0
- package/dist/transports/gemini-generatecontent.d.ts +56 -0
- package/dist/transports/gemini-generatecontent.js +688 -0
- package/dist/transports/gemini-lyria-realtime.d.ts +117 -0
- package/dist/transports/gemini-lyria-realtime.js +295 -0
- package/dist/transports/gemini-media.d.ts +53 -0
- package/dist/transports/gemini-media.js +383 -0
- package/dist/transports/media-resolve.d.ts +50 -0
- package/dist/transports/media-resolve.js +91 -0
- package/dist/transports/minimax-media.d.ts +56 -0
- package/dist/transports/minimax-media.js +433 -0
- package/dist/transports/openai-chat.d.ts +81 -0
- package/dist/transports/openai-chat.js +782 -0
- package/dist/transports/openai-media.d.ts +24 -0
- package/dist/transports/openai-media.js +118 -0
- package/dist/transports/openai-responses.d.ts +63 -0
- package/dist/transports/openai-responses.js +778 -0
- package/dist/transports/qwen-media.d.ts +59 -0
- package/dist/transports/qwen-media.js +411 -0
- package/dist/transports/realtime-transport.d.ts +183 -0
- package/dist/transports/realtime-transport.js +332 -0
- package/dist/transports/volcengine-grounding.d.ts +58 -0
- package/dist/transports/volcengine-grounding.js +69 -0
- package/dist/transports/volcengine-media.d.ts +94 -0
- package/dist/transports/volcengine-media.js +801 -0
- package/dist/transports/volcengine-responses.d.ts +64 -0
- package/dist/transports/volcengine-responses.js +797 -0
- package/dist/transports/zhipu-media.d.ts +82 -0
- package/dist/transports/zhipu-media.js +522 -0
- package/dist/transports/zhipu-tool-api.d.ts +35 -0
- package/dist/transports/zhipu-tool-api.js +126 -0
- package/dist/wire-types.d.ts +51 -0
- package/dist/wire-types.js +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Responses API TransportSSE streaming implementation.
|
|
3
|
+
*
|
|
4
|
+
* Implements the OpenAI Responses API (`POST /v1/responses`),
|
|
5
|
+
* the officially recommended path for GPT-5.x text generation.
|
|
6
|
+
*
|
|
7
|
+
* Key differences from OpenAI Chat Completions:
|
|
8
|
+
* - Endpoint: POST {baseUrl}/v1/responses
|
|
9
|
+
* - Request body uses `input` (not `messages`), `instructions`, `reasoning`
|
|
10
|
+
* - SSE events: response.output_text.delta, response.function_call_arguments.delta,
|
|
11
|
+
* response.completed, etc.
|
|
12
|
+
* - Tool defs: { type: "function", name, parameters } (not nested under `function:`)
|
|
13
|
+
* - Tool results: { type: "function_call_output", call_id, output }
|
|
14
|
+
* - Context persistence: previous_response_id for server-side session continuation
|
|
15
|
+
* - Structured output: `text: { format: { type: "json_schema", ... } }`
|
|
16
|
+
* - Reasoning: `reasoning: { effort, summary }` for GPT-5.x models
|
|
17
|
+
*
|
|
18
|
+
* Wire format reference:
|
|
19
|
+
* https://developers.openai.com/api/docs/api-reference/responses/create
|
|
20
|
+
* https://developers.openai.com/api/docs/api-reference/responses/streaming-events
|
|
21
|
+
*
|
|
22
|
+
* Design: Closely mirrors volcengine-responses.ts patterns while adapting to
|
|
23
|
+
* OpenAI-specific wire format. Shared LLMChunk output makes upper layers
|
|
24
|
+
* transport-agnostic.
|
|
25
|
+
*/
|
|
26
|
+
import { MEDIA_MAX_UPLOAD_SIZE } from "../constants.js";
|
|
27
|
+
import { isLocalUrl, resolveMediaUrl, resolveMediaUrlViaUpload } from "./media-resolve.js";
|
|
28
|
+
import { DEFAULT_MAX_RETRIES, STREAM_IDLE_TIMEOUT_MS, retryDelay, retrySleep, extractHttpStatus, isTransientStatus, } from "../retry.js";
|
|
29
|
+
import { isGPT5xModel, isGPT5NanoModel } from "../model-detection.js";
|
|
30
|
+
export class OpenAIResponsesTransport {
|
|
31
|
+
baseUrl;
|
|
32
|
+
extraHeaders;
|
|
33
|
+
timeoutMs;
|
|
34
|
+
quirks;
|
|
35
|
+
fileUploadAdapter;
|
|
36
|
+
constructor(config) {
|
|
37
|
+
if (!config.baseUrl) {
|
|
38
|
+
throw new Error("OpenAIResponsesTransport: baseUrl is required");
|
|
39
|
+
}
|
|
40
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
41
|
+
this.extraHeaders = config.extraHeaders ?? {};
|
|
42
|
+
this.timeoutMs = config.timeoutMs ?? 180_000;
|
|
43
|
+
this.quirks = config.quirks ?? {};
|
|
44
|
+
this.fileUploadAdapter = config.fileUploadAdapter;
|
|
45
|
+
}
|
|
46
|
+
async *stream(request, apiKey, signal) {
|
|
47
|
+
// Responses API endpoint: POST /v1/responses
|
|
48
|
+
const hasVersionInBase = /\/v\d+$/.test(this.baseUrl);
|
|
49
|
+
const url = hasVersionInBase
|
|
50
|
+
? `${this.baseUrl}/responses`
|
|
51
|
+
: `${this.baseUrl}/v1/responses`;
|
|
52
|
+
const body = await this.buildRequestBody(request, apiKey, signal);
|
|
53
|
+
const headers = {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
Authorization: `Bearer ${apiKey}`,
|
|
56
|
+
...this.extraHeaders,
|
|
57
|
+
};
|
|
58
|
+
// Retry loop with exponential backoff (CC parity)
|
|
59
|
+
let lastError = null;
|
|
60
|
+
for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
|
|
61
|
+
if (signal?.aborted)
|
|
62
|
+
throw new Error("Request aborted");
|
|
63
|
+
if (attempt > 0 && lastError) {
|
|
64
|
+
await retrySleep(retryDelay(attempt), signal);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
yield* this.fetchAndStream(url, headers, body, signal);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
72
|
+
const isIdleTimeout = lastError.message.includes("Stream idle timeout");
|
|
73
|
+
if (!isTransientStatus(extractHttpStatus(lastError)) && !isIdleTimeout)
|
|
74
|
+
throw lastError;
|
|
75
|
+
if (attempt === DEFAULT_MAX_RETRIES)
|
|
76
|
+
throw lastError;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 鈹€鈹€ Request Body Builder 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
81
|
+
async buildRequestBody(request, apiKey, signal) {
|
|
82
|
+
const body = {
|
|
83
|
+
model: request.model,
|
|
84
|
+
input: convertMessagesForOpenAIResponses(await resolveMessagesMediaForResponses(request.messages, this.fileUploadAdapter, apiKey, signal), this.quirks),
|
|
85
|
+
stream: true,
|
|
86
|
+
};
|
|
87
|
+
// Server-side context continuation (mutually exclusive)
|
|
88
|
+
if (request.conversationId) {
|
|
89
|
+
body.conversation = request.conversationId;
|
|
90
|
+
}
|
|
91
|
+
else if (request.previousResponseId) {
|
|
92
|
+
body.previous_response_id = request.previousResponseId;
|
|
93
|
+
}
|
|
94
|
+
// Storage control
|
|
95
|
+
if (request.store !== undefined) {
|
|
96
|
+
body.store = request.store;
|
|
97
|
+
}
|
|
98
|
+
// Per-turn instructions (system/developer message)
|
|
99
|
+
if (request.instructions) {
|
|
100
|
+
body.instructions = request.instructions;
|
|
101
|
+
}
|
|
102
|
+
// Structured output: text.format
|
|
103
|
+
if (request.structuredOutput) {
|
|
104
|
+
if (request.structuredOutput.mode === "json_object") {
|
|
105
|
+
body.text = { format: { type: "json_object" } };
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
body.text = {
|
|
109
|
+
format: {
|
|
110
|
+
type: "json_schema",
|
|
111
|
+
name: request.structuredOutput.name,
|
|
112
|
+
strict: request.structuredOutput.strict ?? true,
|
|
113
|
+
schema: request.structuredOutput.schema,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Tools 閳?OpenAI Responses API format
|
|
119
|
+
// Responses API uses flat tool defs: { type: "function", name, description, parameters, strict }
|
|
120
|
+
// (not the Chat Completions nested { type: "function", function: { name, ... } } format)
|
|
121
|
+
if (request.tools && request.tools.length > 0) {
|
|
122
|
+
body.tools = request.tools.map(tool => ({
|
|
123
|
+
type: "function",
|
|
124
|
+
name: tool.function.name,
|
|
125
|
+
description: tool.function.description,
|
|
126
|
+
parameters: tool.function.parameters,
|
|
127
|
+
strict: true,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
// tool_choice
|
|
131
|
+
if (request.toolChoice !== undefined) {
|
|
132
|
+
if (typeof request.toolChoice === "string") {
|
|
133
|
+
body.tool_choice = request.toolChoice;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
body.tool_choice = { type: request.toolChoice.type, name: request.toolChoice.name };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const suppressSampling = isGPT5xModel(request.model);
|
|
140
|
+
// GPT-5 Responses models reject explicit sampling params.
|
|
141
|
+
if (!suppressSampling && request.temperature !== undefined) {
|
|
142
|
+
body.temperature = request.temperature;
|
|
143
|
+
}
|
|
144
|
+
// top_p
|
|
145
|
+
if (!suppressSampling && request.topP !== undefined) {
|
|
146
|
+
body.top_p = request.topP;
|
|
147
|
+
}
|
|
148
|
+
// Max tokens 閳?max_output_tokens in Responses API
|
|
149
|
+
if (request.maxTokens !== undefined) {
|
|
150
|
+
body.max_output_tokens = request.maxTokens;
|
|
151
|
+
}
|
|
152
|
+
// Reasoning effort for GPT-5.x
|
|
153
|
+
if (request.reasoning) {
|
|
154
|
+
let effort = request.reasoning.effort === "minimal" ? "none" : request.reasoning.effort;
|
|
155
|
+
// gpt-5.4-nano only supports up to medium effort (openai-ProviderMax 鎼?)
|
|
156
|
+
if (isGPT5NanoModel(request.model) && (effort === "high" || effort === "xhigh")) {
|
|
157
|
+
effort = "medium";
|
|
158
|
+
}
|
|
159
|
+
const reasoning = { effort };
|
|
160
|
+
// Reasoning summary: let model produce a reasoning summary (Responses API only)
|
|
161
|
+
// Official API has both generate_summary and summary fields (openai-ProviderMax 鎼?)
|
|
162
|
+
reasoning.summary = "auto";
|
|
163
|
+
reasoning.generate_summary = "auto";
|
|
164
|
+
body.reasoning = reasoning;
|
|
165
|
+
}
|
|
166
|
+
// Predicted output for speculative decoding (code editing optimization)
|
|
167
|
+
if (request.prediction && isGPT5xModel(request.model)) {
|
|
168
|
+
body.prediction = request.prediction;
|
|
169
|
+
}
|
|
170
|
+
// Truncation: auto for safety (server-side context trimming)
|
|
171
|
+
body.truncation = "auto";
|
|
172
|
+
// Prompt cache optimization (openai-ProviderMax 鎼?1)
|
|
173
|
+
if (request.promptCacheKey) {
|
|
174
|
+
body.prompt_cache_key = request.promptCacheKey;
|
|
175
|
+
}
|
|
176
|
+
if (request.promptCacheRetention) {
|
|
177
|
+
body.prompt_cache_retention = request.promptCacheRetention;
|
|
178
|
+
}
|
|
179
|
+
// Service tier scheduling (openai-ProviderMax 鎼?4)
|
|
180
|
+
if (request.serviceTier) {
|
|
181
|
+
body.service_tier = request.serviceTier;
|
|
182
|
+
}
|
|
183
|
+
// Encrypted reasoning for ZDR (openai-ProviderMax 鎼?)
|
|
184
|
+
if (request.reasoning?.includeEncryptedReasoning) {
|
|
185
|
+
if (request.store === undefined) {
|
|
186
|
+
body.store = false;
|
|
187
|
+
}
|
|
188
|
+
body.include = ["reasoning.encrypted_content"];
|
|
189
|
+
}
|
|
190
|
+
// Context managementserver-side compression (openai-ProviderMax 鎼?1)
|
|
191
|
+
if (request.contextManagement) {
|
|
192
|
+
body.context_management = request.contextManagement.edits.map((edit) => {
|
|
193
|
+
const e = { type: edit.type };
|
|
194
|
+
if (edit.type === "clear_thinking" && edit.keep !== undefined) {
|
|
195
|
+
e.keep = edit.keep;
|
|
196
|
+
}
|
|
197
|
+
else if (edit.type === "clear_tool_uses") {
|
|
198
|
+
if (edit.trigger)
|
|
199
|
+
e.trigger = edit.trigger;
|
|
200
|
+
if (edit.keep)
|
|
201
|
+
e.keep = edit.keep;
|
|
202
|
+
if (edit.excludeTools)
|
|
203
|
+
e.exclude_tools = edit.excludeTools;
|
|
204
|
+
if (edit.clearToolInput !== undefined)
|
|
205
|
+
e.clear_tool_input = edit.clearToolInput;
|
|
206
|
+
}
|
|
207
|
+
return e;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// 鈹€鈹€ Builtin tools injection (quirks-driven, same pattern as Gemini/GLM/Kimi) 鈹€鈹€
|
|
211
|
+
// Priority: explicit openaiBuiltinTools > quirks-driven auto-injection
|
|
212
|
+
// Both respect disableBuiltinTools flag (session-level toggle, T15 parity)
|
|
213
|
+
if (!request.disableBuiltinTools) {
|
|
214
|
+
const existingTools = body.tools ?? [];
|
|
215
|
+
const includes = body.include ?? [];
|
|
216
|
+
if (request.openaiBuiltinTools && request.openaiBuiltinTools.length > 0) {
|
|
217
|
+
// Explicit caller-specified builtin tools (highest priority)
|
|
218
|
+
for (const bt of request.openaiBuiltinTools) {
|
|
219
|
+
existingTools.push({ ...bt });
|
|
220
|
+
}
|
|
221
|
+
// Auto-expand `include` for builtin tool result details
|
|
222
|
+
const builtinTypes = new Set(request.openaiBuiltinTools.map((t) => t.type));
|
|
223
|
+
if (builtinTypes.has("web_search_preview"))
|
|
224
|
+
includes.push("web_search_call.action.sources");
|
|
225
|
+
if (builtinTypes.has("file_search"))
|
|
226
|
+
includes.push("file_search_call.results");
|
|
227
|
+
if (builtinTypes.has("code_interpreter"))
|
|
228
|
+
includes.push("code_interpreter_call.outputs");
|
|
229
|
+
if (builtinTypes.has("computer_use_preview"))
|
|
230
|
+
includes.push("computer_call_output.output.image_url");
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// Quirks-driven auto-injection (same pattern as Gemini/GLM transports)
|
|
234
|
+
if (this.quirks.builtinWebSearch) {
|
|
235
|
+
existingTools.push({ type: "web_search_preview" });
|
|
236
|
+
includes.push("web_search_call.action.sources");
|
|
237
|
+
}
|
|
238
|
+
if (this.quirks.builtinCodeInterpreter) {
|
|
239
|
+
existingTools.push({ type: "code_interpreter" });
|
|
240
|
+
includes.push("code_interpreter_call.outputs");
|
|
241
|
+
}
|
|
242
|
+
if (this.quirks.builtinFileSearch) {
|
|
243
|
+
existingTools.push({ type: "file_search" });
|
|
244
|
+
includes.push("file_search_call.results");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (existingTools.length > 0)
|
|
248
|
+
body.tools = existingTools;
|
|
249
|
+
if (includes.length > 0)
|
|
250
|
+
body.include = includes;
|
|
251
|
+
}
|
|
252
|
+
// Max builtin tool calls limit (openai-ProviderMax 鎼?)
|
|
253
|
+
if (request.maxToolCalls !== undefined) {
|
|
254
|
+
body.max_tool_calls = request.maxToolCalls;
|
|
255
|
+
}
|
|
256
|
+
// Parallel tool calls control
|
|
257
|
+
if (request.parallelToolCalls !== undefined) {
|
|
258
|
+
body.parallel_tool_calls = request.parallelToolCalls;
|
|
259
|
+
}
|
|
260
|
+
// Text output verbosity (openai-ProviderMax 鎼?)
|
|
261
|
+
if (request.textVerbosity) {
|
|
262
|
+
const text = body.text ?? {};
|
|
263
|
+
text.verbosity = request.textVerbosity;
|
|
264
|
+
body.text = text;
|
|
265
|
+
}
|
|
266
|
+
return body;
|
|
267
|
+
}
|
|
268
|
+
// 鈹€鈹€ Fetch + Stream 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
269
|
+
async *fetchAndStream(url, headers, body, signal) {
|
|
270
|
+
const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
|
271
|
+
const combinedSignal = signal
|
|
272
|
+
? AbortSignal.any([signal, timeoutSignal])
|
|
273
|
+
: timeoutSignal;
|
|
274
|
+
const response = await fetch(url, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers,
|
|
277
|
+
body: JSON.stringify(body),
|
|
278
|
+
signal: combinedSignal,
|
|
279
|
+
});
|
|
280
|
+
if (!response.ok) {
|
|
281
|
+
const errorBody = await response.text().catch(() => "");
|
|
282
|
+
const err = new Error(`OpenAI Responses API error ${response.status}: ${errorBody.slice(0, 500)}`);
|
|
283
|
+
err.status = response.status;
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
if (!response.body) {
|
|
287
|
+
throw new Error("OpenAI Responses API returned no response body");
|
|
288
|
+
}
|
|
289
|
+
// Check if the response is non-streaming JSON (fallback)
|
|
290
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
291
|
+
if (contentType.includes("application/json") && !contentType.includes("text/event-stream")) {
|
|
292
|
+
yield* this.handleNonStreamingResponse(response);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
yield* this.parseSSEStream(response.body);
|
|
296
|
+
}
|
|
297
|
+
// 鈹€鈹€ Non-streaming fallback 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
298
|
+
async *handleNonStreamingResponse(response) {
|
|
299
|
+
const data = await response.json();
|
|
300
|
+
// Emit response ID for context chain
|
|
301
|
+
if (data.id) {
|
|
302
|
+
yield { type: "response_id", id: data.id };
|
|
303
|
+
}
|
|
304
|
+
if (data.usage) {
|
|
305
|
+
yield {
|
|
306
|
+
type: "usage",
|
|
307
|
+
promptTokens: data.usage.input_tokens ?? 0,
|
|
308
|
+
completionTokens: data.usage.output_tokens ?? 0,
|
|
309
|
+
reasoningTokens: data.usage.output_tokens_details?.reasoning_tokens,
|
|
310
|
+
cacheReadTokens: data.usage.input_tokens_details?.cached_tokens,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// Process output items
|
|
314
|
+
if (data.output) {
|
|
315
|
+
for (const item of data.output) {
|
|
316
|
+
if (item.type === "message" && item.content) {
|
|
317
|
+
for (const block of item.content) {
|
|
318
|
+
if (block.type === "output_text") {
|
|
319
|
+
yield { type: "delta", text: block.text ?? "" };
|
|
320
|
+
// Extract annotations from content blocks
|
|
321
|
+
if (block.annotations && block.annotations.length > 0) {
|
|
322
|
+
yield {
|
|
323
|
+
type: "annotations",
|
|
324
|
+
annotations: block.annotations.map(a => ({
|
|
325
|
+
...a,
|
|
326
|
+
type: a.type ?? "url_citation",
|
|
327
|
+
url: a.url,
|
|
328
|
+
title: a.title,
|
|
329
|
+
})),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (item.type === "reasoning" && item.summary) {
|
|
336
|
+
for (const block of item.summary) {
|
|
337
|
+
if (block.type === "summary_text") {
|
|
338
|
+
yield { type: "reasoning_delta", text: block.text ?? "" };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (item.type === "function_call") {
|
|
343
|
+
yield {
|
|
344
|
+
type: "tool_call_delta",
|
|
345
|
+
index: 0,
|
|
346
|
+
id: item.call_id,
|
|
347
|
+
name: item.name,
|
|
348
|
+
arguments: item.arguments ?? "",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
yield { type: "done", finishReason: data.status === "completed" ? "stop" : (data.status ?? "stop") };
|
|
354
|
+
}
|
|
355
|
+
// 鈹€鈹€ SSE Stream Parser 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
356
|
+
/**
|
|
357
|
+
* Parse OpenAI Responses API SSE stream.
|
|
358
|
+
*
|
|
359
|
+
* Event format: "event: <type>\ndata: <json>\n\n"
|
|
360
|
+
* Key events:
|
|
361
|
+
* - response.output_text.delta 閳?text content delta
|
|
362
|
+
* - response.reasoning_summary_text.delta 閳?reasoning summary text
|
|
363
|
+
* - response.function_call_arguments.delta 閳?tool call arguments streaming
|
|
364
|
+
* - response.output_item.added 閳?new output item started
|
|
365
|
+
* - response.output_item.done 閳?output item completed
|
|
366
|
+
* - response.content_part.done 閳?content part completed (annotations)
|
|
367
|
+
* - response.completed 閳?full response complete with usage
|
|
368
|
+
* - response.failed 閳?error
|
|
369
|
+
*/
|
|
370
|
+
async *parseSSEStream(body) {
|
|
371
|
+
const decoder = new TextDecoder();
|
|
372
|
+
let buffer = "";
|
|
373
|
+
let currentEvent = "";
|
|
374
|
+
let idleTimer = null;
|
|
375
|
+
const abortController = new AbortController();
|
|
376
|
+
// Track tool call indices for multiple parallel function calls
|
|
377
|
+
let toolCallIndex = 0;
|
|
378
|
+
const toolCallIdToIndex = new Map();
|
|
379
|
+
const resetIdleTimer = () => {
|
|
380
|
+
if (idleTimer)
|
|
381
|
+
clearTimeout(idleTimer);
|
|
382
|
+
idleTimer = setTimeout(() => {
|
|
383
|
+
abortController.abort();
|
|
384
|
+
}, STREAM_IDLE_TIMEOUT_MS);
|
|
385
|
+
};
|
|
386
|
+
try {
|
|
387
|
+
resetIdleTimer();
|
|
388
|
+
const reader = body.getReader();
|
|
389
|
+
try {
|
|
390
|
+
while (true) {
|
|
391
|
+
const { done, value } = await reader.read();
|
|
392
|
+
if (done)
|
|
393
|
+
break;
|
|
394
|
+
if (abortController.signal.aborted) {
|
|
395
|
+
throw new Error("Stream idle timeout");
|
|
396
|
+
}
|
|
397
|
+
resetIdleTimer();
|
|
398
|
+
buffer += decoder.decode(value, { stream: true });
|
|
399
|
+
let newlineIdx;
|
|
400
|
+
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
401
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
402
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
403
|
+
if (!line) {
|
|
404
|
+
currentEvent = "";
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (line.startsWith(":"))
|
|
408
|
+
continue; // SSE comment
|
|
409
|
+
if (line.startsWith("event:")) {
|
|
410
|
+
currentEvent = line.slice(6).trim();
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (line.startsWith("data:")) {
|
|
414
|
+
const dataStr = line.slice(5).trim();
|
|
415
|
+
if (dataStr === "[DONE]")
|
|
416
|
+
return;
|
|
417
|
+
let parsed;
|
|
418
|
+
try {
|
|
419
|
+
parsed = JSON.parse(dataStr);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
yield* this.processEvent(currentEvent, parsed, toolCallIdToIndex, () => toolCallIndex++);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
reader.releaseLock();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
if (idleTimer)
|
|
435
|
+
clearTimeout(idleTimer);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// 鈹€鈹€ Event Processing 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
439
|
+
*processEvent(eventType, data, toolCallIdToIndex, nextIndex) {
|
|
440
|
+
switch (eventType) {
|
|
441
|
+
// 鈹€鈹€ Text content delta 鈹€鈹€
|
|
442
|
+
case "response.output_text.delta": {
|
|
443
|
+
const delta = data.delta;
|
|
444
|
+
if (delta) {
|
|
445
|
+
yield { type: "delta", text: delta };
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
// 鈹€鈹€ Reasoning summary delta 鈹€鈹€
|
|
450
|
+
case "response.reasoning_summary_text.delta": {
|
|
451
|
+
const delta = data.delta;
|
|
452
|
+
if (delta) {
|
|
453
|
+
yield { type: "reasoning_delta", text: delta };
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
// 鈹€鈹€ Function call arguments streaming 鈹€鈹€
|
|
458
|
+
case "response.function_call_arguments.delta": {
|
|
459
|
+
const delta = data.delta;
|
|
460
|
+
const callId = data.call_id;
|
|
461
|
+
const name = data.name;
|
|
462
|
+
if (delta !== undefined && callId) {
|
|
463
|
+
let idx = toolCallIdToIndex.get(callId);
|
|
464
|
+
if (idx === undefined) {
|
|
465
|
+
idx = nextIndex();
|
|
466
|
+
toolCallIdToIndex.set(callId, idx);
|
|
467
|
+
}
|
|
468
|
+
yield {
|
|
469
|
+
type: "tool_call_delta",
|
|
470
|
+
index: idx,
|
|
471
|
+
id: callId,
|
|
472
|
+
name: name,
|
|
473
|
+
arguments: delta,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
// 鈹€鈹€ Output item added (function_call start) 鈹€鈹€
|
|
479
|
+
case "response.output_item.added": {
|
|
480
|
+
const item = data.item;
|
|
481
|
+
if (item && item.type === "function_call") {
|
|
482
|
+
const callId = item.call_id;
|
|
483
|
+
const name = item.name;
|
|
484
|
+
if (callId && !toolCallIdToIndex.has(callId)) {
|
|
485
|
+
const idx = nextIndex();
|
|
486
|
+
toolCallIdToIndex.set(callId, idx);
|
|
487
|
+
yield {
|
|
488
|
+
type: "tool_call_delta",
|
|
489
|
+
index: idx,
|
|
490
|
+
id: callId,
|
|
491
|
+
name: name,
|
|
492
|
+
arguments: "",
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
// 鈹€鈹€ Function call arguments done 鈹€鈹€
|
|
499
|
+
case "response.function_call_arguments.done":
|
|
500
|
+
break;
|
|
501
|
+
// 鈹€鈹€ Content part doneextract annotations 鈹€鈹€
|
|
502
|
+
case "response.content_part.done": {
|
|
503
|
+
const part = data.part;
|
|
504
|
+
if (part?.type === "output_text") {
|
|
505
|
+
const anns = part.annotations;
|
|
506
|
+
if (anns && anns.length > 0) {
|
|
507
|
+
yield {
|
|
508
|
+
type: "annotations",
|
|
509
|
+
annotations: anns.map(a => ({
|
|
510
|
+
type: a.type ?? "url_citation",
|
|
511
|
+
url: a.url,
|
|
512
|
+
title: a.title,
|
|
513
|
+
...a,
|
|
514
|
+
})),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
// 鈹€鈹€ Output item doneextract annotations from completed messages 鈹€鈹€
|
|
521
|
+
case "response.output_item.done": {
|
|
522
|
+
const item = data.item;
|
|
523
|
+
if (item?.type === "message") {
|
|
524
|
+
const content = item.content;
|
|
525
|
+
if (content) {
|
|
526
|
+
for (const block of content) {
|
|
527
|
+
const anns = block.annotations;
|
|
528
|
+
if (anns && anns.length > 0) {
|
|
529
|
+
yield {
|
|
530
|
+
type: "annotations",
|
|
531
|
+
annotations: anns.map(a => ({
|
|
532
|
+
type: a.type ?? "url_citation",
|
|
533
|
+
url: a.url,
|
|
534
|
+
title: a.title,
|
|
535
|
+
...a,
|
|
536
|
+
})),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
// 鈹€鈹€ Response completedextract usage 鈹€鈹€
|
|
545
|
+
case "response.completed": {
|
|
546
|
+
const resp = data.response;
|
|
547
|
+
if (resp) {
|
|
548
|
+
const respId = resp.id;
|
|
549
|
+
if (respId) {
|
|
550
|
+
yield { type: "response_id", id: respId };
|
|
551
|
+
}
|
|
552
|
+
const usage = resp.usage;
|
|
553
|
+
if (usage) {
|
|
554
|
+
const inputDetails = usage.input_tokens_details;
|
|
555
|
+
const outputDetails = usage.output_tokens_details;
|
|
556
|
+
yield {
|
|
557
|
+
type: "usage",
|
|
558
|
+
promptTokens: usage.input_tokens ?? 0,
|
|
559
|
+
completionTokens: usage.output_tokens ?? 0,
|
|
560
|
+
reasoningTokens: outputDetails?.reasoning_tokens,
|
|
561
|
+
cacheReadTokens: inputDetails?.cached_tokens,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const status = resp.status;
|
|
565
|
+
yield { type: "done", finishReason: status === "incomplete" ? "tool_calls" : "stop" };
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
yield { type: "done", finishReason: "stop" };
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
// 鈹€鈹€ Response failed 鈹€鈹€
|
|
573
|
+
case "response.failed": {
|
|
574
|
+
const resp = data.response;
|
|
575
|
+
const error = resp?.error;
|
|
576
|
+
const message = error?.message ?? "Unknown error";
|
|
577
|
+
yield { type: "error", message };
|
|
578
|
+
yield { type: "done", finishReason: "error" };
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
// 鈹€鈹€ Web search tool events (builtin) 鈹€鈹€
|
|
582
|
+
default: {
|
|
583
|
+
if (eventType.startsWith("response.web_search_call")) {
|
|
584
|
+
yield {
|
|
585
|
+
type: "builtin_tool_status",
|
|
586
|
+
toolType: "web_search",
|
|
587
|
+
event: eventType,
|
|
588
|
+
data: data,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
// Other events (response.created, output_text.done, etc.) are informational.
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// 鈹€鈹€ Message Conversion for Responses API 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
598
|
+
/**
|
|
599
|
+
* Pre-resolve local media URLs for Responses API.
|
|
600
|
+
* Images: prefer upload via adapter (no size limit), fallback to base64 (legacy, 閳?0MB).
|
|
601
|
+
* Audio: must always be base64 (OpenAI input_audio.data requirement).
|
|
602
|
+
*/
|
|
603
|
+
async function resolveMessagesMediaForResponses(messages, uploadAdapter, apiKey, signal) {
|
|
604
|
+
const needsResolution = messages.some((m) => m.audioUrls?.some(isLocalUrl) || m.imageUrls?.some(isLocalUrl));
|
|
605
|
+
if (!needsResolution)
|
|
606
|
+
return messages;
|
|
607
|
+
return Promise.all(messages.map(async (msg) => {
|
|
608
|
+
// Resolve imageUrls on both user messages and tool result messages
|
|
609
|
+
if (msg.role !== "user" && msg.role !== "tool")
|
|
610
|
+
return msg;
|
|
611
|
+
const patch = {};
|
|
612
|
+
// Audio MUST be base64 (OpenAI API requirementno upload alternative)
|
|
613
|
+
if (msg.role === "user" && msg.audioUrls?.some(isLocalUrl)) {
|
|
614
|
+
patch.audioUrls = await Promise.all(msg.audioUrls.map((url) => isLocalUrl(url) ? resolveMediaUrl(url) : Promise.resolve(url)));
|
|
615
|
+
}
|
|
616
|
+
// Images: upload via adapter (requiredno base64 fallback)
|
|
617
|
+
if (msg.imageUrls?.some(isLocalUrl)) {
|
|
618
|
+
if (!uploadAdapter || !apiKey) {
|
|
619
|
+
throw new Error("FileUploadAdapter required for local image URLs. Configure OSS_ACCESS_KEY_ID/OSS_ACCESS_KEY_SECRET or QLOGICAGENT_HUB_URL.");
|
|
620
|
+
}
|
|
621
|
+
patch.imageUrls = await Promise.all(msg.imageUrls.map((url) => isLocalUrl(url)
|
|
622
|
+
? resolveMediaUrlViaUpload(url, { uploadAdapter, apiKey, signal })
|
|
623
|
+
: Promise.resolve(url)));
|
|
624
|
+
}
|
|
625
|
+
return Object.keys(patch).length > 0 ? { ...msg, ...patch } : msg;
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Convert qlogicagent ChatMessage[] into OpenAI Responses API `input` format.
|
|
630
|
+
*
|
|
631
|
+
* Responses API input is an array of items:
|
|
632
|
+
* - { role: "developer", content: "..." } 閳?system messages use "developer" role
|
|
633
|
+
* - { role: "user", content: "..." | [...] } 閳?text or multimodal content array
|
|
634
|
+
* - { role: "assistant", content: "..." }
|
|
635
|
+
* - { type: "function_call_output", call_id: "...", output: "..." }
|
|
636
|
+
*
|
|
637
|
+
* Key differences from Chat Completions:
|
|
638
|
+
* - System 閳?developer role (OpenAI Responses API convention)
|
|
639
|
+
* - Vision: { type: "input_image", image_url: "..." } (not image_url object)
|
|
640
|
+
* - Tool results: { type: "function_call_output" } (not role: "tool")
|
|
641
|
+
* - Tool calls from assistant: { type: "function_call", call_id, name, arguments }
|
|
642
|
+
*/
|
|
643
|
+
function convertMessagesForOpenAIResponses(messages, quirks = {}) {
|
|
644
|
+
const result = [];
|
|
645
|
+
for (const msg of messages) {
|
|
646
|
+
// System messages 閳?developer role (OpenAI Responses API uses "developer" for system instructions)
|
|
647
|
+
if (msg.role === "system") {
|
|
648
|
+
result.push({ role: "developer", content: msg.content ?? "" });
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
// User messages: handle vision/multimodal content blocks
|
|
652
|
+
if (msg.role === "user") {
|
|
653
|
+
const hasImages = msg.imageUrls && msg.imageUrls.length > 0;
|
|
654
|
+
const hasVideos = msg.videoUrls && msg.videoUrls.length > 0;
|
|
655
|
+
const hasAudios = msg.audioUrls && msg.audioUrls.length > 0;
|
|
656
|
+
const hasFiles = msg.fileIds && msg.fileIds.length > 0;
|
|
657
|
+
const hasMultimodal = hasImages || hasVideos || hasAudios || hasFiles;
|
|
658
|
+
if (hasMultimodal) {
|
|
659
|
+
const content = [];
|
|
660
|
+
if (hasImages) {
|
|
661
|
+
const multiImage = msg.imageUrls.length > 1;
|
|
662
|
+
for (let imgIdx = 0; imgIdx < msg.imageUrls.length; imgIdx++) {
|
|
663
|
+
const url = msg.imageUrls[imgIdx];
|
|
664
|
+
if (multiImage) {
|
|
665
|
+
content.push({ type: "input_text", text: `[Image ${imgIdx + 1}]` });
|
|
666
|
+
}
|
|
667
|
+
const img = { type: "input_image", image_url: url };
|
|
668
|
+
if (msg.imageDetail)
|
|
669
|
+
img.detail = msg.imageDetail;
|
|
670
|
+
content.push(img);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (hasVideos) {
|
|
674
|
+
for (const url of msg.videoUrls) {
|
|
675
|
+
content.push({ type: "input_video", video_url: url });
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (hasAudios) {
|
|
679
|
+
for (const url of msg.audioUrls) {
|
|
680
|
+
// input_audio.data expects base64 content, not a URL.
|
|
681
|
+
// Caller must pre-resolve local URLs via resolveMediaUrl().
|
|
682
|
+
let data = url;
|
|
683
|
+
let format = msg.audioFormat ?? "mp3";
|
|
684
|
+
if (url.startsWith("data:")) {
|
|
685
|
+
const match = /^data:audio\/([^;]+);base64,(.+)$/.exec(url);
|
|
686
|
+
if (match) {
|
|
687
|
+
format = match[1] === "mpeg" ? "mp3" : match[1];
|
|
688
|
+
data = match[2];
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
content.push({ type: "input_audio", data, format });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (hasFiles) {
|
|
695
|
+
for (const f of msg.fileIds) {
|
|
696
|
+
const mime = f.mimeType || "";
|
|
697
|
+
const tooLarge = f.size != null && f.size > MEDIA_MAX_UPLOAD_SIZE;
|
|
698
|
+
if (tooLarge) {
|
|
699
|
+
const sizeLabel = `${(f.size / (1024 * 1024)).toFixed(1)}MB`;
|
|
700
|
+
content.push({ type: "input_text", text: `[Attached: ${f.id} (${mime || "unknown"}, ${sizeLabel}) \u2014 file too large for direct vision, use tools to process]` });
|
|
701
|
+
}
|
|
702
|
+
else if (quirks.supportsDocumentVision && (mime === "application/pdf" || f.id.endsWith(".pdf"))) {
|
|
703
|
+
// Only use input_file for platform file IDs, not HTTP URLs
|
|
704
|
+
if (f.id.startsWith("http://") || f.id.startsWith("https://")) {
|
|
705
|
+
content.push({ type: "input_text", text: `[Attached PDF: ${f.id}]` });
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
content.push({ type: "input_file", file_id: f.id });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
else if (f.id.startsWith("http://") || f.id.startsWith("https://")) {
|
|
712
|
+
const label = mime ? `[Attached: ${f.id} (${mime})]` : `[Attached: ${f.id}]`;
|
|
713
|
+
content.push({ type: "input_text", text: label });
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
content.push({ type: "input_file", file_id: f.id });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (msg.content) {
|
|
721
|
+
content.push({ type: "input_text", text: msg.content });
|
|
722
|
+
}
|
|
723
|
+
result.push({ role: "user", content });
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
result.push({ role: "user", content: msg.content ?? "" });
|
|
727
|
+
}
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
// Assistant messages: include tool calls as separate function_call items
|
|
731
|
+
if (msg.role === "assistant") {
|
|
732
|
+
// Emit text content
|
|
733
|
+
if (msg.content) {
|
|
734
|
+
result.push({ role: "assistant", content: msg.content });
|
|
735
|
+
}
|
|
736
|
+
// Emit tool calls as function_call output items (for multi-turn replay)
|
|
737
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
738
|
+
for (const tc of msg.tool_calls) {
|
|
739
|
+
result.push({
|
|
740
|
+
type: "function_call",
|
|
741
|
+
call_id: tc.id,
|
|
742
|
+
name: tc.function.name,
|
|
743
|
+
arguments: tc.function.arguments,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
// Tool result messages 閳?function_call_output
|
|
750
|
+
if (msg.role === "tool") {
|
|
751
|
+
if (!msg.tool_call_id)
|
|
752
|
+
continue;
|
|
753
|
+
// If tool result has imageUrls, include as content array (GPT-5.x support)
|
|
754
|
+
if (msg.imageUrls && msg.imageUrls.length > 0) {
|
|
755
|
+
const content = [];
|
|
756
|
+
if (msg.content)
|
|
757
|
+
content.push({ type: "input_text", text: msg.content });
|
|
758
|
+
for (const url of msg.imageUrls) {
|
|
759
|
+
content.push({ type: "input_image", image_url: url });
|
|
760
|
+
}
|
|
761
|
+
result.push({
|
|
762
|
+
type: "function_call_output",
|
|
763
|
+
call_id: msg.tool_call_id,
|
|
764
|
+
output: content,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
result.push({
|
|
769
|
+
type: "function_call_output",
|
|
770
|
+
call_id: msg.tool_call_id,
|
|
771
|
+
output: msg.content ?? "",
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return result;
|
|
778
|
+
}
|