mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
fix: match OpenRouter model ids in the gateway model listing (#1365)
The model picker was empty behind OpenRouter: `normalizeModelId` did not match `anthropic/claude-opus-5.5:batch[1m]` to `claude-opus-5-5`. It now strips a leading `anthropic/` and a `:variant` suffix, and maps `5.5` to `5-5`.
This commit is contained in:
@@ -264,25 +264,40 @@ function resolveSettingsModel(
|
||||
const MODEL_FAMILY_ALIASES = new Set(["opus", "sonnet", "haiku", "fable"]);
|
||||
|
||||
/** A model id without the spellings that name the same model: provider
|
||||
* prefixes, dates, version suffixes, `-fast` and context hints. `deniedModels`
|
||||
* and gateway listings both match on it. */
|
||||
* prefixes, dates, version suffixes, `:variant` suffixes, dotted versions,
|
||||
* `-fast` and context hints. `deniedModels` and gateway listings both match on
|
||||
* it. OpenRouter lists `anthropic/claude-opus-5.5:batch[1m]`, which normalizes
|
||||
* to `claude-opus-5-5`; `anthropic/openai/gpt-5` keeps its `openai/` prefix and
|
||||
* matches no Claude id. */
|
||||
function normalizeModelId(value: string): string {
|
||||
let id = stripContextHints(value.trim().toLowerCase());
|
||||
const providerPrefix = id.lastIndexOf("anthropic.");
|
||||
if (providerPrefix !== -1) id = id.slice(providerPrefix + "anthropic.".length);
|
||||
return id
|
||||
return (
|
||||
id
|
||||
// Only a leading `anthropic/`: OpenRouter's `anthropic/openai/gpt-5` must
|
||||
// keep `openai/`, which `lastIndexOf` would strip along with it.
|
||||
.replace(/^anthropic\//, "")
|
||||
// `:batch`, `:free`. A letter must follow the colon, so Bedrock's `-v1:0`
|
||||
// is left for the `-vN:M` step below.
|
||||
.replace(/:[a-z][a-z0-9-]*$/, "")
|
||||
// `claude-opus-5.5` -> `claude-opus-5-5`; only dots between two digits.
|
||||
.replace(/(?<=\d)\.(?=\d)/g, "-")
|
||||
.replace(/@.*$/, "")
|
||||
.replace(/-v\d+(?::\d+)?$/, "")
|
||||
.replace(/-fast$/, "")
|
||||
.replace(/-\d{8}$/, "");
|
||||
.replace(/-\d{8}$/, "")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether managed `deniedModels` blocks a model id, following the CLI's rules
|
||||
* closely enough for the picker: a family alias blocks the whole family, and a
|
||||
* model id blocks that version in every spelling plus the later minor versions
|
||||
* it prefixes (`claude-opus-5` also blocks `claude-opus-5-5`). The CLI stays
|
||||
* the enforcer; this only keeps the picker from offering a refused row.
|
||||
* it prefixes (`claude-opus-5` also blocks `claude-opus-5-5`). Entries share
|
||||
* `normalizeModelId`, so `claude-opus-4.5` or `anthropic/claude-opus-4.5` also
|
||||
* blocks `claude-opus-4-5`, which the CLI may still allow. The CLI stays the
|
||||
* enforcer; this only keeps the picker from offering a refused row.
|
||||
*/
|
||||
export function isDeniedModel(modelId: string, deniedModels: readonly string[]): boolean {
|
||||
const id = normalizeModelId(modelId);
|
||||
|
||||
@@ -280,9 +280,46 @@ describe("narrowToGatewayModels", () => {
|
||||
expect(narrowed.map((m) => m.value)).toEqual(["sonnet"]);
|
||||
});
|
||||
|
||||
it("matches OpenRouter ids: vendor prefix, dotted version and :variant suffix", () => {
|
||||
const models: ModelInfo[] = [
|
||||
{
|
||||
value: "default",
|
||||
resolvedModel: "claude-opus-5-5[1m]",
|
||||
displayName: "Default",
|
||||
description: "",
|
||||
},
|
||||
{ value: "opus", displayName: "Opus", description: "" },
|
||||
{
|
||||
value: "sonnet",
|
||||
resolvedModel: "claude-sonnet-5-5",
|
||||
displayName: "Sonnet",
|
||||
description: "",
|
||||
},
|
||||
{ value: "haiku", resolvedModel: "claude-haiku-4-5", displayName: "Haiku", description: "" },
|
||||
{ value: "fable", resolvedModel: "claude-fable-5-1", displayName: "Fable", description: "" },
|
||||
];
|
||||
|
||||
const narrowed = narrowToGatewayModels(models, [
|
||||
"anthropic/claude-opus-5.5:batch[1m]",
|
||||
"anthropic/claude-opus-5.5[1m]",
|
||||
"anthropic/claude-sonnet-5.5[1m]",
|
||||
"anthropic/openai/gpt-6.1-sol:batch[1m]",
|
||||
"anthropic/google/gemini-4-argon[1m]",
|
||||
]);
|
||||
|
||||
expect(narrowed.map((m) => m.value)).toEqual(["default", "opus", "sonnet"]);
|
||||
});
|
||||
|
||||
it("matches a model OpenRouter lists only as a :variant", () => {
|
||||
const narrowed = narrowToGatewayModels(MODELS, ["anthropic/claude-sonnet-5:batch[1m]"]);
|
||||
|
||||
expect(narrowed.map((m) => m.value)).toEqual(["sonnet", "sonnet[1m]"]);
|
||||
});
|
||||
|
||||
it("leaves no model when the listing matches none", () => {
|
||||
expect(narrowToGatewayModels(MODELS, ["gpt-5", "gemini-3-pro"])).toEqual([]);
|
||||
expect(narrowToGatewayModels(MODELS, [])).toEqual([]);
|
||||
expect(narrowToGatewayModels(MODELS, ["anthropic/openai/gpt-5"])).toEqual([]);
|
||||
// A default row without resolvedModel is not a match on its own.
|
||||
expect(
|
||||
narrowToGatewayModels(
|
||||
|
||||
@@ -263,6 +263,14 @@ describe("isDeniedModel", () => {
|
||||
expect(isDeniedModel("us.anthropic.claude-opus-5-v1:0", denied)).toBe(true);
|
||||
expect(isDeniedModel("claude-opus-5-5", denied)).toBe(true);
|
||||
expect(isDeniedModel("claude-opus-4-8", denied)).toBe(false);
|
||||
expect(isDeniedModel("anthropic/claude-opus-5.5:batch[1m]", denied)).toBe(true);
|
||||
expect(isDeniedModel("anthropic/us.anthropic.claude-opus-5-v1:0", denied)).toBe(true);
|
||||
});
|
||||
|
||||
it("reads an entry in OpenRouter or dotted spelling as the dashed SDK id", () => {
|
||||
expect(isDeniedModel("claude-opus-4-5", ["claude-opus-4.5"])).toBe(true);
|
||||
expect(isDeniedModel("claude-opus-5-5", ["anthropic/claude-opus-5.5"])).toBe(true);
|
||||
expect(isDeniedModel("claude-opus-5", ["anthropic/claude-opus-5.5"])).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks only that version when the entry names a minor", () => {
|
||||
|
||||
Reference in New Issue
Block a user