Skip to content

feat: badge model display names, configurable via options.badge#43

Open
BenjaminDSmithy wants to merge 1 commit into
agustif:mainfrom
BenjaminDSmithy:feat/format-tag-model-name
Open

feat: badge model display names, configurable via options.badge#43
BenjaminDSmithy wants to merge 1 commit into
agustif:mainfrom
BenjaminDSmithy:feat/format-tag-model-name

Conversation

@BenjaminDSmithy

@BenjaminDSmithy BenjaminDSmithy commented Jul 9, 2026

Copy link
Copy Markdown

What

Append a metadata badge to each model's display name so otherwise-identical builds are distinguishable in the OpenCode model picker, with the verbosity controlled by a new provider.lmstudio.options.badge option:

Tier Result
off Devstral Small
format Devstral Small (MLX)
compact Devstral Small (Q4_K_M · 14.3 GB · MLX)
full Devstral Small (24B · Q4_K_M · 14.3 GB · MLX)

Default is full, so a user with no config sees exactly the same output as before this option existed — the change is purely additive. The option only dials the badge down.

{ "provider": { "lmstudio": { "options": { "badge": "compact" } } } }

Why

GET /api/v1/models already carries params_string, quantization.name, size_bytes, and format, but none of it reached ModelConfig. In practice an MLX quant and a GGUF quant of the same model share a display_name, and multiple quant levels collapse to the same row — the picker shows identical names with no way to tell them apart. This surfaces metadata the endpoint already returns; no lms CLI or SDK dependency, so it stays remote-server-safe. Raised against the rc.2 feedback tracker (#34).

How

src/plugin/enhance-config.ts:

  • BADGE_TIERS / BadgeTier / DEFAULT_BADGE_TIER = "full", and resolveBadgeTier(raw) which fails open to full for any non-enum value (a typo must never break startup — matches the plugin's discovery-error-degrades ethos).
  • modelBadge(model, tier) selects an order-preserving subset of [params, quant, size, format]. compact drops params on principle: params is identical across the quant/format siblings of one model, so it never disambiguates them — the tier keeps only the segments that do. (This avoids parsing display_name, which would be exactly the kind of name heuristic the README rules out.) Within a tier, each segment is still dropped when unreported.
  • enhanceConfig resolves the tier once from existing?.options?.badge and threads it into toModelConfig. Separator is a single named constant (not a config knob — kept minimal). Only name changes; the model key/ID is untouched.

src/types/index.ts: adds params_string and size_bytes to LMStudioModelSchema, both .nullable().optional() so older servers and the existing fixture still validate.

The badge key and forwarded options

badge is a plugin-only option, and it is deliberately not stripped from the options forwarded to @ai-sdk/openai-compatible. Two reasons, both verified against the bundled versions:

  1. Safe: createOpenAICompatible reads only named fields (baseURL, apiKey, headers, queryParams, fetch, …) and ignores unknown keys — badge is never placed in a header/URL/body, never sent on the wire, never throws.
  2. Idempotent: options is the one config record that persists across repeated config-hook invocations (it's how apiKey survives re-runs). Stripping badge would revert the user's tier to the default on the second config load. An enhanceConfig-twice test asserts both the option's persistence and that the chosen tier is still applied on the second run.

If you'd rather strip it anyway, I'm happy to add a persistent side-channel for the tier — but not-stripping is the simpler correct choice here.

Contract note

The rc.2 contract (#34) says "display names come directly from LM Studio metadata." This decorates the display name; the key is still verbatim, and every segment is LM Studio metadata. docs/v1-contract.md is updated with the badge mapping row and the default. If you'd prefer the default to be off (badge fully opt-in) rather than full, that's a one-line change — say the word.

Tests

  • modelBadge per-tier (incl. intra-tier segment drop) + formatSize boundaries + resolveBadgeTier fallback.
  • enhanceConfig integration: off yields bare names, compact applies + survives a second run (no-strip idempotency), invalid value falls back to full.
  • 29→32 pass; tsc --noEmit clean; eslint clean; lockfile untouched.
  • Independent second-opinion review (codex, different model family): no P0/P1/P2 issues; confirmed no empty-parens/dangling-separator cases, the no-config regression is nil, and the no-strip decision is safe + idempotent.

README (Model mapping), CHANGELOG.md (Unreleased → Added), and docs/v1-contract.md updated.

@BenjaminDSmithy BenjaminDSmithy force-pushed the feat/format-tag-model-name branch from bb235ee to fcca9d1 Compare July 9, 2026 13:15
@BenjaminDSmithy BenjaminDSmithy changed the title feat: tag model display names with runtime format (MLX/GGUF) feat: badge model display names with params, quant, size, format Jul 9, 2026
@BenjaminDSmithy BenjaminDSmithy force-pushed the feat/format-tag-model-name branch from fcca9d1 to ab55c8e Compare July 9, 2026 14:06
@BenjaminDSmithy BenjaminDSmithy changed the title feat: badge model display names with params, quant, size, format feat: badge model display names, configurable via options.badge Jul 9, 2026
LM Studio's native /api/v1/models reports params_string ("27B"),
quantization.name ("Q4_K_M"), size_bytes, and format ("mlx"|"gguf"),
but none of it reached the OpenCode model config. Builds that share a
display_name (an MLX quant and its GGUF sibling, or two quant levels of
one model) were indistinguishable in the picker.

Append a metadata badge to the display name, with the verbosity set by
a new provider.lmstudio.options.badge option:

  off     -> Devstral Small
  format  -> Devstral Small (MLX)
  compact -> Devstral Small (Q4_K_M · 14.3 GB · MLX)
  full    -> Devstral Small (24B · Q4_K_M · 14.3 GB · MLX)   [default]

Tiers are order-preserving subsets of [params, quant, size, format].
compact drops params because params is identical across the quant and
format siblings of one model, so it never disambiguates them. Within a
tier, each segment is still dropped when LM Studio does not report it,
so the badge degrades cleanly on older servers; a metadata-less record
keeps the bare display_name. Size uses LM Studio's decimal (base-1000)
convention. The separator is a single named constant (not a config
option). Only the display name changes; the model key (ID) is untouched.

The default is "full", so a user with no config sees the same output as
before this option existed — the change is purely additive. An
unrecognised badge value fails open to "full" rather than breaking
OpenCode startup, matching the plugin's discovery-error-degrades ethos.

The badge key is plugin-only and is deliberately NOT stripped from the
forwarded provider options: @ai-sdk/openai-compatible reads only named
fields and ignores unknown keys (never sent on the wire), and options is
the one config record that persists across repeated config-hook runs, so
stripping would revert the user's tier to the default on the next load
(the same reason apiKey is written back into options).

Adds params_string and size_bytes to the native model schema (both
optional for backward compatibility). Covered by tier, size-format,
resolve-fallback, and enhanceConfig idempotency tests (32 pass);
README, CHANGELOG, and the v1 contract doc updated.
@BenjaminDSmithy BenjaminDSmithy force-pushed the feat/format-tag-model-name branch from ab55c8e to e9807f2 Compare July 9, 2026 14:35
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