@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,797 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Volcengine Responses API TransportSSE streaming implementation.
|
|
3
|
+
*
|
|
4
|
+
* Implements the fire mountain ark Responses API (`/api/v3/responses`),
|
|
5
|
+
* which is the officially recommended primary path for Doubao LLM text generation
|
|
6
|
+
* (250615+ models: doubao-seed-2.0 series).
|
|
7
|
+
*
|
|
8
|
+
* Key differences from OpenAI Chat Completions:
|
|
9
|
+
* - Endpoint: POST {baseUrl}/v3/responses
|
|
10
|
+
* - Request body uses `input` (not `messages`), `instructions`, `thinking`, `reasoning`
|
|
11
|
+
* - SSE events: response.output_text.delta, response.reasoning_summary_text.delta,
|
|
12
|
+
* response.function_call_arguments.delta, response.completed, etc.
|
|
13
|
+
* - Tool calling: function_call / function_call_output with call_id
|
|
14
|
+
* - Context persistence: previous_response_id for server-side session continuation
|
|
15
|
+
* - Deep thinking: thinking.type (enabled/disabled/auto) + reasoning.effort
|
|
16
|
+
*
|
|
17
|
+
* Docs: https://www.volcengine.com/docs/82379/1399008
|
|
18
|
+
*/
|
|
19
|
+
import { MEDIA_MAX_UPLOAD_SIZE } from "../constants.js";
|
|
20
|
+
import { isLocalUrl, resolveMediaUrlViaUpload } from "./media-resolve.js";
|
|
21
|
+
import { DEFAULT_MAX_RETRIES, STREAM_IDLE_TIMEOUT_MS, retryDelay, retrySleep, extractHttpStatus, isTransientStatus, } from "../retry.js";
|
|
22
|
+
export class VolcengineResponsesTransport {
|
|
23
|
+
baseUrl;
|
|
24
|
+
extraHeaders;
|
|
25
|
+
timeoutMs;
|
|
26
|
+
quirks;
|
|
27
|
+
fileUploadAdapter;
|
|
28
|
+
constructor(config) {
|
|
29
|
+
if (!config.baseUrl) {
|
|
30
|
+
throw new Error("VolcengineResponsesTransport: baseUrl is required");
|
|
31
|
+
}
|
|
32
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
33
|
+
this.extraHeaders = config.extraHeaders ?? {};
|
|
34
|
+
this.timeoutMs = config.timeoutMs ?? 180_000;
|
|
35
|
+
this.quirks = config.quirks ?? {};
|
|
36
|
+
this.fileUploadAdapter = config.fileUploadAdapter;
|
|
37
|
+
}
|
|
38
|
+
async *stream(request, apiKey, signal) {
|
|
39
|
+
// Responses API endpoint: POST /api/v3/responses
|
|
40
|
+
// Normalize baseUrl: strip trailing /v3 or /vN, then always append /v3/responses
|
|
41
|
+
const base = this.baseUrl.replace(/\/v\d+$/, "");
|
|
42
|
+
const url = `${base}/v3/responses`;
|
|
43
|
+
const body = await this.buildRequestBody(request, apiKey, signal);
|
|
44
|
+
const headers = {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Authorization: `Bearer ${apiKey}`,
|
|
47
|
+
...this.extraHeaders,
|
|
48
|
+
};
|
|
49
|
+
// Builtin tools require beta feature-gate headers (鎼?9.8)
|
|
50
|
+
if (request.builtinTools && !request.disableBuiltinTools && request.builtinTools.length > 0) {
|
|
51
|
+
for (const bt of request.builtinTools) {
|
|
52
|
+
if (bt.type === "builtin_web_search")
|
|
53
|
+
headers["ark-beta-web-search"] = "true";
|
|
54
|
+
else if (bt.type === "builtin_image_process")
|
|
55
|
+
headers["ark-beta-image-process"] = "true";
|
|
56
|
+
else if (bt.type === "builtin_knowledge_search")
|
|
57
|
+
headers["ark-beta-knowledge-search"] = "true";
|
|
58
|
+
else if (bt.type === "builtin_doubao_app")
|
|
59
|
+
headers["ark-beta-doubao-app"] = "true";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Retry loop with exponential backoff (CC parity)
|
|
63
|
+
let lastError = null;
|
|
64
|
+
for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
|
|
65
|
+
if (signal?.aborted)
|
|
66
|
+
throw new Error("Request aborted");
|
|
67
|
+
if (attempt > 0 && lastError) {
|
|
68
|
+
await retrySleep(retryDelay(attempt), signal);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
yield* this.fetchAndStream(url, headers, body, signal);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
76
|
+
const isIdleTimeout = lastError.message.includes("Stream idle timeout");
|
|
77
|
+
if (!isTransientStatus(extractHttpStatus(lastError)) && !isIdleTimeout)
|
|
78
|
+
throw lastError;
|
|
79
|
+
if (attempt === DEFAULT_MAX_RETRIES)
|
|
80
|
+
throw lastError;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 鈹€鈹€ Capability Constraint Resolution (鎼?0.7, 鎼?0.9, 鎼?0.10) 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
85
|
+
/**
|
|
86
|
+
* Resolve known Volcengine Responses API incompatibilities:
|
|
87
|
+
* - instructions + caching 閳?drop caching (鎼?0.7)
|
|
88
|
+
* - caching + json_schema 閳?downgrade to json_object (鎼?0.10)
|
|
89
|
+
* - caching + builtin_web_search/image_process 閳?drop those builtin tools
|
|
90
|
+
* Returns a shallow copy with fields adjusted; never mutates the original.
|
|
91
|
+
*/
|
|
92
|
+
resolveConstraints(request) {
|
|
93
|
+
let req = request;
|
|
94
|
+
const hasCaching = req.caching && req.caching.type === "enabled";
|
|
95
|
+
if (hasCaching) {
|
|
96
|
+
// 鎼?0.7: instructions and caching are mutually exclusiveinstructions wins
|
|
97
|
+
if (req.instructions) {
|
|
98
|
+
req = { ...req, caching: undefined };
|
|
99
|
+
// After dropping caching, remaining constraints no longer apply
|
|
100
|
+
return req;
|
|
101
|
+
}
|
|
102
|
+
// 鎼?0.10: caching + json_schema 閳?downgrade to json_object
|
|
103
|
+
if (req.structuredOutput && req.structuredOutput.mode === "json_schema") {
|
|
104
|
+
req = { ...req, structuredOutput: { mode: "json_object" } };
|
|
105
|
+
}
|
|
106
|
+
// 鎼?0.9 / 鎼?131-3146: caching incompatible with web_search and image_process
|
|
107
|
+
if (req.builtinTools && req.builtinTools.length > 0) {
|
|
108
|
+
const filtered = req.builtinTools.filter((bt) => bt.type !== "builtin_web_search" && bt.type !== "builtin_image_process");
|
|
109
|
+
if (filtered.length !== req.builtinTools.length) {
|
|
110
|
+
req = { ...req, builtinTools: filtered.length > 0 ? filtered : undefined };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return req;
|
|
115
|
+
}
|
|
116
|
+
// 鈹€鈹€ Request Body Builder 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
117
|
+
async buildRequestBody(request, apiKey, signal) {
|
|
118
|
+
// W11-W13: Resolve Volcengine-specific capability conflicts before building body
|
|
119
|
+
const req = this.resolveConstraints(request);
|
|
120
|
+
// Pre-resolve local media URLs (Volcengine API can't reach localhost)
|
|
121
|
+
const resolvedMessages = await resolveMessagesMediaForVolcengine(req.messages, this.fileUploadAdapter, apiKey, signal);
|
|
122
|
+
const body = {
|
|
123
|
+
model: req.model,
|
|
124
|
+
input: convertMessagesForResponses(resolvedMessages, this.quirks),
|
|
125
|
+
stream: true,
|
|
126
|
+
};
|
|
127
|
+
// W3: Server-side context continuation (鎼?)
|
|
128
|
+
if (req.previousResponseId) {
|
|
129
|
+
body.previous_response_id = req.previousResponseId;
|
|
130
|
+
}
|
|
131
|
+
// W4: Storage control (鎼?.1)
|
|
132
|
+
if (req.store !== undefined) {
|
|
133
|
+
body.store = req.store;
|
|
134
|
+
}
|
|
135
|
+
if (req.storeExpireAt !== undefined) {
|
|
136
|
+
body.expire_at = req.storeExpireAt;
|
|
137
|
+
}
|
|
138
|
+
// W5: Per-turn instructions (鎼?)note: incompatible with caching
|
|
139
|
+
if (req.instructions) {
|
|
140
|
+
body.instructions = req.instructions;
|
|
141
|
+
}
|
|
142
|
+
// W6: Structured output (鎼?6)
|
|
143
|
+
if (req.structuredOutput) {
|
|
144
|
+
if (req.structuredOutput.mode === "json_object") {
|
|
145
|
+
body.text = { format: { type: "json_object" } };
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const fmt = {
|
|
149
|
+
type: "json_schema",
|
|
150
|
+
name: req.structuredOutput.name,
|
|
151
|
+
schema: req.structuredOutput.schema,
|
|
152
|
+
};
|
|
153
|
+
if (req.structuredOutput.strict !== undefined) {
|
|
154
|
+
fmt.strict = req.structuredOutput.strict;
|
|
155
|
+
}
|
|
156
|
+
body.text = { format: fmt };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// W7: Caching (鎼?0)
|
|
160
|
+
if (req.caching) {
|
|
161
|
+
const cachingObj = { type: req.caching.type };
|
|
162
|
+
if (req.caching.prefix !== undefined)
|
|
163
|
+
cachingObj.prefix = req.caching.prefix;
|
|
164
|
+
body.caching = cachingObj;
|
|
165
|
+
// 鎼?0.3: Prefix cache creation requires stream=false
|
|
166
|
+
if (req.caching.prefix) {
|
|
167
|
+
body.stream = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// W8: Context management edits (鎼?1, beta)
|
|
171
|
+
if (req.contextManagement) {
|
|
172
|
+
const edits = req.contextManagement.edits.map((edit) => {
|
|
173
|
+
const e = { type: edit.type };
|
|
174
|
+
if (edit.type === "clear_thinking" && edit.keep !== undefined) {
|
|
175
|
+
e.keep = edit.keep;
|
|
176
|
+
}
|
|
177
|
+
else if (edit.type === "clear_tool_uses") {
|
|
178
|
+
if (edit.trigger)
|
|
179
|
+
e.trigger = edit.trigger;
|
|
180
|
+
if (edit.keep)
|
|
181
|
+
e.keep = edit.keep;
|
|
182
|
+
if (edit.excludeTools)
|
|
183
|
+
e.exclude_tools = edit.excludeTools;
|
|
184
|
+
if (edit.clearToolInput !== undefined)
|
|
185
|
+
e.clear_tool_input = edit.clearToolInput;
|
|
186
|
+
}
|
|
187
|
+
return e;
|
|
188
|
+
});
|
|
189
|
+
body.context_management = { edits };
|
|
190
|
+
}
|
|
191
|
+
// Tools 閳?Volcengine Responses API format
|
|
192
|
+
const tools = [];
|
|
193
|
+
if (req.tools && req.tools.length > 0) {
|
|
194
|
+
for (const tool of req.tools) {
|
|
195
|
+
tools.push({
|
|
196
|
+
type: "function",
|
|
197
|
+
name: tool.function.name,
|
|
198
|
+
description: tool.function.description,
|
|
199
|
+
parameters: tool.function.parameters,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Builtin tools (鎼?9.8): web_search, image_process, knowledge_search, doubao_app
|
|
204
|
+
// Provider builtins and system function tools COEXISTthey use different type values:
|
|
205
|
+
// system: { type: "function", name: "web_search", ... }
|
|
206
|
+
// builtin: { type: "web_search" }
|
|
207
|
+
// The LLM uses builtins for fast inline search (platform-side, transparent)
|
|
208
|
+
// and system tools for explicit deep/structured search (agent executes).
|
|
209
|
+
// Per B4: respect disableBuiltinTools flag
|
|
210
|
+
if (req.builtinTools && !req.disableBuiltinTools && req.builtinTools.length > 0) {
|
|
211
|
+
for (const bt of req.builtinTools) {
|
|
212
|
+
// Strip "builtin_" prefixVolcengine API uses bare names
|
|
213
|
+
const volcType = bt.type.replace(/^builtin_/, "");
|
|
214
|
+
const entry = { type: volcType };
|
|
215
|
+
// Spread tool-specific config fields at top level (web_search.sources, .limit, etc.)
|
|
216
|
+
if (bt.config)
|
|
217
|
+
Object.assign(entry, bt.config);
|
|
218
|
+
tools.push(entry);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (tools.length > 0) {
|
|
222
|
+
body.tools = tools;
|
|
223
|
+
}
|
|
224
|
+
// max_tool_callsVolcengine tool dispatch scheduling (鎼?9.15)
|
|
225
|
+
if (req.maxToolCalls !== undefined) {
|
|
226
|
+
body.max_tool_calls = req.maxToolCalls;
|
|
227
|
+
}
|
|
228
|
+
// tool_choiceVolcengine supports string or named function object
|
|
229
|
+
if (req.toolChoice !== undefined) {
|
|
230
|
+
if (typeof req.toolChoice === "string") {
|
|
231
|
+
body.tool_choice = req.toolChoice;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// Named function call: { type: "function", name: "..." }
|
|
235
|
+
body.tool_choice = { type: req.toolChoice.type, name: req.toolChoice.name };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Temperature
|
|
239
|
+
if (req.temperature !== undefined) {
|
|
240
|
+
body.temperature = req.temperature;
|
|
241
|
+
}
|
|
242
|
+
// top_p (鎼?7.11: nucleus sampling)
|
|
243
|
+
if (req.topP !== undefined) {
|
|
244
|
+
body.top_p = req.topP;
|
|
245
|
+
}
|
|
246
|
+
// Max tokens 閳?max_output_tokens in Responses API
|
|
247
|
+
if (req.maxTokens !== undefined) {
|
|
248
|
+
body.max_output_tokens = req.maxTokens;
|
|
249
|
+
}
|
|
250
|
+
// Deep thinking: thinking.type + reasoning.effort (鎼?7)
|
|
251
|
+
// thinking: { type: "enabled" | "disabled" | "auto" }
|
|
252
|
+
// reasoning: { effort: "minimal" | "low" | "medium" | "high" }
|
|
253
|
+
// "minimal" maps to thinking.type="disabled" (skip thinking entirely)
|
|
254
|
+
if (req.reasoning) {
|
|
255
|
+
const effort = req.reasoning.effort ?? "high";
|
|
256
|
+
if (effort === "minimal") {
|
|
257
|
+
body.thinking = { type: "disabled" };
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
body.thinking = { type: "enabled" };
|
|
261
|
+
body.reasoning = { effort };
|
|
262
|
+
}
|
|
263
|
+
// 鎼?7.7: Request encrypted original reasoning content
|
|
264
|
+
if (req.reasoning.includeEncryptedReasoning) {
|
|
265
|
+
body.include = ["reasoning.encrypted_content"];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Prefix completion 閳?assistant with partial=true (鎼? prefill mode)
|
|
269
|
+
if (req.prefixMessage) {
|
|
270
|
+
const input = body.input;
|
|
271
|
+
input.push({
|
|
272
|
+
role: "assistant",
|
|
273
|
+
content: req.prefixMessage,
|
|
274
|
+
partial: true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return body;
|
|
278
|
+
}
|
|
279
|
+
// 鈹€鈹€ Fetch + Stream 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
280
|
+
async *fetchAndStream(url, headers, body, signal) {
|
|
281
|
+
const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
|
282
|
+
const combinedSignal = signal
|
|
283
|
+
? AbortSignal.any([signal, timeoutSignal])
|
|
284
|
+
: timeoutSignal;
|
|
285
|
+
const response = await fetch(url, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers,
|
|
288
|
+
body: JSON.stringify(body),
|
|
289
|
+
signal: combinedSignal,
|
|
290
|
+
});
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
const errorBody = await response.text().catch(() => "");
|
|
293
|
+
const err = new Error(`Volcengine Responses API error ${response.status}: ${errorBody.slice(0, 500)}`);
|
|
294
|
+
err.status = response.status;
|
|
295
|
+
throw err;
|
|
296
|
+
}
|
|
297
|
+
if (!response.body) {
|
|
298
|
+
throw new Error("Volcengine Responses API returned no response body");
|
|
299
|
+
}
|
|
300
|
+
// Check if the response is actually SSE (some non-streaming fallback)
|
|
301
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
302
|
+
if (contentType.includes("application/json") && !contentType.includes("text/event-stream")) {
|
|
303
|
+
yield* this.handleNonStreamingResponse(response);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
yield* this.parseSSEStream(response.body);
|
|
307
|
+
}
|
|
308
|
+
// 鈹€鈹€ Non-streaming fallback 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
309
|
+
async *handleNonStreamingResponse(response) {
|
|
310
|
+
const data = await response.json();
|
|
311
|
+
// W10: Emit response ID for context chain
|
|
312
|
+
if (data.id) {
|
|
313
|
+
yield { type: "response_id", id: data.id };
|
|
314
|
+
}
|
|
315
|
+
if (data.usage) {
|
|
316
|
+
yield {
|
|
317
|
+
type: "usage",
|
|
318
|
+
promptTokens: data.usage.input_tokens ?? 0,
|
|
319
|
+
completionTokens: data.usage.output_tokens ?? 0,
|
|
320
|
+
reasoningTokens: data.usage.output_tokens_details?.reasoning_tokens,
|
|
321
|
+
cacheReadTokens: data.usage.input_tokens_details?.cached_tokens,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// Process output items
|
|
325
|
+
if (data.output) {
|
|
326
|
+
for (const item of data.output) {
|
|
327
|
+
if (item.type === "message" && item.content) {
|
|
328
|
+
for (const block of item.content) {
|
|
329
|
+
if (block.type === "output_text") {
|
|
330
|
+
yield { type: "delta", text: block.text ?? "" };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (item.type === "reasoning" && item.summary) {
|
|
335
|
+
for (const block of item.summary) {
|
|
336
|
+
if (block.type === "summary_text") {
|
|
337
|
+
yield { type: "reasoning_delta", text: block.text ?? "" };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (item.type === "function_call") {
|
|
342
|
+
yield {
|
|
343
|
+
type: "tool_call_delta",
|
|
344
|
+
index: 0,
|
|
345
|
+
id: item.call_id,
|
|
346
|
+
name: item.name,
|
|
347
|
+
arguments: item.arguments ?? "",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
yield { type: "done", finishReason: data.status === "completed" ? "stop" : (data.status ?? "stop") };
|
|
353
|
+
}
|
|
354
|
+
// 鈹€鈹€ SSE Stream Parser 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
355
|
+
/**
|
|
356
|
+
* Parse Volcengine Responses API SSE stream.
|
|
357
|
+
*
|
|
358
|
+
* Event format: "event: <type>\ndata: <json>\n\n"
|
|
359
|
+
* Key events:
|
|
360
|
+
* - response.output_text.delta 閳?text content delta
|
|
361
|
+
* - response.reasoning_summary_text.delta 閳?thinking/reasoning text
|
|
362
|
+
* - response.function_call_arguments.delta 閳?tool call arguments streaming
|
|
363
|
+
* - response.output_item.added 閳?new output item started
|
|
364
|
+
* - response.output_item.done 閳?output item completed
|
|
365
|
+
* - response.completed 閳?full response complete with usage
|
|
366
|
+
* - response.failed 閳?error
|
|
367
|
+
*/
|
|
368
|
+
async *parseSSEStream(body) {
|
|
369
|
+
const decoder = new TextDecoder();
|
|
370
|
+
let buffer = "";
|
|
371
|
+
let currentEvent = "";
|
|
372
|
+
let idleTimer = null;
|
|
373
|
+
const abortController = new AbortController();
|
|
374
|
+
// Track tool call indices for multiple parallel function calls
|
|
375
|
+
let toolCallIndex = 0;
|
|
376
|
+
const toolCallIdToIndex = new Map();
|
|
377
|
+
const resetIdleTimer = () => {
|
|
378
|
+
if (idleTimer)
|
|
379
|
+
clearTimeout(idleTimer);
|
|
380
|
+
idleTimer = setTimeout(() => {
|
|
381
|
+
abortController.abort();
|
|
382
|
+
}, STREAM_IDLE_TIMEOUT_MS);
|
|
383
|
+
};
|
|
384
|
+
try {
|
|
385
|
+
resetIdleTimer();
|
|
386
|
+
const reader = body.getReader();
|
|
387
|
+
try {
|
|
388
|
+
while (true) {
|
|
389
|
+
const { done, value } = await reader.read();
|
|
390
|
+
if (done)
|
|
391
|
+
break;
|
|
392
|
+
if (abortController.signal.aborted) {
|
|
393
|
+
throw new Error("Stream idle timeout");
|
|
394
|
+
}
|
|
395
|
+
resetIdleTimer();
|
|
396
|
+
buffer += decoder.decode(value, { stream: true });
|
|
397
|
+
let newlineIdx;
|
|
398
|
+
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
399
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
400
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
401
|
+
if (!line) {
|
|
402
|
+
// Empty line = event boundary, process accumulated event+data
|
|
403
|
+
currentEvent = "";
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (line.startsWith(":"))
|
|
407
|
+
continue; // SSE comment
|
|
408
|
+
// event: <type> (with or without space after colon)
|
|
409
|
+
if (line.startsWith("event:")) {
|
|
410
|
+
currentEvent = line.slice(6).trim();
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
// data: <json>
|
|
414
|
+
if (line.startsWith("data:")) {
|
|
415
|
+
const dataStr = line.slice(5).trim();
|
|
416
|
+
if (dataStr === "[DONE]")
|
|
417
|
+
return;
|
|
418
|
+
let parsed;
|
|
419
|
+
try {
|
|
420
|
+
parsed = JSON.parse(dataStr);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
yield* this.processEvent(currentEvent, parsed, toolCallIdToIndex, () => toolCallIndex++);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
reader.releaseLock();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
if (idleTimer)
|
|
436
|
+
clearTimeout(idleTimer);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// 鈹€鈹€ Event Processing 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
440
|
+
*processEvent(eventType, data, toolCallIdToIndex, nextIndex) {
|
|
441
|
+
switch (eventType) {
|
|
442
|
+
// 鈹€鈹€ Text content delta 鈹€鈹€
|
|
443
|
+
case "response.output_text.delta": {
|
|
444
|
+
const delta = data.delta;
|
|
445
|
+
if (delta) {
|
|
446
|
+
yield { type: "delta", text: delta };
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
// 鈹€鈹€ Reasoning/thinking summary delta 鈹€鈹€
|
|
451
|
+
case "response.reasoning_summary_text.delta": {
|
|
452
|
+
const delta = data.delta;
|
|
453
|
+
if (delta) {
|
|
454
|
+
yield { type: "reasoning_delta", text: delta };
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
// 鈹€鈹€ Function call arguments streaming 鈹€鈹€
|
|
459
|
+
case "response.function_call_arguments.delta": {
|
|
460
|
+
const delta = data.delta;
|
|
461
|
+
const callId = data.call_id;
|
|
462
|
+
const name = data.name;
|
|
463
|
+
if (delta !== undefined && callId) {
|
|
464
|
+
let idx = toolCallIdToIndex.get(callId);
|
|
465
|
+
if (idx === undefined) {
|
|
466
|
+
idx = nextIndex();
|
|
467
|
+
toolCallIdToIndex.set(callId, idx);
|
|
468
|
+
}
|
|
469
|
+
yield {
|
|
470
|
+
type: "tool_call_delta",
|
|
471
|
+
index: idx,
|
|
472
|
+
id: callId,
|
|
473
|
+
name: name,
|
|
474
|
+
arguments: delta,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
// 鈹€鈹€ Output item added (function_call start) 鈹€鈹€
|
|
480
|
+
case "response.output_item.added": {
|
|
481
|
+
const item = data.item;
|
|
482
|
+
if (item && item.type === "function_call") {
|
|
483
|
+
const callId = item.call_id;
|
|
484
|
+
const name = item.name;
|
|
485
|
+
if (callId && !toolCallIdToIndex.has(callId)) {
|
|
486
|
+
const idx = nextIndex();
|
|
487
|
+
toolCallIdToIndex.set(callId, idx);
|
|
488
|
+
yield {
|
|
489
|
+
type: "tool_call_delta",
|
|
490
|
+
index: idx,
|
|
491
|
+
id: callId,
|
|
492
|
+
name: name,
|
|
493
|
+
arguments: "",
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
// 鈹€鈹€ Function call arguments done 鈹€鈹€
|
|
500
|
+
case "response.function_call_arguments.done":
|
|
501
|
+
// Arguments fully received; no action needed (accumulation in tool loop handles it)
|
|
502
|
+
break;
|
|
503
|
+
// 鈹€鈹€ Output item doneextract annotations + finalize function_call args 鈹€鈹€
|
|
504
|
+
case "response.output_item.done": {
|
|
505
|
+
const item = data.item;
|
|
506
|
+
if (item?.type === "message") {
|
|
507
|
+
const content = item.content;
|
|
508
|
+
if (content) {
|
|
509
|
+
for (const block of content) {
|
|
510
|
+
const anns = block.annotations;
|
|
511
|
+
if (anns && anns.length > 0) {
|
|
512
|
+
yield {
|
|
513
|
+
type: "annotations",
|
|
514
|
+
annotations: anns.map(a => ({
|
|
515
|
+
type: a.type ?? "url_citation",
|
|
516
|
+
url: a.url,
|
|
517
|
+
title: a.title,
|
|
518
|
+
...a,
|
|
519
|
+
})),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Volcengine reasoning models may deliver function_call with complete arguments
|
|
526
|
+
// in output_item.done without streaming function_call_arguments.delta events.
|
|
527
|
+
if (item?.type === "function_call") {
|
|
528
|
+
const callId = item.call_id;
|
|
529
|
+
const name = item.name;
|
|
530
|
+
const args = item.arguments ?? "";
|
|
531
|
+
if (callId && args) {
|
|
532
|
+
let idx = toolCallIdToIndex.get(callId);
|
|
533
|
+
const alreadySeen = idx !== undefined;
|
|
534
|
+
if (!alreadySeen) {
|
|
535
|
+
idx = nextIndex();
|
|
536
|
+
toolCallIdToIndex.set(callId, idx);
|
|
537
|
+
}
|
|
538
|
+
yield {
|
|
539
|
+
type: "tool_call_delta",
|
|
540
|
+
index: idx,
|
|
541
|
+
id: callId,
|
|
542
|
+
// Only emit name if not already emitted via output_item.added
|
|
543
|
+
name: alreadySeen ? undefined : name,
|
|
544
|
+
arguments: args,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
// 鈹€鈹€ Response completedextract usage 鈹€鈹€
|
|
551
|
+
case "response.completed": {
|
|
552
|
+
const resp = data.response;
|
|
553
|
+
if (resp) {
|
|
554
|
+
// W10: Emit response ID for server-side context chain (鎼?)
|
|
555
|
+
const respId = resp.id;
|
|
556
|
+
if (respId) {
|
|
557
|
+
yield { type: "response_id", id: respId };
|
|
558
|
+
}
|
|
559
|
+
const usage = resp.usage;
|
|
560
|
+
if (usage) {
|
|
561
|
+
const inputDetails = usage.input_tokens_details;
|
|
562
|
+
const outputDetails = usage.output_tokens_details;
|
|
563
|
+
yield {
|
|
564
|
+
type: "usage",
|
|
565
|
+
promptTokens: usage.input_tokens ?? 0,
|
|
566
|
+
completionTokens: usage.output_tokens ?? 0,
|
|
567
|
+
reasoningTokens: outputDetails?.reasoning_tokens,
|
|
568
|
+
cacheReadTokens: inputDetails?.cached_tokens,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
// Check if response ended with function calls (incomplete = need tool results)
|
|
572
|
+
const status = resp.status;
|
|
573
|
+
yield { type: "done", finishReason: status === "incomplete" ? "tool_calls" : "stop" };
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
yield { type: "done", finishReason: "stop" };
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
// 鈹€鈹€ Response failed 鈹€鈹€
|
|
581
|
+
case "response.failed": {
|
|
582
|
+
const resp = data.response;
|
|
583
|
+
const error = resp?.error;
|
|
584
|
+
const message = error?.message ?? "Unknown error";
|
|
585
|
+
yield { type: "error", message };
|
|
586
|
+
yield { type: "done", finishReason: "error" };
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
// 鈹€鈹€ Builtin tool lifecycle events (鎼?9.12): web_search, image_process 鈹€鈹€
|
|
590
|
+
// These are informational SSE events emitted during platform-executed tool runs.
|
|
591
|
+
// Forward them so upper layers can show search progress, cost, and citations.
|
|
592
|
+
default: {
|
|
593
|
+
if (eventType.startsWith("response.web_search_call")) {
|
|
594
|
+
yield {
|
|
595
|
+
type: "builtin_tool_status",
|
|
596
|
+
toolType: "web_search",
|
|
597
|
+
event: eventType,
|
|
598
|
+
data: data,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
else if (eventType.startsWith("response.image_process")) {
|
|
602
|
+
yield {
|
|
603
|
+
type: "builtin_tool_status",
|
|
604
|
+
toolType: "image_process",
|
|
605
|
+
event: eventType,
|
|
606
|
+
data: data,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
// Other events (output_text.done, etc.) are purely informational.
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// 鈹€鈹€ Local media resolution for Volcengine 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
616
|
+
/**
|
|
617
|
+
* Pre-resolve local media URLs. Volcengine's API accepts URLs for images/video/audio,
|
|
618
|
+
* but cannot reach localhost. Upload to File API when adapter available, otherwise base64 fallback.
|
|
619
|
+
*/
|
|
620
|
+
async function resolveMessagesMediaForVolcengine(messages, uploadAdapter, apiKey, signal) {
|
|
621
|
+
const needsResolution = messages.some((m) => m.imageUrls?.some(isLocalUrl) || m.videoUrls?.some(isLocalUrl) || m.audioUrls?.some(isLocalUrl));
|
|
622
|
+
if (!needsResolution)
|
|
623
|
+
return messages;
|
|
624
|
+
const resolve = (url) => {
|
|
625
|
+
if (!isLocalUrl(url))
|
|
626
|
+
return Promise.resolve(url);
|
|
627
|
+
if (!uploadAdapter || !apiKey) {
|
|
628
|
+
throw new Error("FileUploadAdapter required for local media URLs. Configure OSS_ACCESS_KEY_ID/OSS_ACCESS_KEY_SECRET or QLOGICAGENT_HUB_URL.");
|
|
629
|
+
}
|
|
630
|
+
return resolveMediaUrlViaUpload(url, { uploadAdapter, apiKey, signal });
|
|
631
|
+
};
|
|
632
|
+
return Promise.all(messages.map(async (msg) => {
|
|
633
|
+
if (msg.role !== "user" && msg.role !== "tool")
|
|
634
|
+
return msg;
|
|
635
|
+
const patch = {};
|
|
636
|
+
if (msg.imageUrls?.some(isLocalUrl)) {
|
|
637
|
+
patch.imageUrls = await Promise.all(msg.imageUrls.map(resolve));
|
|
638
|
+
}
|
|
639
|
+
if (msg.role === "user" && msg.videoUrls?.some(isLocalUrl)) {
|
|
640
|
+
patch.videoUrls = await Promise.all(msg.videoUrls.map(resolve));
|
|
641
|
+
}
|
|
642
|
+
if (msg.role === "user" && msg.audioUrls?.some(isLocalUrl)) {
|
|
643
|
+
patch.audioUrls = await Promise.all(msg.audioUrls.map(resolve));
|
|
644
|
+
}
|
|
645
|
+
return Object.keys(patch).length > 0 ? { ...msg, ...patch } : msg;
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
// 鈹€鈹€ Message Conversion for Responses API 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
|
|
649
|
+
/**
|
|
650
|
+
* Convert qlogicagent ChatMessage[] into Volcengine Responses API `input` format.
|
|
651
|
+
*
|
|
652
|
+
* Responses API input is an array of message objects with roles:
|
|
653
|
+
* - { role: "user", content: "..." } or { role: "user", content: [{type:"input_text",...},{type:"input_image",...}] }
|
|
654
|
+
* - { role: "assistant", content: "..." }
|
|
655
|
+
* - { role: "system", content: "..." } (extracted to top-level `instructions`)
|
|
656
|
+
* - { type: "function_call_output", call_id: "...", output: "..." }
|
|
657
|
+
*
|
|
658
|
+
* Key differences from OpenAI Chat:
|
|
659
|
+
* - Vision uses input_image (not image_url) with url field
|
|
660
|
+
* - Tool results use function_call_output type (not role: "tool")
|
|
661
|
+
* - System messages map to instructions (handled separately)
|
|
662
|
+
*/
|
|
663
|
+
function convertMessagesForResponses(messages, quirks = {}) {
|
|
664
|
+
const result = [];
|
|
665
|
+
for (const msg of messages) {
|
|
666
|
+
// 鈹€鈹€ System messages 閳?pass as-is (Responses API supports role: "system" in input) 鈹€鈹€
|
|
667
|
+
if (msg.role === "system") {
|
|
668
|
+
result.push({ role: "system", content: msg.content ?? "" });
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
// 鈹€鈹€ User messages: handle vision content blocks 鈹€鈹€
|
|
672
|
+
if (msg.role === "user") {
|
|
673
|
+
const hasImages = msg.imageUrls && msg.imageUrls.length > 0;
|
|
674
|
+
const hasVideos = msg.videoUrls && msg.videoUrls.length > 0;
|
|
675
|
+
const hasAudios = msg.audioUrls && msg.audioUrls.length > 0;
|
|
676
|
+
const hasFiles = msg.fileIds && msg.fileIds.length > 0;
|
|
677
|
+
const hasMultimodal = hasImages || hasVideos || hasAudios || hasFiles;
|
|
678
|
+
if (hasMultimodal) {
|
|
679
|
+
const content = [];
|
|
680
|
+
// Volcengine Responses API: media content before text (official wire order)
|
|
681
|
+
if (hasImages) {
|
|
682
|
+
const multiImage = msg.imageUrls.length > 1;
|
|
683
|
+
for (let imgIdx = 0; imgIdx < msg.imageUrls.length; imgIdx++) {
|
|
684
|
+
const url = msg.imageUrls[imgIdx];
|
|
685
|
+
if (multiImage) {
|
|
686
|
+
content.push({ type: "input_text", text: `[Image ${imgIdx + 1}]` });
|
|
687
|
+
}
|
|
688
|
+
const img = { type: "input_image", image_url: url };
|
|
689
|
+
if (msg.imageDetail)
|
|
690
|
+
img.detail = msg.imageDetail;
|
|
691
|
+
if (msg.imagePixelLimit)
|
|
692
|
+
img.image_pixel_limit = msg.imagePixelLimit;
|
|
693
|
+
content.push(img);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (hasVideos) {
|
|
697
|
+
for (const url of msg.videoUrls) {
|
|
698
|
+
const vid = { type: "input_video", video_url: url };
|
|
699
|
+
if (msg.videoFps !== undefined)
|
|
700
|
+
vid.fps = msg.videoFps;
|
|
701
|
+
content.push(vid);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (hasAudios) {
|
|
705
|
+
for (const url of msg.audioUrls) {
|
|
706
|
+
const aud = { type: "input_audio", audio_url: url };
|
|
707
|
+
if (msg.audioFormat)
|
|
708
|
+
aud.format = msg.audioFormat;
|
|
709
|
+
content.push(aud);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (hasFiles) {
|
|
713
|
+
for (const f of msg.fileIds) {
|
|
714
|
+
const mime = f.mimeType || "";
|
|
715
|
+
const tooLarge = f.size != null && f.size > MEDIA_MAX_UPLOAD_SIZE;
|
|
716
|
+
if (tooLarge) {
|
|
717
|
+
const sizeLabel = `${(f.size / (1024 * 1024)).toFixed(1)}MB`;
|
|
718
|
+
content.push({ type: "input_text", text: `[Attached: ${f.id} (${mime || "unknown"}, ${sizeLabel}) \u2014 file too large for direct vision, use tools to process]` });
|
|
719
|
+
}
|
|
720
|
+
else if (quirks.supportsDocumentVision && (mime === "application/pdf" || f.id.endsWith(".pdf"))) {
|
|
721
|
+
// Only use input_file for platform file IDs, not HTTP URLs
|
|
722
|
+
if (f.id.startsWith("http://") || f.id.startsWith("https://")) {
|
|
723
|
+
content.push({ type: "input_text", text: `[Attached PDF: ${f.id}]` });
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
content.push({ type: "input_file", file_id: f.id });
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
else if (f.id.startsWith("http://") || f.id.startsWith("https://")) {
|
|
730
|
+
const label = mime ? `[Attached: ${f.id} (${mime})]` : `[Attached: ${f.id}]`;
|
|
731
|
+
content.push({ type: "input_text", text: label });
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
content.push({ type: "input_file", file_id: f.id });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (msg.content) {
|
|
739
|
+
content.push({ type: "input_text", text: msg.content });
|
|
740
|
+
}
|
|
741
|
+
result.push({ role: "user", content });
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
result.push({ role: "user", content: msg.content ?? "" });
|
|
745
|
+
}
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
// 鈹€鈹€ Assistant messages: include tool calls as separate function_call items 鈹€鈹€
|
|
749
|
+
if (msg.role === "assistant") {
|
|
750
|
+
// Emit text content only if non-empty (empty assistant content with tool_calls causes 400)
|
|
751
|
+
if (msg.content) {
|
|
752
|
+
result.push({ role: "assistant", content: msg.content });
|
|
753
|
+
}
|
|
754
|
+
// Emit tool calls as function_call items (required for multi-turn replay)
|
|
755
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
756
|
+
for (const tc of msg.tool_calls) {
|
|
757
|
+
result.push({
|
|
758
|
+
type: "function_call",
|
|
759
|
+
call_id: tc.id,
|
|
760
|
+
name: tc.function.name,
|
|
761
|
+
arguments: tc.function.arguments || "{}",
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
// 鈹€鈹€ Tool result messages 閳?function_call_output 鈹€鈹€
|
|
768
|
+
if (msg.role === "tool") {
|
|
769
|
+
// G2: call_id is required by Volcengineskip malformed tool results
|
|
770
|
+
if (!msg.tool_call_id)
|
|
771
|
+
continue;
|
|
772
|
+
// If tool result has imageUrls, include as content array
|
|
773
|
+
if (msg.imageUrls && msg.imageUrls.length > 0) {
|
|
774
|
+
const content = [];
|
|
775
|
+
if (msg.content)
|
|
776
|
+
content.push({ type: "input_text", text: msg.content });
|
|
777
|
+
for (const url of msg.imageUrls) {
|
|
778
|
+
content.push({ type: "input_image", image_url: url });
|
|
779
|
+
}
|
|
780
|
+
result.push({
|
|
781
|
+
type: "function_call_output",
|
|
782
|
+
call_id: msg.tool_call_id,
|
|
783
|
+
output: content,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
result.push({
|
|
788
|
+
type: "function_call_output",
|
|
789
|
+
call_id: msg.tool_call_id,
|
|
790
|
+
output: msg.content ?? "",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return result;
|
|
797
|
+
}
|