feat(claude): offer Mythos 5.1 when Claude Code advertises it (#6403)

* feat(claude): add Mythos 5.1 with its model capabilities

* feat(claude): gate Mythos on advertised model identity

* fix(claude): retain capabilities for explicitly configured Mythos

* test(usage): load SQLite before timed store tests

The first Windows OMP store case exceeded its five-second budget while subsequent equivalent cases completed under a second. Load the builtin during test collection so its cold initialization does not consume the first test deadline.

* test(workspaces): publish complete service environment fixtures

Waiting for file existence races with writeFileSync creating the file before writing its contents. Rename completed JSON snapshots into place so the existing readiness check cannot expose an empty file.
This commit is contained in:
Mohamed Boudra
2026-10-09 21:05:40 +01:00
committed by GitHub
parent a1da1b8f23
commit aec01ea605
9 changed files with 434 additions and 13 deletions
+2
View File
@@ -61,6 +61,8 @@ Core direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`code
Claude first-party model metadata lives in `packages/server/src/server/agent/providers/claude/model-manifest.ts`. When adding or updating a Claude model, update that manifest only; the model picker thinking options and Claude-specific feature gates are derived from the manifest. Do not add model-specific Claude capability lists in feature code.
Keep the curated Claude catalog: `supportedModels()` has historically omitted older versions and carries less metadata. Discovery is only an opt-in visibility gate, not a replacement catalog or an entitlement guarantee. Claude Code v2.1.197 added `resolvedModel` to disambiguate alias rows; a bare family alias does not establish access to a particular version. Explicit user-configured models must remain selectable when discovery fails or omits them.
Paseo tools are not implemented as MCP tools internally. They live in a shared tool catalog under `packages/server/src/server/agent/tools/`; MCP is only the fallback adapter. The daemon resolves `agents.providers.<provider>.paseoTools` by the exact provider ID. The catalog policy belongs to the caller: it filters the tools exposed to the current agent. When that agent calls `create_agent`, the child receives the policy for the child provider ID; the caller's policy is not inherited.
A provider that can register runtime tools directly should set `supportsNativePaseoTools: true` and consume the already-filtered `launchContext.paseoTools` in `createSession`/`resumeSession`. When native tools are present, `AgentManager` strips the internal Paseo MCP server from the provider launch config so the provider does not receive the same tools twice. Providers that only know MCP should keep `supportsMcpServers: true` and let the daemon inject `/mcp/agents`; the MCP server builds the same policy-filtered catalog for that caller. Filtering is enforced at catalog registration in both paths. Browser tools remain subject to the daemon browser-tools setting and browser-host availability.
@@ -426,6 +426,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
try {
const client = new ClaudeAgentClient({
logger,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
],
resolveBinary: async () => "/test/claude/bin",
resolveVersion: async () => "2.1.219",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: emptyConfigDir } },
@@ -438,6 +446,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5",
"claude-mythos-5-1",
"claude-fable-5-1",
"claude-fable-5",
"claude-fable-5[1m]",
@@ -472,6 +481,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
try {
const client = new ClaudeAgentClient({
logger,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
],
resolveVersion: async () => {
throw new Error("unrecognized version output");
},
@@ -495,6 +512,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
try {
const client = new ClaudeAgentClient({
logger,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
],
resolveBinary: async () => "/test/claude/bin",
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: emptyConfigDir } },
@@ -509,6 +534,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
};
expect(getThinkingIds("claude-opus-5")).toContain("ultracode");
expect(getThinkingIds("claude-mythos-5-1")).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
"ultracode",
]);
expect(getThinkingIds("claude-fable-5-1")).toContain("ultracode");
expect(getThinkingIds("claude-fable-5")).toContain("ultracode");
expect(getThinkingIds("claude-opus-4-8[1m]")).toContain("ultracode");
@@ -1232,6 +1265,7 @@ describe("ClaudeAgentSession features", () => {
test.each([
["supported model", "claude-opus-4-8", { type: "disabled" }, undefined],
["Mythos 5.1", "claude-mythos-5-1", { type: "adaptive", display: "summarized" }, "high"],
["Haiku 5.5", "claude-haiku-5-5", { type: "disabled" }, undefined],
["unsupported model", "claude-fable-5", { type: "adaptive", display: "summarized" }, "high"],
["custom model", "openrouter/anthropic/claude-opus-4-8", undefined, undefined],
@@ -76,6 +76,7 @@ import {
type ClaudeProviderOptions,
} from "./options.js";
import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
import { discoverClaudeModels } from "./model-discovery.js";
import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./query.js";
import {
realClaudeRewindSdk,
@@ -412,6 +413,7 @@ export interface ClaudeContentChunk {
}
interface ClaudeAgentClientOptions {
discoverModels?: typeof discoverClaudeModels;
defaults?: { agents?: Record<string, AgentDefinition> };
logger: Logger;
runtimeSettings?: ProviderRuntimeSettings;
@@ -1514,6 +1516,7 @@ export function readEventIdentifiers(message: SDKMessage): EventIdentifiers {
}
export class ClaudeAgentClient implements AgentClient {
private readonly discoverModels: typeof discoverClaudeModels;
readonly provider = "claude" as const;
readonly capabilities = CLAUDE_CAPABILITIES;
@@ -1526,6 +1529,7 @@ export class ClaudeAgentClient implements AgentClient {
private readonly rewindSdk: ClaudeRewindSdk;
constructor(options: ClaudeAgentClientOptions) {
this.discoverModels = options.discoverModels ?? discoverClaudeModels;
this.defaults = options.defaults;
this.logger = options.logger.child({ module: "agent", provider: "claude" });
this.runtimeSettings = options.runtimeSettings;
@@ -1608,8 +1612,35 @@ export class ClaudeAgentClient implements AgentClient {
this.logger.warn({ err: error }, "Failed to resolve Claude Code version for model catalog");
}
const env = this.buildProviderEnv();
let discoveredModelIds = new Set<string>();
try {
const advertisedModels = await runProviderRefreshActivity(context, "models", () =>
this.discoverModels({
resolveBinary: this.resolveBinary,
runtimeSettings: this.runtimeSettings,
env,
signal: context?.signal,
}),
);
discoveredModelIds = new Set(
advertisedModels
.map((model) => normalizeClaudeRuntimeModelId(model.resolvedModel ?? model.value))
.filter((id): id is string => id !== null),
);
} catch (error) {
context?.signal.throwIfAborted();
this.logger.warn(
{ err: error },
"Claude model discovery failed; omitting discovery-required models",
);
}
const models = await runProviderRefreshActivity(context, "settings", () =>
getClaudeModelsWithSettings(this.logger, claudeConfigDir(env), claudeCodeVersion),
getClaudeModelsWithSettings(
this.logger,
claudeConfigDir(env),
claudeCodeVersion,
discoveredModelIds,
),
);
const modeCatalog = claudeModeCatalog(env);
return {
@@ -0,0 +1,89 @@
import type { ChildProcess } from "node:child_process";
import os from "node:os";
import type { ModelInfo, Query, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import { raceProviderRefreshAbort } from "../../provider-refresh-deadline.js";
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
import { terminateWithTreeKill } from "../../../../utils/tree-kill.js";
import { claudeQuery, type ClaudeQueryInput, type ClaudeQueryContext } from "./query.js";
interface ClaudeModelDiscoveryOptions {
resolveBinary: () => Promise<string>;
runtimeSettings?: ProviderRuntimeSettings;
env: NodeJS.ProcessEnv;
signal?: AbortSignal;
}
type OpenDiscoveryQuery = (
input: ClaudeQueryInput,
context: ClaudeQueryContext,
) => Pick<Query, "supportedModels" | "close">;
// The curated catalog survives a failed or slow probe. This process never receives
// a prompt and must not persist a session or launch user MCP servers/hooks.
export async function discoverClaudeModels(
options: ClaudeModelDiscoveryOptions,
openQuery: OpenDiscoveryQuery = claudeQuery,
): Promise<ModelInfo[]> {
const controller = new AbortController();
const signal = options.signal
? AbortSignal.any([options.signal, controller.signal])
: controller.signal;
const timer = setTimeout(
() => controller.abort(new Error("Claude model discovery timed out")),
5_000,
);
let endInput!: () => void;
const ended = new Promise<void>((resolve) => {
endInput = resolve;
});
const input: AsyncIterable<SDKUserMessage> = {
[Symbol.asyncIterator]() {
return {
next: async () => {
await ended;
return { done: true, value: undefined };
},
};
},
};
let query: ReturnType<OpenDiscoveryQuery> | undefined;
let child: ChildProcess | undefined;
try {
signal.throwIfAborted();
const binary = await raceProviderRefreshAbort(signal, options.resolveBinary());
query = openQuery(
{
prompt: input,
options: {
pathToClaudeCodeExecutable: binary,
cwd: os.homedir(),
env: options.env,
persistSession: false,
tools: [],
mcpServers: {},
strictMcpConfig: true,
settingSources: ["user"],
settings: { disableAllHooks: true },
abortController: controller,
},
},
{
runtimeSettings: options.runtimeSettings,
onChildProcess: (process) => {
child = process;
},
},
);
return await raceProviderRefreshAbort(signal, query.supportedModels());
} finally {
clearTimeout(timer);
endInput();
try {
query?.close();
} finally {
if (child) {
await terminateWithTreeKill(child, { gracefulTimeoutMs: 200, forceTimeoutMs: 200 });
}
}
}
}
@@ -9,6 +9,7 @@ interface ClaudeModelManifestEntry {
description: string;
defaultPriority?: number;
minimumClaudeCodeVersion?: string;
requiresDiscovery?: boolean;
contextWindowMaxTokens?: number;
effortLevels?: readonly ClaudeEffortLevel[];
defaultThinkingOptionId?: ClaudeEffortLevel;
@@ -57,6 +58,14 @@ export const CLAUDE_MODEL_MANIFEST = [
supportsThinkingDisabled: true,
supportsFastMode: true,
},
{
id: "claude-mythos-5-1",
label: "Mythos 5.1",
requiresDiscovery: true,
description: "Mythos 5.1 · Requires verified access",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
},
{
id: "claude-fable-5-1",
label: "Fable 5.1",
@@ -217,9 +226,16 @@ function buildThinkingOptions(
return options;
}
export function getClaudeManifestModels(claudeCodeVersion?: string): AgentModelDefinition[] {
export function getClaudeManifestModels(
claudeCodeVersion?: string,
discoveredModelIds?: ReadonlySet<string>,
): AgentModelDefinition[] {
const availableModels: readonly ClaudeModelManifestEntry[] = CLAUDE_MODEL_MANIFEST.filter(
(model) => isModelAvailableInClaudeCode(model, claudeCodeVersion),
(model) =>
isModelAvailableInClaudeCode(model, claudeCodeVersion) &&
(discoveredModelIds === undefined ||
!("requiresDiscovery" in model && model.requiresDiscovery) ||
discoveredModelIds.has(model.id)),
);
const defaultModel = availableModels.reduce<ClaudeModelManifestEntry | undefined>(
(selected, candidate) =>
@@ -359,7 +375,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
}
const singleSegmentMatch = trimmed.match(
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
/^(?:claude[-_ ])?(mythos|fable|opus|sonnet|haiku)[-_ ]+(\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
);
if (singleSegmentMatch) {
return normalizeSingleSegmentClaudeModelId(
@@ -370,7 +386,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
}
const runtimeMatch = trimmed.match(
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
/^(?:claude[-_ ])?(mythos|fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
);
if (!runtimeMatch) {
return null;
@@ -403,7 +419,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
}
const runtimeMatch = trimmed.match(
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
/claude[-_ ](mythos|fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
);
if (runtimeMatch) {
const normalizedModelId = normalizeMajorMinorClaudeModelId(
@@ -418,7 +434,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
}
const singleSegmentMatch = trimmed.match(
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?/i,
/claude[-_ ](mythos|fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?/i,
);
if (!singleSegmentMatch) {
return null;
@@ -13,6 +13,8 @@ import {
parseClaudeCodeVersion,
resolveClaudeDisabledThinkingForModel,
} from "./model-manifest.js";
import { discoverClaudeModels } from "./model-discovery.js";
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js";
const createdClaudeConfigDirs: string[] = [];
@@ -43,6 +45,14 @@ function createCatalogClient(claudeCodeVersion = "2.1.293"): ClaudeAgentClient {
return new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => claudeCodeVersion,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
],
});
}
@@ -52,6 +62,7 @@ describe("getClaudeModels", () => {
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5-5",
"claude-opus-5",
"claude-mythos-5-1",
"claude-fable-5-1",
"claude-fable-5",
"claude-fable-5[1m]",
@@ -87,6 +98,7 @@ describe("getClaudeModels", () => {
new Map([
["claude-opus-5-5", 1_000_000],
["claude-opus-5", 1_000_000],
["claude-mythos-5-1", 1_000_000],
["claude-fable-5-1", 1_000_000],
["claude-fable-5", 1_000_000],
["claude-fable-5[1m]", 1_000_000],
@@ -221,6 +233,118 @@ describe("getClaudeModels", () => {
});
describe("ClaudeAgentClient.fetchCatalog", () => {
it.each([
{ value: "mythos", resolvedModel: "claude-mythos-5-1", expected: true },
{ value: "claude-mythos-5-1", expected: true },
{ value: "claude-mythos-5-1[1m]", expected: true },
{ value: "global.anthropic.claude-mythos-5-1", expected: true },
{ value: "mythos", expected: false },
{ value: "mythos", resolvedModel: "claude-mythos-5-2", expected: false },
{ value: "claude-mythos-5-1", resolvedModel: "claude-fable-5-1", expected: false },
])(
"uses advertised identity $value / $resolvedModel to gate Mythos",
async ({ value, resolvedModel, expected }) => {
const configDir = await createClaudeConfigDir({});
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => [
{ value, resolvedModel, displayName: "Untrusted label", description: "" },
],
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: configDir,
force: false,
});
expect(models.find((model) => model.id === "claude-mythos-5-1")).toEqual(
expected ? findClaudeModel("claude-mythos-5-1") : undefined,
);
},
);
it("preserves the curated catalog and explicitly configured models after discovery failure", async () => {
const configDir = await createClaudeConfigDir({ model: "claude-mythos-5-1" });
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => {
throw new Error("discovery unavailable");
},
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: configDir,
force: false,
});
expect(models.filter((model) => model.id !== "claude-mythos-5-1")).toEqual(
getClaudeModels("2.1.293").filter((model) => model.id !== "claude-mythos-5-1"),
);
expect(models.find((model) => model.id === "claude-mythos-5-1")).toEqual({
...findClaudeModel("claude-mythos-5-1"),
label: "claude-mythos-5-1",
description: "From Claude settings.json model",
});
});
it("omits unconfirmed Mythos after a failed probe and retries on refresh", async () => {
const configDir = await createClaudeConfigDir({});
let fail = true;
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => {
if (fail) throw new Error("discovery unavailable");
return [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
];
},
});
const options = { scope: "workspace" as const, cwd: configDir, force: true };
expect(
(await client.fetchCatalog(options)).models.some((model) => model.id === "claude-mythos-5-1"),
).toBe(false);
fail = false;
expect(
(await client.fetchCatalog(options)).models.find((model) => model.id === "claude-mythos-5-1"),
).toEqual(findClaudeModel("claude-mythos-5-1"));
});
it("keeps the curated catalog when discovery does not advertise Mythos", async () => {
const configDir = await createClaudeConfigDir({});
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => [
{
value: "opus",
resolvedModel: "claude-opus-5-5",
displayName: "Opus",
description: "Latest",
},
],
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: configDir,
force: false,
});
expect(models.map((model) => model.id)).toEqual(
getClaudeModels("2.1.293")
.filter((model) => model.id !== "claude-mythos-5-1")
.map((model) => model.id),
);
});
it("appends concrete models from Claude settings.json", async () => {
const configDir = await createClaudeConfigDir({
model: "us.anthropic.claude-opus-4-7[1m]",
@@ -298,6 +422,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.280",
discoverModels: async () => [],
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: providerConfigDir } },
});
@@ -724,3 +849,124 @@ describe("claudeManifestModelSupportsFastMode", () => {
expect(claudeManifestModelSupportsFastMode("claude-fable-5-1")).toBe(false);
});
});
describe("Claude Mythos 5.1 catalog", () => {
it("offers one 1M model with high default effort and always-on thinking", () => {
const models = getClaudeModels().filter((model) => model.id.startsWith("claude-mythos"));
expect(models).toHaveLength(1);
expect(models[0]).toMatchObject({
id: "claude-mythos-5-1",
label: "Mythos 5.1",
contextWindowMaxTokens: 1_000_000,
defaultThinkingOptionId: "high",
});
expect(models[0].thinkingOptions?.map((option) => option.id)).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
]);
expect(
models[0].thinkingOptions?.filter((option) => option.isDefault).map((option) => option.id),
).toEqual(["high"]);
expect(resolveClaudeDisabledThinkingForModel("claude-mythos-5-1")).toEqual({
supported: false,
fallbackThinkingOptionId: "high",
});
expect(claudeManifestModelSupportsFastMode("claude-mythos-5-1")).toBe(false);
expect(getClaudeModels().find((model) => model.isDefault)?.id).toBe("claude-opus-5-5");
});
it.each([
"claude-mythos-5-1[1m]",
"claude-mythos-5-1-20260901",
"claude-mythos-5-1-20260901[1m]",
"Mythos 5.1",
])("resolves %s to the single catalog entry", (id) => {
expect(findClaudeModel(id)?.id).toBe("claude-mythos-5-1");
});
it.each(["anthropic/claude-mythos-5-1", "us.anthropic.claude-mythos-5-1-v1:0"])(
"normalizes runtime identity without granting first-party capabilities: %s",
(id) => {
expect(normalizeClaudeRuntimeModelId(id)).toBe("claude-mythos-5-1");
expect(normalizeClaudeManifestModelId(id)).toBeNull();
},
);
});
describe("Claude model discovery process", () => {
it("does not send a prompt or persist a session and closes after discovery", async () => {
let closed = false;
let promptEnded: Promise<IteratorResult<unknown>> | undefined;
const advertised: ModelInfo[] = [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
];
const models = await discoverClaudeModels(
{ resolveBinary: async () => "/claude", env: { CLAUDE_CONFIG_DIR: "/provider-config" } },
(input) => {
expect(input.options).toMatchObject({
pathToClaudeCodeExecutable: "/claude",
env: { CLAUDE_CONFIG_DIR: "/provider-config" },
persistSession: false,
tools: [],
mcpServers: {},
strictMcpConfig: true,
settingSources: ["user"],
settings: { disableAllHooks: true },
});
if (typeof input.prompt === "string") throw new Error("unexpected inference prompt");
promptEnded = input.prompt[Symbol.asyncIterator]().next();
return {
supportedModels: async () => advertised,
close: () => {
closed = true;
},
};
},
);
expect(models).toEqual(advertised);
expect(closed).toBe(true);
await expect(promptEnded).resolves.toEqual({ done: true, value: undefined });
});
it("times out and closes an unresponsive discovery process", async () => {
let closed = false;
await expect(
discoverClaudeModels({ resolveBinary: async () => "/claude", env: {} }, () => ({
supportedModels: () => new Promise(() => {}),
close: () => {
closed = true;
},
})),
).rejects.toThrow("Claude model discovery timed out");
expect(closed).toBe(true);
}, 10_000);
it("closes a stalled probe when its refresh is canceled", async () => {
const controller = new AbortController();
const failure = new Error("refresh canceled");
let closed = false;
const result = discoverClaudeModels(
{ resolveBinary: async () => "/claude", env: {}, signal: controller.signal },
() => ({
supportedModels: () => {
controller.abort(failure);
return new Promise(() => {});
},
close: () => {
closed = true;
},
}),
);
await expect(result).rejects.toBe(failure);
expect(closed).toBe(true);
});
});
@@ -52,8 +52,9 @@ export async function getClaudeModelsWithSettings(
logger: Logger,
configDir: string,
claudeCodeVersion?: string,
discoveredModelIds: ReadonlySet<string> = new Set(),
): Promise<AgentModelDefinition[]> {
const hardcodedModels = getClaudeModels(claudeCodeVersion);
const hardcodedModels = getClaudeManifestModels(claudeCodeVersion, discoveredModelIds);
const settingsModels = await readClaudeSettingsModels(logger, configDir);
if (settingsModels.length === 0) {
return hardcodedModels;
@@ -70,7 +71,10 @@ export async function getClaudeModelsWithSettings(
}
continue;
}
models.push(model);
const manifestModel = getClaudeModels(claudeCodeVersion).find(
(candidate) => candidate.id === model.id,
);
models.push({ ...manifestModel, ...model });
}
return models;
@@ -422,12 +422,12 @@ describe.skipIf(isPlatform("win32"))("worktree-bootstrap POSIX-only", () => {
api: {
type: "service",
command:
"node -e \"const fs=require('fs'); fs.writeFileSync('api-env.json', JSON.stringify(process.env)); setTimeout(()=>{}, 30000)\"",
"node -e \"const fs=require('fs'); fs.writeFileSync('api-env.json.tmp', JSON.stringify(process.env)); fs.renameSync('api-env.json.tmp', 'api-env.json'); setTimeout(()=>{}, 30000)\"",
},
web: {
type: "service",
command:
"node -e \"const fs=require('fs'); fs.writeFileSync('web-env.json', JSON.stringify(process.env)); setTimeout(()=>{}, 30000)\"",
"node -e \"const fs=require('fs'); fs.writeFileSync('web-env.json.tmp', JSON.stringify(process.env)); fs.renameSync('web-env.json.tmp', 'web-env.json'); setTimeout(()=>{}, 30000)\"",
},
},
}),
@@ -1,6 +1,7 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, beforeEach, expect, test } from "vitest";
import { discover, fetchUsage } from "./usage.js";
@@ -43,7 +44,6 @@ async function logins(options: Parameters<typeof discover>[1]) {
}
async function database(path: string) {
await mkdir((await import("node:path")).dirname(path), { recursive: true });
const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(path);
db.exec(
"CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)",
@@ -248,7 +248,6 @@ test("OMP fetch re-reads its row after rotation and skips a newly disabled login
const path = join(home, ".omp", "agent", "agent.db");
await database(path);
const [input] = await logins(lookup());
const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(path);
db.prepare("UPDATE auth_credentials SET data = ? WHERE id = 1").run(
JSON.stringify({ access: "fixture-rotated-omp", accountId: "omp-account", expires: 2000 }),