Skip to content

feat: add metadata overrides and custom source for model aliases#163

Merged
mcowger merged 10 commits intomcowger:mainfrom
darkspadez:claude/custom-provider-override-WhvhX
Apr 15, 2026
Merged

feat: add metadata overrides and custom source for model aliases#163
mcowger merged 10 commits intomcowger:mainfrom
darkspadez:claude/custom-provider-override-WhvhX

Conversation

@darkspadez
Copy link
Copy Markdown
Contributor

@darkspadez darkspadez commented Apr 14, 2026

Lets users override individual fields (name, description, context length,
pricing, architecture, supported parameters, top provider limits) on top of
models.dev / OpenRouter / Catwalk catalog data, and adds a new 'custom'
metadata source where users supply everything themselves — seeded with
sensible defaults.

@darkspadez darkspadez changed the title Claude/custom provider override whvh x feat: add metadata overrides and custom source for model aliases Apr 14, 2026
Comment thread packages/backend/src/db/config-repository.ts
Comment thread packages/frontend/src/pages/Debug.tsx
Comment thread packages/backend/src/db/config-repository.ts
Comment thread packages/frontend/src/pages/Debug.tsx
Comment thread packages/backend/src/db/config-repository.ts Outdated
Comment thread packages/frontend/src/pages/Debug.tsx Outdated
Comment thread packages/backend/src/db/config-repository.ts Outdated
Copy link
Copy Markdown
Owner

@mcowger mcowger left a comment

Choose a reason for hiding this comment

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

Two issues introduced by this PR:

1. N+1 query worsened in getAllAliases() (config-repository.ts:483)

The PR adds a getMetadataOverrideRow(row.id) call inside the existing per-alias loop, which already fires one targets query per alias. This turns 1+N into 1+2N sequential round-trips. Please batch-fetch all override rows before the loop using inArray across all alias IDs (can be run in parallel with the targets fetch), then look them up by alias ID in a Map.

2. TypeScript type errors on principal?.keyName (Debug.tsx:269, Errors.tsx:149, Logs.tsx:391)

keyName only exists on the 'limited' variant of the Principal union — not on { role: 'admin' }. TypeScript cannot narrow the union through the opaque isLimited boolean, so these three lines are type errors. Use the discriminant directly instead: principal?.role === 'limited' && principal.keyName.

@mcowger
Copy link
Copy Markdown
Owner

mcowger commented Apr 15, 2026

@claude review this please, like a pirate.

@claude
Copy link
Copy Markdown

claude bot commented Apr 15, 2026

Claude Code is working…

I'll analyze this and get back to you.

View job run

claude and others added 9 commits April 15, 2026 14:37
Lets users override individual fields (name, description, context length,
pricing, architecture, supported parameters, top provider limits) on top of
models.dev / OpenRouter / Catwalk catalog data, and adds a new 'custom'
metadata source where users supply everything themselves — seeded with
sensible defaults.

- New table alias_metadata_overrides (SQLite + PostgreSQL) keyed by alias_id
  with typed columns per field. Cascades on alias delete.
- Extend ModelMetadataSchema to a discriminated union; adds 'custom' source
  with required overrides; other sources gain optional overrides.
- mergeOverrides() helper: scalars replace, nested objects spread-merge,
  arrays replace. GET /v1/models applies overrides on top of catalog hits.
- /v1/metadata/search continues to reject 'custom' (no catalog to search).
- Frontend adds an Override toggle in the Metadata accordion, a grouped
  edit form (Basic / Pricing / Architecture / Capabilities), and a Custom
  source option that pre-fills safe defaults.
- config.ts: custom source now requires a non-empty name (Zod min(1)) so
  { source: 'custom', overrides: { description: '...' } } no longer validates
  into an empty-name record.
- models.ts route: for non-custom sources, a catalog miss now returns the
  base record and skips mergeOverrides entirely instead of silently
  synthesizing a partial entry from overrides alone — hides nothing.
- config-repository.ts: saveAlias now wraps the full alias + targets +
  overrides replacement in a single transaction so partial failures roll
  back cleanly.
- lib/api.ts: AliasMetadata is now a discriminated union — non-custom
  requires source_path, custom allows it to be omitted. Mirrors backend.
- Models.tsx: switching to 'custom' merges seeded defaults with any
  existing overrides (overrides take precedence) and preserves the prior
  source_path. The 'name' field in custom mode is no longer auto-deleted
  when the user clears it, and handleSave refuses to submit custom
  metadata with a blank name with a clear inline error.
- Postgres schema: jsonb columns are typed as string[] via .$type<string[]>()
  so InferSelectModel / InferInsertModel produce the right shape. No SQL
  change — purely type-level.
- SQLite schema: metadata_source comment now lists 'custom' alongside the
  other three values to match the PostgreSQL enum and runtime behavior.
…rors

Both methods built their `where` clause from `this.schema.*` before invoking
`ensureDb()`, which is what lazily initializes `this.schema`. On a cold
backend the first call threw `TypeError: null is not an object (evaluating
'this.schema.debugLogs')`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…arams

Improves the metadata override UX so users can see and tweak the current
catalog values instead of starting from a blank form, and modernizes the
list inputs to use tag-style selectors.

Backend:
- Add GET /v1/metadata/lookup endpoint that returns the full normalized
  metadata record for a single (source, source_path) pair, with 400/404/503
  error paths. Used by the frontend to prefill the override form.
- Update Catwalk catalog URL from /providers to /v2/providers in both the
  ModelMetadataManager default and docs/CONFIGURATION.md.
- Bump drizzle journal timestamps to avoid ordering conflicts.
- Add coverage for the new lookup route (missing params, uninitialized
  source, unknown source_path, happy path).

Frontend:
- Add NormalizedModelMetadata type and api.getModelMetadata client that
  gracefully returns null on 404/503 so callers can fall back to blank.
- On opening the override panel (or selecting a new catalog model while
  override is on), fetch the catalog record and merge its values into the
  overrides blob — user-entered fields always win on conflict.
- Track a catalogReference alongside the editing alias and rework
  countOverrides to only count fields that actually differ from the
  reference (catalog values for catalog sources, buildCustomDefaults for
  custom), so the "N fields overridden" strip reflects real edits.
- Replace comma-separated Input fields for input/output modalities and
  supported_parameters with TagSelect dropdowns backed by suggestion lists.
- Extend TagSelect with an allowCustom prop: Enter/comma commits the
  current text as a new tag, Backspace on an empty input peels the last
  tag, pasted "a, b, c" splits into tags, and the dropdown shows a
  "Create '<search>'" affordance when the typed value isn't in options.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stop the override form from overwriting the new catalog's values when a user
switches between sources or models, and clear the stale source_path when the
catalog changes (paths are not portable between openrouter/models.dev/custom).
TagSelect now batches comma-pasted tags into a single onChange and commits
suggestion clicks via onMouseDown so they win the race against blur.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ts merge

TagSelect: Introduce a single normalize() helper and use it for duplicate
detection in filteredOptions, showCreateOption, and addCustomTags so the
dropdown's "Create" affordance and the commit paths (Enter/blur/comma) agree
on case. Gate the input's onBlur commit on event.relatedTarget leaving the
container, and revert dropdown items to onClick now that blur no longer
commits partial search text when clicking a suggestion.

Models: Deep-merge buildCustomDefaults with existingOverrides when switching
to the custom source so a partial nested override (e.g. only
architecture.input_modalities) no longer wipes default sibling fields like
architecture.output_modalities. Mirrors the existing merge pattern in
populateOverridesFromCatalog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pending metadata-catalog search timeouts could overwrite results after
the user switched source or cleared the query, and clearing metadata
left isOverrideOpen stuck on so a later source pick would reopen the
override form against the new catalog. Cancel the debounce and reset
related UI state in handleMetadataSearch, clearMetadata, the
source-selector onChange, and the modal-open effect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…inant

Replace per-alias metadata override lookup in getAllAliases() with a
single inArray query run in parallel with the targets fetch, then index
both into maps keyed by aliasId. The old loop issued 1+2N sequential
round-trips.

On the frontend, the Principal union's keyName only exists on the
'limited' variant, and TypeScript can't narrow through the opaque
isLimited boolean. Switch Debug/Errors/Logs to the discriminant check
(principal?.role === 'limited' && principal.keyName) and drop isLimited
from the destructure where it's no longer used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebase PR branch onto current main (which has advanced past the branch's
base), replay only the new metadata-override feature commits, then
regenerate migrations at the correct next-available indices:
  SQLite → 0029_simple_skullbuster (alias_metadata_overrides + provider_reported_cost re-add)
  PG     → 0033_colossal_exiles    (alias_metadata_overrides + metadata_source enum)
@mcowger mcowger force-pushed the claude/custom-provider-override-WhvhX branch from 227cf2b to 29f56f0 Compare April 15, 2026 21:41
…y.send()

In Fastify v5, async hooks must throw to abort the hook chain rather than
calling reply.send() and returning. Sending in an async preHandler does not
reliably set reply.sent before subsequent hooks are invoked, causing a
double-send ERR_HTTP_HEADERS_SENT crash on all 401/403 rejection paths.

Introduce ManagementAuthError (statusCode + authBody) thrown by
authenticate() and requireAdmin(). A setErrorHandler() in
registerManagementRoutes() catches it and formats the expected
{ error: { message, type, code } } response body.
@mcowger mcowger merged commit 9db46cc into mcowger:main Apr 15, 2026
1 check failed
github-actions bot pushed a commit that referenced this pull request Apr 16, 2026
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.

3 participants