Skip to content

feat(platform): quantization variants as first-class selectable models#1697

Merged
yannickmonney merged 2 commits into
mainfrom
feat/quantization-variants-as-models
May 10, 2026
Merged

feat(platform): quantization variants as first-class selectable models#1697
yannickmonney merged 2 commits into
mainfrom
feat/quantization-variants-as-models

Conversation

@yannickmonney

@yannickmonney yannickmonney commented May 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Each providerOptions.provider.quantizations entry becomes its own selectable model in the chat picker, arena picker, and agent supportedModels editor — e.g. GLM 5.1 with an FP8 badge and GLM 5.1 with an FP4 badge as separate entries.
  • The chosen variant is encoded as provider:base@quant end-to-end; on the wire to OpenRouter the model id is bare and the variant rides in provider.quantizations: [chosen].
  • No JSON schema changes, no DB migrations, no new i18n keys. Existing agent JSONs referring to a bare base id still resolve at runtime (provider picks); they re-bind to a variant on the user's next selection.

Approach

  • Encoding: 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.
  • UI rule: when a model declares quantizations, selectors render only the variant entries (no unsplit base option), forcing an explicit pick.
  • Runtime: resolveModelData parses the suffix, validates it against the declared array, calls pinQuantization to narrow the merged passthrough to a single element, returns the bare modelId. buildCallProviderOptions namespaces the result; the AI SDK spreads it as top-level body fields. governance.stripModelRefQualifier continues 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.tsparseModelRef/formatModelRef/stripModelRefQualifier extended for @quant
  • lib/shared/utils/expand-model-variants.ts (new) — pure expansion + badge label helper
  • convex/lib/provider_options.tspinQuantization (shallow-clones, narrows to [quant])
  • convex/providers/file_actions.tsresolveModelData parses + validates + pins; listProviders/getAllModelIds/getAllConfiguredModelIds expose quantizations
  • convex/agents/file_actions.tssaveAgent rejects bogus variants with UNKNOWN_MODEL_VARIANT
  • app/features/chat/components/{model-selector,arena/arena-model-selector}.tsx — render variant entries with badges
  • app/routes/dashboard/$id/agents/$agentId/instructions.tsx — agent editor offers per-variant entries; variant-aware dedup; variant in displayed name

Test plan

  • bun run check — 38/38 turbo tasks green (5602 vitest, lint, typecheck, format-check)
  • bunx knip — clean
  • CodeRabbit review across 3 passes — addressed every actionable finding
  • Composed merge → pinQuantization → buildCallProviderOptions test asserts the exact OpenRouter body shape
  • Manual: with TALE_DEBUG_LLM_WIRE=1, send a chat with GLM 5.1 (FP4); confirm wire body has model: "z-ai/glm-5.1" and provider: { quantizations: ["fp4"] }
  • Manual: switch variant mid-conversation; confirm wire body updates
  • Manual: try saving an agent JSON with a bogus variant (@fp16) → UNKNOWN_MODEL_VARIANT error
  • Manual: existing agent with bare openrouter:z-ai/glm-5.1 still runs; chat picker re-binds on next selection
  • Manual: anthropic/claude-opus-4.6 (no quantizations) still appears as a single entry

Summary by CodeRabbit

Release Notes

  • New Features

    • Model selectors now support quantization variants, enabling users to select specific model optimizations alongside standard models.
    • Model variant options are identified with descriptive badges in selection menus.
    • Agent instruction configurations can now reference specific model quantization variants.
  • Tests

    • Added comprehensive test coverage for model variant expansion, reference parsing, and quantization handling.

Review Change Stack

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).
@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR implements model quantization variant support across the platform. It introduces utilities to parse model references with @quantization suffixes, expands base model selections into per-quantization variants, and validates variant declarations at the backend. Frontend components render quantization-aware model selectors with variant badges. The backend exposes quantization metadata through provider listing and validates agent configurations against declared variants.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the summary, approach, files changed, and test plan but the pre-merge checklist items are not filled out (all boxes are empty). Complete the pre-merge checklist by ticking or marking N/A for all items: bun run check, i18n keys, docs updates, lint checks, and README updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: exposing quantization variants as independently selectable models in the platform.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/quantization-variants-as-models

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Trigger label drops the selected quantization — variants become indistinguishable when the dropdown is closed.

getDisplayName calls stripModelRefQualifier, which strips the @<quant> suffix, so picking openrouter:z-ai/glm-5.1@fp8 versus …@fp4 shows the same currentLabel ("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 win

Two minor parity issues vs. model-selector.tsx: stale selections aren't cleared, and trigger labels hide the variant.

  1. Stale modelA/modelB not cleared when they fall outside filteredModels. If context already holds a base ref (e.g. openrouter:z-ai/glm-5.1) and the agent now declares quantizations, filteredModels only contains …@fp8 / …@fp4. The sync useEffect only runs its assignments when !modelA / !modelB, so a stale selection persists and the SearchableSelect's value no longer matches any option. model-selector.tsx explicitly clears non-matching overrides — apply the same here.

  2. Trigger labels strip the variant. getDisplayName(currentModelA) runs through stripModelRefQualifier, so the closed A/B triggers show identical text for sibling variants (e.g. both …@fp8 and …@fp4 render as "GLM 5.1"). Since the picker forces an explicit variant choice, the resting trigger should reflect it — same fix as suggested for model-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 win

Consider falling back to parsed providerName for robustness.

The code at line 581 discards providerName from parseModelRef, relying instead on args.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 when args.providerName is 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

📥 Commits

Reviewing files that changed from the base of the PR and between bf3b05c and d2ab95a.

📒 Files selected for processing (10)
  • services/platform/app/features/chat/components/arena/arena-model-selector.tsx
  • services/platform/app/features/chat/components/model-selector.tsx
  • services/platform/app/routes/dashboard/$id/agents/$agentId/instructions.tsx
  • services/platform/convex/agents/file_actions.ts
  • services/platform/convex/lib/provider_options.test.ts
  • services/platform/convex/lib/provider_options.ts
  • services/platform/convex/providers/file_actions.ts
  • services/platform/lib/shared/utils/__tests__/expand-model-variants.test.ts
  • services/platform/lib/shared/utils/__tests__/model-ref.test.ts
  • services/platform/lib/shared/utils/expand-model-variants.ts

Comment thread services/platform/convex/providers/file_actions.ts Outdated
…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.
@yannickmonney yannickmonney merged commit 6479e7b into main May 10, 2026
25 checks passed
@yannickmonney yannickmonney deleted the feat/quantization-variants-as-models branch May 10, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant