@velum-labs/routekit-tool-codex 0.17.0 → 0.17.2
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/install.d.ts +13 -1
- package/dist/install.js +45 -24
- package/dist/test/install.test.js +77 -7
- package/package.json +6 -6
package/dist/install.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ModelReasoningCapabilities } from "@velum-labs/routekit-contracts";
|
|
2
2
|
export type CodexInstallProfile = {
|
|
3
3
|
modelId: string;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Legacy selector for the one persistent profile. New callers should use
|
|
6
|
+
* `CodexInstallInput.profileId`; when both are absent it is `routekit`.
|
|
7
|
+
*/
|
|
5
8
|
profileId?: string;
|
|
6
9
|
description?: string;
|
|
7
10
|
reasoning?: ModelReasoningCapabilities;
|
|
@@ -16,7 +19,16 @@ export type CodexInstallOwner = {
|
|
|
16
19
|
};
|
|
17
20
|
export type CodexInstallInput = {
|
|
18
21
|
gatewayUrl: string;
|
|
22
|
+
/**
|
|
23
|
+
* The RouteKit models made available through Codex's model picker. This
|
|
24
|
+
* remains named `profiles` for API compatibility, although persistent
|
|
25
|
+
* installs now write one RouteKit profile rather than one file per model.
|
|
26
|
+
*/
|
|
19
27
|
profiles: readonly CodexInstallProfile[];
|
|
28
|
+
/** Model selected when the single RouteKit profile is first opened. */
|
|
29
|
+
defaultModel?: string;
|
|
30
|
+
/** Safe selector for the one persistent RouteKit profile. */
|
|
31
|
+
profileId?: string;
|
|
20
32
|
owner: CodexInstallOwner;
|
|
21
33
|
codexHome?: string;
|
|
22
34
|
};
|
package/dist/install.js
CHANGED
|
@@ -26,11 +26,7 @@ function catalogFileName(ownerId) {
|
|
|
26
26
|
}
|
|
27
27
|
return `.${ownerId}-model-catalog.json`;
|
|
28
28
|
}
|
|
29
|
-
function
|
|
30
|
-
return profile.profileId ?? profile.modelId;
|
|
31
|
-
}
|
|
32
|
-
function profileFileName(profile) {
|
|
33
|
-
const selector = profileSelector(profile);
|
|
29
|
+
function profileFileName(selector) {
|
|
34
30
|
if (selector.length === 0 ||
|
|
35
31
|
selector.includes("/") ||
|
|
36
32
|
selector.includes("\\") ||
|
|
@@ -39,6 +35,24 @@ function profileFileName(profile) {
|
|
|
39
35
|
}
|
|
40
36
|
return `${selector}.config.toml`;
|
|
41
37
|
}
|
|
38
|
+
function selectedProfileId(input) {
|
|
39
|
+
return input.profileId ?? input.profiles[0]?.profileId ?? "routekit";
|
|
40
|
+
}
|
|
41
|
+
function selectedDefaultModel(input) {
|
|
42
|
+
const model = input.defaultModel ?? input.profiles[0]?.modelId;
|
|
43
|
+
if (model === undefined)
|
|
44
|
+
throw new Error("at least one Codex catalog model is required");
|
|
45
|
+
if (!input.profiles.some((profile) => profile.modelId === model)) {
|
|
46
|
+
throw new Error(`the Codex default model ${JSON.stringify(model)} is not in the RouteKit catalog`);
|
|
47
|
+
}
|
|
48
|
+
return model;
|
|
49
|
+
}
|
|
50
|
+
function orderedCatalogProfiles(profiles, defaultModel) {
|
|
51
|
+
return [
|
|
52
|
+
...profiles.filter((profile) => profile.modelId === defaultModel),
|
|
53
|
+
...profiles.filter((profile) => profile.modelId !== defaultModel)
|
|
54
|
+
];
|
|
55
|
+
}
|
|
42
56
|
/** Serialize one additive, owner-marked Codex provider block. */
|
|
43
57
|
export function codexIntegrationBlock(input) {
|
|
44
58
|
const base = trimTrailingSlashes(input.gatewayUrl);
|
|
@@ -47,6 +61,7 @@ export function codexIntegrationBlock(input) {
|
|
|
47
61
|
const filesComment = profileFilesComment(input.owner.id);
|
|
48
62
|
const catalogComment = catalogFileComment(input.owner.id);
|
|
49
63
|
const catalogFile = catalogFileName(input.owner.id);
|
|
64
|
+
const profileId = selectedProfileId(input);
|
|
50
65
|
const body = tomlStringify({
|
|
51
66
|
model_providers: {
|
|
52
67
|
[input.owner.providerId]: {
|
|
@@ -63,9 +78,10 @@ export function codexIntegrationBlock(input) {
|
|
|
63
78
|
`# Managed by \`${input.owner.installCommand}\`; do not edit between these markers.`,
|
|
64
79
|
`# Rerun that command to update; use \`${input.owner.uninstallCommand}\` to remove.`,
|
|
65
80
|
`# Start the gateway first: ${input.owner.startCommand}`,
|
|
66
|
-
`# Then launch: codex --profile ${
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
`# Then launch: codex --profile ${profileId}`,
|
|
82
|
+
"# That one profile keeps the RouteKit provider selected while Codex's model picker",
|
|
83
|
+
"# reads the catalog below. It does not change your normal Codex default model.",
|
|
84
|
+
`${filesComment} ${profileFileName(profileId)}`,
|
|
69
85
|
`${catalogComment} ${catalogFile}`,
|
|
70
86
|
"",
|
|
71
87
|
body.trimEnd(),
|
|
@@ -134,14 +150,6 @@ function assertNoConflicts(outside, input) {
|
|
|
134
150
|
throw new Error(`your Codex config already defines [model_providers.${input.owner.providerId}] outside the ` +
|
|
135
151
|
`${input.owner.id}-managed block; remove or rename it, then rerun \`${input.owner.installCommand}\``);
|
|
136
152
|
}
|
|
137
|
-
const legacyProfiles = outside.profiles;
|
|
138
|
-
for (const profile of input.profiles) {
|
|
139
|
-
const selector = profileSelector(profile);
|
|
140
|
-
if (isRecord(legacyProfiles) && legacyProfiles[selector] !== undefined) {
|
|
141
|
-
throw new Error(`your Codex config already defines [profiles.${selector}] outside the ` +
|
|
142
|
-
`${input.owner.id}-managed block; remove or rename it, then rerun \`${input.owner.installCommand}\``);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
153
|
}
|
|
146
154
|
function normalize(content) {
|
|
147
155
|
const trimmed = content.replace(/\s+$/, "");
|
|
@@ -159,9 +167,19 @@ function removeOwnedProfileFile(path, ownerId) {
|
|
|
159
167
|
// Best-effort cleanup; an orphaned profile does not alter the main config.
|
|
160
168
|
}
|
|
161
169
|
}
|
|
170
|
+
function assertProfileFileCanBeManaged(path, ownerId) {
|
|
171
|
+
if (!existsSync(path))
|
|
172
|
+
return;
|
|
173
|
+
if (readFileSync(path, "utf8").includes(`Managed by ${ownerId}`))
|
|
174
|
+
return;
|
|
175
|
+
throw new Error(`refusing to overwrite an existing Codex profile: ${path}; ` +
|
|
176
|
+
`rename it, then rerun the RouteKit install`);
|
|
177
|
+
}
|
|
162
178
|
export function installCodexIntegration(input) {
|
|
163
179
|
if (input.profiles.length === 0)
|
|
164
|
-
throw new Error("at least one Codex
|
|
180
|
+
throw new Error("at least one Codex catalog model is required");
|
|
181
|
+
const defaultModel = selectedDefaultModel(input);
|
|
182
|
+
const profileId = selectedProfileId(input);
|
|
165
183
|
const configPath = codexIntegrationConfigPath(input.codexHome);
|
|
166
184
|
const codexHome = dirname(configPath);
|
|
167
185
|
const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
@@ -182,25 +200,28 @@ export function installCodexIntegration(input) {
|
|
|
182
200
|
if (!isRecord(providers) || providers[input.owner.providerId] === undefined) {
|
|
183
201
|
throw new Error("internal error: the assembled Codex config lost its managed provider block");
|
|
184
202
|
}
|
|
203
|
+
const persistentProfilePath = join(codexHome, profileFileName(profileId));
|
|
204
|
+
assertProfileFileCanBeManaged(persistentProfilePath, input.owner.id);
|
|
185
205
|
mkdirSync(codexHome, { recursive: true });
|
|
186
|
-
writeFileSync(catalogPath, codexPersistentModelCatalogJson(input.profiles.map((profile) => ({
|
|
206
|
+
writeFileSync(catalogPath, codexPersistentModelCatalogJson(orderedCatalogProfiles(input.profiles, defaultModel).map((profile) => ({
|
|
187
207
|
id: profile.modelId,
|
|
188
208
|
...(profile.reasoning !== undefined ? { reasoning: profile.reasoning } : {})
|
|
189
209
|
})), readCodexHomeModelsCache(codexHome)[0]), { mode: 0o600 });
|
|
190
|
-
|
|
210
|
+
writeFileSync(persistentProfilePath, `# Managed by ${input.owner.id}\n${codexProfileFileToml(defaultModel, input.owner.providerId, catalogPath)}`, { mode: 0o600 });
|
|
211
|
+
// Write the new profile before replacing the managed block, then clean up
|
|
212
|
+
// legacy per-model files last. An interrupted migration therefore leaves a
|
|
213
|
+
// working old or new profile, never a config that points at a missing file.
|
|
214
|
+
writeFileSync(configPath, next);
|
|
215
|
+
const nextFiles = new Set([persistentProfilePath]);
|
|
191
216
|
for (const stale of ownedProfileFiles(managed, codexHome, input.owner.id)) {
|
|
192
217
|
if (!nextFiles.has(stale))
|
|
193
218
|
removeOwnedProfileFile(stale, input.owner.id);
|
|
194
219
|
}
|
|
195
|
-
writeFileSync(configPath, next);
|
|
196
|
-
for (const profile of input.profiles) {
|
|
197
|
-
writeFileSync(join(codexHome, profileFileName(profile)), `# Managed by ${input.owner.id}\n${codexProfileFileToml(profile.modelId, input.owner.providerId, catalogPath)}`, { mode: 0o600 });
|
|
198
|
-
}
|
|
199
220
|
return {
|
|
200
221
|
configPath,
|
|
201
222
|
catalogPath,
|
|
202
223
|
action: managed !== undefined ? "updated" : "installed",
|
|
203
|
-
profiles:
|
|
224
|
+
profiles: [profileId]
|
|
204
225
|
};
|
|
205
226
|
}
|
|
206
227
|
export function uninstallCodexIntegration(input) {
|
|
@@ -12,7 +12,7 @@ const OWNER = {
|
|
|
12
12
|
uninstallCommand: "example uninstall codex",
|
|
13
13
|
startCommand: "example serve"
|
|
14
14
|
};
|
|
15
|
-
test("Codex managed install
|
|
15
|
+
test("Codex managed install adds one picker-backed profile and removes only owner-marked config", () => {
|
|
16
16
|
const home = mkdtempSync(join(tmpdir(), "routekit-codex-install-"));
|
|
17
17
|
const configPath = join(home, "config.toml");
|
|
18
18
|
writeFileSync(configPath, 'model = "user-default"\n');
|
|
@@ -24,33 +24,41 @@ test("Codex managed install updates and removes only owner-marked config", () =>
|
|
|
24
24
|
{ modelId: "opaque-primary" },
|
|
25
25
|
{ modelId: "opaque-secondary", description: "Secondary route" }
|
|
26
26
|
],
|
|
27
|
+
defaultModel: "opaque-secondary",
|
|
27
28
|
codexHome: home
|
|
28
29
|
});
|
|
29
30
|
assert.equal(installed.action, "installed");
|
|
31
|
+
assert.deepEqual(installed.profiles, ["routekit"]);
|
|
30
32
|
assert.match(readFileSync(configPath, "utf8"), /model = "user-default"/);
|
|
31
33
|
assert.match(readFileSync(configPath, "utf8"), /base_url = "http:\/\/127\.0\.0\.1:9999\/v1"/);
|
|
32
|
-
assert.
|
|
34
|
+
assert.match(readFileSync(configPath, "utf8"), /codex --profile routekit/);
|
|
35
|
+
assert.equal(existsSync(join(home, "routekit.config.toml")), true);
|
|
36
|
+
assert.equal(existsSync(join(home, "opaque-primary.config.toml")), false);
|
|
37
|
+
assert.equal(existsSync(join(home, "opaque-secondary.config.toml")), false);
|
|
33
38
|
assert.equal(existsSync(installed.catalogPath), true);
|
|
34
|
-
assert.match(readFileSync(join(home, "
|
|
35
|
-
assert.
|
|
39
|
+
assert.match(readFileSync(join(home, "routekit.config.toml"), "utf8"), /model = "opaque-secondary"/);
|
|
40
|
+
assert.match(readFileSync(join(home, "routekit.config.toml"), "utf8"), /model_catalog_json/);
|
|
41
|
+
assert.deepEqual(JSON.parse(readFileSync(installed.catalogPath, "utf8")).models.map((model) => model.slug), ["opaque-secondary", "opaque-primary"]);
|
|
36
42
|
const updated = installCodexIntegration({
|
|
37
43
|
gatewayUrl: "http://127.0.0.1:8888",
|
|
38
44
|
owner: OWNER,
|
|
39
45
|
profiles: [{ modelId: "opaque-primary" }],
|
|
46
|
+
defaultModel: "opaque-primary",
|
|
40
47
|
codexHome: home
|
|
41
48
|
});
|
|
42
49
|
assert.equal(updated.action, "updated");
|
|
43
|
-
assert.
|
|
50
|
+
assert.deepEqual(updated.profiles, ["routekit"]);
|
|
51
|
+
assert.match(readFileSync(join(home, "routekit.config.toml"), "utf8"), /model = "opaque-primary"/);
|
|
44
52
|
assert.equal(uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }).removed, true);
|
|
45
53
|
assert.equal(readFileSync(configPath, "utf8"), 'model = "user-default"\n');
|
|
46
|
-
assert.equal(existsSync(join(home, "
|
|
54
|
+
assert.equal(existsSync(join(home, "routekit.config.toml")), false);
|
|
47
55
|
assert.equal(existsSync(installed.catalogPath), false);
|
|
48
56
|
}
|
|
49
57
|
finally {
|
|
50
58
|
rmSync(home, { recursive: true, force: true });
|
|
51
59
|
}
|
|
52
60
|
});
|
|
53
|
-
test("Codex
|
|
61
|
+
test("Codex's single persistent profile can use a safe custom selector", () => {
|
|
54
62
|
const home = mkdtempSync(join(tmpdir(), "routekit-codex-opaque-"));
|
|
55
63
|
try {
|
|
56
64
|
const result = installCodexIntegration({
|
|
@@ -67,3 +75,65 @@ test("Codex profiles can preserve an opaque model id behind a safe selector", ()
|
|
|
67
75
|
rmSync(home, { recursive: true, force: true });
|
|
68
76
|
}
|
|
69
77
|
});
|
|
78
|
+
test("Codex reinstall migrates a legacy per-model installation to one RouteKit profile", () => {
|
|
79
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-migrate-"));
|
|
80
|
+
const configPath = join(home, "config.toml");
|
|
81
|
+
const legacyProfiles = ["routekit-model-1.config.toml", "routekit-model-2.config.toml"];
|
|
82
|
+
writeFileSync(configPath, [
|
|
83
|
+
'model = "user-default"',
|
|
84
|
+
"",
|
|
85
|
+
"# >>> example-host integration >>>",
|
|
86
|
+
"# example-host-profile-files: routekit-model-1.config.toml routekit-model-2.config.toml",
|
|
87
|
+
"# example-host-catalog-file: .example-host-model-catalog.json",
|
|
88
|
+
"",
|
|
89
|
+
"[model_providers.example_route]",
|
|
90
|
+
'name = "Example Host gateway"',
|
|
91
|
+
'base_url = "http://127.0.0.1:9999/v1"',
|
|
92
|
+
'wire_api = "responses"',
|
|
93
|
+
"requires_openai_auth = false",
|
|
94
|
+
'env_key = "ROUTEKIT_GATEWAY_TOKEN"',
|
|
95
|
+
"",
|
|
96
|
+
"# <<< example-host integration <<<",
|
|
97
|
+
""
|
|
98
|
+
].join("\n"));
|
|
99
|
+
for (const profile of legacyProfiles) {
|
|
100
|
+
writeFileSync(join(home, profile), "# Managed by example-host\nmodel = \"opaque-primary\"\n");
|
|
101
|
+
}
|
|
102
|
+
writeFileSync(join(home, ".example-host-model-catalog.json"), "{\"models\":[]}\n");
|
|
103
|
+
try {
|
|
104
|
+
const result = installCodexIntegration({
|
|
105
|
+
gatewayUrl: "http://127.0.0.1:8888",
|
|
106
|
+
owner: OWNER,
|
|
107
|
+
profiles: [{ modelId: "opaque-primary" }, { modelId: "opaque-secondary" }],
|
|
108
|
+
defaultModel: "opaque-secondary",
|
|
109
|
+
codexHome: home
|
|
110
|
+
});
|
|
111
|
+
assert.equal(result.action, "updated");
|
|
112
|
+
assert.deepEqual(result.profiles, ["routekit"]);
|
|
113
|
+
assert.equal(existsSync(join(home, "routekit.config.toml")), true);
|
|
114
|
+
for (const profile of legacyProfiles)
|
|
115
|
+
assert.equal(existsSync(join(home, profile)), false);
|
|
116
|
+
assert.match(readFileSync(configPath, "utf8"), /example-host-profile-files: routekit\.config\.toml/);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
rmSync(home, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
test("Codex install refuses to overwrite a user-owned routekit profile", () => {
|
|
123
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-profile-conflict-"));
|
|
124
|
+
const configPath = join(home, "config.toml");
|
|
125
|
+
writeFileSync(join(home, "routekit.config.toml"), 'model = "user-profile"\n');
|
|
126
|
+
try {
|
|
127
|
+
assert.throws(() => installCodexIntegration({
|
|
128
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
129
|
+
owner: OWNER,
|
|
130
|
+
profiles: [{ modelId: "opaque-primary" }],
|
|
131
|
+
codexHome: home
|
|
132
|
+
}), /refusing to overwrite an existing Codex profile/);
|
|
133
|
+
assert.equal(existsSync(configPath), false);
|
|
134
|
+
assert.equal(existsSync(join(home, ".example-host-model-catalog.json")), false);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
rmSync(home, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
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.17.
|
|
4
|
+
"version": "0.17.2",
|
|
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.17.
|
|
33
|
-
"@velum-labs/routekit-harness-core": "0.17.
|
|
34
|
-
"@velum-labs/routekit-
|
|
35
|
-
"@velum-labs/routekit-
|
|
36
|
-
"@velum-labs/routekit-
|
|
32
|
+
"@velum-labs/routekit-contracts": "0.17.2",
|
|
33
|
+
"@velum-labs/routekit-harness-core": "0.17.2",
|
|
34
|
+
"@velum-labs/routekit-runtime": "0.17.2",
|
|
35
|
+
"@velum-labs/routekit-tools": "0.17.2",
|
|
36
|
+
"@velum-labs/routekit-registry": "0.17.2"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
39
39
|
"llm",
|