跳转至

22. packages/ai 的认证、模型目录与通用运行时工具

这个模块干什么

认证层把存储 credential、环境变量、OAuth 刷新和 provider 专属环境统一解析成一次请求可用的 apiKey、headers、base URL 与环境覆盖。(packages/ai/src/auth/types.ts:3-11;packages/ai/src/auth/resolve.ts:40-78) Models 持有 provider 集合,负责认证应用、动态目录刷新、模型查询、成本计算和最终派发。(packages/ai/src/models.ts:122-187) 模型目录既支持构建期的强类型静态 catalog,也支持 provider 专属持久存储的动态 refresh。(packages/ai/src/model-catalog.ts:3-27;packages/ai/src/models-store.ts:3-45) utils/ 收集流解析、错误归一化、重试、上下文估算、工具校验、环境与取消等所有 adapter 共用的细节。(packages/ai/src/index.ts:39-47)

核心文件速查

文件 一句话职责 必读优先级
src/auth/types.ts 定义 credential store、API key/OAuth 方法、交互提示和认证结果契约。packages/ai/src/auth/types.ts:3-220 ⭐⭐⭐
src/auth/resolve.ts 实现显式覆盖、存储 credential、环境回退与锁内 OAuth 刷新。packages/ai/src/auth/resolve.ts:40-148 ⭐⭐⭐
src/models.ts 将认证解析、模型刷新、headers 合并和 provider 调用串起来。packages/ai/src/models.ts:218-526 ⭐⭐⭐
src/env-api-keys.ts 集中管理环境 key 发现,以及 Vertex/Bedrock 的 ambient 认证状态。packages/ai/src/env-api-keys.ts:68-185 ⭐⭐
src/auth/oauth/load.ts 延迟导入 OAuth flow,或切到 Bun 静态注入的 loader。packages/ai/src/auth/oauth/load.ts:3-68 ⭐⭐
src/bun-oauth.ts 在独立 Bun 二进制中注册静态嵌入的 OAuth flows。packages/ai/src/bun-oauth.ts:1-21 ⭐⭐
src/models-store.ts 定义 provider 隔离的动态模型目录持久化边界。packages/ai/src/models-store.ts:3-45 ⭐⭐
src/model-catalog.ts 将按 API 分组的生成目录压平且保留模型 API 类型。packages/ai/src/model-catalog.ts:3-27
src/session-resources.ts 注册和聚合清理 session 级资源。packages/ai/src/session-resources.ts:1-24
src/utils/ 提供所有协议 adapter 复用的低层行为。packages/ai/src/index.ts:39-47 ⭐⭐⭐

精读

1. oauth.tsbun-oauth.ts:两个入口,两个目的

oauth.ts 不是 OAuth 实现入口,只 type-export 给 coding-agent extension 兼容层使用的 OAuth 声明。(packages/ai/src/oauth.ts:1-10) 因此从这个入口拿到的是 OAuthCredentials、提示、设备码和回调类型,而不是能发起登录的 provider flow。(packages/ai/src/oauth.ts:1-10)

bun-oauth.ts 则是独立 Bun 二进制的运行时拼装点。(packages/ai/src/bun-oauth.ts:1-10) registerBunOAuthFlows() 静态 import Anthropic、OpenAI Codex、GitHub Copilot、OpenRouter、Kimi、xAI 与 Radius 的 flows,再注册 loader 表。(packages/ai/src/bun-oauth.ts:1-21) 这对应 auth/oauth/load.ts 的双路径:有 bundledLoaders 时直接返回静态 flow,否则通过变量 specifier 动态 import Node-only flow 模块。(packages/ai/src/auth/oauth/load.ts:3-29;packages/ai/src/auth/oauth/load.ts:31-68) 变量 specifier 还会按当前模块扩展名在 .ts.js 之间改写,以同时服务源码和构建产物。(packages/ai/src/auth/oauth/load.ts:4-12) lazyOAuth() 进一步把实际 import 延迟到第一次 loginrefreshtoAuth,并缓存同一个加载 Promise。(packages/ai/src/auth/helpers.ts:29-48)

2. auth/types.ts:谁保存什么,谁负责刷新什么

一次请求真正需要的 ModelAuth 只有可选 apiKey、headers、baseUrl 三项;表达不了的值应当留在 provider 配置而非塞进认证对象。(packages/ai/src/auth/types.ts:3-11) ApiKeyCredential 持有可选 key 和 provider 专属 env,后者适合 Cloudflare account/gateway ID 等非机密配置。(packages/ai/src/auth/types.ts:13-21) OAuthCredential 统一保存 access、refresh、过期时间,并允许 provider 追加其自有字段。(packages/ai/src/auth/types.ts:23-37) 一个 provider 只对应一份带 discriminator 的 Credential。(packages/ai/src/auth/types.ts:36-37)

CredentialStore 是应用拥有的边界:read() 取 credential,list() 只能列非机密元数据,modify() 是唯一写入口,delete() 表示登出。(packages/ai/src/auth/types.ts:45-88) modify() 必须是按 provider 串行的 read-modify-write,这使 OAuth 刷新可在锁内运行,避免并发请求两次刷新轮换 token。(packages/ai/src/auth/types.ts:45-58;packages/ai/src/auth/types.ts:73-84) 存储找不到条目返回 undefined,真正的存储失败才 reject;Models 会将后者包装成 ModelsError("auth")。(packages/ai/src/auth/types.ts:54-58)

AuthContext 将读取环境与检查文件抽象出来,使浏览器和测试可替换平台依赖。(packages/ai/src/auth/types.ts:90-95) 认证解析结果 AuthResult 不只给 auth,还带 provider-scoped env 和用于 UI 的 source 描述。(packages/ai/src/auth/types.ts:97-104) AuthInteraction 是 API-key 和 OAuth 登录共享的输入输出通道:prompt() 返回输入,notify() 发信息、授权 URL、设备码或进度。(packages/ai/src/auth/types.ts:113-155)

ApiKeyAuth.resolve() 从已存 key 和 ambient 来源组合 auth;返回 undefined 明确表示未配置。(packages/ai/src/auth/types.ts:157-182) ApiKeyAuth.check() 是可选的无副作用 availability 检查,适合避免在列表页执行昂贵 resolve。(packages/ai/src/auth/types.ts:168-173) OAuthAuthrefresh()toAuth() 分开:前者可能联网并写回 credential,后者只从已有效 credential 导出请求 auth。(packages/ai/src/auth/types.ts:184-210) 每个 provider 至少要有 apiKeyoauth 一项;即使是 ambient/keyless 服务,也用 apiKey.resolve() 表示是否配置好。(packages/ai/src/auth/types.ts:212-220)

3. auth/context.tscredential-store.tshelpers.ts:默认实现很小,但并发语义明确

defaultProviderAuthContext() 在 Node/Bun 中从 process.env 读非空字符串,在浏览器中自然返回 undefined。(packages/ai/src/auth/context.ts:14-28) 它的 fileExists() 动态 import node:fs/promises,会展开 ~;任何失败都返回 false,因此浏览器不会因 Node builtin 崩溃。(packages/ai/src/auth/context.ts:19-45)

InMemoryCredentialStore 用 provider ID 为键保存 credential,并为每个 ID 维护一条 Promise chain。(packages/ai/src/auth/credential-store.ts:3-24) read()list() 分别给完整 credential 与不含 secret 的 { providerId, type } 元数据。(packages/ai/src/auth/credential-store.ts:26-32) modify() 在 chain 中读取 current、运行函数、仅在返回新值时写回,并返回写后或原 credential。(packages/ai/src/auth/credential-store.ts:34-44) delete() 也进入同一 chain,所以 logout 不会与 refresh 并发交错。(packages/ai/src/auth/credential-store.ts:46-50)

常规 API-key provider 可直接用 envApiKeyAuth(name, envVars)。(packages/ai/src/auth/helpers.ts:3-9) 它的优先级是 stored key 优先,否则按给定数组顺序取第一个存在的环境变量,并自带 secret prompt 登录。(packages/ai/src/auth/helpers.ts:9-27) 有 profile、ADC、账户 ID 或特殊 headers 的 provider 应写自己的 ApiKeyAuth,而不是滥用标准 helper。(packages/ai/src/auth/helpers.ts:4-8)

4. auth/resolve.ts:认证优先级与 OAuth 刷新锁

resolveProviderAuth() 是聊天 Models 与图像 ImagesModels 共用的解析函数。(packages/ai/src/auth/resolve.ts:40-51) 显式 apiKey 覆盖只会在 provider 声明了 apiKey handler 时转成一个临时 API-key credential。(packages/ai/src/auth/resolve.ts:52-60) 若请求传入 env,函数会创建 overlay context:先读本次覆盖值,再读基础 context。(packages/ai/src/auth/resolve.ts:52-53;packages/ai/src/auth/resolve.ts:80-85)

随后读取存储 credential;只要存储中已有任何 credential,它就拥有 provider,解析不会静默改查环境变量。(packages/ai/src/auth/resolve.ts:40-45;packages/ai/src/auth/resolve.ts:62-72) 存储的是 OAuth 且 provider 有 OAuth handler 时,走 resolveStoredOAuth()。(packages/ai/src/auth/resolve.ts:62-66) 存储的是 API key 且 provider 有对应 handler 时,合并请求级 env 后走 resolveApiKey()。(packages/ai/src/auth/resolve.ts:67-70) credential 类型与 handler 不匹配时直接返回 undefined,不试图降级到其他认证来源。(packages/ai/src/auth/resolve.ts:71-77) 只有根本没有 stored credential,才允许 API-key handler 查询环境、AWS profile 或 ADC 等 ambient 来源。(packages/ai/src/auth/resolve.ts:74-77)

过期 OAuth 先作无锁检查,确实过期才进入 credentials.modify()。(packages/ai/src/auth/resolve.ts:87-105) 进入锁后再查当前 credential 是否已经被另一请求刷新;只有仍过期才真正调用 oauth.refresh()。(packages/ai/src/auth/resolve.ts:101-120) 刷新失败被包装为 ModelsError("oauth"),存储修改失败则是 ModelsError("auth")。(packages/ai/src/auth/resolve.ts:105-125) 刷新后的 credential 通过 oauth.toAuth() 变成请求 auth,且 source 固定标为 OAuth。(packages/ai/src/auth/resolve.ts:122-126) API-key handler 的异常同样被补上 provider ID 后包装为 ModelsError("auth")。(packages/ai/src/auth/resolve.ts:129-148)

5. OAuth flow 里的可复用安全部件

generatePKCE() 用 32 随机字节生成 base64url verifier,再做 SHA-256 得到 challenge;它只依赖 Web Crypto,适用于 Node 20+ 与浏览器。(packages/ai/src/auth/oauth/pkce.ts:1-34) pollOAuthDeviceCodeFlow() 实现设备码轮询,默认 5 秒、最小 1 秒,并支持 abort 与过期截止时间。(packages/ai/src/auth/oauth/device-code.ts:1-66) 服务器返回 slow_down 时,函数采用服务器 interval 或增加 5 秒,而不是继续过快轮询。(packages/ai/src/auth/oauth/device-code.ts:69-97) oauthSuccessHtml()oauthErrorHtml() 都经 escapeHtml() 处理动态文案,供回调 server 返回结果页。(packages/ai/src/auth/oauth/oauth-page.ts:3-16;packages/ai/src/auth/oauth/oauth-page.ts:94-109)

Anthropic flow 使用 loopback callback、PKCE、state 校验和授权码换 token;它明确限制在 Node/Bun CLI 环境。(packages/ai/src/auth/oauth/anthropic.ts:1-50;packages/ai/src/auth/oauth/anthropic.ts:229-302) OpenAI Codex flow 同时支持浏览器 callback 与设备码登录,最终将 token 内的 account ID 一起保存。(packages/ai/src/auth/oauth/openai-codex.ts:426-538) GitHub Copilot flow 用设备码换 GitHub access token,再换 Copilot token,并抓取该账户可选择的模型 ID。(packages/ai/src/auth/oauth/github-copilot.ts:195-287;packages/ai/src/auth/oauth/github-copilot.ts:329-379) OpenRouter 的 OAuth 最终换到永久 API key,因此 refresh() 只是返回原 credential。(packages/ai/src/auth/oauth/openrouter.ts:1-10;packages/ai/src/auth/oauth/openrouter.ts:236-245)

6. env-api-keys.ts:旧兼容层和状态探测用的环境目录

文件刻意不使用 Node 顶层 import,避免浏览器/Vite 在解析根包时跟入 node:fsnode:osnode:path。(packages/ai/src/env-api-keys.ts:1-24) 它在 Node/Bun 中异步预载这些模块,专门用来检测 Vertex ADC 文件存在性。(packages/ai/src/env-api-keys.ts:13-24;packages/ai/src/env-api-keys.ts:33-66) getApiKeyEnvVars() 对 provider 给出已知环境变量;Anthropic 有 AUTH_TOKEN、OAuth token 和 API key 三个候选,Copilot 是单独分支。(packages/ai/src/env-api-keys.ts:68-118) findEnvKeys() 只报告实际 API-key 变量,不把 AWS profile、IAM 或 ADC 这种 ambient 认证算进 key 列表。(packages/ai/src/env-api-keys.ts:120-135) getEnvApiKey() 取第一个可用候选,但刻意跳过 Anthropic AUTH_TOKEN,因为它必须走 bearer header。(packages/ai/src/env-api-keys.ts:137-149) Vertex 若有 ADC 文件、project 与 location,会返回 "<authenticated>" 状态标志而非泄露或伪造 key。(packages/ai/src/env-api-keys.ts:151-163) Bedrock 的 profile、IAM key、bearer token、ECS 与 web identity 来源也都映射为同一个状态标志。(packages/ai/src/env-api-keys.ts:165-185) 新代码的 request auth 应从 Models.getAuth() 来;这个文件的 getEnvApiKey() 主要服务 compat/状态场景。(packages/ai/src/compat.ts:222-230;packages/ai/src/models.ts:411-429)

7. models.ts:认证结果如何真正影响一次请求

ModelsImpl 默认持有 provider Map、credential store、models store 与 auth context,setProvider() 以 ID upsert。(packages/ai/src/models.ts:218-240) getAuth(providerId | model)resolveProviderAuth();传入模型时还会把静态 model.headers 合并进已解析 auth headers。(packages/ai/src/models.ts:411-429) login() 根据 api_keyoauth 选 provider 方法,再用 store 的 modify() 持久化结果。(packages/ai/src/models.ts:431-445) logout() 只委托 store 删除该 provider 的 credential。(packages/ai/src/models.ts:447-453)

applyAuth(model, options) 是流调用前唯一的装配点。(packages/ai/src/models.ts:463-486) 它强制模型的 provider 已注册,未注册时报 ModelsError("provider")。(packages/ai/src/models.ts:455-461;packages/ai/src/models.ts:463-474) 它把请求显式 apiKeyenv 作为 resolve override 传进去,因此显式值在 auth resolution 前就有最高优先级。(packages/ai/src/models.ts:467-474) 最终 API key 以 options.apiKey ?? auth.apiKey 选取,headers 先认证后显式,再可经 transformHeaders 作最后变换。(packages/ai/src/models.ts:475-481) 认证返回 baseUrl 时,函数复制模型并覆盖 endpoint;认证 env 与请求 env 会合并后一起给 adapter。(packages/ai/src/models.ts:481-486) 它从 options 中移除仅属于 ModelstransformHeaders,防止 provider API 收到不认识的参数。(packages/ai/src/models.ts:477-484)

stream()streamSimple() 都放进 lazyStream(),故认证失败从统一事件流暴露,而不是在创建流时直接 throw。(packages/ai/src/models.ts:489-501;packages/ai/src/models.ts:512-517) complete()completeSimple() 只等待各自流的结果,保证有且仅有一条调用链。(packages/ai/src/models.ts:504-525) checkAuth() 不会强制刷新 OAuth;getAvailable() 并发检查认证后返回可用 provider 的目录。(packages/ai/src/models.ts:364-409)

动态 refresh() 会跳过非动态 provider,以 provider ID 作用域创建 models store,并并发执行刷新。(packages/ai/src/models.ts:276-303) OAuth 动态目录刷新同样使用 store 锁刷新过期 credential;API-key 动态目录则用其 resolver 得到有效 credential。(packages/ai/src/models.ts:330-354) 刷新失败时它保留原错误,并尽力以 allowNetwork: false 恢复缓存目录。(packages/ai/src/models.ts:304-323) createProvider() 的通用动态实现先读缓存,再在允许网络时 fetch,最后写入 checkedAt。(packages/ai/src/models.ts:556-617)

calculateCost() 以 token 百万单价算输入、输出、读写缓存成本,并从最高命中的 input 阈值价阶取费率。(packages/ai/src/models.ts:639-658) getSupportedThinkingLevels() 根据 model.reasoningthinkingLevelMap 过滤可用档位,xhigh/max 必须显式映射才开放。(packages/ai/src/models.ts:661-672) clampThinkingLevel() 会优先保留可用请求档位,否则向更高再向更低档位寻找最近可用值。(packages/ai/src/models.ts:674-693)

8. model-catalog.tsmodels-store.ts:静态类型和动态缓存并存

ModelCatalog<TGroups, TProvider> 在类型层从「按 API 分组」推导模型 ID 对应的 API 类型,并固定 provider 字面量。(packages/ai/src/model-catalog.ts:3-20) flattenModelCatalog() 在运行时只做一次 Object.assign 压平,不承担拉取或校验责任。(packages/ai/src/model-catalog.ts:22-27) 它适合 *.models.ts wrapper 从生成目录导出精确的 provider/model/API 类型;生成的 models.generated.ts 本章只需知道是其数据来源。(packages/ai/src/model-catalog.ts:15-27;packages/ai/src/providers/all.ts:1-4)

ModelsStoreEntry 保存模型数组、远端 Last-Modified、最近检查时间和原样 ETag。(packages/ai/src/models-store.ts:3-14) ModelsStore 以 provider ID 读写删;ProviderModelsStore 去掉 ID 参数,让 provider 看不到别家的目录。(packages/ai/src/models-store.ts:16-28) InMemoryModelsStore 在读写边界用 structuredClone,避免调用方通过引用改写缓存内部对象。(packages/ai/src/models-store.ts:30-45)

9. session-resources.ts:session 生命周期没有隐藏单例逻辑

registerSessionResourceCleanup(cleanup) 将 cleanup 放进 Set,并返回一个注销函数。(packages/ai/src/session-resources.ts:1-10) cleanupSessionResources(sessionId?) 调全部 cleanup,把每个异常收集起来,最后才抛 AggregateError。(packages/ai/src/session-resources.ts:12-24) 这允许一个 session ID 关闭多类资源,也避免第一个清理失败阻止其余资源释放。(packages/ai/src/session-resources.ts:12-24)

10. utils/:adapter 共享的细节不要在每个 provider 重写

流、JSON 与 headers

EventStream 提供队列化 async iteration,AssistantMessageEventStreamdone/error 映射到最终 assistant 消息。(packages/ai/src/utils/event-stream.ts:4-83) repairJson() 修复字符串中的控制字符和非法 escape,parseJsonWithRepair() 先正常 parse、失败后再修复。(packages/ai/src/utils/json-parse.ts:27-95) parseStreamingJson() 在完整 JSON 失败时尝试 partial-json,再失败也稳定返回空对象,供工具参数 delta 使用。(packages/ai/src/utils/json-parse.ts:97-124) headersToRecord()Headers 转普通对象;providerHeadersToRecord() 则过滤值为 null 的删除标记。(packages/ai/src/utils/headers.ts:3-18)

错误、重试与溢出

normalizeProviderError() 探测 Mistral、OpenAI、Google、Bedrock 的不同 status/body 字段,并将 body 截断到上限。(packages/ai/src/utils/error-body.ts:1-54;packages/ai/src/utils/error-body.ts:56-101) formatProviderError() 只有当 SDK message 未携带 body 时才拼入 status/body,避免重复显示。(packages/ai/src/utils/error-body.ts:102-132) retryProviderRequest() 模拟 OpenAI/Anthropic SDK 的可重试状态判断,但把退避 sleep 变为可由 AbortSignal 中断。(packages/ai/src/utils/provider-retry.ts:22-35;packages/ai/src/utils/provider-retry.ts:97-125) 它优先尊重 retry-after-msretry-after,否则用带随机扰动的指数退避,并强制最大延迟。(packages/ai/src/utils/provider-retry.ts:37-67) retryAssistantCall() 面向已有 AssistantMessage 的外层重试:abort 永不重试,额度/账单类错误也快速失败。(packages/ai/src/utils/retry.ts:144-211;packages/ai/src/utils/retry.ts:213-227) isContextOverflow() 既匹配各厂商错误文案,也能依据 usage 检测 z.ai 静默溢出及 Xiaomi 的「填满上下文、零输出、length」信号。(packages/ai/src/utils/overflow.ts:80-168)

上下文、工具与文本

estimateContextTokens() 优先使用最近适用助手消息的实际 usage,只估算其后的消息;没有 usage 才全量按字符估算。(packages/ai/src/utils/estimate.ts:63-143) 估算按四字符一 token,图片按固定字符量,且将 system prompt、工具和延迟加载工具纳入上下文预算。(packages/ai/src/utils/estimate.ts:14-43;packages/ai/src/utils/estimate.ts:105-143) splitDeferredTools() 从历史的 addedToolNames 与已调用工具找出可延迟声明的工具,把当前工具切为 immediate/deferred。(packages/ai/src/utils/deferred-tools.ts:7-38) validateToolCall() 先按名称找工具,validateToolArguments() 克隆参数、进行 TypeBox 转换和 JSON-schema 兼容强制转换,再输出带路径的验证错误。(packages/ai/src/utils/validation.ts:256-310) StringEnum()type: "string"enum,专门避开 Google 等不接受 anyOf/const 的 schema 形状。(packages/ai/src/utils/typebox-helpers.ts:3-24) contentText() 只拼接 text 块,自动忽略图片、thinking 与工具调用。(packages/ai/src/utils/text.ts:1-12)

环境、取消、代理与小工具

getProviderEnvValue() 的优先级是请求级 envprocess.env、Bun sandbox 的 /proc/self/environ 回退。(packages/ai/src/utils/provider-env.ts:5-52) combineAbortSignals() 将多个输入信号合并为一个,并返回 cleanup() 以移除监听器。(packages/ai/src/utils/abort-signals.ts:1-41) resolveHttpProxyUrlForTarget() 遵守协议代理变量与 NO_PROXY,拒绝 SOCKS/PAC,只允许 HTTP(S) proxy。(packages/ai/src/utils/node-http-proxy.ts:13-87;packages/ai/src/utils/node-http-proxy.ts:89-112) sanitizeSurrogates() 只删除孤立 surrogate,不影响成对 emoji,防止 provider JSON 序列化报错。(packages/ai/src/utils/sanitize-unicode.ts:1-25) shortHash() 为长 ID 产生快速确定性短 hash,适合协议对 item ID 长度的限制。(packages/ai/src/utils/hash.ts:1-13) uuidv7() 生成时间有序 UUID,并在同一或回拨毫秒内递增 sequence 以维护顺序。(packages/ai/src/utils/uuid.ts:1-48) createAssistantMessageDiagnostic() 从异常提取结构化错误信息,appendAssistantMessageDiagnostic() 用不可变数组追加到 assistant 消息。(packages/ai/src/utils/diagnostics.ts:1-45)

数据流

下面是一次带 stored OAuth 的模型请求从 collection 到 adapter 的完整路径:

models.streamSimple(model, context, options)
  -> lazyStream(setup)
  -> Models.applyAuth()
  -> resolveProviderAuth()
  -> CredentialStore.read()/modify()(若 token 过期则 refresh)
  -> OAuthAuth.toAuth()
  -> 合并 auth/model/request headers 与 env
  -> Provider.streamSimple()
  -> API adapter 的 AssistantMessageEventStream

Models.streamSimple() 在 lazy setup 中先取 provider,再执行 applyAuth(),最后调用 provider 的 streamSimple()。(packages/ai/src/models.ts:512-517) applyAuth() 把显式 apiKey/env 传给 getAuth(),认证未配置会变为 ModelsError("auth")。(packages/ai/src/models.ts:463-486) getAuth() 委托 resolveProviderAuth(),传模型时再加模型静态 headers。(packages/ai/src/models.ts:411-429) 解析器读到 stored OAuth credential 后,只有过期才通过 CredentialStore.modify() 进入双检锁刷新。(packages/ai/src/auth/resolve.ts:62-77;packages/ai/src/auth/resolve.ts:87-120) 认证产出的 API key、headers、base URL 与环境覆盖在 applyAuth() 统一组装,然后才进入 provider。(packages/ai/src/models.ts:475-486) 外层 lazyStream() 捕获认证或加载失败,并将其编码为统一 error 事件,而不是让消费者处理两种失败通道。(packages/ai/src/api/lazy.ts:41-61)

自测

  1. 为什么已有 stored credential 时不能在 refresh 失败后悄悄退回环境变量?
  2. CredentialStore.modify() 对 OAuth token rotation 解决了哪一种并发问题?
  3. Models.applyAuth() 中 headers、base URL 和 request-level env 的合并顺序是什么?
  4. 动态模型目录的 ModelsStore 为什么要再包一层 ProviderModelsStore
  5. provider adapter 为什么应该复用 parseStreamingJson()retryProviderRequest() 和错误归一化工具?