Skip to content

Providers: one word everywhere, a connected provider always lists its models, and adding one takes one step #1508

Description

@santoshkumarradha

Summary

Three problems, one piece of work:

  1. 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.
  2. 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.
  3. 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:

  1. 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.
  2. 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.
  3. Settings → Providers lists both openrouter and local, with model counts. enter → refresh models asks the stub again.
  4. + 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.
  5. 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.
  6. /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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:chatThe v3 surface a person sits in front of (internal/tui3)area:providerRouting, lanes, refusals, hedging, what a call costsbugSomething the code does that it should notfeatureWork that adds a capability; developers break it into tasks

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions