create-quorum-router 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/bin/create-quorum-router.js +25 -4
- package/package.json +1 -1
- package/templates/basic/README.md +64 -6
- package/templates/basic/deno.json +1 -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/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
|
@@ -13,9 +13,10 @@ cd my-quorum-router-demo
|
|
|
13
13
|
deno --version
|
|
14
14
|
deno task smoke
|
|
15
15
|
deno task intake
|
|
16
|
+
deno task supabase:status
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
Current package version: `create-quorum-router@0.1.
|
|
19
|
+
Current package version: `create-quorum-router@0.1.7`. Releases are published
|
|
19
20
|
from an immutable Git tag through GitHub Actions OIDC Trusted Publishing.
|
|
20
21
|
|
|
21
22
|
## What the generated project supports
|
|
@@ -63,6 +64,19 @@ preferred public dogfood path.
|
|
|
63
64
|
Do not commit `.env`, `router.config.local.json`, `.quorum-router/`, or `out/`.
|
|
64
65
|
Do not paste tokens into chat/logs.
|
|
65
66
|
|
|
67
|
+
## Optional user-owned Supabase audit
|
|
68
|
+
|
|
69
|
+
The generated project works without Supabase. Audit defaults to `disabled`.
|
|
70
|
+
Users who opt in apply the generated migration to a Supabase project they own,
|
|
71
|
+
choose `optional` or `required` in the non-secret feature config, inject a
|
|
72
|
+
project URL, publishable/anon key, and Supabase Auth session JWT at runtime, and
|
|
73
|
+
run `deno task supabase:status` before routing.
|
|
74
|
+
|
|
75
|
+
The runtime rejects service-role/admin credentials. Audit records exclude
|
|
76
|
+
prompts, model responses, credentials, client-supplied actor/org identity, and
|
|
77
|
+
client timestamps. Agent Bus, Realtime, state sync, and analytics dashboards are
|
|
78
|
+
not part of this integration.
|
|
79
|
+
|
|
66
80
|
## Runtime boundaries
|
|
67
81
|
|
|
68
82
|
- Best Route/direct is production-ready best-answer routing.
|
|
@@ -71,7 +85,7 @@ Do not paste tokens into chat/logs.
|
|
|
71
85
|
slice only when separately configured with signed policy and distinct
|
|
72
86
|
approval.
|
|
73
87
|
- The generated scaffold does not enable mutation by default.
|
|
74
|
-
- No service-role runtime.
|
|
88
|
+
- No service-role/admin runtime credentials.
|
|
75
89
|
- No live Supabase Agent Bus runtime writes.
|
|
76
90
|
- Public launch requires the repository verification, package tarball, registry
|
|
77
91
|
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.7";
|
|
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.7.
|
|
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.
|
|
@@ -39,6 +41,7 @@ deno task intake
|
|
|
39
41
|
deno task auth:status
|
|
40
42
|
deno task models:list
|
|
41
43
|
deno task health
|
|
44
|
+
deno task supabase:status
|
|
42
45
|
```
|
|
43
46
|
|
|
44
47
|
`smoke` proves the local scaffold runs with deterministic fixtures only. It does
|
|
@@ -51,6 +54,37 @@ under `out/`, and recommends the next command.
|
|
|
51
54
|
In short: intake is the first real setup command before `route:once`,
|
|
52
55
|
`best-route`, or read-only `agent-chat`.
|
|
53
56
|
|
|
57
|
+
## Optional BYO Supabase audit
|
|
58
|
+
|
|
59
|
+
Supabase is not required. With no Supabase config or env, every existing task
|
|
60
|
+
keeps its prior behavior and `deno task supabase:status` exits successfully with
|
|
61
|
+
`state: disabled` without making a network request.
|
|
62
|
+
|
|
63
|
+
To persist route outcomes in a Supabase project you own:
|
|
64
|
+
|
|
65
|
+
1. Create or choose your Supabase project.
|
|
66
|
+
2. Apply **both** files under `supabase/migrations/` in filename order using an
|
|
67
|
+
admin context outside the router runtime. The later limits migration is
|
|
68
|
+
mandatory.
|
|
69
|
+
3. Ensure the active Supabase Auth user session JWT has an authenticated `sub`.
|
|
70
|
+
4. Copy `router.config.example.json` to `router.config.json` and set only
|
|
71
|
+
`features.supabase.audit.mode` to `optional` or `required`.
|
|
72
|
+
5. Inject `QUORUM_ROUTER_SUPABASE_URL`, `QUORUM_ROUTER_SUPABASE_ANON_KEY`, and
|
|
73
|
+
`QUORUM_ROUTER_SUPABASE_SESSION_JWT` at runtime.
|
|
74
|
+
6. Run `deno task supabase:status`, then run `route:once` or `best-route`.
|
|
75
|
+
|
|
76
|
+
`optional` preserves a successful route and prints a warning when audit delivery
|
|
77
|
+
fails. `required` withholds the route result and exits nonzero when audit
|
|
78
|
+
delivery fails. The RPC receives only the route decision and bounded metadata.
|
|
79
|
+
It never receives prompts, model responses, credentials, `org_id`, `actor_id`,
|
|
80
|
+
or `created_at`; the database derives both the actor and this single-user BYO
|
|
81
|
+
audit namespace from `auth.uid()` and owns the timestamp. There is no central or
|
|
82
|
+
shared database and no client tenant claim.
|
|
83
|
+
|
|
84
|
+
Runtime service-role/admin credentials are forbidden. Agent Bus, Realtime
|
|
85
|
+
wakeup, state sync, and an analytics dashboard are future features and are not
|
|
86
|
+
enabled by this audit integration.
|
|
87
|
+
|
|
54
88
|
## Real provider dogfood commands
|
|
55
89
|
|
|
56
90
|
Run only after `intake` reports a usable OAuth/session/wrapper provider:
|
|
@@ -87,6 +121,25 @@ Behavior:
|
|
|
87
121
|
Only use this with repositories you are allowed to send to the selected
|
|
88
122
|
provider.
|
|
89
123
|
|
|
124
|
+
### Cost-aware Best Route
|
|
125
|
+
|
|
126
|
+
Cost-aware routing is disabled unless both budget variables are set. Estimates
|
|
127
|
+
are user-supplied per invocation, not live provider billing data.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
QUORUM_ROUTER_MAX_BUDGET_USD=0.05 \
|
|
131
|
+
QUORUM_ROUTER_ESTIMATED_COSTS_JSON='{"openai/gpt-5":0.03,"xai/grok":0.02}' \
|
|
132
|
+
RUN_EXTERNAL_MODEL_DOGFOOD=1 \
|
|
133
|
+
deno task best-route --prompt "Choose the safest launch copy."
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Best Route preserves candidate quality/readiness order, admits candidates while
|
|
137
|
+
their configured estimates fit the budget, and records selected and excluded
|
|
138
|
+
model IDs plus reasons in `out/best-route-trace.json`. Models without an
|
|
139
|
+
estimate are excluded when cost-aware routing is enabled. If no candidate fits,
|
|
140
|
+
the command fails before invoking a provider. This is pre-invocation budget
|
|
141
|
+
control; it does not claim exact or live API spend.
|
|
142
|
+
|
|
90
143
|
### Forced wrapper provider/model selection
|
|
91
144
|
|
|
92
145
|
Use these only when you want a specific local wrapper/model. The scaffold fails
|
|
@@ -152,17 +205,22 @@ not paste them into chat/logs and do not commit `.env`.
|
|
|
152
205
|
- `.gitignore` — excludes `.env`, `out/`, `router.config.local.json`,
|
|
153
206
|
`provider_config.json`, and `.quorum-router/`.
|
|
154
207
|
- `router.config.example.json` — non-secret example boundaries.
|
|
208
|
+
- `supabase/migrations/20260701130000_workflow_access_audit.sql` — optional BYO
|
|
209
|
+
Supabase audit migration
|
|
210
|
+
- `supabase/migrations/20260712211500_workflow_access_audit_limits.sql` —
|
|
211
|
+
required RPC narrowing and payload-limit migration
|
|
155
212
|
- `src/cli.ts` — command dispatcher.
|
|
213
|
+
- `src/supabase.ts` — offline status and selective audit RPC hook.
|
|
156
214
|
- `src/intake.ts` — first-run onboarding.
|
|
157
215
|
- `src/auth.ts`, `src/auth_oauth.ts`, `src/auth_session.ts`,
|
|
158
216
|
`src/auth_env_fallback.ts` — auth/session/fallback boundaries.
|
|
159
217
|
- `src/provider_registry.ts`, `src/model_inventory.ts`, `src/wrapper_client.ts`,
|
|
160
218
|
`src/provider_client.ts` — provider discovery and safe invocation.
|
|
161
219
|
- `src/best_route.ts`, `src/agent_chat.ts` — gated dogfood commands.
|
|
220
|
+
- `src/cost_aware.ts` — estimated-cost budget selection for Best Route.
|
|
162
221
|
- `src/trace.ts`, `src/redact.ts`, `src/schema.ts`, `src/fixture_smoke.ts` —
|
|
163
222
|
trace/redaction/schema/fixture support.
|
|
164
223
|
- `out/.gitkeep` — local output directory placeholder.
|
|
165
224
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
does not publish npm, create a GitHub release, or mutate tags/dist-tags.
|
|
225
|
+
This generated scaffold operates locally. It does not publish npm packages,
|
|
226
|
+
create GitHub releases, or mutate tags or dist-tags.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"auth:logout": "deno run --allow-read --allow-write --allow-env src/cli.ts auth:logout",
|
|
13
13
|
"models:list": "deno run --allow-read --allow-write --allow-env --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts models:list",
|
|
14
14
|
"health": "deno run --allow-read --allow-write --allow-env --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts health",
|
|
15
|
+
"supabase:status": "deno run --allow-read --allow-env src/cli.ts supabase:status",
|
|
15
16
|
"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
17
|
"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
18
|
"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 };
|
|
@@ -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}`);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readRouterEnv } from "./env.ts";
|
|
2
|
+
import type { CostAwareTrace, ModelInventoryEntry } from "./schema.ts";
|
|
3
|
+
|
|
4
|
+
export type CostExclusion = {
|
|
5
|
+
model_id: string;
|
|
6
|
+
estimated_cost_usd?: number;
|
|
7
|
+
reason: "missing_estimate" | "budget_exceeded";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CostAwareSelection =
|
|
11
|
+
| CostAwareTrace
|
|
12
|
+
| {
|
|
13
|
+
enabled: false;
|
|
14
|
+
selected_model_ids: string[];
|
|
15
|
+
excluded: CostExclusion[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CostAwareConfig = {
|
|
19
|
+
maxBudgetUsd: number;
|
|
20
|
+
estimates: Map<string, number>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function normalizedModelId(value: string): string {
|
|
24
|
+
return value.trim().toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parsePositiveBudget(raw: string): number {
|
|
28
|
+
const value = Number(raw);
|
|
29
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"QuorumRouter blocked: QUORUM_ROUTER_MAX_BUDGET_USD must be a finite number > 0",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseEstimates(raw: string): Map<string, number> {
|
|
38
|
+
let parsed: unknown;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"QuorumRouter blocked: QUORUM_ROUTER_ESTIMATED_COSTS_JSON must be valid JSON",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"QuorumRouter blocked: QUORUM_ROUTER_ESTIMATED_COSTS_JSON must be a model-id to USD object",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const estimates = new Map<string, number>();
|
|
53
|
+
for (const [rawModelId, rawCost] of Object.entries(parsed)) {
|
|
54
|
+
const modelId = normalizedModelId(rawModelId);
|
|
55
|
+
if (
|
|
56
|
+
!modelId || typeof rawCost !== "number" || !Number.isFinite(rawCost) ||
|
|
57
|
+
rawCost < 0
|
|
58
|
+
) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"QuorumRouter blocked: every cost estimate must use a non-empty model id and a finite USD number >= 0",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (estimates.has(modelId)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`QuorumRouter blocked: duplicate normalized cost estimate for ${modelId}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
estimates.set(modelId, rawCost);
|
|
69
|
+
}
|
|
70
|
+
if (estimates.size === 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"QuorumRouter blocked: QUORUM_ROUTER_ESTIMATED_COSTS_JSON must contain at least one estimate",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return estimates;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function readCostAwareConfig(): CostAwareConfig | undefined {
|
|
79
|
+
const rawBudget = readRouterEnv("QUORUM_ROUTER_MAX_BUDGET_USD");
|
|
80
|
+
const rawEstimates = readRouterEnv("QUORUM_ROUTER_ESTIMATED_COSTS_JSON");
|
|
81
|
+
if (rawBudget === undefined && rawEstimates === undefined) return undefined;
|
|
82
|
+
if (rawBudget === undefined || rawEstimates === undefined) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"QuorumRouter blocked: cost-aware routing requires both QUORUM_ROUTER_MAX_BUDGET_USD and QUORUM_ROUTER_ESTIMATED_COSTS_JSON",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
maxBudgetUsd: parsePositiveBudget(rawBudget),
|
|
89
|
+
estimates: parseEstimates(rawEstimates),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function selectCandidatesWithinBudget(
|
|
94
|
+
candidates: ModelInventoryEntry[],
|
|
95
|
+
config = readCostAwareConfig(),
|
|
96
|
+
): { candidates: ModelInventoryEntry[]; cost: CostAwareSelection } {
|
|
97
|
+
if (!config) {
|
|
98
|
+
return {
|
|
99
|
+
candidates,
|
|
100
|
+
cost: {
|
|
101
|
+
enabled: false,
|
|
102
|
+
selected_model_ids: candidates.map((candidate) => candidate.model_id),
|
|
103
|
+
excluded: [],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const selected: ModelInventoryEntry[] = [];
|
|
109
|
+
const excluded: CostExclusion[] = [];
|
|
110
|
+
let estimatedTotalUsd = 0;
|
|
111
|
+
for (const candidate of candidates) {
|
|
112
|
+
const estimate = config.estimates.get(
|
|
113
|
+
normalizedModelId(candidate.model_id),
|
|
114
|
+
);
|
|
115
|
+
if (estimate === undefined) {
|
|
116
|
+
excluded.push({
|
|
117
|
+
model_id: candidate.model_id,
|
|
118
|
+
reason: "missing_estimate",
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (estimatedTotalUsd + estimate > config.maxBudgetUsd) {
|
|
123
|
+
excluded.push({
|
|
124
|
+
model_id: candidate.model_id,
|
|
125
|
+
estimated_cost_usd: estimate,
|
|
126
|
+
reason: "budget_exceeded",
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
selected.push(candidate);
|
|
131
|
+
estimatedTotalUsd += estimate;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (selected.length === 0) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"QuorumRouter blocked: cost-aware routing selected no candidates within the configured budget",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
candidates: selected,
|
|
141
|
+
cost: {
|
|
142
|
+
enabled: true,
|
|
143
|
+
pricing_source: "configured_estimate_not_live_billing",
|
|
144
|
+
max_budget_usd: config.maxBudgetUsd,
|
|
145
|
+
estimated_total_usd: estimatedTotalUsd,
|
|
146
|
+
selected_model_ids: selected.map((candidate) => candidate.model_id),
|
|
147
|
+
excluded,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -9,6 +9,7 @@ export type ProviderSpec = {
|
|
|
9
9
|
source: "wrapper" | "oauth_session" | "local_cli" | "env_fallback";
|
|
10
10
|
command?: string;
|
|
11
11
|
args_template?: string[];
|
|
12
|
+
prompt_transport?: "stdin" | "file";
|
|
12
13
|
list_models_args?: string[];
|
|
13
14
|
list_blocked_reason?: string;
|
|
14
15
|
notes: string[];
|
|
@@ -145,8 +146,9 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
|
|
|
145
146
|
"__CWD__",
|
|
146
147
|
"--output-last-message",
|
|
147
148
|
"__OUT__",
|
|
148
|
-
"
|
|
149
|
+
"-",
|
|
149
150
|
],
|
|
151
|
+
prompt_transport: "stdin",
|
|
150
152
|
list_blocked_reason:
|
|
151
153
|
"codex models is tty/stdin dependent in this environment; model catalog listing is unavailable from non-interactive Deno.",
|
|
152
154
|
notes: [
|
|
@@ -160,7 +162,8 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
|
|
|
160
162
|
auth_mode: "oauth",
|
|
161
163
|
source: "oauth_session",
|
|
162
164
|
command: "claude",
|
|
163
|
-
args_template: ["-p"
|
|
165
|
+
args_template: ["-p"],
|
|
166
|
+
prompt_transport: "stdin",
|
|
164
167
|
list_blocked_reason:
|
|
165
168
|
"Claude Code model listing is blocked by organization policy / disabled subscription access in this environment.",
|
|
166
169
|
notes: [
|
|
@@ -174,7 +177,8 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
|
|
|
174
177
|
auth_mode: "oauth",
|
|
175
178
|
source: "oauth_session",
|
|
176
179
|
command: "gemini",
|
|
177
|
-
args_template: ["-p", "
|
|
180
|
+
args_template: ["-p", ""],
|
|
181
|
+
prompt_transport: "stdin",
|
|
178
182
|
list_blocked_reason:
|
|
179
183
|
"Gemini CLI list/invoke paths require a trusted directory in non-interactive runs unless the user opts into the trust setting.",
|
|
180
184
|
notes: [
|
|
@@ -189,14 +193,15 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
|
|
|
189
193
|
source: "oauth_session",
|
|
190
194
|
command: "grok",
|
|
191
195
|
args_template: [
|
|
192
|
-
"-
|
|
193
|
-
"
|
|
196
|
+
"--prompt-file",
|
|
197
|
+
"__PROMPT_FILE__",
|
|
194
198
|
"--output-format",
|
|
195
199
|
"plain",
|
|
196
200
|
"--permission-mode",
|
|
197
201
|
"plan",
|
|
198
202
|
"--disable-web-search",
|
|
199
203
|
],
|
|
204
|
+
prompt_transport: "file",
|
|
200
205
|
list_models_args: ["models"],
|
|
201
206
|
notes: [
|
|
202
207
|
"Uses existing Grok CLI session; `grok models` is safe list-only discovery.",
|
|
@@ -209,7 +214,8 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
|
|
|
209
214
|
auth_mode: "session",
|
|
210
215
|
source: "local_cli",
|
|
211
216
|
command: "devin",
|
|
212
|
-
args_template: ["-p", "
|
|
217
|
+
args_template: ["-p", "--prompt-file", "__PROMPT_FILE__"],
|
|
218
|
+
prompt_transport: "file",
|
|
213
219
|
list_blocked_reason:
|
|
214
220
|
"Devin CLI does not expose a models subcommand in this environment.",
|
|
215
221
|
notes: ["Uses existing Devin CLI session."],
|
|
@@ -221,7 +227,8 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
|
|
|
221
227
|
auth_mode: "session",
|
|
222
228
|
source: "local_cli",
|
|
223
229
|
command: "qwen",
|
|
224
|
-
args_template: ["-p", "
|
|
230
|
+
args_template: ["-p", ""],
|
|
231
|
+
prompt_transport: "stdin",
|
|
225
232
|
list_blocked_reason:
|
|
226
233
|
"Qwen CLI model listing path is stdin/API-key dependent in this environment.",
|
|
227
234
|
notes: ["Uses existing local Qwen CLI session."],
|