feat(platform): quantization variants as first-class selectable models#1697
Conversation
Models declaring providerOptions.provider.quantizations now expand into
one selectable model per variant in the chat picker, arena picker, and
agent supportedModels editor (e.g. GLM 5.1 fp8 and GLM 5.1 fp4 as
separate entries with FP8/FP4 badges). The chosen variant is encoded as
provider:base@quant in the model ref; resolveModelData strips the suffix
back to a bare modelId and pins providerOptions.provider.quantizations
to a single-element array, so OpenRouter receives the correct wire shape
({"model":"z-ai/glm-5.1","provider":{"quantizations":["fp8"]}}).
- model-ref: parseModelRef/formatModelRef extract @quant suffix; token
regex ^[a-z0-9]{1,16}$; stripModelRefQualifier returns bare id
- expand-model-variants: pure helper expands base refs into per-variant
entries (no plain base when quantizations declared)
- pinQuantization: clones merged providerOptions and narrows
provider.quantizations to [chosen]
- saveAgent + resolveModelData reject unknown variants with
UNKNOWN_MODEL_VARIANT listing the declared options
- listProviders / getAllModelIds / getAllConfiguredModelIds expose the
model's quantizations so the UI can expand client-side
- No JSON schema changes, no DB migrations, no new i18n keys; existing
base-id refs in agent JSON continue to resolve at runtime
Tests: 31 new vitest cases across model-ref, expand-model-variants, and
provider_options (including a composed merge → pin → namespace pipeline
test that locks the OpenRouter wire shape).
📝 WalkthroughWalkthroughThis PR implements model quantization variant support across the platform. It introduces utilities to parse model references with Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
services/platform/app/features/chat/components/model-selector.tsx (1)
260-263:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTrigger label drops the selected quantization — variants become indistinguishable when the dropdown is closed.
getDisplayNamecallsstripModelRefQualifier, which strips the@<quant>suffix, so pickingopenrouter:z-ai/glm-5.1@fp8versus…@fp4shows the samecurrentLabel("GLM 5.1") in the trigger. Since the UI rule of this PR is to force an explicit variant pick, the active variant should also be visible at rest — otherwise users have to reopen the picker to verify their selection.💡 Surface the variant alongside the current label
- const currentLabel = - currentModelId === AUTO_MODEL - ? t('modelSelector.auto') - : getDisplayName(currentModelId); + const currentLabel = + currentModelId === AUTO_MODEL + ? t('modelSelector.auto') + : getDisplayName(currentModelId); + const currentVariant = + currentModelId === AUTO_MODEL + ? undefined + : parseModelRef(currentModelId).quantization;…and in the trigger:
<Cpu className="size-3.5" aria-hidden="true" /> <span>{currentLabel}</span> + {currentVariant ? ( + <Badge variant="outline" className="text-[10px] font-normal"> + {getVariantBadgeLabel(currentVariant)} + </Badge> + ) : null} <ChevronDown className="size-3" aria-hidden="true" />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/platform/app/features/chat/components/model-selector.tsx` around lines 260 - 263, current trigger label drops the quantization suffix because getDisplayName uses stripModelRefQualifier, so currentModelId variants (e.g., openrouter:z-ai/glm-5.1@fp8 vs `@fp4`) look identical; update the logic that computes currentLabel (used with AUTO_MODEL and currentModelId) to include the variant/qualifier: call getDisplayName(currentModelId) for the base name and extract the qualifier from currentModelId (the "@..." suffix) and append it (or use a helper that preserves qualifiers) so the trigger shows "GLM 5.1 `@fp8`" vs "@fp4"; reference currentLabel, currentModelId, AUTO_MODEL, getDisplayName, and stripModelRefQualifier when locating the code to change.services/platform/app/features/chat/components/arena/arena-model-selector.tsx (1)
153-167:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTwo minor parity issues vs.
model-selector.tsx: stale selections aren't cleared, and trigger labels hide the variant.
Stale
modelA/modelBnot cleared when they fall outsidefilteredModels. If context already holds a base ref (e.g.openrouter:z-ai/glm-5.1) and the agent now declares quantizations,filteredModelsonly contains…@fp8/…@fp4. The syncuseEffectonly runs its assignments when!modelA/!modelB, so a stale selection persists and theSearchableSelect'svalueno longer matches any option.model-selector.tsxexplicitly clears non-matching overrides — apply the same here.Trigger labels strip the variant.
getDisplayName(currentModelA)runs throughstripModelRefQualifier, so the closed A/B triggers show identical text for sibling variants (e.g. both…@fp8and…@fp4render as "GLM 5.1"). Since the picker forces an explicit variant choice, the resting trigger should reflect it — same fix as suggested formodel-selector.tsx.💡 Add stale-selection cleanup and surface the variant
useEffect(() => { + if (modelA && !filteredModels.includes(modelA)) { + setModelA(filteredModels[0] ?? null); + } + if (modelB && !filteredModels.includes(modelB)) { + setModelB(filteredModels[1] ?? filteredModels[0] ?? null); + } if (filteredModels.length >= 2) { if (!modelA && filteredModels[0]) { setModelA(filteredModels[0]); } if (!modelB && filteredModels[1]) { setModelB(filteredModels[1]); } } }, [filteredModels, modelA, modelB, setModelA, setModelB]);For each trigger, render a small variant badge alongside the display name (mirroring the option-rendering branch already present in
options).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/platform/app/features/chat/components/arena/arena-model-selector.tsx` around lines 153 - 167, The current sync useEffect only sets modelA/modelB when they are empty but doesn’t clear them if their selected refs are no longer present in filteredModels; update the effect in arena-model-selector.tsx (the block using filteredModels, modelA, modelB, setModelA, setModelB) to also null out or replace modelA/modelB when their current value is not included in filteredModels (mirror the stale-selection cleanup from model-selector.tsx). Also change the trigger rendering that currently calls getDisplayName(currentModelA)/currentModelB (which uses stripModelRefQualifier) to render the display name plus a small variant badge showing the qualifier/variant from the model ref (use the same variant extraction logic used in the options branch so closed A/B triggers reflect the `@fp8/`@fp4 variant). Ensure SearchableSelect value still matches an option after these changes.services/platform/convex/providers/file_actions.ts (1)
581-608: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winConsider falling back to parsed
providerNamefor robustness.The code at line 581 discards
providerNamefromparseModelRef, relying instead onargs.providerName. This is safe because all callers properly extract the provider prefix from qualified refs (e.g.,"openrouter:gpt-4") and pass provider and modelId as separate arguments. However, to be defensive against future misuse, fall back to the parsed provider name whenargs.providerNameis unset:Suggested fallback
- const { modelId: bareModelId, quantization } = parseModelRef(args.modelId); + const { + providerName: parsedProviderName, + modelId: bareModelId, + quantization, + } = parseModelRef(args.modelId); + const effectiveProviderName = args.providerName ?? parsedProviderName; - const candidates = args.providerName - ? providers.filter((p) => p.name === args.providerName) + const candidates = effectiveProviderName + ? providers.filter((p) => p.name === effectiveProviderName) : providers;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/platform/convex/providers/file_actions.ts` around lines 581 - 608, The code currently ignores the parsed provider name from parseModelRef and only checks args.providerName; update resolve logic to fall back to the parsed providerName when args.providerName is falsy: after const { modelId: bareModelId, quantization } = parseModelRef(args.modelId) also capture the parsed provider (e.g., const { providerName: parsedProvider } = parseModelRef(...)) and use parsedProvider wherever args.providerName is tested or used (for example in the block that checks if (!args.providerName && secondaryMatchProviders.length > 0) and when forming the warning/qualification suggestion), so that when args.providerName is unset the code uses parsedProvider to disambiguate/pin the selection while preserving existing behavior when args.providerName is provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@services/platform/convex/providers/file_actions.ts`:
- Around line 388-391: listProviders, getAllModelIds, and
getAllConfiguredModelIds call readQuantizations(m.providerOptions) which only
sees model-level providerOptions while resolveModelData uses merged options via
mergeModelLevel(provider.config.providerOptions, definition.providerOptions),
causing provider-level quantizations to be ignored by the UI but honored at
runtime; fix by switching those callers to read quantizations from the merged
provider options (i.e., merge provider.config.providerOptions with
definition.providerOptions before calling readQuantizations) or alternatively
add a clear JSDoc/schema note on readQuantizations that quantizations must be
declared at the model level—update listProviders, getAllModelIds,
getAllConfiguredModelIds to use the same merge logic as resolveModelData (reuse
mergeModelLevel) so UI and runtime behavior match.
---
Outside diff comments:
In
`@services/platform/app/features/chat/components/arena/arena-model-selector.tsx`:
- Around line 153-167: The current sync useEffect only sets modelA/modelB when
they are empty but doesn’t clear them if their selected refs are no longer
present in filteredModels; update the effect in arena-model-selector.tsx (the
block using filteredModels, modelA, modelB, setModelA, setModelB) to also null
out or replace modelA/modelB when their current value is not included in
filteredModels (mirror the stale-selection cleanup from model-selector.tsx).
Also change the trigger rendering that currently calls
getDisplayName(currentModelA)/currentModelB (which uses stripModelRefQualifier)
to render the display name plus a small variant badge showing the
qualifier/variant from the model ref (use the same variant extraction logic used
in the options branch so closed A/B triggers reflect the `@fp8/`@fp4 variant).
Ensure SearchableSelect value still matches an option after these changes.
In `@services/platform/app/features/chat/components/model-selector.tsx`:
- Around line 260-263: current trigger label drops the quantization suffix
because getDisplayName uses stripModelRefQualifier, so currentModelId variants
(e.g., openrouter:z-ai/glm-5.1@fp8 vs `@fp4`) look identical; update the logic
that computes currentLabel (used with AUTO_MODEL and currentModelId) to include
the variant/qualifier: call getDisplayName(currentModelId) for the base name and
extract the qualifier from currentModelId (the "@..." suffix) and append it (or
use a helper that preserves qualifiers) so the trigger shows "GLM 5.1 `@fp8`" vs
"@fp4"; reference currentLabel, currentModelId, AUTO_MODEL, getDisplayName, and
stripModelRefQualifier when locating the code to change.
In `@services/platform/convex/providers/file_actions.ts`:
- Around line 581-608: The code currently ignores the parsed provider name from
parseModelRef and only checks args.providerName; update resolve logic to fall
back to the parsed providerName when args.providerName is falsy: after const {
modelId: bareModelId, quantization } = parseModelRef(args.modelId) also capture
the parsed provider (e.g., const { providerName: parsedProvider } =
parseModelRef(...)) and use parsedProvider wherever args.providerName is tested
or used (for example in the block that checks if (!args.providerName &&
secondaryMatchProviders.length > 0) and when forming the warning/qualification
suggestion), so that when args.providerName is unset the code uses
parsedProvider to disambiguate/pin the selection while preserving existing
behavior when args.providerName is provided.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 11437f6b-e9fe-4b9d-9ea3-b5d3e515a252
📒 Files selected for processing (10)
services/platform/app/features/chat/components/arena/arena-model-selector.tsxservices/platform/app/features/chat/components/model-selector.tsxservices/platform/app/routes/dashboard/$id/agents/$agentId/instructions.tsxservices/platform/convex/agents/file_actions.tsservices/platform/convex/lib/provider_options.test.tsservices/platform/convex/lib/provider_options.tsservices/platform/convex/providers/file_actions.tsservices/platform/lib/shared/utils/__tests__/expand-model-variants.test.tsservices/platform/lib/shared/utils/__tests__/model-ref.test.tsservices/platform/lib/shared/utils/expand-model-variants.ts
…d options
- resolveModelData now falls back to the parseModelRef-extracted provider
name when args.providerName is unset, so a fully-qualified
modelId like "openrouter:z-ai/glm-5.1@fp8" pins the lookup without a
redundant arg.
- listProviders / getAllModelIds / getAllConfiguredModelIds read
quantizations from merged provider+model providerOptions (via
readEffectiveQuantizations) so the UI's variant expansion matches what
resolveModelData would pin at runtime when a provider declares
quantizations at the top of the JSON.
- Chat & arena selectors: getDisplayName now appends the variant token
("GLM 5.1 (FP4)") so closed triggers and selected-row labels are
distinguishable between fp8 and fp4 picks.
- Arena: clear modelA/modelB when the persisted ref is no longer in the
expanded filteredModels list (e.g. governance tightened or expansion
replaced a bare ref with @-qualified variants), and use isInList to
guard the SearchableSelect value so it always matches an option.
Summary
providerOptions.provider.quantizationsentry becomes its own selectable model in the chat picker, arena picker, and agentsupportedModelseditor — e.g. GLM 5.1 with anFP8badge and GLM 5.1 with anFP4badge as separate entries.provider:base@quantend-to-end; on the wire to OpenRouter the model id is bare and the variant rides inprovider.quantizations: [chosen].Approach
provider:base-id@quant(e.g.openrouter:z-ai/glm-5.1@fp8). Token regex^[a-z0-9]{1,16}$so@followed by uppercase / punctuation / empty stays part of the modelId — parser is predictable, never half-parses.quantizations, selectors render only the variant entries (no unsplit base option), forcing an explicit pick.resolveModelDataparses the suffix, validates it against the declared array, callspinQuantizationto narrow the merged passthrough to a single element, returns the bare modelId.buildCallProviderOptionsnamespaces the result; the AI SDK spreads it as top-level body fields.governance.stripModelRefQualifiercontinues returning the bare id, so policies stay variant-agnostic.Wire shape (verified by composed test)
For
openrouter:z-ai/glm-5.1@fp8:{ "model": "z-ai/glm-5.1", "provider": { "quantizations": ["fp8"] } }Files
lib/shared/utils/model-ref.ts—parseModelRef/formatModelRef/stripModelRefQualifierextended for@quantlib/shared/utils/expand-model-variants.ts(new) — pure expansion + badge label helperconvex/lib/provider_options.ts—pinQuantization(shallow-clones, narrows to[quant])convex/providers/file_actions.ts—resolveModelDataparses + validates + pins;listProviders/getAllModelIds/getAllConfiguredModelIdsexposequantizationsconvex/agents/file_actions.ts—saveAgentrejects bogus variants withUNKNOWN_MODEL_VARIANTapp/features/chat/components/{model-selector,arena/arena-model-selector}.tsx— render variant entries with badgesapp/routes/dashboard/$id/agents/$agentId/instructions.tsx— agent editor offers per-variant entries; variant-aware dedup; variant in displayed nameTest plan
bun run check— 38/38 turbo tasks green (5602 vitest, lint, typecheck, format-check)bunx knip— cleanmerge → pinQuantization → buildCallProviderOptionstest asserts the exact OpenRouter body shapeTALE_DEBUG_LLM_WIRE=1, send a chat with GLM 5.1 (FP4); confirm wire body hasmodel: "z-ai/glm-5.1"andprovider: { quantizations: ["fp4"] }@fp16) →UNKNOWN_MODEL_VARIANTerroropenrouter:z-ai/glm-5.1still runs; chat picker re-binds on next selectionanthropic/claude-opus-4.6(no quantizations) still appears as a single entrySummary by CodeRabbit
Release Notes
New Features
Tests