@velum-labs/routekit-tool-codex 0.16.8 → 0.17.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 CHANGED
@@ -18,3 +18,10 @@ import { codexTool } from "@velum-labs/routekit-tool-codex";
18
18
 
19
19
  - CLI reference: [../../docs/cli.md](../../docs/cli.md)
20
20
  - Maintainer reference: [../../docs/typescript-reference.md](../../docs/typescript-reference.md)
21
+
22
+ ## Native client ownership
23
+
24
+ The launcher uses the user's normal `CODEX_HOME`, applies gateway routing as
25
+ process-scoped configuration, and keeps credentials in the environment. RouteKit
26
+ does not observe, store, resume, or delete Codex sessions; use Codex's native
27
+ history and commands directly.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { ToolIntegration } from "@velum-labs/routekit-tools";
2
2
  export declare const codexTool: ToolIntegration;
3
- export { codexDriverConfigSchema, createCodexDriver } from "./driver.js";
4
3
  export type { CodexDriverConfig } from "./driver.js";
5
- export { codexAgentRoles, codexAgentRoleToml, codexAuthPath, codexCatalogEntries, codexLaunchConfigToml, codexListedStockSlugs, codexModelCatalogJson, codexProfileFiles, codexProfileFileToml, hasCodexLogin, isCodexConfigFailure, launchCodex, readCodexCatalogTemplate, readCodexModelsCache } from "./launch.js";
6
- export type { CodexAgentRole, CodexModelPreset } from "./launch.js";
7
- export { codexIntegrationBlock, installCodexIntegration, uninstallCodexIntegration } from "./install.js";
4
+ export { codexDriverConfigSchema, createCodexDriver } from "./driver.js";
8
5
  export type { CodexInstallInput, CodexInstallOwner, CodexInstallProfile, CodexInstallResult } from "./install.js";
6
+ export { codexIntegrationBlock, codexIntegrationConfigPath, installCodexIntegration, uninstallCodexIntegration } from "./install.js";
7
+ export type { CodexAgentRole, CodexModelPreset } from "./launch.js";
8
+ export { codexAgentRoles, codexAgentRoleToml, codexAuthPath, codexCatalogEntries, codexLaunchConfigToml, codexListedStockSlugs, codexModelCatalogJson, codexPersistentModelCatalogJson, codexProfileFiles, codexProfileFileToml, hasCodexLogin, isCodexConfigFailure, launchCodex, readCodexCatalogTemplate, readCodexHomeModelsCache, readCodexModelsCache } from "./launch.js";
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ export const codexTool = {
9
9
  binary: "codex",
10
10
  packageName: "@velum-labs/routekit-tool-codex",
11
11
  installHint: "install the Codex CLI: https://github.com/openai/codex",
12
- authSummary: "Codex uses an ephemeral gateway-backed provider.",
12
+ authSummary: "Codex uses RouteKit's OpenAI-compatible gateway provider.",
13
13
  setupSnippet: ({ gatewayUrl, model = "gateway-model" }) => codexLaunchConfigToml({
14
14
  gatewayUrl,
15
15
  defaultModel: model
@@ -36,5 +36,5 @@ export const codexTool = {
36
36
  }
37
37
  };
38
38
  export { codexDriverConfigSchema, createCodexDriver } from "./driver.js";
39
- export { codexAgentRoles, codexAgentRoleToml, codexAuthPath, codexCatalogEntries, codexLaunchConfigToml, codexListedStockSlugs, codexModelCatalogJson, codexProfileFiles, codexProfileFileToml, hasCodexLogin, isCodexConfigFailure, launchCodex, readCodexCatalogTemplate, readCodexModelsCache } from "./launch.js";
40
- export { codexIntegrationBlock, installCodexIntegration, uninstallCodexIntegration } from "./install.js";
39
+ export { codexIntegrationBlock, codexIntegrationConfigPath, installCodexIntegration, uninstallCodexIntegration } from "./install.js";
40
+ export { codexAgentRoles, codexAgentRoleToml, codexAuthPath, codexCatalogEntries, codexLaunchConfigToml, codexListedStockSlugs, codexModelCatalogJson, codexPersistentModelCatalogJson, codexProfileFiles, codexProfileFileToml, hasCodexLogin, isCodexConfigFailure, launchCodex, readCodexCatalogTemplate, readCodexHomeModelsCache, readCodexModelsCache } from "./launch.js";
package/dist/install.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import type { ModelReasoningCapabilities } from "@velum-labs/routekit-contracts";
1
2
  export type CodexInstallProfile = {
2
3
  modelId: string;
3
4
  /** Safe Codex profile selector; defaults to `modelId`. */
4
5
  profileId?: string;
5
6
  description?: string;
7
+ reasoning?: ModelReasoningCapabilities;
6
8
  };
7
9
  export type CodexInstallOwner = {
8
10
  id: string;
@@ -20,9 +22,11 @@ export type CodexInstallInput = {
20
22
  };
21
23
  export type CodexInstallResult = {
22
24
  configPath: string;
25
+ catalogPath: string;
23
26
  action: "installed" | "updated";
24
27
  profiles: string[];
25
28
  };
29
+ export declare function codexIntegrationConfigPath(codexHome: string | undefined): string;
26
30
  /** Serialize one additive, owner-marked Codex provider block. */
27
31
  export declare function codexIntegrationBlock(input: CodexInstallInput): string;
28
32
  export declare function installCodexIntegration(input: CodexInstallInput): CodexInstallResult;
package/dist/install.js CHANGED
@@ -4,8 +4,8 @@ import { dirname, join } from "node:path";
4
4
  import { parse as tomlParse, stringify as tomlStringify } from "smol-toml";
5
5
  import { SUBSCRIPTIONS } from "@velum-labs/routekit-registry";
6
6
  import { trimTrailingSlashes } from "@velum-labs/routekit-runtime";
7
- import { codexProfileFileToml } from "./launch.js";
8
- function codexConfigPath(codexHome) {
7
+ import { codexPersistentModelCatalogJson, codexProfileFileToml, readCodexHomeModelsCache } from "./launch.js";
8
+ export function codexIntegrationConfigPath(codexHome) {
9
9
  if (codexHome !== undefined)
10
10
  return join(codexHome, "config.toml");
11
11
  const registryPath = SUBSCRIPTIONS.codex.configPath ?? "~/.codex/config.toml";
@@ -17,6 +17,15 @@ function marker(ownerId, edge) {
17
17
  function profileFilesComment(ownerId) {
18
18
  return `# ${ownerId}-profile-files:`;
19
19
  }
20
+ function catalogFileComment(ownerId) {
21
+ return `# ${ownerId}-catalog-file:`;
22
+ }
23
+ function catalogFileName(ownerId) {
24
+ if (!/^[A-Za-z0-9_-]+$/.test(ownerId)) {
25
+ throw new Error(`Codex integration owner id is not path-safe: ${JSON.stringify(ownerId)}`);
26
+ }
27
+ return `.${ownerId}-model-catalog.json`;
28
+ }
20
29
  function profileSelector(profile) {
21
30
  return profile.profileId ?? profile.modelId;
22
31
  }
@@ -36,13 +45,16 @@ export function codexIntegrationBlock(input) {
36
45
  const begin = marker(input.owner.id, "begin");
37
46
  const end = marker(input.owner.id, "end");
38
47
  const filesComment = profileFilesComment(input.owner.id);
48
+ const catalogComment = catalogFileComment(input.owner.id);
49
+ const catalogFile = catalogFileName(input.owner.id);
39
50
  const body = tomlStringify({
40
51
  model_providers: {
41
52
  [input.owner.providerId]: {
42
53
  name: `${input.owner.displayName} gateway`,
43
54
  base_url: `${base}/v1`,
44
55
  wire_api: "responses",
45
- requires_openai_auth: false
56
+ requires_openai_auth: false,
57
+ env_key: "ROUTEKIT_GATEWAY_TOKEN"
46
58
  }
47
59
  }
48
60
  });
@@ -54,6 +66,7 @@ export function codexIntegrationBlock(input) {
54
66
  `# Then launch: codex --profile ${input.profiles[0] !== undefined ? profileSelector(input.profiles[0]) : "gateway-model"}`,
55
67
  ...input.profiles.map((profile) => `# codex --profile ${profileSelector(profile)}${profile.description !== undefined ? ` (${profile.description})` : ""}`),
56
68
  `${filesComment} ${input.profiles.map(profileFileName).join(" ")}`,
69
+ `${catalogComment} ${catalogFile}`,
57
70
  "",
58
71
  body.trimEnd(),
59
72
  "",
@@ -73,6 +86,19 @@ function ownedProfileFiles(managed, codexHome, ownerId) {
73
86
  .filter((name) => name.endsWith(".config.toml") && !name.includes("/") && !name.includes("\\"))
74
87
  .map((name) => join(codexHome, name));
75
88
  }
89
+ function ownedCatalogFile(managed, codexHome, ownerId) {
90
+ if (managed === undefined)
91
+ return undefined;
92
+ const prefix = catalogFileComment(ownerId);
93
+ const line = managed.split("\n").find((entry) => entry.startsWith(prefix));
94
+ if (line === undefined)
95
+ return undefined;
96
+ const file = line.slice(prefix.length).trim();
97
+ if (file.length === 0 || file.includes("/") || file.includes("\\") || file !== catalogFileName(ownerId)) {
98
+ return undefined;
99
+ }
100
+ return join(codexHome, file);
101
+ }
76
102
  function splitManagedBlock(content, ownerId) {
77
103
  const beginMarker = marker(ownerId, "begin");
78
104
  const endMarker = marker(ownerId, "end");
@@ -136,12 +162,17 @@ function removeOwnedProfileFile(path, ownerId) {
136
162
  export function installCodexIntegration(input) {
137
163
  if (input.profiles.length === 0)
138
164
  throw new Error("at least one Codex profile is required");
139
- const configPath = codexConfigPath(input.codexHome);
165
+ const configPath = codexIntegrationConfigPath(input.codexHome);
140
166
  const codexHome = dirname(configPath);
141
167
  const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
142
168
  const { before, managed, after } = splitManagedBlock(existing, input.owner.id);
143
169
  const outside = parseTomlOrThrow(`${normalize(before)}\n${normalize(after)}`, `your Codex config (${configPath})`);
144
170
  assertNoConflicts(outside, input);
171
+ const catalogPath = join(codexHome, catalogFileName(input.owner.id));
172
+ if (managed === undefined && existsSync(catalogPath)) {
173
+ throw new Error(`refusing to overwrite an existing RouteKit catalog file: ${catalogPath}; ` +
174
+ `move it aside, then rerun \`${input.owner.installCommand}\``);
175
+ }
145
176
  const block = codexIntegrationBlock(input);
146
177
  const head = normalize(before);
147
178
  const tail = normalize(after);
@@ -152,6 +183,10 @@ export function installCodexIntegration(input) {
152
183
  throw new Error("internal error: the assembled Codex config lost its managed provider block");
153
184
  }
154
185
  mkdirSync(codexHome, { recursive: true });
186
+ writeFileSync(catalogPath, codexPersistentModelCatalogJson(input.profiles.map((profile) => ({
187
+ id: profile.modelId,
188
+ ...(profile.reasoning !== undefined ? { reasoning: profile.reasoning } : {})
189
+ })), readCodexHomeModelsCache(codexHome)[0]), { mode: 0o600 });
155
190
  const nextFiles = new Set(input.profiles.map((profile) => join(codexHome, profileFileName(profile))));
156
191
  for (const stale of ownedProfileFiles(managed, codexHome, input.owner.id)) {
157
192
  if (!nextFiles.has(stale))
@@ -159,16 +194,17 @@ export function installCodexIntegration(input) {
159
194
  }
160
195
  writeFileSync(configPath, next);
161
196
  for (const profile of input.profiles) {
162
- writeFileSync(join(codexHome, profileFileName(profile)), `# Managed by ${input.owner.id}\n${codexProfileFileToml(profile.modelId, input.owner.providerId)}`);
197
+ writeFileSync(join(codexHome, profileFileName(profile)), `# Managed by ${input.owner.id}\n${codexProfileFileToml(profile.modelId, input.owner.providerId, catalogPath)}`, { mode: 0o600 });
163
198
  }
164
199
  return {
165
200
  configPath,
201
+ catalogPath,
166
202
  action: managed !== undefined ? "updated" : "installed",
167
203
  profiles: input.profiles.map(profileSelector)
168
204
  };
169
205
  }
170
206
  export function uninstallCodexIntegration(input) {
171
- const configPath = codexConfigPath(input.codexHome);
207
+ const configPath = codexIntegrationConfigPath(input.codexHome);
172
208
  if (!existsSync(configPath))
173
209
  return { configPath, removed: false };
174
210
  const existing = readFileSync(configPath, "utf8");
@@ -178,6 +214,9 @@ export function uninstallCodexIntegration(input) {
178
214
  for (const owned of ownedProfileFiles(managed, dirname(configPath), input.ownerId)) {
179
215
  removeOwnedProfileFile(owned, input.ownerId);
180
216
  }
217
+ const catalogPath = ownedCatalogFile(managed, dirname(configPath), input.ownerId);
218
+ if (catalogPath !== undefined)
219
+ rmSync(catalogPath, { force: true });
181
220
  const head = normalize(before);
182
221
  const tail = normalize(after);
183
222
  writeFileSync(configPath, `${head}${head.length > 0 && tail.length > 0 ? "\n" : ""}${tail}`);
package/dist/launch.d.ts CHANGED
@@ -1,8 +1,17 @@
1
+ import { spawn } from "node:child_process";
2
+ import { type ModelReasoningCapabilities } from "@velum-labs/routekit-contracts";
1
3
  import type { AgentProfile, ToolLaunchContext, ToolLaunchSpec } from "@velum-labs/routekit-tools";
2
4
  export type CodexModelPreset = Record<string, unknown>;
5
+ /** Metadata persisted beside a RouteKit-owned Codex profile. */
6
+ export type CodexPersistentCatalogModel = {
7
+ id: string;
8
+ label?: string;
9
+ reasoning?: ModelReasoningCapabilities;
10
+ };
3
11
  export declare function isCodexConfigFailure(code: number, stderr: string): boolean;
4
12
  export declare function tomlKey(name: string): string;
5
13
  export declare function readCodexModelsCache(home?: string): CodexModelPreset[];
14
+ export declare function readCodexHomeModelsCache(codexHome: string): CodexModelPreset[];
6
15
  export declare function readCodexCatalogTemplate(home?: string): CodexModelPreset | undefined;
7
16
  export declare function codexAuthPath(home?: string): string;
8
17
  export declare function hasCodexLogin(home?: string): boolean;
@@ -21,7 +30,13 @@ export declare function codexCatalogEntries(spec: Pick<ToolLaunchSpec, "defaultM
21
30
  export declare function codexModelCatalogJson(spec: Pick<ToolLaunchSpec, "defaultModel" | "models">, template: CodexModelPreset, stockModels?: readonly CodexModelPreset[], options?: {
22
31
  appendUnlistedStock?: boolean;
23
32
  }): string;
24
- export declare function codexProfileFileToml(model: string, provider?: string): string;
33
+ /**
34
+ * Serialize a versioned ModelInfo catalog for persistent profiles. It uses a
35
+ * target Codex home's stock metadata when available, and a complete fallback
36
+ * for first-run homes that have not downloaded `models_cache.json` yet.
37
+ */
38
+ export declare function codexPersistentModelCatalogJson(models: readonly CodexPersistentCatalogModel[], template?: CodexModelPreset | undefined): string;
39
+ export declare function codexProfileFileToml(model: string, provider?: string, modelCatalogPath?: string): string;
25
40
  export declare function codexProfileFiles(home: string, models: readonly string[], provider?: string): string[];
26
41
  export type CodexAgentRole = AgentProfile & {
27
42
  configPath: string;
@@ -29,4 +44,10 @@ export type CodexAgentRole = AgentProfile & {
29
44
  export declare function codexAgentRoles(home: string, profiles: readonly AgentProfile[]): CodexAgentRole[];
30
45
  export declare function codexAgentRoleToml(profile: AgentProfile): string;
31
46
  export declare function codexLaunchConfigToml(spec: Pick<ToolLaunchSpec, "gatewayUrl" | "defaultModel" | "reasoning" | "auth">, modelCatalogPath?: string, roles?: readonly CodexAgentRole[]): string;
32
- export declare function launchCodex(ctx: ToolLaunchContext): Promise<number>;
47
+ export type CodexLaunchDependencies = {
48
+ spawnProcess?: typeof spawn;
49
+ env?: Record<string, string | undefined>;
50
+ };
51
+ export declare function resolveCodexHome(env?: Record<string, string | undefined>): string;
52
+ /** Launch against the user's real Codex home; only generated catalog data is temporary. */
53
+ export declare function launchCodex(ctx: ToolLaunchContext, deps?: CodexLaunchDependencies): Promise<number>;
package/dist/launch.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { stringify as tomlStringify } from "smol-toml";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { isAbsolute, join } from "node:path";
6
5
  import { isCodexPickerEligibleModel, reasoningEffortDescriptors } from "@velum-labs/routekit-contracts";
7
6
  import { trimTrailingSlashes } from "@velum-labs/routekit-runtime";
7
+ import { stringify as tomlStringify } from "smol-toml";
8
8
  const PROVIDER_ID = "routekit";
9
9
  const CATALOG_FILE = "model-catalog.json";
10
10
  /** Model-agnostic agent prompt, matching the gateway's synthesized entries. */
@@ -29,9 +29,9 @@ export function tomlKey(name) {
29
29
  function modelsCachePath(home) {
30
30
  return join(home, ".codex", "models_cache.json");
31
31
  }
32
- export function readCodexModelsCache(home = homedir()) {
32
+ function readCodexModelsCachePath(path) {
33
33
  try {
34
- const parsed = JSON.parse(readFileSync(modelsCachePath(home), "utf8"));
34
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
35
35
  return Array.isArray(parsed.models)
36
36
  ? parsed.models.filter((entry) => entry !== null && typeof entry === "object")
37
37
  : [];
@@ -40,6 +40,12 @@ export function readCodexModelsCache(home = homedir()) {
40
40
  return [];
41
41
  }
42
42
  }
43
+ export function readCodexModelsCache(home = homedir()) {
44
+ return readCodexModelsCachePath(modelsCachePath(home));
45
+ }
46
+ export function readCodexHomeModelsCache(codexHome) {
47
+ return readCodexModelsCachePath(join(codexHome, "models_cache.json"));
48
+ }
43
49
  export function readCodexCatalogTemplate(home = homedir()) {
44
50
  return readCodexModelsCache(home)[0];
45
51
  }
@@ -79,9 +85,7 @@ export function codexListedStockSlugs(home = homedir()) {
79
85
  });
80
86
  }
81
87
  function codexModelId(modelId) {
82
- return modelId.startsWith("codex/")
83
- ? modelId.slice("codex/".length)
84
- : modelId;
88
+ return modelId.startsWith("codex/") ? modelId.slice("codex/".length) : modelId;
85
89
  }
86
90
  function catalogModels(spec) {
87
91
  return spec.models.filter((model) => model.id === spec.defaultModel ||
@@ -90,10 +94,10 @@ function catalogModels(spec) {
90
94
  }
91
95
  function catalogIds(spec) {
92
96
  return [
93
- ...new Set([spec.defaultModel, ...catalogModels(spec).flatMap((model) => [
94
- model.id,
95
- ...(model.aliases ?? [])
96
- ])].map(codexModelId))
97
+ ...new Set([
98
+ spec.defaultModel,
99
+ ...catalogModels(spec).flatMap((model) => [model.id, ...(model.aliases ?? [])])
100
+ ].map(codexModelId))
97
101
  ];
98
102
  }
99
103
  /** True when a bare catalog id was projected from a `codex/`-namespaced model. */
@@ -189,8 +193,93 @@ export function codexCatalogEntries(spec, template, stockModels = [], options =
189
193
  export function codexModelCatalogJson(spec, template, stockModels = [], options = {}) {
190
194
  return JSON.stringify({ models: codexCatalogEntries(spec, template, stockModels, options) }, null, 2);
191
195
  }
192
- export function codexProfileFileToml(model, provider = PROVIDER_ID) {
193
- return `${tomlStringify({ model, model_provider: provider }).trimEnd()}\n`;
196
+ /**
197
+ * Serialize a versioned ModelInfo catalog for persistent profiles. It uses a
198
+ * target Codex home's stock metadata when available, and a complete fallback
199
+ * for first-run homes that have not downloaded `models_cache.json` yet.
200
+ */
201
+ export function codexPersistentModelCatalogJson(models, template = undefined) {
202
+ const unique = new Set();
203
+ const catalogModels = models.flatMap((model) => {
204
+ if (model.id.length === 0 || unique.has(model.id))
205
+ return [];
206
+ unique.add(model.id);
207
+ return [
208
+ {
209
+ id: model.id,
210
+ ...(model.label !== undefined ? { label: model.label } : {}),
211
+ ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {})
212
+ }
213
+ ];
214
+ });
215
+ const defaultModel = catalogModels[0]?.id;
216
+ if (defaultModel === undefined)
217
+ throw new Error("at least one Codex catalog model is required");
218
+ // A normal Codex home has a version-matched stock entry we can use as the
219
+ // schema template. This fallback has the full required ModelInfo shape for
220
+ // first-run/empty homes, then codexCatalogEntries neutralizes the fields
221
+ // that would otherwise leak stock-model behavior into a routed model.
222
+ const fallbackTemplate = {
223
+ slug: "routekit-template",
224
+ display_name: "RouteKit",
225
+ description: "Gateway-routed model.",
226
+ default_reasoning_level: "medium",
227
+ supported_reasoning_levels: [],
228
+ shell_type: "shell_command",
229
+ visibility: "list",
230
+ supported_in_api: true,
231
+ priority: 0,
232
+ additional_speed_tiers: [],
233
+ service_tiers: [],
234
+ default_service_tier: "",
235
+ availability_nux: null,
236
+ upgrade: null,
237
+ base_instructions: NEUTRAL_INSTRUCTIONS,
238
+ model_messages: { instructions_template: NEUTRAL_INSTRUCTIONS },
239
+ include_skills_usage_instructions: false,
240
+ default_reasoning_summary: "none",
241
+ support_verbosity: true,
242
+ default_verbosity: "low",
243
+ apply_patch_tool_type: "freeform",
244
+ web_search_tool_type: "text_and_image",
245
+ truncation_policy: { mode: "tokens", limit: 10_000 },
246
+ supports_parallel_tool_calls: true,
247
+ supports_image_detail_original: true,
248
+ context_window: 272_000,
249
+ max_context_window: 272_000,
250
+ comp_hash: "3000",
251
+ effective_context_window_percent: 95,
252
+ experimental_supported_tools: [],
253
+ input_modalities: ["text", "image"],
254
+ supports_search_tool: true,
255
+ use_responses_lite: false,
256
+ tool_mode: "code_mode",
257
+ multi_agent_version: "v2"
258
+ };
259
+ const generated = codexCatalogEntries({ defaultModel, models: catalogModels }, template ?? fallbackTemplate, [], { appendUnlistedStock: false });
260
+ // The launcher strips the `codex/` namespace because it supplies a runtime
261
+ // provider override. Persistent profiles deliberately retain the gateway's
262
+ // public model id, so restore that exact spelling in their metadata too.
263
+ const entries = catalogModels.map((model, priority) => {
264
+ const generatedEntry = generated.find((entry) => entry.slug === codexModelId(model.id));
265
+ if (generatedEntry === undefined) {
266
+ throw new Error(`persistent Codex catalog omitted model ${JSON.stringify(model.id)}`);
267
+ }
268
+ return {
269
+ ...generatedEntry,
270
+ slug: model.id,
271
+ display_name: model.label ?? model.id,
272
+ priority
273
+ };
274
+ });
275
+ return JSON.stringify({ models: entries }, null, 2);
276
+ }
277
+ export function codexProfileFileToml(model, provider = PROVIDER_ID, modelCatalogPath) {
278
+ return `${tomlStringify({
279
+ model,
280
+ model_provider: provider,
281
+ ...(modelCatalogPath !== undefined ? { model_catalog_json: modelCatalogPath } : {})
282
+ }).trimEnd()}\n`;
194
283
  }
195
284
  export function codexProfileFiles(home, models, provider = PROVIDER_ID) {
196
285
  const written = [];
@@ -233,9 +322,7 @@ export function codexLaunchConfigToml(spec, modelCatalogPath, roles = []) {
233
322
  if (modelCatalogPath !== undefined) {
234
323
  lines.push(`model_catalog_json = ${JSON.stringify(modelCatalogPath)}`);
235
324
  }
236
- lines.push("", `[model_providers.${PROVIDER_ID}]`, `name = "RouteKit gateway"`, `base_url = ${JSON.stringify(`${trimTrailingSlashes(spec.gatewayUrl)}/v1`)}`, `wire_api = "responses"`, `requires_openai_auth = false`, ...(spec.auth?.token !== undefined
237
- ? [`env_key = "ROUTEKIT_GATEWAY_TOKEN"`]
238
- : []), "");
325
+ lines.push("", `[model_providers.${PROVIDER_ID}]`, `name = "RouteKit gateway"`, `base_url = ${JSON.stringify(`${trimTrailingSlashes(spec.gatewayUrl)}/v1`)}`, `wire_api = "responses"`, `requires_openai_auth = false`, ...(spec.auth?.token !== undefined ? [`env_key = "ROUTEKIT_GATEWAY_TOKEN"`] : []), "");
239
326
  if (roles.length > 0) {
240
327
  lines.push("[features]", "multi_agent = true", "", "[agents]", "max_depth = 1", "");
241
328
  for (const role of roles) {
@@ -244,66 +331,78 @@ export function codexLaunchConfigToml(spec, modelCatalogPath, roles = []) {
244
331
  }
245
332
  return lines.join("\n");
246
333
  }
247
- function spawnCodex(args, home, cwd, token) {
334
+ export function resolveCodexHome(env = process.env) {
335
+ const configured = env.CODEX_HOME;
336
+ if (configured !== undefined && configured.length > 0) {
337
+ if (!isAbsolute(configured))
338
+ throw new Error("CODEX_HOME must be an absolute path");
339
+ return configured;
340
+ }
341
+ return join(env.HOME ?? env.USERPROFILE ?? homedir(), ".codex");
342
+ }
343
+ function tomlValue(value) {
344
+ return JSON.stringify(value);
345
+ }
346
+ function codexLaunchOverrides(spec, catalogPath, roles = []) {
347
+ const values = [
348
+ ["model", codexModelId(spec.defaultModel)],
349
+ ["model_provider", PROVIDER_ID],
350
+ [`model_providers.${PROVIDER_ID}.name`, "RouteKit gateway"],
351
+ [`model_providers.${PROVIDER_ID}.base_url`, `${trimTrailingSlashes(spec.gatewayUrl)}/v1`],
352
+ [`model_providers.${PROVIDER_ID}.wire_api`, "responses"],
353
+ [`model_providers.${PROVIDER_ID}.requires_openai_auth`, false]
354
+ ];
355
+ if (spec.auth?.token !== undefined)
356
+ values.push([`model_providers.${PROVIDER_ID}.env_key`, "ROUTEKIT_GATEWAY_TOKEN"]);
357
+ if (spec.reasoning?.mode === "effort")
358
+ values.push(["model_reasoning_effort", spec.reasoning.effort]);
359
+ if (catalogPath !== undefined)
360
+ values.push(["model_catalog_json", catalogPath]);
361
+ if (roles.length > 0) {
362
+ values.push(["features.multi_agent", true], ["agents.max_depth", 1]);
363
+ for (const role of roles) {
364
+ values.push([`agents.${role.id}.description`, role.description], [`agents.${role.id}.config_file`, role.configPath]);
365
+ }
366
+ }
367
+ return values.flatMap(([key, value]) => [
368
+ "-c",
369
+ `${key}=${typeof value === "string" ? tomlValue(value) : String(value)}`
370
+ ]);
371
+ }
372
+ function spawnCodex(args, home, cwd, token, dependencies) {
373
+ const spawnProcess = dependencies.spawnProcess ?? spawn;
374
+ const child = spawnProcess("codex", [...args], {
375
+ stdio: "inherit",
376
+ env: {
377
+ ...(dependencies.env ?? process.env),
378
+ CODEX_HOME: home,
379
+ ...(token !== undefined ? { ROUTEKIT_GATEWAY_TOKEN: token } : {})
380
+ },
381
+ ...(cwd !== undefined ? { cwd } : {})
382
+ });
248
383
  return new Promise((resolve, reject) => {
249
- const child = spawn("codex", args, {
250
- stdio: ["inherit", "inherit", "pipe"],
251
- // env-spread-allowed: interactive user tool inherits the user's shell configuration
252
- env: {
253
- ...process.env,
254
- CODEX_HOME: home,
255
- ...(token !== undefined ? { ROUTEKIT_GATEWAY_TOKEN: token } : {})
256
- },
257
- ...(cwd !== undefined ? { cwd } : {})
258
- });
259
- let stderr = "";
260
- child.stderr?.on("data", (chunk) => {
261
- process.stderr.write(chunk);
262
- stderr = (stderr + chunk.toString("utf8")).slice(-8192);
263
- });
264
- child.on("error", reject);
265
- child.on("exit", (code) => resolve({ code: code ?? 0, stderr }));
384
+ child.once("error", reject);
385
+ child.once("exit", (code, signal) => resolve(code ?? (signal === null ? 0 : 1)));
266
386
  });
267
387
  }
268
- export async function launchCodex(ctx) {
269
- const { spec } = ctx;
270
- const home = createIsolatedCodexHome("routekit-codex-");
271
- ctx.registerDisposer(() => rmSync(home, { recursive: true, force: true }));
272
- if (hasCodexLogin() && spec.auth?.token === undefined) {
273
- copyFileSync(codexAuthPath(), join(home, "auth.json"));
274
- }
275
- const ids = [...catalogIds(spec), ...codexListedStockSlugs()];
276
- codexProfileFiles(home, ids);
277
- const template = readCodexCatalogTemplate();
278
- const catalogPath = template === undefined ? undefined : join(home, CATALOG_FILE);
388
+ /** Launch against the user's real Codex home; only generated catalog data is temporary. */
389
+ export async function launchCodex(ctx, deps = {}) {
390
+ const home = resolveCodexHome(deps.env ?? process.env);
391
+ const temp = mkdtempSync(join(tmpdir(), "rk-codex-"));
392
+ ctx.registerDisposer(() => rmSync(temp, { recursive: true, force: true }));
393
+ const stockModels = readCodexHomeModelsCache(home);
394
+ const template = stockModels[0];
395
+ const catalogPath = template === undefined ? undefined : join(temp, CATALOG_FILE);
279
396
  if (catalogPath !== undefined && template !== undefined) {
280
- // The stock cache supplies verbatim ModelInfo for gateway models that are
281
- // Codex-native. Unlisted stock models are not appended: without a codex
282
- // route in the gateway catalog they would not resolve.
283
- writeFileSync(catalogPath, codexModelCatalogJson(spec, template, readCodexModelsCache(), {
284
- appendUnlistedStock: false
285
- }));
397
+ writeFileSync(catalogPath, codexModelCatalogJson(ctx.spec, template, stockModels, { appendUnlistedStock: false }), { mode: 0o600 });
286
398
  }
287
- const roles = codexAgentRoles(home, spec.agentProfiles ?? []);
399
+ const roles = codexAgentRoles(temp, ctx.spec.agentProfiles ?? []);
288
400
  if (roles.length > 0) {
289
- mkdirSync(join(home, PROFILE_DIR), { recursive: true });
401
+ mkdirSync(join(temp, PROFILE_DIR), { recursive: true, mode: 0o700 });
290
402
  for (const role of roles)
291
- writeFileSync(role.configPath, codexAgentRoleToml(role));
403
+ writeFileSync(role.configPath, codexAgentRoleToml(role), { mode: 0o600 });
292
404
  }
293
- const configPath = join(home, "config.toml");
294
- const writeConfig = (catalog, activeRoles) => {
295
- writeFileSync(configPath, codexLaunchConfigToml(spec, catalog, activeRoles));
296
- };
297
- writeConfig(catalogPath, roles);
405
+ const overrides = codexLaunchOverrides(ctx.spec, catalogPath, roles);
298
406
  ctx.prepareForPassthrough();
299
- let result = await spawnCodex(spec.args, home, spec.cwd, spec.auth?.token);
300
- if (catalogPath !== undefined && isCodexConfigFailure(result.code, result.stderr)) {
301
- writeConfig(undefined, roles);
302
- result = await spawnCodex(spec.args, home, spec.cwd, spec.auth?.token);
303
- }
304
- if (roles.length > 0 && isCodexConfigFailure(result.code, result.stderr)) {
305
- writeConfig(undefined, []);
306
- result = await spawnCodex(spec.args, home, spec.cwd, spec.auth?.token);
307
- }
308
- return result.code;
407
+ return await spawnCodex([...overrides, ...ctx.spec.args], home, ctx.spec.cwd, ctx.spec.auth?.token, deps);
309
408
  }
@@ -30,6 +30,9 @@ test("Codex managed install updates and removes only owner-marked config", () =>
30
30
  assert.match(readFileSync(configPath, "utf8"), /model = "user-default"/);
31
31
  assert.match(readFileSync(configPath, "utf8"), /base_url = "http:\/\/127\.0\.0\.1:9999\/v1"/);
32
32
  assert.equal(existsSync(join(home, "opaque-secondary.config.toml")), true);
33
+ assert.equal(existsSync(installed.catalogPath), true);
34
+ assert.match(readFileSync(join(home, "opaque-primary.config.toml"), "utf8"), /model_catalog_json/);
35
+ assert.deepEqual(JSON.parse(readFileSync(installed.catalogPath, "utf8")).models.map((model) => model.slug), ["opaque-primary", "opaque-secondary"]);
33
36
  const updated = installCodexIntegration({
34
37
  gatewayUrl: "http://127.0.0.1:8888",
35
38
  owner: OWNER,
@@ -41,6 +44,7 @@ test("Codex managed install updates and removes only owner-marked config", () =>
41
44
  assert.equal(uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }).removed, true);
42
45
  assert.equal(readFileSync(configPath, "utf8"), 'model = "user-default"\n');
43
46
  assert.equal(existsSync(join(home, "opaque-primary.config.toml")), false);
47
+ assert.equal(existsSync(installed.catalogPath), false);
44
48
  }
45
49
  finally {
46
50
  rmSync(home, { recursive: true, force: true });
@@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { test } from "node:test";
6
- import { codexAgentRoleToml, codexCatalogEntries, createIsolatedCodexHome, codexLaunchConfigToml, codexModelCatalogJson } from "../launch.js";
6
+ import { codexAgentRoleToml, codexCatalogEntries, codexLaunchConfigToml, codexModelCatalogJson, codexPersistentModelCatalogJson, createIsolatedCodexHome, resolveCodexHome } from "../launch.js";
7
7
  const SPEC = {
8
8
  gatewayUrl: "http://127.0.0.1:9999",
9
9
  defaultModel: "opaque-primary",
@@ -50,13 +50,7 @@ test("Codex launcher serializes namespaced models without interpreting provider
50
50
  { effort: "quick", description: "quick" },
51
51
  { effort: "deep", description: "deep" }
52
52
  ]);
53
- // Codex rejects the whole catalog file when any entry omits this field, so
54
- // undiscovered models must serialize an explicit empty list.
55
53
  assert.deepEqual(entries[2]?.supported_reasoning_levels, []);
56
- assert.equal(entries[2]?.default_reasoning_level, undefined);
57
- assert.ok(entries
58
- .slice(0, 3)
59
- .every((entry) => Array.isArray(entry.supported_reasoning_levels)), "every gateway-routed entry carries supported_reasoning_levels");
60
54
  assert.deepEqual(JSON.parse(codexModelCatalogJson(SPEC, template)).models, entries.slice(0, 3));
61
55
  });
62
56
  test("Codex launcher filters incompatible OpenRouter models and aliases", () => {
@@ -75,190 +69,81 @@ test("Codex launcher filters incompatible OpenRouter models and aliases", () =>
75
69
  ],
76
70
  args: []
77
71
  };
78
- const entries = codexCatalogEntries(spec, { slug: "stock" }, [], {
79
- appendUnlistedStock: false
80
- });
81
- assert.deepEqual(entries.map((entry) => entry.slug), ["openai/unknown", "openrouter/reasoning", "reasoning-alias", "openai-alias"]);
82
- });
83
- test("Codex launcher retains an incompatible selected default deterministically", () => {
84
- const spec = {
85
- gatewayUrl: "http://127.0.0.1:9999",
86
- defaultModel: "selected-alias",
87
- models: [
88
- {
89
- id: "openrouter/chat-only",
90
- provider: "openrouter",
91
- aliases: ["selected-alias", "second-alias"]
92
- },
93
- { id: "openrouter/hidden", provider: "openrouter", aliases: ["hidden-alias"] }
94
- ],
95
- args: []
96
- };
97
- assert.deepEqual(codexCatalogEntries(spec, { slug: "stock" }, [], {
98
- appendUnlistedStock: false
99
- }).map((entry) => entry.slug), ["selected-alias", "openrouter/chat-only", "second-alias"]);
100
- });
101
- test("Codex launcher exposes only provider-discovered Claude effort levels", () => {
102
- const spec = {
103
- gatewayUrl: "http://127.0.0.1:9999",
104
- defaultModel: "claude-code/claude-fable-5",
105
- models: [
106
- {
107
- id: "claude-code/claude-fable-5",
108
- reasoning: {
109
- status: "supported",
110
- efforts: [{ id: "low" }, { id: "high" }, { id: "max" }],
111
- budget: { minTokens: 1_024 },
112
- adaptive: true,
113
- wireShape: "anthropic",
114
- provenance: "provider"
115
- }
116
- }
117
- ],
118
- args: []
119
- };
120
- const [entry] = codexCatalogEntries(spec, {
121
- slug: "stock",
122
- visibility: "list",
123
- supported_reasoning_levels: [{ effort: "template" }],
124
- default_reasoning_level: "template"
125
- });
126
- assert.deepEqual(entry?.supported_reasoning_levels, [
127
- { effort: "low", description: "low" },
128
- { effort: "high", description: "high" },
129
- { effort: "max", description: "max" }
130
- ]);
131
- assert.equal(entry?.default_reasoning_level, undefined);
72
+ assert.deepEqual(codexCatalogEntries(spec, { slug: "stock" }, [], { appendUnlistedStock: false }).map((entry) => entry.slug), ["openai/unknown", "openrouter/reasoning", "reasoning-alias", "openai-alias"]);
132
73
  });
133
- test("Codex launcher neutralizes stock-model behavior fields from the template", () => {
74
+ test("Codex launcher neutralizes stock-model behavior for gateway-routed models", () => {
134
75
  const template = {
135
76
  slug: "gpt-stock",
136
- display_name: "Stock",
137
- visibility: "list",
138
- supported_reasoning_levels: [{ effort: "medium" }],
139
- default_reasoning_level: "medium",
140
- // Real stock entries carry fields that change how Codex talks to the
141
- // model; none of them may leak into gateway-routed entries.
142
77
  tool_mode: "code_mode_only",
143
78
  use_responses_lite: true,
144
79
  additional_speed_tiers: ["fast"],
145
- service_tiers: [{ id: "priority", name: "Fast" }],
80
+ service_tiers: [{ id: "priority" }],
146
81
  default_service_tier: "priority",
147
82
  base_instructions: "You are Codex, an agent based on GPT-5.",
148
- model_messages: {
149
- instructions_template: "You are Codex, an agent based on GPT-5.",
150
- instructions_variables: null
151
- }
83
+ model_messages: { instructions_template: "You are Codex, an agent based on GPT-5." }
152
84
  };
153
85
  const [entry] = codexCatalogEntries(SPEC, template);
154
86
  assert.ok(entry);
155
87
  assert.equal("tool_mode" in entry, false);
156
- assert.equal("default_service_tier" in entry, false);
157
88
  assert.equal(entry.use_responses_lite, false);
158
89
  assert.deepEqual(entry.additional_speed_tiers, []);
159
90
  assert.deepEqual(entry.service_tiers, []);
160
- // The developer message must not claim a stock model's identity.
161
91
  assert.equal(entry.base_instructions, "You are a coding agent.");
162
- assert.deepEqual(entry.model_messages, {
163
- instructions_template: "You are a coding agent.",
164
- instructions_variables: null
165
- });
166
- // A minimal template gains no wire-shape fields it never had.
167
- const [minimal] = codexCatalogEntries(SPEC, { slug: "s", visibility: "list" });
168
- assert.ok(minimal);
169
- assert.equal("use_responses_lite" in minimal, false);
170
- assert.equal("service_tiers" in minimal, false);
92
+ assert.deepEqual(entry.model_messages, { instructions_template: "You are a coding agent." });
171
93
  });
172
- test("Codex launcher passes stock ModelInfo through for codex-native models only", () => {
94
+ test("Codex keeps real stock ModelInfo only for Codex-native models", () => {
173
95
  const spec = {
174
96
  gatewayUrl: "http://127.0.0.1:9999",
175
97
  defaultModel: "codex/gpt-5.5",
176
- models: [
177
- { id: "codex/gpt-5.5" },
178
- // A foreign model that happens to collide with a stock slug must NOT
179
- // inherit the stock entry: it is not the Codex-native model.
180
- { id: "claude-code/gpt-5.4" },
181
- { id: "claude-code/claude-sonnet-5" }
182
- ],
98
+ models: [{ id: "codex/gpt-5.5" }, { id: "claude-code/gpt-5.4" }],
183
99
  args: []
184
100
  };
185
- const template = { slug: "stock", display_name: "Stock", visibility: "list" };
186
- const stock = [
187
- {
188
- slug: "gpt-5.5",
189
- display_name: "GPT-5.5",
190
- description: "Stock Codex model.",
191
- base_instructions: "You are Codex, an agent based on GPT-5.",
192
- tool_mode: "code_mode_only",
193
- use_responses_lite: true,
194
- supported_reasoning_levels: [{ effort: "xhigh" }],
195
- default_reasoning_level: "xhigh",
196
- visibility: "hidden"
197
- },
198
- { slug: "gpt-5.4", display_name: "GPT-5.4" },
199
- { slug: "gpt-unrelated", display_name: "Unrelated" }
200
- ];
201
- const entries = codexCatalogEntries(spec, template, stock, {
101
+ const stock = [{ slug: "gpt-5.5", tool_mode: "code_mode_only" }, { slug: "gpt-5.4" }];
102
+ const [native, foreign] = codexCatalogEntries(spec, { slug: "stock" }, stock, {
202
103
  appendUnlistedStock: false
203
104
  });
204
- assert.deepEqual(entries.map((entry) => entry.slug), ["gpt-5.5", "claude-code/gpt-5.4", "claude-code/claude-sonnet-5"]);
205
- const [native, foreignCollision, foreign] = entries;
206
- // Native passthrough keeps the tuned stock behavior, pinned to list + HTTP.
207
- assert.equal(native?.base_instructions, "You are Codex, an agent based on GPT-5.");
208
105
  assert.equal(native?.tool_mode, "code_mode_only");
209
- assert.equal(native?.use_responses_lite, true);
210
- assert.equal(native?.default_reasoning_level, "xhigh");
211
- assert.equal(native?.visibility, "list");
212
- assert.equal(native?.prefer_websockets, false);
213
- // Foreign models never inherit stock entries, colliding slug or not.
214
- assert.equal(foreignCollision?.base_instructions, undefined);
215
- assert.equal("tool_mode" in (foreignCollision ?? {}), false);
216
- assert.equal("tool_mode" in (foreign ?? {}), false);
217
- // Unlisted stock models stay out when appending is disabled.
218
- assert.ok(!entries.some((entry) => entry.slug === "gpt-unrelated"));
106
+ assert.equal(foreign?.tool_mode, undefined);
219
107
  });
220
- test("Codex launcher serializes one gateway provider and generic agent profiles", () => {
108
+ test("Codex launcher serializes a gateway provider and generic agent profiles", () => {
221
109
  const role = { ...PROFILE, configPath: "/tmp/reviewer.toml" };
222
- const config = codexLaunchConfigToml(SPEC, "/tmp/catalog.json", [role]);
110
+ const config = codexLaunchConfigToml({ ...SPEC, auth: { token: "test" } }, "/tmp/catalog.json", [
111
+ role
112
+ ]);
223
113
  assert.match(config, /model = "opaque-primary"/);
224
114
  assert.match(config, /base_url = "http:\/\/127\.0\.0\.1:9999\/v1"/);
115
+ assert.match(config, /env_key = "ROUTEKIT_GATEWAY_TOKEN"/);
225
116
  assert.match(config, /config_file = "\/tmp\/reviewer\.toml"/);
226
- const profile = codexAgentRoleToml(PROFILE);
227
- assert.match(profile, /model = "opaque-secondary"/);
228
- assert.match(profile, /developer_instructions = "Return concise findings\."/);
117
+ assert.match(codexAgentRoleToml(PROFILE), /developer_instructions = "Return concise findings\."/);
229
118
  });
230
- test("Codex launcher projects codex models to native picker ids", () => {
231
- const spec = {
232
- gatewayUrl: "http://127.0.0.1:9999",
233
- defaultModel: "codex/gpt-5.5",
234
- models: [
235
- { id: "codex/gpt-5.5", label: "GPT-5.5 subscription" },
236
- { id: "claude-code/claude-sonnet-4-6" }
237
- ],
238
- args: []
239
- };
240
- const template = {
241
- slug: "stock",
242
- display_name: "Stock",
243
- visibility: "list"
244
- };
245
- assert.deepEqual(codexCatalogEntries(spec, template).map((entry) => [
246
- entry.slug,
247
- entry.display_name
248
- ]), [
249
- ["gpt-5.5", "GPT-5.5 subscription"],
250
- [
251
- "claude-code/claude-sonnet-4-6",
252
- "claude-code/claude-sonnet-4-6"
253
- ]
119
+ test("persistent Codex profiles carry matching model metadata", () => {
120
+ const catalog = JSON.parse(codexPersistentModelCatalogJson([
121
+ {
122
+ id: "openai/gpt-4o-mini",
123
+ reasoning: {
124
+ status: "supported",
125
+ efforts: [{ id: "low" }, { id: "high" }],
126
+ defaultEffort: "low",
127
+ provenance: "provider"
128
+ }
129
+ },
130
+ { id: "codex/gpt-5.6" }
131
+ ]));
132
+ assert.deepEqual(catalog.models.map((model) => model.slug), [
133
+ "openai/gpt-4o-mini",
134
+ "codex/gpt-5.6"
254
135
  ]);
255
- assert.match(codexLaunchConfigToml(spec), /model = "gpt-5\.5"/);
256
- assert.match(codexAgentRoleToml({
257
- ...PROFILE,
258
- model: "codex/gpt-5.5"
259
- }), /model = "gpt-5\.5"/);
136
+ assert.deepEqual(catalog.models[0]?.supported_reasoning_levels, [
137
+ { effort: "low", description: "low" },
138
+ { effort: "high", description: "high" }
139
+ ]);
140
+ assert.equal(catalog.models[0]?.supports_reasoning_summaries, true);
141
+ });
142
+ test("Codex normal home honors absolute CODEX_HOME and rejects relative values", () => {
143
+ assert.equal(resolveCodexHome({ CODEX_HOME: "/private/codex" }), "/private/codex");
144
+ assert.throws(() => resolveCodexHome({ CODEX_HOME: "relative" }), /absolute path/);
260
145
  });
261
- test("isolated Codex homes live under the user cache instead of the system temp root", () => {
146
+ test("isolated Codex homes used by the harness live under the user cache", () => {
262
147
  const root = mkdtempSync(join(tmpdir(), "routekit-codex-home-test-"));
263
148
  const userHome = join(root, "home");
264
149
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-tool-codex",
3
3
  "private": false,
4
- "version": "0.16.8",
4
+ "version": "0.17.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -29,11 +29,11 @@
29
29
  "@openai/codex-sdk": "0.145.0",
30
30
  "smol-toml": "1.7.0",
31
31
  "zod": "4.4.3",
32
- "@velum-labs/routekit-contracts": "0.16.8",
33
- "@velum-labs/routekit-harness-core": "0.16.8",
34
- "@velum-labs/routekit-runtime": "0.16.8",
35
- "@velum-labs/routekit-tools": "0.16.8",
36
- "@velum-labs/routekit-registry": "0.16.8"
32
+ "@velum-labs/routekit-contracts": "0.17.0",
33
+ "@velum-labs/routekit-harness-core": "0.17.0",
34
+ "@velum-labs/routekit-registry": "0.17.0",
35
+ "@velum-labs/routekit-runtime": "0.17.0",
36
+ "@velum-labs/routekit-tools": "0.17.0"
37
37
  },
38
38
  "keywords": [
39
39
  "llm",