Problem
Provider selection uses substring matching on the whole model reference. provider_for_model (crates/llm/src/lib.rs:358-391) routes on model.contains("anthropic"), model.contains("claude"), model.contains("openai"), etc. And the session handler strips only a provider/model prefix (crates/server/src/handler/session.rs:270 — model.split('/').nth(1)).
A gateway-connected model reference like http://127.0.0.1:8787/v1#claude-sonnet-4-5 or http://gateway:port/model/claude-x contains "claude" → routed to Anthropic's real API with the gateway's key. url#model refs aren't parsed at all.
This mirrors upstream anomalyco/opencode#40071 ("fix(provider): parse URL-based provider IDs", closes #39926): parseModel only expected providerID/modelID and broke when the provider ID is itself a URL (gateway/.well-known models). Prompts work because IDs are sent separately, but combined command strings must be parsed server-side — exactly what crates/server/src/handler/session.rs does.
Impact
- A model configured via
OTHER_API_BASE_URL/gateway can be silently sent to the wrong endpoint.
- Keys can leak to the wrong provider; responses come back malformed or fail auth.
Spec / Acceptance criteria
- Implement a
parse_model_ref(ref: &str) -> (provider, model) that handles, in order:
provider/model (e.g. anthropic/claude-sonnet-4)
url#model (e.g. http://host:port/v1#claude-x)
url/model gateway forms
- bare model name (resolve via existing heuristics)
- In
provider_for_model, match the parsed provider ID exactly (including "other"/extra providers by base URL) before any substring fallback. Never route a URL-bearing ref by substring.
- Session handler uses the parsed provider+model (drop the ad-hoc
split('/').nth(1)).
- Tests:
http://127.0.0.1:8787/v1#claude-sonnet-4-5 → routes to the local gateway provider, not Anthropic; deepseek/deepseek-chat → DeepSeek.
Reference: upstream anomalyco/opencode#40071 (closes #39926); our code: crates/llm/src/lib.rs:263-306, 358-391, crates/server/src/handler/session.rs:262-270.
Problem
Provider selection uses substring matching on the whole model reference.
provider_for_model(crates/llm/src/lib.rs:358-391) routes onmodel.contains("anthropic"),model.contains("claude"),model.contains("openai"), etc. And the session handler strips only aprovider/modelprefix (crates/server/src/handler/session.rs:270—model.split('/').nth(1)).A gateway-connected model reference like
http://127.0.0.1:8787/v1#claude-sonnet-4-5orhttp://gateway:port/model/claude-xcontains "claude" → routed to Anthropic's real API with the gateway's key.url#modelrefs aren't parsed at all.This mirrors upstream anomalyco/opencode#40071 ("fix(provider): parse URL-based provider IDs", closes #39926):
parseModelonly expectedproviderID/modelIDand broke when the provider ID is itself a URL (gateway/.well-known models). Prompts work because IDs are sent separately, but combined command strings must be parsed server-side — exactly whatcrates/server/src/handler/session.rsdoes.Impact
OTHER_API_BASE_URL/gateway can be silently sent to the wrong endpoint.Spec / Acceptance criteria
parse_model_ref(ref: &str) -> (provider, model)that handles, in order:provider/model(e.g.anthropic/claude-sonnet-4)url#model(e.g.http://host:port/v1#claude-x)url/modelgateway formsprovider_for_model, match the parsed provider ID exactly (including "other"/extra providers by base URL) before any substring fallback. Never route a URL-bearing ref by substring.split('/').nth(1)).http://127.0.0.1:8787/v1#claude-sonnet-4-5→ routes to the local gateway provider, not Anthropic;deepseek/deepseek-chat→ DeepSeek.Reference: upstream anomalyco/opencode#40071 (closes #39926); our code:
crates/llm/src/lib.rs:263-306, 358-391,crates/server/src/handler/session.rs:262-270.