portos-ai-toolkit 0.1.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/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +76 -0
- package/src/client/api.js +102 -0
- package/src/client/components/ProviderDropdown.jsx +39 -0
- package/src/client/components/index.js +5 -0
- package/src/client/hooks/index.js +6 -0
- package/src/client/hooks/useProviders.js +96 -0
- package/src/client/hooks/useRuns.js +94 -0
- package/src/client/index.js +11 -0
- package/src/client/pages/AIProviders.jsx +665 -0
- package/src/index.js +8 -0
- package/src/server/index.d.ts +87 -0
- package/src/server/index.js +95 -0
- package/src/server/prompts.js +234 -0
- package/src/server/providers.js +253 -0
- package/src/server/providers.test.js +120 -0
- package/src/server/routes/prompts.js +96 -0
- package/src/server/routes/providers.js +105 -0
- package/src/server/routes/runs.js +157 -0
- package/src/server/runner.js +475 -0
- package/src/server/validation.js +51 -0
- package/src/shared/constants.js +26 -0
- package/src/shared/index.js +5 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript declarations for @portos/ai-toolkit/server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ProviderService {
|
|
6
|
+
getAllProviders(): Promise<{ activeProvider: string | null; providers: any[] }>;
|
|
7
|
+
getProviderById(id: string): Promise<any | null>;
|
|
8
|
+
getActiveProvider(): Promise<any | null>;
|
|
9
|
+
setActiveProvider(id: string): Promise<any | null>;
|
|
10
|
+
createProvider(data: any): Promise<any>;
|
|
11
|
+
updateProvider(id: string, updates: any): Promise<any | null>;
|
|
12
|
+
deleteProvider(id: string): Promise<boolean>;
|
|
13
|
+
testProvider(id: string): Promise<{ success: boolean; [key: string]: any }>;
|
|
14
|
+
refreshProviderModels(id: string): Promise<any | null>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RunnerService {
|
|
18
|
+
createRun(options: any): Promise<any>;
|
|
19
|
+
executeCliRun(...args: any[]): Promise<string>;
|
|
20
|
+
executeApiRun(...args: any[]): Promise<string>;
|
|
21
|
+
stopRun(runId: string): Promise<boolean>;
|
|
22
|
+
getRun(runId: string): Promise<any | null>;
|
|
23
|
+
getRunOutput(runId: string): Promise<string | null>;
|
|
24
|
+
getRunPrompt(runId: string): Promise<string | null>;
|
|
25
|
+
listRuns(limit?: number, offset?: number, source?: string): Promise<{ total: number; runs: any[] }>;
|
|
26
|
+
deleteRun(runId: string): Promise<boolean>;
|
|
27
|
+
deleteFailedRuns(): Promise<number>;
|
|
28
|
+
isRunActive(runId: string): Promise<boolean>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PromptsService {
|
|
32
|
+
init(): Promise<void>;
|
|
33
|
+
getStages(): Record<string, any>;
|
|
34
|
+
getStage(name: string): any | null;
|
|
35
|
+
getStageTemplate(name: string): Promise<string | null>;
|
|
36
|
+
updateStageTemplate(name: string, content: string): Promise<void>;
|
|
37
|
+
updateStageConfig(name: string, config: any): Promise<void>;
|
|
38
|
+
createStage(stageName: string, config: any, template?: string): Promise<void>;
|
|
39
|
+
deleteStage(stageName: string): Promise<void>;
|
|
40
|
+
getVariables(): Record<string, any>;
|
|
41
|
+
getVariable(key: string): any | null;
|
|
42
|
+
updateVariable(key: string, data: any): Promise<void>;
|
|
43
|
+
createVariable(key: string, data: any): Promise<void>;
|
|
44
|
+
deleteVariable(key: string): Promise<void>;
|
|
45
|
+
buildPrompt(stageName: string, data?: any): Promise<string>;
|
|
46
|
+
previewPrompt(stageName: string, testData?: any): Promise<string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AIToolkit {
|
|
50
|
+
services: {
|
|
51
|
+
providers: ProviderService;
|
|
52
|
+
runner: RunnerService;
|
|
53
|
+
prompts: PromptsService;
|
|
54
|
+
};
|
|
55
|
+
routes: {
|
|
56
|
+
providers: any;
|
|
57
|
+
runs: any;
|
|
58
|
+
prompts: any;
|
|
59
|
+
};
|
|
60
|
+
mountRoutes(app: any, basePath?: string): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AIToolkitConfig {
|
|
64
|
+
dataDir?: string;
|
|
65
|
+
providersFile?: string;
|
|
66
|
+
runsDir?: string;
|
|
67
|
+
promptsDir?: string;
|
|
68
|
+
screenshotsDir?: string;
|
|
69
|
+
sampleProvidersFile?: string;
|
|
70
|
+
io?: any;
|
|
71
|
+
asyncHandler?: (fn: any) => any;
|
|
72
|
+
hooks?: {
|
|
73
|
+
onRunCreated?: (metadata: any) => void;
|
|
74
|
+
onRunStarted?: (run: any) => void;
|
|
75
|
+
onRunCompleted?: (metadata: any, output: string) => void;
|
|
76
|
+
onRunFailed?: (metadata: any, error: string, output: string) => void;
|
|
77
|
+
};
|
|
78
|
+
maxConcurrentRuns?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createAIToolkit(config?: AIToolkitConfig): AIToolkit;
|
|
82
|
+
export function createProviderService(config?: Partial<AIToolkitConfig>): ProviderService;
|
|
83
|
+
export function createRunnerService(config?: Partial<AIToolkitConfig>): RunnerService;
|
|
84
|
+
export function createPromptsService(config?: Partial<AIToolkitConfig>): PromptsService;
|
|
85
|
+
export function createProvidersRoutes(service: ProviderService, options?: any): any;
|
|
86
|
+
export function createRunsRoutes(service: RunnerService, options?: any): any;
|
|
87
|
+
export function createPromptsRoutes(service: PromptsService, options?: any): any;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Toolkit Server
|
|
3
|
+
* Configurable AI provider, runner, and prompt services with Express routes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createProviderService } from './providers.js';
|
|
7
|
+
import { createRunnerService } from './runner.js';
|
|
8
|
+
import { createPromptsService } from './prompts.js';
|
|
9
|
+
import { createProvidersRoutes } from './routes/providers.js';
|
|
10
|
+
import { createRunsRoutes } from './routes/runs.js';
|
|
11
|
+
import { createPromptsRoutes } from './routes/prompts.js';
|
|
12
|
+
|
|
13
|
+
export * from './validation.js';
|
|
14
|
+
export { createProviderService, createRunnerService, createPromptsService };
|
|
15
|
+
export { createProvidersRoutes, createRunsRoutes, createPromptsRoutes };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a complete AI toolkit instance with services and routes
|
|
19
|
+
*/
|
|
20
|
+
export function createAIToolkit(config = {}) {
|
|
21
|
+
const {
|
|
22
|
+
dataDir = './data',
|
|
23
|
+
providersFile = 'providers.json',
|
|
24
|
+
runsDir = 'runs',
|
|
25
|
+
promptsDir = 'prompts',
|
|
26
|
+
screenshotsDir = './data/screenshots',
|
|
27
|
+
sampleProvidersFile = null,
|
|
28
|
+
|
|
29
|
+
// Socket.IO instance for real-time updates
|
|
30
|
+
io = null,
|
|
31
|
+
|
|
32
|
+
// Optional async handler wrapper (e.g., for error handling)
|
|
33
|
+
asyncHandler = (fn) => fn,
|
|
34
|
+
|
|
35
|
+
// Hooks for lifecycle events
|
|
36
|
+
hooks = {},
|
|
37
|
+
|
|
38
|
+
// Runner config
|
|
39
|
+
maxConcurrentRuns = 5
|
|
40
|
+
} = config;
|
|
41
|
+
|
|
42
|
+
// Create services
|
|
43
|
+
const providerService = createProviderService({
|
|
44
|
+
dataDir,
|
|
45
|
+
providersFile,
|
|
46
|
+
sampleFile: sampleProvidersFile
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const runnerService = createRunnerService({
|
|
50
|
+
dataDir,
|
|
51
|
+
runsDir,
|
|
52
|
+
screenshotsDir,
|
|
53
|
+
providerService,
|
|
54
|
+
hooks,
|
|
55
|
+
maxConcurrentRuns
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const promptsService = createPromptsService({
|
|
59
|
+
dataDir,
|
|
60
|
+
promptsDir
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Initialize prompts service
|
|
64
|
+
promptsService.init().catch(err => {
|
|
65
|
+
console.error(`❌ Failed to initialize prompts: ${err.message}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Create routes
|
|
69
|
+
const providersRouter = createProvidersRoutes(providerService, { asyncHandler });
|
|
70
|
+
const runsRouter = createRunsRoutes(runnerService, { asyncHandler, io });
|
|
71
|
+
const promptsRouter = createPromptsRoutes(promptsService, { asyncHandler });
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
// Services
|
|
75
|
+
services: {
|
|
76
|
+
providers: providerService,
|
|
77
|
+
runner: runnerService,
|
|
78
|
+
prompts: promptsService
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Routes
|
|
82
|
+
routes: {
|
|
83
|
+
providers: providersRouter,
|
|
84
|
+
runs: runsRouter,
|
|
85
|
+
prompts: promptsRouter
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Convenience method to mount all routes
|
|
89
|
+
mountRoutes(app, basePath = '/api') {
|
|
90
|
+
app.use(`${basePath}/providers`, providersRouter);
|
|
91
|
+
app.use(`${basePath}/runs`, runsRouter);
|
|
92
|
+
app.use(`${basePath}/prompts`, promptsRouter);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a prompts service with configurable storage
|
|
7
|
+
*/
|
|
8
|
+
export function createPromptsService(config = {}) {
|
|
9
|
+
const {
|
|
10
|
+
dataDir = './data',
|
|
11
|
+
promptsDir = 'prompts'
|
|
12
|
+
} = config;
|
|
13
|
+
|
|
14
|
+
const PROMPTS_PATH = join(dataDir, promptsDir);
|
|
15
|
+
|
|
16
|
+
let stageConfig = null;
|
|
17
|
+
let variables = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load or reload prompts configuration
|
|
21
|
+
*/
|
|
22
|
+
async function loadPrompts() {
|
|
23
|
+
const configPath = join(PROMPTS_PATH, 'stage-config.json');
|
|
24
|
+
const varsPath = join(PROMPTS_PATH, 'variables.json');
|
|
25
|
+
|
|
26
|
+
if (existsSync(configPath)) {
|
|
27
|
+
stageConfig = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
28
|
+
} else {
|
|
29
|
+
stageConfig = { stages: {} };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (existsSync(varsPath)) {
|
|
33
|
+
variables = JSON.parse(await readFile(varsPath, 'utf-8'));
|
|
34
|
+
} else {
|
|
35
|
+
variables = { variables: {} };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`📝 Loaded ${Object.keys(stageConfig.stages || {}).length} prompt stages`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Apply Mustache-like template substitution
|
|
43
|
+
*/
|
|
44
|
+
function applyTemplate(template, data) {
|
|
45
|
+
let result = template;
|
|
46
|
+
|
|
47
|
+
// Handle sections {{#key}}...{{/key}}
|
|
48
|
+
result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (match, key, content) => {
|
|
49
|
+
const value = data[key];
|
|
50
|
+
if (!value) return '';
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
return value.map(item => {
|
|
53
|
+
if (typeof item === 'object') {
|
|
54
|
+
return applyTemplate(content, item);
|
|
55
|
+
}
|
|
56
|
+
return content.replace(/\{\{\.\}\}/g, item);
|
|
57
|
+
}).join('');
|
|
58
|
+
}
|
|
59
|
+
return applyTemplate(content, data);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Handle inverted sections {{^key}}...{{/key}}
|
|
63
|
+
result = result.replace(/\{\{\^(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (match, key, content) => {
|
|
64
|
+
return data[key] ? '' : content;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Handle simple variables {{key}}
|
|
68
|
+
result = result.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
69
|
+
return data[key] !== undefined ? String(data[key]) : '';
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return result.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
/**
|
|
77
|
+
* Load prompts configuration
|
|
78
|
+
*/
|
|
79
|
+
async init() {
|
|
80
|
+
await loadPrompts();
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all stages
|
|
85
|
+
*/
|
|
86
|
+
getStages() {
|
|
87
|
+
return stageConfig?.stages || {};
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get a single stage
|
|
92
|
+
*/
|
|
93
|
+
getStage(stageName) {
|
|
94
|
+
return stageConfig?.stages?.[stageName] || null;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get stage template content
|
|
99
|
+
*/
|
|
100
|
+
async getStageTemplate(stageName) {
|
|
101
|
+
const templatePath = join(PROMPTS_PATH, 'stages', `${stageName}.md`);
|
|
102
|
+
if (!existsSync(templatePath)) return null;
|
|
103
|
+
return readFile(templatePath, 'utf-8');
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update stage template
|
|
108
|
+
*/
|
|
109
|
+
async updateStageTemplate(stageName, content) {
|
|
110
|
+
const stagesDir = join(PROMPTS_PATH, 'stages');
|
|
111
|
+
if (!existsSync(stagesDir)) await mkdir(stagesDir, { recursive: true });
|
|
112
|
+
await writeFile(join(stagesDir, `${stageName}.md`), content);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Update stage configuration
|
|
117
|
+
*/
|
|
118
|
+
async updateStageConfig(stageName, updatedConfig) {
|
|
119
|
+
if (!stageConfig) await loadPrompts();
|
|
120
|
+
stageConfig.stages[stageName] = { ...stageConfig.stages[stageName], ...updatedConfig };
|
|
121
|
+
await writeFile(join(PROMPTS_PATH, 'stage-config.json'), JSON.stringify(stageConfig, null, 2));
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a new stage
|
|
126
|
+
*/
|
|
127
|
+
async createStage(stageName, config, template = '') {
|
|
128
|
+
if (!stageConfig) await loadPrompts();
|
|
129
|
+
if (stageConfig.stages[stageName]) {
|
|
130
|
+
throw new Error(`Stage ${stageName} already exists`);
|
|
131
|
+
}
|
|
132
|
+
stageConfig.stages[stageName] = config;
|
|
133
|
+
await writeFile(join(PROMPTS_PATH, 'stage-config.json'), JSON.stringify(stageConfig, null, 2));
|
|
134
|
+
|
|
135
|
+
// Create template file
|
|
136
|
+
const stagesDir = join(PROMPTS_PATH, 'stages');
|
|
137
|
+
if (!existsSync(stagesDir)) await mkdir(stagesDir, { recursive: true });
|
|
138
|
+
await writeFile(join(stagesDir, `${stageName}.md`), template);
|
|
139
|
+
|
|
140
|
+
console.log(`✅ Created prompt stage: ${stageName}`);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Delete a stage
|
|
145
|
+
*/
|
|
146
|
+
async deleteStage(stageName) {
|
|
147
|
+
if (!stageConfig) await loadPrompts();
|
|
148
|
+
if (!stageConfig.stages[stageName]) {
|
|
149
|
+
throw new Error(`Stage ${stageName} not found`);
|
|
150
|
+
}
|
|
151
|
+
delete stageConfig.stages[stageName];
|
|
152
|
+
await writeFile(join(PROMPTS_PATH, 'stage-config.json'), JSON.stringify(stageConfig, null, 2));
|
|
153
|
+
|
|
154
|
+
// Delete template file if it exists
|
|
155
|
+
const templatePath = join(PROMPTS_PATH, 'stages', `${stageName}.md`);
|
|
156
|
+
if (existsSync(templatePath)) {
|
|
157
|
+
const { unlink } = await import('fs/promises');
|
|
158
|
+
await unlink(templatePath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`🗑️ Deleted prompt stage: ${stageName}`);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get all variables
|
|
166
|
+
*/
|
|
167
|
+
getVariables() {
|
|
168
|
+
return variables?.variables || {};
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get a single variable
|
|
173
|
+
*/
|
|
174
|
+
getVariable(key) {
|
|
175
|
+
return variables?.variables?.[key] || null;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Update a variable
|
|
180
|
+
*/
|
|
181
|
+
async updateVariable(key, data) {
|
|
182
|
+
if (!variables) await loadPrompts();
|
|
183
|
+
variables.variables[key] = { ...variables.variables[key], ...data };
|
|
184
|
+
await writeFile(join(PROMPTS_PATH, 'variables.json'), JSON.stringify(variables, null, 2));
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create a new variable
|
|
189
|
+
*/
|
|
190
|
+
async createVariable(key, data) {
|
|
191
|
+
if (!variables) await loadPrompts();
|
|
192
|
+
if (variables.variables[key]) {
|
|
193
|
+
throw new Error(`Variable ${key} already exists`);
|
|
194
|
+
}
|
|
195
|
+
variables.variables[key] = data;
|
|
196
|
+
await writeFile(join(PROMPTS_PATH, 'variables.json'), JSON.stringify(variables, null, 2));
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Delete a variable
|
|
201
|
+
*/
|
|
202
|
+
async deleteVariable(key) {
|
|
203
|
+
if (!variables) await loadPrompts();
|
|
204
|
+
delete variables.variables[key];
|
|
205
|
+
await writeFile(join(PROMPTS_PATH, 'variables.json'), JSON.stringify(variables, null, 2));
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Build a prompt from template and variables
|
|
210
|
+
*/
|
|
211
|
+
async buildPrompt(stageName, data = {}) {
|
|
212
|
+
const stage = stageConfig?.stages?.[stageName];
|
|
213
|
+
if (!stage) throw new Error(`Stage ${stageName} not found`);
|
|
214
|
+
|
|
215
|
+
let template = await this.getStageTemplate(stageName);
|
|
216
|
+
if (!template) throw new Error(`Template for ${stageName} not found`);
|
|
217
|
+
|
|
218
|
+
const allVars = { ...data };
|
|
219
|
+
for (const varName of stage.variables || []) {
|
|
220
|
+
const v = variables?.variables?.[varName];
|
|
221
|
+
if (v) allVars[varName] = v.content;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return applyTemplate(template, allVars);
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Preview a prompt with test data
|
|
229
|
+
*/
|
|
230
|
+
async previewPrompt(stageName, testData = {}) {
|
|
231
|
+
return this.buildPrompt(stageName, testData);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a provider service with configurable storage
|
|
11
|
+
*/
|
|
12
|
+
export function createProviderService(config = {}) {
|
|
13
|
+
const {
|
|
14
|
+
dataDir = './data',
|
|
15
|
+
providersFile = 'providers.json',
|
|
16
|
+
sampleFile = null
|
|
17
|
+
} = config;
|
|
18
|
+
|
|
19
|
+
const PROVIDERS_PATH = join(dataDir, providersFile);
|
|
20
|
+
|
|
21
|
+
async function ensureDataDir() {
|
|
22
|
+
if (!existsSync(dataDir)) {
|
|
23
|
+
await mkdir(dataDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function loadProviders() {
|
|
28
|
+
await ensureDataDir();
|
|
29
|
+
|
|
30
|
+
if (!existsSync(PROVIDERS_PATH)) {
|
|
31
|
+
// Copy from sample if exists
|
|
32
|
+
if (sampleFile && existsSync(sampleFile)) {
|
|
33
|
+
const sample = await readFile(sampleFile, 'utf-8');
|
|
34
|
+
await writeFile(PROVIDERS_PATH, sample);
|
|
35
|
+
return JSON.parse(sample);
|
|
36
|
+
}
|
|
37
|
+
return { activeProvider: null, providers: {} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = await readFile(PROVIDERS_PATH, 'utf-8');
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function saveProviders(data) {
|
|
45
|
+
await ensureDataDir();
|
|
46
|
+
await writeFile(PROVIDERS_PATH, JSON.stringify(data, null, 2));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
/**
|
|
51
|
+
* Get all providers with active provider info
|
|
52
|
+
*/
|
|
53
|
+
async getAllProviders() {
|
|
54
|
+
const data = await loadProviders();
|
|
55
|
+
return {
|
|
56
|
+
activeProvider: data.activeProvider,
|
|
57
|
+
providers: Object.values(data.providers)
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get a specific provider by ID
|
|
63
|
+
*/
|
|
64
|
+
async getProviderById(id) {
|
|
65
|
+
const data = await loadProviders();
|
|
66
|
+
return data.providers[id] || null;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the currently active provider
|
|
71
|
+
*/
|
|
72
|
+
async getActiveProvider() {
|
|
73
|
+
const data = await loadProviders();
|
|
74
|
+
if (!data.activeProvider) return null;
|
|
75
|
+
return data.providers[data.activeProvider] || null;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set the active provider
|
|
80
|
+
*/
|
|
81
|
+
async setActiveProvider(id) {
|
|
82
|
+
const data = await loadProviders();
|
|
83
|
+
if (!data.providers[id]) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
data.activeProvider = id;
|
|
87
|
+
await saveProviders(data);
|
|
88
|
+
return data.providers[id];
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a new provider
|
|
93
|
+
*/
|
|
94
|
+
async createProvider(providerData) {
|
|
95
|
+
const data = await loadProviders();
|
|
96
|
+
const id = providerData.id || providerData.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
97
|
+
|
|
98
|
+
if (data.providers[id]) {
|
|
99
|
+
throw new Error('Provider with this ID already exists');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const provider = {
|
|
103
|
+
id,
|
|
104
|
+
name: providerData.name,
|
|
105
|
+
type: providerData.type || 'cli',
|
|
106
|
+
command: providerData.command || null,
|
|
107
|
+
args: providerData.args || [],
|
|
108
|
+
endpoint: providerData.endpoint || null,
|
|
109
|
+
apiKey: providerData.apiKey || '',
|
|
110
|
+
models: providerData.models || [],
|
|
111
|
+
defaultModel: providerData.defaultModel || null,
|
|
112
|
+
timeout: providerData.timeout || 300000,
|
|
113
|
+
enabled: providerData.enabled !== false,
|
|
114
|
+
envVars: providerData.envVars || {}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
data.providers[id] = provider;
|
|
118
|
+
|
|
119
|
+
// Set as active if it's the first provider
|
|
120
|
+
if (!data.activeProvider) {
|
|
121
|
+
data.activeProvider = id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await saveProviders(data);
|
|
125
|
+
return provider;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Update an existing provider
|
|
130
|
+
*/
|
|
131
|
+
async updateProvider(id, updates) {
|
|
132
|
+
const data = await loadProviders();
|
|
133
|
+
|
|
134
|
+
if (!data.providers[id]) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const provider = {
|
|
139
|
+
...data.providers[id],
|
|
140
|
+
...updates,
|
|
141
|
+
id // Prevent ID override
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
data.providers[id] = provider;
|
|
145
|
+
await saveProviders(data);
|
|
146
|
+
return provider;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Delete a provider
|
|
151
|
+
*/
|
|
152
|
+
async deleteProvider(id) {
|
|
153
|
+
const data = await loadProviders();
|
|
154
|
+
|
|
155
|
+
if (!data.providers[id]) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
delete data.providers[id];
|
|
160
|
+
|
|
161
|
+
// Clear active if it was deleted
|
|
162
|
+
if (data.activeProvider === id) {
|
|
163
|
+
const remaining = Object.keys(data.providers);
|
|
164
|
+
data.activeProvider = remaining.length > 0 ? remaining[0] : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await saveProviders(data);
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Test provider connectivity
|
|
173
|
+
*/
|
|
174
|
+
async testProvider(id) {
|
|
175
|
+
const data = await loadProviders();
|
|
176
|
+
const provider = data.providers[id];
|
|
177
|
+
|
|
178
|
+
if (!provider) {
|
|
179
|
+
return { success: false, error: 'Provider not found' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (provider.type === 'cli') {
|
|
183
|
+
// Test CLI availability
|
|
184
|
+
const { stdout, stderr } = await execAsync(`which ${provider.command}`).catch(() => ({ stdout: '', stderr: 'not found' }));
|
|
185
|
+
|
|
186
|
+
if (!stdout.trim()) {
|
|
187
|
+
return { success: false, error: `Command '${provider.command}' not found in PATH` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Try to get version or help
|
|
191
|
+
const { stdout: versionOut } = await execAsync(`${provider.command} --version 2>/dev/null || ${provider.command} -v 2>/dev/null || echo "available"`).catch(() => ({ stdout: 'available' }));
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
success: true,
|
|
195
|
+
path: stdout.trim(),
|
|
196
|
+
version: versionOut.trim()
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (provider.type === 'api') {
|
|
201
|
+
// Test API endpoint
|
|
202
|
+
const modelsUrl = `${provider.endpoint}/models`;
|
|
203
|
+
const response = await fetch(modelsUrl, {
|
|
204
|
+
headers: provider.apiKey ? { 'Authorization': `Bearer ${provider.apiKey}` } : {}
|
|
205
|
+
}).catch(err => ({ ok: false, error: err.message }));
|
|
206
|
+
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
return { success: false, error: `API not reachable: ${response.error || response.status}` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const models = await response.json().catch(() => ({ data: [] }));
|
|
212
|
+
return {
|
|
213
|
+
success: true,
|
|
214
|
+
endpoint: provider.endpoint,
|
|
215
|
+
models: models.data?.map(m => m.id) || []
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { success: false, error: 'Unknown provider type' };
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Refresh models from API provider
|
|
224
|
+
*/
|
|
225
|
+
async refreshProviderModels(id) {
|
|
226
|
+
const data = await loadProviders();
|
|
227
|
+
const provider = data.providers[id];
|
|
228
|
+
|
|
229
|
+
if (!provider || provider.type !== 'api') {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const modelsUrl = `${provider.endpoint}/models`;
|
|
234
|
+
const response = await fetch(modelsUrl, {
|
|
235
|
+
headers: provider.apiKey ? { 'Authorization': `Bearer ${provider.apiKey}` } : {}
|
|
236
|
+
}).catch(() => null);
|
|
237
|
+
|
|
238
|
+
if (!response?.ok) return null;
|
|
239
|
+
|
|
240
|
+
const responseData = await response.json().catch(() => ({ data: [] }));
|
|
241
|
+
const models = responseData.data?.map(m => m.id) || [];
|
|
242
|
+
|
|
243
|
+
const updatedProvider = {
|
|
244
|
+
...data.providers[id],
|
|
245
|
+
models
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
data.providers[id] = updatedProvider;
|
|
249
|
+
await saveProviders(data);
|
|
250
|
+
return updatedProvider;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|