DSH / Atlas
2026-08-13implementedbug-fix

The configurable-provider directory withholds OAuth-only providers

可配置提供方目录不再提供仅以 OAuth 认证的提供方

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

Source compatibility: Chinese title uses a compatible non-canonical header

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.

The withholding has since been retired: credential records and authorization flows gave the adapter a durable credential store and a login flow, so openai-codex is offered again and catalogProviderTakesApiKey is gone. The two boundaries below are what kept that reversal to one predicate and one directory filter.

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.

中文

问题

模型设置页把 openai-codex 当作普通 pi-ai 路由提供出来,配的还是每个 pi-ai 提供方共用的那句占位文案:填入 API 密钥,或留空使用环境认证。照此配置后发送消息,本轮以 Provider is not configured: openai-codex 失败,并被适配器归入兜底的 PI_AI_ERROR

占位文案所邀请的那种配置姿态在这条路由上不可能工作。pi-ai 的 resolveProviderAuth 抵达 OAuth 提供方只有一条路径——集合的 CredentialStore 里已经存着的凭据——对它没有任何 ambient 回退;而 openai-codex 正是已安装 catalog 中唯一只声明 auth.oauth、没有 auth.apiKey 的提供方。PiAiAdapter.current() 以不带参数的 createModels() 构造集合,于是用的是 pi-ai 默认的 InMemoryCredentialStore:每次启动都是空的,每次配置变更产生新快照时又重建一份。本仓库没有任何位置调用 Models.login();pi-ai 库这一半也不会去读 Codex 自己的 ~/.codex/auth.json——它的 OAuth 模块是一套 PKCE 登录流程,凭据由宿主应用持久化,这正是 pi CLI 提供、而本适配器没有提供的东西。

于是页面用自己占位文案所描述的「留空」姿态,提供了一个根本没有「留空」姿态的提供方——而失败信息指向的是配置键,不是缺失的能力。唯一能让这条路由完成认证的,是把一个 ChatGPT OAuth token 粘进密钥框,那既不是这个提供所描述的用法,也会过期且这里没有任何环节会去刷新它。

决策

目录只提供本适配器认得的东西。catalogProviderTakesApiKey(provider) 回答 pi-ai 为某路由安装的提供方是否声明了 api-key 方法——这是 harness 唯一能供给的方法,因为它通过自己的凭据 seam 解析密钥,再作为请求的 apiKey 覆盖交给 pi-ai——directoryEntries() 跳过不满足它的 catalog 路由。

不尝试实现 OAuth。它需要持久化凭据存储、登录流程,以及运行登录的界面;这三样都不是发布阻塞项的修复,而在它们缺席时仍把提供方摆出来,正是这次报告的成因。

两条边界把「不提供」的范围收窄:

  • catalog 成员身份不变。 catalogProviderIds() 仍回答 pi-ai 装了什么,因此目录条目上的 declared 标记仍然表示「没有已安装提供方对应这条路由」,而不是「这条路由不被提供」。
  • 联合的 profile 那一半无条件保留。 settings 文档已经写过的路由保留条目,因此已存储的 openai-codex profile 仍然可见、可编辑、可删除,而不会滞留在文档里、页面上却没有任何入口能移除它。

resolution 未被触动。在仅 OAuth 的路由上指定 apiKeyEnv 的 profile 仍会构造出可用的提供方——routeAuth 会在 catalog 的 OAuth 旁边补上 harness 的 api-key 方法,而 pi-ai 的 Codex API 从 token 本身推导 account id——因此把它写进 settings.yamlcordis.yml 的部署保留这条路径。改为在 resolveProfiles 里强制拒绝会在注册时就否掉这类 profile;又因为 validate 在启动时与写入时同样运行,一份已经写有无密钥 OAuth 路由的文档会让整个 namespace 注册失败,而不只是一个提供方失败。

备选方案

  • resolveProfiles 里拒绝无密钥的仅 OAuth 路由。 这才是本仓库通常强制决策的位置,而目录过滤是一层 cordis.yml entry 可以绕过的表面。因上述启动行为被否决:已存储的 profile 会连带拖垮该 namespace 中其他所有路由,对一次发布而言,这是拿一个提供方的缺陷换取全体的缺陷。留下的缺口是:被修的是「提供」而不是「能力」——部署仍可手写一条页面上已经无法添加的路由。
  • 保留提供,只修正占位文案。 那么该输入框只能写「此提供方需要本构建无法运行的登录」,等于一张唯一诚实内容就是「它不能用」的卡片。
  • Provider is not configured 映射成具名 LlmError 值得做,而且触发原因本次改动并未消除——任何留空密钥、其提供方又在进程环境里找不到东西的 api-key 路由,都会产生同一句话。作为独立改动暂缓:它改进的是诊断,而不是移除一个坏掉的提供。
  • ~/.codex/auth.json 读进 pi-ai 的 CredentialStore 这能让 Codex 在没有登录流程的情况下可用,刷新也由 pi-ai 负责。但它为一个提供方把 harness 绑定到另一个工具的私有文件格式上,这属于 OAuth 那项工作的决策,而不是发布期的修复。

影响

openai-codex 从提供方选择器、以及模型设置页所 join 的目录中消失;其余已安装提供方一概不受影响,包括在 api-key 方法之外另提供 OAuth 的那六个(anthropicgithub-copilotkimi-codingopenrouterradiusxai),它们保留条目也保留密钥路径。将来若出现只带 OAuth 的提供方,会被自动排除,而不是靠列名。

两处相邻缺口仍在,并记录在包 README 中:不指定凭据的路由仍走 catalog 提供方自带的发现,而它只读进程环境变量——不读 ~/.aws/credentials,也不读 harness 凭据 seam——且由此产生的失败仍是兜底的 PI_AI_ERROR

该扣留其后已被撤销:凭据记录与授权 flow 为适配器补上了可持久的凭据存储与登录 flow,因此 openai-codex 重新被提供,catalogProviderTakesApiKey 也已删除。下文那两条边界正是把这次反转限制在一个判定函数与一个目录过滤器之内的原因。

测试

包测试钉住联合的两半:不予提供的路由不出现在 listConfigurableProviders() 中,而 anthropicopenai 仍在;已存储的 openai-codex profile 仍产出完整条目且 declared: false。既有的 resolution 测试未改动且依然通过,这正是「不提供」没有收窄手写 profile 可服务范围的证据。models-settingsonboarding-usable-provider 两条 web e2e golden 恰好各少了 openai-codex 这一行选项,录自真实装配的应用。