@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.
- package/dist/bridge-protocol.d.ts +39 -0
- package/dist/bridge-protocol.d.ts.map +1 -0
- package/dist/bridge-protocol.js +39 -0
- package/dist/bridge-protocol.js.map +1 -0
- package/dist/comfy-executor.d.ts +41 -0
- package/dist/comfy-executor.d.ts.map +1 -0
- package/dist/comfy-executor.js +305 -0
- package/dist/comfy-executor.js.map +1 -0
- package/dist/context-packer.d.ts +22 -0
- package/dist/context-packer.d.ts.map +1 -0
- package/dist/context-packer.js +66 -0
- package/dist/context-packer.js.map +1 -0
- package/dist/context.d.ts +405 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +1455 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node-executor.d.ts +53 -0
- package/dist/node-executor.d.ts.map +1 -0
- package/dist/node-executor.js +9 -0
- package/dist/node-executor.js.map +1 -0
- package/dist/providers/aki-manifest.json +34 -0
- package/dist/providers/aki-provider.d.ts +65 -0
- package/dist/providers/aki-provider.d.ts.map +1 -0
- package/dist/providers/aki-provider.js +517 -0
- package/dist/providers/aki-provider.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +55 -0
- package/dist/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/providers/anthropic-provider.js +563 -0
- package/dist/providers/anthropic-provider.js.map +1 -0
- package/dist/providers/base-provider.d.ts +194 -0
- package/dist/providers/base-provider.d.ts.map +1 -0
- package/dist/providers/base-provider.js +515 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/cerebras-provider.d.ts +20 -0
- package/dist/providers/cerebras-provider.d.ts.map +1 -0
- package/dist/providers/cerebras-provider.js +52 -0
- package/dist/providers/cerebras-provider.js.map +1 -0
- package/dist/providers/claude-agent-provider.d.ts +107 -0
- package/dist/providers/claude-agent-provider.d.ts.map +1 -0
- package/dist/providers/claude-agent-provider.js +586 -0
- package/dist/providers/claude-agent-provider.js.map +1 -0
- package/dist/providers/cohere-provider.d.ts +36 -0
- package/dist/providers/cohere-provider.d.ts.map +1 -0
- package/dist/providers/cohere-provider.js +116 -0
- package/dist/providers/cohere-provider.js.map +1 -0
- package/dist/providers/cost-calculator.d.ts +109 -0
- package/dist/providers/cost-calculator.d.ts.map +1 -0
- package/dist/providers/cost-calculator.js +475 -0
- package/dist/providers/cost-calculator.js.map +1 -0
- package/dist/providers/deepseek-provider.d.ts +25 -0
- package/dist/providers/deepseek-provider.d.ts.map +1 -0
- package/dist/providers/deepseek-provider.js +58 -0
- package/dist/providers/deepseek-provider.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +52 -0
- package/dist/providers/fake-provider.d.ts.map +1 -0
- package/dist/providers/fake-provider.js +107 -0
- package/dist/providers/fake-provider.js.map +1 -0
- package/dist/providers/fal-provider.d.ts +32 -0
- package/dist/providers/fal-provider.d.ts.map +1 -0
- package/dist/providers/fal-provider.js +445 -0
- package/dist/providers/fal-provider.js.map +1 -0
- package/dist/providers/gemini-provider.d.ts +109 -0
- package/dist/providers/gemini-provider.d.ts.map +1 -0
- package/dist/providers/gemini-provider.js +943 -0
- package/dist/providers/gemini-provider.js.map +1 -0
- package/dist/providers/groq-provider.d.ts +20 -0
- package/dist/providers/groq-provider.d.ts.map +1 -0
- package/dist/providers/groq-provider.js +52 -0
- package/dist/providers/groq-provider.js.map +1 -0
- package/dist/providers/huggingface-provider.d.ts +59 -0
- package/dist/providers/huggingface-provider.d.ts.map +1 -0
- package/dist/providers/huggingface-provider.js +305 -0
- package/dist/providers/huggingface-provider.js.map +1 -0
- package/dist/providers/index.d.ts +69 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +137 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/jina-provider.d.ts +37 -0
- package/dist/providers/jina-provider.d.ts.map +1 -0
- package/dist/providers/jina-provider.js +118 -0
- package/dist/providers/jina-provider.js.map +1 -0
- package/dist/providers/kie-provider.d.ts +19 -0
- package/dist/providers/kie-provider.d.ts.map +1 -0
- package/dist/providers/kie-provider.js +118 -0
- package/dist/providers/kie-provider.js.map +1 -0
- package/dist/providers/llama-provider.d.ts +49 -0
- package/dist/providers/llama-provider.d.ts.map +1 -0
- package/dist/providers/llama-provider.js +381 -0
- package/dist/providers/llama-provider.js.map +1 -0
- package/dist/providers/lmstudio-provider.d.ts +22 -0
- package/dist/providers/lmstudio-provider.d.ts.map +1 -0
- package/dist/providers/lmstudio-provider.js +63 -0
- package/dist/providers/lmstudio-provider.js.map +1 -0
- package/dist/providers/manifest-models.d.ts +12 -0
- package/dist/providers/manifest-models.d.ts.map +1 -0
- package/dist/providers/manifest-models.js +96 -0
- package/dist/providers/manifest-models.js.map +1 -0
- package/dist/providers/meshy-provider.d.ts +53 -0
- package/dist/providers/meshy-provider.d.ts.map +1 -0
- package/dist/providers/meshy-provider.js +230 -0
- package/dist/providers/meshy-provider.js.map +1 -0
- package/dist/providers/minimax-provider.d.ts +66 -0
- package/dist/providers/minimax-provider.d.ts.map +1 -0
- package/dist/providers/minimax-provider.js +531 -0
- package/dist/providers/minimax-provider.js.map +1 -0
- package/dist/providers/mistral-provider.d.ts +26 -0
- package/dist/providers/mistral-provider.d.ts.map +1 -0
- package/dist/providers/mistral-provider.js +74 -0
- package/dist/providers/mistral-provider.js.map +1 -0
- package/dist/providers/moonshot-provider.d.ts +24 -0
- package/dist/providers/moonshot-provider.d.ts.map +1 -0
- package/dist/providers/moonshot-provider.js +43 -0
- package/dist/providers/moonshot-provider.js.map +1 -0
- package/dist/providers/ollama-provider.d.ts +71 -0
- package/dist/providers/ollama-provider.d.ts.map +1 -0
- package/dist/providers/ollama-provider.js +480 -0
- package/dist/providers/ollama-provider.js.map +1 -0
- package/dist/providers/openai-provider.d.ts +103 -0
- package/dist/providers/openai-provider.d.ts.map +1 -0
- package/dist/providers/openai-provider.js +917 -0
- package/dist/providers/openai-provider.js.map +1 -0
- package/dist/providers/openrouter-provider.d.ts +49 -0
- package/dist/providers/openrouter-provider.d.ts.map +1 -0
- package/dist/providers/openrouter-provider.js +147 -0
- package/dist/providers/openrouter-provider.js.map +1 -0
- package/dist/providers/provider-registry.d.ts +24 -0
- package/dist/providers/provider-registry.d.ts.map +1 -0
- package/dist/providers/provider-registry.js +124 -0
- package/dist/providers/provider-registry.js.map +1 -0
- package/dist/providers/python-provider.d.ts +75 -0
- package/dist/providers/python-provider.d.ts.map +1 -0
- package/dist/providers/python-provider.js +157 -0
- package/dist/providers/python-provider.js.map +1 -0
- package/dist/providers/replicate-provider.d.ts +73 -0
- package/dist/providers/replicate-provider.d.ts.map +1 -0
- package/dist/providers/replicate-provider.js +484 -0
- package/dist/providers/replicate-provider.js.map +1 -0
- package/dist/providers/rodin-provider.d.ts +41 -0
- package/dist/providers/rodin-provider.d.ts.map +1 -0
- package/dist/providers/rodin-provider.js +236 -0
- package/dist/providers/rodin-provider.js.map +1 -0
- package/dist/providers/scripted-provider.d.ts +141 -0
- package/dist/providers/scripted-provider.d.ts.map +1 -0
- package/dist/providers/scripted-provider.js +246 -0
- package/dist/providers/scripted-provider.js.map +1 -0
- package/dist/providers/together-provider.d.ts +20 -0
- package/dist/providers/together-provider.d.ts.map +1 -0
- package/dist/providers/together-provider.js +56 -0
- package/dist/providers/together-provider.js.map +1 -0
- package/dist/providers/types.d.ts +202 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/vllm-provider.d.ts +22 -0
- package/dist/providers/vllm-provider.d.ts.map +1 -0
- package/dist/providers/vllm-provider.js +66 -0
- package/dist/providers/vllm-provider.js.map +1 -0
- package/dist/providers/voyage-provider.d.ts +37 -0
- package/dist/providers/voyage-provider.d.ts.map +1 -0
- package/dist/providers/voyage-provider.js +124 -0
- package/dist/providers/voyage-provider.js.map +1 -0
- package/dist/providers/xai-provider.d.ts +24 -0
- package/dist/providers/xai-provider.d.ts.map +1 -0
- package/dist/providers/xai-provider.js +57 -0
- package/dist/providers/xai-provider.js.map +1 -0
- package/dist/python-bridge-types.d.ts +70 -0
- package/dist/python-bridge-types.d.ts.map +1 -0
- package/dist/python-bridge-types.js +2 -0
- package/dist/python-bridge-types.js.map +1 -0
- package/dist/python-node-executor.d.ts +20 -0
- package/dist/python-node-executor.d.ts.map +1 -0
- package/dist/python-node-executor.js +219 -0
- package/dist/python-node-executor.js.map +1 -0
- package/dist/python-stdio-bridge.d.ts +67 -0
- package/dist/python-stdio-bridge.d.ts.map +1 -0
- package/dist/python-stdio-bridge.js +704 -0
- package/dist/python-stdio-bridge.js.map +1 -0
- package/dist/recommended-models.d.ts +17 -0
- package/dist/recommended-models.d.ts.map +1 -0
- package/dist/recommended-models.js +139 -0
- package/dist/recommended-models.js.map +1 -0
- package/dist/telemetry.d.ts +61 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +128 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/trace-exporters.d.ts +72 -0
- package/dist/trace-exporters.d.ts.map +1 -0
- package/dist/trace-exporters.js +216 -0
- package/dist/trace-exporters.js.map +1 -0
- package/dist/tracing-helpers.d.ts +99 -0
- package/dist/tracing-helpers.d.ts.map +1 -0
- package/dist/tracing-helpers.js +217 -0
- package/dist/tracing-helpers.js.map +1 -0
- 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
|