@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 +7 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/install.d.ts +4 -0
- package/dist/install.js +45 -6
- package/dist/launch.d.ts +23 -2
- package/dist/launch.js +170 -71
- package/dist/test/install.test.js +4 -0
- package/dist/test/launch.test.js +43 -158
- package/package.json +6 -6
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 {
|
|
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
|
|
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 {
|
|
40
|
-
export {
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
32
|
+
function readCodexModelsCachePath(path) {
|
|
33
33
|
try {
|
|
34
|
-
const parsed = JSON.parse(readFileSync(
|
|
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([
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
const home =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
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
|
-
|
|
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(
|
|
399
|
+
const roles = codexAgentRoles(temp, ctx.spec.agentProfiles ?? []);
|
|
288
400
|
if (roles.length > 0) {
|
|
289
|
-
mkdirSync(join(
|
|
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
|
|
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
|
-
|
|
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 });
|
package/dist/test/launch.test.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
186
|
-
const
|
|
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(
|
|
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
|
|
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", [
|
|
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
|
-
|
|
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
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
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.
|
|
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.
|
|
33
|
-
"@velum-labs/routekit-harness-core": "0.
|
|
34
|
-
"@velum-labs/routekit-
|
|
35
|
-
"@velum-labs/routekit-
|
|
36
|
-
"@velum-labs/routekit-
|
|
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",
|