create-quorum-router 0.1.5 → 0.1.8
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 +22 -2
- package/bin/create-quorum-router.js +25 -4
- package/package.json +1 -1
- package/templates/basic/README.md +72 -6
- package/templates/basic/deno.json +2 -0
- package/templates/basic/router.config.example.json +11 -0
- package/templates/basic/src/auth_session.ts +1 -0
- package/templates/basic/src/best_route.ts +10 -1
- package/templates/basic/src/calibration.ts +249 -0
- package/templates/basic/src/calibration_demo.ts +25 -0
- package/templates/basic/src/cli.ts +14 -2
- package/templates/basic/src/cost_aware.ts +150 -0
- package/templates/basic/src/provider_registry.ts +14 -7
- package/templates/basic/src/schema.ts +87 -0
- package/templates/basic/src/supabase.ts +462 -0
- package/templates/basic/src/trace.ts +3 -0
- package/templates/basic/src/wrapper_client.ts +46 -13
- package/templates/basic/supabase/migrations/20260701130000_workflow_access_audit.sql +125 -0
- package/templates/basic/supabase/migrations/20260712211500_workflow_access_audit_limits.sql +94 -0
package/README.md
CHANGED
|
@@ -12,10 +12,12 @@ npx --yes create-quorum-router@latest my-quorum-router-demo
|
|
|
12
12
|
cd my-quorum-router-demo
|
|
13
13
|
deno --version
|
|
14
14
|
deno task smoke
|
|
15
|
+
deno task calibration:demo
|
|
15
16
|
deno task intake
|
|
17
|
+
deno task supabase:status
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
Current package version: `create-quorum-router@0.1.
|
|
20
|
+
Current package version: `create-quorum-router@0.1.8`. Releases are published
|
|
19
21
|
from an immutable Git tag through GitHub Actions OIDC Trusted Publishing.
|
|
20
22
|
|
|
21
23
|
## What the generated project supports
|
|
@@ -23,6 +25,10 @@ from an immutable Git tag through GitHub Actions OIDC Trusted Publishing.
|
|
|
23
25
|
`deno task smoke` is deterministic fixture-only and does not call a provider
|
|
24
26
|
API.
|
|
25
27
|
|
|
28
|
+
`deno task calibration:demo` runs the bundled calibration-by-task API against
|
|
29
|
+
deterministic local observations. The report is advisory-only and is not
|
|
30
|
+
connected to routing weights, provider eligibility, or execution.
|
|
31
|
+
|
|
26
32
|
`deno task intake` is the first real setup command. It detects local provider
|
|
27
33
|
wrappers, checks OAuth/session status, runs safe list-only model inventory where
|
|
28
34
|
possible, writes redacted local health artifacts under `out/`, and recommends
|
|
@@ -31,6 +37,7 @@ the next command.
|
|
|
31
37
|
```bash
|
|
32
38
|
deno task check
|
|
33
39
|
deno task smoke
|
|
40
|
+
deno task calibration:demo
|
|
34
41
|
deno task intake
|
|
35
42
|
deno task auth:status
|
|
36
43
|
deno task auth:login
|
|
@@ -63,6 +70,19 @@ preferred public dogfood path.
|
|
|
63
70
|
Do not commit `.env`, `router.config.local.json`, `.quorum-router/`, or `out/`.
|
|
64
71
|
Do not paste tokens into chat/logs.
|
|
65
72
|
|
|
73
|
+
## Optional user-owned Supabase audit
|
|
74
|
+
|
|
75
|
+
The generated project works without Supabase. Audit defaults to `disabled`.
|
|
76
|
+
Users who opt in apply the generated migration to a Supabase project they own,
|
|
77
|
+
choose `optional` or `required` in the non-secret feature config, inject a
|
|
78
|
+
project URL, publishable/anon key, and Supabase Auth session JWT at runtime, and
|
|
79
|
+
run `deno task supabase:status` before routing.
|
|
80
|
+
|
|
81
|
+
The runtime rejects service-role/admin credentials. Audit records exclude
|
|
82
|
+
prompts, model responses, credentials, client-supplied actor/org identity, and
|
|
83
|
+
client timestamps. Agent Bus, Realtime, state sync, and analytics dashboards are
|
|
84
|
+
not part of this integration.
|
|
85
|
+
|
|
66
86
|
## Runtime boundaries
|
|
67
87
|
|
|
68
88
|
- Best Route/direct is production-ready best-answer routing.
|
|
@@ -71,7 +91,7 @@ Do not paste tokens into chat/logs.
|
|
|
71
91
|
slice only when separately configured with signed policy and distinct
|
|
72
92
|
approval.
|
|
73
93
|
- The generated scaffold does not enable mutation by default.
|
|
74
|
-
- No service-role runtime.
|
|
94
|
+
- No service-role/admin runtime credentials.
|
|
75
95
|
- No live Supabase Agent Bus runtime writes.
|
|
76
96
|
- Public launch requires the repository verification, package tarball, registry
|
|
77
97
|
readback, and clean-room NPX scaffold checks to pass.
|
|
@@ -3,7 +3,7 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
5
|
|
|
6
|
-
const VERSION = "0.1.
|
|
6
|
+
const VERSION = "0.1.8";
|
|
7
7
|
const SUPPORTED_TEMPLATES = new Set(["basic"]);
|
|
8
8
|
|
|
9
9
|
function usage() {
|
|
@@ -56,13 +56,33 @@ function parseArgs(argv) {
|
|
|
56
56
|
return options;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function
|
|
59
|
+
function rejectSymlink(destination) {
|
|
60
|
+
try {
|
|
61
|
+
if (fs.lstatSync(destination).isSymbolicLink()) {
|
|
62
|
+
throw new Error(`refusing to write through symlink: ${destination}`);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error && error.code === "ENOENT") return;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function copyRecursive(from, to, targetRoot) {
|
|
71
|
+
const relative = path.relative(targetRoot, to);
|
|
72
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
73
|
+
throw new Error(`refusing to write outside target directory: ${to}`);
|
|
74
|
+
}
|
|
75
|
+
rejectSymlink(to);
|
|
60
76
|
const stat = fs.statSync(from);
|
|
61
77
|
if (stat.isDirectory()) {
|
|
62
78
|
fs.mkdirSync(to, { recursive: true });
|
|
63
79
|
for (const entry of fs.readdirSync(from)) {
|
|
64
80
|
const targetEntry = entry === "gitignore" ? ".gitignore" : entry;
|
|
65
|
-
copyRecursive(
|
|
81
|
+
copyRecursive(
|
|
82
|
+
path.join(from, entry),
|
|
83
|
+
path.join(to, targetEntry),
|
|
84
|
+
targetRoot,
|
|
85
|
+
);
|
|
66
86
|
}
|
|
67
87
|
return;
|
|
68
88
|
}
|
|
@@ -97,6 +117,7 @@ function main() {
|
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
const targetDir = path.resolve(process.cwd(), parsed.dir);
|
|
120
|
+
rejectSymlink(targetDir);
|
|
100
121
|
if (isNonEmptyDirectory(targetDir) && !parsed.force) {
|
|
101
122
|
throw new Error(
|
|
102
123
|
`refusing to overwrite non-empty directory: ${targetDir}\n` +
|
|
@@ -106,7 +127,7 @@ function main() {
|
|
|
106
127
|
|
|
107
128
|
const templateDir = path.join(__dirname, "..", "templates", parsed.template);
|
|
108
129
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
109
|
-
copyRecursive(templateDir, targetDir);
|
|
130
|
+
copyRecursive(templateDir, targetDir, targetDir);
|
|
110
131
|
fs.mkdirSync(path.join(targetDir, "out"), { recursive: true });
|
|
111
132
|
fs.writeFileSync(path.join(targetDir, "out", ".gitkeep"), "");
|
|
112
133
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# QuorumRouter generated workspace
|
|
2
2
|
|
|
3
3
|
This generated workspace contains the MIT-licensed QuorumRouter current release.
|
|
4
|
-
npm latest targets v0.1.
|
|
4
|
+
npm latest targets v0.1.8.
|
|
5
5
|
|
|
6
6
|
QuorumRouter is **MIT**. It is **open source**. Commercial and production use
|
|
7
7
|
are permitted under the MIT License.
|
|
@@ -19,8 +19,10 @@ are permitted under the MIT License.
|
|
|
19
19
|
- Conversation-only `agent_chat` is explicit opt-in.
|
|
20
20
|
- SafeLoop-backed production repository execution is not enabled by this
|
|
21
21
|
generated scaffold; it requires external signed policy and distinct approval.
|
|
22
|
-
- No service-role runtime
|
|
23
|
-
|
|
22
|
+
- No service-role runtime; service-role/admin credentials are rejected even when
|
|
23
|
+
audit is disabled.
|
|
24
|
+
- BYO Supabase audit is disabled by default and writes only when explicitly set
|
|
25
|
+
to `optional` or `required`.
|
|
24
26
|
- No live Supabase Agent Bus runtime writes.
|
|
25
27
|
- Best Route/direct is the production-ready best-answer routing path.
|
|
26
28
|
- `agent-chat` is read-only explicit opt-in only.
|
|
@@ -35,15 +37,22 @@ deno --version
|
|
|
35
37
|
|
|
36
38
|
```bash
|
|
37
39
|
deno task smoke
|
|
40
|
+
deno task calibration:demo
|
|
38
41
|
deno task intake
|
|
39
42
|
deno task auth:status
|
|
40
43
|
deno task models:list
|
|
41
44
|
deno task health
|
|
45
|
+
deno task supabase:status
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
`smoke` proves the local scaffold runs with deterministic fixtures only. It does
|
|
45
49
|
**not** call a real provider API.
|
|
46
50
|
|
|
51
|
+
`calibration:demo` exercises the bundled calibration-by-task API with local
|
|
52
|
+
fixture observations. Calibration reports are advisory-only: the scaffold does
|
|
53
|
+
not use them to change routing weights, ranks, provider eligibility, quorum, or
|
|
54
|
+
execution.
|
|
55
|
+
|
|
47
56
|
`intake` detects local provider wrappers, checks OAuth/session status, runs safe
|
|
48
57
|
model inventory/list-only probes where possible, writes local health traces
|
|
49
58
|
under `out/`, and recommends the next command.
|
|
@@ -51,6 +60,37 @@ under `out/`, and recommends the next command.
|
|
|
51
60
|
In short: intake is the first real setup command before `route:once`,
|
|
52
61
|
`best-route`, or read-only `agent-chat`.
|
|
53
62
|
|
|
63
|
+
## Optional BYO Supabase audit
|
|
64
|
+
|
|
65
|
+
Supabase is not required. With no Supabase config or env, every existing task
|
|
66
|
+
keeps its prior behavior and `deno task supabase:status` exits successfully with
|
|
67
|
+
`state: disabled` without making a network request.
|
|
68
|
+
|
|
69
|
+
To persist route outcomes in a Supabase project you own:
|
|
70
|
+
|
|
71
|
+
1. Create or choose your Supabase project.
|
|
72
|
+
2. Apply **both** files under `supabase/migrations/` in filename order using an
|
|
73
|
+
admin context outside the router runtime. The later limits migration is
|
|
74
|
+
mandatory.
|
|
75
|
+
3. Ensure the active Supabase Auth user session JWT has an authenticated `sub`.
|
|
76
|
+
4. Copy `router.config.example.json` to `router.config.json` and set only
|
|
77
|
+
`features.supabase.audit.mode` to `optional` or `required`.
|
|
78
|
+
5. Inject `QUORUM_ROUTER_SUPABASE_URL`, `QUORUM_ROUTER_SUPABASE_ANON_KEY`, and
|
|
79
|
+
`QUORUM_ROUTER_SUPABASE_SESSION_JWT` at runtime.
|
|
80
|
+
6. Run `deno task supabase:status`, then run `route:once` or `best-route`.
|
|
81
|
+
|
|
82
|
+
`optional` preserves a successful route and prints a warning when audit delivery
|
|
83
|
+
fails. `required` withholds the route result and exits nonzero when audit
|
|
84
|
+
delivery fails. The RPC receives only the route decision and bounded metadata.
|
|
85
|
+
It never receives prompts, model responses, credentials, `org_id`, `actor_id`,
|
|
86
|
+
or `created_at`; the database derives both the actor and this single-user BYO
|
|
87
|
+
audit namespace from `auth.uid()` and owns the timestamp. There is no central or
|
|
88
|
+
shared database and no client tenant claim.
|
|
89
|
+
|
|
90
|
+
Runtime service-role/admin credentials are forbidden. Agent Bus, Realtime
|
|
91
|
+
wakeup, state sync, and an analytics dashboard are future features and are not
|
|
92
|
+
enabled by this audit integration.
|
|
93
|
+
|
|
54
94
|
## Real provider dogfood commands
|
|
55
95
|
|
|
56
96
|
Run only after `intake` reports a usable OAuth/session/wrapper provider:
|
|
@@ -87,6 +127,25 @@ Behavior:
|
|
|
87
127
|
Only use this with repositories you are allowed to send to the selected
|
|
88
128
|
provider.
|
|
89
129
|
|
|
130
|
+
### Cost-aware Best Route
|
|
131
|
+
|
|
132
|
+
Cost-aware routing is disabled unless both budget variables are set. Estimates
|
|
133
|
+
are user-supplied per invocation, not live provider billing data.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
QUORUM_ROUTER_MAX_BUDGET_USD=0.05 \
|
|
137
|
+
QUORUM_ROUTER_ESTIMATED_COSTS_JSON='{"openai/gpt-5":0.03,"xai/grok":0.02}' \
|
|
138
|
+
RUN_EXTERNAL_MODEL_DOGFOOD=1 \
|
|
139
|
+
deno task best-route --prompt "Choose the safest launch copy."
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Best Route preserves candidate quality/readiness order, admits candidates while
|
|
143
|
+
their configured estimates fit the budget, and records selected and excluded
|
|
144
|
+
model IDs plus reasons in `out/best-route-trace.json`. Models without an
|
|
145
|
+
estimate are excluded when cost-aware routing is enabled. If no candidate fits,
|
|
146
|
+
the command fails before invoking a provider. This is pre-invocation budget
|
|
147
|
+
control; it does not claim exact or live API spend.
|
|
148
|
+
|
|
90
149
|
### Forced wrapper provider/model selection
|
|
91
150
|
|
|
92
151
|
Use these only when you want a specific local wrapper/model. The scaffold fails
|
|
@@ -152,17 +211,24 @@ not paste them into chat/logs and do not commit `.env`.
|
|
|
152
211
|
- `.gitignore` — excludes `.env`, `out/`, `router.config.local.json`,
|
|
153
212
|
`provider_config.json`, and `.quorum-router/`.
|
|
154
213
|
- `router.config.example.json` — non-secret example boundaries.
|
|
214
|
+
- `supabase/migrations/20260701130000_workflow_access_audit.sql` — optional BYO
|
|
215
|
+
Supabase audit migration
|
|
216
|
+
- `supabase/migrations/20260712211500_workflow_access_audit_limits.sql` —
|
|
217
|
+
required RPC narrowing and payload-limit migration
|
|
155
218
|
- `src/cli.ts` — command dispatcher.
|
|
219
|
+
- `src/supabase.ts` — offline status and selective audit RPC hook.
|
|
156
220
|
- `src/intake.ts` — first-run onboarding.
|
|
157
221
|
- `src/auth.ts`, `src/auth_oauth.ts`, `src/auth_session.ts`,
|
|
158
222
|
`src/auth_env_fallback.ts` — auth/session/fallback boundaries.
|
|
159
223
|
- `src/provider_registry.ts`, `src/model_inventory.ts`, `src/wrapper_client.ts`,
|
|
160
224
|
`src/provider_client.ts` — provider discovery and safe invocation.
|
|
161
225
|
- `src/best_route.ts`, `src/agent_chat.ts` — gated dogfood commands.
|
|
226
|
+
- `src/cost_aware.ts` — estimated-cost budget selection for Best Route.
|
|
227
|
+
- `src/calibration.ts`, `src/calibration_demo.ts` — strict advisory calibration
|
|
228
|
+
aggregation and an offline runnable example.
|
|
162
229
|
- `src/trace.ts`, `src/redact.ts`, `src/schema.ts`, `src/fixture_smoke.ts` —
|
|
163
230
|
trace/redaction/schema/fixture support.
|
|
164
231
|
- `out/.gitkeep` — local output directory placeholder.
|
|
165
232
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
does not publish npm, create a GitHub release, or mutate tags/dist-tags.
|
|
233
|
+
This generated scaffold operates locally. It does not publish npm packages,
|
|
234
|
+
create GitHub releases, or mutate tags or dist-tags.
|
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
"tasks": {
|
|
7
7
|
"check": "deno check main.ts src/*.ts",
|
|
8
8
|
"smoke": "deno run main.ts",
|
|
9
|
+
"calibration:demo": "deno run src/calibration_demo.ts",
|
|
9
10
|
"intake": "deno run --allow-read --allow-write --allow-env --allow-net --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts intake",
|
|
10
11
|
"auth:status": "deno run --allow-read --allow-env --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts auth:status",
|
|
11
12
|
"auth:login": "deno run src/cli.ts auth:login",
|
|
12
13
|
"auth:logout": "deno run --allow-read --allow-write --allow-env src/cli.ts auth:logout",
|
|
13
14
|
"models:list": "deno run --allow-read --allow-write --allow-env --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts models:list",
|
|
14
15
|
"health": "deno run --allow-read --allow-write --allow-env --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts health",
|
|
16
|
+
"supabase:status": "deno run --allow-read --allow-env src/cli.ts supabase:status",
|
|
15
17
|
"route:once": "deno run --allow-read --allow-write --allow-env --allow-net --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts route:once",
|
|
16
18
|
"best-route": "deno run --allow-read --allow-write --allow-env --allow-net --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts best-route",
|
|
17
19
|
"agent-chat": "deno run --allow-read --allow-write --allow-env --allow-net --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts agent-chat"
|
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
"enabled_by": "QUORUM_ROUTER_AUTH_MODE=env",
|
|
6
6
|
"usage": "private manual fallback only"
|
|
7
7
|
},
|
|
8
|
+
"features": {
|
|
9
|
+
"supabase": {
|
|
10
|
+
"audit": {
|
|
11
|
+
"mode": "disabled"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"cost_aware": {
|
|
16
|
+
"enabled_by": "QUORUM_ROUTER_MAX_BUDGET_USD + QUORUM_ROUTER_ESTIMATED_COSTS_JSON",
|
|
17
|
+
"pricing_source": "user-supplied estimates, not live billing"
|
|
18
|
+
},
|
|
8
19
|
"runtime_boundaries": {
|
|
9
20
|
"best_route_direct": "production-ready best-answer routing",
|
|
10
21
|
"agent_chat": "experimental explicit opt-in only",
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { buildTrace, score, writeTrace } from "./trace.ts";
|
|
22
22
|
import { callWrapper } from "./wrapper_client.ts";
|
|
23
23
|
import { preparePromptWithContext } from "./context.ts";
|
|
24
|
+
import { selectCandidatesWithinBudget } from "./cost_aware.ts";
|
|
24
25
|
|
|
25
26
|
function hasExplicitSelection(request: ProviderSelectionRequest): boolean {
|
|
26
27
|
return Boolean(
|
|
@@ -261,7 +262,12 @@ export async function runBestRoute(
|
|
|
261
262
|
> {
|
|
262
263
|
assertOptIn();
|
|
263
264
|
const authMode = parseAuthMode(readRouterEnv("QUORUM_ROUTER_AUTH_MODE"));
|
|
264
|
-
const { request, candidates } =
|
|
265
|
+
const { request, candidates: discoveredCandidates } =
|
|
266
|
+
await discoverCandidates(
|
|
267
|
+
authMode,
|
|
268
|
+
);
|
|
269
|
+
const costAwareDecision = selectCandidatesWithinBudget(discoveredCandidates);
|
|
270
|
+
const candidates = costAwareDecision.candidates;
|
|
265
271
|
if (candidates.length === 0) {
|
|
266
272
|
throw new Error(
|
|
267
273
|
"OAuth/session-first provider unavailable. best-route has no usable OAuth/session/wrapper provider yet. Next: deno task auth:login",
|
|
@@ -316,6 +322,9 @@ export async function runBestRoute(
|
|
|
316
322
|
selectionHonored(request, result)
|
|
317
323
|
),
|
|
318
324
|
fallbackUsed: usedEnvFallback,
|
|
325
|
+
costAware: costAwareDecision.cost.enabled
|
|
326
|
+
? costAwareDecision.cost
|
|
327
|
+
: undefined,
|
|
319
328
|
});
|
|
320
329
|
const tracePath = await writeTrace("best-route-trace", trace);
|
|
321
330
|
return { results, tracePath, trace };
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const MAX_CALIBRATION_OBSERVATIONS = 10_000;
|
|
4
|
+
export const MAX_CALIBRATION_IDENTIFIER_LENGTH = 256;
|
|
5
|
+
|
|
6
|
+
function stableMetric(value: number): number {
|
|
7
|
+
const rounded = Number(value.toPrecision(15));
|
|
8
|
+
return rounded === 0 ? 0 : rounded;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function boundedCanonicalText(maxLength: number) {
|
|
12
|
+
return z.string().trim().transform((value) => value.normalize("NFC")).pipe(
|
|
13
|
+
z.string().min(1).max(maxLength).refine(
|
|
14
|
+
(value) => !/[\p{Cc}\p{Cf}\p{Default_Ignorable_Code_Point}]/u.test(value),
|
|
15
|
+
"control and default-ignorable characters are not allowed",
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hasValidRfc3339Offset(value: string): boolean {
|
|
21
|
+
if (value.endsWith("Z")) return true;
|
|
22
|
+
const match = /[+-](\d{2}):(\d{2})$/.exec(value);
|
|
23
|
+
if (!match) return false;
|
|
24
|
+
return Number(match[1]) <= 23 && Number(match[2]) <= 59;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ObservationIdSchema = boundedCanonicalText(
|
|
28
|
+
MAX_CALIBRATION_IDENTIFIER_LENGTH,
|
|
29
|
+
);
|
|
30
|
+
const TaskTypeSchema = boundedCanonicalText(MAX_CALIBRATION_IDENTIFIER_LENGTH);
|
|
31
|
+
const SourceLabelSchema = boundedCanonicalText(
|
|
32
|
+
MAX_CALIBRATION_IDENTIFIER_LENGTH,
|
|
33
|
+
);
|
|
34
|
+
const EvaluationTimestampSchema = z.string().datetime({ offset: true }).refine(
|
|
35
|
+
hasValidRfc3339Offset,
|
|
36
|
+
"evaluated_at must use a valid RFC 3339 UTC offset",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const TaskCalibrationSourceSchema = z.object({
|
|
40
|
+
provider: SourceLabelSchema,
|
|
41
|
+
model: SourceLabelSchema,
|
|
42
|
+
}).strict();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Structurally validated calibration input supplied by the caller.
|
|
46
|
+
*
|
|
47
|
+
* `evaluation_basis` records an unverified provenance assertion; this schema
|
|
48
|
+
* cannot establish label independence, label quality, example matching, or
|
|
49
|
+
* freedom from leakage. `confidence` must be the model's probability, recorded
|
|
50
|
+
* before the correctness label is observed, that this specific answer is
|
|
51
|
+
* correct. The schema can enforce only the numeric range, not that provenance.
|
|
52
|
+
*/
|
|
53
|
+
export const TaskCalibrationObservationSchema = z.object({
|
|
54
|
+
observation_id: ObservationIdSchema,
|
|
55
|
+
task_type: TaskTypeSchema,
|
|
56
|
+
source: TaskCalibrationSourceSchema,
|
|
57
|
+
evaluation_basis: z.literal("caller_attested_external_ground_truth"),
|
|
58
|
+
correct: z.boolean(),
|
|
59
|
+
confidence: z.number().finite().min(0).max(1),
|
|
60
|
+
evaluated_at: EvaluationTimestampSchema,
|
|
61
|
+
}).strict();
|
|
62
|
+
|
|
63
|
+
export const TaskCalibrationObservationListSchema = z.array(
|
|
64
|
+
TaskCalibrationObservationSchema,
|
|
65
|
+
).max(MAX_CALIBRATION_OBSERVATIONS).superRefine((observations, context) => {
|
|
66
|
+
const ids = new Set<string>();
|
|
67
|
+
for (const [index, observation] of observations.entries()) {
|
|
68
|
+
if (ids.has(observation.observation_id)) {
|
|
69
|
+
context.addIssue({
|
|
70
|
+
code: "custom",
|
|
71
|
+
message: "observation_id must be unique",
|
|
72
|
+
path: [index, "observation_id"],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
ids.add(observation.observation_id);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const TaskCalibrationOptionsSchema = z.object({
|
|
80
|
+
minimum_sample_count: z.number().finite().int().positive().max(
|
|
81
|
+
MAX_CALIBRATION_OBSERVATIONS,
|
|
82
|
+
).default(20),
|
|
83
|
+
}).strict();
|
|
84
|
+
|
|
85
|
+
export const TaskCalibrationGroupSchema = z.object({
|
|
86
|
+
task_type: TaskTypeSchema,
|
|
87
|
+
source: TaskCalibrationSourceSchema,
|
|
88
|
+
sample_count: z.number().int().positive().max(MAX_CALIBRATION_OBSERVATIONS),
|
|
89
|
+
accuracy: z.number().finite().min(0).max(1),
|
|
90
|
+
mean_confidence: z.number().finite().min(0).max(1),
|
|
91
|
+
brier_score: z.number().finite().min(0).max(1),
|
|
92
|
+
mean_calibration_bias: z.number().finite().min(-1).max(1),
|
|
93
|
+
// "sufficient" means only that the configured sample-count threshold is met.
|
|
94
|
+
sample_status: z.enum(["insufficient", "sufficient"]),
|
|
95
|
+
}).strict();
|
|
96
|
+
|
|
97
|
+
export const TaskCalibrationReportSchema = z.object({
|
|
98
|
+
schema_version: z.literal("quorum-router.calibration-by-task.v1"),
|
|
99
|
+
advisory_only: z.literal(true),
|
|
100
|
+
minimum_sample_count: z.number().int().positive().max(
|
|
101
|
+
MAX_CALIBRATION_OBSERVATIONS,
|
|
102
|
+
),
|
|
103
|
+
groups: z.array(TaskCalibrationGroupSchema).max(MAX_CALIBRATION_OBSERVATIONS),
|
|
104
|
+
}).strict().superRefine((report, context) => {
|
|
105
|
+
const groupKeys = new Set<string>();
|
|
106
|
+
for (const [index, group] of report.groups.entries()) {
|
|
107
|
+
const groupKey = JSON.stringify([
|
|
108
|
+
group.task_type,
|
|
109
|
+
group.source.provider,
|
|
110
|
+
group.source.model,
|
|
111
|
+
]);
|
|
112
|
+
if (groupKeys.has(groupKey)) {
|
|
113
|
+
context.addIssue({
|
|
114
|
+
code: "custom",
|
|
115
|
+
message: "calibration report groups must be unique",
|
|
116
|
+
path: ["groups", index],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
groupKeys.add(groupKey);
|
|
120
|
+
|
|
121
|
+
const expectedStatus = group.sample_count < report.minimum_sample_count
|
|
122
|
+
? "insufficient"
|
|
123
|
+
: "sufficient";
|
|
124
|
+
if (group.sample_status !== expectedStatus) {
|
|
125
|
+
context.addIssue({
|
|
126
|
+
code: "custom",
|
|
127
|
+
message: "sample_status must match the configured sample threshold",
|
|
128
|
+
path: ["groups", index, "sample_status"],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const expectedBias = stableMetric(
|
|
133
|
+
group.mean_confidence - group.accuracy,
|
|
134
|
+
);
|
|
135
|
+
if (group.mean_calibration_bias !== expectedBias) {
|
|
136
|
+
context.addIssue({
|
|
137
|
+
code: "custom",
|
|
138
|
+
message: "mean_calibration_bias must equal mean_confidence - accuracy",
|
|
139
|
+
path: ["groups", index, "mean_calibration_bias"],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export type TaskCalibrationSource = z.infer<
|
|
146
|
+
typeof TaskCalibrationSourceSchema
|
|
147
|
+
>;
|
|
148
|
+
export type TaskCalibrationObservation = z.infer<
|
|
149
|
+
typeof TaskCalibrationObservationSchema
|
|
150
|
+
>;
|
|
151
|
+
export type TaskCalibrationOptions = z.input<
|
|
152
|
+
typeof TaskCalibrationOptionsSchema
|
|
153
|
+
>;
|
|
154
|
+
export type TaskCalibrationGroup = z.infer<
|
|
155
|
+
typeof TaskCalibrationGroupSchema
|
|
156
|
+
>;
|
|
157
|
+
export type TaskCalibrationReport = z.infer<
|
|
158
|
+
typeof TaskCalibrationReportSchema
|
|
159
|
+
>;
|
|
160
|
+
|
|
161
|
+
function stableSum(values: readonly number[]): number {
|
|
162
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
163
|
+
let total = 0;
|
|
164
|
+
let compensation = 0;
|
|
165
|
+
|
|
166
|
+
for (const value of sorted) {
|
|
167
|
+
const next = total + value;
|
|
168
|
+
compensation += Math.abs(total) >= Math.abs(value)
|
|
169
|
+
? (total - next) + value
|
|
170
|
+
: (value - next) + total;
|
|
171
|
+
total = next;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return total + compensation;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function compareCodeUnits(left: string, right: string): number {
|
|
178
|
+
if (left < right) return -1;
|
|
179
|
+
if (left > right) return 1;
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function aggregateTaskCalibration(
|
|
184
|
+
observations: readonly unknown[],
|
|
185
|
+
options: TaskCalibrationOptions = {},
|
|
186
|
+
): TaskCalibrationReport {
|
|
187
|
+
const parsedObservations = TaskCalibrationObservationListSchema.parse(
|
|
188
|
+
observations,
|
|
189
|
+
);
|
|
190
|
+
const { minimum_sample_count } = TaskCalibrationOptionsSchema.parse(options);
|
|
191
|
+
const grouped = new Map<string, TaskCalibrationObservation[]>();
|
|
192
|
+
|
|
193
|
+
for (const observation of parsedObservations) {
|
|
194
|
+
const key = JSON.stringify([
|
|
195
|
+
observation.task_type,
|
|
196
|
+
observation.source.provider,
|
|
197
|
+
observation.source.model,
|
|
198
|
+
]);
|
|
199
|
+
const group = grouped.get(key);
|
|
200
|
+
if (group) {
|
|
201
|
+
group.push(observation);
|
|
202
|
+
} else {
|
|
203
|
+
grouped.set(key, [observation]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const groups = [...grouped.values()].map((group) => {
|
|
208
|
+
const sample_count = group.length;
|
|
209
|
+
const correctCount = group.reduce(
|
|
210
|
+
(total, observation) => total + Number(observation.correct),
|
|
211
|
+
0,
|
|
212
|
+
);
|
|
213
|
+
const confidenceTotal = stableSum(
|
|
214
|
+
group.map((observation) => observation.confidence),
|
|
215
|
+
);
|
|
216
|
+
const brierTotal = stableSum(
|
|
217
|
+
group.map((observation) => {
|
|
218
|
+
const outcome = Number(observation.correct);
|
|
219
|
+
return (observation.confidence - outcome) ** 2;
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
const accuracy = stableMetric(correctCount / sample_count);
|
|
223
|
+
const mean_confidence = stableMetric(confidenceTotal / sample_count);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
task_type: group[0].task_type,
|
|
227
|
+
source: group[0].source,
|
|
228
|
+
sample_count,
|
|
229
|
+
accuracy,
|
|
230
|
+
mean_confidence,
|
|
231
|
+
brier_score: stableMetric(brierTotal / sample_count),
|
|
232
|
+
mean_calibration_bias: stableMetric(mean_confidence - accuracy),
|
|
233
|
+
sample_status: sample_count < minimum_sample_count
|
|
234
|
+
? "insufficient" as const
|
|
235
|
+
: "sufficient" as const,
|
|
236
|
+
};
|
|
237
|
+
}).sort((left, right) =>
|
|
238
|
+
compareCodeUnits(left.task_type, right.task_type) ||
|
|
239
|
+
compareCodeUnits(left.source.provider, right.source.provider) ||
|
|
240
|
+
compareCodeUnits(left.source.model, right.source.model)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return TaskCalibrationReportSchema.parse({
|
|
244
|
+
schema_version: "quorum-router.calibration-by-task.v1",
|
|
245
|
+
advisory_only: true,
|
|
246
|
+
minimum_sample_count,
|
|
247
|
+
groups,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { aggregateTaskCalibration } from "./calibration.ts";
|
|
2
|
+
|
|
3
|
+
const evaluated_at = "2026-07-13T00:00:00Z";
|
|
4
|
+
const report = aggregateTaskCalibration([
|
|
5
|
+
{
|
|
6
|
+
observation_id: "demo-1",
|
|
7
|
+
task_type: "code-review",
|
|
8
|
+
source: { provider: "OpenAI", model: "example-model" },
|
|
9
|
+
evaluation_basis: "caller_attested_external_ground_truth",
|
|
10
|
+
correct: true,
|
|
11
|
+
confidence: 0.8,
|
|
12
|
+
evaluated_at,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
observation_id: "demo-2",
|
|
16
|
+
task_type: "code-review",
|
|
17
|
+
source: { provider: "OpenAI", model: "example-model" },
|
|
18
|
+
evaluation_basis: "caller_attested_external_ground_truth",
|
|
19
|
+
correct: false,
|
|
20
|
+
confidence: 0.6,
|
|
21
|
+
evaluated_at,
|
|
22
|
+
},
|
|
23
|
+
], { minimum_sample_count: 2 });
|
|
24
|
+
|
|
25
|
+
console.log(JSON.stringify(report, null, 2));
|
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
} from "./model_inventory.ts";
|
|
12
12
|
import { redact, summarize } from "./redact.ts";
|
|
13
13
|
import { parseAuthMode } from "./schema.ts";
|
|
14
|
+
import {
|
|
15
|
+
auditRouteOutcome,
|
|
16
|
+
preflightRequiredSupabaseAudit,
|
|
17
|
+
runSupabaseStatus,
|
|
18
|
+
} from "./supabase.ts";
|
|
14
19
|
import { buildTrace, writeTrace } from "./trace.ts";
|
|
15
20
|
|
|
16
21
|
const DEFAULT_PROMPT = "Review this README for risky claims.";
|
|
@@ -22,6 +27,7 @@ type CommandName =
|
|
|
22
27
|
| "auth:logout"
|
|
23
28
|
| "models:list"
|
|
24
29
|
| "health"
|
|
30
|
+
| "supabase:status"
|
|
25
31
|
| "route:once"
|
|
26
32
|
| "best-route"
|
|
27
33
|
| "agent-chat";
|
|
@@ -32,11 +38,12 @@ function commandName(): CommandName {
|
|
|
32
38
|
command === "intake" || command === "auth:status" ||
|
|
33
39
|
command === "auth:login" || command === "auth:logout" ||
|
|
34
40
|
command === "models:list" || command === "health" ||
|
|
41
|
+
command === "supabase:status" ||
|
|
35
42
|
command === "route:once" || command === "best-route" ||
|
|
36
43
|
command === "agent-chat"
|
|
37
44
|
) return command;
|
|
38
45
|
throw new Error(
|
|
39
|
-
"usage: deno task intake|auth:status|auth:login|auth:logout|models:list|health|route:once|best-route|agent-chat",
|
|
46
|
+
"usage: deno task intake|auth:status|auth:login|auth:logout|models:list|health|supabase:status|route:once|best-route|agent-chat",
|
|
40
47
|
);
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -101,10 +108,13 @@ try {
|
|
|
101
108
|
else if (command === "auth:logout") await runAuthLogout();
|
|
102
109
|
else if (command === "models:list") await runModelsList();
|
|
103
110
|
else if (command === "health") await runHealth();
|
|
111
|
+
else if (command === "supabase:status") await runSupabaseStatus();
|
|
104
112
|
else if (command === "route:once") {
|
|
113
|
+
await preflightRequiredSupabaseAudit();
|
|
105
114
|
const { results, tracePath, trace } = await invokeSelected(
|
|
106
115
|
promptFromArgs(),
|
|
107
116
|
);
|
|
117
|
+
await auditRouteOutcome(trace);
|
|
108
118
|
console.log("QuorumRouter route:once");
|
|
109
119
|
console.log(`provider: ${results[0].provider}`);
|
|
110
120
|
console.log(`model: ${results[0].model}`);
|
|
@@ -116,7 +126,9 @@ try {
|
|
|
116
126
|
console.log(`final: ${summarize(results[0].response_summary, 500)}`);
|
|
117
127
|
console.log(`trace: ${tracePath}`);
|
|
118
128
|
} else if (command === "best-route") {
|
|
119
|
-
|
|
129
|
+
await preflightRequiredSupabaseAudit();
|
|
130
|
+
const { results, tracePath, trace } = await runBestRoute(promptFromArgs());
|
|
131
|
+
await auditRouteOutcome(trace);
|
|
120
132
|
console.log("QuorumRouter best-route");
|
|
121
133
|
console.log(`models_called: ${results.length}`);
|
|
122
134
|
console.log(`trace: ${tracePath}`);
|