@bytesbrains/cruise 0.0.0-stage → 0.12.1
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 +202 -0
- package/README.md +59 -2
- package/dist/budget-FIRNSN67.mjs +132 -0
- package/dist/chunk-2FB2T3UI.mjs +122 -0
- package/dist/chunk-2NVDAK5J.mjs +76 -0
- package/dist/chunk-42ERHAJD.mjs +79 -0
- package/dist/chunk-4447IUTF.mjs +7 -0
- package/dist/chunk-5TVD3ZAI.mjs +69 -0
- package/dist/chunk-7NQHBXVZ.mjs +104 -0
- package/dist/chunk-BNP63KB2.mjs +42 -0
- package/dist/chunk-FGLMW5IY.mjs +575 -0
- package/dist/chunk-GUZNI3NS.mjs +267 -0
- package/dist/chunk-HQDQTXRX.mjs +57 -0
- package/dist/chunk-ICJVJSTC.mjs +42 -0
- package/dist/chunk-JZPUTXM7.mjs +236 -0
- package/dist/chunk-LALZUKEC.mjs +137 -0
- package/dist/chunk-N3YKWKOA.mjs +9 -0
- package/dist/chunk-O7TNRKSA.mjs +2 -0
- package/dist/chunk-RT4R2W6H.mjs +21 -0
- package/dist/chunk-SYTYOTO4.mjs +18 -0
- package/dist/chunk-T3ICD43U.mjs +15 -0
- package/dist/chunk-THEXUHQC.mjs +98 -0
- package/dist/chunk-UNXM6FSM.mjs +83 -0
- package/dist/chunk-WS6NFRIU.mjs +66 -0
- package/dist/chunk-XOH3XKM3.mjs +126 -0
- package/dist/compress-3DWG35BY.mjs +49 -0
- package/dist/conformance-NPFDYCQL.mjs +922 -0
- package/dist/cruise.mjs +693 -0
- package/dist/fallback-P5KK55M4.mjs +139 -0
- package/dist/keys-FWPQPMTW.mjs +127 -0
- package/dist/ledger-XBGWXY6C.mjs +97 -0
- package/dist/member-ZBV4YG2M.mjs +74 -0
- package/dist/report-VRYJ3W5L.mjs +414 -0
- package/dist/service-key-N2RZ7FMZ.mjs +59 -0
- package/dist/smoke-ZNYX6RO5.mjs +105 -0
- package/dist/sweep-SZX7OIDK.mjs +33 -0
- package/dist/tenant-4YDGI23Q.mjs +122 -0
- package/dist/tier-4L3SPLFP.mjs +116 -0
- package/package.json +38 -4
- package/templates/docker-compose.yml +66 -0
- package/templates/env.example +44 -0
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built by packages/cli/build.ts.
|
|
3
|
+
import {
|
|
4
|
+
listFlag,
|
|
5
|
+
planShard,
|
|
6
|
+
selectModels
|
|
7
|
+
} from "./chunk-WS6NFRIU.mjs";
|
|
8
|
+
import {
|
|
9
|
+
usageFromJson
|
|
10
|
+
} from "./chunk-ICJVJSTC.mjs";
|
|
11
|
+
import "./chunk-LALZUKEC.mjs";
|
|
12
|
+
import {
|
|
13
|
+
parseKeyPrefix
|
|
14
|
+
} from "./chunk-5TVD3ZAI.mjs";
|
|
15
|
+
import {
|
|
16
|
+
connect,
|
|
17
|
+
environmentOf,
|
|
18
|
+
parseArgs,
|
|
19
|
+
requireString,
|
|
20
|
+
run
|
|
21
|
+
} from "./chunk-7NQHBXVZ.mjs";
|
|
22
|
+
import "./chunk-JZPUTXM7.mjs";
|
|
23
|
+
|
|
24
|
+
// src/conformance-tools.ts
|
|
25
|
+
var TOOL_NAME = "get_weather";
|
|
26
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
var firstChoice = (body) => {
|
|
28
|
+
const choices = isObject(body) && Array.isArray(body.choices) ? body.choices : [];
|
|
29
|
+
return isObject(choices[0]) ? choices[0] : void 0;
|
|
30
|
+
};
|
|
31
|
+
var terminator = (choice) => String(choice?.finish_reason ?? choice?.stop_reason ?? "<absent>");
|
|
32
|
+
function callsOf(body) {
|
|
33
|
+
const calls = firstChoice(body)?.message?.tool_calls;
|
|
34
|
+
if (Array.isArray(calls) && calls.length > 0) {
|
|
35
|
+
return calls.map((call) => {
|
|
36
|
+
const fn = isObject(call) && isObject(call.function) ? call.function : {};
|
|
37
|
+
return { name: fn.name, args: fn.arguments };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const output = isObject(body) && Array.isArray(body.output) ? body.output : [];
|
|
41
|
+
return output.filter((item) => isObject(item) && item.type === "function_call").map((item) => ({ name: item.name, args: item.arguments }));
|
|
42
|
+
}
|
|
43
|
+
function malformed(call) {
|
|
44
|
+
if (call.name !== TOOL_NAME) return `Called '${String(call.name).slice(0, 80)}', which is not the offered tool '${TOOL_NAME}'.`;
|
|
45
|
+
let args = call.args;
|
|
46
|
+
try {
|
|
47
|
+
if (typeof args === "string") args = JSON.parse(args);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
if (!isObject(args)) return `Arguments are not a JSON object: ${String(call.args).slice(0, 80)}`;
|
|
51
|
+
return typeof args.city === "string" ? null : `Arguments omit the required string 'city': ${JSON.stringify(args).slice(0, 80)}`;
|
|
52
|
+
}
|
|
53
|
+
function checkToolCall(body, name = "tool_call") {
|
|
54
|
+
const choice = firstChoice(body);
|
|
55
|
+
const finish = isObject(body) && Array.isArray(body.output) && choice === void 0 ? "completed" : terminator(choice);
|
|
56
|
+
const calls = callsOf(body);
|
|
57
|
+
if (calls.length > 0) {
|
|
58
|
+
const problem = calls.map(malformed).find((p) => p !== null);
|
|
59
|
+
return problem === void 0 ? { check: name, outcome: "pass", observed: `${finish}, ${calls.length} call(s)` } : { check: name, outcome: "fail", observed: `${finish}, ${calls.length} call(s)`, detail: `Malformed tool call. ${problem}` };
|
|
60
|
+
}
|
|
61
|
+
const content = typeof choice?.message?.content === "string" ? choice.message.content : "";
|
|
62
|
+
const looksLikeProse = /"?(name|function|get_weather)"?\s*[:=]/i.test(content);
|
|
63
|
+
return {
|
|
64
|
+
check: name,
|
|
65
|
+
outcome: "fail",
|
|
66
|
+
observed: finish,
|
|
67
|
+
detail: looksLikeProse ? "Tool call emitted as text content, with no tool_calls array." : "No tool_calls array returned."
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function checkParallelToolCalls(body) {
|
|
71
|
+
const calls = callsOf(body);
|
|
72
|
+
const problem = calls.map(malformed).find((p) => p !== null);
|
|
73
|
+
const count = calls.filter((call) => malformed(call) === null).length;
|
|
74
|
+
if (count > 1) return { check: "parallel_tool_calls", outcome: "pass", observed: `${count} call(s)` };
|
|
75
|
+
return {
|
|
76
|
+
check: "parallel_tool_calls",
|
|
77
|
+
outcome: "fail",
|
|
78
|
+
observed: `${count} call(s), ${terminator(firstChoice(body))}`,
|
|
79
|
+
detail: problem !== void 0 ? `Returned ${calls.length} call(s), of which ${count} could be run. ${problem}` : count === 1 ? "Returned a single tool call. Consistent with either an inability to parallelise or a choice not to, so this only records absence after every attempt agrees." : "Returned no tool calls at all, despite tool calling being supported."
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/conformance-search.ts
|
|
84
|
+
var WEB_SEARCH_CHECK = {
|
|
85
|
+
name: "web_search",
|
|
86
|
+
decides: "web_search",
|
|
87
|
+
attempts: 2,
|
|
88
|
+
request: {
|
|
89
|
+
messages: [
|
|
90
|
+
{
|
|
91
|
+
role: "user",
|
|
92
|
+
content: "Search the web: what is the latest Node.js LTS release, and when was it published? Answer in one sentence."
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
// One search is the whole question, and `max_uses` bounds it where the
|
|
96
|
+
// host honours one. Perplexity does not (0101), and the conformance key
|
|
97
|
+
// is the one key served there anyway.
|
|
98
|
+
tools: [{ type: "web_search", max_uses: 1 }],
|
|
99
|
+
max_tokens: 512
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function checkWebSearch(body) {
|
|
103
|
+
const searches = usageFromJson(body)?.web_search_requests;
|
|
104
|
+
if (searches === void 0) {
|
|
105
|
+
return { check: "web_search", outcome: "fail", observed: "<unreported>", detail: "No search count in usage: a search the ledger cannot see cannot be billed." };
|
|
106
|
+
}
|
|
107
|
+
return searches >= 1 ? { check: "web_search", outcome: "pass", observed: `searches=${searches}` } : { check: "web_search", outcome: "fail", observed: "searches=0", detail: "The host reported no search for a request that asked for one." };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/conformance.ts
|
|
111
|
+
var WEATHER_TOOL = {
|
|
112
|
+
type: "function",
|
|
113
|
+
function: {
|
|
114
|
+
name: TOOL_NAME,
|
|
115
|
+
description: "Get the current weather for a city.",
|
|
116
|
+
parameters: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: { city: { type: "string" } },
|
|
119
|
+
required: ["city"]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var RED_PNG = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAJ0lEQVR42u3NsQkAAAjAsP7/tF7hIASyp6lTCQQCgUAgEAgEgi/BAjLD/C5w/SM9AAAAAElFTkSuQmCC";
|
|
124
|
+
var CHECKS = [
|
|
125
|
+
{
|
|
126
|
+
name: "chat_completion",
|
|
127
|
+
decides: null,
|
|
128
|
+
request: {
|
|
129
|
+
messages: [{ role: "user", content: "Reply with exactly: ok" }],
|
|
130
|
+
// Generous on purpose. A reasoning model spends completion tokens thinking
|
|
131
|
+
// before it emits any content, so a tight cap truncates it to empty output
|
|
132
|
+
// and the check records a working model as broken — measured against
|
|
133
|
+
// deepseek-v4-flash, which used 11 of 13 completion tokens reasoning.
|
|
134
|
+
max_tokens: 256
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "usage_reported",
|
|
139
|
+
decides: "usage_reported",
|
|
140
|
+
request: {
|
|
141
|
+
messages: [{ role: "user", content: "Reply with exactly: ok" }],
|
|
142
|
+
max_tokens: 256
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "tool_call",
|
|
147
|
+
decides: "tools",
|
|
148
|
+
// OpenAI's gpt-6-astra (and gpt-5.6-*) refuse function tools on the legacy
|
|
149
|
+
// /v1/chat/completions endpoint, requiring /v1/responses. Cruise's translation
|
|
150
|
+
// adapter (#545) transparently routes tool calls for these models to
|
|
151
|
+
// /v1/responses upstream, allowing tools=1 to be measured honestly.
|
|
152
|
+
//
|
|
153
|
+
// Three attempts since #549. On 2026-09-30 gemini-3.1-pro-preview passed on
|
|
154
|
+
// production at 04:10 UTC and answered `MALFORMED_FUNCTION_CALL` twice within
|
|
155
|
+
// the hour, passing on staging both times between. One sample recorded
|
|
156
|
+
// tools = 0 and took it out of all four coding lanes; absence refuses
|
|
157
|
+
// callers, so it needs every attempt to agree (see `attempts`).
|
|
158
|
+
attempts: 3,
|
|
159
|
+
request: {
|
|
160
|
+
messages: [{ role: "user", content: "What is the weather in Paris? Use the tool." }],
|
|
161
|
+
tools: [WEATHER_TOOL]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "parallel_tool_calls",
|
|
166
|
+
decides: "parallel_tool_calls",
|
|
167
|
+
// Undefined without tool calling at all, and retried before concluding
|
|
168
|
+
// absence — see `dependsOn` and `attempts`.
|
|
169
|
+
dependsOn: "tool_call",
|
|
170
|
+
attempts: 3,
|
|
171
|
+
request: {
|
|
172
|
+
messages: [
|
|
173
|
+
{
|
|
174
|
+
role: "system",
|
|
175
|
+
content: "You call tools. get_weather accepts exactly one city per call, so covering several cities requires several separate calls in one response. Never ask the user to wait, and never combine cities into one call."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
role: "user",
|
|
179
|
+
content: "I need the current weather for all three of Paris, Tokyo and Cairo right now. Call get_weather once per city."
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
tools: [WEATHER_TOOL],
|
|
183
|
+
parallel_tool_calls: true
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "tool_choice_forced",
|
|
188
|
+
decides: "tool_choice_forced",
|
|
189
|
+
// Allowed and forced come apart (#570): Claude Fable 5.1, Opus 5.5 and
|
|
190
|
+
// Sonnet 5.5 call a tool when allowed and answer 400 when `tool_choice`
|
|
191
|
+
// forces one, and `tool_call` sends no `tool_choice`, so it measured them
|
|
192
|
+
// `tools = 1` and lanes promised what a forced request then refused.
|
|
193
|
+
// `"required"` stands for both forcing shapes — a host that refuses one
|
|
194
|
+
// refuses the other, since both become Anthropic's `any`/`tool` — and is
|
|
195
|
+
// the form `messages-translate.ts` emits for `any`. Same evaluator as
|
|
196
|
+
// `tool_call`: forced or not, a malformed call is not a call.
|
|
197
|
+
dependsOn: "tool_call",
|
|
198
|
+
attempts: 2,
|
|
199
|
+
request: {
|
|
200
|
+
messages: [{ role: "user", content: "What is the weather in Paris?" }],
|
|
201
|
+
tools: [WEATHER_TOOL],
|
|
202
|
+
tool_choice: "required"
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "streaming",
|
|
207
|
+
decides: "streaming",
|
|
208
|
+
streaming: true,
|
|
209
|
+
request: {
|
|
210
|
+
messages: [{ role: "user", content: "Count from one to five." }],
|
|
211
|
+
stream: true,
|
|
212
|
+
max_tokens: 64
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "json_schema",
|
|
217
|
+
decides: "json_schema",
|
|
218
|
+
// A fail here is the provider refusing `response_format` or the model
|
|
219
|
+
// answering outside the schema. Retried once: a strict schema is a thing a
|
|
220
|
+
// model can get right on the second sample.
|
|
221
|
+
attempts: 2,
|
|
222
|
+
request: {
|
|
223
|
+
messages: [{ role: "user", content: "Where is the Eiffel Tower? Answer with the city and the country." }],
|
|
224
|
+
response_format: {
|
|
225
|
+
type: "json_schema",
|
|
226
|
+
json_schema: {
|
|
227
|
+
name: "place",
|
|
228
|
+
strict: true,
|
|
229
|
+
schema: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: { city: { type: "string" }, country: { type: "string" } },
|
|
232
|
+
required: ["city", "country"],
|
|
233
|
+
additionalProperties: false
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
max_tokens: 256
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "vision",
|
|
242
|
+
decides: "vision",
|
|
243
|
+
// A 32×32 solid red PNG, inline, so the check depends on no host. A model
|
|
244
|
+
// without image input refuses the part or describes what it cannot see;
|
|
245
|
+
// either is a fail, and the second is why the answer is checked for the
|
|
246
|
+
// colour rather than for being non-empty.
|
|
247
|
+
attempts: 2,
|
|
248
|
+
request: {
|
|
249
|
+
messages: [
|
|
250
|
+
{
|
|
251
|
+
role: "user",
|
|
252
|
+
content: [
|
|
253
|
+
{ type: "text", text: "What colour is this image? Answer with one word." },
|
|
254
|
+
{ type: "image_url", image_url: { url: `data:image/png;base64,${RED_PNG}` } }
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
// Generous for the same reason as chat_completion: gemma-4 spent 86
|
|
259
|
+
// completion tokens reasoning before saying "Red", and a 64-token cap
|
|
260
|
+
// recorded a model that can see as one that cannot. Measured 2026-08-22.
|
|
261
|
+
max_tokens: 256
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
WEB_SEARCH_CHECK,
|
|
265
|
+
{
|
|
266
|
+
name: "truncation",
|
|
267
|
+
decides: null,
|
|
268
|
+
request: {
|
|
269
|
+
messages: [{ role: "user", content: "Write a long essay about the sea." }],
|
|
270
|
+
max_tokens: 8
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
];
|
|
274
|
+
var terminator2 = (choice) => String(choice?.finish_reason ?? choice?.stop_reason ?? "<absent>");
|
|
275
|
+
function checkChatCompletion(body) {
|
|
276
|
+
const choice = body.choices?.[0];
|
|
277
|
+
if (!choice) return { check: "chat_completion", outcome: "fail", detail: "No choices returned." };
|
|
278
|
+
const content = typeof choice.message?.content === "string" ? choice.message.content : "";
|
|
279
|
+
const finish = terminator2(choice);
|
|
280
|
+
if (choice.message?.refusal != null || finish.toLowerCase().includes("refus")) {
|
|
281
|
+
return { check: "chat_completion", outcome: "fail", observed: finish, detail: "Refusal returned as success." };
|
|
282
|
+
}
|
|
283
|
+
if (content.trim() === "") {
|
|
284
|
+
const reasoning = body.usage?.completion_tokens_details?.reasoning_tokens;
|
|
285
|
+
const spentThinking = typeof reasoning === "number" && reasoning > 0;
|
|
286
|
+
return {
|
|
287
|
+
check: "chat_completion",
|
|
288
|
+
outcome: "fail",
|
|
289
|
+
observed: finish,
|
|
290
|
+
detail: spentThinking ? `Truncated before any content: ${reasoning} completion tokens went to reasoning. Raise max_tokens for this model.` : "Empty content returned as success."
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return { check: "chat_completion", outcome: "pass", observed: finish };
|
|
294
|
+
}
|
|
295
|
+
function checkUsageReported(body) {
|
|
296
|
+
const usage = body.usage;
|
|
297
|
+
const ok = typeof usage?.prompt_tokens === "number" && typeof usage?.completion_tokens === "number";
|
|
298
|
+
return {
|
|
299
|
+
check: "usage_reported",
|
|
300
|
+
outcome: ok ? "pass" : "fail",
|
|
301
|
+
observed: usage ? JSON.stringify(usage) : "<absent>",
|
|
302
|
+
...ok ? {} : { detail: "No usable usage block; spend cannot be attributed." }
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function checkTruncation(body) {
|
|
306
|
+
const finish = terminator2(body.choices?.[0]);
|
|
307
|
+
const truncated = /length|max_tokens|truncat/i.test(finish);
|
|
308
|
+
return {
|
|
309
|
+
check: "truncation",
|
|
310
|
+
outcome: truncated ? "pass" : "fail",
|
|
311
|
+
observed: finish,
|
|
312
|
+
...truncated ? {} : { detail: "Truncation not distinguishable from a clean completion by its terminator." }
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function checkStreaming(sse) {
|
|
316
|
+
const dataLines = sse.split("\n").filter((line) => line.trim().startsWith("data:"));
|
|
317
|
+
const terminated = dataLines.some((line) => line.includes("[DONE]"));
|
|
318
|
+
let assembled = "";
|
|
319
|
+
let usageSeen = false;
|
|
320
|
+
for (const line of dataLines) {
|
|
321
|
+
const payload = line.trim().slice(5).trim();
|
|
322
|
+
if (payload === "" || payload === "[DONE]") continue;
|
|
323
|
+
try {
|
|
324
|
+
const chunk = JSON.parse(payload);
|
|
325
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
326
|
+
if (typeof delta === "string") assembled += delta;
|
|
327
|
+
if (chunk.usage) usageSeen = true;
|
|
328
|
+
} catch {
|
|
329
|
+
return { check: "streaming", outcome: "fail", detail: "Chunk was not valid JSON." };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const problems = [];
|
|
333
|
+
if (dataLines.length === 0) problems.push("no data chunks");
|
|
334
|
+
if (!terminated) problems.push("no [DONE] terminator");
|
|
335
|
+
if (assembled.trim() === "") problems.push("no content assembled");
|
|
336
|
+
return {
|
|
337
|
+
check: "streaming",
|
|
338
|
+
outcome: problems.length === 0 ? "pass" : "fail",
|
|
339
|
+
observed: `${dataLines.length} chunk(s), usage ${usageSeen ? "present" : "absent"}`,
|
|
340
|
+
...problems.length === 0 ? {} : { detail: problems.join("; ") }
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function contentText(body) {
|
|
344
|
+
const content = body.choices?.[0]?.message?.content;
|
|
345
|
+
if (typeof content === "string") return content;
|
|
346
|
+
if (Array.isArray(content)) {
|
|
347
|
+
return content.map((part) => typeof part?.text === "string" ? part.text : "").join("");
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
function checkJsonSchema(body) {
|
|
352
|
+
const text = contentText(body);
|
|
353
|
+
if (text === null || text.trim() === "") {
|
|
354
|
+
return { check: "json_schema", outcome: "fail", detail: "No content.", observed: terminator2(body.choices?.[0]) };
|
|
355
|
+
}
|
|
356
|
+
let parsed;
|
|
357
|
+
try {
|
|
358
|
+
parsed = JSON.parse(text);
|
|
359
|
+
} catch {
|
|
360
|
+
return { check: "json_schema", outcome: "fail", detail: "Content is not JSON.", observed: text.slice(0, 80) };
|
|
361
|
+
}
|
|
362
|
+
const object = parsed;
|
|
363
|
+
const keys = Object.keys(object ?? {}).sort();
|
|
364
|
+
const shaped = typeof object === "object" && object !== null && typeof object.city === "string" && typeof object.country === "string" && keys.join(",") === "city,country";
|
|
365
|
+
return shaped ? { check: "json_schema", outcome: "pass", observed: text.slice(0, 80) } : { check: "json_schema", outcome: "fail", detail: "JSON does not match the schema.", observed: text.slice(0, 80) };
|
|
366
|
+
}
|
|
367
|
+
function checkVision(body) {
|
|
368
|
+
const text = contentText(body);
|
|
369
|
+
if (text === null || text.trim() === "") {
|
|
370
|
+
return { check: "vision", outcome: "fail", detail: "No content.", observed: terminator2(body.choices?.[0]) };
|
|
371
|
+
}
|
|
372
|
+
const observed = text.trim().slice(0, 80);
|
|
373
|
+
if (/\b(not|no|never|cannot|can't|unable|isn't|doesn't)\b/i.test(text)) {
|
|
374
|
+
return { check: "vision", outcome: "fail", detail: "Hedged or negated rather than answered.", observed };
|
|
375
|
+
}
|
|
376
|
+
return /\bred(dish)?\b/i.test(text) ? { check: "vision", outcome: "pass", observed } : { check: "vision", outcome: "fail", detail: "Did not identify the colour.", observed };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/conformance-image.ts
|
|
380
|
+
var IMAGE_PROMPT = "A plain grey square on a white background.";
|
|
381
|
+
var INTRODUCTION_SIZES = ["1024x1024", "1536x1024", "1024x1536"];
|
|
382
|
+
var INTRODUCTION_QUALITIES = ["low", "medium", "high"];
|
|
383
|
+
function checkImageGeneration(body) {
|
|
384
|
+
const first = body.data?.[0];
|
|
385
|
+
if (first === void 0) {
|
|
386
|
+
return { check: "image_generation", outcome: "fail", detail: "No data returned." };
|
|
387
|
+
}
|
|
388
|
+
const b64 = typeof first.b64_json === "string" ? first.b64_json : "";
|
|
389
|
+
const url = typeof first.url === "string" ? first.url : "";
|
|
390
|
+
if (b64.trim() === "" && url.trim() === "") {
|
|
391
|
+
return {
|
|
392
|
+
check: "image_generation",
|
|
393
|
+
outcome: "fail",
|
|
394
|
+
observed: Object.keys(first).join(",") || "<empty object>",
|
|
395
|
+
detail: "An entry was returned carrying neither b64_json nor url."
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
check: "image_generation",
|
|
400
|
+
outcome: "pass",
|
|
401
|
+
observed: b64 !== "" ? `b64_json, ${b64.length} chars` : "url"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function checkImageUsage(body) {
|
|
405
|
+
const usage = body.usage;
|
|
406
|
+
const input = usage?.input_tokens;
|
|
407
|
+
const output = usage?.output_tokens;
|
|
408
|
+
const ok = typeof input === "number" && typeof output === "number";
|
|
409
|
+
return {
|
|
410
|
+
check: "image_usage",
|
|
411
|
+
outcome: ok ? "pass" : "fail",
|
|
412
|
+
observed: usage ? JSON.stringify(usage) : "<absent>",
|
|
413
|
+
...ok ? {} : { detail: "No usable usage block; spend cannot be attributed." }
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
var IMAGE_CHECKS = [
|
|
417
|
+
{ name: "image_generation", decides: null, evaluate: checkImageGeneration },
|
|
418
|
+
{ name: "image_usage", decides: "usage_reported", evaluate: checkImageUsage }
|
|
419
|
+
];
|
|
420
|
+
var COMPONENT_IMAGE_CHECKS = [
|
|
421
|
+
{ name: "image_generation", decides: null, evaluate: checkImageGeneration }
|
|
422
|
+
];
|
|
423
|
+
function outputTokensOf(body) {
|
|
424
|
+
const output = body.usage?.output_tokens;
|
|
425
|
+
return typeof output === "number" && Number.isInteger(output) && output > 0 ? output : null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/conformance-verdict.ts
|
|
429
|
+
var HOST_FEATURE_CHECKS = /* @__PURE__ */ new Set(["web_search", "tool_choice_forced"]);
|
|
430
|
+
function answeredByModel(results) {
|
|
431
|
+
return results.filter(
|
|
432
|
+
(result) => result.outcome === "pass" || result.outcome === "fail" && !(HOST_FEATURE_CHECKS.has(result.check) && /^HTTP \d{3}\b/.test(result.observed ?? ""))
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
function unservableReason(results) {
|
|
436
|
+
if (answeredByModel(results).length > 0) return null;
|
|
437
|
+
const errors = results.filter((result) => result.outcome === "error");
|
|
438
|
+
if (errors.length === 0) return null;
|
|
439
|
+
const refusals = errors.filter((result) => /^HTTP 40[13]\b/.test(result.observed ?? ""));
|
|
440
|
+
if (refusals.length !== errors.length) return null;
|
|
441
|
+
const codes = [...new Set(refusals.map((result) => result.observed))].sort();
|
|
442
|
+
return `The provider refused every check: ${codes.join(", ")}. This is an entitlement \u2014 a plan, a tier or a licence \u2014 not a property of the model, and no further run will change it.`;
|
|
443
|
+
}
|
|
444
|
+
function failedEveryCheckReason(results) {
|
|
445
|
+
const answered = answeredByModel(results);
|
|
446
|
+
if (answered.length === 0) return null;
|
|
447
|
+
if (answered.some((result) => result.outcome === "pass")) return null;
|
|
448
|
+
const statuses = (of) => [...new Set(of.map((result) => result.observed).filter((o) => !!o))].sort();
|
|
449
|
+
const observed = statuses(answered);
|
|
450
|
+
const saw = observed.length > 0 ? ` (${observed.join(", ")})` : "";
|
|
451
|
+
const errors = results.filter((result) => result.outcome === "error");
|
|
452
|
+
const erroredStatuses = statuses(errors);
|
|
453
|
+
const errored = errors.length > 0 ? ` ${errors.length} more errored${erroredStatuses.length > 0 ? ` (${erroredStatuses.join(", ")})` : ""} and say nothing either way.` : "";
|
|
454
|
+
return `Every check the model answered failed${saw}.${errored} A model that has not been seen to serve is unmeasured whatever the date says; the next run in which any check passes will measure it.`;
|
|
455
|
+
}
|
|
456
|
+
function flagsFromResults(results, checks = CHECKS) {
|
|
457
|
+
const decidedBy = new Map(checks.filter((c) => c.decides).map((c) => [c.name, c.decides]));
|
|
458
|
+
const flags = {};
|
|
459
|
+
for (const result of results) {
|
|
460
|
+
const column = decidedBy.get(result.check);
|
|
461
|
+
if (!column) continue;
|
|
462
|
+
if (result.outcome === "pass") flags[column] = 1;
|
|
463
|
+
else if (result.outcome === "fail") flags[column] = 0;
|
|
464
|
+
}
|
|
465
|
+
return flags;
|
|
466
|
+
}
|
|
467
|
+
function errorCode(raw) {
|
|
468
|
+
try {
|
|
469
|
+
return String(JSON.parse(raw).error?.code ?? "");
|
|
470
|
+
} catch {
|
|
471
|
+
return "";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function errorParam(raw) {
|
|
475
|
+
try {
|
|
476
|
+
return String(JSON.parse(raw).error?.param ?? "");
|
|
477
|
+
} catch {
|
|
478
|
+
return "";
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
var REQUEST_SHAPE_PARAMS = /* @__PURE__ */ new Set([
|
|
482
|
+
"model",
|
|
483
|
+
"temperature",
|
|
484
|
+
"top_p",
|
|
485
|
+
"max_tokens",
|
|
486
|
+
"max_completion_tokens",
|
|
487
|
+
"n",
|
|
488
|
+
"user"
|
|
489
|
+
]);
|
|
490
|
+
var OUR_GATES = /* @__PURE__ */ new Set([
|
|
491
|
+
"unsupported_capability",
|
|
492
|
+
"model_unmeasured",
|
|
493
|
+
"measurement_stale",
|
|
494
|
+
"model_not_found",
|
|
495
|
+
"upstream_not_supported",
|
|
496
|
+
"limit_exceeded"
|
|
497
|
+
]);
|
|
498
|
+
function refusalIsOurs(status, raw) {
|
|
499
|
+
const code = errorCode(raw);
|
|
500
|
+
const clientRefusal = status === 400 || status === 422;
|
|
501
|
+
if (!OUR_GATES.has(code) && clientRefusal && (errorParam(raw) === "tool_choice" || /tool_choice|web_search/i.test(raw))) return false;
|
|
502
|
+
const providerSaysModelCannot = status === 403 && /doesn't support|not supported/i.test(raw);
|
|
503
|
+
return status >= 500 || status === 429 || status === 401 || status === 402 || status === 403 && !providerSaysModelCannot || status === 400 && REQUEST_SHAPE_PARAMS.has(errorParam(raw)) || code === "unsupported_parameter" || OUR_GATES.has(code);
|
|
504
|
+
}
|
|
505
|
+
var REFUSAL_HEADERS = [
|
|
506
|
+
"content-type",
|
|
507
|
+
"server-timing",
|
|
508
|
+
"cf-aig-cache-status",
|
|
509
|
+
"x-cruise-upstream-status",
|
|
510
|
+
"x-cruise-model",
|
|
511
|
+
"x-cruise-fell-back-from"
|
|
512
|
+
];
|
|
513
|
+
var DETAIL_LIMIT = 300;
|
|
514
|
+
function refusalOf(status, raw, headers) {
|
|
515
|
+
const code = errorCode(raw);
|
|
516
|
+
const named = REFUSAL_HEADERS.flatMap((name) => {
|
|
517
|
+
const value = headers.get(name);
|
|
518
|
+
return value === null ? [] : [`${name}: ${value}`];
|
|
519
|
+
});
|
|
520
|
+
const detail = raw.length > 0 ? raw : `Empty body. ${named.join("; ")}`.trim();
|
|
521
|
+
return {
|
|
522
|
+
outcome: refusalIsOurs(status, raw) ? "error" : "fail",
|
|
523
|
+
observed: `HTTP ${status}${code ? ` ${code}` : ""}`,
|
|
524
|
+
detail: detail.slice(0, DETAIL_LIMIT)
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// scripts/conformance-image.ts
|
|
529
|
+
function variantsToMeasure(measured, full, sizes = INTRODUCTION_SIZES, qualities = INTRODUCTION_QUALITIES) {
|
|
530
|
+
if (full || measured.length === 0) {
|
|
531
|
+
return sizes.flatMap((size) => qualities.map((quality) => ({ size, quality })));
|
|
532
|
+
}
|
|
533
|
+
const cheapest = measured.reduce((low, row) => row.output_tokens < low.output_tokens ? row : low);
|
|
534
|
+
return [{ size: cheapest.size, quality: cheapest.quality }];
|
|
535
|
+
}
|
|
536
|
+
async function generate(base, key, model, variant) {
|
|
537
|
+
try {
|
|
538
|
+
const response = await fetch(`${base}/v1/images/generations`, {
|
|
539
|
+
method: "POST",
|
|
540
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
541
|
+
body: JSON.stringify({ model, prompt: IMAGE_PROMPT, n: 1, ...variant })
|
|
542
|
+
});
|
|
543
|
+
const raw = await response.text();
|
|
544
|
+
if (!response.ok) {
|
|
545
|
+
return { body: null, failure: refusalOf(response.status, raw, response.headers) };
|
|
546
|
+
}
|
|
547
|
+
return { body: JSON.parse(raw), failure: null };
|
|
548
|
+
} catch (cause) {
|
|
549
|
+
return { body: null, failure: { outcome: "error", detail: String(cause).slice(0, 300) } };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async function measureImageModel(db, base, key, model, runId, options, log = console.log) {
|
|
553
|
+
if (options.pricedBy === "component") {
|
|
554
|
+
const attempt = await generate(base, key, model, {});
|
|
555
|
+
if (attempt.body !== null) log(` ${model} component-priced generation ok`);
|
|
556
|
+
return attempt.body === null ? COMPONENT_IMAGE_CHECKS.map((check) => ({ check: check.name, ...attempt.failure })) : COMPONENT_IMAGE_CHECKS.map((check) => check.evaluate(attempt.body));
|
|
557
|
+
}
|
|
558
|
+
const { results: measured } = await db.prepare(`SELECT size, quality, output_tokens FROM model_image_variants WHERE model = ?`).bind(model).all();
|
|
559
|
+
const wanted = variantsToMeasure(measured, options.full, options.sizes, options.qualities);
|
|
560
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
561
|
+
let results = null;
|
|
562
|
+
for (const variant of wanted) {
|
|
563
|
+
const attempt = await generate(base, key, model, variant);
|
|
564
|
+
if (attempt.body !== null) {
|
|
565
|
+
const tokens = outputTokensOf(attempt.body);
|
|
566
|
+
if (tokens !== null) {
|
|
567
|
+
await db.prepare(
|
|
568
|
+
`INSERT INTO model_image_variants (model, size, quality, output_tokens, measured_at, measured_by, source)
|
|
569
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'conformance')
|
|
570
|
+
ON CONFLICT (model, size, quality) DO UPDATE
|
|
571
|
+
SET output_tokens = ?4, measured_at = ?5, measured_by = ?6, source = 'conformance'`
|
|
572
|
+
).bind(model, variant.size, variant.quality, tokens, today, `conformance ${runId}`).run();
|
|
573
|
+
log(` ${model} ${variant.size} ${variant.quality.padEnd(6)} ${tokens} output token(s)`);
|
|
574
|
+
} else {
|
|
575
|
+
log(` ${model} ${variant.size} ${variant.quality.padEnd(6)} no usable output token count \u2014 not recorded`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (results === null) {
|
|
579
|
+
results = attempt.body === null ? IMAGE_CHECKS.map((check) => ({ check: check.name, ...attempt.failure })) : IMAGE_CHECKS.map((check) => check.evaluate(attempt.body));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return results ?? [];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/conformance-speech.ts
|
|
586
|
+
var SPEECH_INPUT = "The quick brown fox jumps over the lazy dog.";
|
|
587
|
+
var SPEECH_FORMAT = "mp3";
|
|
588
|
+
var SPEECH_MIME_TYPES = {
|
|
589
|
+
mp3: ["audio/mpeg", "audio/mp3"]
|
|
590
|
+
};
|
|
591
|
+
var SPEECH_VOICES = {
|
|
592
|
+
openai: "alloy",
|
|
593
|
+
elevenlabs: "JBFqnCBsd6RMkjVDRZzb"
|
|
594
|
+
};
|
|
595
|
+
function checkSpeechGeneration(response, format = SPEECH_FORMAT) {
|
|
596
|
+
const raw = (response.contentType ?? "").toLowerCase();
|
|
597
|
+
const type = raw.split(";")[0].trim();
|
|
598
|
+
if (response.bytes === 0) {
|
|
599
|
+
return { check: "speech_generation", outcome: "fail", observed: type || "<no content-type>", detail: "Empty body returned as success." };
|
|
600
|
+
}
|
|
601
|
+
const observed = `${type || "<no content-type>"}, ${response.bytes} byte(s)`;
|
|
602
|
+
if (!type.startsWith("audio/")) {
|
|
603
|
+
return { check: "speech_generation", outcome: "fail", observed, detail: `Body is not audio; asked for ${format}.` };
|
|
604
|
+
}
|
|
605
|
+
const expected = SPEECH_MIME_TYPES[format] ?? [];
|
|
606
|
+
if (!expected.includes(type)) {
|
|
607
|
+
return {
|
|
608
|
+
check: "speech_generation",
|
|
609
|
+
outcome: "fail",
|
|
610
|
+
observed,
|
|
611
|
+
detail: `Audio in a format that was not asked for: wanted ${expected.join(" or ")} for ${format}.`
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
return { check: "speech_generation", outcome: "pass", observed };
|
|
615
|
+
}
|
|
616
|
+
var SPEECH_CHECKS = [
|
|
617
|
+
{ name: "speech_generation", decides: null, evaluate: checkSpeechGeneration }
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
// scripts/conformance-speech.ts
|
|
621
|
+
function voiceFor(model, overrides = {}) {
|
|
622
|
+
const upstream = model.slice(0, model.indexOf("/"));
|
|
623
|
+
return overrides[upstream] ?? SPEECH_VOICES[upstream] ?? null;
|
|
624
|
+
}
|
|
625
|
+
async function measureSpeechModel(base, key, model, voices = {}) {
|
|
626
|
+
const voice = voiceFor(model, voices);
|
|
627
|
+
if (voice === null) {
|
|
628
|
+
return SPEECH_CHECKS.map((check) => ({
|
|
629
|
+
check: check.name,
|
|
630
|
+
outcome: "error",
|
|
631
|
+
detail: `No voice is known for this model's host; pass --voice-<upstream> <id>.`
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
let observed;
|
|
635
|
+
try {
|
|
636
|
+
const response = await fetch(`${base}/v1/audio/speech`, {
|
|
637
|
+
method: "POST",
|
|
638
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
639
|
+
body: JSON.stringify({ model, input: SPEECH_INPUT, voice, response_format: SPEECH_FORMAT })
|
|
640
|
+
});
|
|
641
|
+
if (!response.ok) {
|
|
642
|
+
const failure = refusalOf(response.status, await response.text(), response.headers);
|
|
643
|
+
return SPEECH_CHECKS.map((check) => ({ check: check.name, ...failure }));
|
|
644
|
+
}
|
|
645
|
+
const bytes = (await response.arrayBuffer()).byteLength;
|
|
646
|
+
observed = { contentType: response.headers.get("content-type"), bytes };
|
|
647
|
+
} catch (cause) {
|
|
648
|
+
return SPEECH_CHECKS.map((check) => ({ check: check.name, outcome: "error", detail: String(cause).slice(0, 300) }));
|
|
649
|
+
}
|
|
650
|
+
return SPEECH_CHECKS.map((check) => check.evaluate(observed));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/conformance-embedding.ts
|
|
654
|
+
var EMBEDDING_INPUT = "A plain sentence, embedded to prove the route works.";
|
|
655
|
+
function checkEmbeddingVector(body, dimensions) {
|
|
656
|
+
const first = body.data?.[0];
|
|
657
|
+
if (first === void 0) {
|
|
658
|
+
return { check: "embedding_vector", outcome: "fail", detail: "No data returned." };
|
|
659
|
+
}
|
|
660
|
+
const vector = first.embedding;
|
|
661
|
+
if (!Array.isArray(vector) || vector.length === 0) {
|
|
662
|
+
return {
|
|
663
|
+
check: "embedding_vector",
|
|
664
|
+
outcome: "fail",
|
|
665
|
+
observed: typeof vector === "string" ? `a ${vector.length}-character string` : String(vector),
|
|
666
|
+
detail: "An entry was returned carrying no vector."
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (!vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
670
|
+
return { check: "embedding_vector", outcome: "fail", observed: `${vector.length} value(s)`, detail: "The vector holds something that is not a number." };
|
|
671
|
+
}
|
|
672
|
+
const observed = `${vector.length} dimension(s)`;
|
|
673
|
+
if (dimensions !== null && vector.length !== dimensions) {
|
|
674
|
+
return {
|
|
675
|
+
check: "embedding_vector",
|
|
676
|
+
outcome: "fail",
|
|
677
|
+
observed,
|
|
678
|
+
detail: `The catalogue publishes ${dimensions} dimensions for this model, which is the figure a caller has sized storage by.`
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
return { check: "embedding_vector", outcome: "pass", observed };
|
|
682
|
+
}
|
|
683
|
+
function checkEmbeddingUsage(body) {
|
|
684
|
+
const usage = body.usage;
|
|
685
|
+
const ok = typeof usage?.prompt_tokens === "number";
|
|
686
|
+
return {
|
|
687
|
+
check: "embedding_usage",
|
|
688
|
+
outcome: ok ? "pass" : "fail",
|
|
689
|
+
observed: usage ? JSON.stringify(usage) : "<absent>",
|
|
690
|
+
...ok ? {} : { detail: "No usable prompt token count; spend cannot be attributed." }
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
var EMBEDDING_CHECKS = [
|
|
694
|
+
{ name: "embedding_vector", decides: null, evaluate: checkEmbeddingVector },
|
|
695
|
+
{ name: "embedding_usage", decides: "usage_reported", evaluate: (body) => checkEmbeddingUsage(body) }
|
|
696
|
+
];
|
|
697
|
+
|
|
698
|
+
// scripts/conformance-embedding.ts
|
|
699
|
+
async function measureEmbeddingModel(base, key, model, dimensions) {
|
|
700
|
+
let body;
|
|
701
|
+
try {
|
|
702
|
+
const response = await fetch(`${base}/v1/embeddings`, {
|
|
703
|
+
method: "POST",
|
|
704
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
705
|
+
body: JSON.stringify({ model, input: EMBEDDING_INPUT })
|
|
706
|
+
});
|
|
707
|
+
const raw = await response.text();
|
|
708
|
+
if (!response.ok) {
|
|
709
|
+
const failure = refusalOf(response.status, raw, response.headers);
|
|
710
|
+
return EMBEDDING_CHECKS.map((check) => ({ check: check.name, ...failure }));
|
|
711
|
+
}
|
|
712
|
+
body = JSON.parse(raw);
|
|
713
|
+
} catch (cause) {
|
|
714
|
+
return EMBEDDING_CHECKS.map((check) => ({ check: check.name, outcome: "error", detail: String(cause).slice(0, 300) }));
|
|
715
|
+
}
|
|
716
|
+
return EMBEDDING_CHECKS.map((check) => check.evaluate(body, dimensions));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/conformance-fold.ts
|
|
720
|
+
async function foldFlags(db, model, results, checks, runId, today) {
|
|
721
|
+
const flags = flagsFromResults(results, checks);
|
|
722
|
+
const columns = Object.keys(flags);
|
|
723
|
+
const assignments = columns.map((column) => `${column} = ?`);
|
|
724
|
+
const values = columns.map((column) => flags[column]);
|
|
725
|
+
const by = `conformance ${runId}`;
|
|
726
|
+
const cleared = unservableReason(results) ?? failedEveryCheckReason(results);
|
|
727
|
+
if (cleared !== null) {
|
|
728
|
+
await db.prepare(
|
|
729
|
+
`UPDATE models
|
|
730
|
+
SET ${[...assignments, "measured_at = NULL", "measurement_note = ?"].join(", ")},
|
|
731
|
+
measured_by = ?, measurement_source = 'conformance'
|
|
732
|
+
WHERE id = ?`
|
|
733
|
+
).bind(...values, cleared, by, model).run();
|
|
734
|
+
return { outcome: "cleared", reason: cleared };
|
|
735
|
+
}
|
|
736
|
+
const decidesNothing = checks.every((check) => check.decides === null);
|
|
737
|
+
const passed = results.some((result) => result.outcome === "pass");
|
|
738
|
+
if (passed && (columns.length > 0 || decidesNothing)) {
|
|
739
|
+
await db.prepare(
|
|
740
|
+
`UPDATE models
|
|
741
|
+
SET ${[...assignments, "measured_at = ?"].join(", ")}, measured_by = ?,
|
|
742
|
+
measurement_source = 'conformance', measurement_note = NULL
|
|
743
|
+
WHERE id = ?`
|
|
744
|
+
).bind(...values, today, by, model).run();
|
|
745
|
+
return { outcome: "stamped" };
|
|
746
|
+
}
|
|
747
|
+
return { outcome: "left" };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// scripts/conformance.ts
|
|
751
|
+
function evaluate(definition, raw) {
|
|
752
|
+
if (definition.streaming) return checkStreaming(raw);
|
|
753
|
+
let body;
|
|
754
|
+
try {
|
|
755
|
+
body = JSON.parse(raw);
|
|
756
|
+
} catch {
|
|
757
|
+
return { check: definition.name, outcome: "error", detail: "Response was not valid JSON." };
|
|
758
|
+
}
|
|
759
|
+
switch (definition.name) {
|
|
760
|
+
case "chat_completion":
|
|
761
|
+
return checkChatCompletion(body);
|
|
762
|
+
case "usage_reported":
|
|
763
|
+
return checkUsageReported(body);
|
|
764
|
+
case "tool_call":
|
|
765
|
+
return checkToolCall(body);
|
|
766
|
+
case "parallel_tool_calls":
|
|
767
|
+
return checkParallelToolCalls(body);
|
|
768
|
+
case "tool_choice_forced":
|
|
769
|
+
return checkToolCall(body, "tool_choice_forced");
|
|
770
|
+
case "json_schema":
|
|
771
|
+
return checkJsonSchema(body);
|
|
772
|
+
case "vision":
|
|
773
|
+
return checkVision(body);
|
|
774
|
+
case "web_search":
|
|
775
|
+
return checkWebSearch(body);
|
|
776
|
+
case "truncation":
|
|
777
|
+
return checkTruncation(body);
|
|
778
|
+
default:
|
|
779
|
+
return { check: definition.name, outcome: "skip", detail: "No evaluator." };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function runCheck(base, key, model, definition) {
|
|
783
|
+
try {
|
|
784
|
+
const path = typeof definition.path === "function" ? definition.path(model) : definition.path ?? "/v1/chat/completions";
|
|
785
|
+
const response = await fetch(`${base}${path}`, {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
788
|
+
body: JSON.stringify({ ...definition.request, model })
|
|
789
|
+
});
|
|
790
|
+
const raw = await response.text();
|
|
791
|
+
if (!response.ok) {
|
|
792
|
+
return { check: definition.name, ...refusalOf(response.status, raw, response.headers) };
|
|
793
|
+
}
|
|
794
|
+
return evaluate(definition, raw);
|
|
795
|
+
} catch (cause) {
|
|
796
|
+
return { check: definition.name, outcome: "error", detail: String(cause).slice(0, 300) };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function parseShard(raw) {
|
|
800
|
+
if (raw === void 0) return { index: 1, total: 1 };
|
|
801
|
+
const match = /^(\d+)\/(\d+)$/.exec(String(raw));
|
|
802
|
+
if (!match) throw new Error(`--shard must look like 2/3, got '${String(raw)}'`);
|
|
803
|
+
return { index: Number(match[1]), total: Number(match[2]) };
|
|
804
|
+
}
|
|
805
|
+
run(async () => {
|
|
806
|
+
const args = parseArgs(process.argv.slice(2));
|
|
807
|
+
const environment = environmentOf(args);
|
|
808
|
+
const base = requireString(args, "base").replace(/\/$/, "");
|
|
809
|
+
const key = requireString(args, "key");
|
|
810
|
+
const db = connect(environment);
|
|
811
|
+
const prefix = parseKeyPrefix(key);
|
|
812
|
+
const holder = prefix ? await db.prepare(`SELECT conformance, revoked_at FROM virtual_keys WHERE prefix = ?`).bind(prefix).first() : null;
|
|
813
|
+
if (prefix === null || holder === null) {
|
|
814
|
+
throw new Error(`--key is not a virtual key this environment knows (${prefix ?? "unparseable"}).`);
|
|
815
|
+
}
|
|
816
|
+
if (holder.revoked_at !== null) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
`--key ${prefix} was revoked ${holder.revoked_at}. Every request would answer 401, and a run of uniform 401s is read as the providers refusing us \u2014 it would clear measurements rather than take them.`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
if (holder.conformance !== 1) {
|
|
822
|
+
throw new Error(
|
|
823
|
+
`--key ${prefix} is not a conformance key. Only a conformance key is exempt from the unmeasured gate, so an unmeasured model would answer 'model_unmeasured' and never be reached. Issue one with: npm run key:issue -- --env <env> --project <p> --actor <a> --conformance`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
const shard = parseShard(args.shard);
|
|
827
|
+
const { results: models } = await db.prepare(`SELECT m.id, m.measured_at, m.modality, m.priced_by, m.dimensions FROM models m JOIN upstreams u ON u.id = m.upstream_id
|
|
828
|
+
WHERE m.enabled = 1 AND u.enabled = 1
|
|
829
|
+
ORDER BY m.id`).all();
|
|
830
|
+
const selected = selectModels(models, { ids: listFlag(args.models, "models"), modalities: listFlag(args.modality, "modality") });
|
|
831
|
+
if (selected.length < models.length) {
|
|
832
|
+
console.log(`selected ${selected.length} of ${models.length} model(s)`);
|
|
833
|
+
}
|
|
834
|
+
const mine = planShard(selected, shard.index, shard.total);
|
|
835
|
+
if (mine.length === 0) {
|
|
836
|
+
console.log(shard.total === 1 ? "no routed models" : `no routed models in shard ${shard.index}/${shard.total}`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (shard.total > 1) {
|
|
840
|
+
console.log(`shard ${shard.index}/${shard.total}: ${mine.length} of ${selected.length} models`);
|
|
841
|
+
}
|
|
842
|
+
const runId = crypto.randomUUID();
|
|
843
|
+
await db.prepare(`INSERT INTO conformance_runs (id, environment, note) VALUES (?, ?, ?)`).bind(runId, environment, typeof args.note === "string" ? args.note : null).run();
|
|
844
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
845
|
+
let failures = 0;
|
|
846
|
+
const fold = async (model, results, checks) => {
|
|
847
|
+
const verdict = await foldFlags(db, model, results, checks, runId, today);
|
|
848
|
+
if (verdict.outcome === "cleared") console.log(` ${model}: measurement cleared \u2014 ${verdict.reason}`);
|
|
849
|
+
};
|
|
850
|
+
const record = async (model, result, label) => {
|
|
851
|
+
await db.prepare(
|
|
852
|
+
`INSERT INTO conformance_results (run_id, model, check_name, outcome, observed, detail)
|
|
853
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
854
|
+
).bind(runId, model, result.check, result.outcome, result.observed ?? null, result.detail ?? null).run();
|
|
855
|
+
const mark = { pass: "ok", fail: "FAIL", skip: "skip", error: "ERROR" }[result.outcome];
|
|
856
|
+
console.log(`${model} ${label.padEnd(20)} ${mark}${result.observed ? " " + result.observed : ""}`);
|
|
857
|
+
if (result.outcome === "fail" || result.outcome === "error") failures++;
|
|
858
|
+
};
|
|
859
|
+
for (const { id: model, modality, priced_by: pricedBy, dimensions } of mine) {
|
|
860
|
+
if (modality === "image") {
|
|
861
|
+
const checks = pricedBy === "component" ? COMPONENT_IMAGE_CHECKS : IMAGE_CHECKS;
|
|
862
|
+
const results2 = await measureImageModel(db, base, key, model, runId, {
|
|
863
|
+
full: args["image-variants"] === true || args["image-variants"] === "true",
|
|
864
|
+
sizes: listFlag(args.sizes, "sizes"),
|
|
865
|
+
qualities: listFlag(args.qualities, "qualities"),
|
|
866
|
+
pricedBy
|
|
867
|
+
});
|
|
868
|
+
for (const result of results2) await record(model, result, result.check);
|
|
869
|
+
await fold(model, results2, checks);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (modality === "embedding") {
|
|
873
|
+
const results2 = await measureEmbeddingModel(base, key, model, dimensions);
|
|
874
|
+
for (const result of results2) await record(model, result, result.check);
|
|
875
|
+
await fold(model, results2, EMBEDDING_CHECKS);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (modality === "speech") {
|
|
879
|
+
const voices = typeof args["voice-elevenlabs"] === "string" ? { elevenlabs: args["voice-elevenlabs"] } : {};
|
|
880
|
+
const results2 = await measureSpeechModel(base, key, model, voices);
|
|
881
|
+
for (const result of results2) await record(model, result, result.check);
|
|
882
|
+
await fold(model, results2, SPEECH_CHECKS);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const results = [];
|
|
886
|
+
const outcomeOf = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const definition of CHECKS) {
|
|
888
|
+
let result;
|
|
889
|
+
if (definition.dependsOn && outcomeOf.get(definition.dependsOn) !== "pass") {
|
|
890
|
+
result = {
|
|
891
|
+
check: definition.name,
|
|
892
|
+
outcome: "skip",
|
|
893
|
+
detail: `Skipped: '${definition.dependsOn}' did not pass, so this cannot be measured.`
|
|
894
|
+
};
|
|
895
|
+
} else {
|
|
896
|
+
const attempts = Math.max(1, definition.attempts ?? 1);
|
|
897
|
+
result = await runCheck(base, key, model, definition);
|
|
898
|
+
for (let attempt = 2; attempt <= attempts && result.outcome === "fail"; attempt++) {
|
|
899
|
+
const retry = await runCheck(base, key, model, definition);
|
|
900
|
+
if (retry.outcome === "pass") {
|
|
901
|
+
result = { ...retry, detail: `Passed on attempt ${attempt} of ${attempts}.` };
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
result = retry;
|
|
905
|
+
}
|
|
906
|
+
if (result.outcome === "fail" && attempts > 1) {
|
|
907
|
+
result = {
|
|
908
|
+
...result,
|
|
909
|
+
detail: `${result.detail ?? ""} Failed all ${attempts} attempts.`.trim()
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
outcomeOf.set(definition.name, result.outcome);
|
|
914
|
+
results.push(result);
|
|
915
|
+
await record(model, result, definition.name);
|
|
916
|
+
}
|
|
917
|
+
await fold(model, results, CHECKS);
|
|
918
|
+
}
|
|
919
|
+
await db.prepare(`UPDATE conformance_runs SET finished_at = datetime('now') WHERE id = ?`).bind(runId).run();
|
|
920
|
+
console.log(`
|
|
921
|
+
run ${runId}: ${mine.length} model(s), ${failures} failing check(s)`);
|
|
922
|
+
});
|