openmerit 0.1.1 → 0.1.3
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 +89 -47
- package/dist/cli.js +48 -121
- package/dist/daemon.js +131 -52
- package/dist/diagnostics.js +194 -0
- package/dist/frontier.js +13 -6
- package/dist/integrations.js +19 -0
- package/dist/judge.js +5 -5
- package/dist/llm.js +102 -1
- package/dist/pi-trials.js +60 -24
- package/dist/policy.js +8 -3
- package/dist/recommend.js +27 -14
- package/dist/routes.js +59 -0
- package/dist/standalone.js +220 -0
- package/dist/store.js +43 -10
- package/dist/strategist.js +18 -14
- package/dist/traces.js +6 -2
- package/dist/trials.js +50 -11
- package/examples/task.example.json +1 -0
- package/extension/openmerit.ts +134 -32
- package/instructions/OPENMERIT.md +2 -2
- package/package.json +5 -2
- package/rules.md +39 -0
package/dist/llm.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** Minimal provider clients for judge and strategist requests. */
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
3
6
|
import { paths } from "./store.js";
|
|
7
|
+
import { legacyOpenRouterRoute } from "./routes.js";
|
|
4
8
|
const OR = "https://openrouter.ai/api/v1";
|
|
5
9
|
/** Load OPENROUTER_API_KEY from env or ~/.openmerit/.env. */
|
|
6
10
|
export function loadKey() {
|
|
@@ -100,3 +104,100 @@ export async function chat(key, model, prompt, maxTokens, temperature) {
|
|
|
100
104
|
export async function chatWithImages(key, model, prompt, images, maxTokens) {
|
|
101
105
|
return providerFor(model, key).chatWithImages(model, prompt, images, maxTokens);
|
|
102
106
|
}
|
|
107
|
+
export function directChatClient(key) {
|
|
108
|
+
const id = (model) => typeof model === "string" ? model : model.meritId;
|
|
109
|
+
return {
|
|
110
|
+
chat: (model, prompt, maxTokens, temperature) => chat(key, id(model), prompt, maxTokens, temperature),
|
|
111
|
+
chatWithImages: (model, prompt, images, maxTokens) => chatWithImages(key, id(model), prompt, images, maxTokens),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function textContent(content) {
|
|
115
|
+
if (typeof content === "string")
|
|
116
|
+
return content;
|
|
117
|
+
if (!Array.isArray(content))
|
|
118
|
+
return "";
|
|
119
|
+
return content.filter((part) => !!part && typeof part === "object" && part.type === "text")
|
|
120
|
+
.map((part) => part.text ?? "").join("\n");
|
|
121
|
+
}
|
|
122
|
+
/** Use pi's model registry and credential store without exposing provider secrets to OpenMerit. */
|
|
123
|
+
export class PiCliChatClient {
|
|
124
|
+
executable;
|
|
125
|
+
onSpend;
|
|
126
|
+
id = "pi-cli";
|
|
127
|
+
constructor(executable = process.env.OPENMERIT_PI_BIN?.trim() || "pi", onSpend) {
|
|
128
|
+
this.executable = executable;
|
|
129
|
+
this.onSpend = onSpend;
|
|
130
|
+
}
|
|
131
|
+
chat(model, prompt, _maxTokens, _temperature) {
|
|
132
|
+
return this.run(model, prompt, []);
|
|
133
|
+
}
|
|
134
|
+
chatWithImages(model, prompt, images, _maxTokens) {
|
|
135
|
+
return this.run(model, prompt, images);
|
|
136
|
+
}
|
|
137
|
+
async run(target, prompt, images) {
|
|
138
|
+
const route = typeof target === "string" ? legacyOpenRouterRoute(target) : target;
|
|
139
|
+
const dir = mkdtempSync(join(tmpdir(), "openmerit-pi-chat-"));
|
|
140
|
+
try {
|
|
141
|
+
const imageArgs = images.map((image, index) => {
|
|
142
|
+
const suffix = { "image/jpeg": "jpg", "image/png": "png", "image/webp": "webp", "image/gif": "gif" }[image.mimeType];
|
|
143
|
+
if (!suffix)
|
|
144
|
+
throw new Error(`unsupported pi image: ${image.mimeType}`);
|
|
145
|
+
const file = join(dir, `image-${index}.${suffix}`);
|
|
146
|
+
writeFileSync(file, Buffer.from(image.data, "base64"));
|
|
147
|
+
return `@${file}`;
|
|
148
|
+
});
|
|
149
|
+
const child = spawn(this.executable, [
|
|
150
|
+
"--provider", route.provider, "--model", route.modelId, "--mode", "json", "--offline",
|
|
151
|
+
"--no-extensions", "--no-tools", "--print", ...imageArgs, prompt,
|
|
152
|
+
], { cwd: dir, stdio: ["ignore", "pipe", "pipe"] });
|
|
153
|
+
let stdout = "";
|
|
154
|
+
let stderr = "";
|
|
155
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
156
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
157
|
+
const code = await new Promise((resolve, reject) => {
|
|
158
|
+
child.on("error", reject);
|
|
159
|
+
child.on("close", (value) => resolve(value ?? 1));
|
|
160
|
+
});
|
|
161
|
+
let answer = "";
|
|
162
|
+
let promptTokens = 0;
|
|
163
|
+
let completionTokens = 0;
|
|
164
|
+
let totalTokens = 0;
|
|
165
|
+
let costUsd = 0;
|
|
166
|
+
let failed = false;
|
|
167
|
+
for (const line of stdout.split("\n")) {
|
|
168
|
+
if (!line.trim())
|
|
169
|
+
continue;
|
|
170
|
+
let event;
|
|
171
|
+
try {
|
|
172
|
+
event = JSON.parse(line);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (event.type !== "message_end" || event.message?.role !== "assistant")
|
|
178
|
+
continue;
|
|
179
|
+
answer = textContent(event.message.content) || answer;
|
|
180
|
+
promptTokens += event.message.usage?.input ?? 0;
|
|
181
|
+
completionTokens += event.message.usage?.output ?? 0;
|
|
182
|
+
totalTokens += event.message.usage?.totalTokens ?? 0;
|
|
183
|
+
costUsd += event.message.usage?.cost?.total ?? 0;
|
|
184
|
+
if (event.message.stopReason === "error")
|
|
185
|
+
failed = true;
|
|
186
|
+
}
|
|
187
|
+
if (code !== 0 || failed || !answer.trim()) {
|
|
188
|
+
throw new Error(`pi model call ${route.provider}/${route.modelId} failed: ` +
|
|
189
|
+
(stderr.trim().slice(0, 240) || `exit ${code}, empty answer`));
|
|
190
|
+
}
|
|
191
|
+
this.onSpend?.(costUsd);
|
|
192
|
+
return { content: answer, usage: {
|
|
193
|
+
prompt_tokens: promptTokens || undefined,
|
|
194
|
+
completion_tokens: completionTokens || undefined,
|
|
195
|
+
total_tokens: totalTokens || promptTokens + completionTokens || undefined,
|
|
196
|
+
costUsd,
|
|
197
|
+
} };
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
rmSync(dir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/dist/pi-trials.js
CHANGED
|
@@ -8,6 +8,8 @@ import { judge, judgeWithImages } from "./judge.js";
|
|
|
8
8
|
import { knownInvoiceScore } from "./invoice-eval.js";
|
|
9
9
|
import { paths, readJson } from "./store.js";
|
|
10
10
|
import { sessionFiles, sessionImages, taskInputKey } from "./task-input.js";
|
|
11
|
+
import { legacyOpenRouterRoute, meritModelId, modelRoute, routeKey } from "./routes.js";
|
|
12
|
+
import { directChatClient } from "./llm.js";
|
|
11
13
|
function answerText(content) {
|
|
12
14
|
if (typeof content === "string")
|
|
13
15
|
return content;
|
|
@@ -107,10 +109,13 @@ export function recordedActiveTask(task, model, images = [], sessionFile, sessio
|
|
|
107
109
|
toolErrors++;
|
|
108
110
|
}
|
|
109
111
|
const userImages = user ? sessionImages(user.content) : null;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
const files = sessionFiles(task);
|
|
113
|
+
if (!user || !userImages || taskInputKey(answerText(user.content), userImages, sessionFiles(answerText(user.content))) !==
|
|
114
|
+
taskInputKey(task, images, files))
|
|
112
115
|
return null;
|
|
113
|
-
const matching = assistants.filter((m) =>
|
|
116
|
+
const matching = assistants.filter((m) => typeof model === "string"
|
|
117
|
+
? meritModelId(m.provider ?? "openrouter", m.model ?? "") === model
|
|
118
|
+
: m.provider === model.provider && m.model === model.modelId);
|
|
114
119
|
if (matching.length === 0 || matching.some((m) => m.stopReason === "error"))
|
|
115
120
|
return null;
|
|
116
121
|
const answer = matching.map((m) => answerText(m.content)).filter(Boolean).at(-1) ?? "";
|
|
@@ -167,23 +172,52 @@ export function settledActiveTask(snapshot) {
|
|
|
167
172
|
const files = sessionFiles(task);
|
|
168
173
|
if (!task.trim() || taskInputKey(task, images, files) !== st.settledTaskKey)
|
|
169
174
|
return null;
|
|
170
|
-
const
|
|
175
|
+
const route = st.currentRoute ?? legacyOpenRouterRoute(st.currentModel);
|
|
176
|
+
const run = recordedActiveTask(task, route, images, st.sessionFile, st.sessionBytes);
|
|
171
177
|
if (!run)
|
|
172
178
|
return null;
|
|
179
|
+
const routes = st.routes?.length ? st.routes : [route];
|
|
173
180
|
return { task, images, model: st.currentModel, run, sessionFile: st.sessionFile,
|
|
174
|
-
settledAt: st.settledAt, sessionBytes: st.sessionBytes, cwd: st.cwd ?? process.cwd(), usedTools, files
|
|
181
|
+
settledAt: st.settledAt, sessionBytes: st.sessionBytes, cwd: st.cwd ?? process.cwd(), usedTools, files,
|
|
182
|
+
route, routes };
|
|
175
183
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
export function piExecutable() {
|
|
185
|
+
return process.env.OPENMERIT_PI_BIN?.trim() || "pi";
|
|
186
|
+
}
|
|
187
|
+
function compactNumber(value) {
|
|
188
|
+
const match = value.match(/^(\d+(?:\.\d+)?)([KMG])?$/i);
|
|
189
|
+
if (!match)
|
|
190
|
+
return undefined;
|
|
191
|
+
const scale = { K: 1_000, M: 1_000_000, G: 1_000_000_000 }[(match[2] ?? "").toUpperCase()] ?? 1;
|
|
192
|
+
return Math.round(Number(match[1]) * scale);
|
|
193
|
+
}
|
|
194
|
+
/** Read provider/model routes from pi when no live extension snapshot is available. */
|
|
195
|
+
export function availablePiRoutes(snapshot) {
|
|
196
|
+
if (snapshot?.length)
|
|
197
|
+
return snapshot;
|
|
198
|
+
const result = spawnSync(piExecutable(), ["--offline", "--list-models"], { encoding: "utf8" });
|
|
179
199
|
if (result.error || result.status !== 0)
|
|
180
|
-
throw new Error("could not list
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
throw new Error("could not list models from pi");
|
|
201
|
+
const routes = [];
|
|
202
|
+
for (const line of result.stdout.split("\n").slice(1)) {
|
|
203
|
+
const [provider, id, context, maxOut, , images] = line.trim().split(/\s+/);
|
|
204
|
+
if (!provider || !id || id.startsWith("~"))
|
|
205
|
+
continue;
|
|
206
|
+
routes.push(modelRoute(provider, id, {
|
|
207
|
+
input: images === "yes" ? ["text", "image"] : ["text"],
|
|
208
|
+
contextWindow: compactNumber(context),
|
|
209
|
+
maxTokens: compactNumber(maxOut),
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
return routes;
|
|
213
|
+
}
|
|
214
|
+
/** Backward-compatible logical-id view used by the standalone OpenRouter trial command. */
|
|
215
|
+
export function availablePiModels() {
|
|
216
|
+
return new Set(availablePiRoutes().map((route) => route.meritId));
|
|
184
217
|
}
|
|
185
218
|
/** Each invocation is a fresh pi run, so model B does not inherit model A's answer. */
|
|
186
219
|
export async function executePiTask(model, task, cwd = process.cwd(), images = [], files = []) {
|
|
220
|
+
const route = typeof model === "string" ? legacyOpenRouterRoute(model) : model;
|
|
187
221
|
const imageDir = images.length ? mkdtempSync(join(tmpdir(), "openmerit-pi-image-")) : null;
|
|
188
222
|
const suffix = {
|
|
189
223
|
"image/jpeg": "jpg", "image/png": "png", "image/webp": "webp", "image/gif": "gif",
|
|
@@ -209,10 +243,10 @@ export async function executePiTask(model, task, cwd = process.cwd(), images = [
|
|
|
209
243
|
});
|
|
210
244
|
const candidateTask = [...attachmentPaths.entries()].reduce((text, [source, target]) => text.split(source).join(target), task);
|
|
211
245
|
const before = fileSnapshot(workspace);
|
|
212
|
-
const traceFile = paths.trialTrace(`${
|
|
246
|
+
const traceFile = paths.trialTrace(`${routeKey(route)}:${task}:${Date.now()}`);
|
|
213
247
|
const traceLines = [];
|
|
214
|
-
const child = spawn(
|
|
215
|
-
"--provider",
|
|
248
|
+
const child = spawn(piExecutable(), [
|
|
249
|
+
"--provider", route.provider, "--model", route.modelId, "--mode", "json",
|
|
216
250
|
"--offline", "--no-extensions", "--approve", ...piTrialToolArgs(),
|
|
217
251
|
"--print", ...imageArgs, ...fileArgs, candidateTask,
|
|
218
252
|
], { cwd: workspace, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -275,7 +309,7 @@ export async function executePiTask(model, task, cwd = process.cwd(), images = [
|
|
|
275
309
|
if (!after.has(file))
|
|
276
310
|
changedFiles++;
|
|
277
311
|
if (exitCode !== 0 || !answer.trim()) {
|
|
278
|
-
throw new Error(`pi trial ${
|
|
312
|
+
throw new Error(`pi trial ${route.meritId} via ${route.provider} failed: ${stderr.trim().slice(0, 180) || `exit ${exitCode}, empty answer`}`);
|
|
279
313
|
}
|
|
280
314
|
return { answer, costUsd, latencyMs: Date.now() - started, errors, sessionId,
|
|
281
315
|
toolCalls, toolErrors, changedFiles, traceFile };
|
|
@@ -287,29 +321,30 @@ export async function executePiTask(model, task, cwd = process.cwd(), images = [
|
|
|
287
321
|
}
|
|
288
322
|
}
|
|
289
323
|
/** Quality uses the same OpenMerit judge; cost and latency come from pi itself. */
|
|
290
|
-
export async function runPiTrial(key, judgeModel, task, rubric, model, entry, cwd = process.cwd(), images = [], files = [], harness = defaultHarness) {
|
|
324
|
+
export async function runPiTrial(key, judgeModel, task, rubric, model, entry, cwd = process.cwd(), images = [], files = [], harness = defaultHarness, client = directChatClient(key)) {
|
|
291
325
|
try {
|
|
292
|
-
const run = await harness.runTask(model, { task, cwd, images, files });
|
|
293
|
-
return await scorePiRun(key, judgeModel, task, rubric, model, entry, run, "pi_trial", images);
|
|
326
|
+
const run = await harness.runTask(entry?.route ?? model, { task, cwd, images, files });
|
|
327
|
+
return await scorePiRun(key, judgeModel, task, rubric, model, entry, run, "pi_trial", images, client);
|
|
294
328
|
}
|
|
295
329
|
catch (e) {
|
|
296
330
|
const error = String(e);
|
|
297
331
|
return {
|
|
298
|
-
point: { model, score: 0, price: entry?.price ?? 0,
|
|
332
|
+
point: { schemaVersion: 1, model, route: entry?.route, score: 0, price: entry?.price ?? 0,
|
|
333
|
+
priceKnown: entry?.priceKnown ?? !!entry,
|
|
299
334
|
ts: new Date().toISOString(), source: "pi_trial", toolCalls: 0, toolErrors: 1,
|
|
300
335
|
changedFiles: 0, why: error.slice(0, 160) },
|
|
301
336
|
costUsd: 0, error,
|
|
302
337
|
};
|
|
303
338
|
}
|
|
304
339
|
}
|
|
305
|
-
export async function scorePiRun(key, judgeModel, task, rubric, model, entry, run, source, images = []) {
|
|
340
|
+
export async function scorePiRun(key, judgeModel, task, rubric, model, entry, run, source, images = [], client = directChatClient(key)) {
|
|
306
341
|
let score = 0;
|
|
307
342
|
let why = "judge unavailable";
|
|
308
343
|
try {
|
|
309
344
|
const pinned = images.length ? await knownInvoiceScore(task, images, run.answer) : null;
|
|
310
345
|
const j = pinned ?? (images.length
|
|
311
|
-
? await judgeWithImages(key, judgeModel, task, rubric, run.answer, images)
|
|
312
|
-
: await judge(key, judgeModel, task, rubric, run.answer));
|
|
346
|
+
? await judgeWithImages(key, judgeModel, task, rubric, run.answer, images, client)
|
|
347
|
+
: await judge(key, judgeModel, task, rubric, run.answer, client));
|
|
313
348
|
score = Math.max(0, Math.min(1, j.score));
|
|
314
349
|
why = j.why;
|
|
315
350
|
}
|
|
@@ -318,7 +353,8 @@ export async function scorePiRun(key, judgeModel, task, rubric, model, entry, ru
|
|
|
318
353
|
}
|
|
319
354
|
return {
|
|
320
355
|
point: {
|
|
321
|
-
model, score, price: entry?.price ?? 0,
|
|
356
|
+
schemaVersion: 1, model, route: entry?.route, score, price: entry?.price ?? 0,
|
|
357
|
+
priceKnown: entry?.priceKnown ?? !!entry,
|
|
322
358
|
latencyMs: run.latencyMs, ts: new Date().toISOString(), source,
|
|
323
359
|
why: `${why}${run.errors ? `; pi errors: ${run.errors}` : ""}; ` +
|
|
324
360
|
`tools=${run.toolCalls}, toolErrors=${run.toolErrors}, changedFiles=${run.changedFiles}`,
|
package/dist/policy.js
CHANGED
|
@@ -60,10 +60,11 @@ export function loadPolicy(path) {
|
|
|
60
60
|
max_usd_per_m: num(raw.max_usd_per_m, d.max_usd_per_m),
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
64
|
-
export function providerAllowed(policy, modelId) {
|
|
63
|
+
/** Match either a logical vendor/model id or its concrete route provider. */
|
|
64
|
+
export function providerAllowed(policy, modelId, routeProvider) {
|
|
65
65
|
const vendor = modelId.split("/")[0];
|
|
66
|
-
const match = (pat) => pat === "*" || pat === modelId ||
|
|
66
|
+
const match = (pat) => pat === "*" || pat === modelId || pat === routeProvider ||
|
|
67
|
+
(pat.endsWith("/*") && (pat.slice(0, -2) === vendor || pat.slice(0, -2) === routeProvider));
|
|
67
68
|
if (policy.providers.deny.some(match))
|
|
68
69
|
return false;
|
|
69
70
|
return policy.providers.allow.some(match);
|
|
@@ -87,6 +88,10 @@ export function gate(policy, input) {
|
|
|
87
88
|
reasons.push("recommendation is not bound to a pi session");
|
|
88
89
|
return { autoApply: false, reasons };
|
|
89
90
|
}
|
|
91
|
+
if (input.priceKnown === false) {
|
|
92
|
+
reasons.push("candidate or current-model price is unknown");
|
|
93
|
+
return { autoApply: false, reasons };
|
|
94
|
+
}
|
|
90
95
|
if (policy.mode !== "auto" || !policy.auto_apply.enabled) {
|
|
91
96
|
reasons.push(`policy mode is "${policy.mode}" (auto_apply ${policy.auto_apply.enabled ? "enabled" : "disabled"})`);
|
|
92
97
|
return { autoApply: false, reasons };
|
package/dist/recommend.js
CHANGED
|
@@ -2,40 +2,49 @@
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { paretoFrontier, pickBest, pickFallback } from "./frontier.js";
|
|
4
4
|
import { gate, providerAllowed } from "./policy.js";
|
|
5
|
-
|
|
5
|
+
import { pointKey, routeLabel } from "./routes.js";
|
|
6
|
+
export function buildRecommendation(taskKey, taskLabel, currentModel, points, policy, sessionFile = null, currentRoute = currentModel ? points.find((p) => p.model === currentModel)?.route ?? null : null) {
|
|
6
7
|
const scored = points.filter((p) => p.score > 0);
|
|
7
8
|
if (scored.length === 0)
|
|
8
9
|
return null;
|
|
9
10
|
const best = pickBest(scored);
|
|
10
11
|
if (!best)
|
|
11
12
|
return null;
|
|
12
|
-
if (currentModel && best.model === currentModel
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if (currentModel && best.model === currentModel &&
|
|
14
|
+
(!currentRoute || pointKey(best) === `${currentRoute.provider}:${currentRoute.modelId}`))
|
|
15
|
+
return null;
|
|
16
|
+
const current = currentModel ? scored.find((p) => currentRoute
|
|
17
|
+
? pointKey(p) === `${currentRoute.provider}:${currentRoute.modelId}`
|
|
18
|
+
: p.model === currentModel) : undefined;
|
|
15
19
|
const scoreGain = current ? best.score - current.score : 0;
|
|
16
20
|
// pickBest may choose a cheaper or faster model at equal quality.
|
|
17
21
|
if (current && scoreGain < 0)
|
|
18
22
|
return null;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
23
|
+
const priceKnown = best.priceKnown !== false && (!current || current.priceKnown !== false);
|
|
24
|
+
const priceRatio = priceKnown && current && current.price > 0 ? best.price / current.price : 1;
|
|
25
|
+
const frontierModels = paretoFrontier(scored).map(pointKey);
|
|
26
|
+
const onFrontier = frontierModels.includes(pointKey(best));
|
|
22
27
|
const fb = pickFallback(scored, best, policy.fallback.min_score);
|
|
23
28
|
const decision = gate(policy, {
|
|
24
29
|
scoreGain,
|
|
25
30
|
priceRatio,
|
|
31
|
+
priceKnown,
|
|
26
32
|
onFrontier,
|
|
27
|
-
providerOk: providerAllowed(policy, best.model),
|
|
33
|
+
providerOk: providerAllowed(policy, best.model, best.route?.provider),
|
|
28
34
|
baselineMeasured: !!current,
|
|
29
35
|
sessionBound: !!sessionFile,
|
|
30
36
|
});
|
|
31
|
-
const
|
|
37
|
+
const bestLabel = best.route ? routeLabel(best.route) : best.model;
|
|
38
|
+
const reason = `${bestLabel} scores ${best.score.toFixed(2)} vs ` +
|
|
32
39
|
(current ? `${current.score.toFixed(2)} for ${current.model}` : "no baseline measured") +
|
|
33
|
-
` (${scoreGain > 0 ? `quality gain ${scoreGain.toFixed(2)}` : "equal measured quality"}),
|
|
34
|
-
(
|
|
40
|
+
` (${scoreGain > 0 ? `quality gain ${scoreGain.toFixed(2)}` : "equal measured quality"}), ` +
|
|
41
|
+
(best.priceKnown === false ? "with unknown price" : `at $${best.price.toFixed(2)}/M`) +
|
|
42
|
+
(current && priceKnown ? ` (${priceRatio.toFixed(2)}x current price)` : "") +
|
|
35
43
|
(best.why ? `. Judge: ${best.why}` : "");
|
|
36
44
|
return {
|
|
45
|
+
schemaVersion: 1,
|
|
37
46
|
id: createHash("sha1")
|
|
38
|
-
.update(`${taskKey}:${best
|
|
47
|
+
.update(`${taskKey}:${pointKey(best)}:${Date.now()}`)
|
|
39
48
|
.digest("hex")
|
|
40
49
|
.slice(0, 10),
|
|
41
50
|
ts: new Date().toISOString(),
|
|
@@ -43,17 +52,21 @@ export function buildRecommendation(taskKey, taskLabel, currentModel, points, po
|
|
|
43
52
|
taskLabel,
|
|
44
53
|
sessionFile,
|
|
45
54
|
currentModel,
|
|
46
|
-
|
|
55
|
+
currentRoute,
|
|
56
|
+
recommended: { model: best.model, route: best.route, reason },
|
|
47
57
|
fallback: fb
|
|
48
58
|
? {
|
|
49
59
|
model: fb.model,
|
|
50
|
-
|
|
60
|
+
route: fb.route,
|
|
61
|
+
reason: `score ${fb.score.toFixed(2)} ` +
|
|
62
|
+
(fb.priceKnown === false ? "with unknown price" : `at $${fb.price.toFixed(2)}/M`) +
|
|
51
63
|
(fb.latencyMs ? `, ~${Math.round(fb.latencyMs)}ms` : ""),
|
|
52
64
|
}
|
|
53
65
|
: null,
|
|
54
66
|
evidence: {
|
|
55
67
|
scoreGain: Math.round(scoreGain * 1000) / 1000,
|
|
56
68
|
priceRatio: Math.round(priceRatio * 100) / 100,
|
|
69
|
+
priceKnown,
|
|
57
70
|
onFrontier,
|
|
58
71
|
trials: scored.length,
|
|
59
72
|
},
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Provider-neutral model-route normalization. */
|
|
2
|
+
export function meritModelId(provider, modelId) {
|
|
3
|
+
if (provider === "openrouter")
|
|
4
|
+
return modelId;
|
|
5
|
+
return modelId.startsWith(`${provider}/`) ? modelId : `${provider}/${modelId}`;
|
|
6
|
+
}
|
|
7
|
+
export function modelRoute(provider, modelId, details = {}) {
|
|
8
|
+
return {
|
|
9
|
+
provider,
|
|
10
|
+
modelId,
|
|
11
|
+
meritId: meritModelId(provider, modelId),
|
|
12
|
+
input: details.input ?? ["text"],
|
|
13
|
+
...details,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function legacyOpenRouterRoute(meritId) {
|
|
17
|
+
return modelRoute("openrouter", meritId);
|
|
18
|
+
}
|
|
19
|
+
export function routeKey(route) {
|
|
20
|
+
return `${route.provider}:${route.modelId}`;
|
|
21
|
+
}
|
|
22
|
+
export function pointKey(point) {
|
|
23
|
+
return point.route ? routeKey(point.route) : `openrouter:${point.model}`;
|
|
24
|
+
}
|
|
25
|
+
export function routeLabel(route) {
|
|
26
|
+
return route.provider === "openrouter" ? route.meritId : `${route.meritId} via ${route.provider}`;
|
|
27
|
+
}
|
|
28
|
+
export function routePrice(route) {
|
|
29
|
+
if (!route.cost || !Number.isFinite(route.cost.input) || !Number.isFinite(route.cost.output))
|
|
30
|
+
return { price: 0, known: false };
|
|
31
|
+
return { price: Math.round(((route.cost.input + route.cost.output) / 2 + Number.EPSILON) * 1e4) / 1e4,
|
|
32
|
+
known: true };
|
|
33
|
+
}
|
|
34
|
+
export function catalogEntryFromRoute(route) {
|
|
35
|
+
const priced = routePrice(route);
|
|
36
|
+
return {
|
|
37
|
+
id: route.meritId,
|
|
38
|
+
name: routeLabel(route),
|
|
39
|
+
ctx: route.contextWindow ?? 0,
|
|
40
|
+
pp: route.cost ? route.cost.input / 1e6 : 0,
|
|
41
|
+
pc: route.cost ? route.cost.output / 1e6 : 0,
|
|
42
|
+
price: priced.price,
|
|
43
|
+
priceKnown: priced.known,
|
|
44
|
+
inputModalities: route.input,
|
|
45
|
+
route,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function routeCatalog(routes) {
|
|
49
|
+
const out = new Map();
|
|
50
|
+
for (const route of routes)
|
|
51
|
+
out.set(routeKey(route), catalogEntryFromRoute(route));
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** Preserve the route while enriching an OpenRouter entry with its live catalog metadata. */
|
|
55
|
+
export function enrichRouteEntry(entry, live) {
|
|
56
|
+
if (!live)
|
|
57
|
+
return entry;
|
|
58
|
+
return { ...live, route: entry.route, id: entry.id, name: entry.name, priceKnown: true };
|
|
59
|
+
}
|