@armadra/agent 0.6.1 → 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 +17 -0
- package/CHANGELOG.zh-CN.md +12 -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 +22 -11
- package/dist/ai/providers/discovered-cache.js +56 -12
- package/dist/ai/providers/registry.js +7 -6
- 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 +2 -1
- package/dist/auth/chatgpt/cli.js +14 -7
- 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 +341 -180
- package/dist/cli/compose-providers.d.ts +2 -1
- package/dist/cli/compose-providers.js +6 -3
- package/dist/cli/startup-steps.js +4 -1
- package/dist/cli/subcommands/models-discover.d.ts +4 -2
- package/dist/cli/subcommands/models-discover.js +40 -8
- package/dist/config/json-schema.js +1 -0
- package/dist/config/key-docs.js +7 -1
- package/dist/config/schema-w6.d.ts +1 -1
- package/dist/config/schema-w6.js +2 -1
- package/dist/config/types-w6.d.ts +5 -0
- package/dist/i18n/catalog.d.ts +7 -0
- package/dist/i18n/messages/auth.d.ts +12 -0
- package/dist/i18n/messages/auth.js +20 -0
- package/dist/i18n/messages/config-keys.d.ts +2 -0
- package/dist/i18n/messages/config-keys.js +2 -0
- package/dist/i18n/messages/config.d.ts +2 -0
- package/docs/en/providers.md +8 -4
- package/docs/providers.md +25 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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
|
+
|
|
8
25
|
## 0.6.1 (2026-10-03)
|
|
9
26
|
|
|
10
27
|
- **The model picker shows only configured models; ChatGPT models can be picked**: `/model` (and the startup picker and
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
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
|
+
|
|
7
19
|
## 0.6.1(2026-10-03)
|
|
8
20
|
|
|
9
21
|
- **模型选择器只显示已配置的模型,ChatGPT 模型可选**:`/model`(以及启动选择器、`/config` 的模型项)只列有 key、OAuth 已登录或
|
|
@@ -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 &&
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* 模型表为空、接受任意 slug 的供应商(`chatgpt` 的订阅后端)在选择器里原本一个模型都看不到。
|
|
5
5
|
* `ama auth login chatgpt` 成功后与 `ama models discover chatgpt` 把账户可用的 slug 写进这里;组装注册表时
|
|
6
6
|
* (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商,元数据用 models.dev(快照 ⊕ 用户刷新)补全。
|
|
7
|
-
* `ama auth logout chatgpt` 删掉它。只存 slug
|
|
7
|
+
* `ama auth logout chatgpt` 删掉它。只存 slug、显示名、后端给的元数据(codex:上下文窗口、输入模态、推理强度,
|
|
8
|
+
* models.dev 补不到时用)、flavor 与时间戳——没有 token、账户 id。
|
|
9
|
+
*
|
|
10
|
+
* ChatGPT 的缓存带登录 flavor;组装时与当前登录的 flavor 不符视为过期、不并入(选择器提示重新发现)。
|
|
8
11
|
*/
|
|
9
|
-
import type { Model, ProviderData } from "../types.js";
|
|
12
|
+
import type { Model, ModelThinkingLevel, ProviderData } from "../types.js";
|
|
10
13
|
import type { ModelsDevIndex } from "./models-dev.js";
|
|
11
14
|
export declare const DISCOVERED_CACHE_VERSION = 1;
|
|
12
15
|
export interface DiscoveredCacheFile {
|
|
@@ -16,10 +19,15 @@ export interface DiscoveredCacheFile {
|
|
|
16
19
|
fetchedAt: string;
|
|
17
20
|
/** ChatGPT 登录的 flavor(siwc / codex);其它供应商不写。 */
|
|
18
21
|
flavor?: string;
|
|
19
|
-
models:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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[];
|
|
23
31
|
}
|
|
24
32
|
/** 缓存文件位置。 */
|
|
25
33
|
export declare function discoveredCachePath(dataDir: string, provider: string): string;
|
|
@@ -27,15 +35,16 @@ export declare function discoveredCachePath(dataDir: string, provider: string):
|
|
|
27
35
|
export declare function readDiscoveredCache(dataDir: string, provider: string): DiscoveredCacheFile | undefined;
|
|
28
36
|
/** 写缓存(先写临时文件再改名);返回路径。 */
|
|
29
37
|
export declare function writeDiscoveredCache(dataDir: string, provider: string, input: {
|
|
30
|
-
models: readonly {
|
|
31
|
-
id: string;
|
|
38
|
+
models: readonly (Omit<DiscoveredCacheModel, "name"> & {
|
|
32
39
|
name?: string | undefined;
|
|
33
|
-
}[];
|
|
40
|
+
})[];
|
|
34
41
|
flavor?: string | undefined;
|
|
35
42
|
}, now?: number): string;
|
|
36
43
|
/** 删缓存;删了返回 true。 */
|
|
37
44
|
export declare function clearDiscoveredCache(dataDir: string, provider: string): boolean;
|
|
38
|
-
/**
|
|
45
|
+
/** 后端给的推理强度 → 思考级别映射(`none` 对应 off;没列出的级别不支持)。 */
|
|
46
|
+
export declare function thinkingMapOf(levels: readonly string[]): Partial<Record<ModelThinkingLevel, string | null>>;
|
|
47
|
+
/** 缓存里的模型 → 注册表模型(models.dev 补元数据,补不到用后端给的,再补不到用自定义缺省)。 */
|
|
39
48
|
export declare function discoveredModels(file: DiscoveredCacheFile, provider: ProviderData, modelsDev: () => ModelsDevIndex | undefined): Model[];
|
|
40
49
|
/** 注册表里能追加模型的最小接口(`ProviderRegistry.addModels`)。 */
|
|
41
50
|
export interface DiscoveredTarget {
|
|
@@ -45,4 +54,6 @@ export interface DiscoveredTarget {
|
|
|
45
54
|
/**
|
|
46
55
|
* 把缓存并入模型表为空的供应商(本地服务除外:它们每次启动现场探测)。返回并入了的供应商 id。
|
|
47
56
|
*/
|
|
48
|
-
export declare function mergeDiscoveredModels(registry: DiscoveredTarget, dataDir: string, modelsDev: () => ModelsDevIndex | undefined
|
|
57
|
+
export declare function mergeDiscoveredModels(registry: DiscoveredTarget, dataDir: string, modelsDev: () => ModelsDevIndex | undefined,
|
|
58
|
+
/** 当前登录的 flavor(只有 OAuth 供应商有):缓存的 flavor 与之不符视为过期,不并入。 */
|
|
59
|
+
loginFlavor?: (providerId: string) => string | undefined): string[];
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* 模型表为空、接受任意 slug 的供应商(`chatgpt` 的订阅后端)在选择器里原本一个模型都看不到。
|
|
5
5
|
* `ama auth login chatgpt` 成功后与 `ama models discover chatgpt` 把账户可用的 slug 写进这里;组装注册表时
|
|
6
6
|
* (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商,元数据用 models.dev(快照 ⊕ 用户刷新)补全。
|
|
7
|
-
* `ama auth logout chatgpt` 删掉它。只存 slug
|
|
7
|
+
* `ama auth logout chatgpt` 删掉它。只存 slug、显示名、后端给的元数据(codex:上下文窗口、输入模态、推理强度,
|
|
8
|
+
* models.dev 补不到时用)、flavor 与时间戳——没有 token、账户 id。
|
|
9
|
+
*
|
|
10
|
+
* ChatGPT 的缓存带登录 flavor;组装时与当前登录的 flavor 不符视为过期、不并入(选择器提示重新发现)。
|
|
8
11
|
*/
|
|
9
12
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
10
13
|
import { dirname, join } from "node:path";
|
|
@@ -15,11 +18,24 @@ export const DISCOVERED_CACHE_VERSION = 1;
|
|
|
15
18
|
export function discoveredCachePath(dataDir, provider) {
|
|
16
19
|
return join(dataDir, "models", "discovered", `${provider}.json`);
|
|
17
20
|
}
|
|
18
|
-
|
|
21
|
+
/** 未知值 → 缓存模型;id 不对返回 undefined,元数据字段形状不对的丢掉。 */
|
|
22
|
+
function toEntry(value) {
|
|
19
23
|
if (typeof value !== "object" || value === null)
|
|
20
|
-
return
|
|
21
|
-
const
|
|
22
|
-
|
|
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;
|
|
23
39
|
}
|
|
24
40
|
/** 读缓存;不存在、损坏或形状不对返回 undefined(不报错,选择器给提示行)。 */
|
|
25
41
|
export function readDiscoveredCache(dataDir, provider) {
|
|
@@ -40,9 +56,7 @@ export function readDiscoveredCache(dataDir, provider) {
|
|
|
40
56
|
provider,
|
|
41
57
|
fetchedAt: typeof file.fetchedAt === "string" ? file.fetchedAt : "",
|
|
42
58
|
...(typeof file.flavor === "string" ? { flavor: file.flavor } : {}),
|
|
43
|
-
models: file.models
|
|
44
|
-
.filter(isEntry)
|
|
45
|
-
.map((m) => (m.name ? { id: m.id, name: m.name } : { id: m.id })),
|
|
59
|
+
models: file.models.map(toEntry).filter((m) => m !== undefined),
|
|
46
60
|
};
|
|
47
61
|
}
|
|
48
62
|
/** 写缓存(先写临时文件再改名);返回路径。 */
|
|
@@ -53,7 +67,7 @@ export function writeDiscoveredCache(dataDir, provider, input, now = Date.now())
|
|
|
53
67
|
provider,
|
|
54
68
|
fetchedAt: new Date(now).toISOString(),
|
|
55
69
|
...(input.flavor !== undefined ? { flavor: input.flavor } : {}),
|
|
56
|
-
models: input.models.map((m) =>
|
|
70
|
+
models: input.models.map(toEntry).filter((m) => m !== undefined),
|
|
57
71
|
};
|
|
58
72
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
59
73
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
@@ -69,11 +83,36 @@ export function clearDiscoveredCache(dataDir, provider) {
|
|
|
69
83
|
rmSync(path, { force: true });
|
|
70
84
|
return true;
|
|
71
85
|
}
|
|
72
|
-
|
|
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 补元数据,补不到用后端给的,再补不到用自定义缺省)。 */
|
|
73
111
|
export function discoveredModels(file, provider, modelsDev) {
|
|
74
112
|
return file.models.map((m) => {
|
|
75
|
-
const { entry } = enrichEntry(m.name ? { id: m.id, name: m.name } : { id: m.id }, modelsDev);
|
|
113
|
+
const { entry, metadata } = enrichEntry(m.name ? { id: m.id, name: m.name } : { id: m.id }, modelsDev);
|
|
76
114
|
const model = withCustomDefaults(entry, provider.id, provider.api);
|
|
115
|
+
withBackendMetadata(model, m, metadata.match !== undefined);
|
|
77
116
|
// 只挂发现时登录的 flavor 对应的渠道(订阅后端的另一条渠道用不了,不在说明与 `@` 行里出现);
|
|
78
117
|
// 缓存没写 flavor 或渠道不存在时用供应商的缺省渠道
|
|
79
118
|
const names = (provider.channels ?? []).map((c) => c.name);
|
|
@@ -87,7 +126,9 @@ export function discoveredModels(file, provider, modelsDev) {
|
|
|
87
126
|
/**
|
|
88
127
|
* 把缓存并入模型表为空的供应商(本地服务除外:它们每次启动现场探测)。返回并入了的供应商 id。
|
|
89
128
|
*/
|
|
90
|
-
export function mergeDiscoveredModels(registry, dataDir, modelsDev
|
|
129
|
+
export function mergeDiscoveredModels(registry, dataDir, modelsDev,
|
|
130
|
+
/** 当前登录的 flavor(只有 OAuth 供应商有):缓存的 flavor 与之不符视为过期,不并入。 */
|
|
131
|
+
loginFlavor = () => undefined) {
|
|
91
132
|
const merged = [];
|
|
92
133
|
for (const provider of registry.list()) {
|
|
93
134
|
if (provider.models.length > 0 || !provider.requiresApiKey)
|
|
@@ -95,6 +136,9 @@ export function mergeDiscoveredModels(registry, dataDir, modelsDev) {
|
|
|
95
136
|
const file = readDiscoveredCache(dataDir, provider.id);
|
|
96
137
|
if (file === undefined || file.models.length === 0)
|
|
97
138
|
continue;
|
|
139
|
+
const flavor = loginFlavor(provider.id);
|
|
140
|
+
if (flavor !== undefined && file.flavor !== undefined && file.flavor !== flavor)
|
|
141
|
+
continue;
|
|
98
142
|
registry.addModels(provider.id, discoveredModels(file, provider, modelsDev), "discovered");
|
|
99
143
|
merged.push(provider.id);
|
|
100
144
|
}
|
|
@@ -380,9 +380,8 @@ export class ProviderRegistry {
|
|
|
380
380
|
candidates: available.filter(Boolean).map((c) => `${provider.id}/${model.id}@${c}`),
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
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 };
|
|
386
385
|
}
|
|
387
386
|
/**
|
|
388
387
|
* `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
|
|
@@ -415,9 +414,11 @@ export class ProviderRegistry {
|
|
|
415
414
|
const open = this.relayed.has(provider.id) || this.openTables.has(provider.id);
|
|
416
415
|
const relayed = provider.models.length === 0 || open;
|
|
417
416
|
if (synthesize && relayed && id.length > 0) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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])];
|
|
421
422
|
return { ok: true, model: synthesized, provider };
|
|
422
423
|
}
|
|
423
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;
|
|
@@ -2,13 +2,14 @@
|
|
|
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 { parseUsagePayload } from "../../ai/apis/chatgpt-rate-limits.js";
|
|
10
|
-
import {
|
|
11
|
-
import { DEFAULT_ORIGINATOR } from "./presets.js";
|
|
12
|
+
import { DEFAULT_CODEX_CLIENT_VERSION, DEFAULT_ORIGINATOR } from "./presets.js";
|
|
12
13
|
export const BACKEND_TIMEOUT_MS = 15_000;
|
|
13
14
|
function headers(auth) {
|
|
14
15
|
const out = {
|
|
@@ -26,10 +27,37 @@ function listed(item) {
|
|
|
26
27
|
const visibility = item["visibility"];
|
|
27
28
|
return visibility === undefined || visibility === "list";
|
|
28
29
|
}
|
|
30
|
+
/** codex 模型条目里的元数据;字段缺失或形状不对就不填。 */
|
|
31
|
+
function codexMetadata(item) {
|
|
32
|
+
const out = {};
|
|
33
|
+
const window = item["context_window"];
|
|
34
|
+
if (typeof window === "number" && Number.isInteger(window) && window > 0)
|
|
35
|
+
out.contextWindow = window;
|
|
36
|
+
const modalities = item["input_modalities"];
|
|
37
|
+
if (Array.isArray(modalities)) {
|
|
38
|
+
const input = ["text", "image"].filter((x) => modalities.includes(x));
|
|
39
|
+
if (input.includes("text"))
|
|
40
|
+
out.input = input;
|
|
41
|
+
}
|
|
42
|
+
const levels = item["supported_reasoning_levels"];
|
|
43
|
+
if (Array.isArray(levels)) {
|
|
44
|
+
const efforts = levels
|
|
45
|
+
.map((level) => typeof level === "string"
|
|
46
|
+
? level
|
|
47
|
+
: typeof level === "object" && level !== null
|
|
48
|
+
? level["effort"]
|
|
49
|
+
: undefined)
|
|
50
|
+
.filter((x) => typeof x === "string" && x !== "");
|
|
51
|
+
if (efforts.length > 0)
|
|
52
|
+
out.reasoningLevels = [...new Set(efforts)];
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
29
56
|
export async function listChatGptModels(fetchFn, baseUrl, auth) {
|
|
30
57
|
const base = baseUrl.replace(/\/+$/, "");
|
|
58
|
+
const version = auth.clientVersion ?? DEFAULT_CODEX_CLIENT_VERSION;
|
|
31
59
|
const url = auth.flavor === "codex"
|
|
32
|
-
? `${base}/models?client_version=${encodeURIComponent(
|
|
60
|
+
? `${base}/models?client_version=${encodeURIComponent(version)}`
|
|
33
61
|
: `${base}/models`;
|
|
34
62
|
const response = await fetchFn(url, {
|
|
35
63
|
headers: headers(auth),
|
|
@@ -49,7 +77,11 @@ export async function listChatGptModels(fetchFn, baseUrl, auth) {
|
|
|
49
77
|
continue;
|
|
50
78
|
seen.add(id);
|
|
51
79
|
const name = item["display_name"];
|
|
52
|
-
out.push(
|
|
80
|
+
out.push({
|
|
81
|
+
id,
|
|
82
|
+
...(typeof name === "string" && name !== "" ? { name } : {}),
|
|
83
|
+
...(auth.flavor === "codex" ? codexMetadata(item) : {}),
|
|
84
|
+
});
|
|
53
85
|
}
|
|
54
86
|
return out;
|
|
55
87
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* - login:flavor 取 `--flavor` > 用户级 `auth.chatgpt.flavor` > siwc;`--paste` / `--device`(只 codex)/ 缺省
|
|
5
5
|
* 浏览器;codex 首次在 TTY 下一次性确认「非官方、仅个人使用」(非 TTY 需 `--yes`),`acknowledgedAt` 入条目;
|
|
6
6
|
* 成功后在刷新锁下写 auth.json(0600)。
|
|
7
|
-
*
|
|
7
|
+
* 成功后先删旧的发现缓存,再查一次模型列表写缓存(`<dataDir>/models/discovered/chatgpt.json`;0 个也写空表并带
|
|
8
|
+
* flavor,免得残留另一种登录方式的缓存;查询失败静默)。codex 返回 0 个时提示 codexClientVersion 可能过旧。
|
|
8
9
|
* - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地;
|
|
9
10
|
* 同时删发现缓存。
|
|
10
11
|
* - status:flavor、计划、掩码邮箱、access token 剩余、needsLogin;codex 另查 `wham/usage` 显示配额。
|
package/dist/auth/chatgpt/cli.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* - login:flavor 取 `--flavor` > 用户级 `auth.chatgpt.flavor` > siwc;`--paste` / `--device`(只 codex)/ 缺省
|
|
5
5
|
* 浏览器;codex 首次在 TTY 下一次性确认「非官方、仅个人使用」(非 TTY 需 `--yes`),`acknowledgedAt` 入条目;
|
|
6
6
|
* 成功后在刷新锁下写 auth.json(0600)。
|
|
7
|
-
*
|
|
7
|
+
* 成功后先删旧的发现缓存,再查一次模型列表写缓存(`<dataDir>/models/discovered/chatgpt.json`;0 个也写空表并带
|
|
8
|
+
* flavor,免得残留另一种登录方式的缓存;查询失败静默)。codex 返回 0 个时提示 codexClientVersion 可能过旧。
|
|
8
9
|
* - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地;
|
|
9
10
|
* 同时删发现缓存。
|
|
10
11
|
* - status:flavor、计划、掩码邮箱、access token 剩余、needsLogin;codex 另查 `wham/usage` 显示配额。
|
|
@@ -25,7 +26,7 @@ import { clearDiscoveredCache, writeDiscoveredCache } from "../../ai/providers/d
|
|
|
25
26
|
import { fetchCodexUsage, listChatGptModels } from "./backend-client.js";
|
|
26
27
|
import { maskEmail } from "./claims.js";
|
|
27
28
|
import { loginChatGpt, revokeChatGpt } from "./login.js";
|
|
28
|
-
import { CHATGPT_PROVIDER_ID, chatgptBaseUrl } from "./presets.js";
|
|
29
|
+
import { CHATGPT_PROVIDER_ID, chatgptBaseUrl, codexClientVersion } from "./presets.js";
|
|
29
30
|
import { quotaParts } from "./quota-text.js";
|
|
30
31
|
function userAuthConfig(io) {
|
|
31
32
|
try {
|
|
@@ -186,9 +187,14 @@ export async function runLogin(args, io, deps = {}) {
|
|
|
186
187
|
if (entry.planType === "free")
|
|
187
188
|
io.stdout(`${m.freePlanHint(entry.planType)}\n`);
|
|
188
189
|
if (flavor === "siwc")
|
|
189
|
-
io.stdout(`${m.siwcLimitHint}\n`);
|
|
190
|
+
io.stdout(`${m.siwcLimitHint}\n${m.siwcEligibilityHint}\n`);
|
|
190
191
|
const count = await cacheModels(io, entry, config, deps);
|
|
191
|
-
|
|
192
|
+
const done = count === 0 && flavor === "codex"
|
|
193
|
+
? m.codexNoModels(codexClientVersion(config, io.env))
|
|
194
|
+
: count === undefined || count === 0
|
|
195
|
+
? m.modelHint
|
|
196
|
+
: m.modelsCached(count);
|
|
197
|
+
io.stdout(`${done}\n`);
|
|
192
198
|
return ExitCode.Ok;
|
|
193
199
|
}
|
|
194
200
|
/**
|
|
@@ -196,16 +202,17 @@ export async function runLogin(args, io, deps = {}) {
|
|
|
196
202
|
* 任何失败静默(返回 undefined,调用方改提示 `ama models discover chatgpt`)。
|
|
197
203
|
*/
|
|
198
204
|
async function cacheModels(io, entry, config, deps) {
|
|
205
|
+
const dataDir = resolveDataDir({ env: io.env });
|
|
199
206
|
try {
|
|
207
|
+
// 先删旧缓存:换了登录方式时另一 flavor 的缓存不能留下
|
|
208
|
+
clearDiscoveredCache(dataDir, CHATGPT_PROVIDER_ID);
|
|
200
209
|
const models = await listChatGptModels(deps.fetch ?? fetch, chatgptBaseUrl(entry.flavor, io.env), {
|
|
201
210
|
flavor: entry.flavor,
|
|
202
211
|
accessToken: entry.accessToken,
|
|
203
212
|
accountId: entry.accountId,
|
|
204
213
|
originator: config?.originator,
|
|
214
|
+
clientVersion: codexClientVersion(config, io.env),
|
|
205
215
|
});
|
|
206
|
-
if (models.length === 0)
|
|
207
|
-
return undefined;
|
|
208
|
-
const dataDir = resolveDataDir({ env: io.env });
|
|
209
216
|
writeDiscoveredCache(dataDir, CHATGPT_PROVIDER_ID, { models, flavor: entry.flavor });
|
|
210
217
|
return models.length;
|
|
211
218
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT 订阅:请求渠道跟随当前登录方式。
|
|
3
|
+
*
|
|
4
|
+
* `siwc` 与 `codex` 两条渠道协议相同(openai-responses),只差端点、请求头与请求体白名单。会话里的模型是启动
|
|
5
|
+
* (或选模型)时按当时的登录物化的;用户中途 `ama auth logout` 再换另一种方式登录后,同一会话的下一次请求
|
|
6
|
+
* 拿到的 token 属于新的 flavor。这里在**请求时**按 token 的 flavor(活 token 登记)把模型改到对应渠道:
|
|
7
|
+
*
|
|
8
|
+
* - 模型引用没有显式 `@渠道`(`channelPinned` 不为 true)→ 改到登录 flavor 的渠道(端点、渠道头、compat);
|
|
9
|
+
* - 显式写了另一渠道 → 不动,协议层报 `chatgpt_flavor_mismatch`(chatgpt-backend.ts);
|
|
10
|
+
* - 不是 ChatGPT 后端、token 不是 OAuth、flavor 已一致 → 原样返回。
|
|
11
|
+
*
|
|
12
|
+
* 会话里记录的模型(`model_change`)不算显式:恢复时由 startup-steps 去掉渠道再解析。
|
|
13
|
+
*/
|
|
14
|
+
import type { Model, ProviderData } from "../../ai/types.js";
|
|
15
|
+
/** 按 `apiKey` 对应登录的 flavor 改写模型的渠道;无需改动时返回原对象。 */
|
|
16
|
+
export declare function followChatGptLogin(model: Model, provider: ProviderData | undefined, apiKey: string | undefined): Model;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT 订阅:请求渠道跟随当前登录方式。
|
|
3
|
+
*
|
|
4
|
+
* `siwc` 与 `codex` 两条渠道协议相同(openai-responses),只差端点、请求头与请求体白名单。会话里的模型是启动
|
|
5
|
+
* (或选模型)时按当时的登录物化的;用户中途 `ama auth logout` 再换另一种方式登录后,同一会话的下一次请求
|
|
6
|
+
* 拿到的 token 属于新的 flavor。这里在**请求时**按 token 的 flavor(活 token 登记)把模型改到对应渠道:
|
|
7
|
+
*
|
|
8
|
+
* - 模型引用没有显式 `@渠道`(`channelPinned` 不为 true)→ 改到登录 flavor 的渠道(端点、渠道头、compat);
|
|
9
|
+
* - 显式写了另一渠道 → 不动,协议层报 `chatgpt_flavor_mismatch`(chatgpt-backend.ts);
|
|
10
|
+
* - 不是 ChatGPT 后端、token 不是 OAuth、flavor 已一致 → 原样返回。
|
|
11
|
+
*
|
|
12
|
+
* 会话里记录的模型(`model_change`)不算显式:恢复时由 startup-steps 去掉渠道再解析。
|
|
13
|
+
*/
|
|
14
|
+
import { liveToken } from "../oauth/live.js";
|
|
15
|
+
function backendOf(compat) {
|
|
16
|
+
if (typeof compat !== "object" || compat === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
const value = compat.chatgptBackend;
|
|
19
|
+
return value === "siwc" || value === "codex" ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** 按 `apiKey` 对应登录的 flavor 改写模型的渠道;无需改动时返回原对象。 */
|
|
22
|
+
export function followChatGptLogin(model, provider, apiKey) {
|
|
23
|
+
const current = backendOf(model.compat);
|
|
24
|
+
if (current === undefined || model.channelPinned === true)
|
|
25
|
+
return model;
|
|
26
|
+
const flavor = liveToken(apiKey)?.flavor;
|
|
27
|
+
if (flavor === undefined || flavor === current)
|
|
28
|
+
return model;
|
|
29
|
+
const channels = provider?.channels ?? [];
|
|
30
|
+
const target = channels.find((c) => backendOf(c.compat) === flavor) ?? channels.find((c) => c.name === flavor);
|
|
31
|
+
if (target === undefined)
|
|
32
|
+
return model;
|
|
33
|
+
const previous = channels.find((c) => c.name === model.channel);
|
|
34
|
+
// 去掉旧渠道带来的头(codex 的 originator 等),换上新渠道的
|
|
35
|
+
const headers = { ...model.headers };
|
|
36
|
+
for (const name of Object.keys(previous?.headers ?? {}))
|
|
37
|
+
delete headers[name];
|
|
38
|
+
Object.assign(headers, target.headers);
|
|
39
|
+
const next = {
|
|
40
|
+
...model,
|
|
41
|
+
api: target.api,
|
|
42
|
+
baseUrl: target.baseUrl,
|
|
43
|
+
channel: target.name,
|
|
44
|
+
compat: { ...model.compat, ...target.compat, chatgptBackend: flavor },
|
|
45
|
+
};
|
|
46
|
+
if (Object.keys(headers).length > 0)
|
|
47
|
+
next.headers = headers;
|
|
48
|
+
else
|
|
49
|
+
delete next.headers;
|
|
50
|
+
return next;
|
|
51
|
+
}
|