@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.
Files changed (93) hide show
  1. package/dist/adapters/aliyun-oss-file-upload-adapter.d.ts +44 -0
  2. package/dist/adapters/aliyun-oss-file-upload-adapter.js +96 -0
  3. package/dist/adapters/gemini-file-upload-adapter.d.ts +26 -0
  4. package/dist/adapters/gemini-file-upload-adapter.js +92 -0
  5. package/dist/adapters/hub-oss-file-upload-adapter.d.ts +29 -0
  6. package/dist/adapters/hub-oss-file-upload-adapter.js +53 -0
  7. package/dist/adapters/index.d.ts +10 -0
  8. package/dist/adapters/index.js +10 -0
  9. package/dist/adapters/openai-file-upload-adapter.d.ts +38 -0
  10. package/dist/adapters/openai-file-upload-adapter.js +56 -0
  11. package/dist/adapters/volcengine-file-upload-adapter.d.ts +24 -0
  12. package/dist/adapters/volcengine-file-upload-adapter.js +45 -0
  13. package/dist/builtin-providers.d.ts +8 -0
  14. package/dist/builtin-providers.js +2237 -0
  15. package/dist/constants.d.ts +1 -0
  16. package/dist/constants.js +1 -0
  17. package/dist/credentials.d.ts +1 -0
  18. package/dist/credentials.js +8 -0
  19. package/dist/debug-transport.d.ts +12 -0
  20. package/dist/debug-transport.js +99 -0
  21. package/dist/errors.d.ts +11 -0
  22. package/dist/errors.js +12 -0
  23. package/dist/events.d.ts +48 -0
  24. package/dist/events.js +1 -0
  25. package/dist/file-upload-service.d.ts +68 -0
  26. package/dist/file-upload-service.js +110 -0
  27. package/dist/gemini-schema-utils.d.ts +17 -0
  28. package/dist/gemini-schema-utils.js +76 -0
  29. package/dist/index.d.ts +37 -0
  30. package/dist/index.js +33 -0
  31. package/dist/llm-client.d.ts +43 -0
  32. package/dist/llm-client.js +217 -0
  33. package/dist/media-client.d.ts +42 -0
  34. package/dist/media-client.js +174 -0
  35. package/dist/media-transport.d.ts +176 -0
  36. package/dist/media-transport.js +16 -0
  37. package/dist/media.d.ts +2 -0
  38. package/dist/media.js +1 -0
  39. package/dist/model-detection.d.ts +22 -0
  40. package/dist/model-detection.js +28 -0
  41. package/dist/paths.d.ts +2 -0
  42. package/dist/paths.js +11 -0
  43. package/dist/provider-def.d.ts +220 -0
  44. package/dist/provider-def.js +9 -0
  45. package/dist/provider-registry.d.ts +51 -0
  46. package/dist/provider-registry.js +130 -0
  47. package/dist/provider-tool-api.d.ts +44 -0
  48. package/dist/provider-tool-api.js +9 -0
  49. package/dist/provider-variant-resolver.d.ts +35 -0
  50. package/dist/provider-variant-resolver.js +174 -0
  51. package/dist/retry.d.ts +37 -0
  52. package/dist/retry.js +71 -0
  53. package/dist/transport.d.ts +281 -0
  54. package/dist/transport.js +27 -0
  55. package/dist/transports/anthropic-messages.d.ts +65 -0
  56. package/dist/transports/anthropic-messages.js +1004 -0
  57. package/dist/transports/gemini-cache-api.d.ts +86 -0
  58. package/dist/transports/gemini-cache-api.js +141 -0
  59. package/dist/transports/gemini-file-api.d.ts +90 -0
  60. package/dist/transports/gemini-file-api.js +164 -0
  61. package/dist/transports/gemini-generatecontent.d.ts +56 -0
  62. package/dist/transports/gemini-generatecontent.js +688 -0
  63. package/dist/transports/gemini-lyria-realtime.d.ts +117 -0
  64. package/dist/transports/gemini-lyria-realtime.js +295 -0
  65. package/dist/transports/gemini-media.d.ts +53 -0
  66. package/dist/transports/gemini-media.js +383 -0
  67. package/dist/transports/media-resolve.d.ts +50 -0
  68. package/dist/transports/media-resolve.js +91 -0
  69. package/dist/transports/minimax-media.d.ts +56 -0
  70. package/dist/transports/minimax-media.js +433 -0
  71. package/dist/transports/openai-chat.d.ts +81 -0
  72. package/dist/transports/openai-chat.js +782 -0
  73. package/dist/transports/openai-media.d.ts +24 -0
  74. package/dist/transports/openai-media.js +118 -0
  75. package/dist/transports/openai-responses.d.ts +63 -0
  76. package/dist/transports/openai-responses.js +778 -0
  77. package/dist/transports/qwen-media.d.ts +59 -0
  78. package/dist/transports/qwen-media.js +411 -0
  79. package/dist/transports/realtime-transport.d.ts +183 -0
  80. package/dist/transports/realtime-transport.js +332 -0
  81. package/dist/transports/volcengine-grounding.d.ts +58 -0
  82. package/dist/transports/volcengine-grounding.js +69 -0
  83. package/dist/transports/volcengine-media.d.ts +94 -0
  84. package/dist/transports/volcengine-media.js +801 -0
  85. package/dist/transports/volcengine-responses.d.ts +64 -0
  86. package/dist/transports/volcengine-responses.js +797 -0
  87. package/dist/transports/zhipu-media.d.ts +82 -0
  88. package/dist/transports/zhipu-media.js +522 -0
  89. package/dist/transports/zhipu-tool-api.d.ts +35 -0
  90. package/dist/transports/zhipu-tool-api.js +126 -0
  91. package/dist/wire-types.d.ts +51 -0
  92. package/dist/wire-types.js +1 -0
  93. 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
+ }