@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 +232 -0
- package/dist/index.cjs +197 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -15
- package/dist/index.d.ts +65 -15
- package/dist/index.js +197 -70
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/ui/react/components/index.d.cts +2 -3
- package/dist/ui/react/components/index.d.ts +2 -3
- package/package.json +3 -2
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
1230
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
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
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
|
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:
|
|
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:
|
|
1303
|
-
completion_tokens:
|
|
1304
|
-
total_tokens:
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
|
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
|
-
|
|
1340
|
-
|
|
1341
|
-
if (lower === "
|
|
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
|
};
|