@velum-labs/routekit-tool-claude 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 CHANGED
@@ -7,6 +7,8 @@ export type ClaudeInstallOwner = {
7
7
  };
8
8
  export type ClaudeInstallInput = {
9
9
  gatewayUrl: string;
10
+ /** Canonical RouteKit catalog ids permitted by the current model policy. */
11
+ models: readonly string[];
10
12
  owner: ClaudeInstallOwner;
11
13
  claudeConfigDir?: string;
12
14
  };
package/dist/install.js CHANGED
@@ -6,9 +6,12 @@ import { SUBSCRIPTIONS } from "@velum-labs/routekit-registry";
6
6
  import { acquireLifecycleLock, trimTrailingSlashes, writeFileAtomic } from "@velum-labs/routekit-runtime";
7
7
  const MANAGED_ENV_KEYS = [
8
8
  "ANTHROPIC_BASE_URL",
9
+ "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT"
10
+ ];
11
+ const RETIRED_MANAGED_ENV_KEYS = [
12
+ "ANTHROPIC_AUTH_TOKEN",
9
13
  "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
10
14
  ];
11
- const RETIRED_MANAGED_ENV_KEYS = ["ANTHROPIC_AUTH_TOKEN"];
12
15
  function reached(boundary) {
13
16
  // Deliberately not part of the package API: tests install a same-process
14
17
  // throw hook to model termination immediately after an atomic boundary.
@@ -91,6 +94,9 @@ function isStringRecord(value) {
91
94
  return (isRecord(value) &&
92
95
  Object.values(value).every((entry) => typeof entry === "string"));
93
96
  }
97
+ function isStringArray(value) {
98
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
99
+ }
94
100
  function isInstalledManifest(value) {
95
101
  return (isRecord(value) &&
96
102
  value.version === 2 &&
@@ -99,7 +105,11 @@ function isInstalledManifest(value) {
99
105
  isSnapshot(value.original) &&
100
106
  typeof value.exactRestoreEligible === "boolean" &&
101
107
  typeof value.installedContentHash === "string" &&
102
- isStringRecord(value.managedEnvValues));
108
+ isStringRecord(value.managedEnvValues) &&
109
+ (value.managedPickerModels === undefined || isStringArray(value.managedPickerModels)) &&
110
+ (value.managedAvailableModels === undefined || value.managedAvailableModels === true) &&
111
+ (value.managedEnforceAvailableModels === undefined ||
112
+ value.managedEnforceAvailableModels === true));
103
113
  }
104
114
  function parseManifest(content, manifestPath) {
105
115
  let parsed;
@@ -159,9 +169,36 @@ function serialize(value) {
159
169
  function managedEnv(input) {
160
170
  return {
161
171
  ANTHROPIC_BASE_URL: trimTrailingSlashes(input.gatewayUrl),
162
- CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
172
+ CLAUDE_CODE_ALWAYS_ENABLE_EFFORT: "1"
163
173
  };
164
174
  }
175
+ const CLAUDE_PICKER_PREFIX = "anthropic.routekit.";
176
+ function pickerModels(input) {
177
+ const models = new Set();
178
+ for (const model of input.models) {
179
+ if (typeof model !== "string" || model.length === 0) {
180
+ throw new Error("RouteKit's Claude picker catalog contains an invalid model id");
181
+ }
182
+ models.add(`${CLAUDE_PICKER_PREFIX}${model}`);
183
+ }
184
+ return [...models];
185
+ }
186
+ function availableModels(settings, configPath) {
187
+ if (settings.availableModels === undefined)
188
+ return undefined;
189
+ if (!isStringArray(settings.availableModels)) {
190
+ throw new Error(`the "availableModels" field in your Claude settings (${configPath}) must be an array of strings`);
191
+ }
192
+ return [...settings.availableModels];
193
+ }
194
+ function enforceAvailableModels(settings, configPath) {
195
+ if (settings.enforceAvailableModels === undefined)
196
+ return undefined;
197
+ if (typeof settings.enforceAvailableModels !== "boolean") {
198
+ throw new Error(`the "enforceAvailableModels" field in your Claude settings (${configPath}) must be a boolean`);
199
+ }
200
+ return settings.enforceAvailableModels;
201
+ }
165
202
  function entryIfExists(path) {
166
203
  try {
167
204
  return lstatSync(path);
@@ -412,6 +449,10 @@ export async function installClaudeIntegration(input) {
412
449
  const settings = parseSettings(beforeSettings.content ?? "{}\n", configPath);
413
450
  const env = { ...(settings.env ?? {}) };
414
451
  const nextManaged = managedEnv(input);
452
+ const desiredPickerModels = pickerModels(input);
453
+ if (desiredPickerModels.length === 0) {
454
+ throw new Error("RouteKit's Claude picker catalog cannot be empty");
455
+ }
415
456
  if (previousManifest === undefined) {
416
457
  for (const key of MANAGED_ENV_KEYS) {
417
458
  if (env[key] !== undefined) {
@@ -426,7 +467,33 @@ export async function installClaudeIntegration(input) {
426
467
  for (const key of removableKeys)
427
468
  delete env[key];
428
469
  Object.assign(env, nextManaged);
429
- const nextSettings = { ...settings, env };
470
+ const currentPickerModels = availableModels(settings, configPath);
471
+ const priorManagedPickerModels = previousManifest?.managedPickerModels ?? [];
472
+ if (priorManagedPickerModels.some((model) => currentPickerModels === undefined || !currentPickerModels.includes(model))) {
473
+ throw new Error(`your Claude settings changed RouteKit-managed availableModels in ${configPath}; ` +
474
+ `restore those entries before rerunning the install command`);
475
+ }
476
+ const userPickerModels = (currentPickerModels ?? []).filter((model) => !priorManagedPickerModels.includes(model));
477
+ const nextManagedPickerModels = desiredPickerModels.filter((model) => !userPickerModels.includes(model));
478
+ const nextPickerModels = [...userPickerModels, ...nextManagedPickerModels];
479
+ const currentEnforceAvailableModels = enforceAvailableModels(settings, configPath);
480
+ if (previousManifest?.managedEnforceAvailableModels === true &&
481
+ currentEnforceAvailableModels !== true) {
482
+ throw new Error(`your Claude settings changed RouteKit-managed enforceAvailableModels in ${configPath}; ` +
483
+ "restore that value before rerunning the install command");
484
+ }
485
+ const nextManagedEnforceAvailableModels = previousManifest?.managedEnforceAvailableModels === true ||
486
+ currentEnforceAvailableModels === undefined
487
+ ? true
488
+ : undefined;
489
+ const nextSettings = {
490
+ ...settings,
491
+ env,
492
+ availableModels: nextPickerModels,
493
+ ...(nextManagedEnforceAvailableModels === true
494
+ ? { enforceAvailableModels: true }
495
+ : {})
496
+ };
430
497
  const nextContent = serialize(nextSettings);
431
498
  const targetSettings = snapshot(nextContent, 0o600);
432
499
  const exactRestoreEligible = previousManifest === undefined
@@ -440,7 +507,14 @@ export async function installClaudeIntegration(input) {
440
507
  original: previousManifest?.original ?? beforeSettings,
441
508
  exactRestoreEligible,
442
509
  installedContentHash: targetSettings.hash,
443
- managedEnvValues: nextManaged
510
+ managedEnvValues: nextManaged,
511
+ managedPickerModels: nextManagedPickerModels,
512
+ ...(previousManifest?.managedAvailableModels === true || currentPickerModels === undefined
513
+ ? { managedAvailableModels: true }
514
+ : {}),
515
+ ...(nextManagedEnforceAvailableModels === true
516
+ ? { managedEnforceAvailableModels: true }
517
+ : {})
444
518
  };
445
519
  const pendingManifest = {
446
520
  version: 2,
@@ -467,7 +541,11 @@ export async function installClaudeIntegration(input) {
467
541
  return {
468
542
  configPath,
469
543
  action: previousManifest === undefined ? "installed" : "updated",
470
- managedKeys: Object.keys(nextManaged)
544
+ managedKeys: [
545
+ ...Object.keys(nextManaged),
546
+ "availableModels",
547
+ ...(nextManagedEnforceAvailableModels === true ? ["enforceAvailableModels"] : [])
548
+ ]
471
549
  };
472
550
  });
473
551
  }
@@ -521,6 +599,21 @@ export async function uninstallClaudeIntegration(input) {
521
599
  delete next.env;
522
600
  else
523
601
  next.env = env;
602
+ const currentPickerModels = availableModels(settings, configPath);
603
+ if (currentPickerModels !== undefined) {
604
+ const remainingPickerModels = currentPickerModels.filter((model) => !(installed.managedPickerModels ?? []).includes(model));
605
+ if (installed.managedAvailableModels === true && remainingPickerModels.length === 0) {
606
+ delete next.availableModels;
607
+ }
608
+ else {
609
+ next.availableModels = remainingPickerModels;
610
+ }
611
+ }
612
+ const currentEnforceAvailableModels = enforceAvailableModels(settings, configPath);
613
+ if (installed.managedEnforceAvailableModels === true &&
614
+ currentEnforceAvailableModels === true) {
615
+ delete next.enforceAvailableModels;
616
+ }
524
617
  targetSettings = snapshot(serialize(next), beforeSettings.mode);
525
618
  }
526
619
  else {
package/dist/launch.js CHANGED
@@ -1,22 +1,16 @@
1
- import { EFFORT_QUALIFIED_MODEL_CODEC, effortQualifiedClientModel } from "@velum-labs/routekit-contracts";
2
1
  import { spawnTool } from "@velum-labs/routekit-runtime";
3
2
  export function claudeEnv(gatewayUrl, authToken) {
4
3
  return {
5
4
  ANTHROPIC_BASE_URL: gatewayUrl,
6
5
  ANTHROPIC_AUTH_TOKEN: authToken ?? "routekit",
7
- CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
6
+ CLAUDE_CODE_ALWAYS_ENABLE_EFFORT: "1",
8
7
  ...(process.env.CLAUDE_CONFIG_DIR !== undefined
9
8
  ? { CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR }
10
9
  : {})
11
10
  };
12
11
  }
13
12
  export function claudeModelId(modelId) {
14
- const pickerId = modelId.startsWith("claude-code/")
15
- ? modelId.slice("claude-code/".length)
16
- : modelId;
17
- return pickerId.startsWith("claude") || pickerId.startsWith("anthropic")
18
- ? pickerId
19
- : `claude-${pickerId}`;
13
+ return `anthropic.routekit.${modelId}`;
20
14
  }
21
15
  /** Serialize host-authored profiles once into Claude's session agent format. */
22
16
  export function claudeAgentsJson(profiles) {
@@ -35,10 +29,16 @@ function hasAgentsArg(args) {
35
29
  function hasModelArg(args) {
36
30
  return args.some((arg) => arg === "--model" || arg.startsWith("--model="));
37
31
  }
32
+ function hasEffortArg(args) {
33
+ return args.some((arg) => arg === "--effort" || arg.startsWith("--effort="));
34
+ }
38
35
  export function claudeLaunchArgs(ctx) {
39
36
  const args = [...ctx.spec.args];
40
37
  if (!hasModelArg(args)) {
41
- args.unshift("--model", effortQualifiedClientModel(claudeModelId(ctx.spec.defaultModel), ctx.spec.reasoning, EFFORT_QUALIFIED_MODEL_CODEC));
38
+ args.unshift("--model", claudeModelId(ctx.spec.defaultModel));
39
+ if (ctx.spec.reasoning?.mode === "effort" && !hasEffortArg(args)) {
40
+ args.unshift("--effort", ctx.spec.reasoning.effort);
41
+ }
42
42
  }
43
43
  const profiles = ctx.spec.agentProfiles ?? [];
44
44
  if (profiles.length > 0 && !hasAgentsArg(args)) {
@@ -18,6 +18,7 @@ const OWNER = {
18
18
  function install(configDirectory, gatewayUrl = "http://127.0.0.1:9999/", _authToken) {
19
19
  return installClaudeIntegration({
20
20
  gatewayUrl,
21
+ models: ["openai/mock-model"],
21
22
  owner: OWNER,
22
23
  claudeConfigDir: configDirectory
23
24
  });
@@ -32,11 +33,16 @@ test("Claude managed install updates and restores the exact original settings",
32
33
  assert.equal(installed.action, "installed");
33
34
  assert.deepEqual(installed.managedKeys.sort(), [
34
35
  "ANTHROPIC_BASE_URL",
35
- "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"
36
+ "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT",
37
+ "availableModels",
38
+ "enforceAvailableModels"
36
39
  ]);
37
40
  const settings = JSON.parse(readFileSync(configPath, "utf8"));
38
41
  assert.deepEqual(settings.permissions, { allow: ["Bash(git status)"] });
39
42
  assert.equal(settings.env.ANTHROPIC_BASE_URL, "http://127.0.0.1:9999");
43
+ assert.equal(settings.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT, "1");
44
+ assert.deepEqual(settings.availableModels, ["anthropic.routekit.openai/mock-model"]);
45
+ assert.equal(settings.enforceAvailableModels, true);
40
46
  assert.equal("ANTHROPIC_AUTH_TOKEN" in settings.env, false);
41
47
  assert.equal(statSync(configPath).mode & 0o777, 0o600);
42
48
  const manifestPath = join(configDirectory, `.${OWNER.id}-integration.json`);
@@ -83,6 +89,48 @@ test("Claude uninstall preserves user edits made after install", async () => {
83
89
  rmSync(configDirectory, { recursive: true, force: true });
84
90
  }
85
91
  });
92
+ test("Claude install merges and refreshes only RouteKit-owned picker entries", async () => {
93
+ const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-picker-merge-"));
94
+ const configPath = join(configDirectory, "settings.json");
95
+ writeFileSync(configPath, `${JSON.stringify({
96
+ availableModels: ["claude-opus-4-6"],
97
+ enforceAvailableModels: false
98
+ })}\n`);
99
+ try {
100
+ await installClaudeIntegration({
101
+ gatewayUrl: "http://127.0.0.1:9999",
102
+ models: ["openai/gpt-5.6", "codex/gpt-5.6"],
103
+ owner: OWNER,
104
+ claudeConfigDir: configDirectory
105
+ });
106
+ let settings = JSON.parse(readFileSync(configPath, "utf8"));
107
+ assert.deepEqual(settings.availableModels, [
108
+ "claude-opus-4-6",
109
+ "anthropic.routekit.openai/gpt-5.6",
110
+ "anthropic.routekit.codex/gpt-5.6"
111
+ ]);
112
+ assert.equal(settings.enforceAvailableModels, false);
113
+ await installClaudeIntegration({
114
+ gatewayUrl: "http://127.0.0.1:9999",
115
+ models: ["openai/gpt-5.7"],
116
+ owner: OWNER,
117
+ claudeConfigDir: configDirectory
118
+ });
119
+ settings = JSON.parse(readFileSync(configPath, "utf8"));
120
+ assert.deepEqual(settings.availableModels, [
121
+ "claude-opus-4-6",
122
+ "anthropic.routekit.openai/gpt-5.7"
123
+ ]);
124
+ await uninstallClaudeIntegration({ ownerId: OWNER.id, claudeConfigDir: configDirectory });
125
+ assert.deepEqual(JSON.parse(readFileSync(configPath, "utf8")), {
126
+ availableModels: ["claude-opus-4-6"],
127
+ enforceAvailableModels: false
128
+ });
129
+ }
130
+ finally {
131
+ rmSync(configDirectory, { recursive: true, force: true });
132
+ }
133
+ });
86
134
  test("Claude install refuses malformed settings and user-owned env conflicts", async () => {
87
135
  const configDirectory = mkdtempSync(join(tmpdir(), "routekit-claude-conflict-"));
88
136
  const configPath = join(configDirectory, "settings.json");
@@ -121,6 +169,7 @@ function spawnBlockedInstall(configDirectory, boundary) {
121
169
  };
122
170
  await installClaudeIntegration({
123
171
  gatewayUrl: "http://127.0.0.1:9999",
172
+ models: ["openai/mock-model"],
124
173
  owner: ${JSON.stringify(OWNER)},
125
174
  claudeConfigDir: configDirectory
126
175
  });
@@ -287,11 +336,13 @@ test("explicit config directory wins over CLAUDE_CONFIG_DIR, which wins over the
287
336
  process.env.CLAUDE_CONFIG_DIR = environmentDirectory;
288
337
  const fromEnvironment = await installClaudeIntegration({
289
338
  gatewayUrl: "http://127.0.0.1:9999",
339
+ models: ["openai/mock-model"],
290
340
  owner: OWNER
291
341
  });
292
342
  assert.equal(fromEnvironment.configPath, join(environmentDirectory, "settings.json"));
293
343
  const explicit = await installClaudeIntegration({
294
344
  gatewayUrl: "http://127.0.0.1:9999",
345
+ models: ["openai/mock-model"],
295
346
  owner: OWNER,
296
347
  claudeConfigDir: explicitDirectory
297
348
  });
@@ -361,6 +412,8 @@ test("updating a v2 install removes an owned token without persisting its replac
361
412
  const settings = JSON.parse(readFileSync(configPath, "utf8"));
362
413
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
363
414
  assert.equal("ANTHROPIC_AUTH_TOKEN" in settings.env, false);
415
+ assert.equal("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY" in settings.env, false);
416
+ assert.equal(settings.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT, "1");
364
417
  assert.equal("ANTHROPIC_AUTH_TOKEN" in manifest.managedEnvValues, false);
365
418
  assert.equal(readFileSync(manifestPath, "utf8").includes("old-secret"), false);
366
419
  assert.equal(readFileSync(manifestPath, "utf8").includes("new-secret"), false);
@@ -30,14 +30,14 @@ test("claudeAgentsJson serializes generic profiles", () => {
30
30
  reviewer: {
31
31
  description: "Review changes.",
32
32
  prompt: "Return findings.",
33
- model: "claude-opaque-model"
33
+ model: "anthropic.routekit.opaque-model"
34
34
  }
35
35
  });
36
36
  });
37
- test("Claude launcher projects claude-code models to native picker ids", () => {
37
+ test("Claude launcher projects RouteKit models to native picker ids", () => {
38
38
  assert.deepEqual(claudeLaunchArgs(context([], [], "claude-code/claude-sonnet-4-6")), [
39
39
  "--model",
40
- "claude-sonnet-4-6"
40
+ "anthropic.routekit.claude-code/claude-sonnet-4-6"
41
41
  ]);
42
42
  assert.deepEqual(JSON.parse(claudeAgentsJson([
43
43
  {
@@ -56,12 +56,12 @@ test("Claude launcher projects claude-code models to native picker ids", () => {
56
56
  native: {
57
57
  description: "Use the subscription pool.",
58
58
  prompt: "Review.",
59
- model: "claude-opus-4-8"
59
+ model: "anthropic.routekit.claude-code/claude-opus-4-8"
60
60
  },
61
61
  cross: {
62
62
  description: "Use Codex.",
63
63
  prompt: "Review.",
64
- model: "claude-codex/gpt-5.5"
64
+ model: "anthropic.routekit.codex/gpt-5.5"
65
65
  }
66
66
  });
67
67
  });
@@ -80,13 +80,21 @@ test("Claude launcher forwards an explicit isolated config directory", () => {
80
80
  });
81
81
  test("claudeLaunchArgs adds profiles unless the user supplied agents", () => {
82
82
  const args = claudeLaunchArgs(context(["--verbose"]));
83
- assert.deepEqual(args.slice(0, 4), ["--model", "claude-opaque-model", "--verbose", "--agents"]);
83
+ assert.deepEqual(args.slice(0, 4), [
84
+ "--model",
85
+ "anthropic.routekit.opaque-model",
86
+ "--verbose",
87
+ "--agents"
88
+ ]);
84
89
  assert.deepEqual(claudeLaunchArgs(context(["--agents={}"])), [
85
90
  "--model",
86
- "claude-opaque-model",
91
+ "anthropic.routekit.opaque-model",
87
92
  "--agents={}"
88
93
  ]);
89
- assert.deepEqual(claudeLaunchArgs(context([], [])), ["--model", "claude-opaque-model"]);
94
+ assert.deepEqual(claudeLaunchArgs(context([], [])), [
95
+ "--model",
96
+ "anthropic.routekit.opaque-model"
97
+ ]);
90
98
  assert.deepEqual(claudeLaunchArgs(context(["--model", "claude-user-selected"], [])), [
91
99
  "--model",
92
100
  "claude-user-selected"
@@ -100,7 +108,12 @@ test("Claude launcher projects validated effort onto the picker model id", () =>
100
108
  reasoning: { mode: "effort", effort: "high" }
101
109
  }
102
110
  };
103
- assert.deepEqual(claudeLaunchArgs(withEffort), ["--model", "claude-sonnet-4-6:high"]);
111
+ assert.deepEqual(claudeLaunchArgs(withEffort), [
112
+ "--effort",
113
+ "high",
114
+ "--model",
115
+ "anthropic.routekit.claude-code/claude-sonnet-4-6"
116
+ ]);
104
117
  const crossProvider = {
105
118
  ...context([], [], "codex/gpt-5.5"),
106
119
  spec: {
@@ -108,7 +121,12 @@ test("Claude launcher projects validated effort onto the picker model id", () =>
108
121
  reasoning: { mode: "effort", effort: "deep" }
109
122
  }
110
123
  };
111
- assert.deepEqual(claudeLaunchArgs(crossProvider), ["--model", "claude-codex/gpt-5.5:deep"]);
124
+ assert.deepEqual(claudeLaunchArgs(crossProvider), [
125
+ "--effort",
126
+ "deep",
127
+ "--model",
128
+ "anthropic.routekit.codex/gpt-5.5"
129
+ ]);
112
130
  assert.deepEqual(claudeLaunchArgs({
113
131
  ...withEffort,
114
132
  spec: {
@@ -120,7 +138,7 @@ test("Claude launcher projects validated effort onto the picker model id", () =>
120
138
  test("Claude launches keep native session flags under native-client control", () => {
121
139
  assert.deepEqual(claudeLaunchArgs(context(["--continue", "--resume", "native-id"], [])), [
122
140
  "--model",
123
- "claude-opaque-model",
141
+ "anthropic.routekit.opaque-model",
124
142
  "--continue",
125
143
  "--resume",
126
144
  "native-id"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-tool-claude",
3
3
  "private": false,
4
- "version": "0.17.0",
4
+ "version": "0.17.2",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -28,11 +28,11 @@
28
28
  "dependencies": {
29
29
  "@anthropic-ai/claude-agent-sdk": "0.3.198",
30
30
  "zod": "4.4.3",
31
- "@velum-labs/routekit-contracts": "0.17.0",
32
- "@velum-labs/routekit-harness-core": "0.17.0",
33
- "@velum-labs/routekit-registry": "0.17.0",
34
- "@velum-labs/routekit-runtime": "0.17.0",
35
- "@velum-labs/routekit-tools": "0.17.0"
31
+ "@velum-labs/routekit-contracts": "0.17.2",
32
+ "@velum-labs/routekit-harness-core": "0.17.2",
33
+ "@velum-labs/routekit-registry": "0.17.2",
34
+ "@velum-labs/routekit-runtime": "0.17.2",
35
+ "@velum-labs/routekit-tools": "0.17.2"
36
36
  },
37
37
  "keywords": [
38
38
  "llm",