Fixes #27727. Diagnosis and first commit by @UditSinghChauhan.
## Summary
In the New AI Provider form, picking "openai" pre-fills the label
"Openai". The form uses the label as the provider name, so it saved
`AI_PROVIDERS.openai` with no models. That entry replaces the built-in
OpenAI provider, so all OpenAI models disappeared. The admin page still
listed it as built in, so it offered no Remove button. Its API key link
pointed at `OPENAI_API_KEY`, which has no effect while the override
exists.
```diff
addProvider(providerName)
assertCustomAiProviderAccess
check name pattern
+ if providerName is a built-in provider name
+ throw BUILT_IN_AI_PROVIDER_NAME
save AI_PROVIDERS[providerName]
getMaskedProviders
- source = isBuiltInName ? catalog : custom
+ source = isBuiltInName and not in AI_PROVIDERS ? catalog : custom
+ apiKeyConfigVariable only for catalog providers
```
```diff
<SettingsAdminNewAiProvider>
+ label matches a built-in provider: banner with an Open link to that provider's page (models + API key)
save
- any error: "Failed to add provider" toast
+ BUILT_IN_AI_PROVIDER_NAME: error on the Label field
+ any other error: "Failed to add provider" toast
```
- Setting `AI_PROVIDERS` directly is unchanged: a same-named entry still
replaces the built-in provider (#25753). Its description now says so
instead of "deep-merged".
- An instance that already has the bad entry sees OpenAI as a custom
provider. **Remove provider** brings the built-in models back.
## Evidence
- **Before:** the reporter's OpenAI page lists no models and has no way
to remove the entry (screenshot in #27727).
**After:** @UditSinghChauhan removed the reporter's override on a local
dev stack: OpenAI went from 0 back to 44 models, with no restart. The
form shows the rejection on the Label field:
<img width="640" alt="New AI Provider form rejecting the openai label"
src="https://github.com/user-attachments/assets/9d1a0527-61da-48cc-acfb-e72d6c3c2ec3"
/>
- New integration spec
`admin-panel-ai-provider-catalog-override.integration-spec.ts`, run
against the admin panel API:
```text
addAiProvider("openai")
main: saves the entry -> fails
here: BUILT_IN_AI_PROVIDER_NAME -> passes
AI_PROVIDERS = { openai: {...} }, then getAiProviders
main: openai is catalog, links OPENAI_API_KEY -> fails
here: openai is custom, no key link -> passes
removeAiProvider("openai"), then getAiProviders
openai is catalog again, links OPENAI_API_KEY
```
Not run locally (the integration suite needs more memory than was free),
so CI is its first run. The changed files pass oxlint, oxfmt and a
scoped typecheck.
## Merge Danger
**Door:** two-way
Nothing is migrated or deleted. Existing `AI_PROVIDERS` entries are only
shown differently.
**Blast Radius:** admin panel
- An instance that overrides a built-in provider through `AI_PROVIDERS`
sees it under custom providers, with Remove and per-model remove
buttons. That is accurate, since the override decides its models, but
admins will see the provider move between the two lists.
- `addAiProvider` now rejects built-in names. This is on the
`/admin-panel` API, which the breaking-changes check does not cover.
---------
Co-authored-by: Udit Singh Chauhan <187852344+UditSinghChauhan@users.noreply.github.com>