@armadra/agent 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/CHANGELOG.zh-CN.md +32 -0
- package/dist/agent/session-settings.d.ts +4 -1
- package/dist/agent/session-settings.js +7 -2
- package/dist/ai/apis/chatgpt-backend.d.ts +7 -2
- package/dist/ai/apis/chatgpt-backend.js +12 -4
- package/dist/ai/providers/discovered-cache.d.ts +59 -0
- package/dist/ai/providers/discovered-cache.js +146 -0
- package/dist/ai/providers/model-visibility.d.ts +33 -0
- package/dist/ai/providers/model-visibility.js +70 -0
- package/dist/ai/providers/registry.d.ts +2 -0
- package/dist/ai/providers/registry.js +13 -7
- package/dist/ai/types.d.ts +5 -0
- package/dist/auth/chatgpt/backend-client.d.ts +11 -2
- package/dist/auth/chatgpt/backend-client.js +38 -6
- package/dist/auth/chatgpt/cli.d.ts +4 -1
- package/dist/auth/chatgpt/cli.js +40 -6
- package/dist/auth/chatgpt/follow.d.ts +16 -0
- package/dist/auth/chatgpt/follow.js +51 -0
- package/dist/auth/chatgpt/presets.d.ts +8 -0
- package/dist/auth/chatgpt/presets.js +12 -0
- package/dist/auth/testing/fake-oauth.d.ts +4 -1
- package/dist/auth/testing/fake-oauth.js +24 -1
- package/dist/bundle/ama.cjs +1663 -799
- package/dist/cli/compose-providers.d.ts +2 -0
- package/dist/cli/compose-providers.js +9 -0
- package/dist/cli/startup-steps.js +4 -1
- package/dist/cli/subcommands/config-set.js +19 -11
- package/dist/cli/subcommands/models-discover.d.ts +4 -1
- package/dist/cli/subcommands/models-discover.js +51 -3
- package/dist/cli/subcommands/models-enable.d.ts +13 -0
- package/dist/cli/subcommands/models-enable.js +145 -0
- package/dist/cli/subcommands/models.d.ts +2 -0
- package/dist/cli/subcommands/models.js +10 -2
- package/dist/config/json-schema.js +2 -1
- package/dist/config/key-docs.js +7 -1
- package/dist/config/schema-w5.js +9 -2
- package/dist/config/schema-w6.d.ts +1 -1
- package/dist/config/schema-w6.js +2 -1
- package/dist/config/settings-registry.d.ts +3 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types-w5.d.ts +7 -0
- package/dist/config/types-w5.js +2 -0
- package/dist/config/types-w6.d.ts +5 -0
- package/dist/i18n/catalog.d.ts +50 -3
- package/dist/i18n/messages/auth.d.ts +16 -2
- package/dist/i18n/messages/auth.js +34 -2
- package/dist/i18n/messages/cli-help.js +4 -0
- package/dist/i18n/messages/config-keys.d.ts +6 -0
- package/dist/i18n/messages/config-keys.js +8 -2
- package/dist/i18n/messages/config.d.ts +8 -0
- package/dist/i18n/messages/config.js +2 -0
- package/dist/i18n/messages/errors.d.ts +1 -1
- package/dist/i18n/messages/interactive-startup.d.ts +0 -4
- package/dist/i18n/messages/interactive-startup.js +0 -4
- package/dist/i18n/messages/interactive.d.ts +0 -4
- package/dist/i18n/messages/panels.d.ts +43 -0
- package/dist/i18n/messages/panels.js +43 -0
- package/dist/i18n/messages/settings.d.ts +7 -0
- package/dist/i18n/messages/settings.js +7 -0
- package/dist/i18n/messages/subcommands-config.d.ts +2 -0
- package/dist/i18n/messages/subcommands-config.js +2 -0
- package/dist/i18n/messages/subcommands-models.d.ts +34 -0
- package/dist/i18n/messages/subcommands-models.js +34 -0
- package/dist/i18n/messages/subcommands.d.ts +28 -0
- package/dist/i18n/messages/subcommands.js +3 -0
- package/dist/modes/interactive/commands.d.ts +3 -0
- package/dist/modes/interactive/commands.js +18 -11
- package/dist/modes/interactive/config-ui.js +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -0
- package/dist/modes/interactive/model-items.d.ts +45 -0
- package/dist/modes/interactive/model-items.js +134 -0
- package/dist/modes/interactive/model-picker.d.ts +57 -0
- package/dist/modes/interactive/model-picker.js +191 -0
- package/dist/modes/interactive/startup-ui.d.ts +3 -9
- package/dist/modes/interactive/startup-ui.js +8 -60
- package/dist/tui/components/select-list.d.ts +2 -0
- package/dist/tui/components/select-list.js +7 -0
- package/docs/en/providers.md +9 -3
- package/docs/en/tui.md +8 -1
- package/docs/providers.md +27 -6
- package/docs/tui.md +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ English · [简体中文](CHANGELOG.zh-CN.md)
|
|
|
5
5
|
> This file is in English starting with 0.6.0. Release notes for 0.1 through 0.5.1 are in Chinese in
|
|
6
6
|
> [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md). New entries go into both files.
|
|
7
7
|
|
|
8
|
+
## 0.6.2 (2026-10-03)
|
|
9
|
+
|
|
10
|
+
- **ChatGPT codex sign-in lists models, and the channel follows the sign-in method**: the codex model list
|
|
11
|
+
(`GET /models?client_version=…`) used to send ama's own version, which the backend filters against each model's minimum
|
|
12
|
+
Codex client version, so it returned no models; it now sends a Codex CLI version (default `0.160.0`, new user-level
|
|
13
|
+
`auth.chatgpt.codexClientVersion`, environment `AMA_CHATGPT_CODEX_CLIENT_VERSION`) and suggests raising it when codex still
|
|
14
|
+
returns none. The context window, input modalities and reasoning efforts from the codex response go into the discovery cache
|
|
15
|
+
and are used when models.dev has no data. Requests to `chatgpt` now pick the channel from the current sign-in at request time:
|
|
16
|
+
a model without an explicit `@channel` follows it, so a running session keeps working after switching between siwc and codex,
|
|
17
|
+
and a resumed session ignores the channel it recorded; only an explicit `@channel` that differs reports
|
|
18
|
+
`chatgpt_flavor_mismatch`, now with a clearer message. Login always rewrites the discovery cache (an empty list included)
|
|
19
|
+
after deleting the old one, and a cache whose flavor differs from the current sign-in counts as stale. `not_eligible` now
|
|
20
|
+
lists the likely causes (plan, workspace account, region or preview rollout — the most likely one for Pro accounts) and
|
|
21
|
+
suggests `--flavor codex`; a siwc login notes that eligibility is only confirmed on the first request. Also fixed: an
|
|
22
|
+
unlisted slug with an explicit `@channel` (`chatgpt/<slug>@siwc`) kept the default channel's endpoint. Docs:
|
|
23
|
+
docs/en/providers.md "ChatGPT login".
|
|
24
|
+
|
|
25
|
+
## 0.6.1 (2026-10-03)
|
|
26
|
+
|
|
27
|
+
- **The model picker shows only configured models; ChatGPT models can be picked**: `/model` (and the startup picker and
|
|
28
|
+
`/config` model items) lists only providers with a key, a valid OAuth sign-in or a local server, one row per model on
|
|
29
|
+
multi-channel providers (other channels in the description; type `@` in the filter for `model@channel` rows); the current
|
|
30
|
+
model is pinned when it is not listed. `Tab` switches to "all" (unconfigured providers marked "no key configured", with an
|
|
31
|
+
`ama auth set` hint), `Space` adds / removes the highlighted model to / from the new user-level `models.enabled` list
|
|
32
|
+
(`provider/model[@channel]`, `provider/*`), which, once set, is all the picker shows; `ama models enable | disable` and
|
|
33
|
+
`ama models list --enabled` edit and show it. `ama auth login chatgpt` now fetches the models available to the account
|
|
34
|
+
(read-only, no usage) into `<dataDir>/models/discovered/chatgpt.json`, `ama models discover chatgpt` rewrites it and
|
|
35
|
+
logout deletes it; the registry merges the cache into providers with an empty model table, so ChatGPT models appear in
|
|
36
|
+
`/model` and `ama models list`. Note: with only a ChatGPT sign-in and no `defaultModel`, the first cached ChatGPT model can
|
|
37
|
+
now be chosen as the default model. Docs: docs/en/tui.md, docs/en/providers.md "ChatGPT login".
|
|
38
|
+
|
|
39
|
+
- **`ama auth login chatgpt` explains a refused sign-in**: when the OAuth callback carries `error=access_denied` (or another
|
|
40
|
+
error) the message now lists the likely causes (the authorization page was cancelled or plan usage was not checked; the
|
|
41
|
+
account / plan is not eligible — sharing is Plus / Pro only, Team / Enterprise workspaces may not offer it, the browser may be
|
|
42
|
+
signed in to another account; the region is not supported) and, for siwc, suggests the fallback
|
|
43
|
+
`ama auth login chatgpt --flavor codex`. Nothing is retried or switched automatically.
|
|
44
|
+
|
|
45
|
+
- **`ama config set` reports the written value when a higher layer overrides it**: with e.g. `AMA_LANG=zh`,
|
|
46
|
+
`ama config set ui.language en` used to print the effective value `zh` labelled "(user)". The first line is now the value just
|
|
47
|
+
written and its layer (`ui.language = en (written to user)`), and the second line names the override and the effective value
|
|
48
|
+
(`Still overridden by AMA_LANG (zh); the effective value is zh`). `ama config get` still shows the effective value and its
|
|
49
|
+
real source.
|
|
50
|
+
|
|
8
51
|
## 0.6.0 (2026-10-03)
|
|
9
52
|
|
|
10
53
|
Wave 6: the agent bar and sub-agent view, traces, memory, ChatGPT login, the `/config` settings panel, and a bilingual
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
> 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
|
|
6
6
|
|
|
7
|
+
## 0.6.2(2026-10-03)
|
|
8
|
+
|
|
9
|
+
- **ChatGPT codex 方式能列出模型,渠道跟随登录方式**:codex 模型列表(`GET /models?client_version=…`)原来发 ama 自己的版本号,
|
|
10
|
+
后端按每个模型的最低 Codex 客户端版本过滤,于是一个模型都不返回;现在发 Codex CLI 版本号(缺省 `0.160.0`,新增用户级
|
|
11
|
+
`auth.chatgpt.codexClientVersion` 与环境变量 `AMA_CHATGPT_CODEX_CLIENT_VERSION`),仍为 0 个时提示调高。codex 响应里的上下文
|
|
12
|
+
窗口、输入模态、推理强度写进发现缓存,models.dev 补不到时用它。`chatgpt` 的请求在请求时按当前登录方式选渠道:没写 `@渠道` 的
|
|
13
|
+
模型跟随登录,运行中的会话在 siwc 与 codex 之间换登录后照常继续,恢复会话时也不沿用记录的渠道;只有显式写了不符的 `@渠道`
|
|
14
|
+
才报 `chatgpt_flavor_mismatch`,文案更清楚。登录先删旧发现缓存再重写(0 个也写空表),缓存 flavor 与当前登录不符视为过期。
|
|
15
|
+
`not_eligible` 改为列出可能原因(套餐、工作空间账户、地区受限或预览期未开放——Pro 账户最可能是这一条)并提示
|
|
16
|
+
`--flavor codex`;siwc 登录成功后说明能否共享额度要到首次请求才能确认。另修复:表外 slug 显式写 `@渠道`
|
|
17
|
+
(`chatgpt/<slug>@siwc`)时仍用缺省渠道的地址。文档:docs/providers.md「ChatGPT 登录」。
|
|
18
|
+
|
|
19
|
+
## 0.6.1(2026-10-03)
|
|
20
|
+
|
|
21
|
+
- **模型选择器只显示已配置的模型,ChatGPT 模型可选**:`/model`(以及启动选择器、`/config` 的模型项)只列有 key、OAuth 已登录或
|
|
22
|
+
本地服务的供应商;多渠道供应商每个模型一行(其它渠道写在说明里,筛选文本输入 `@` 列出 `模型@渠道` 行);当前模型不在列表里时
|
|
23
|
+
置顶。`Tab` 切到「全部」(没配置的供应商标「未配置 key」,选中时提示 `ama auth set`),`Space` 把高亮的模型加入 / 移出新的
|
|
24
|
+
用户级清单 `models.enabled`(`provider/model[@channel]`、`provider/*`),设置后选择器只列清单内的模型;`ama models enable |
|
|
25
|
+
disable` 与 `ama models list --enabled` 在命令行编辑和查看。`ama auth login chatgpt` 成功后拉取账户可用的模型(只读、不消耗
|
|
26
|
+
额度)缓存到 `<dataDir>/models/discovered/chatgpt.json`,`ama models discover chatgpt` 重写、logout 删除;注册表把缓存并入模型
|
|
27
|
+
表为空的供应商,ChatGPT 模型因此出现在 `/model` 与 `ama models list` 里。注意:只登录了 ChatGPT、没设 `defaultModel` 时,
|
|
28
|
+
缺省模型现在可能选中缓存里的第一个 ChatGPT 模型。文档:docs/tui.md、docs/providers.md「ChatGPT 登录」。
|
|
29
|
+
|
|
30
|
+
- **`ama auth login chatgpt` 授权被拒时说明原因**:OAuth 回调带 `error=access_denied`(或其它 error)时,提示改为列出可能原因
|
|
31
|
+
(在授权页取消或没勾选使用 ChatGPT 套餐额度;账户 / 套餐不符合——额度共享只对 Plus / Pro 开放,Team / Enterprise 工作空间可能
|
|
32
|
+
未开放,浏览器可能登录了别的账户;所在地区受限),siwc 下另提示可改用备用 `ama auth login chatgpt --flavor codex`。不自动重试、
|
|
33
|
+
不自动换 flavor。
|
|
34
|
+
|
|
35
|
+
- **`ama config set` 被更高优先级来源覆盖时,回执显示刚写入的值**:如 `AMA_LANG=zh` 下 `ama config set ui.language en`,原来
|
|
36
|
+
第一行显示覆盖后的生效值 `zh` 却标「(用户级)」。现在第一行是刚写入的值与写入层(`ui.language = en(已写入用户级)`),第二行
|
|
37
|
+
提示覆盖来源与生效值(`当前仍被 AMA_LANG(zh)覆盖,生效值为 zh`)。`ama config get` 仍显示生效值与真实来源。
|
|
38
|
+
|
|
7
39
|
## 0.6.0(2026-10-03)
|
|
8
40
|
|
|
9
41
|
第六波:Agent 栏与子 Agent 视图、轨迹、记忆(Memory)、ChatGPT 登录、`/config` 设置面板、中英双语界面。设计依据与决定表见
|
|
@@ -11,7 +11,10 @@ import type { ToolDefinition } from "../tools/types.js";
|
|
|
11
11
|
import type { StreamFn } from "./loop.js";
|
|
12
12
|
import type { AgentSessionOptions, SessionCore } from "./session-core.js";
|
|
13
13
|
import type { SystemPromptInput } from "./system-prompt.js";
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* 按模型的协议分派到已注册的实现(会话流的最内层)。ChatGPT 订阅模型先按本次 token 的登录方式改渠道
|
|
16
|
+
* (没写 `@渠道` 时;auth/chatgpt/follow.ts),会话中途换登录方式不用重启。
|
|
17
|
+
*/
|
|
15
18
|
export declare function providerStream(providers: ProviderRegistryApi): StreamFn;
|
|
16
19
|
export type StaticSystemInput = Omit<SystemPromptInput, "tools" | "cwd">;
|
|
17
20
|
export interface SessionSettingsDeps {
|
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
* 纯搬迁:`setModel / setThinkingLevel / setPermissionMode / setActiveTools / addTool /
|
|
6
6
|
* updateSystem / announceStart / getTools` 的行为与事件顺序不变;`AgentSessionImpl` 以同名成员转发。
|
|
7
7
|
*/
|
|
8
|
+
import { followChatGptLogin } from "../auth/chatgpt/follow.js";
|
|
8
9
|
import { AmaError } from "../errors.js";
|
|
9
10
|
import { appendModelChange, findModelOrThrow, sessionStartEvent } from "./session-sync.js";
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* 按模型的协议分派到已注册的实现(会话流的最内层)。ChatGPT 订阅模型先按本次 token 的登录方式改渠道
|
|
13
|
+
* (没写 `@渠道` 时;auth/chatgpt/follow.ts),会话中途换登录方式不用重启。
|
|
14
|
+
*/
|
|
11
15
|
export function providerStream(providers) {
|
|
12
|
-
return (
|
|
16
|
+
return (requested, context, streamOptions) => {
|
|
17
|
+
const model = followChatGptLogin(requested, providers.get(requested.provider), streamOptions.apiKey);
|
|
13
18
|
const api = providers.getApi(model.api);
|
|
14
19
|
if (api === undefined) {
|
|
15
20
|
throw new AmaError("provider_not_found", `no implementation registered for api ${model.api}`);
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* - 出错恢复(每种至多一次):401 → 强制刷新重试;SIWC 400 `subscription_sharing_unsupported_capability` → 删去
|
|
11
11
|
* `error.param` 重试;codex 400 `Instructions are not valid` → 本会话切 developer-message 重试;
|
|
12
12
|
* - 错误映射(文案带码前缀,宿主按码判断):429 配额 → `quota_exceeded`(不重试);失效 → `auth_expired`;
|
|
13
|
-
* SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible
|
|
13
|
+
* SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`(i18n 文案列出原因);503 留给会话层现有重试;
|
|
14
|
+
* - 渠道跟随登录方式由会话层做(auth/chatgpt/follow.ts);到这里仍不符说明用户显式写了 `@渠道`,报
|
|
15
|
+
* `chatgpt_flavor_mismatch`(i18n);
|
|
14
16
|
* - 用量:`cost = 0`、`billing: "subscription"`。
|
|
15
17
|
*/
|
|
16
18
|
import { type LiveToken } from "../../auth/oauth/live.js";
|
|
@@ -25,7 +27,10 @@ export declare function sessionKeyOf(model: Model, options: StreamOptions): stri
|
|
|
25
27
|
export declare function resetChatGptState(): void;
|
|
26
28
|
export declare function instructionsModeOf(model: Model, compat: OpenAIResponsesCompat, options: StreamOptions): "native" | "developer-message";
|
|
27
29
|
export declare function transformChatGptBody(body: Json, model: Model, compat: OpenAIResponsesCompat, options: StreamOptions): Json;
|
|
28
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* 发请求前:needsLogin 直接报 auth_expired;登录 flavor 与渠道不符报错(没写 `@渠道` 的模型会话层已改到登录的
|
|
32
|
+
* 渠道,走到这里的是显式指定了另一渠道)。
|
|
33
|
+
*/
|
|
29
34
|
export declare function chatgptPrecheck(model: Model, compat: OpenAIResponsesCompat, apiKey?: string): void;
|
|
30
35
|
export declare function chatgptHeaders(compat: OpenAIResponsesCompat, options: StreamOptions, apiKey: string | undefined): Record<string, string>;
|
|
31
36
|
/** 包一层 onResponse:解析响应头里的配额。 */
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
* - 出错恢复(每种至多一次):401 → 强制刷新重试;SIWC 400 `subscription_sharing_unsupported_capability` → 删去
|
|
11
11
|
* `error.param` 重试;codex 400 `Instructions are not valid` → 本会话切 developer-message 重试;
|
|
12
12
|
* - 错误映射(文案带码前缀,宿主按码判断):429 配额 → `quota_exceeded`(不重试);失效 → `auth_expired`;
|
|
13
|
-
* SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible
|
|
13
|
+
* SIWC 403 `subscription_sharing_user_not_eligible` → `not_eligible`(i18n 文案列出原因);503 留给会话层现有重试;
|
|
14
|
+
* - 渠道跟随登录方式由会话层做(auth/chatgpt/follow.ts);到这里仍不符说明用户显式写了 `@渠道`,报
|
|
15
|
+
* `chatgpt_flavor_mismatch`(i18n);
|
|
14
16
|
* - 用量:`cost = 0`、`billing: "subscription"`。
|
|
15
17
|
*/
|
|
16
18
|
import { liveToken } from "../../auth/oauth/live.js";
|
|
17
19
|
import { authExpiredError } from "../../auth/oauth/refresh.js";
|
|
20
|
+
import { msg } from "../../i18n/index.js";
|
|
18
21
|
import { parseQuotaHeaders, parseRateLimitEvent, quotaFromLimitError, } from "./chatgpt-rate-limits.js";
|
|
19
22
|
export const SIWC_FORBIDDEN_FIELDS = [
|
|
20
23
|
"background",
|
|
@@ -81,7 +84,10 @@ export function transformChatGptBody(body, model, compat, options) {
|
|
|
81
84
|
out["input"] = input;
|
|
82
85
|
return out;
|
|
83
86
|
}
|
|
84
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* 发请求前:needsLogin 直接报 auth_expired;登录 flavor 与渠道不符报错(没写 `@渠道` 的模型会话层已改到登录的
|
|
89
|
+
* 渠道,走到这里的是显式指定了另一渠道)。
|
|
90
|
+
*/
|
|
85
91
|
export function chatgptPrecheck(model, compat, apiKey) {
|
|
86
92
|
const live = liveToken(apiKey);
|
|
87
93
|
if (live === undefined)
|
|
@@ -89,7 +95,9 @@ export function chatgptPrecheck(model, compat, apiKey) {
|
|
|
89
95
|
if (live.needsLogin === true)
|
|
90
96
|
throw authExpiredError(live.provider);
|
|
91
97
|
if (live.flavor !== compat.chatgptBackend) {
|
|
92
|
-
|
|
98
|
+
const pinned = String(compat.chatgptBackend);
|
|
99
|
+
const ref = `${model.provider}/${model.id}@${pinned}`;
|
|
100
|
+
throw new Error(`chatgpt_flavor_mismatch: ${msg().auth.request.flavorMismatch(live.flavor, ref, pinned)}`);
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
export function chatgptHeaders(compat, options, apiKey) {
|
|
@@ -196,7 +204,7 @@ export function recoverChatGptError(error, model, compat, options, apiKey, tried
|
|
|
196
204
|
code === "subscription_sharing_user_not_eligible")
|
|
197
205
|
return {
|
|
198
206
|
kind: "fail",
|
|
199
|
-
error: new Error(
|
|
207
|
+
error: new Error(`not_eligible: ${msg().auth.request.notEligible}`),
|
|
200
208
|
};
|
|
201
209
|
if (backend === "siwc" &&
|
|
202
210
|
error.status === 400 &&
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发现得到的模型列表缓存:`<dataDir>/models/discovered/<provider>.json`。
|
|
3
|
+
*
|
|
4
|
+
* 模型表为空、接受任意 slug 的供应商(`chatgpt` 的订阅后端)在选择器里原本一个模型都看不到。
|
|
5
|
+
* `ama auth login chatgpt` 成功后与 `ama models discover chatgpt` 把账户可用的 slug 写进这里;组装注册表时
|
|
6
|
+
* (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商,元数据用 models.dev(快照 ⊕ 用户刷新)补全。
|
|
7
|
+
* `ama auth logout chatgpt` 删掉它。只存 slug、显示名、后端给的元数据(codex:上下文窗口、输入模态、推理强度,
|
|
8
|
+
* models.dev 补不到时用)、flavor 与时间戳——没有 token、账户 id。
|
|
9
|
+
*
|
|
10
|
+
* ChatGPT 的缓存带登录 flavor;组装时与当前登录的 flavor 不符视为过期、不并入(选择器提示重新发现)。
|
|
11
|
+
*/
|
|
12
|
+
import type { Model, ModelThinkingLevel, ProviderData } from "../types.js";
|
|
13
|
+
import type { ModelsDevIndex } from "./models-dev.js";
|
|
14
|
+
export declare const DISCOVERED_CACHE_VERSION = 1;
|
|
15
|
+
export interface DiscoveredCacheFile {
|
|
16
|
+
version: number;
|
|
17
|
+
provider: string;
|
|
18
|
+
/** ISO 时间。 */
|
|
19
|
+
fetchedAt: string;
|
|
20
|
+
/** ChatGPT 登录的 flavor(siwc / codex);其它供应商不写。 */
|
|
21
|
+
flavor?: string;
|
|
22
|
+
models: DiscoveredCacheModel[];
|
|
23
|
+
}
|
|
24
|
+
/** 缓存里的一个模型:slug、显示名与后端给的元数据(都可缺)。 */
|
|
25
|
+
export interface DiscoveredCacheModel {
|
|
26
|
+
id: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
contextWindow?: number;
|
|
29
|
+
input?: ("text" | "image")[];
|
|
30
|
+
reasoningLevels?: string[];
|
|
31
|
+
}
|
|
32
|
+
/** 缓存文件位置。 */
|
|
33
|
+
export declare function discoveredCachePath(dataDir: string, provider: string): string;
|
|
34
|
+
/** 读缓存;不存在、损坏或形状不对返回 undefined(不报错,选择器给提示行)。 */
|
|
35
|
+
export declare function readDiscoveredCache(dataDir: string, provider: string): DiscoveredCacheFile | undefined;
|
|
36
|
+
/** 写缓存(先写临时文件再改名);返回路径。 */
|
|
37
|
+
export declare function writeDiscoveredCache(dataDir: string, provider: string, input: {
|
|
38
|
+
models: readonly (Omit<DiscoveredCacheModel, "name"> & {
|
|
39
|
+
name?: string | undefined;
|
|
40
|
+
})[];
|
|
41
|
+
flavor?: string | undefined;
|
|
42
|
+
}, now?: number): string;
|
|
43
|
+
/** 删缓存;删了返回 true。 */
|
|
44
|
+
export declare function clearDiscoveredCache(dataDir: string, provider: string): boolean;
|
|
45
|
+
/** 后端给的推理强度 → 思考级别映射(`none` 对应 off;没列出的级别不支持)。 */
|
|
46
|
+
export declare function thinkingMapOf(levels: readonly string[]): Partial<Record<ModelThinkingLevel, string | null>>;
|
|
47
|
+
/** 缓存里的模型 → 注册表模型(models.dev 补元数据,补不到用后端给的,再补不到用自定义缺省)。 */
|
|
48
|
+
export declare function discoveredModels(file: DiscoveredCacheFile, provider: ProviderData, modelsDev: () => ModelsDevIndex | undefined): Model[];
|
|
49
|
+
/** 注册表里能追加模型的最小接口(`ProviderRegistry.addModels`)。 */
|
|
50
|
+
export interface DiscoveredTarget {
|
|
51
|
+
list(): readonly ProviderData[];
|
|
52
|
+
addModels(providerId: string, models: readonly Model[], source?: "discovered"): void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 把缓存并入模型表为空的供应商(本地服务除外:它们每次启动现场探测)。返回并入了的供应商 id。
|
|
56
|
+
*/
|
|
57
|
+
export declare function mergeDiscoveredModels(registry: DiscoveredTarget, dataDir: string, modelsDev: () => ModelsDevIndex | undefined,
|
|
58
|
+
/** 当前登录的 flavor(只有 OAuth 供应商有):缓存的 flavor 与之不符视为过期,不并入。 */
|
|
59
|
+
loginFlavor?: (providerId: string) => string | undefined): string[];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发现得到的模型列表缓存:`<dataDir>/models/discovered/<provider>.json`。
|
|
3
|
+
*
|
|
4
|
+
* 模型表为空、接受任意 slug 的供应商(`chatgpt` 的订阅后端)在选择器里原本一个模型都看不到。
|
|
5
|
+
* `ama auth login chatgpt` 成功后与 `ama models discover chatgpt` 把账户可用的 slug 写进这里;组装注册表时
|
|
6
|
+
* (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商,元数据用 models.dev(快照 ⊕ 用户刷新)补全。
|
|
7
|
+
* `ama auth logout chatgpt` 删掉它。只存 slug、显示名、后端给的元数据(codex:上下文窗口、输入模态、推理强度,
|
|
8
|
+
* models.dev 补不到时用)、flavor 与时间戳——没有 token、账户 id。
|
|
9
|
+
*
|
|
10
|
+
* ChatGPT 的缓存带登录 flavor;组装时与当前登录的 flavor 不符视为过期、不并入(选择器提示重新发现)。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { withCustomDefaults } from "./catalog.js";
|
|
15
|
+
import { enrichEntry } from "./enrich.js";
|
|
16
|
+
export const DISCOVERED_CACHE_VERSION = 1;
|
|
17
|
+
/** 缓存文件位置。 */
|
|
18
|
+
export function discoveredCachePath(dataDir, provider) {
|
|
19
|
+
return join(dataDir, "models", "discovered", `${provider}.json`);
|
|
20
|
+
}
|
|
21
|
+
/** 未知值 → 缓存模型;id 不对返回 undefined,元数据字段形状不对的丢掉。 */
|
|
22
|
+
function toEntry(value) {
|
|
23
|
+
if (typeof value !== "object" || value === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
const raw = value;
|
|
26
|
+
const { id, name, contextWindow, input, reasoningLevels } = raw;
|
|
27
|
+
if (typeof id !== "string" || id === "")
|
|
28
|
+
return undefined;
|
|
29
|
+
const out = { id };
|
|
30
|
+
if (typeof name === "string" && name !== "")
|
|
31
|
+
out.name = name;
|
|
32
|
+
if (typeof contextWindow === "number" && Number.isInteger(contextWindow) && contextWindow > 0)
|
|
33
|
+
out.contextWindow = contextWindow;
|
|
34
|
+
if (Array.isArray(input) && input.includes("text"))
|
|
35
|
+
out.input = ["text", "image"].filter((x) => input.includes(x));
|
|
36
|
+
if (Array.isArray(reasoningLevels) && reasoningLevels.every((x) => typeof x === "string"))
|
|
37
|
+
out.reasoningLevels = reasoningLevels;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** 读缓存;不存在、损坏或形状不对返回 undefined(不报错,选择器给提示行)。 */
|
|
41
|
+
export function readDiscoveredCache(dataDir, provider) {
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(readFileSync(discoveredCachePath(dataDir, provider), "utf8"));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
50
|
+
return undefined;
|
|
51
|
+
const file = parsed;
|
|
52
|
+
if (file.provider !== provider || !Array.isArray(file.models))
|
|
53
|
+
return undefined;
|
|
54
|
+
return {
|
|
55
|
+
version: typeof file.version === "number" ? file.version : DISCOVERED_CACHE_VERSION,
|
|
56
|
+
provider,
|
|
57
|
+
fetchedAt: typeof file.fetchedAt === "string" ? file.fetchedAt : "",
|
|
58
|
+
...(typeof file.flavor === "string" ? { flavor: file.flavor } : {}),
|
|
59
|
+
models: file.models.map(toEntry).filter((m) => m !== undefined),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** 写缓存(先写临时文件再改名);返回路径。 */
|
|
63
|
+
export function writeDiscoveredCache(dataDir, provider, input, now = Date.now()) {
|
|
64
|
+
const path = discoveredCachePath(dataDir, provider);
|
|
65
|
+
const file = {
|
|
66
|
+
version: DISCOVERED_CACHE_VERSION,
|
|
67
|
+
provider,
|
|
68
|
+
fetchedAt: new Date(now).toISOString(),
|
|
69
|
+
...(input.flavor !== undefined ? { flavor: input.flavor } : {}),
|
|
70
|
+
models: input.models.map(toEntry).filter((m) => m !== undefined),
|
|
71
|
+
};
|
|
72
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
73
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
74
|
+
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`);
|
|
75
|
+
renameSync(tmp, path);
|
|
76
|
+
return path;
|
|
77
|
+
}
|
|
78
|
+
/** 删缓存;删了返回 true。 */
|
|
79
|
+
export function clearDiscoveredCache(dataDir, provider) {
|
|
80
|
+
const path = discoveredCachePath(dataDir, provider);
|
|
81
|
+
if (!existsSync(path))
|
|
82
|
+
return false;
|
|
83
|
+
rmSync(path, { force: true });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
87
|
+
/** 后端给的推理强度 → 思考级别映射(`none` 对应 off;没列出的级别不支持)。 */
|
|
88
|
+
export function thinkingMapOf(levels) {
|
|
89
|
+
const map = {
|
|
90
|
+
off: levels.includes("none") ? "none" : null,
|
|
91
|
+
};
|
|
92
|
+
for (const level of EFFORT_LEVELS)
|
|
93
|
+
map[level] = levels.includes(level) ? level : null;
|
|
94
|
+
return map;
|
|
95
|
+
}
|
|
96
|
+
/** models.dev 没补上的字段用后端给的元数据。 */
|
|
97
|
+
function withBackendMetadata(model, cached, matched) {
|
|
98
|
+
if (model.contextWindow === undefined && cached.contextWindow !== undefined)
|
|
99
|
+
model.contextWindow = cached.contextWindow;
|
|
100
|
+
if (matched)
|
|
101
|
+
return;
|
|
102
|
+
if (cached.input !== undefined)
|
|
103
|
+
model.input = [...cached.input];
|
|
104
|
+
const efforts = (cached.reasoningLevels ?? []).filter((x) => x !== "none");
|
|
105
|
+
if (efforts.length > 0) {
|
|
106
|
+
model.reasoning = true;
|
|
107
|
+
model.thinkingLevelMap = thinkingMapOf(cached.reasoningLevels ?? []);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** 缓存里的模型 → 注册表模型(models.dev 补元数据,补不到用后端给的,再补不到用自定义缺省)。 */
|
|
111
|
+
export function discoveredModels(file, provider, modelsDev) {
|
|
112
|
+
return file.models.map((m) => {
|
|
113
|
+
const { entry, metadata } = enrichEntry(m.name ? { id: m.id, name: m.name } : { id: m.id }, modelsDev);
|
|
114
|
+
const model = withCustomDefaults(entry, provider.id, provider.api);
|
|
115
|
+
withBackendMetadata(model, m, metadata.match !== undefined);
|
|
116
|
+
// 只挂发现时登录的 flavor 对应的渠道(订阅后端的另一条渠道用不了,不在说明与 `@` 行里出现);
|
|
117
|
+
// 缓存没写 flavor 或渠道不存在时用供应商的缺省渠道
|
|
118
|
+
const names = (provider.channels ?? []).map((c) => c.name);
|
|
119
|
+
if (names.length > 0) {
|
|
120
|
+
const own = file.flavor !== undefined && names.includes(file.flavor) ? file.flavor : undefined;
|
|
121
|
+
model.channels = [own ?? provider.defaultChannel ?? names[0]];
|
|
122
|
+
}
|
|
123
|
+
return model;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 把缓存并入模型表为空的供应商(本地服务除外:它们每次启动现场探测)。返回并入了的供应商 id。
|
|
128
|
+
*/
|
|
129
|
+
export function mergeDiscoveredModels(registry, dataDir, modelsDev,
|
|
130
|
+
/** 当前登录的 flavor(只有 OAuth 供应商有):缓存的 flavor 与之不符视为过期,不并入。 */
|
|
131
|
+
loginFlavor = () => undefined) {
|
|
132
|
+
const merged = [];
|
|
133
|
+
for (const provider of registry.list()) {
|
|
134
|
+
if (provider.models.length > 0 || !provider.requiresApiKey)
|
|
135
|
+
continue;
|
|
136
|
+
const file = readDiscoveredCache(dataDir, provider.id);
|
|
137
|
+
if (file === undefined || file.models.length === 0)
|
|
138
|
+
continue;
|
|
139
|
+
const flavor = loginFlavor(provider.id);
|
|
140
|
+
if (flavor !== undefined && file.flavor !== undefined && file.flavor !== flavor)
|
|
141
|
+
continue;
|
|
142
|
+
registry.addModels(provider.id, discoveredModels(file, provider, modelsDev), "discovered");
|
|
143
|
+
merged.push(provider.id);
|
|
144
|
+
}
|
|
145
|
+
return merged;
|
|
146
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型选择器的可见性规则(`/model`、启动选择器、`/config` 的模型项、`ama models list --enabled` 共用)。
|
|
3
|
+
*
|
|
4
|
+
* - 供应商的可用状态:本地(`requiresApiKey: false`)、有 key、OAuth 已登录、OAuth 需重新登录、没配置;
|
|
5
|
+
* 前三种算「已配置」,缺省只显示它们的模型。
|
|
6
|
+
* - `models.enabled`(用户级):`provider/model[@channel]` 或 `provider/*`;设置后只显示清单内的模型。
|
|
7
|
+
* 清单为空等同不设(写回时删掉这个键)。
|
|
8
|
+
*/
|
|
9
|
+
import type { ProviderData, ProviderRegistryApi } from "../types.js";
|
|
10
|
+
export type ProviderAccess = "local" | "key" | "oauth" | "needsLogin" | "none";
|
|
11
|
+
/** 解析 key 判断供应商的可用状态(OAuth 条目会按需刷新,与发请求前相同)。 */
|
|
12
|
+
export declare function providerAccess(registry: ProviderRegistryApi, provider: ProviderData): Promise<ProviderAccess>;
|
|
13
|
+
export declare function accessReady(access: ProviderAccess): boolean;
|
|
14
|
+
/** `provider/model@channel` → 三段(模型 id 里可以有 `/`)。 */
|
|
15
|
+
export declare function splitEnabledRef(ref: string): {
|
|
16
|
+
provider: string;
|
|
17
|
+
model: string;
|
|
18
|
+
channel?: string;
|
|
19
|
+
} | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* 清单是否包含该行:`exact` 是逐字列出(`provider/model`,渠道行要 `provider/model@channel`),
|
|
22
|
+
* `wildcard` 只经 `provider/*` 匹配上。
|
|
23
|
+
*/
|
|
24
|
+
export declare function enabledMatch(list: readonly string[], ref: string): {
|
|
25
|
+
kind: "exact" | "wildcard";
|
|
26
|
+
pattern: string;
|
|
27
|
+
} | undefined;
|
|
28
|
+
/** 追加(去重、保持顺序)。 */
|
|
29
|
+
export declare function addEnabled(list: readonly string[] | undefined, refs: readonly string[]): string[];
|
|
30
|
+
/** 移出;移空返回 undefined(删掉键,恢复「显示全部已配置」)。 */
|
|
31
|
+
export declare function removeEnabled(list: readonly string[] | undefined, refs: readonly string[]): string[] | undefined;
|
|
32
|
+
/** 配置里的清单:非数组或空数组视为未设置。 */
|
|
33
|
+
export declare function enabledList(value: unknown): readonly string[] | undefined;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型选择器的可见性规则(`/model`、启动选择器、`/config` 的模型项、`ama models list --enabled` 共用)。
|
|
3
|
+
*
|
|
4
|
+
* - 供应商的可用状态:本地(`requiresApiKey: false`)、有 key、OAuth 已登录、OAuth 需重新登录、没配置;
|
|
5
|
+
* 前三种算「已配置」,缺省只显示它们的模型。
|
|
6
|
+
* - `models.enabled`(用户级):`provider/model[@channel]` 或 `provider/*`;设置后只显示清单内的模型。
|
|
7
|
+
* 清单为空等同不设(写回时删掉这个键)。
|
|
8
|
+
*/
|
|
9
|
+
import { liveToken } from "../../auth/oauth/live.js";
|
|
10
|
+
/** 解析 key 判断供应商的可用状态(OAuth 条目会按需刷新,与发请求前相同)。 */
|
|
11
|
+
export async function providerAccess(registry, provider) {
|
|
12
|
+
if (!provider.requiresApiKey)
|
|
13
|
+
return "local";
|
|
14
|
+
const key = await registry
|
|
15
|
+
.resolveApiKey(provider.id)
|
|
16
|
+
.catch(() => ({ apiKey: undefined, source: "none" }));
|
|
17
|
+
if (key.apiKey === undefined)
|
|
18
|
+
return "none";
|
|
19
|
+
if (key.source !== "oauth")
|
|
20
|
+
return "key";
|
|
21
|
+
return liveToken(key.apiKey)?.needsLogin === true ? "needsLogin" : "oauth";
|
|
22
|
+
}
|
|
23
|
+
export function accessReady(access) {
|
|
24
|
+
return access === "local" || access === "key" || access === "oauth";
|
|
25
|
+
}
|
|
26
|
+
/** `provider/model@channel` → 三段(模型 id 里可以有 `/`)。 */
|
|
27
|
+
export function splitEnabledRef(ref) {
|
|
28
|
+
const slash = ref.indexOf("/");
|
|
29
|
+
if (slash <= 0 || slash === ref.length - 1)
|
|
30
|
+
return undefined;
|
|
31
|
+
const provider = ref.slice(0, slash);
|
|
32
|
+
const rest = ref.slice(slash + 1);
|
|
33
|
+
const at = rest.lastIndexOf("@");
|
|
34
|
+
if (at > 0)
|
|
35
|
+
return { provider, model: rest.slice(0, at), channel: rest.slice(at + 1) };
|
|
36
|
+
return { provider, model: rest };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 清单是否包含该行:`exact` 是逐字列出(`provider/model`,渠道行要 `provider/model@channel`),
|
|
40
|
+
* `wildcard` 只经 `provider/*` 匹配上。
|
|
41
|
+
*/
|
|
42
|
+
export function enabledMatch(list, ref) {
|
|
43
|
+
if (list.includes(ref))
|
|
44
|
+
return { kind: "exact", pattern: ref };
|
|
45
|
+
const parts = splitEnabledRef(ref);
|
|
46
|
+
if (parts === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const wildcard = `${parts.provider}/*`;
|
|
49
|
+
return list.includes(wildcard) ? { kind: "wildcard", pattern: wildcard } : undefined;
|
|
50
|
+
}
|
|
51
|
+
/** 追加(去重、保持顺序)。 */
|
|
52
|
+
export function addEnabled(list, refs) {
|
|
53
|
+
const out = [...(list ?? [])];
|
|
54
|
+
for (const ref of refs)
|
|
55
|
+
if (!out.includes(ref))
|
|
56
|
+
out.push(ref);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** 移出;移空返回 undefined(删掉键,恢复「显示全部已配置」)。 */
|
|
60
|
+
export function removeEnabled(list, refs) {
|
|
61
|
+
const out = (list ?? []).filter((ref) => !refs.includes(ref));
|
|
62
|
+
return out.length > 0 ? out : undefined;
|
|
63
|
+
}
|
|
64
|
+
/** 配置里的清单:非数组或空数组视为未设置。 */
|
|
65
|
+
export function enabledList(value) {
|
|
66
|
+
if (!Array.isArray(value))
|
|
67
|
+
return undefined;
|
|
68
|
+
const list = value.filter((x) => typeof x === "string");
|
|
69
|
+
return list.length > 0 ? list : undefined;
|
|
70
|
+
}
|
|
@@ -63,6 +63,8 @@ export declare class ProviderRegistry implements ProviderRegistryApi {
|
|
|
63
63
|
private readonly envBaseUrls;
|
|
64
64
|
/** baseUrl 指向非官方主机的内置供应商:接受目录外的 model id。 */
|
|
65
65
|
private readonly relayed;
|
|
66
|
+
/** 模型表只来自发现缓存的供应商:表外的 id 照样合成(与空表相同)。 */
|
|
67
|
+
private readonly openTables;
|
|
66
68
|
/** config 里写了自己 `channels` 的供应商(内置渠道不因改 baseUrl 作废)。 */
|
|
67
69
|
private readonly userChannels;
|
|
68
70
|
/** config 里写了供应商级 `api` 的供应商(单渠道回落时目录模型跟它走)。 */
|
|
@@ -57,6 +57,8 @@ export class ProviderRegistry {
|
|
|
57
57
|
envBaseUrls = new Map();
|
|
58
58
|
/** baseUrl 指向非官方主机的内置供应商:接受目录外的 model id。 */
|
|
59
59
|
relayed = new Set();
|
|
60
|
+
/** 模型表只来自发现缓存的供应商:表外的 id 照样合成(与空表相同)。 */
|
|
61
|
+
openTables = new Set();
|
|
60
62
|
/** config 里写了自己 `channels` 的供应商(内置渠道不因改 baseUrl 作废)。 */
|
|
61
63
|
userChannels = new Set();
|
|
62
64
|
/** config 里写了供应商级 `api` 的供应商(单渠道回落时目录模型跟它走)。 */
|
|
@@ -344,6 +346,8 @@ export class ProviderRegistry {
|
|
|
344
346
|
const provider = this.providers.get(providerId);
|
|
345
347
|
if (!provider)
|
|
346
348
|
return;
|
|
349
|
+
if (source === "discovered" && provider.models.length === 0 && provider.requiresApiKey)
|
|
350
|
+
this.openTables.add(providerId);
|
|
347
351
|
for (const raw of models) {
|
|
348
352
|
this.raw.delete(`${providerId}/${raw.id}`);
|
|
349
353
|
const model = this.materialize(provider, { ...raw, provider: providerId });
|
|
@@ -376,9 +380,8 @@ export class ProviderRegistry {
|
|
|
376
380
|
candidates: available.filter(Boolean).map((c) => `${provider.id}/${model.id}@${c}`),
|
|
377
381
|
};
|
|
378
382
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return { ok: true, model: this.materialize(provider, model, channel), provider };
|
|
383
|
+
const picked = channel === model.channel ? model : this.materialize(provider, model, channel);
|
|
384
|
+
return { ok: true, model: { ...picked, channelPinned: true }, provider };
|
|
382
385
|
}
|
|
383
386
|
/**
|
|
384
387
|
* `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
|
|
@@ -408,11 +411,14 @@ export class ProviderRegistry {
|
|
|
408
411
|
const model = provider.models.find((m) => m.id === id);
|
|
409
412
|
if (model)
|
|
410
413
|
return { ok: true, model, provider };
|
|
411
|
-
const
|
|
414
|
+
const open = this.relayed.has(provider.id) || this.openTables.has(provider.id);
|
|
415
|
+
const relayed = provider.models.length === 0 || open;
|
|
412
416
|
if (synthesize && relayed && id.length > 0) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
417
|
+
// 按所要的渠道直接物化(先按缺省渠道物化再换渠道会留下缺省渠道的地址与 compat)
|
|
418
|
+
const known = provider.channels?.some((c) => c.name === channel) ? channel : undefined;
|
|
419
|
+
const synthesized = this.synthesize(provider, id, known);
|
|
420
|
+
if (known)
|
|
421
|
+
synthesized.channels = [...new Set([...(synthesized.channels ?? []), known])];
|
|
416
422
|
return { ok: true, model: synthesized, provider };
|
|
417
423
|
}
|
|
418
424
|
return { ok: false, reason: "not_found", candidates: this.similar(id, provider.id) };
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -321,6 +321,11 @@ export interface Model {
|
|
|
321
321
|
requiresApiKey?: boolean;
|
|
322
322
|
/** 所选渠道名(registry 物化时填入;隐式 `default` 渠道不填)。 */
|
|
323
323
|
channel?: string;
|
|
324
|
+
/**
|
|
325
|
+
* 渠道是引用里显式写的 `@渠道`(`findModel("p/m@c")` 填 true)。ChatGPT 订阅供应商据此区分:没写的渠道在
|
|
326
|
+
* 请求时跟随当前登录方式,写了的不跟随(不符时报 `chatgpt_flavor_mismatch`)。
|
|
327
|
+
*/
|
|
328
|
+
channelPinned?: boolean;
|
|
324
329
|
/** 该模型挂载的全部渠道,首个为首选(多渠道供应商才有)。 */
|
|
325
330
|
channels?: string[];
|
|
326
331
|
/** 模型家族(如 `claude-sonnet`)。 */
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* ChatGPT 后端的只读查询([W6-O]):模型列表(`ama models discover chatgpt`)与 codex 的 `wham/usage`。
|
|
3
3
|
*
|
|
4
4
|
* - SIWC:`GET {base}/models`,只留 `visibility === "list"`(没有该字段的也留);
|
|
5
|
-
* - codex:`GET {base}/models?client_version=<
|
|
5
|
+
* - codex:`GET {base}/models?client_version=<Codex CLI 版本>`(`codexClientVersion()`;后端按每个模型的
|
|
6
|
+
* `minimal_client_version` 过滤,发 ama 自己的版本号会一个都拿不到),`models[].slug`(同样按 visibility 筛),
|
|
7
|
+
* 另取 `context_window`、`input_modalities`、`supported_reasoning_levels` 作元数据(models.dev 补不到时用);
|
|
6
8
|
* `GET https://chatgpt.com/backend-api/wham/usage`(由 codex base 推出),私有格式,解析失败返回 undefined。
|
|
7
|
-
* 只取 slug
|
|
9
|
+
* SIWC 只取 slug 与显示名。请求带 Bearer;codex 另带 `ChatGPT-Account-ID` 与 `originator`。
|
|
8
10
|
*/
|
|
9
11
|
import { type QuotaSnapshot } from "../../ai/apis/chatgpt-rate-limits.js";
|
|
10
12
|
import type { ChatGptFlavor } from "../../config/types-w6.js";
|
|
@@ -15,10 +17,17 @@ export interface BackendAuth {
|
|
|
15
17
|
accessToken: string;
|
|
16
18
|
accountId?: string | undefined;
|
|
17
19
|
originator?: string | undefined;
|
|
20
|
+
/** codex 模型列表的 `client_version`,缺省 `DEFAULT_CODEX_CLIENT_VERSION`。 */
|
|
21
|
+
clientVersion?: string | undefined;
|
|
18
22
|
}
|
|
19
23
|
export interface DiscoveredModel {
|
|
20
24
|
id: string;
|
|
21
25
|
name?: string;
|
|
26
|
+
/** 以下只有 codex 后端给。 */
|
|
27
|
+
contextWindow?: number;
|
|
28
|
+
input?: ("text" | "image")[];
|
|
29
|
+
/** 支持的推理强度(`none` / `minimal` / `low` / `medium` / `high` / `xhigh` …)。 */
|
|
30
|
+
reasoningLevels?: string[];
|
|
22
31
|
}
|
|
23
32
|
export declare function listChatGptModels(fetchFn: FetchLike, baseUrl: string, auth: BackendAuth): Promise<DiscoveredModel[]>;
|
|
24
33
|
export declare function usageUrl(codexBaseUrl: string): string;
|