@contentgrowth/llm-service 0.8.3 → 0.8.5

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