@velum-labs/routekit-tool-codex 1.4.0 → 1.5.1

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/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 a stock-model billing offer; and
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}=${typeof value === "string" ? tomlValue(value) : String(value)}`
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 the user's real Codex home; only generated catalog data is temporary. */
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 home = resolveCodexHome(deps.env ?? process.env);
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(home);
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) {
@@ -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);