portos-ai-toolkit 0.4.0 → 0.5.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/package.json
CHANGED
|
@@ -16,6 +16,23 @@
|
|
|
16
16
|
"enabled": true,
|
|
17
17
|
"envVars": {}
|
|
18
18
|
},
|
|
19
|
+
"claude-code-bedrock": {
|
|
20
|
+
"id": "claude-code-bedrock",
|
|
21
|
+
"name": "Claude Code CLI: Bedrock",
|
|
22
|
+
"type": "cli",
|
|
23
|
+
"command": "claude",
|
|
24
|
+
"args": ["--print"],
|
|
25
|
+
"models": ["us.anthropic.claude-sonnet-4-5-20250929-v1:0", "global.anthropic.claude-opus-4-5-20251101-v1:0"],
|
|
26
|
+
"defaultModel": "global.anthropic.claude-opus-4-5-20251101-v1:0",
|
|
27
|
+
"lightModel": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
|
28
|
+
"mediumModel": "global.anthropic.claude-opus-4-5-20251101-v1:0",
|
|
29
|
+
"heavyModel": "global.anthropic.claude-opus-4-5-20251101-v1:0",
|
|
30
|
+
"timeout": 300000,
|
|
31
|
+
"enabled": false,
|
|
32
|
+
"envVars": {
|
|
33
|
+
"CLAUDE_CODE_USE_BEDROCK": "1"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
19
36
|
"codex": {
|
|
20
37
|
"id": "codex",
|
|
21
38
|
"name": "Codex CLI",
|
package/src/server/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface ProviderService {
|
|
|
12
12
|
deleteProvider(id: string): Promise<boolean>;
|
|
13
13
|
testProvider(id: string): Promise<{ success: boolean; [key: string]: any }>;
|
|
14
14
|
refreshProviderModels(id: string): Promise<any | null>;
|
|
15
|
+
getSampleProviders(): Promise<any[]>;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface RunnerService {
|
package/src/server/providers.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
import { exec } from 'child_process';
|
|
5
6
|
import { promisify } from 'util';
|
|
6
7
|
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const DEFAULT_SAMPLE_PATH = join(__dirname, '../defaults/providers.sample.json');
|
|
10
|
+
|
|
7
11
|
const execAsync = promisify(exec);
|
|
8
12
|
|
|
9
13
|
/**
|
|
@@ -226,25 +230,33 @@ export function createProviderService(config = {}) {
|
|
|
226
230
|
},
|
|
227
231
|
|
|
228
232
|
/**
|
|
229
|
-
* Refresh models from
|
|
233
|
+
* Refresh models from provider using provider-specific strategies
|
|
230
234
|
*/
|
|
231
235
|
async refreshProviderModels(id) {
|
|
232
236
|
const data = await loadProviders();
|
|
233
237
|
const provider = data.providers[id];
|
|
234
238
|
|
|
235
|
-
if (!provider
|
|
239
|
+
if (!provider) {
|
|
236
240
|
return null;
|
|
237
241
|
}
|
|
238
242
|
|
|
239
|
-
|
|
240
|
-
const response = await fetch(modelsUrl, {
|
|
241
|
-
headers: provider.apiKey ? { 'Authorization': `Bearer ${provider.apiKey}` } : {}
|
|
242
|
-
}).catch(() => null);
|
|
243
|
+
let models = [];
|
|
243
244
|
|
|
244
|
-
|
|
245
|
+
try {
|
|
246
|
+
// Provider-specific refresh strategies
|
|
247
|
+
if (provider.type === 'api') {
|
|
248
|
+
models = await this._refreshAPIProviderModels(provider);
|
|
249
|
+
} else if (provider.type === 'cli') {
|
|
250
|
+
models = await this._refreshCLIProviderModels(provider);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(`Failed to refresh models for ${provider.name}:`, error.message);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
245
256
|
|
|
246
|
-
|
|
247
|
-
|
|
257
|
+
if (!models || models.length === 0) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
248
260
|
|
|
249
261
|
const updatedProvider = {
|
|
250
262
|
...data.providers[id],
|
|
@@ -254,6 +266,155 @@ export function createProviderService(config = {}) {
|
|
|
254
266
|
data.providers[id] = updatedProvider;
|
|
255
267
|
await saveProviders(data);
|
|
256
268
|
return updatedProvider;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Refresh models from API providers
|
|
273
|
+
* Supports OpenAI-compatible endpoints (OpenAI, LM Studio, etc.)
|
|
274
|
+
* and Ollama-style endpoints
|
|
275
|
+
*/
|
|
276
|
+
async _refreshAPIProviderModels(provider) {
|
|
277
|
+
// Try Ollama format first if endpoint suggests it
|
|
278
|
+
if (provider.endpoint?.includes('ollama') || provider.endpoint?.includes(':11434')) {
|
|
279
|
+
const ollamaUrl = `${provider.endpoint}/api/tags`;
|
|
280
|
+
const response = await fetch(ollamaUrl).catch(() => null);
|
|
281
|
+
|
|
282
|
+
if (response?.ok) {
|
|
283
|
+
const data = await response.json().catch(() => null);
|
|
284
|
+
if (data?.models) {
|
|
285
|
+
return data.models.map(m => m.name || m.model);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Try OpenAI-compatible format (default)
|
|
291
|
+
const modelsUrl = `${provider.endpoint}/models`;
|
|
292
|
+
const headers = {};
|
|
293
|
+
|
|
294
|
+
if (provider.apiKey) {
|
|
295
|
+
headers['Authorization'] = `Bearer ${provider.apiKey}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const response = await fetch(modelsUrl, { headers }).catch(() => null);
|
|
299
|
+
|
|
300
|
+
if (!response?.ok) {
|
|
301
|
+
throw new Error(`HTTP ${response?.status || 'error'}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const responseData = await response.json().catch(() => ({ data: [] }));
|
|
305
|
+
|
|
306
|
+
// OpenAI format: { data: [{ id: "model-name" }] }
|
|
307
|
+
if (responseData.data && Array.isArray(responseData.data)) {
|
|
308
|
+
return responseData.data.map(m => m.id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Alternative format: { models: ["model-name"] }
|
|
312
|
+
if (responseData.models && Array.isArray(responseData.models)) {
|
|
313
|
+
return responseData.models;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return [];
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Refresh models from CLI providers using provider-specific APIs
|
|
321
|
+
*/
|
|
322
|
+
async _refreshCLIProviderModels(provider) {
|
|
323
|
+
const providerName = provider.name.toLowerCase();
|
|
324
|
+
|
|
325
|
+
// Claude/Anthropic - fetch from Anthropic API
|
|
326
|
+
if (providerName.includes('claude') || provider.command === 'claude') {
|
|
327
|
+
return await this._fetchAnthropicModels(provider);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Gemini - fetch from Google AI API
|
|
331
|
+
if (providerName.includes('gemini') || provider.command === 'gemini') {
|
|
332
|
+
return await this._fetchGeminiModels(provider);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// For other CLI providers, we can't refresh models
|
|
336
|
+
throw new Error('Model refresh not supported for this CLI provider');
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Fetch available Claude models from Anthropic API
|
|
341
|
+
*/
|
|
342
|
+
async _fetchAnthropicModels(provider) {
|
|
343
|
+
// Check for API key in provider or environment
|
|
344
|
+
const apiKey = provider.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
345
|
+
|
|
346
|
+
if (!apiKey) {
|
|
347
|
+
throw new Error('Anthropic API key required for model refresh');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Known Claude models as of January 2025
|
|
351
|
+
// Anthropic doesn't have a public models list endpoint yet
|
|
352
|
+
return [
|
|
353
|
+
'claude-opus-4-6',
|
|
354
|
+
'claude-opus-4',
|
|
355
|
+
'claude-sonnet-4-6',
|
|
356
|
+
'claude-sonnet-4',
|
|
357
|
+
'claude-3-7-sonnet-20250219',
|
|
358
|
+
'claude-3-5-sonnet-20241022',
|
|
359
|
+
'claude-3-5-sonnet-20240620',
|
|
360
|
+
'claude-3-5-haiku-20241022',
|
|
361
|
+
'claude-3-opus-20240229',
|
|
362
|
+
'claude-3-sonnet-20240229',
|
|
363
|
+
'claude-3-haiku-20240307'
|
|
364
|
+
];
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Fetch available Gemini models from Google AI API
|
|
369
|
+
*/
|
|
370
|
+
async _fetchGeminiModels(provider) {
|
|
371
|
+
const apiKey = provider.apiKey || process.env.GOOGLE_API_KEY;
|
|
372
|
+
|
|
373
|
+
if (!apiKey) {
|
|
374
|
+
throw new Error('Google API key required for model refresh');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const response = await fetch(
|
|
378
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
|
379
|
+
).catch(() => null);
|
|
380
|
+
|
|
381
|
+
if (!response?.ok) {
|
|
382
|
+
throw new Error(`HTTP ${response?.status || 'error'}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const data = await response.json().catch(() => ({ models: [] }));
|
|
386
|
+
|
|
387
|
+
// Filter to only generative models
|
|
388
|
+
return (data.models || [])
|
|
389
|
+
.filter(m => m.supportedGenerationMethods?.includes('generateContent'))
|
|
390
|
+
.map(m => m.name.replace('models/', ''));
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get sample providers not yet in user's configuration.
|
|
395
|
+
* Reads toolkit's built-in defaults and overlays with host app's sample file.
|
|
396
|
+
*/
|
|
397
|
+
async getSampleProviders() {
|
|
398
|
+
const data = await loadProviders();
|
|
399
|
+
const existingIds = new Set(Object.keys(data.providers));
|
|
400
|
+
|
|
401
|
+
// Read toolkit's built-in sample
|
|
402
|
+
let sampleProviders = {};
|
|
403
|
+
if (existsSync(DEFAULT_SAMPLE_PATH)) {
|
|
404
|
+
const content = await readFile(DEFAULT_SAMPLE_PATH, 'utf-8');
|
|
405
|
+
const parsed = JSON.parse(content);
|
|
406
|
+
sampleProviders = { ...parsed.providers };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Overlay with host app's sample (takes precedence)
|
|
410
|
+
if (sampleFile && existsSync(sampleFile)) {
|
|
411
|
+
const content = await readFile(sampleFile, 'utf-8');
|
|
412
|
+
const parsed = JSON.parse(content);
|
|
413
|
+
sampleProviders = { ...sampleProviders, ...parsed.providers };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Filter out providers already in user's config
|
|
417
|
+
return Object.values(sampleProviders).filter(p => !existingIds.has(p.id));
|
|
257
418
|
}
|
|
258
419
|
};
|
|
259
420
|
}
|
|
@@ -117,4 +117,75 @@ describe('Provider Service', () => {
|
|
|
117
117
|
})
|
|
118
118
|
).rejects.toThrow('Provider with this ID already exists');
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
describe('getSampleProviders', () => {
|
|
122
|
+
it('should return sample providers from default sample file', async () => {
|
|
123
|
+
// No providers created yet — all samples should be returned
|
|
124
|
+
const samples = await providerService.getSampleProviders();
|
|
125
|
+
expect(Array.isArray(samples)).toBe(true);
|
|
126
|
+
expect(samples.length).toBeGreaterThan(0);
|
|
127
|
+
// Should include claude-code-bedrock from the default sample
|
|
128
|
+
const bedrock = samples.find(p => p.id === 'claude-code-bedrock');
|
|
129
|
+
expect(bedrock).toBeDefined();
|
|
130
|
+
expect(bedrock.name).toBe('Claude Code CLI: Bedrock');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should exclude providers already in user config', async () => {
|
|
134
|
+
// Create a provider with an ID that matches a sample
|
|
135
|
+
await providerService.createProvider({
|
|
136
|
+
id: 'claude-code',
|
|
137
|
+
name: 'Claude Code CLI',
|
|
138
|
+
type: 'cli',
|
|
139
|
+
command: 'claude'
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const samples = await providerService.getSampleProviders();
|
|
143
|
+
const claudeCode = samples.find(p => p.id === 'claude-code');
|
|
144
|
+
expect(claudeCode).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should overlay host app sample over toolkit defaults', async () => {
|
|
148
|
+
// Pre-create providers.json with one existing provider so loadProviders
|
|
149
|
+
// doesn't bootstrap from sampleFile
|
|
150
|
+
const providersPath = join(TEST_DATA_DIR, 'providers-overlay.json');
|
|
151
|
+
await writeFile(providersPath, JSON.stringify({
|
|
152
|
+
activeProvider: 'existing',
|
|
153
|
+
providers: {
|
|
154
|
+
existing: { id: 'existing', name: 'Existing', type: 'cli', command: 'test' }
|
|
155
|
+
}
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
// Create a host app sample with a unique provider
|
|
159
|
+
const samplePath = join(TEST_DATA_DIR, 'custom-sample.json');
|
|
160
|
+
await writeFile(samplePath, JSON.stringify({
|
|
161
|
+
activeProvider: 'custom-cli',
|
|
162
|
+
providers: {
|
|
163
|
+
'custom-cli': {
|
|
164
|
+
id: 'custom-cli',
|
|
165
|
+
name: 'Custom CLI',
|
|
166
|
+
type: 'cli',
|
|
167
|
+
command: 'custom',
|
|
168
|
+
args: [],
|
|
169
|
+
models: [],
|
|
170
|
+
timeout: 300000,
|
|
171
|
+
enabled: true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
const serviceWithSample = createProviderService({
|
|
177
|
+
dataDir: TEST_DATA_DIR,
|
|
178
|
+
providersFile: 'providers-overlay.json',
|
|
179
|
+
sampleFile: samplePath
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const samples = await serviceWithSample.getSampleProviders();
|
|
183
|
+
const custom = samples.find(p => p.id === 'custom-cli');
|
|
184
|
+
expect(custom).toBeDefined();
|
|
185
|
+
expect(custom.name).toBe('Custom CLI');
|
|
186
|
+
// 'existing' should NOT appear (already in user's config)
|
|
187
|
+
const existing = samples.find(p => p.id === 'existing');
|
|
188
|
+
expect(existing).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
120
191
|
});
|
|
@@ -35,6 +35,12 @@ export function createProvidersRoutes(providerService, options = {}) {
|
|
|
35
35
|
res.json(provider);
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
|
+
// GET /providers/samples - Get sample providers not yet in user's config
|
|
39
|
+
router.get('/samples', asyncHandler(async (req, res) => {
|
|
40
|
+
const providers = await providerService.getSampleProviders();
|
|
41
|
+
res.json({ providers });
|
|
42
|
+
}));
|
|
43
|
+
|
|
38
44
|
// GET /providers/:id - Get provider by ID
|
|
39
45
|
router.get('/:id', asyncHandler(async (req, res) => {
|
|
40
46
|
const provider = await providerService.getProviderById(req.params.id);
|
package/src/server/runner.js
CHANGED
|
@@ -188,6 +188,7 @@ export function createRunnerService(config = {}) {
|
|
|
188
188
|
await writeFile(join(runDir, 'output.txt'), '');
|
|
189
189
|
|
|
190
190
|
hooks.onRunCreated?.(metadata);
|
|
191
|
+
console.log(`🤖 AI run [${source}]: ${provider.name}/${metadata.model}`);
|
|
191
192
|
|
|
192
193
|
const effectiveTimeout = timeout || provider.timeout;
|
|
193
194
|
|