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,224 @@
|
|
|
1
|
+
import type { ModelInventoryEntry, ProviderResult } from "./schema.ts";
|
|
2
|
+
import { redact, summarize } from "./redact.ts";
|
|
3
|
+
|
|
4
|
+
export function buildWrapperArgs(
|
|
5
|
+
entry: ModelInventoryEntry,
|
|
6
|
+
prompt: string,
|
|
7
|
+
outPath: string,
|
|
8
|
+
): string[] {
|
|
9
|
+
const args = (entry.args_template ?? []).map((arg) =>
|
|
10
|
+
arg === "__PROMPT__"
|
|
11
|
+
? prompt
|
|
12
|
+
: arg === "__CWD__"
|
|
13
|
+
? Deno.cwd()
|
|
14
|
+
: arg === "__OUT__"
|
|
15
|
+
? outPath
|
|
16
|
+
: arg
|
|
17
|
+
);
|
|
18
|
+
if (entry.command === "grok" && entry.invocation_model) {
|
|
19
|
+
if (entry.invocation_model === "grok-build") {
|
|
20
|
+
const withoutPlanMode: string[] = [];
|
|
21
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
22
|
+
if (args[index] === "--permission-mode" && args[index + 1] === "plan") {
|
|
23
|
+
index += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
withoutPlanMode.push(args[index]);
|
|
27
|
+
}
|
|
28
|
+
return [
|
|
29
|
+
"--model",
|
|
30
|
+
entry.invocation_model,
|
|
31
|
+
...withoutPlanMode,
|
|
32
|
+
"--permission-mode",
|
|
33
|
+
"bypassPermissions",
|
|
34
|
+
"--deny",
|
|
35
|
+
"*",
|
|
36
|
+
"--no-subagents",
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
return ["--model", entry.invocation_model, ...args];
|
|
40
|
+
}
|
|
41
|
+
return args;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function outputWithTimeout(
|
|
45
|
+
child: Deno.ChildProcess,
|
|
46
|
+
timeoutMs: number,
|
|
47
|
+
): Promise<Deno.CommandOutput> {
|
|
48
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
49
|
+
let killId: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
try {
|
|
51
|
+
return await Promise.race([
|
|
52
|
+
child.output(),
|
|
53
|
+
new Promise<Deno.CommandOutput>((_, reject) => {
|
|
54
|
+
timeoutId = setTimeout(() => {
|
|
55
|
+
try {
|
|
56
|
+
child.kill("SIGTERM");
|
|
57
|
+
} catch { /* best effort */ }
|
|
58
|
+
killId = setTimeout(() => {
|
|
59
|
+
try {
|
|
60
|
+
child.kill("SIGKILL");
|
|
61
|
+
} catch { /* best effort */ }
|
|
62
|
+
reject(
|
|
63
|
+
new Error(
|
|
64
|
+
"QuorumRouter blocked: wrapper timed out after 120000ms",
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
}, 2_000);
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
} finally {
|
|
72
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
73
|
+
if (killId) clearTimeout(killId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function safeWrapperEnv(): Record<string, string> {
|
|
78
|
+
const allowed = [
|
|
79
|
+
"PATH",
|
|
80
|
+
"HOME",
|
|
81
|
+
"TMPDIR",
|
|
82
|
+
"TMP",
|
|
83
|
+
"TEMP",
|
|
84
|
+
"XDG_CONFIG_HOME",
|
|
85
|
+
"XDG_CACHE_HOME",
|
|
86
|
+
"LANG",
|
|
87
|
+
"LC_ALL",
|
|
88
|
+
"TERM",
|
|
89
|
+
];
|
|
90
|
+
const env: Record<string, string> = {};
|
|
91
|
+
for (const name of allowed) {
|
|
92
|
+
const value = Deno.env.get(name);
|
|
93
|
+
if (value) env[name] = value;
|
|
94
|
+
}
|
|
95
|
+
return env;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const SHARED_RUNTIME_NOISE = [
|
|
99
|
+
/AuthRequiredError/i,
|
|
100
|
+
/rmcp::transport::worker/i,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const FATAL_CLI_NOISE = [
|
|
104
|
+
...SHARED_RUNTIME_NOISE,
|
|
105
|
+
/authentication required/i,
|
|
106
|
+
/not logged in/i,
|
|
107
|
+
/^ERROR\s/mi,
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const STDOUT_FATAL_CLI_DIAGNOSTIC_LINE = [
|
|
111
|
+
/^\s*AuthRequiredError\s*$/i,
|
|
112
|
+
/rmcp::transport::worker/i,
|
|
113
|
+
/^\s*authentication required\s*$/i,
|
|
114
|
+
/^\s*not logged in\s*$/i,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const BANNER_OR_RUNTIME_LINE = [
|
|
118
|
+
...SHARED_RUNTIME_NOISE,
|
|
119
|
+
/^Reading additional input from stdin\.?$/i,
|
|
120
|
+
/^OpenAI Codex v\S+/i,
|
|
121
|
+
/^ERROR\s/i,
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
function stripRuntimeNoise(text: string): string {
|
|
125
|
+
return text.split(/\r?\n/).filter((line) => {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
return !BANNER_OR_RUNTIME_LINE.some((pattern) => pattern.test(trimmed));
|
|
128
|
+
}).join("\n").trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function extractUsableWrapperContent(args: {
|
|
132
|
+
provider: string;
|
|
133
|
+
model: string;
|
|
134
|
+
fileOutput: string;
|
|
135
|
+
stdout: string;
|
|
136
|
+
stderr: string;
|
|
137
|
+
}): string {
|
|
138
|
+
const stderrFatal = FATAL_CLI_NOISE.find((pattern) =>
|
|
139
|
+
pattern.test(args.stderr)
|
|
140
|
+
);
|
|
141
|
+
const stdoutFatal = args.stdout.split(/\r?\n/).find((line) =>
|
|
142
|
+
STDOUT_FATAL_CLI_DIAGNOSTIC_LINE.some((pattern) => pattern.test(line))
|
|
143
|
+
);
|
|
144
|
+
if (stderrFatal || stdoutFatal) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`QuorumRouter blocked: ${args.provider}/${args.model} emitted CLI runtime/auth error noise`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const candidate of [args.fileOutput, args.stdout]) {
|
|
151
|
+
const cleaned = stripRuntimeNoise(candidate);
|
|
152
|
+
if (cleaned.length > 0) return cleaned;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
throw new Error(
|
|
156
|
+
`QuorumRouter blocked: ${args.provider}/${args.model} returned no usable model answer after sanitizing CLI banner/runtime output`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function callWrapper(
|
|
161
|
+
entry: ModelInventoryEntry,
|
|
162
|
+
prompt: string,
|
|
163
|
+
): Promise<ProviderResult> {
|
|
164
|
+
if (!entry.command) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`QuorumRouter blocked: ${entry.provider} has no command`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
await Deno.mkdir("out", {
|
|
170
|
+
recursive: true,
|
|
171
|
+
});
|
|
172
|
+
const outPath = `out/tmp-${crypto.randomUUID()}.txt`;
|
|
173
|
+
try {
|
|
174
|
+
const runWrapper = () =>
|
|
175
|
+
new Deno.Command(entry.command!, {
|
|
176
|
+
args: buildWrapperArgs(entry, prompt, outPath),
|
|
177
|
+
cwd: Deno.env.get("TMPDIR") || "/tmp",
|
|
178
|
+
clearEnv: true,
|
|
179
|
+
env: safeWrapperEnv(),
|
|
180
|
+
stdin: "null",
|
|
181
|
+
stdout: "piped",
|
|
182
|
+
stderr: "piped",
|
|
183
|
+
}).spawn();
|
|
184
|
+
let output = await outputWithTimeout(runWrapper(), 120_000);
|
|
185
|
+
if (
|
|
186
|
+
output.code === 141 && output.stdout.length === 0 &&
|
|
187
|
+
output.stderr.length === 0
|
|
188
|
+
) {
|
|
189
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
190
|
+
output = await outputWithTimeout(runWrapper(), 120_000);
|
|
191
|
+
}
|
|
192
|
+
const stdout = new TextDecoder().decode(output.stdout);
|
|
193
|
+
const stderr = new TextDecoder().decode(output.stderr);
|
|
194
|
+
const fileOutput = await Deno.readTextFile(outPath).catch(() => "");
|
|
195
|
+
if (output.code !== 0) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`QuorumRouter blocked: ${entry.provider}/${entry.model} exited ${output.code}: ${
|
|
198
|
+
summarize(redact(stderr || stdout || fileOutput), 400)
|
|
199
|
+
}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const content = extractUsableWrapperContent({
|
|
203
|
+
provider: entry.provider,
|
|
204
|
+
model: entry.model,
|
|
205
|
+
fileOutput,
|
|
206
|
+
stdout,
|
|
207
|
+
stderr,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
provider: entry.provider,
|
|
211
|
+
model: entry.model,
|
|
212
|
+
model_id: entry.model_id,
|
|
213
|
+
source: entry.source,
|
|
214
|
+
command: entry.command,
|
|
215
|
+
listed_models: entry.listed_models,
|
|
216
|
+
response_received: true,
|
|
217
|
+
schema_valid: true,
|
|
218
|
+
response_summary: summarize(content),
|
|
219
|
+
raw_content: content,
|
|
220
|
+
};
|
|
221
|
+
} finally {
|
|
222
|
+
await Deno.remove(outPath).catch(() => undefined);
|
|
223
|
+
}
|
|
224
|
+
}
|