Skip to content

feat(providers): remap outbound model IDs for deployment-key endpoints - #900

Open
vignesh-chaturvedi wants to merge 1 commit into
workweave:mainfrom
vignesh-chaturvedi:feat/deployment-model-aliases
Open

feat(providers): remap outbound model IDs for deployment-key endpoints#900
vignesh-chaturvedi wants to merge 1 commit into
workweave:mainfrom
vignesh-chaturvedi:feat/deployment-model-aliases

Conversation

@vignesh-chaturvedi

Copy link
Copy Markdown

What

Adds ROUTER_<PROVIDER>_MODEL_ALIASES, a JSON map of catalog model ID → the model name a provider's endpoint publishes, applied to the outbound request body on the deployment-key path.

Closes #526
Closes #541

Why

Pointing a provider at an OpenAI-compatible endpoint that isn't the vendor itself is already a supported, documented setup — .env.example describes OPENROUTER_BASE_URL as "override for vLLM/Together/etc." But if that endpoint publishes the catalog's models under different names, there is no way to rewrite them, and every turn 400s.

#541 reproduces it end to end: the router sends deepseek/deepseek-v4-flash, the gateway expects deepseek-v4-flash, and the response is Model ... is not supported.

Rewriting exists, but only on two paths that don't cover this case:

Path Mechanism Covers a re-named deployment endpoint?
Catalog UpstreamID upstreamIDsForProvider, baked into the binding No — fixed at build time, and OpenRouter/XAI bindings carry none
BYOK key (#892) ExternalAPIKey.ModelAliasesproxy.ApplyModelAlias No — internal/proxy/credentials.go:50 documents it as "non-empty only on BYOK credentials"

So a self-hosted deployment using OPENROUTER_API_KEY + OPENROUTER_BASE_URL — exactly #541's repro — has no mechanism at all. This adds the third layer.

How

resolveModelAliases reads ROUTER_<PROVIDER>_MODEL_ALIASES for each OpenAI-compatible provider and layers the entries over that provider's catalog-derived UpstreamID map. The merged map goes to the existing NewClientWithModelIDMap, so the rewrite itself reuses rewriteModelField unchanged — no new code on the request path.

Precedence, outermost wins: catalog UpstreamID < ROUTER_<PROVIDER>_MODEL_ALIASES < BYOK model_aliases. The BYOK layer keeps winning for free, because Client.Proxy already applies it after the catalog map:

body := rewriteModelField(prep.Body, c.modelIDMap)
// Applied after the catalog map so a BYOK endpoint's own naming wins.
body = proxy.ApplyModelAlias(ctx, body, decision.Model)

Only the outbound wire name changes. Routing, pricing, and analytics stay keyed on the catalog ID.

Failure modes. A malformed value aborts boot (panic, consistent with config.MustGet and ROUTER_DEPLOYMENT_MODE) — the error names the env var. An alias naming a model outside the deployed catalog is logged and skipped rather than fatal, so retiring a model (#896, #897) can't turn a stale alias into a failed start.

Validation is against catalog IDs, not per-provider bindings, deliberately. An operator pointing OPENROUTER_BASE_URL at a different gateway may well be served models the catalog binds elsewhere — #526's own list includes z-ai/glm-5.2, which the catalog binds to Together and Fireworks. Rejecting those would reject the use case.

Placement. The logic sits in cmd/router rather than internal/config because it needs internal/router/catalog, and AGENTS.md requires internal/config to stay a leaf that imports nothing else under internal/. It is boot-time wiring, which is the composition root's job.

Behavior change

None without configuration. resolveModelAliases with no env vars set produces exactly the maps in use today:

Provider Catalog-derived entries Resolved entries
openrouter 0 0
fireworks 12 12
makora 1 1
together 5 5
xai 0 0
bedrock 4 4

OpenRouter and XAI now construct via NewClientWithModelIDMap where they previously called NewClient. Both resolve to zero entries, so the map is nil and rewriteModelField returns the body untouched — an unconfigured deployment puts the same bytes on the wire as before.

Testing

make build, make vet, make test, and make test-statusline are green, and make generate-statusline leaves the tree clean. (I couldn't run sqlc generate locally, but no db/queries/ or migration files are touched, so internal/sqlc/ is untouched.)

model_aliases_wire_test.go reproduces #541 at the wire. An httptest stub stands in for a gateway publishing bare names, and the assertion is the model field it actually receives:

Case Model on the wire
No alias configured (the #541 bug) deepseek/deepseek-v4-flash — what the gateway rejects
ROUTER_OPENROUTER_MODEL_ALIASES set deepseek-v4-flash — what it accepts
A different, unaliased model xiaomi/mimo-v2.5-pro, unchanged

I chose this over booting the compose stack because it pins the exact byte #541 is about, runs in CI on every change, and needs no upstream key. It also closes a gap: nothing currently asserts modelIDMap rewriting at all.

model_aliases_test.go adds 11 cases: parsing (empty, valid, malformed JSON, JSON array, empty key, empty value), env override application, override beating a catalog UpstreamID, unaliased bindings surviving the merge, unknown models skipped without error, and a malformed value erroring with the env var named. They discover a suitable model from catalog.Models at run time rather than pinning an ID, so catalog churn doesn't break them.

Mutation-checked: dropping the merged[id] = upstreamID write fails TestResolveModelAliasesAppliesEnvOverride, TestResolveModelAliasesOverridesCatalogUpstreamID, and the wire test's alias case, while the two no-alias cases correctly keep passing.

Not run: the full docker compose stack against a real re-named endpoint. The evidence above is the test suite plus the resolved-map parity table.

Docs

  • docs/CONFIGURATION.md — new "Deployment-level model aliases" subsection plus a row in the provider table.
  • .env.example — commented ROUTER_OPENROUTER_MODEL_ALIASES example next to the other provider vars.
  • cmd/CLAUDE.md and cmd/AGENTS.mdresolveModelAliases added to the composition-root helper list, same edit in both halves of the mirror.

Incidental

upstreamIDsForProvider moves into the new file alongside its only caller. Its doc comment had drifted onto registerDeploymentKeyedProvider in main.go; moving the function reunites the two.

An OpenAI-compatible endpoint reached through a provider's *_BASE_URL may
publish the catalog's models under its own names, and the deployment-key
path had no way to rewrite them. ROUTER_<PROVIDER>_MODEL_ALIASES takes a
JSON map of catalog model ID to upstream name and layers it over the
catalog's per-binding UpstreamID.

Only the outbound wire name changes: routing, pricing, and analytics stay
keyed on the catalog ID. A BYOK key's model_aliases still wins, since it is
applied after the catalog map in openaicompat.Client.Proxy. A malformed
value aborts boot; an alias naming a model outside the deployed catalog is
logged and skipped, so retiring a model cannot turn a stale alias into a
failed start.

OpenRouter and XAI now build with a model ID map where they previously
passed nil. Both resolve to zero catalog entries, so an unconfigured
deployment puts exactly the same bytes on the wire as it does today.

upstreamIDsForProvider moves to the new file with it; its doc comment had
drifted onto registerDeploymentKeyedProvider in main.go.

Closes workweave#526
Closes workweave#541

Signed-off-by: Vignesh Chaturvedi <vigneshchaturvedi@gmail.com>
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

@vignesh-chaturvedi

Copy link
Copy Markdown
Author

Worth flagging prior art I should have led with: #557 already targets #526 and
#541 with the same core mechanism — an env var parsed at the composition root
and fed into NewClientWithModelIDMap. @Shangmin-Chen got there first.

The difference is scope. #557 adds OPENROUTER_MODEL_ID_MAP, which covers the
OpenCode Go case in #541. This PR generalizes to ROUTER_<PROVIDER>_MODEL_ALIASES
across all six OpenAI-compatible providers, which is what the second half of #526
asks for — Bedrock reached through an OpenAI-compatible proxy needs the same
remapping and can't use an OpenRouter-specific variable. It also layers over the
catalog's existing per-binding UpstreamID instead of sitting beside it, so
Fireworks/Together/Makora/Bedrock keep their built-in maps while an operator can
override a single entry.

If you'd rather land #557 and treat multi-provider support as a follow-up, I'm
happy to close this and rebase the generalization on top of it -- that ordering is
fine by me. Either way I wanted the overlap on the record rather than have two
PRs quietly competing.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Thanks for this — it follows the repo conventions closely, so no changes needed from a conventions standpoint.

What I checked:

  • Placement / layer model. Boot-time wiring in cmd/router because it needs internal/router/catalog, keeping internal/config a leaf — exactly what root AGENTS.md (layer model + import rules) and cmd/AGENTS.md ("only place that constructs concrete adapters") ask for. resolveModelAliases added to the composition-root helper list in both halves of the mirror (cmd/AGENTS.md + cmd/CLAUDE.md), which is easy to miss.
  • No magic strings. aliasableProviders and all test fixtures use providers.Provider* constants (AGENTS.md → Go style). Literal catalog model IDs in tests match the existing cmd/router test style (e.g. hmm_roster_source_test.go).
  • Comments. Short, single-paragraph, why-only (AGENTS.md line 15). The #541 regression reference is consistent with existing usage across the repo.
  • Logging. logger.Warn single line, snake_case attribute keys, plain-English message.
  • Fail-fast on misconfiguration. Panic at boot for a malformed value matches "misconfiguration must abort the process rather than silently degrade"; skipping-with-a-warn for a model outside the deployed catalog is the right call given model retirement.
  • Tests. Non-tautological — I ran your mutation check independently: dropping merged[id] = upstreamID fails TestResolveModelAliasesAppliesEnvOverride, TestResolveModelAliasesOverridesCatalogUpstreamID, and the wire test's alias subtest, while the no-alias cases keep passing. make precommit is green on my end too (vet + build + full test suite + 22 status-line checks).

Also verified the two claims the design rests on: NewClient is literally NewClientWithModelIDMap(..., nil), so the OpenRouter/XAI switch is a no-op when nothing is configured; and Client.Proxy applies proxy.ApplyModelAlias after rewriteModelField, so BYOK aliases still win.

The only open item is the one you already flagged yourself — the scope overlap with #557. That's a maintainer sequencing call rather than a code issue, and flagging it proactively was the right thing to do.

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.

Reproduction: model alias mismatch with OpenCode Go (related to #526) model alias support for provider variations

1 participant