create-quorum-router 0.1.5
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 +21 -0
- package/README.md +89 -0
- package/bin/create-quorum-router.js +161 -0
- package/package.json +19 -0
- package/templates/basic/README.md +168 -0
- package/templates/basic/deno.json +19 -0
- package/templates/basic/gitignore +5 -0
- package/templates/basic/main.ts +3 -0
- package/templates/basic/out/.gitkeep +0 -0
- package/templates/basic/router.config.example.json +15 -0
- package/templates/basic/src/agent_chat.ts +262 -0
- package/templates/basic/src/auth.ts +58 -0
- package/templates/basic/src/auth_env_fallback.ts +78 -0
- package/templates/basic/src/auth_oauth.ts +19 -0
- package/templates/basic/src/auth_session.ts +271 -0
- package/templates/basic/src/best_route.ts +322 -0
- package/templates/basic/src/cli.ts +158 -0
- package/templates/basic/src/context.ts +345 -0
- package/templates/basic/src/env.ts +10 -0
- package/templates/basic/src/fixture_smoke.ts +29 -0
- package/templates/basic/src/intake.ts +65 -0
- package/templates/basic/src/model_inventory.ts +59 -0
- package/templates/basic/src/provider_client.ts +12 -0
- package/templates/basic/src/provider_registry.ts +254 -0
- package/templates/basic/src/redact.ts +65 -0
- package/templates/basic/src/schema.ts +138 -0
- package/templates/basic/src/trace.ts +140 -0
- package/templates/basic/src/wrapper_client.ts +224 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { ModelInventoryEntry } from "./schema.ts";
|
|
2
|
+
import { readRouterEnv } from "./env.ts";
|
|
3
|
+
|
|
4
|
+
export type ProviderSpec = {
|
|
5
|
+
provider: string;
|
|
6
|
+
model: string;
|
|
7
|
+
model_id: string;
|
|
8
|
+
auth_mode: "oauth" | "session" | "env";
|
|
9
|
+
source: "wrapper" | "oauth_session" | "local_cli" | "env_fallback";
|
|
10
|
+
command?: string;
|
|
11
|
+
args_template?: string[];
|
|
12
|
+
list_models_args?: string[];
|
|
13
|
+
list_blocked_reason?: string;
|
|
14
|
+
notes: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ProviderSelectionRequest = {
|
|
18
|
+
providerLabel?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function normalizeSelectionValue(
|
|
23
|
+
value: string | undefined,
|
|
24
|
+
): string | undefined {
|
|
25
|
+
const trimmed = value?.trim().toLowerCase();
|
|
26
|
+
return trimmed ? trimmed : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readProviderSelectionRequest(): ProviderSelectionRequest {
|
|
30
|
+
return {
|
|
31
|
+
providerLabel: readRouterEnv("QUORUM_ROUTER_PROVIDER_LABEL")?.trim() ||
|
|
32
|
+
readRouterEnv("QUORUM_ROUTER_WRAPPER_PROVIDER_LABEL")?.trim() ||
|
|
33
|
+
undefined,
|
|
34
|
+
model: readRouterEnv("QUORUM_ROUTER_PROVIDER_MODEL")?.trim() ||
|
|
35
|
+
readRouterEnv("QUORUM_ROUTER_WRAPPER_PROVIDER_MODEL")?.trim() ||
|
|
36
|
+
undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function aliasSet(values: Array<string | undefined>): string[] {
|
|
41
|
+
return [
|
|
42
|
+
...new Set(
|
|
43
|
+
values.map(normalizeSelectionValue).filter((value) =>
|
|
44
|
+
Boolean(value)
|
|
45
|
+
) as string[],
|
|
46
|
+
),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function providerAliasesForCommand(
|
|
51
|
+
provider: string,
|
|
52
|
+
model: string,
|
|
53
|
+
modelId: string,
|
|
54
|
+
_source: string,
|
|
55
|
+
command?: string,
|
|
56
|
+
): string[] {
|
|
57
|
+
const commandAliases: Record<string, string[]> = {
|
|
58
|
+
grok: ["grok", "grok-cli", "xai", "xai-cli"],
|
|
59
|
+
codex: ["openai", "codex", "codex-cli", "openai-codex"],
|
|
60
|
+
claude: ["anthropic", "claude", "claude-code"],
|
|
61
|
+
gemini: ["google", "gemini", "gemini-cli"],
|
|
62
|
+
devin: ["cognition", "devin", "devin-cli"],
|
|
63
|
+
qwen: ["alibaba", "qwen", "qwen-cli"],
|
|
64
|
+
};
|
|
65
|
+
const providerAliases: Record<string, string[]> = {
|
|
66
|
+
xai: ["grok", "grok-cli", "xai", "xai-cli"],
|
|
67
|
+
openai: ["openai", "codex", "codex-cli", "openai-codex"],
|
|
68
|
+
anthropic: ["anthropic", "claude", "claude-code"],
|
|
69
|
+
google: ["google", "gemini", "gemini-cli"],
|
|
70
|
+
cognition: ["cognition", "devin", "devin-cli"],
|
|
71
|
+
alibaba: ["alibaba", "qwen", "qwen-cli"],
|
|
72
|
+
};
|
|
73
|
+
const normalizedProvider = normalizeSelectionValue(provider);
|
|
74
|
+
return aliasSet([
|
|
75
|
+
provider,
|
|
76
|
+
model,
|
|
77
|
+
modelId,
|
|
78
|
+
command,
|
|
79
|
+
...(command ? commandAliases[command] ?? [] : []),
|
|
80
|
+
...(normalizedProvider ? providerAliases[normalizedProvider] ?? [] : []),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function providerSelectionMatches(
|
|
85
|
+
entry: {
|
|
86
|
+
provider: string;
|
|
87
|
+
model: string;
|
|
88
|
+
model_id: string;
|
|
89
|
+
source: string;
|
|
90
|
+
command?: string;
|
|
91
|
+
},
|
|
92
|
+
requestedProviderLabel: string | undefined,
|
|
93
|
+
): boolean {
|
|
94
|
+
const requested = normalizeSelectionValue(requestedProviderLabel);
|
|
95
|
+
if (!requested) return true;
|
|
96
|
+
return providerAliasesForCommand(
|
|
97
|
+
entry.provider,
|
|
98
|
+
entry.model,
|
|
99
|
+
entry.model_id,
|
|
100
|
+
entry.source,
|
|
101
|
+
entry.command,
|
|
102
|
+
).includes(requested);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function modelSelectionMatches(
|
|
106
|
+
entry: {
|
|
107
|
+
provider?: string;
|
|
108
|
+
model: string;
|
|
109
|
+
model_id: string;
|
|
110
|
+
listed_models?: string[];
|
|
111
|
+
},
|
|
112
|
+
requestedModel: string | undefined,
|
|
113
|
+
): boolean {
|
|
114
|
+
const requested = normalizeSelectionValue(requestedModel);
|
|
115
|
+
if (!requested) return true;
|
|
116
|
+
const providerPrefix = normalizeSelectionValue(entry.provider)?.replace(
|
|
117
|
+
/[^a-z0-9]+/g,
|
|
118
|
+
"",
|
|
119
|
+
);
|
|
120
|
+
const listedModels = entry.listed_models ?? [];
|
|
121
|
+
return aliasSet([
|
|
122
|
+
entry.model,
|
|
123
|
+
entry.model_id,
|
|
124
|
+
...listedModels,
|
|
125
|
+
...(providerPrefix
|
|
126
|
+
? listedModels.map((model) => `${providerPrefix}/${model}`)
|
|
127
|
+
: []),
|
|
128
|
+
]).includes(requested);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
|
|
132
|
+
{
|
|
133
|
+
provider: "OpenAI",
|
|
134
|
+
model: "codex-cli",
|
|
135
|
+
model_id: "openai/codex-cli",
|
|
136
|
+
auth_mode: "oauth",
|
|
137
|
+
source: "oauth_session",
|
|
138
|
+
command: "codex",
|
|
139
|
+
args_template: [
|
|
140
|
+
"exec",
|
|
141
|
+
"--sandbox",
|
|
142
|
+
"read-only",
|
|
143
|
+
"--skip-git-repo-check",
|
|
144
|
+
"--cd",
|
|
145
|
+
"__CWD__",
|
|
146
|
+
"--output-last-message",
|
|
147
|
+
"__OUT__",
|
|
148
|
+
"__PROMPT__",
|
|
149
|
+
],
|
|
150
|
+
list_blocked_reason:
|
|
151
|
+
"codex models is tty/stdin dependent in this environment; model catalog listing is unavailable from non-interactive Deno.",
|
|
152
|
+
notes: [
|
|
153
|
+
"Uses existing Codex CLI OAuth/session; command presence and invocation are verified separately.",
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
provider: "Anthropic",
|
|
158
|
+
model: "claude-code",
|
|
159
|
+
model_id: "anthropic/claude-code",
|
|
160
|
+
auth_mode: "oauth",
|
|
161
|
+
source: "oauth_session",
|
|
162
|
+
command: "claude",
|
|
163
|
+
args_template: ["-p", "__PROMPT__"],
|
|
164
|
+
list_blocked_reason:
|
|
165
|
+
"Claude Code model listing is blocked by organization policy / disabled subscription access in this environment.",
|
|
166
|
+
notes: [
|
|
167
|
+
"Uses existing Claude Code session; do not trigger login from dogfood runner.",
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
provider: "Google",
|
|
172
|
+
model: "gemini-cli",
|
|
173
|
+
model_id: "google/gemini-cli",
|
|
174
|
+
auth_mode: "oauth",
|
|
175
|
+
source: "oauth_session",
|
|
176
|
+
command: "gemini",
|
|
177
|
+
args_template: ["-p", "__PROMPT__"],
|
|
178
|
+
list_blocked_reason:
|
|
179
|
+
"Gemini CLI list/invoke paths require a trusted directory in non-interactive runs unless the user opts into the trust setting.",
|
|
180
|
+
notes: [
|
|
181
|
+
"Gemini CLI can require workspace trust; inventory does not trigger login or trust flows.",
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
provider: "xAI",
|
|
186
|
+
model: "grok-cli",
|
|
187
|
+
model_id: "xai/grok-cli",
|
|
188
|
+
auth_mode: "session",
|
|
189
|
+
source: "oauth_session",
|
|
190
|
+
command: "grok",
|
|
191
|
+
args_template: [
|
|
192
|
+
"-p",
|
|
193
|
+
"__PROMPT__",
|
|
194
|
+
"--output-format",
|
|
195
|
+
"plain",
|
|
196
|
+
"--permission-mode",
|
|
197
|
+
"plan",
|
|
198
|
+
"--disable-web-search",
|
|
199
|
+
],
|
|
200
|
+
list_models_args: ["models"],
|
|
201
|
+
notes: [
|
|
202
|
+
"Uses existing Grok CLI session; `grok models` is safe list-only discovery.",
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
provider: "Cognition",
|
|
207
|
+
model: "devin-cli",
|
|
208
|
+
model_id: "cognition/devin-cli",
|
|
209
|
+
auth_mode: "session",
|
|
210
|
+
source: "local_cli",
|
|
211
|
+
command: "devin",
|
|
212
|
+
args_template: ["-p", "__PROMPT__"],
|
|
213
|
+
list_blocked_reason:
|
|
214
|
+
"Devin CLI does not expose a models subcommand in this environment.",
|
|
215
|
+
notes: ["Uses existing Devin CLI session."],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
provider: "Alibaba",
|
|
219
|
+
model: "qwen-cli",
|
|
220
|
+
model_id: "alibaba/qwen-cli",
|
|
221
|
+
auth_mode: "session",
|
|
222
|
+
source: "local_cli",
|
|
223
|
+
command: "qwen",
|
|
224
|
+
args_template: ["-p", "__PROMPT__"],
|
|
225
|
+
list_blocked_reason:
|
|
226
|
+
"Qwen CLI model listing path is stdin/API-key dependent in this environment.",
|
|
227
|
+
notes: ["Uses existing local Qwen CLI session."],
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
export function envFallbackEntry(configured: boolean): ModelInventoryEntry {
|
|
232
|
+
const label = readRouterEnv("QUORUM_ROUTER_PROVIDER_LABEL")?.trim() ||
|
|
233
|
+
"OpenAI-compatible env fallback";
|
|
234
|
+
const model = readRouterEnv("QUORUM_ROUTER_PROVIDER_MODEL")?.trim() ||
|
|
235
|
+
"missing-model";
|
|
236
|
+
return {
|
|
237
|
+
provider: label,
|
|
238
|
+
auth_mode: "env",
|
|
239
|
+
model,
|
|
240
|
+
model_id: `env/${model}`,
|
|
241
|
+
source: "env_fallback",
|
|
242
|
+
available: configured,
|
|
243
|
+
blocked_reason: configured
|
|
244
|
+
? undefined
|
|
245
|
+
: "missing explicit private env fallback configuration",
|
|
246
|
+
can_list_models: false,
|
|
247
|
+
list_blocked_reason:
|
|
248
|
+
"Generic env fallback uses a single user-selected model and does not perform provider catalog discovery.",
|
|
249
|
+
can_invoke: configured,
|
|
250
|
+
notes: [
|
|
251
|
+
"Env fallback is explicit-only and is not the preferred public dogfood path.",
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const REDACTION_PATTERNS: RegExp[] = [
|
|
2
|
+
/Bearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
|
|
3
|
+
/session[_-]?id\s*[=:]\s*[0-9a-f]{8,}(?:-[0-9a-f]{4,})*/gi,
|
|
4
|
+
/["']?authorization["']?\s*[:=]\s*["']?[^"'\n\r,}]+["']?/gi,
|
|
5
|
+
/["']?refresh[_-]?token["']?\s*[:=]\s*["']?[^"'\n\r,}]+["']?/gi,
|
|
6
|
+
/["']?access[_-]?token["']?\s*[:=]\s*["']?[^"'\n\r,}]+["']?/gi,
|
|
7
|
+
/["']?api[_-]?key["']?\s*[:=]\s*["']?[^"'\n\r,}]+["']?/gi,
|
|
8
|
+
/["']?session(?:\s+|[_-]?)id["']?\s*[:=]\s*["']?[^"'\n\r,}]+["']?/gi,
|
|
9
|
+
/["']?cookie["']?\s*[:=]\s*["']?[^"'\n\r,}]+["']?/gi,
|
|
10
|
+
/["']?password["']?\s*[:=]\s*["']?[^"'\n\r,}]+["']?/gi,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const SENSITIVE_ENV = [
|
|
14
|
+
"QUORUM_ROUTER_PROVIDER_API_KEY",
|
|
15
|
+
"OPENAI_API_KEY",
|
|
16
|
+
"ANTHROPIC_API_KEY",
|
|
17
|
+
"XAI_API_KEY",
|
|
18
|
+
"GROK_API_KEY",
|
|
19
|
+
"GLM_API_KEY",
|
|
20
|
+
"ZHIPUAI_API_KEY",
|
|
21
|
+
"BIGMODEL_API_KEY",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function knownSecretValues(): string[] {
|
|
25
|
+
return SENSITIVE_ENV.flatMap((name) => {
|
|
26
|
+
const value = Deno.env.get(name);
|
|
27
|
+
return value && value.length >= 8 ? [value] : [];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function redact(text: string, extraSecrets: string[] = []): string {
|
|
32
|
+
let safe = text;
|
|
33
|
+
for (
|
|
34
|
+
const secret of [...knownSecretValues(), ...extraSecrets].filter(Boolean)
|
|
35
|
+
.sort((a, b) => b.length - a.length)
|
|
36
|
+
) {
|
|
37
|
+
safe = safe.split(secret).join("[REDACTED]");
|
|
38
|
+
}
|
|
39
|
+
for (const pattern of REDACTION_PATTERNS) {
|
|
40
|
+
safe = safe.replace(pattern, "[REDACTED]");
|
|
41
|
+
}
|
|
42
|
+
return safe;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function summarize(text: string, max = 500): string {
|
|
46
|
+
const collapsed = redact(text).replace(/\s+/g, " ").trim();
|
|
47
|
+
return collapsed.length > max ? `${collapsed.slice(0, max)}…` : collapsed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function redactionOk(value: unknown): boolean {
|
|
51
|
+
const text = JSON.stringify(value);
|
|
52
|
+
const unescaped = text.replace(/\\"/g, '"');
|
|
53
|
+
const unsafeKeyPattern =
|
|
54
|
+
/["']?(authorization|refresh[_-]?token|access[_-]?token|cookie|password|session(?:\s+|[_-]?)id|api[_-]?key)["']?\s*[:=]/i;
|
|
55
|
+
if (unsafeKeyPattern.test(text) || unsafeKeyPattern.test(unescaped)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (redact(text) !== text || redact(unescaped) !== unescaped) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
for (const secret of knownSecretValues()) {
|
|
62
|
+
if (secret && text.includes(secret)) return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export type AuthMode = "auto" | "wrapper" | "oauth" | "session" | "env";
|
|
2
|
+
export type InventorySource =
|
|
3
|
+
| "wrapper"
|
|
4
|
+
| "oauth_session"
|
|
5
|
+
| "local_cli"
|
|
6
|
+
| "env_fallback"
|
|
7
|
+
| "static_config";
|
|
8
|
+
|
|
9
|
+
export type ModelInventoryEntry = {
|
|
10
|
+
provider: string;
|
|
11
|
+
auth_mode: AuthMode | "session" | "oauth" | "env";
|
|
12
|
+
model: string;
|
|
13
|
+
model_id: string;
|
|
14
|
+
source: InventorySource;
|
|
15
|
+
available: boolean;
|
|
16
|
+
blocked_reason?: string;
|
|
17
|
+
can_list_models: boolean;
|
|
18
|
+
listed_models?: string[];
|
|
19
|
+
list_blocked_reason?: string;
|
|
20
|
+
can_invoke: boolean;
|
|
21
|
+
notes: string[];
|
|
22
|
+
command?: string;
|
|
23
|
+
args_template?: string[];
|
|
24
|
+
invocation_model?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ModelInventory = {
|
|
28
|
+
generated_at: string;
|
|
29
|
+
auth_mode: AuthMode;
|
|
30
|
+
entries: ModelInventoryEntry[];
|
|
31
|
+
available_count: number;
|
|
32
|
+
blocked_count: number;
|
|
33
|
+
env_fallback_configured: boolean;
|
|
34
|
+
env_fallback_used: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ProviderResult = {
|
|
38
|
+
provider: string;
|
|
39
|
+
model: string;
|
|
40
|
+
model_id?: string;
|
|
41
|
+
source?: string;
|
|
42
|
+
command?: string;
|
|
43
|
+
listed_models?: string[];
|
|
44
|
+
response_received: boolean;
|
|
45
|
+
schema_valid: boolean;
|
|
46
|
+
response_summary: string;
|
|
47
|
+
raw_content: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ScoreRow = {
|
|
52
|
+
provider: string;
|
|
53
|
+
model: string;
|
|
54
|
+
schema_valid: boolean;
|
|
55
|
+
response_received: boolean;
|
|
56
|
+
clarity: number;
|
|
57
|
+
completeness: number;
|
|
58
|
+
risk: number;
|
|
59
|
+
instruction_fit: number;
|
|
60
|
+
final_score: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type AgentChatTurn = {
|
|
64
|
+
round: number;
|
|
65
|
+
provider: string;
|
|
66
|
+
model: string;
|
|
67
|
+
reply_to?: { provider: string; model: string; round: number };
|
|
68
|
+
content: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type PromptContextTrace = {
|
|
72
|
+
prompt_has_context: boolean;
|
|
73
|
+
original_prompt_chars: number;
|
|
74
|
+
effective_prompt_chars: number;
|
|
75
|
+
prompt_truncated: boolean;
|
|
76
|
+
context_chars: number;
|
|
77
|
+
github_repo?: string;
|
|
78
|
+
github_default_branch?: string;
|
|
79
|
+
files_included: string[];
|
|
80
|
+
files_considered: number;
|
|
81
|
+
tree_truncated: boolean;
|
|
82
|
+
context_fetch_error?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type DogfoodTrace = {
|
|
86
|
+
run_id: string;
|
|
87
|
+
timestamp: string;
|
|
88
|
+
command: string;
|
|
89
|
+
mode: "route_once" | "best_route" | "agent_chat" | "health";
|
|
90
|
+
auth_mode: AuthMode;
|
|
91
|
+
provider?: string;
|
|
92
|
+
model?: string;
|
|
93
|
+
requested_provider_label?: string;
|
|
94
|
+
requested_model?: string;
|
|
95
|
+
selected_provider?: string;
|
|
96
|
+
selected_model?: string;
|
|
97
|
+
provider_selection_honored: boolean;
|
|
98
|
+
fallback_used: boolean;
|
|
99
|
+
prompt_hash?: string;
|
|
100
|
+
prompt_summary?: string;
|
|
101
|
+
prompt_context?: PromptContextTrace;
|
|
102
|
+
response_summary?: string;
|
|
103
|
+
schema_valid: boolean;
|
|
104
|
+
redaction_ok: boolean;
|
|
105
|
+
credential_value_present: boolean;
|
|
106
|
+
sensitive_value_present: boolean;
|
|
107
|
+
selected_route?: { provider: string; model: string; final_score?: number };
|
|
108
|
+
score_table?: ScoreRow[];
|
|
109
|
+
agent_chat_turns?: AgentChatTurn[];
|
|
110
|
+
errors: string[];
|
|
111
|
+
boundaries: string[];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function parseAuthMode(value: string | undefined): AuthMode {
|
|
115
|
+
const mode = (value ?? "auto").trim().toLowerCase();
|
|
116
|
+
if (["auto", "wrapper", "oauth", "session", "env"].includes(mode)) {
|
|
117
|
+
return mode as AuthMode;
|
|
118
|
+
}
|
|
119
|
+
throw new Error(
|
|
120
|
+
`QuorumRouter blocked: invalid QUORUM_ROUTER_AUTH_MODE '${mode}'`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function assertOptIn(): void {
|
|
125
|
+
if (Deno.env.get("RUN_EXTERNAL_MODEL_DOGFOOD") !== "1") {
|
|
126
|
+
throw new Error(
|
|
127
|
+
"QuorumRouter external dogfood blocked: set RUN_EXTERNAL_MODEL_DOGFOOD=1 to invoke real providers",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function assertAgentChatOptIn(): void {
|
|
133
|
+
if (Deno.env.get("RUN_EXPERIMENTAL_AGENT_CHAT") !== "1") {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"agent_chat blocked: set RUN_EXPERIMENTAL_AGENT_CHAT=1; live multi-model dialogue is explicit opt-in",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentChatTurn,
|
|
3
|
+
AuthMode,
|
|
4
|
+
DogfoodTrace,
|
|
5
|
+
PromptContextTrace,
|
|
6
|
+
ProviderResult,
|
|
7
|
+
ScoreRow,
|
|
8
|
+
} from "./schema.ts";
|
|
9
|
+
import { redactionOk, summarize } from "./redact.ts";
|
|
10
|
+
|
|
11
|
+
export const OUT_DIR = "out";
|
|
12
|
+
|
|
13
|
+
export function boundaries(agentChat = false): string[] {
|
|
14
|
+
return [
|
|
15
|
+
"deno task intake is the first real setup command before launch",
|
|
16
|
+
"deno task smoke is fixture-only and not real provider dogfood",
|
|
17
|
+
"Best Route/direct remains production-ready best-answer routing",
|
|
18
|
+
agentChat
|
|
19
|
+
? "live multi-model agent_chat is explicit opt-in and requires two working identities"
|
|
20
|
+
: "agent_chat remains explicit opt-in",
|
|
21
|
+
"SafeLoop-backed production repository execution requires explicit external configuration and distinct approval",
|
|
22
|
+
"no live Supabase Agent Bus runtime writes",
|
|
23
|
+
"no service-role runtime",
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function promptHash(prompt: string): Promise<string> {
|
|
28
|
+
const bytes = await crypto.subtle.digest(
|
|
29
|
+
"SHA-256",
|
|
30
|
+
new TextEncoder().encode(prompt),
|
|
31
|
+
);
|
|
32
|
+
return Array.from(new Uint8Array(bytes)).map((b) =>
|
|
33
|
+
b.toString(16).padStart(2, "0")
|
|
34
|
+
).join("");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function score(result: ProviderResult): ScoreRow {
|
|
38
|
+
const text = result.response_summary;
|
|
39
|
+
const clarity = text.length > 20 ? 4 : 2;
|
|
40
|
+
const completeness = text.length > 120 ? 4 : 3;
|
|
41
|
+
const risk =
|
|
42
|
+
/secret|token|password|launch now|production autonomous/i.test(text)
|
|
43
|
+
? 1
|
|
44
|
+
: 5;
|
|
45
|
+
const instruction_fit = result.schema_valid && result.response_received
|
|
46
|
+
? 4
|
|
47
|
+
: 1;
|
|
48
|
+
const final_score = clarity + completeness + risk + instruction_fit;
|
|
49
|
+
return {
|
|
50
|
+
provider: result.provider,
|
|
51
|
+
model: result.model,
|
|
52
|
+
schema_valid: result.schema_valid,
|
|
53
|
+
response_received: result.response_received,
|
|
54
|
+
clarity,
|
|
55
|
+
completeness,
|
|
56
|
+
risk,
|
|
57
|
+
instruction_fit,
|
|
58
|
+
final_score,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function buildTrace(args: {
|
|
63
|
+
command: string;
|
|
64
|
+
mode: DogfoodTrace["mode"];
|
|
65
|
+
authMode: AuthMode;
|
|
66
|
+
prompt?: string;
|
|
67
|
+
promptContext?: PromptContextTrace;
|
|
68
|
+
results?: ProviderResult[];
|
|
69
|
+
selected?: ScoreRow;
|
|
70
|
+
scores?: ScoreRow[];
|
|
71
|
+
errors?: string[];
|
|
72
|
+
agentChat?: boolean;
|
|
73
|
+
requestedProviderLabel?: string;
|
|
74
|
+
requestedModel?: string;
|
|
75
|
+
providerSelectionHonored?: boolean;
|
|
76
|
+
fallbackUsed?: boolean;
|
|
77
|
+
agentChatTurns?: AgentChatTurn[];
|
|
78
|
+
}): Promise<DogfoodTrace> {
|
|
79
|
+
const responseSummary = args.results?.map((r) =>
|
|
80
|
+
`[${r.provider}/${r.model}] ${r.response_summary}`
|
|
81
|
+
).join("\n");
|
|
82
|
+
const trace: DogfoodTrace = {
|
|
83
|
+
run_id: crypto.randomUUID(),
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
command: args.command,
|
|
86
|
+
mode: args.mode,
|
|
87
|
+
auth_mode: args.authMode,
|
|
88
|
+
provider: args.selected?.provider ?? args.results?.[0]?.provider,
|
|
89
|
+
model: args.selected?.model ?? args.results?.[0]?.model,
|
|
90
|
+
requested_provider_label: args.requestedProviderLabel,
|
|
91
|
+
requested_model: args.requestedModel,
|
|
92
|
+
selected_provider: args.selected?.provider ?? args.results?.[0]?.provider,
|
|
93
|
+
selected_model: args.selected?.model ?? args.results?.[0]?.model,
|
|
94
|
+
provider_selection_honored: args.providerSelectionHonored ?? true,
|
|
95
|
+
fallback_used: args.fallbackUsed ?? false,
|
|
96
|
+
prompt_hash: args.prompt ? await promptHash(args.prompt) : undefined,
|
|
97
|
+
prompt_summary: args.prompt ? summarize(args.prompt, 160) : undefined,
|
|
98
|
+
prompt_context: args.promptContext,
|
|
99
|
+
response_summary: responseSummary
|
|
100
|
+
? summarize(responseSummary, 800)
|
|
101
|
+
: undefined,
|
|
102
|
+
schema_valid: args.results
|
|
103
|
+
? args.results.every((r) => r.schema_valid)
|
|
104
|
+
: true,
|
|
105
|
+
redaction_ok: false,
|
|
106
|
+
credential_value_present: false,
|
|
107
|
+
sensitive_value_present: false,
|
|
108
|
+
selected_route: args.selected
|
|
109
|
+
? {
|
|
110
|
+
provider: args.selected.provider,
|
|
111
|
+
model: args.selected.model,
|
|
112
|
+
final_score: args.selected.final_score,
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
115
|
+
score_table: args.scores,
|
|
116
|
+
agent_chat_turns: args.agentChatTurns,
|
|
117
|
+
errors: args.errors ?? [],
|
|
118
|
+
boundaries: boundaries(args.agentChat),
|
|
119
|
+
};
|
|
120
|
+
trace.redaction_ok = redactionOk(trace);
|
|
121
|
+
return trace;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function writeTrace(
|
|
125
|
+
name: string,
|
|
126
|
+
trace: DogfoodTrace,
|
|
127
|
+
): Promise<string> {
|
|
128
|
+
if (!trace.redaction_ok || !redactionOk(trace)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"QuorumRouter blocked: refusing to write trace that failed redaction checks",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
await Deno.mkdir(OUT_DIR, { recursive: true });
|
|
134
|
+
const path = `${OUT_DIR}/${name}.json`;
|
|
135
|
+
await Deno.writeTextFile(path, `${JSON.stringify(trace, null, 2)}\n`, {
|
|
136
|
+
mode: 0o600,
|
|
137
|
+
});
|
|
138
|
+
await Deno.chmod(path, 0o600).catch(() => undefined);
|
|
139
|
+
return path;
|
|
140
|
+
}
|