@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/README.md +232 -0
- package/dist/index.cjs +226 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -13
- package/dist/index.d.ts +69 -13
- package/dist/index.js +226 -68
- 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
|
}
|
|
@@ -1197,14 +1222,66 @@ ${prompt}` : prompt;
|
|
|
1197
1222
|
throw error;
|
|
1198
1223
|
}
|
|
1199
1224
|
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Text-to-Speech synthesis using Gemini TTS models
|
|
1227
|
+
* @param {string} text - Text to synthesize
|
|
1228
|
+
* @param {Object} options - TTS options
|
|
1229
|
+
* @param {string} options.voice - Voice name (default: 'Aoede')
|
|
1230
|
+
* @param {string} options.model - Model to use (default: from config)
|
|
1231
|
+
* @returns {Promise<Buffer>} Audio data as Buffer
|
|
1232
|
+
*/
|
|
1233
|
+
async textToSpeech(text, options = {}) {
|
|
1234
|
+
var _a, _b, _c, _d, _e;
|
|
1235
|
+
const voice = options.voice || "Aoede";
|
|
1236
|
+
const modelName = options.model || this.models.tts || "gemini-3.1-flash-tts-preview";
|
|
1237
|
+
const requestOptions = {
|
|
1238
|
+
model: modelName,
|
|
1239
|
+
contents: [{
|
|
1240
|
+
role: "user",
|
|
1241
|
+
parts: [{ text }]
|
|
1242
|
+
}],
|
|
1243
|
+
config: {
|
|
1244
|
+
responseModalities: ["AUDIO"],
|
|
1245
|
+
speechConfig: {
|
|
1246
|
+
voiceConfig: {
|
|
1247
|
+
prebuiltVoiceConfig: {
|
|
1248
|
+
voiceName: voice
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
try {
|
|
1255
|
+
const response = await this._generateContent(requestOptions);
|
|
1256
|
+
const candidate = (_a = response.candidates) == null ? void 0 : _a[0];
|
|
1257
|
+
if (!candidate) {
|
|
1258
|
+
throw new LLMServiceException("No candidates returned from TTS model", 500);
|
|
1259
|
+
}
|
|
1260
|
+
const audioPart = (_c = (_b = candidate.content) == null ? void 0 : _b.parts) == null ? void 0 : _c.find((p) => p.inlineData || p.inline_data);
|
|
1261
|
+
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);
|
|
1262
|
+
if (!audioB64) {
|
|
1263
|
+
throw new LLMServiceException("No audio data returned from TTS model", 500);
|
|
1264
|
+
}
|
|
1265
|
+
return Buffer.from(audioB64, "base64");
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
console.error(`[${this.constructor.name}] textToSpeech failed (API Key: ${this._getMaskedApiKey()}):`, error);
|
|
1268
|
+
throw error;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1200
1271
|
};
|
|
1201
1272
|
|
|
1202
1273
|
// src/llm/providers/anthropic-provider.js
|
|
1274
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
1203
1275
|
var AnthropicProvider = class extends BaseLLMProvider {
|
|
1204
1276
|
constructor(config) {
|
|
1205
1277
|
super(config);
|
|
1206
1278
|
this.models = config.models || {};
|
|
1207
1279
|
this.defaultModel = this.models.default || "claude-3-5-sonnet-20241022";
|
|
1280
|
+
const clientOptions = { apiKey: config.apiKey || "" };
|
|
1281
|
+
if (config.baseURL) {
|
|
1282
|
+
clientOptions.baseURL = config.baseURL;
|
|
1283
|
+
}
|
|
1284
|
+
this.client = new import_sdk.default(clientOptions);
|
|
1208
1285
|
}
|
|
1209
1286
|
async chat(userMessage, systemPrompt = "", options = {}) {
|
|
1210
1287
|
var _a;
|
|
@@ -1241,82 +1318,69 @@ var AnthropicProvider = class extends BaseLLMProvider {
|
|
|
1241
1318
|
);
|
|
1242
1319
|
}
|
|
1243
1320
|
async _chatCompletionWithModel(messages, systemPrompt, tools, modelName, maxTokens, temperature, options = {}) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const formattedMessages = messages.map((m) => ({
|
|
1247
|
-
role: m.role === "assistant" ? "assistant" : "user",
|
|
1248
|
-
content: m.content
|
|
1249
|
-
}));
|
|
1250
|
-
const headers = {
|
|
1251
|
-
"Content-Type": "application/json",
|
|
1252
|
-
"x-api-key": this.config.apiKey || "",
|
|
1253
|
-
"anthropic-version": "2023-06-01"
|
|
1254
|
-
};
|
|
1255
|
-
const payload = {
|
|
1256
|
-
model: modelName,
|
|
1257
|
-
messages: formattedMessages,
|
|
1258
|
-
max_tokens: maxTokens,
|
|
1259
|
-
temperature
|
|
1260
|
-
};
|
|
1261
|
-
if (systemPrompt) {
|
|
1262
|
-
payload.system = systemPrompt;
|
|
1263
|
-
}
|
|
1321
|
+
var _a, _b, _c, _d;
|
|
1322
|
+
let effectiveSystem = systemPrompt || void 0;
|
|
1264
1323
|
if (options.responseFormat) {
|
|
1265
1324
|
const formatType = typeof options.responseFormat === "string" ? options.responseFormat : options.responseFormat.type;
|
|
1266
1325
|
if (formatType === "json" || formatType === "json_schema") {
|
|
1267
|
-
const
|
|
1326
|
+
const schema = options.responseFormat.schema || options.responseSchema;
|
|
1327
|
+
const schemaText = schema ? `
|
|
1268
1328
|
|
|
1269
|
-
Your output must comply strictly with this JSON Schema: ${JSON.stringify(
|
|
1329
|
+
Your output must comply strictly with this JSON Schema: ${JSON.stringify(schema)}` : "";
|
|
1270
1330
|
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}`;
|
|
1271
|
-
|
|
1272
|
-
payload.system = `${payload.system}
|
|
1331
|
+
effectiveSystem = effectiveSystem ? `${effectiveSystem}
|
|
1273
1332
|
|
|
1274
|
-
${jsonPrompt}
|
|
1275
|
-
} else {
|
|
1276
|
-
payload.system = jsonPrompt;
|
|
1277
|
-
}
|
|
1333
|
+
${jsonPrompt}` : jsonPrompt;
|
|
1278
1334
|
}
|
|
1279
1335
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1336
|
+
const anthropicMessages = this._formatMessages(messages);
|
|
1337
|
+
const anthropicTools = tools ? this._formatTools(tools) : void 0;
|
|
1338
|
+
const requestParams = {
|
|
1339
|
+
model: modelName,
|
|
1340
|
+
max_tokens: maxTokens,
|
|
1341
|
+
temperature,
|
|
1342
|
+
messages: anthropicMessages,
|
|
1343
|
+
...effectiveSystem ? { system: effectiveSystem } : {},
|
|
1344
|
+
...anthropicTools ? { tools: anthropicTools } : {}
|
|
1345
|
+
};
|
|
1346
|
+
let response;
|
|
1282
1347
|
try {
|
|
1283
|
-
|
|
1284
|
-
method: "POST",
|
|
1285
|
-
headers,
|
|
1286
|
-
body: JSON.stringify(payload)
|
|
1287
|
-
});
|
|
1288
|
-
responseText = await res.text();
|
|
1289
|
-
if (!res.ok) {
|
|
1290
|
-
console.error(`[AnthropicProvider] API request failed with status ${res.status}:`, responseText);
|
|
1291
|
-
throw new LLMServiceException(`Anthropic API Error: ${responseText}`, res.status);
|
|
1292
|
-
}
|
|
1293
|
-
responseData = JSON.parse(responseText);
|
|
1348
|
+
response = await this.client.messages.create(requestParams);
|
|
1294
1349
|
} catch (error) {
|
|
1295
1350
|
console.error(`[AnthropicProvider] request failed (API Key: ${this._getMaskedApiKey()}):`, error);
|
|
1296
1351
|
throw error;
|
|
1297
1352
|
}
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1301
|
-
|
|
1353
|
+
const textBlocks = response.content.filter((b) => b.type === "text");
|
|
1354
|
+
const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
|
|
1355
|
+
const contentText = textBlocks.map((b) => b.text).join("");
|
|
1356
|
+
if (!contentText && toolUseBlocks.length === 0) {
|
|
1357
|
+
console.error("[AnthropicProvider] Model returned empty response (no text, no tool calls)");
|
|
1302
1358
|
throw new LLMServiceException(
|
|
1303
1359
|
"Model returned empty response. This usually means the prompt or schema is confusing the model.",
|
|
1304
1360
|
500
|
|
1305
1361
|
);
|
|
1306
1362
|
}
|
|
1307
|
-
const
|
|
1363
|
+
const toolCalls = toolUseBlocks.length > 0 ? toolUseBlocks.map((block) => ({
|
|
1364
|
+
id: block.id,
|
|
1365
|
+
type: "function",
|
|
1366
|
+
function: {
|
|
1367
|
+
name: block.name,
|
|
1368
|
+
// Anthropic returns args as a parsed object; serialize to string to match OpenAI shape
|
|
1369
|
+
arguments: JSON.stringify(block.input || {})
|
|
1370
|
+
}
|
|
1371
|
+
})) : null;
|
|
1372
|
+
const rawFinishReason = response.stop_reason;
|
|
1308
1373
|
const normalizedFinishReason = this.normalizeFinishReason(rawFinishReason);
|
|
1309
1374
|
const result = {
|
|
1310
1375
|
content: contentText,
|
|
1311
|
-
tool_calls:
|
|
1312
|
-
// REST client tool calls mapping not required for playbook studio
|
|
1376
|
+
tool_calls: toolCalls,
|
|
1313
1377
|
finishReason: normalizedFinishReason,
|
|
1314
1378
|
_rawFinishReason: rawFinishReason,
|
|
1315
1379
|
_responseFormat: options.responseFormat,
|
|
1316
1380
|
usage: {
|
|
1317
|
-
prompt_tokens:
|
|
1318
|
-
completion_tokens:
|
|
1319
|
-
total_tokens:
|
|
1381
|
+
prompt_tokens: ((_a = response.usage) == null ? void 0 : _a.input_tokens) || 0,
|
|
1382
|
+
completion_tokens: ((_b = response.usage) == null ? void 0 : _b.output_tokens) || 0,
|
|
1383
|
+
total_tokens: (((_c = response.usage) == null ? void 0 : _c.input_tokens) || 0) + (((_d = response.usage) == null ? void 0 : _d.output_tokens) || 0)
|
|
1320
1384
|
},
|
|
1321
1385
|
model: modelName
|
|
1322
1386
|
};
|
|
@@ -1325,11 +1389,110 @@ ${jsonPrompt}`;
|
|
|
1325
1389
|
}
|
|
1326
1390
|
return result;
|
|
1327
1391
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1392
|
+
/**
|
|
1393
|
+
* Convert standard {role, content, tool_calls, tool_call_id} messages
|
|
1394
|
+
* to Anthropic's message format.
|
|
1395
|
+
*
|
|
1396
|
+
* Key differences from OpenAI:
|
|
1397
|
+
* - No 'system' role in messages array (handled via top-level `system` param)
|
|
1398
|
+
* - Tool results go as role:'user' with content type:'tool_result'
|
|
1399
|
+
* - Assistant messages with tool calls use an array of content blocks
|
|
1400
|
+
* - Roles must strictly alternate user/assistant
|
|
1401
|
+
*/
|
|
1402
|
+
_formatMessages(messages) {
|
|
1403
|
+
const result = [];
|
|
1404
|
+
for (const msg of messages) {
|
|
1405
|
+
if (msg.role === "system") {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (msg.role === "tool") {
|
|
1409
|
+
const toolResultBlock = {
|
|
1410
|
+
type: "tool_result",
|
|
1411
|
+
tool_use_id: msg.tool_call_id,
|
|
1412
|
+
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
|
|
1413
|
+
};
|
|
1414
|
+
const last = result[result.length - 1];
|
|
1415
|
+
if (last && last.role === "user" && Array.isArray(last.content)) {
|
|
1416
|
+
last.content.push(toolResultBlock);
|
|
1417
|
+
} else {
|
|
1418
|
+
result.push({ role: "user", content: [toolResultBlock] });
|
|
1419
|
+
}
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (msg.role === "assistant") {
|
|
1423
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
1424
|
+
const contentBlocks = [];
|
|
1425
|
+
if (msg.content) {
|
|
1426
|
+
contentBlocks.push({ type: "text", text: msg.content });
|
|
1427
|
+
}
|
|
1428
|
+
for (const tc of msg.tool_calls) {
|
|
1429
|
+
let inputArgs = tc.function.arguments;
|
|
1430
|
+
if (typeof inputArgs === "string") {
|
|
1431
|
+
try {
|
|
1432
|
+
inputArgs = JSON.parse(inputArgs);
|
|
1433
|
+
} catch (_) {
|
|
1434
|
+
inputArgs = {};
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
contentBlocks.push({
|
|
1438
|
+
type: "tool_use",
|
|
1439
|
+
id: tc.id,
|
|
1440
|
+
name: tc.function.name,
|
|
1441
|
+
input: inputArgs
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
result.push({ role: "assistant", content: contentBlocks });
|
|
1445
|
+
} else {
|
|
1446
|
+
result.push({ role: "assistant", content: msg.content || "" });
|
|
1447
|
+
}
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
result.push({ role: "user", content: msg.content || "" });
|
|
1331
1451
|
}
|
|
1332
|
-
return
|
|
1452
|
+
return result;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Convert OpenAI-style tool definitions to Anthropic format.
|
|
1456
|
+
* OpenAI: { type: 'function', function: { name, description, parameters } }
|
|
1457
|
+
* Anthropic: { name, description, input_schema }
|
|
1458
|
+
*/
|
|
1459
|
+
_formatTools(tools) {
|
|
1460
|
+
return tools.map((tool) => {
|
|
1461
|
+
const fn = tool.function || tool;
|
|
1462
|
+
return {
|
|
1463
|
+
name: fn.name,
|
|
1464
|
+
description: fn.description || "",
|
|
1465
|
+
input_schema: fn.parameters || fn.input_schema || { type: "object", properties: {} }
|
|
1466
|
+
};
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
async executeTools(tool_calls, messages, tenantId, toolImplementations, env) {
|
|
1470
|
+
const toolResults = await Promise.all(
|
|
1471
|
+
tool_calls.map(async (toolCall) => {
|
|
1472
|
+
const toolName = toolCall.function.name;
|
|
1473
|
+
const tool = toolImplementations[toolName];
|
|
1474
|
+
console.log(`[Tool Call] ${toolName} with arguments:`, toolCall.function.arguments);
|
|
1475
|
+
if (!tool) {
|
|
1476
|
+
console.error(`[Tool Error] Tool '${toolName}' not found`);
|
|
1477
|
+
return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Tool '${toolName}' not found.` }) };
|
|
1478
|
+
}
|
|
1479
|
+
try {
|
|
1480
|
+
const output = await tool(toolCall.function.arguments, { env, tenantId });
|
|
1481
|
+
const preview = typeof output === "string" ? output.length > 200 ? output.substring(0, 200) + "..." : output : JSON.stringify(output).substring(0, 200) + "...";
|
|
1482
|
+
console.log(`[Tool Result] ${toolName} returned:`, preview);
|
|
1483
|
+
return { tool_call_id: toolCall.id, output };
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
console.error(`[Tool Error] ${toolName} failed:`, error.message);
|
|
1486
|
+
return { tool_call_id: toolCall.id, output: JSON.stringify({ error: `Error executing tool '${toolName}': ${error.message}` }) };
|
|
1487
|
+
}
|
|
1488
|
+
})
|
|
1489
|
+
);
|
|
1490
|
+
toolResults.forEach(
|
|
1491
|
+
(result) => messages.push({ role: "tool", tool_call_id: result.tool_call_id, content: result.output })
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
_getModelForTier(tier) {
|
|
1495
|
+
return this.models[tier] || this.defaultModel;
|
|
1333
1496
|
}
|
|
1334
1497
|
_shouldAutoParse(options) {
|
|
1335
1498
|
if (!options.responseFormat) return false;
|
|
@@ -1350,15 +1513,10 @@ ${jsonPrompt}`;
|
|
|
1350
1513
|
}
|
|
1351
1514
|
normalizeFinishReason(providerReason) {
|
|
1352
1515
|
const lower = (providerReason || "").toLowerCase();
|
|
1353
|
-
if (lower === "end_turn")
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
if (lower === "
|
|
1357
|
-
return "truncated";
|
|
1358
|
-
}
|
|
1359
|
-
if (lower === "stop_sequence") {
|
|
1360
|
-
return "completed";
|
|
1361
|
-
}
|
|
1516
|
+
if (lower === "end_turn") return "completed";
|
|
1517
|
+
if (lower === "max_tokens") return "truncated";
|
|
1518
|
+
if (lower === "stop_sequence") return "completed";
|
|
1519
|
+
if (lower === "tool_use") return "tool_call";
|
|
1362
1520
|
return super.normalizeFinishReason(providerReason);
|
|
1363
1521
|
}
|
|
1364
1522
|
};
|