@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 +1527 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +529 -0
- package/dist/index.d.ts +529 -0
- package/dist/index.js +1478 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +3 -0
- package/dist/ui/react/components/index.cjs +1474 -0
- package/dist/ui/react/components/index.cjs.map +1 -0
- package/dist/ui/react/components/index.d.cts +292 -0
- package/dist/ui/react/components/index.d.ts +292 -0
- package/dist/ui/react/components/index.js +1432 -0
- package/dist/ui/react/components/index.js.map +1 -0
- package/package.json +46 -10
- package/src/index.js +0 -9
- package/src/llm/config-manager.js +0 -45
- package/src/llm/config-provider.js +0 -140
- package/src/llm/json-utils.js +0 -147
- package/src/llm/providers/base-provider.js +0 -134
- package/src/llm/providers/gemini-provider.js +0 -609
- package/src/llm/providers/openai-provider.js +0 -203
- package/src/llm-service.js +0 -281
- package/src/utils/error-handler.js +0 -117
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
|
-
import { BaseLLMProvider } from './base-provider.js';
|
|
3
|
-
import { extractJsonFromResponse } from '../json-utils.js';
|
|
4
|
-
import { LLMServiceException } from '../../llm-service.js';
|
|
5
|
-
|
|
6
|
-
export class OpenAIProvider extends BaseLLMProvider {
|
|
7
|
-
constructor(config) {
|
|
8
|
-
super(config);
|
|
9
|
-
this.client = new OpenAI({ apiKey: config.apiKey });
|
|
10
|
-
this.models = config.models;
|
|
11
|
-
this.defaultModel = config.models.default;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async chat(userMessage, systemPrompt = '', options = {}) {
|
|
15
|
-
const messages = [{ role: 'user', content: userMessage }];
|
|
16
|
-
const tier = options.tier || 'default';
|
|
17
|
-
const effectiveModel = this._getModelForTier(tier);
|
|
18
|
-
const effectiveMaxTokens = options.maxTokens || this.config.maxTokens;
|
|
19
|
-
const effectiveTemperature = options.temperature !== undefined ? options.temperature : this.config.temperature;
|
|
20
|
-
|
|
21
|
-
const response = await this._chatCompletionWithModel(
|
|
22
|
-
messages,
|
|
23
|
-
systemPrompt,
|
|
24
|
-
null,
|
|
25
|
-
effectiveModel,
|
|
26
|
-
effectiveMaxTokens,
|
|
27
|
-
effectiveTemperature
|
|
28
|
-
);
|
|
29
|
-
return { text: response.content };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async chatCompletion(messages, systemPrompt, tools = null, options = {}) {
|
|
33
|
-
return this._chatCompletionWithModel(
|
|
34
|
-
messages,
|
|
35
|
-
systemPrompt,
|
|
36
|
-
tools,
|
|
37
|
-
this.defaultModel,
|
|
38
|
-
this.config.maxTokens,
|
|
39
|
-
this.config.temperature,
|
|
40
|
-
options
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
|
|
45
|
-
const requestPayload = {
|
|
46
|
-
model: modelName,
|
|
47
|
-
temperature: options.temperature ?? temperature,
|
|
48
|
-
max_tokens: options.maxTokens ?? maxTokens,
|
|
49
|
-
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
50
|
-
tools: tools,
|
|
51
|
-
tool_choice: tools ? 'auto' : undefined,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Add JSON mode support if requested
|
|
55
|
-
if (options.responseFormat) {
|
|
56
|
-
requestPayload.response_format = this._buildResponseFormat(options);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let response;
|
|
60
|
-
try {
|
|
61
|
-
response = await this.client.chat.completions.create(requestPayload);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error(`[OpenAIProvider] chat completion failed (API Key: ${this._getMaskedApiKey()}):`, error);
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
const message = response.choices[0].message;
|
|
67
|
-
|
|
68
|
-
// Validate that we have EITHER content OR tool calls
|
|
69
|
-
if (!message.content && (!message.tool_calls || message.tool_calls.length === 0)) {
|
|
70
|
-
console.error('[OpenAIProvider] Model returned empty response (no text, no tool calls)');
|
|
71
|
-
throw new LLMServiceException(
|
|
72
|
-
'Model returned empty response. This usually means the prompt or schema is confusing the model.',
|
|
73
|
-
500
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Normalize the finish reason to standard value for consistent handling
|
|
78
|
-
const rawFinishReason = response.choices[0].finish_reason;
|
|
79
|
-
const normalizedFinishReason = this.normalizeFinishReason(rawFinishReason);
|
|
80
|
-
|
|
81
|
-
// Return with parsed JSON if applicable
|
|
82
|
-
return {
|
|
83
|
-
content: message.content,
|
|
84
|
-
tool_calls: message.tool_calls,
|
|
85
|
-
finishReason: normalizedFinishReason, // Standardized: 'completed', 'truncated', etc.
|
|
86
|
-
_rawFinishReason: rawFinishReason, // Keep original for debugging
|
|
87
|
-
// Add metadata about response format
|
|
88
|
-
_responseFormat: options.responseFormat,
|
|
89
|
-
// Auto-parse JSON if requested
|
|
90
|
-
...(options.responseFormat && this._shouldAutoParse(options) ? {
|
|
91
|
-
parsedContent: this._safeJsonParse(message.content)
|
|
92
|
-
} : {})
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
_buildResponseFormat(options) {
|
|
97
|
-
if (!options.responseFormat) {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Handle responseFormat as either string or object { type, schema }
|
|
102
|
-
const formatType = typeof options.responseFormat === 'string'
|
|
103
|
-
? options.responseFormat
|
|
104
|
-
: options.responseFormat.type;
|
|
105
|
-
|
|
106
|
-
const schema = typeof options.responseFormat === 'object'
|
|
107
|
-
? options.responseFormat.schema
|
|
108
|
-
: options.responseSchema || null;
|
|
109
|
-
|
|
110
|
-
switch (formatType) {
|
|
111
|
-
case 'json':
|
|
112
|
-
// If schema is provided, use strict mode; otherwise use legacy json_object
|
|
113
|
-
if (schema) {
|
|
114
|
-
console.log('[OpenAIProvider] Using Strict JSON mode with schema');
|
|
115
|
-
return {
|
|
116
|
-
type: 'json_schema',
|
|
117
|
-
json_schema: {
|
|
118
|
-
name: options.schemaName || 'response_schema',
|
|
119
|
-
strict: options.strictSchema ?? true,
|
|
120
|
-
schema: schema
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
} else {
|
|
124
|
-
console.warn('[OpenAIProvider] Using legacy json_object mode without schema - may produce markdown wrappers');
|
|
125
|
-
return { type: 'json_object' };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
case 'json_schema':
|
|
129
|
-
if (!schema) {
|
|
130
|
-
throw new Error('responseSchema required when using json_schema format');
|
|
131
|
-
}
|
|
132
|
-
console.log('[OpenAIProvider] Using Strict JSON mode with schema');
|
|
133
|
-
return {
|
|
134
|
-
type: 'json_schema',
|
|
135
|
-
json_schema: {
|
|
136
|
-
name: options.schemaName || 'response_schema',
|
|
137
|
-
strict: options.strictSchema ?? true,
|
|
138
|
-
schema: schema
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
default:
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
_shouldAutoParse(options) {
|
|
148
|
-
return options.autoParse !== false; // Default true
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
_safeJsonParse(content) {
|
|
152
|
-
if (!content) return null;
|
|
153
|
-
|
|
154
|
-
// Use the robust JSON extractor that handles:
|
|
155
|
-
// - Markdown code blocks (```json ... ```)
|
|
156
|
-
// - Plain JSON objects
|
|
157
|
-
// - Over-escaped content (\\\\n instead of \\n)
|
|
158
|
-
// - Brace extraction as fallback
|
|
159
|
-
const parsed = extractJsonFromResponse(content);
|
|
160
|
-
|
|
161
|
-
if (parsed) {
|
|
162
|
-
console.log('[OpenAIProvider] Successfully parsed JSON from response');
|
|
163
|
-
} else {
|
|
164
|
-
console.error('[OpenAIProvider] Failed to extract valid JSON from response');
|
|
165
|
-
console.error('[OpenAIProvider] Content preview:', content.substring(0, 200));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return parsed;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
|
|
172
|
-
const toolResults = await Promise.all(
|
|
173
|
-
tool_calls.map(async (toolCall) => {
|
|
174
|
-
const toolName = toolCall.function.name;
|
|
175
|
-
const tool = toolImplementations[toolName];
|
|
176
|
-
|
|
177
|
-
console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.arguments);
|
|
178
|
-
|
|
179
|
-
if (!tool) {
|
|
180
|
-
console.error(`[Tool Error] Tool '${toolName}' not found`);
|
|
181
|
-
return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Tool '${toolName}' not found.` }) };
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
const output = await tool(toolCall.function.arguments, { env, tenantId });
|
|
185
|
-
console.log(`[Tool Result] ${toolName} returned:`, output.substring(0, 200) + (output.length > 200 ? '...' : ''));
|
|
186
|
-
return { tool_call_id: toolCall.id, output };
|
|
187
|
-
} catch (error) {
|
|
188
|
-
console.error(`[Tool Error] ${toolName} failed:`, error.message);
|
|
189
|
-
return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Error executing tool '${toolName}': ${error.message}` }) };
|
|
190
|
-
}
|
|
191
|
-
})
|
|
192
|
-
);
|
|
193
|
-
toolResults.forEach(result => messages.push({ role: 'tool', tool_call_id: result.tool_call_id, content: result.output }));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
_getModelForTier(tier) {
|
|
197
|
-
return this.models[tier] || this.models.default;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async videoGeneration(prompt, images, modelName, systemPrompt, options = {}) {
|
|
201
|
-
throw new Error('Video generation is not supported by OpenAI provider yet.');
|
|
202
|
-
}
|
|
203
|
-
}
|
package/src/llm-service.js
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import { ConfigManager } from './llm/config-manager.js';
|
|
2
|
-
import { OpenAIProvider } from './llm/providers/openai-provider.js';
|
|
3
|
-
import { GeminiProvider } from './llm/providers/gemini-provider.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* LLM Service Module
|
|
7
|
-
*
|
|
8
|
-
* This module provides a unified interface for interacting with different LLM providers.
|
|
9
|
-
* It now supports BYOK (Bring Your Own Key) via Tenant Configuration.
|
|
10
|
-
*/
|
|
11
|
-
export class LLMService {
|
|
12
|
-
constructor(env, toolImplementations = {}) {
|
|
13
|
-
this.env = env;
|
|
14
|
-
this.toolImplementations = toolImplementations;
|
|
15
|
-
// Cache for provider instances to avoid recreation within the same request if possible
|
|
16
|
-
this.providerCache = new Map();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async _getProvider(tenantId) {
|
|
20
|
-
// Check cache first
|
|
21
|
-
const cacheKey = tenantId || 'system';
|
|
22
|
-
if (this.providerCache.has(cacheKey)) {
|
|
23
|
-
return this.providerCache.get(cacheKey);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const config = await ConfigManager.getConfig(tenantId, this.env);
|
|
27
|
-
|
|
28
|
-
if (!config.apiKey) {
|
|
29
|
-
throw new LLMServiceException(`LLM service is not configured for ${config.provider}. Missing API Key.`, 500);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let provider;
|
|
33
|
-
if (config.provider === 'openai') {
|
|
34
|
-
provider = new OpenAIProvider(config);
|
|
35
|
-
} else if (config.provider === 'gemini') {
|
|
36
|
-
provider = new GeminiProvider(config);
|
|
37
|
-
} else {
|
|
38
|
-
throw new LLMServiceException(`Unsupported LLM provider: ${config.provider}`, 500);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
this.providerCache.set(cacheKey, provider);
|
|
42
|
-
return provider;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Check if LLM service is configured for a tenant (or system default)
|
|
47
|
-
*/
|
|
48
|
-
async isConfigured(tenantId) {
|
|
49
|
-
const config = await ConfigManager.getConfig(tenantId, this.env);
|
|
50
|
-
return !!config.apiKey;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Simple chat interface for single-turn conversations
|
|
55
|
-
*/
|
|
56
|
-
async chat(userMessage, tenantId, systemPrompt = '', options = {}) {
|
|
57
|
-
const provider = await this._getProvider(tenantId);
|
|
58
|
-
return provider.chat(userMessage, systemPrompt, options);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Interact with LLM for generation
|
|
63
|
-
*
|
|
64
|
-
* Intelligent signature detection supports:
|
|
65
|
-
* - chatCompletion(messages, tenantId, systemPrompt)
|
|
66
|
-
* - chatCompletion(messages, tenantId, systemPrompt, tools)
|
|
67
|
-
* - chatCompletion(messages, tenantId, systemPrompt, options)
|
|
68
|
-
* - chatCompletion(messages, tenantId, systemPrompt, tools, options)
|
|
69
|
-
*
|
|
70
|
-
* @param {Array} messages - Conversation messages
|
|
71
|
-
* @param {string} tenantId - Tenant identifier
|
|
72
|
-
* @param {string} systemPrompt - System instructions
|
|
73
|
-
* @param {Array|Object} toolsOrOptions - Tools array or options object
|
|
74
|
-
* @param {Object} optionsParam - Options object (if tools provided)
|
|
75
|
-
* @returns {Object} Response with content, tool_calls, and optionally parsedContent
|
|
76
|
-
*/
|
|
77
|
-
async chatCompletion(messages, tenantId, systemPrompt, toolsOrOptions = null, optionsParam = {}) {
|
|
78
|
-
const provider = await this._getProvider(tenantId);
|
|
79
|
-
|
|
80
|
-
if (!systemPrompt?.trim()) {
|
|
81
|
-
throw new LLMServiceException('No prompt set for bot', 503);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Intelligent parameter detection
|
|
85
|
-
const { tools, options } = this._parseToolsAndOptions(toolsOrOptions, optionsParam);
|
|
86
|
-
|
|
87
|
-
return provider.chatCompletion(messages, systemPrompt, tools, options);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Helper to intelligently detect tools vs options parameters
|
|
92
|
-
* @private
|
|
93
|
-
*/
|
|
94
|
-
_parseToolsAndOptions(param1, param2) {
|
|
95
|
-
// If param1 is null/undefined, use defaults
|
|
96
|
-
if (param1 === null || param1 === undefined) {
|
|
97
|
-
return { tools: null, options: param2 || {} };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// If param1 is an array, it's tools
|
|
101
|
-
if (Array.isArray(param1)) {
|
|
102
|
-
return { tools: param1, options: param2 || {} };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// If param1 is an object, check if it looks like options or tools
|
|
106
|
-
if (typeof param1 === 'object') {
|
|
107
|
-
// Check for known options keys
|
|
108
|
-
const optionsKeys = ['responseFormat', 'responseSchema', 'temperature', 'maxTokens', 'tier', 'autoParse', 'strictSchema', 'schemaName'];
|
|
109
|
-
const hasOptionsKeys = optionsKeys.some(key => key in param1);
|
|
110
|
-
|
|
111
|
-
if (hasOptionsKeys) {
|
|
112
|
-
// param1 is options
|
|
113
|
-
return { tools: null, options: param1 };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Could be tools in object format (legacy support)
|
|
117
|
-
// Treat as tools if it doesn't have options keys
|
|
118
|
-
return { tools: param1, options: param2 || {} };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Default: treat as tools
|
|
122
|
-
return { tools: param1, options: param2 || {} };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Convenience method for JSON-only responses
|
|
127
|
-
* Automatically enables JSON mode and returns parsed object
|
|
128
|
-
*
|
|
129
|
-
* @param {Array} messages - Conversation messages
|
|
130
|
-
* @param {string} tenantId - Tenant identifier
|
|
131
|
-
* @param {string} systemPrompt - System instructions
|
|
132
|
-
* @param {Object} schema - Optional JSON schema for validation
|
|
133
|
-
* @param {Array} tools - Optional tools array
|
|
134
|
-
* @returns {Object} Parsed JSON object
|
|
135
|
-
* @throws {LLMServiceException} If JSON parsing fails
|
|
136
|
-
*/
|
|
137
|
-
async chatCompletionJson(messages, tenantId, systemPrompt, schema = null, tools = null) {
|
|
138
|
-
const options = {
|
|
139
|
-
responseFormat: schema ? { type: 'json_schema', schema } : 'json',
|
|
140
|
-
autoParse: true
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const response = await this.chatCompletion(messages, tenantId, systemPrompt, tools, options);
|
|
144
|
-
|
|
145
|
-
// Return parsed content directly or throw if parsing failed
|
|
146
|
-
if (response.parsedContent !== null && response.parsedContent !== undefined) {
|
|
147
|
-
return response.parsedContent;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Fallback: try to parse the raw content
|
|
151
|
-
try {
|
|
152
|
-
return JSON.parse(response.content);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
throw new LLMServiceException(
|
|
155
|
-
'LLM returned invalid JSON despite JSON mode being enabled',
|
|
156
|
-
500,
|
|
157
|
-
{ rawContent: response.content, parseError: e.message }
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async chatWithTools(messages, tenantId, systemPrompt, tools = [], options = {}) {
|
|
163
|
-
const provider = await this._getProvider(tenantId);
|
|
164
|
-
|
|
165
|
-
let currentMessages = [...messages];
|
|
166
|
-
const MAX_ITERATIONS = 10; // Prevent infinite loops
|
|
167
|
-
let iteration = 0;
|
|
168
|
-
|
|
169
|
-
// Initial call
|
|
170
|
-
const initialResponse = await provider.chatCompletion(
|
|
171
|
-
currentMessages,
|
|
172
|
-
systemPrompt,
|
|
173
|
-
tools,
|
|
174
|
-
options
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
let { content, tool_calls, parsedContent, finishReason, _rawFinishReason } = initialResponse;
|
|
178
|
-
|
|
179
|
-
// Tool execution loop with safety limit
|
|
180
|
-
while (tool_calls && iteration < MAX_ITERATIONS) {
|
|
181
|
-
iteration++;
|
|
182
|
-
console.log(`[Tool Call] Iteration ${iteration}/${MAX_ITERATIONS} with finish reason ${finishReason}: Assistant wants to use tools:`, tool_calls);
|
|
183
|
-
currentMessages.push({ role: 'assistant', content: content || '', tool_calls });
|
|
184
|
-
|
|
185
|
-
// Execute tools using the provider's helper (which formats results for that provider)
|
|
186
|
-
await provider.executeTools(tool_calls, currentMessages, tenantId, this.toolImplementations, this.env);
|
|
187
|
-
|
|
188
|
-
// Next call
|
|
189
|
-
const nextResponse = await provider.chatCompletion(
|
|
190
|
-
currentMessages,
|
|
191
|
-
systemPrompt,
|
|
192
|
-
tools,
|
|
193
|
-
options
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
content = nextResponse.content;
|
|
197
|
-
tool_calls = nextResponse.tool_calls;
|
|
198
|
-
parsedContent = nextResponse.parsedContent; // Preserve parsedContent from final response
|
|
199
|
-
finishReason = nextResponse.finishReason; // Preserve finishReason from final response
|
|
200
|
-
_rawFinishReason = nextResponse._rawFinishReason; // Preserve raw finish reason for debugging
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (iteration >= MAX_ITERATIONS) {
|
|
204
|
-
console.warn(`[Tool Call] Reached maximum iterations (${MAX_ITERATIONS}). Forcing completion.`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Return both content and parsedContent (if available)
|
|
208
|
-
return { content, parsedContent, toolCalls: tool_calls, finishReason, _rawFinishReason };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Generate a video (async wrapper with polling - backward compatibility)
|
|
213
|
-
*/
|
|
214
|
-
async videoGeneration(prompt, images, tenantId, modelName, systemPrompt, options = {}) {
|
|
215
|
-
const { operationName } = await this.startVideoGeneration(prompt, images, tenantId, modelName, systemPrompt, options);
|
|
216
|
-
|
|
217
|
-
let status = await this.getVideoGenerationStatus(operationName, tenantId);
|
|
218
|
-
|
|
219
|
-
while (!status.done) {
|
|
220
|
-
console.log(`Waiting for video generation... Progress: ${status.progress}%`);
|
|
221
|
-
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
|
|
222
|
-
status = await this.getVideoGenerationStatus(operationName, tenantId);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (status.error) {
|
|
226
|
-
throw new Error(`Video generation failed: ${status.error.message || JSON.stringify(status.error)}`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
content: status.content || "Video generation completed.",
|
|
231
|
-
videoUri: status.videoUri
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Start video generation (returns operation name for polling)
|
|
237
|
-
*/
|
|
238
|
-
async startVideoGeneration(prompt, images, tenantId, modelName, systemPrompt, options = {}) {
|
|
239
|
-
const provider = await this._getProvider(tenantId);
|
|
240
|
-
return provider.startVideoGeneration(prompt, images, modelName, systemPrompt, options);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Get video generation status
|
|
245
|
-
*/
|
|
246
|
-
async getVideoGenerationStatus(operationName, tenantId) {
|
|
247
|
-
const provider = await this._getProvider(tenantId);
|
|
248
|
-
return provider.getVideoGenerationStatus(operationName);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Generate an image
|
|
253
|
-
* Falls back to system keys if tenant doesn't have image capability enabled
|
|
254
|
-
* @param {string} prompt - Text description of the image
|
|
255
|
-
* @param {string} tenantId - Tenant identifier
|
|
256
|
-
* @param {string} systemPrompt - System instructions for generation
|
|
257
|
-
* @param {Object} options - Generation options (aspectRatio, images, etc.)
|
|
258
|
-
*/
|
|
259
|
-
async imageGeneration(prompt, tenantId, systemPrompt, options = {}) {
|
|
260
|
-
// Check if tenant has image capability enabled
|
|
261
|
-
if (tenantId) {
|
|
262
|
-
const config = await ConfigManager.getConfig(tenantId, this.env);
|
|
263
|
-
if (config.isTenantOwned && !config.capabilities?.image) {
|
|
264
|
-
console.log(`[LLMService] Tenant ${tenantId} BYOK doesn't have image capability. Using system keys.`);
|
|
265
|
-
tenantId = null; // Fall back to system
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const provider = await this._getProvider(tenantId);
|
|
270
|
-
return provider.imageGeneration(prompt, systemPrompt, options);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export class LLMServiceException extends Error {
|
|
275
|
-
constructor(message, statusCode = 500, details = null) {
|
|
276
|
-
super(message);
|
|
277
|
-
this.name = 'LLMServiceException';
|
|
278
|
-
this.statusCode = statusCode;
|
|
279
|
-
this.details = details || {};
|
|
280
|
-
}
|
|
281
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Error Handling Utility for LLM Service
|
|
3
|
-
* Provides centralized error parsing and user-friendly message generation.
|
|
4
|
-
* Returns plain objects - framework-specific response handling is done by consumers.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Parse an error and return a standardized error response object.
|
|
9
|
-
* Detects specific error types like service overload, rate limits, and input issues.
|
|
10
|
-
*
|
|
11
|
-
* @param {Error} error - The caught error
|
|
12
|
-
* @param {string} operation - The operation being performed (e.g., 'generate image', 'edit article')
|
|
13
|
-
* @param {string} context - Context for logging (e.g., 'image_generation', 'ai_edit')
|
|
14
|
-
* @returns {{ message: string, error: string, retryable: boolean, statusCode: number }}
|
|
15
|
-
*/
|
|
16
|
-
export function handleApiError(error, operation = 'complete this operation', context = 'api') {
|
|
17
|
-
console.error(`[${context}] Error:`, error);
|
|
18
|
-
|
|
19
|
-
const errorMessage = error.message?.toLowerCase() || '';
|
|
20
|
-
const errorString = JSON.stringify(error).toLowerCase();
|
|
21
|
-
|
|
22
|
-
// Check for model overload (503)
|
|
23
|
-
if (errorMessage.includes('overloaded') || errorMessage.includes('503') ||
|
|
24
|
-
errorString.includes('unavailable') || errorString.includes('overloaded')) {
|
|
25
|
-
return {
|
|
26
|
-
message: 'AI service is busy. Please try again.',
|
|
27
|
-
error: 'service_overloaded',
|
|
28
|
-
retryable: true,
|
|
29
|
-
statusCode: 503
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check for rate limiting (429)
|
|
34
|
-
if (errorMessage.includes('quota') || errorMessage.includes('429') ||
|
|
35
|
-
errorMessage.includes('too many requests') || errorMessage.includes('rate limit') ||
|
|
36
|
-
errorString.includes('resource_exhausted')) {
|
|
37
|
-
return {
|
|
38
|
-
message: 'Too many requests. Please try again later.',
|
|
39
|
-
error: 'rate_limited',
|
|
40
|
-
retryable: true,
|
|
41
|
-
statusCode: 429
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Check for context length / input too long
|
|
46
|
-
if (errorMessage.includes('context length') || errorMessage.includes('too long') ||
|
|
47
|
-
errorString.includes('invalid_argument')) {
|
|
48
|
-
return {
|
|
49
|
-
message: 'Content too big. Try making focused edits.',
|
|
50
|
-
error: 'input_too_long',
|
|
51
|
-
retryable: false,
|
|
52
|
-
statusCode: 422
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check for user quota exceeded (from our own quota system)
|
|
57
|
-
if (errorMessage.includes('quotaerror') || errorMessage.includes('quota_exceeded')) {
|
|
58
|
-
return {
|
|
59
|
-
message: 'You have reached your usage limit for this month. Please upgrade your plan to continue.',
|
|
60
|
-
error: 'user_quota_exceeded',
|
|
61
|
-
retryable: false,
|
|
62
|
-
statusCode: 402
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check for trial limitation errors
|
|
67
|
-
if (errorMessage.includes('trial')) {
|
|
68
|
-
return {
|
|
69
|
-
message: 'This feature is not available during the free trial. Please upgrade to use this feature.',
|
|
70
|
-
error: 'trial_limitation',
|
|
71
|
-
retryable: false,
|
|
72
|
-
statusCode: 402
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Check for authentication/configuration errors
|
|
77
|
-
if (errorMessage.includes('api key') || errorMessage.includes('authentication') || errorMessage.includes('unauthorized')) {
|
|
78
|
-
return {
|
|
79
|
-
message: 'Service is not available at this time. Please contact support.',
|
|
80
|
-
error: 'not_configured',
|
|
81
|
-
retryable: false,
|
|
82
|
-
statusCode: 503
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Check for invalid input errors
|
|
87
|
-
if (errorMessage.includes('invalid') || errorMessage.includes('bad request')) {
|
|
88
|
-
return {
|
|
89
|
-
message: 'Invalid request. Please check your input and try again.',
|
|
90
|
-
error: 'invalid_input',
|
|
91
|
-
retryable: false,
|
|
92
|
-
statusCode: 400
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Default error
|
|
97
|
-
return {
|
|
98
|
-
message: `An error occurred while trying to ${operation}. Please try again.`,
|
|
99
|
-
error: 'operation_failed',
|
|
100
|
-
retryable: true,
|
|
101
|
-
statusCode: 500
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Sanitize error messages to prevent leaking technical details.
|
|
107
|
-
* Returns an Error with a clean error code that can be handled by the API layer.
|
|
108
|
-
* Use this when you want to throw an error rather than return a JSON response.
|
|
109
|
-
*
|
|
110
|
-
* @param {Error} error - The original error
|
|
111
|
-
* @param {string} context - Context of where error occurred (e.g., 'image_generation', 'ai_edit')
|
|
112
|
-
* @returns {Error} - Sanitized error with clean message code
|
|
113
|
-
*/
|
|
114
|
-
export function sanitizeError(error, context = 'general') {
|
|
115
|
-
const result = handleApiError(error, 'complete this operation', context);
|
|
116
|
-
return new Error(result.error);
|
|
117
|
-
}
|