Repository navigation
fix: switching models on an oai: endpoint works, and a session's replies name its real backend - #468
Merged
Merged
Conversation
…ies name its real backend
The model dropdown read an endpoint's catalog (configured models, else the
server's /models) but the switch (#setModel) checked the pick against
#currentModels, which is always empty for endpoints: they report no catalog
through onModels. Every pick failed with "Available: " and the session
stayed on its default. One #pickableModels list now serves both.
On an endpoint an id it isn't configured with (the dropdown's custom id) is
passed to the server; a Claude-shaped id is refused first, since
resolveAgainstList passes any claude-* id through, and the error says why
instead of an empty "Available:".
The agent label was hardcoded "<name> (Claude)": it now names the backend
("Codex", "Qwen Code", the endpoint's name) and follows a backend switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aped; Bedrock ids, control characters, a removed endpoint and a mid-check backend switch are refused; an endpoint's label is its display name as written - A gateway may serve Claude models: a model the endpoint lists (by id or display name) is taken as is; only a Claude-shaped id it doesn't list is refused, now including Bedrock's [region.]anthropic.claude-* ids. - A model id follows the endpoint store's own rule (one line, at most 256 characters, no control characters), so nothing else reaches the gateway. - A removed endpoint takes no model; a backend switch while the list was fetched refuses the pick instead of checking it against the old backend. - An endpoint's label is its display name as the user wrote it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
akashjavelin
approved these changes
Nov 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Switching models on an OpenAI-compatible endpoint (
oai:…, e.g. rakuten-oss) now works. Before, every pick failed withUnknown model "…". Available:(nothing after "Available:"), and the session stayed on its default model.Also, a session's replies now name the backend they come from, instead of always "(Claude)".
What was wrong
/models. The switch (#setModel) checked the pick against#currentModels, which knows only catalogs a backend reports. Endpoints report none, so for them that list is always empty and every pick was refused.resolveAgainstListpasses any fullclaude-*id through whatever the list contains, so on an endpointclaude-sonnet-4-5would have been accepted and sent to the gateway.`${name} (Claude)`for every session, whatever its backend. On an endpoint, that made "the model says it's Claude" look like codeoid's doing.What changed
#pickableModels(providerId)list serves both the dropdown and the switch, so they can't disagree again.opus, and Bedrock's[region.]anthropic.claude-*. Example:"claude-sonnet-4-5" is a Claude model, not one this endpoint serves. Configured: conf-a, conf-b. Any other model id is passed to the server.Verification
End to end with a real daemon (
codeoid start --local), the real CLI adding the endpoint (codeoid endpoint set … --model conf-a --model conf-b), and codeoid's own client library switching models the way the web UI does, against a fake OpenAI-compatible gateway that records which model each request asked for.mainconf-bfrom the dropdownAvailable:conf-a(stuck on the default)conf-bserved-aclaude-sonnet-4-5e2e (Claude)e2e (Rakuten OSS)New regression test in
openai-endpoints.test.ts, against that file's fake server. It covers each case above and fails onmainwith the reported error. A unit test covers the backend label.Audit: one round with three sandboxed reviewers (correctness, security, real behaviour). It found that the first version refused Claude-shaped models a gateway itself lists, plus the lows now covered above. All are fixed, with unit and daemon tests: a gateway's Claude model is accepted and sent; Bedrock ids are refused; control characters, including a header-injection attempt, are refused; a removed endpoint is refused.
Checks: typecheck and lint are clean. The full suite passes (6,166, 0 fail), all 23 e2e specs pass against a fresh web build, and the end-to-end run passes on the final code.
Known limit: on an endpoint with no configured models whose server is slow, a switch waits for its
/models(up to the catalog's 5 s timeout), as the dropdown already does. Configuring the endpoint's models avoids the fetch.Not changed
A gateway model (DeepSeek, GLM) answering "I'm Claude" does it on its own. Codeoid's prompt for endpoints names no model, and these turns show
$?cost, which only non-Claude turns do. Naming the real model in the endpoint's system prompt would be a separate, optional change.🤖 Implementation context (for agents / maintainers)
src/daemon/session-manager.ts#pickableModels(providerId): the endpoint catalog via#endpointCatalog.models, else#currentModels.#modelsListuses it.#setModel'sresolveon an endpoint:resolveModelIdForProvider(id, providerId) === nullrefuses a Claude-shaped id; otherwiseresolveAgainstList(id, models) ?? resolveModelIdForProvider(...).src/daemon/session.ts: new exportedbackendLabel(provider)(the display name minus a trailing parenthetical, else the id). It's used for the default agent identity in the constructor and refreshed in the provider switch while the identity is the localagent:<id>one.src/tests/openai-endpoints.test.ts: "a model the dropdown offers is one the switch accepts …", plus "a session's agent label".#endpointCatalog.modelsnever throws (an unreachable server gives[]), so a pick still passes through when the server is down.🤖 Generated with Claude Code