@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,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
+ }