tuneprompt 1.0.5 → 1.0.7
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/dist/commands/init.js +0 -3
- package/dist/commands/run.js +57 -85
- package/dist/engine/optimizer.d.ts +1 -0
- package/dist/engine/optimizer.js +45 -12
- package/dist/services/cloud.service.js +9 -2
- package/dist/storage/database.d.ts +2 -0
- package/dist/storage/database.js +40 -8
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -91,9 +91,6 @@ async function initCommand() {
|
|
|
91
91
|
const envContent = `OPENAI_API_KEY=your_key_here
|
|
92
92
|
ANTHROPIC_API_KEY=your_key_here
|
|
93
93
|
OPENROUTER_API_KEY=your_key_here
|
|
94
|
-
|
|
95
|
-
# For self-hosted or local testing:
|
|
96
|
-
# TUNEPROMPT_API_URL=http://localhost:3000
|
|
97
94
|
`;
|
|
98
95
|
fs.writeFileSync(envPath, envContent);
|
|
99
96
|
console.log(chalk_1.default.green('✓ Created .env'));
|
package/dist/commands/run.js
CHANGED
|
@@ -65,64 +65,25 @@ async function runTests(options = {}) {
|
|
|
65
65
|
const results = await runner.runTests(testCases);
|
|
66
66
|
spinner.stop();
|
|
67
67
|
// Save to database
|
|
68
|
+
// Save to database
|
|
68
69
|
const db = new database_1.TestDatabase();
|
|
69
70
|
db.saveRun(results);
|
|
70
|
-
|
|
71
|
+
// Calculate results for cloud upload (and for sync logic)
|
|
72
|
+
const currentRunId = results.id; // Assuming results has ID
|
|
71
73
|
// Report results
|
|
72
74
|
const reporter = new reporter_1.TestReporter();
|
|
73
75
|
reporter.printResults(results, config.outputFormat);
|
|
74
|
-
// Calculate results for cloud upload
|
|
75
|
-
const testResults = results.results.map((result) => {
|
|
76
|
-
// Map from internal TestResult to cloud service TestResult
|
|
77
|
-
const mappedResult = {
|
|
78
|
-
test_name: result.testCase.description,
|
|
79
|
-
test_description: result.testCase.description,
|
|
80
|
-
prompt: typeof result.testCase.prompt === 'string'
|
|
81
|
-
? result.testCase.prompt
|
|
82
|
-
: JSON.stringify(result.testCase.prompt),
|
|
83
|
-
input_data: result.testCase.variables,
|
|
84
|
-
expected_output: result.expectedOutput,
|
|
85
|
-
actual_output: result.actualOutput,
|
|
86
|
-
score: result.score,
|
|
87
|
-
method: result.testCase.config?.method || 'exact',
|
|
88
|
-
status: result.status,
|
|
89
|
-
model: result.metadata.provider || '',
|
|
90
|
-
tokens_used: result.metadata.tokens,
|
|
91
|
-
latency_ms: result.metadata.duration,
|
|
92
|
-
cost_usd: result.metadata.cost,
|
|
93
|
-
error_message: result.error,
|
|
94
|
-
error_type: undefined, // No error type in current TestResult interface
|
|
95
|
-
};
|
|
96
|
-
return mappedResult;
|
|
97
|
-
});
|
|
98
|
-
// Calculate total cost from all test results
|
|
99
|
-
const totalCost = results.results.reduce((sum, result) => {
|
|
100
|
-
return sum + (result.metadata.cost || 0);
|
|
101
|
-
}, 0);
|
|
102
|
-
const resultsSummary = {
|
|
103
|
-
totalTests: results.results.length,
|
|
104
|
-
passedTests: results.passed,
|
|
105
|
-
failedTests: results.failed,
|
|
106
|
-
durationMs: Date.now() - startTime,
|
|
107
|
-
totalCost: totalCost || 0.05, // fallback value
|
|
108
|
-
tests: testResults,
|
|
109
|
-
};
|
|
110
|
-
// Print results to console (existing logic)
|
|
111
|
-
console.log(chalk_1.default.green(`\n✅ ${resultsSummary.passedTests} passed`));
|
|
112
|
-
console.log(chalk_1.default.red(`❌ ${resultsSummary.failedTests} failed\n`));
|
|
113
|
-
// Show upsell hint if tests failed
|
|
114
|
-
displayRunSummary(results.results);
|
|
115
|
-
// NEW: Cloud upload logic
|
|
116
76
|
const isCI = options.ci ||
|
|
117
77
|
process.env.CI === 'true' ||
|
|
118
78
|
!!process.env.GITHUB_ACTIONS ||
|
|
119
79
|
!!process.env.GITLAB_CI;
|
|
120
80
|
const shouldUpload = options.cloud || isCI;
|
|
121
81
|
if (shouldUpload) {
|
|
122
|
-
await
|
|
82
|
+
await syncPendingRuns(db, options);
|
|
123
83
|
}
|
|
84
|
+
db.close();
|
|
124
85
|
// Exit with error code if tests failed
|
|
125
|
-
if (
|
|
86
|
+
if (results.failed > 0) {
|
|
126
87
|
process.exit(1);
|
|
127
88
|
}
|
|
128
89
|
}
|
|
@@ -139,35 +100,34 @@ exports.runCommand = new commander_1.Command('run')
|
|
|
139
100
|
.action(async (options) => {
|
|
140
101
|
await runTests(options);
|
|
141
102
|
});
|
|
142
|
-
async function
|
|
103
|
+
async function syncPendingRuns(db, options) {
|
|
104
|
+
const pendingRuns = db.getPendingUploads();
|
|
105
|
+
if (pendingRuns.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
console.log(chalk_1.default.blue(`\n☁️ Syncing ${pendingRuns.length} pending run(s) to Cloud...`));
|
|
143
108
|
const cloudService = new cloud_service_1.CloudService();
|
|
144
109
|
await cloudService.init();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
console.log(chalk_1.default.yellow('\n⚠️ Not authenticated with Cloud.'));
|
|
148
|
-
console.log(chalk_1.default.gray('Results saved locally. Run `tuneprompt activate` to enable cloud sync\n'));
|
|
110
|
+
if (!(await cloudService.isAuthenticated())) {
|
|
111
|
+
console.log(chalk_1.default.yellow('⚠️ Not authenticated. Run `tuneprompt activate` first.'));
|
|
149
112
|
return;
|
|
150
113
|
}
|
|
151
|
-
// Get
|
|
114
|
+
// Get project ID once
|
|
152
115
|
let projectId;
|
|
153
116
|
try {
|
|
154
117
|
const projects = await cloudService.getProjects();
|
|
155
118
|
if (projects.length === 0) {
|
|
156
|
-
console.log(chalk_1.default.blue('📁 Creating default project...'));
|
|
157
119
|
const project = await cloudService.createProject('Default Project');
|
|
158
120
|
projectId = project.id;
|
|
159
|
-
console.log(chalk_1.default.green(`✅ Project created: ${projectId}`));
|
|
160
121
|
}
|
|
161
122
|
else {
|
|
162
|
-
projectId = projects[0].id;
|
|
163
|
-
console.log(chalk_1.default.gray(`📋 Using existing project: ${projectId}`));
|
|
123
|
+
projectId = projects[0].id;
|
|
164
124
|
}
|
|
165
125
|
}
|
|
166
|
-
catch (
|
|
167
|
-
console.log(chalk_1.default.yellow('⚠️ Failed to get project')
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.log(chalk_1.default.yellow('⚠️ Failed to get project info'));
|
|
168
128
|
return;
|
|
169
129
|
}
|
|
170
|
-
//
|
|
130
|
+
// Common Git/Env context
|
|
171
131
|
let gitContext = {};
|
|
172
132
|
try {
|
|
173
133
|
gitContext = {
|
|
@@ -176,10 +136,7 @@ async function uploadToCloud(results, options) {
|
|
|
176
136
|
commit_message: (0, child_process_1.execSync)('git log -1 --pretty=%B', { encoding: 'utf-8' }).trim(),
|
|
177
137
|
};
|
|
178
138
|
}
|
|
179
|
-
catch {
|
|
180
|
-
// Not a git repo
|
|
181
|
-
}
|
|
182
|
-
// Detect CI provider
|
|
139
|
+
catch { }
|
|
183
140
|
let ciProvider;
|
|
184
141
|
if (process.env.GITHUB_ACTIONS)
|
|
185
142
|
ciProvider = 'github';
|
|
@@ -189,28 +146,43 @@ async function uploadToCloud(results, options) {
|
|
|
189
146
|
ciProvider = 'jenkins';
|
|
190
147
|
else if (process.env.CIRCLECI)
|
|
191
148
|
ciProvider = 'circleci';
|
|
192
|
-
//
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
149
|
+
// Upload each run
|
|
150
|
+
for (const run of pendingRuns) {
|
|
151
|
+
const runData = {
|
|
152
|
+
project_id: projectId,
|
|
153
|
+
environment: options.ci ? 'ci' : 'local',
|
|
154
|
+
ci_provider: ciProvider,
|
|
155
|
+
total_tests: run.totalTests,
|
|
156
|
+
passed_tests: run.passed,
|
|
157
|
+
failed_tests: run.failed,
|
|
158
|
+
duration_ms: run.duration,
|
|
159
|
+
cost_usd: run.results.reduce((sum, r) => sum + (r.metadata.cost || 0), 0) || 0.05, // fallback
|
|
160
|
+
started_at: new Date(run.timestamp.getTime() - run.duration).toISOString(),
|
|
161
|
+
completed_at: run.timestamp.toISOString(),
|
|
162
|
+
test_results: run.results.map(r => ({
|
|
163
|
+
test_name: r.testCase.description,
|
|
164
|
+
test_description: r.testCase.description,
|
|
165
|
+
prompt: typeof r.testCase.prompt === 'string' ? r.testCase.prompt : JSON.stringify(r.testCase.prompt),
|
|
166
|
+
input_data: r.testCase.variables,
|
|
167
|
+
expected_output: r.expectedOutput,
|
|
168
|
+
actual_output: r.actualOutput,
|
|
169
|
+
score: r.score,
|
|
170
|
+
method: r.testCase.config?.method || 'exact',
|
|
171
|
+
status: r.status,
|
|
172
|
+
model: r.metadata.provider || '',
|
|
173
|
+
tokens_used: r.metadata.tokens,
|
|
174
|
+
latency_ms: r.metadata.duration,
|
|
175
|
+
cost_usd: r.metadata.cost,
|
|
176
|
+
})),
|
|
177
|
+
...gitContext // Applying current git context to old runs slightly inaccurate but acceptable
|
|
178
|
+
};
|
|
179
|
+
const uploadResult = await cloudService.uploadRun(runData);
|
|
180
|
+
if (uploadResult.success) {
|
|
181
|
+
db.markAsUploaded(run.id);
|
|
182
|
+
console.log(chalk_1.default.green(` ✓ Uploaded run from ${run.timestamp.toLocaleTimeString()}`));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.log(chalk_1.default.red(` ✗ Failed to upload run ${run.id}: ${uploadResult.error}`));
|
|
186
|
+
}
|
|
215
187
|
}
|
|
216
188
|
}
|
package/dist/engine/optimizer.js
CHANGED
|
@@ -44,19 +44,34 @@ const constraintExtractor_1 = require("./constraintExtractor");
|
|
|
44
44
|
class PromptOptimizer {
|
|
45
45
|
anthropic;
|
|
46
46
|
openai;
|
|
47
|
+
openrouter;
|
|
47
48
|
constructor() {
|
|
48
49
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
49
|
-
if (anthropicKey &&
|
|
50
|
+
if (anthropicKey &&
|
|
51
|
+
!anthropicKey.includes('your_key') &&
|
|
52
|
+
!anthropicKey.startsWith('api_key') &&
|
|
53
|
+
anthropicKey !== 'phc_xxxxx') {
|
|
50
54
|
this.anthropic = new sdk_1.default({
|
|
51
55
|
apiKey: anthropicKey
|
|
52
56
|
});
|
|
53
57
|
}
|
|
54
58
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
55
|
-
if (openaiKey && !openaiKey.
|
|
59
|
+
if (openaiKey && !openaiKey.includes('your_key')) {
|
|
56
60
|
this.openai = new openai_1.default({
|
|
57
61
|
apiKey: openaiKey
|
|
58
62
|
});
|
|
59
63
|
}
|
|
64
|
+
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
|
65
|
+
if (openrouterKey && !openrouterKey.includes('your_key')) {
|
|
66
|
+
this.openrouter = new openai_1.default({
|
|
67
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
68
|
+
apiKey: openrouterKey,
|
|
69
|
+
defaultHeaders: {
|
|
70
|
+
'HTTP-Referer': 'https://tuneprompt.xyz',
|
|
71
|
+
'X-Title': 'TunePrompt CLI',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
60
75
|
}
|
|
61
76
|
/**
|
|
62
77
|
* Main optimization method
|
|
@@ -118,7 +133,7 @@ class PromptOptimizer {
|
|
|
118
133
|
if (provider === 'anthropic' && this.anthropic) {
|
|
119
134
|
console.log(`⚡ Using Anthropic for candidate generation...`);
|
|
120
135
|
const response = await this.anthropic.messages.create({
|
|
121
|
-
model: 'claude-sonnet-
|
|
136
|
+
model: 'claude-3-5-sonnet-20240620',
|
|
122
137
|
max_tokens: 4000,
|
|
123
138
|
temperature: 0.7, // Some creativity for prompt rewriting
|
|
124
139
|
messages: [{
|
|
@@ -174,16 +189,34 @@ class PromptOptimizer {
|
|
|
174
189
|
}
|
|
175
190
|
];
|
|
176
191
|
}
|
|
177
|
-
else if (provider === 'openrouter') {
|
|
178
|
-
// For OpenRouter, we'll use the shadowTester to get a response
|
|
192
|
+
else if (provider === 'openrouter' && this.openrouter) {
|
|
179
193
|
console.log(`⚡ Using OpenRouter for candidate generation...`);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
const response = await this.openrouter.chat.completions.create({
|
|
195
|
+
model: 'anthropic/claude-3-sonnet', // Default robust model on OpenRouter
|
|
196
|
+
messages: [{
|
|
197
|
+
role: 'user',
|
|
198
|
+
content: metaPrompt
|
|
199
|
+
}],
|
|
200
|
+
response_format: { type: 'json_object' }
|
|
201
|
+
});
|
|
202
|
+
const content = response.choices[0]?.message?.content;
|
|
203
|
+
if (!content) {
|
|
204
|
+
// Fallback if model doesn't support JSON mode or returns empty
|
|
205
|
+
throw new Error('No content returned from OpenRouter');
|
|
206
|
+
}
|
|
207
|
+
const parsed = JSON.parse(content);
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
prompt: parsed.candidateA.prompt,
|
|
211
|
+
reasoning: parsed.candidateA.reasoning,
|
|
185
212
|
score: 0
|
|
186
|
-
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
prompt: parsed.candidateB.prompt,
|
|
216
|
+
reasoning: parsed.candidateB.reasoning,
|
|
217
|
+
score: 0
|
|
218
|
+
}
|
|
219
|
+
];
|
|
187
220
|
}
|
|
188
221
|
}
|
|
189
222
|
catch (error) {
|
|
@@ -195,7 +228,7 @@ class PromptOptimizer {
|
|
|
195
228
|
console.error('All providers failed for candidate generation');
|
|
196
229
|
return [{
|
|
197
230
|
prompt: this.createFallbackPrompt(failedTest),
|
|
198
|
-
reasoning: '
|
|
231
|
+
reasoning: 'Generated using fallback method',
|
|
199
232
|
score: 0
|
|
200
233
|
}];
|
|
201
234
|
}
|
|
@@ -10,7 +10,7 @@ class CloudService {
|
|
|
10
10
|
backendUrl;
|
|
11
11
|
subscriptionId;
|
|
12
12
|
constructor() {
|
|
13
|
-
this.backendUrl = process.env.TUNEPROMPT_API_URL || process.env.BACKEND_URL || 'https://api.
|
|
13
|
+
this.backendUrl = process.env.TUNEPROMPT_API_URL || process.env.BACKEND_URL || 'https://i8e3mu8jlk.execute-api.ap-south-1.amazonaws.com/dev';
|
|
14
14
|
}
|
|
15
15
|
async init() {
|
|
16
16
|
// Load subscription ID from local storage (Phase 2 activation)
|
|
@@ -21,9 +21,16 @@ class CloudService {
|
|
|
21
21
|
return license?.subscriptionId;
|
|
22
22
|
}
|
|
23
23
|
async uploadRun(data) {
|
|
24
|
-
|
|
24
|
+
// Enforce Pro plan check
|
|
25
|
+
const license = (0, license_1.loadLicense)();
|
|
26
|
+
if (!this.subscriptionId || !license) {
|
|
25
27
|
return { success: false, error: 'Not activated. Run `tuneprompt activate` first.' };
|
|
26
28
|
}
|
|
29
|
+
// Check for specific pro plans (if we ever add free tiers)
|
|
30
|
+
const proPlans = ['pro-monthly', 'pro-yearly', 'lifetime'];
|
|
31
|
+
if (!proPlans.includes(license.plan)) {
|
|
32
|
+
return { success: false, error: 'Cloud features are restricted to Pro users. Please upgrade your plan.' };
|
|
33
|
+
}
|
|
27
34
|
try {
|
|
28
35
|
const response = await axios_1.default.post(`${this.backendUrl}/api/cloud/ingest-run`, data, {
|
|
29
36
|
headers: {
|
package/dist/storage/database.js
CHANGED
|
@@ -87,26 +87,38 @@ class TestDatabase {
|
|
|
87
87
|
CREATE INDEX IF NOT EXISTS idx_run_timestamp ON test_runs(timestamp);
|
|
88
88
|
CREATE INDEX IF NOT EXISTS idx_result_run ON test_results(run_id);
|
|
89
89
|
`);
|
|
90
|
+
// Migration for uploaded status
|
|
91
|
+
try {
|
|
92
|
+
this.db.exec(`ALTER TABLE test_runs ADD COLUMN uploaded INTEGER DEFAULT 0`);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
// Column might already exist
|
|
96
|
+
}
|
|
97
|
+
// Migration for provider column
|
|
98
|
+
try {
|
|
99
|
+
this.db.exec(`ALTER TABLE test_results ADD COLUMN provider TEXT`);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
// Column might already exist
|
|
103
|
+
}
|
|
90
104
|
// Run external migrations (Phase 2)
|
|
91
|
-
// Note: runMigrations is async but contains synchronous better-sqlite3 calls
|
|
92
|
-
// so it executes immediately. We catch any promise rejection just in case.
|
|
93
105
|
(0, migrate_1.runMigrations)(this.db).catch((err) => {
|
|
94
106
|
console.error('Phase 2 migration failed:', err);
|
|
95
107
|
});
|
|
96
108
|
}
|
|
97
109
|
saveRun(run) {
|
|
98
110
|
const insertRun = this.db.prepare(`
|
|
99
|
-
INSERT INTO test_runs (id, timestamp, total_tests, passed, failed, duration)
|
|
100
|
-
VALUES (?, ?, ?, ?, ?,
|
|
111
|
+
INSERT INTO test_runs (id, timestamp, total_tests, passed, failed, duration, uploaded)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, 0)
|
|
101
113
|
`);
|
|
102
114
|
insertRun.run(run.id, run.timestamp.getTime(), run.totalTests, run.passed, run.failed, run.duration);
|
|
103
115
|
const insertResult = this.db.prepare(`
|
|
104
116
|
INSERT INTO test_results
|
|
105
|
-
(id, run_id, description, prompt, variables, expect, config, file_path, status, score, actual_output, expected_output, error, duration, tokens, cost)
|
|
106
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
117
|
+
(id, run_id, description, prompt, variables, expect, config, file_path, status, score, actual_output, expected_output, error, duration, tokens, cost, provider)
|
|
118
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
107
119
|
`);
|
|
108
120
|
for (const result of run.results) {
|
|
109
|
-
insertResult.run(result.id, run.id, result.testCase.description, typeof result.testCase.prompt === 'string' ? result.testCase.prompt : JSON.stringify(result.testCase.prompt), result.testCase.variables ? JSON.stringify(result.testCase.variables) : null, typeof result.testCase.expect === 'string' ? result.testCase.expect : JSON.stringify(result.testCase.expect), result.testCase.config ? JSON.stringify(result.testCase.config) : null, result.testCase.filePath || null, result.status, result.score, result.actualOutput, result.expectedOutput, result.error || null, result.metadata.duration, result.metadata.tokens || null, result.metadata.cost || null);
|
|
121
|
+
insertResult.run(result.id, run.id, result.testCase.description, typeof result.testCase.prompt === 'string' ? result.testCase.prompt : JSON.stringify(result.testCase.prompt), result.testCase.variables ? JSON.stringify(result.testCase.variables) : null, typeof result.testCase.expect === 'string' ? result.testCase.expect : JSON.stringify(result.testCase.expect), result.testCase.config ? JSON.stringify(result.testCase.config) : null, result.testCase.filePath || null, result.status, result.score, result.actualOutput, result.expectedOutput, result.error || null, result.metadata.duration, result.metadata.tokens || null, result.metadata.cost || null, result.metadata.provider || null);
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
getRecentRuns(limit = 10) {
|
|
@@ -125,6 +137,25 @@ class TestDatabase {
|
|
|
125
137
|
results: this.getRunResults(run.id)
|
|
126
138
|
}));
|
|
127
139
|
}
|
|
140
|
+
getPendingUploads() {
|
|
141
|
+
const runs = this.db.prepare(`
|
|
142
|
+
SELECT * FROM test_runs
|
|
143
|
+
WHERE uploaded = 0 OR uploaded IS NULL
|
|
144
|
+
ORDER BY timestamp ASC
|
|
145
|
+
`).all();
|
|
146
|
+
return runs.map(run => ({
|
|
147
|
+
id: run.id,
|
|
148
|
+
timestamp: new Date(run.timestamp),
|
|
149
|
+
totalTests: run.total_tests,
|
|
150
|
+
passed: run.passed,
|
|
151
|
+
failed: run.failed,
|
|
152
|
+
duration: run.duration,
|
|
153
|
+
results: this.getRunResults(run.id)
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
markAsUploaded(runId) {
|
|
157
|
+
this.db.prepare(`UPDATE test_runs SET uploaded = 1 WHERE id = ?`).run(runId);
|
|
158
|
+
}
|
|
128
159
|
getRunResults(runId) {
|
|
129
160
|
const results = this.db.prepare(`
|
|
130
161
|
SELECT * FROM test_results WHERE run_id = ?
|
|
@@ -167,7 +198,8 @@ class TestDatabase {
|
|
|
167
198
|
duration: r.duration,
|
|
168
199
|
timestamp: new Date(),
|
|
169
200
|
tokens: r.tokens,
|
|
170
|
-
cost: r.cost
|
|
201
|
+
cost: r.cost,
|
|
202
|
+
provider: r.provider
|
|
171
203
|
}
|
|
172
204
|
};
|
|
173
205
|
});
|