Skip to content

feat: per-agent provider profiles, models read from the provider, OpenCode keeps Zen - #93

Merged
deymosh merged 6 commits into
masterfrom
feat/provider-model-sync
Oct 8, 2026
Merged

deymosh merged 6 commits into
masterfrom
feat/provider-model-sync

Conversation

@deymosh

@deymosh deymosh commented Oct 8, 2026

Copy link
Copy Markdown
Owner

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.

  • Models come from the provider. A profile can read its model list from the endpoint's /v1/models on every save, instead of hand-typed ids (the default on the phone; Refresh models on each card). One reader in sdk/providerModels.ts now serves every driver; the Claude and DeepSeek "gateway" readers were two halves of it.
  • A profile belongs to one agent. An endpoint that speaks one agent's API need not speak another's. The catalog says what a profile does for an agent:
    • 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 after initialize and on every change (set-providers); a changed list restarts OpenCode's server and running sessions resume.
  • Talking to a profile's endpoint is the agent host's job. The model list (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.
  • LAN gateways. A base URL may now be 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.
  • Phone. Each agent's providers get a page of their own, like plugins and MCP servers: list, empty state, and a full-page editor. A profile saved before profiles had an agent shows on every agent's page, to be taken over with one tap.

Security

  • Tokens: phone → bridge only on set-provider-profile; the bridge stores them (0600) and hands them to the host as Secret. 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.
  • Provider requests from the host follow no redirect (a redirect could take the token elsewhere or off https) and read answers up to a size cap; model ids and labels are sanitized and capped at 200 per profile.

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.
  • Live, against the test bridge's OpenCode 1.18.32 and a LAN gateway (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 on codedeck-home/OpenCode Go/deepseek-v4.1-flash answered.

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

deymosh and others added 6 commits October 8, 2026 15:34
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>
@deymosh
deymosh merged commit c8e20df into master Oct 8, 2026
6 checks passed
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.

1 participant