Merge pull request #2464 from deepseek-harness/fix/withhold-oauth-only-providers
不再向模型设置页提供 harness 无法认证的仅 OAuth 提供方
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-13-oauth-only-providers-withheld.md
|
||||
2026-08-13-oauth-only-providers-withheld.md: 8c85c2900fd0af7c97a3ac8f04d3c6a2f469f8b0
|
||||
2026-08-13-oauth-only-providers-withheld.zh.md: 8f05d096a5d474407585bcbd84bd47da8a1abb2e
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note: The configurable-provider directory withholds OAuth-only providers
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-13-oauth-only-providers-withheld.zh.md)
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note:可配置提供方目录不再提供仅以 OAuth 认证的提供方
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-13-oauth-only-providers-withheld.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
模型设置页把 `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.yaml` 或 `cordis.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 的那六个(`anthropic`、`github-copilot`、`kimi-coding`、`openrouter`、`radius`、`xai`),它们保留条目也保留密钥路径。将来若出现只带 OAuth 的提供方,会被自动排除,而不是靠列名。
|
||||
|
||||
两处相邻缺口仍在,并记录在包 README 中:不指定凭据的路由仍走 catalog 提供方自带的发现,而它只读进程环境变量——不读 `~/.aws/credentials`,也不读 harness 凭据 seam——且由此产生的失败仍是兜底的 `PI_AI_ERROR`。
|
||||
|
||||
## 测试
|
||||
|
||||
包测试钉住联合的两半:不予提供的路由不出现在 `listConfigurableProviders()` 中,而 `anthropic` 与 `openai` 仍在;已存储的 `openai-codex` profile 仍产出完整条目且 `declared: false`。既有的 resolution 测试未改动且依然通过,这正是「不提供」没有收窄手写 profile 可服务范围的证据。`models-settings` 与 `onboarding-usable-provider` 两条 web e2e golden 恰好各少了 `openai-codex` 这一行选项,录自真实装配的应用。
|
||||
@@ -44,7 +44,6 @@
|
||||
- option "moonshotai-cn"
|
||||
- option "nvidia"
|
||||
- option "openai"
|
||||
- option "openai-codex"
|
||||
- option "opencode"
|
||||
- option "opencode-go"
|
||||
- option "openrouter"
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
- option "moonshotai-cn"
|
||||
- option "nvidia"
|
||||
- option "openai"
|
||||
- option "openai-codex"
|
||||
- option "opencode"
|
||||
- option "opencode-go"
|
||||
- option "openrouter"
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
|
||||
README.md: e6d04133f3dfa4eb4cd9aa997ad1f9bb93fa50a0
|
||||
README.zh.md: e33bcac7081006573f5c6a8a8a3221fa57839c4f
|
||||
README.md: 6120f8d982c6d475cd508e6cf9e41cabfc9ba159
|
||||
README.zh.md: 4b47976c6c6c67968b5b93edbdfd5dfa9530eb1d
|
||||
@@ -189,6 +189,8 @@ Recorded response content appends to the next request and does not invalidate it
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **A provider that authenticates through OAuth alone is not offered** — pi-ai resolves OAuth from a *stored* OAuth credential, and this adapter builds its `Models` collection with no credential store and runs no login flow, so every request on such a route fails `Provider is not configured` before it goes out. The configurable-provider directory withholds them; `openai-codex` is the only one the installed catalog ships. A route a settings document already names keeps its entry so a configuration surface can edit or delete it, and `apiKeyEnv` still authenticates it with that key — which for Codex is a token that expires with nothing here to refresh it.
|
||||
- **Provider-native discovery reads the process environment only** — a route naming no credential defers to the catalog provider's own resolution, which interrogates environment variables (`AZURE_OPENAI_API_KEY`, `AWS_PROFILE`, `AWS_ACCESS_KEY_ID`, and each provider's own set). It reads no local credential directory, so `~/.aws/credentials` without an exported `AWS_PROFILE` resolves as unconfigured, and a value held by the harness credential seam is invisible to it unless the process environment carries it too.
|
||||
- **Settings can add or override routes, not remove composition routes** — the user layer merges over the composition `base`, so deleting a `cordis.yml`-provided provider is a composition change; `replace` on the namespace only resets the user layer.
|
||||
- **The layered merge has no delete for dict keys** — the settings seam merges the composition `base` and the user layer per key, recursively, so a `reasoningEfforts` level, `modelOverrides` entry, or `compat` field the base declares cannot be removed by the user layer, only overridden — and for `reasoningEfforts` absence *is* the meaning ("not offered"), so a base-declared level stays offered. This only triggers when a `cordis.yml` entry config declares per-model reasoning fields for the same model the user layer edits; the supported posture is to leave those to the settings document (the shipped composition mounts the adapter dormant), and a `models` list is an array replacing wholesale, which is the in-band escape.
|
||||
- **`headers` can carry a credential the redactor never sees** — the profile's `headers` dict is plain strings, so `Authorization` or `api-key` set there is returned verbatim by a redacted `describe()` and rendered by any configuration UI. Store credentials as `apiKeyEnv` references; making the dict write-only is deferred with the rest of the [wire-boundary work](../llm/README.md#known-limitations-and-deferred-work).
|
||||
|
||||
@@ -190,6 +190,8 @@ pi-ai 事件会变为 harness 推理、文本、工具调用、usage 与 finish
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **仅以 OAuth 认证的提供方不予提供**:pi-ai 的 OAuth 只从*已存储*的 OAuth 凭据解析,而本适配器构造 `Models` 集合时不注入凭据存储、也不运行登录流程,因此这类路由的每个请求都会在发出之前以 `Provider is not configured` 失败。可配置提供方目录因此不列出它们;已安装 catalog 中只有 `openai-codex` 属于此类。settings 文档已经写过的路由仍保留目录条目,配置界面据此可以编辑或删除;`apiKeyEnv` 也仍能用该密钥完成认证——对 Codex 而言那是一个会过期、且这里没有任何环节会去刷新的 token。
|
||||
- **提供方自带的凭据发现只读进程环境**:不指定凭据的路由交由 catalog 提供方自行解析,而它探测的是环境变量(`AZURE_OPENAI_API_KEY`、`AWS_PROFILE`、`AWS_ACCESS_KEY_ID` 以及各提供方自己的那一组)。它不读任何本地凭据目录,因此只有 `~/.aws/credentials` 而未导出 `AWS_PROFILE` 会被解析为未配置;由 harness 凭据 seam 保管的值,除非进程环境里也有,否则对它不可见。
|
||||
- **settings 能新增或覆盖路由,但不能移除组合路由**:用户层合并在组合 `base` 之上,因此删除 `cordis.yml` 提供的提供方属于组合变更;对该 namespace 执行 `replace` 只会重置用户层。
|
||||
- **分层合并对字典键没有删除语义**:settings seam 把组合 `base` 与用户层按键递归合并,因此 base 声明的某个 `reasoningEfforts` 档位、`modelOverrides` 条目或 `compat` 字段,用户层只能覆盖、无法移除——而 `reasoningEfforts` 里缺席本身*就是*语义(「不提供」),于是 base 声明过的档位会一直被提供。只有 `cordis.yml` entry config 为用户层正在编辑的同一模型声明了按模型推理字段才会触发;受支持的姿态是把这些字段留给 settings 文档(shipped 组合以 dormant 方式挂载该适配器),且 `models` 列表是数组、整体替换,这是带内的解决办法。
|
||||
- **`headers` 可能承载一条脱敏器看不见的凭据**:profile 的 `headers` 是纯字符串字典,因此设在其中的 `Authorization` 或 `api-key` 会被脱敏后的 `describe()` 原样返回,并被任何配置 UI 渲染出来。请把凭据存为 `apiKeyEnv` 引用;把该字典整体改为只写与其余[协议边界工作](../llm/README.md#known-limitations-and-deferred-work)一并暂缓。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user