@contentgrowth/llm-service 1.2.3 → 1.3.0

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
  }
@@ -726,8 +751,20 @@ ${msg.content}`;
726
751
  const isLastAssistantMessage = index === geminiMessages.map((m, i) => m.role === "assistant" ? i : -1).filter((i) => i >= 0).pop();
727
752
  if (msg.tool_calls) {
728
753
  parts2 = msg.tool_calls.map((tc) => {
754
+ let args = tc.function.args;
755
+ if (tc.function.arguments) {
756
+ if (typeof tc.function.arguments === "string") {
757
+ try {
758
+ args = JSON.parse(tc.function.arguments);
759
+ } catch (e) {
760
+ args = {};
761
+ }
762
+ } else {
763
+ args = tc.function.arguments;
764
+ }
765
+ }
729
766
  const part = {
730
- functionCall: { name: tc.function.name, args: tc.function.arguments || tc.function.args }
767
+ functionCall: { name: tc.function.name, args }
731
768
  };
732
769
  if (tc.thought_signature) {
733
770
  part.thoughtSignature = tc.thought_signature;
@@ -812,7 +849,10 @@ ${msg.content}`;
812
849
  thought_signature: responseThoughtSignature,
813
850
  tool_calls: toolCalls ? (Array.isArray(toolCalls) ? toolCalls : [toolCalls]).map((fc) => ({
814
851
  type: "function",
815
- function: fc,
852
+ function: {
853
+ name: fc.name,
854
+ arguments: JSON.stringify(fc.args || {})
855
+ },
816
856
  thought_signature: fc.thought_signature
817
857
  })) : null,
818
858
  finishReason: normalizedFinishReason,
@@ -1134,11 +1174,17 @@ ${prompt}` : prompt;
1134
1174
  };
1135
1175
 
1136
1176
  // src/llm/providers/anthropic-provider.js
1177
+ import Anthropic from "@anthropic-ai/sdk";
1137
1178
  var AnthropicProvider = class extends BaseLLMProvider {
1138
1179
  constructor(config) {
1139
1180
  super(config);
1140
1181
  this.models = config.models || {};
1141
1182
  this.defaultModel = this.models.default || "claude-3-5-sonnet-20241022";
1183
+ const clientOptions = { apiKey: config.apiKey || "" };
1184
+ if (config.baseURL) {
1185
+ clientOptions.baseURL = config.baseURL;
1186
+ }
1187
+ this.client = new Anthropic(clientOptions);
1142
1188
  }
1143
1189
  async chat(userMessage, systemPrompt = "", options = {}) {
1144
1190
  var _a;
@@ -1175,82 +1221,69 @@ var AnthropicProvider = class extends BaseLLMProvider {
1175
1221
  );
1176
1222
  }
1177
1223
  async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
1178
- const baseURL = this.config.baseURL || "https://api.anthropic.com";
1179
- const url = `${baseURL.replace(/\/$/, "")}/v1/messages`;
1180
- const formattedMessages = messages.map((m) => ({
1181
- role: m.role === "assistant" ? "assistant" : "user",
1182
- content: m.content
1183
- }));
1184
- const headers = {
1185
- "Content-Type": "application/json",
1186
- "x-api-key": this.config.apiKey || "",
1187
- "anthropic-version": "2023-06-01"
1188
- };
1189
- const payload = {
1190
- model: modelName,
1191
- messages: formattedMessages,
1192
- max_tokens: maxTokens,
1193
- temperature
1194
- };
1195
- if (systemPrompt) {
1196
- payload.system = systemPrompt;
1197
- }
1224
+ var _a, _b, _c, _d;
1225
+ let effectiveSystem = systemPrompt || void 0;
1198
1226
  if (options.responseFormat) {
1199
1227
  const formatType = typeof options.responseFormat === "string" ? options.responseFormat : options.responseFormat.type;
1200
1228
  if (formatType === "json" || formatType === "json_schema") {
1201
- const schemaText = options.responseFormat.schema || options.responseSchema ? `
1229
+ const schema = options.responseFormat.schema || options.responseSchema;
1230
+ const schemaText = schema ? `
1202
1231
 
1203
- Your output must comply strictly with this JSON Schema: ${JSON.stringify(options.responseFormat.schema || options.responseSchema)}` : "";
1232
+ Your output must comply strictly with this JSON Schema: ${JSON.stringify(schema)}` : "";
1204
1233
  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}`;
1205
- if (payload.system) {
1206
- payload.system = `${payload.system}
1234
+ effectiveSystem = effectiveSystem ? `${effectiveSystem}
1207
1235
 
1208
- ${jsonPrompt}`;
1209
- } else {
1210
- payload.system = jsonPrompt;
1211
- }
1236
+ ${jsonPrompt}` : jsonPrompt;
1212
1237
  }
1213
1238
  }
1214
- let responseText;
1215
- let responseData;
1239
+ const anthropicMessages = this._formatMessages(messages);
1240
+ const anthropicTools = tools ? this._formatTools(tools) : void 0;
1241
+ const requestParams = {
1242
+ model: modelName,
1243
+ max_tokens: maxTokens,
1244
+ temperature,
1245
+ messages: anthropicMessages,
1246
+ ...effectiveSystem ? { system: effectiveSystem } : {},
1247
+ ...anthropicTools ? { tools: anthropicTools } : {}
1248
+ };
1249
+ let response;
1216
1250
  try {
1217
- const res = await fetch(url, {
1218
- method: "POST",
1219
- headers,
1220
- body: JSON.stringify(payload)
1221
- });
1222
- responseText = await res.text();
1223
- if (!res.ok) {
1224
- console.error(`[AnthropicProvider] API request failed with status ${res.status}:`, responseText);
1225
- throw new LLMServiceException(`Anthropic API Error: ${responseText}`, res.status);
1226
- }
1227
- responseData = JSON.parse(responseText);
1251
+ response = await this.client.messages.create(requestParams);
1228
1252
  } catch (error) {
1229
1253
  console.error(`[AnthropicProvider] request failed (API Key: ${this._getMaskedApiKey()}):`, error);
1230
1254
  throw error;
1231
1255
  }
1232
- const contentBlock = responseData.content && responseData.content[0];
1233
- const contentText = contentBlock ? contentBlock.text : "";
1234
- if (!contentText) {
1235
- console.error("[AnthropicProvider] Model returned empty response content");
1256
+ const textBlocks = response.content.filter((b) => b.type === "text");
1257
+ const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
1258
+ const contentText = textBlocks.map((b) => b.text).join("");
1259
+ if (!contentText && toolUseBlocks.length === 0) {
1260
+ console.error("[AnthropicProvider] Model returned empty response (no text, no tool calls)");
1236
1261
  throw new LLMServiceException(
1237
1262
  "Model returned empty response. This usually means the prompt or schema is confusing the model.",
1238
1263
  500
1239
1264
  );
1240
1265
  }
1241
- const rawFinishReason = responseData.stop_reason;
1266
+ const toolCalls = toolUseBlocks.length > 0 ? toolUseBlocks.map((block) => ({
1267
+ id: block.id,
1268
+ type: "function",
1269
+ function: {
1270
+ name: block.name,
1271
+ // Anthropic returns args as a parsed object; serialize to string to match OpenAI shape
1272
+ arguments: JSON.stringify(block.input || {})
1273
+ }
1274
+ })) : null;
1275
+ const rawFinishReason = response.stop_reason;
1242
1276
  const normalizedFinishReason = this.normalizeFinishReason(rawFinishReason);
1243
1277
  const result = {
1244
1278
  content: contentText,
1245
- tool_calls: null,
1246
- // REST client tool calls mapping not required for playbook studio
1279
+ tool_calls: toolCalls,
1247
1280
  finishReason: normalizedFinishReason,
1248
1281
  _rawFinishReason: rawFinishReason,
1249
1282
  _responseFormat: options.responseFormat,
1250
1283
  usage: {
1251
- prompt_tokens: responseData.usage ? responseData.usage.input_tokens : 0,
1252
- completion_tokens: responseData.usage ? responseData.usage.output_tokens : 0,
1253
- total_tokens: responseData.usage ? responseData.usage.input_tokens + responseData.usage.output_tokens : 0
1284
+ prompt_tokens: ((_a = response.usage) == null ? void 0 : _a.input_tokens) || 0,
1285
+ completion_tokens: ((_b = response.usage) == null ? void 0 : _b.output_tokens) || 0,
1286
+ total_tokens: (((_c = response.usage) == null ? void 0 : _c.input_tokens) || 0) + (((_d = response.usage) == null ? void 0 : _d.output_tokens) || 0)
1254
1287
  },
1255
1288
  model: modelName
1256
1289
  };
@@ -1259,11 +1292,110 @@ ${jsonPrompt}`;
1259
1292
  }
1260
1293
  return result;
1261
1294
  }
1262
- _getModelForTier(tier) {
1263
- if (this.models[tier]) {
1264
- return this.models[tier];
1295
+ /**
1296
+ * Convert standard {role, content, tool_calls, tool_call_id} messages
1297
+ * to Anthropic's message format.
1298
+ *
1299
+ * Key differences from OpenAI:
1300
+ * - No 'system' role in messages array (handled via top-level `system` param)
1301
+ * - Tool results go as role:'user' with content type:'tool_result'
1302
+ * - Assistant messages with tool calls use an array of content blocks
1303
+ * - Roles must strictly alternate user/assistant
1304
+ */
1305
+ _formatMessages(messages) {
1306
+ const result = [];
1307
+ for (const msg of messages) {
1308
+ if (msg.role === "system") {
1309
+ continue;
1310
+ }
1311
+ if (msg.role === "tool") {
1312
+ const toolResultBlock = {
1313
+ type: "tool_result",
1314
+ tool_use_id: msg.tool_call_id,
1315
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
1316
+ };
1317
+ const last = result[result.length - 1];
1318
+ if (last && last.role === "user" && Array.isArray(last.content)) {
1319
+ last.content.push(toolResultBlock);
1320
+ } else {
1321
+ result.push({ role: "user", content: [toolResultBlock] });
1322
+ }
1323
+ continue;
1324
+ }
1325
+ if (msg.role === "assistant") {
1326
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
1327
+ const contentBlocks = [];
1328
+ if (msg.content) {
1329
+ contentBlocks.push({ type: "text", text: msg.content });
1330
+ }
1331
+ for (const tc of msg.tool_calls) {
1332
+ let inputArgs = tc.function.arguments;
1333
+ if (typeof inputArgs === "string") {
1334
+ try {
1335
+ inputArgs = JSON.parse(inputArgs);
1336
+ } catch (_) {
1337
+ inputArgs = {};
1338
+ }
1339
+ }
1340
+ contentBlocks.push({
1341
+ type: "tool_use",
1342
+ id: tc.id,
1343
+ name: tc.function.name,
1344
+ input: inputArgs
1345
+ });
1346
+ }
1347
+ result.push({ role: "assistant", content: contentBlocks });
1348
+ } else {
1349
+ result.push({ role: "assistant", content: msg.content || "" });
1350
+ }
1351
+ continue;
1352
+ }
1353
+ result.push({ role: "user", content: msg.content || "" });
1265
1354
  }
1266
- return this.defaultModel;
1355
+ return result;
1356
+ }
1357
+ /**
1358
+ * Convert OpenAI-style tool definitions to Anthropic format.
1359
+ * OpenAI: { type: 'function', function: { name, description, parameters } }
1360
+ * Anthropic: { name, description, input_schema }
1361
+ */
1362
+ _formatTools(tools) {
1363
+ return tools.map((tool) => {
1364
+ const fn = tool.function || tool;
1365
+ return {
1366
+ name: fn.name,
1367
+ description: fn.description || "",
1368
+ input_schema: fn.parameters || fn.input_schema || { type: "object", properties: {} }
1369
+ };
1370
+ });
1371
+ }
1372
+ async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
1373
+ const toolResults = await Promise.all(
1374
+ tool_calls.map(async (toolCall) => {
1375
+ const toolName = toolCall.function.name;
1376
+ const tool = toolImplementations[toolName];
1377
+ console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.arguments);
1378
+ if (!tool) {
1379
+ console.error(`[Tool Error] Tool '${toolName}' not found`);
1380
+ return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Tool '${toolName}' not found.` }) };
1381
+ }
1382
+ try {
1383
+ const output = await tool(toolCall.function.arguments, { env, tenantId });
1384
+ const preview = typeof output === "string" ? output.length > 200 ? output.substring(0, 200) + "..." : output : JSON.stringify(output).substring(0, 200) + "...";
1385
+ console.log(`[Tool Result] ${toolName} returned:`, preview);
1386
+ return { tool_call_id: toolCall.id, output };
1387
+ } catch (error) {
1388
+ console.error(`[Tool Error] ${toolName} failed:`, error.message);
1389
+ return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Error executing tool '${toolName}': ${error.message}` }) };
1390
+ }
1391
+ })
1392
+ );
1393
+ toolResults.forEach(
1394
+ (result) => messages.push({ role: "tool", tool_call_id: result.tool_call_id, content: result.output })
1395
+ );
1396
+ }
1397
+ _getModelForTier(tier) {
1398
+ return this.models[tier] || this.defaultModel;
1267
1399
  }
1268
1400
  _shouldAutoParse(options) {
1269
1401
  if (!options.responseFormat) return false;
@@ -1284,15 +1416,10 @@ ${jsonPrompt}`;
1284
1416
  }
1285
1417
  normalizeFinishReason(providerReason) {
1286
1418
  const lower = (providerReason || "").toLowerCase();
1287
- if (lower === "end_turn") {
1288
- return "completed";
1289
- }
1290
- if (lower === "max_tokens") {
1291
- return "truncated";
1292
- }
1293
- if (lower === "stop_sequence") {
1294
- return "completed";
1295
- }
1419
+ if (lower === "end_turn") return "completed";
1420
+ if (lower === "max_tokens") return "truncated";
1421
+ if (lower === "stop_sequence") return "completed";
1422
+ if (lower === "tool_use") return "tool_call";
1296
1423
  return super.normalizeFinishReason(providerReason);
1297
1424
  }
1298
1425
  };