@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,688 @@
1
+ /**
2
+ * Gemini generateContent TransportNative Gemini API streaming implementation.
3
+ *
4
+ * Targets Gemini 3 series exclusively (3.1 Pro, 3 Flash, 3.1 Flash-Lite).
5
+ * Uses the native Gemini REST API instead of the OpenAI compatibility layer,
6
+ * unlocking Gemini-exclusive features unavailable via the compat endpoint:
7
+ * - thinkingConfig (thinkingLevelG3 native control)
8
+ * - Google Search / Maps Grounding
9
+ * - Code Execution
10
+ * - Safety Settings fine-grained control
11
+ * - Thought Signatures for multi-turn reasoning continuity
12
+ * - URL Context / File Search
13
+ * - systemInstruction top-level field
14
+ *
15
+ * Streaming endpoint: POST .../models/{model}:streamGenerateContent?alt=sse
16
+ * Non-streaming: POST .../models/{model}:generateContent
17
+ * Auth: x-goog-api-key header
18
+ *
19
+ * Protocol reference: https://ai.google.dev/gemini-api/docs
20
+ * Aligned with gemini-ProviderMax.md native protocol strategy.
21
+ */
22
+ import { MEDIA_MAX_UPLOAD_SIZE } from "../constants.js";
23
+ import { isLocalUrl, resolveMediaUrlViaUpload } from "./media-resolve.js";
24
+ import { DEFAULT_MAX_RETRIES, STREAM_IDLE_TIMEOUT_MS, retryDelay, retrySleep, extractHttpStatus, isTransientStatus, } from "../retry.js";
25
+ import { cleanSchemaForGemini } from "../gemini-schema-utils.js";
26
+ // 鈹€鈹€ Safety categories for Agent use (gemini-ProviderMax 鎼?) 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
27
+ const AGENT_SAFETY_SETTINGS = [
28
+ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_ONLY_HIGH" },
29
+ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" },
30
+ { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_ONLY_HIGH" },
31
+ { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_ONLY_HIGH" },
32
+ { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_ONLY_HIGH" },
33
+ ];
34
+ // 鈹€鈹€ Thinking effort 閳?Gemini 3 thinkingLevel mapping 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
35
+ function mapEffortToThinkingLevel(effort) {
36
+ switch (effort) {
37
+ case "minimal": return "MINIMAL";
38
+ case "low": return "LOW";
39
+ case "medium": return "MEDIUM";
40
+ case "high": return "HIGH";
41
+ default: return "HIGH";
42
+ }
43
+ }
44
+ // 鈹€鈹€ Transport class 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
45
+ export class GeminiGenerateContentTransport {
46
+ baseUrl;
47
+ timeoutMs;
48
+ quirks;
49
+ fileUploadAdapter;
50
+ constructor(config) {
51
+ if (!config.baseUrl) {
52
+ throw new Error("GeminiGenerateContentTransport: baseUrl is required");
53
+ }
54
+ this.baseUrl = config.baseUrl
55
+ .replace(/\/openai\/?$/, "")
56
+ .replace(/\/+$/, "");
57
+ this.timeoutMs = config.timeoutMs ?? 180_000;
58
+ this.quirks = config.quirks ?? {};
59
+ this.fileUploadAdapter = config.fileUploadAdapter;
60
+ }
61
+ async *stream(request, apiKey, signal) {
62
+ const streamUrl = `${this.baseUrl}/models/${encodeURIComponent(request.model)}:streamGenerateContent?alt=sse`;
63
+ const body = await this.buildRequestBody(request, apiKey, signal);
64
+ const headers = {
65
+ "Content-Type": "application/json",
66
+ "x-goog-api-key": apiKey,
67
+ };
68
+ // Retry loop with exponential backoff (CC parity)
69
+ let lastError = null;
70
+ for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
71
+ if (signal?.aborted)
72
+ throw new Error("Request aborted");
73
+ if (attempt > 0 && lastError) {
74
+ await retrySleep(retryDelay(attempt), signal);
75
+ }
76
+ try {
77
+ yield* this.fetchAndStream(streamUrl, headers, body, signal);
78
+ return;
79
+ }
80
+ catch (err) {
81
+ lastError = err instanceof Error ? err : new Error(String(err));
82
+ const isIdleTimeout = lastError.message.includes("Stream idle timeout");
83
+ if (!isTransientStatus(extractHttpStatus(lastError)) && !isIdleTimeout)
84
+ throw lastError;
85
+ if (attempt === DEFAULT_MAX_RETRIES) {
86
+ // Last attempt: try non-streaming fallback
87
+ if ((isIdleTimeout || isTransientStatus(extractHttpStatus(lastError))) && !request.streamRequired) {
88
+ try {
89
+ yield* this.nonStreamingFallback(request, apiKey, signal);
90
+ return;
91
+ }
92
+ catch {
93
+ throw lastError;
94
+ }
95
+ }
96
+ throw lastError;
97
+ }
98
+ }
99
+ }
100
+ }
101
+ // 鈹€鈹€ Request Body Builder 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
102
+ async buildRequestBody(request, apiKey, signal) {
103
+ const resolved = await resolveMessagesMediaForGemini(request.messages, this.fileUploadAdapter, apiKey, signal);
104
+ const { systemInstruction, contents } = convertMessages(resolved);
105
+ const body = { contents };
106
+ if (systemInstruction) {
107
+ body.system_instruction = systemInstruction;
108
+ }
109
+ // Tools: function declarations + native Gemini tools
110
+ const tools = this.buildTools(request);
111
+ if (tools.length > 0) {
112
+ body.tools = tools;
113
+ }
114
+ // Tool config (function calling mode)
115
+ if (request.tools && request.tools.length > 0 && request.toolChoice) {
116
+ body.toolConfig = this.buildToolConfig(request.toolChoice);
117
+ }
118
+ // Generation config
119
+ const generationConfig = this.buildGenerationConfig(request);
120
+ if (Object.keys(generationConfig).length > 0) {
121
+ body.generationConfig = generationConfig;
122
+ }
123
+ // Safety settings for agent use (gemini-ProviderMax 鎼?)
124
+ body.safetySettings = AGENT_SAFETY_SETTINGS;
125
+ // Explicit cache reference (gemini-ProviderMax 鎼?)
126
+ if (request.cachedContent) {
127
+ body.cachedContent = request.cachedContent;
128
+ }
129
+ return body;
130
+ }
131
+ buildTools(request) {
132
+ const tools = [];
133
+ // Function declarations
134
+ if (request.tools && request.tools.length > 0) {
135
+ const functionDeclarations = request.tools.map(convertToolDef);
136
+ tools.push({ functionDeclarations });
137
+ }
138
+ // Native Gemini tools (Google Search, Code Execution, URL Context, Maps, File Search)
139
+ if (!request.disableBuiltinTools) {
140
+ if (this.quirks.builtinWebSearch) {
141
+ tools.push({ googleSearch: {} });
142
+ }
143
+ if (this.quirks.builtinCodeInterpreter) {
144
+ tools.push({ codeExecution: {} });
145
+ }
146
+ if (this.quirks.builtinUrlContext) {
147
+ tools.push({ urlContext: {} });
148
+ }
149
+ if (this.quirks.builtinMapsGrounding) {
150
+ tools.push({ googleMaps: {} });
151
+ }
152
+ if (this.quirks.builtinFileSearch) {
153
+ tools.push({ fileSearch: {} });
154
+ }
155
+ }
156
+ return tools;
157
+ }
158
+ buildToolConfig(toolChoice) {
159
+ if (typeof toolChoice === "string") {
160
+ const modeMap = {
161
+ auto: "AUTO",
162
+ none: "NONE",
163
+ required: "ANY",
164
+ };
165
+ return {
166
+ functionCallingConfig: { mode: modeMap[toolChoice] ?? "AUTO" },
167
+ };
168
+ }
169
+ // Specific function
170
+ return {
171
+ functionCallingConfig: {
172
+ mode: "ANY",
173
+ allowedFunctionNames: [toolChoice.name],
174
+ },
175
+ };
176
+ }
177
+ buildGenerationConfig(request) {
178
+ const config = {};
179
+ // TemperatureG3 recommends keeping default 1.0
180
+ if (request.temperature !== undefined) {
181
+ config.temperature = request.temperature;
182
+ }
183
+ if (request.topP !== undefined) {
184
+ config.topP = request.topP;
185
+ }
186
+ if (request.maxTokens !== undefined) {
187
+ config.maxOutputTokens = request.maxTokens;
188
+ }
189
+ // Thinking configG3 thinkingLevel (gemini-ProviderMax 鎼?)
190
+ if (request.reasoning) {
191
+ let level = mapEffortToThinkingLevel(request.reasoning.effort);
192
+ // 3.1 Pro does not support MINIMALclamp to LOW
193
+ if (level === "MINIMAL" && request.model.includes("pro")) {
194
+ level = "LOW";
195
+ }
196
+ config.thinkingConfig = {
197
+ thinkingLevel: level,
198
+ includeThoughts: true,
199
+ };
200
+ }
201
+ // Structured output (gemini-ProviderMax 鎼?)
202
+ if (request.structuredOutput) {
203
+ if (request.structuredOutput.mode === "json_object") {
204
+ config.responseMimeType = "application/json";
205
+ }
206
+ else if (request.structuredOutput.mode === "json_schema") {
207
+ config.responseMimeType = "application/json";
208
+ config.responseSchema = request.structuredOutput.schema;
209
+ }
210
+ }
211
+ return config;
212
+ }
213
+ // 鈹€鈹€ Fetch + Stream 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
214
+ async *fetchAndStream(url, headers, body, signal) {
215
+ const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
216
+ const combinedSignal = signal
217
+ ? AbortSignal.any([signal, timeoutSignal])
218
+ : timeoutSignal;
219
+ const response = await fetch(url, {
220
+ method: "POST",
221
+ headers,
222
+ body: JSON.stringify(body),
223
+ signal: combinedSignal,
224
+ });
225
+ if (!response.ok) {
226
+ const errorBody = await response.text().catch(() => "");
227
+ const err = new Error(`Gemini API error ${response.status}: ${errorBody.slice(0, 500)}`);
228
+ err.status = response.status;
229
+ throw err;
230
+ }
231
+ if (!response.body) {
232
+ throw new Error("Gemini API returned no response body");
233
+ }
234
+ // Gemini SSE format: same data: prefix as OpenAI
235
+ const contentType = response.headers.get("content-type") ?? "";
236
+ if (contentType.includes("application/json") && !contentType.includes("text/event-stream")) {
237
+ // Non-streaming response (shouldn't happen with streamGenerateContent, but handle gracefully)
238
+ const data = await response.json();
239
+ yield* this.processResponse(data);
240
+ return;
241
+ }
242
+ yield* this.parseSSEStreamWithWatchdog(response.body);
243
+ }
244
+ /**
245
+ * Parse SSE stream with 90s idle watchdog (CC parity).
246
+ */
247
+ async *parseSSEStreamWithWatchdog(body) {
248
+ const decoder = new TextDecoder();
249
+ let buffer = "";
250
+ let idleTimer = null;
251
+ const abortController = new AbortController();
252
+ const resetIdleTimer = () => {
253
+ if (idleTimer)
254
+ clearTimeout(idleTimer);
255
+ idleTimer = setTimeout(() => {
256
+ abortController.abort();
257
+ }, STREAM_IDLE_TIMEOUT_MS);
258
+ };
259
+ try {
260
+ resetIdleTimer();
261
+ const reader = body.getReader();
262
+ try {
263
+ while (true) {
264
+ const { done, value } = await reader.read();
265
+ if (done)
266
+ break;
267
+ if (abortController.signal.aborted) {
268
+ throw new Error("Stream idle timeout");
269
+ }
270
+ resetIdleTimer();
271
+ buffer += decoder.decode(value, { stream: true });
272
+ let newlineIdx;
273
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
274
+ const line = buffer.slice(0, newlineIdx).trim();
275
+ buffer = buffer.slice(newlineIdx + 1);
276
+ if (!line)
277
+ continue;
278
+ if (line.startsWith(":"))
279
+ continue;
280
+ if (!line.startsWith("data: "))
281
+ continue;
282
+ const data = line.slice(6);
283
+ if (data === "[DONE]")
284
+ return;
285
+ let parsed;
286
+ try {
287
+ parsed = JSON.parse(data);
288
+ }
289
+ catch {
290
+ continue;
291
+ }
292
+ yield* this.processResponse(parsed);
293
+ }
294
+ }
295
+ }
296
+ finally {
297
+ reader.releaseLock();
298
+ }
299
+ }
300
+ finally {
301
+ if (idleTimer)
302
+ clearTimeout(idleTimer);
303
+ }
304
+ }
305
+ /**
306
+ * Process a single Gemini response chunk, yielding LLMChunk events.
307
+ */
308
+ *processResponse(response) {
309
+ // Error check
310
+ if (response.error) {
311
+ yield { type: "error", message: `Gemini error ${response.error.code}: ${response.error.message}` };
312
+ return;
313
+ }
314
+ // Usage metadata
315
+ if (response.usageMetadata) {
316
+ const u = response.usageMetadata;
317
+ yield {
318
+ type: "usage",
319
+ promptTokens: u.promptTokenCount ?? 0,
320
+ completionTokens: u.candidatesTokenCount ?? 0,
321
+ reasoningTokens: u.thoughtsTokenCount,
322
+ cacheReadTokens: u.cachedContentTokenCount,
323
+ };
324
+ }
325
+ if (!response.candidates || response.candidates.length === 0)
326
+ return;
327
+ for (const candidate of response.candidates) {
328
+ // Safety block
329
+ if (candidate.finishReason === "SAFETY") {
330
+ yield { type: "error", message: "Response blocked by Gemini safety filters" };
331
+ yield { type: "done", finishReason: "content_filter" };
332
+ return;
333
+ }
334
+ const parts = candidate.content?.parts ?? [];
335
+ let toolCallIndex = 0;
336
+ for (const part of parts) {
337
+ // Thought / reasoning content
338
+ if (part.thought && part.text) {
339
+ yield { type: "reasoning_delta", text: part.text };
340
+ // If part has thoughtSignature, yield as reasoning_block_complete for passback
341
+ if (part.thoughtSignature) {
342
+ yield {
343
+ type: "reasoning_block_complete",
344
+ thinking: part.text,
345
+ signature: part.thoughtSignature,
346
+ };
347
+ }
348
+ continue;
349
+ }
350
+ // Text content
351
+ if (part.text && !part.thought) {
352
+ yield { type: "delta", text: part.text };
353
+ continue;
354
+ }
355
+ // Function call
356
+ if (part.functionCall) {
357
+ const fc = part.functionCall;
358
+ yield {
359
+ type: "tool_call_delta",
360
+ index: toolCallIndex,
361
+ id: fc.id ?? `gemini-tc-${toolCallIndex}`,
362
+ name: fc.name,
363
+ arguments: JSON.stringify(fc.args ?? {}),
364
+ };
365
+ // If function call part has a thought signature, emit for passback
366
+ if (part.thoughtSignature) {
367
+ yield {
368
+ type: "reasoning_block_complete",
369
+ thinking: "",
370
+ signature: part.thoughtSignature,
371
+ };
372
+ }
373
+ toolCallIndex++;
374
+ continue;
375
+ }
376
+ // Code execution result (from codeExecution tool)
377
+ if (part.codeExecutionResult) {
378
+ const output = part.codeExecutionResult.output ?? "";
379
+ yield { type: "delta", text: `\n\`\`\`output\n${output}\n\`\`\`\n` };
380
+ continue;
381
+ }
382
+ // Executable code (from codeExecution tool)
383
+ if (part.executableCode) {
384
+ const lang = part.executableCode.language ?? "python";
385
+ yield { type: "delta", text: `\n\`\`\`${lang}\n${part.executableCode.code}\n\`\`\`\n` };
386
+ continue;
387
+ }
388
+ }
389
+ // Grounding metadata 閳?annotations (Google Search / Maps citations)
390
+ if (candidate.groundingMetadata?.groundingChunks) {
391
+ const annotations = candidate.groundingMetadata.groundingChunks
392
+ .filter((c) => c.web?.uri)
393
+ .map((c) => ({
394
+ type: "web_search",
395
+ url: c.web.uri,
396
+ title: c.web.title,
397
+ }));
398
+ if (annotations.length > 0) {
399
+ yield { type: "annotations", annotations };
400
+ }
401
+ }
402
+ // Finish reason
403
+ if (candidate.finishReason) {
404
+ yield { type: "done", finishReason: mapGeminiFinishReason(candidate.finishReason) };
405
+ }
406
+ }
407
+ }
408
+ // 鈹€鈹€ Non-streaming fallback 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
409
+ async *nonStreamingFallback(request, apiKey, signal) {
410
+ const url = `${this.baseUrl}/models/${encodeURIComponent(request.model)}:generateContent`;
411
+ const body = await this.buildRequestBody(request, apiKey, signal);
412
+ const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
413
+ const combinedSignal = signal
414
+ ? AbortSignal.any([signal, timeoutSignal])
415
+ : timeoutSignal;
416
+ const response = await fetch(url, {
417
+ method: "POST",
418
+ headers: {
419
+ "Content-Type": "application/json",
420
+ "x-goog-api-key": apiKey,
421
+ },
422
+ body: JSON.stringify(body),
423
+ signal: combinedSignal,
424
+ });
425
+ if (!response.ok) {
426
+ const errorBody = await response.text().catch(() => "");
427
+ throw new Error(`Gemini API error ${response.status}: ${errorBody.slice(0, 500)}`);
428
+ }
429
+ const data = await response.json();
430
+ yield* this.processResponse(data);
431
+ }
432
+ }
433
+ // 鈹€鈹€ Local media resolution for Gemini 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
434
+ /**
435
+ * Pre-resolve local media URLs for Gemini.
436
+ * Gemini's fileData uses fileUri (must be publicly accessible or a Gemini File API URI).
437
+ * Local MediaStore URLs 閳?upload to Gemini File API when adapter available, otherwise data URLs.
438
+ */
439
+ async function resolveMessagesMediaForGemini(messages, uploadAdapter, apiKey, signal) {
440
+ const needsResolution = messages.some((m) => m.imageUrls?.some(isLocalUrl) || m.videoUrls?.some(isLocalUrl) ||
441
+ m.audioUrls?.some(isLocalUrl) || m.fileIds?.some((f) => isLocalUrl(f.id)));
442
+ if (!needsResolution)
443
+ return messages;
444
+ const resolve = (url, _fallbackMime) => {
445
+ if (!isLocalUrl(url))
446
+ return Promise.resolve(url);
447
+ if (!uploadAdapter || !apiKey) {
448
+ throw new Error("FileUploadAdapter required for local media URLs. Configure OSS_ACCESS_KEY_ID/OSS_ACCESS_KEY_SECRET or QLOGICAGENT_HUB_URL.");
449
+ }
450
+ return resolveMediaUrlViaUpload(url, { uploadAdapter, apiKey, signal });
451
+ };
452
+ return Promise.all(messages.map(async (msg) => {
453
+ if (msg.role !== "user" && msg.role !== "tool")
454
+ return msg;
455
+ const patch = {};
456
+ if (msg.imageUrls?.some(isLocalUrl)) {
457
+ patch.imageUrls = await Promise.all(msg.imageUrls.map((url) => resolve(url)));
458
+ }
459
+ if (msg.role === "user" && msg.videoUrls?.some(isLocalUrl)) {
460
+ patch.videoUrls = await Promise.all(msg.videoUrls.map((url) => resolve(url)));
461
+ }
462
+ if (msg.role === "user" && msg.audioUrls?.some(isLocalUrl)) {
463
+ patch.audioUrls = await Promise.all(msg.audioUrls.map((url) => resolve(url)));
464
+ }
465
+ if (msg.role === "user" && msg.fileIds?.some((f) => isLocalUrl(f.id))) {
466
+ patch.fileIds = await Promise.all(msg.fileIds.map(async (f) => {
467
+ if (!isLocalUrl(f.id))
468
+ return f;
469
+ const resolved = await resolve(f.id, f.mimeType);
470
+ return { ...f, id: resolved };
471
+ }));
472
+ }
473
+ return Object.keys(patch).length > 0 ? { ...msg, ...patch } : msg;
474
+ }));
475
+ }
476
+ // 鈹€鈹€ Message conversion: ChatMessage[] 閳?Gemini contents + systemInstruction 鈹€鈹€
477
+ function convertMessages(messages) {
478
+ let systemInstruction = null;
479
+ const contents = [];
480
+ // Collect consecutive tool results to merge into a single user turn
481
+ let pendingToolResponses = [];
482
+ const flushToolResponses = () => {
483
+ if (pendingToolResponses.length > 0) {
484
+ contents.push({ role: "user", parts: pendingToolResponses });
485
+ pendingToolResponses = [];
486
+ }
487
+ };
488
+ for (const msg of messages) {
489
+ switch (msg.role) {
490
+ case "system": {
491
+ // Gemini: system instruction is a top-level field, not a content entry
492
+ systemInstruction = {
493
+ parts: [{ text: msg.content ?? "" }],
494
+ };
495
+ break;
496
+ }
497
+ case "user": {
498
+ flushToolResponses();
499
+ const parts = [];
500
+ if (msg.content) {
501
+ parts.push({ text: msg.content });
502
+ }
503
+ // Vision: image URLs 閳?inline_data or fileData parts
504
+ if (msg.imageUrls && msg.imageUrls.length > 0) {
505
+ for (const url of msg.imageUrls) {
506
+ if (url.startsWith("data:")) {
507
+ // data URI 閳?inlineData
508
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
509
+ if (match) {
510
+ parts.push({
511
+ inlineData: { mimeType: match[1], data: match[2] },
512
+ });
513
+ }
514
+ }
515
+ else {
516
+ // Regular URL 閳?fileData (Gemini can fetch URLs)
517
+ parts.push({
518
+ fileData: { mimeType: "image/jpeg", fileUri: url },
519
+ });
520
+ }
521
+ }
522
+ }
523
+ // Audio URLs
524
+ if (msg.audioUrls && msg.audioUrls.length > 0) {
525
+ for (const url of msg.audioUrls) {
526
+ const mimeType = msg.audioFormat
527
+ ? `audio/${msg.audioFormat === "m4a" ? "mp4" : msg.audioFormat}`
528
+ : "audio/mp3";
529
+ parts.push({
530
+ fileData: { mimeType, fileUri: url },
531
+ });
532
+ }
533
+ }
534
+ // Video URLs
535
+ if (msg.videoUrls && msg.videoUrls.length > 0) {
536
+ for (const url of msg.videoUrls) {
537
+ parts.push({
538
+ fileData: { mimeType: "video/mp4", fileUri: url },
539
+ });
540
+ }
541
+ }
542
+ // File attachments (PDF, documents, etc.)
543
+ // Size-aware: files over 50MB degrade to text annotation
544
+ if (msg.fileIds && msg.fileIds.length > 0) {
545
+ for (const f of msg.fileIds) {
546
+ const tooLarge = f.size != null && f.size > MEDIA_MAX_UPLOAD_SIZE;
547
+ if (tooLarge) {
548
+ const mime = f.mimeType || "unknown";
549
+ const sizeLabel = `${(f.size / (1024 * 1024)).toFixed(1)}MB`;
550
+ parts.push({ text: `[Attached: ${f.id} (${mime}, ${sizeLabel})file too large for direct vision, use tools to process]` });
551
+ }
552
+ else if (f.id.startsWith("data:")) {
553
+ // Pre-resolved base64 data URL 閳?inlineData
554
+ const match = f.id.match(/^data:([^;]+);base64,(.+)$/);
555
+ if (match) {
556
+ parts.push({
557
+ inlineData: { mimeType: match[1], data: match[2] },
558
+ });
559
+ }
560
+ }
561
+ else if (f.id.startsWith("http://") || f.id.startsWith("https://")) {
562
+ // Public URL 閳?fileData (Gemini fetches server-side)
563
+ parts.push({
564
+ fileData: { mimeType: f.mimeType ?? "application/octet-stream", fileUri: f.id },
565
+ });
566
+ }
567
+ else {
568
+ // Gemini File API URI or other reference
569
+ parts.push({
570
+ fileData: { mimeType: f.mimeType ?? "application/octet-stream", fileUri: f.id },
571
+ });
572
+ }
573
+ }
574
+ }
575
+ if (parts.length > 0) {
576
+ contents.push({ role: "user", parts });
577
+ }
578
+ break;
579
+ }
580
+ case "assistant": {
581
+ flushToolResponses();
582
+ const parts = [];
583
+ // Thinking blocks 閳?thought parts with signatures (Gemini thought signature passback)
584
+ if (msg.thinkingBlocks && msg.thinkingBlocks.length > 0) {
585
+ for (const tb of msg.thinkingBlocks) {
586
+ const thoughtPart = {
587
+ text: tb.thinking,
588
+ thought: true,
589
+ };
590
+ if (tb.signature) {
591
+ thoughtPart.thoughtSignature = tb.signature;
592
+ }
593
+ parts.push(thoughtPart);
594
+ }
595
+ }
596
+ if (msg.content) {
597
+ parts.push({ text: msg.content });
598
+ }
599
+ // Tool calls 閳?functionCall parts
600
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
601
+ for (const tc of msg.tool_calls) {
602
+ let args = {};
603
+ try {
604
+ args = JSON.parse(tc.function.arguments);
605
+ }
606
+ catch {
607
+ // Keep empty args on parse failure
608
+ }
609
+ parts.push({
610
+ functionCall: {
611
+ name: tc.function.name,
612
+ args,
613
+ id: tc.id,
614
+ },
615
+ });
616
+ }
617
+ }
618
+ if (parts.length > 0) {
619
+ contents.push({ role: "model", parts });
620
+ }
621
+ break;
622
+ }
623
+ case "tool": {
624
+ // Tool results 閳?functionResponse parts, merged into a user turn
625
+ let responseContent;
626
+ try {
627
+ responseContent = JSON.parse(msg.content ?? "{}");
628
+ }
629
+ catch {
630
+ responseContent = { result: msg.content ?? "" };
631
+ }
632
+ pendingToolResponses.push({
633
+ functionResponse: {
634
+ name: msg.name ?? "",
635
+ response: responseContent,
636
+ id: msg.tool_call_id ?? "",
637
+ },
638
+ });
639
+ // Images from tool results: add as inlineData/fileData parts in the same turn
640
+ if (msg.imageUrls && msg.imageUrls.length > 0) {
641
+ for (const url of msg.imageUrls) {
642
+ if (url.startsWith("data:")) {
643
+ const match = /^data:([^;]+);base64,(.+)$/.exec(url);
644
+ if (match) {
645
+ pendingToolResponses.push({ inlineData: { mimeType: match[1], data: match[2] } });
646
+ }
647
+ }
648
+ else {
649
+ // Gemini uses fileData for public URLs (file_uri format)
650
+ const mimeType = url.endsWith(".png") ? "image/png"
651
+ : url.endsWith(".jpg") || url.endsWith(".jpeg") ? "image/jpeg"
652
+ : url.endsWith(".gif") ? "image/gif"
653
+ : url.endsWith(".webp") ? "image/webp"
654
+ : "image/png";
655
+ pendingToolResponses.push({ fileData: { mimeType, fileUri: url } });
656
+ }
657
+ }
658
+ }
659
+ break;
660
+ }
661
+ }
662
+ }
663
+ flushToolResponses();
664
+ return { systemInstruction, contents };
665
+ }
666
+ // 鈹€鈹€ Tool definition conversion: OpenAI format 閳?Gemini functionDeclaration 鈹€鈹€
667
+ function convertToolDef(tool) {
668
+ const decl = {
669
+ name: tool.function.name,
670
+ description: tool.function.description,
671
+ };
672
+ if (tool.function.parameters) {
673
+ decl.parameters = cleanSchemaForGemini(tool.function.parameters);
674
+ }
675
+ return decl;
676
+ }
677
+ // 鈹€鈹€ Gemini finishReason 閳?standard values 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
678
+ function mapGeminiFinishReason(reason) {
679
+ switch (reason) {
680
+ case "STOP": return "stop";
681
+ case "MAX_TOKENS": return "length";
682
+ case "SAFETY": return "content_filter";
683
+ case "RECITATION": return "content_filter";
684
+ case "LANGUAGE": return "content_filter";
685
+ case "FINISH_REASON_UNSPECIFIED": return "stop";
686
+ default: return reason.toLowerCase();
687
+ }
688
+ }