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

View File

@@ -141,6 +141,26 @@ export function catalogProviderIds(): readonly string[] {
return getBuiltinProviders()
}
/**
* Whether the installed catalog provider for one route declares an api-key
* method — the only authentication this adapter obtains on its own.
*
* A key is what the harness resolves through its own credential seam and hands
* pi-ai per request. pi-ai's other method, OAuth, resolves from a *stored*
* OAuth credential alone: `resolveProviderAuth` has no ambient path for it,
* this adapter builds its `Models` collection with no credential store, and
* nothing here runs a login flow. So a provider offering OAuth by itself
* leaves nothing for this adapter to authenticate with, and the posture such a
* provider invites — no key configured, credentials discovered by the provider
* — fails every request with `Provider is not configured`.
* @param provider - provider route key.
* @returns whether the catalog provider takes an api key; false for a route
* pi-ai does not ship, which the caller answers for separately.
*/
export function catalogProviderTakesApiKey(provider: string): boolean {
return catalogProvider(provider)?.auth.apiKey !== undefined
}
/**
* The installed catalog models for one route, indexed by model id.
* @param provider - provider route key.

View File

@@ -61,7 +61,7 @@ import { assertUsableApiKey, LlmError } from '@deepseek-ai/dsh-llm'
import type { AdapterRegistrationHandle, DirectoryRegistrationHandle, LlmConfigurableProvider } from '@deepseek-ai/dsh-llm'
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
import { PiAiAdapter } from './adapter.ts'
import { catalogProviderIds } from './catalog.ts'
import { catalogProviderIds, catalogProviderTakesApiKey } from './catalog.ts'
import { assertServiceable, Config, resolveProfiles } from './config.ts'
import type { ResolvedPiAiProviderProfile } from './config.ts'
import { discoverModels } from './discovery.ts'
@@ -105,10 +105,15 @@ function registrationFacts(profiles: ReadonlyMap<string, ResolvedPiAiProviderPro
}
/**
* The configurable-provider directory: every installed catalog route, plus
* every route the current profiles declare. A hand-declared route has no
* catalog entry, so without this union it would have no settings address and
* configuration surfaces could neither show nor edit it.
* The configurable-provider directory: every installed catalog route this
* adapter can authenticate, plus every route the current profiles declare. A
* hand-declared route has no catalog entry, so without this union it would
* have no settings address and configuration surfaces could neither show nor
* edit it.
*
* The profile half is unconditional, which is what keeps a route already
* stored against a withheld provider editable and deletable rather than
* stranded in the settings document with nothing on the page to remove it.
* @param profiles - the currently resolved provider profiles.
* @returns the directory entries in catalog order, declared routes last.
*/
@@ -129,7 +134,14 @@ function directoryEntries(
declared: !catalog.has(provider),
})
}
for (const provider of catalog) declare(provider, provider)
// A provider whose only native method is OAuth leaves this adapter nothing
// to authenticate with, so offering it would put a card on the settings page
// whose own posture — no key, credentials discovered by the provider — fails
// every request. Catalog *membership* is unaffected, so `declare` above still
// answers what pi-ai ships.
for (const provider of catalog) {
if (catalogProviderTakesApiKey(provider)) declare(provider, provider)
}
for (const [provider, profile] of profiles) declare(provider, profile.displayName)
return [...entries.values()]
}