@contentgrowth/llm-service 1.2.4 → 1.3.1

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 CHANGED
@@ -74,6 +74,7 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
74
74
  models: MODEL_CONFIGS[tenantConfig.provider],
75
75
  // for backward compatibility, api_key is deprecated and shall never be used!!
76
76
  apiKey: tenantConfig.apiKey || tenantConfig.api_key,
77
+ baseURL: tenantConfig.baseURL || tenantConfig.base_url || void 0,
77
78
  project: tenantConfig.project,
78
79
  location: tenantConfig.location,
79
80
  temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
@@ -87,9 +88,11 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
87
88
  const provider = ((_a = env.LLM_PROVIDER) == null ? void 0 : _a.toLowerCase()) || "openai";
88
89
  const providerDefaults = MODEL_CONFIGS[provider] || MODEL_CONFIGS["openai"];
89
90
  let apiKey;
91
+ let baseURL;
90
92
  let models = { ...providerDefaults };
91
93
  if (provider === "openai") {
92
94
  apiKey = env.OPENAI_API_KEY;
95
+ baseURL = env.OPENAI_BASE_URL || env.LLM_BASE_URL || void 0;
93
96
  models = {
94
97
  default: env.OPENAI_MODEL || providerDefaults.default,
95
98
  edge: env.OPENAI_MODEL_EDGE || providerDefaults.edge,
@@ -99,6 +102,7 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
99
102
  };
100
103
  } else if (provider === "gemini") {
101
104
  apiKey = env.GEMINI_API_KEY;
105
+ baseURL = env.GEMINI_BASE_URL || env.LLM_BASE_URL || void 0;
102
106
  models = {
103
107
  default: env.GEMINI_MODEL || providerDefaults.default,
104
108
  edge: env.GEMINI_MODEL_EDGE || providerDefaults.edge,
@@ -108,8 +112,19 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
108
112
  image: env.GEMINI_IMAGE_MODEL || providerDefaults.image,
109
113
  video: env.GEMINI_VIDEO_MODEL || providerDefaults.video
110
114
  };
115
+ } else if (provider === "anthropic") {
116
+ apiKey = env.ANTHROPIC_API_KEY;
117
+ baseURL = env.ANTHROPIC_BASE_URL || env.LLM_BASE_URL || void 0;
118
+ models = {
119
+ default: env.ANTHROPIC_MODEL || providerDefaults.default,
120
+ edge: env.ANTHROPIC_MODEL_EDGE || providerDefaults.edge,
121
+ fast: env.ANTHROPIC_MODEL_FAST || providerDefaults.fast,
122
+ cost: env.ANTHROPIC_MODEL_COST || providerDefaults.cost,
123
+ free: env.ANTHROPIC_MODEL_FREE || providerDefaults.free
124
+ };
111
125
  } else if (provider === "vertex") {
112
126
  apiKey = env.VERTEX_API_KEY;
127
+ baseURL = env.VERTEX_BASE_URL || env.LLM_BASE_URL || void 0;
113
128
  const project = env.VERTEX_PROJECT || env.GOOGLE_CLOUD_PROJECT;
114
129
  const location = env.VERTEX_LOCATION || env.GOOGLE_CLOUD_LOCATION || "us-central1";
115
130
  models = {
@@ -124,6 +139,7 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
124
139
  return {
125
140
  provider,
126
141
  apiKey,
142
+ baseURL,
127
143
  project,
128
144
  location,
129
145
  models,
@@ -134,6 +150,7 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
134
150
  return {
135
151
  provider,
136
152
  apiKey,
153
+ baseURL,
137
154
  models,
138
155
  temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
139
156
  maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "65536")
@@ -397,7 +414,11 @@ function extractTextAndJson(input) {
397
414
  var OpenAIProvider = class extends BaseLLMProvider {
398
415
  constructor(config) {
399
416
  super(config);
400
- this.client = new OpenAI({ apiKey: config.apiKey });
417
+ const clientOptions = { apiKey: config.apiKey };
418
+ if (config.baseURL) {
419
+ clientOptions.baseURL = config.baseURL;
420
+ }
421
+ this.client = new OpenAI(clientOptions);
401
422
  this.models = config.models;
402
423
  this.defaultModel = config.models.default;
403
424
  }
@@ -579,11 +600,13 @@ var GoogleProvider = class extends BaseLLMProvider {
579
600
  this.models = config.models;
580
601
  this.defaultModel = config.models.default;
581
602
  this._pendingOperations = /* @__PURE__ */ new Map();
603
+ const httpOptions = config.baseURL ? { baseUrl: config.baseURL } : void 0;
582
604
  if (config.provider === "vertex") {
583
605
  if (config.apiKey) {
584
606
  this.client = new GoogleGenAI({
585
607
  vertexai: true,
586
- apiKey: config.apiKey
608
+ apiKey: config.apiKey,
609
+ ...httpOptions ? { httpOptions } : {}
587
610
  });
588
611
  } else {
589
612
  if (!config.project) {
@@ -592,12 +615,14 @@ var GoogleProvider = class extends BaseLLMProvider {
592
615
  this.client = new GoogleGenAI({
593
616
  vertexai: true,
594
617
  project: config.project,
595
- location: config.location || "us-central1"
618
+ location: config.location || "us-central1",
619
+ ...httpOptions ? { httpOptions } : {}
596
620
  });
597
621
  }
598
622
  } else {
599
623
  this.client = new GoogleGenAI({
600
- apiKey: config.apiKey
624
+ apiKey: config.apiKey,
625
+ ...httpOptions ? { httpOptions } : {}
601
626
  });
602
627
  }
603
628
  }
@@ -1146,14 +1171,66 @@ ${prompt}` : prompt;
1146
1171
  throw error;
1147
1172
  }
1148
1173
  }
1174
+ /**
1175
+ * Text-to-Speech synthesis using Gemini TTS models
1176
+ * @param {string} text - Text to synthesize
1177
+ * @param {Object} options - TTS options
1178
+ * @param {string} options.voice - Voice name (default: 'Aoede')
1179
+ * @param {string} options.model - Model to use (default: from config)
1180
+ * @returns {Promise<Buffer>} Audio data as Buffer
1181
+ */
1182
+ async textToSpeech(text, options = {}) {
1183
+ var _a, _b, _c, _d, _e;
1184
+ const voice = options.voice || "Aoede";
1185
+ const modelName = options.model || this.models.tts || "gemini-3.1-flash-tts-preview";
1186
+ const requestOptions = {
1187
+ model: modelName,
1188
+ contents: [{
1189
+ role: "user",
1190
+ parts: [{ text }]
1191
+ }],
1192
+ config: {
1193
+ responseModalities: ["AUDIO"],
1194
+ speechConfig: {
1195
+ voiceConfig: {
1196
+ prebuiltVoiceConfig: {
1197
+ voiceName: voice
1198
+ }
1199
+ }
1200
+ }
1201
+ }
1202
+ };
1203
+ try {
1204
+ const response = await this._generateContent(requestOptions);
1205
+ const candidate = (_a = response.candidates) == null ? void 0 : _a[0];
1206
+ if (!candidate) {
1207
+ throw new LLMServiceException("No candidates returned from TTS model", 500);
1208
+ }
1209
+ const audioPart = (_c = (_b = candidate.content) == null ? void 0 : _b.parts) == null ? void 0 : _c.find((p) => p.inlineData || p.inline_data);
1210
+ const audioB64 = ((_d = audioPart == null ? void 0 : audioPart.inlineData) == null ? void 0 : _d.data) || ((_e = audioPart == null ? void 0 : audioPart.inline_data) == null ? void 0 : _e.data);
1211
+ if (!audioB64) {
1212
+ throw new LLMServiceException("No audio data returned from TTS model", 500);
1213
+ }
1214
+ return Buffer.from(audioB64, "base64");
1215
+ } catch (error) {
1216
+ console.error(`[${this.constructor.name}] textToSpeech failed (API Key: ${this._getMaskedApiKey()}):`, error);
1217
+ throw error;
1218
+ }
1219
+ }
1149
1220
  };
1150
1221
 
1151
1222
  // src/llm/providers/anthropic-provider.js
1223
+ import Anthropic from "@anthropic-ai/sdk";
1152
1224
  var AnthropicProvider = class extends BaseLLMProvider {
1153
1225
  constructor(config) {
1154
1226
  super(config);
1155
1227
  this.models = config.models || {};
1156
1228
  this.defaultModel = this.models.default || "claude-3-5-sonnet-20241022";
1229
+ const clientOptions = { apiKey: config.apiKey || "" };
1230
+ if (config.baseURL) {
1231
+ clientOptions.baseURL = config.baseURL;
1232
+ }
1233
+ this.client = new Anthropic(clientOptions);
1157
1234
  }
1158
1235
  async chat(userMessage, systemPrompt = "", options = {}) {
1159
1236
  var _a;
@@ -1190,82 +1267,69 @@ var AnthropicProvider = class extends BaseLLMProvider {
1190
1267
  );
1191
1268
  }
1192
1269
  async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
1193
- const baseURL = this.config.baseURL || "https://api.anthropic.com";
1194
- const url = `${baseURL.replace(/\/$/, "")}/v1/messages`;
1195
- const formattedMessages = messages.map((m) => ({
1196
- role: m.role === "assistant" ? "assistant" : "user",
1197
- content: m.content
1198
- }));
1199
- const headers = {
1200
- "Content-Type": "application/json",
1201
- "x-api-key": this.config.apiKey || "",
1202
- "anthropic-version": "2023-06-01"
1203
- };
1204
- const payload = {
1205
- model: modelName,
1206
- messages: formattedMessages,
1207
- max_tokens: maxTokens,
1208
- temperature
1209
- };
1210
- if (systemPrompt) {
1211
- payload.system = systemPrompt;
1212
- }
1270
+ var _a, _b, _c, _d;
1271
+ let effectiveSystem = systemPrompt || void 0;
1213
1272
  if (options.responseFormat) {
1214
1273
  const formatType = typeof options.responseFormat === "string" ? options.responseFormat : options.responseFormat.type;
1215
1274
  if (formatType === "json" || formatType === "json_schema") {
1216
- const schemaText = options.responseFormat.schema || options.responseSchema ? `
1275
+ const schema = options.responseFormat.schema || options.responseSchema;
1276
+ const schemaText = schema ? `
1217
1277
 
1218
- Your output must comply strictly with this JSON Schema: ${JSON.stringify(options.responseFormat.schema || options.responseSchema)}` : "";
1278
+ Your output must comply strictly with this JSON Schema: ${JSON.stringify(schema)}` : "";
1219
1279
  const jsonPrompt = `IMPORTANT: You must respond ONLY in raw JSON format. Do not write any conversational text, explanations, or markdown code blocks (e.g. do NOT wrap your answer in \`\`\`json ... \`\`\`). Your response must start with '{' and end with '}'.${schemaText}`;
1220
- if (payload.system) {
1221
- payload.system = `${payload.system}
1280
+ effectiveSystem = effectiveSystem ? `${effectiveSystem}
1222
1281
 
1223
- ${jsonPrompt}`;
1224
- } else {
1225
- payload.system = jsonPrompt;
1226
- }
1282
+ ${jsonPrompt}` : jsonPrompt;
1227
1283
  }
1228
1284
  }
1229
- let responseText;
1230
- let responseData;
1285
+ const anthropicMessages = this._formatMessages(messages);
1286
+ const anthropicTools = tools ? this._formatTools(tools) : void 0;
1287
+ const requestParams = {
1288
+ model: modelName,
1289
+ max_tokens: maxTokens,
1290
+ temperature,
1291
+ messages: anthropicMessages,
1292
+ ...effectiveSystem ? { system: effectiveSystem } : {},
1293
+ ...anthropicTools ? { tools: anthropicTools } : {}
1294
+ };
1295
+ let response;
1231
1296
  try {
1232
- const res = await fetch(url, {
1233
- method: "POST",
1234
- headers,
1235
- body: JSON.stringify(payload)
1236
- });
1237
- responseText = await res.text();
1238
- if (!res.ok) {
1239
- console.error(`[AnthropicProvider] API request failed with status ${res.status}:`, responseText);
1240
- throw new LLMServiceException(`Anthropic API Error: ${responseText}`, res.status);
1241
- }
1242
- responseData = JSON.parse(responseText);
1297
+ response = await this.client.messages.create(requestParams);
1243
1298
  } catch (error) {
1244
1299
  console.error(`[AnthropicProvider] request failed (API Key: ${this._getMaskedApiKey()}):`, error);
1245
1300
  throw error;
1246
1301
  }
1247
- const contentBlock = responseData.content && responseData.content[0];
1248
- const contentText = contentBlock ? contentBlock.text : "";
1249
- if (!contentText) {
1250
- console.error("[AnthropicProvider] Model returned empty response content");
1302
+ const textBlocks = response.content.filter((b) => b.type === "text");
1303
+ const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
1304
+ const contentText = textBlocks.map((b) => b.text).join("");
1305
+ if (!contentText && toolUseBlocks.length === 0) {
1306
+ console.error("[AnthropicProvider] Model returned empty response (no text, no tool calls)");
1251
1307
  throw new LLMServiceException(
1252
1308
  "Model returned empty response. This usually means the prompt or schema is confusing the model.",
1253
1309
  500
1254
1310
  );
1255
1311
  }
1256
- const rawFinishReason = responseData.stop_reason;
1312
+ const toolCalls = toolUseBlocks.length > 0 ? toolUseBlocks.map((block) => ({
1313
+ id: block.id,
1314
+ type: "function",
1315
+ function: {
1316
+ name: block.name,
1317
+ // Anthropic returns args as a parsed object; serialize to string to match OpenAI shape
1318
+ arguments: JSON.stringify(block.input || {})
1319
+ }
1320
+ })) : null;
1321
+ const rawFinishReason = response.stop_reason;
1257
1322
  const normalizedFinishReason = this.normalizeFinishReason(rawFinishReason);
1258
1323
  const result = {
1259
1324
  content: contentText,
1260
- tool_calls: null,
1261
- // REST client tool calls mapping not required for playbook studio
1325
+ tool_calls: toolCalls,
1262
1326
  finishReason: normalizedFinishReason,
1263
1327
  _rawFinishReason: rawFinishReason,
1264
1328
  _responseFormat: options.responseFormat,
1265
1329
  usage: {
1266
- prompt_tokens: responseData.usage ? responseData.usage.input_tokens : 0,
1267
- completion_tokens: responseData.usage ? responseData.usage.output_tokens : 0,
1268
- total_tokens: responseData.usage ? responseData.usage.input_tokens + responseData.usage.output_tokens : 0
1330
+ prompt_tokens: ((_a = response.usage) == null ? void 0 : _a.input_tokens) || 0,
1331
+ completion_tokens: ((_b = response.usage) == null ? void 0 : _b.output_tokens) || 0,
1332
+ total_tokens: (((_c = response.usage) == null ? void 0 : _c.input_tokens) || 0) + (((_d = response.usage) == null ? void 0 : _d.output_tokens) || 0)
1269
1333
  },
1270
1334
  model: modelName
1271
1335
  };
@@ -1274,11 +1338,110 @@ ${jsonPrompt}`;
1274
1338
  }
1275
1339
  return result;
1276
1340
  }
1277
- _getModelForTier(tier) {
1278
- if (this.models[tier]) {
1279
- return this.models[tier];
1341
+ /**
1342
+ * Convert standard {role, content, tool_calls, tool_call_id} messages
1343
+ * to Anthropic's message format.
1344
+ *
1345
+ * Key differences from OpenAI:
1346
+ * - No 'system' role in messages array (handled via top-level `system` param)
1347
+ * - Tool results go as role:'user' with content type:'tool_result'
1348
+ * - Assistant messages with tool calls use an array of content blocks
1349
+ * - Roles must strictly alternate user/assistant
1350
+ */
1351
+ _formatMessages(messages) {
1352
+ const result = [];
1353
+ for (const msg of messages) {
1354
+ if (msg.role === "system") {
1355
+ continue;
1356
+ }
1357
+ if (msg.role === "tool") {
1358
+ const toolResultBlock = {
1359
+ type: "tool_result",
1360
+ tool_use_id: msg.tool_call_id,
1361
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
1362
+ };
1363
+ const last = result[result.length - 1];
1364
+ if (last && last.role === "user" && Array.isArray(last.content)) {
1365
+ last.content.push(toolResultBlock);
1366
+ } else {
1367
+ result.push({ role: "user", content: [toolResultBlock] });
1368
+ }
1369
+ continue;
1370
+ }
1371
+ if (msg.role === "assistant") {
1372
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
1373
+ const contentBlocks = [];
1374
+ if (msg.content) {
1375
+ contentBlocks.push({ type: "text", text: msg.content });
1376
+ }
1377
+ for (const tc of msg.tool_calls) {
1378
+ let inputArgs = tc.function.arguments;
1379
+ if (typeof inputArgs === "string") {
1380
+ try {
1381
+ inputArgs = JSON.parse(inputArgs);
1382
+ } catch (_) {
1383
+ inputArgs = {};
1384
+ }
1385
+ }
1386
+ contentBlocks.push({
1387
+ type: "tool_use",
1388
+ id: tc.id,
1389
+ name: tc.function.name,
1390
+ input: inputArgs
1391
+ });
1392
+ }
1393
+ result.push({ role: "assistant", content: contentBlocks });
1394
+ } else {
1395
+ result.push({ role: "assistant", content: msg.content || "" });
1396
+ }
1397
+ continue;
1398
+ }
1399
+ result.push({ role: "user", content: msg.content || "" });
1280
1400
  }
1281
- return this.defaultModel;
1401
+ return result;
1402
+ }
1403
+ /**
1404
+ * Convert OpenAI-style tool definitions to Anthropic format.
1405
+ * OpenAI: { type: 'function', function: { name, description, parameters } }
1406
+ * Anthropic: { name, description, input_schema }
1407
+ */
1408
+ _formatTools(tools) {
1409
+ return tools.map((tool) => {
1410
+ const fn = tool.function || tool;
1411
+ return {
1412
+ name: fn.name,
1413
+ description: fn.description || "",
1414
+ input_schema: fn.parameters || fn.input_schema || { type: "object", properties: {} }
1415
+ };
1416
+ });
1417
+ }
1418
+ async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
1419
+ const toolResults = await Promise.all(
1420
+ tool_calls.map(async (toolCall) => {
1421
+ const toolName = toolCall.function.name;
1422
+ const tool = toolImplementations[toolName];
1423
+ console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.arguments);
1424
+ if (!tool) {
1425
+ console.error(`[Tool Error] Tool '${toolName}' not found`);
1426
+ return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Tool '${toolName}' not found.` }) };
1427
+ }
1428
+ try {
1429
+ const output = await tool(toolCall.function.arguments, { env, tenantId });
1430
+ const preview = typeof output === "string" ? output.length > 200 ? output.substring(0, 200) + "..." : output : JSON.stringify(output).substring(0, 200) + "...";
1431
+ console.log(`[Tool Result] ${toolName} returned:`, preview);
1432
+ return { tool_call_id: toolCall.id, output };
1433
+ } catch (error) {
1434
+ console.error(`[Tool Error] ${toolName} failed:`, error.message);
1435
+ return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Error executing tool '${toolName}': ${error.message}` }) };
1436
+ }
1437
+ })
1438
+ );
1439
+ toolResults.forEach(
1440
+ (result) => messages.push({ role: "tool", tool_call_id: result.tool_call_id, content: result.output })
1441
+ );
1442
+ }
1443
+ _getModelForTier(tier) {
1444
+ return this.models[tier] || this.defaultModel;
1282
1445
  }
1283
1446
  _shouldAutoParse(options) {
1284
1447
  if (!options.responseFormat) return false;
@@ -1299,15 +1462,10 @@ ${jsonPrompt}`;
1299
1462
  }
1300
1463
  normalizeFinishReason(providerReason) {
1301
1464
  const lower = (providerReason || "").toLowerCase();
1302
- if (lower === "end_turn") {
1303
- return "completed";
1304
- }
1305
- if (lower === "max_tokens") {
1306
- return "truncated";
1307
- }
1308
- if (lower === "stop_sequence") {
1309
- return "completed";
1310
- }
1465
+ if (lower === "end_turn") return "completed";
1466
+ if (lower === "max_tokens") return "truncated";
1467
+ if (lower === "stop_sequence") return "completed";
1468
+ if (lower === "tool_use") return "tool_call";
1311
1469
  return super.normalizeFinishReason(providerReason);
1312
1470
  }
1313
1471
  };