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"]);
|
const MODEL_FAMILY_ALIASES = new Set(["opus", "sonnet", "haiku", "fable"]);
|
||||||
|
|
||||||
/** A model id without the spellings that name the same model: provider
|
/** A model id without the spellings that name the same model: provider
|
||||||
* prefixes, dates, version suffixes, `-fast` and context hints. `deniedModels`
|
* prefixes, dates, version suffixes, `:variant` suffixes, dotted versions,
|
||||||
* and gateway listings both match on it. */
|
* `-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 {
|
function normalizeModelId(value: string): string {
|
||||||
let id = stripContextHints(value.trim().toLowerCase());
|
let id = stripContextHints(value.trim().toLowerCase());
|
||||||
const providerPrefix = id.lastIndexOf("anthropic.");
|
const providerPrefix = id.lastIndexOf("anthropic.");
|
||||||
if (providerPrefix !== -1) id = id.slice(providerPrefix + "anthropic.".length);
|
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(/@.*$/, "")
|
||||||
.replace(/-v\d+(?::\d+)?$/, "")
|
.replace(/-v\d+(?::\d+)?$/, "")
|
||||||
.replace(/-fast$/, "")
|
.replace(/-fast$/, "")
|
||||||
.replace(/-\d{8}$/, "");
|
.replace(/-\d{8}$/, "")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether managed `deniedModels` blocks a model id, following the CLI's rules
|
* 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
|
* 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
|
* 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
|
* it prefixes (`claude-opus-5` also blocks `claude-opus-5-5`). Entries share
|
||||||
* the enforcer; this only keeps the picker from offering a refused row.
|
* `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 {
|
export function isDeniedModel(modelId: string, deniedModels: readonly string[]): boolean {
|
||||||
const id = normalizeModelId(modelId);
|
const id = normalizeModelId(modelId);
|
||||||
|
|||||||
@@ -280,9 +280,46 @@ describe("narrowToGatewayModels", () => {
|
|||||||
expect(narrowed.map((m) => m.value)).toEqual(["sonnet"]);
|
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", () => {
|
it("leaves no model when the listing matches none", () => {
|
||||||
expect(narrowToGatewayModels(MODELS, ["gpt-5", "gemini-3-pro"])).toEqual([]);
|
expect(narrowToGatewayModels(MODELS, ["gpt-5", "gemini-3-pro"])).toEqual([]);
|
||||||
expect(narrowToGatewayModels(MODELS, [])).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.
|
// A default row without resolvedModel is not a match on its own.
|
||||||
expect(
|
expect(
|
||||||
narrowToGatewayModels(
|
narrowToGatewayModels(
|
||||||
|
|||||||
@@ -263,6 +263,14 @@ describe("isDeniedModel", () => {
|
|||||||
expect(isDeniedModel("us.anthropic.claude-opus-5-v1:0", denied)).toBe(true);
|
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-5-5", denied)).toBe(true);
|
||||||
expect(isDeniedModel("claude-opus-4-8", denied)).toBe(false);
|
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", () => {
|
it("blocks only that version when the entry names a minor", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user