From 5bf53990ba37a7bc8a6e08ae1d3981fd795ecdb8 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Fri, 17 Jul 2026 11:31:37 +0200 Subject: [PATCH 1/6] Group model picker by family for a logical order Sort the model picker by family (tier) first, then oldest-to-newest within each family, instead of by raw version number alone. Sorting by version alone interleaves families (e.g. a Sonnet 5 lands between Opus versions), which reads as an arbitrary order. Grouping keeps every family contiguous. Generated-By: PostHog Code Task-Id: ea8239ab-8fc1-46b7-8dc6-d3661caf8800 --- packages/agent/src/adapters/base-acp-agent.ts | 12 ++++------ packages/agent/src/gateway-models.test.ts | 18 +++++++++----- packages/agent/src/gateway-models.ts | 24 +++++++++++++++++++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/agent/src/adapters/base-acp-agent.ts b/packages/agent/src/adapters/base-acp-agent.ts index 249b3655e9..ea7758942b 100644 --- a/packages/agent/src/adapters/base-acp-agent.ts +++ b/packages/agent/src/adapters/base-acp-agent.ts @@ -18,11 +18,11 @@ import type { } from "@agentclientprotocol/sdk"; import { restrictedModelMeta } from "@posthog/shared"; import { + compareModelsForPicker, DEFAULT_GATEWAY_MODEL, fetchGatewayModels, formatGatewayModelName, type GatewayModel, - getClaudeModelRecency, isAnthropicModel, isCloudflareModel, isCloudflareModelId, @@ -163,12 +163,10 @@ export abstract class BaseAcpAgent implements Agent { // silently dropping them. ...(model.allowed ? {} : { _meta: restrictedModelMeta() }), })) - // Sort oldest-to-newest so the picker is deterministic and the newest - // model lands at the end of the list, closest to the trigger. - .sort( - (a, b) => - getClaudeModelRecency(a.value) - getClaudeModelRecency(b.value), - ); + // Group by family, then oldest-to-newest within each family, so the + // picker is deterministic and reads logically instead of interleaving + // families by raw version number. + .sort((a, b) => compareModelsForPicker(a.value, b.value)); // Models the Claude adapter can drive: Anthropic ids, plus Cloudflare `@cf/` ids the gateway // serves over its Anthropic-Messages surface. Anything else (e.g. a Codex/GPT id) is a genuine diff --git a/packages/agent/src/gateway-models.test.ts b/packages/agent/src/gateway-models.test.ts index bd5d34dab0..bcb00e45b8 100644 --- a/packages/agent/src/gateway-models.test.ts +++ b/packages/agent/src/gateway-models.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { + compareModelsForPicker, fetchGatewayModels, fetchModelsList, formatGatewayModelName, @@ -116,24 +117,29 @@ describe("getClaudeModelRecency", () => { ); }); - it("produces the full picker display order, oldest to newest", () => { +}); + +describe("compareModelsForPicker", () => { + it("groups models by family, then orders each family oldest to newest", () => { // Models as the gateway might return them — arbitrary order. const gatewayOrder = [ "claude-fable-5", "claude-opus-4-8", "claude-mystery", + "claude-sonnet-5", "claude-haiku-4-5", "claude-sonnet-4-6", "claude-opus-4-7", ]; - const displayed = [...gatewayOrder].sort( - (a, b) => getClaudeModelRecency(a) - getClaudeModelRecency(b), - ); - // The menu opens upward, so the newest model (last here) sits closest to - // the trigger. Unknown/unversioned models rank newest and trail the list. + const displayed = [...gatewayOrder].sort(compareModelsForPicker); + // Each family stays contiguous (both Sonnets together, both Opuses + // together) instead of interleaving by raw version number. The menu opens + // upward, so the newest flagship family sits closest to the trigger, and + // unknown/non-Anthropic models trail the known families. expect(displayed).toEqual([ "claude-haiku-4-5", "claude-sonnet-4-6", + "claude-sonnet-5", "claude-opus-4-7", "claude-opus-4-8", "claude-fable-5", diff --git a/packages/agent/src/gateway-models.ts b/packages/agent/src/gateway-models.ts index 62a379665d..ce088ffb32 100644 --- a/packages/agent/src/gateway-models.ts +++ b/packages/agent/src/gateway-models.ts @@ -276,6 +276,30 @@ export function getClaudeModelRecency(modelId: string): number { return major * 1000 + minor; } +// Anthropic model families ordered by tier, fastest/smallest first. The picker +// menu opens upward (side="top"), so families later in this list sit closer to +// the trigger. +const MODEL_FAMILY_ORDER = ["haiku", "sonnet", "opus", "fable"]; + +function getModelFamilyRank(modelId: string): number { + const id = modelId.toLowerCase(); + const index = MODEL_FAMILY_ORDER.findIndex((family) => id.includes(family)); + // Non-Anthropic-family models (e.g. Cloudflare `@cf/...`) group after the + // known families. + return index === -1 ? MODEL_FAMILY_ORDER.length : index; +} + +// Comparator for the model picker. Groups models by family (tier) first, then +// orders each family's versions oldest-to-newest. Sorting by version alone +// interleaves families (e.g. a Sonnet 5 lands between Opus versions), which +// reads as an arbitrary order; grouping keeps every family contiguous and the +// newest flagship closest to the trigger. +export function compareModelsForPicker(a: string, b: string): number { + const familyDiff = getModelFamilyRank(a) - getModelFamilyRank(b); + if (familyDiff !== 0) return familyDiff; + return getClaudeModelRecency(a) - getClaudeModelRecency(b); +} + const PROVIDER_PREFIXES = ["anthropic/", "openai/", "google-vertex/"]; export function formatGatewayModelName(model: GatewayModel): string { From 0ffe380db5fd88cbed36ce8c785ffa52ae900d3c Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Fri, 17 Jul 2026 11:58:42 +0200 Subject: [PATCH 2/6] Invert family order (Fable first) and uppercase GPT/GLM names Order model families most-capable first (Fable, Opus, Sonnet, Haiku) and display OpenAI/Cloudflare model names with their acronym uppercased (gpt-5.5 -> GPT-5.5, glm-5.2 -> GLM-5.2). Generated-By: PostHog Code Task-Id: ea8239ab-8fc1-46b7-8dc6-d3661caf8800 --- packages/agent/src/gateway-models.test.ts | 30 +++++++++++------------ packages/agent/src/gateway-models.ts | 20 +++++++++------ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/agent/src/gateway-models.test.ts b/packages/agent/src/gateway-models.test.ts index bcb00e45b8..13f33e9f64 100644 --- a/packages/agent/src/gateway-models.test.ts +++ b/packages/agent/src/gateway-models.test.ts @@ -35,7 +35,7 @@ describe("formatGatewayModelName", () => { ).toBe("Claude Opus 4.8"); }); - it("formats OpenAI models as raw lowercase model ids", () => { + it("uppercases the GPT acronym in OpenAI model ids", () => { expect( formatGatewayModelName({ id: "GPT-5.5", @@ -45,23 +45,23 @@ describe("formatGatewayModelName", () => { supports_vision: true, allowed: true, }), - ).toBe("gpt-5.5"); + ).toBe("GPT-5.5"); }); - it("strips the openai/ prefix from OpenAI model ids", () => { + it("strips the openai/ prefix and uppercases GPT", () => { expect( formatGatewayModelName({ - id: "openai/gpt-5.5", + id: "openai/gpt-5.6-sol", owned_by: "openai", context_window: 200000, supports_streaming: true, supports_vision: true, allowed: true, }), - ).toBe("gpt-5.5"); + ).toBe("GPT-5.6-sol"); }); - it("formats Cloudflare models as the lowercase final path segment", () => { + it("formats Cloudflare models as the final path segment with GLM uppercased", () => { expect( formatGatewayModelName({ id: "@cf/zai-org/glm-5.2", @@ -71,7 +71,7 @@ describe("formatGatewayModelName", () => { supports_vision: false, allowed: true, }), - ).toBe("glm-5.2"); + ).toBe("GLM-5.2"); }); it("blocks deprecated Claude gateway models", () => { @@ -132,17 +132,17 @@ describe("compareModelsForPicker", () => { "claude-opus-4-7", ]; const displayed = [...gatewayOrder].sort(compareModelsForPicker); - // Each family stays contiguous (both Sonnets together, both Opuses - // together) instead of interleaving by raw version number. The menu opens - // upward, so the newest flagship family sits closest to the trigger, and - // unknown/non-Anthropic models trail the known families. + // Families are ordered most-capable first and each stays contiguous (both + // Sonnets together, both Opuses together) instead of interleaving by raw + // version number. Within a family, versions run oldest-to-newest. + // Unknown/non-Anthropic models trail the known families. expect(displayed).toEqual([ - "claude-haiku-4-5", - "claude-sonnet-4-6", - "claude-sonnet-5", + "claude-fable-5", "claude-opus-4-7", "claude-opus-4-8", - "claude-fable-5", + "claude-sonnet-4-6", + "claude-sonnet-5", + "claude-haiku-4-5", "claude-mystery", ]); }); diff --git a/packages/agent/src/gateway-models.ts b/packages/agent/src/gateway-models.ts index ce088ffb32..b4c7fb8a52 100644 --- a/packages/agent/src/gateway-models.ts +++ b/packages/agent/src/gateway-models.ts @@ -276,10 +276,8 @@ export function getClaudeModelRecency(modelId: string): number { return major * 1000 + minor; } -// Anthropic model families ordered by tier, fastest/smallest first. The picker -// menu opens upward (side="top"), so families later in this list sit closer to -// the trigger. -const MODEL_FAMILY_ORDER = ["haiku", "sonnet", "opus", "fable"]; +// Anthropic model families ordered by tier, most capable first. +const MODEL_FAMILY_ORDER = ["fable", "opus", "sonnet", "haiku"]; function getModelFamilyRank(modelId: string): number { const id = modelId.toLowerCase(); @@ -292,8 +290,7 @@ function getModelFamilyRank(modelId: string): number { // Comparator for the model picker. Groups models by family (tier) first, then // orders each family's versions oldest-to-newest. Sorting by version alone // interleaves families (e.g. a Sonnet 5 lands between Opus versions), which -// reads as an arbitrary order; grouping keeps every family contiguous and the -// newest flagship closest to the trigger. +// reads as an arbitrary order; grouping keeps every family contiguous. export function compareModelsForPicker(a: string, b: string): number { const familyDiff = getModelFamilyRank(a) - getModelFamilyRank(b); if (familyDiff !== 0) return familyDiff; @@ -302,13 +299,20 @@ export function compareModelsForPicker(a: string, b: string): number { const PROVIDER_PREFIXES = ["anthropic/", "openai/", "google-vertex/"]; +// Uppercase the leading acronym in a model id so provider prefixes read as +// their brand (e.g. "gpt-5.5" -> "GPT-5.5", "glm-5.2" -> "GLM-5.2"). +function uppercaseLeadingAcronym(name: string): string { + return name.replace(/^[a-z]+/, (prefix) => prefix.toUpperCase()); +} + export function formatGatewayModelName(model: GatewayModel): string { if (isCloudflareModel(model)) { - return (model.id.split("/").pop() ?? model.id).toLowerCase(); + const name = (model.id.split("/").pop() ?? model.id).toLowerCase(); + return uppercaseLeadingAcronym(name); } if (isOpenAIModel(model)) { - return stripProviderPrefix(model.id).toLowerCase(); + return uppercaseLeadingAcronym(stripProviderPrefix(model.id).toLowerCase()); } return formatModelId(model.id); From 58466341a0940029835ce949222a4bbc4bd503e5 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Fri, 17 Jul 2026 12:14:26 +0200 Subject: [PATCH 3/6] Fix formatting in gateway-models test Remove a stray blank line flagged by `biome ci` in the quality check. Generated-By: PostHog Code Task-Id: ea8239ab-8fc1-46b7-8dc6-d3661caf8800 --- packages/agent/src/gateway-models.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/agent/src/gateway-models.test.ts b/packages/agent/src/gateway-models.test.ts index 13f33e9f64..7949834830 100644 --- a/packages/agent/src/gateway-models.test.ts +++ b/packages/agent/src/gateway-models.test.ts @@ -116,7 +116,6 @@ describe("getClaudeModelRecency", () => { getClaudeModelRecency("claude-fable-5"), ); }); - }); describe("compareModelsForPicker", () => { From 5fee4ab02ee1105e2eb3847a7b7093da99461fd3 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Fri, 17 Jul 2026 12:22:15 +0200 Subject: [PATCH 4/6] Order newest version first, format provider suffixes, trim comments Sort each family newest-version-first (so Opus 4.8 sits above 4.7 with Fable at the top of the picker), render OpenAI/Cloudflare model names with the acronym uppercased, version attached, and any codename suffix title-cased (gpt-5.6-sol -> "GPT-5.6 Sol"), and trim the code comments to a minimum. Generated-By: PostHog Code Task-Id: ea8239ab-8fc1-46b7-8dc6-d3661caf8800 --- packages/agent/src/adapters/base-acp-agent.ts | 3 -- packages/agent/src/gateway-models.test.ts | 18 +++----- packages/agent/src/gateway-models.ts | 43 ++++++++----------- 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/packages/agent/src/adapters/base-acp-agent.ts b/packages/agent/src/adapters/base-acp-agent.ts index ea7758942b..243228a65a 100644 --- a/packages/agent/src/adapters/base-acp-agent.ts +++ b/packages/agent/src/adapters/base-acp-agent.ts @@ -163,9 +163,6 @@ export abstract class BaseAcpAgent implements Agent { // silently dropping them. ...(model.allowed ? {} : { _meta: restrictedModelMeta() }), })) - // Group by family, then oldest-to-newest within each family, so the - // picker is deterministic and reads logically instead of interleaving - // families by raw version number. .sort((a, b) => compareModelsForPicker(a.value, b.value)); // Models the Claude adapter can drive: Anthropic ids, plus Cloudflare `@cf/` ids the gateway diff --git a/packages/agent/src/gateway-models.test.ts b/packages/agent/src/gateway-models.test.ts index 7949834830..a4749cf264 100644 --- a/packages/agent/src/gateway-models.test.ts +++ b/packages/agent/src/gateway-models.test.ts @@ -48,7 +48,7 @@ describe("formatGatewayModelName", () => { ).toBe("GPT-5.5"); }); - it("strips the openai/ prefix and uppercases GPT", () => { + it("strips the openai/ prefix, uppercases GPT, and title-cases the suffix", () => { expect( formatGatewayModelName({ id: "openai/gpt-5.6-sol", @@ -58,7 +58,7 @@ describe("formatGatewayModelName", () => { supports_vision: true, allowed: true, }), - ).toBe("GPT-5.6-sol"); + ).toBe("GPT-5.6 Sol"); }); it("formats Cloudflare models as the final path segment with GLM uppercased", () => { @@ -119,28 +119,24 @@ describe("getClaudeModelRecency", () => { }); describe("compareModelsForPicker", () => { - it("groups models by family, then orders each family oldest to newest", () => { + it("groups models by family, most capable first, newest version first", () => { // Models as the gateway might return them — arbitrary order. const gatewayOrder = [ "claude-fable-5", - "claude-opus-4-8", + "claude-opus-4-7", "claude-mystery", "claude-sonnet-5", "claude-haiku-4-5", "claude-sonnet-4-6", - "claude-opus-4-7", + "claude-opus-4-8", ]; const displayed = [...gatewayOrder].sort(compareModelsForPicker); - // Families are ordered most-capable first and each stays contiguous (both - // Sonnets together, both Opuses together) instead of interleaving by raw - // version number. Within a family, versions run oldest-to-newest. - // Unknown/non-Anthropic models trail the known families. expect(displayed).toEqual([ "claude-fable-5", - "claude-opus-4-7", "claude-opus-4-8", - "claude-sonnet-4-6", + "claude-opus-4-7", "claude-sonnet-5", + "claude-sonnet-4-6", "claude-haiku-4-5", "claude-mystery", ]); diff --git a/packages/agent/src/gateway-models.ts b/packages/agent/src/gateway-models.ts index b4c7fb8a52..4f247c0545 100644 --- a/packages/agent/src/gateway-models.ts +++ b/packages/agent/src/gateway-models.ts @@ -258,16 +258,8 @@ export function getProviderName(ownedBy: string): string { return PROVIDER_NAMES[ownedBy] ?? ownedBy; } -// Sort key for ordering models oldest-to-newest in pickers. The model menu -// opens upward (side="top"), so the last item sits closest to the trigger — -// sorting ascending by this key puts the newest model right under the user's -// cursor. The key is the version embedded in the model id, e.g. -// "claude-sonnet-4-6" -> 4006, "claude-opus-4-8" -> 4008, "claude-fable-5" -> -// 5000; a higher number means a newer model. An id with no recognisable -// version (a brand-new or unexpected release) ranks as newest so it still -// surfaces at the end rather than at an arbitrary gateway-determined position. -// Only the first version group is read, so a trailing date suffix (e.g. -// "-20251001") is ignored; the minor component is assumed to be < 1000. +// Version embedded in the model id, e.g. "claude-opus-4-8" -> 4008. Ids with no +// recognisable version rank newest. A trailing date suffix is ignored. export function getClaudeModelRecency(modelId: string): number { const match = modelId.toLowerCase().match(/-(\d+)(?:[-.](\d+))?/); if (!match) return Number.MAX_SAFE_INTEGER; @@ -276,43 +268,44 @@ export function getClaudeModelRecency(modelId: string): number { return major * 1000 + minor; } -// Anthropic model families ordered by tier, most capable first. +// Families ordered most-capable first; unknown families sort last. const MODEL_FAMILY_ORDER = ["fable", "opus", "sonnet", "haiku"]; function getModelFamilyRank(modelId: string): number { const id = modelId.toLowerCase(); const index = MODEL_FAMILY_ORDER.findIndex((family) => id.includes(family)); - // Non-Anthropic-family models (e.g. Cloudflare `@cf/...`) group after the - // known families. return index === -1 ? MODEL_FAMILY_ORDER.length : index; } -// Comparator for the model picker. Groups models by family (tier) first, then -// orders each family's versions oldest-to-newest. Sorting by version alone -// interleaves families (e.g. a Sonnet 5 lands between Opus versions), which -// reads as an arbitrary order; grouping keeps every family contiguous. +// Group by family, then newest version first within each family. export function compareModelsForPicker(a: string, b: string): number { const familyDiff = getModelFamilyRank(a) - getModelFamilyRank(b); if (familyDiff !== 0) return familyDiff; - return getClaudeModelRecency(a) - getClaudeModelRecency(b); + return getClaudeModelRecency(b) - getClaudeModelRecency(a); } const PROVIDER_PREFIXES = ["anthropic/", "openai/", "google-vertex/"]; -// Uppercase the leading acronym in a model id so provider prefixes read as -// their brand (e.g. "gpt-5.5" -> "GPT-5.5", "glm-5.2" -> "GLM-5.2"). -function uppercaseLeadingAcronym(name: string): string { - return name.replace(/^[a-z]+/, (prefix) => prefix.toUpperCase()); +// Uppercase the acronym, keep the version attached, title-case any suffix: +// "gpt-5.6-sol" -> "GPT-5.6 Sol", "glm-5.2" -> "GLM-5.2". +function formatProviderModelName(modelId: string): string { + const [acronym, version, ...suffix] = modelId.split("-"); + const head = version + ? `${acronym.toUpperCase()}-${version}` + : acronym.toUpperCase(); + const tail = suffix.map( + (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ); + return [head, ...tail].join(" "); } export function formatGatewayModelName(model: GatewayModel): string { if (isCloudflareModel(model)) { - const name = (model.id.split("/").pop() ?? model.id).toLowerCase(); - return uppercaseLeadingAcronym(name); + return formatProviderModelName(model.id.split("/").pop() ?? model.id); } if (isOpenAIModel(model)) { - return uppercaseLeadingAcronym(stripProviderPrefix(model.id).toLowerCase()); + return formatProviderModelName(stripProviderPrefix(model.id)); } return formatModelId(model.id); From c520469a33d0a7773e445902b895821cefbb102c Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Fri, 17 Jul 2026 12:30:30 +0200 Subject: [PATCH 5/6] Keep picker order in the Claude availableModels allowlist The Claude adapter's availableModels allowlist rebuilt the picker in the order the models were listed in settings, overriding the family/version sort (Codex, which does not apply the allowlist, was already correct). Re-sort the filtered list with the picker comparator so the allowlist only restricts which models appear, not their order. Generated-By: PostHog Code Task-Id: ea8239ab-8fc1-46b7-8dc6-d3661caf8800 --- .../src/adapters/claude/session/model-config.test.ts | 12 ++++++++++++ .../src/adapters/claude/session/model-config.ts | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/packages/agent/src/adapters/claude/session/model-config.test.ts b/packages/agent/src/adapters/claude/session/model-config.test.ts index 98b0b2ddf0..5561454baa 100644 --- a/packages/agent/src/adapters/claude/session/model-config.test.ts +++ b/packages/agent/src/adapters/claude/session/model-config.test.ts @@ -19,6 +19,18 @@ describe("applyAvailableModelsAllowlist", () => { ).toEqual(rawModelOptions); }); + it("reorders the allowlist to the picker order instead of the listed order", () => { + expect( + applyAvailableModelsAllowlist(rawModelOptions, [ + "claude-sonnet-4-6", + "claude-opus-4-8", + ]).options, + ).toEqual([ + { value: "claude-opus-4-8", name: "Claude Opus 4.8" }, + { value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, + ]); + }); + it("switches the current model when the previous one is filtered out", () => { expect( applyAvailableModelsAllowlist(rawModelOptions, ["claude-sonnet-4-6"]), diff --git a/packages/agent/src/adapters/claude/session/model-config.ts b/packages/agent/src/adapters/claude/session/model-config.ts index f83ed1b717..d2f754730f 100644 --- a/packages/agent/src/adapters/claude/session/model-config.ts +++ b/packages/agent/src/adapters/claude/session/model-config.ts @@ -1,4 +1,5 @@ import type { SessionConfigSelectOption } from "@agentclientprotocol/sdk"; +import { compareModelsForPicker } from "../../../gateway-models"; export interface ModelConfigOptions { currentModelId: string; @@ -30,6 +31,9 @@ export function applyAvailableModelsAllowlist( if (filtered.length === 0) return modelOptions; + // The allowlist is a filter, not a display order: keep the picker's ordering. + filtered.sort((a, b) => compareModelsForPicker(a.value, b.value)); + const currentModelId = filtered.some( (o) => o.value === modelOptions.currentModelId, ) From ca6a9fa01a44f5fac22f4ffff7fca3866f3bd68e Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Fri, 17 Jul 2026 12:47:26 +0200 Subject: [PATCH 6/6] Only uppercase known provider acronyms (GPT, GLM) Limit acronym uppercasing to GPT and GLM so ordinary Cloudflare model names (e.g. llama-3.1-8b-instruct) are not mangled into misleading all-caps labels. Addresses a Greptile review finding. Generated-By: PostHog Code Task-Id: ea8239ab-8fc1-46b7-8dc6-d3661caf8800 --- packages/agent/src/gateway-models.test.ts | 13 +++++++++++++ packages/agent/src/gateway-models.ts | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/agent/src/gateway-models.test.ts b/packages/agent/src/gateway-models.test.ts index a4749cf264..a92614c462 100644 --- a/packages/agent/src/gateway-models.test.ts +++ b/packages/agent/src/gateway-models.test.ts @@ -74,6 +74,19 @@ describe("formatGatewayModelName", () => { ).toBe("GLM-5.2"); }); + it("leaves non-acronym Cloudflare models lowercase", () => { + expect( + formatGatewayModelName({ + id: "@cf/meta/llama-3.1-8b-instruct", + owned_by: "cloudflare", + context_window: 128000, + supports_streaming: true, + supports_vision: false, + allowed: true, + }), + ).toBe("llama-3.1-8b-instruct"); + }); + it("blocks deprecated Claude gateway models", () => { expect(isBlockedModelId("claude-opus-4-5")).toBe(true); expect(isBlockedModelId("claude-opus-4-6")).toBe(true); diff --git a/packages/agent/src/gateway-models.ts b/packages/agent/src/gateway-models.ts index 4f247c0545..9faf9e5f7d 100644 --- a/packages/agent/src/gateway-models.ts +++ b/packages/agent/src/gateway-models.ts @@ -286,10 +286,14 @@ export function compareModelsForPicker(a: string, b: string): number { const PROVIDER_PREFIXES = ["anthropic/", "openai/", "google-vertex/"]; -// Uppercase the acronym, keep the version attached, title-case any suffix: -// "gpt-5.6-sol" -> "GPT-5.6 Sol", "glm-5.2" -> "GLM-5.2". +const KNOWN_ACRONYMS = new Set(["gpt", "glm"]); + +// For a known acronym, uppercase it, keep the version attached, and title-case +// any suffix: "gpt-5.6-sol" -> "GPT-5.6 Sol", "glm-5.2" -> "GLM-5.2". Other ids +// stay lowercase to avoid mangling ordinary names (e.g. "llama-3.1-8b"). function formatProviderModelName(modelId: string): string { const [acronym, version, ...suffix] = modelId.split("-"); + if (!KNOWN_ACRONYMS.has(acronym.toLowerCase())) return modelId.toLowerCase(); const head = version ? `${acronym.toUpperCase()}-${version}` : acronym.toUpperCase();