Repository navigation
feat: per-agent provider profiles, models read from the provider, OpenCode keeps Zen - #93
Merged
Merged
Conversation
The Claude and DeepSeek drivers each read the models of the endpoint they are pointed at from its /v1/models, with two parsers that knew different halves of what endpoints answer. That endpoint can be a provider's own API or a gateway in front of several; the list is the same either way. sdk/providerModels.ts now holds the one reader: where the list lives, the shapes it comes in (data, models or a bare array; claude-code-router's encoded ids and 1M markers), the upstream prefix of a routed id, context sizes, and a phone-sized cap. Claude keeps only its 1M bookkeeping on top; DeepSeek's catalog module is now catalog.ts. Co-Authored-By: Claude Code <noreply@anthropic.com>
A provider profile had to list its models by hand, copied from the provider's own /v1/models. Now the phone can ask the bridge to read them instead: with modelsFromProvider set, every save reads the endpoint's list with the profile's token and stores what it serves (at most 200), or refuses the save when there is no list to store. A default model the provider no longer lists is dropped, and a newer save of the same profile wins over a list still on its way. The token goes only to the URL the profile names: the request follows no redirect (neither does the token check any more), the answer is read up to a size cap, and ids and labels that are not model names are dropped. The flag is stored and reported back, so a phone shows where a profile's models come from and can read them again. On the phone the editor reads the models from the provider by default (OpenRouter's preset too), and a profile card offers Refresh models. A model list now always shows who offers each model, also when one provider offers them all. Co-Authored-By: Claude Code <noreply@anthropic.com>
OpenCode only ran on the providers in the machine's own config, so a gateway or another provider meant editing opencode.json on the machine, models included. It now takes a provider profile like the other agents: the profile is an OpenAI-compatible endpoint, reached as a custom provider of an OpenCode server started for that profile alone. Its config arrives in the environment, on top of the operator's (MCP servers, plugins and agents still load): the profile is the only provider enabled and is the model and the small model, so nothing in the session falls back to the operator's accounts. The token sits in that server's environment only, referenced from the config, and the server answers only with a password made for it, since its API reads the config back. A server is shared by the sessions on the same profile and stops a while after the last one; a changed profile gets a new one. Session model ids are the profile's own (a slash in one is not a provider prefix), and a session switches only among them. Co-Authored-By: Claude Code <noreply@anthropic.com>
A provider base URL had to be https, or http to this machine only, so a gateway or model server at home (http://192.168.1.2:3458) could not be a profile. Plain http is now also allowed to an IP address of the user's own network: 10/8, 172.16/12, 192.168/16, 100.64/10 (where VPN overlays such as Tailscale number their machines) and IPv6 unique local addresses. Only addresses count, never names, which DNS could point anywhere, and only their canonical spelling, since a WHATWG parser reads shorthands and leading zeros as other addresses. The token then crosses that network in cleartext, which the user chose by pointing a profile there. The rule for Nostr traffic does not change: registering a phone with a relay's admin endpoint keeps the strict https-or-loopback rule (plus onion services), now its own function. Co-Authored-By: Claude Code <noreply@anthropic.com>
An endpoint that speaks one agent's API need not speak another's (an OpenAI-compatible gateway is not a Claude Code endpoint), so a provider profile now names its agent. What a profile does is that agent's catalog entry: with supports.providers a session is bound to one, as Claude Code and DeepSeek sessions are; with the new supports.providerModels the agent adds its profiles' models to its own list. OpenCode does the latter: its server starts with every OpenCode profile as a provider of its own, beside OpenCode Zen and the operator's providers, which all stay. A profile's token sits only in that server's environment, and the server answers only with a password made for it, since its API reads its config back. The bridge hands an agent its profiles after initialize and on every change; a changed list restarts OpenCode's server, and sessions on it resume through the bridge's restart. This replaces the per-profile OpenCode servers, which hid Zen. Talking to a profile's endpoint is now the agent host's job, since only the agent knows the API it speaks: the bridge asks the profile's agent to read the endpoint's models (list-provider-models) and to check its token (check-provider), and its own Anthropic-only token check and model reader are gone. sdk/providerApi.ts holds the two APIs drivers pick from (OpenAI's and Anthropic's sign-in and one-token request); neither follows a redirect, and an answer is read up to a size cap. A profile stored before profiles named their agent keeps an empty one; no agent uses it until a save names one. HostMessage::is_reply now names every kind, so a new reply cannot be left out of it unnoticed (provider-models was, at first). Co-Authored-By: Claude Code <noreply@anthropic.com>
Provider profiles now belong to one agent, so the phone keeps them the way it keeps plugins and MCP servers: the machine's page lists the agents that take providers, each opening its own page, which says what a provider does for that agent (a session runs on it, or its models join the agent's list). Adding or editing opens a full-page editor with the endpoint, the token and the models; the http rule now names your own network. A profile saved before profiles had an agent shows on every agent's page, to be taken over with one tap or deleted. A new session offers only the chosen agent's own providers. Snapshots cover the list, the empty page and the editor. Co-Authored-By: Claude Code <noreply@anthropic.com>
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.
What changes
Provider profiles (the phone's "AI providers") become the one place to point an agent at another endpoint, without editing files on the machine.
/v1/modelson every save, instead of hand-typed ids (the default on the phone; Refresh models on each card). One reader insdk/providerModels.tsnow serves every driver; the Claude and DeepSeek "gateway" readers were two halves of it.supports.providers— a session is bound to one (Claude Code, DeepSeek Harness);supports.providerModels(new) — the agent adds its profiles' models to its own list. OpenCode does this: its server starts with every OpenCode profile as an extra provider, beside OpenCode Zen and the operator's providers, nothing removed. The bridge hands the agent its profiles afterinitializeand on every change (set-providers); a changed list restarts OpenCode's server and running sessions resume.list-provider-models) and the token check (check-provider) run in the profile's agent's driver, on the API that agent speaks (sdk/providerApi.ts: OpenAI or Anthropic sign-in). The bridge's Anthropic-only token check is gone.http://to an IP address of the user's own network (10/8, 172.16/12, 192.168/16, 100.64/10, fc00::/7; addresses only, canonical spelling), besides https and this machine. Registering a phone with a relay's admin endpoint keeps the strict rule.Security
set-provider-profile; the bridge stores them (0600) and hands them to the host asSecret. OpenCode gets them only in its server's environment (referenced from the config as{env:…}, never written to a file), and the server it starts now requires a per-start password, since its API reads the config back.Verification
cargo clippy --workspace --all-targets -D warnings,cargo test --workspace, the ignored driver-protocol spawn and bridge end-to-end tests; agent host typecheck + 536 tests; Android unit, Paparazzi (new snapshots for the providers pages) and lint.http://192.168.1.2:3458): the gateway's 14 models were read, a wrong token was rejected and the right one accepted, OpenCode's list showed them under "Home gateway" beside OpenCode Zen's 11, and a session oncodedeck-home/OpenCode Go/deepseek-v4.1-flashanswered.Not yet done: the bridge image in Docker is not rebuilt with this, and the phone build was not installed on a device.
🤖 Generated with Claude Code