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.
This commit is contained in:
Yichen Jiang
2026-08-13 13:15:47 +08:00
parent 137c3c9254
commit a5a83bd1d9
11 changed files with 171 additions and 10 deletions
@@ -933,4 +933,39 @@ describe('configurable-provider directory', () => {
await ctx.settings.replace(settingsNamespace('llm-pi-ai'), {})
expect(ctx.llm.listConfigurableProviders()).toHaveLength(catalogOnly)
})
it('withholds a catalog route this adapter cannot authenticate', async () => {
const ctx = await harness({})
const offered = ctx.llm.listConfigurableProviders().map(entry => entry.provider)
// `openai-codex` is the one installed provider that authenticates through
// OAuth alone. pi-ai resolves OAuth only from a *stored* credential, this
// adapter constructs its collection with no credential store, and nothing
// here runs a login flow — so every request on such a route fails with
// `Provider is not configured` before it goes out. Offering it would put a
// provider on the settings page that no amount of configuration can make
// work.
expect(offered).not.toContain('openai-codex')
// A provider that offers OAuth *beside* an api-key method keeps its entry:
// the key is a path this adapter can serve.
expect(offered).toContain('anthropic')
expect(offered).toContain('openai')
})
it('still lists a withheld route a stored profile names, as a catalog route', async () => {
// Withholding the offer must not strand a profile someone already stored:
// the route keeps its entry so a configuration surface can edit or delete
// it, and `declared` still answers catalog membership rather than the
// offer, so the page does not mislabel it as a route this deployment
// invented.
const ctx = await harness({ providers: { 'openai-codex': { apiKeyEnv: KEY_ENV } } })
expect(ctx.llm.listConfigurableProviders()).toContainEqual({
provider: 'openai-codex',
displayName: 'openai-codex',
settingsNs: 'llm-pi-ai',
settingsPath: ['providers', 'openai-codex'],
declared: false,
})
})
})