@eddyskywalker/dsh-chatgpt-subscription 0.42.0 → 0.43.1
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 +61 -0
- package/README.md +1 -0
- package/lib/client.js +256 -182
- package/lib/client.js.map +1 -1
- package/lib/index.js +813 -237
- package/lib/types/client/command-code/CommandCodeComposerQuota.d.ts.map +1 -1
- package/lib/types/client/common/format.d.ts +16 -0
- package/lib/types/client/common/format.d.ts.map +1 -1
- package/lib/types/client/kimi-code/KimiCodeComposerQuota.d.ts.map +1 -1
- package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
- package/lib/types/client/kimi-code/KimiQuotaCard.d.ts.map +1 -1
- package/lib/types/client/kimi-code/styles.d.ts.map +1 -1
- package/lib/types/client/locales-ru/opendesign.d.ts.map +1 -1
- package/lib/types/client/locales-ru/qoder.d.ts.map +1 -1
- package/lib/types/client/opendesign/OpenDesignSection.d.ts.map +1 -1
- package/lib/types/client/opendesign/locales.d.ts +12 -0
- package/lib/types/client/opendesign/locales.d.ts.map +1 -1
- package/lib/types/client/qoder/QoderSection.d.ts.map +1 -1
- package/lib/types/client/qoder/locales.d.ts +9 -1
- package/lib/types/client/qoder/locales.d.ts.map +1 -1
- package/lib/types/client/qoder/styles.d.ts.map +1 -1
- package/lib/types/compat.d.ts +1 -1
- package/lib/types/host/claude/types.d.ts +7 -1
- package/lib/types/host/claude/types.d.ts.map +1 -1
- package/lib/types/host/glm-coding-plan/types.d.ts +5 -1
- package/lib/types/host/glm-coding-plan/types.d.ts.map +1 -1
- package/lib/types/host/kimi-code/account-pool.d.ts +26 -1
- package/lib/types/host/kimi-code/account-pool.d.ts.map +1 -1
- package/lib/types/host/kimi-code/client.d.ts +24 -0
- package/lib/types/host/kimi-code/client.d.ts.map +1 -1
- package/lib/types/host/kimi-code/oauth.d.ts +16 -0
- package/lib/types/host/kimi-code/oauth.d.ts.map +1 -1
- package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
- package/lib/types/host/opendesign/adapter.d.ts +11 -13
- package/lib/types/host/opendesign/adapter.d.ts.map +1 -1
- package/lib/types/host/opendesign/catalog.d.ts +14 -0
- package/lib/types/host/opendesign/catalog.d.ts.map +1 -1
- package/lib/types/host/opendesign/line.d.ts +17 -6
- package/lib/types/host/opendesign/line.d.ts.map +1 -1
- package/lib/types/host/opendesign/routes.d.ts.map +1 -1
- package/lib/types/host/opendesign/token-store.d.ts +91 -4
- package/lib/types/host/opendesign/token-store.d.ts.map +1 -1
- package/lib/types/host/opendesign/types.d.ts.map +1 -1
- package/lib/types/host/qoder/client.d.ts.map +1 -1
- package/lib/types/host/qoder/mapper.d.ts +74 -2
- package/lib/types/host/qoder/mapper.d.ts.map +1 -1
- package/lib/types/host/qoder/types.d.ts +6 -1
- package/lib/types/host/qoder/types.d.ts.map +1 -1
- package/lib/types/host/step-code/types.d.ts +5 -1
- package/lib/types/host/step-code/types.d.ts.map +1 -1
- package/lib/types/shared/opendesign-contracts.d.ts +24 -1
- package/lib/types/shared/opendesign-contracts.d.ts.map +1 -1
- package/lib/types/shared/qoder-contracts.d.ts +10 -0
- package/lib/types/shared/qoder-contracts.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -141,7 +141,7 @@ const ROUTE_PREFIX$4 = "/api/dsh-chatgpt-subscription";
|
|
|
141
141
|
* `test/responses-client.test.ts` asserts the two agree, so a release that
|
|
142
142
|
* forgets this constant fails the suite instead of shipping quietly.
|
|
143
143
|
*/
|
|
144
|
-
const PLUGIN_VERSION = "0.
|
|
144
|
+
const PLUGIN_VERSION = "0.43.1";
|
|
145
145
|
const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
|
|
146
146
|
const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
|
|
147
147
|
const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
|
|
@@ -26310,9 +26310,11 @@ function identityFromTokens(accessToken, refreshToken) {
|
|
|
26310
26310
|
const refresh = refreshToken === void 0 ? void 0 : decodeJwtPayload(refreshToken);
|
|
26311
26311
|
const userId = claimString(access?.user_id) ?? claimString(refresh?.user_id) ?? claimString(access?.sub) ?? claimString(refresh?.sub);
|
|
26312
26312
|
const email = (claimString(access?.email) ?? claimString(refresh?.email))?.toLowerCase();
|
|
26313
|
+
const nickname = claimString(access?.nickname) ?? claimString(refresh?.nickname) ?? claimString(access?.name) ?? claimString(refresh?.name) ?? claimString(access?.preferred_username) ?? claimString(refresh?.preferred_username);
|
|
26313
26314
|
return {
|
|
26314
26315
|
...userId === void 0 ? {} : { userId },
|
|
26315
|
-
...email === void 0 ? {} : { email }
|
|
26316
|
+
...email === void 0 ? {} : { email },
|
|
26317
|
+
...nickname === void 0 ? {} : { nickname }
|
|
26316
26318
|
};
|
|
26317
26319
|
}
|
|
26318
26320
|
function parseTokenResponse$2(record) {
|
|
@@ -26511,7 +26513,7 @@ async function runDeviceLogin(options) {
|
|
|
26511
26513
|
});
|
|
26512
26514
|
if (outcome.kind === "success") {
|
|
26513
26515
|
const identity = identityFromTokens(outcome.token.accessToken, outcome.token.refreshToken);
|
|
26514
|
-
const
|
|
26516
|
+
const signedIn = {
|
|
26515
26517
|
accessToken: outcome.token.accessToken,
|
|
26516
26518
|
refreshToken: outcome.token.refreshToken,
|
|
26517
26519
|
expiresAt: outcome.token.expiresAt,
|
|
@@ -26523,8 +26525,10 @@ async function runDeviceLogin(options) {
|
|
|
26523
26525
|
baseUrl: codingBaseUrl(options.region),
|
|
26524
26526
|
authenticatedAt: Date.now(),
|
|
26525
26527
|
...identity.userId === void 0 ? {} : { userId: identity.userId },
|
|
26526
|
-
...identity.email === void 0 ? {} : { email: identity.email }
|
|
26528
|
+
...identity.email === void 0 ? {} : { email: identity.email },
|
|
26529
|
+
...identity.nickname === void 0 ? {} : { nickname: identity.nickname }
|
|
26527
26530
|
};
|
|
26531
|
+
const credentials = options.enrich === void 0 ? signedIn : await options.enrich(signedIn).catch(() => signedIn);
|
|
26528
26532
|
await options.store.write(credentials);
|
|
26529
26533
|
if (options.onSave) await options.onSave(credentials).catch(() => void 0);
|
|
26530
26534
|
await persistRegion(options.region);
|
|
@@ -26573,7 +26577,8 @@ async function beginWebLogin(store, options = {}) {
|
|
|
26573
26577
|
region,
|
|
26574
26578
|
signal: controller.signal,
|
|
26575
26579
|
open,
|
|
26576
|
-
...options.onSave === void 0 ? {} : { onSave: options.onSave }
|
|
26580
|
+
...options.onSave === void 0 ? {} : { onSave: options.onSave },
|
|
26581
|
+
...options.enrich === void 0 ? {} : { enrich: options.enrich }
|
|
26577
26582
|
});
|
|
26578
26583
|
webLoginFlow = {
|
|
26579
26584
|
...webLoginFlow,
|
|
@@ -27365,6 +27370,92 @@ function parsePlanLevel(payload) {
|
|
|
27365
27370
|
return asString$18(membership?.level ?? membership?.levelId ?? membership?.level_id) ?? null;
|
|
27366
27371
|
}
|
|
27367
27372
|
/**
|
|
27373
|
+
* Field names the service uses for the account's own name.
|
|
27374
|
+
*
|
|
27375
|
+
* `/me` documents a nickname; the platform also echoes a display name, and a
|
|
27376
|
+
* gateway envelope adds nothing but depth. All of them are the same fact: what
|
|
27377
|
+
* to title the account's card with, and what a `账号 N` placeholder should
|
|
27378
|
+
* become.
|
|
27379
|
+
*/
|
|
27380
|
+
const PROFILE_NAME_KEYS = [
|
|
27381
|
+
"nickname",
|
|
27382
|
+
"name",
|
|
27383
|
+
"username",
|
|
27384
|
+
"display_name",
|
|
27385
|
+
"displayName",
|
|
27386
|
+
"preferred_username"
|
|
27387
|
+
];
|
|
27388
|
+
/**
|
|
27389
|
+
* Every record a profile payload may keep the identity in, outermost first.
|
|
27390
|
+
*
|
|
27391
|
+
* The endpoint has answered flat (`{ nickname, email, ... }`) and nested
|
|
27392
|
+
* (`{ data: { userInfo: { ... } } }`) on different gateway versions, and a
|
|
27393
|
+
* nested envelope may hold credentials-adjacent siblings at the top level too,
|
|
27394
|
+
* so the search never commits to one shape: each container is tried in turn and
|
|
27395
|
+
* the first one that names a field wins.
|
|
27396
|
+
*/
|
|
27397
|
+
function profileRecords(payload) {
|
|
27398
|
+
const root = asRecord$13(payload);
|
|
27399
|
+
if (root === void 0) return [];
|
|
27400
|
+
const data = asRecord$13(root.data);
|
|
27401
|
+
return [
|
|
27402
|
+
root,
|
|
27403
|
+
data,
|
|
27404
|
+
asRecord$13(root.userInfo) ?? asRecord$13(data?.userInfo),
|
|
27405
|
+
asRecord$13(root.user) ?? asRecord$13(data?.user)
|
|
27406
|
+
].filter((record) => record !== void 0);
|
|
27407
|
+
}
|
|
27408
|
+
/** First value among `keys` found in any profile container, outermost first. */
|
|
27409
|
+
function profileString(payload, keys) {
|
|
27410
|
+
for (const record of profileRecords(payload)) {
|
|
27411
|
+
const found = firstString$1(record, keys);
|
|
27412
|
+
if (found !== void 0) return found;
|
|
27413
|
+
}
|
|
27414
|
+
}
|
|
27415
|
+
/**
|
|
27416
|
+
* Fold the facts a `/me` read established into a credential.
|
|
27417
|
+
*
|
|
27418
|
+
* Sign-in only names the account through its token, which carries an id and —
|
|
27419
|
+
* for an account with a mailbox — an address. The nickname lives on `/me` and
|
|
27420
|
+
* nowhere else, so the caller reads it once and stores it here; without this,
|
|
27421
|
+
* a phone-signed-in account could never be called anything but `账号 N`.
|
|
27422
|
+
*
|
|
27423
|
+
* Only fields the profile actually names are written: a gateway that drops the
|
|
27424
|
+
* nickname must not blank one learned earlier, and the token pair is copied
|
|
27425
|
+
* through untouched. Returns the same object when nothing new was learned, so a
|
|
27426
|
+
* caller can tell "nothing to store" from "store this".
|
|
27427
|
+
*/
|
|
27428
|
+
function credentialsWithProfile(credentials, profile) {
|
|
27429
|
+
const nickname = profileString(profile, PROFILE_NAME_KEYS);
|
|
27430
|
+
const email = profileString(profile, ["email"])?.toLowerCase();
|
|
27431
|
+
const userId = profileString(profile, [
|
|
27432
|
+
"user_id",
|
|
27433
|
+
"userId",
|
|
27434
|
+
"global_id",
|
|
27435
|
+
"globalId"
|
|
27436
|
+
]);
|
|
27437
|
+
const planName = parsePlanName(profile) ?? void 0;
|
|
27438
|
+
const next = { ...credentials };
|
|
27439
|
+
let changed = false;
|
|
27440
|
+
if (nickname !== void 0 && next.nickname !== nickname) {
|
|
27441
|
+
next.nickname = nickname;
|
|
27442
|
+
changed = true;
|
|
27443
|
+
}
|
|
27444
|
+
if (email !== void 0 && next.email !== email) {
|
|
27445
|
+
next.email = email;
|
|
27446
|
+
changed = true;
|
|
27447
|
+
}
|
|
27448
|
+
if (userId !== void 0 && next.userId !== userId) {
|
|
27449
|
+
next.userId = userId;
|
|
27450
|
+
changed = true;
|
|
27451
|
+
}
|
|
27452
|
+
if (planName !== void 0 && next.planName !== planName) {
|
|
27453
|
+
next.planName = planName;
|
|
27454
|
+
changed = true;
|
|
27455
|
+
}
|
|
27456
|
+
return changed ? next : credentials;
|
|
27457
|
+
}
|
|
27458
|
+
/**
|
|
27368
27459
|
* Fetch and cache the account's quota snapshot.
|
|
27369
27460
|
*
|
|
27370
27461
|
* A 401 is surfaced as a rejection so the caller can invalidate the stored
|
|
@@ -27415,6 +27506,9 @@ async function fetchAccountQuota$3(store, options = {}) {
|
|
|
27415
27506
|
...credentials,
|
|
27416
27507
|
...learned
|
|
27417
27508
|
}).catch(() => void 0);
|
|
27509
|
+
if (options.onAccount !== void 0) try {
|
|
27510
|
+
await options.onAccount(account, credentials);
|
|
27511
|
+
} catch {}
|
|
27418
27512
|
const snapshot = {
|
|
27419
27513
|
account,
|
|
27420
27514
|
planName: account.planName,
|
|
@@ -27468,12 +27562,8 @@ function accountFromCredentials$1(credentials, payload, profile) {
|
|
|
27468
27562
|
const planLevel = (profile === void 0 ? null : parsePlanLevel(profile)) ?? (payload === void 0 ? null : parsePlanLevel(payload));
|
|
27469
27563
|
return {
|
|
27470
27564
|
userId: credentials.userId ?? identity.userId ?? null,
|
|
27471
|
-
nickname: (profile === void 0 ?
|
|
27472
|
-
|
|
27473
|
-
"username",
|
|
27474
|
-
"name"
|
|
27475
|
-
])) ?? credentials.nickname ?? null,
|
|
27476
|
-
email: credentials.email ?? identity.email ?? null,
|
|
27565
|
+
nickname: (profile === void 0 ? void 0 : profileString(profile, PROFILE_NAME_KEYS)) ?? credentials.nickname ?? null,
|
|
27566
|
+
email: credentials.email ?? identity.email ?? (profile === void 0 ? void 0 : profileString(profile, ["email"])?.toLowerCase()) ?? null,
|
|
27477
27567
|
planName,
|
|
27478
27568
|
planLevel,
|
|
27479
27569
|
region: credentials.region,
|
|
@@ -31991,6 +32081,46 @@ function processLine(line, state, wire) {
|
|
|
31991
32081
|
function kimiCodePoolPath() {
|
|
31992
32082
|
return path.join(dshHomeDir(), "storages", "kimi-code-pool.json");
|
|
31993
32083
|
}
|
|
32084
|
+
/**
|
|
32085
|
+
* Aliases that name nobody: the placeholder this pool mints at sign-in.
|
|
32086
|
+
*
|
|
32087
|
+
* `defaultAlias` writes `账号 N` when the credential carried neither an address
|
|
32088
|
+
* nor a name (a phone-signed-in account carries neither), and the legacy
|
|
32089
|
+
* single-credential mirror wrote `主账号`. Both mean "no name learned yet", so
|
|
32090
|
+
* both may be upgraded once the profile reports one — while anything else is a
|
|
32091
|
+
* name someone chose or an address already on the card, and stays put.
|
|
32092
|
+
*/
|
|
32093
|
+
const PLACEHOLDER_ALIAS = /^(?:账号|帐号|account)\s*\d*$|^主账号$|^primary$/i;
|
|
32094
|
+
/**
|
|
32095
|
+
* Whether an alias is still the placeholder a sign-in mints, rather than a name
|
|
32096
|
+
* somebody recognises.
|
|
32097
|
+
*
|
|
32098
|
+
* Exported because the routes decide whether one more profile read is worth it:
|
|
32099
|
+
* a row still titled `账号 N` is exactly the row whose name nobody has learned.
|
|
32100
|
+
*/
|
|
32101
|
+
function isPlaceholderAlias(alias) {
|
|
32102
|
+
return PLACEHOLDER_ALIAS.test(alias.trim());
|
|
32103
|
+
}
|
|
32104
|
+
/** A field worth storing, or undefined when the service left it blank. */
|
|
32105
|
+
function learnedField(value) {
|
|
32106
|
+
const text = (value ?? "").trim();
|
|
32107
|
+
return text === "" ? void 0 : text;
|
|
32108
|
+
}
|
|
32109
|
+
/**
|
|
32110
|
+
* The alias one sign-in should leave the row with.
|
|
32111
|
+
*
|
|
32112
|
+
* A fresh sign-in computes one from the credential it just produced, and that is
|
|
32113
|
+
* the truth when it names something (an address, or a nickname in the token). It
|
|
32114
|
+
* names nothing for a phone account, though, while the row may already carry the
|
|
32115
|
+
* nickname a profile read learned or a title the user typed — dropping those
|
|
32116
|
+
* back to `账号 N` on a re-sign-in would undo both. So a minted placeholder only
|
|
32117
|
+
* beats another placeholder; anything that names somebody is kept.
|
|
32118
|
+
*/
|
|
32119
|
+
function aliasForSignIn(minted, previous) {
|
|
32120
|
+
if (!isPlaceholderAlias(minted)) return minted;
|
|
32121
|
+
if (previous !== void 0 && !isPlaceholderAlias(previous)) return previous;
|
|
32122
|
+
return minted;
|
|
32123
|
+
}
|
|
31994
32124
|
function optionalString$11(record, key) {
|
|
31995
32125
|
const value = record[key];
|
|
31996
32126
|
if (value === void 0 || value === null) return void 0;
|
|
@@ -32089,9 +32219,9 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
|
|
|
32089
32219
|
parsePoolData: parseKimiCodePoolData,
|
|
32090
32220
|
dedupeKey: (credentials) => credentials.userId ?? credentials.email,
|
|
32091
32221
|
defaultAlias: (credentials, position) => credentials.email ?? credentials.nickname ?? `账号 ${position}`,
|
|
32092
|
-
createAccount: ({ id, alias, credentials, addedAt, isPrimary }) => ({
|
|
32222
|
+
createAccount: ({ id, alias, credentials, addedAt, isPrimary, existing }) => ({
|
|
32093
32223
|
id,
|
|
32094
|
-
alias,
|
|
32224
|
+
alias: aliasForSignIn(alias, existing?.alias),
|
|
32095
32225
|
credentials,
|
|
32096
32226
|
addedAt,
|
|
32097
32227
|
isPrimary,
|
|
@@ -32183,6 +32313,47 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
|
|
|
32183
32313
|
mirrorStore() {
|
|
32184
32314
|
return this.store;
|
|
32185
32315
|
}
|
|
32316
|
+
/**
|
|
32317
|
+
* Remember who the service says one pooled account is.
|
|
32318
|
+
*
|
|
32319
|
+
* Sign-in can only name an account through its token, which carries an id and
|
|
32320
|
+
* — for an account with a mailbox — an address. That is why a phone-signed-in
|
|
32321
|
+
* row is titled `账号 N` and never changes on its own: the nickname lives on
|
|
32322
|
+
* `/me`, which only the quota read asks, and nothing used to carry the answer
|
|
32323
|
+
* back to the row. Every reading now calls this with what the profile said, so
|
|
32324
|
+
* the card can name its own account without a re-sign-in.
|
|
32325
|
+
*
|
|
32326
|
+
* Only the placeholder yields: a name someone chose stays, because silently
|
|
32327
|
+
* renaming a row a user named is worse than leaving a default in place. The
|
|
32328
|
+
* learned facts also go into the mirrored credential, so a later re-sign-in
|
|
32329
|
+
* (which mints the alias from the credential) keeps the name, and so do the
|
|
32330
|
+
* single-account fallbacks that read that copy.
|
|
32331
|
+
*/
|
|
32332
|
+
async learnAccount(accountId, account) {
|
|
32333
|
+
const nickname = learnedField(account.nickname);
|
|
32334
|
+
const email = learnedField(account.email)?.toLowerCase();
|
|
32335
|
+
const userId = learnedField(account.userId);
|
|
32336
|
+
const planName = learnedField(account.planName);
|
|
32337
|
+
if (nickname === void 0 && email === void 0 && userId === void 0 && planName === void 0) return;
|
|
32338
|
+
await this.updatePool((data) => {
|
|
32339
|
+
const target = data.accounts.find((entry) => entry.id === accountId);
|
|
32340
|
+
if (target === void 0) return;
|
|
32341
|
+
if (nickname !== void 0) target.nickname = nickname;
|
|
32342
|
+
if (email !== void 0) target.email = email;
|
|
32343
|
+
if (userId !== void 0) target.userId = userId;
|
|
32344
|
+
if (planName !== void 0) target.planName = planName;
|
|
32345
|
+
const next = { ...target.credentials };
|
|
32346
|
+
if (nickname !== void 0) next.nickname = nickname;
|
|
32347
|
+
if (email !== void 0) next.email = email;
|
|
32348
|
+
if (userId !== void 0) next.userId = userId;
|
|
32349
|
+
if (planName !== void 0) next.planName = planName;
|
|
32350
|
+
target.credentials = next;
|
|
32351
|
+
if (isPlaceholderAlias(target.alias)) {
|
|
32352
|
+
const name = nickname ?? email;
|
|
32353
|
+
if (name !== void 0) target.alias = name;
|
|
32354
|
+
}
|
|
32355
|
+
});
|
|
32356
|
+
}
|
|
32186
32357
|
};
|
|
32187
32358
|
//#endregion
|
|
32188
32359
|
//#region src/host/kimi-code/routes.ts
|
|
@@ -32426,6 +32597,57 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
|
|
|
32426
32597
|
return (_credentials) => pool.renewCredential(accountId, fetchFn);
|
|
32427
32598
|
};
|
|
32428
32599
|
/**
|
|
32600
|
+
* Learning seam for the quota fetch, bound to one pool account.
|
|
32601
|
+
*
|
|
32602
|
+
* The reading is the only thing that asks `/me`, so it is also the only place
|
|
32603
|
+
* the account's nickname can be found. The pool owns the row, so the facts go
|
|
32604
|
+
* there: without this the card stayed titled `账号 N` forever, because nothing
|
|
32605
|
+
* ever carried the profile back to the account it describes.
|
|
32606
|
+
*/
|
|
32607
|
+
const learnFor = (accountId) => {
|
|
32608
|
+
const pool = accountPool;
|
|
32609
|
+
if (pool === void 0 || accountId === void 0) return void 0;
|
|
32610
|
+
return (account) => pool.learnAccount(accountId, account);
|
|
32611
|
+
};
|
|
32612
|
+
/**
|
|
32613
|
+
* Accounts whose name has already been asked for, once per process.
|
|
32614
|
+
*
|
|
32615
|
+
* A row titled `账号 N` needs exactly one `/me`; retrying on every status poll
|
|
32616
|
+
* (the card asks every minute) would spend a request a minute on an account
|
|
32617
|
+
* whose profile simply has no nickname to give.
|
|
32618
|
+
*/
|
|
32619
|
+
const nameProbes = /* @__PURE__ */ new Set();
|
|
32620
|
+
/**
|
|
32621
|
+
* One profile read for a row still carrying the sign-in placeholder.
|
|
32622
|
+
*
|
|
32623
|
+
* The quota read is the natural place to learn a name, but it happens only
|
|
32624
|
+
* when the snapshot is stale — and not at all while the usage call is failing
|
|
32625
|
+
* — so a fresh cache could hold the card at `账号 N` for as long as the TTL
|
|
32626
|
+
* lasts. This reads `/me` with the token the status call already holds, once
|
|
32627
|
+
* per account, and reports whether the row was renamed.
|
|
32628
|
+
*/
|
|
32629
|
+
const probeName = async (target, rows) => {
|
|
32630
|
+
const pool = accountPool;
|
|
32631
|
+
if (pool === void 0 || target.id === void 0) return false;
|
|
32632
|
+
const row = rows.find((entry) => entry.id === target.id);
|
|
32633
|
+
if (row === void 0 || !isPlaceholderAlias(row.alias)) return false;
|
|
32634
|
+
if (nameProbes.has(target.id)) return false;
|
|
32635
|
+
nameProbes.add(target.id);
|
|
32636
|
+
try {
|
|
32637
|
+
const credentials = await effectiveCredentials(target);
|
|
32638
|
+
if (credentials === void 0) return false;
|
|
32639
|
+
const profile = await fetchProfile(store, {
|
|
32640
|
+
fetchFn,
|
|
32641
|
+
credentials
|
|
32642
|
+
});
|
|
32643
|
+
if (profile === null) return false;
|
|
32644
|
+
await pool.learnAccount(target.id, accountFromCredentials$1(credentials, void 0, profile));
|
|
32645
|
+
return true;
|
|
32646
|
+
} catch {
|
|
32647
|
+
return false;
|
|
32648
|
+
}
|
|
32649
|
+
};
|
|
32650
|
+
/**
|
|
32429
32651
|
* The credential the active account should be used with right now.
|
|
32430
32652
|
*
|
|
32431
32653
|
* Every card path goes through the pool here. A pool owns both the rotation
|
|
@@ -32467,17 +32689,20 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
|
|
|
32467
32689
|
...accountPool === void 0 ? {} : {
|
|
32468
32690
|
credentials,
|
|
32469
32691
|
accountId: poolActive.id,
|
|
32470
|
-
renewFn: renewFor(poolActive.id)
|
|
32692
|
+
renewFn: renewFor(poolActive.id),
|
|
32693
|
+
onAccount: learnFor(poolActive.id)
|
|
32471
32694
|
}
|
|
32472
32695
|
});
|
|
32473
32696
|
};
|
|
32474
32697
|
if (cached === null) await quotaRefresh.run(refresh);
|
|
32475
32698
|
else quotaRefresh.start(refresh);
|
|
32476
32699
|
}
|
|
32700
|
+
let value = await readStatus(url.searchParams.get("sessionId") ?? void 0);
|
|
32701
|
+
if (await probeName(poolActive, value.accounts ?? [])) value = await readStatus(url.searchParams.get("sessionId") ?? void 0);
|
|
32477
32702
|
return sendJson$8(response, 200, {
|
|
32478
32703
|
ok: true,
|
|
32479
32704
|
value: {
|
|
32480
|
-
...
|
|
32705
|
+
...value,
|
|
32481
32706
|
quotaError: quotaRefresh.lastError(),
|
|
32482
32707
|
quotaRefreshing: quotaRefresh.refreshing
|
|
32483
32708
|
}
|
|
@@ -32497,6 +32722,13 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
|
|
|
32497
32722
|
fetchFn,
|
|
32498
32723
|
region: requested,
|
|
32499
32724
|
openBrowser: options.openBrowser,
|
|
32725
|
+
enrich: async (credentials) => {
|
|
32726
|
+
const profile = await fetchProfile(store, {
|
|
32727
|
+
fetchFn,
|
|
32728
|
+
credentials
|
|
32729
|
+
}).catch(() => null);
|
|
32730
|
+
return profile === null ? credentials : credentialsWithProfile(credentials, profile);
|
|
32731
|
+
},
|
|
32500
32732
|
...accountPool === void 0 ? {} : { onSave: (credentials) => accountPool.addAccount(credentials) }
|
|
32501
32733
|
})
|
|
32502
32734
|
});
|
|
@@ -32536,7 +32768,8 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
|
|
|
32536
32768
|
...accountPool === void 0 ? {} : {
|
|
32537
32769
|
credentials: await effectiveCredentials(poolActive),
|
|
32538
32770
|
accountId: poolActive.id,
|
|
32539
|
-
renewFn: renewFor(poolActive.id)
|
|
32771
|
+
renewFn: renewFor(poolActive.id),
|
|
32772
|
+
onAccount: learnFor(poolActive.id)
|
|
32540
32773
|
}
|
|
32541
32774
|
});
|
|
32542
32775
|
return sendJson$8(response, 200, {
|
|
@@ -32566,7 +32799,8 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
|
|
|
32566
32799
|
...accountPool === void 0 ? {} : {
|
|
32567
32800
|
credentials: await effectiveCredentials(quotaActive),
|
|
32568
32801
|
accountId: quotaActive.id,
|
|
32569
|
-
renewFn: renewFor(quotaActive.id)
|
|
32802
|
+
renewFn: renewFor(quotaActive.id),
|
|
32803
|
+
onAccount: learnFor(quotaActive.id)
|
|
32570
32804
|
}
|
|
32571
32805
|
}) === null) return sendJson$8(response, 502, {
|
|
32572
32806
|
ok: false,
|
|
@@ -65296,14 +65530,14 @@ function publishedCatalogMeta(store, id) {
|
|
|
65296
65530
|
function formatModelHint$1(contextWindow, input, reasoning) {
|
|
65297
65531
|
return formatModelCapacity$1(contextWindow) + " · " + input.join("+") + (reasoning ? " · reasoning" : "");
|
|
65298
65532
|
}
|
|
65299
|
-
function uniqueModels
|
|
65533
|
+
function uniqueModels(settings) {
|
|
65300
65534
|
const ids = new Set(DEFAULT_MODELS);
|
|
65301
65535
|
for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
|
|
65302
65536
|
return [...ids];
|
|
65303
65537
|
}
|
|
65304
65538
|
/** Every model the card should render, whether or not it is advertised. */
|
|
65305
65539
|
function projectModels$1(settings, store) {
|
|
65306
|
-
const known = uniqueModels
|
|
65540
|
+
const known = uniqueModels(settings);
|
|
65307
65541
|
const hidden = new Set(settings.hiddenModels);
|
|
65308
65542
|
const enabledSet = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
|
|
65309
65543
|
return known.map((id) => {
|
|
@@ -65959,7 +66193,7 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
65959
66193
|
});
|
|
65960
66194
|
const body = await readRequestJson$2(request).catch(() => ({}));
|
|
65961
66195
|
const current = await settingsStore.read();
|
|
65962
|
-
const known = uniqueModels
|
|
66196
|
+
const known = uniqueModels(current);
|
|
65963
66197
|
const hidden = new Set(current.hiddenModels);
|
|
65964
66198
|
if (path === "models/visibility") {
|
|
65965
66199
|
const model = typeof body.model === "string" ? body.model.trim() : "";
|
|
@@ -69668,11 +69902,15 @@ function normalizeQoderQuotaSnapshot(value, region, fetchedAt = Date.now()) {
|
|
|
69668
69902
|
if (addOn !== void 0) buckets.push(addOn);
|
|
69669
69903
|
const org = normalizeQoderBucket(value.orgResourcePackage, "org");
|
|
69670
69904
|
if (org !== void 0) buckets.push(org);
|
|
69671
|
-
|
|
69905
|
+
const quotaExceeded = value.isQuotaExceeded === true;
|
|
69906
|
+
const upgradeUrl = asText$1(value.upgradeUrl) ?? null;
|
|
69907
|
+
if (buckets.length === 0 && !quotaExceeded) return void 0;
|
|
69672
69908
|
const expiresRaw = asQuotaNumber(value.expiresAt) ?? null;
|
|
69673
69909
|
return {
|
|
69674
69910
|
buckets,
|
|
69675
69911
|
expiresAt: expiresRaw !== null && expiresRaw <= 864e13 ? expiresRaw : null,
|
|
69912
|
+
quotaExceeded,
|
|
69913
|
+
upgradeUrl,
|
|
69676
69914
|
fetchedAt,
|
|
69677
69915
|
region
|
|
69678
69916
|
};
|
|
@@ -70410,7 +70648,7 @@ function processQoderStreamLine(line, state) {
|
|
|
70410
70648
|
const envelope = safeJsonParse(payload);
|
|
70411
70649
|
if (!isRecord$6(envelope)) return [];
|
|
70412
70650
|
const statusCode = optionalNumber(envelope.statusCodeValue);
|
|
70413
|
-
if (statusCode !== void 0 && statusCode !== 200) throw
|
|
70651
|
+
if (statusCode !== void 0 && statusCode !== 200) throw classifyQoderEnvelopeFailure(statusCode, envelope);
|
|
70414
70652
|
const innerStr = asString$2(envelope.body);
|
|
70415
70653
|
if (innerStr === "[DONE]") {
|
|
70416
70654
|
state.done = true;
|
|
@@ -70421,7 +70659,11 @@ function processQoderStreamLine(line, state) {
|
|
|
70421
70659
|
if (!isRecord$6(inner)) return [];
|
|
70422
70660
|
const out = [];
|
|
70423
70661
|
const errorPayload = isRecord$6(inner.error) ? inner.error : void 0;
|
|
70424
|
-
if (errorPayload !== void 0)
|
|
70662
|
+
if (errorPayload !== void 0) {
|
|
70663
|
+
const innerFacts = qoderFailureFacts([errorPayload]);
|
|
70664
|
+
if (isQoderQuotaExhausted(innerFacts, void 0) || isQoderResourceLimited(innerFacts, void 0)) throw classifyQoderFailureFacts(void 0, "", innerFacts);
|
|
70665
|
+
throw new LlmError$1("Qoder(订阅) stream error: " + (asString$2(errorPayload.message) ?? "unknown error"), isContextOverflow(errorPayload) ? CONTEXT_OVERFLOW_CODE : "PROVIDER_ERROR");
|
|
70666
|
+
}
|
|
70425
70667
|
const usage = parseQoderUsage(inner.usage);
|
|
70426
70668
|
if (usage !== void 0) {
|
|
70427
70669
|
state.sawUsage = true;
|
|
@@ -70496,32 +70738,196 @@ function assertQoderStreamComplete(state) {
|
|
|
70496
70738
|
}
|
|
70497
70739
|
/** 错误体里引用回消息的最大长度。 */
|
|
70498
70740
|
const MAX_FAILURE_DETAIL_LENGTH = 600;
|
|
70499
|
-
/**
|
|
70500
|
-
|
|
70501
|
-
|
|
70502
|
-
|
|
70503
|
-
|
|
70504
|
-
|
|
70741
|
+
/** 事实扫描的深度与节点上限:上游的错误体很小,但别让它变成一次无界的走树。 */
|
|
70742
|
+
const FAILURE_FACT_MAX_DEPTH = 6;
|
|
70743
|
+
const FAILURE_FACT_MAX_NODES = 200;
|
|
70744
|
+
/** 一个码字段的值(字符串或数字)转成字符串。 */
|
|
70745
|
+
function qoderCodeText(value) {
|
|
70746
|
+
if (typeof value === "string") {
|
|
70747
|
+
const trimmed = value.trim();
|
|
70748
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
70749
|
+
}
|
|
70750
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
70751
|
+
}
|
|
70752
|
+
/** 一个 URL 字段的值。 */
|
|
70753
|
+
function qoderUrlText(value) {
|
|
70754
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
70755
|
+
}
|
|
70756
|
+
/**
|
|
70757
|
+
* 扫一遍上游的失败载荷,把认得的字段收成事实。
|
|
70758
|
+
*
|
|
70759
|
+
* 为什么要扫而不是只取顶层:上游把这几个字段放在**不同层**(信封外层 / body /
|
|
70760
|
+
* body 里的 error 对象),而码是**一条链**(403 → 10605)。逐层找比猜位置可靠:
|
|
70761
|
+
* 猜错位置就等于把「排队」读成「凭据被拒」。
|
|
70762
|
+
*/
|
|
70763
|
+
function qoderFailureFacts(roots) {
|
|
70764
|
+
const facts = {
|
|
70765
|
+
codes: [],
|
|
70766
|
+
subCodes: []
|
|
70767
|
+
};
|
|
70768
|
+
const budget = { nodes: FAILURE_FACT_MAX_NODES };
|
|
70769
|
+
for (const root of roots) collectQoderFailureFacts(root, facts, 0, budget);
|
|
70770
|
+
return facts;
|
|
70771
|
+
}
|
|
70772
|
+
function collectQoderFailureFacts(node, facts, depth, budget) {
|
|
70773
|
+
if (depth > FAILURE_FACT_MAX_DEPTH || budget.nodes <= 0) return;
|
|
70774
|
+
if (Array.isArray(node)) {
|
|
70775
|
+
for (const item of node) collectQoderFailureFacts(item, facts, depth + 1, budget);
|
|
70776
|
+
return;
|
|
70777
|
+
}
|
|
70778
|
+
if (!isRecord$6(node)) return;
|
|
70779
|
+
budget.nodes -= 1;
|
|
70780
|
+
for (const [key, value] of Object.entries(node)) {
|
|
70781
|
+
switch (key) {
|
|
70782
|
+
case "code":
|
|
70783
|
+
case "errorCode":
|
|
70784
|
+
case "error_code": {
|
|
70785
|
+
const text = qoderCodeText(value);
|
|
70786
|
+
if (text !== void 0) facts.codes.push(text);
|
|
70787
|
+
break;
|
|
70788
|
+
}
|
|
70789
|
+
case "subCode":
|
|
70790
|
+
case "sub_code": {
|
|
70791
|
+
const text = qoderCodeText(value);
|
|
70792
|
+
if (text !== void 0) facts.subCodes.push(text);
|
|
70793
|
+
break;
|
|
70794
|
+
}
|
|
70795
|
+
case "queueCount":
|
|
70796
|
+
case "queue_count":
|
|
70797
|
+
facts.queueCount ??= optionalNumber(value);
|
|
70798
|
+
break;
|
|
70799
|
+
case "retryAfterSeconds":
|
|
70800
|
+
case "retry_after_seconds":
|
|
70801
|
+
facts.retryAfterSeconds ??= optionalNumber(value);
|
|
70802
|
+
break;
|
|
70803
|
+
case "serviceAvailable":
|
|
70804
|
+
case "service_available":
|
|
70805
|
+
if (typeof value === "boolean") facts.serviceAvailable ??= value;
|
|
70806
|
+
break;
|
|
70807
|
+
case "isQueued":
|
|
70808
|
+
case "is_queued":
|
|
70809
|
+
if (typeof value === "boolean") facts.isQueued ??= value;
|
|
70810
|
+
break;
|
|
70811
|
+
case "modelKey":
|
|
70812
|
+
case "model_key":
|
|
70813
|
+
facts.modelKey ??= qoderCodeText(value);
|
|
70814
|
+
break;
|
|
70815
|
+
case "pricingUrl":
|
|
70816
|
+
case "pricing_url":
|
|
70817
|
+
facts.pricingUrl ??= qoderUrlText(value);
|
|
70818
|
+
break;
|
|
70819
|
+
case "upgradeUrl":
|
|
70820
|
+
case "upgrade_url":
|
|
70821
|
+
facts.upgradeUrl ??= qoderUrlText(value);
|
|
70822
|
+
break;
|
|
70823
|
+
case "isQuotaExceeded":
|
|
70824
|
+
case "is_quota_exceeded":
|
|
70825
|
+
if (typeof value === "boolean") facts.quotaExceeded ??= value;
|
|
70826
|
+
break;
|
|
70827
|
+
default: break;
|
|
70828
|
+
}
|
|
70829
|
+
collectQoderFailureFacts(value, facts, depth + 1, budget);
|
|
70830
|
+
}
|
|
70831
|
+
}
|
|
70832
|
+
/**
|
|
70833
|
+
* 上游是不是在说「资源不足 / 排队中」。
|
|
70834
|
+
*
|
|
70835
|
+
* 认两种凭据:上游**点名**的(\`RESOURCE_LIMIT\` / 10605)与 403 搭配的容量信号
|
|
70836
|
+
* (服务不可用 / 这次在排队 / 前面排了人)。点名的凭据压过状态码——429 带上
|
|
70837
|
+
* RESOURCE_LIMIT 说的是「上游没容量」,不是「这个账号被限流」(与 kimi-code 把
|
|
70838
|
+
* 「引擎过载」从 429 里分出来是同一判断);只有排队人数这种弱信号时,429 的含义
|
|
70839
|
+
* 不变,号池照旧冷却账号并轮转。
|
|
70840
|
+
*/
|
|
70841
|
+
function isQoderResourceLimited(facts, status) {
|
|
70842
|
+
if (facts.subCodes.some((code) => code.toUpperCase() === "RESOURCE_LIMIT")) return true;
|
|
70843
|
+
if (facts.codes.includes("10605")) return true;
|
|
70844
|
+
if (status !== 403) return false;
|
|
70845
|
+
return facts.serviceAvailable === false || facts.isQueued === true || facts.queueCount !== void 0 && facts.queueCount > 0;
|
|
70846
|
+
}
|
|
70847
|
+
/**
|
|
70848
|
+
* 上游是不是在说「额度用尽」。
|
|
70849
|
+
*
|
|
70850
|
+
* 认两种凭据:业务码 112,以及 403 搭配的额度信号(\`isQuotaExceeded\` / 购买页)。
|
|
70851
|
+
* 额度用尽不是「这个账号坏了」,所以它归 QUOTA 而不是 AUTH——它只能等周期重置或加购。
|
|
70852
|
+
*/
|
|
70853
|
+
function isQoderQuotaExhausted(facts, status) {
|
|
70854
|
+
if (facts.codes.includes("112")) return true;
|
|
70855
|
+
if (status !== 403) return false;
|
|
70856
|
+
return facts.quotaExceeded === true || facts.pricingUrl !== void 0;
|
|
70857
|
+
}
|
|
70858
|
+
/** 上游排队 / 资源不足的文案:说清「不是凭据问题」,并带上上游给的排队事实。 */
|
|
70859
|
+
function qoderResourceLimitMessage(facts, status) {
|
|
70860
|
+
const details = [];
|
|
70861
|
+
const codes = [...facts.subCodes, ...facts.codes].filter((code) => code !== "" && code !== String(status));
|
|
70862
|
+
if (codes.length > 0) details.push("code " + codes.join(" / "));
|
|
70863
|
+
if (facts.queueCount !== void 0) details.push("队列 " + facts.queueCount + " 人");
|
|
70864
|
+
if (facts.retryAfterSeconds !== void 0) details.push("上游建议 " + facts.retryAfterSeconds + " 秒后再试");
|
|
70865
|
+
if (facts.modelKey !== void 0) details.push("modelKey " + facts.modelKey);
|
|
70866
|
+
return "Qoder(订阅) 上游资源不足,这个模型正在排队" + (details.length === 0 ? "" : ":" + details.join(",")) + "。这是上游容量问题,不是凭据问题——账号仍是登录状态,不需要重新登录;稍后重试即可。";
|
|
70867
|
+
}
|
|
70868
|
+
/** 上游额度用尽的文案:额度恢复前这一请求不会成功,并给出上游给的购买/升级页。 */
|
|
70869
|
+
function qoderQuotaExhaustedMessage(facts, status) {
|
|
70870
|
+
const details = [];
|
|
70871
|
+
const codes = facts.codes.filter((code) => code !== "" && code !== String(status));
|
|
70872
|
+
if (codes.length > 0) details.push("code " + codes.join(" / "));
|
|
70873
|
+
const url = facts.pricingUrl ?? facts.upgradeUrl;
|
|
70874
|
+
if (url !== void 0) details.push(url);
|
|
70875
|
+
return "Qoder(订阅) 额度已用尽,当前请求被上游拒绝" + (details.length === 0 ? "" : ":" + details.join(",")) + "。额度恢复之前这一请求不会成功;可以等周期重置,或在额度页升级套餐。";
|
|
70505
70876
|
}
|
|
70506
70877
|
/**
|
|
70507
70878
|
* 把一次失败(非 2xx 响应,或信封里的错误状态)分类成 DSH failure。
|
|
70508
70879
|
*
|
|
70509
70880
|
* 错误码是 harness 的共享词汇而不是本线路自己的:号池认得 \`AUTH\`
|
|
70510
70881
|
* (「这个账号必须重新登录」),而重试集合不会捡起一次套餐拒绝。
|
|
70511
|
-
* Qoder
|
|
70512
|
-
*
|
|
70882
|
+
* Qoder 的业务码各有归处:
|
|
70883
|
+
*
|
|
70884
|
+
* - \`103\`(重复 request_id)不是可安全自动重试的状态(qodercli2api §8 原话);
|
|
70885
|
+
* - \`105\`(Login expired)归 AUTH;
|
|
70886
|
+
* - \`10605\` / \`RESOURCE_LIMIT\`(上游排队、资源不足)归 **SERVER**——它描述的是
|
|
70887
|
+
* 上游容量,换账号与重新登录都救不了,退避重试才有意义(与 kimi-code 的
|
|
70888
|
+
* 「引擎过载」同一判断:过载不是账号被限流,所以不借 RATE_LIMIT 的语义);
|
|
70889
|
+
* - \`112\`(额度用尽,带 pricingUrl)归 **QUOTA**——额度恢复前这一请求不可能成功,
|
|
70890
|
+
* 而 DSH 正是用这个码显示「当前请求的额度已用尽」。
|
|
70891
|
+
*
|
|
70892
|
+
* 剩下认不出原因的 403 仍然归 AUTH:那时「凭据被拒」是上游最常见的那一种。
|
|
70893
|
+
* 但把一次排队误报成「API 密钥无效」会让人去重建一把根本没坏的令牌,
|
|
70894
|
+
* 所以上面两条必须先于它判掉。
|
|
70513
70895
|
*/
|
|
70514
70896
|
function classifyQoderFailure(status, detail) {
|
|
70515
|
-
|
|
70516
|
-
|
|
70517
|
-
|
|
70518
|
-
|
|
70519
|
-
|
|
70520
|
-
|
|
70521
|
-
|
|
70522
|
-
|
|
70523
|
-
|
|
70524
|
-
|
|
70897
|
+
return classifyQoderFailureFacts(status, detail.trim().slice(0, MAX_FAILURE_DETAIL_LENGTH), qoderFailureFacts([safeJsonParse(detail)]));
|
|
70898
|
+
}
|
|
70899
|
+
/**
|
|
70900
|
+
* 信封路径的失败分类。
|
|
70901
|
+
*
|
|
70902
|
+
* 上游把排队人数、\`subCode\` 这些事实放在**信封层**还是 body 里都可能(body 本身
|
|
70903
|
+
* 还是一段 JSON 字符串,要再解析一次),所以两处都扫:只读 body 会把「排队」读成
|
|
70904
|
+
* 「凭据被拒」。
|
|
70905
|
+
*/
|
|
70906
|
+
function classifyQoderEnvelopeFailure(status, envelope) {
|
|
70907
|
+
const bodyText = asString$2(envelope.body) ?? "";
|
|
70908
|
+
return classifyQoderFailureFacts(status, bodyText.trim().slice(0, MAX_FAILURE_DETAIL_LENGTH), qoderFailureFacts([envelope, safeJsonParse(bodyText)]));
|
|
70909
|
+
}
|
|
70910
|
+
/** 事实齐了以后的那次判定(AUTH / QUOTA / SERVER 的先后是硬要求)。 */
|
|
70911
|
+
function classifyQoderFailureFacts(status, detail, facts) {
|
|
70912
|
+
const suffix = detail === "" ? "" : ": " + detail;
|
|
70913
|
+
const code = facts.codes.length > 0 ? facts.codes[0] : void 0;
|
|
70914
|
+
const options = status === void 0 ? {} : { status };
|
|
70915
|
+
if (status === 401 || facts.codes.includes("105")) return new LlmError$1("Qoder(订阅) rejected the credential (" + (status ?? "no status") + (code === void 0 ? "" : ", code " + code) + "). Sign the account in again." + suffix, "AUTH", options);
|
|
70916
|
+
if (isQoderQuotaExhausted(facts, status)) return new LlmError$1(qoderQuotaExhaustedMessage(facts, status) + suffix, "QUOTA", options);
|
|
70917
|
+
if (isQoderResourceLimited(facts, status)) {
|
|
70918
|
+
const after = facts.retryAfterSeconds;
|
|
70919
|
+
return new LlmError$1(qoderResourceLimitMessage(facts, status) + suffix, "SERVER", after === void 0 || after <= 0 ? options : {
|
|
70920
|
+
...options,
|
|
70921
|
+
providerRetryAfterMs: after * 1e3
|
|
70922
|
+
});
|
|
70923
|
+
}
|
|
70924
|
+
if (status === 403) return new LlmError$1("Qoder(订阅) rejected the credential (" + status + (code === void 0 ? "" : ", code " + code) + "). Sign the account in again." + suffix, "AUTH", options);
|
|
70925
|
+
if (code === "103") return new LlmError$1("Qoder(订阅) rejected a duplicated request id (code 103)." + suffix, "PROVIDER_ERROR", options);
|
|
70926
|
+
if (status === 429) return new LlmError$1("Qoder(订阅) rate limit or plan limit (" + status + ")." + suffix, "RATE_LIMIT", options);
|
|
70927
|
+
if (status !== void 0 && isHttpContextOverflow(status, detail)) return new LlmError$1("Qoder(订阅) says the conversation no longer fits the model window." + suffix, CONTEXT_OVERFLOW_CODE, options);
|
|
70928
|
+
if (status === 404) return new LlmError$1("Qoder(订阅) does not know this model or route (" + status + ")." + suffix, "NOT_FOUND", options);
|
|
70929
|
+
if (status !== void 0 && status >= 500) return new LlmError$1("Qoder(订阅) upstream error (" + status + ")." + suffix, "SERVER", options);
|
|
70930
|
+
return new LlmError$1("Qoder(订阅) request failed (" + (status ?? "no status") + ")." + suffix, "PROVIDER_ERROR", options);
|
|
70525
70931
|
}
|
|
70526
70932
|
//#endregion
|
|
70527
70933
|
//#region src/host/qoder/adapter.ts
|
|
@@ -73021,8 +73427,7 @@ const MODEL_CATALOG$1 = Object.freeze({
|
|
|
73021
73427
|
output: .28
|
|
73022
73428
|
},
|
|
73023
73429
|
contextWindow: 1048576,
|
|
73024
|
-
input: ["text", "image"]
|
|
73025
|
-
reasoning: false
|
|
73430
|
+
input: ["text", "image"]
|
|
73026
73431
|
},
|
|
73027
73432
|
"mimo-v2.6-pro": {
|
|
73028
73433
|
name: "MiMo V2.6 Pro",
|
|
@@ -73031,8 +73436,7 @@ const MODEL_CATALOG$1 = Object.freeze({
|
|
|
73031
73436
|
output: .87
|
|
73032
73437
|
},
|
|
73033
73438
|
contextWindow: 1048576,
|
|
73034
|
-
input: ["text", "image"]
|
|
73035
|
-
reasoning: false
|
|
73439
|
+
input: ["text", "image"]
|
|
73036
73440
|
},
|
|
73037
73441
|
"minimax-m3.1-flash": {
|
|
73038
73442
|
name: "MiniMax M3.1 Flash",
|
|
@@ -73076,8 +73480,7 @@ const MODEL_CATALOG$1 = Object.freeze({
|
|
|
73076
73480
|
},
|
|
73077
73481
|
contextWindow: 1e6,
|
|
73078
73482
|
maxTokens: 65536,
|
|
73079
|
-
input: ["text"]
|
|
73080
|
-
reasoning: false
|
|
73483
|
+
input: ["text"]
|
|
73081
73484
|
}
|
|
73082
73485
|
});
|
|
73083
73486
|
/** Subscription models current at release time; the live catalog extends this. */
|
|
@@ -73099,6 +73502,96 @@ function catalogEntry$1(model) {
|
|
|
73099
73502
|
return MODEL_CATALOG$1[model];
|
|
73100
73503
|
}
|
|
73101
73504
|
//#endregion
|
|
73505
|
+
//#region src/host/opendesign/catalog.ts
|
|
73506
|
+
/**
|
|
73507
|
+
* Merge the shipped table, the last gateway listing and the user's per-model
|
|
73508
|
+
* override into the descriptor the harness validates.
|
|
73509
|
+
*
|
|
73510
|
+
* Context window and output cap are provider facts, not preferences: the harness
|
|
73511
|
+
* sizes its prompt budget against `context.contextWindow`, so an understated
|
|
73512
|
+
* window silently truncates history. This line ships no per-model windows (the
|
|
73513
|
+
* gateway publishes none to an API key), so the window is the line default until
|
|
73514
|
+
* a live listing or a user override states one.
|
|
73515
|
+
*
|
|
73516
|
+
* @module dsh-chatgpt-subscription/opendesign-catalog
|
|
73517
|
+
*/
|
|
73518
|
+
/** The wire's whole modality vocabulary; a listing may claim more than it accepts. */
|
|
73519
|
+
const SEAM_MODALITIES = /* @__PURE__ */ new Set(["text", "image"]);
|
|
73520
|
+
function definedOnly(value) {
|
|
73521
|
+
if (value === void 0) return {};
|
|
73522
|
+
const result = {};
|
|
73523
|
+
for (const [key, entry] of Object.entries(value)) if (entry !== void 0 && entry !== null) result[key] = entry;
|
|
73524
|
+
return result;
|
|
73525
|
+
}
|
|
73526
|
+
/**
|
|
73527
|
+
* The output budget to advertise for one model.
|
|
73528
|
+
*
|
|
73529
|
+
* `defaultMaxTokens` is what a request asks for, not what the model can emit,
|
|
73530
|
+
* and the harness spends it twice: it fills a request's `max_tokens` from it
|
|
73531
|
+
* and reserves the same number as completion room before deciding to compact.
|
|
73532
|
+
* So a published ceiling clamps the configured budget rather than becoming it,
|
|
73533
|
+
* and a budget is never taken past half the window.
|
|
73534
|
+
*/
|
|
73535
|
+
function resolveOutputBudget(budget, ceiling, contextWindow) {
|
|
73536
|
+
const wanted = Number.isFinite(budget) && (budget ?? 0) > 0 ? budget : void 0;
|
|
73537
|
+
const most = Number.isFinite(ceiling) && (ceiling ?? 0) > 0 ? ceiling : void 0;
|
|
73538
|
+
const chosen = wanted === void 0 ? most : most === void 0 ? wanted : Math.min(wanted, most);
|
|
73539
|
+
if (chosen === void 0) return void 0;
|
|
73540
|
+
const half = Number.isFinite(contextWindow) && contextWindow > 0 ? Math.floor(contextWindow / 2) : void 0;
|
|
73541
|
+
return half === void 0 ? chosen : Math.min(chosen, half);
|
|
73542
|
+
}
|
|
73543
|
+
/**
|
|
73544
|
+
* Whether the subscription is known to serve one model.
|
|
73545
|
+
*
|
|
73546
|
+
* `servedModelIds === null` is "nobody has ever measured this plan": the absence
|
|
73547
|
+
* of a fact is not a denial, so nothing is hidden on its strength and the shipped
|
|
73548
|
+
* table stands in exactly as it did before this fact existed. An array is a
|
|
73549
|
+
* measurement taken from a successful scan, and an id outside it is a model this
|
|
73550
|
+
* plan does not serve — it stays in the editor as a described model, it just stops
|
|
73551
|
+
* being advertised.
|
|
73552
|
+
*
|
|
73553
|
+
* The user's own override lives one layer up (`forceVisibleModels`): this function
|
|
73554
|
+
* answers what the plan said, never what the user decided.
|
|
73555
|
+
*/
|
|
73556
|
+
function isModelServed(model, servedModelIds) {
|
|
73557
|
+
return servedModelIds === null || servedModelIds.includes(model);
|
|
73558
|
+
}
|
|
73559
|
+
/**
|
|
73560
|
+
* Resolve the metadata one model is advertised with.
|
|
73561
|
+
*
|
|
73562
|
+
* Precedence per field: the user's override, then the last gateway listing,
|
|
73563
|
+
* then the shipped table, then the route-wide fallback. The listing sits above
|
|
73564
|
+
* the shipped table for the fields the table cannot carry (window, modalities):
|
|
73565
|
+
* the table's windows do not exist to be beaten, and its names/prices are
|
|
73566
|
+
* curated, so a listing that states no name keeps the table's.
|
|
73567
|
+
*/
|
|
73568
|
+
function resolveModelInfo(model, options) {
|
|
73569
|
+
const synced = options.synced.find((entry) => entry.id === model);
|
|
73570
|
+
const shipped = catalogEntry$1(model);
|
|
73571
|
+
const published = {
|
|
73572
|
+
...definedOnly(shipped),
|
|
73573
|
+
...definedOnly(synced)
|
|
73574
|
+
};
|
|
73575
|
+
const fallbackWindow = options.defaultContextWindow ?? 128e3;
|
|
73576
|
+
const contextWindow = options.contextWindowOverride ?? published.contextWindow ?? fallbackWindow;
|
|
73577
|
+
const maxTokens = resolveOutputBudget(options.defaultMaxTokens ?? 64e3, published.maxTokens, contextWindow);
|
|
73578
|
+
const reasoning = published.reasoning ?? shipped?.reasoning ?? true;
|
|
73579
|
+
const efforts = (Array.isArray(published.efforts) && published.efforts.length > 0 ? published.efforts : void 0) ?? (shipped?.efforts !== void 0 && shipped.efforts.length > 0 ? [...shipped.efforts] : DEFAULT_EFFORTS);
|
|
73580
|
+
const defaultEffort = reasoning && shipped?.defaultEffort !== void 0 && efforts.includes(shipped.defaultEffort) ? shipped.defaultEffort : void 0;
|
|
73581
|
+
const input = (Array.isArray(published.input) && published.input.length > 0 ? published.input : shipped?.input ?? ["text"]).filter((entry) => SEAM_MODALITIES.has(entry));
|
|
73582
|
+
return {
|
|
73583
|
+
id: model,
|
|
73584
|
+
displayName: String(published.name ?? "").trim() || model,
|
|
73585
|
+
contextWindow,
|
|
73586
|
+
maxTokens: maxTokens ?? options.defaultMaxTokens ?? 64e3,
|
|
73587
|
+
reasoning,
|
|
73588
|
+
...reasoning ? { efforts } : { efforts: [] },
|
|
73589
|
+
...defaultEffort === void 0 ? {} : { defaultEffort },
|
|
73590
|
+
input: input.length > 0 ? input : ["text"],
|
|
73591
|
+
...shipped?.pricing === void 0 ? {} : { pricing: shipped.pricing }
|
|
73592
|
+
};
|
|
73593
|
+
}
|
|
73594
|
+
//#endregion
|
|
73102
73595
|
//#region src/host/opendesign/token-store.ts
|
|
73103
73596
|
/** Whether one credential is a device-flow session rather than a pasted key. */
|
|
73104
73597
|
function isOpenDesignOAuthCredentials(credentials) {
|
|
@@ -73254,6 +73747,24 @@ function readCatalogModels(value) {
|
|
|
73254
73747
|
}
|
|
73255
73748
|
return models;
|
|
73256
73749
|
}
|
|
73750
|
+
/**
|
|
73751
|
+
* One stored id list, normalized: non-empty strings only, deduplicated, and
|
|
73752
|
+
* `null` when the value is not a list at all.
|
|
73753
|
+
*
|
|
73754
|
+
* `null` and `[]` stay distinct here; see the tri-state note on
|
|
73755
|
+
* {@link OpenDesignModelSettings.servedModelIds}.
|
|
73756
|
+
*/
|
|
73757
|
+
function readIdList(value) {
|
|
73758
|
+
if (!Array.isArray(value)) return null;
|
|
73759
|
+
const ids = [];
|
|
73760
|
+
for (const entry of value) {
|
|
73761
|
+
if (typeof entry !== "string") continue;
|
|
73762
|
+
const id = entry.trim();
|
|
73763
|
+
if (id === "" || ids.includes(id)) continue;
|
|
73764
|
+
ids.push(id);
|
|
73765
|
+
}
|
|
73766
|
+
return ids;
|
|
73767
|
+
}
|
|
73257
73768
|
function positiveInteger(value, fallback, allowZero = false) {
|
|
73258
73769
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) return fallback;
|
|
73259
73770
|
return value;
|
|
@@ -73268,6 +73779,8 @@ function parseOpenDesignModelSettings(value) {
|
|
|
73268
73779
|
enabledModelIds,
|
|
73269
73780
|
catalogModels: readCatalogModels(record.catalogModels),
|
|
73270
73781
|
hiddenModels: Array.isArray(record.hiddenModels) ? record.hiddenModels.filter((id) => typeof id === "string") : [],
|
|
73782
|
+
servedModelIds: readIdList(record.servedModelIds),
|
|
73783
|
+
forceVisibleModels: readIdList(record.forceVisibleModels) ?? [],
|
|
73271
73784
|
contextWindowOverrides: readNumberOverrides(record.contextWindowOverrides),
|
|
73272
73785
|
defaultContextWindow: positiveInteger(record.defaultContextWindow, DEFAULT_CONTEXT_WINDOW),
|
|
73273
73786
|
defaultMaxTokens: positiveInteger(record.defaultMaxTokens, DEFAULT_MAX_TOKENS),
|
|
@@ -73285,12 +73798,30 @@ function defaultOpenDesignModelSettings() {
|
|
|
73285
73798
|
*
|
|
73286
73799
|
* The file is written atomically (temp + rename) so a crash mid-write leaves the
|
|
73287
73800
|
* previous document intact rather than a truncated one that reads as "reset all
|
|
73288
|
-
* settings"
|
|
73801
|
+
* settings", and writes are SERIALIZED behind one queue so two read→compute→write
|
|
73802
|
+
* cycles can never interleave and erase each other's fields. Reads do not queue:
|
|
73803
|
+
* see {@link OpenDesignModelSettingsStore.pending}.
|
|
73289
73804
|
*/
|
|
73290
73805
|
var OpenDesignModelSettingsStore = class {
|
|
73291
73806
|
filePath;
|
|
73292
73807
|
cached = null;
|
|
73293
73808
|
loadPromise = null;
|
|
73809
|
+
/**
|
|
73810
|
+
* Tail of the write queue.
|
|
73811
|
+
*
|
|
73812
|
+
* Every mutator derives a WHOLE document from the one it read, so two of them in
|
|
73813
|
+
* flight at once each erase the other's fields. That is not hypothetical here:
|
|
73814
|
+
* the catalog sync is fire-and-forget (`void syncCatalog()` on account add and on
|
|
73815
|
+
* a completed sign-in), so it really can land between a selection change's read
|
|
73816
|
+
* and its write. Each mutation therefore runs its entire read → compute → write
|
|
73817
|
+
* inside one turn of this queue.
|
|
73818
|
+
*
|
|
73819
|
+
* `read()` stays OUTSIDE the queue on purpose: it is the hot path (every status
|
|
73820
|
+
* request, every keystroke-driven checkbox), and waiting behind a write would buy
|
|
73821
|
+
* nothing — by the time a queued operation runs, every write queued before it has
|
|
73822
|
+
* already landed, so the plain read it performs IS the in-lock read.
|
|
73823
|
+
*/
|
|
73824
|
+
pending = Promise.resolve();
|
|
73294
73825
|
constructor(filePath = modelSettingsPath$7()) {
|
|
73295
73826
|
this.filePath = filePath;
|
|
73296
73827
|
}
|
|
@@ -73300,7 +73831,14 @@ var OpenDesignModelSettingsStore = class {
|
|
|
73300
73831
|
status() {
|
|
73301
73832
|
return this.cached ?? defaultOpenDesignModelSettings();
|
|
73302
73833
|
}
|
|
73303
|
-
/**
|
|
73834
|
+
/**
|
|
73835
|
+
* Read the document at activation time, synchronously.
|
|
73836
|
+
*
|
|
73837
|
+
* Synchronous by contract, so it cannot join the write queue — and it does not
|
|
73838
|
+
* need to: it only READS, a rename makes the file either the old or the new
|
|
73839
|
+
* document (never a half-written one), and a queued write assigns `cached` after
|
|
73840
|
+
* that rename, so a reading taken mid-write is superseded by the write's own.
|
|
73841
|
+
*/
|
|
73304
73842
|
readSync() {
|
|
73305
73843
|
if (this.cached !== null) return this.cached;
|
|
73306
73844
|
try {
|
|
@@ -73330,7 +73868,30 @@ var OpenDesignModelSettingsStore = class {
|
|
|
73330
73868
|
this.cached = defaultOpenDesignModelSettings();
|
|
73331
73869
|
return this.cached;
|
|
73332
73870
|
}
|
|
73871
|
+
/**
|
|
73872
|
+
* Run one operation with exclusive access to the document.
|
|
73873
|
+
*
|
|
73874
|
+
* The queue's tail never carries a rejection: an operation that throws rejects
|
|
73875
|
+
* its OWN caller only, and the tail is flattened to a fulfilled promise right
|
|
73876
|
+
* afterwards, so a failed write cannot poison the writes queued behind it.
|
|
73877
|
+
*/
|
|
73878
|
+
async exclusive(operation) {
|
|
73879
|
+
const result = this.pending.then(operation);
|
|
73880
|
+
this.pending = result.then(() => void 0, () => void 0);
|
|
73881
|
+
return await result;
|
|
73882
|
+
}
|
|
73883
|
+
/** Replace the whole document, queued behind any write already in flight. */
|
|
73333
73884
|
async write(settings) {
|
|
73885
|
+
return await this.exclusive(() => this.writeUnlocked(settings));
|
|
73886
|
+
}
|
|
73887
|
+
/**
|
|
73888
|
+
* The file half of a write, for callers that ALREADY hold the queue.
|
|
73889
|
+
*
|
|
73890
|
+
* No public mutator may call another one: awaiting a queued mutator from inside
|
|
73891
|
+
* `exclusive` would wait for a queue position behind itself and deadlock. That is
|
|
73892
|
+
* why every mutator below composes `read()` with THIS, never with `write()`.
|
|
73893
|
+
*/
|
|
73894
|
+
async writeUnlocked(settings) {
|
|
73334
73895
|
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
73335
73896
|
const tmp = `${this.filePath}.tmp.${Date.now()}`;
|
|
73336
73897
|
await fs.writeFile(tmp, JSON.stringify({
|
|
@@ -73342,32 +73903,75 @@ var OpenDesignModelSettingsStore = class {
|
|
|
73342
73903
|
return settings;
|
|
73343
73904
|
}
|
|
73344
73905
|
async update(patch) {
|
|
73345
|
-
|
|
73346
|
-
|
|
73347
|
-
|
|
73348
|
-
|
|
73349
|
-
|
|
73350
|
-
|
|
73351
|
-
|
|
73352
|
-
|
|
73906
|
+
return await this.exclusive(async () => {
|
|
73907
|
+
const current = await this.read();
|
|
73908
|
+
const next = {
|
|
73909
|
+
...current,
|
|
73910
|
+
...patch,
|
|
73911
|
+
contextWindowOverrides: patch.contextWindowOverrides === void 0 ? current.contextWindowOverrides : patch.contextWindowOverrides
|
|
73912
|
+
};
|
|
73913
|
+
return await this.writeUnlocked(next);
|
|
73914
|
+
});
|
|
73353
73915
|
}
|
|
73354
|
-
/**
|
|
73355
|
-
|
|
73356
|
-
|
|
73357
|
-
|
|
73358
|
-
|
|
73359
|
-
|
|
73916
|
+
/**
|
|
73917
|
+
* Record a catalog sync without touching the user's selection.
|
|
73918
|
+
*
|
|
73919
|
+
* `servedModelIds` is written only when the caller actually measured it: a sync
|
|
73920
|
+
* in which every scan failed must leave the previous measurement standing rather
|
|
73921
|
+
* than clearing the filter, so `undefined` keeps the stored value while an array
|
|
73922
|
+
* (including an empty one) replaces it.
|
|
73923
|
+
*/
|
|
73924
|
+
async storeCatalog(models, servedModelIds) {
|
|
73925
|
+
return await this.exclusive(async () => {
|
|
73926
|
+
const current = await this.read();
|
|
73927
|
+
return await this.writeUnlocked(servedModelIds === void 0 ? {
|
|
73928
|
+
...current,
|
|
73929
|
+
catalogModels: models
|
|
73930
|
+
} : {
|
|
73931
|
+
...current,
|
|
73932
|
+
catalogModels: models,
|
|
73933
|
+
servedModelIds
|
|
73934
|
+
});
|
|
73935
|
+
});
|
|
73936
|
+
}
|
|
73937
|
+
/**
|
|
73938
|
+
* Replace the model selection, dropping any forced reveal it excludes.
|
|
73939
|
+
*
|
|
73940
|
+
* One `read` for both the prune input and the write baseline, so the two cannot
|
|
73941
|
+
* disagree: computing the prune outside and then patching would read the document
|
|
73942
|
+
* twice, and a forced reveal added in that gap would be recorded and then erased
|
|
73943
|
+
* by the second read's snapshot.
|
|
73944
|
+
*
|
|
73945
|
+
* The prune belongs to the selection rather than to `models/visibility` because
|
|
73946
|
+
* the card's only un-tick path is the selection patch: it removes the id from the
|
|
73947
|
+
* whitelist and never posts `{visible:false}`, so a forced reveal would otherwise
|
|
73948
|
+
* outlive the tick that asked for it. `null` is "no filter" — not a "keep only
|
|
73949
|
+
* these" statement — so it prunes nothing.
|
|
73950
|
+
*/
|
|
73951
|
+
async selectModels(ids) {
|
|
73952
|
+
return await this.exclusive(async () => {
|
|
73953
|
+
const current = await this.read();
|
|
73954
|
+
return await this.writeUnlocked(ids === null ? {
|
|
73955
|
+
...current,
|
|
73956
|
+
enabledModelIds: null
|
|
73957
|
+
} : {
|
|
73958
|
+
...current,
|
|
73959
|
+
enabledModelIds: ids,
|
|
73960
|
+
forceVisibleModels: current.forceVisibleModels.filter((id) => ids.includes(id))
|
|
73961
|
+
});
|
|
73360
73962
|
});
|
|
73361
73963
|
}
|
|
73362
73964
|
/** Store or clear (null) one model's context-window override. */
|
|
73363
73965
|
async setContextWindowOverride(model, contextWindow) {
|
|
73364
|
-
|
|
73365
|
-
|
|
73366
|
-
|
|
73367
|
-
|
|
73368
|
-
|
|
73369
|
-
|
|
73370
|
-
|
|
73966
|
+
return await this.exclusive(async () => {
|
|
73967
|
+
const current = await this.read();
|
|
73968
|
+
const overrides = { ...current.contextWindowOverrides };
|
|
73969
|
+
if (contextWindow === null) delete overrides[model];
|
|
73970
|
+
else overrides[model] = contextWindow;
|
|
73971
|
+
return await this.writeUnlocked({
|
|
73972
|
+
...current,
|
|
73973
|
+
contextWindowOverrides: overrides
|
|
73974
|
+
});
|
|
73371
73975
|
});
|
|
73372
73976
|
}
|
|
73373
73977
|
};
|
|
@@ -73521,80 +74125,6 @@ var OpenDesignAccountPool = class extends AccountPoolCore {
|
|
|
73521
74125
|
}
|
|
73522
74126
|
};
|
|
73523
74127
|
//#endregion
|
|
73524
|
-
//#region src/host/opendesign/catalog.ts
|
|
73525
|
-
/**
|
|
73526
|
-
* Merge the shipped table, the last gateway listing and the user's per-model
|
|
73527
|
-
* override into the descriptor the harness validates.
|
|
73528
|
-
*
|
|
73529
|
-
* Context window and output cap are provider facts, not preferences: the harness
|
|
73530
|
-
* sizes its prompt budget against `context.contextWindow`, so an understated
|
|
73531
|
-
* window silently truncates history. This line ships no per-model windows (the
|
|
73532
|
-
* gateway publishes none to an API key), so the window is the line default until
|
|
73533
|
-
* a live listing or a user override states one.
|
|
73534
|
-
*
|
|
73535
|
-
* @module dsh-chatgpt-subscription/opendesign-catalog
|
|
73536
|
-
*/
|
|
73537
|
-
/** The wire's whole modality vocabulary; a listing may claim more than it accepts. */
|
|
73538
|
-
const SEAM_MODALITIES = /* @__PURE__ */ new Set(["text", "image"]);
|
|
73539
|
-
function definedOnly(value) {
|
|
73540
|
-
if (value === void 0) return {};
|
|
73541
|
-
const result = {};
|
|
73542
|
-
for (const [key, entry] of Object.entries(value)) if (entry !== void 0 && entry !== null) result[key] = entry;
|
|
73543
|
-
return result;
|
|
73544
|
-
}
|
|
73545
|
-
/**
|
|
73546
|
-
* The output budget to advertise for one model.
|
|
73547
|
-
*
|
|
73548
|
-
* `defaultMaxTokens` is what a request asks for, not what the model can emit,
|
|
73549
|
-
* and the harness spends it twice: it fills a request's `max_tokens` from it
|
|
73550
|
-
* and reserves the same number as completion room before deciding to compact.
|
|
73551
|
-
* So a published ceiling clamps the configured budget rather than becoming it,
|
|
73552
|
-
* and a budget is never taken past half the window.
|
|
73553
|
-
*/
|
|
73554
|
-
function resolveOutputBudget(budget, ceiling, contextWindow) {
|
|
73555
|
-
const wanted = Number.isFinite(budget) && (budget ?? 0) > 0 ? budget : void 0;
|
|
73556
|
-
const most = Number.isFinite(ceiling) && (ceiling ?? 0) > 0 ? ceiling : void 0;
|
|
73557
|
-
const chosen = wanted === void 0 ? most : most === void 0 ? wanted : Math.min(wanted, most);
|
|
73558
|
-
if (chosen === void 0) return void 0;
|
|
73559
|
-
const half = Number.isFinite(contextWindow) && contextWindow > 0 ? Math.floor(contextWindow / 2) : void 0;
|
|
73560
|
-
return half === void 0 ? chosen : Math.min(chosen, half);
|
|
73561
|
-
}
|
|
73562
|
-
/**
|
|
73563
|
-
* Resolve the metadata one model is advertised with.
|
|
73564
|
-
*
|
|
73565
|
-
* Precedence per field: the user's override, then the last gateway listing,
|
|
73566
|
-
* then the shipped table, then the route-wide fallback. The listing sits above
|
|
73567
|
-
* the shipped table for the fields the table cannot carry (window, modalities):
|
|
73568
|
-
* the table's windows do not exist to be beaten, and its names/prices are
|
|
73569
|
-
* curated, so a listing that states no name keeps the table's.
|
|
73570
|
-
*/
|
|
73571
|
-
function resolveModelInfo(model, options) {
|
|
73572
|
-
const synced = options.synced.find((entry) => entry.id === model);
|
|
73573
|
-
const shipped = catalogEntry$1(model);
|
|
73574
|
-
const published = {
|
|
73575
|
-
...definedOnly(shipped),
|
|
73576
|
-
...definedOnly(synced)
|
|
73577
|
-
};
|
|
73578
|
-
const fallbackWindow = options.defaultContextWindow ?? 128e3;
|
|
73579
|
-
const contextWindow = options.contextWindowOverride ?? published.contextWindow ?? fallbackWindow;
|
|
73580
|
-
const maxTokens = resolveOutputBudget(options.defaultMaxTokens ?? 64e3, published.maxTokens, contextWindow);
|
|
73581
|
-
const reasoning = published.reasoning ?? shipped?.reasoning ?? true;
|
|
73582
|
-
const efforts = (Array.isArray(published.efforts) && published.efforts.length > 0 ? published.efforts : void 0) ?? (shipped?.efforts !== void 0 && shipped.efforts.length > 0 ? [...shipped.efforts] : DEFAULT_EFFORTS);
|
|
73583
|
-
const defaultEffort = reasoning && shipped?.defaultEffort !== void 0 && efforts.includes(shipped.defaultEffort) ? shipped.defaultEffort : void 0;
|
|
73584
|
-
const input = (Array.isArray(published.input) && published.input.length > 0 ? published.input : shipped?.input ?? ["text"]).filter((entry) => SEAM_MODALITIES.has(entry));
|
|
73585
|
-
return {
|
|
73586
|
-
id: model,
|
|
73587
|
-
displayName: String(published.name ?? "").trim() || model,
|
|
73588
|
-
contextWindow,
|
|
73589
|
-
maxTokens: maxTokens ?? options.defaultMaxTokens ?? 64e3,
|
|
73590
|
-
reasoning,
|
|
73591
|
-
...reasoning ? { efforts } : { efforts: [] },
|
|
73592
|
-
...defaultEffort === void 0 ? {} : { defaultEffort },
|
|
73593
|
-
input: input.length > 0 ? input : ["text"],
|
|
73594
|
-
...shipped?.pricing === void 0 ? {} : { pricing: shipped.pricing }
|
|
73595
|
-
};
|
|
73596
|
-
}
|
|
73597
|
-
//#endregion
|
|
73598
74128
|
//#region src/host/opendesign/client.ts
|
|
73599
74129
|
/**
|
|
73600
74130
|
* Transport for the OpenDesign link gateway. Transport only: no account or
|
|
@@ -75019,27 +75549,6 @@ var OpenDesignAdapter = class extends LlmAdapter {
|
|
|
75019
75549
|
settings() {
|
|
75020
75550
|
return this.modelSettings.read();
|
|
75021
75551
|
}
|
|
75022
|
-
/** Every id the line knows: the shipped table plus the synced live catalog. */
|
|
75023
|
-
knownModelIds(settings) {
|
|
75024
|
-
const ids = new Set(DEFAULT_MODELS$1);
|
|
75025
|
-
for (const model of settings.catalogModels) ids.add(model.id);
|
|
75026
|
-
return [...ids];
|
|
75027
|
-
}
|
|
75028
|
-
/**
|
|
75029
|
-
* The ids the route advertises, narrowed by the user's selection and hidden
|
|
75030
|
-
* list.
|
|
75031
|
-
*
|
|
75032
|
-
* The selection is captured against this same id vocabulary (the gateway's
|
|
75033
|
-
* own wire ids), so a cold start filters the shipped table through it without
|
|
75034
|
-
* the empty-intersection trap a foreign-vocabulary whitelist would spring —
|
|
75035
|
-
* and an explicit "everything off" (`[]`) still wins, because that intent
|
|
75036
|
-
* does not depend on a vocabulary at all.
|
|
75037
|
-
*/
|
|
75038
|
-
visibleModelIds(settings) {
|
|
75039
|
-
const hidden = new Set(settings.hiddenModels);
|
|
75040
|
-
const enabled = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
|
|
75041
|
-
return this.knownModelIds(settings).filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)));
|
|
75042
|
-
}
|
|
75043
75552
|
/** One model's resolved descriptor, from the shipped table plus live data. */
|
|
75044
75553
|
async modelInfo(modelId) {
|
|
75045
75554
|
const settings = await this.settings();
|
|
@@ -75054,7 +75563,7 @@ var OpenDesignAdapter = class extends LlmAdapter {
|
|
|
75054
75563
|
const prov = provider || "opendesign";
|
|
75055
75564
|
const settings = await this.settings();
|
|
75056
75565
|
if (settings.enabled === false) return [];
|
|
75057
|
-
const ids =
|
|
75566
|
+
const ids = openDesignVisibleModelIds(settings);
|
|
75058
75567
|
return await Promise.all(ids.map(async (id) => {
|
|
75059
75568
|
const info = await this.modelInfo(id);
|
|
75060
75569
|
return {
|
|
@@ -75131,28 +75640,33 @@ var OpenDesignAdapter = class extends LlmAdapter {
|
|
|
75131
75640
|
try {
|
|
75132
75641
|
let response;
|
|
75133
75642
|
let transportError;
|
|
75134
|
-
for (let transportTry = 0; transportTry <= TRANSPORT_RETRIES; transportTry += 1)
|
|
75135
|
-
|
|
75136
|
-
|
|
75137
|
-
|
|
75138
|
-
|
|
75139
|
-
|
|
75140
|
-
|
|
75141
|
-
|
|
75142
|
-
|
|
75143
|
-
|
|
75144
|
-
|
|
75145
|
-
|
|
75146
|
-
|
|
75147
|
-
|
|
75148
|
-
|
|
75149
|
-
|
|
75150
|
-
|
|
75151
|
-
|
|
75643
|
+
for (let transportTry = 0; transportTry <= TRANSPORT_RETRIES; transportTry += 1) {
|
|
75644
|
+
const headerDeadline = withAttemptTimeout(upstream, this.options.headerTimeoutMs);
|
|
75645
|
+
try {
|
|
75646
|
+
response = await openChatStream$1({
|
|
75647
|
+
fetchFn: this.options.fetchFn,
|
|
75648
|
+
baseURL,
|
|
75649
|
+
apiKey,
|
|
75650
|
+
body,
|
|
75651
|
+
signal: headerDeadline.signal
|
|
75652
|
+
});
|
|
75653
|
+
headerDeadline.clear();
|
|
75654
|
+
break;
|
|
75655
|
+
} catch (error) {
|
|
75656
|
+
headerDeadline.clear();
|
|
75657
|
+
if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: error });
|
|
75658
|
+
transportError = error;
|
|
75659
|
+
if (transportTry < TRANSPORT_RETRIES && isPreConnectionFailure(error)) {
|
|
75660
|
+
try {
|
|
75661
|
+
await setTimeout$1(TRANSPORT_RETRY_BASE_MS * (transportTry + 1), void 0, { signal: upstream });
|
|
75662
|
+
} catch (waitError) {
|
|
75663
|
+
if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: waitError });
|
|
75664
|
+
throw waitError;
|
|
75665
|
+
}
|
|
75666
|
+
continue;
|
|
75152
75667
|
}
|
|
75153
|
-
|
|
75668
|
+
break;
|
|
75154
75669
|
}
|
|
75155
|
-
break;
|
|
75156
75670
|
}
|
|
75157
75671
|
if (response === void 0) {
|
|
75158
75672
|
lastError = new LlmError(`${errorChain$1(transportError)} The request never reached the gateway: ${baseURL} failed during TLS/connect. Check the network or proxy.`, "TRANSPORT", { cause: transportError });
|
|
@@ -75220,9 +75734,31 @@ var OpenDesignAdapter = class extends LlmAdapter {
|
|
|
75220
75734
|
await this.accountPool.markCooldown(accountId, POOL_COOLDOWN_MS$1, `${PROVIDER_NAME$2} credits exhausted`).catch(() => void 0);
|
|
75221
75735
|
}
|
|
75222
75736
|
};
|
|
75223
|
-
/**
|
|
75224
|
-
|
|
75225
|
-
|
|
75737
|
+
/**
|
|
75738
|
+
* A caller signal combined with the attempt's CLEARABLE header deadline.
|
|
75739
|
+
*
|
|
75740
|
+
* The clear is not a nicety, it is the difference between a header deadline and
|
|
75741
|
+
* a request deadline. `AbortSignal.timeout` is absolute and cannot be disarmed,
|
|
75742
|
+
* so attaching one to the fetch signal and leaving it there keeps counting
|
|
75743
|
+
* through the whole response body — a model that thinks for more than
|
|
75744
|
+
* `ATTEMPT_TIMEOUT_MS` then dies mid-answer with "The operation was aborted due
|
|
75745
|
+
* to timeout", is retried by the host, and dies again at the same wall clock.
|
|
75746
|
+
* That is exactly what happened here: two attempts streamed reasoning for 155s
|
|
75747
|
+
* and 173s and both were cut 180s after their request went out.
|
|
75748
|
+
*
|
|
75749
|
+
* Cleared after the headers (see the transport loop), at which point the stream
|
|
75750
|
+
* idle watchdog owns liveness: it watches for SILENCE and resets on every
|
|
75751
|
+
* chunk, so a long thinking pass is fine and a dead socket is still caught.
|
|
75752
|
+
*/
|
|
75753
|
+
function withAttemptTimeout(signal, timeoutMs = ATTEMPT_TIMEOUT_MS) {
|
|
75754
|
+
const deadline = new AbortController();
|
|
75755
|
+
const timer = setTimeout(() => {
|
|
75756
|
+
deadline.abort(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
|
|
75757
|
+
}, timeoutMs);
|
|
75758
|
+
return {
|
|
75759
|
+
signal: AbortSignal.any([signal, deadline.signal]),
|
|
75760
|
+
clear: () => clearTimeout(timer)
|
|
75761
|
+
};
|
|
75226
75762
|
}
|
|
75227
75763
|
/**
|
|
75228
75764
|
* Whether a transport failure happened before any request was sent.
|
|
@@ -75540,6 +76076,15 @@ const OPENDESIGN_ROUTE_PREFIX = "/opendesign/api";
|
|
|
75540
76076
|
const OPENDESIGN_CONSOLE_URL = "https://open-design.ai/cloud";
|
|
75541
76077
|
//#endregion
|
|
75542
76078
|
//#region src/host/opendesign/routes.ts
|
|
76079
|
+
/**
|
|
76080
|
+
* Accounts one catalog sync asks at most.
|
|
76081
|
+
*
|
|
76082
|
+
* Each account costs up to two upstream reads, so this is what stands between a
|
|
76083
|
+
* twenty-key pool and forty requests behind one button. It bounds the SCAN, not
|
|
76084
|
+
* the pool: an account beyond the cap contributes nothing to the union, because
|
|
76085
|
+
* the cap is a hard bound on the measurement rather than a rotating window.
|
|
76086
|
+
*/
|
|
76087
|
+
const MAX_SYNC_ACCOUNTS = 8;
|
|
75543
76088
|
function sendJson(response, status, body) {
|
|
75544
76089
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
75545
76090
|
response.end(JSON.stringify(body));
|
|
@@ -75592,17 +76137,20 @@ function formatModelHint(contextWindow, input, reasoning, efforts, pricing) {
|
|
|
75592
76137
|
const base = formatModelCapacity(contextWindow) + " · " + input.join("+") + (ladder === "" ? "" : " · " + ladder);
|
|
75593
76138
|
return pricing === void 0 ? base : `${base} · $${pricing.input}/$${pricing.output}`;
|
|
75594
76139
|
}
|
|
75595
|
-
/**
|
|
75596
|
-
|
|
75597
|
-
|
|
75598
|
-
|
|
75599
|
-
|
|
75600
|
-
|
|
75601
|
-
|
|
76140
|
+
/**
|
|
76141
|
+
* Every model the card should render, whether or not it is advertised.
|
|
76142
|
+
*
|
|
76143
|
+
* `enabled` is the state of the card's checklist row, and it is read from the ONE
|
|
76144
|
+
* visibility rule (`openDesignVisibleModelIds` in ./line.ts) — the very function
|
|
76145
|
+
* the adapter offers the picker with. A row that reads "checked" while no picker
|
|
76146
|
+
* offers it is the contradiction this projection must not be able to produce, so
|
|
76147
|
+
* the two are not two calculations: a model the plan withholds reads as unchecked
|
|
76148
|
+
* until the user forces it, because that is what the picker does with it.
|
|
76149
|
+
*/
|
|
75602
76150
|
function projectModels(settings) {
|
|
75603
|
-
const known =
|
|
75604
|
-
const
|
|
75605
|
-
const
|
|
76151
|
+
const known = openDesignKnownModelIds(settings);
|
|
76152
|
+
const visible = new Set(openDesignVisibleModelIds(settings));
|
|
76153
|
+
const forcedVisible = new Set(settings.forceVisibleModels);
|
|
75606
76154
|
return known.map((id) => {
|
|
75607
76155
|
const base = resolveModelInfo(id, {
|
|
75608
76156
|
synced: settings.catalogModels,
|
|
@@ -75615,6 +76163,7 @@ function projectModels(settings) {
|
|
|
75615
76163
|
defaultMaxTokens: settings.defaultMaxTokens,
|
|
75616
76164
|
contextWindowOverride: settings.contextWindowOverrides[id]
|
|
75617
76165
|
});
|
|
76166
|
+
const served = isModelServed(id, settings.servedModelIds);
|
|
75618
76167
|
return {
|
|
75619
76168
|
id,
|
|
75620
76169
|
name: info.displayName,
|
|
@@ -75624,7 +76173,9 @@ function projectModels(settings) {
|
|
|
75624
76173
|
maxTokens: info.maxTokens,
|
|
75625
76174
|
reasoning: info.reasoning,
|
|
75626
76175
|
input: [...info.input],
|
|
75627
|
-
enabled:
|
|
76176
|
+
enabled: visible.has(id),
|
|
76177
|
+
served,
|
|
76178
|
+
forcedVisible: forcedVisible.has(id),
|
|
75628
76179
|
...info.pricing === void 0 ? {} : { pricing: info.pricing },
|
|
75629
76180
|
...settings.contextWindowOverrides[id] === void 0 ? {} : { contextWindowOverride: settings.contextWindowOverrides[id] }
|
|
75630
76181
|
};
|
|
@@ -75717,16 +76268,22 @@ function registerOpenDesignRoutes(ctx, options) {
|
|
|
75717
76268
|
};
|
|
75718
76269
|
const status = async () => await getOpenDesignWebStatus(options, usageStore);
|
|
75719
76270
|
/**
|
|
75720
|
-
* Sync the live catalog with
|
|
76271
|
+
* Sync the live catalog with every usable account's key.
|
|
75721
76272
|
*
|
|
75722
|
-
* The gateway's listing is the authority on what
|
|
75723
|
-
*
|
|
75724
|
-
*
|
|
75725
|
-
*
|
|
75726
|
-
*
|
|
76273
|
+
* The gateway's listing is the authority on what a key's plan serves, and a plan
|
|
76274
|
+
* belongs to an ACCOUNT rather than to the line: two pooled keys can serve two
|
|
76275
|
+
* different model sets, so every account is asked and the union is what gets
|
|
76276
|
+
* remembered. That union is persisted as `servedModelIds` — the fact the picker
|
|
76277
|
+
* needs and the one this sync used to throw away — and it is written only when a
|
|
76278
|
+
* scan really succeeded, so a network failure cannot switch the filter off.
|
|
76279
|
+
*
|
|
76280
|
+
* A failed or refused read changes nothing (the shipped table still stands in),
|
|
76281
|
+
* and the sync's answer says which happened. Announces only a real change: the
|
|
76282
|
+
* picker rebuilds on `llm/adapters-updated`, so a no-change sync must stay
|
|
76283
|
+
* silent.
|
|
75727
76284
|
*/
|
|
75728
76285
|
const syncCatalog = async () => {
|
|
75729
|
-
const known =
|
|
76286
|
+
const known = openDesignKnownModelIds(await settingsStore.read());
|
|
75730
76287
|
const data = await accountPool.read().catch(() => null);
|
|
75731
76288
|
const now = Date.now();
|
|
75732
76289
|
const accounts = (data?.accounts ?? []).filter((account) => isUsable(account, now));
|
|
@@ -75737,27 +76294,22 @@ function registerOpenDesignRoutes(ctx, options) {
|
|
|
75737
76294
|
};
|
|
75738
76295
|
const scans = [];
|
|
75739
76296
|
let controlRefused = false;
|
|
75740
|
-
|
|
76297
|
+
const settings = await settingsStore.read();
|
|
76298
|
+
for (const account of accounts.slice(0, MAX_SYNC_ACCOUNTS)) {
|
|
75741
76299
|
const controlKey = controlKeyFor(account.credentials);
|
|
75742
|
-
if (controlKey
|
|
75743
|
-
|
|
75744
|
-
|
|
75745
|
-
|
|
75746
|
-
|
|
75747
|
-
|
|
75748
|
-
|
|
75749
|
-
|
|
75750
|
-
continue;
|
|
76300
|
+
if (controlKey !== void 0) {
|
|
76301
|
+
const scan = await fetchControlPlaneCatalog({
|
|
76302
|
+
fetchFn,
|
|
76303
|
+
controlKey,
|
|
76304
|
+
knownIds: known
|
|
76305
|
+
});
|
|
76306
|
+
if (scan === null) controlRefused = true;
|
|
76307
|
+
else if (scan.ids.length > 0) scans.push(scan);
|
|
75751
76308
|
}
|
|
75752
|
-
if (scan.ids.length > 0) scans.push(scan);
|
|
75753
|
-
break;
|
|
75754
|
-
}
|
|
75755
|
-
{
|
|
75756
|
-
const settings = await settingsStore.read();
|
|
75757
76309
|
const gateway = await fetchGatewayCatalog$1({
|
|
75758
76310
|
fetchFn,
|
|
75759
76311
|
baseURL: settings.baseURL,
|
|
75760
|
-
apiKey: bearerFor(
|
|
76312
|
+
apiKey: bearerFor(account.credentials)
|
|
75761
76313
|
});
|
|
75762
76314
|
if (gateway !== null && gateway.ids.length > 0) scans.push(gateway);
|
|
75763
76315
|
}
|
|
@@ -75787,9 +76339,10 @@ function registerOpenDesignRoutes(ctx, options) {
|
|
|
75787
76339
|
const merged = [...existing.values()].filter((entry) => {
|
|
75788
76340
|
return !current.catalogModels.some((old) => old.id === entry.id) || syncedIds.has(entry.id);
|
|
75789
76341
|
});
|
|
76342
|
+
const served = [...new Set(scans.flatMap((scan) => scan.ids))];
|
|
75790
76343
|
const fingerprintBefore = current.catalogModels.map((entry) => entry.id).join("\n");
|
|
75791
76344
|
const fingerprintAfter = merged.map((entry) => entry.id).join("\n");
|
|
75792
|
-
await settingsStore.storeCatalog(merged);
|
|
76345
|
+
await settingsStore.storeCatalog(merged, served);
|
|
75793
76346
|
if (fingerprintBefore !== fingerprintAfter) emitModelsUpdated();
|
|
75794
76347
|
return {
|
|
75795
76348
|
added,
|
|
@@ -76033,8 +76586,7 @@ function registerOpenDesignRoutes(ctx, options) {
|
|
|
76033
76586
|
const body = await readRequestJson(request).catch(() => ({}));
|
|
76034
76587
|
let touched = false;
|
|
76035
76588
|
if (body.enabledModelIds === null || Array.isArray(body.enabledModelIds)) {
|
|
76036
|
-
|
|
76037
|
-
await settingsStore.update({ enabledModelIds: ids });
|
|
76589
|
+
await settingsStore.selectModels(body.enabledModelIds === null ? null : body.enabledModelIds.filter((id) => typeof id === "string"));
|
|
76038
76590
|
touched = true;
|
|
76039
76591
|
}
|
|
76040
76592
|
if (typeof body.enabled === "boolean") {
|
|
@@ -76055,16 +76607,24 @@ function registerOpenDesignRoutes(ctx, options) {
|
|
|
76055
76607
|
});
|
|
76056
76608
|
const body = await readRequestJson(request).catch(() => ({}));
|
|
76057
76609
|
const current = await settingsStore.read();
|
|
76058
|
-
const known =
|
|
76610
|
+
const known = openDesignKnownModelIds(current);
|
|
76059
76611
|
const hidden = new Set(current.hiddenModels);
|
|
76612
|
+
const forced = new Set(current.forceVisibleModels);
|
|
76060
76613
|
if (path === "models/visibility") {
|
|
76061
76614
|
const model = typeof body.model === "string" ? body.model.trim() : "";
|
|
76062
76615
|
if (!known.includes(model)) return sendJson(response, 400, {
|
|
76063
76616
|
ok: false,
|
|
76064
76617
|
error: `${JSON.stringify(model)} is not one of this route's models`
|
|
76065
76618
|
});
|
|
76066
|
-
|
|
76067
|
-
|
|
76619
|
+
const served = isModelServed(model, current.servedModelIds);
|
|
76620
|
+
if (body.visible === false) {
|
|
76621
|
+
if (served) hidden.add(model);
|
|
76622
|
+
forced.delete(model);
|
|
76623
|
+
} else {
|
|
76624
|
+
hidden.delete(model);
|
|
76625
|
+
if (served) forced.delete(model);
|
|
76626
|
+
else forced.add(model);
|
|
76627
|
+
}
|
|
76068
76628
|
} else {
|
|
76069
76629
|
const mode = body.mode;
|
|
76070
76630
|
if (mode !== "all" && mode !== "none" && mode !== "invert") return sendJson(response, 400, {
|
|
@@ -76077,7 +76637,11 @@ function registerOpenDesignRoutes(ctx, options) {
|
|
|
76077
76637
|
const wasHidden = current.hiddenModels.includes(id);
|
|
76078
76638
|
return mode === "all" ? false : mode === "none" ? true : !wasHidden;
|
|
76079
76639
|
}) : [...hidden];
|
|
76080
|
-
|
|
76640
|
+
const nextForced = path !== "models/visibility-all" ? [...forced] : body.mode === "invert" ? current.forceVisibleModels : [];
|
|
76641
|
+
await settingsStore.update({
|
|
76642
|
+
hiddenModels: nextHidden,
|
|
76643
|
+
forceVisibleModels: nextForced
|
|
76644
|
+
});
|
|
76081
76645
|
emitModelsUpdated();
|
|
76082
76646
|
return sendJson(response, 200, {
|
|
76083
76647
|
ok: true,
|
|
@@ -76285,18 +76849,30 @@ function openDesignKnownModelIds(settings) {
|
|
|
76285
76849
|
return [...ids];
|
|
76286
76850
|
}
|
|
76287
76851
|
/**
|
|
76288
|
-
* Every id the line advertises right now.
|
|
76852
|
+
* Every id the line advertises right now — THE one visibility rule.
|
|
76853
|
+
*
|
|
76854
|
+
* The adapter (what the picker is offered), the status route (what each checklist
|
|
76855
|
+
* row reports as checked) and the hub card (how many models the line advertises)
|
|
76856
|
+
* all read this function and nothing else. Three private copies is how the hub
|
|
76857
|
+
* card ended up counting models the picker refused to show, which is exactly the
|
|
76858
|
+
* contradiction this rule exists to make impossible.
|
|
76859
|
+
*
|
|
76860
|
+
* Three layers, applied in this order:
|
|
76289
76861
|
*
|
|
76290
|
-
*
|
|
76291
|
-
*
|
|
76292
|
-
*
|
|
76293
|
-
*
|
|
76294
|
-
*
|
|
76862
|
+
* 1. the user's hidden list;
|
|
76863
|
+
* 2. the user's tick list — unset means every known model, an explicit `[]` means
|
|
76864
|
+
* everything off (the tick list and the shipped table speak the same wire ids,
|
|
76865
|
+
* so a cold-start intersection cannot empty the picker);
|
|
76866
|
+
* 3. the PLAN, once somebody measured it: an id the last successful scan did not
|
|
76867
|
+
* list is not served, and only a forced reveal (`forceVisibleModels`) keeps it.
|
|
76868
|
+
* Before that first scan `servedModelIds` is `null` and this layer hides
|
|
76869
|
+
* nothing — an unmeasured plan is not a denial.
|
|
76295
76870
|
*/
|
|
76296
76871
|
function openDesignVisibleModelIds(settings) {
|
|
76297
76872
|
const hidden = new Set(settings.hiddenModels);
|
|
76298
76873
|
const enabled = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
|
|
76299
|
-
|
|
76874
|
+
const forced = new Set(settings.forceVisibleModels);
|
|
76875
|
+
return openDesignKnownModelIds(settings).filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)) && (isModelServed(id, settings.servedModelIds) || forced.has(id)));
|
|
76300
76876
|
}
|
|
76301
76877
|
/**
|
|
76302
76878
|
* Build the line.
|