@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/README.md CHANGED
@@ -81,6 +81,238 @@ ConfigManager.setConfigProvider(new MyDatabaseConfigProvider());
81
81
  const service = new LLMService(env);
82
82
  ```
83
83
 
84
+ ### Google Gemini & Vertex AI Configuration
85
+
86
+ The service provides unified support for Google's AI platforms through the `@google/genai` SDK:
87
+
88
+ #### 1. Gemini AI Studio (Consumer API)
89
+ Uses your AI Studio API key directly.
90
+ ```env
91
+ LLM_PROVIDER=gemini
92
+ GEMINI_API_KEY=your-api-key-from-ai-studio
93
+ GEMINI_MODEL=gemini-3.5-flash
94
+ ```
95
+
96
+ #### 2. Vertex AI (Enterprise) — Two Authentication Modes
97
+
98
+ | Mode | Use Case | Required Config |
99
+ |------|----------|-----------------|
100
+ | **ADC / IAM (Default)** | Production, service accounts, Workload Identity | `VERTEX_PROJECT`, `VERTEX_LOCATION` |
101
+ | **Express Mode (API Key)** | Quick setup, dev/test, no service account | `VERTEX_API_KEY` (project auto-resolved) |
102
+
103
+ **Vertex Express Mode (API Key)** — Recommended for quick setup:
104
+ ```env
105
+ LLM_PROVIDER=vertex
106
+ VERTEX_API_KEY=AIza... # API key from Vertex AI Studio
107
+ # VERTEX_PROJECT and VERTEX_LOCATION are OPTIONAL — SDK resolves project from key
108
+ ```
109
+
110
+ **Vertex ADC/IAM Mode** — For production with service accounts:
111
+ ```env
112
+ LLM_PROVIDER=vertex
113
+ VERTEX_PROJECT=your-gcp-project-id
114
+ VERTEX_LOCATION=us-central1
115
+ # VERTEX_API_KEY is NOT needed (uses Application Default Credentials)
116
+ ```
117
+
118
+ **Legacy aliases** (also supported):
119
+ - `GOOGLE_CLOUD_PROJECT` → `VERTEX_PROJECT`
120
+ - `GOOGLE_CLOUD_LOCATION` → `VERTEX_LOCATION`
121
+
122
+ #### Model Overrides (All Providers)
123
+ ```env
124
+ VERTEX_MODEL=gemini-3.5-flash
125
+ VERTEX_MODEL_EDGE=gemini-3.5-pro
126
+ VERTEX_MODEL_FAST=gemini-3.5-flash
127
+ VERTEX_IMAGE_MODEL=gemini-3.5-pro-image-preview
128
+ VERTEX_VIDEO_MODEL=veo
129
+ ```
130
+
131
+ ---
132
+
133
+ ### Programmatic Configuration (No Environment Variables)
134
+
135
+ You can configure the provider directly via code by implementing a custom `ConfigProvider`. This is useful when you don't want to rely on environment variables or need dynamic configuration.
136
+
137
+ #### Vertex Express Mode (API Key) — Programmatic
138
+
139
+ ```javascript
140
+ import { LLMService, ConfigManager, BaseConfigProvider } from '@contentgrowth/llm-service';
141
+
142
+ class VertexExpressConfigProvider extends BaseConfigProvider {
143
+ constructor(apiKey) {
144
+ super();
145
+ this.apiKey = apiKey;
146
+ }
147
+
148
+ async getConfig(tenantId, env) {
149
+ return {
150
+ provider: 'vertex',
151
+ apiKey: this.apiKey,
152
+ // project & location are OPTIONAL — SDK resolves from API key
153
+ models: {
154
+ default: 'gemini-3.5-flash',
155
+ edge: 'gemini-3.5-pro',
156
+ fast: 'gemini-3.5-flash',
157
+ image: 'gemini-3.5-pro-image-preview',
158
+ video: 'veo',
159
+ },
160
+ temperature: 0.7,
161
+ maxTokens: 65536,
162
+ capabilities: { chat: true, image: true, video: true },
163
+ };
164
+ }
165
+ }
166
+
167
+ // Register once at startup
168
+ ConfigManager.setConfigProvider(new VertexExpressConfigProvider('AIza...'));
169
+
170
+ // Use normally
171
+ const service = new LLMService(env);
172
+ const response = await service.chat('Hello Vertex!', 'tenant-id');
173
+ ```
174
+
175
+ #### Vertex ADC/IAM Mode — Programmatic
176
+
177
+ ```javascript
178
+ class VertexADCConfigProvider extends BaseConfigProvider {
179
+ constructor(project, location = 'us-central1') {
180
+ super();
181
+ this.project = project;
182
+ this.location = location;
183
+ }
184
+
185
+ async getConfig(tenantId, env) {
186
+ return {
187
+ provider: 'vertex',
188
+ project: this.project,
189
+ location: this.location,
190
+ // apiKey NOT needed — uses Application Default Credentials
191
+ models: {
192
+ default: 'gemini-3.5-flash',
193
+ edge: 'gemini-3.5-pro',
194
+ fast: 'gemini-3.5-flash',
195
+ image: 'gemini-3.5-pro-image-preview',
196
+ video: 'veo',
197
+ },
198
+ temperature: 0.7,
199
+ maxTokens: 65536,
200
+ capabilities: { chat: true, image: true, video: true },
201
+ };
202
+ }
203
+ }
204
+
205
+ ConfigManager.setConfigProvider(new VertexADCConfigProvider('my-gcp-project', 'us-central1'));
206
+ ```
207
+
208
+ #### Gemini AI Studio — Programmatic
209
+
210
+ ```javascript
211
+ class GeminiStudioConfigProvider extends BaseConfigProvider {
212
+ constructor(apiKey) {
213
+ super();
214
+ this.apiKey = apiKey;
215
+ }
216
+
217
+ async getConfig(tenantId, env) {
218
+ return {
219
+ provider: 'gemini',
220
+ apiKey: this.apiKey,
221
+ models: {
222
+ default: 'gemini-3.5-flash',
223
+ edge: 'gemini-3.5-pro',
224
+ fast: 'gemini-3.5-flash',
225
+ image: 'gemini-3.5-pro-image-preview',
226
+ video: 'veo',
227
+ },
228
+ temperature: 0.7,
229
+ maxTokens: 65536,
230
+ capabilities: { chat: true, image: true, video: true },
231
+ };
232
+ }
233
+ }
234
+
235
+ ConfigManager.setConfigProvider(new GeminiStudioConfigProvider('your-gemini-api-key'));
236
+ ```
237
+
238
+ ---
239
+
240
+ ### Custom & OpenAI-Compatible Providers
241
+
242
+ Many third-party LLM proxies and gateways (LiteLLM, OpenRouter, custom corporate proxies) expose an OpenAI-compatible API. Because the OpenAI provider in this library accepts a `baseURL` override, you can point it at any of these services and use any model they expose — including Gemini, Claude, or your own fine-tuned models — without writing a custom provider.
243
+
244
+ #### How it works
245
+
246
+ Set `LLM_PROVIDER=openai`, point `OPENAI_BASE_URL` at your proxy, and set `OPENAI_MODEL` to whatever model name your proxy advertises. The library sends standard OpenAI-format requests; your proxy handles the translation.
247
+
248
+ #### Example: Gemini via an OpenAI-compatible proxy
249
+
250
+ ```env
251
+ LLM_PROVIDER=openai
252
+ OPENAI_API_KEY=your-proxy-api-key
253
+ OPENAI_BASE_URL=https://your-proxy.example.com/v1
254
+ OPENAI_MODEL=gemini-3.5-flash
255
+ ```
256
+
257
+ No code changes needed. All features — chat, tool calling, JSON mode — work through the proxy using the OpenAI wire format.
258
+
259
+ #### Example: Programmatic configuration
260
+
261
+ ```javascript
262
+ import { LLMService, ConfigManager, BaseConfigProvider } from '@contentgrowth/llm-service';
263
+
264
+ class ProxyConfigProvider extends BaseConfigProvider {
265
+ async getConfig(tenantId, env) {
266
+ return {
267
+ provider: 'openai',
268
+ apiKey: 'your-proxy-api-key',
269
+ baseURL: 'https://your-proxy.example.com/v1',
270
+ models: {
271
+ default: 'gemini-3.5-flash',
272
+ fast: 'gemini-3.5-flash',
273
+ edge: 'gemini-3.5-pro',
274
+ },
275
+ temperature: 0.7,
276
+ maxTokens: 65536,
277
+ };
278
+ }
279
+ }
280
+
281
+ ConfigManager.setConfigProvider(new ProxyConfigProvider());
282
+ const service = new LLMService(env);
283
+ ```
284
+
285
+ #### BYOK tenants via proxy
286
+
287
+ For multi-tenant setups, tenants can supply their own proxy endpoint alongside their API key:
288
+
289
+ ```json
290
+ {
291
+ "provider": "openai",
292
+ "api_key": "tenant-proxy-key",
293
+ "baseURL": "https://tenant-proxy.example.com/v1"
294
+ }
295
+ ```
296
+
297
+ #### baseURL support across all providers
298
+
299
+ Every provider respects a `baseURL` override, both from environment variables and from the tenant config object:
300
+
301
+ | Provider | Env var | Shared fallback |
302
+ |----------|---------|-----------------|
303
+ | `openai` | `OPENAI_BASE_URL` | `LLM_BASE_URL` |
304
+ | `gemini` | `GEMINI_BASE_URL` | `LLM_BASE_URL` |
305
+ | `anthropic` | `ANTHROPIC_BASE_URL` | `LLM_BASE_URL` |
306
+ | `vertex` | `VERTEX_BASE_URL` | `LLM_BASE_URL` |
307
+
308
+ `LLM_BASE_URL` applies to whichever provider is active, useful when routing all traffic through a single proxy regardless of provider.
309
+
310
+ #### Caveat: JSON schema mode
311
+
312
+ If your proxy doesn't natively support OpenAI's `response_format: { type: 'json_schema' }` for the model it's proxying, strict schema mode may fall back to `json_object` or return an error. Proxies like LiteLLM handle this by translating to the target model's native format, but not all do. Test with a `chatCompletionJson` call first to confirm schema mode works end-to-end.
313
+
314
+ ---
315
+
84
316
  ### JSON Mode & Structured Outputs
85
317
 
86
318
  The service supports native JSON mode for OpenAI and Gemini, guaranteeing valid JSON responses without escaping issues.
package/dist/index.cjs CHANGED
@@ -125,6 +125,7 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
125
125
  models: MODEL_CONFIGS[tenantConfig.provider],
126
126
  // for backward compatibility, api_key is deprecated and shall never be used!!
127
127
  apiKey: tenantConfig.apiKey || tenantConfig.api_key,
128
+ baseURL: tenantConfig.baseURL || tenantConfig.base_url || void 0,
128
129
  project: tenantConfig.project,
129
130
  location: tenantConfig.location,
130
131
  temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
@@ -138,9 +139,11 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
138
139
  const provider = ((_a = env.LLM_PROVIDER) == null ? void 0 : _a.toLowerCase()) || "openai";
139
140
  const providerDefaults = MODEL_CONFIGS[provider] || MODEL_CONFIGS["openai"];
140
141
  let apiKey;
142
+ let baseURL;
141
143
  let models = { ...providerDefaults };
142
144
  if (provider === "openai") {
143
145
  apiKey = env.OPENAI_API_KEY;
146
+ baseURL = env.OPENAI_BASE_URL || env.LLM_BASE_URL || void 0;
144
147
  models = {
145
148
  default: env.OPENAI_MODEL || providerDefaults.default,
146
149
  edge: env.OPENAI_MODEL_EDGE || providerDefaults.edge,
@@ -150,6 +153,7 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
150
153
  };
151
154
  } else if (provider === "gemini") {
152
155
  apiKey = env.GEMINI_API_KEY;
156
+ baseURL = env.GEMINI_BASE_URL || env.LLM_BASE_URL || void 0;
153
157
  models = {
154
158
  default: env.GEMINI_MODEL || providerDefaults.default,
155
159
  edge: env.GEMINI_MODEL_EDGE || providerDefaults.edge,
@@ -159,8 +163,19 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
159
163
  image: env.GEMINI_IMAGE_MODEL || providerDefaults.image,
160
164
  video: env.GEMINI_VIDEO_MODEL || providerDefaults.video
161
165
  };
166
+ } else if (provider === "anthropic") {
167
+ apiKey = env.ANTHROPIC_API_KEY;
168
+ baseURL = env.ANTHROPIC_BASE_URL || env.LLM_BASE_URL || void 0;
169
+ models = {
170
+ default: env.ANTHROPIC_MODEL || providerDefaults.default,
171
+ edge: env.ANTHROPIC_MODEL_EDGE || providerDefaults.edge,
172
+ fast: env.ANTHROPIC_MODEL_FAST || providerDefaults.fast,
173
+ cost: env.ANTHROPIC_MODEL_COST || providerDefaults.cost,
174
+ free: env.ANTHROPIC_MODEL_FREE || providerDefaults.free
175
+ };
162
176
  } else if (provider === "vertex") {
163
177
  apiKey = env.VERTEX_API_KEY;
178
+ baseURL = env.VERTEX_BASE_URL || env.LLM_BASE_URL || void 0;
164
179
  const project = env.VERTEX_PROJECT || env.GOOGLE_CLOUD_PROJECT;
165
180
  const location = env.VERTEX_LOCATION || env.GOOGLE_CLOUD_LOCATION || "us-central1";
166
181
  models = {
@@ -175,6 +190,7 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
175
190
  return {
176
191
  provider,
177
192
  apiKey,
193
+ baseURL,
178
194
  project,
179
195
  location,
180
196
  models,
@@ -185,6 +201,7 @@ var DefaultConfigProvider = class extends BaseConfigProvider {
185
201
  return {
186
202
  provider,
187
203
  apiKey,
204
+ baseURL,
188
205
  models,
189
206
  temperature: parseFloat(env.DEFAULT_TEMPERATURE || "0.7"),
190
207
  maxTokens: parseInt(env.DEFAULT_MAX_TOKENS || "65536")
@@ -448,7 +465,11 @@ function extractTextAndJson(input) {
448
465
  var OpenAIProvider = class extends BaseLLMProvider {
449
466
  constructor(config) {
450
467
  super(config);
451
- this.client = new import_openai.default({ apiKey: config.apiKey });
468
+ const clientOptions = { apiKey: config.apiKey };
469
+ if (config.baseURL) {
470
+ clientOptions.baseURL = config.baseURL;
471
+ }
472
+ this.client = new import_openai.default(clientOptions);
452
473
  this.models = config.models;
453
474
  this.defaultModel = config.models.default;
454
475
  }
@@ -630,11 +651,13 @@ var GoogleProvider = class extends BaseLLMProvider {
630
651
  this.models = config.models;
631
652
  this.defaultModel = config.models.default;
632
653
  this._pendingOperations = /* @__PURE__ */ new Map();
654
+ const httpOptions = config.baseURL ? { baseUrl: config.baseURL } : void 0;
633
655
  if (config.provider === "vertex") {
634
656
  if (config.apiKey) {
635
657
  this.client = new import_genai.GoogleGenAI({
636
658
  vertexai: true,
637
- apiKey: config.apiKey
659
+ apiKey: config.apiKey,
660
+ ...httpOptions ? { httpOptions } : {}
638
661
  });
639
662
  } else {
640
663
  if (!config.project) {
@@ -643,12 +666,14 @@ var GoogleProvider = class extends BaseLLMProvider {
643
666
  this.client = new import_genai.GoogleGenAI({
644
667
  vertexai: true,
645
668
  project: config.project,
646
- location: config.location || "us-central1"
669
+ location: config.location || "us-central1",
670
+ ...httpOptions ? { httpOptions } : {}
647
671
  });
648
672
  }
649
673
  } else {
650
674
  this.client = new import_genai.GoogleGenAI({
651
- apiKey: config.apiKey
675
+ apiKey: config.apiKey,
676
+ ...httpOptions ? { httpOptions } : {}
652
677
  });
653
678
  }
654
679
  }
@@ -777,8 +802,20 @@ ${msg.content}`;
777
802
  const isLastAssistantMessage = index === geminiMessages.map((m, i) => m.role === "assistant" ? i : -1).filter((i) => i >= 0).pop();
778
803
  if (msg.tool_calls) {
779
804
  parts2 = msg.tool_calls.map((tc) => {
805
+ let args = tc.function.args;
806
+ if (tc.function.arguments) {
807
+ if (typeof tc.function.arguments === "string") {
808
+ try {
809
+ args = JSON.parse(tc.function.arguments);
810
+ } catch (e) {
811
+ args = {};
812
+ }
813
+ } else {
814
+ args = tc.function.arguments;
815
+ }
816
+ }
780
817
  const part = {
781
- functionCall: { name: tc.function.name, args: tc.function.arguments || tc.function.args }
818
+ functionCall: { name: tc.function.name, args }
782
819
  };
783
820
  if (tc.thought_signature) {
784
821
  part.thoughtSignature = tc.thought_signature;
@@ -863,7 +900,10 @@ ${msg.content}`;
863
900
  thought_signature: responseThoughtSignature,
864
901
  tool_calls: toolCalls ? (Array.isArray(toolCalls) ? toolCalls : [toolCalls]).map((fc) => ({
865
902
  type: "function",
866
- function: fc,
903
+ function: {
904
+ name: fc.name,
905
+ arguments: JSON.stringify(fc.args || {})
906
+ },
867
907
  thought_signature: fc.thought_signature
868
908
  })) : null,
869
909
  finishReason: normalizedFinishReason,
@@ -1185,11 +1225,17 @@ ${prompt}` : prompt;
1185
1225
  };
1186
1226
 
1187
1227
  // src/llm/providers/anthropic-provider.js
1228
+ var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
1188
1229
  var AnthropicProvider = class extends BaseLLMProvider {
1189
1230
  constructor(config) {
1190
1231
  super(config);
1191
1232
  this.models = config.models || {};
1192
1233
  this.defaultModel = this.models.default || "claude-3-5-sonnet-20241022";
1234
+ const clientOptions = { apiKey: config.apiKey || "" };
1235
+ if (config.baseURL) {
1236
+ clientOptions.baseURL = config.baseURL;
1237
+ }
1238
+ this.client = new import_sdk.default(clientOptions);
1193
1239
  }
1194
1240
  async chat(userMessage, systemPrompt = "", options = {}) {
1195
1241
  var _a;
@@ -1226,82 +1272,69 @@ var AnthropicProvider = class extends BaseLLMProvider {
1226
1272
  );
1227
1273
  }
1228
1274
  async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
1229
- const baseURL = this.config.baseURL || "https://api.anthropic.com";
1230
- const url = `${baseURL.replace(/\/$/, "")}/v1/messages`;
1231
- const formattedMessages = messages.map((m) => ({
1232
- role: m.role === "assistant" ? "assistant" : "user",
1233
- content: m.content
1234
- }));
1235
- const headers = {
1236
- "Content-Type": "application/json",
1237
- "x-api-key": this.config.apiKey || "",
1238
- "anthropic-version": "2023-06-01"
1239
- };
1240
- const payload = {
1241
- model: modelName,
1242
- messages: formattedMessages,
1243
- max_tokens: maxTokens,
1244
- temperature
1245
- };
1246
- if (systemPrompt) {
1247
- payload.system = systemPrompt;
1248
- }
1275
+ var _a, _b, _c, _d;
1276
+ let effectiveSystem = systemPrompt || void 0;
1249
1277
  if (options.responseFormat) {
1250
1278
  const formatType = typeof options.responseFormat === "string" ? options.responseFormat : options.responseFormat.type;
1251
1279
  if (formatType === "json" || formatType === "json_schema") {
1252
- const schemaText = options.responseFormat.schema || options.responseSchema ? `
1280
+ const schema = options.responseFormat.schema || options.responseSchema;
1281
+ const schemaText = schema ? `
1253
1282
 
1254
- Your output must comply strictly with this JSON Schema: ${JSON.stringify(options.responseFormat.schema || options.responseSchema)}` : "";
1283
+ Your output must comply strictly with this JSON Schema: ${JSON.stringify(schema)}` : "";
1255
1284
  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}`;
1256
- if (payload.system) {
1257
- payload.system = `${payload.system}
1285
+ effectiveSystem = effectiveSystem ? `${effectiveSystem}
1258
1286
 
1259
- ${jsonPrompt}`;
1260
- } else {
1261
- payload.system = jsonPrompt;
1262
- }
1287
+ ${jsonPrompt}` : jsonPrompt;
1263
1288
  }
1264
1289
  }
1265
- let responseText;
1266
- let responseData;
1290
+ const anthropicMessages = this._formatMessages(messages);
1291
+ const anthropicTools = tools ? this._formatTools(tools) : void 0;
1292
+ const requestParams = {
1293
+ model: modelName,
1294
+ max_tokens: maxTokens,
1295
+ temperature,
1296
+ messages: anthropicMessages,
1297
+ ...effectiveSystem ? { system: effectiveSystem } : {},
1298
+ ...anthropicTools ? { tools: anthropicTools } : {}
1299
+ };
1300
+ let response;
1267
1301
  try {
1268
- const res = await fetch(url, {
1269
- method: "POST",
1270
- headers,
1271
- body: JSON.stringify(payload)
1272
- });
1273
- responseText = await res.text();
1274
- if (!res.ok) {
1275
- console.error(`[AnthropicProvider] API request failed with status ${res.status}:`, responseText);
1276
- throw new LLMServiceException(`Anthropic API Error: ${responseText}`, res.status);
1277
- }
1278
- responseData = JSON.parse(responseText);
1302
+ response = await this.client.messages.create(requestParams);
1279
1303
  } catch (error) {
1280
1304
  console.error(`[AnthropicProvider] request failed (API Key: ${this._getMaskedApiKey()}):`, error);
1281
1305
  throw error;
1282
1306
  }
1283
- const contentBlock = responseData.content && responseData.content[0];
1284
- const contentText = contentBlock ? contentBlock.text : "";
1285
- if (!contentText) {
1286
- console.error("[AnthropicProvider] Model returned empty response content");
1307
+ const textBlocks = response.content.filter((b) => b.type === "text");
1308
+ const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
1309
+ const contentText = textBlocks.map((b) => b.text).join("");
1310
+ if (!contentText && toolUseBlocks.length === 0) {
1311
+ console.error("[AnthropicProvider] Model returned empty response (no text, no tool calls)");
1287
1312
  throw new LLMServiceException(
1288
1313
  "Model returned empty response. This usually means the prompt or schema is confusing the model.",
1289
1314
  500
1290
1315
  );
1291
1316
  }
1292
- const rawFinishReason = responseData.stop_reason;
1317
+ const toolCalls = toolUseBlocks.length > 0 ? toolUseBlocks.map((block) => ({
1318
+ id: block.id,
1319
+ type: "function",
1320
+ function: {
1321
+ name: block.name,
1322
+ // Anthropic returns args as a parsed object; serialize to string to match OpenAI shape
1323
+ arguments: JSON.stringify(block.input || {})
1324
+ }
1325
+ })) : null;
1326
+ const rawFinishReason = response.stop_reason;
1293
1327
  const normalizedFinishReason = this.normalizeFinishReason(rawFinishReason);
1294
1328
  const result = {
1295
1329
  content: contentText,
1296
- tool_calls: null,
1297
- // REST client tool calls mapping not required for playbook studio
1330
+ tool_calls: toolCalls,
1298
1331
  finishReason: normalizedFinishReason,
1299
1332
  _rawFinishReason: rawFinishReason,
1300
1333
  _responseFormat: options.responseFormat,
1301
1334
  usage: {
1302
- prompt_tokens: responseData.usage ? responseData.usage.input_tokens : 0,
1303
- completion_tokens: responseData.usage ? responseData.usage.output_tokens : 0,
1304
- total_tokens: responseData.usage ? responseData.usage.input_tokens + responseData.usage.output_tokens : 0
1335
+ prompt_tokens: ((_a = response.usage) == null ? void 0 : _a.input_tokens) || 0,
1336
+ completion_tokens: ((_b = response.usage) == null ? void 0 : _b.output_tokens) || 0,
1337
+ total_tokens: (((_c = response.usage) == null ? void 0 : _c.input_tokens) || 0) + (((_d = response.usage) == null ? void 0 : _d.output_tokens) || 0)
1305
1338
  },
1306
1339
  model: modelName
1307
1340
  };
@@ -1310,11 +1343,110 @@ ${jsonPrompt}`;
1310
1343
  }
1311
1344
  return result;
1312
1345
  }
1313
- _getModelForTier(tier) {
1314
- if (this.models[tier]) {
1315
- return this.models[tier];
1346
+ /**
1347
+ * Convert standard {role, content, tool_calls, tool_call_id} messages
1348
+ * to Anthropic's message format.
1349
+ *
1350
+ * Key differences from OpenAI:
1351
+ * - No 'system' role in messages array (handled via top-level `system` param)
1352
+ * - Tool results go as role:'user' with content type:'tool_result'
1353
+ * - Assistant messages with tool calls use an array of content blocks
1354
+ * - Roles must strictly alternate user/assistant
1355
+ */
1356
+ _formatMessages(messages) {
1357
+ const result = [];
1358
+ for (const msg of messages) {
1359
+ if (msg.role === "system") {
1360
+ continue;
1361
+ }
1362
+ if (msg.role === "tool") {
1363
+ const toolResultBlock = {
1364
+ type: "tool_result",
1365
+ tool_use_id: msg.tool_call_id,
1366
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
1367
+ };
1368
+ const last = result[result.length - 1];
1369
+ if (last && last.role === "user" && Array.isArray(last.content)) {
1370
+ last.content.push(toolResultBlock);
1371
+ } else {
1372
+ result.push({ role: "user", content: [toolResultBlock] });
1373
+ }
1374
+ continue;
1375
+ }
1376
+ if (msg.role === "assistant") {
1377
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
1378
+ const contentBlocks = [];
1379
+ if (msg.content) {
1380
+ contentBlocks.push({ type: "text", text: msg.content });
1381
+ }
1382
+ for (const tc of msg.tool_calls) {
1383
+ let inputArgs = tc.function.arguments;
1384
+ if (typeof inputArgs === "string") {
1385
+ try {
1386
+ inputArgs = JSON.parse(inputArgs);
1387
+ } catch (_) {
1388
+ inputArgs = {};
1389
+ }
1390
+ }
1391
+ contentBlocks.push({
1392
+ type: "tool_use",
1393
+ id: tc.id,
1394
+ name: tc.function.name,
1395
+ input: inputArgs
1396
+ });
1397
+ }
1398
+ result.push({ role: "assistant", content: contentBlocks });
1399
+ } else {
1400
+ result.push({ role: "assistant", content: msg.content || "" });
1401
+ }
1402
+ continue;
1403
+ }
1404
+ result.push({ role: "user", content: msg.content || "" });
1316
1405
  }
1317
- return this.defaultModel;
1406
+ return result;
1407
+ }
1408
+ /**
1409
+ * Convert OpenAI-style tool definitions to Anthropic format.
1410
+ * OpenAI: { type: 'function', function: { name, description, parameters } }
1411
+ * Anthropic: { name, description, input_schema }
1412
+ */
1413
+ _formatTools(tools) {
1414
+ return tools.map((tool) => {
1415
+ const fn = tool.function || tool;
1416
+ return {
1417
+ name: fn.name,
1418
+ description: fn.description || "",
1419
+ input_schema: fn.parameters || fn.input_schema || { type: "object", properties: {} }
1420
+ };
1421
+ });
1422
+ }
1423
+ async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
1424
+ const toolResults = await Promise.all(
1425
+ tool_calls.map(async (toolCall) => {
1426
+ const toolName = toolCall.function.name;
1427
+ const tool = toolImplementations[toolName];
1428
+ console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.arguments);
1429
+ if (!tool) {
1430
+ console.error(`[Tool Error] Tool '${toolName}' not found`);
1431
+ return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Tool '${toolName}' not found.` }) };
1432
+ }
1433
+ try {
1434
+ const output = await tool(toolCall.function.arguments, { env, tenantId });
1435
+ const preview = typeof output === "string" ? output.length > 200 ? output.substring(0, 200) + "..." : output : JSON.stringify(output).substring(0, 200) + "...";
1436
+ console.log(`[Tool Result] ${toolName} returned:`, preview);
1437
+ return { tool_call_id: toolCall.id, output };
1438
+ } catch (error) {
1439
+ console.error(`[Tool Error] ${toolName} failed:`, error.message);
1440
+ return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Error executing tool '${toolName}': ${error.message}` }) };
1441
+ }
1442
+ })
1443
+ );
1444
+ toolResults.forEach(
1445
+ (result) => messages.push({ role: "tool", tool_call_id: result.tool_call_id, content: result.output })
1446
+ );
1447
+ }
1448
+ _getModelForTier(tier) {
1449
+ return this.models[tier] || this.defaultModel;
1318
1450
  }
1319
1451
  _shouldAutoParse(options) {
1320
1452
  if (!options.responseFormat) return false;
@@ -1335,15 +1467,10 @@ ${jsonPrompt}`;
1335
1467
  }
1336
1468
  normalizeFinishReason(providerReason) {
1337
1469
  const lower = (providerReason || "").toLowerCase();
1338
- if (lower === "end_turn") {
1339
- return "completed";
1340
- }
1341
- if (lower === "max_tokens") {
1342
- return "truncated";
1343
- }
1344
- if (lower === "stop_sequence") {
1345
- return "completed";
1346
- }
1470
+ if (lower === "end_turn") return "completed";
1471
+ if (lower === "max_tokens") return "truncated";
1472
+ if (lower === "stop_sequence") return "completed";
1473
+ if (lower === "tool_use") return "tool_call";
1347
1474
  return super.normalizeFinishReason(providerReason);
1348
1475
  }
1349
1476
  };