@velum-labs/routekit-tool-codex 1.3.2 → 1.5.0
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 +5 -4
- package/dist/driver.js +14 -11
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/install.d.ts +26 -1
- package/dist/install.js +358 -30
- package/dist/launch.d.ts +5 -3
- package/dist/launch.js +69 -20
- package/dist/test/driver.test.js +2 -1
- package/dist/test/install.test.js +471 -1
- package/dist/test/launch.test.js +81 -5
- package/package.json +7 -7
package/dist/launch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { isAbsolute, join } from "node:path";
|
|
5
5
|
import { codexCompatibility, isCodexPickerEligibleModel, reasoningEffortDescriptors } from "@velum-labs/routekit-contracts";
|
|
@@ -7,6 +7,7 @@ import { gatewayOpenAiBaseUrl } from "@velum-labs/routekit-runtime/network";
|
|
|
7
7
|
import { stringify as tomlStringify } from "smol-toml";
|
|
8
8
|
const PROVIDER_ID = "routekit";
|
|
9
9
|
const CATALOG_FILE = "model-catalog.json";
|
|
10
|
+
const ROUTEKIT_CODEX_HOME = join("codex");
|
|
10
11
|
/** Model-agnostic agent prompt, matching the gateway's synthesized entries. */
|
|
11
12
|
const NEUTRAL_INSTRUCTIONS = "You are a coding agent.";
|
|
12
13
|
const PROFILE_DIR = "agent-profiles";
|
|
@@ -71,6 +72,25 @@ export function createIsolatedCodexHome(prefix, env = process.env) {
|
|
|
71
72
|
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
72
73
|
return mkdtempSync(join(parent, prefix));
|
|
73
74
|
}
|
|
75
|
+
/** Stable RouteKit-owned Codex home used by interactive launchers. */
|
|
76
|
+
export function resolveRouteKitCodexHome(env = process.env) {
|
|
77
|
+
const configured = env.CODEX_HOME;
|
|
78
|
+
if (configured !== undefined && configured.length > 0) {
|
|
79
|
+
if (!isAbsolute(configured))
|
|
80
|
+
throw new Error("CODEX_HOME must be an absolute path");
|
|
81
|
+
return configured;
|
|
82
|
+
}
|
|
83
|
+
const routekit = env.ROUTEKIT_HOME;
|
|
84
|
+
if (routekit !== undefined && routekit.length > 0) {
|
|
85
|
+
if (!isAbsolute(routekit))
|
|
86
|
+
throw new Error("ROUTEKIT_HOME must be an absolute path");
|
|
87
|
+
return join(routekit, ROUTEKIT_CODEX_HOME);
|
|
88
|
+
}
|
|
89
|
+
return join(env.HOME ?? env.USERPROFILE ?? homedir(), ".routekit", ROUTEKIT_CODEX_HOME);
|
|
90
|
+
}
|
|
91
|
+
function resolveCodexSourceHome(env = process.env) {
|
|
92
|
+
return join(env.HOME ?? env.USERPROFILE ?? homedir(), ".codex");
|
|
93
|
+
}
|
|
74
94
|
function presetSlug(entry) {
|
|
75
95
|
return typeof entry.slug === "string" && entry.slug.length > 0 ? entry.slug : undefined;
|
|
76
96
|
}
|
|
@@ -127,6 +147,23 @@ function isCodexNativeId(spec, id) {
|
|
|
127
147
|
return (spec.defaultModel === namespaced ||
|
|
128
148
|
spec.models.some((model) => model.id === namespaced || model.aliases?.includes(namespaced) === true));
|
|
129
149
|
}
|
|
150
|
+
function codexServiceTierEntries(capabilities) {
|
|
151
|
+
if (capabilities?.wireShape !== "openai-service-tier")
|
|
152
|
+
return [];
|
|
153
|
+
return capabilities.tiers.map((tier) => ({
|
|
154
|
+
id: tier.id,
|
|
155
|
+
name: tier.name ??
|
|
156
|
+
(tier.id === "priority" || tier.id === "fast"
|
|
157
|
+
? "Fast"
|
|
158
|
+
: tier.id === "flex"
|
|
159
|
+
? "Flex"
|
|
160
|
+
: tier.id),
|
|
161
|
+
description: tier.description ??
|
|
162
|
+
(tier.id === "priority" || tier.id === "fast"
|
|
163
|
+
? "Faster responses with increased usage."
|
|
164
|
+
: `${tier.name ?? tier.id} service tier.`)
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
130
167
|
export function codexCatalogEntries(spec, template, stockModels = [], options = {}) {
|
|
131
168
|
const appendUnlistedStock = options.appendUnlistedStock ?? true;
|
|
132
169
|
const models = catalogModels(spec);
|
|
@@ -142,12 +179,15 @@ export function codexCatalogEntries(spec, template, stockModels = [], options =
|
|
|
142
179
|
// gateway-routed entries: reasoning tiers are replaced by each model's
|
|
143
180
|
// discovered capabilities; `tool_mode` (e.g. "code_mode_only") and
|
|
144
181
|
// `use_responses_lite` alter (or drop entirely) the tool declarations Codex
|
|
145
|
-
// sends; service tiers are
|
|
146
|
-
// `base_instructions` / `model_messages` become the developer message, so a
|
|
182
|
+
// sends; service tiers are replaced by authoritative per-model discovery;
|
|
183
|
+
// and `base_instructions` / `model_messages` become the developer message, so a
|
|
147
184
|
// stock prompt ("You are Codex, an agent based on GPT-5...") would tell
|
|
148
185
|
// every routed model it is GPT-5. Fields are reset to neutral values only
|
|
149
186
|
// when the template carries them, so the output still matches the installed
|
|
150
187
|
// Codex version's required fields.
|
|
188
|
+
const templateHasAdditionalSpeedTiers = Object.hasOwn(template, "additional_speed_tiers");
|
|
189
|
+
const templateHasServiceTiers = Object.hasOwn(template, "service_tiers");
|
|
190
|
+
const templateHasDefaultServiceTier = Object.hasOwn(template, "default_service_tier");
|
|
151
191
|
const { supported_reasoning_levels: _templateLevels, default_reasoning_level: _templateDefault, supports_reasoning_summaries: _templateSummaries, tool_mode: _templateToolMode, default_service_tier: _templateServiceTier, ...neutralTemplate } = template;
|
|
152
192
|
for (const [field, neutral] of [
|
|
153
193
|
["use_responses_lite", false],
|
|
@@ -181,6 +221,9 @@ export function codexCatalogEntries(spec, template, stockModels = [], options =
|
|
|
181
221
|
effort: effort.id,
|
|
182
222
|
description: effort.label
|
|
183
223
|
}));
|
|
224
|
+
const serviceTiers = codexServiceTierEntries(model?.serviceTiers);
|
|
225
|
+
const supportsFast = serviceTiers.some((tier) => tier.id === "priority" || tier.id === "fast");
|
|
226
|
+
const defaultServiceTier = model?.serviceTiers?.defaultTier;
|
|
184
227
|
return {
|
|
185
228
|
...neutralTemplate,
|
|
186
229
|
prefer_websockets: false,
|
|
@@ -197,6 +240,15 @@ export function codexCatalogEntries(spec, template, stockModels = [], options =
|
|
|
197
240
|
...(model?.reasoning?.defaultEffort !== undefined
|
|
198
241
|
? { default_reasoning_level: model.reasoning.defaultEffort }
|
|
199
242
|
: {}),
|
|
243
|
+
...(templateHasAdditionalSpeedTiers || serviceTiers.length > 0
|
|
244
|
+
? { additional_speed_tiers: supportsFast ? ["fast"] : [] }
|
|
245
|
+
: {}),
|
|
246
|
+
...(templateHasServiceTiers || serviceTiers.length > 0 ? { service_tiers: serviceTiers } : {}),
|
|
247
|
+
...(defaultServiceTier !== undefined
|
|
248
|
+
? { default_service_tier: defaultServiceTier }
|
|
249
|
+
: templateHasDefaultServiceTier
|
|
250
|
+
? { default_service_tier: null }
|
|
251
|
+
: {}),
|
|
200
252
|
supports_reasoning_summaries: model?.reasoning?.status === "supported"
|
|
201
253
|
};
|
|
202
254
|
});
|
|
@@ -229,7 +281,8 @@ export function codexPersistentModelCatalogJson(models, template = undefined) {
|
|
|
229
281
|
{
|
|
230
282
|
id: model.id,
|
|
231
283
|
...(model.label !== undefined ? { label: model.label } : {}),
|
|
232
|
-
...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {})
|
|
284
|
+
...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
|
|
285
|
+
...(model.serviceTiers !== undefined ? { serviceTiers: model.serviceTiers } : {})
|
|
233
286
|
}
|
|
234
287
|
];
|
|
235
288
|
});
|
|
@@ -334,6 +387,7 @@ export function codexAgentRoleToml(profile) {
|
|
|
334
387
|
}
|
|
335
388
|
export function codexLaunchConfigToml(spec, modelCatalogPath, roles = []) {
|
|
336
389
|
const lines = [
|
|
390
|
+
`cli_auth_credentials_store = "file"`,
|
|
337
391
|
`model = ${JSON.stringify(codexModelId(spec.defaultModel))}`,
|
|
338
392
|
`model_provider = ${JSON.stringify(PROVIDER_ID)}`
|
|
339
393
|
];
|
|
@@ -352,20 +406,9 @@ export function codexLaunchConfigToml(spec, modelCatalogPath, roles = []) {
|
|
|
352
406
|
}
|
|
353
407
|
return lines.join("\n");
|
|
354
408
|
}
|
|
355
|
-
export function resolveCodexHome(env = process.env) {
|
|
356
|
-
const configured = env.CODEX_HOME;
|
|
357
|
-
if (configured !== undefined && configured.length > 0) {
|
|
358
|
-
if (!isAbsolute(configured))
|
|
359
|
-
throw new Error("CODEX_HOME must be an absolute path");
|
|
360
|
-
return configured;
|
|
361
|
-
}
|
|
362
|
-
return join(env.HOME ?? env.USERPROFILE ?? homedir(), ".codex");
|
|
363
|
-
}
|
|
364
|
-
function tomlValue(value) {
|
|
365
|
-
return JSON.stringify(value);
|
|
366
|
-
}
|
|
367
409
|
function codexLaunchOverrides(spec, catalogPath, roles = []) {
|
|
368
410
|
const values = [
|
|
411
|
+
["cli_auth_credentials_store", "file"],
|
|
369
412
|
["model", codexModelId(spec.defaultModel)],
|
|
370
413
|
["model_provider", PROVIDER_ID],
|
|
371
414
|
[`model_providers.${PROVIDER_ID}.name`, "RouteKit gateway"],
|
|
@@ -387,7 +430,7 @@ function codexLaunchOverrides(spec, catalogPath, roles = []) {
|
|
|
387
430
|
}
|
|
388
431
|
return values.flatMap(([key, value]) => [
|
|
389
432
|
"-c",
|
|
390
|
-
`${key}=${
|
|
433
|
+
`${key}=${JSON.stringify(value)}`
|
|
391
434
|
]);
|
|
392
435
|
}
|
|
393
436
|
function spawnCodex(args, home, cwd, token, dependencies) {
|
|
@@ -406,7 +449,7 @@ function spawnCodex(args, home, cwd, token, dependencies) {
|
|
|
406
449
|
child.once("exit", (code, signal) => resolve(code ?? (signal === null ? 0 : 1)));
|
|
407
450
|
});
|
|
408
451
|
}
|
|
409
|
-
/** Launch against
|
|
452
|
+
/** Launch against a RouteKit-owned Codex home with file-only auth persistence. */
|
|
410
453
|
export async function launchCodex(ctx, deps = {}) {
|
|
411
454
|
if (!ctx.spec.models.some((model) => model.id === ctx.spec.defaultModel)) {
|
|
412
455
|
throw new Error(`Codex startup model "${ctx.spec.defaultModel}" is absent from the advertised catalog`);
|
|
@@ -415,10 +458,16 @@ export async function launchCodex(ctx, deps = {}) {
|
|
|
415
458
|
model.aliases?.includes(ctx.spec.defaultModel) === true)) {
|
|
416
459
|
throw new Error(`Codex startup model "${ctx.spec.defaultModel}" is not in the compatible model picker`);
|
|
417
460
|
}
|
|
418
|
-
const
|
|
461
|
+
const sourceHome = resolveCodexSourceHome(deps.env ?? process.env);
|
|
462
|
+
const home = resolveRouteKitCodexHome(deps.env ?? process.env);
|
|
463
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
464
|
+
chmodSync(home, 0o700);
|
|
465
|
+
const configPath = join(home, "config.toml");
|
|
466
|
+
writeFileSync(configPath, codexLaunchConfigToml(ctx.spec), { mode: 0o600 });
|
|
467
|
+
chmodSync(configPath, 0o600);
|
|
419
468
|
const temp = mkdtempSync(join(tmpdir(), "rk-codex-"));
|
|
420
469
|
ctx.registerDisposer(() => rmSync(temp, { recursive: true, force: true }));
|
|
421
|
-
const stockModels = readCodexHomeModelsCache(
|
|
470
|
+
const stockModels = readCodexHomeModelsCache(sourceHome);
|
|
422
471
|
const template = stockModels[0];
|
|
423
472
|
const catalogPath = template === undefined ? undefined : join(temp, CATALOG_FILE);
|
|
424
473
|
if (catalogPath !== undefined && template !== undefined) {
|
package/dist/test/driver.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { chmodSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { test } from "node:test";
|
|
@@ -161,6 +161,7 @@ test("gateway-routed codex homes are owned by their harness instance", async ()
|
|
|
161
161
|
assert.notEqual(firstHome, secondHome);
|
|
162
162
|
assert.ok(firstHome.startsWith(join(userHome, ".cache", "routekit", "codex")));
|
|
163
163
|
assert.ok(secondHome.startsWith(join(userHome, ".cache", "routekit", "codex")));
|
|
164
|
+
assert.equal(readFileSync(join(firstHome, "config.toml"), "utf8"), 'cli_auth_credentials_store = "file"\n');
|
|
164
165
|
await first.dispose();
|
|
165
166
|
assert.equal(existsSync(firstHome), false);
|
|
166
167
|
assert.equal(existsSync(secondHome), true);
|