@nodetool-ai/runtime 0.7.0-rc.11

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 (198) hide show
  1. package/dist/bridge-protocol.d.ts +39 -0
  2. package/dist/bridge-protocol.d.ts.map +1 -0
  3. package/dist/bridge-protocol.js +39 -0
  4. package/dist/bridge-protocol.js.map +1 -0
  5. package/dist/comfy-executor.d.ts +41 -0
  6. package/dist/comfy-executor.d.ts.map +1 -0
  7. package/dist/comfy-executor.js +305 -0
  8. package/dist/comfy-executor.js.map +1 -0
  9. package/dist/context-packer.d.ts +22 -0
  10. package/dist/context-packer.d.ts.map +1 -0
  11. package/dist/context-packer.js +66 -0
  12. package/dist/context-packer.js.map +1 -0
  13. package/dist/context.d.ts +405 -0
  14. package/dist/context.d.ts.map +1 -0
  15. package/dist/context.js +1455 -0
  16. package/dist/context.js.map +1 -0
  17. package/dist/index.d.ts +18 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +15 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/node-executor.d.ts +53 -0
  22. package/dist/node-executor.d.ts.map +1 -0
  23. package/dist/node-executor.js +9 -0
  24. package/dist/node-executor.js.map +1 -0
  25. package/dist/providers/aki-manifest.json +34 -0
  26. package/dist/providers/aki-provider.d.ts +65 -0
  27. package/dist/providers/aki-provider.d.ts.map +1 -0
  28. package/dist/providers/aki-provider.js +517 -0
  29. package/dist/providers/aki-provider.js.map +1 -0
  30. package/dist/providers/anthropic-provider.d.ts +55 -0
  31. package/dist/providers/anthropic-provider.d.ts.map +1 -0
  32. package/dist/providers/anthropic-provider.js +563 -0
  33. package/dist/providers/anthropic-provider.js.map +1 -0
  34. package/dist/providers/base-provider.d.ts +194 -0
  35. package/dist/providers/base-provider.d.ts.map +1 -0
  36. package/dist/providers/base-provider.js +515 -0
  37. package/dist/providers/base-provider.js.map +1 -0
  38. package/dist/providers/cerebras-provider.d.ts +20 -0
  39. package/dist/providers/cerebras-provider.d.ts.map +1 -0
  40. package/dist/providers/cerebras-provider.js +52 -0
  41. package/dist/providers/cerebras-provider.js.map +1 -0
  42. package/dist/providers/claude-agent-provider.d.ts +107 -0
  43. package/dist/providers/claude-agent-provider.d.ts.map +1 -0
  44. package/dist/providers/claude-agent-provider.js +586 -0
  45. package/dist/providers/claude-agent-provider.js.map +1 -0
  46. package/dist/providers/cohere-provider.d.ts +36 -0
  47. package/dist/providers/cohere-provider.d.ts.map +1 -0
  48. package/dist/providers/cohere-provider.js +116 -0
  49. package/dist/providers/cohere-provider.js.map +1 -0
  50. package/dist/providers/cost-calculator.d.ts +109 -0
  51. package/dist/providers/cost-calculator.d.ts.map +1 -0
  52. package/dist/providers/cost-calculator.js +475 -0
  53. package/dist/providers/cost-calculator.js.map +1 -0
  54. package/dist/providers/deepseek-provider.d.ts +25 -0
  55. package/dist/providers/deepseek-provider.d.ts.map +1 -0
  56. package/dist/providers/deepseek-provider.js +58 -0
  57. package/dist/providers/deepseek-provider.js.map +1 -0
  58. package/dist/providers/fake-provider.d.ts +52 -0
  59. package/dist/providers/fake-provider.d.ts.map +1 -0
  60. package/dist/providers/fake-provider.js +107 -0
  61. package/dist/providers/fake-provider.js.map +1 -0
  62. package/dist/providers/fal-provider.d.ts +32 -0
  63. package/dist/providers/fal-provider.d.ts.map +1 -0
  64. package/dist/providers/fal-provider.js +445 -0
  65. package/dist/providers/fal-provider.js.map +1 -0
  66. package/dist/providers/gemini-provider.d.ts +109 -0
  67. package/dist/providers/gemini-provider.d.ts.map +1 -0
  68. package/dist/providers/gemini-provider.js +943 -0
  69. package/dist/providers/gemini-provider.js.map +1 -0
  70. package/dist/providers/groq-provider.d.ts +20 -0
  71. package/dist/providers/groq-provider.d.ts.map +1 -0
  72. package/dist/providers/groq-provider.js +52 -0
  73. package/dist/providers/groq-provider.js.map +1 -0
  74. package/dist/providers/huggingface-provider.d.ts +59 -0
  75. package/dist/providers/huggingface-provider.d.ts.map +1 -0
  76. package/dist/providers/huggingface-provider.js +305 -0
  77. package/dist/providers/huggingface-provider.js.map +1 -0
  78. package/dist/providers/index.d.ts +69 -0
  79. package/dist/providers/index.d.ts.map +1 -0
  80. package/dist/providers/index.js +137 -0
  81. package/dist/providers/index.js.map +1 -0
  82. package/dist/providers/jina-provider.d.ts +37 -0
  83. package/dist/providers/jina-provider.d.ts.map +1 -0
  84. package/dist/providers/jina-provider.js +118 -0
  85. package/dist/providers/jina-provider.js.map +1 -0
  86. package/dist/providers/kie-provider.d.ts +19 -0
  87. package/dist/providers/kie-provider.d.ts.map +1 -0
  88. package/dist/providers/kie-provider.js +118 -0
  89. package/dist/providers/kie-provider.js.map +1 -0
  90. package/dist/providers/llama-provider.d.ts +49 -0
  91. package/dist/providers/llama-provider.d.ts.map +1 -0
  92. package/dist/providers/llama-provider.js +381 -0
  93. package/dist/providers/llama-provider.js.map +1 -0
  94. package/dist/providers/lmstudio-provider.d.ts +22 -0
  95. package/dist/providers/lmstudio-provider.d.ts.map +1 -0
  96. package/dist/providers/lmstudio-provider.js +63 -0
  97. package/dist/providers/lmstudio-provider.js.map +1 -0
  98. package/dist/providers/manifest-models.d.ts +12 -0
  99. package/dist/providers/manifest-models.d.ts.map +1 -0
  100. package/dist/providers/manifest-models.js +96 -0
  101. package/dist/providers/manifest-models.js.map +1 -0
  102. package/dist/providers/meshy-provider.d.ts +53 -0
  103. package/dist/providers/meshy-provider.d.ts.map +1 -0
  104. package/dist/providers/meshy-provider.js +230 -0
  105. package/dist/providers/meshy-provider.js.map +1 -0
  106. package/dist/providers/minimax-provider.d.ts +66 -0
  107. package/dist/providers/minimax-provider.d.ts.map +1 -0
  108. package/dist/providers/minimax-provider.js +531 -0
  109. package/dist/providers/minimax-provider.js.map +1 -0
  110. package/dist/providers/mistral-provider.d.ts +26 -0
  111. package/dist/providers/mistral-provider.d.ts.map +1 -0
  112. package/dist/providers/mistral-provider.js +74 -0
  113. package/dist/providers/mistral-provider.js.map +1 -0
  114. package/dist/providers/moonshot-provider.d.ts +24 -0
  115. package/dist/providers/moonshot-provider.d.ts.map +1 -0
  116. package/dist/providers/moonshot-provider.js +43 -0
  117. package/dist/providers/moonshot-provider.js.map +1 -0
  118. package/dist/providers/ollama-provider.d.ts +71 -0
  119. package/dist/providers/ollama-provider.d.ts.map +1 -0
  120. package/dist/providers/ollama-provider.js +480 -0
  121. package/dist/providers/ollama-provider.js.map +1 -0
  122. package/dist/providers/openai-provider.d.ts +103 -0
  123. package/dist/providers/openai-provider.d.ts.map +1 -0
  124. package/dist/providers/openai-provider.js +917 -0
  125. package/dist/providers/openai-provider.js.map +1 -0
  126. package/dist/providers/openrouter-provider.d.ts +49 -0
  127. package/dist/providers/openrouter-provider.d.ts.map +1 -0
  128. package/dist/providers/openrouter-provider.js +147 -0
  129. package/dist/providers/openrouter-provider.js.map +1 -0
  130. package/dist/providers/provider-registry.d.ts +24 -0
  131. package/dist/providers/provider-registry.d.ts.map +1 -0
  132. package/dist/providers/provider-registry.js +124 -0
  133. package/dist/providers/provider-registry.js.map +1 -0
  134. package/dist/providers/python-provider.d.ts +75 -0
  135. package/dist/providers/python-provider.d.ts.map +1 -0
  136. package/dist/providers/python-provider.js +157 -0
  137. package/dist/providers/python-provider.js.map +1 -0
  138. package/dist/providers/replicate-provider.d.ts +73 -0
  139. package/dist/providers/replicate-provider.d.ts.map +1 -0
  140. package/dist/providers/replicate-provider.js +484 -0
  141. package/dist/providers/replicate-provider.js.map +1 -0
  142. package/dist/providers/rodin-provider.d.ts +41 -0
  143. package/dist/providers/rodin-provider.d.ts.map +1 -0
  144. package/dist/providers/rodin-provider.js +236 -0
  145. package/dist/providers/rodin-provider.js.map +1 -0
  146. package/dist/providers/scripted-provider.d.ts +141 -0
  147. package/dist/providers/scripted-provider.d.ts.map +1 -0
  148. package/dist/providers/scripted-provider.js +246 -0
  149. package/dist/providers/scripted-provider.js.map +1 -0
  150. package/dist/providers/together-provider.d.ts +20 -0
  151. package/dist/providers/together-provider.d.ts.map +1 -0
  152. package/dist/providers/together-provider.js +56 -0
  153. package/dist/providers/together-provider.js.map +1 -0
  154. package/dist/providers/types.d.ts +202 -0
  155. package/dist/providers/types.d.ts.map +1 -0
  156. package/dist/providers/types.js +2 -0
  157. package/dist/providers/types.js.map +1 -0
  158. package/dist/providers/vllm-provider.d.ts +22 -0
  159. package/dist/providers/vllm-provider.d.ts.map +1 -0
  160. package/dist/providers/vllm-provider.js +66 -0
  161. package/dist/providers/vllm-provider.js.map +1 -0
  162. package/dist/providers/voyage-provider.d.ts +37 -0
  163. package/dist/providers/voyage-provider.d.ts.map +1 -0
  164. package/dist/providers/voyage-provider.js +124 -0
  165. package/dist/providers/voyage-provider.js.map +1 -0
  166. package/dist/providers/xai-provider.d.ts +24 -0
  167. package/dist/providers/xai-provider.d.ts.map +1 -0
  168. package/dist/providers/xai-provider.js +57 -0
  169. package/dist/providers/xai-provider.js.map +1 -0
  170. package/dist/python-bridge-types.d.ts +70 -0
  171. package/dist/python-bridge-types.d.ts.map +1 -0
  172. package/dist/python-bridge-types.js +2 -0
  173. package/dist/python-bridge-types.js.map +1 -0
  174. package/dist/python-node-executor.d.ts +20 -0
  175. package/dist/python-node-executor.d.ts.map +1 -0
  176. package/dist/python-node-executor.js +219 -0
  177. package/dist/python-node-executor.js.map +1 -0
  178. package/dist/python-stdio-bridge.d.ts +67 -0
  179. package/dist/python-stdio-bridge.d.ts.map +1 -0
  180. package/dist/python-stdio-bridge.js +704 -0
  181. package/dist/python-stdio-bridge.js.map +1 -0
  182. package/dist/recommended-models.d.ts +17 -0
  183. package/dist/recommended-models.d.ts.map +1 -0
  184. package/dist/recommended-models.js +139 -0
  185. package/dist/recommended-models.js.map +1 -0
  186. package/dist/telemetry.d.ts +61 -0
  187. package/dist/telemetry.d.ts.map +1 -0
  188. package/dist/telemetry.js +128 -0
  189. package/dist/telemetry.js.map +1 -0
  190. package/dist/trace-exporters.d.ts +72 -0
  191. package/dist/trace-exporters.d.ts.map +1 -0
  192. package/dist/trace-exporters.js +216 -0
  193. package/dist/trace-exporters.js.map +1 -0
  194. package/dist/tracing-helpers.d.ts +99 -0
  195. package/dist/tracing-helpers.d.ts.map +1 -0
  196. package/dist/tracing-helpers.js +217 -0
  197. package/dist/tracing-helpers.js.map +1 -0
  198. package/package.json +64 -0
@@ -0,0 +1,943 @@
1
+ import { createLogger } from "@nodetool-ai/config";
2
+ import { BaseProvider } from "./base-provider.js";
3
+ const log = createLogger("nodetool.runtime.providers.gemini");
4
+ const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
5
+ function sanitizeToolName(name) {
6
+ let sanitized = (name ?? "").trim();
7
+ sanitized = sanitized.replace(/[^a-zA-Z0-9_.:-]/g, "_");
8
+ sanitized = sanitized.replace(/_+/g, "_");
9
+ if (!sanitized)
10
+ sanitized = "_tool";
11
+ if (!/^[a-zA-Z_]/.test(sanitized))
12
+ sanitized = `_${sanitized}`;
13
+ if (sanitized.length > 64)
14
+ sanitized = sanitized.slice(0, 64);
15
+ if (!sanitized)
16
+ sanitized = "_tool";
17
+ return sanitized;
18
+ }
19
+ export class GeminiProvider extends BaseProvider {
20
+ static requiredSecrets() {
21
+ return ["GEMINI_API_KEY"];
22
+ }
23
+ apiKey;
24
+ _fetch;
25
+ constructor(secrets, options = {}) {
26
+ super("gemini");
27
+ const apiKey = secrets.GEMINI_API_KEY;
28
+ if (!apiKey) {
29
+ throw new Error("GEMINI_API_KEY is required");
30
+ }
31
+ this.apiKey = apiKey;
32
+ this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
33
+ }
34
+ getContainerEnv() {
35
+ return { GEMINI_API_KEY: this.apiKey };
36
+ }
37
+ async hasToolSupport(_model) {
38
+ return true;
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // Model listing
42
+ // ---------------------------------------------------------------------------
43
+ async getAvailableLanguageModels() {
44
+ const url = `${GEMINI_API_BASE}/models?key=${this.apiKey}`;
45
+ let response;
46
+ try {
47
+ response = await this._fetch(url);
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ if (!response.ok)
53
+ return [];
54
+ const payload = (await response.json());
55
+ const items = payload.models ?? [];
56
+ return items
57
+ .filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent"))
58
+ .filter((m) => !!m.name)
59
+ .map((m) => {
60
+ const id = m.name.split("/").pop();
61
+ return {
62
+ id,
63
+ name: m.displayName ?? id,
64
+ provider: "gemini"
65
+ };
66
+ });
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Message conversion helpers
70
+ // ---------------------------------------------------------------------------
71
+ async messageContentToGeminiPart(content) {
72
+ if (content.type === "text") {
73
+ return { text: content.text };
74
+ }
75
+ if (content.type === "image_url") {
76
+ const img = content.image;
77
+ let base64Data;
78
+ let mimeType = img.mimeType ?? "image/jpeg";
79
+ if (img.data) {
80
+ if (typeof img.data === "string") {
81
+ base64Data = img.data;
82
+ }
83
+ else {
84
+ base64Data = Buffer.from(img.data).toString("base64");
85
+ }
86
+ }
87
+ else if (img.uri) {
88
+ if (img.uri.startsWith("data:")) {
89
+ const idx = img.uri.indexOf(",");
90
+ const header = img.uri.slice(5, idx);
91
+ mimeType = header.split(";")[0] || mimeType;
92
+ base64Data = img.uri.slice(idx + 1);
93
+ }
94
+ else {
95
+ const resp = await this._fetch(img.uri);
96
+ if (!resp.ok)
97
+ throw new Error(`Failed to fetch image: ${resp.status}`);
98
+ mimeType = resp.headers.get("content-type") ?? mimeType;
99
+ base64Data = Buffer.from(await resp.arrayBuffer()).toString("base64");
100
+ }
101
+ }
102
+ else {
103
+ base64Data = "";
104
+ }
105
+ return { inlineData: { mimeType, data: base64Data } };
106
+ }
107
+ if (content.type === "audio") {
108
+ const aud = content.audio;
109
+ let base64Data;
110
+ let mimeType = aud.mimeType ?? "audio/mp3";
111
+ if (aud.data) {
112
+ if (typeof aud.data === "string") {
113
+ base64Data = aud.data;
114
+ }
115
+ else {
116
+ base64Data = Buffer.from(aud.data).toString("base64");
117
+ }
118
+ }
119
+ else if (aud.uri) {
120
+ if (aud.uri.startsWith("data:")) {
121
+ const idx = aud.uri.indexOf(",");
122
+ const header = aud.uri.slice(5, idx);
123
+ mimeType = header.split(";")[0] || mimeType;
124
+ base64Data = aud.uri.slice(idx + 1);
125
+ }
126
+ else {
127
+ const resp = await this._fetch(aud.uri);
128
+ if (!resp.ok)
129
+ throw new Error(`Failed to fetch audio: ${resp.status}`);
130
+ mimeType = resp.headers.get("content-type") ?? mimeType;
131
+ base64Data = Buffer.from(await resp.arrayBuffer()).toString("base64");
132
+ }
133
+ }
134
+ else {
135
+ base64Data = "";
136
+ }
137
+ return { inlineData: { mimeType, data: base64Data } };
138
+ }
139
+ return { text: "[unsupported content type]" };
140
+ }
141
+ /**
142
+ * Convert our Message array into Gemini contents + optional system instruction.
143
+ */
144
+ async convertMessages(messages) {
145
+ let systemInstruction;
146
+ const contents = [];
147
+ for (const msg of messages) {
148
+ if (msg.role === "system") {
149
+ systemInstruction =
150
+ typeof msg.content === "string"
151
+ ? msg.content
152
+ : (msg.content ?? [])
153
+ .filter((c) => c.type === "text")
154
+ .map((c) => c.text)
155
+ .join(" ");
156
+ continue;
157
+ }
158
+ if (msg.role === "tool") {
159
+ // Tool result → model role with functionResponse part
160
+ const responseText = typeof msg.content === "string"
161
+ ? msg.content
162
+ : JSON.stringify(msg.content);
163
+ contents.push({
164
+ role: "user",
165
+ parts: [
166
+ {
167
+ functionResponse: {
168
+ name: msg.toolCallId ?? "unknown",
169
+ response: { result: responseText }
170
+ }
171
+ }
172
+ ]
173
+ });
174
+ continue;
175
+ }
176
+ if (msg.role === "assistant") {
177
+ // If we have raw Gemini parts (with thought content), replay them exactly
178
+ if (msg._rawGeminiParts && Array.isArray(msg._rawGeminiParts)) {
179
+ contents.push({
180
+ role: "model",
181
+ parts: msg._rawGeminiParts
182
+ });
183
+ continue;
184
+ }
185
+ const parts = [];
186
+ // Tool calls
187
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
188
+ for (const tc of msg.toolCalls) {
189
+ const part = {
190
+ functionCall: { name: tc.name, args: tc.args }
191
+ };
192
+ if (tc.thought_signature) {
193
+ part.thoughtSignature = tc.thought_signature;
194
+ }
195
+ parts.push(part);
196
+ }
197
+ }
198
+ // Text / content
199
+ if (typeof msg.content === "string" && msg.content) {
200
+ parts.push({ text: msg.content });
201
+ }
202
+ else if (Array.isArray(msg.content)) {
203
+ for (const c of msg.content) {
204
+ parts.push(await this.messageContentToGeminiPart(c));
205
+ }
206
+ }
207
+ if (parts.length > 0) {
208
+ contents.push({ role: "model", parts });
209
+ }
210
+ continue;
211
+ }
212
+ // user
213
+ const parts = [];
214
+ if (typeof msg.content === "string") {
215
+ parts.push({ text: msg.content });
216
+ }
217
+ else if (Array.isArray(msg.content)) {
218
+ for (const c of msg.content) {
219
+ parts.push(await this.messageContentToGeminiPart(c));
220
+ }
221
+ }
222
+ if (parts.length > 0) {
223
+ contents.push({ role: "user", parts });
224
+ }
225
+ }
226
+ return { contents, systemInstruction };
227
+ }
228
+ formatTools(tools) {
229
+ const nameMap = new Map();
230
+ const reverseMap = new Map();
231
+ const usedNames = new Set();
232
+ const declarations = [];
233
+ for (const tool of tools) {
234
+ const original = tool.name;
235
+ let unique = sanitizeToolName(original);
236
+ let suffix = 2;
237
+ while (usedNames.has(unique)) {
238
+ const sfx = `_${suffix}`;
239
+ unique = `${sanitizeToolName(original).slice(0, 64 - sfx.length)}${sfx}`;
240
+ suffix++;
241
+ }
242
+ usedNames.add(unique);
243
+ nameMap.set(original, unique);
244
+ reverseMap.set(unique, original);
245
+ declarations.push({
246
+ name: unique,
247
+ description: tool.description ?? "",
248
+ parameters: tool.inputSchema ?? { type: "object", properties: {} }
249
+ });
250
+ }
251
+ return {
252
+ geminiTools: declarations.length > 0 ? [{ functionDeclarations: declarations }] : [],
253
+ nameMap,
254
+ reverseMap
255
+ };
256
+ }
257
+ // ---------------------------------------------------------------------------
258
+ // Non-streaming generation
259
+ // ---------------------------------------------------------------------------
260
+ async generateMessage(args) {
261
+ const { model, tools = [], maxTokens = 16384, temperature, topP } = args;
262
+ const { contents, systemInstruction } = await this.convertMessages(args.messages);
263
+ const { geminiTools, reverseMap } = this.formatTools(tools);
264
+ const body = { contents };
265
+ if (systemInstruction) {
266
+ body.systemInstruction = { parts: [{ text: systemInstruction }] };
267
+ }
268
+ if (geminiTools.length > 0) {
269
+ body.tools = geminiTools;
270
+ }
271
+ const generationConfig = {
272
+ maxOutputTokens: maxTokens
273
+ };
274
+ if (temperature != null)
275
+ generationConfig.temperature = temperature;
276
+ if (topP != null)
277
+ generationConfig.topP = topP;
278
+ body.generationConfig = generationConfig;
279
+ log.debug("Gemini request", { model });
280
+ const url = `${GEMINI_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`;
281
+ const response = await this._fetch(url, {
282
+ method: "POST",
283
+ headers: { "Content-Type": "application/json" },
284
+ body: JSON.stringify(body)
285
+ });
286
+ if (!response.ok) {
287
+ const text = await response.text();
288
+ log.error("Gemini request failed", {
289
+ model,
290
+ error: `${response.status}: ${text.slice(0, 200)}`
291
+ });
292
+ throw new Error(`Gemini API error ${response.status}: ${text}`);
293
+ }
294
+ const data = (await response.json());
295
+ if (data.error) {
296
+ throw new Error(`Gemini API error: ${data.error.message}`);
297
+ }
298
+ const candidate = data.candidates?.[0];
299
+ if (!candidate?.content?.parts) {
300
+ throw new Error("Gemini returned no candidates");
301
+ }
302
+ return this.extractMessage(candidate.content.parts, reverseMap);
303
+ }
304
+ // ---------------------------------------------------------------------------
305
+ // Streaming generation
306
+ // ---------------------------------------------------------------------------
307
+ async *generateMessages(args) {
308
+ const { model, tools = [], maxTokens = 16384, temperature, topP } = args;
309
+ const { contents, systemInstruction } = await this.convertMessages(args.messages);
310
+ const { geminiTools, reverseMap } = this.formatTools(tools);
311
+ const body = { contents };
312
+ if (systemInstruction) {
313
+ body.systemInstruction = { parts: [{ text: systemInstruction }] };
314
+ }
315
+ if (geminiTools.length > 0) {
316
+ body.tools = geminiTools;
317
+ }
318
+ const generationConfig = {
319
+ maxOutputTokens: maxTokens
320
+ };
321
+ if (temperature != null)
322
+ generationConfig.temperature = temperature;
323
+ if (topP != null)
324
+ generationConfig.topP = topP;
325
+ body.generationConfig = generationConfig;
326
+ log.debug("Gemini request", { model });
327
+ const url = `${GEMINI_API_BASE}/models/${model}:streamGenerateContent?alt=sse&key=${this.apiKey}`;
328
+ const response = await this._fetch(url, {
329
+ method: "POST",
330
+ headers: { "Content-Type": "application/json" },
331
+ body: JSON.stringify(body)
332
+ });
333
+ if (!response.ok) {
334
+ const text = await response.text();
335
+ log.error("Gemini request failed", {
336
+ model,
337
+ error: `${response.status}: ${text.slice(0, 200)}`
338
+ });
339
+ throw new Error(`Gemini API error ${response.status}: ${text}`);
340
+ }
341
+ if (!response.body) {
342
+ throw new Error("Gemini streaming response has no body");
343
+ }
344
+ const reader = response.body.getReader();
345
+ const decoder = new TextDecoder();
346
+ let buffer = "";
347
+ // Accumulate all parts across SSE events for raw replay.
348
+ // Gemini thinking models emit thought parts and function calls across
349
+ // separate SSE events, but they must all be sent back together.
350
+ const allParts = [];
351
+ const pendingToolCalls = [];
352
+ try {
353
+ while (true) {
354
+ const { done, value } = await reader.read();
355
+ if (done)
356
+ break;
357
+ buffer += decoder.decode(value, { stream: true });
358
+ const lines = buffer.split("\n");
359
+ buffer = lines.pop() ?? "";
360
+ for (const line of lines) {
361
+ if (!line.startsWith("data: "))
362
+ continue;
363
+ const jsonStr = line.slice(6).trim();
364
+ if (!jsonStr || jsonStr === "[DONE]")
365
+ continue;
366
+ let event;
367
+ try {
368
+ event = JSON.parse(jsonStr);
369
+ }
370
+ catch {
371
+ continue;
372
+ }
373
+ const parts = event.candidates?.[0]?.content?.parts;
374
+ if (!parts)
375
+ continue;
376
+ for (const part of parts) {
377
+ allParts.push(part);
378
+ if (part.text !== undefined && !part.thought) {
379
+ const chunk = {
380
+ type: "chunk",
381
+ content: part.text,
382
+ done: false
383
+ };
384
+ yield chunk;
385
+ }
386
+ else if (part.functionCall) {
387
+ const originalName = reverseMap.get(part.functionCall.name) ??
388
+ part.functionCall.name;
389
+ const toolCall = {
390
+ id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
391
+ name: originalName,
392
+ args: part.functionCall.args ?? {}
393
+ };
394
+ if (part.thoughtSignature) {
395
+ toolCall.thought_signature = part.thoughtSignature;
396
+ }
397
+ pendingToolCalls.push(toolCall);
398
+ }
399
+ // Thought text parts are silently accumulated in allParts
400
+ }
401
+ }
402
+ }
403
+ }
404
+ finally {
405
+ reader.releaseLock();
406
+ }
407
+ // Attach accumulated raw parts to tool calls for thought replay
408
+ const hasThoughts = allParts.some((p) => p.thought);
409
+ for (const tc of pendingToolCalls) {
410
+ if (hasThoughts) {
411
+ tc._rawGeminiParts = allParts;
412
+ }
413
+ yield tc;
414
+ }
415
+ // Emit synthetic done chunk
416
+ const doneChunk = {
417
+ type: "chunk",
418
+ content: "",
419
+ done: true
420
+ };
421
+ yield doneChunk;
422
+ }
423
+ // ---------------------------------------------------------------------------
424
+ // Helpers
425
+ // ---------------------------------------------------------------------------
426
+ extractMessage(parts, reverseMap) {
427
+ const textParts = [];
428
+ const toolCalls = [];
429
+ for (const part of parts) {
430
+ if (part.text !== undefined) {
431
+ textParts.push(part.text);
432
+ }
433
+ else if (part.functionCall) {
434
+ const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
435
+ const tc = {
436
+ id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
437
+ name: originalName,
438
+ args: part.functionCall.args ?? {}
439
+ };
440
+ if (part.thoughtSignature) {
441
+ tc.thought_signature = part.thoughtSignature;
442
+ }
443
+ toolCalls.push(tc);
444
+ }
445
+ }
446
+ const hasThoughts = parts.some((p) => p.thought);
447
+ const msg = {
448
+ role: "assistant",
449
+ content: textParts.join("") || null,
450
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
451
+ };
452
+ if (hasThoughts) {
453
+ msg._rawGeminiParts = parts;
454
+ }
455
+ return msg;
456
+ }
457
+ // ---------------------------------------------------------------------------
458
+ // Model listing — image, TTS, ASR, video, embedding
459
+ // ---------------------------------------------------------------------------
460
+ async getAvailableImageModels() {
461
+ return [
462
+ {
463
+ id: "gemini-2.0-flash-preview-image-generation",
464
+ name: "Gemini 2.0 Flash Image Gen",
465
+ provider: "gemini"
466
+ },
467
+ { id: "imagen-3.0-generate-002", name: "Imagen 3.0", provider: "gemini" }
468
+ ];
469
+ }
470
+ async getAvailableTTSModels() {
471
+ const voices = [
472
+ "Zephyr",
473
+ "Puck",
474
+ "Charon",
475
+ "Kore",
476
+ "Fenrir",
477
+ "Leda",
478
+ "Orus",
479
+ "Aoede",
480
+ "Callirrhoe",
481
+ "Autonoe",
482
+ "Enceladus",
483
+ "Iapetus",
484
+ "Umbriel",
485
+ "Algieba",
486
+ "Despina",
487
+ "Erinome",
488
+ "Algenib",
489
+ "Rasalgethi",
490
+ "Laomedeia",
491
+ "Achernar",
492
+ "Alnilam",
493
+ "Schedar",
494
+ "Gacrux",
495
+ "Pulcherrima",
496
+ "Achird",
497
+ "Zubenelgenubi",
498
+ "Vindemiatrix",
499
+ "Sadachbia",
500
+ "Sadaltager",
501
+ "Sulafat"
502
+ ];
503
+ return [
504
+ {
505
+ id: "gemini-2.5-pro-preview-tts",
506
+ name: "Gemini 2.5 Pro TTS",
507
+ provider: "gemini",
508
+ voices
509
+ }
510
+ ];
511
+ }
512
+ async getAvailableASRModels() {
513
+ return [
514
+ { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", provider: "gemini" },
515
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", provider: "gemini" }
516
+ ];
517
+ }
518
+ async getAvailableVideoModels() {
519
+ return [
520
+ {
521
+ id: "veo-3.1-generate-preview",
522
+ name: "Veo 3.1 Preview",
523
+ provider: "gemini"
524
+ },
525
+ { id: "veo-2.0-generate-001", name: "Veo 2.0", provider: "gemini" }
526
+ ];
527
+ }
528
+ async getAvailableEmbeddingModels() {
529
+ return [
530
+ {
531
+ id: "text-embedding-004",
532
+ name: "Text Embedding 004",
533
+ provider: "gemini",
534
+ dimensions: 768
535
+ },
536
+ {
537
+ id: "gemini-embedding-001",
538
+ name: "Gemini Embedding 001",
539
+ provider: "gemini",
540
+ dimensions: 3072
541
+ }
542
+ ];
543
+ }
544
+ // ---------------------------------------------------------------------------
545
+ // Embeddings
546
+ // ---------------------------------------------------------------------------
547
+ async generateEmbedding(args) {
548
+ const { text, model, dimensions } = args;
549
+ if (!text || (Array.isArray(text) && text.length === 0)) {
550
+ throw new Error("text must not be empty");
551
+ }
552
+ const texts = typeof text === "string" ? [text] : text;
553
+ // Gemini embedContent supports a single content; batch by calling per text
554
+ const embeddings = [];
555
+ for (const t of texts) {
556
+ const body = {
557
+ content: { parts: [{ text: t }] }
558
+ };
559
+ if (dimensions) {
560
+ body.outputDimensionality = dimensions;
561
+ }
562
+ const url = `${GEMINI_API_BASE}/models/${model}:embedContent?key=${this.apiKey}`;
563
+ const response = await this._fetch(url, {
564
+ method: "POST",
565
+ headers: { "Content-Type": "application/json" },
566
+ body: JSON.stringify(body)
567
+ });
568
+ if (!response.ok) {
569
+ const errText = await response.text();
570
+ throw new Error(`Gemini embedding error ${response.status}: ${errText}`);
571
+ }
572
+ const data = (await response.json());
573
+ if (!data.embedding?.values) {
574
+ throw new Error("No embedding returned from Gemini API");
575
+ }
576
+ embeddings.push(data.embedding.values);
577
+ }
578
+ return embeddings;
579
+ }
580
+ // ---------------------------------------------------------------------------
581
+ // Text-to-image
582
+ // ---------------------------------------------------------------------------
583
+ async textToImage(params) {
584
+ if (!params.prompt) {
585
+ throw new Error("The input prompt cannot be empty.");
586
+ }
587
+ const modelId = params.model.id;
588
+ if (modelId.startsWith("gemini-")) {
589
+ // Use generateContent with IMAGE response modality
590
+ const body = {
591
+ contents: [{ role: "user", parts: [{ text: params.prompt }] }],
592
+ generationConfig: {
593
+ responseModalities: ["IMAGE", "TEXT"]
594
+ }
595
+ };
596
+ const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
597
+ const response = await this._fetch(url, {
598
+ method: "POST",
599
+ headers: { "Content-Type": "application/json" },
600
+ body: JSON.stringify(body)
601
+ });
602
+ if (!response.ok) {
603
+ const errText = await response.text();
604
+ throw new Error(`Gemini text-to-image failed ${response.status}: ${errText}`);
605
+ }
606
+ const data = (await response.json());
607
+ const parts = data.candidates?.[0]?.content?.parts;
608
+ if (!parts)
609
+ throw new Error("No candidates in response");
610
+ for (const part of parts) {
611
+ if (part.inlineData?.data) {
612
+ return Uint8Array.from(Buffer.from(part.inlineData.data, "base64"));
613
+ }
614
+ }
615
+ throw new Error("No image data returned in response");
616
+ }
617
+ // Imagen models use generateImages endpoint
618
+ const body = {
619
+ instances: [{ prompt: params.prompt }],
620
+ parameters: { sampleCount: 1 }
621
+ };
622
+ const url = `${GEMINI_API_BASE}/models/${modelId}:generateImages?key=${this.apiKey}`;
623
+ const response = await this._fetch(url, {
624
+ method: "POST",
625
+ headers: { "Content-Type": "application/json" },
626
+ body: JSON.stringify(body)
627
+ });
628
+ if (!response.ok) {
629
+ const errText = await response.text();
630
+ throw new Error(`Gemini image generation failed ${response.status}: ${errText}`);
631
+ }
632
+ const data = (await response.json());
633
+ // Try predictions format first (Vertex-style), then generatedImages
634
+ const b64 = data.predictions?.[0]?.bytesBase64Encoded ??
635
+ data.generatedImages?.[0]?.image?.imageBytes;
636
+ if (!b64)
637
+ throw new Error("No image data in response");
638
+ return Uint8Array.from(Buffer.from(b64, "base64"));
639
+ }
640
+ // ---------------------------------------------------------------------------
641
+ // Image-to-image
642
+ // ---------------------------------------------------------------------------
643
+ async imageToImage(image, params) {
644
+ if (!params.prompt) {
645
+ throw new Error("The input prompt cannot be empty.");
646
+ }
647
+ const modelId = params.model.id;
648
+ if (!modelId.startsWith("gemini-")) {
649
+ throw new Error(`Model ${modelId} does not support image-to-image. Only gemini-* models supported.`);
650
+ }
651
+ const imageBase64 = Buffer.from(image).toString("base64");
652
+ const body = {
653
+ contents: [
654
+ {
655
+ role: "user",
656
+ parts: [
657
+ { text: params.prompt },
658
+ { inlineData: { mimeType: "image/png", data: imageBase64 } }
659
+ ]
660
+ }
661
+ ],
662
+ generationConfig: {
663
+ responseModalities: ["IMAGE", "TEXT"]
664
+ }
665
+ };
666
+ const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
667
+ const response = await this._fetch(url, {
668
+ method: "POST",
669
+ headers: { "Content-Type": "application/json" },
670
+ body: JSON.stringify(body)
671
+ });
672
+ if (!response.ok) {
673
+ const errText = await response.text();
674
+ throw new Error(`Gemini image-to-image failed ${response.status}: ${errText}`);
675
+ }
676
+ const data = (await response.json());
677
+ const parts = data.candidates?.[0]?.content?.parts;
678
+ if (!parts)
679
+ throw new Error("No candidates in response");
680
+ for (const part of parts) {
681
+ if (part.inlineData?.data) {
682
+ return Uint8Array.from(Buffer.from(part.inlineData.data, "base64"));
683
+ }
684
+ }
685
+ throw new Error("No image data returned in response");
686
+ }
687
+ // ---------------------------------------------------------------------------
688
+ // Text-to-speech
689
+ // ---------------------------------------------------------------------------
690
+ async *textToSpeech(args) {
691
+ const { text, model, voice = "Puck" } = args;
692
+ const body = {
693
+ contents: [{ role: "user", parts: [{ text }] }],
694
+ generationConfig: {
695
+ responseModalities: ["AUDIO"],
696
+ speechConfig: {
697
+ voiceConfig: {
698
+ prebuiltVoiceConfig: { voiceName: voice }
699
+ }
700
+ }
701
+ }
702
+ };
703
+ const url = `${GEMINI_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`;
704
+ const response = await this._fetch(url, {
705
+ method: "POST",
706
+ headers: { "Content-Type": "application/json" },
707
+ body: JSON.stringify(body)
708
+ });
709
+ if (!response.ok) {
710
+ const errText = await response.text();
711
+ throw new Error(`Gemini TTS failed ${response.status}: ${errText}`);
712
+ }
713
+ const data = (await response.json());
714
+ const parts = data.candidates?.[0]?.content?.parts;
715
+ if (!parts)
716
+ throw new Error("No audio in response");
717
+ for (const part of parts) {
718
+ if (part.inlineData?.data) {
719
+ const raw = Buffer.from(part.inlineData.data, "base64");
720
+ // Gemini TTS returns raw PCM int16 at 24kHz
721
+ const samples = new Int16Array(raw.buffer, raw.byteOffset, raw.byteLength / 2);
722
+ yield { samples };
723
+ }
724
+ }
725
+ }
726
+ // ---------------------------------------------------------------------------
727
+ // Automatic speech recognition
728
+ // ---------------------------------------------------------------------------
729
+ async automaticSpeechRecognition(args) {
730
+ const { audio, model, language, temperature = 0 } = args;
731
+ if (!audio || audio.length === 0) {
732
+ throw new Error("audio must not be empty");
733
+ }
734
+ // Detect MIME type from audio header
735
+ let mimeType = "audio/wav";
736
+ if (audio[0] === 0x52 &&
737
+ audio[1] === 0x49 &&
738
+ audio[2] === 0x46 &&
739
+ audio[3] === 0x46) {
740
+ mimeType = "audio/wav"; // RIFF
741
+ }
742
+ else if (audio[0] === 0x49 && audio[1] === 0x44 && audio[2] === 0x33) {
743
+ mimeType = "audio/mp3"; // ID3
744
+ }
745
+ else if (audio[0] === 0xff && (audio[1] === 0xfb || audio[1] === 0xf3)) {
746
+ mimeType = "audio/mp3"; // MPEG sync
747
+ }
748
+ else if (audio[0] === 0x66 &&
749
+ audio[1] === 0x4c &&
750
+ audio[2] === 0x61 &&
751
+ audio[3] === 0x43) {
752
+ mimeType = "audio/flac"; // fLaC
753
+ }
754
+ else if (audio[0] === 0x4f &&
755
+ audio[1] === 0x67 &&
756
+ audio[2] === 0x67 &&
757
+ audio[3] === 0x53) {
758
+ mimeType = "audio/ogg"; // OggS
759
+ }
760
+ let promptText = args.prompt ?? "Transcribe this audio to text.";
761
+ if (language) {
762
+ promptText = `${promptText} The audio is in ${language}.`;
763
+ }
764
+ const audioBase64 = Buffer.from(audio).toString("base64");
765
+ const body = {
766
+ contents: [
767
+ {
768
+ role: "user",
769
+ parts: [
770
+ { inlineData: { mimeType, data: audioBase64 } },
771
+ { text: promptText }
772
+ ]
773
+ }
774
+ ],
775
+ generationConfig: {
776
+ temperature
777
+ }
778
+ };
779
+ const url = `${GEMINI_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`;
780
+ const response = await this._fetch(url, {
781
+ method: "POST",
782
+ headers: { "Content-Type": "application/json" },
783
+ body: JSON.stringify(body)
784
+ });
785
+ if (!response.ok) {
786
+ const errText = await response.text();
787
+ throw new Error(`Gemini ASR failed ${response.status}: ${errText}`);
788
+ }
789
+ const data = (await response.json());
790
+ const parts = data.candidates?.[0]?.content?.parts;
791
+ if (!parts)
792
+ return { text: "" };
793
+ const text = parts
794
+ .filter((p) => p.text !== undefined)
795
+ .map((p) => p.text)
796
+ .join("");
797
+ return { text };
798
+ }
799
+ // ---------------------------------------------------------------------------
800
+ // Text-to-video (Veo models — async operation with polling)
801
+ // ---------------------------------------------------------------------------
802
+ async textToVideo(params) {
803
+ if (!params.prompt) {
804
+ throw new Error("The input prompt cannot be empty.");
805
+ }
806
+ const modelId = params.model.id;
807
+ if (!modelId.startsWith("veo-")) {
808
+ throw new Error(`Model ${modelId} is not a Veo model. Only Veo models support text-to-video.`);
809
+ }
810
+ const body = {
811
+ instances: [{ prompt: params.prompt }]
812
+ };
813
+ const parameters = {};
814
+ if (params.negativePrompt)
815
+ parameters.negativePrompt = params.negativePrompt;
816
+ if (params.aspectRatio)
817
+ parameters.aspectRatio = params.aspectRatio;
818
+ if (params.seed != null)
819
+ parameters.seed = params.seed;
820
+ if (Object.keys(parameters).length > 0)
821
+ body.parameters = parameters;
822
+ // Initiate async generation
823
+ const url = `${GEMINI_API_BASE}/models/${modelId}:generateVideos?key=${this.apiKey}`;
824
+ const response = await this._fetch(url, {
825
+ method: "POST",
826
+ headers: { "Content-Type": "application/json" },
827
+ body: JSON.stringify(body)
828
+ });
829
+ if (!response.ok) {
830
+ const errText = await response.text();
831
+ throw new Error(`Gemini video generation failed ${response.status}: ${errText}`);
832
+ }
833
+ const operation = (await response.json());
834
+ // Poll for completion
835
+ const maxWait = 600_000; // 10 minutes
836
+ const pollInterval = 10_000;
837
+ let elapsed = 0;
838
+ let current = operation;
839
+ while (!current.done && elapsed < maxWait) {
840
+ await new Promise((r) => setTimeout(r, pollInterval));
841
+ elapsed += pollInterval;
842
+ if (!current.name)
843
+ throw new Error("No operation name for polling");
844
+ const pollUrl = `${GEMINI_API_BASE}/${current.name}?key=${this.apiKey}`;
845
+ const pollResp = await this._fetch(pollUrl);
846
+ if (!pollResp.ok) {
847
+ const errText = await pollResp.text();
848
+ throw new Error(`Poll failed ${pollResp.status}: ${errText}`);
849
+ }
850
+ current = (await pollResp.json());
851
+ }
852
+ if (!current.done)
853
+ throw new Error("Video generation timed out");
854
+ const videoUri = current.response?.generatedVideos?.[0]?.video?.uri;
855
+ if (!videoUri)
856
+ throw new Error("No video URI in response");
857
+ // Download the video
858
+ const dlResp = await this._fetch(`${videoUri}&key=${this.apiKey}`);
859
+ if (!dlResp.ok)
860
+ throw new Error(`Video download failed: ${dlResp.status}`);
861
+ return new Uint8Array(await dlResp.arrayBuffer());
862
+ }
863
+ // ---------------------------------------------------------------------------
864
+ // Image-to-video (Veo models)
865
+ // ---------------------------------------------------------------------------
866
+ async imageToVideo(image, params) {
867
+ if (!image || image.length === 0) {
868
+ throw new Error("Input image cannot be empty.");
869
+ }
870
+ const modelId = params.model.id;
871
+ if (!modelId.startsWith("veo-")) {
872
+ throw new Error(`Model ${modelId} is not a Veo model. Only Veo models support image-to-video.`);
873
+ }
874
+ const imageBase64 = Buffer.from(image).toString("base64");
875
+ const prompt = params.prompt ?? "Animate this image";
876
+ const body = {
877
+ instances: [
878
+ {
879
+ prompt,
880
+ image: { bytesBase64Encoded: imageBase64, mimeType: "image/png" }
881
+ }
882
+ ]
883
+ };
884
+ const parameters = {};
885
+ if (params.negativePrompt)
886
+ parameters.negativePrompt = params.negativePrompt;
887
+ if (params.aspectRatio)
888
+ parameters.aspectRatio = params.aspectRatio;
889
+ if (params.seed != null)
890
+ parameters.seed = params.seed;
891
+ if (Object.keys(parameters).length > 0)
892
+ body.parameters = parameters;
893
+ const url = `${GEMINI_API_BASE}/models/${modelId}:generateVideos?key=${this.apiKey}`;
894
+ const response = await this._fetch(url, {
895
+ method: "POST",
896
+ headers: { "Content-Type": "application/json" },
897
+ body: JSON.stringify(body)
898
+ });
899
+ if (!response.ok) {
900
+ const errText = await response.text();
901
+ throw new Error(`Gemini image-to-video failed ${response.status}: ${errText}`);
902
+ }
903
+ const operation = (await response.json());
904
+ // Poll for completion
905
+ const maxWait = 600_000;
906
+ const pollInterval = 10_000;
907
+ let elapsed = 0;
908
+ let current = operation;
909
+ while (!current.done && elapsed < maxWait) {
910
+ await new Promise((r) => setTimeout(r, pollInterval));
911
+ elapsed += pollInterval;
912
+ if (!current.name)
913
+ throw new Error("No operation name for polling");
914
+ const pollUrl = `${GEMINI_API_BASE}/${current.name}?key=${this.apiKey}`;
915
+ const pollResp = await this._fetch(pollUrl);
916
+ if (!pollResp.ok) {
917
+ const errText = await pollResp.text();
918
+ throw new Error(`Poll failed ${pollResp.status}: ${errText}`);
919
+ }
920
+ current = (await pollResp.json());
921
+ }
922
+ if (!current.done)
923
+ throw new Error("Image-to-video generation timed out");
924
+ const videoUri = current.response?.generatedVideos?.[0]?.video?.uri;
925
+ if (!videoUri)
926
+ throw new Error("No video URI in response");
927
+ const dlResp = await this._fetch(`${videoUri}&key=${this.apiKey}`);
928
+ if (!dlResp.ok)
929
+ throw new Error(`Video download failed: ${dlResp.status}`);
930
+ return new Uint8Array(await dlResp.arrayBuffer());
931
+ }
932
+ // ---------------------------------------------------------------------------
933
+ // Error detection
934
+ // ---------------------------------------------------------------------------
935
+ isContextLengthError(error) {
936
+ const msg = String(error).toLowerCase();
937
+ return (msg.includes("context length") ||
938
+ msg.includes("maximum context") ||
939
+ msg.includes("too long") ||
940
+ msg.includes("token limit"));
941
+ }
942
+ }
943
+ //# sourceMappingURL=gemini-provider.js.map