@contentgrowth/llm-service 0.8.2 → 0.8.4
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/index.cjs +1527 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +529 -0
- package/dist/index.d.ts +529 -0
- package/dist/index.js +1478 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +3 -0
- package/dist/ui/react/components/index.cjs +1386 -0
- package/dist/ui/react/components/index.cjs.map +1 -0
- package/dist/ui/react/components/index.d.cts +291 -0
- package/dist/ui/react/components/index.d.ts +291 -0
- package/dist/ui/react/components/index.js +1344 -0
- package/dist/ui/react/components/index.js.map +1 -0
- package/package.json +46 -10
- package/src/index.js +0 -9
- package/src/llm/config-manager.js +0 -45
- package/src/llm/config-provider.js +0 -140
- package/src/llm/json-utils.js +0 -147
- package/src/llm/providers/base-provider.js +0 -134
- package/src/llm/providers/gemini-provider.js +0 -607
- package/src/llm/providers/openai-provider.js +0 -203
- package/src/llm-service.js +0 -281
- package/src/utils/error-handler.js +0 -117
package/dist/index.js
ADDED
|
@@ -0,0 +1,1478 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/llm/config-provider.js
|
|
6
|
+
var BaseConfigProvider = class {
|
|
7
|
+
/**
|
|
8
|
+
* Retrieve configuration for a specific tenant.
|
|
9
|
+
* @param {string} tenantId
|
|
10
|
+
* @param {Object} env
|
|
11
|
+
* @returns {Promise<Object>} Configuration object
|
|
12
|
+
*/
|
|
13
|
+
async getConfig(tenantId, env) {
|
|
14
|
+
throw new Error("Method not implemented");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var DefaultConfigProvider = class extends BaseConfigProvider {
|
|
18
|
+
async getConfig(tenantId, env) {
|
|
19
|
+
var _a;
|
|
20
|
+
if (!tenantId) {
|
|
21
|
+
return this._getSystemConfig(env);
|
|
22
|
+
}
|
|
23
|
+
const cacheKey = `tenant:${tenantId}:llm_config`;
|
|
24
|
+
const cached = await ((_a = env.TENANT_LLM_CONFIG) == null ? void 0 : _a.get(cacheKey));
|
|
25
|
+
if (cached !== null && cached !== void 0) {
|
|
26
|
+
if (cached === "{}") {
|
|
27
|
+
return this._getSystemConfig(env);
|
|
28
|
+
}
|
|
29
|
+
const config = JSON.parse(cached);
|
|
30
|
+
return this._buildTenantConfig(config, env);
|
|
31
|
+
}
|
|
32
|
+
const tenantConfig = await this._loadFromTenantDO(tenantId, env);
|
|
33
|
+
if (tenantConfig && tenantConfig.enabled && tenantConfig.api_key) {
|
|
34
|
+
if (env.TENANT_LLM_CONFIG) {
|
|
35
|
+
await env.TENANT_LLM_CONFIG.put(
|
|
36
|
+
cacheKey,
|
|
37
|
+
JSON.stringify(tenantConfig),
|
|
38
|
+
{ expirationTtl: 3600 }
|
|
39
|
+
// 1 hour
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return this._buildTenantConfig(tenantConfig, env);
|
|
43
|
+
} else {
|
|
44
|
+
if (env.TENANT_LLM_CONFIG) {
|
|
45
|
+
await env.TENANT_LLM_CONFIG.put(
|
|
46
|
+
cacheKey,
|
|
47
|
+
"{}",
|
|
48
|
+
{ expirationTtl: 3600 }
|
|
49
|
+
// 1 hour
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return this._getSystemConfig(env);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async _loadFromTenantDO(tenantId, env) {
|
|
56
|
+
try {
|
|
57
|
+
if (!env.TENANT_DO) return null;
|
|
58
|
+
const doId = env.TENANT_DO.idFromName(tenantId);
|
|
59
|
+
const stub = env.TENANT_DO.get(doId);
|
|
60
|
+
const response = await stub.fetch(new Request(`http://do/llm-config/${tenantId}`));
|
|
61
|
+
if (response.ok) {
|
|
62
|
+
const config = await response.json();
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`[ConfigManager] Failed to load from TenantDO for tenant ${tenantId}:`, error);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
_buildTenantConfig(tenantConfig, env) {
|
|
72
|
+
return {
|
|
73
|
+
provider: tenantConfig.provider,
|
|
74
|
+
apiKey: tenantConfig.api_key,
|
|
75
|
+
models: MODEL_CONFIGS[tenantConfig.provider],
|
|
76
|
+
temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
|
|
77
|
+
maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "16384"),
|
|
78
|
+
capabilities: tenantConfig.capabilities || { chat: true, image: false, video: false },
|
|
79
|
+
isTenantOwned: true
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
_getSystemConfig(env) {
|
|
83
|
+
var _a;
|
|
84
|
+
const provider = ((_a = env.LLM_PROVIDER) == null ? void 0 : _a.toLowerCase()) || "openai";
|
|
85
|
+
const providerDefaults = MODEL_CONFIGS[provider] || MODEL_CONFIGS["openai"];
|
|
86
|
+
let apiKey;
|
|
87
|
+
let models = { ...providerDefaults };
|
|
88
|
+
if (provider === "openai") {
|
|
89
|
+
apiKey = env.OPENAI_API_KEY;
|
|
90
|
+
models = {
|
|
91
|
+
default: env.OPENAI_MODEL || providerDefaults.default,
|
|
92
|
+
edge: env.OPENAI_MODEL_EDGE || providerDefaults.edge,
|
|
93
|
+
fast: env.OPENAI_MODEL_FAST || providerDefaults.fast,
|
|
94
|
+
cost: env.OPENAI_MODEL_COST || providerDefaults.cost,
|
|
95
|
+
free: env.OPENAI_MODEL_FREE || providerDefaults.free
|
|
96
|
+
};
|
|
97
|
+
} else if (provider === "gemini") {
|
|
98
|
+
apiKey = env.GEMINI_API_KEY;
|
|
99
|
+
models = {
|
|
100
|
+
default: env.GEMINI_MODEL || providerDefaults.default,
|
|
101
|
+
edge: env.GEMINI_MODEL_EDGE || providerDefaults.edge,
|
|
102
|
+
fast: env.GEMINI_MODEL_FAST || providerDefaults.fast,
|
|
103
|
+
cost: env.GEMINI_MODEL_COST || providerDefaults.cost,
|
|
104
|
+
free: env.GEMINI_MODEL_FREE || providerDefaults.free,
|
|
105
|
+
image: env.GEMINI_IMAGE_MODEL || providerDefaults.image,
|
|
106
|
+
video: env.GEMINI_VIDEO_MODEL || providerDefaults.video
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
provider,
|
|
111
|
+
apiKey,
|
|
112
|
+
models,
|
|
113
|
+
temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
|
|
114
|
+
maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "16384")
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/llm/config-manager.js
|
|
120
|
+
var MODEL_CONFIGS = {
|
|
121
|
+
openai: {
|
|
122
|
+
default: "gpt-4o",
|
|
123
|
+
edge: "gpt-4o",
|
|
124
|
+
fast: "gpt-4o-mini",
|
|
125
|
+
cost: "gpt-4o-mini",
|
|
126
|
+
free: "gpt-4o-mini"
|
|
127
|
+
},
|
|
128
|
+
gemini: {
|
|
129
|
+
default: "gemini-3-flash-preview",
|
|
130
|
+
// 'gemini-2.5-flash',
|
|
131
|
+
edge: "gemini-3-pro-preview",
|
|
132
|
+
// 'gemini-2.5-pro',
|
|
133
|
+
fast: "gemini-3-flash-preview",
|
|
134
|
+
// 'gemini-2.5-flash-lite',
|
|
135
|
+
cost: "gemini-3-flash-preview",
|
|
136
|
+
// 'gemini-2.5-flash-lite',
|
|
137
|
+
free: "gemini-3-flash-preview",
|
|
138
|
+
// 'gemini-2.0-flash-lite',
|
|
139
|
+
video: "veo",
|
|
140
|
+
image: "gemini-3-pro-image-preview"
|
|
141
|
+
// Default image generation model
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var ConfigManager = class {
|
|
145
|
+
/**
|
|
146
|
+
* Set a custom configuration provider.
|
|
147
|
+
* @param {BaseConfigProvider} provider
|
|
148
|
+
*/
|
|
149
|
+
static setConfigProvider(provider) {
|
|
150
|
+
this._provider = provider;
|
|
151
|
+
}
|
|
152
|
+
static async getConfig(tenantId, env) {
|
|
153
|
+
return this._provider.getConfig(tenantId, env);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
__publicField(ConfigManager, "_provider", new DefaultConfigProvider());
|
|
157
|
+
|
|
158
|
+
// src/llm/providers/openai-provider.js
|
|
159
|
+
import OpenAI from "openai";
|
|
160
|
+
|
|
161
|
+
// src/llm/providers/base-provider.js
|
|
162
|
+
var FINISH_REASONS = {
|
|
163
|
+
COMPLETED: "completed",
|
|
164
|
+
// Normal completion (OpenAI: stop, Gemini: STOP, Anthropic: end_turn)
|
|
165
|
+
TRUNCATED: "truncated",
|
|
166
|
+
// Hit max tokens (OpenAI: length, Gemini: MAX_TOKENS, Anthropic: max_tokens)
|
|
167
|
+
CONTENT_FILTER: "content_filter",
|
|
168
|
+
// Content was filtered
|
|
169
|
+
TOOL_CALL: "tool_call",
|
|
170
|
+
// Stopped for tool call
|
|
171
|
+
UNKNOWN: "unknown"
|
|
172
|
+
// Unknown/unmapped reason
|
|
173
|
+
};
|
|
174
|
+
var BaseLLMProvider = class {
|
|
175
|
+
constructor(config) {
|
|
176
|
+
this.config = config;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Normalize provider-specific finish reason to standard value.
|
|
180
|
+
* Override in subclass if provider uses different values.
|
|
181
|
+
* @param {string} providerReason - The provider's native finish reason
|
|
182
|
+
* @returns {string} Standardized finish reason from FINISH_REASONS
|
|
183
|
+
*/
|
|
184
|
+
normalizeFinishReason(providerReason) {
|
|
185
|
+
const upperReason = (providerReason || "").toUpperCase();
|
|
186
|
+
if (["STOP", "END_TURN"].includes(upperReason)) {
|
|
187
|
+
return FINISH_REASONS.COMPLETED;
|
|
188
|
+
}
|
|
189
|
+
if (["LENGTH", "MAX_TOKENS"].includes(upperReason)) {
|
|
190
|
+
return FINISH_REASONS.TRUNCATED;
|
|
191
|
+
}
|
|
192
|
+
if (["CONTENT_FILTER", "SAFETY"].includes(upperReason)) {
|
|
193
|
+
return FINISH_REASONS.CONTENT_FILTER;
|
|
194
|
+
}
|
|
195
|
+
if (["TOOL_CALLS", "TOOL_USE", "FUNCTION_CALL"].includes(upperReason)) {
|
|
196
|
+
return FINISH_REASONS.TOOL_CALL;
|
|
197
|
+
}
|
|
198
|
+
return FINISH_REASONS.UNKNOWN;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Simple chat interface for single-turn conversations
|
|
202
|
+
* @param {string} userMessage
|
|
203
|
+
* @param {string} systemPrompt
|
|
204
|
+
* @param {Object} options
|
|
205
|
+
*/
|
|
206
|
+
async chat(userMessage, systemPrompt, options) {
|
|
207
|
+
throw new Error("Method not implemented");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Advanced chat completion with tool support
|
|
211
|
+
* @param {Array} messages
|
|
212
|
+
* @param {string} systemPrompt
|
|
213
|
+
* @param {Array} tools
|
|
214
|
+
* @param {Object} options - Generation options (responseFormat, temperature, etc.)
|
|
215
|
+
*/
|
|
216
|
+
async chatCompletion(messages, systemPrompt, tools, options = {}) {
|
|
217
|
+
throw new Error("Method not implemented");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Execute tools requested by the LLM
|
|
221
|
+
* @param {Array} toolCalls
|
|
222
|
+
* @param {Array} messages
|
|
223
|
+
* @param {string} tenantId
|
|
224
|
+
* @param {Object} toolImplementations
|
|
225
|
+
* @param {Object} env
|
|
226
|
+
*/
|
|
227
|
+
async executeTools(toolCalls, messages, tenantId, toolImplementations, env) {
|
|
228
|
+
throw new Error("Method not implemented");
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Generate image
|
|
232
|
+
* Subclasses should override this method.
|
|
233
|
+
* Model can be overridden via options.model, otherwise uses config.models.image
|
|
234
|
+
* @param {string} prompt - Text description of the image
|
|
235
|
+
* @param {string} systemPrompt - System instructions for generation
|
|
236
|
+
* @param {Object} options - Generation options (aspectRatio, images, model, etc.)
|
|
237
|
+
* @returns {Promise<{imageData: string, mimeType: string}>}
|
|
238
|
+
*/
|
|
239
|
+
async imageGeneration(prompt, systemPrompt, options = {}) {
|
|
240
|
+
throw new Error("Image generation not supported by this provider");
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Start video generation (returns operation name for polling)
|
|
244
|
+
* @param {string} prompt
|
|
245
|
+
* @param {Array} images
|
|
246
|
+
* @param {string} modelName
|
|
247
|
+
* @param {string} systemPrompt
|
|
248
|
+
* @param {Object} options
|
|
249
|
+
* @returns {Promise<{operationName: string}>}
|
|
250
|
+
*/
|
|
251
|
+
async startVideoGeneration(prompt, images, modelName, systemPrompt, options) {
|
|
252
|
+
throw new Error("Video generation not supported by this provider");
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get video generation status (poll operation)
|
|
256
|
+
* @param {string} operationName
|
|
257
|
+
* @returns {Promise<{done: boolean, progress: number, state: string, videoUri?: string, error?: object}>}
|
|
258
|
+
*/
|
|
259
|
+
async getVideoGenerationStatus(operationName) {
|
|
260
|
+
throw new Error("Video generation not supported by this provider");
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Helper to get the last 6 digits of the API key for logging.
|
|
264
|
+
* @returns {string} "..." + last 6 chars, or "not_set"
|
|
265
|
+
*/
|
|
266
|
+
_getMaskedApiKey() {
|
|
267
|
+
const key = this.config.apiKey;
|
|
268
|
+
if (!key) return "not_set";
|
|
269
|
+
if (key.length <= 6) return "...";
|
|
270
|
+
return "..." + key.slice(-6);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/llm/json-utils.js
|
|
275
|
+
function extractJsonFromResponse(text) {
|
|
276
|
+
if (!text || typeof text !== "string") {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
function sanitizeJsonString(str) {
|
|
280
|
+
return str.replace(/"(?:[^"\\]|\\.)*"/g, (match2) => {
|
|
281
|
+
return match2.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function tryParseJson(jsonStr) {
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(jsonStr);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
if (jsonStr.includes("\\\\\\\\")) {
|
|
289
|
+
console.warn("Initial JSON parse failed, attempting normalization:", e.message);
|
|
290
|
+
try {
|
|
291
|
+
let normalized = jsonStr.replace(/\\\\\\\\/g, "\\\\");
|
|
292
|
+
return JSON.parse(normalized);
|
|
293
|
+
} catch (e2) {
|
|
294
|
+
console.warn("Normalized JSON parse also failed:", e2.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const sanitized = sanitizeJsonString(jsonStr);
|
|
299
|
+
return JSON.parse(sanitized);
|
|
300
|
+
} catch (e3) {
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const jsonRegex = /```(?:json)?\s*({[\s\S]*?})\s*```/;
|
|
306
|
+
const match = text.match(jsonRegex);
|
|
307
|
+
if (match && match[1]) {
|
|
308
|
+
try {
|
|
309
|
+
return tryParseJson(match[1]);
|
|
310
|
+
} catch (e) {
|
|
311
|
+
console.warn("Could not parse the content of a matched JSON block.", e.message);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const firstBrace = text.indexOf("{");
|
|
315
|
+
const lastBrace = text.lastIndexOf("}");
|
|
316
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
317
|
+
const potentialJson = text.substring(firstBrace, lastBrace + 1);
|
|
318
|
+
try {
|
|
319
|
+
return tryParseJson(potentialJson);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
console.error("Error parsing JSON extracted in { and }", e);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
function extractTextAndJson(input) {
|
|
327
|
+
if (!input) return { text: "", json: null };
|
|
328
|
+
const json = extractJsonFromResponse(input);
|
|
329
|
+
if (!json) {
|
|
330
|
+
return { text: input, json: null };
|
|
331
|
+
}
|
|
332
|
+
let text = input;
|
|
333
|
+
const fencedRegex = /```(?:json)?\s*({[\s\S]*?})\s*```/;
|
|
334
|
+
const fencedMatch = input.match(fencedRegex);
|
|
335
|
+
if (fencedMatch) {
|
|
336
|
+
text = input.replace(fencedMatch[0], "").trim();
|
|
337
|
+
return { text, json };
|
|
338
|
+
}
|
|
339
|
+
const firstBrace = input.indexOf("{");
|
|
340
|
+
const lastBrace = input.lastIndexOf("}");
|
|
341
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
342
|
+
const pre = input.substring(0, firstBrace);
|
|
343
|
+
const post = input.substring(lastBrace + 1);
|
|
344
|
+
text = (pre + post).trim();
|
|
345
|
+
return { text, json };
|
|
346
|
+
}
|
|
347
|
+
return { text: input, json };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/llm/providers/openai-provider.js
|
|
351
|
+
var OpenAIProvider = class extends BaseLLMProvider {
|
|
352
|
+
constructor(config) {
|
|
353
|
+
super(config);
|
|
354
|
+
this.client = new OpenAI({ apiKey: config.apiKey });
|
|
355
|
+
this.models = config.models;
|
|
356
|
+
this.defaultModel = config.models.default;
|
|
357
|
+
}
|
|
358
|
+
async chat(userMessage, systemPrompt = "", options = {}) {
|
|
359
|
+
const messages = [{ role: "user", content: userMessage }];
|
|
360
|
+
const tier = options.tier || "default";
|
|
361
|
+
const effectiveModel = this._getModelForTier(tier);
|
|
362
|
+
const effectiveMaxTokens = options.maxTokens || this.config.maxTokens;
|
|
363
|
+
const effectiveTemperature = options.temperature !== void 0 ? options.temperature : this.config.temperature;
|
|
364
|
+
const response = await this._chatCompletionWithModel(
|
|
365
|
+
messages,
|
|
366
|
+
systemPrompt,
|
|
367
|
+
null,
|
|
368
|
+
effectiveModel,
|
|
369
|
+
effectiveMaxTokens,
|
|
370
|
+
effectiveTemperature
|
|
371
|
+
);
|
|
372
|
+
return { text: response.content };
|
|
373
|
+
}
|
|
374
|
+
async chatCompletion(messages, systemPrompt, tools = null, options = {}) {
|
|
375
|
+
return this._chatCompletionWithModel(
|
|
376
|
+
messages,
|
|
377
|
+
systemPrompt,
|
|
378
|
+
tools,
|
|
379
|
+
this.defaultModel,
|
|
380
|
+
this.config.maxTokens,
|
|
381
|
+
this.config.temperature,
|
|
382
|
+
options
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
|
|
386
|
+
var _a, _b;
|
|
387
|
+
const requestPayload = {
|
|
388
|
+
model: modelName,
|
|
389
|
+
temperature: (_a = options.temperature) != null ? _a : temperature,
|
|
390
|
+
max_tokens: (_b = options.maxTokens) != null ? _b : maxTokens,
|
|
391
|
+
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
|
392
|
+
tools,
|
|
393
|
+
tool_choice: tools ? "auto" : void 0
|
|
394
|
+
};
|
|
395
|
+
if (options.responseFormat) {
|
|
396
|
+
requestPayload.response_format = this._buildResponseFormat(options);
|
|
397
|
+
}
|
|
398
|
+
let response;
|
|
399
|
+
try {
|
|
400
|
+
response = await this.client.chat.completions.create(requestPayload);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error(`[OpenAIProvider] chat completion failed (API Key: ${this._getMaskedApiKey()}):`, error);
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
const message = response.choices[0].message;
|
|
406
|
+
if (!message.content && (!message.tool_calls || message.tool_calls.length === 0)) {
|
|
407
|
+
console.error("[OpenAIProvider] Model returned empty response (no text, no tool calls)");
|
|
408
|
+
throw new LLMServiceException(
|
|
409
|
+
"Model returned empty response. This usually means the prompt or schema is confusing the model.",
|
|
410
|
+
500
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const rawFinishReason = response.choices[0].finish_reason;
|
|
414
|
+
const normalizedFinishReason = this.normalizeFinishReason(rawFinishReason);
|
|
415
|
+
return {
|
|
416
|
+
content: message.content,
|
|
417
|
+
tool_calls: message.tool_calls,
|
|
418
|
+
finishReason: normalizedFinishReason,
|
|
419
|
+
// Standardized: 'completed', 'truncated', etc.
|
|
420
|
+
_rawFinishReason: rawFinishReason,
|
|
421
|
+
// Keep original for debugging
|
|
422
|
+
// Add metadata about response format
|
|
423
|
+
_responseFormat: options.responseFormat,
|
|
424
|
+
// Auto-parse JSON if requested
|
|
425
|
+
...options.responseFormat && this._shouldAutoParse(options) ? {
|
|
426
|
+
parsedContent: this._safeJsonParse(message.content)
|
|
427
|
+
} : {}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
_buildResponseFormat(options) {
|
|
431
|
+
var _a, _b;
|
|
432
|
+
if (!options.responseFormat) {
|
|
433
|
+
return void 0;
|
|
434
|
+
}
|
|
435
|
+
const formatType = typeof options.responseFormat === "string" ? options.responseFormat : options.responseFormat.type;
|
|
436
|
+
const schema = typeof options.responseFormat === "object" ? options.responseFormat.schema : options.responseSchema || null;
|
|
437
|
+
switch (formatType) {
|
|
438
|
+
case "json":
|
|
439
|
+
if (schema) {
|
|
440
|
+
console.log("[OpenAIProvider] Using Strict JSON mode with schema");
|
|
441
|
+
return {
|
|
442
|
+
type: "json_schema",
|
|
443
|
+
json_schema: {
|
|
444
|
+
name: options.schemaName || "response_schema",
|
|
445
|
+
strict: (_a = options.strictSchema) != null ? _a : true,
|
|
446
|
+
schema
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
} else {
|
|
450
|
+
console.warn("[OpenAIProvider] Using legacy json_object mode without schema - may produce markdown wrappers");
|
|
451
|
+
return { type: "json_object" };
|
|
452
|
+
}
|
|
453
|
+
case "json_schema":
|
|
454
|
+
if (!schema) {
|
|
455
|
+
throw new Error("responseSchema required when using json_schema format");
|
|
456
|
+
}
|
|
457
|
+
console.log("[OpenAIProvider] Using Strict JSON mode with schema");
|
|
458
|
+
return {
|
|
459
|
+
type: "json_schema",
|
|
460
|
+
json_schema: {
|
|
461
|
+
name: options.schemaName || "response_schema",
|
|
462
|
+
strict: (_b = options.strictSchema) != null ? _b : true,
|
|
463
|
+
schema
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
default:
|
|
467
|
+
return void 0;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
_shouldAutoParse(options) {
|
|
471
|
+
return options.autoParse !== false;
|
|
472
|
+
}
|
|
473
|
+
_safeJsonParse(content) {
|
|
474
|
+
if (!content) return null;
|
|
475
|
+
const parsed = extractJsonFromResponse(content);
|
|
476
|
+
if (parsed) {
|
|
477
|
+
console.log("[OpenAIProvider] Successfully parsed JSON from response");
|
|
478
|
+
} else {
|
|
479
|
+
console.error("[OpenAIProvider] Failed to extract valid JSON from response");
|
|
480
|
+
console.error("[OpenAIProvider] Content preview:", content.substring(0, 200));
|
|
481
|
+
}
|
|
482
|
+
return parsed;
|
|
483
|
+
}
|
|
484
|
+
async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
|
|
485
|
+
const toolResults = await Promise.all(
|
|
486
|
+
tool_calls.map(async (toolCall) => {
|
|
487
|
+
const toolName = toolCall.function.name;
|
|
488
|
+
const tool = toolImplementations[toolName];
|
|
489
|
+
console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.arguments);
|
|
490
|
+
if (!tool) {
|
|
491
|
+
console.error(`[Tool Error] Tool '${toolName}' not found`);
|
|
492
|
+
return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Tool '${toolName}' not found.` }) };
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const output = await tool(toolCall.function.arguments, { env, tenantId });
|
|
496
|
+
console.log(`[Tool Result] ${toolName} returned:`, output.substring(0, 200) + (output.length > 200 ? "..." : ""));
|
|
497
|
+
return { tool_call_id: toolCall.id, output };
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.error(`[Tool Error] ${toolName} failed:`, error.message);
|
|
500
|
+
return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Error executing tool '${toolName}': ${error.message}` }) };
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
);
|
|
504
|
+
toolResults.forEach((result) => messages.push({ role: "tool", tool_call_id: result.tool_call_id, content: result.output }));
|
|
505
|
+
}
|
|
506
|
+
_getModelForTier(tier) {
|
|
507
|
+
return this.models[tier] || this.models.default;
|
|
508
|
+
}
|
|
509
|
+
async videoGeneration(prompt, images, modelName, systemPrompt, options = {}) {
|
|
510
|
+
throw new Error("Video generation is not supported by OpenAI provider yet.");
|
|
511
|
+
}
|
|
512
|
+
async startDeepResearch(prompt, options = {}) {
|
|
513
|
+
throw new Error("Deep Research is not supported by OpenAI provider.");
|
|
514
|
+
}
|
|
515
|
+
async getDeepResearchStatus(operationId) {
|
|
516
|
+
throw new Error("Deep Research is not supported by OpenAI provider.");
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/llm/providers/gemini-provider.js
|
|
521
|
+
import { GoogleGenAI } from "@google/genai";
|
|
522
|
+
var GeminiProvider = class extends BaseLLMProvider {
|
|
523
|
+
constructor(config) {
|
|
524
|
+
super(config);
|
|
525
|
+
const clientConfig = {};
|
|
526
|
+
if (config.project || config.location) {
|
|
527
|
+
console.log(`[GeminiProvider] Initializing with Vertex AI (Project: ${config.project}, Location: ${config.location || "us-central1"})`);
|
|
528
|
+
clientConfig.vertexAI = {
|
|
529
|
+
project: config.project,
|
|
530
|
+
location: config.location || "us-central1"
|
|
531
|
+
};
|
|
532
|
+
} else {
|
|
533
|
+
clientConfig.apiKey = config.apiKey;
|
|
534
|
+
}
|
|
535
|
+
this.client = new GoogleGenAI(clientConfig);
|
|
536
|
+
this.models = config.models;
|
|
537
|
+
this.defaultModel = config.models.default;
|
|
538
|
+
this._pendingOperations = /* @__PURE__ */ new Map();
|
|
539
|
+
}
|
|
540
|
+
async chat(userMessage, systemPrompt = "", options = {}) {
|
|
541
|
+
const messages = [{ role: "user", content: userMessage }];
|
|
542
|
+
const tier = options.tier || "default";
|
|
543
|
+
const effectiveModel = this._getModelForTier(tier);
|
|
544
|
+
const effectiveMaxTokens = options.maxTokens || this.config.maxTokens;
|
|
545
|
+
const effectiveTemperature = options.temperature !== void 0 ? options.temperature : this.config.temperature;
|
|
546
|
+
const response = await this._chatCompletionWithModel(
|
|
547
|
+
messages,
|
|
548
|
+
systemPrompt,
|
|
549
|
+
null,
|
|
550
|
+
effectiveModel,
|
|
551
|
+
effectiveMaxTokens,
|
|
552
|
+
effectiveTemperature
|
|
553
|
+
);
|
|
554
|
+
return { text: response.content };
|
|
555
|
+
}
|
|
556
|
+
async chatCompletion(messages, systemPrompt, tools = null, options = {}) {
|
|
557
|
+
return this._chatCompletionWithModel(
|
|
558
|
+
messages,
|
|
559
|
+
systemPrompt,
|
|
560
|
+
tools,
|
|
561
|
+
this.defaultModel,
|
|
562
|
+
this.config.maxTokens,
|
|
563
|
+
this.config.temperature,
|
|
564
|
+
options
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
|
|
568
|
+
var _a, _b, _c, _d;
|
|
569
|
+
const generationConfig = {
|
|
570
|
+
temperature: (_a = options.temperature) != null ? _a : temperature,
|
|
571
|
+
maxOutputTokens: (_b = options.maxTokens) != null ? _b : maxTokens
|
|
572
|
+
};
|
|
573
|
+
if (options.responseFormat) {
|
|
574
|
+
const formatConfig = this._buildGenerationConfig(options, maxTokens, temperature);
|
|
575
|
+
Object.assign(generationConfig, formatConfig);
|
|
576
|
+
}
|
|
577
|
+
const geminiMessages = [];
|
|
578
|
+
let systemContentBuffer = [];
|
|
579
|
+
for (const msg of messages) {
|
|
580
|
+
if (msg.role === "system") {
|
|
581
|
+
systemContentBuffer.push(msg.content);
|
|
582
|
+
} else {
|
|
583
|
+
if (msg.role === "user" && systemContentBuffer.length > 0) {
|
|
584
|
+
const fullContent = `${systemContentBuffer.join("\n")}
|
|
585
|
+
|
|
586
|
+
${msg.content}`;
|
|
587
|
+
geminiMessages.push({ ...msg, content: fullContent });
|
|
588
|
+
systemContentBuffer = [];
|
|
589
|
+
} else {
|
|
590
|
+
geminiMessages.push(msg);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const contents = geminiMessages.map((msg, index) => {
|
|
595
|
+
var _a2, _b2, _c2, _d2;
|
|
596
|
+
let role = "";
|
|
597
|
+
let parts2;
|
|
598
|
+
switch (msg.role) {
|
|
599
|
+
case "user":
|
|
600
|
+
role = "user";
|
|
601
|
+
parts2 = [{ text: msg.content }];
|
|
602
|
+
if (index === geminiMessages.length - 1) {
|
|
603
|
+
let reminder = "";
|
|
604
|
+
if (options.responseFormat === "json" || ((_a2 = options.responseFormat) == null ? void 0 : _a2.type) === "json_schema" || options.responseSchema) {
|
|
605
|
+
reminder = "\n\n[SYSTEM NOTE: The output MUST be valid JSON as per the schema. Do not include markdown formatting or explanations.]";
|
|
606
|
+
} else {
|
|
607
|
+
reminder = "\n\n[SYSTEM NOTE: Please ensure your response adheres strictly to the constraints defined in the System Prompt.]";
|
|
608
|
+
}
|
|
609
|
+
const lastPart = parts2.find((p) => p.text);
|
|
610
|
+
if (lastPart) {
|
|
611
|
+
lastPart.text += reminder;
|
|
612
|
+
} else {
|
|
613
|
+
parts2.push({ text: reminder });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
617
|
+
case "assistant":
|
|
618
|
+
role = "model";
|
|
619
|
+
const isLastAssistantMessage = index === geminiMessages.map((m, i) => m.role === "assistant" ? i : -1).filter((i) => i >= 0).pop();
|
|
620
|
+
if (msg.tool_calls) {
|
|
621
|
+
parts2 = msg.tool_calls.map((tc) => {
|
|
622
|
+
const part = {
|
|
623
|
+
functionCall: { name: tc.function.name, args: tc.function.arguments || tc.function.args }
|
|
624
|
+
};
|
|
625
|
+
if (tc.thought_signature) {
|
|
626
|
+
console.log(`[GeminiProvider] Sending thought_signature in tool_call (${tc.thought_signature.length} chars)`);
|
|
627
|
+
part.thoughtSignature = tc.thought_signature;
|
|
628
|
+
}
|
|
629
|
+
return part;
|
|
630
|
+
});
|
|
631
|
+
} else {
|
|
632
|
+
const part = { text: msg.content || "" };
|
|
633
|
+
if (isLastAssistantMessage && msg.thought_signature) {
|
|
634
|
+
console.log(`[GeminiProvider] Sending thought_signature in text message (${msg.thought_signature.length} chars)`);
|
|
635
|
+
part.thoughtSignature = msg.thought_signature;
|
|
636
|
+
}
|
|
637
|
+
parts2 = [part];
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
case "tool":
|
|
641
|
+
role = "user";
|
|
642
|
+
const preceding_message = messages[index - 1];
|
|
643
|
+
const tool_call = (_b2 = preceding_message == null ? void 0 : preceding_message.tool_calls) == null ? void 0 : _b2.find((tc) => tc.id === msg.tool_call_id);
|
|
644
|
+
parts2 = [{
|
|
645
|
+
functionResponse: {
|
|
646
|
+
name: ((_c2 = tool_call == null ? void 0 : tool_call.function) == null ? void 0 : _c2.name) || "unknown_tool",
|
|
647
|
+
response: { content: msg.content }
|
|
648
|
+
}
|
|
649
|
+
}];
|
|
650
|
+
if (options.responseFormat === "json" || ((_d2 = options.responseFormat) == null ? void 0 : _d2.type) === "json_schema" || options.responseSchema) {
|
|
651
|
+
parts2.push({ text: "\n\n[SYSTEM NOTE: The output MUST be valid JSON as per the schema. Do not include markdown formatting or explanations.]" });
|
|
652
|
+
} else {
|
|
653
|
+
parts2.push({ text: "\n\n[SYSTEM NOTE: Please ensure your response adheres strictly to the constraints defined in the System Prompt.]" });
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
656
|
+
default:
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
return { role, parts: parts2 };
|
|
660
|
+
}).filter(Boolean);
|
|
661
|
+
while (contents.length > 0 && contents[0].role !== "user") {
|
|
662
|
+
contents.shift();
|
|
663
|
+
}
|
|
664
|
+
if (contents.length === 0) {
|
|
665
|
+
throw new LLMServiceException("Cannot process a conversation with no user messages.", 400);
|
|
666
|
+
}
|
|
667
|
+
const requestOptions = {
|
|
668
|
+
model: modelName,
|
|
669
|
+
contents,
|
|
670
|
+
config: generationConfig
|
|
671
|
+
};
|
|
672
|
+
if (systemPrompt) {
|
|
673
|
+
requestOptions.config.systemInstruction = { parts: [{ text: systemPrompt }] };
|
|
674
|
+
}
|
|
675
|
+
if (tools && tools.length > 0) {
|
|
676
|
+
requestOptions.config.tools = [{ functionDeclarations: tools.map((t) => t.function) }];
|
|
677
|
+
if (requestOptions.config.responseMimeType === "application/json") {
|
|
678
|
+
console.warn("[GeminiProvider] Disabling strict JSON mode because tools are present. Relying on system prompt.");
|
|
679
|
+
delete requestOptions.config.responseMimeType;
|
|
680
|
+
delete requestOptions.config.responseSchema;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
let response;
|
|
684
|
+
try {
|
|
685
|
+
response = await this.client.models.generateContent(requestOptions);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
console.error(`[GeminiProvider] generateContent failed (API Key: ${this._getMaskedApiKey()}):`, error);
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
const candidate = (_c = response.candidates) == null ? void 0 : _c[0];
|
|
691
|
+
if (!candidate) {
|
|
692
|
+
throw new LLMServiceException("No candidates returned from model", 500);
|
|
693
|
+
}
|
|
694
|
+
const parts = ((_d = candidate.content) == null ? void 0 : _d.parts) || [];
|
|
695
|
+
let textContent = "";
|
|
696
|
+
let toolCalls = null;
|
|
697
|
+
let responseThoughtSignature = null;
|
|
698
|
+
for (const part of parts) {
|
|
699
|
+
if (part.text) {
|
|
700
|
+
textContent += part.text;
|
|
701
|
+
if (part.thought_signature || part.thoughtSignature) {
|
|
702
|
+
responseThoughtSignature = part.thought_signature || part.thoughtSignature;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (part.functionCall) {
|
|
706
|
+
if (!toolCalls) toolCalls = [];
|
|
707
|
+
const sig = part.thought_signature || part.thoughtSignature;
|
|
708
|
+
if (sig) {
|
|
709
|
+
part.functionCall.thought_signature = sig;
|
|
710
|
+
if (!responseThoughtSignature) responseThoughtSignature = sig;
|
|
711
|
+
}
|
|
712
|
+
toolCalls.push(part.functionCall);
|
|
713
|
+
}
|
|
714
|
+
if (!part.text && !part.functionCall && (part.thought_signature || part.thoughtSignature)) {
|
|
715
|
+
responseThoughtSignature = part.thought_signature || part.thoughtSignature;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (!textContent && (!toolCalls || toolCalls.length === 0)) {
|
|
719
|
+
console.error("[GeminiProvider] Model returned empty response (no text, no tool calls)");
|
|
720
|
+
console.error("[GeminiProvider] Finish Reason:", candidate.finishReason);
|
|
721
|
+
console.error("[GeminiProvider] Safety Ratings:", JSON.stringify(candidate.safetyRatings, null, 2));
|
|
722
|
+
console.error("[GeminiProvider] Full Candidate:", JSON.stringify(candidate, null, 2));
|
|
723
|
+
throw new LLMServiceException(
|
|
724
|
+
`Model returned empty response. Finish Reason: ${candidate.finishReason}.`,
|
|
725
|
+
500
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const normalizedFinishReason = this.normalizeFinishReason(candidate.finishReason);
|
|
729
|
+
return {
|
|
730
|
+
content: textContent,
|
|
731
|
+
thought_signature: responseThoughtSignature,
|
|
732
|
+
// Return signature to caller
|
|
733
|
+
tool_calls: toolCalls ? (Array.isArray(toolCalls) ? toolCalls : [toolCalls]).map((fc) => ({
|
|
734
|
+
type: "function",
|
|
735
|
+
function: fc,
|
|
736
|
+
thought_signature: fc.thought_signature
|
|
737
|
+
})) : null,
|
|
738
|
+
finishReason: normalizedFinishReason,
|
|
739
|
+
// Standardized: 'completed', 'truncated', etc.
|
|
740
|
+
_rawFinishReason: candidate.finishReason,
|
|
741
|
+
// Keep original for debugging
|
|
742
|
+
_responseFormat: options.responseFormat,
|
|
743
|
+
...options.responseFormat && this._shouldAutoParse(options) ? {
|
|
744
|
+
parsedContent: this._safeJsonParse(textContent)
|
|
745
|
+
} : {}
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
_buildGenerationConfig(options, maxTokens, temperature) {
|
|
749
|
+
var _a, _b;
|
|
750
|
+
const config = {
|
|
751
|
+
temperature: (_a = options.temperature) != null ? _a : temperature,
|
|
752
|
+
maxOutputTokens: (_b = options.maxTokens) != null ? _b : maxTokens
|
|
753
|
+
};
|
|
754
|
+
if (options.responseFormat) {
|
|
755
|
+
const formatType = typeof options.responseFormat === "string" ? options.responseFormat : options.responseFormat.type;
|
|
756
|
+
const schema = typeof options.responseFormat === "object" ? options.responseFormat.schema : options.responseSchema || null;
|
|
757
|
+
if (formatType === "json" || formatType === "json_schema") {
|
|
758
|
+
config.responseMimeType = "application/json";
|
|
759
|
+
if (schema) {
|
|
760
|
+
config.responseSchema = this._convertToGeminiSchema(schema);
|
|
761
|
+
} else {
|
|
762
|
+
console.warn("[GeminiProvider] Using legacy JSON mode without schema - may produce markdown wrappers");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return config;
|
|
767
|
+
}
|
|
768
|
+
_convertToGeminiSchema(jsonSchema) {
|
|
769
|
+
const convertType = (type) => {
|
|
770
|
+
switch (type) {
|
|
771
|
+
case "string":
|
|
772
|
+
return "STRING";
|
|
773
|
+
case "number":
|
|
774
|
+
return "NUMBER";
|
|
775
|
+
case "integer":
|
|
776
|
+
return "INTEGER";
|
|
777
|
+
case "boolean":
|
|
778
|
+
return "BOOLEAN";
|
|
779
|
+
case "array":
|
|
780
|
+
return "ARRAY";
|
|
781
|
+
case "object":
|
|
782
|
+
return "OBJECT";
|
|
783
|
+
default:
|
|
784
|
+
return "STRING";
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
const convert = (schema) => {
|
|
788
|
+
const result = {
|
|
789
|
+
type: convertType(schema.type)
|
|
790
|
+
};
|
|
791
|
+
if (schema.properties) {
|
|
792
|
+
result.properties = {};
|
|
793
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
794
|
+
result.properties[key] = convert(value);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (schema.items) {
|
|
798
|
+
result.items = convert(schema.items);
|
|
799
|
+
}
|
|
800
|
+
if (schema.required) {
|
|
801
|
+
result.required = schema.required;
|
|
802
|
+
}
|
|
803
|
+
if (schema.nullable) {
|
|
804
|
+
result.nullable = schema.nullable;
|
|
805
|
+
}
|
|
806
|
+
if (schema.description) {
|
|
807
|
+
result.description = schema.description;
|
|
808
|
+
}
|
|
809
|
+
return result;
|
|
810
|
+
};
|
|
811
|
+
return convert(jsonSchema);
|
|
812
|
+
}
|
|
813
|
+
_shouldAutoParse(options) {
|
|
814
|
+
return options.autoParse !== false;
|
|
815
|
+
}
|
|
816
|
+
_safeJsonParse(content) {
|
|
817
|
+
if (!content) return null;
|
|
818
|
+
const parsed = extractJsonFromResponse(content);
|
|
819
|
+
if (!parsed) {
|
|
820
|
+
console.error("[GeminiProvider] Failed to extract valid JSON from response");
|
|
821
|
+
console.error("[GeminiProvider] Content preview:", content.substring(0, 200));
|
|
822
|
+
}
|
|
823
|
+
return parsed;
|
|
824
|
+
}
|
|
825
|
+
async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
|
|
826
|
+
const toolResults = await Promise.all(
|
|
827
|
+
tool_calls.map(async (toolCall, index) => {
|
|
828
|
+
const toolName = toolCall.function.name;
|
|
829
|
+
const tool = toolImplementations[toolName];
|
|
830
|
+
const tool_call_id = `gemini-tool-call-${index}`;
|
|
831
|
+
toolCall.id = tool_call_id;
|
|
832
|
+
if (!tool) {
|
|
833
|
+
console.error(`[Tool Error] Tool '${toolName}' not found`);
|
|
834
|
+
return { tool_call_id, output: JSON.stringify({ error: `Tool '${toolName}' not found.` }) };
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
const output = await tool(toolCall.function.args, { env, tenantId });
|
|
838
|
+
return { tool_call_id, output };
|
|
839
|
+
} catch (error) {
|
|
840
|
+
console.error(`[Tool Error] ${toolName} failed:`, error.message);
|
|
841
|
+
return { tool_call_id, output: JSON.stringify({ error: `Error executing tool '${toolName}': ${error.message}` }) };
|
|
842
|
+
}
|
|
843
|
+
})
|
|
844
|
+
);
|
|
845
|
+
toolResults.forEach((result) => messages.push({ role: "tool", tool_call_id: result.tool_call_id, content: result.output }));
|
|
846
|
+
}
|
|
847
|
+
async imageGeneration(prompt, systemPrompt, options = {}) {
|
|
848
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
849
|
+
const modelName = options.model || this.models.image || "gemini-3-pro-image-preview";
|
|
850
|
+
console.log(`[GeminiProvider] Generating image with model: ${modelName}`);
|
|
851
|
+
const generationConfig = {
|
|
852
|
+
responseModalities: ["IMAGE"]
|
|
853
|
+
};
|
|
854
|
+
if (options.aspectRatio) {
|
|
855
|
+
generationConfig.imageConfig = {
|
|
856
|
+
aspectRatio: options.aspectRatio
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const parts = [{ text: prompt }];
|
|
860
|
+
if (options.images && options.images.length > 0) {
|
|
861
|
+
options.images.forEach((img) => {
|
|
862
|
+
parts.push({
|
|
863
|
+
inlineData: {
|
|
864
|
+
data: img.data,
|
|
865
|
+
mimeType: img.mimeType
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
const requestOptions = {
|
|
871
|
+
model: modelName,
|
|
872
|
+
contents: [{
|
|
873
|
+
role: "user",
|
|
874
|
+
parts
|
|
875
|
+
}],
|
|
876
|
+
config: generationConfig
|
|
877
|
+
};
|
|
878
|
+
if (systemPrompt) {
|
|
879
|
+
requestOptions.config.systemInstruction = { parts: [{ text: systemPrompt }] };
|
|
880
|
+
}
|
|
881
|
+
const response = await this.client.models.generateContent(requestOptions);
|
|
882
|
+
const imagePart = (_d = (_c = (_b = (_a = response.candidates) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) == null ? void 0 : _c.parts) == null ? void 0 : _d.find(
|
|
883
|
+
(part) => {
|
|
884
|
+
var _a2;
|
|
885
|
+
return part.inlineData && ((_a2 = part.inlineData.mimeType) == null ? void 0 : _a2.startsWith("image/"));
|
|
886
|
+
}
|
|
887
|
+
);
|
|
888
|
+
if (!imagePart || !imagePart.inlineData) {
|
|
889
|
+
const textPart = (_h = (_g = (_f = (_e = response.candidates) == null ? void 0 : _e[0]) == null ? void 0 : _f.content) == null ? void 0 : _g.parts) == null ? void 0 : _h.find((p) => p.text);
|
|
890
|
+
const candidate = (_i = response.candidates) == null ? void 0 : _i[0];
|
|
891
|
+
console.error("[GeminiProvider] Image generation failed (no image data)");
|
|
892
|
+
if (candidate) {
|
|
893
|
+
console.error("[GeminiProvider] Finish Reason:", candidate.finishReason);
|
|
894
|
+
console.error("[GeminiProvider] Safety Ratings:", JSON.stringify(candidate.safetyRatings, null, 2));
|
|
895
|
+
console.error("[GeminiProvider] Full Candidate:", JSON.stringify(candidate, null, 2));
|
|
896
|
+
}
|
|
897
|
+
if (textPart) {
|
|
898
|
+
console.warn("[GeminiProvider] Model returned text instead of image:", textPart.text);
|
|
899
|
+
}
|
|
900
|
+
throw new Error(`No image data in response. Finish Reason: ${candidate == null ? void 0 : candidate.finishReason}`);
|
|
901
|
+
}
|
|
902
|
+
let thoughtSignature = null;
|
|
903
|
+
if (imagePart.thought_signature || imagePart.thoughtSignature) {
|
|
904
|
+
thoughtSignature = imagePart.thought_signature || imagePart.thoughtSignature;
|
|
905
|
+
} else {
|
|
906
|
+
const signaturePart = (_m = (_l = (_k = (_j = response.candidates) == null ? void 0 : _j[0]) == null ? void 0 : _k.content) == null ? void 0 : _l.parts) == null ? void 0 : _m.find((p) => p.thought_signature || p.thoughtSignature);
|
|
907
|
+
if (signaturePart) {
|
|
908
|
+
thoughtSignature = signaturePart.thought_signature || signaturePart.thoughtSignature;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (thoughtSignature && thoughtSignature.length > 5e4) {
|
|
912
|
+
console.warn(`[GeminiProvider] \u26A0\uFE0F Thought signature is abnormally large (${thoughtSignature.length} chars). Replacing with bypass token to save context.`);
|
|
913
|
+
thoughtSignature = "skip_thought_signature_validator";
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
imageData: imagePart.inlineData.data,
|
|
917
|
+
mimeType: imagePart.inlineData.mimeType,
|
|
918
|
+
thought_signature: thoughtSignature
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
_getModelForTier(tier) {
|
|
922
|
+
return this.models[tier] || this.models.default;
|
|
923
|
+
}
|
|
924
|
+
async startVideoGeneration(prompt, images, modelName, systemPrompt, options = {}) {
|
|
925
|
+
const effectivePrompt = systemPrompt ? `${systemPrompt}
|
|
926
|
+
|
|
927
|
+
${prompt}` : prompt;
|
|
928
|
+
const requestConfig = {
|
|
929
|
+
model: modelName,
|
|
930
|
+
prompt: effectivePrompt,
|
|
931
|
+
config: {
|
|
932
|
+
durationSeconds: options.durationSeconds || 6,
|
|
933
|
+
aspectRatio: options.aspectRatio || "16:9",
|
|
934
|
+
numberOfVideos: 1,
|
|
935
|
+
// Pass reference images if provided
|
|
936
|
+
...images && images.length > 0 ? { referenceImages: images } : {}
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
const logConfig = JSON.parse(JSON.stringify(requestConfig));
|
|
940
|
+
if (logConfig.config && logConfig.config.referenceImages) {
|
|
941
|
+
logConfig.config.referenceImages = logConfig.config.referenceImages.map((img) => ({
|
|
942
|
+
...img,
|
|
943
|
+
data: `... (${img.data ? img.data.length : 0} bytes)`
|
|
944
|
+
// Summarize data
|
|
945
|
+
}));
|
|
946
|
+
}
|
|
947
|
+
console.log("[GeminiProvider] startVideoGeneration request:", JSON.stringify(logConfig, null, 2));
|
|
948
|
+
try {
|
|
949
|
+
const operation = await this.client.models.generateVideos(requestConfig);
|
|
950
|
+
this._pendingOperations.set(operation.name, operation);
|
|
951
|
+
return { operationName: operation.name };
|
|
952
|
+
} catch (error) {
|
|
953
|
+
console.error(`[GeminiProvider] startVideoGeneration failed (API Key: ${this._getMaskedApiKey()}):`, error);
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async getVideoGenerationStatus(operationName) {
|
|
958
|
+
var _a, _b, _c, _d, _e, _f;
|
|
959
|
+
console.log(`[GeminiProvider] Checking status for operation: ${operationName}`);
|
|
960
|
+
let operation = this._pendingOperations.get(operationName);
|
|
961
|
+
if (!operation) {
|
|
962
|
+
operation = await this.client.models.getOperation(operationName);
|
|
963
|
+
}
|
|
964
|
+
operation = await operation.get();
|
|
965
|
+
this._pendingOperations.set(operationName, operation);
|
|
966
|
+
const result = {
|
|
967
|
+
done: operation.done,
|
|
968
|
+
progress: ((_a = operation.metadata) == null ? void 0 : _a.progressPercent) || 0,
|
|
969
|
+
state: ((_b = operation.metadata) == null ? void 0 : _b.state) || (operation.done ? "COMPLETED" : "PROCESSING")
|
|
970
|
+
};
|
|
971
|
+
console.log(`[GeminiProvider] Operation status: ${result.state}, Progress: ${result.progress}%`);
|
|
972
|
+
if (operation.done) {
|
|
973
|
+
this._pendingOperations.delete(operationName);
|
|
974
|
+
if (operation.error) {
|
|
975
|
+
console.error("[GeminiProvider] Video generation failed:", JSON.stringify(operation.error, null, 2));
|
|
976
|
+
result.error = operation.error;
|
|
977
|
+
} else {
|
|
978
|
+
const videoResult = operation.response;
|
|
979
|
+
result.videoUri = ((_d = (_c = videoResult.videos) == null ? void 0 : _c[0]) == null ? void 0 : _d.gcsUri) || videoResult.uri || ((_f = (_e = videoResult.generatedAssets) == null ? void 0 : _e[0]) == null ? void 0 : _f.uri);
|
|
980
|
+
result.content = "Video generation completed.";
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return result;
|
|
984
|
+
}
|
|
985
|
+
async startDeepResearch(prompt, options = {}) {
|
|
986
|
+
const agent = options.agent || "deep-research-pro-preview-12-2025";
|
|
987
|
+
console.log(`[GeminiProvider] Starting Deep Research with agent: ${agent}`);
|
|
988
|
+
try {
|
|
989
|
+
const interaction = await this.client.interactions.create({
|
|
990
|
+
agent,
|
|
991
|
+
input: prompt,
|
|
992
|
+
background: true,
|
|
993
|
+
// Required for long running
|
|
994
|
+
store: true
|
|
995
|
+
// Required for polling
|
|
996
|
+
});
|
|
997
|
+
console.log(`[GeminiProvider] Deep Research started. Interaction ID: ${interaction.id}`);
|
|
998
|
+
return { operationId: interaction.id };
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
console.error(`[GeminiProvider] startDeepResearch failed:`, error);
|
|
1001
|
+
throw error;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async getDeepResearchStatus(operationId) {
|
|
1005
|
+
try {
|
|
1006
|
+
const result = await this.client.interactions.get(operationId);
|
|
1007
|
+
const state = result.state;
|
|
1008
|
+
const response = {
|
|
1009
|
+
state,
|
|
1010
|
+
done: state === "completed" || state === "failed",
|
|
1011
|
+
error: state === "failed" ? result.error : null
|
|
1012
|
+
};
|
|
1013
|
+
if (state === "completed") {
|
|
1014
|
+
response.content = result.output;
|
|
1015
|
+
response.citations = result.grounding_metadata;
|
|
1016
|
+
}
|
|
1017
|
+
return response;
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
console.error(`[GeminiProvider] getDeepResearchStatus failed for ${operationId}:`, error);
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// src/llm-service.js
|
|
1026
|
+
var LLMService = class {
|
|
1027
|
+
constructor(env, toolImplementations = {}) {
|
|
1028
|
+
this.env = env;
|
|
1029
|
+
this.toolImplementations = toolImplementations;
|
|
1030
|
+
this.providerCache = /* @__PURE__ */ new Map();
|
|
1031
|
+
}
|
|
1032
|
+
async _getProvider(tenantId) {
|
|
1033
|
+
const cacheKey = tenantId || "system";
|
|
1034
|
+
if (this.providerCache.has(cacheKey)) {
|
|
1035
|
+
return this.providerCache.get(cacheKey);
|
|
1036
|
+
}
|
|
1037
|
+
const config = await ConfigManager.getConfig(tenantId, this.env);
|
|
1038
|
+
if (!config.apiKey) {
|
|
1039
|
+
throw new LLMServiceException(`LLM service is not configured for ${config.provider}. Missing API Key.`, 500);
|
|
1040
|
+
}
|
|
1041
|
+
let provider;
|
|
1042
|
+
if (config.provider === "openai") {
|
|
1043
|
+
provider = new OpenAIProvider(config);
|
|
1044
|
+
} else if (config.provider === "gemini") {
|
|
1045
|
+
provider = new GeminiProvider(config);
|
|
1046
|
+
} else {
|
|
1047
|
+
throw new LLMServiceException(`Unsupported LLM provider: ${config.provider}`, 500);
|
|
1048
|
+
}
|
|
1049
|
+
this.providerCache.set(cacheKey, provider);
|
|
1050
|
+
return provider;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Check if LLM service is configured for a tenant (or system default)
|
|
1054
|
+
*/
|
|
1055
|
+
async isConfigured(tenantId) {
|
|
1056
|
+
const config = await ConfigManager.getConfig(tenantId, this.env);
|
|
1057
|
+
return !!config.apiKey;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Simple chat interface for single-turn conversations
|
|
1061
|
+
*/
|
|
1062
|
+
async chat(userMessage, tenantId, systemPrompt = "", options = {}) {
|
|
1063
|
+
const provider = await this._getProvider(tenantId);
|
|
1064
|
+
return provider.chat(userMessage, systemPrompt, options);
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Interact with LLM for generation
|
|
1068
|
+
*
|
|
1069
|
+
* Intelligent signature detection supports:
|
|
1070
|
+
* - chatCompletion(messages, tenantId, systemPrompt)
|
|
1071
|
+
* - chatCompletion(messages, tenantId, systemPrompt, tools)
|
|
1072
|
+
* - chatCompletion(messages, tenantId, systemPrompt, options)
|
|
1073
|
+
* - chatCompletion(messages, tenantId, systemPrompt, tools, options)
|
|
1074
|
+
*
|
|
1075
|
+
* @param {Array} messages - Conversation messages
|
|
1076
|
+
* @param {string} tenantId - Tenant identifier
|
|
1077
|
+
* @param {string} systemPrompt - System instructions
|
|
1078
|
+
* @param {Array|Object} toolsOrOptions - Tools array or options object
|
|
1079
|
+
* @param {Object} optionsParam - Options object (if tools provided)
|
|
1080
|
+
* @returns {Object} Response with content, tool_calls, and optionally parsedContent
|
|
1081
|
+
*/
|
|
1082
|
+
async chatCompletion(messages, tenantId, systemPrompt, toolsOrOptions = null, optionsParam = {}) {
|
|
1083
|
+
const provider = await this._getProvider(tenantId);
|
|
1084
|
+
if (!(systemPrompt == null ? void 0 : systemPrompt.trim())) {
|
|
1085
|
+
throw new LLMServiceException("No prompt set for bot", 503);
|
|
1086
|
+
}
|
|
1087
|
+
const { tools, options } = this._parseToolsAndOptions(toolsOrOptions, optionsParam);
|
|
1088
|
+
return provider.chatCompletion(messages, systemPrompt, tools, options);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Helper to intelligently detect tools vs options parameters
|
|
1092
|
+
* @private
|
|
1093
|
+
*/
|
|
1094
|
+
_parseToolsAndOptions(param1, param2) {
|
|
1095
|
+
if (param1 === null || param1 === void 0) {
|
|
1096
|
+
return { tools: null, options: param2 || {} };
|
|
1097
|
+
}
|
|
1098
|
+
if (Array.isArray(param1)) {
|
|
1099
|
+
return { tools: param1, options: param2 || {} };
|
|
1100
|
+
}
|
|
1101
|
+
if (typeof param1 === "object") {
|
|
1102
|
+
const optionsKeys = ["responseFormat", "responseSchema", "temperature", "maxTokens", "tier", "autoParse", "strictSchema", "schemaName"];
|
|
1103
|
+
const hasOptionsKeys = optionsKeys.some((key) => key in param1);
|
|
1104
|
+
if (hasOptionsKeys) {
|
|
1105
|
+
return { tools: null, options: param1 };
|
|
1106
|
+
}
|
|
1107
|
+
return { tools: param1, options: param2 || {} };
|
|
1108
|
+
}
|
|
1109
|
+
return { tools: param1, options: param2 || {} };
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Convenience method for JSON-only responses
|
|
1113
|
+
* Automatically enables JSON mode and returns parsed object
|
|
1114
|
+
*
|
|
1115
|
+
* @param {Array} messages - Conversation messages
|
|
1116
|
+
* @param {string} tenantId - Tenant identifier
|
|
1117
|
+
* @param {string} systemPrompt - System instructions
|
|
1118
|
+
* @param {Object} schema - Optional JSON schema for validation
|
|
1119
|
+
* @param {Array} tools - Optional tools array
|
|
1120
|
+
* @returns {Object} Parsed JSON object
|
|
1121
|
+
* @throws {LLMServiceException} If JSON parsing fails
|
|
1122
|
+
*/
|
|
1123
|
+
async chatCompletionJson(messages, tenantId, systemPrompt, schema = null, tools = null) {
|
|
1124
|
+
const options = {
|
|
1125
|
+
responseFormat: schema ? { type: "json_schema", schema } : "json",
|
|
1126
|
+
autoParse: true
|
|
1127
|
+
};
|
|
1128
|
+
const response = await this.chatCompletion(messages, tenantId, systemPrompt, tools, options);
|
|
1129
|
+
if (response.parsedContent !== null && response.parsedContent !== void 0) {
|
|
1130
|
+
return response.parsedContent;
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
return JSON.parse(response.content);
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
throw new LLMServiceException(
|
|
1136
|
+
"LLM returned invalid JSON despite JSON mode being enabled",
|
|
1137
|
+
500,
|
|
1138
|
+
{ rawContent: response.content, parseError: e.message }
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async chatWithTools(messages, tenantId, systemPrompt, tools = [], options = {}) {
|
|
1143
|
+
const provider = await this._getProvider(tenantId);
|
|
1144
|
+
let currentMessages = [...messages];
|
|
1145
|
+
const MAX_ITERATIONS = 10;
|
|
1146
|
+
let iteration = 0;
|
|
1147
|
+
const initialResponse = await provider.chatCompletion(
|
|
1148
|
+
currentMessages,
|
|
1149
|
+
systemPrompt,
|
|
1150
|
+
tools,
|
|
1151
|
+
options
|
|
1152
|
+
);
|
|
1153
|
+
let { content, tool_calls, parsedContent, finishReason, _rawFinishReason } = initialResponse;
|
|
1154
|
+
while (tool_calls && iteration < MAX_ITERATIONS) {
|
|
1155
|
+
iteration++;
|
|
1156
|
+
console.log(`[Tool Call] Iteration ${iteration}/${MAX_ITERATIONS} with finish reason ${finishReason}: Assistant wants to use tools:`, tool_calls);
|
|
1157
|
+
currentMessages.push({ role: "assistant", content: content || "", tool_calls });
|
|
1158
|
+
await provider.executeTools(tool_calls, currentMessages, tenantId, this.toolImplementations, this.env);
|
|
1159
|
+
const nextResponse = await provider.chatCompletion(
|
|
1160
|
+
currentMessages,
|
|
1161
|
+
systemPrompt,
|
|
1162
|
+
tools,
|
|
1163
|
+
options
|
|
1164
|
+
);
|
|
1165
|
+
content = nextResponse.content;
|
|
1166
|
+
tool_calls = nextResponse.tool_calls;
|
|
1167
|
+
parsedContent = nextResponse.parsedContent;
|
|
1168
|
+
finishReason = nextResponse.finishReason;
|
|
1169
|
+
_rawFinishReason = nextResponse._rawFinishReason;
|
|
1170
|
+
}
|
|
1171
|
+
if (iteration >= MAX_ITERATIONS) {
|
|
1172
|
+
console.warn(`[Tool Call] Reached maximum iterations (${MAX_ITERATIONS}). Forcing completion.`);
|
|
1173
|
+
}
|
|
1174
|
+
return { content, parsedContent, toolCalls: tool_calls, finishReason, _rawFinishReason };
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Generate a video (async wrapper with polling - backward compatibility)
|
|
1178
|
+
*/
|
|
1179
|
+
async videoGeneration(prompt, images, tenantId, modelName, systemPrompt, options = {}) {
|
|
1180
|
+
const { operationName } = await this.startVideoGeneration(prompt, images, tenantId, modelName, systemPrompt, options);
|
|
1181
|
+
let status = await this.getVideoGenerationStatus(operationName, tenantId);
|
|
1182
|
+
while (!status.done) {
|
|
1183
|
+
console.log(`Waiting for video generation... Progress: ${status.progress}%`);
|
|
1184
|
+
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
|
1185
|
+
status = await this.getVideoGenerationStatus(operationName, tenantId);
|
|
1186
|
+
}
|
|
1187
|
+
if (status.error) {
|
|
1188
|
+
throw new Error(`Video generation failed: ${status.error.message || JSON.stringify(status.error)}`);
|
|
1189
|
+
}
|
|
1190
|
+
return {
|
|
1191
|
+
content: status.content || "Video generation completed.",
|
|
1192
|
+
videoUri: status.videoUri
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Start video generation (returns operation name for polling)
|
|
1197
|
+
*/
|
|
1198
|
+
async startVideoGeneration(prompt, images, tenantId, modelName, systemPrompt, options = {}) {
|
|
1199
|
+
const provider = await this._getProvider(tenantId);
|
|
1200
|
+
return provider.startVideoGeneration(prompt, images, modelName, systemPrompt, options);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Get video generation status
|
|
1204
|
+
*/
|
|
1205
|
+
async getVideoGenerationStatus(operationName, tenantId) {
|
|
1206
|
+
const provider = await this._getProvider(tenantId);
|
|
1207
|
+
return provider.getVideoGenerationStatus(operationName);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Generate an image
|
|
1211
|
+
* Falls back to system keys if tenant doesn't have image capability enabled
|
|
1212
|
+
* @param {string} prompt - Text description of the image
|
|
1213
|
+
* @param {string} tenantId - Tenant identifier
|
|
1214
|
+
* @param {string} systemPrompt - System instructions for generation
|
|
1215
|
+
* @param {Object} options - Generation options (aspectRatio, images, etc.)
|
|
1216
|
+
*/
|
|
1217
|
+
async imageGeneration(prompt, tenantId, systemPrompt, options = {}) {
|
|
1218
|
+
var _a;
|
|
1219
|
+
if (tenantId) {
|
|
1220
|
+
const config = await ConfigManager.getConfig(tenantId, this.env);
|
|
1221
|
+
if (config.isTenantOwned && !((_a = config.capabilities) == null ? void 0 : _a.image)) {
|
|
1222
|
+
console.log(`[LLMService] Tenant ${tenantId} BYOK doesn't have image capability. Using system keys.`);
|
|
1223
|
+
tenantId = null;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const provider = await this._getProvider(tenantId);
|
|
1227
|
+
return provider.imageGeneration(prompt, systemPrompt, options);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Start a Deep Research task
|
|
1231
|
+
* @param {string} prompt - Research prompt/query
|
|
1232
|
+
* @param {string} tenantId - Tenant identifier
|
|
1233
|
+
* @param {Object} options - Options (agentId, etc.)
|
|
1234
|
+
* @returns {Promise<{operationId: string}>}
|
|
1235
|
+
*/
|
|
1236
|
+
async startDeepResearch(prompt, tenantId, options = {}) {
|
|
1237
|
+
const provider = await this._getProvider(tenantId);
|
|
1238
|
+
return provider.startDeepResearch(prompt, options);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Get status of a Deep Research task
|
|
1242
|
+
* @param {string} operationId - ID returned from startDeepResearch
|
|
1243
|
+
* @param {string} tenantId - Tenant identifier
|
|
1244
|
+
* @returns {Promise<{state: string, content?: string, citations?: any[]}>}
|
|
1245
|
+
*/
|
|
1246
|
+
async getDeepResearchStatus(operationId, tenantId) {
|
|
1247
|
+
const provider = await this._getProvider(tenantId);
|
|
1248
|
+
return provider.getDeepResearchStatus(operationId);
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
var LLMServiceException = class extends Error {
|
|
1252
|
+
constructor(message, statusCode = 500, details = null) {
|
|
1253
|
+
super(message);
|
|
1254
|
+
this.name = "LLMServiceException";
|
|
1255
|
+
this.statusCode = statusCode;
|
|
1256
|
+
this.details = details || {};
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
// src/utils/error-handler.js
|
|
1261
|
+
function handleApiError(error, operation = "complete this operation", context = "api") {
|
|
1262
|
+
var _a;
|
|
1263
|
+
console.error(`[${context}] Error:`, error);
|
|
1264
|
+
const errorMessage = ((_a = error.message) == null ? void 0 : _a.toLowerCase()) || "";
|
|
1265
|
+
const errorString = JSON.stringify(error).toLowerCase();
|
|
1266
|
+
if (errorMessage.includes("overloaded") || errorMessage.includes("503") || errorString.includes("unavailable") || errorString.includes("overloaded")) {
|
|
1267
|
+
return {
|
|
1268
|
+
message: "AI service is busy. Please try again.",
|
|
1269
|
+
error: "service_overloaded",
|
|
1270
|
+
retryable: true,
|
|
1271
|
+
statusCode: 503
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
if (errorMessage.includes("quota") || errorMessage.includes("429") || errorMessage.includes("too many requests") || errorMessage.includes("rate limit") || errorString.includes("resource_exhausted")) {
|
|
1275
|
+
return {
|
|
1276
|
+
message: "Too many requests. Please try again later.",
|
|
1277
|
+
error: "rate_limited",
|
|
1278
|
+
retryable: true,
|
|
1279
|
+
statusCode: 429
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
if (errorMessage.includes("context length") || errorMessage.includes("too long") || errorString.includes("invalid_argument")) {
|
|
1283
|
+
return {
|
|
1284
|
+
message: "Content too big. Try making focused edits.",
|
|
1285
|
+
error: "input_too_long",
|
|
1286
|
+
retryable: false,
|
|
1287
|
+
statusCode: 422
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
if (errorMessage.includes("quotaerror") || errorMessage.includes("quota_exceeded")) {
|
|
1291
|
+
return {
|
|
1292
|
+
message: "You have reached your usage limit for this month. Please upgrade your plan to continue.",
|
|
1293
|
+
error: "user_quota_exceeded",
|
|
1294
|
+
retryable: false,
|
|
1295
|
+
statusCode: 402
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
if (errorMessage.includes("trial")) {
|
|
1299
|
+
return {
|
|
1300
|
+
message: "This feature is not available during the free trial. Please upgrade to use this feature.",
|
|
1301
|
+
error: "trial_limitation",
|
|
1302
|
+
retryable: false,
|
|
1303
|
+
statusCode: 402
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
if (errorMessage.includes("api key") || errorMessage.includes("authentication") || errorMessage.includes("unauthorized")) {
|
|
1307
|
+
return {
|
|
1308
|
+
message: "Service is not available at this time. Please contact support.",
|
|
1309
|
+
error: "not_configured",
|
|
1310
|
+
retryable: false,
|
|
1311
|
+
statusCode: 503
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
if (errorMessage.includes("invalid") || errorMessage.includes("bad request")) {
|
|
1315
|
+
return {
|
|
1316
|
+
message: "Invalid request. Please check your input and try again.",
|
|
1317
|
+
error: "invalid_input",
|
|
1318
|
+
retryable: false,
|
|
1319
|
+
statusCode: 400
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
message: `An error occurred while trying to ${operation}. Please try again.`,
|
|
1324
|
+
error: "operation_failed",
|
|
1325
|
+
retryable: true,
|
|
1326
|
+
statusCode: 500
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function sanitizeError(error, context = "general") {
|
|
1330
|
+
const result = handleApiError(error, "complete this operation", context);
|
|
1331
|
+
return new Error(result.error);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/transcription-service.js
|
|
1335
|
+
import OpenAI2 from "openai";
|
|
1336
|
+
var TranscriptionService = class {
|
|
1337
|
+
/**
|
|
1338
|
+
* @param {Object} config
|
|
1339
|
+
* @param {string} config.provider - 'openai' | 'generic'
|
|
1340
|
+
* @param {string} config.apiKey - API key for the provider
|
|
1341
|
+
* @param {string} [config.endpoint] - Custom endpoint URL (required for 'generic')
|
|
1342
|
+
* @param {string} [config.model] - Model to use (default: 'whisper-1')
|
|
1343
|
+
*/
|
|
1344
|
+
constructor(config) {
|
|
1345
|
+
this.provider = config.provider || "openai";
|
|
1346
|
+
this.apiKey = config.apiKey;
|
|
1347
|
+
this.endpoint = config.endpoint;
|
|
1348
|
+
this.model = config.model || "whisper-1";
|
|
1349
|
+
if (this.provider === "openai") {
|
|
1350
|
+
this.client = new OpenAI2({ apiKey: this.apiKey });
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Transcribe audio to text
|
|
1355
|
+
* @param {File|Blob} audioFile - Audio file to transcribe
|
|
1356
|
+
* @param {Object} options - Transcription options
|
|
1357
|
+
* @param {string} [options.language] - Language hint (ISO-639-1 code)
|
|
1358
|
+
* @param {string} [options.model] - Override default model
|
|
1359
|
+
* @returns {Promise<{text: string}>}
|
|
1360
|
+
*/
|
|
1361
|
+
async transcribe(audioFile, options = {}) {
|
|
1362
|
+
const model = options.model || this.model;
|
|
1363
|
+
const language = options.language;
|
|
1364
|
+
if (this.provider === "openai") {
|
|
1365
|
+
return this._transcribeOpenAI(audioFile, model, language);
|
|
1366
|
+
} else if (this.provider === "generic") {
|
|
1367
|
+
return this._transcribeGeneric(audioFile, model, language);
|
|
1368
|
+
} else {
|
|
1369
|
+
throw new TranscriptionServiceException(
|
|
1370
|
+
`Unsupported transcription provider: ${this.provider}`,
|
|
1371
|
+
400
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
async _transcribeOpenAI(audioFile, model, language) {
|
|
1376
|
+
try {
|
|
1377
|
+
const response = await this.client.audio.transcriptions.create({
|
|
1378
|
+
file: audioFile,
|
|
1379
|
+
model,
|
|
1380
|
+
language
|
|
1381
|
+
});
|
|
1382
|
+
return { text: response.text };
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
console.error("[TranscriptionService] OpenAI transcription failed:", error);
|
|
1385
|
+
throw new TranscriptionServiceException(
|
|
1386
|
+
`Transcription failed: ${error.message}`,
|
|
1387
|
+
500
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
async _transcribeGeneric(audioFile, model, language) {
|
|
1392
|
+
if (!this.endpoint) {
|
|
1393
|
+
throw new TranscriptionServiceException(
|
|
1394
|
+
"Endpoint is required for generic transcription provider",
|
|
1395
|
+
400
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
try {
|
|
1399
|
+
const formData = new FormData();
|
|
1400
|
+
formData.append("file", audioFile);
|
|
1401
|
+
formData.append("model", model);
|
|
1402
|
+
if (language) {
|
|
1403
|
+
formData.append("language", language);
|
|
1404
|
+
}
|
|
1405
|
+
const response = await fetch(this.endpoint, {
|
|
1406
|
+
method: "POST",
|
|
1407
|
+
headers: {
|
|
1408
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1409
|
+
},
|
|
1410
|
+
body: formData
|
|
1411
|
+
});
|
|
1412
|
+
if (!response.ok) {
|
|
1413
|
+
const errorText = await response.text();
|
|
1414
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
1415
|
+
}
|
|
1416
|
+
const result = await response.json();
|
|
1417
|
+
return { text: result.text };
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
console.error("[TranscriptionService] Generic transcription failed:", error);
|
|
1420
|
+
throw new TranscriptionServiceException(
|
|
1421
|
+
`Transcription failed: ${error.message}`,
|
|
1422
|
+
500
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
var TranscriptionServiceException = class extends Error {
|
|
1428
|
+
constructor(message, statusCode = 500) {
|
|
1429
|
+
super(message);
|
|
1430
|
+
this.name = "TranscriptionServiceException";
|
|
1431
|
+
this.statusCode = statusCode;
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
// src/handlers/speech-handler.js
|
|
1436
|
+
function createSpeechHandler(app, getConfig) {
|
|
1437
|
+
app.post("/transcribe", async (c) => {
|
|
1438
|
+
try {
|
|
1439
|
+
const config = await getConfig(c);
|
|
1440
|
+
if (!config || !config.apiKey) {
|
|
1441
|
+
return c.json({ error: "Transcription service not configured" }, 503);
|
|
1442
|
+
}
|
|
1443
|
+
const formData = await c.req.formData();
|
|
1444
|
+
const audioFile = formData.get("audio");
|
|
1445
|
+
if (!audioFile) {
|
|
1446
|
+
return c.json({ error: "No audio file provided" }, 400);
|
|
1447
|
+
}
|
|
1448
|
+
const language = formData.get("language") || void 0;
|
|
1449
|
+
const service = new TranscriptionService(config);
|
|
1450
|
+
const result = await service.transcribe(audioFile, { language });
|
|
1451
|
+
return c.json(result);
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
console.error("[SpeechHandler] Transcription error:", error);
|
|
1454
|
+
const statusCode = error.statusCode || 500;
|
|
1455
|
+
return c.json({ error: error.message }, statusCode);
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
return app;
|
|
1459
|
+
}
|
|
1460
|
+
export {
|
|
1461
|
+
BaseConfigProvider,
|
|
1462
|
+
ConfigManager,
|
|
1463
|
+
DefaultConfigProvider,
|
|
1464
|
+
FINISH_REASONS,
|
|
1465
|
+
GeminiProvider,
|
|
1466
|
+
LLMService,
|
|
1467
|
+
LLMServiceException,
|
|
1468
|
+
MODEL_CONFIGS,
|
|
1469
|
+
OpenAIProvider,
|
|
1470
|
+
TranscriptionService,
|
|
1471
|
+
TranscriptionServiceException,
|
|
1472
|
+
createSpeechHandler,
|
|
1473
|
+
extractJsonFromResponse,
|
|
1474
|
+
extractTextAndJson,
|
|
1475
|
+
handleApiError,
|
|
1476
|
+
sanitizeError
|
|
1477
|
+
};
|
|
1478
|
+
//# sourceMappingURL=index.js.map
|