@metrio-ai/cli 1.0.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 +29 -0
- package/README.md +38 -0
- package/dist/auth/credentials.js +81 -0
- package/dist/auth/login.js +143 -0
- package/dist/auth/oauth.js +44 -0
- package/dist/commands/auth.js +74 -0
- package/dist/commands/chat.js +235 -0
- package/dist/commands/prompt-create-version.js +211 -0
- package/dist/commands/prompt.js +284 -0
- package/dist/commands/shared.js +328 -0
- package/dist/commands/tools.js +114 -0
- package/dist/commands/workflow.js +666 -0
- package/dist/console/client.js +108 -0
- package/dist/errors.js +27 -0
- package/dist/index.js +105 -0
- package/package.json +41 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CREATE_VERSION_HELP = void 0;
|
|
37
|
+
exports.parseCreateVersionArgs = parseCreateVersionArgs;
|
|
38
|
+
exports.runCreateVersionCommand = runCreateVersionCommand;
|
|
39
|
+
/**
|
|
40
|
+
* `metrio prompt create-version` — creates a new version of a prompt from a
|
|
41
|
+
* local file (prompt-as-code). The file's entire content becomes the system
|
|
42
|
+
* prompt. The created version does NOT go live unless --set-as-head is given;
|
|
43
|
+
* promotion is an explicit act so CI pushes are safe by default.
|
|
44
|
+
*/
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const client_1 = require("@metrio-ai/client");
|
|
48
|
+
const shared_1 = require("./shared");
|
|
49
|
+
exports.CREATE_VERSION_HELP = `metrio prompt create-version - create a prompt version from a file
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
metrio prompt create-version -p <id> -f <file> [options]
|
|
53
|
+
|
|
54
|
+
Required:
|
|
55
|
+
-p, --prompt-id <id> Prompt ID (integer; or set METRIO_PROMPT_ID)
|
|
56
|
+
-f, --file <path> Prompt file; the entire content becomes the system prompt
|
|
57
|
+
|
|
58
|
+
Options:
|
|
59
|
+
--model-tier <tier> Model tier (wins over provider/name)
|
|
60
|
+
--model-provider <p> Model provider (pair with --model-name)
|
|
61
|
+
--model-name <m> Model name (pair with --model-provider)
|
|
62
|
+
--set-as-head Promote the new version to head (default: off)
|
|
63
|
+
--description <text> Version description
|
|
64
|
+
--output-format <fmt> Output format: "text" or "json"
|
|
65
|
+
--json Print the created version as JSON
|
|
66
|
+
-e, --env <file> Env file to load (default: .env in the current dir)
|
|
67
|
+
--api-key <key> API key (overrides env file / environment)
|
|
68
|
+
--base-url <url> Base API URL (overrides env file / environment)
|
|
69
|
+
-h, --help Show this help
|
|
70
|
+
|
|
71
|
+
The new version is created with the project API key (METRIOAI_API_KEY) — the
|
|
72
|
+
prompt must belong to that key's project. Without --set-as-head the version is
|
|
73
|
+
created but not promoted, so pushing from CI never changes what runs.
|
|
74
|
+
`;
|
|
75
|
+
/** Parses argv for `prompt create-version`. Throws on malformed input. */
|
|
76
|
+
function parseCreateVersionArgs(argv) {
|
|
77
|
+
const options = {
|
|
78
|
+
setAsHead: false,
|
|
79
|
+
json: false,
|
|
80
|
+
help: false,
|
|
81
|
+
};
|
|
82
|
+
const next = (i, flag) => {
|
|
83
|
+
const value = argv[i + 1];
|
|
84
|
+
if (value === undefined)
|
|
85
|
+
throw new Error(`Missing value for ${flag}`);
|
|
86
|
+
return value;
|
|
87
|
+
};
|
|
88
|
+
for (let i = 0; i < argv.length; i++) {
|
|
89
|
+
let arg = argv[i];
|
|
90
|
+
let inline;
|
|
91
|
+
// Accept both `--flag value` and `--flag=value`.
|
|
92
|
+
const eq = arg.indexOf('=');
|
|
93
|
+
if (arg.startsWith('--') && eq > 2) {
|
|
94
|
+
inline = arg.slice(eq + 1);
|
|
95
|
+
arg = arg.slice(0, eq);
|
|
96
|
+
}
|
|
97
|
+
const take = (flag) => (inline !== undefined ? inline : next(i++, flag));
|
|
98
|
+
const noValue = (flag) => {
|
|
99
|
+
if (inline !== undefined)
|
|
100
|
+
throw new Error(`${flag} does not take a value`);
|
|
101
|
+
};
|
|
102
|
+
switch (arg) {
|
|
103
|
+
case '-p':
|
|
104
|
+
case '--prompt-id': {
|
|
105
|
+
const raw = take(arg);
|
|
106
|
+
const id = Number(raw);
|
|
107
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
108
|
+
throw new Error(`--prompt-id must be a positive integer, got "${raw}"`);
|
|
109
|
+
}
|
|
110
|
+
options.promptId = id;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case '-f':
|
|
114
|
+
case '--file':
|
|
115
|
+
options.file = take(arg);
|
|
116
|
+
break;
|
|
117
|
+
case '--model-tier':
|
|
118
|
+
options.modelTier = take(arg);
|
|
119
|
+
break;
|
|
120
|
+
case '--model-provider':
|
|
121
|
+
options.modelProvider = take(arg);
|
|
122
|
+
break;
|
|
123
|
+
case '--model-name':
|
|
124
|
+
options.modelName = take(arg);
|
|
125
|
+
break;
|
|
126
|
+
case '--set-as-head':
|
|
127
|
+
noValue(arg);
|
|
128
|
+
options.setAsHead = true;
|
|
129
|
+
break;
|
|
130
|
+
case '--description':
|
|
131
|
+
options.description = take(arg);
|
|
132
|
+
break;
|
|
133
|
+
case '--output-format': {
|
|
134
|
+
const raw = take(arg);
|
|
135
|
+
if (raw !== 'text' && raw !== 'json') {
|
|
136
|
+
throw new Error(`--output-format must be "text" or "json", got "${raw}"`);
|
|
137
|
+
}
|
|
138
|
+
options.outputFormat = raw;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case '--json':
|
|
142
|
+
noValue(arg);
|
|
143
|
+
options.json = true;
|
|
144
|
+
break;
|
|
145
|
+
case '-e':
|
|
146
|
+
case '--env':
|
|
147
|
+
options.envFile = take(arg);
|
|
148
|
+
break;
|
|
149
|
+
case '--api-key':
|
|
150
|
+
options.apiKey = take(arg);
|
|
151
|
+
break;
|
|
152
|
+
case '--base-url':
|
|
153
|
+
options.baseUrl = take(arg);
|
|
154
|
+
break;
|
|
155
|
+
case '-h':
|
|
156
|
+
case '--help':
|
|
157
|
+
noValue(arg);
|
|
158
|
+
options.help = true;
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return options;
|
|
165
|
+
}
|
|
166
|
+
/** The `prompt create-version` subcommand. */
|
|
167
|
+
async function runCreateVersionCommand(argv) {
|
|
168
|
+
const options = parseCreateVersionArgs(argv);
|
|
169
|
+
if (options.help) {
|
|
170
|
+
process.stdout.write(exports.CREATE_VERSION_HELP);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!options.file) {
|
|
174
|
+
throw new Error('Missing required option: --file');
|
|
175
|
+
}
|
|
176
|
+
let systemPrompt;
|
|
177
|
+
try {
|
|
178
|
+
systemPrompt = fs.readFileSync(path.resolve(options.file), 'utf8');
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
throw new Error(`Cannot read prompt file: ${options.file} (${err.message})`);
|
|
182
|
+
}
|
|
183
|
+
if (systemPrompt.trim() === '') {
|
|
184
|
+
throw new Error(`Prompt file is empty: ${options.file}`);
|
|
185
|
+
}
|
|
186
|
+
(0, shared_1.loadEnv)(options.envFile);
|
|
187
|
+
const promptId = (0, shared_1.resolvePromptId)(options.promptId);
|
|
188
|
+
const client = new client_1.MetrioAI({
|
|
189
|
+
apiKey: (0, shared_1.resolveApiKey)(options.apiKey),
|
|
190
|
+
baseUrl: (0, shared_1.resolveBaseUrl)(options.baseUrl),
|
|
191
|
+
});
|
|
192
|
+
const res = await client.createPromptVersion({
|
|
193
|
+
promptId,
|
|
194
|
+
systemPrompt,
|
|
195
|
+
modelTier: options.modelTier,
|
|
196
|
+
modelProvider: options.modelProvider,
|
|
197
|
+
modelName: options.modelName,
|
|
198
|
+
// Omit when false so the request only carries explicit promotion.
|
|
199
|
+
setAsHead: options.setAsHead || undefined,
|
|
200
|
+
description: options.description,
|
|
201
|
+
outputFormat: options.outputFormat,
|
|
202
|
+
});
|
|
203
|
+
if (options.json) {
|
|
204
|
+
process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const v = res.version;
|
|
208
|
+
process.stdout.write(`Created version ${v.serial} (id ${v.id}) for prompt ${promptId}` +
|
|
209
|
+
`${options.setAsHead ? ', set as head' : ''}\n`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCases = parseCases;
|
|
4
|
+
exports.mergeVariables = mergeVariables;
|
|
5
|
+
exports.runPromptCommand = runPromptCommand;
|
|
6
|
+
/**
|
|
7
|
+
* `metrio prompt run-batch` — runs several prompt cases in one process.
|
|
8
|
+
*
|
|
9
|
+
* Each case is an independent chat completion; the win over a shell loop is
|
|
10
|
+
* paying process start-up and env loading once instead of per case. Output is
|
|
11
|
+
* JSONL in input order so results can be diffed across runs.
|
|
12
|
+
*/
|
|
13
|
+
const client_1 = require("@metrio-ai/client");
|
|
14
|
+
const client_2 = require("@metrio-ai/client");
|
|
15
|
+
const shared_1 = require("./shared");
|
|
16
|
+
const prompt_create_version_1 = require("./prompt-create-version");
|
|
17
|
+
const PROMPT_HELP = `metrio prompt - prompt subcommands
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
metrio prompt run-batch -p <id> --cases <file.jsonl> [options]
|
|
21
|
+
metrio prompt create-version -p <id> -f <file> [options]
|
|
22
|
+
|
|
23
|
+
Run "metrio prompt create-version --help" for that command's options.
|
|
24
|
+
The rest of this help covers run-batch:
|
|
25
|
+
|
|
26
|
+
Required:
|
|
27
|
+
-p, --prompt-id <id> Prompt ID to run (integer; or set METRIO_PROMPT_ID)
|
|
28
|
+
--cases <file> JSONL cases file; "-" reads stdin
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--concurrency <n> Cases to run at once (default: 4)
|
|
32
|
+
-t, --tag <tag> Version tag applied to every case (a case may override)
|
|
33
|
+
--var <name=value> Global template variable (repeatable; a case may override)
|
|
34
|
+
--request-tag <tag> Request-level tag recorded on every run, e.g. "test"
|
|
35
|
+
(repeatable; max 5; each 1-32 chars, lowercase
|
|
36
|
+
alphanumeric start, may contain "-" and "_")
|
|
37
|
+
--timezone <tz> IANA timezone, e.g. "Asia/Taipei"
|
|
38
|
+
-o, --out <file> Write result JSONL here (default: stdout)
|
|
39
|
+
-e, --env <file> Env file to load (default: .env in the current dir)
|
|
40
|
+
--api-key <key> API key (overrides env file / environment)
|
|
41
|
+
--base-url <url> Base API URL (overrides env file / environment)
|
|
42
|
+
-h, --help Show this help
|
|
43
|
+
|
|
44
|
+
Cases file format (one JSON object per line; blank and "//" lines are ignored):
|
|
45
|
+
{"id":"c1","messages":[{"role":"user","content":"…"}],"system":"…","variables":{"k":"v"},"tag":"v3"}
|
|
46
|
+
|
|
47
|
+
"messages" uses the same format as the chat command's messages file, including
|
|
48
|
+
"file" attachments — attachment paths resolve relative to the cases file (or the
|
|
49
|
+
current directory when reading stdin). A case without an "id" is named by its
|
|
50
|
+
line number, e.g. "case-3".
|
|
51
|
+
|
|
52
|
+
Results are JSONL in input order, one line per case:
|
|
53
|
+
{"id":"c1","ok":true,"response":"…","inputTokens":120,"outputTokens":45,"elapsedTime":1240,"taskId":"…"}
|
|
54
|
+
{"id":"c2","ok":false,"error":{"message":"…","status":429}}
|
|
55
|
+
|
|
56
|
+
Progress goes to stderr as each case finishes — "[2/3] c2 ok 1240ms" in
|
|
57
|
+
completion order, then a "3 cases: 2 ok, 1 failed" tally — so stdout carries
|
|
58
|
+
nothing but the result rows. Those rows are written once the whole batch has
|
|
59
|
+
finished, not streamed per case, so watch stderr for live progress.
|
|
60
|
+
|
|
61
|
+
A failing case does not stop the batch. The exit code is 1 if any case failed.
|
|
62
|
+
`;
|
|
63
|
+
/**
|
|
64
|
+
* Parses a JSONL cases file into prompt cases, validating the fields this
|
|
65
|
+
* command sends. Line numbering and the `case-<line>` fallback id come from
|
|
66
|
+
* the shared JSONL reader, so every batch command names cases the same way.
|
|
67
|
+
*/
|
|
68
|
+
function parseCases(raw) {
|
|
69
|
+
return (0, shared_1.parseJsonlObjects)(raw).map(({ line, value }) => {
|
|
70
|
+
const entry = value;
|
|
71
|
+
if (!Array.isArray(entry.messages) || entry.messages.length === 0) {
|
|
72
|
+
throw new Error(`Case on line ${line} must have a non-empty "messages" array`);
|
|
73
|
+
}
|
|
74
|
+
// A non-object `variables` (e.g. an array) would survive Object.entries in
|
|
75
|
+
// mergeVariables as bogus numeric-keyed variables and the case would run
|
|
76
|
+
// "ok" with unintended inputs — reject it here instead.
|
|
77
|
+
if (entry.variables !== undefined &&
|
|
78
|
+
(entry.variables === null ||
|
|
79
|
+
typeof entry.variables !== 'object' ||
|
|
80
|
+
Array.isArray(entry.variables))) {
|
|
81
|
+
throw new Error(`Case on line ${line}: "variables" must be a JSON object`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
id: (0, shared_1.caseIdFor)(value, line),
|
|
85
|
+
messages: entry.messages,
|
|
86
|
+
system: entry.system,
|
|
87
|
+
variables: entry.variables,
|
|
88
|
+
tag: entry.tag,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Merges the global `--var` flags with a case's own variables. The case wins on
|
|
94
|
+
* conflict, so a batch can share defaults and still vary one field per case.
|
|
95
|
+
* Returns `undefined` when there is nothing to send.
|
|
96
|
+
*/
|
|
97
|
+
function mergeVariables(globals, caseVars) {
|
|
98
|
+
const merged = new Map();
|
|
99
|
+
for (const variable of globals)
|
|
100
|
+
merged.set(variable.name, variable.value);
|
|
101
|
+
if (caseVars) {
|
|
102
|
+
for (const [name, value] of Object.entries(caseVars))
|
|
103
|
+
merged.set(name, String(value));
|
|
104
|
+
}
|
|
105
|
+
if (merged.size === 0)
|
|
106
|
+
return undefined;
|
|
107
|
+
return [...merged].map(([name, value]) => ({ name, value }));
|
|
108
|
+
}
|
|
109
|
+
/** Parses argv for `prompt run-batch`. Throws on malformed input. */
|
|
110
|
+
function parsePromptArgs(argv) {
|
|
111
|
+
const options = {
|
|
112
|
+
concurrency: 4,
|
|
113
|
+
variables: [],
|
|
114
|
+
tags: [],
|
|
115
|
+
help: false,
|
|
116
|
+
};
|
|
117
|
+
const next = (i, flag) => {
|
|
118
|
+
const value = argv[i + 1];
|
|
119
|
+
if (value === undefined)
|
|
120
|
+
throw new Error(`Missing value for ${flag}`);
|
|
121
|
+
return value;
|
|
122
|
+
};
|
|
123
|
+
for (let i = 0; i < argv.length; i++) {
|
|
124
|
+
let arg = argv[i];
|
|
125
|
+
let inline;
|
|
126
|
+
// Accept both `--flag value` and `--flag=value`.
|
|
127
|
+
const eq = arg.indexOf('=');
|
|
128
|
+
if (arg.startsWith('--') && eq > 2) {
|
|
129
|
+
inline = arg.slice(eq + 1);
|
|
130
|
+
arg = arg.slice(0, eq);
|
|
131
|
+
}
|
|
132
|
+
const take = (flag) => (inline !== undefined ? inline : next(i++, flag));
|
|
133
|
+
switch (arg) {
|
|
134
|
+
case '-p':
|
|
135
|
+
case '--prompt-id': {
|
|
136
|
+
const raw = take(arg);
|
|
137
|
+
const id = Number(raw);
|
|
138
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
139
|
+
throw new Error(`--prompt-id must be a positive integer, got "${raw}"`);
|
|
140
|
+
}
|
|
141
|
+
options.promptId = id;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case '--cases':
|
|
145
|
+
options.casesFile = take(arg);
|
|
146
|
+
break;
|
|
147
|
+
case '--concurrency': {
|
|
148
|
+
const raw = take(arg);
|
|
149
|
+
const n = Number(raw);
|
|
150
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
151
|
+
throw new Error(`--concurrency must be a positive integer, got "${raw}"`);
|
|
152
|
+
}
|
|
153
|
+
options.concurrency = n;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case '-t':
|
|
157
|
+
case '--tag':
|
|
158
|
+
options.tag = take(arg);
|
|
159
|
+
break;
|
|
160
|
+
case '--timezone':
|
|
161
|
+
options.timezone = take(arg);
|
|
162
|
+
break;
|
|
163
|
+
case '--var': {
|
|
164
|
+
const raw = take(arg);
|
|
165
|
+
const sep = raw.indexOf('=');
|
|
166
|
+
if (sep === -1) {
|
|
167
|
+
throw new Error(`--var must be in the form name=value, got "${raw}"`);
|
|
168
|
+
}
|
|
169
|
+
const name = raw.slice(0, sep).trim();
|
|
170
|
+
if (!name)
|
|
171
|
+
throw new Error(`--var name cannot be empty, got "${raw}"`);
|
|
172
|
+
options.variables.push({ name, value: raw.slice(sep + 1) });
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case '--request-tag': {
|
|
176
|
+
const tag = take(arg).trim();
|
|
177
|
+
if (!tag)
|
|
178
|
+
throw new Error('--request-tag cannot be empty');
|
|
179
|
+
options.tags.push(tag);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case '-o':
|
|
183
|
+
case '--out':
|
|
184
|
+
options.out = take(arg);
|
|
185
|
+
break;
|
|
186
|
+
case '-e':
|
|
187
|
+
case '--env':
|
|
188
|
+
options.envFile = take(arg);
|
|
189
|
+
break;
|
|
190
|
+
case '--api-key':
|
|
191
|
+
options.apiKey = take(arg);
|
|
192
|
+
break;
|
|
193
|
+
case '--base-url':
|
|
194
|
+
options.baseUrl = take(arg);
|
|
195
|
+
break;
|
|
196
|
+
case '-h':
|
|
197
|
+
case '--help':
|
|
198
|
+
if (inline !== undefined) {
|
|
199
|
+
throw new Error('--help does not take a value');
|
|
200
|
+
}
|
|
201
|
+
options.help = true;
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return options;
|
|
208
|
+
}
|
|
209
|
+
/** The `prompt run-batch` subcommand. */
|
|
210
|
+
async function runPromptCommand(argv) {
|
|
211
|
+
const action = argv[0];
|
|
212
|
+
if (action === undefined || action === '-h' || action === '--help') {
|
|
213
|
+
process.stdout.write(PROMPT_HELP);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (action === 'create-version') {
|
|
217
|
+
await (0, prompt_create_version_1.runCreateVersionCommand)(argv.slice(1));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (action !== 'run-batch') {
|
|
221
|
+
throw new Error(`Unknown prompt command: "${action}". Expected "run-batch" or "create-version".`);
|
|
222
|
+
}
|
|
223
|
+
const options = parsePromptArgs(argv.slice(1));
|
|
224
|
+
if (options.help) {
|
|
225
|
+
process.stdout.write(PROMPT_HELP);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!options.casesFile) {
|
|
229
|
+
throw new Error('Missing required option: --cases');
|
|
230
|
+
}
|
|
231
|
+
(0, shared_1.loadEnv)(options.envFile);
|
|
232
|
+
// Prompt ID may come from the flag or the env file; the flag wins.
|
|
233
|
+
const promptId = (0, shared_1.resolvePromptId)(options.promptId);
|
|
234
|
+
const { raw, baseDir } = await (0, shared_1.readCasesSource)(options.casesFile);
|
|
235
|
+
const cases = parseCases(raw);
|
|
236
|
+
const client = new client_1.MetrioAI({
|
|
237
|
+
apiKey: (0, shared_1.resolveApiKey)(options.apiKey),
|
|
238
|
+
baseUrl: (0, shared_1.resolveBaseUrl)(options.baseUrl),
|
|
239
|
+
});
|
|
240
|
+
let completed = 0;
|
|
241
|
+
const results = await (0, shared_1.mapWithConcurrency)(cases, options.concurrency, async (c) => {
|
|
242
|
+
const result = await runOneCase(client, promptId, c, baseDir, options);
|
|
243
|
+
completed++;
|
|
244
|
+
process.stderr.write(`[${completed}/${cases.length}] ${c.id} ${result.ok ? `ok ${result.elapsedTime}ms` : `failed: ${result.error?.message}`}\n`);
|
|
245
|
+
return result;
|
|
246
|
+
});
|
|
247
|
+
(0, shared_1.emitBatchResults)(results, options.out);
|
|
248
|
+
}
|
|
249
|
+
/** Runs a single case, converting any failure into a result row. */
|
|
250
|
+
async function runOneCase(client, promptId, c, baseDir, options) {
|
|
251
|
+
try {
|
|
252
|
+
const messages = (0, shared_1.buildMessages)(c.messages, baseDir);
|
|
253
|
+
if (c.system !== undefined && c.system.trim() !== '') {
|
|
254
|
+
messages.unshift({ role: 'system', content: { type: 'text', text: c.system } });
|
|
255
|
+
}
|
|
256
|
+
const res = await client.chatCompletion({
|
|
257
|
+
promptId,
|
|
258
|
+
messages,
|
|
259
|
+
tag: c.tag ?? options.tag,
|
|
260
|
+
timezone: options.timezone,
|
|
261
|
+
variables: mergeVariables(options.variables, c.variables),
|
|
262
|
+
tags: options.tags.length ? options.tags : undefined,
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
id: c.id,
|
|
266
|
+
ok: true,
|
|
267
|
+
response: res.response,
|
|
268
|
+
inputTokens: res.inputTokens,
|
|
269
|
+
outputTokens: res.outputTokens,
|
|
270
|
+
elapsedTime: res.elapsedTime,
|
|
271
|
+
taskId: res.taskId,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
return {
|
|
276
|
+
id: c.id,
|
|
277
|
+
ok: false,
|
|
278
|
+
error: {
|
|
279
|
+
message: err instanceof Error ? err.message : String(err),
|
|
280
|
+
status: err instanceof client_2.MetrioApiError ? err.statusCode : undefined,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|