@xiaozhiclaw/provider-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/aliyun-oss-file-upload-adapter.d.ts +44 -0
- package/dist/adapters/aliyun-oss-file-upload-adapter.js +96 -0
- package/dist/adapters/gemini-file-upload-adapter.d.ts +26 -0
- package/dist/adapters/gemini-file-upload-adapter.js +92 -0
- package/dist/adapters/hub-oss-file-upload-adapter.d.ts +29 -0
- package/dist/adapters/hub-oss-file-upload-adapter.js +53 -0
- package/dist/adapters/index.d.ts +10 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/openai-file-upload-adapter.d.ts +38 -0
- package/dist/adapters/openai-file-upload-adapter.js +56 -0
- package/dist/adapters/volcengine-file-upload-adapter.d.ts +24 -0
- package/dist/adapters/volcengine-file-upload-adapter.js +45 -0
- package/dist/builtin-providers.d.ts +8 -0
- package/dist/builtin-providers.js +2237 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/credentials.d.ts +1 -0
- package/dist/credentials.js +8 -0
- package/dist/debug-transport.d.ts +12 -0
- package/dist/debug-transport.js +99 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +12 -0
- package/dist/events.d.ts +48 -0
- package/dist/events.js +1 -0
- package/dist/file-upload-service.d.ts +68 -0
- package/dist/file-upload-service.js +110 -0
- package/dist/gemini-schema-utils.d.ts +17 -0
- package/dist/gemini-schema-utils.js +76 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +33 -0
- package/dist/llm-client.d.ts +43 -0
- package/dist/llm-client.js +217 -0
- package/dist/media-client.d.ts +42 -0
- package/dist/media-client.js +174 -0
- package/dist/media-transport.d.ts +176 -0
- package/dist/media-transport.js +16 -0
- package/dist/media.d.ts +2 -0
- package/dist/media.js +1 -0
- package/dist/model-detection.d.ts +22 -0
- package/dist/model-detection.js +28 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +11 -0
- package/dist/provider-def.d.ts +220 -0
- package/dist/provider-def.js +9 -0
- package/dist/provider-registry.d.ts +51 -0
- package/dist/provider-registry.js +130 -0
- package/dist/provider-tool-api.d.ts +44 -0
- package/dist/provider-tool-api.js +9 -0
- package/dist/provider-variant-resolver.d.ts +35 -0
- package/dist/provider-variant-resolver.js +174 -0
- package/dist/retry.d.ts +37 -0
- package/dist/retry.js +71 -0
- package/dist/transport.d.ts +281 -0
- package/dist/transport.js +27 -0
- package/dist/transports/anthropic-messages.d.ts +65 -0
- package/dist/transports/anthropic-messages.js +1004 -0
- package/dist/transports/gemini-cache-api.d.ts +86 -0
- package/dist/transports/gemini-cache-api.js +141 -0
- package/dist/transports/gemini-file-api.d.ts +90 -0
- package/dist/transports/gemini-file-api.js +164 -0
- package/dist/transports/gemini-generatecontent.d.ts +56 -0
- package/dist/transports/gemini-generatecontent.js +688 -0
- package/dist/transports/gemini-lyria-realtime.d.ts +117 -0
- package/dist/transports/gemini-lyria-realtime.js +295 -0
- package/dist/transports/gemini-media.d.ts +53 -0
- package/dist/transports/gemini-media.js +383 -0
- package/dist/transports/media-resolve.d.ts +50 -0
- package/dist/transports/media-resolve.js +91 -0
- package/dist/transports/minimax-media.d.ts +56 -0
- package/dist/transports/minimax-media.js +433 -0
- package/dist/transports/openai-chat.d.ts +81 -0
- package/dist/transports/openai-chat.js +782 -0
- package/dist/transports/openai-media.d.ts +24 -0
- package/dist/transports/openai-media.js +118 -0
- package/dist/transports/openai-responses.d.ts +63 -0
- package/dist/transports/openai-responses.js +778 -0
- package/dist/transports/qwen-media.d.ts +59 -0
- package/dist/transports/qwen-media.js +411 -0
- package/dist/transports/realtime-transport.d.ts +183 -0
- package/dist/transports/realtime-transport.js +332 -0
- package/dist/transports/volcengine-grounding.d.ts +58 -0
- package/dist/transports/volcengine-grounding.js +69 -0
- package/dist/transports/volcengine-media.d.ts +94 -0
- package/dist/transports/volcengine-media.js +801 -0
- package/dist/transports/volcengine-responses.d.ts +64 -0
- package/dist/transports/volcengine-responses.js +797 -0
- package/dist/transports/zhipu-media.d.ts +82 -0
- package/dist/transports/zhipu-media.js +522 -0
- package/dist/transports/zhipu-tool-api.d.ts +35 -0
- package/dist/transports/zhipu-tool-api.js +126 -0
- package/dist/wire-types.d.ts +51 -0
- package/dist/wire-types.js +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,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
|
+
}
|