Summary
Three problems, one piece of work:
- A connected provider's model list is fetched once, when it is connected, and never again. If the cached list is missing (a cleaned profile, a new machine, a hand-written
model_sources row, a profile copied from elsewhere), the provider stays connected, but /model has nothing to offer from it. Nothing refetches the list: not the next launch, and not ctrl+r.
- There is no one word for "the thing you connect that serves models". The same thing is called provider, service, model service, connection, custom connection and
models across /model, /connect, Settings, the CLI and the manual. Each of those words also means something else somewhere.
- Adding a provider is hard to find and has too many steps. You have to know about
/connect, find the models group inside a panel described as "your connected accounts", and type an address by hand, even when a model server is already running on this machine.
This issue settles the words, fixes the listing, and gives every surface the same way to see and add providers.
What was measured
Build fbd8e42c3 (dev at 7f136b301 plus open PRs), on Spark with an isolated CODEAF_HOME. The profile's only non-default row is:
{"id":"custom-local","written":"local","address":"http://127.0.0.1:8317/v1","key":"...","listed":true,"order":1}
A stub server on 8317 answers GET /v1/models with two models.
/connect shows ✓ local as connected.
/model shows the openrouter group with 547 models, and a local group whose only row is the dim no list from this service · type a model id.
- The stub received zero requests across launch, opening
/model, and the Settings Providers tab.
- The same result on dev commits
7f136b301, 5256d7083, debffabf6, b378e9275 and c084b60d7. This is not a recent regression. People notice it after anything that removes the per-provider cache files (v3/models-<key>.json, model-catalog-<key>.json).
Where
- The only fetch is at connect time:
beginModelConnect in internal/tui3/modelservices.go (around line 623), through refreshService in cmd/codeaf/chatv3_modelshelf.go.
- At launch,
v3ModelShelf.setSources (cmd/codeaf/chatv3_modelshelf.go, around line 57) fills each provider's compartment from tui3.CachedModelsFor only. When the file is missing, the compartment is empty.
modelsForService returns an empty compartment as an empty answer on purpose, because it is on the draw path, so nothing off the frame is ever asked to fill it.
- ctrl+r in
/model is RefreshModels, which refreshes the OpenRouter catalog only.
1. The words (decided here, applied everywhere a person reads)
| Concept |
The word |
Retired from person-facing text |
| What you connect that serves models: OpenRouter, DeepSeek, Z.ai, Codex, Ollama, a custom OpenAI-compatible server |
provider |
service, model service, default service, connection, custom connection, active connection, the models group head |
One of the companies OpenRouter routes a model to (the provider row, → providers in /model, the fold) |
host (→ hosts, row host, "served by ") |
provider, for this meaning |
| Slack, Google, tool servers and the like |
account |
service, connection (the tab can stay named Connections, since it holds accounts) |
A long-running process you keep (docs/SERVICES.md) |
unchanged |
not touched here |
Why: a person connecting OpenRouter or their own server calls it a provider, and the Settings tab is already named Providers. The routing meaning is the rarer one (one row, one fold) and needs its own word so the two can sit on the same tab without clashing.
This reverses the vocabulary of #1027 and #1023 for the routing meaning. Config keys and identifiers do not change: model_sources, lane.talk, modelsource.* and internal/lane stay as they are. This is a change of vocabulary, not architecture.
The law test moves with the words. internal/tui3/providerword_law_test.go currently rejects only lane. It should also reject service, model service and connection in person-facing model-provider strings, and provider in host-meaning strings, over the same surfaces it scans today. It must fail on the strings listed below if one is left unchanged.
Strings known to change (from an audit of dev 7f136b301; the implementer re-greps, since the list is a starting point, not the set):
internal/tui3/modelservices.go: no list from this service · type a model id; …connect a service or pick a model; add custom connection; active connection
internal/tui3/settings.go: the services section head (around line 1430); the provider row and its note "one model is served by many providers" (around lines 741 and 756)
internal/tui3/palette.go: → providers and tab providers (around lines 2536 and 2540); the fold's hints
internal/tui3/lanes.go: provider · <name>; internal/tui3/phase.go: all providers slow
internal/tui3/connectcaps.go: group head models; internal/tui3/commands.go: /connect description your connected accounts · connect another
internal/tui3/firstrun.go, internal/tui3/onboarding.go: "default service", "provider account"
cmd/codeaf/main.go and cmd/codeaf/connect.go: connect <service>, "model services this profile knows", "no model service is connected"
- the manual pages
services.md, lanes.md and accounts.md, and docs/GUIDE.md
2. The listing fix
- At launch, every connected provider that lists models and has an empty or missing cache is fetched in the background, off the event loop, through the same door the connect path uses. When the fetch succeeds, its group fills without a reopen.
- ctrl+r in
/model refreshes every connected provider, not only OpenRouter. When one fails, the others still land.
- A provider that failed to list still has its group, and the group says why and what to do (below). It is never an empty group, and never silently absent.
- The draw path still never reads a file or makes a network call. The fetch is a command; the frame reads the compartment.
- The engine side (
--host) gets the same rule, so a hosted engine lists the far machine's providers the same way.
3. How providers look, everywhere
/model, when two or more providers are connected. The group head carries a status in dim text:
model ↓ in/M out/M window
local 127.0.0.1:8317 · 12 models
grok-4.3 256k
claude-opus-5 1M
openrouter 547 models
z-ai/glm-5.3 $1.4 $4.4 1M
...
+ add a provider
→ hosts · alt+s sort · enter switch · ctrl+r refresh · esc
The dim status line under a provider's head is one of:
listing models…
can't list models · connection refused · ctrl+r retry
lists no models · type a model id
Settings → Providers. The first section lists every connected provider, including the default one, which today appears only as an openrouter key row:
providers
openrouter openrouter.ai 547 models key saved
local 127.0.0.1:8317 12 models listed 2m ago
+ add a provider
enter on a provider offers: refresh models · rename · change key · disconnect. The host, routing and speed guard rows stay on the tab, below a routing head.
/connect. The group head models becomes providers, and the command's description becomes providers and accounts · connect another.
CLI. codeaf connect help and output use provider: codeaf connect <provider>, "providers this profile knows", "no provider is connected".
+ add a provider is the same flow on every surface (next section). There is one flow, reached from three places.
4. Adding a provider, made easy
+ add a provider opens one list:
add a provider
found on this machine
● 127.0.0.1:8317 12 models enter connect
● Ollama :11434 4 models enter connect
providers
OpenRouter key
DeepSeek key
Z.ai region · key
Codex browser
any OpenAI-compatible server address
- Found on this machine: when the list opens, probe
GET /v1/models on the usual local ports: 11434 (Ollama), 1234 (LM Studio), 8000 (vLLM), 8080 (llama.cpp) and 8317. Each probe has a short timeout, runs off the frame, and asks only for loopback. A server that answers is shown with its model count; enter connects it with the port's name as the default name.
- Any OpenAI-compatible server: typing an address probes it live and shows
12 models or the refusal before anything is saved. A key is asked for only when the server answers 401 or 403.
- Naming stays as today (
PrepareCustomSource): the host becomes the default name, and it can be edited before saving.
Acceptance
End to end first, on the real binary with an isolated CODEAF_HOME and CODEAF_PROFILE_DIR, telemetry off, and a stub /v1/models server:
- A profile with a
model_sources row and no cache files: launch, then open /model. The provider's group fills with the stub's models without a reopen, and the stub logs a request. Red on dev today.
- The stub down at launch: the group reads
can't list models · connection refused · ctrl+r retry. Start the stub, press ctrl+r, and the group fills.
- Settings → Providers lists both
openrouter and local, with model counts. enter → refresh models asks the stub again.
+ add a provider with a stub on 8317 and nothing else running: the "found on this machine" section shows exactly one server with the stub's model count, and enter connects it.
- The word law: every string in section 1 is changed, and the extended law test goes red when any one of them is restored. The mutation is part of the PR's evidence.
/model on a profile with only OpenRouter connected is unchanged apart from → hosts.
The draw path stays free of I/O: the existing tests that pin "no file read on paint" still pass, and a new one proves the launch fetch runs as a command.
Out of scope
Related
#1023 (host pin in /model, whose wording this supersedes), #1027 (the provider-word rule, whose routing meaning this reverses), #1303 (Settings re-tab), #1414 (task catalog and custom providers).
Summary
Three problems, one piece of work:
model_sourcesrow, a profile copied from elsewhere), the provider stays connected, but/modelhas nothing to offer from it. Nothing refetches the list: not the next launch, and not ctrl+r.modelsacross/model,/connect, Settings, the CLI and the manual. Each of those words also means something else somewhere./connect, find themodelsgroup inside a panel described as "your connected accounts", and type an address by hand, even when a model server is already running on this machine.This issue settles the words, fixes the listing, and gives every surface the same way to see and add providers.
What was measured
Build
fbd8e42c3(dev at7f136b301plus open PRs), on Spark with an isolatedCODEAF_HOME. The profile's only non-default row is:{"id":"custom-local","written":"local","address":"http://127.0.0.1:8317/v1","key":"...","listed":true,"order":1}A stub server on 8317 answers
GET /v1/modelswith two models./connectshows✓ localas connected./modelshows theopenroutergroup with 547 models, and alocalgroup whose only row is the dimno list from this service · type a model id./model, and the Settings Providers tab.7f136b301,5256d7083,debffabf6,b378e9275andc084b60d7. This is not a recent regression. People notice it after anything that removes the per-provider cache files (v3/models-<key>.json,model-catalog-<key>.json).Where
beginModelConnectininternal/tui3/modelservices.go(around line 623), throughrefreshServiceincmd/codeaf/chatv3_modelshelf.go.v3ModelShelf.setSources(cmd/codeaf/chatv3_modelshelf.go, around line 57) fills each provider's compartment fromtui3.CachedModelsForonly. When the file is missing, the compartment is empty.modelsForServicereturns an empty compartment as an empty answer on purpose, because it is on the draw path, so nothing off the frame is ever asked to fill it./modelisRefreshModels, which refreshes the OpenRouter catalog only.1. The words (decided here, applied everywhere a person reads)
modelsgroup headproviderrow,→ providersin/model, the fold)→ hosts, rowhost, "served by ")Connections, since it holds accounts)docs/SERVICES.md)Why: a person connecting OpenRouter or their own server calls it a provider, and the Settings tab is already named
Providers. The routing meaning is the rarer one (one row, one fold) and needs its own word so the two can sit on the same tab without clashing.This reverses the vocabulary of #1027 and #1023 for the routing meaning. Config keys and identifiers do not change:
model_sources,lane.talk,modelsource.*andinternal/lanestay as they are. This is a change of vocabulary, not architecture.The law test moves with the words.
internal/tui3/providerword_law_test.gocurrently rejects onlylane. It should also rejectservice,model serviceandconnectionin person-facing model-provider strings, andproviderin host-meaning strings, over the same surfaces it scans today. It must fail on the strings listed below if one is left unchanged.Strings known to change (from an audit of dev
7f136b301; the implementer re-greps, since the list is a starting point, not the set):internal/tui3/modelservices.go:no list from this service · type a model id;…connect a service or pick a model;add custom connection;active connectioninternal/tui3/settings.go: theservicessection head (around line 1430); theproviderrow and its note "one model is served by many providers" (around lines 741 and 756)internal/tui3/palette.go:→ providersandtab providers(around lines 2536 and 2540); the fold's hintsinternal/tui3/lanes.go:provider · <name>;internal/tui3/phase.go:all providers slowinternal/tui3/connectcaps.go: group headmodels;internal/tui3/commands.go:/connectdescriptionyour connected accounts · connect anotherinternal/tui3/firstrun.go,internal/tui3/onboarding.go: "default service", "provider account"cmd/codeaf/main.goandcmd/codeaf/connect.go:connect <service>, "model services this profile knows", "no model service is connected"services.md,lanes.mdandaccounts.md, anddocs/GUIDE.md2. The listing fix
/modelrefreshes every connected provider, not only OpenRouter. When one fails, the others still land.--host) gets the same rule, so a hosted engine lists the far machine's providers the same way.3. How providers look, everywhere
/model, when two or more providers are connected. The group head carries a status in dim text:The dim status line under a provider's head is one of:
listing models…can't list models · connection refused · ctrl+r retrylists no models · type a model idSettings → Providers. The first section lists every connected provider, including the default one, which today appears only as an
openrouter keyrow:enter on a provider offers: refresh models · rename · change key · disconnect. The
host,routingandspeed guardrows stay on the tab, below aroutinghead./connect. The group headmodelsbecomesproviders, and the command's description becomesproviders and accounts · connect another.CLI.
codeaf connecthelp and output use provider:codeaf connect <provider>, "providers this profile knows", "no provider is connected".+ add a provideris the same flow on every surface (next section). There is one flow, reached from three places.4. Adding a provider, made easy
+ add a provideropens one list:GET /v1/modelson the usual local ports: 11434 (Ollama), 1234 (LM Studio), 8000 (vLLM), 8080 (llama.cpp) and 8317. Each probe has a short timeout, runs off the frame, and asks only for loopback. A server that answers is shown with its model count; enter connects it with the port's name as the default name.12 modelsor the refusal before anything is saved. A key is asked for only when the server answers 401 or 403.PrepareCustomSource): the host becomes the default name, and it can be edited before saving.Acceptance
End to end first, on the real binary with an isolated
CODEAF_HOMEandCODEAF_PROFILE_DIR, telemetry off, and a stub/v1/modelsserver:model_sourcesrow and no cache files: launch, then open/model. The provider's group fills with the stub's models without a reopen, and the stub logs a request. Red on dev today.can't list models · connection refused · ctrl+r retry. Start the stub, press ctrl+r, and the group fills.openrouterandlocal, with model counts. enter → refresh models asks the stub again.+ add a providerwith a stub on 8317 and nothing else running: the "found on this machine" section shows exactly one server with the stub's model count, and enter connects it./modelon a profile with only OpenRouter connected is unchanged apart from→ hosts.The draw path stays free of I/O: the existing tests that pin "no file read on paint" still pass, and a new one proves the launch fetch runs as a command.
Out of scope
v3TaskModels). Link it, don't fold it in.Related
#1023 (host pin in
/model, whose wording this supersedes), #1027 (the provider-word rule, whose routing meaning this reverses), #1303 (Settings re-tab), #1414 (task catalog and custom providers).