Files
deepseek-harness/.agents/notes/implemented/bug-fix/2026-08-13-oauth-only-providers-withheld.md
T
Yichen Jiang a5a83bd1d9 fix(llm-pi-ai): withhold OAuth-only providers from the configurable directory
pi-ai resolves an OAuth provider from a stored OAuth credential alone, and
this adapter builds its Models collection with no credential store and runs
no login flow. `openai-codex` — the one installed provider declaring
`auth.oauth` with no `auth.apiKey` — was therefore offered on the Models page
with the keyless placeholder every pi-ai route carries, and every request on
it failed `Provider is not configured` before going out.

`catalogProviderTakesApiKey()` answers whether pi-ai's installed provider for
a route declares the one method this adapter can supply, and the directory
skips the catalog routes that fail it. Catalog membership is unchanged, so
`declared` still answers what pi-ai ships; the profile half of the union stays
unconditional, so a route a settings document already names keeps its entry
and can be edited or deleted.

Resolution is untouched: a profile naming `apiKeyEnv` on such a route still
builds a working provider.
2026-08-13 13:15:47 +08:00

6.2 KiB

Agent Note: The configurable-provider directory withholds OAuth-only providers

Status: implemented

English | 中文

Problem

The Models page offered openai-codex like any other pi-ai route, with the placeholder every pi-ai provider carries: enter a key, or leave it blank to authenticate from the environment. Configuring it that way and sending a message failed the turn with Provider is not configured: openai-codex, reported as the adapter's catch-all PI_AI_ERROR.

The posture the placeholder invited could not work on that route. pi-ai's resolveProviderAuth reaches an OAuth provider through one path — a credential already in the collection's CredentialStore — and has no ambient fallback for it, while openai-codex is the one installed provider declaring auth.oauth with no auth.apiKey beside it. PiAiAdapter.current() constructs its collection with createModels() and no options, so the store is pi-ai's default InMemoryCredentialStore: empty at every boot, and rebuilt from scratch each time a configuration change produces a new snapshot. No code here runs Models.login(), and pi-ai's library half does not read Codex's own ~/.codex/auth.json either — its OAuth module is a PKCE login flow whose credential the host application persists, which is what the pi CLI supplies and this adapter does not.

So the page advertised, with the keyless posture its own placeholder describes, a provider that has no keyless posture — and the failure named the configuration key rather than the missing capability. The one thing that does authenticate the route is a ChatGPT OAuth token pasted into the key field, which is not what the offer describes and expires with nothing here to refresh it.

Decision

The directory offers only what this adapter can authenticate. catalogProviderTakesApiKey(provider) answers whether pi-ai's installed provider for a route declares an api-key method — the one method the harness can feed, since it resolves a key through its own credential seam and hands it over as the request's apiKey override — and directoryEntries() skips the catalog routes that fail it.

OAuth support is not attempted. It needs a persistent credential store, a login flow, and a surface to run it from; none of those is a release-blocking fix, and shipping the offer without them is what produced the report.

Two boundaries keep the withholding narrow:

  • Catalog membership is unchanged. catalogProviderIds() still answers what pi-ai ships, so the declared flag on a directory entry keeps meaning "no installed provider answers for this route" rather than "this route is not offered".
  • The profile half of the union is unconditional. A route a settings document already names keeps its entry, so a stored openai-codex profile stays visible, editable, and deletable instead of being stranded in the document with nothing on the page to remove it.

Resolution is untouched. A profile naming apiKeyEnv on an OAuth-only route still builds a working provider — routeAuth adds the harness api-key method beside the catalog's OAuth, and pi-ai's Codex API derives the account id from the token itself — so a deployment that writes one into settings.yaml or cordis.yml keeps that path. Enforcing the withholding in resolveProfiles instead would have refused such a profile at registration, and because validate runs at boot as well as at write time, a document already naming a keyless OAuth route would fail the whole namespace's registration rather than one provider.

Alternatives considered

  • Rejecting a keyless OAuth-only route in resolveProfiles. This is where the repo normally enforces a decision, and the directory filter is a surface that a cordis.yml entry bypasses. It was refused for the boot behavior above: an existing stored profile would take down every other route in the namespace with it, which for a release trades a one-provider defect for a total one. The gap is that the offer, not the capability, is what got fixed — a deployment can still hand-write the route it can no longer add from the page.
  • Keeping the offer and correcting only the placeholder text. The field would then have to say the provider needs a login this build cannot run, which is a card whose only honest content is that it does not work.
  • Mapping Provider is not configured to a named LlmError. Worth doing, and reachable for reasons this change does not remove — any api-key route left blank whose provider finds nothing in the process environment produces the same message. Deferred as a separate change: it improves a diagnostic rather than removing a broken offer.
  • Reading ~/.codex/auth.json into a pi-ai CredentialStore. It makes Codex work without a login flow, and pi-ai owns the refresh. It also binds the harness to another tool's private file format for one provider, which is a decision for the OAuth work rather than a release fix.

Consequences

openai-codex disappears from the provider picker and from the directory the Models page joins; every other installed provider is unaffected, including the six that offer OAuth beside an api-key method (anthropic, github-copilot, kimi-coding, openrouter, radius, xai), which keep their entries and their key path. A future provider that ships OAuth alone is withheld automatically rather than by name.

Two adjacent gaps remain and are recorded in the package README: a route naming no credential still resolves through the catalog provider's own discovery, which reads process environment variables only — not ~/.aws/credentials, and not the harness credential seam — and the resulting failure is still the catch-all PI_AI_ERROR.

Testing

Package tests pin both halves of the union: the withheld route is absent from listConfigurableProviders() while anthropic and openai stay, and a stored openai-codex profile still produces a full entry with declared: false. The existing resolution tests are unchanged and still pass, which is what shows the withholding did not narrow what a hand-written profile can serve. The models-settings and onboarding-usable-provider web e2e goldens lost exactly the openai-codex option line, recorded against the real assembled application.