@contentgrowth/llm-service 0.8.3 → 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.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