@eddyskywalker/dsh-chatgpt-subscription 0.38.0 → 0.39.0
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 +142 -1
- package/README.md +48 -11
- package/lib/client.js +805 -151
- package/lib/client.js.map +1 -1
- package/lib/index.js +2174 -700
- package/lib/types/client/CodexSubscriptionSection.d.ts +1 -1
- package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +16 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/cline-pass/ClinePassSection.d.ts.map +1 -1
- package/lib/types/client/cline-pass/api.d.ts +16 -1
- package/lib/types/client/cline-pass/api.d.ts.map +1 -1
- package/lib/types/client/cline-pass/locales.d.ts +144 -18
- package/lib/types/client/cline-pass/locales.d.ts.map +1 -1
- package/lib/types/client/cline-pass/styles.d.ts.map +1 -1
- package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
- package/lib/types/client/kimi-code/locales.d.ts +8 -17
- package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
- package/lib/types/client/locales-ru/cline-pass.d.ts.map +1 -1
- package/lib/types/client/locales-ru/hub.d.ts.map +1 -1
- package/lib/types/client/locales-ru/kimi-code.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +43 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/minimax-code/commands-contract.d.ts +14 -4
- package/lib/types/client/minimax-code/commands-contract.d.ts.map +1 -1
- package/lib/types/client/minimax-code/locales.d.ts +2 -2
- package/lib/types/client/progress-updates/settings-binding.d.ts +8 -1
- package/lib/types/client/progress-updates/settings-binding.d.ts.map +1 -1
- package/lib/types/client/progress-updates/settings-section.d.ts.map +1 -1
- package/lib/types/client/styles.d.ts.map +1 -1
- package/lib/types/compat.d.ts +33 -0
- package/lib/types/compat.d.ts.map +1 -1
- package/lib/types/host/agent-preset.d.ts +5 -0
- package/lib/types/host/agent-preset.d.ts.map +1 -1
- package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
- package/lib/types/host/antigravity/mapper.d.ts +0 -1
- package/lib/types/host/antigravity/mapper.d.ts.map +1 -1
- package/lib/types/host/antigravity/types.d.ts +1 -2
- package/lib/types/host/antigravity/types.d.ts.map +1 -1
- package/lib/types/host/claude/mapper.d.ts +5 -7
- package/lib/types/host/claude/mapper.d.ts.map +1 -1
- package/lib/types/host/claude/model-catalog.d.ts.map +1 -1
- package/lib/types/host/cline-pass/adapter.d.ts +1 -1
- package/lib/types/host/cline-pass/adapter.d.ts.map +1 -1
- package/lib/types/host/cline-pass/client.d.ts +16 -3
- package/lib/types/host/cline-pass/client.d.ts.map +1 -1
- package/lib/types/host/cline-pass/engine.d.ts +11 -3
- package/lib/types/host/cline-pass/engine.d.ts.map +1 -1
- package/lib/types/host/cline-pass/line.d.ts +1 -1
- package/lib/types/host/cline-pass/line.d.ts.map +1 -1
- package/lib/types/host/cline-pass/protocol.d.ts +18 -0
- package/lib/types/host/cline-pass/protocol.d.ts.map +1 -1
- package/lib/types/host/cline-pass/routes.d.ts +2 -0
- package/lib/types/host/cline-pass/routes.d.ts.map +1 -1
- package/lib/types/host/cline-pass/store.d.ts +2 -0
- package/lib/types/host/cline-pass/store.d.ts.map +1 -1
- package/lib/types/host/cline-pass/token-store.d.ts +11 -0
- package/lib/types/host/cline-pass/token-store.d.ts.map +1 -1
- package/lib/types/host/cline-pass/tools.d.ts.map +1 -1
- package/lib/types/host/cline-pass/types.d.ts +24 -2
- package/lib/types/host/cline-pass/types.d.ts.map +1 -1
- package/lib/types/host/codex-account-pool.d.ts +3 -0
- package/lib/types/host/codex-account-pool.d.ts.map +1 -1
- package/lib/types/host/common/account-pool.d.ts +59 -5
- package/lib/types/host/common/account-pool.d.ts.map +1 -1
- package/lib/types/host/common/credential-read-cache.d.ts +11 -1
- package/lib/types/host/common/credential-read-cache.d.ts.map +1 -1
- package/lib/types/host/common/credentials-compat.d.ts +73 -0
- package/lib/types/host/common/credentials-compat.d.ts.map +1 -0
- package/lib/types/host/common/pool-storage-settings.d.ts +41 -0
- package/lib/types/host/common/pool-storage-settings.d.ts.map +1 -0
- package/lib/types/host/common/pool-storage.d.ts +143 -0
- package/lib/types/host/common/pool-storage.d.ts.map +1 -0
- package/lib/types/host/credential-store-secret-service.d.ts +27 -0
- package/lib/types/host/credential-store-secret-service.d.ts.map +1 -1
- package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
- package/lib/types/host/kimi-code/cache-hint.d.ts +29 -15
- package/lib/types/host/kimi-code/cache-hint.d.ts.map +1 -1
- package/lib/types/host/kimi-code/mapper.d.ts +34 -31
- package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
- package/lib/types/host/kimi-code/model-catalog.d.ts +32 -0
- package/lib/types/host/kimi-code/model-catalog.d.ts.map +1 -1
- package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
- package/lib/types/host/kimi-code/token-store.d.ts +1 -11
- package/lib/types/host/kimi-code/token-store.d.ts.map +1 -1
- package/lib/types/host/kimi-code/types.d.ts +38 -14
- package/lib/types/host/kimi-code/types.d.ts.map +1 -1
- package/lib/types/host/oauth-service.d.ts +83 -1
- package/lib/types/host/oauth-service.d.ts.map +1 -1
- package/lib/types/host/preset-sync.d.ts +2 -2
- package/lib/types/host/preset-sync.d.ts.map +1 -1
- package/lib/types/host/qoder/account-pool.d.ts +4 -0
- package/lib/types/host/qoder/account-pool.d.ts.map +1 -1
- package/lib/types/host/qoder/client.d.ts +24 -2
- package/lib/types/host/qoder/client.d.ts.map +1 -1
- package/lib/types/host/qoder/oauth.d.ts +17 -0
- package/lib/types/host/qoder/oauth.d.ts.map +1 -1
- package/lib/types/host/qoder/routes.d.ts.map +1 -1
- package/lib/types/host/routes.d.ts +10 -0
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/host/subagent-model-authorization.d.ts +20 -5
- package/lib/types/host/subagent-model-authorization.d.ts.map +1 -1
- package/lib/types/host/token-store-linux.d.ts.map +1 -1
- package/lib/types/host/token-store.d.ts +16 -0
- package/lib/types/host/token-store.d.ts.map +1 -1
- package/lib/types/index.d.ts +26 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/cline-pass-contracts.d.ts +48 -2
- package/lib/types/shared/cline-pass-contracts.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +86 -0
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/lib/types/shared/kimi-code-contracts.d.ts +0 -16
- package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
- package/package.json +37 -37
- package/presets/dispatch/agent.cordis.yml +25 -11
package/lib/index.js
CHANGED
|
@@ -100,6 +100,37 @@ const OAUTH_SCOPE = "openid profile email offline_access";
|
|
|
100
100
|
const CODEX_ORIGINATOR = "opencode";
|
|
101
101
|
/** OAuth presents the same identity the request path does (see above). */
|
|
102
102
|
const OAUTH_ORIGINATOR = CODEX_ORIGINATOR;
|
|
103
|
+
/**
|
|
104
|
+
* The device-code grant: the one sign-in path a headless host can finish.
|
|
105
|
+
*
|
|
106
|
+
* The browser flow redirects to a *fixed* `http://localhost:1455/auth/callback`
|
|
107
|
+
* registered against this OAuth client, so it cannot be completed from a server:
|
|
108
|
+
* nothing listens on that loopback port there, and an HTTPS page cannot reach it
|
|
109
|
+
* anyway (mixed content). The device grant takes the redirect out of the
|
|
110
|
+
* picture — the user types a short code on a page on their own machine and this
|
|
111
|
+
* process polls for the result.
|
|
112
|
+
*
|
|
113
|
+
* Same client id as the browser flow, because it is the same registration. These
|
|
114
|
+
* are that registration's device endpoints, verified against the live service.
|
|
115
|
+
*/
|
|
116
|
+
const OAUTH_DEVICE_USERCODE_URL = `${CHATGPT_OAUTH_ISSUER}/api/accounts/deviceauth/usercode`;
|
|
117
|
+
const OAUTH_DEVICE_TOKEN_URL = `${CHATGPT_OAUTH_ISSUER}/api/accounts/deviceauth/token`;
|
|
118
|
+
const OAUTH_DEVICE_VERIFY_URL = `${CHATGPT_OAUTH_ISSUER}/codex/device`;
|
|
119
|
+
/**
|
|
120
|
+
* The redirect the device grant redeems its authorization code against.
|
|
121
|
+
*
|
|
122
|
+
* Nothing listens on it: the device endpoint hands the code to this process
|
|
123
|
+
* directly and the token endpoint only checks it against the grant.
|
|
124
|
+
*/
|
|
125
|
+
const OAUTH_DEVICE_REDIRECT_URI = `${CHATGPT_OAUTH_ISSUER}/deviceauth/callback`;
|
|
126
|
+
/** Deadline used when a grant states no usable `expires_at` (the live one is ~15 min out). */
|
|
127
|
+
const OAUTH_DEVICE_GRANT_LIFETIME_MS = 15 * 6e4;
|
|
128
|
+
/**
|
|
129
|
+
* Floor for a fractional poll interval (a grant answering `"0.2"`), so it cannot
|
|
130
|
+
* turn the loop into a spin. `"0"`, a negative or an unparseable value never get
|
|
131
|
+
* this far: `parseGrantInterval` already replaced them with the 5 s default.
|
|
132
|
+
*/
|
|
133
|
+
const OAUTH_DEVICE_MIN_POLL_INTERVAL_MS = 1e3;
|
|
103
134
|
const TOKEN_REFRESH_MARGIN_MS = 6e4;
|
|
104
135
|
const ROUTE_PREFIX$4 = "/api/dsh-chatgpt-subscription";
|
|
105
136
|
const PLUGIN_VERSION = "0.1.0-alpha.0";
|
|
@@ -878,6 +909,61 @@ function asNumber$7(value) {
|
|
|
878
909
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
879
910
|
}
|
|
880
911
|
//#endregion
|
|
912
|
+
//#region src/host/token-store.ts
|
|
913
|
+
/**
|
|
914
|
+
* A stored document that cannot be read back as one.
|
|
915
|
+
*
|
|
916
|
+
* Distinct from a store that did not answer, on purpose. A corrupt document is
|
|
917
|
+
* something the NEXT WRITE repairs; an unreadable store is not, and recovering
|
|
918
|
+
* from it as if the pool were empty is how a transient keyring failure erases
|
|
919
|
+
* every account: the read comes back empty, the caller mutates the empty pool and
|
|
920
|
+
* saves it. Stores throw this class for their own "payload is invalid" case and
|
|
921
|
+
* leave availability failures as plain errors, which is the only distinction the
|
|
922
|
+
* pool core needs (see `AccountPoolCore.loadPoolData`).
|
|
923
|
+
*/
|
|
924
|
+
var CorruptStoredDocumentError = class extends Error {
|
|
925
|
+
constructor(message, options) {
|
|
926
|
+
super(message, options);
|
|
927
|
+
this.name = "CorruptStoredDocumentError";
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
/** Whether a store failure means "this document does not parse", not "the store is unreachable". */
|
|
931
|
+
function isCorruptStoredDocument(error) {
|
|
932
|
+
return error instanceof CorruptStoredDocumentError;
|
|
933
|
+
}
|
|
934
|
+
function parseStoredCredentials(value) {
|
|
935
|
+
if (typeof value !== "object" || value === null) throw new Error("credential bundle is not an object");
|
|
936
|
+
const record = value;
|
|
937
|
+
if (typeof record.accessToken !== "string" || record.accessToken === "") throw new Error("access token is missing");
|
|
938
|
+
if (typeof record.refreshToken !== "string" || record.refreshToken === "") throw new Error("refresh token is missing");
|
|
939
|
+
if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)) throw new Error("expiry is invalid");
|
|
940
|
+
const optional = (key) => {
|
|
941
|
+
const candidate = record[key];
|
|
942
|
+
if (candidate === void 0) return void 0;
|
|
943
|
+
if (typeof candidate !== "string") throw new Error(`${key} is invalid`);
|
|
944
|
+
return candidate;
|
|
945
|
+
};
|
|
946
|
+
const credentials = {
|
|
947
|
+
accessToken: record.accessToken,
|
|
948
|
+
refreshToken: record.refreshToken,
|
|
949
|
+
expiresAt: record.expiresAt,
|
|
950
|
+
idToken: optional("idToken"),
|
|
951
|
+
accountId: optional("accountId"),
|
|
952
|
+
email: optional("email"),
|
|
953
|
+
planType: optional("planType")
|
|
954
|
+
};
|
|
955
|
+
if (record.adopted === true) {
|
|
956
|
+
const source = optional("source");
|
|
957
|
+
if (source !== void 0) {
|
|
958
|
+
credentials.adopted = true;
|
|
959
|
+
credentials.source = source;
|
|
960
|
+
const sourcePath = optional("sourcePath");
|
|
961
|
+
if (sourcePath !== void 0) credentials.sourcePath = sourcePath;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return credentials;
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
881
967
|
//#region src/host/common/credential-read-cache.ts
|
|
882
968
|
/**
|
|
883
969
|
* Identity a file-backed credential store reports when nothing is stored.
|
|
@@ -944,6 +1030,16 @@ var CredentialReadCache = class {
|
|
|
944
1030
|
entry = null;
|
|
945
1031
|
pending = null;
|
|
946
1032
|
/**
|
|
1033
|
+
* Bumped by every {@link invalidate}.
|
|
1034
|
+
*
|
|
1035
|
+
* A read that was already in flight when the value changed must not fill the
|
|
1036
|
+
* snapshot: with a 15-second keyring identity bucket, its bytes are the ones
|
|
1037
|
+
* that were just replaced, and the next `load()` would answer with a refresh
|
|
1038
|
+
* token the pool has already spent — up to fifteen seconds of exactly the
|
|
1039
|
+
* double spend the single-flight path in `freshAccount` exists to prevent.
|
|
1040
|
+
*/
|
|
1041
|
+
generation = 0;
|
|
1042
|
+
/**
|
|
947
1043
|
* @param identity - cheap identity of the stored value; `null` disables
|
|
948
1044
|
* caching for the read.
|
|
949
1045
|
*/
|
|
@@ -969,6 +1065,7 @@ var CredentialReadCache = class {
|
|
|
969
1065
|
return null;
|
|
970
1066
|
}
|
|
971
1067
|
if (this.pending !== null && this.pending.identity === identity) return this.pending.promise;
|
|
1068
|
+
const generation = this.generation;
|
|
972
1069
|
const promise = read();
|
|
973
1070
|
this.pending = {
|
|
974
1071
|
identity,
|
|
@@ -976,7 +1073,7 @@ var CredentialReadCache = class {
|
|
|
976
1073
|
};
|
|
977
1074
|
const settle = (value) => {
|
|
978
1075
|
if (this.pending !== null && this.pending.promise === promise) this.pending = null;
|
|
979
|
-
this.entry = {
|
|
1076
|
+
if (generation === this.generation) this.entry = {
|
|
980
1077
|
identity,
|
|
981
1078
|
value
|
|
982
1079
|
};
|
|
@@ -986,8 +1083,9 @@ var CredentialReadCache = class {
|
|
|
986
1083
|
});
|
|
987
1084
|
return promise;
|
|
988
1085
|
}
|
|
989
|
-
/** Drop the snapshot; the next read consults storage again. */
|
|
1086
|
+
/** Drop the snapshot; the next read consults storage again and in-flight ones stop filling it. */
|
|
990
1087
|
invalidate() {
|
|
1088
|
+
this.generation += 1;
|
|
991
1089
|
this.entry = null;
|
|
992
1090
|
this.pending = null;
|
|
993
1091
|
}
|
|
@@ -996,198 +1094,6 @@ function isMissing$1(error) {
|
|
|
996
1094
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
997
1095
|
}
|
|
998
1096
|
//#endregion
|
|
999
|
-
//#region src/host/token-store.ts
|
|
1000
|
-
function parseStoredCredentials(value) {
|
|
1001
|
-
if (typeof value !== "object" || value === null) throw new Error("credential bundle is not an object");
|
|
1002
|
-
const record = value;
|
|
1003
|
-
if (typeof record.accessToken !== "string" || record.accessToken === "") throw new Error("access token is missing");
|
|
1004
|
-
if (typeof record.refreshToken !== "string" || record.refreshToken === "") throw new Error("refresh token is missing");
|
|
1005
|
-
if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)) throw new Error("expiry is invalid");
|
|
1006
|
-
const optional = (key) => {
|
|
1007
|
-
const candidate = record[key];
|
|
1008
|
-
if (candidate === void 0) return void 0;
|
|
1009
|
-
if (typeof candidate !== "string") throw new Error(`${key} is invalid`);
|
|
1010
|
-
return candidate;
|
|
1011
|
-
};
|
|
1012
|
-
const credentials = {
|
|
1013
|
-
accessToken: record.accessToken,
|
|
1014
|
-
refreshToken: record.refreshToken,
|
|
1015
|
-
expiresAt: record.expiresAt,
|
|
1016
|
-
idToken: optional("idToken"),
|
|
1017
|
-
accountId: optional("accountId"),
|
|
1018
|
-
email: optional("email"),
|
|
1019
|
-
planType: optional("planType")
|
|
1020
|
-
};
|
|
1021
|
-
if (record.adopted === true) {
|
|
1022
|
-
const source = optional("source");
|
|
1023
|
-
if (source !== void 0) {
|
|
1024
|
-
credentials.adopted = true;
|
|
1025
|
-
credentials.source = source;
|
|
1026
|
-
const sourcePath = optional("sourcePath");
|
|
1027
|
-
if (sourcePath !== void 0) credentials.sourcePath = sourcePath;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
return credentials;
|
|
1031
|
-
}
|
|
1032
|
-
//#endregion
|
|
1033
|
-
//#region src/host/token-store-windows.ts
|
|
1034
|
-
const PROTECT_SCRIPT = String.raw`
|
|
1035
|
-
$ErrorActionPreference = 'Stop'
|
|
1036
|
-
[Console]::InputEncoding = [Text.Encoding]::UTF8
|
|
1037
|
-
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
1038
|
-
$OutputEncoding = [Text.Encoding]::UTF8
|
|
1039
|
-
Add-Type -AssemblyName System.Security
|
|
1040
|
-
$path = $env:DSH_CODEX_TOKEN_PATH
|
|
1041
|
-
$plain = [Console]::In.ReadToEnd()
|
|
1042
|
-
$bytes = [Text.Encoding]::UTF8.GetBytes($plain)
|
|
1043
|
-
$cipher = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
1044
|
-
$directory = [IO.Path]::GetDirectoryName($path)
|
|
1045
|
-
[IO.Directory]::CreateDirectory($directory) | Out-Null
|
|
1046
|
-
$temporary = $path + '.tmp-' + [Guid]::NewGuid().ToString('N')
|
|
1047
|
-
try {
|
|
1048
|
-
[IO.File]::WriteAllBytes($temporary, $cipher)
|
|
1049
|
-
if ([IO.File]::Exists($path)) {
|
|
1050
|
-
[IO.File]::Replace($temporary, $path, [System.Management.Automation.Language.NullString]::Value)
|
|
1051
|
-
} else {
|
|
1052
|
-
[IO.File]::Move($temporary, $path)
|
|
1053
|
-
}
|
|
1054
|
-
} finally {
|
|
1055
|
-
if ([IO.File]::Exists($temporary)) { [IO.File]::Delete($temporary) }
|
|
1056
|
-
}
|
|
1057
|
-
`;
|
|
1058
|
-
const UNPROTECT_SCRIPT = String.raw`
|
|
1059
|
-
$ErrorActionPreference = 'Stop'
|
|
1060
|
-
[Console]::InputEncoding = [Text.Encoding]::UTF8
|
|
1061
|
-
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
1062
|
-
$OutputEncoding = [Text.Encoding]::UTF8
|
|
1063
|
-
Add-Type -AssemblyName System.Security
|
|
1064
|
-
$path = $env:DSH_CODEX_TOKEN_PATH
|
|
1065
|
-
if (-not [IO.File]::Exists($path)) { exit 3 }
|
|
1066
|
-
$cipher = [IO.File]::ReadAllBytes($path)
|
|
1067
|
-
$bytes = [Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
1068
|
-
[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))
|
|
1069
|
-
`;
|
|
1070
|
-
const CLEAR_SCRIPT = String.raw`
|
|
1071
|
-
$ErrorActionPreference = 'Stop'
|
|
1072
|
-
$path = $env:DSH_CODEX_TOKEN_PATH
|
|
1073
|
-
if ([IO.File]::Exists($path)) { [IO.File]::Delete($path) }
|
|
1074
|
-
`;
|
|
1075
|
-
function defaultDpapiCredentialPath() {
|
|
1076
|
-
return join(process.env.DSH_HOME?.trim() || join(homedir(), ".dsh"), "storages", "dsh-chatgpt-subscription", "oauth.dpapi");
|
|
1077
|
-
}
|
|
1078
|
-
var WindowsDpapiCredentialStore = class {
|
|
1079
|
-
path;
|
|
1080
|
-
parse;
|
|
1081
|
-
storage = {
|
|
1082
|
-
kind: "windows-dpapi",
|
|
1083
|
-
encrypted: true
|
|
1084
|
-
};
|
|
1085
|
-
/**
|
|
1086
|
-
* Decrypted snapshot, validated against the file's own mtime and size.
|
|
1087
|
-
*
|
|
1088
|
-
* Every uncached read is a spawned `powershell.exe` (~200 ms), and one
|
|
1089
|
-
* settings request reads several credentials, so the snapshot is what keeps a
|
|
1090
|
-
* tab switch from costing seconds.
|
|
1091
|
-
*/
|
|
1092
|
-
cache = new CredentialReadCache(() => fileCredentialIdentity(this.path));
|
|
1093
|
-
constructor(path, parse) {
|
|
1094
|
-
this.path = path;
|
|
1095
|
-
this.parse = parse;
|
|
1096
|
-
if (process.platform !== "win32") throw new Error("Windows DPAPI storage requires Windows");
|
|
1097
|
-
if (dirname(path) === path) throw new Error("invalid DPAPI credential path");
|
|
1098
|
-
}
|
|
1099
|
-
async load() {
|
|
1100
|
-
return this.cache.load(async () => {
|
|
1101
|
-
const result = await runPowerShell(UNPROTECT_SCRIPT, this.path, "");
|
|
1102
|
-
if (result.code === 3) return null;
|
|
1103
|
-
if (result.code !== 0) throw new Error("DPAPI credential read failed");
|
|
1104
|
-
try {
|
|
1105
|
-
return this.parse(JSON.parse(result.stdout));
|
|
1106
|
-
} catch {
|
|
1107
|
-
throw new Error("DPAPI credential payload is invalid");
|
|
1108
|
-
}
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
async save(value) {
|
|
1112
|
-
if ((await runPowerShell(PROTECT_SCRIPT, this.path, JSON.stringify(value))).code !== 0) throw new Error("DPAPI credential write failed");
|
|
1113
|
-
this.cache.invalidate();
|
|
1114
|
-
}
|
|
1115
|
-
async clear() {
|
|
1116
|
-
if ((await runPowerShell(CLEAR_SCRIPT, this.path, "")).code !== 0) throw new Error("DPAPI credential deletion failed");
|
|
1117
|
-
this.cache.invalidate();
|
|
1118
|
-
}
|
|
1119
|
-
};
|
|
1120
|
-
var WindowsDpapiTokenStore = class extends WindowsDpapiCredentialStore {
|
|
1121
|
-
constructor(path = defaultDpapiCredentialPath()) {
|
|
1122
|
-
super(path, parseStoredCredentials);
|
|
1123
|
-
}
|
|
1124
|
-
};
|
|
1125
|
-
/**
|
|
1126
|
-
* How long a helper may run before it is killed and the read fails.
|
|
1127
|
-
*
|
|
1128
|
-
* A healthy spawn costs ~200 ms (see the cache note on this class), so the guard
|
|
1129
|
-
* exists only for a child that never exits, and it was set at 10 s - fifty times
|
|
1130
|
-
* the healthy cost. That turned out to be too tight for one real environment: a
|
|
1131
|
-
* cold Windows runner whose freshly-installed dependency tree is being scanned
|
|
1132
|
-
* on first spawn, where the helper legitimately took longer than 10 s and the
|
|
1133
|
-
* kill turned a working credential read into `DPAPI helper timed out`.
|
|
1134
|
-
*
|
|
1135
|
-
* The guard is kept, and only its headroom widened: 30 s is still bounded, still
|
|
1136
|
-
* far above anything a warm machine needs, and a hung helper now blocks one
|
|
1137
|
-
* credential read for 30 s rather than 10 - a worse wait in a case that is already
|
|
1138
|
-
* a failure, in exchange for not failing a machine that is merely cold.
|
|
1139
|
-
*/
|
|
1140
|
-
const POWERSHELL_HELPER_TIMEOUT_MS = 3e4;
|
|
1141
|
-
function runPowerShell(script, path, stdin) {
|
|
1142
|
-
return new Promise((resolve, reject) => {
|
|
1143
|
-
const child = spawn("powershell.exe", [
|
|
1144
|
-
"-NoLogo",
|
|
1145
|
-
"-NoProfile",
|
|
1146
|
-
"-NonInteractive",
|
|
1147
|
-
"-Command",
|
|
1148
|
-
script
|
|
1149
|
-
], {
|
|
1150
|
-
env: {
|
|
1151
|
-
...process.env,
|
|
1152
|
-
DSH_CODEX_TOKEN_PATH: path
|
|
1153
|
-
},
|
|
1154
|
-
stdio: [
|
|
1155
|
-
"pipe",
|
|
1156
|
-
"pipe",
|
|
1157
|
-
"pipe"
|
|
1158
|
-
],
|
|
1159
|
-
windowsHide: true
|
|
1160
|
-
});
|
|
1161
|
-
let stdout = "";
|
|
1162
|
-
let stderrLength = 0;
|
|
1163
|
-
const timer = setTimeout(() => {
|
|
1164
|
-
child.kill();
|
|
1165
|
-
reject(/* @__PURE__ */ new Error("DPAPI helper timed out"));
|
|
1166
|
-
}, POWERSHELL_HELPER_TIMEOUT_MS);
|
|
1167
|
-
child.stdout.setEncoding("utf8");
|
|
1168
|
-
child.stdout.on("data", (chunk) => {
|
|
1169
|
-
stdout += chunk;
|
|
1170
|
-
if (stdout.length > 1 << 20) child.kill();
|
|
1171
|
-
});
|
|
1172
|
-
child.stderr.on("data", (chunk) => {
|
|
1173
|
-
stderrLength += chunk.length;
|
|
1174
|
-
if (stderrLength > 1 << 20) child.kill();
|
|
1175
|
-
});
|
|
1176
|
-
child.once("error", (error) => {
|
|
1177
|
-
clearTimeout(timer);
|
|
1178
|
-
reject(error);
|
|
1179
|
-
});
|
|
1180
|
-
child.once("close", (code) => {
|
|
1181
|
-
clearTimeout(timer);
|
|
1182
|
-
resolve({
|
|
1183
|
-
code: code ?? 1,
|
|
1184
|
-
stdout
|
|
1185
|
-
});
|
|
1186
|
-
});
|
|
1187
|
-
child.stdin.end(stdin);
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
//#endregion
|
|
1191
1097
|
//#region src/host/token-store-macos.ts
|
|
1192
1098
|
const DEFAULT_SERVICE = "dsh-chatgpt-subscription";
|
|
1193
1099
|
const DEFAULT_ACCOUNT = "oauth";
|
|
@@ -1242,7 +1148,7 @@ var MacKeychainCredentialStore = class {
|
|
|
1242
1148
|
try {
|
|
1243
1149
|
return this.parse(parseSecurityPayload(result.stdout));
|
|
1244
1150
|
} catch {
|
|
1245
|
-
throw new
|
|
1151
|
+
throw new CorruptStoredDocumentError("Keychain credential payload is invalid");
|
|
1246
1152
|
}
|
|
1247
1153
|
});
|
|
1248
1154
|
}
|
|
@@ -1370,9 +1276,9 @@ function envelopeFrom(parsed) {
|
|
|
1370
1276
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
1371
1277
|
const marker = parsed[ENVELOPE_KEY];
|
|
1372
1278
|
if (marker === void 0) return null;
|
|
1373
|
-
if (typeof marker !== "object" || marker === null) throw new
|
|
1279
|
+
if (typeof marker !== "object" || marker === null) throw new CorruptStoredDocumentError(INVALID);
|
|
1374
1280
|
const { gen, chunks, bytes } = marker;
|
|
1375
|
-
if (typeof gen !== "string" || gen === "" || typeof chunks !== "number" || !Number.isInteger(chunks) || chunks < 1 || chunks > MAX_SHARDS || typeof bytes !== "number" || !Number.isInteger(bytes) || bytes < 0) throw new
|
|
1281
|
+
if (typeof gen !== "string" || gen === "" || typeof chunks !== "number" || !Number.isInteger(chunks) || chunks < 1 || chunks > MAX_SHARDS || typeof bytes !== "number" || !Number.isInteger(bytes) || bytes < 0) throw new CorruptStoredDocumentError(INVALID);
|
|
1376
1282
|
return {
|
|
1377
1283
|
gen,
|
|
1378
1284
|
chunks,
|
|
@@ -1390,6 +1296,18 @@ var SecretServiceCredentialStore = class {
|
|
|
1390
1296
|
this.account = account;
|
|
1391
1297
|
this.parse = parse;
|
|
1392
1298
|
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Label the desktop keyring manager shows for this entry.
|
|
1301
|
+
*
|
|
1302
|
+
* Derived from the entry's own `service` attribute: it used to be a hardcoded
|
|
1303
|
+
* 'DSH Antigravity OAuth' for every caller, so the ChatGPT, Claude, Qoder and
|
|
1304
|
+
* Minimax pools all appeared in seahorse under Antigravity's name and a user
|
|
1305
|
+
* could not tell which secret they were looking at. The label is display-only,
|
|
1306
|
+
* so entries stored under the old one keep working.
|
|
1307
|
+
*/
|
|
1308
|
+
get label() {
|
|
1309
|
+
return `DSH ${this.service}`;
|
|
1310
|
+
}
|
|
1393
1311
|
attributes() {
|
|
1394
1312
|
return [
|
|
1395
1313
|
"service",
|
|
@@ -1411,6 +1329,31 @@ var SecretServiceCredentialStore = class {
|
|
|
1411
1329
|
`${this.account}#${gen}#${index}`
|
|
1412
1330
|
];
|
|
1413
1331
|
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Whether this host has a usable Secret Service at all.
|
|
1334
|
+
*
|
|
1335
|
+
* One lookup is the whole question, and its ANSWER is not the point: an entry
|
|
1336
|
+
* that is not there exits 1 with empty stderr, which still proves the daemon
|
|
1337
|
+
* answered. A host with no `secret-tool`, no session bus, no
|
|
1338
|
+
* `org.freedesktop.secrets` or a locked keyring fails instead — by refusing the
|
|
1339
|
+
* spawn, or by exiting non-zero with a message. Never throws, because it is
|
|
1340
|
+
* asked by a fallback chain whose next step is the answer to "no".
|
|
1341
|
+
*/
|
|
1342
|
+
async ping() {
|
|
1343
|
+
try {
|
|
1344
|
+
const result = await runSecretTool(["lookup", ...this.attributes()]);
|
|
1345
|
+
if (result.code === 0 || result.code === 1 && !result.hasStderr) return { available: true };
|
|
1346
|
+
return {
|
|
1347
|
+
available: false,
|
|
1348
|
+
reason: UNAVAILABLE
|
|
1349
|
+
};
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
return {
|
|
1352
|
+
available: false,
|
|
1353
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1414
1357
|
async load() {
|
|
1415
1358
|
return this.cache.load(async () => {
|
|
1416
1359
|
const result = await runSecretTool(["lookup", ...this.attributes()]);
|
|
@@ -1420,18 +1363,18 @@ var SecretServiceCredentialStore = class {
|
|
|
1420
1363
|
try {
|
|
1421
1364
|
parsed = JSON.parse(result.stdout);
|
|
1422
1365
|
} catch {
|
|
1423
|
-
throw new
|
|
1366
|
+
throw new CorruptStoredDocumentError(INVALID);
|
|
1424
1367
|
}
|
|
1425
1368
|
const envelope = envelopeFrom(parsed);
|
|
1426
1369
|
if (envelope === null) try {
|
|
1427
1370
|
return this.parse(parsed);
|
|
1428
1371
|
} catch {
|
|
1429
|
-
throw new
|
|
1372
|
+
throw new CorruptStoredDocumentError(INVALID);
|
|
1430
1373
|
}
|
|
1431
1374
|
const pieces = [];
|
|
1432
1375
|
for (let index = 0; index < envelope.chunks; index += 1) {
|
|
1433
1376
|
const shard = await runSecretTool(["lookup", ...this.shardAttributes(envelope.gen, index)]);
|
|
1434
|
-
if (shard.code !== 0) throw new
|
|
1377
|
+
if (shard.code !== 0) throw new CorruptStoredDocumentError(INVALID);
|
|
1435
1378
|
pieces.push(shard.stdout);
|
|
1436
1379
|
}
|
|
1437
1380
|
try {
|
|
@@ -1439,7 +1382,7 @@ var SecretServiceCredentialStore = class {
|
|
|
1439
1382
|
if (decoded.length !== envelope.bytes) throw new Error("shard byte count mismatch");
|
|
1440
1383
|
return this.parse(JSON.parse(decoded.toString("utf8")));
|
|
1441
1384
|
} catch {
|
|
1442
|
-
throw new
|
|
1385
|
+
throw new CorruptStoredDocumentError(INVALID);
|
|
1443
1386
|
}
|
|
1444
1387
|
});
|
|
1445
1388
|
}
|
|
@@ -1450,7 +1393,7 @@ var SecretServiceCredentialStore = class {
|
|
|
1450
1393
|
if (bytes < SECRET_TOOL_STDIN_LIMIT) {
|
|
1451
1394
|
if ((await runSecretTool([
|
|
1452
1395
|
"store",
|
|
1453
|
-
"--label=
|
|
1396
|
+
"--label=" + this.label,
|
|
1454
1397
|
...this.attributes()
|
|
1455
1398
|
], payload)).code !== 0) throw new Error(UNAVAILABLE);
|
|
1456
1399
|
} else await this.saveChunked(payload, bytes);
|
|
@@ -1473,7 +1416,7 @@ var SecretServiceCredentialStore = class {
|
|
|
1473
1416
|
const piece = encoded.slice(index * SHARD_PAYLOAD_SIZE, (index + 1) * SHARD_PAYLOAD_SIZE);
|
|
1474
1417
|
if ((await runSecretTool([
|
|
1475
1418
|
"store",
|
|
1476
|
-
"--label=
|
|
1419
|
+
"--label=" + this.label,
|
|
1477
1420
|
...this.shardAttributes(gen, index)
|
|
1478
1421
|
], piece)).code !== 0) {
|
|
1479
1422
|
await this.clearShards(gen, index);
|
|
@@ -1487,7 +1430,7 @@ var SecretServiceCredentialStore = class {
|
|
|
1487
1430
|
};
|
|
1488
1431
|
if ((await runSecretTool([
|
|
1489
1432
|
"store",
|
|
1490
|
-
"--label=
|
|
1433
|
+
"--label=" + this.label,
|
|
1491
1434
|
...this.attributes()
|
|
1492
1435
|
], JSON.stringify({ [ENVELOPE_KEY]: envelope }))).code !== 0) {
|
|
1493
1436
|
await this.clearShards(gen, chunks);
|
|
@@ -1582,14 +1525,491 @@ function runSecretTool(args, stdin = "") {
|
|
|
1582
1525
|
child.stdin.end(stdin);
|
|
1583
1526
|
});
|
|
1584
1527
|
}
|
|
1528
|
+
//#endregion
|
|
1529
|
+
//#region src/host/token-store-windows.ts
|
|
1530
|
+
const PROTECT_SCRIPT = String.raw`
|
|
1531
|
+
$ErrorActionPreference = 'Stop'
|
|
1532
|
+
[Console]::InputEncoding = [Text.Encoding]::UTF8
|
|
1533
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
1534
|
+
$OutputEncoding = [Text.Encoding]::UTF8
|
|
1535
|
+
Add-Type -AssemblyName System.Security
|
|
1536
|
+
$path = $env:DSH_CODEX_TOKEN_PATH
|
|
1537
|
+
$plain = [Console]::In.ReadToEnd()
|
|
1538
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes($plain)
|
|
1539
|
+
$cipher = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
1540
|
+
$directory = [IO.Path]::GetDirectoryName($path)
|
|
1541
|
+
[IO.Directory]::CreateDirectory($directory) | Out-Null
|
|
1542
|
+
$temporary = $path + '.tmp-' + [Guid]::NewGuid().ToString('N')
|
|
1543
|
+
try {
|
|
1544
|
+
[IO.File]::WriteAllBytes($temporary, $cipher)
|
|
1545
|
+
if ([IO.File]::Exists($path)) {
|
|
1546
|
+
[IO.File]::Replace($temporary, $path, [System.Management.Automation.Language.NullString]::Value)
|
|
1547
|
+
} else {
|
|
1548
|
+
[IO.File]::Move($temporary, $path)
|
|
1549
|
+
}
|
|
1550
|
+
} finally {
|
|
1551
|
+
if ([IO.File]::Exists($temporary)) { [IO.File]::Delete($temporary) }
|
|
1552
|
+
}
|
|
1553
|
+
`;
|
|
1554
|
+
const UNPROTECT_SCRIPT = String.raw`
|
|
1555
|
+
$ErrorActionPreference = 'Stop'
|
|
1556
|
+
[Console]::InputEncoding = [Text.Encoding]::UTF8
|
|
1557
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
1558
|
+
$OutputEncoding = [Text.Encoding]::UTF8
|
|
1559
|
+
Add-Type -AssemblyName System.Security
|
|
1560
|
+
$path = $env:DSH_CODEX_TOKEN_PATH
|
|
1561
|
+
if (-not [IO.File]::Exists($path)) { exit 3 }
|
|
1562
|
+
$cipher = [IO.File]::ReadAllBytes($path)
|
|
1563
|
+
$bytes = [Security.Cryptography.ProtectedData]::Unprotect($cipher, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
1564
|
+
[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))
|
|
1565
|
+
`;
|
|
1566
|
+
const CLEAR_SCRIPT = String.raw`
|
|
1567
|
+
$ErrorActionPreference = 'Stop'
|
|
1568
|
+
$path = $env:DSH_CODEX_TOKEN_PATH
|
|
1569
|
+
if ([IO.File]::Exists($path)) { [IO.File]::Delete($path) }
|
|
1570
|
+
`;
|
|
1571
|
+
function defaultDpapiCredentialPath() {
|
|
1572
|
+
return join(process.env.DSH_HOME?.trim() || join(homedir(), ".dsh"), "storages", "dsh-chatgpt-subscription", "oauth.dpapi");
|
|
1573
|
+
}
|
|
1574
|
+
var WindowsDpapiCredentialStore = class {
|
|
1575
|
+
path;
|
|
1576
|
+
parse;
|
|
1577
|
+
storage = {
|
|
1578
|
+
kind: "windows-dpapi",
|
|
1579
|
+
encrypted: true
|
|
1580
|
+
};
|
|
1581
|
+
/**
|
|
1582
|
+
* Decrypted snapshot, validated against the file's own mtime and size.
|
|
1583
|
+
*
|
|
1584
|
+
* Every uncached read is a spawned `powershell.exe` (~200 ms), and one
|
|
1585
|
+
* settings request reads several credentials, so the snapshot is what keeps a
|
|
1586
|
+
* tab switch from costing seconds.
|
|
1587
|
+
*/
|
|
1588
|
+
cache = new CredentialReadCache(() => fileCredentialIdentity(this.path));
|
|
1589
|
+
constructor(path, parse) {
|
|
1590
|
+
this.path = path;
|
|
1591
|
+
this.parse = parse;
|
|
1592
|
+
if (process.platform !== "win32") throw new Error("Windows DPAPI storage requires Windows");
|
|
1593
|
+
if (dirname(path) === path) throw new Error("invalid DPAPI credential path");
|
|
1594
|
+
}
|
|
1595
|
+
async load() {
|
|
1596
|
+
return this.cache.load(async () => {
|
|
1597
|
+
const result = await runPowerShell(UNPROTECT_SCRIPT, this.path, "");
|
|
1598
|
+
if (result.code === 3) return null;
|
|
1599
|
+
if (result.code !== 0) throw new Error("DPAPI credential read failed");
|
|
1600
|
+
try {
|
|
1601
|
+
return this.parse(JSON.parse(result.stdout));
|
|
1602
|
+
} catch {
|
|
1603
|
+
throw new CorruptStoredDocumentError("DPAPI credential payload is invalid");
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
async save(value) {
|
|
1608
|
+
if ((await runPowerShell(PROTECT_SCRIPT, this.path, JSON.stringify(value))).code !== 0) throw new Error("DPAPI credential write failed");
|
|
1609
|
+
this.cache.invalidate();
|
|
1610
|
+
}
|
|
1611
|
+
async clear() {
|
|
1612
|
+
if ((await runPowerShell(CLEAR_SCRIPT, this.path, "")).code !== 0) throw new Error("DPAPI credential deletion failed");
|
|
1613
|
+
this.cache.invalidate();
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
var WindowsDpapiTokenStore = class extends WindowsDpapiCredentialStore {
|
|
1617
|
+
constructor(path = defaultDpapiCredentialPath()) {
|
|
1618
|
+
super(path, parseStoredCredentials);
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
/**
|
|
1622
|
+
* How long a helper may run before it is killed and the read fails.
|
|
1623
|
+
*
|
|
1624
|
+
* A healthy spawn costs ~200 ms (see the cache note on this class), so the guard
|
|
1625
|
+
* exists only for a child that never exits, and it was set at 10 s - fifty times
|
|
1626
|
+
* the healthy cost. That turned out to be too tight for one real environment: a
|
|
1627
|
+
* cold Windows runner whose freshly-installed dependency tree is being scanned
|
|
1628
|
+
* on first spawn, where the helper legitimately took longer than 10 s and the
|
|
1629
|
+
* kill turned a working credential read into `DPAPI helper timed out`.
|
|
1630
|
+
*
|
|
1631
|
+
* The guard is kept, and only its headroom widened: 30 s is still bounded, still
|
|
1632
|
+
* far above anything a warm machine needs, and a hung helper now blocks one
|
|
1633
|
+
* credential read for 30 s rather than 10 - a worse wait in a case that is already
|
|
1634
|
+
* a failure, in exchange for not failing a machine that is merely cold.
|
|
1635
|
+
*/
|
|
1636
|
+
const POWERSHELL_HELPER_TIMEOUT_MS = 3e4;
|
|
1637
|
+
function runPowerShell(script, path, stdin) {
|
|
1638
|
+
return new Promise((resolve, reject) => {
|
|
1639
|
+
const child = spawn("powershell.exe", [
|
|
1640
|
+
"-NoLogo",
|
|
1641
|
+
"-NoProfile",
|
|
1642
|
+
"-NonInteractive",
|
|
1643
|
+
"-Command",
|
|
1644
|
+
script
|
|
1645
|
+
], {
|
|
1646
|
+
env: {
|
|
1647
|
+
...process.env,
|
|
1648
|
+
DSH_CODEX_TOKEN_PATH: path
|
|
1649
|
+
},
|
|
1650
|
+
stdio: [
|
|
1651
|
+
"pipe",
|
|
1652
|
+
"pipe",
|
|
1653
|
+
"pipe"
|
|
1654
|
+
],
|
|
1655
|
+
windowsHide: true
|
|
1656
|
+
});
|
|
1657
|
+
let stdout = "";
|
|
1658
|
+
let stderrLength = 0;
|
|
1659
|
+
const timer = setTimeout(() => {
|
|
1660
|
+
child.kill();
|
|
1661
|
+
reject(/* @__PURE__ */ new Error("DPAPI helper timed out"));
|
|
1662
|
+
}, POWERSHELL_HELPER_TIMEOUT_MS);
|
|
1663
|
+
child.stdout.setEncoding("utf8");
|
|
1664
|
+
child.stdout.on("data", (chunk) => {
|
|
1665
|
+
stdout += chunk;
|
|
1666
|
+
if (stdout.length > 1 << 20) child.kill();
|
|
1667
|
+
});
|
|
1668
|
+
child.stderr.on("data", (chunk) => {
|
|
1669
|
+
stderrLength += chunk.length;
|
|
1670
|
+
if (stderrLength > 1 << 20) child.kill();
|
|
1671
|
+
});
|
|
1672
|
+
child.once("error", (error) => {
|
|
1673
|
+
clearTimeout(timer);
|
|
1674
|
+
reject(error);
|
|
1675
|
+
});
|
|
1676
|
+
child.once("close", (code) => {
|
|
1677
|
+
clearTimeout(timer);
|
|
1678
|
+
resolve({
|
|
1679
|
+
code: code ?? 1,
|
|
1680
|
+
stdout
|
|
1681
|
+
});
|
|
1682
|
+
});
|
|
1683
|
+
child.stdin.end(stdin);
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
//#endregion
|
|
1687
|
+
//#region src/host/common/credentials-compat.ts
|
|
1688
|
+
/**
|
|
1689
|
+
* One `<scope>/<id>` segment, exactly as the seam's own `credentialKey` requires.
|
|
1690
|
+
*
|
|
1691
|
+
* Copied from the shipped builder (`@deepseek-ai/dsh-credentials`,
|
|
1692
|
+
* `KEY_SEGMENT_PATTERN`) rather than invented: a key that this plugin builds and
|
|
1693
|
+
* the seam rejects is worse than no seam at all — the write succeeds, the record
|
|
1694
|
+
* document stops parsing on the next start, and every credential on the host
|
|
1695
|
+
* goes with it.
|
|
1696
|
+
*/
|
|
1697
|
+
const KEY_SEGMENT_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
1698
|
+
/** Whether one segment of a record key could be handed to the seam at all. */
|
|
1699
|
+
function isCredentialKeySegment(value) {
|
|
1700
|
+
return KEY_SEGMENT_PATTERN.test(value);
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* The seam, when `ctx.get('credentials')` returned something that is one.
|
|
1704
|
+
*
|
|
1705
|
+
* Takes the resolved SERVICE rather than the context: on a Cordis context a
|
|
1706
|
+
* service outside `inject` may only be read through `ctx.get`, so the lookup
|
|
1707
|
+
* belongs to the plugin entry (`src/index.ts`) and this function only decides
|
|
1708
|
+
* whether what came back is the record half the pool can use.
|
|
1709
|
+
*
|
|
1710
|
+
* Verified by shape, not by `!== undefined`: a host may carry a name that is not
|
|
1711
|
+
* this seam, and a service answering half the calls would fail in the middle of a
|
|
1712
|
+
* save — after the token exchange, which is exactly the failure the `poolStorage`
|
|
1713
|
+
* option exists to remove.
|
|
1714
|
+
*/
|
|
1715
|
+
function asCredentialsSeam(service) {
|
|
1716
|
+
if (typeof service !== "object" || service === null) return void 0;
|
|
1717
|
+
const candidate = service;
|
|
1718
|
+
if (typeof candidate.readRecord !== "function" || typeof candidate.modifyRecord !== "function" || typeof candidate.deleteRecord !== "function") return;
|
|
1719
|
+
return candidate;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Address of one pool document inside the seam's record space.
|
|
1723
|
+
*
|
|
1724
|
+
* Scoped by plugin rather than by provider line: a record's payload is written in
|
|
1725
|
+
* its owner's format, so two lines of this plugin may share the scope and stay
|
|
1726
|
+
* apart by id.
|
|
1727
|
+
*
|
|
1728
|
+
* Both segments are validated HERE, by the seam's own rule, because the seam's
|
|
1729
|
+
* write path does not validate them: a key it would reject on the next start
|
|
1730
|
+
* (`parseCredentialKey` runs in `parseRecords`, and an invalid one fails the
|
|
1731
|
+
* provider's activation) is a key that must never be written at all. A caller
|
|
1732
|
+
* that cannot build a legal id gets a `TypeError` now, in the line that named it,
|
|
1733
|
+
* instead of a credential document that stops loading after a restart.
|
|
1734
|
+
*/
|
|
1735
|
+
function credentialsRecordKey(scope, id) {
|
|
1736
|
+
for (const segment of [scope, id]) if (!isCredentialKeySegment(segment)) throw new TypeError(`credential key segment "${segment}" must match ${String(KEY_SEGMENT_PATTERN)}`);
|
|
1737
|
+
return `${scope}/${id}`;
|
|
1738
|
+
}
|
|
1739
|
+
//#endregion
|
|
1740
|
+
//#region src/host/common/pool-storage.ts
|
|
1741
|
+
/**
|
|
1742
|
+
* Which store holds a provider line's account pool.
|
|
1743
|
+
*
|
|
1744
|
+
* The pool document is one JSON object per line. The store used to be decided by
|
|
1745
|
+
* `process.platform` alone: DPAPI on Windows, Keychain on macOS, and on Linux the
|
|
1746
|
+
* Secret Service — which a headless server does not have. There the token exchange
|
|
1747
|
+
* succeeded, saving failed, and the account could not be added at all; the only
|
|
1748
|
+
* workaround was running `gnome-keyring` unlocked with an empty password, which
|
|
1749
|
+
* protects the keyring file by permissions and nothing else.
|
|
1750
|
+
*
|
|
1751
|
+
* So the store becomes a choice with exactly two values:
|
|
1752
|
+
*
|
|
1753
|
+
* - `keyring` (default) — the platform store: DPAPI on Windows, the Keychain on
|
|
1754
|
+
* macOS, the Secret Service on Linux. Every existing install has this, and
|
|
1755
|
+
* nobody who never touches the setting sees a change.
|
|
1756
|
+
* - `credentials` — DSH's own credential store, through `ctx.credentials`. The
|
|
1757
|
+
* option a server install should use: no external daemon, one store for every
|
|
1758
|
+
* secret the host holds.
|
|
1759
|
+
*
|
|
1760
|
+
* There is deliberately NO automatic mode. An `auto` that picks by probing would
|
|
1761
|
+
* let one pool move between two stores depending on whether a daemon happened to
|
|
1762
|
+
* answer, and a pool that moves looks exactly like a pool whose accounts were
|
|
1763
|
+
* lost. A wrong choice is reported instead — the card names the store in use, says
|
|
1764
|
+
* why the configured one is unusable and offers the switch — and the accounts that
|
|
1765
|
+
* stayed behind in the other store are still there.
|
|
1766
|
+
*/
|
|
1767
|
+
/** Store a deployment gets when no `poolStorage` is configured anywhere. */
|
|
1768
|
+
const DEFAULT_POOL_STORAGE_MODE = "keyring";
|
|
1769
|
+
/** Record scope the pool document occupies in DSH's credential store. */
|
|
1770
|
+
const POOL_RECORD_SCOPE = "dsh-chatgpt-subscription";
|
|
1771
|
+
/**
|
|
1772
|
+
* Record id prefix for one pool: `<scope>/pool-<digest>`.
|
|
1773
|
+
*
|
|
1774
|
+
* The prefix is load-bearing, not decoration. The seam requires each key segment
|
|
1775
|
+
* to match `/^[a-z][a-z0-9-]*$/` while a pool's id is a sha256 hex digest, and
|
|
1776
|
+
* 62.5% of digests start with a digit — those keys are rejected on the NEXT start,
|
|
1777
|
+
* when the record document is parsed, and an invalid key fails the whole provider's
|
|
1778
|
+
* activation: the DeepSeek key, the Telegram token and every other secret in
|
|
1779
|
+
* `$DSH_HOME/.credentials.yaml` become unreadable. `pool-` puts a letter first and
|
|
1780
|
+
* leaves the digest untouched, so the address stays stable and the Secret Service
|
|
1781
|
+
* entry (which uses the bare digest as its account attribute) is unaffected.
|
|
1782
|
+
*/
|
|
1783
|
+
function poolRecordKey(account) {
|
|
1784
|
+
return credentialsRecordKey(POOL_RECORD_SCOPE, `pool-${account}`);
|
|
1785
|
+
}
|
|
1786
|
+
let defaults = { mode: () => DEFAULT_POOL_STORAGE_MODE };
|
|
1787
|
+
/**
|
|
1788
|
+
* Revision of the deployment-wide choice.
|
|
1789
|
+
*
|
|
1790
|
+
* Every resolved pool remembers the revision it resolved under and re-resolves
|
|
1791
|
+
* when this moves. Without it, switching the store in the settings card would keep
|
|
1792
|
+
* serving the old backend until the host restarted: the resolution is deliberately
|
|
1793
|
+
* cached — the decision must not be retaken per request — so the switch has to
|
|
1794
|
+
* invalidate it explicitly.
|
|
1795
|
+
*/
|
|
1796
|
+
let revision = 0;
|
|
1797
|
+
/** Record the deployment-wide storage choice; call once, before any pool is built. */
|
|
1798
|
+
function configurePoolStorage(next) {
|
|
1799
|
+
defaults = next.mode === void 0 ? {
|
|
1800
|
+
...next,
|
|
1801
|
+
mode: () => DEFAULT_POOL_STORAGE_MODE
|
|
1802
|
+
} : next;
|
|
1803
|
+
revision += 1;
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Announce that the stored choice changed.
|
|
1807
|
+
*
|
|
1808
|
+
* Called by the settings route after the new value is on disk; every pool drops
|
|
1809
|
+
* the backend it cached and resolves again on its next read.
|
|
1810
|
+
*/
|
|
1811
|
+
function poolStorageChanged() {
|
|
1812
|
+
revision += 1;
|
|
1813
|
+
}
|
|
1814
|
+
/** The revision the current choice stands at. */
|
|
1815
|
+
function poolStorageRevision() {
|
|
1816
|
+
return revision;
|
|
1817
|
+
}
|
|
1818
|
+
const BACKEND_LABELS = {
|
|
1819
|
+
keyring: "desktop keyring (Secret Service)",
|
|
1820
|
+
credentials: "DSH credential store",
|
|
1821
|
+
dpapi: "Windows DPAPI",
|
|
1822
|
+
keychain: "macOS Keychain"
|
|
1823
|
+
};
|
|
1824
|
+
/** Whether the backend encrypts the document at rest. */
|
|
1825
|
+
function poolStorageEncrypted(backend) {
|
|
1826
|
+
return backend === "keyring" || backend === "dpapi" || backend === "keychain";
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* The pool document as a record in DSH's own credential store.
|
|
1830
|
+
*
|
|
1831
|
+
* Stored through the seam's record half (see `credentials-compat.ts`): the
|
|
1832
|
+
* document is this plugin's own format, and a `grant` payload is exactly the slot
|
|
1833
|
+
* the seam keeps for a value only its owner can interpret.
|
|
1834
|
+
*
|
|
1835
|
+
* Writes are LAST-WRITER-WINS, and honestly so. `modifyRecord` is the seam's only
|
|
1836
|
+
* write path and it does hand back the record as it stands under its lock, but a
|
|
1837
|
+
* whole pool document is written from one in-memory copy: a second process that
|
|
1838
|
+
* saved between this process's read and this write is overwritten. The lock makes
|
|
1839
|
+
* the write atomic, not the transaction. This is the same guarantee the keyring
|
|
1840
|
+
* backend has always given, and exactly what the `keyring` option gives — one
|
|
1841
|
+
* process is the normal case (one `DSH_HOME`, one host), and it is stated here
|
|
1842
|
+
* rather than implied away.
|
|
1843
|
+
*/
|
|
1844
|
+
var CredentialsRecordStore = class {
|
|
1845
|
+
seam;
|
|
1846
|
+
key;
|
|
1847
|
+
parse;
|
|
1848
|
+
constructor(seam, key, parse) {
|
|
1849
|
+
this.seam = seam;
|
|
1850
|
+
this.key = key;
|
|
1851
|
+
this.parse = parse;
|
|
1852
|
+
}
|
|
1853
|
+
/** The `<scope>/<id>` address this pool occupies; a location, never a credential. */
|
|
1854
|
+
recordKey() {
|
|
1855
|
+
return this.key;
|
|
1856
|
+
}
|
|
1857
|
+
async load() {
|
|
1858
|
+
const record = await this.seam.readRecord(this.key);
|
|
1859
|
+
if (record === void 0) return null;
|
|
1860
|
+
if (record.kind !== "grant") throw new CorruptStoredDocumentError("pool record is not a grant");
|
|
1861
|
+
try {
|
|
1862
|
+
return this.parse(record.payload);
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
throw new CorruptStoredDocumentError("DSH credential store pool payload is invalid", { cause: error });
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
async save(value) {
|
|
1868
|
+
const payload = JSON.parse(JSON.stringify(value));
|
|
1869
|
+
await this.seam.modifyRecord(this.key, async () => ({
|
|
1870
|
+
kind: "grant",
|
|
1871
|
+
payload
|
|
1872
|
+
}));
|
|
1873
|
+
}
|
|
1874
|
+
async clear() {
|
|
1875
|
+
await this.seam.deleteRecord(this.key);
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
/**
|
|
1879
|
+
* A backend that cannot be used on this host, kept so the failure is reported with
|
|
1880
|
+
* its reason instead of being replaced by a silent substitution.
|
|
1881
|
+
*
|
|
1882
|
+
* `poolStorage` is an instruction. Answering it with a different store would make
|
|
1883
|
+
* the settings card describe a backend the user did not choose, and — worse —
|
|
1884
|
+
* would spread one deployment's accounts across two stores without saying so.
|
|
1885
|
+
* Every operation therefore fails with the sentence the card also shows, and
|
|
1886
|
+
* `unavailable` tells the card which reason code to translate.
|
|
1887
|
+
*/
|
|
1888
|
+
var UnusablePoolStore = class {
|
|
1889
|
+
message;
|
|
1890
|
+
constructor(message) {
|
|
1891
|
+
this.message = message;
|
|
1892
|
+
}
|
|
1893
|
+
async load() {
|
|
1894
|
+
throw new Error(this.message);
|
|
1895
|
+
}
|
|
1896
|
+
async save() {
|
|
1897
|
+
throw new Error(this.message);
|
|
1898
|
+
}
|
|
1899
|
+
async clear() {
|
|
1900
|
+
throw new Error(this.message);
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
/** What the user is told when `keyring` is configured and there is no keyring. */
|
|
1904
|
+
const KEYRING_UNREACHABLE = "poolStorage is \"keyring\", but this host has no reachable desktop keyring: the Secret Service does not answer (no secret-tool, no session bus, or a locked keyring). Switch \"Account storage\" to the DSH credential store, or set poolStorage: \"credentials\".";
|
|
1905
|
+
/** What the user is told when `credentials` is configured and the harness has no such seam. */
|
|
1906
|
+
const NO_CREDENTIALS_SERVICE = "poolStorage is \"credentials\", but this harness has no ctx.credentials service, so there is no DSH credential store to write into. Switch \"Account storage\" to the desktop keyring, or set poolStorage: \"keyring\".";
|
|
1907
|
+
/**
|
|
1908
|
+
* Resolve the pool store for one line.
|
|
1909
|
+
*
|
|
1910
|
+
* Async because `credentials` availability is a runtime fact and the configured
|
|
1911
|
+
* value may still be loading from disk; callers hold the promise rather than the
|
|
1912
|
+
* store (see `AccountPoolCore`).
|
|
1913
|
+
*/
|
|
1914
|
+
async function resolvePoolStorage(request) {
|
|
1915
|
+
const platform = request.platform ?? process.platform;
|
|
1916
|
+
const logger = request.logger ?? defaults.logger ?? console;
|
|
1917
|
+
await defaults.ready?.();
|
|
1918
|
+
const mode = request.mode ?? defaults.mode?.() ?? "keyring";
|
|
1919
|
+
const seam = request.credentials ?? defaults.credentials?.();
|
|
1920
|
+
if (mode === "credentials") {
|
|
1921
|
+
if (seam === void 0) {
|
|
1922
|
+
logger.warn(`[dsh-chatgpt-subscription] ${NO_CREDENTIALS_SERVICE}`);
|
|
1923
|
+
return unusable(mode, "credentials", "no-credentials-service", NO_CREDENTIALS_SERVICE);
|
|
1924
|
+
}
|
|
1925
|
+
const store = new CredentialsRecordStore(seam, poolRecordKey(request.account), request.parsePoolData);
|
|
1926
|
+
logger.info(`[dsh-chatgpt-subscription] account pool stored in ${BACKEND_LABELS.credentials}`);
|
|
1927
|
+
return resolved(store, mode, "credentials");
|
|
1928
|
+
}
|
|
1929
|
+
if (platform === "win32" || platform === "darwin") {
|
|
1930
|
+
const backend = platform === "win32" ? "dpapi" : "keychain";
|
|
1931
|
+
const store = request.desktopStore ?? (platform === "win32" ? new WindowsDpapiCredentialStore(`${request.poolFile}.dpapi`, request.parsePoolData) : new MacKeychainCredentialStore(request.keychainService, request.account, request.parsePoolData));
|
|
1932
|
+
logger.info(`[dsh-chatgpt-subscription] account pool stored in ${BACKEND_LABELS[backend]}`);
|
|
1933
|
+
return resolved(store, mode, backend);
|
|
1934
|
+
}
|
|
1935
|
+
if (platform !== "linux") throw new Error(`${request.displayName} pool storage requires Windows, macOS, or Linux.`);
|
|
1936
|
+
const keyring = request.keyring ?? new SecretServiceCredentialStore(request.keychainService, request.account, request.parsePoolData);
|
|
1937
|
+
const availability = await probeKeyring(keyring);
|
|
1938
|
+
if (!availability.available) {
|
|
1939
|
+
const reason = availability.reason === void 0 ? KEYRING_UNREACHABLE : `${KEYRING_UNREACHABLE} (${availability.reason})`;
|
|
1940
|
+
logger.warn(`[dsh-chatgpt-subscription] ${reason}`);
|
|
1941
|
+
return unusable(mode, "keyring", "keyring-unreachable", reason);
|
|
1942
|
+
}
|
|
1943
|
+
logger.info(`[dsh-chatgpt-subscription] account pool stored in ${BACKEND_LABELS.keyring}`);
|
|
1944
|
+
return resolved(keyring, mode, "keyring");
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* The store a caller-supplied backend stands in for, on this platform.
|
|
1948
|
+
*
|
|
1949
|
+
* `AccountPoolHooks.backend` replaces the STORE, not the choice, and adoption
|
|
1950
|
+
* harnesses use it without a `storage` to go with it. Falling back to
|
|
1951
|
+
* `{ backend: 'keyring', encrypted: false }` there described a backend that cannot
|
|
1952
|
+
* exist (the platform keyring is encrypted on every platform it exists on) and the
|
|
1953
|
+
* wrong one on Windows or macOS.
|
|
1954
|
+
*/
|
|
1955
|
+
function poolStorageBackendForPlatform(platform) {
|
|
1956
|
+
if (platform === "win32") return "dpapi";
|
|
1957
|
+
if (platform === "darwin") return "keychain";
|
|
1958
|
+
return "keyring";
|
|
1959
|
+
}
|
|
1960
|
+
function resolved(store, mode, backend) {
|
|
1961
|
+
return {
|
|
1962
|
+
store,
|
|
1963
|
+
storage: {
|
|
1964
|
+
mode,
|
|
1965
|
+
backend,
|
|
1966
|
+
encrypted: poolStorageEncrypted(backend)
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
function unusable(mode, backend, unavailable, message) {
|
|
1971
|
+
return {
|
|
1972
|
+
store: new UnusablePoolStore(`ChatGPT account pool storage is unavailable: ${message}`),
|
|
1973
|
+
storage: {
|
|
1974
|
+
mode,
|
|
1975
|
+
backend,
|
|
1976
|
+
encrypted: poolStorageEncrypted(backend),
|
|
1977
|
+
unavailable
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
/** Whether the keyring answers at all; a backend without a probe is assumed to. */
|
|
1982
|
+
async function probeKeyring(keyring) {
|
|
1983
|
+
if (typeof keyring.ping !== "function") return { available: true };
|
|
1984
|
+
return keyring.ping();
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Human description of where a resolved pool lives, for diagnostics.
|
|
1988
|
+
*
|
|
1989
|
+
* A location, never a credential: the sentence the pool printed before, now
|
|
1990
|
+
* derived from the backend that actually won.
|
|
1991
|
+
*/
|
|
1992
|
+
function describePoolStoragePath(platform, poolFile, storage, keychainService, account) {
|
|
1993
|
+
if (storage.backend === "dpapi") return `${poolFile}.dpapi`;
|
|
1994
|
+
if (storage.backend === "credentials") return `credentials://${poolRecordKey(account)}`;
|
|
1995
|
+
return `${platform === "darwin" ? "Keychain" : "Secret Service"}: ${keychainService}/${account}`;
|
|
1996
|
+
}
|
|
1585
1997
|
/** Shortest cooldown a 429 may impose; a provider asking for less is clamped. */
|
|
1586
1998
|
const MIN_COOLDOWN_MS = 1e4;
|
|
1587
|
-
/** Stable keychain account name for one pool file, isolated per `DSH_HOME`. */
|
|
1588
1999
|
/** Accept a stored rotation strategy, or fall back to sequential. */
|
|
1589
2000
|
function normalizeRotationStrategy(value) {
|
|
1590
2001
|
return value === "round-robin" || value === "sticky" ? value : "sequential";
|
|
1591
2002
|
}
|
|
1592
|
-
|
|
2003
|
+
/**
|
|
2004
|
+
* Stable keychain account name for one pool file, isolated per `DSH_HOME`.
|
|
2005
|
+
*
|
|
2006
|
+
* A bare sha256 hex digest: that is what the Secret Service entry, the DPAPI file
|
|
2007
|
+
* suffix and the Keychain account all address, and what survives a restart. It is
|
|
2008
|
+
* NOT a legal credential-record id on its own — see `poolRecordKey` in
|
|
2009
|
+
* `pool-storage.ts`, which prefixes it — so nothing may hand this value to the
|
|
2010
|
+
* `ctx.credentials` seam directly.
|
|
2011
|
+
*/
|
|
2012
|
+
function poolCredentialAccount(filePath) {
|
|
1593
2013
|
return createHash("sha256").update(path.resolve(filePath)).digest("hex");
|
|
1594
2014
|
}
|
|
1595
2015
|
const poolOperations = /* @__PURE__ */ new Map();
|
|
@@ -1614,7 +2034,11 @@ var PoolAccountUnavailableError = class extends LlmError {};
|
|
|
1614
2034
|
var AccountPoolCore = class {
|
|
1615
2035
|
hooks;
|
|
1616
2036
|
filePath;
|
|
1617
|
-
backend
|
|
2037
|
+
/** The resolved backend, and the promise that resolves it (see {@link backendStore}). */
|
|
2038
|
+
backendRef = {};
|
|
2039
|
+
backendPromise;
|
|
2040
|
+
/** Choice revision {@link backendPromise} was resolved under. */
|
|
2041
|
+
backendRevision = -1;
|
|
1618
2042
|
/**
|
|
1619
2043
|
* Bumped by every write that can move which account serves a request.
|
|
1620
2044
|
*
|
|
@@ -1629,7 +2053,6 @@ var AccountPoolCore = class {
|
|
|
1629
2053
|
constructor(hooks) {
|
|
1630
2054
|
this.hooks = hooks;
|
|
1631
2055
|
this.filePath = hooks.poolFile;
|
|
1632
|
-
this.backend = hooks.backend ?? createPoolBackend(hooks);
|
|
1633
2056
|
const resolved = path.resolve(this.filePath);
|
|
1634
2057
|
this.operationKey = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
1635
2058
|
}
|
|
@@ -1649,10 +2072,71 @@ var AccountPoolCore = class {
|
|
|
1649
2072
|
currentIdentityRevision() {
|
|
1650
2073
|
return poolIdentityRevisions.get(this.operationKey) ?? 0;
|
|
1651
2074
|
}
|
|
1652
|
-
/**
|
|
2075
|
+
/**
|
|
2076
|
+
* Human description of where this pool's credentials live.
|
|
2077
|
+
*
|
|
2078
|
+
* Derived from the backend that was resolved, not from the platform: on a
|
|
2079
|
+
* server that answered `poolStorage` with the credential store or a file, the
|
|
2080
|
+
* old sentence named a keyring entry that does not exist.
|
|
2081
|
+
*/
|
|
1653
2082
|
path() {
|
|
1654
|
-
|
|
1655
|
-
return
|
|
2083
|
+
const selected = this.backendRef.current;
|
|
2084
|
+
return selected === void 0 ? defaultPoolStoragePath(process.platform, this.filePath, this.hooks.keychainService) : describePoolStoragePath(process.platform, this.filePath, selected.storage, this.hooks.keychainService, poolCredentialAccount(this.filePath));
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* The backend holding this pool, and why it was chosen, for the settings surface.
|
|
2088
|
+
*
|
|
2089
|
+
* Resolves the store on first call, exactly as a read does: a card opened before
|
|
2090
|
+
* any request has to be able to answer "where are my accounts?" as well.
|
|
2091
|
+
*/
|
|
2092
|
+
async poolStorage() {
|
|
2093
|
+
return (await this.backendStore()).storage;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* The pool store: resolved once, and re-resolved only when the setting changes.
|
|
2097
|
+
*
|
|
2098
|
+
* Lazy because the choice is a question about the host — does the keyring answer
|
|
2099
|
+
* — and a pool is built from a constructor, which cannot await. CACHED for the
|
|
2100
|
+
* life of the process on purpose: a read that re-detected the store on every
|
|
2101
|
+
* model request would pay a `secret-tool` launch per request, and could hand two
|
|
2102
|
+
* requests of one turn two different stores. The only thing that invalidates the
|
|
2103
|
+
* cache is {@link poolStorageRevision} moving, which happens when the settings
|
|
2104
|
+
* card writes a new choice.
|
|
2105
|
+
*/
|
|
2106
|
+
backendStore() {
|
|
2107
|
+
const revision = poolStorageRevision();
|
|
2108
|
+
if (this.backendPromise === void 0 || this.backendRevision !== revision) {
|
|
2109
|
+
this.backendRevision = revision;
|
|
2110
|
+
this.backendPromise = this.resolveBackend();
|
|
2111
|
+
}
|
|
2112
|
+
return this.backendPromise;
|
|
2113
|
+
}
|
|
2114
|
+
async resolveBackend() {
|
|
2115
|
+
if (this.hooks.backend !== void 0) {
|
|
2116
|
+
const backend = poolStorageBackendForPlatform(this.hooks.platform ?? process.platform);
|
|
2117
|
+
const resolution = {
|
|
2118
|
+
store: this.hooks.backend,
|
|
2119
|
+
storage: this.hooks.storage ?? {
|
|
2120
|
+
mode: this.hooks.poolStorage ?? "keyring",
|
|
2121
|
+
backend,
|
|
2122
|
+
encrypted: poolStorageEncrypted(backend)
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
this.backendRef.current = resolution;
|
|
2126
|
+
return resolution;
|
|
2127
|
+
}
|
|
2128
|
+
const resolution = await resolvePoolStorage({
|
|
2129
|
+
displayName: this.hooks.displayName,
|
|
2130
|
+
...this.hooks.poolStorage === void 0 ? {} : { mode: this.hooks.poolStorage },
|
|
2131
|
+
poolFile: this.filePath,
|
|
2132
|
+
keychainService: this.hooks.keychainService,
|
|
2133
|
+
account: poolCredentialAccount(this.filePath),
|
|
2134
|
+
parsePoolData: this.hooks.parsePoolData,
|
|
2135
|
+
...this.hooks.platform === void 0 ? {} : { platform: this.hooks.platform },
|
|
2136
|
+
...this.hooks.keyring === void 0 ? {} : { keyring: this.hooks.keyring }
|
|
2137
|
+
});
|
|
2138
|
+
this.backendRef.current = resolution;
|
|
2139
|
+
return resolution;
|
|
1656
2140
|
}
|
|
1657
2141
|
/**
|
|
1658
2142
|
* Queue one storage transaction for this pool file.
|
|
@@ -1672,8 +2156,12 @@ var AccountPoolCore = class {
|
|
|
1672
2156
|
}
|
|
1673
2157
|
async saveVerified(data) {
|
|
1674
2158
|
const normalized = this.hooks.parsePoolData(data);
|
|
1675
|
-
await this.
|
|
1676
|
-
|
|
2159
|
+
const { store } = await this.backendStore();
|
|
2160
|
+
await store.load().catch((error) => {
|
|
2161
|
+
if (!isCorruptStoredDocument(error)) throw error;
|
|
2162
|
+
});
|
|
2163
|
+
await store.save(normalized);
|
|
2164
|
+
if (!isDeepStrictEqual(await store.load(), normalized)) throw new Error(`${this.hooks.displayName} pool storage verification failed`);
|
|
1677
2165
|
}
|
|
1678
2166
|
/**
|
|
1679
2167
|
* Read the pool.
|
|
@@ -1695,9 +2183,13 @@ var AccountPoolCore = class {
|
|
|
1695
2183
|
*/
|
|
1696
2184
|
async loadPoolData() {
|
|
1697
2185
|
let current = null;
|
|
2186
|
+
let resolution;
|
|
1698
2187
|
try {
|
|
1699
|
-
|
|
1700
|
-
|
|
2188
|
+
resolution = await this.backendStore();
|
|
2189
|
+
current = await resolution.store.load();
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
if (isCorruptStoredDocument(error)) {} else if (resolution?.storage.unavailable === void 0) throw error;
|
|
2192
|
+
}
|
|
1701
2193
|
if (current !== null && Array.isArray(current.accounts) && current.accounts.length > 0) return current;
|
|
1702
2194
|
try {
|
|
1703
2195
|
const legacy = await this.hooks.legacyAccount?.();
|
|
@@ -2203,13 +2695,16 @@ var AccountPoolCore = class {
|
|
|
2203
2695
|
};
|
|
2204
2696
|
}
|
|
2205
2697
|
};
|
|
2206
|
-
/**
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2698
|
+
/**
|
|
2699
|
+
* Where the pool would live on this platform before anything is resolved.
|
|
2700
|
+
*
|
|
2701
|
+
* Only used for the diagnostic sentence while the backend is still being
|
|
2702
|
+
* resolved; {@link AccountPoolCore.path} switches to the real answer afterwards.
|
|
2703
|
+
*/
|
|
2704
|
+
function defaultPoolStoragePath(platform, filePath, keychainService) {
|
|
2705
|
+
if (platform === "win32") return `${filePath}.dpapi`;
|
|
2706
|
+
if (platform === "darwin") return `Keychain: ${keychainService}/${poolCredentialAccount(filePath)}`;
|
|
2707
|
+
return `Secret Service: ${keychainService}/${poolCredentialAccount(filePath)}`;
|
|
2213
2708
|
}
|
|
2214
2709
|
//#endregion
|
|
2215
2710
|
//#region src/host/token-store-linux.ts
|
|
@@ -2252,7 +2747,7 @@ var LinuxFileTokenStore = class {
|
|
|
2252
2747
|
const payload = await handle.readFile({ encoding: "utf8" });
|
|
2253
2748
|
return parseStoredCredentials(JSON.parse(payload));
|
|
2254
2749
|
} catch (error) {
|
|
2255
|
-
throw new
|
|
2750
|
+
throw new CorruptStoredDocumentError("Linux credential payload is invalid or insecure", { cause: error });
|
|
2256
2751
|
} finally {
|
|
2257
2752
|
await handle.close();
|
|
2258
2753
|
}
|
|
@@ -2546,6 +3041,18 @@ var OAuthService = class {
|
|
|
2546
3041
|
loginEvents = /* @__PURE__ */ new Map();
|
|
2547
3042
|
listeners = /* @__PURE__ */ new Map();
|
|
2548
3043
|
activeLogin = null;
|
|
3044
|
+
/**
|
|
3045
|
+
* True between taking the login slot and having something to put in
|
|
3046
|
+
* {@link activeLogin}.
|
|
3047
|
+
*
|
|
3048
|
+
* Both starts have work to do before the login exists (a storage read, the
|
|
3049
|
+
* device grant), and that work is awaited. A check made before the first
|
|
3050
|
+
* `await` and an assignment made after it do not exclude each other: two
|
|
3051
|
+
* overlapping calls both pass the check, the second overwrites the first, and
|
|
3052
|
+
* the first flow's `AbortController` becomes unreachable — Cancel stops the
|
|
3053
|
+
* wrong flow and the abandoned one keeps polling until its grant expires.
|
|
3054
|
+
*/
|
|
3055
|
+
startPending = false;
|
|
2549
3056
|
refreshPromise = null;
|
|
2550
3057
|
lastLoginError;
|
|
2551
3058
|
disposed = false;
|
|
@@ -2593,19 +3100,55 @@ var OAuthService = class {
|
|
|
2593
3100
|
accounts: [],
|
|
2594
3101
|
rotationStrategy: "sequential"
|
|
2595
3102
|
};
|
|
2596
|
-
|
|
3103
|
+
let readFailed = false;
|
|
3104
|
+
const [accounts, data, poolStorage] = await Promise.all([
|
|
3105
|
+
pool.listAccounts().catch(() => {
|
|
3106
|
+
readFailed = true;
|
|
3107
|
+
return [];
|
|
3108
|
+
}),
|
|
3109
|
+
pool.read().catch(() => {
|
|
3110
|
+
readFailed = true;
|
|
3111
|
+
return null;
|
|
3112
|
+
}),
|
|
3113
|
+
pool.poolStorage().catch(() => void 0)
|
|
3114
|
+
]);
|
|
3115
|
+
const reason = readFailed && poolStorage !== void 0 && poolStorage.unavailable === void 0 ? {
|
|
3116
|
+
...poolStorage,
|
|
3117
|
+
unavailable: "store-unreadable"
|
|
3118
|
+
} : poolStorage;
|
|
2597
3119
|
return {
|
|
2598
3120
|
accounts,
|
|
2599
3121
|
...data?.activeAccountId === void 0 ? {} : { activeAccountId: data.activeAccountId },
|
|
2600
|
-
rotationStrategy: data?.rotationStrategy ?? "sequential"
|
|
3122
|
+
rotationStrategy: data?.rotationStrategy ?? "sequential",
|
|
3123
|
+
...reason === void 0 ? {} : { poolStorage: reason }
|
|
2601
3124
|
};
|
|
2602
3125
|
}
|
|
3126
|
+
/**
|
|
3127
|
+
* Take the one login slot, or say that it is taken.
|
|
3128
|
+
*
|
|
3129
|
+
* Called synchronously, before the first `await` of the start it belongs to:
|
|
3130
|
+
* nothing can suspend between the check and the flag, so two overlapping
|
|
3131
|
+
* starts cannot both get through. The caller's `finally` releases the flag once
|
|
3132
|
+
* the login itself is in {@link activeLogin} (or the start has failed).
|
|
3133
|
+
*/
|
|
3134
|
+
beginStart() {
|
|
3135
|
+
if (this.startPending || this.activeLogin !== null) throw new OAuthServiceError("login-active", "A ChatGPT sign-in is already in progress.");
|
|
3136
|
+
this.startPending = true;
|
|
3137
|
+
}
|
|
2603
3138
|
async startLogin() {
|
|
2604
3139
|
this.assertAvailable();
|
|
3140
|
+
this.beginStart();
|
|
3141
|
+
try {
|
|
3142
|
+
return await this.startBrowserLogin();
|
|
3143
|
+
} finally {
|
|
3144
|
+
this.startPending = false;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
/** The body of {@link startLogin}, with the slot already taken. */
|
|
3148
|
+
async startBrowserLogin() {
|
|
2605
3149
|
await this.store.load().catch(() => {
|
|
2606
3150
|
throw new OAuthServiceError("storage-failed", "Secure credential storage is unavailable. Fix its ownership or permissions before signing in.");
|
|
2607
3151
|
});
|
|
2608
|
-
if (this.activeLogin !== null) throw new OAuthServiceError("login-active", "A ChatGPT sign-in is already in progress.");
|
|
2609
3152
|
this.lastLoginError = void 0;
|
|
2610
3153
|
const loginId = this.random(24).toString("base64url");
|
|
2611
3154
|
const verifier = this.random(48).toString("base64url");
|
|
@@ -2629,8 +3172,10 @@ var OAuthService = class {
|
|
|
2629
3172
|
this.activeLogin = {
|
|
2630
3173
|
id: loginId,
|
|
2631
3174
|
expiresAt,
|
|
2632
|
-
|
|
2633
|
-
|
|
3175
|
+
stop: () => {
|
|
3176
|
+
clearTimeout(timeout);
|
|
3177
|
+
server.cancel(new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled."));
|
|
3178
|
+
}
|
|
2634
3179
|
};
|
|
2635
3180
|
this.publish({
|
|
2636
3181
|
type: "pending",
|
|
@@ -2652,6 +3197,57 @@ var OAuthService = class {
|
|
|
2652
3197
|
if (this.activeLogin === null || this.activeLogin.id !== loginId) throw new OAuthServiceError("bad-request", "The requested sign-in is not active.");
|
|
2653
3198
|
this.cancelActive(new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled."), "cancelled");
|
|
2654
3199
|
}
|
|
3200
|
+
/**
|
|
3201
|
+
* Start a device-code sign-in, the path a headless host can finish.
|
|
3202
|
+
*
|
|
3203
|
+
* Answers as soon as the grant exists, with the code the user has to type and
|
|
3204
|
+
* the page to type it on; the wait for confirmation runs in the background and
|
|
3205
|
+
* reports through the same event stream the browser flow uses, so the card
|
|
3206
|
+
* needs no second mechanism and cancellation is the same call.
|
|
3207
|
+
*/
|
|
3208
|
+
async startDeviceLogin() {
|
|
3209
|
+
this.assertAvailable();
|
|
3210
|
+
this.beginStart();
|
|
3211
|
+
try {
|
|
3212
|
+
return await this.startDeviceLoginInner();
|
|
3213
|
+
} finally {
|
|
3214
|
+
this.startPending = false;
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
/** The body of {@link startDeviceLogin}, with the slot already taken. */
|
|
3218
|
+
async startDeviceLoginInner() {
|
|
3219
|
+
await this.store.load().catch(() => {
|
|
3220
|
+
throw new OAuthServiceError("storage-failed", "Secure credential storage is unavailable. Fix its ownership or permissions before signing in.");
|
|
3221
|
+
});
|
|
3222
|
+
this.lastLoginError = void 0;
|
|
3223
|
+
const controller = new AbortController();
|
|
3224
|
+
const grant = await this.requestDeviceGrant(controller.signal);
|
|
3225
|
+
const loginId = this.random(24).toString("base64url");
|
|
3226
|
+
const device = {
|
|
3227
|
+
userCode: grant.userCode,
|
|
3228
|
+
verificationUrl: OAUTH_DEVICE_VERIFY_URL,
|
|
3229
|
+
expiresAt: Math.floor(grant.expiresAt / 1e3),
|
|
3230
|
+
now: Math.floor(this.now() / 1e3)
|
|
3231
|
+
};
|
|
3232
|
+
this.activeLogin = {
|
|
3233
|
+
id: loginId,
|
|
3234
|
+
expiresAt: grant.expiresAt,
|
|
3235
|
+
stop: () => controller.abort(),
|
|
3236
|
+
device
|
|
3237
|
+
};
|
|
3238
|
+
this.publish({
|
|
3239
|
+
type: "pending",
|
|
3240
|
+
loginId
|
|
3241
|
+
});
|
|
3242
|
+
this.pollDeviceGrant(loginId, device, grant, controller.signal).catch((error) => {
|
|
3243
|
+
this.failLogin(loginId, error);
|
|
3244
|
+
});
|
|
3245
|
+
this.logger.info("[dsh-chatgpt-subscription] device-code login started");
|
|
3246
|
+
return {
|
|
3247
|
+
loginId,
|
|
3248
|
+
...device
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
2655
3251
|
subscribe(loginId, listener) {
|
|
2656
3252
|
const current = this.loginEvents.get(loginId);
|
|
2657
3253
|
if (current === void 0) return null;
|
|
@@ -2764,14 +3360,14 @@ var OAuthService = class {
|
|
|
2764
3360
|
this.listeners.clear();
|
|
2765
3361
|
this.loginEvents.clear();
|
|
2766
3362
|
}
|
|
2767
|
-
async exchangeCode(code, verifier, signal) {
|
|
3363
|
+
async exchangeCode(code, verifier, signal, redirectUri = OAUTH_REDIRECT_URI) {
|
|
2768
3364
|
const response = await this.fetchFn(OAUTH_TOKEN_URL$1, {
|
|
2769
3365
|
method: "POST",
|
|
2770
3366
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
2771
3367
|
body: new URLSearchParams({
|
|
2772
3368
|
grant_type: "authorization_code",
|
|
2773
3369
|
code,
|
|
2774
|
-
redirect_uri:
|
|
3370
|
+
redirect_uri: redirectUri,
|
|
2775
3371
|
client_id: CHATGPT_OAUTH_CLIENT_ID,
|
|
2776
3372
|
code_verifier: verifier
|
|
2777
3373
|
}).toString(),
|
|
@@ -2784,6 +3380,20 @@ var OAuthService = class {
|
|
|
2784
3380
|
throw new OAuthServiceError("oauth-token-exchange-failed", `ChatGPT token exchange failed (${response.status}${detail === null ? "" : `, ${detail}`}).`);
|
|
2785
3381
|
}
|
|
2786
3382
|
const credentials = credentialsFromTokenResponse(await response.json(), this.now());
|
|
3383
|
+
await this.persistCredentials(credentials, signal);
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Write freshly redeemed credentials into the pool, unless the sign-in was
|
|
3387
|
+
* cancelled while the token answer was on its way.
|
|
3388
|
+
*
|
|
3389
|
+
* The abort check used to live only in the poll loop, *after* the exchange, so
|
|
3390
|
+
* a Cancel landing between the token `200` and the write left the host having
|
|
3391
|
+
* answered `{cancelled:true}` and published `cancelled` while the pool quietly
|
|
3392
|
+
* gained an account. The check belongs at the last moment before the write —
|
|
3393
|
+
* the narrower the window, the fewer writes it misses.
|
|
3394
|
+
*/
|
|
3395
|
+
async persistCredentials(credentials, signal) {
|
|
3396
|
+
if (signal.aborted) throw new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled.");
|
|
2787
3397
|
if (this.pool !== null) {
|
|
2788
3398
|
await this.pool.addAccount(credentials).catch(() => {
|
|
2789
3399
|
throw new OAuthServiceError("storage-failed", "ChatGPT credentials could not be saved securely.");
|
|
@@ -2795,6 +3405,104 @@ var OAuthService = class {
|
|
|
2795
3405
|
});
|
|
2796
3406
|
this.credentialRevision += 1;
|
|
2797
3407
|
}
|
|
3408
|
+
/**
|
|
3409
|
+
* Ask for a device code, and normalise what the grant says about itself.
|
|
3410
|
+
*
|
|
3411
|
+
* `interval` arrives as a *string* (`"5"`) on the live service and
|
|
3412
|
+
* `expires_at` as an ISO timestamp, so both are parsed rather than trusted to
|
|
3413
|
+
* be the type they should be. A grant that states neither still works: the
|
|
3414
|
+
* defaults are the values the service actually uses.
|
|
3415
|
+
*/
|
|
3416
|
+
async requestDeviceGrant(signal) {
|
|
3417
|
+
const response = await this.fetchFn(OAUTH_DEVICE_USERCODE_URL, {
|
|
3418
|
+
method: "POST",
|
|
3419
|
+
headers: { "content-type": "application/json" },
|
|
3420
|
+
body: JSON.stringify({ client_id: CHATGPT_OAUTH_CLIENT_ID }),
|
|
3421
|
+
signal
|
|
3422
|
+
}).catch(() => {
|
|
3423
|
+
throw new OAuthServiceError("oauth-token-exchange-failed", "The ChatGPT device-code request could not be reached.");
|
|
3424
|
+
});
|
|
3425
|
+
if (!response.ok) {
|
|
3426
|
+
const detail = await oauthErrorIdentifier(response);
|
|
3427
|
+
throw new OAuthServiceError("oauth-token-exchange-failed", `The ChatGPT device code could not be issued (${response.status}${detail === null ? "" : `, ${detail}`}).`, response.status);
|
|
3428
|
+
}
|
|
3429
|
+
const payload = await response.json().catch(() => null);
|
|
3430
|
+
const record = isRecord$49(payload) ? payload : {};
|
|
3431
|
+
const authId = nonEmptyString$10(record.device_auth_id);
|
|
3432
|
+
const userCode = nonEmptyString$10(record.user_code) ?? nonEmptyString$10(record.usercode);
|
|
3433
|
+
if (authId === void 0 || userCode === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT returned no device code.");
|
|
3434
|
+
return {
|
|
3435
|
+
authId,
|
|
3436
|
+
userCode,
|
|
3437
|
+
interval: parseGrantInterval(record.interval),
|
|
3438
|
+
expiresAt: parseGrantDeadline(record.expires_at, this.now())
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* Wait for the user to confirm the code, then redeem the grant.
|
|
3443
|
+
*
|
|
3444
|
+
* Three rules, and each one is a way this loop goes wrong if it is dropped:
|
|
3445
|
+
* the wait between polls is the grant's own `interval` (clamped, see
|
|
3446
|
+
* {@link pollDelayMs}); no single wait runs past the deadline; and nothing is
|
|
3447
|
+
* accepted at or after the deadline, because a grant that expired upstream is
|
|
3448
|
+
* not a sign-in — redeeming it would surface the server's error as if the
|
|
3449
|
+
* user's code had failed.
|
|
3450
|
+
*/
|
|
3451
|
+
async pollDeviceGrant(loginId, device, grant, signal) {
|
|
3452
|
+
const deadline = grant.expiresAt;
|
|
3453
|
+
const interval = grant.interval;
|
|
3454
|
+
for (;;) {
|
|
3455
|
+
if (signal.aborted) return;
|
|
3456
|
+
const remaining = deadline - this.now();
|
|
3457
|
+
if (remaining <= 0) throw new OAuthServiceError("login-expired", "The device code expired before it was confirmed. Start the sign-in again for a fresh code.");
|
|
3458
|
+
await sleep$6(Math.min(pollDelayMs(interval, deadline - this.now()), remaining), signal);
|
|
3459
|
+
if (signal.aborted) return;
|
|
3460
|
+
if (this.now() >= deadline) throw new OAuthServiceError("login-expired", "The device code expired before it was confirmed. Start the sign-in again for a fresh code.");
|
|
3461
|
+
const outcome = await this.pollDeviceToken(grant, signal);
|
|
3462
|
+
if (outcome === "pending") continue;
|
|
3463
|
+
if (signal.aborted) return;
|
|
3464
|
+
await this.exchangeCode(outcome.authorizationCode, outcome.codeVerifier, signal, OAUTH_DEVICE_REDIRECT_URI);
|
|
3465
|
+
if (signal.aborted) return;
|
|
3466
|
+
this.completeLogin(loginId);
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
/**
|
|
3471
|
+
* One confirmation check.
|
|
3472
|
+
*
|
|
3473
|
+
* `403`/`404` (and any payload whose status says pending) mean "not yet" — the
|
|
3474
|
+
* user has not typed the code or has not confirmed it. Everything else is
|
|
3475
|
+
* terminal and carries the status with any error code the service named, so
|
|
3476
|
+
* the card can say what happened instead of polling until the deadline.
|
|
3477
|
+
*/
|
|
3478
|
+
async pollDeviceToken(grant, signal) {
|
|
3479
|
+
const response = await this.fetchFn(OAUTH_DEVICE_TOKEN_URL, {
|
|
3480
|
+
method: "POST",
|
|
3481
|
+
headers: { "content-type": "application/json" },
|
|
3482
|
+
body: JSON.stringify({
|
|
3483
|
+
device_auth_id: grant.authId,
|
|
3484
|
+
user_code: grant.userCode
|
|
3485
|
+
}),
|
|
3486
|
+
signal
|
|
3487
|
+
}).catch(() => {
|
|
3488
|
+
if (signal.aborted) throw new OAuthServiceError("login-cancelled", "ChatGPT sign-in was cancelled.");
|
|
3489
|
+
throw new OAuthServiceError("oauth-token-exchange-failed", "The ChatGPT device confirmation could not be reached.");
|
|
3490
|
+
});
|
|
3491
|
+
const payload = await response.json().catch(() => null);
|
|
3492
|
+
if (isPendingDeviceAnswer(response, payload)) return "pending";
|
|
3493
|
+
if (!response.ok) {
|
|
3494
|
+
const detail = deviceErrorIdentifier(payload);
|
|
3495
|
+
throw new OAuthServiceError("oauth-token-exchange-failed", `The ChatGPT device confirmation was refused (${response.status}${detail === void 0 ? "" : `, ${detail}`}).`, response.status);
|
|
3496
|
+
}
|
|
3497
|
+
const record = isRecord$49(payload) ? payload : {};
|
|
3498
|
+
const authorizationCode = nonEmptyString$10(record.authorization_code);
|
|
3499
|
+
const codeVerifier = nonEmptyString$10(record.code_verifier);
|
|
3500
|
+
if (authorizationCode === void 0 || codeVerifier === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT confirmed the device code but returned no authorization code.");
|
|
3501
|
+
return {
|
|
3502
|
+
authorizationCode,
|
|
3503
|
+
codeVerifier
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
2798
3506
|
refreshCredentials(stored) {
|
|
2799
3507
|
if (this.refreshPromise !== null) return this.refreshPromise;
|
|
2800
3508
|
this.refreshPromise = this.performRefresh(stored).finally(() => {
|
|
@@ -2881,11 +3589,7 @@ var OAuthService = class {
|
|
|
2881
3589
|
...this.store.storage,
|
|
2882
3590
|
available: storageAvailable
|
|
2883
3591
|
},
|
|
2884
|
-
login:
|
|
2885
|
-
active: active !== null,
|
|
2886
|
-
loginId: active?.id ?? null,
|
|
2887
|
-
expiresAt: active === null ? null : Math.floor(active.expiresAt / 1e3)
|
|
2888
|
-
},
|
|
3592
|
+
login: this.loginStatus(active),
|
|
2889
3593
|
...this.lastLoginError === void 0 ? {} : { error: this.lastLoginError }
|
|
2890
3594
|
};
|
|
2891
3595
|
const identity = extractIdentity(credentials);
|
|
@@ -2902,17 +3606,32 @@ var OAuthService = class {
|
|
|
2902
3606
|
...this.store.storage,
|
|
2903
3607
|
available: storageAvailable
|
|
2904
3608
|
},
|
|
2905
|
-
login:
|
|
2906
|
-
active: active !== null,
|
|
2907
|
-
loginId: active?.id ?? null,
|
|
2908
|
-
expiresAt: active === null ? null : Math.floor(active.expiresAt / 1e3)
|
|
2909
|
-
},
|
|
3609
|
+
login: this.loginStatus(active),
|
|
2910
3610
|
...this.lastLoginError === void 0 ? {} : { error: this.lastLoginError }
|
|
2911
3611
|
};
|
|
2912
3612
|
}
|
|
3613
|
+
/**
|
|
3614
|
+
* The `login` slice of the status, shared by both branches above.
|
|
3615
|
+
*
|
|
3616
|
+
* A device flow also carries its code: the card is the only place the user can
|
|
3617
|
+
* read it, and a settings page opened a second time (or reloaded) would
|
|
3618
|
+
* otherwise show "waiting" with nothing to type anywhere on it.
|
|
3619
|
+
*/
|
|
3620
|
+
loginStatus(active) {
|
|
3621
|
+
const device = active?.device;
|
|
3622
|
+
return {
|
|
3623
|
+
active: active !== null,
|
|
3624
|
+
loginId: active?.id ?? null,
|
|
3625
|
+
expiresAt: active === null ? null : Math.floor(active.expiresAt / 1e3),
|
|
3626
|
+
...device === void 0 ? {} : { device: {
|
|
3627
|
+
...device,
|
|
3628
|
+
now: Math.floor(this.now() / 1e3)
|
|
3629
|
+
} }
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
2913
3632
|
completeLogin(loginId) {
|
|
2914
3633
|
if (this.activeLogin?.id !== loginId) return;
|
|
2915
|
-
|
|
3634
|
+
this.activeLogin.stop();
|
|
2916
3635
|
this.activeLogin = null;
|
|
2917
3636
|
this.lastLoginError = void 0;
|
|
2918
3637
|
this.publish({
|
|
@@ -2923,7 +3642,7 @@ var OAuthService = class {
|
|
|
2923
3642
|
}
|
|
2924
3643
|
failLogin(loginId, error) {
|
|
2925
3644
|
if (this.activeLogin?.id !== loginId) return;
|
|
2926
|
-
|
|
3645
|
+
this.activeLogin.stop();
|
|
2927
3646
|
this.activeLogin = null;
|
|
2928
3647
|
const mapped = publicError(error, "oauth-callback-invalid");
|
|
2929
3648
|
this.lastLoginError = mapped;
|
|
@@ -2937,9 +3656,8 @@ var OAuthService = class {
|
|
|
2937
3656
|
cancelActive(error, outcome) {
|
|
2938
3657
|
const active = this.activeLogin;
|
|
2939
3658
|
if (active === null) return;
|
|
2940
|
-
clearTimeout(active.timeout);
|
|
2941
3659
|
this.activeLogin = null;
|
|
2942
|
-
active.
|
|
3660
|
+
active.stop();
|
|
2943
3661
|
this.publish(outcome === "cancelled" ? {
|
|
2944
3662
|
type: "cancelled",
|
|
2945
3663
|
loginId: active.id
|
|
@@ -3042,6 +3760,119 @@ function maskAccountId(accountId) {
|
|
|
3042
3760
|
if (accountId === void 0) return null;
|
|
3043
3761
|
return `…${accountId.slice(-4)}`;
|
|
3044
3762
|
}
|
|
3763
|
+
/**
|
|
3764
|
+
* How long to wait before the next confirmation check.
|
|
3765
|
+
*
|
|
3766
|
+
* Two clamps, for two different bad values. A floor keeps a fractional interval
|
|
3767
|
+
* (a grant answering `"0.2"`) from turning the loop into a spin against the
|
|
3768
|
+
* service — `"0"`, a negative and anything unparseable never reach it, because
|
|
3769
|
+
* {@link parseGrantInterval} already turns those into the 5 s default. The
|
|
3770
|
+
* deadline keeps an absurd interval from sleeping past the point where the grant
|
|
3771
|
+
* dies, which would leave the card "waiting" for a code that can no longer be
|
|
3772
|
+
* redeemed. The normal value — `"5"` — passes through untouched.
|
|
3773
|
+
*/
|
|
3774
|
+
function pollDelayMs(intervalSeconds, remainingMs) {
|
|
3775
|
+
const requested = intervalSeconds * 1e3;
|
|
3776
|
+
return Math.max(OAUTH_DEVICE_MIN_POLL_INTERVAL_MS, Math.min(requested, Math.max(remainingMs, 0)));
|
|
3777
|
+
}
|
|
3778
|
+
/** The grant's `interval`, which arrives as a string on the live service. */
|
|
3779
|
+
function parseGrantInterval(value) {
|
|
3780
|
+
const seconds = typeof value === "number" ? value : Number(value);
|
|
3781
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds : 5;
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* The grant's `expires_at`, which the live service sends as an ISO timestamp.
|
|
3785
|
+
*
|
|
3786
|
+
* A number is accepted too, and its magnitude decides the unit: a Unix timestamp
|
|
3787
|
+
* in seconds and one in milliseconds differ by three orders of magnitude, so
|
|
3788
|
+
* guessing is safe here in a way it is not for a field that could be either by a
|
|
3789
|
+
* factor of a thousand. Anything unparseable falls back to the grant lifetime
|
|
3790
|
+
* this client expects — the deadline still exists, it just is not the grant's.
|
|
3791
|
+
*/
|
|
3792
|
+
function parseGrantDeadline(value, now) {
|
|
3793
|
+
if (typeof value === "string" && value !== "") {
|
|
3794
|
+
const parsed = Date.parse(value);
|
|
3795
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
3796
|
+
}
|
|
3797
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value < 0xe8d4a51000 ? value * 1e3 : value;
|
|
3798
|
+
return now + OAUTH_DEVICE_GRANT_LIFETIME_MS;
|
|
3799
|
+
}
|
|
3800
|
+
/**
|
|
3801
|
+
* Wait `ms`, or less if the sign-in is cancelled first.
|
|
3802
|
+
*
|
|
3803
|
+
* Resolves rather than rejects on abort: the caller already knows it was
|
|
3804
|
+
* cancelled and checks the signal, so a rejected promise here would only add an
|
|
3805
|
+
* unhandled path that says the same thing a second time.
|
|
3806
|
+
*/
|
|
3807
|
+
function sleep$6(ms, signal) {
|
|
3808
|
+
return new Promise((resolve) => {
|
|
3809
|
+
if (signal.aborted) {
|
|
3810
|
+
resolve();
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
const done = () => {
|
|
3814
|
+
clearTimeout(timer);
|
|
3815
|
+
signal.removeEventListener("abort", done);
|
|
3816
|
+
resolve();
|
|
3817
|
+
};
|
|
3818
|
+
const timer = setTimeout(done, ms);
|
|
3819
|
+
timer.unref?.();
|
|
3820
|
+
signal.addEventListener("abort", done, { once: true });
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
/** Status values that mean "the user has not confirmed yet", not a failure. */
|
|
3824
|
+
const DEVICE_PENDING_STATUSES = /* @__PURE__ */ new Set([
|
|
3825
|
+
"pending",
|
|
3826
|
+
"authorization_pending",
|
|
3827
|
+
"deviceauth_pending",
|
|
3828
|
+
"not_found"
|
|
3829
|
+
]);
|
|
3830
|
+
/**
|
|
3831
|
+
* Whether one confirmation check was the grant saying "not yet".
|
|
3832
|
+
*
|
|
3833
|
+
* Two ways to say it, and the second is deliberately narrow. The live service
|
|
3834
|
+
* answers `403`/`404` while waiting, and those statuses mean "not yet" whatever
|
|
3835
|
+
* the body says. A body that merely *looks* pending counts only on a status that
|
|
3836
|
+
* could carry it — a `200`, or the `400` some builds answer — because honouring it
|
|
3837
|
+
* everywhere let a `401`/`429`/`500` that happened to mention `status: "pending"`
|
|
3838
|
+
* retry an invalid user code until the deadline, hiding a reason the user could
|
|
3839
|
+
* have been told at once. Every other status is a failure, read by its code.
|
|
3840
|
+
*/
|
|
3841
|
+
function isPendingDeviceAnswer(response, payload) {
|
|
3842
|
+
if (response.status === 403 || response.status === 404) return true;
|
|
3843
|
+
if (!response.ok && response.status !== 400) return false;
|
|
3844
|
+
if (!isRecord$49(payload)) return false;
|
|
3845
|
+
const status = payload.status;
|
|
3846
|
+
if (typeof status === "string" && DEVICE_PENDING_STATUSES.has(status.toLowerCase())) return true;
|
|
3847
|
+
const error = payload.error;
|
|
3848
|
+
if (isRecord$49(error)) {
|
|
3849
|
+
const code = error.code;
|
|
3850
|
+
if (typeof code === "string" && DEVICE_PENDING_STATUSES.has(code.toLowerCase())) return true;
|
|
3851
|
+
}
|
|
3852
|
+
return false;
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* The error code a device endpoint named, if it named one.
|
|
3856
|
+
*
|
|
3857
|
+
* A named `error.code` is read before the top-level `status`, so a status word
|
|
3858
|
+
* (`pending`) cannot be quoted as the reason when the body also says what went
|
|
3859
|
+
* wrong.
|
|
3860
|
+
*/
|
|
3861
|
+
function deviceErrorIdentifier(payload) {
|
|
3862
|
+
if (!isRecord$49(payload)) return void 0;
|
|
3863
|
+
const candidates = [payload.code];
|
|
3864
|
+
const error = payload.error;
|
|
3865
|
+
if (typeof error === "string") candidates.push(error);
|
|
3866
|
+
else if (isRecord$49(error)) candidates.push(error.code, error.type);
|
|
3867
|
+
candidates.push(payload.status);
|
|
3868
|
+
for (const candidate of candidates) if (typeof candidate === "string" && /^[a-z0-9_.-]{1,64}$/i.test(candidate)) return candidate;
|
|
3869
|
+
}
|
|
3870
|
+
function nonEmptyString$10(value) {
|
|
3871
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
3872
|
+
}
|
|
3873
|
+
function isRecord$49(value) {
|
|
3874
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3875
|
+
}
|
|
3045
3876
|
async function oauthErrorIdentifier(response) {
|
|
3046
3877
|
const payload = await response.json().catch(() => null);
|
|
3047
3878
|
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
|
|
@@ -3692,6 +4523,7 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
3692
4523
|
},
|
|
3693
4524
|
emptyMessage: "未登录 ChatGPT 账号,请在「设置 → 订阅服务 → ChatGPT」中添加并登录账号。",
|
|
3694
4525
|
...options.backend === void 0 ? {} : { backend: options.backend },
|
|
4526
|
+
...options.keyring === void 0 ? {} : { keyring: options.keyring },
|
|
3695
4527
|
...options.maxAccounts === void 0 ? {} : { maxAccounts: options.maxAccounts }
|
|
3696
4528
|
};
|
|
3697
4529
|
super(hooks);
|
|
@@ -4467,8 +5299,7 @@ const SCOPES = [
|
|
|
4467
5299
|
const DEFAULT_CLIENT_ID = Buffer.from("MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==", "base64").toString("utf8");
|
|
4468
5300
|
const DEFAULT_CLIENT_SECRET = Buffer.from("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=", "base64").toString("utf8");
|
|
4469
5301
|
const ANTIGRAVITY_SYSTEM_INSTRUCTION = "You are Antigravity, a powerful agentic AI coding assistant designed by Google DeepMind. You are pair programming with a user to solve coding tasks. Be concise, practical, and tool-aware.";
|
|
4470
|
-
const ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION = "CRITICAL: NEVER output rule checks, formatting guidelines, constraint checklists, or thinking/personality preambles in the final response. Output only the final response.
|
|
4471
|
-
const ANTIGRAVITY_PROGRESS_INSTRUCTION = "Progress and tool execution rule: before EVERY tool call, emit 1-2 short sentences of ordinary, visible assistant text stating what you are about to do and why, written as normal response text the user can read. Never write those sentences only inside your thinking/thought summary: the visible text part must exist, otherwise the progress is not delivered. These progress lines are required and take precedence over the no-preamble rule. Keep them brief, professional, and factual; do not restate the whole plan. Only present the comprehensive final answer and summary in the final turn, after all tool operations are completed.";
|
|
5302
|
+
const ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION = "CRITICAL: NEVER output rule checks, formatting guidelines, constraint checklists, or thinking/personality preambles in the final response. Output only the final response.";
|
|
4472
5303
|
const GEMINI_ROLE = {
|
|
4473
5304
|
user: "user",
|
|
4474
5305
|
model: "model"
|
|
@@ -6768,12 +7599,7 @@ function gemini25ThinkingBudget(runtimeModel, effort) {
|
|
|
6768
7599
|
const max = isPro ? 32768 : 24576;
|
|
6769
7600
|
return Math.min(max, Math.max(min, effort === "off" || effort === "none" ? 0 : effort === "high" || effort === "xhigh" ? max : effort === "medium" ? 16384 : 4096));
|
|
6770
7601
|
}
|
|
6771
|
-
function progressExplanationInstruction$1(tools) {
|
|
6772
|
-
if (!tools?.length) return void 0;
|
|
6773
|
-
return ANTIGRAVITY_PROGRESS_INSTRUCTION;
|
|
6774
|
-
}
|
|
6775
7602
|
function buildRequest$2(options, model, projectId, runtimeModel, effort, images = NO_RESOLVED_IMAGES$5, wire = {}) {
|
|
6776
|
-
const progressInstruction = progressExplanationInstruction$1(options.tools);
|
|
6777
7603
|
const request = {
|
|
6778
7604
|
contents: convertMessages(options, model, runtimeModel, images),
|
|
6779
7605
|
systemInstruction: {
|
|
@@ -6782,8 +7608,7 @@ function buildRequest$2(options, model, projectId, runtimeModel, effort, images
|
|
|
6782
7608
|
{ text: ANTIGRAVITY_SYSTEM_INSTRUCTION },
|
|
6783
7609
|
{ text: `Please ignore following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` },
|
|
6784
7610
|
{ text: ANTIGRAVITY_NO_PREAMBLE_INSTRUCTION },
|
|
6785
|
-
...options.system ? [{ text: sanitizeText$8(options.system) }] : []
|
|
6786
|
-
...progressInstruction ? [{ text: progressInstruction }] : []
|
|
7611
|
+
...options.system ? [{ text: sanitizeText$8(options.system) }] : []
|
|
6787
7612
|
]
|
|
6788
7613
|
}
|
|
6789
7614
|
};
|
|
@@ -7371,15 +8196,6 @@ function parseAntigravityPoolData(value) {
|
|
|
7371
8196
|
if (activeAccountId) res.activeAccountId = activeAccountId;
|
|
7372
8197
|
return res;
|
|
7373
8198
|
}
|
|
7374
|
-
function poolCredentialAccount(filePath) {
|
|
7375
|
-
return createHash("sha256").update(path.resolve(filePath)).digest("hex");
|
|
7376
|
-
}
|
|
7377
|
-
function createPoolCredentialBackend(filePath) {
|
|
7378
|
-
if (process.platform === "win32") return new WindowsDpapiCredentialStore(`${filePath}.dpapi`, parseAntigravityPoolData);
|
|
7379
|
-
if (process.platform === "darwin") return new MacKeychainCredentialStore("dsh-antigravity-pool", poolCredentialAccount(filePath), parseAntigravityPoolData);
|
|
7380
|
-
if (process.platform === "linux") return new SecretServiceCredentialStore("dsh-antigravity-pool", poolCredentialAccount(filePath), parseAntigravityPoolData);
|
|
7381
|
-
throw new Error("Antigravity pool encrypted storage requires Windows, macOS, or Linux.");
|
|
7382
|
-
}
|
|
7383
8199
|
/**
|
|
7384
8200
|
* One bucket's reset moment as Unix milliseconds, when it parses as a date.
|
|
7385
8201
|
*
|
|
@@ -7441,7 +8257,7 @@ const MAX_DETAIL_CHARS = 500;
|
|
|
7441
8257
|
var AccountPoolStore = class {
|
|
7442
8258
|
core;
|
|
7443
8259
|
legacyStore;
|
|
7444
|
-
constructor(filePath = poolPath(), backend
|
|
8260
|
+
constructor(filePath = poolPath(), backend, legacyStore = new FileCredentialStore$3()) {
|
|
7445
8261
|
this.legacyStore = legacyStore;
|
|
7446
8262
|
const hooks = {
|
|
7447
8263
|
providerId: "antigravity",
|
|
@@ -7504,7 +8320,7 @@ var AccountPoolStore = class {
|
|
|
7504
8320
|
emptyMessage: "未登录 Antigravity 账号,请在「设置 → Antigravity」中添加并登录账号。",
|
|
7505
8321
|
allUnavailableMessage: (count, waitMinutes) => `全部 ${count} 个 Antigravity 账号均处于配额限制或冷却中 (429)。` + (waitMinutes === null ? "目前没有处于冷却中的账号。" : `最短预计在 ${waitMinutes} 分钟后解除冷却。`),
|
|
7506
8322
|
allAuthFailedMessage: (count) => `全部 ${count} 个 Antigravity 账号均需要重新登录。`,
|
|
7507
|
-
backend
|
|
8323
|
+
...backend === void 0 ? {} : { backend }
|
|
7508
8324
|
};
|
|
7509
8325
|
this.core = new AccountPoolCore(hooks);
|
|
7510
8326
|
}
|
|
@@ -12671,6 +13487,12 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
12671
13487
|
value: await oauth.startLogin()
|
|
12672
13488
|
});
|
|
12673
13489
|
return;
|
|
13490
|
+
case `${ROUTE_PREFIX$4}/login/device/start`:
|
|
13491
|
+
json(response, {
|
|
13492
|
+
ok: true,
|
|
13493
|
+
value: await oauth.startDeviceLogin()
|
|
13494
|
+
});
|
|
13495
|
+
return;
|
|
12674
13496
|
case `${ROUTE_PREFIX$4}/login/cancel`: {
|
|
12675
13497
|
const loginId = field(body, "loginId");
|
|
12676
13498
|
if (loginId === null) throw new Error("missing loginId");
|
|
@@ -12681,6 +13503,24 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
12681
13503
|
});
|
|
12682
13504
|
return;
|
|
12683
13505
|
}
|
|
13506
|
+
case `${ROUTE_PREFIX$4}/storage`: {
|
|
13507
|
+
const storage = adopt?.storage;
|
|
13508
|
+
if (storage === void 0) {
|
|
13509
|
+
jsonError(response, 404, {
|
|
13510
|
+
code: "bad-request",
|
|
13511
|
+
message: "The account storage setting is not available on this host."
|
|
13512
|
+
});
|
|
13513
|
+
return;
|
|
13514
|
+
}
|
|
13515
|
+
const mode = field(body, "mode");
|
|
13516
|
+
if (mode !== "keyring" && mode !== "credentials") throw new PreferenceError("mode must be \"keyring\" or \"credentials\".");
|
|
13517
|
+
await storage.update(mode);
|
|
13518
|
+
json(response, {
|
|
13519
|
+
ok: true,
|
|
13520
|
+
value: await refreshedStatus()
|
|
13521
|
+
});
|
|
13522
|
+
return;
|
|
13523
|
+
}
|
|
12684
13524
|
case `${ROUTE_PREFIX$4}/accounts`: {
|
|
12685
13525
|
if (accountPool === void 0) throw new Error("The ChatGPT account pool is not installed.");
|
|
12686
13526
|
const action = field(body, "action");
|
|
@@ -15695,6 +16535,65 @@ function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetch
|
|
|
15695
16535
|
});
|
|
15696
16536
|
}
|
|
15697
16537
|
//#endregion
|
|
16538
|
+
//#region src/host/common/pool-storage-settings.ts
|
|
16539
|
+
/**
|
|
16540
|
+
* Where the account-pool storage choice is remembered between restarts.
|
|
16541
|
+
*
|
|
16542
|
+
* The profile's `poolStorage` is the deployment's default, and the settings card
|
|
16543
|
+
* writes the operator's own answer here instead of rewriting `cordis.yml`: that
|
|
16544
|
+
* file is the harness composition, and a card that edited it would be rewriting
|
|
16545
|
+
* the deployment to change a preference. The document lives beside the plugin's
|
|
16546
|
+
* other storage under the harness home, is written atomically, validated with the
|
|
16547
|
+
* same schema machinery as the preferences, and — because it stores `null` until
|
|
16548
|
+
* somebody chooses — the profile's value keeps winning until a card actually says
|
|
16549
|
+
* otherwise.
|
|
16550
|
+
* @module dsh-chatgpt-subscription/pool-storage-settings
|
|
16551
|
+
*/
|
|
16552
|
+
/** Where the card's choice is kept. */
|
|
16553
|
+
function poolStorageSettingsPath() {
|
|
16554
|
+
return join(dshHomeDir(), "storages", "dsh-chatgpt-subscription-pool-storage.json");
|
|
16555
|
+
}
|
|
16556
|
+
const schema = z.object({ mode: z.union([
|
|
16557
|
+
z.const("keyring"),
|
|
16558
|
+
z.const("credentials"),
|
|
16559
|
+
z.const(null)
|
|
16560
|
+
]).default(null) });
|
|
16561
|
+
/** The choice the settings card writes, and the pools read. */
|
|
16562
|
+
var PoolStorageSettings = class {
|
|
16563
|
+
store;
|
|
16564
|
+
constructor(filePath = poolStorageSettingsPath()) {
|
|
16565
|
+
this.store = new FilePreferencesStore(schema, filePath);
|
|
16566
|
+
}
|
|
16567
|
+
/** Load the document once; a missing or corrupt one counts as "nothing chosen". */
|
|
16568
|
+
hydrate() {
|
|
16569
|
+
return this.store.hydrate();
|
|
16570
|
+
}
|
|
16571
|
+
/** The chosen mode, or `null` while the profile's value stands. */
|
|
16572
|
+
chosen() {
|
|
16573
|
+
return this.store.get().mode;
|
|
16574
|
+
}
|
|
16575
|
+
/**
|
|
16576
|
+
* The mode to use: the card's choice, else the profile's value, else the default.
|
|
16577
|
+
*
|
|
16578
|
+
* @param configured - `poolStorage` from the profile, when it carries one.
|
|
16579
|
+
*/
|
|
16580
|
+
effective(configured) {
|
|
16581
|
+
return this.chosen() ?? configured ?? "keyring";
|
|
16582
|
+
}
|
|
16583
|
+
/**
|
|
16584
|
+
* Persist one choice and tell the pools to resolve their backend again.
|
|
16585
|
+
*
|
|
16586
|
+
* The announcement is part of the write rather than the route's job: a caller
|
|
16587
|
+
* that stores a mode without invalidating the caches would leave every pool on
|
|
16588
|
+
* the old store until the host restarted, which is the one failure this whole
|
|
16589
|
+
* option exists to avoid — reading and writing in different places.
|
|
16590
|
+
*/
|
|
16591
|
+
async update(mode) {
|
|
16592
|
+
await this.store.update({ mode });
|
|
16593
|
+
poolStorageChanged();
|
|
16594
|
+
}
|
|
16595
|
+
};
|
|
16596
|
+
//#endregion
|
|
15698
16597
|
//#region src/host/common/context-budget-fetch.ts
|
|
15699
16598
|
/**
|
|
15700
16599
|
* Recover a rejected input+output budget without dropping model-visible history.
|
|
@@ -23621,166 +24520,6 @@ var OllamaAccountPool = class extends AccountPoolCore {
|
|
|
23621
24520
|
}
|
|
23622
24521
|
};
|
|
23623
24522
|
//#endregion
|
|
23624
|
-
//#region src/host/kimi-code/model-catalog.ts
|
|
23625
|
-
/**
|
|
23626
|
-
* The ids the subscription serves today.
|
|
23627
|
-
*
|
|
23628
|
-
* `kimi-for-coding` is the alias that never changes: Moonshot upgrades the model
|
|
23629
|
-
* behind it in place (K2.7 Code became K2.8 Preview without a config change), so
|
|
23630
|
-
* this entry is the durable default a user can leave selected.
|
|
23631
|
-
*/
|
|
23632
|
-
const KIMI_CODE_MODELS = [
|
|
23633
|
-
{
|
|
23634
|
-
id: "k3",
|
|
23635
|
-
name: "K3",
|
|
23636
|
-
version: "K3",
|
|
23637
|
-
contextWindow: 262144,
|
|
23638
|
-
maxContextWindow: 1048576,
|
|
23639
|
-
maxTokens: 32768,
|
|
23640
|
-
inputModalities: [
|
|
23641
|
-
"text",
|
|
23642
|
-
"image",
|
|
23643
|
-
"video"
|
|
23644
|
-
],
|
|
23645
|
-
reasoningEfforts: [
|
|
23646
|
-
"low",
|
|
23647
|
-
"high",
|
|
23648
|
-
"max"
|
|
23649
|
-
],
|
|
23650
|
-
defaultReasoningEffort: "high",
|
|
23651
|
-
minimumPlan: "Moderato",
|
|
23652
|
-
contextPlan: "Allegretto",
|
|
23653
|
-
description: "The most capable flagship coding model: 2.8T parameters, 1M context window. The 1M context consumes about twice the quota of k3-256k.",
|
|
23654
|
-
supportsDynamicTools: true,
|
|
23655
|
-
quotaMultiplier: 2,
|
|
23656
|
-
speed: "regular"
|
|
23657
|
-
},
|
|
23658
|
-
{
|
|
23659
|
-
id: "k3-256k",
|
|
23660
|
-
name: "K3 (256K)",
|
|
23661
|
-
version: "K3",
|
|
23662
|
-
contextWindow: 262144,
|
|
23663
|
-
maxContextWindow: null,
|
|
23664
|
-
maxTokens: 32768,
|
|
23665
|
-
inputModalities: ["text", "image"],
|
|
23666
|
-
reasoningEfforts: [
|
|
23667
|
-
"low",
|
|
23668
|
-
"high",
|
|
23669
|
-
"max"
|
|
23670
|
-
],
|
|
23671
|
-
defaultReasoningEffort: "high",
|
|
23672
|
-
minimumPlan: "Moderato",
|
|
23673
|
-
contextPlan: null,
|
|
23674
|
-
description: "The 256K context version of K3, available to every Moderato member and above. Costs about half the quota of k3 with 1M context, and does not accept video input.",
|
|
23675
|
-
supportsDynamicTools: true,
|
|
23676
|
-
quotaMultiplier: 1,
|
|
23677
|
-
speed: "regular"
|
|
23678
|
-
},
|
|
23679
|
-
{
|
|
23680
|
-
id: "kimi-for-coding",
|
|
23681
|
-
name: "Kimi for Coding",
|
|
23682
|
-
version: "K2.8 Preview",
|
|
23683
|
-
contextWindow: 1048576,
|
|
23684
|
-
maxContextWindow: null,
|
|
23685
|
-
maxTokens: 32768,
|
|
23686
|
-
inputModalities: [
|
|
23687
|
-
"text",
|
|
23688
|
-
"image",
|
|
23689
|
-
"video"
|
|
23690
|
-
],
|
|
23691
|
-
reasoningEfforts: [
|
|
23692
|
-
"low",
|
|
23693
|
-
"high",
|
|
23694
|
-
"max"
|
|
23695
|
-
],
|
|
23696
|
-
defaultReasoningEffort: "max",
|
|
23697
|
-
minimumPlan: null,
|
|
23698
|
-
contextPlan: null,
|
|
23699
|
-
description: "Performance close to K3 with more efficient thinking, and up to 1M context on every plan. Good at code completion and routine development tasks.",
|
|
23700
|
-
supportsDynamicTools: true,
|
|
23701
|
-
quotaMultiplier: 1,
|
|
23702
|
-
speed: "regular"
|
|
23703
|
-
},
|
|
23704
|
-
{
|
|
23705
|
-
id: "kimi-for-coding-highspeed",
|
|
23706
|
-
name: "Kimi for Coding HighSpeed",
|
|
23707
|
-
version: "K2.7 Code HighSpeed",
|
|
23708
|
-
contextWindow: 262144,
|
|
23709
|
-
maxContextWindow: null,
|
|
23710
|
-
maxTokens: 32768,
|
|
23711
|
-
inputModalities: [
|
|
23712
|
-
"text",
|
|
23713
|
-
"image",
|
|
23714
|
-
"video"
|
|
23715
|
-
],
|
|
23716
|
-
reasoningEfforts: ["high"],
|
|
23717
|
-
defaultReasoningEffort: "high",
|
|
23718
|
-
minimumPlan: "Allegretto",
|
|
23719
|
-
contextPlan: null,
|
|
23720
|
-
description: "The high-speed version of K2.7 Code with the same coding ability and roughly 5-6x faster output, at 3x quota usage. Requires the Allegretto plan or above.",
|
|
23721
|
-
supportsDynamicTools: false,
|
|
23722
|
-
quotaMultiplier: 3,
|
|
23723
|
-
speed: "highspeed"
|
|
23724
|
-
},
|
|
23725
|
-
{
|
|
23726
|
-
id: "k3.1-pro",
|
|
23727
|
-
name: "K3.1 Pro",
|
|
23728
|
-
version: "K3.1 Pro",
|
|
23729
|
-
contextWindow: 262144,
|
|
23730
|
-
maxContextWindow: 1048576,
|
|
23731
|
-
maxTokens: 32768,
|
|
23732
|
-
inputModalities: [
|
|
23733
|
-
"text",
|
|
23734
|
-
"image",
|
|
23735
|
-
"video"
|
|
23736
|
-
],
|
|
23737
|
-
reasoningEfforts: [
|
|
23738
|
-
"low",
|
|
23739
|
-
"high",
|
|
23740
|
-
"max"
|
|
23741
|
-
],
|
|
23742
|
-
defaultReasoningEffort: "max",
|
|
23743
|
-
thinkingType: "only",
|
|
23744
|
-
minimumPlan: "Moderato",
|
|
23745
|
-
contextPlan: "Allegretto",
|
|
23746
|
-
description: "The K3.1 Pro flagship: 3T parameters, a 256K context window on Plus/Moderato and 1M on Pro/Allegretto and above. Thinking runs at low/high/max, defaults to max, and cannot be disabled.",
|
|
23747
|
-
supportsDynamicTools: true,
|
|
23748
|
-
quotaMultiplier: null,
|
|
23749
|
-
speed: "regular"
|
|
23750
|
-
},
|
|
23751
|
-
{
|
|
23752
|
-
id: "k3.1-pro-highspeed",
|
|
23753
|
-
name: "K3.1 Pro Highspeed",
|
|
23754
|
-
version: "K3.1 Pro Highspeed",
|
|
23755
|
-
contextWindow: 1048576,
|
|
23756
|
-
maxContextWindow: null,
|
|
23757
|
-
maxTokens: 32768,
|
|
23758
|
-
inputModalities: [
|
|
23759
|
-
"text",
|
|
23760
|
-
"image",
|
|
23761
|
-
"video"
|
|
23762
|
-
],
|
|
23763
|
-
reasoningEfforts: [
|
|
23764
|
-
"low",
|
|
23765
|
-
"high",
|
|
23766
|
-
"max"
|
|
23767
|
-
],
|
|
23768
|
-
defaultReasoningEffort: "max",
|
|
23769
|
-
thinkingType: "only",
|
|
23770
|
-
minimumPlan: "Allegretto",
|
|
23771
|
-
contextPlan: null,
|
|
23772
|
-
description: "The high-speed K3.1 Pro with a 1M context window and faster output, at about twice the quota of the regular version. Requires the Pro/Allegretto plan or above.",
|
|
23773
|
-
supportsDynamicTools: true,
|
|
23774
|
-
quotaMultiplier: null,
|
|
23775
|
-
speed: "highspeed"
|
|
23776
|
-
}
|
|
23777
|
-
];
|
|
23778
|
-
const BY_ID$1 = new Map(KIMI_CODE_MODELS.map((model) => [model.id, model]));
|
|
23779
|
-
/** Registry entry for one model id, or undefined when the id is unknown. */
|
|
23780
|
-
function kimiCodeModelDef(modelId) {
|
|
23781
|
-
return BY_ID$1.get(modelId.trim());
|
|
23782
|
-
}
|
|
23783
|
-
//#endregion
|
|
23784
24523
|
//#region src/host/common/request-images.ts
|
|
23785
24524
|
function isRecord$39(value) {
|
|
23786
24525
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -24174,6 +24913,194 @@ function imageBlockToInline$2(block, images) {
|
|
|
24174
24913
|
} : void 0;
|
|
24175
24914
|
}
|
|
24176
24915
|
//#endregion
|
|
24916
|
+
//#region src/host/kimi-code/model-catalog.ts
|
|
24917
|
+
/**
|
|
24918
|
+
* Request-body ceiling the 1M-context tier defaults to.
|
|
24919
|
+
*
|
|
24920
|
+
* The 2 MB figure this line used to apply to every model comes from the gateway's
|
|
24921
|
+
* generic error example and was never measured against a 1M window, while a real
|
|
24922
|
+
* 1M session (3-4 MB of Chinese prose) hits it long before the window is full.
|
|
24923
|
+
* The official kimi-cli ships no byte guard at all, so the ceiling is this
|
|
24924
|
+
* plugin's own safety net rather than a client requirement.
|
|
24925
|
+
*
|
|
24926
|
+
* 8 MiB is sized so bytes never become the binding limit for a 1M session: that
|
|
24927
|
+
* window's body is roughly 4 MB of English or 4.5 MB of dense Chinese, so 8 MiB
|
|
24928
|
+
* leaves nearly double the headroom. What binds first is the model's own token
|
|
24929
|
+
* window — the service answers "Your request exceeded model token limit", which
|
|
24930
|
+
* common/context-overflow.ts already normalizes to CONTEXT_WINDOW_EXCEEDED, so
|
|
24931
|
+
* compaction and retry behave exactly as they did before.
|
|
24932
|
+
*
|
|
24933
|
+
* The number is still an UPPER BOUND pending measurement, not a documented
|
|
24934
|
+
* gateway limit: a deployment that has measured its own gateway pins the real
|
|
24935
|
+
* figure with DSH_KIMI_CODE_MAX_BODY_BYTES. The 256K tier keeps 2 MB, where the
|
|
24936
|
+
* old ceiling and the window line up.
|
|
24937
|
+
*/
|
|
24938
|
+
const LONG_CONTEXT_MESSAGE_BODY_BYTES = 8 * 1024 * 1024;
|
|
24939
|
+
/**
|
|
24940
|
+
* The ids the subscription serves today.
|
|
24941
|
+
*
|
|
24942
|
+
* `kimi-for-coding` is the alias that never changes: Moonshot upgrades the model
|
|
24943
|
+
* behind it in place (K2.7 Code became K2.8 Preview without a config change), so
|
|
24944
|
+
* this entry is the durable default a user can leave selected.
|
|
24945
|
+
*/
|
|
24946
|
+
const KIMI_CODE_MODELS = [
|
|
24947
|
+
{
|
|
24948
|
+
id: "k3",
|
|
24949
|
+
name: "K3",
|
|
24950
|
+
version: "K3",
|
|
24951
|
+
contextWindow: 262144,
|
|
24952
|
+
maxContextWindow: 1048576,
|
|
24953
|
+
maxTokens: 32768,
|
|
24954
|
+
maxRequestBodyBytes: LONG_CONTEXT_MESSAGE_BODY_BYTES,
|
|
24955
|
+
inputModalities: [
|
|
24956
|
+
"text",
|
|
24957
|
+
"image",
|
|
24958
|
+
"video"
|
|
24959
|
+
],
|
|
24960
|
+
reasoningEfforts: [
|
|
24961
|
+
"low",
|
|
24962
|
+
"high",
|
|
24963
|
+
"max"
|
|
24964
|
+
],
|
|
24965
|
+
defaultReasoningEffort: "high",
|
|
24966
|
+
minimumPlan: "Moderato",
|
|
24967
|
+
contextPlan: "Allegretto",
|
|
24968
|
+
description: "The most capable flagship coding model: 2.8T parameters, 1M context window. The 1M context consumes about twice the quota of k3-256k.",
|
|
24969
|
+
supportsDynamicTools: true,
|
|
24970
|
+
quotaMultiplier: 2,
|
|
24971
|
+
speed: "regular"
|
|
24972
|
+
},
|
|
24973
|
+
{
|
|
24974
|
+
id: "k3-256k",
|
|
24975
|
+
name: "K3 (256K)",
|
|
24976
|
+
version: "K3",
|
|
24977
|
+
contextWindow: 262144,
|
|
24978
|
+
maxContextWindow: null,
|
|
24979
|
+
maxTokens: 32768,
|
|
24980
|
+
maxRequestBodyBytes: MAX_MESSAGE_BODY_BYTES,
|
|
24981
|
+
inputModalities: ["text", "image"],
|
|
24982
|
+
reasoningEfforts: [
|
|
24983
|
+
"low",
|
|
24984
|
+
"high",
|
|
24985
|
+
"max"
|
|
24986
|
+
],
|
|
24987
|
+
defaultReasoningEffort: "high",
|
|
24988
|
+
minimumPlan: "Moderato",
|
|
24989
|
+
contextPlan: null,
|
|
24990
|
+
description: "The 256K context version of K3, available to every Moderato member and above. Costs about half the quota of k3 with 1M context, and does not accept video input.",
|
|
24991
|
+
supportsDynamicTools: true,
|
|
24992
|
+
quotaMultiplier: 1,
|
|
24993
|
+
speed: "regular"
|
|
24994
|
+
},
|
|
24995
|
+
{
|
|
24996
|
+
id: "kimi-for-coding",
|
|
24997
|
+
name: "Kimi for Coding",
|
|
24998
|
+
version: "K2.8 Preview",
|
|
24999
|
+
contextWindow: 1048576,
|
|
25000
|
+
maxContextWindow: null,
|
|
25001
|
+
maxTokens: 32768,
|
|
25002
|
+
maxRequestBodyBytes: LONG_CONTEXT_MESSAGE_BODY_BYTES,
|
|
25003
|
+
inputModalities: [
|
|
25004
|
+
"text",
|
|
25005
|
+
"image",
|
|
25006
|
+
"video"
|
|
25007
|
+
],
|
|
25008
|
+
reasoningEfforts: [
|
|
25009
|
+
"low",
|
|
25010
|
+
"high",
|
|
25011
|
+
"max"
|
|
25012
|
+
],
|
|
25013
|
+
defaultReasoningEffort: "max",
|
|
25014
|
+
minimumPlan: null,
|
|
25015
|
+
contextPlan: null,
|
|
25016
|
+
description: "Performance close to K3 with more efficient thinking, and up to 1M context on every plan. Good at code completion and routine development tasks.",
|
|
25017
|
+
supportsDynamicTools: true,
|
|
25018
|
+
quotaMultiplier: 1,
|
|
25019
|
+
speed: "regular"
|
|
25020
|
+
},
|
|
25021
|
+
{
|
|
25022
|
+
id: "kimi-for-coding-highspeed",
|
|
25023
|
+
name: "Kimi for Coding HighSpeed",
|
|
25024
|
+
version: "K2.7 Code HighSpeed",
|
|
25025
|
+
contextWindow: 262144,
|
|
25026
|
+
maxContextWindow: null,
|
|
25027
|
+
maxTokens: 32768,
|
|
25028
|
+
maxRequestBodyBytes: MAX_MESSAGE_BODY_BYTES,
|
|
25029
|
+
inputModalities: [
|
|
25030
|
+
"text",
|
|
25031
|
+
"image",
|
|
25032
|
+
"video"
|
|
25033
|
+
],
|
|
25034
|
+
reasoningEfforts: ["high"],
|
|
25035
|
+
defaultReasoningEffort: "high",
|
|
25036
|
+
minimumPlan: "Allegretto",
|
|
25037
|
+
contextPlan: null,
|
|
25038
|
+
description: "The high-speed version of K2.7 Code with the same coding ability and roughly 5-6x faster output, at 3x quota usage. Requires the Allegretto plan or above.",
|
|
25039
|
+
supportsDynamicTools: false,
|
|
25040
|
+
quotaMultiplier: 3,
|
|
25041
|
+
speed: "highspeed"
|
|
25042
|
+
},
|
|
25043
|
+
{
|
|
25044
|
+
id: "k3.1-pro",
|
|
25045
|
+
name: "K3.1 Pro",
|
|
25046
|
+
version: "K3.1 Pro",
|
|
25047
|
+
contextWindow: 262144,
|
|
25048
|
+
maxContextWindow: 1048576,
|
|
25049
|
+
maxTokens: 32768,
|
|
25050
|
+
maxRequestBodyBytes: LONG_CONTEXT_MESSAGE_BODY_BYTES,
|
|
25051
|
+
inputModalities: [
|
|
25052
|
+
"text",
|
|
25053
|
+
"image",
|
|
25054
|
+
"video"
|
|
25055
|
+
],
|
|
25056
|
+
reasoningEfforts: [
|
|
25057
|
+
"low",
|
|
25058
|
+
"high",
|
|
25059
|
+
"max"
|
|
25060
|
+
],
|
|
25061
|
+
defaultReasoningEffort: "max",
|
|
25062
|
+
thinkingType: "only",
|
|
25063
|
+
minimumPlan: "Moderato",
|
|
25064
|
+
contextPlan: "Allegretto",
|
|
25065
|
+
description: "The K3.1 Pro flagship: 3T parameters, a 256K context window on Plus/Moderato and 1M on Pro/Allegretto and above. Thinking runs at low/high/max, defaults to max, and cannot be disabled.",
|
|
25066
|
+
supportsDynamicTools: true,
|
|
25067
|
+
quotaMultiplier: null,
|
|
25068
|
+
speed: "regular"
|
|
25069
|
+
},
|
|
25070
|
+
{
|
|
25071
|
+
id: "k3.1-pro-highspeed",
|
|
25072
|
+
name: "K3.1 Pro Highspeed",
|
|
25073
|
+
version: "K3.1 Pro Highspeed",
|
|
25074
|
+
contextWindow: 1048576,
|
|
25075
|
+
maxContextWindow: null,
|
|
25076
|
+
maxTokens: 32768,
|
|
25077
|
+
maxRequestBodyBytes: LONG_CONTEXT_MESSAGE_BODY_BYTES,
|
|
25078
|
+
inputModalities: [
|
|
25079
|
+
"text",
|
|
25080
|
+
"image",
|
|
25081
|
+
"video"
|
|
25082
|
+
],
|
|
25083
|
+
reasoningEfforts: [
|
|
25084
|
+
"low",
|
|
25085
|
+
"high",
|
|
25086
|
+
"max"
|
|
25087
|
+
],
|
|
25088
|
+
defaultReasoningEffort: "max",
|
|
25089
|
+
thinkingType: "only",
|
|
25090
|
+
minimumPlan: "Allegretto",
|
|
25091
|
+
contextPlan: null,
|
|
25092
|
+
description: "The high-speed K3.1 Pro with a 1M context window and faster output, at about twice the quota of the regular version. Requires the Pro/Allegretto plan or above.",
|
|
25093
|
+
supportsDynamicTools: true,
|
|
25094
|
+
quotaMultiplier: null,
|
|
25095
|
+
speed: "highspeed"
|
|
25096
|
+
}
|
|
25097
|
+
];
|
|
25098
|
+
const BY_ID$1 = new Map(KIMI_CODE_MODELS.map((model) => [model.id, model]));
|
|
25099
|
+
/** Registry entry for one model id, or undefined when the id is unknown. */
|
|
25100
|
+
function kimiCodeModelDef(modelId) {
|
|
25101
|
+
return BY_ID$1.get(modelId.trim());
|
|
25102
|
+
}
|
|
25103
|
+
//#endregion
|
|
24177
25104
|
//#region src/host/kimi-code/types.ts
|
|
24178
25105
|
const PROVIDER_ID$8 = "kimi-code";
|
|
24179
25106
|
const PROVIDER_NAME$7 = "Kimi Code";
|
|
@@ -24274,9 +25201,22 @@ function openAIUrl(suffix, region = "mainland-cn") {
|
|
|
24274
25201
|
* spanning both tiers could not express "relax the text ceiling, leave the video
|
|
24275
25202
|
* one alone", and a value chosen for a 2 MB conversation would collapse the video
|
|
24276
25203
|
* tier with it. MiniMax's video budget is likewise a bare constant.
|
|
25204
|
+
*
|
|
25205
|
+
* The text/image tier is now graded by model: this constant is what the 256K
|
|
25206
|
+
* models keep and what an unknown model falls back to, while the 1M models
|
|
25207
|
+
* declare their own, larger ceiling in the registry. {@link maxRequestBodyBytesFor}
|
|
25208
|
+
* is the resolver that applies that grade.
|
|
24277
25209
|
*/
|
|
24278
25210
|
const DEFAULT_MAX_MESSAGE_BODY_BYTES$1 = MAX_MESSAGE_BODY_BYTES;
|
|
24279
|
-
/**
|
|
25211
|
+
/**
|
|
25212
|
+
* Ceiling this process enforces when the caller has no model to key on.
|
|
25213
|
+
*
|
|
25214
|
+
* Kept alongside {@link maxRequestBodyBytesFor} rather than folded into it: this
|
|
25215
|
+
* is the answer for "what does this deployment allow at all", which the video
|
|
25216
|
+
* budget and the settings reporting paths need without naming a model. A request
|
|
25217
|
+
* that DOES name one goes through the per-model resolver below, so a 1M-tier
|
|
25218
|
+
* session gets its own tier instead of this floor.
|
|
25219
|
+
*/
|
|
24280
25220
|
function maxMessageBodyBytes$1(env = process.env) {
|
|
24281
25221
|
const raw = (env.DSH_KIMI_CODE_MAX_BODY_BYTES ?? "").trim();
|
|
24282
25222
|
if (raw === "") return DEFAULT_MAX_MESSAGE_BODY_BYTES$1;
|
|
@@ -24285,6 +25225,35 @@ function maxMessageBodyBytes$1(env = process.env) {
|
|
|
24285
25225
|
return Math.floor(parsed);
|
|
24286
25226
|
}
|
|
24287
25227
|
/**
|
|
25228
|
+
* Request-body ceiling for one model, in precedence order.
|
|
25229
|
+
*
|
|
25230
|
+
* The middleware gateway that rejects a body is shared and the sessions are not:
|
|
25231
|
+
* a 1M-tier session produces several megabytes of conversation the 256K tier
|
|
25232
|
+
* cannot, so one number for both either starves the long tier or leaves the
|
|
25233
|
+
* short one unguarded. Hence three sources, highest priority first:
|
|
25234
|
+
*
|
|
25235
|
+
* 1. DSH_KIMI_CODE_MAX_BODY_BYTES — a measured figure for THIS deployment, so it
|
|
25236
|
+
* wins over every per-model default. It still has to be a positive integer:
|
|
25237
|
+
* a typo falls through to the model's own default rather than switching the
|
|
25238
|
+
* guard off, which is the one failure this variable must never cause;
|
|
25239
|
+
* 2. the model's {@link KimiCodeCatalogModel.maxRequestBodyBytes};
|
|
25240
|
+
* 3. {@link DEFAULT_MAX_MESSAGE_BODY_BYTES}, for a model the registry does not
|
|
25241
|
+
* know — an unknown id keeps the most conservative ceiling rather than
|
|
25242
|
+
* inheriting a long-context allowance nothing has vouched for.
|
|
25243
|
+
*
|
|
25244
|
+
* @param model the registry row for the model being called, or undefined when
|
|
25245
|
+
* the id is not one this route ships.
|
|
25246
|
+
* @param env environment the override is read from, injectable for tests.
|
|
25247
|
+
*/
|
|
25248
|
+
function maxRequestBodyBytesFor(model, env = process.env) {
|
|
25249
|
+
const raw = (env.DSH_KIMI_CODE_MAX_BODY_BYTES ?? "").trim();
|
|
25250
|
+
if (raw !== "") {
|
|
25251
|
+
const parsed = Number(raw);
|
|
25252
|
+
if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
|
|
25253
|
+
}
|
|
25254
|
+
return model?.maxRequestBodyBytes ?? DEFAULT_MAX_MESSAGE_BODY_BYTES$1;
|
|
25255
|
+
}
|
|
25256
|
+
/**
|
|
24288
25257
|
* Bytes one METERED token is assumed to occupy when a deployment asks this route
|
|
24289
25258
|
* to fold its published window against the byte ceiling.
|
|
24290
25259
|
*
|
|
@@ -24518,10 +25487,6 @@ function maxOutputTokensFor$4(modelId, contextWindow) {
|
|
|
24518
25487
|
const capped = (contextWindow ?? kimiCodeModelDef(modelId)?.contextWindow ?? 262144) - CONTEXT_HEADROOM_TOKENS$1;
|
|
24519
25488
|
return Math.max(declared, capped);
|
|
24520
25489
|
}
|
|
24521
|
-
/** Whether a stored preference names a tier this line can send. */
|
|
24522
|
-
function isKimiCacheTtl(value) {
|
|
24523
|
-
return value === "5m" || value === "1h";
|
|
24524
|
-
}
|
|
24525
25490
|
/**
|
|
24526
25491
|
* Reduce the requested output cap so prompt + output fit the window.
|
|
24527
25492
|
*
|
|
@@ -24571,8 +25536,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
24571
25536
|
enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$4],
|
|
24572
25537
|
catalogModels: [],
|
|
24573
25538
|
contextWindowOverrides: {},
|
|
24574
|
-
defaultReasoningEffort: null
|
|
24575
|
-
cacheTtl: null
|
|
25539
|
+
defaultReasoningEffort: null
|
|
24576
25540
|
};
|
|
24577
25541
|
fallbackStore.read().then((stored) => {
|
|
24578
25542
|
snapshot = stored;
|
|
@@ -24589,12 +25553,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
24589
25553
|
enabled: z.boolean().default(true),
|
|
24590
25554
|
enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS$4]),
|
|
24591
25555
|
contextWindowOverrides: z.dict(z.number()).default({}),
|
|
24592
|
-
defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null)
|
|
24593
|
-
cacheTtl: z.union([
|
|
24594
|
-
z.const("5m"),
|
|
24595
|
-
z.const("1h"),
|
|
24596
|
-
z.const(null)
|
|
24597
|
-
]).default(null)
|
|
25556
|
+
defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null)
|
|
24598
25557
|
}));
|
|
24599
25558
|
return {
|
|
24600
25559
|
status: () => {
|
|
@@ -24604,8 +25563,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
24604
25563
|
enabledModelIds: value.enabledModelIds,
|
|
24605
25564
|
catalogModels: [],
|
|
24606
25565
|
contextWindowOverrides: value.contextWindowOverrides,
|
|
24607
|
-
defaultReasoningEffort: value.defaultReasoningEffort
|
|
24608
|
-
cacheTtl: isKimiCacheTtl(value.cacheTtl) ? value.cacheTtl : null
|
|
25566
|
+
defaultReasoningEffort: value.defaultReasoningEffort
|
|
24609
25567
|
};
|
|
24610
25568
|
},
|
|
24611
25569
|
update: async (patch) => {
|
|
@@ -24614,8 +25572,7 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
24614
25572
|
enabled: patch.enabled !== void 0 ? patch.enabled : current.enabled !== false,
|
|
24615
25573
|
enabledModelIds: patch.enabledModelIds ?? current.enabledModelIds,
|
|
24616
25574
|
contextWindowOverrides: patch.contextWindowOverrides ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
|
|
24617
|
-
defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort
|
|
24618
|
-
cacheTtl: patch.cacheTtl !== void 0 ? patch.cacheTtl : current.cacheTtl
|
|
25575
|
+
defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort
|
|
24619
25576
|
};
|
|
24620
25577
|
await scope.update(normalized);
|
|
24621
25578
|
fallbackStore.updateSettings(patch).catch(() => void 0);
|
|
@@ -24809,8 +25766,7 @@ var FileModelSettingsStore$3 = class {
|
|
|
24809
25766
|
enabledModelIds,
|
|
24810
25767
|
catalogModels,
|
|
24811
25768
|
contextWindowOverrides,
|
|
24812
|
-
defaultReasoningEffort
|
|
24813
|
-
cacheTtl: isKimiCacheTtl(record.cacheTtl) ? record.cacheTtl : null
|
|
25769
|
+
defaultReasoningEffort
|
|
24814
25770
|
};
|
|
24815
25771
|
}
|
|
24816
25772
|
} catch {}
|
|
@@ -24819,8 +25775,7 @@ var FileModelSettingsStore$3 = class {
|
|
|
24819
25775
|
enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$4],
|
|
24820
25776
|
catalogModels: [],
|
|
24821
25777
|
contextWindowOverrides: {},
|
|
24822
|
-
defaultReasoningEffort: null
|
|
24823
|
-
cacheTtl: null
|
|
25778
|
+
defaultReasoningEffort: null
|
|
24824
25779
|
};
|
|
24825
25780
|
}
|
|
24826
25781
|
async write(settings) {
|
|
@@ -24836,8 +25791,7 @@ var FileModelSettingsStore$3 = class {
|
|
|
24836
25791
|
...patch.enabled !== void 0 ? { enabled: patch.enabled } : {},
|
|
24837
25792
|
...patch.enabledModelIds !== void 0 ? { enabledModelIds: patch.enabledModelIds } : {},
|
|
24838
25793
|
...patch.contextWindowOverrides !== void 0 ? { contextWindowOverrides: mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) } : {},
|
|
24839
|
-
...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {}
|
|
24840
|
-
...patch.cacheTtl !== void 0 && patch.cacheTtl !== null ? { cacheTtl: isKimiCacheTtl(patch.cacheTtl) ? patch.cacheTtl : current.cacheTtl } : patch.cacheTtl === null ? { cacheTtl: null } : {}
|
|
25794
|
+
...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {}
|
|
24841
25795
|
};
|
|
24842
25796
|
await this.write(next);
|
|
24843
25797
|
return next;
|
|
@@ -24880,20 +25834,6 @@ async function persistRegion(region) {
|
|
|
24880
25834
|
await fs.writeFile(file, region, "utf8");
|
|
24881
25835
|
} catch {}
|
|
24882
25836
|
}
|
|
24883
|
-
//#endregion
|
|
24884
|
-
//#region src/host/kimi-code/cache-hint.ts
|
|
24885
|
-
/**
|
|
24886
|
-
* How long an entry written at each tier survives.
|
|
24887
|
-
*
|
|
24888
|
-
* The tier is LOCKED when the prefix is first written, so this is the lifetime
|
|
24889
|
-
* of the entry a request actually created rather than of the tier as a name.
|
|
24890
|
-
*/
|
|
24891
|
-
const CACHE_TTL_MS = {
|
|
24892
|
-
"5m": 300 * 1e3,
|
|
24893
|
-
"1h": 3600 * 1e3
|
|
24894
|
-
};
|
|
24895
|
-
/** Tier assumed when the conversation states none. */
|
|
24896
|
-
const DEFAULT_TIER = "5m";
|
|
24897
25837
|
/**
|
|
24898
25838
|
* Last time each session did something, epoch ms.
|
|
24899
25839
|
*
|
|
@@ -24938,9 +25878,8 @@ function evaluateCacheHint(input) {
|
|
|
24938
25878
|
if (lastActiveAt === void 0 || totalTokens === void 0) return { kind: "skip" };
|
|
24939
25879
|
if (!Number.isFinite(lastActiveAt) || !Number.isFinite(totalTokens)) return { kind: "skip" };
|
|
24940
25880
|
if (totalTokens < 1e5) return { kind: "skip" };
|
|
24941
|
-
const tier = input.cacheTtl ?? DEFAULT_TIER;
|
|
24942
25881
|
const idleMs = now - lastActiveAt;
|
|
24943
|
-
if (idleMs <=
|
|
25882
|
+
if (idleMs <= 18e5) return { kind: "skip" };
|
|
24944
25883
|
return {
|
|
24945
25884
|
kind: "hint",
|
|
24946
25885
|
idleMs,
|
|
@@ -27183,39 +28122,6 @@ function thinkingBudgetFor(effort, maxTokens) {
|
|
|
27183
28122
|
const budget = Math.min(effort === "low" ? 2048 : effort === "high" ? 8192 : 16384, maxTokens - 1024);
|
|
27184
28123
|
return budget >= 1024 ? budget : void 0;
|
|
27185
28124
|
}
|
|
27186
|
-
/**
|
|
27187
|
-
* Media a single request build may carry.
|
|
27188
|
-
*
|
|
27189
|
-
* Passed as one object so adding a media kind never grows a positional
|
|
27190
|
-
* signature the existing callers already bind.
|
|
27191
|
-
*/
|
|
27192
|
-
/**
|
|
27193
|
-
* The cache field for the Anthropic-compatible wire.
|
|
27194
|
-
*
|
|
27195
|
-
* TOP LEVEL, not per block: the service documents that a `cache_control` inside
|
|
27196
|
-
* a message is ignored, and omitting the field means "read a 5m entry but do
|
|
27197
|
-
* not write one" — which is why this is only attached when the caller asked
|
|
27198
|
-
* for a tier.
|
|
27199
|
-
*/
|
|
27200
|
-
function kimiAnthropicCacheControl(ttl) {
|
|
27201
|
-
return { cache_control: {
|
|
27202
|
-
type: "ephemeral",
|
|
27203
|
-
ttl
|
|
27204
|
-
} };
|
|
27205
|
-
}
|
|
27206
|
-
/**
|
|
27207
|
-
* The cache field for the OpenAI-compatible wire.
|
|
27208
|
-
*
|
|
27209
|
-
* `mode: 'implicit'` is the only mode the service supports — it identifies the
|
|
27210
|
-
* prefix itself, so this is not a request to break the cache but a request to
|
|
27211
|
-
* write it at a chosen TTL.
|
|
27212
|
-
*/
|
|
27213
|
-
function kimiOpenAICacheOptions(ttl) {
|
|
27214
|
-
return { prompt_cache_options: {
|
|
27215
|
-
mode: "implicit",
|
|
27216
|
-
ttl
|
|
27217
|
-
} };
|
|
27218
|
-
}
|
|
27219
28125
|
function textOf$6(content) {
|
|
27220
28126
|
if (typeof content === "string") return sanitizeText$6(content);
|
|
27221
28127
|
if (!Array.isArray(content)) return "";
|
|
@@ -27602,7 +28508,6 @@ function buildOpenAIRequest(options, images = NO_RESOLVED_IMAGES$3, preserveThin
|
|
|
27602
28508
|
stream: true,
|
|
27603
28509
|
stream_options: { include_usage: true },
|
|
27604
28510
|
max_completion_tokens: maxTokens,
|
|
27605
|
-
...media.cacheTtl === null || media.cacheTtl === void 0 ? {} : kimiOpenAICacheOptions(media.cacheTtl),
|
|
27606
28511
|
...stopSequences(options.stop).length > 0 ? { stop: stopSequences(options.stop) } : {},
|
|
27607
28512
|
...options.tools && options.tools.length > 0 ? {
|
|
27608
28513
|
tools: options.tools.map((tool) => ({
|
|
@@ -27737,7 +28642,6 @@ function mergeAnthropicMessages$2(entries) {
|
|
|
27737
28642
|
}
|
|
27738
28643
|
/** Build one `/v1/messages` body. */
|
|
27739
28644
|
function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {}) {
|
|
27740
|
-
const cacheTtl = media.cacheTtl ?? null;
|
|
27741
28645
|
const toolCallIds = new ToolCallIdNormalizer();
|
|
27742
28646
|
const carriers = options.messages.filter((message) => message.role === "system" && messageToolsOf(message) !== void 0);
|
|
27743
28647
|
const unsentDeclarations = carriers.reduce((total, message) => total + (messageToolsOf(message)?.length ?? 0), 0);
|
|
@@ -27769,7 +28673,6 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {
|
|
|
27769
28673
|
model: options.model,
|
|
27770
28674
|
max_tokens: maxTokens,
|
|
27771
28675
|
messages: mergeAnthropicMessages$2(entries),
|
|
27772
|
-
...cacheTtl === null || cacheTtl === void 0 ? {} : kimiAnthropicCacheControl(cacheTtl),
|
|
27773
28676
|
...system === void 0 ? {} : { system },
|
|
27774
28677
|
...options.temperature === void 0 || budget !== void 0 ? {} : { temperature: options.temperature },
|
|
27775
28678
|
...options.stop && options.stop.length > 0 ? { stop_sequences: options.stop } : {},
|
|
@@ -27789,12 +28692,40 @@ function buildRequest(options, wire, images = NO_RESOLVED_IMAGES$3, preserveThin
|
|
|
27789
28692
|
recordDriftCause(trackPrefixStability(options), options.sessionId);
|
|
27790
28693
|
return wire === "anthropic" ? buildAnthropicRequest(options, images, media) : buildOpenAIRequest(options, images, preserveThinking, media);
|
|
27791
28694
|
}
|
|
27792
|
-
|
|
28695
|
+
/**
|
|
28696
|
+
* Reject a request the service would refuse for its size.
|
|
28697
|
+
*
|
|
28698
|
+
* This is the most frequently reported 400 on the coding endpoint, and it is
|
|
28699
|
+
* worth catching locally for two reasons: the message can name the actual
|
|
28700
|
+
* remedy (DSH's compaction), and a request that cannot succeed should not be
|
|
28701
|
+
* sent at all. The measured size is the real serialized body, so it accounts
|
|
28702
|
+
* for tool schemas and inlined images the caller cannot easily estimate.
|
|
28703
|
+
*
|
|
28704
|
+
* The ceiling is the caller's when it passes one, because the text/image tier is
|
|
28705
|
+
* graded by model and only the caller knows which model the turn is for; omitted,
|
|
28706
|
+
* the guard resolves the model-less default (or the video tier) itself.
|
|
28707
|
+
*
|
|
28708
|
+
* The error code is CONTEXT_OVERFLOW_CODE, not PROVIDER_ERROR, because naming the
|
|
28709
|
+
* remedy in prose is not the same as being able to act on it: DSH forces a
|
|
28710
|
+
* compaction and retries the same step only for that one code, and every other
|
|
28711
|
+
* 400-shaped failure ends the turn. A byte ceiling is answered by the same
|
|
28712
|
+
* compression a token ceiling is — the conversation shrinks either way — so the
|
|
28713
|
+
* body of the message stays as specific as it was while the code becomes one the
|
|
28714
|
+
* host can recover from.
|
|
28715
|
+
*
|
|
28716
|
+
* @param body the request body about to be serialized and sent.
|
|
28717
|
+
* @param carriesVideo whether the request attaches video, which has its own tier.
|
|
28718
|
+
* @param context what the body-fit loop already did, when it ran.
|
|
28719
|
+
* @param limit the ceiling already resolved for this request's model.
|
|
28720
|
+
* @returns the serialized body, so a caller that is about to send it does not
|
|
28721
|
+
* serialize a body of up to tens of megabytes a second time.
|
|
28722
|
+
*/
|
|
28723
|
+
function assertRequestBodyFits$1(body, carriesVideo = false, context, limit) {
|
|
27793
28724
|
const serialized = JSON.stringify(body);
|
|
27794
28725
|
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
27795
|
-
const
|
|
27796
|
-
if (bytes <=
|
|
27797
|
-
throw new LlmError$1(oversizedBodyMessage(bytes,
|
|
28726
|
+
const ceiling = limit ?? (carriesVideo ? 67108864 : maxMessageBodyBytes$1());
|
|
28727
|
+
if (bytes <= ceiling) return serialized;
|
|
28728
|
+
throw new LlmError$1(oversizedBodyMessage(bytes, ceiling, body, context), CONTEXT_OVERFLOW_CODE);
|
|
27798
28729
|
}
|
|
27799
28730
|
function walkJson(value, visit) {
|
|
27800
28731
|
if (Array.isArray(value)) {
|
|
@@ -29022,8 +29953,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
29022
29953
|
const media = {
|
|
29023
29954
|
videos,
|
|
29024
29955
|
videoAccepted: inputModalitiesForEntry(options.model, catalog).includes("video"),
|
|
29025
|
-
messageTools: dynamicToolsForEntry(options.model, catalog)
|
|
29026
|
-
cacheTtl: settings.cacheTtl
|
|
29956
|
+
messageTools: dynamicToolsForEntry(options.model, catalog)
|
|
29027
29957
|
};
|
|
29028
29958
|
const contextWindow = this.contextWindowFor(options.model, entry, settings.contextWindowOverrides);
|
|
29029
29959
|
const requestedMax = options.maxTokens ?? maxOutputTokensFor$4(options.model, contextWindow);
|
|
@@ -29034,7 +29964,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
29034
29964
|
maxTokens: clampOutputToContext$1(requestedMax, promptLimit, estimatedInputTokens(requestOptions))
|
|
29035
29965
|
};
|
|
29036
29966
|
const carriesVideo = requestHasVideo(requestOptions);
|
|
29037
|
-
const bodyLimit = carriesVideo ? MAX_VIDEO_MESSAGE_BODY_BYTES :
|
|
29967
|
+
const bodyLimit = carriesVideo ? MAX_VIDEO_MESSAGE_BODY_BYTES : maxRequestBodyBytesFor(kimiCodeModelDef(entry?.id ?? options.model), process.env);
|
|
29038
29968
|
let buildOptions = boundedOptions;
|
|
29039
29969
|
let body = "";
|
|
29040
29970
|
for (let attempt = 0;; attempt++) {
|
|
@@ -29048,12 +29978,12 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
29048
29978
|
excessBytes: bytes - bodyLimit
|
|
29049
29979
|
});
|
|
29050
29980
|
if (attempt > MAX_BODY_FIT_ATTEMPTS || !requestHasSendableImage(buildOptions, images)) {
|
|
29051
|
-
assertRequestBodyFits$1(built, carriesVideo, context());
|
|
29981
|
+
assertRequestBodyFits$1(built, carriesVideo, context(), bodyLimit);
|
|
29052
29982
|
throw new Error("unreachable: assertRequestBodyFits returns or throws");
|
|
29053
29983
|
}
|
|
29054
29984
|
const drop = dropOldestImagesUntilFits(buildOptions, images, bytes - bodyLimit, requestOptions);
|
|
29055
29985
|
if (drop.dropped === 0) {
|
|
29056
|
-
assertRequestBodyFits$1(built, carriesVideo, context());
|
|
29986
|
+
assertRequestBodyFits$1(built, carriesVideo, context(), bodyLimit);
|
|
29057
29987
|
throw new Error("unreachable: assertRequestBodyFits returns or throws");
|
|
29058
29988
|
}
|
|
29059
29989
|
buildOptions = drop.options;
|
|
@@ -29568,12 +30498,11 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
|
|
|
29568
30498
|
lastFetchedAt: quota?.fetchedAt ?? null,
|
|
29569
30499
|
credentialsRejected: active !== null && active !== void 0 && isRefreshTokenRejected$1(active.refreshToken),
|
|
29570
30500
|
cache: cacheStatsOrNull(options.sessionId),
|
|
29571
|
-
cacheHint: cacheHintOrNull(options.sessionId
|
|
30501
|
+
cacheHint: cacheHintOrNull(options.sessionId),
|
|
29572
30502
|
preserveThinking: preserveThinkingEnabled(),
|
|
29573
30503
|
models,
|
|
29574
30504
|
contextWindowOverrides: settings.contextWindowOverrides,
|
|
29575
30505
|
defaultReasoningEffort: settings.defaultReasoningEffort,
|
|
29576
|
-
cacheTtl: settings.cacheTtl,
|
|
29577
30506
|
loginRegion: region,
|
|
29578
30507
|
serving: readOption$5(options.serving, true),
|
|
29579
30508
|
conflict: readOption$5(options.conflict, null)
|
|
@@ -29608,13 +30537,12 @@ function cacheStatsOrNull(sessionId) {
|
|
|
29608
30537
|
* the hint describes the conversation the card is actually showing rather than
|
|
29609
30538
|
* some global figure.
|
|
29610
30539
|
*/
|
|
29611
|
-
function cacheHintOrNull(sessionId
|
|
30540
|
+
function cacheHintOrNull(sessionId) {
|
|
29612
30541
|
const lastActiveAt = sessionLastActiveAt(sessionId);
|
|
29613
30542
|
const decision = evaluateCacheHint({
|
|
29614
30543
|
now: Date.now(),
|
|
29615
30544
|
lastActiveAt,
|
|
29616
|
-
totalTokens: getCacheStats(sessionId).cachedTokens + getCacheStats(sessionId).freshTokens
|
|
29617
|
-
cacheTtl
|
|
30545
|
+
totalTokens: getCacheStats(sessionId).cachedTokens + getCacheStats(sessionId).freshTokens
|
|
29618
30546
|
});
|
|
29619
30547
|
if (decision.kind !== "hint") return null;
|
|
29620
30548
|
return {
|
|
@@ -29838,10 +30766,6 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
|
|
|
29838
30766
|
const effort = body.defaultReasoningEffort;
|
|
29839
30767
|
if (effort === null || isKimiCodeEffort(effort)) patch.defaultReasoningEffort = effort;
|
|
29840
30768
|
}
|
|
29841
|
-
if (body.cacheTtl !== void 0) {
|
|
29842
|
-
const ttl = body.cacheTtl;
|
|
29843
|
-
if (ttl === null || isKimiCacheTtl(ttl)) patch.cacheTtl = ttl;
|
|
29844
|
-
}
|
|
29845
30769
|
if (preferences) await preferences.update(patch);
|
|
29846
30770
|
else await modelSettings.updateSettings(patch);
|
|
29847
30771
|
if (patch.enabledModelIds !== void 0 || patch.enabled !== void 0) ctx.emit?.("llm/adapters-updated");
|
|
@@ -40202,7 +41126,7 @@ const CLAUDE_MODELS = Object.freeze([
|
|
|
40202
41126
|
"xhigh",
|
|
40203
41127
|
"max"
|
|
40204
41128
|
],
|
|
40205
|
-
canDisableThinking:
|
|
41129
|
+
canDisableThinking: true
|
|
40206
41130
|
},
|
|
40207
41131
|
{
|
|
40208
41132
|
id: "claude-opus-5-5",
|
|
@@ -40226,7 +41150,7 @@ const CLAUDE_MODELS = Object.freeze([
|
|
|
40226
41150
|
{
|
|
40227
41151
|
id: "claude-sonnet-4-5",
|
|
40228
41152
|
name: "Claude Sonnet 4.5 (latest)",
|
|
40229
|
-
contextWindow:
|
|
41153
|
+
contextWindow: 2e5,
|
|
40230
41154
|
maxTokens: 64e3,
|
|
40231
41155
|
supportsImage: true,
|
|
40232
41156
|
supportsTemperature: true,
|
|
@@ -40241,7 +41165,7 @@ const CLAUDE_MODELS = Object.freeze([
|
|
|
40241
41165
|
{
|
|
40242
41166
|
id: "claude-sonnet-4-5-20250929",
|
|
40243
41167
|
name: "Claude Sonnet 4.5",
|
|
40244
|
-
contextWindow:
|
|
41168
|
+
contextWindow: 2e5,
|
|
40245
41169
|
maxTokens: 64e3,
|
|
40246
41170
|
supportsImage: true,
|
|
40247
41171
|
supportsTemperature: true,
|
|
@@ -42502,23 +43426,19 @@ function thinkingRequestedOff(effort) {
|
|
|
42502
43426
|
* This is what decides whether the response ceiling is inflated by the level's
|
|
42503
43427
|
* budget, and getting it wrong in the generous direction is not harmless: a
|
|
42504
43428
|
* non-thinking model whose ceiling was raised by a budget it never had would
|
|
42505
|
-
* ask the server for tokens it cannot produce.
|
|
42506
|
-
* caller asked for
|
|
42507
|
-
* to be sent and the model allows one.
|
|
43429
|
+
* ask the server for tokens it cannot produce. Every form thinks unless the
|
|
43430
|
+
* caller asked for thinking off on a model that accepts a disabled request.
|
|
42508
43431
|
*/
|
|
42509
43432
|
function claudeRequestThinks(model, effort) {
|
|
42510
|
-
|
|
42511
|
-
if (mode === "none") return false;
|
|
42512
|
-
if (mode === "mid-convo") return true;
|
|
43433
|
+
if (claudeThinkingMode(model) === "none") return false;
|
|
42513
43434
|
return !(thinkingRequestedOff(effort) && claudeModelCanDisableThinking(model));
|
|
42514
43435
|
}
|
|
42515
43436
|
/**
|
|
42516
43437
|
* Dispatch the four thinking cases for one model and effort.
|
|
42517
43438
|
*
|
|
42518
|
-
* The
|
|
42519
|
-
*
|
|
42520
|
-
*
|
|
42521
|
-
* module doc for what each form is and why.
|
|
43439
|
+
* The disable request is checked first. A mid-convo model that cannot disable
|
|
43440
|
+
* still gets the adaptive form with its block_binding; one that can disable
|
|
43441
|
+
* takes that form without an effort override. See the module doc for the cases.
|
|
42522
43442
|
*
|
|
42523
43443
|
* @param model - exact model id, resolved through the frozen catalog.
|
|
42524
43444
|
* @param effort - the caller's raw effort, passed through as-is.
|
|
@@ -42531,7 +43451,7 @@ function claudeThinking(model, effort, level, maxTokens, customBudgets) {
|
|
|
42531
43451
|
const mode = claudeThinkingMode(model);
|
|
42532
43452
|
if (mode === "none") return {};
|
|
42533
43453
|
const namedEffort = effort !== void 0 && effort !== "" && !thinkingRequestedOff(effort) ? effort : void 0;
|
|
42534
|
-
|
|
43454
|
+
if (thinkingRequestedOff(effort) && claudeModelCanDisableThinking(model)) return { thinking: { type: "disabled" } };
|
|
42535
43455
|
if (mode === "mid-convo") return {
|
|
42536
43456
|
thinking: {
|
|
42537
43457
|
type: "adaptive",
|
|
@@ -42540,7 +43460,6 @@ function claudeThinking(model, effort, level, maxTokens, customBudgets) {
|
|
|
42540
43460
|
},
|
|
42541
43461
|
outputConfig: { effort: namedEffort ?? "high" }
|
|
42542
43462
|
};
|
|
42543
|
-
if (off && claudeModelCanDisableThinking(model)) return { thinking: { type: "disabled" } };
|
|
42544
43463
|
if (mode === "adaptive") return {
|
|
42545
43464
|
thinking: {
|
|
42546
43465
|
type: "adaptive",
|
|
@@ -58821,6 +59740,20 @@ const REASONING_EFFORTS = [
|
|
|
58821
59740
|
* configured request budget rather than taken as the budget itself.
|
|
58822
59741
|
*/
|
|
58823
59742
|
const MODEL_CATALOG = Object.freeze({
|
|
59743
|
+
"cline-pass/deepseek-v4.1-flash-1017": {
|
|
59744
|
+
name: "DeepSeek V4.1 Flash 1017",
|
|
59745
|
+
contextWindow: 1048576,
|
|
59746
|
+
maxTokens: 384e3,
|
|
59747
|
+
reasoning: true,
|
|
59748
|
+
efforts: [
|
|
59749
|
+
"none",
|
|
59750
|
+
"low",
|
|
59751
|
+
"high",
|
|
59752
|
+
"max"
|
|
59753
|
+
],
|
|
59754
|
+
input: ["text", "image"],
|
|
59755
|
+
gatewayHas: ["vision", "implicit-caching"]
|
|
59756
|
+
},
|
|
58824
59757
|
"cline-pass/deepseek-v4.1-flash": {
|
|
58825
59758
|
name: "DeepSeek V4.1 Flash",
|
|
58826
59759
|
contextWindow: 1e6,
|
|
@@ -58983,13 +59916,34 @@ const DEFAULT_MODELS = Object.freeze(Object.keys(MODEL_CATALOG));
|
|
|
58983
59916
|
function catalogEntry(model) {
|
|
58984
59917
|
return MODEL_CATALOG[model];
|
|
58985
59918
|
}
|
|
58986
|
-
/** The default
|
|
58987
|
-
function
|
|
59919
|
+
/** The capability filter a model needs by default; empty for all others. */
|
|
59920
|
+
function defaultGatewayHas(model) {
|
|
59921
|
+
return [...catalogEntry(model)?.gatewayHas ?? []];
|
|
59922
|
+
}
|
|
59923
|
+
/**
|
|
59924
|
+
* Bound one pin's capability filter, the way channels are bounded.
|
|
59925
|
+
*
|
|
59926
|
+
* Applied on read and again on the wire: `has` is copied into every chat request
|
|
59927
|
+
* body, so a filter list is request payload on every turn, not a private note.
|
|
59928
|
+
*/
|
|
59929
|
+
function capGatewayHas(names) {
|
|
59930
|
+
const capped = [];
|
|
59931
|
+
for (const name of names) {
|
|
59932
|
+
const trimmed = name.trim();
|
|
59933
|
+
if (trimmed === "" || trimmed.length > 64) continue;
|
|
59934
|
+
if (!capped.includes(trimmed)) capped.push(trimmed);
|
|
59935
|
+
if (capped.length >= 16) break;
|
|
59936
|
+
}
|
|
59937
|
+
return capped;
|
|
59938
|
+
}
|
|
59939
|
+
/** The default pin: automatic routing, no exclusions, model-default capabilities. */
|
|
59940
|
+
function defaultPin(model = "") {
|
|
58988
59941
|
return {
|
|
58989
59942
|
upstreams: [],
|
|
58990
59943
|
exclude: [],
|
|
58991
59944
|
pinMode: "strict",
|
|
58992
|
-
sort: ""
|
|
59945
|
+
sort: "",
|
|
59946
|
+
has: defaultGatewayHas(model)
|
|
58993
59947
|
};
|
|
58994
59948
|
}
|
|
58995
59949
|
//#endregion
|
|
@@ -59037,6 +59991,16 @@ function jwtExpiryMs(token) {
|
|
|
59037
59991
|
return;
|
|
59038
59992
|
}
|
|
59039
59993
|
}
|
|
59994
|
+
/** Whether a catalog row belongs in the compact, built-in + official + added list. */
|
|
59995
|
+
function isListedClinePassCatalogModel(model) {
|
|
59996
|
+
if (model.origin === "official" || model.origin === "custom") return true;
|
|
59997
|
+
if (model.origin === "gateway") return false;
|
|
59998
|
+
return model.id.startsWith("cline-pass/");
|
|
59999
|
+
}
|
|
60000
|
+
/** The catalog rows to display/advertise, never the whole gateway search index. */
|
|
60001
|
+
function listedClinePassCatalogModels(settings) {
|
|
60002
|
+
return settings.catalogModels.filter(isListedClinePassCatalogModel);
|
|
60003
|
+
}
|
|
59040
60004
|
function credentialPath$1() {
|
|
59041
60005
|
return path.join(dshHomeDir(), "storages", "cline-pass-credentials.json");
|
|
59042
60006
|
}
|
|
@@ -59130,14 +60094,15 @@ var FileCredentialStore$1 = class {
|
|
|
59130
60094
|
}
|
|
59131
60095
|
};
|
|
59132
60096
|
/** One pin as stored, validated; a malformed row falls back to the default. */
|
|
59133
|
-
function readPin(value) {
|
|
59134
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return defaultPin();
|
|
60097
|
+
function readPin(value, model) {
|
|
60098
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return defaultPin(model);
|
|
59135
60099
|
const record = value;
|
|
59136
60100
|
return {
|
|
59137
60101
|
upstreams: Array.isArray(record.upstreams) ? record.upstreams.filter((entry) => typeof entry === "string" && entry.length > 0) : [],
|
|
59138
60102
|
exclude: Array.isArray(record.exclude) ? record.exclude.filter((entry) => typeof entry === "string" && entry.length > 0) : [],
|
|
59139
60103
|
pinMode: record.pinMode === "preferred" ? "preferred" : "strict",
|
|
59140
|
-
sort: typeof record.sort === "string" ? record.sort : ""
|
|
60104
|
+
sort: typeof record.sort === "string" ? record.sort : "",
|
|
60105
|
+
has: Array.isArray(record.has) ? capGatewayHas(record.has.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())) : defaultGatewayHas(model)
|
|
59141
60106
|
};
|
|
59142
60107
|
}
|
|
59143
60108
|
function readPins(value) {
|
|
@@ -59145,7 +60110,7 @@ function readPins(value) {
|
|
|
59145
60110
|
const pins = {};
|
|
59146
60111
|
for (const [model, pin] of Object.entries(value)) {
|
|
59147
60112
|
if (typeof model !== "string" || model.length === 0) continue;
|
|
59148
|
-
pins[model] = readPin(pin);
|
|
60113
|
+
pins[model] = readPin(pin, model);
|
|
59149
60114
|
}
|
|
59150
60115
|
return pins;
|
|
59151
60116
|
}
|
|
@@ -59172,6 +60137,7 @@ function readCatalogModels$1(value) {
|
|
|
59172
60137
|
if (typeof record.reasoning === "boolean") model.reasoning = record.reasoning;
|
|
59173
60138
|
if (Array.isArray(record.input)) model.input = record.input.filter((item) => typeof item === "string");
|
|
59174
60139
|
if (Array.isArray(record.efforts)) model.efforts = record.efforts.filter((item) => typeof item === "string");
|
|
60140
|
+
if (record.origin === "official" || record.origin === "custom" || record.origin === "gateway") model.origin = record.origin;
|
|
59175
60141
|
models.push(model);
|
|
59176
60142
|
}
|
|
59177
60143
|
return models;
|
|
@@ -59180,18 +60146,41 @@ function positiveInteger$1(value, fallback, allowZero = false) {
|
|
|
59180
60146
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) return fallback;
|
|
59181
60147
|
return value;
|
|
59182
60148
|
}
|
|
60149
|
+
/**
|
|
60150
|
+
* Keep a legacy gateway row the user is demonstrably using.
|
|
60151
|
+
*
|
|
60152
|
+
* Rows written before `origin` existed carry none, and the compact list hides
|
|
60153
|
+
* every unmarked non-`cline-pass/*` row. A row that is enabled, pinned or given
|
|
60154
|
+
* a context override is one the user already selected, so hiding it would drop
|
|
60155
|
+
* a working model out of the picker on upgrade; those rows become `custom`
|
|
60156
|
+
* instead. Everything else stays hidden as a search-index row.
|
|
60157
|
+
*/
|
|
60158
|
+
function promoteInUseCatalogRows(models, enabledModelIds, pins, contextWindowOverrides) {
|
|
60159
|
+
const inUse = /* @__PURE__ */ new Set([
|
|
60160
|
+
...enabledModelIds === null ? models.map((model) => model.id) : enabledModelIds,
|
|
60161
|
+
...Object.keys(pins),
|
|
60162
|
+
...Object.keys(contextWindowOverrides)
|
|
60163
|
+
]);
|
|
60164
|
+
if (inUse.size === 0) return models;
|
|
60165
|
+
return models.map((model) => model.origin !== void 0 || model.id.startsWith("cline-pass/") || !inUse.has(model.id) ? model : {
|
|
60166
|
+
...model,
|
|
60167
|
+
origin: "custom"
|
|
60168
|
+
});
|
|
60169
|
+
}
|
|
59183
60170
|
/** Parse one whole settings document. */
|
|
59184
60171
|
function parseClinePassModelSettings(value) {
|
|
59185
60172
|
const record = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
59186
60173
|
const enabledModelIds = record.enabledModelIds === null ? null : Array.isArray(record.enabledModelIds) ? record.enabledModelIds.filter((id) => typeof id === "string") : null;
|
|
60174
|
+
const pins = readPins(record.pins);
|
|
60175
|
+
const contextWindowOverrides = readNumberOverrides$1(record.contextWindowOverrides);
|
|
59187
60176
|
return {
|
|
59188
60177
|
enabled: record.enabled !== false,
|
|
59189
60178
|
baseURL: typeof record.baseURL === "string" && record.baseURL.trim() !== "" ? record.baseURL.trim() : DEFAULT_BASE_URL,
|
|
59190
60179
|
enabledModelIds,
|
|
59191
|
-
catalogModels: readCatalogModels$1(record.catalogModels),
|
|
59192
|
-
pins
|
|
60180
|
+
catalogModels: promoteInUseCatalogRows(readCatalogModels$1(record.catalogModels), enabledModelIds, pins, contextWindowOverrides),
|
|
60181
|
+
pins,
|
|
59193
60182
|
hiddenModels: Array.isArray(record.hiddenModels) ? record.hiddenModels.filter((id) => typeof id === "string") : [],
|
|
59194
|
-
contextWindowOverrides
|
|
60183
|
+
contextWindowOverrides,
|
|
59195
60184
|
defaultContextWindow: positiveInteger$1(record.defaultContextWindow, DEFAULT_CONTEXT_WINDOW$1),
|
|
59196
60185
|
defaultMaxTokens: positiveInteger$1(record.defaultMaxTokens, DEFAULT_MAX_TOKENS$1),
|
|
59197
60186
|
maxConcurrentRequests: positiveInteger$1(record.maxConcurrentRequests, 16, true),
|
|
@@ -59961,6 +60950,83 @@ function resolveModelInfo$1(model, options) {
|
|
|
59961
60950
|
};
|
|
59962
60951
|
}
|
|
59963
60952
|
//#endregion
|
|
60953
|
+
//#region src/shared/cline-pass-contracts.ts
|
|
60954
|
+
/**
|
|
60955
|
+
* Wire contracts for the Cline Pass line's settings API.
|
|
60956
|
+
*
|
|
60957
|
+
* The line is API-key based: there is no OAuth flow, no token expiry, and no
|
|
60958
|
+
* per-account identity beyond the key and the alias a user gives it. What the
|
|
60959
|
+
* card does render beyond the shared account facts is the routing observation
|
|
60960
|
+
* this plugin makes about each model (pipeline, discovered channels, per-channel
|
|
60961
|
+
* verdicts) and the per-model pin the user stores. None of it is a credential.
|
|
60962
|
+
*/
|
|
60963
|
+
const CLINE_PASS_PROVIDER_ID = "cline-pass";
|
|
60964
|
+
const CLINE_PASS_PROVIDER_NAME = "Cline Pass";
|
|
60965
|
+
const CLINE_PASS_ROUTE_PREFIX = "/cline-pass/api";
|
|
60966
|
+
/**
|
|
60967
|
+
* The gateway's anonymous contributor-route label.
|
|
60968
|
+
*
|
|
60969
|
+
* When Cline routes a model id onto the VMC fallback net, the serving channel
|
|
60970
|
+
* reports itself as `openai-compatible-private` and the resolved slug moves
|
|
60971
|
+
* under `private/…-contributor`. Answers from it may be used for training and
|
|
60972
|
+
* carry no zero-data-retention guarantee, so a pin landing there is a fact the
|
|
60973
|
+
* user must see, not a channel like any other.
|
|
60974
|
+
*/
|
|
60975
|
+
const CLINE_PASS_ANONYMOUS_UPSTREAMS = Object.freeze(["openai-compatible-private"]);
|
|
60976
|
+
/** Whether a channel name or resolved slug names the anonymous contributor route. */
|
|
60977
|
+
function isClinePassAnonymousUpstream(name, canonicalSlug) {
|
|
60978
|
+
const value = String(name ?? "").toLowerCase();
|
|
60979
|
+
if (CLINE_PASS_ANONYMOUS_UPSTREAMS.includes(value)) return true;
|
|
60980
|
+
const slug = String(canonicalSlug ?? "").toLowerCase();
|
|
60981
|
+
return slug.startsWith("private/") || slug.includes("-contributor");
|
|
60982
|
+
}
|
|
60983
|
+
/** Whether a submitted pin mode names one of the two accepted modes. */
|
|
60984
|
+
function isClinePassPinMode(value) {
|
|
60985
|
+
return value === "strict" || value === "preferred";
|
|
60986
|
+
}
|
|
60987
|
+
/** Human label for the gateway's quota-window spelling. */
|
|
60988
|
+
function clinePassWindowLabel(type) {
|
|
60989
|
+
switch (type) {
|
|
60990
|
+
case "five_hour": return "5 小时";
|
|
60991
|
+
case "weekly": return "每周";
|
|
60992
|
+
case "monthly": return "每月";
|
|
60993
|
+
default: return type;
|
|
60994
|
+
}
|
|
60995
|
+
}
|
|
60996
|
+
/** Window length in minutes for the shared AccountQuota formatter. */
|
|
60997
|
+
function clinePassWindowDurationMins(type) {
|
|
60998
|
+
switch (type) {
|
|
60999
|
+
case "five_hour": return 300;
|
|
61000
|
+
case "weekly": return 7 * 1440;
|
|
61001
|
+
case "monthly": return 30 * 1440;
|
|
61002
|
+
default: return;
|
|
61003
|
+
}
|
|
61004
|
+
}
|
|
61005
|
+
/**
|
|
61006
|
+
* Project one account's Cline Pass usage reading into the shared
|
|
61007
|
+
* {@link PoolAccountQuotaDto} shape that AccountPoolSection renders inside the
|
|
61008
|
+
* account row. A failed reading returns undefined so the card does not draw
|
|
61009
|
+
* fabricated zero bars.
|
|
61010
|
+
*/
|
|
61011
|
+
function clinePassAccountQuota(reading) {
|
|
61012
|
+
if (reading === void 0 || !reading.ok) return void 0;
|
|
61013
|
+
const windows = reading.windows.map((window) => {
|
|
61014
|
+
const duration = clinePassWindowDurationMins(window.type);
|
|
61015
|
+
const parsedReset = window.resetsAt === "" ? NaN : Date.parse(window.resetsAt);
|
|
61016
|
+
const resetsAt = Number.isFinite(parsedReset) && parsedReset > 0 ? parsedReset : null;
|
|
61017
|
+
return {
|
|
61018
|
+
label: window.label || clinePassWindowLabel(window.type),
|
|
61019
|
+
usedPercent: window.percentUsed,
|
|
61020
|
+
...duration === void 0 ? {} : { windowDurationMins: duration },
|
|
61021
|
+
resetsAt
|
|
61022
|
+
};
|
|
61023
|
+
});
|
|
61024
|
+
return {
|
|
61025
|
+
fetchedAt: reading.fetchedAt,
|
|
61026
|
+
windows
|
|
61027
|
+
};
|
|
61028
|
+
}
|
|
61029
|
+
//#endregion
|
|
59964
61030
|
//#region src/host/cline-pass/protocol.ts
|
|
59965
61031
|
/**
|
|
59966
61032
|
* Cline Pass wire protocol: pipeline detection, pin injection, failover
|
|
@@ -60010,22 +61076,97 @@ function parseRouting(json) {
|
|
|
60010
61076
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
60011
61077
|
const first = isRecord$11(choices[0]) ? choices[0] : {};
|
|
60012
61078
|
const message = isRecord$11(first.message) ? first.message : {};
|
|
60013
|
-
const
|
|
61079
|
+
const delta = isRecord$11(first.delta) ? first.delta : {};
|
|
61080
|
+
const metadata = [
|
|
61081
|
+
message,
|
|
61082
|
+
delta,
|
|
61083
|
+
payload
|
|
61084
|
+
].map(providerMetadataOf).find((entry) => entry !== null) ?? {};
|
|
60014
61085
|
const gateway = isRecord$11(metadata.gateway) ? metadata.gateway : {};
|
|
60015
|
-
const routing = isRecord$11(gateway.routing) ? gateway.routing : {};
|
|
60016
61086
|
const direct = typeof payload.provider === "string" ? payload.provider : null;
|
|
61087
|
+
let routing = isRecord$11(gateway.routing) ? gateway.routing : {};
|
|
61088
|
+
if (typeof routing.finalProvider !== "string") routing = huntRouting(payload) ?? routing;
|
|
61089
|
+
const finalProvider = typeof routing.finalProvider === "string" ? routing.finalProvider : direct === null ? null : slugify(direct);
|
|
61090
|
+
const canonicalSlug = typeof routing.canonicalSlug === "string" ? routing.canonicalSlug : typeof payload.model === "string" && payload.model.includes("/") ? payload.model : null;
|
|
60017
61091
|
return {
|
|
60018
|
-
content: typeof message.content === "string" ? message.content : null,
|
|
60019
|
-
toolCalls: Array.isArray(message.tool_calls) ? message.tool_calls : [],
|
|
61092
|
+
content: typeof message.content === "string" ? message.content : typeof delta.content === "string" ? delta.content : null,
|
|
61093
|
+
toolCalls: Array.isArray(message.tool_calls) ? message.tool_calls : Array.isArray(delta.tool_calls) ? delta.tool_calls : [],
|
|
60020
61094
|
usage: payload.usage ?? null,
|
|
60021
61095
|
pipeline: typeof routing.finalProvider === "string" ? "planner" : direct === null ? null : "direct",
|
|
60022
|
-
canonicalSlug
|
|
60023
|
-
finalProvider
|
|
61096
|
+
canonicalSlug,
|
|
61097
|
+
finalProvider,
|
|
60024
61098
|
finalProviderName: typeof routing.finalProvider === "string" ? routing.finalProvider : direct,
|
|
60025
61099
|
fallbacks: Array.isArray(routing.fallbacksAvailable) ? routing.fallbacksAvailable.map(String) : [],
|
|
60026
|
-
plan: typeof routing.planningReasoning === "string" ? routing.planningReasoning : ""
|
|
61100
|
+
plan: typeof routing.planningReasoning === "string" ? routing.planningReasoning : "",
|
|
61101
|
+
fingerprint: huntFingerprint(payload),
|
|
61102
|
+
anonymous: isClinePassAnonymousUpstream(finalProvider ?? "", canonicalSlug ?? "")
|
|
60027
61103
|
};
|
|
60028
61104
|
}
|
|
61105
|
+
/**
|
|
61106
|
+
* Nodes one payload walk may visit before giving up.
|
|
61107
|
+
*
|
|
61108
|
+
* The hunts run per streamed frame, and a frame is small, but a pathological
|
|
61109
|
+
* payload must not turn a cheap re-read into an unbounded walk.
|
|
61110
|
+
*/
|
|
61111
|
+
const HUNT_NODE_BUDGET = 4e3;
|
|
61112
|
+
/** `provider_metadata` of one node, when present. */
|
|
61113
|
+
function providerMetadataOf(node) {
|
|
61114
|
+
return isRecord$11(node.provider_metadata) ? node.provider_metadata : null;
|
|
61115
|
+
}
|
|
61116
|
+
/**
|
|
61117
|
+
* Depth-bounded hunt for the gateway's routing block anywhere in one payload.
|
|
61118
|
+
*
|
|
61119
|
+
* The fast paths (`message`/`delta`/`payload.provider_metadata.gateway`) are
|
|
61120
|
+
* checked by the caller first; this walk covers the nestings between the shapes
|
|
61121
|
+
* seen so far, so a frame the fast path does not know still yields its routing.
|
|
61122
|
+
*/
|
|
61123
|
+
function huntRouting(node, depth = 0, budget = { left: HUNT_NODE_BUDGET }) {
|
|
61124
|
+
if (depth > 8 || budget.left <= 0 || node === null || typeof node !== "object") return null;
|
|
61125
|
+
budget.left -= 1;
|
|
61126
|
+
if (Array.isArray(node)) {
|
|
61127
|
+
for (const entry of node) {
|
|
61128
|
+
const found = huntRouting(entry, depth + 1, budget);
|
|
61129
|
+
if (found !== null) return found;
|
|
61130
|
+
}
|
|
61131
|
+
return null;
|
|
61132
|
+
}
|
|
61133
|
+
const record = node;
|
|
61134
|
+
const gateway = isRecord$11(record.gateway) ? record.gateway : null;
|
|
61135
|
+
const routing = gateway !== null && isRecord$11(gateway.routing) ? gateway.routing : isRecord$11(record.routing) ? record.routing : null;
|
|
61136
|
+
if (routing !== null && typeof routing.finalProvider === "string") return routing;
|
|
61137
|
+
for (const value of Object.values(record)) {
|
|
61138
|
+
const found = huntRouting(value, depth + 1, budget);
|
|
61139
|
+
if (found !== null) return found;
|
|
61140
|
+
}
|
|
61141
|
+
return null;
|
|
61142
|
+
}
|
|
61143
|
+
/**
|
|
61144
|
+
* Read the first-party DeepSeek fingerprint wherever `provider_metadata`
|
|
61145
|
+
* carries it. Absent means the answer did not prove a first-party upstream —
|
|
61146
|
+
* the anonymous contributor route carries no `deepseek` namespace at all.
|
|
61147
|
+
*/
|
|
61148
|
+
function huntFingerprint(node, depth = 0, budget = { left: HUNT_NODE_BUDGET }) {
|
|
61149
|
+
if (depth > 8 || budget.left <= 0 || node === null || typeof node !== "object") return null;
|
|
61150
|
+
budget.left -= 1;
|
|
61151
|
+
if (Array.isArray(node)) {
|
|
61152
|
+
for (const entry of node) {
|
|
61153
|
+
const found = huntFingerprint(entry, depth + 1, budget);
|
|
61154
|
+
if (found !== null) return found;
|
|
61155
|
+
}
|
|
61156
|
+
return null;
|
|
61157
|
+
}
|
|
61158
|
+
const record = node;
|
|
61159
|
+
const metadata = providerMetadataOf(record);
|
|
61160
|
+
if (metadata !== null) {
|
|
61161
|
+
const deepseek = isRecord$11(metadata.deepseek) ? metadata.deepseek : null;
|
|
61162
|
+
if (deepseek !== null && typeof deepseek.systemFingerprint === "string" && deepseek.systemFingerprint !== "") return deepseek.systemFingerprint;
|
|
61163
|
+
}
|
|
61164
|
+
for (const value of Object.values(record)) {
|
|
61165
|
+
const found = huntFingerprint(value, depth + 1, budget);
|
|
61166
|
+
if (found !== null) return found;
|
|
61167
|
+
}
|
|
61168
|
+
return null;
|
|
61169
|
+
}
|
|
60029
61170
|
/** Lowercase a display name into an upstream slug. */
|
|
60030
61171
|
function slugify(value) {
|
|
60031
61172
|
return String(value).toLowerCase().replace(/\s+/g, "-");
|
|
@@ -60114,6 +61255,7 @@ function injectPrefs(body, meta, attempt) {
|
|
|
60114
61255
|
const excludeList = attempt.excludeList ?? [];
|
|
60115
61256
|
const strict = attempt.strict !== false;
|
|
60116
61257
|
const sort = normalizeSort(attempt.sort);
|
|
61258
|
+
const has = capGatewayHas((attempt.has ?? []).filter((name) => typeof name === "string"));
|
|
60117
61259
|
const excluded = excludeList.filter((name) => normalizeSlug(name) !== normalizeSlug(upstream ?? ""));
|
|
60118
61260
|
const excludedIds = new Set(excluded.map(normalizeSlug));
|
|
60119
61261
|
const spell = (names, pipeline) => names.map((name) => channelNameFor(name, pipeline));
|
|
@@ -60123,7 +61265,7 @@ function injectPrefs(body, meta, attempt) {
|
|
|
60123
61265
|
const both = attempt.spelling !== "detected";
|
|
60124
61266
|
const useGateway = both || pipeline === "planner" || pipeline === null;
|
|
60125
61267
|
const useOpenRouter = both || pipeline === "direct" || pipeline === null;
|
|
60126
|
-
const gatewayUsable = useGateway && (upstream !== null || sort !== null || gatewayAllow !== null && gatewayAllow.length > 0);
|
|
61268
|
+
const gatewayUsable = useGateway && (upstream !== null || sort !== null || has.length > 0 || gatewayAllow !== null && gatewayAllow.length > 0);
|
|
60127
61269
|
const openRouterUsable = useOpenRouter && (upstream !== null || sort !== null || excluded.length > 0 || openRouterAllow !== null && openRouterAllow.length > 0);
|
|
60128
61270
|
if (!gatewayUsable && !openRouterUsable) return next;
|
|
60129
61271
|
if (gatewayUsable) {
|
|
@@ -60135,6 +61277,7 @@ function injectPrefs(body, meta, attempt) {
|
|
|
60135
61277
|
}
|
|
60136
61278
|
else if (gatewayAllow !== null && gatewayAllow.length > 0) gateway.only = spell(gatewayAllow, "planner");
|
|
60137
61279
|
if (sort !== null) gateway.sort = sort;
|
|
61280
|
+
if (has.length > 0) gateway.has = [...has];
|
|
60138
61281
|
const providerOptions = isRecord$11(next.providerOptions) ? next.providerOptions : {};
|
|
60139
61282
|
const currentGateway = isRecord$11(providerOptions.gateway) ? providerOptions.gateway : {};
|
|
60140
61283
|
next.providerOptions = {
|
|
@@ -60192,13 +61335,16 @@ function isAdherenceOk(adherence) {
|
|
|
60192
61335
|
*/
|
|
60193
61336
|
function pinWarnings(attempt, meta) {
|
|
60194
61337
|
const upstream = attempt.upstream ?? null;
|
|
61338
|
+
const warnings = [];
|
|
61339
|
+
if (upstream !== null && upstream !== void 0 && String(upstream).length > 0 && isClinePassAnonymousUpstream(upstream)) warnings.push(`anonymousUpstream: ${upstream} is the gateway's anonymous contributor route — answers from it may be used for training and carry no zero-data-retention guarantee; pin it only when that is acceptable`);
|
|
61340
|
+
const known = Array.isArray(meta?.upstreams) ? meta.upstreams : [];
|
|
61341
|
+
if (upstream !== null && upstream !== void 0 && String(upstream).length > 0 && !isClinePassAnonymousUpstream(upstream) && known.length > 0 && known.every((name) => isClinePassAnonymousUpstream(name))) warnings.push(`pinLikelyIgnored: every channel discovered for this model is the anonymous contributor route (${known.join(", ")}), which silently drops a pin and answers anyway — the request will likely be served by it regardless of ${upstream}`);
|
|
60195
61342
|
const excluded = (attempt.excludeList ?? []).filter((name) => normalizeSlug(name) !== normalizeSlug(upstream ?? ""));
|
|
60196
61343
|
const excludedIds = new Set(excluded.map(normalizeSlug));
|
|
60197
|
-
if (excluded.length === 0) return
|
|
60198
|
-
|
|
60199
|
-
if (known.length === 0) return [`
|
|
60200
|
-
|
|
60201
|
-
return [];
|
|
61344
|
+
if (excluded.length === 0) return warnings;
|
|
61345
|
+
if (known.length === 0) return [...warnings, `excludeUnresolved: this model has no channel list yet, so ${excluded.join(", ")} could not be compiled into an allow-list — run cline_pass_probe for it`];
|
|
61346
|
+
if (known.filter((name) => !excludedIds.has(normalizeSlug(name))).length === 0 && upstream === null) return [...warnings, `PIN_UNRESOLVED: every known channel is excluded (${known.join(", ")}), so the request falls back to the gateway's own routing`];
|
|
61347
|
+
return warnings;
|
|
60202
61348
|
}
|
|
60203
61349
|
/**
|
|
60204
61350
|
* Expand one model's pin configuration into the ordered failover candidates.
|
|
@@ -60218,6 +61364,8 @@ function buildAttempts(config) {
|
|
|
60218
61364
|
sort: normalizeSort(config.sort),
|
|
60219
61365
|
excludeList: exclude
|
|
60220
61366
|
};
|
|
61367
|
+
const has = (config.has ?? []).filter((name) => typeof name === "string" && name.trim().length > 0);
|
|
61368
|
+
if (has.length > 0) base.has = [...has];
|
|
60221
61369
|
if (typeof config.spelling === "string" && config.spelling.length > 0) base.spelling = config.spelling;
|
|
60222
61370
|
if (wanted.length > 0) return wanted.map((upstream, index) => ({
|
|
60223
61371
|
...base,
|
|
@@ -60871,8 +62019,7 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
60871
62019
|
*/
|
|
60872
62020
|
async availableModelIds(settings) {
|
|
60873
62021
|
const ids = new Set(DEFAULT_MODELS);
|
|
60874
|
-
for (const model of settings
|
|
60875
|
-
if (settings.exposeCatalog === true) for (const id of this.store.catalog().ids) ids.add(id);
|
|
62022
|
+
for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
|
|
60876
62023
|
const hidden = new Set(settings.hiddenModels);
|
|
60877
62024
|
const enabled = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
|
|
60878
62025
|
return [...ids].filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)));
|
|
@@ -61051,7 +62198,9 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
61051
62198
|
const observed = {
|
|
61052
62199
|
provider: null,
|
|
61053
62200
|
canonical: null,
|
|
61054
|
-
pipeline: null
|
|
62201
|
+
pipeline: null,
|
|
62202
|
+
fingerprint: null,
|
|
62203
|
+
anonymous: false
|
|
61055
62204
|
};
|
|
61056
62205
|
const iterator = translate(tapRouting(parseServerSentEvents$1(tapChunks$1(response.body, () => {
|
|
61057
62206
|
activity?.pulse();
|
|
@@ -61123,6 +62272,9 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
61123
62272
|
upstream: attempt.upstream,
|
|
61124
62273
|
excludeList: attempt.excludeList
|
|
61125
62274
|
}, meta);
|
|
62275
|
+
if (adherence === "not-adopted") warnings.push(`pinNotAdopted: the gateway served ${observed.provider ?? "(unknown)"} instead of the pinned ${attempt.upstream ?? "(auto)"}`);
|
|
62276
|
+
else if (adherence === "violated") warnings.push(`pinViolated: an excluded channel served this request: ${observed.provider ?? "(unknown)"}`);
|
|
62277
|
+
if (observed.anonymous) warnings.push(`anonymousServed: this answer came from the gateway's anonymous contributor route (${observed.provider ?? "openai-compatible-private"}) — it may be used for training and carries no zero-data-retention guarantee`);
|
|
61126
62278
|
const ms = Date.now() - attemptStarted;
|
|
61127
62279
|
trace.push(attemptLabel);
|
|
61128
62280
|
this.store.record({
|
|
@@ -61138,10 +62290,16 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
61138
62290
|
code: finishFailure?.code ?? "",
|
|
61139
62291
|
account: account.name,
|
|
61140
62292
|
adherence,
|
|
62293
|
+
fingerprint: observed.fingerprint ?? "",
|
|
62294
|
+
anonymous: observed.anonymous,
|
|
61141
62295
|
warnings,
|
|
61142
62296
|
...usage === void 0 ? {} : { usage },
|
|
61143
62297
|
effort: options.reasoningEffort === void 0 ? "" : String(options.reasoningEffort)
|
|
61144
62298
|
});
|
|
62299
|
+
if (observed.fingerprint !== null || observed.provider !== null) this.store.learn(options.model, {
|
|
62300
|
+
...observed.provider === null ? {} : { lastProvider: observed.provider },
|
|
62301
|
+
...observed.fingerprint === null ? {} : { lastFingerprint: observed.fingerprint }
|
|
62302
|
+
});
|
|
61145
62303
|
this.accountPool.recordModel(account.id, options.model);
|
|
61146
62304
|
if (finishFailure === null && attempt.upstream !== null && adherence === "adopted") this.store.learnUpstream(options.model, attempt.upstream, "ok", "", ms);
|
|
61147
62305
|
return;
|
|
@@ -61239,8 +62397,10 @@ async function* tapRouting(payloads, observed) {
|
|
|
61239
62397
|
if (routing.finalProvider !== null) {
|
|
61240
62398
|
observed.provider = routing.finalProvider;
|
|
61241
62399
|
observed.pipeline = routing.pipeline;
|
|
62400
|
+
observed.anonymous = routing.anonymous;
|
|
61242
62401
|
if (routing.canonicalSlug !== null) observed.canonical = routing.canonicalSlug;
|
|
61243
62402
|
}
|
|
62403
|
+
if (routing.fingerprint !== null) observed.fingerprint = routing.fingerprint;
|
|
61244
62404
|
}
|
|
61245
62405
|
yield payload;
|
|
61246
62406
|
}
|
|
@@ -61368,7 +62528,7 @@ function effortsFromOptions(options) {
|
|
|
61368
62528
|
const ordered = REASONING_EFFORTS.map((effort) => effort.id).filter((id) => levels.has(id));
|
|
61369
62529
|
return ordered.length > 0 ? ordered : void 0;
|
|
61370
62530
|
}
|
|
61371
|
-
/** List the gateway's own catalog
|
|
62531
|
+
/** List the gateway's own catalog; empty when the call fails. */
|
|
61372
62532
|
async function fetchGatewayCatalog({ fetchFn, baseURL, apiKey, signal, timeoutMs = 3e4 }) {
|
|
61373
62533
|
try {
|
|
61374
62534
|
const result = await sendJSON(fetchFn, `${trimBase(baseURL)}/models`, {
|
|
@@ -61376,9 +62536,24 @@ async function fetchGatewayCatalog({ fetchFn, baseURL, apiKey, signal, timeoutMs
|
|
|
61376
62536
|
signal,
|
|
61377
62537
|
timeoutMs
|
|
61378
62538
|
});
|
|
61379
|
-
|
|
62539
|
+
const data = isRecord$10(result.json) && Array.isArray(result.json.data) ? result.json.data : [];
|
|
62540
|
+
const ids = [];
|
|
62541
|
+
const models = {};
|
|
62542
|
+
for (const model of data) {
|
|
62543
|
+
if (!isRecord$10(model) || typeof model.id !== "string") continue;
|
|
62544
|
+
ids.push(model.id);
|
|
62545
|
+
const meta = modelMetadata(model);
|
|
62546
|
+
if (Object.keys(meta).length > 0) models[model.id] = meta;
|
|
62547
|
+
}
|
|
62548
|
+
return {
|
|
62549
|
+
ids,
|
|
62550
|
+
models
|
|
62551
|
+
};
|
|
61380
62552
|
} catch {
|
|
61381
|
-
return
|
|
62553
|
+
return {
|
|
62554
|
+
ids: [],
|
|
62555
|
+
models: {}
|
|
62556
|
+
};
|
|
61382
62557
|
}
|
|
61383
62558
|
}
|
|
61384
62559
|
/**
|
|
@@ -61629,6 +62804,7 @@ var ClinePassEngine = class {
|
|
|
61629
62804
|
exclude: this.pinOf(model).exclude,
|
|
61630
62805
|
pinMode: this.pinOf(model).pinMode,
|
|
61631
62806
|
sort: this.pinOf(model).sort,
|
|
62807
|
+
has: this.pinOf(model).has,
|
|
61632
62808
|
spelling: this.options.pinFor(model).spelling
|
|
61633
62809
|
};
|
|
61634
62810
|
}
|
|
@@ -61639,7 +62815,8 @@ var ClinePassEngine = class {
|
|
|
61639
62815
|
upstreams: (pin.upstreams ?? []).map(String),
|
|
61640
62816
|
exclude: (pin.exclude ?? []).map(String),
|
|
61641
62817
|
pinMode: pin.pinMode === "preferred" ? "preferred" : "strict",
|
|
61642
|
-
sort: typeof pin.sort === "string" ? pin.sort : ""
|
|
62818
|
+
sort: typeof pin.sort === "string" ? pin.sort : "",
|
|
62819
|
+
has: Array.isArray(pin.has) ? pin.has.map(String) : defaultGatewayHas(model)
|
|
61643
62820
|
};
|
|
61644
62821
|
}
|
|
61645
62822
|
/**
|
|
@@ -61716,7 +62893,9 @@ var ClinePassEngine = class {
|
|
|
61716
62893
|
if (attempt.upstream === null && attempt.excludeList.length > 0 && outcome.error !== "") this.learnFromError(model, outcome.error);
|
|
61717
62894
|
}
|
|
61718
62895
|
}
|
|
61719
|
-
const
|
|
62896
|
+
const failure = last === null ? "no candidate succeeded" : last.ok !== true ? last.error : last.adherence === "violated" ? `an excluded channel served the request: ${last.routing.finalProvider ?? "(unknown)"}` : `the gateway ignored the pin: served by ${last.routing.finalProvider ?? "(unknown)"}`;
|
|
62897
|
+
const has = (pinConfig.has ?? []).filter((name) => typeof name === "string" && name.trim() !== "");
|
|
62898
|
+
const error = has.length > 0 && /provider|capabilit|filter/i.test(failure) ? `${failure} (this pin also requires capabilities [${has.join(", ")}]; clear them in the pin editor to widen routing)` : failure;
|
|
61720
62899
|
return {
|
|
61721
62900
|
ok: false,
|
|
61722
62901
|
result: last,
|
|
@@ -61778,21 +62957,26 @@ var ClinePassEngine = class {
|
|
|
61778
62957
|
lastProvider: "",
|
|
61779
62958
|
upstreams: [],
|
|
61780
62959
|
availableProviders: [],
|
|
61781
|
-
tier0: []
|
|
62960
|
+
tier0: [],
|
|
62961
|
+
fingerprint: "",
|
|
62962
|
+
anonymous: false
|
|
61782
62963
|
};
|
|
61783
62964
|
}
|
|
61784
62965
|
const routing = parseRouting(payload);
|
|
61785
62966
|
const pipeline = routing.pipeline;
|
|
61786
62967
|
const otherPipeline = pipeline === "planner" ? "direct" : pipeline === "direct" ? "planner" : null;
|
|
62968
|
+
const has = this.pinOf(model).has;
|
|
61787
62969
|
let harvested = null;
|
|
61788
62970
|
if (pipeline !== null) harvested = await this.harvest(model, pipeline, account, {
|
|
61789
62971
|
signal,
|
|
61790
|
-
timeoutMs: this.options.probeTimeoutMs
|
|
62972
|
+
timeoutMs: this.options.probeTimeoutMs,
|
|
62973
|
+
has
|
|
61791
62974
|
});
|
|
61792
62975
|
let otherHarvested = null;
|
|
61793
62976
|
if (otherPipeline !== null) otherHarvested = await this.harvest(model, otherPipeline, account, {
|
|
61794
62977
|
signal,
|
|
61795
|
-
timeoutMs: this.options.probeTimeoutMs
|
|
62978
|
+
timeoutMs: this.options.probeTimeoutMs,
|
|
62979
|
+
has
|
|
61796
62980
|
});
|
|
61797
62981
|
let endpoints = [];
|
|
61798
62982
|
let openrouterSlug = null;
|
|
@@ -61832,6 +63016,7 @@ var ClinePassEngine = class {
|
|
|
61832
63016
|
staleAt: combined.length === 0 && upstreams.length > 0 ? Date.now() : 0,
|
|
61833
63017
|
tier0,
|
|
61834
63018
|
lastProvider: routing.finalProvider ?? previous.lastProvider ?? null,
|
|
63019
|
+
lastFingerprint: routing.fingerprint ?? previous.lastFingerprint ?? null,
|
|
61835
63020
|
lastMs: ms,
|
|
61836
63021
|
probedAt: Date.now(),
|
|
61837
63022
|
lastError: ""
|
|
@@ -61847,7 +63032,9 @@ var ClinePassEngine = class {
|
|
|
61847
63032
|
lastProvider: meta.lastProvider ?? "",
|
|
61848
63033
|
upstreams: meta.upstreams ?? [],
|
|
61849
63034
|
availableProviders: meta.availableProviders ?? [],
|
|
61850
|
-
tier0: meta.tier0 ?? []
|
|
63035
|
+
tier0: meta.tier0 ?? [],
|
|
63036
|
+
fingerprint: meta.lastFingerprint ?? "",
|
|
63037
|
+
anonymous: isClinePassAnonymousUpstream(meta.lastProvider ?? "", meta.canonicalSlug ?? "")
|
|
61851
63038
|
};
|
|
61852
63039
|
}
|
|
61853
63040
|
/**
|
|
@@ -61855,7 +63042,7 @@ var ClinePassEngine = class {
|
|
|
61855
63042
|
* cannot exist: the router fails before spending a token and names every
|
|
61856
63043
|
* provider it could have used.
|
|
61857
63044
|
*/
|
|
61858
|
-
async harvest(model, pipeline, account, { signal, timeoutMs }) {
|
|
63045
|
+
async harvest(model, pipeline, account, { signal, timeoutMs, has = [] }) {
|
|
61859
63046
|
const base = {
|
|
61860
63047
|
model,
|
|
61861
63048
|
messages: [{
|
|
@@ -61866,7 +63053,10 @@ var ClinePassEngine = class {
|
|
|
61866
63053
|
};
|
|
61867
63054
|
const body = pipeline === "planner" ? {
|
|
61868
63055
|
...base,
|
|
61869
|
-
providerOptions: { gateway: {
|
|
63056
|
+
providerOptions: { gateway: {
|
|
63057
|
+
only: ["__probe__"],
|
|
63058
|
+
...has.length > 0 ? { has: [...has] } : {}
|
|
63059
|
+
} }
|
|
61870
63060
|
} : {
|
|
61871
63061
|
...base,
|
|
61872
63062
|
provider: { only: ["__probe__"] }
|
|
@@ -61880,7 +63070,12 @@ var ClinePassEngine = class {
|
|
|
61880
63070
|
signal,
|
|
61881
63071
|
timeoutMs: timeoutMs ?? this.options.probeTimeoutMs
|
|
61882
63072
|
});
|
|
61883
|
-
|
|
63073
|
+
const payload = unwrapEnvelope(result.json);
|
|
63074
|
+
const listed = extractAvailableProviders(errorText$1(payload.error) || result.text, pipeline);
|
|
63075
|
+
if (listed !== null && listed.length > 0) return listed;
|
|
63076
|
+
const routing = parseRouting(payload);
|
|
63077
|
+
if (routing.finalProvider !== null && routing.finalProvider !== "") return [routing.finalProvider];
|
|
63078
|
+
return listed;
|
|
61884
63079
|
} catch {
|
|
61885
63080
|
return null;
|
|
61886
63081
|
}
|
|
@@ -61895,6 +63090,7 @@ var ClinePassEngine = class {
|
|
|
61895
63090
|
async validate(model, { signal, timeoutMs } = {}) {
|
|
61896
63091
|
const meta = this.store.metaOf(model);
|
|
61897
63092
|
const list = Array.isArray(meta.upstreams) ? meta.upstreams : [];
|
|
63093
|
+
const has = this.pinOf(model).has;
|
|
61898
63094
|
const results = [];
|
|
61899
63095
|
for (let index = 0; index < list.length; index += 5) {
|
|
61900
63096
|
if (signal?.aborted === true) break;
|
|
@@ -61906,7 +63102,8 @@ var ClinePassEngine = class {
|
|
|
61906
63102
|
strict: true,
|
|
61907
63103
|
sort: null,
|
|
61908
63104
|
orderRest: [],
|
|
61909
|
-
excludeList: []
|
|
63105
|
+
excludeList: [],
|
|
63106
|
+
has
|
|
61910
63107
|
};
|
|
61911
63108
|
const body = injectPrefs({
|
|
61912
63109
|
model,
|
|
@@ -61985,13 +63182,14 @@ var ClinePassEngine = class {
|
|
|
61985
63182
|
* Send one small chat through a (possibly temporary) pin configuration and
|
|
61986
63183
|
* report what served it. Nothing is persisted.
|
|
61987
63184
|
*/
|
|
61988
|
-
async test(model, { upstreams, exclude, signal } = {}) {
|
|
63185
|
+
async test(model, { upstreams, exclude, has, signal } = {}) {
|
|
61989
63186
|
const stored = this.pinOf(model);
|
|
61990
63187
|
const pin = {
|
|
61991
63188
|
upstreams: Array.isArray(upstreams) ? upstreams : stored.upstreams,
|
|
61992
63189
|
exclude: Array.isArray(exclude) ? exclude : stored.exclude,
|
|
61993
63190
|
pinMode: stored.pinMode,
|
|
61994
63191
|
sort: stored.sort,
|
|
63192
|
+
has: Array.isArray(has) ? has : stored.has,
|
|
61995
63193
|
spelling: this.options.pinFor(model).spelling
|
|
61996
63194
|
};
|
|
61997
63195
|
const started = Date.now();
|
|
@@ -62037,6 +63235,9 @@ var ClinePassEngine = class {
|
|
|
62037
63235
|
adopted: false,
|
|
62038
63236
|
adherence: outcome.result?.adherence ?? "",
|
|
62039
63237
|
plan: routing?.plan ?? "",
|
|
63238
|
+
fingerprint: routing?.fingerprint ?? "",
|
|
63239
|
+
anonymous: isClinePassAnonymousUpstream(routing?.finalProvider ?? "", routing?.canonicalSlug ?? ""),
|
|
63240
|
+
has: [...pin.has ?? []],
|
|
62040
63241
|
warnings
|
|
62041
63242
|
};
|
|
62042
63243
|
}
|
|
@@ -62068,6 +63269,9 @@ var ClinePassEngine = class {
|
|
|
62068
63269
|
adopted: outcome.result?.adherence === "adopted",
|
|
62069
63270
|
adherence: outcome.result?.adherence ?? "",
|
|
62070
63271
|
plan: routing?.plan ?? "",
|
|
63272
|
+
fingerprint: routing?.fingerprint ?? "",
|
|
63273
|
+
anonymous: isClinePassAnonymousUpstream(routing?.finalProvider ?? "", routing?.canonicalSlug ?? ""),
|
|
63274
|
+
has: [...pin.has ?? []],
|
|
62071
63275
|
warnings
|
|
62072
63276
|
};
|
|
62073
63277
|
}
|
|
@@ -62131,7 +63335,7 @@ var ClinePassEngine = class {
|
|
|
62131
63335
|
};
|
|
62132
63336
|
}
|
|
62133
63337
|
}
|
|
62134
|
-
/** Refresh the gateway's own catalog ids. */
|
|
63338
|
+
/** Refresh the gateway's own catalog: ids plus published metadata. */
|
|
62135
63339
|
async gatewayCatalog({ signal, timeoutMs } = {}) {
|
|
62136
63340
|
const account = await this.resolveAccount();
|
|
62137
63341
|
return await fetchGatewayCatalog({
|
|
@@ -62166,66 +63370,6 @@ var ClinePassEngine = class {
|
|
|
62166
63370
|
}
|
|
62167
63371
|
};
|
|
62168
63372
|
//#endregion
|
|
62169
|
-
//#region src/shared/cline-pass-contracts.ts
|
|
62170
|
-
/**
|
|
62171
|
-
* Wire contracts for the Cline Pass line's settings API.
|
|
62172
|
-
*
|
|
62173
|
-
* The line is API-key based: there is no OAuth flow, no token expiry, and no
|
|
62174
|
-
* per-account identity beyond the key and the alias a user gives it. What the
|
|
62175
|
-
* card does render beyond the shared account facts is the routing observation
|
|
62176
|
-
* this plugin makes about each model (pipeline, discovered channels, per-channel
|
|
62177
|
-
* verdicts) and the per-model pin the user stores. None of it is a credential.
|
|
62178
|
-
*/
|
|
62179
|
-
const CLINE_PASS_PROVIDER_ID = "cline-pass";
|
|
62180
|
-
const CLINE_PASS_PROVIDER_NAME = "Cline Pass";
|
|
62181
|
-
const CLINE_PASS_ROUTE_PREFIX = "/cline-pass/api";
|
|
62182
|
-
/** Whether a submitted pin mode names one of the two accepted modes. */
|
|
62183
|
-
function isClinePassPinMode(value) {
|
|
62184
|
-
return value === "strict" || value === "preferred";
|
|
62185
|
-
}
|
|
62186
|
-
/** Human label for the gateway's quota-window spelling. */
|
|
62187
|
-
function clinePassWindowLabel(type) {
|
|
62188
|
-
switch (type) {
|
|
62189
|
-
case "five_hour": return "5 小时";
|
|
62190
|
-
case "weekly": return "每周";
|
|
62191
|
-
case "monthly": return "每月";
|
|
62192
|
-
default: return type;
|
|
62193
|
-
}
|
|
62194
|
-
}
|
|
62195
|
-
/** Window length in minutes for the shared AccountQuota formatter. */
|
|
62196
|
-
function clinePassWindowDurationMins(type) {
|
|
62197
|
-
switch (type) {
|
|
62198
|
-
case "five_hour": return 300;
|
|
62199
|
-
case "weekly": return 7 * 1440;
|
|
62200
|
-
case "monthly": return 30 * 1440;
|
|
62201
|
-
default: return;
|
|
62202
|
-
}
|
|
62203
|
-
}
|
|
62204
|
-
/**
|
|
62205
|
-
* Project one account's Cline Pass usage reading into the shared
|
|
62206
|
-
* {@link PoolAccountQuotaDto} shape that AccountPoolSection renders inside the
|
|
62207
|
-
* account row. A failed reading returns undefined so the card does not draw
|
|
62208
|
-
* fabricated zero bars.
|
|
62209
|
-
*/
|
|
62210
|
-
function clinePassAccountQuota(reading) {
|
|
62211
|
-
if (reading === void 0 || !reading.ok) return void 0;
|
|
62212
|
-
const windows = reading.windows.map((window) => {
|
|
62213
|
-
const duration = clinePassWindowDurationMins(window.type);
|
|
62214
|
-
const parsedReset = window.resetsAt === "" ? NaN : Date.parse(window.resetsAt);
|
|
62215
|
-
const resetsAt = Number.isFinite(parsedReset) && parsedReset > 0 ? parsedReset : null;
|
|
62216
|
-
return {
|
|
62217
|
-
label: window.label || clinePassWindowLabel(window.type),
|
|
62218
|
-
usedPercent: window.percentUsed,
|
|
62219
|
-
...duration === void 0 ? {} : { windowDurationMins: duration },
|
|
62220
|
-
resetsAt
|
|
62221
|
-
};
|
|
62222
|
-
});
|
|
62223
|
-
return {
|
|
62224
|
-
fetchedAt: reading.fetchedAt,
|
|
62225
|
-
windows
|
|
62226
|
-
};
|
|
62227
|
-
}
|
|
62228
|
-
//#endregion
|
|
62229
63373
|
//#region src/host/cline-pass/routes.ts
|
|
62230
63374
|
function sendJson$2(response, status, body) {
|
|
62231
63375
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
@@ -62275,23 +63419,40 @@ function formatModelCapacity$1(tokens) {
|
|
|
62275
63419
|
if (tokens >= 1e3 && tokens % 1e3 === 0) return tokens / 1e3 + "k";
|
|
62276
63420
|
return String(tokens);
|
|
62277
63421
|
}
|
|
63422
|
+
/** `cline-pass/*` is the subscription catalogue; every other id is marketplace credit billing. */
|
|
63423
|
+
function catalogBilling(id) {
|
|
63424
|
+
return id.startsWith("cline-pass/") ? "subscription" : "credits";
|
|
63425
|
+
}
|
|
63426
|
+
/** Published metadata for an id the store already scanned, so an added row keeps its real facts. */
|
|
63427
|
+
function publishedCatalogMeta(store, id) {
|
|
63428
|
+
const entry = store.catalog().models[id];
|
|
63429
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return {};
|
|
63430
|
+
const record = entry;
|
|
63431
|
+
const meta = {};
|
|
63432
|
+
if (typeof record.name === "string") meta.name = record.name;
|
|
63433
|
+
if (typeof record.contextWindow === "number") meta.contextWindow = record.contextWindow;
|
|
63434
|
+
if (typeof record.maxTokens === "number") meta.maxTokens = record.maxTokens;
|
|
63435
|
+
if (typeof record.reasoning === "boolean") meta.reasoning = record.reasoning;
|
|
63436
|
+
if (Array.isArray(record.input)) meta.input = record.input.filter((item) => typeof item === "string");
|
|
63437
|
+
if (Array.isArray(record.efforts)) meta.efforts = record.efforts.filter((item) => typeof item === "string");
|
|
63438
|
+
return meta;
|
|
63439
|
+
}
|
|
62278
63440
|
function formatModelHint$1(contextWindow, input, reasoning) {
|
|
62279
63441
|
return formatModelCapacity$1(contextWindow) + " · " + input.join("+") + (reasoning ? " · reasoning" : "");
|
|
62280
63442
|
}
|
|
62281
|
-
function uniqueModels$1(settings
|
|
63443
|
+
function uniqueModels$1(settings) {
|
|
62282
63444
|
const ids = new Set(DEFAULT_MODELS);
|
|
62283
|
-
for (const model of settings
|
|
62284
|
-
if (settings.exposeCatalog) for (const id of catalogIds) ids.add(id);
|
|
63445
|
+
for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
|
|
62285
63446
|
return [...ids];
|
|
62286
63447
|
}
|
|
62287
63448
|
/** Every model the card should render, whether or not it is advertised. */
|
|
62288
63449
|
function projectModels$1(settings, store) {
|
|
62289
|
-
const known = uniqueModels$1(settings
|
|
63450
|
+
const known = uniqueModels$1(settings);
|
|
62290
63451
|
const hidden = new Set(settings.hiddenModels);
|
|
62291
63452
|
const enabledSet = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
|
|
62292
63453
|
return known.map((id) => {
|
|
62293
63454
|
const meta = store.metaOf(id);
|
|
62294
|
-
const pin = settings.pins[id] ?? defaultPin();
|
|
63455
|
+
const pin = settings.pins[id] ?? defaultPin(id);
|
|
62295
63456
|
const base = resolveModelInfo$1(id, {
|
|
62296
63457
|
displayName: "Cline Pass",
|
|
62297
63458
|
synced: settings.catalogModels,
|
|
@@ -62328,6 +63489,7 @@ function projectModels$1(settings, store) {
|
|
|
62328
63489
|
excluded: pin.exclude,
|
|
62329
63490
|
pinMode: normalizePinMode(pin.pinMode),
|
|
62330
63491
|
sort: pin.sort,
|
|
63492
|
+
has: [...pin.has],
|
|
62331
63493
|
upstreams: (meta.upstreams ?? []).map(String),
|
|
62332
63494
|
upstreamStatus: verdicts,
|
|
62333
63495
|
lastProvider: String(meta.lastProvider ?? ""),
|
|
@@ -62380,7 +63542,7 @@ async function getClinePassWebStatus(options, quotaRefresh) {
|
|
|
62380
63542
|
pool,
|
|
62381
63543
|
models,
|
|
62382
63544
|
catalogSynced: settings.catalogModels.length > 0,
|
|
62383
|
-
catalogCount: settings.
|
|
63545
|
+
catalogCount: listedClinePassCatalogModels(settings).length,
|
|
62384
63546
|
usage: store.usage().accounts.length === 0 ? null : store.usage(),
|
|
62385
63547
|
quotaRefreshing: quotaRefresh?.refreshing === true,
|
|
62386
63548
|
historySize: store.historySize(),
|
|
@@ -62392,7 +63554,6 @@ async function getClinePassWebStatus(options, quotaRefresh) {
|
|
|
62392
63554
|
maxConcurrentRequests: settings.maxConcurrentRequests,
|
|
62393
63555
|
streamIdleTimeoutMs: settings.streamIdleTimeoutMs,
|
|
62394
63556
|
historyLimit: settings.historyLimit,
|
|
62395
|
-
exposeCatalog: settings.exposeCatalog,
|
|
62396
63557
|
pinSpelling: settings.pinSpelling
|
|
62397
63558
|
}
|
|
62398
63559
|
};
|
|
@@ -62415,6 +63576,8 @@ const CLINE_PASS_ROUTES = {
|
|
|
62415
63576
|
"models/visibility-all": "POST",
|
|
62416
63577
|
"models/context-window": "POST",
|
|
62417
63578
|
"catalog/refresh": "POST",
|
|
63579
|
+
"catalog/search": "GET",
|
|
63580
|
+
"catalog/add": "POST",
|
|
62418
63581
|
usage: "GET|POST",
|
|
62419
63582
|
history: "GET"
|
|
62420
63583
|
};
|
|
@@ -62434,6 +63597,27 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62434
63597
|
ctx.emit?.("llm/adapters-updated");
|
|
62435
63598
|
};
|
|
62436
63599
|
/**
|
|
63600
|
+
* The full gateway catalog, cached as the search index for manual add.
|
|
63601
|
+
*
|
|
63602
|
+
* The metadata is kept alongside the ids: an id the official scan never saw
|
|
63603
|
+
* has no other source, and `catalog/add` copies what this call learned so the
|
|
63604
|
+
* added row is sized by the gateway rather than by the 128k default.
|
|
63605
|
+
*/
|
|
63606
|
+
const searchCatalogIds = async () => {
|
|
63607
|
+
const cached = store.catalog().ids;
|
|
63608
|
+
if (cached.length > 0) return cached;
|
|
63609
|
+
const gateway = await engine.gatewayCatalog();
|
|
63610
|
+
store.setCatalog({
|
|
63611
|
+
ids: gateway.ids,
|
|
63612
|
+
sources: ["gateway"],
|
|
63613
|
+
models: {
|
|
63614
|
+
...store.catalog().models,
|
|
63615
|
+
...gateway.models
|
|
63616
|
+
}
|
|
63617
|
+
});
|
|
63618
|
+
return gateway.ids;
|
|
63619
|
+
};
|
|
63620
|
+
/**
|
|
62437
63621
|
* Read every usable account's official quota and replace the snapshot.
|
|
62438
63622
|
*
|
|
62439
63623
|
* Shared by the explicit refresh action and the background one a status read
|
|
@@ -62743,12 +63927,13 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62743
63927
|
ok: false,
|
|
62744
63928
|
error: "this action needs a model id"
|
|
62745
63929
|
});
|
|
62746
|
-
const current = (await settingsStore.read()).pins[model] ?? defaultPin();
|
|
63930
|
+
const current = (await settingsStore.read()).pins[model] ?? defaultPin(model);
|
|
62747
63931
|
const next = {
|
|
62748
63932
|
upstreams: Array.isArray(body.upstreams) ? body.upstreams.map(String) : current.upstreams,
|
|
62749
63933
|
exclude: Array.isArray(body.exclude) ? body.exclude.map(String) : current.exclude,
|
|
62750
63934
|
pinMode: isClinePassPinMode(body.pinMode) ? body.pinMode : current.pinMode,
|
|
62751
|
-
sort: body.sort === void 0 ? current.sort : body.sort === "none" ? "" : String(body.sort)
|
|
63935
|
+
sort: body.sort === void 0 ? current.sort : body.sort === "none" ? "" : String(body.sort),
|
|
63936
|
+
has: Array.isArray(body.has) ? body.has.map(String).map((name) => name.trim()).filter((name) => name !== "") : current.has
|
|
62752
63937
|
};
|
|
62753
63938
|
await settingsStore.setPin(model, next);
|
|
62754
63939
|
const meta = store.metaOf(model);
|
|
@@ -62817,7 +64002,9 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62817
64002
|
lastProvider: result.lastProvider,
|
|
62818
64003
|
upstreams: result.upstreams,
|
|
62819
64004
|
availableProviders: result.availableProviders,
|
|
62820
|
-
tier0: result.tier0
|
|
64005
|
+
tier0: result.tier0,
|
|
64006
|
+
fingerprint: result.fingerprint,
|
|
64007
|
+
anonymous: result.anonymous
|
|
62821
64008
|
},
|
|
62822
64009
|
status: await status()
|
|
62823
64010
|
}
|
|
@@ -62875,7 +64062,8 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62875
64062
|
});
|
|
62876
64063
|
const result = await engine.test(model, {
|
|
62877
64064
|
upstreams: Array.isArray(body.upstreams) ? body.upstreams.map(String) : void 0,
|
|
62878
|
-
exclude: Array.isArray(body.exclude) ? body.exclude.map(String) : void 0
|
|
64065
|
+
exclude: Array.isArray(body.exclude) ? body.exclude.map(String) : void 0,
|
|
64066
|
+
has: Array.isArray(body.has) ? body.has.map(String).map((name) => name.trim()).filter((name) => name !== "") : void 0
|
|
62879
64067
|
});
|
|
62880
64068
|
return sendJson$2(response, 200, {
|
|
62881
64069
|
ok: true,
|
|
@@ -62897,6 +64085,9 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62897
64085
|
adopted: result.adopted,
|
|
62898
64086
|
adherence: result.adherence,
|
|
62899
64087
|
plan: firstLine$2(result.plan, 300),
|
|
64088
|
+
fingerprint: result.fingerprint,
|
|
64089
|
+
anonymous: result.anonymous,
|
|
64090
|
+
has: result.has,
|
|
62900
64091
|
warnings: result.warnings,
|
|
62901
64092
|
trace: result.trace
|
|
62902
64093
|
},
|
|
@@ -62912,7 +64103,7 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62912
64103
|
});
|
|
62913
64104
|
const body = await readRequestJson$2(request).catch(() => ({}));
|
|
62914
64105
|
const current = await settingsStore.read();
|
|
62915
|
-
const known = uniqueModels$1(current
|
|
64106
|
+
const known = uniqueModels$1(current);
|
|
62916
64107
|
const hidden = new Set(current.hiddenModels);
|
|
62917
64108
|
if (path === "models/visibility") {
|
|
62918
64109
|
const model = typeof body.model === "string" ? body.model.trim() : "";
|
|
@@ -62965,6 +64156,94 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62965
64156
|
value: await status()
|
|
62966
64157
|
});
|
|
62967
64158
|
}
|
|
64159
|
+
if (path === "catalog/search") {
|
|
64160
|
+
if (method !== "GET") return sendMethodNotAllowed$2(response);
|
|
64161
|
+
const query = (url.searchParams.get("q") ?? "").trim().toLowerCase();
|
|
64162
|
+
if (query === "") return sendJson$2(response, 200, {
|
|
64163
|
+
ok: true,
|
|
64164
|
+
value: { results: [] }
|
|
64165
|
+
});
|
|
64166
|
+
let ids;
|
|
64167
|
+
try {
|
|
64168
|
+
ids = await searchCatalogIds();
|
|
64169
|
+
} catch (error) {
|
|
64170
|
+
return sendJson$2(response, 502, {
|
|
64171
|
+
ok: false,
|
|
64172
|
+
error: firstLine$2(error instanceof Error ? error.message : error, 240)
|
|
64173
|
+
});
|
|
64174
|
+
}
|
|
64175
|
+
const current = await settingsStore.read();
|
|
64176
|
+
const listed = /* @__PURE__ */ new Set([...DEFAULT_MODELS, ...listedClinePassCatalogModels(current).map((entry) => entry.id)]);
|
|
64177
|
+
return sendJson$2(response, 200, {
|
|
64178
|
+
ok: true,
|
|
64179
|
+
value: { results: ids.filter((id) => id.toLowerCase().includes(query)).slice(0, 30).map((id) => ({
|
|
64180
|
+
id,
|
|
64181
|
+
added: listed.has(id),
|
|
64182
|
+
billing: catalogBilling(id)
|
|
64183
|
+
})) }
|
|
64184
|
+
});
|
|
64185
|
+
}
|
|
64186
|
+
if (path === "catalog/add") {
|
|
64187
|
+
if (method !== "POST") return sendMethodNotAllowed$2(response);
|
|
64188
|
+
if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
|
|
64189
|
+
ok: false,
|
|
64190
|
+
error: "Cross-origin request rejected."
|
|
64191
|
+
});
|
|
64192
|
+
const body = await readRequestJson$2(request).catch(() => ({}));
|
|
64193
|
+
const model = typeof body.model === "string" ? body.model.trim() : "";
|
|
64194
|
+
if (model === "") return sendJson$2(response, 400, {
|
|
64195
|
+
ok: false,
|
|
64196
|
+
error: "this action needs a model id"
|
|
64197
|
+
});
|
|
64198
|
+
let ids;
|
|
64199
|
+
try {
|
|
64200
|
+
ids = await searchCatalogIds();
|
|
64201
|
+
} catch (error) {
|
|
64202
|
+
return sendJson$2(response, 502, {
|
|
64203
|
+
ok: false,
|
|
64204
|
+
error: firstLine$2(error instanceof Error ? error.message : error, 240)
|
|
64205
|
+
});
|
|
64206
|
+
}
|
|
64207
|
+
if (!ids.includes(model)) return sendJson$2(response, 404, {
|
|
64208
|
+
ok: false,
|
|
64209
|
+
error: `${model} is not in the gateway catalog; refresh or search again.`
|
|
64210
|
+
});
|
|
64211
|
+
const billing = catalogBilling(model);
|
|
64212
|
+
if (billing === "credits" && body.acknowledgeBilling !== true) return sendJson$2(response, 409, {
|
|
64213
|
+
ok: false,
|
|
64214
|
+
code: "credits_billing",
|
|
64215
|
+
billing,
|
|
64216
|
+
error: `${model} is a marketplace model billed against the Cline Credits balance, not the Cline Pass plan. Retry with acknowledgeBilling:true to add it anyway.`
|
|
64217
|
+
});
|
|
64218
|
+
const current = await settingsStore.read();
|
|
64219
|
+
const published = publishedCatalogMeta(store, model);
|
|
64220
|
+
const exists = current.catalogModels.some((entry) => entry.id === model);
|
|
64221
|
+
const catalogModels = exists ? current.catalogModels.map((entry) => entry.id === model ? {
|
|
64222
|
+
...entry,
|
|
64223
|
+
...published,
|
|
64224
|
+
origin: "custom"
|
|
64225
|
+
} : entry) : [...current.catalogModels, {
|
|
64226
|
+
id: model,
|
|
64227
|
+
...published,
|
|
64228
|
+
origin: "custom"
|
|
64229
|
+
}];
|
|
64230
|
+
const enabledModelIds = current.enabledModelIds === null ? null : [.../* @__PURE__ */ new Set([...current.enabledModelIds, model])];
|
|
64231
|
+
const hiddenModels = current.hiddenModels.filter((id) => id !== model);
|
|
64232
|
+
await settingsStore.update({
|
|
64233
|
+
catalogModels,
|
|
64234
|
+
enabledModelIds,
|
|
64235
|
+
hiddenModels
|
|
64236
|
+
});
|
|
64237
|
+
emitModelsUpdated();
|
|
64238
|
+
return sendJson$2(response, 200, {
|
|
64239
|
+
ok: true,
|
|
64240
|
+
value: {
|
|
64241
|
+
added: !exists,
|
|
64242
|
+
billing,
|
|
64243
|
+
status: await status()
|
|
64244
|
+
}
|
|
64245
|
+
});
|
|
64246
|
+
}
|
|
62968
64247
|
if (path === "catalog/refresh") {
|
|
62969
64248
|
if (method !== "POST") return sendMethodNotAllowed$2(response);
|
|
62970
64249
|
if (!isSameOriginMutation(request)) return sendJson$2(response, 403, {
|
|
@@ -62985,6 +64264,7 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62985
64264
|
const published = scan.catalog[id] ?? {};
|
|
62986
64265
|
existing.set(id, {
|
|
62987
64266
|
id,
|
|
64267
|
+
origin: "official",
|
|
62988
64268
|
...published.name === void 0 ? {} : { name: published.name },
|
|
62989
64269
|
...published.contextWindow === void 0 ? {} : { contextWindow: published.contextWindow },
|
|
62990
64270
|
...published.maxTokens === void 0 ? {} : { maxTokens: published.maxTokens },
|
|
@@ -62993,17 +64273,18 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
62993
64273
|
...published.efforts === void 0 ? {} : { efforts: published.efforts }
|
|
62994
64274
|
});
|
|
62995
64275
|
}
|
|
62996
|
-
const gateway =
|
|
62997
|
-
|
|
62998
|
-
|
|
62999
|
-
|
|
63000
|
-
|
|
63001
|
-
}
|
|
63002
|
-
await settingsStore.storeCatalog(merged);
|
|
64276
|
+
const gateway = await engine.gatewayCatalog().catch(() => ({
|
|
64277
|
+
ids: [],
|
|
64278
|
+
models: {}
|
|
64279
|
+
}));
|
|
64280
|
+
await settingsStore.storeCatalog([...existing.values()]);
|
|
63003
64281
|
store.setCatalog({
|
|
63004
|
-
ids: [.../* @__PURE__ */ new Set([...scan.models, ...gateway])],
|
|
63005
|
-
sources: scan.sources,
|
|
63006
|
-
models:
|
|
64282
|
+
ids: [.../* @__PURE__ */ new Set([...scan.models, ...gateway.ids])],
|
|
64283
|
+
sources: [...scan.sources, ...gateway.ids.length > 0 ? ["gateway"] : []],
|
|
64284
|
+
models: {
|
|
64285
|
+
...gateway.models,
|
|
64286
|
+
...scan.catalog
|
|
64287
|
+
}
|
|
63007
64288
|
});
|
|
63008
64289
|
emitModelsUpdated();
|
|
63009
64290
|
return sendJson$2(response, 200, {
|
|
@@ -63077,6 +64358,8 @@ function registerClinePassRoutes(ctx, options) {
|
|
|
63077
64358
|
error: entry.error,
|
|
63078
64359
|
attempts: entry.attempts,
|
|
63079
64360
|
adherence: entry.adherence,
|
|
64361
|
+
fingerprint: entry.fingerprint,
|
|
64362
|
+
anonymous: entry.anonymous,
|
|
63080
64363
|
warnings: entry.warnings
|
|
63081
64364
|
}));
|
|
63082
64365
|
return sendJson$2(response, 200, {
|
|
@@ -63197,6 +64480,8 @@ function createClinePassStore({ historyLimit = 100 } = {}) {
|
|
|
63197
64480
|
error: entry.error ?? "",
|
|
63198
64481
|
attempts: entry.attempts ?? [],
|
|
63199
64482
|
adherence: entry.adherence ?? "",
|
|
64483
|
+
fingerprint: entry.fingerprint ?? "",
|
|
64484
|
+
anonymous: entry.anonymous === true,
|
|
63200
64485
|
warnings: entry.warnings ?? [],
|
|
63201
64486
|
...entry.code === void 0 ? {} : { code: entry.code }
|
|
63202
64487
|
};
|
|
@@ -63380,6 +64665,11 @@ const MODEL_SCHEMA = {
|
|
|
63380
64665
|
type: "string",
|
|
63381
64666
|
required: true
|
|
63382
64667
|
},
|
|
64668
|
+
has: {
|
|
64669
|
+
type: "array",
|
|
64670
|
+
required: true,
|
|
64671
|
+
items: { type: "string" }
|
|
64672
|
+
},
|
|
63383
64673
|
pinned: {
|
|
63384
64674
|
type: "array",
|
|
63385
64675
|
required: true,
|
|
@@ -63455,6 +64745,7 @@ function projectModel(status, model) {
|
|
|
63455
64745
|
hidden: !entry.enabled,
|
|
63456
64746
|
pinMode: normalizePinMode(entry.pinMode),
|
|
63457
64747
|
sort: entry.sort,
|
|
64748
|
+
has: entry.has,
|
|
63458
64749
|
pinned: entry.pinned,
|
|
63459
64750
|
excluded: entry.excluded,
|
|
63460
64751
|
upstreams: entry.upstreams,
|
|
@@ -63478,7 +64769,7 @@ function projectModel(status, model) {
|
|
|
63478
64769
|
function registerClinePassTools(ctx, options) {
|
|
63479
64770
|
const { settings, accountPool, store, engine, status, serving } = options;
|
|
63480
64771
|
const disposers = [];
|
|
63481
|
-
const pinOf = async (model) => (await settings.read()).pins[model] ?? defaultPin();
|
|
64772
|
+
const pinOf = async (model) => (await settings.read()).pins[model] ?? defaultPin(model);
|
|
63482
64773
|
disposers.push(ctx.tools.register(defineTool({
|
|
63483
64774
|
name: "cline_pass_status",
|
|
63484
64775
|
description: "Report the Cline Pass provider state: route, accounts and whether each key is stored, account mode, model and pin counts. Read-only and free. Check this first when a Cline Pass model fails.",
|
|
@@ -63643,6 +64934,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
63643
64934
|
const published = scan.catalog[id] ?? {};
|
|
63644
64935
|
merged.push({
|
|
63645
64936
|
id,
|
|
64937
|
+
origin: "official",
|
|
63646
64938
|
...published.name === void 0 ? {} : { name: published.name },
|
|
63647
64939
|
...published.contextWindow === void 0 ? {} : { contextWindow: published.contextWindow },
|
|
63648
64940
|
...published.maxTokens === void 0 ? {} : { maxTokens: published.maxTokens },
|
|
@@ -63652,10 +64944,17 @@ function registerClinePassTools(ctx, options) {
|
|
|
63652
64944
|
});
|
|
63653
64945
|
}
|
|
63654
64946
|
await settings.storeCatalog(merged);
|
|
64947
|
+
const gateway = await engine.gatewayCatalog().catch(() => ({
|
|
64948
|
+
ids: [],
|
|
64949
|
+
models: {}
|
|
64950
|
+
}));
|
|
63655
64951
|
store.setCatalog({
|
|
63656
|
-
ids: scan.models,
|
|
63657
|
-
sources: scan.sources,
|
|
63658
|
-
models:
|
|
64952
|
+
ids: [.../* @__PURE__ */ new Set([...scan.models, ...gateway.ids])],
|
|
64953
|
+
sources: [...scan.sources, ...gateway.ids.length > 0 ? ["gateway"] : []],
|
|
64954
|
+
models: {
|
|
64955
|
+
...gateway.models,
|
|
64956
|
+
...scan.catalog
|
|
64957
|
+
}
|
|
63659
64958
|
});
|
|
63660
64959
|
}
|
|
63661
64960
|
const current = await settings.read();
|
|
@@ -63681,7 +64980,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
63681
64980
|
const catalog = store.catalog();
|
|
63682
64981
|
return {
|
|
63683
64982
|
models,
|
|
63684
|
-
catalogCount:
|
|
64983
|
+
catalogCount: listedClinePassCatalogModels(await settings.read()).length,
|
|
63685
64984
|
catalogSources: catalog.sources,
|
|
63686
64985
|
catalogFetchedAt: Math.round(catalog.fetchedAt),
|
|
63687
64986
|
added
|
|
@@ -63897,6 +65196,11 @@ function registerClinePassTools(ctx, options) {
|
|
|
63897
65196
|
type: "array",
|
|
63898
65197
|
items: { type: "string" },
|
|
63899
65198
|
description: "Upstream channels to exclude for this call only."
|
|
65199
|
+
},
|
|
65200
|
+
has: {
|
|
65201
|
+
type: "array",
|
|
65202
|
+
items: { type: "string" },
|
|
65203
|
+
description: "Vercel gateway capability filter for this call only."
|
|
63900
65204
|
}
|
|
63901
65205
|
},
|
|
63902
65206
|
timeoutMs: 6e5,
|
|
@@ -63937,6 +65241,11 @@ function registerClinePassTools(ctx, options) {
|
|
|
63937
65241
|
type: "string",
|
|
63938
65242
|
required: true
|
|
63939
65243
|
},
|
|
65244
|
+
has: {
|
|
65245
|
+
type: "array",
|
|
65246
|
+
required: true,
|
|
65247
|
+
items: { type: "string" }
|
|
65248
|
+
},
|
|
63940
65249
|
warnings: {
|
|
63941
65250
|
type: "array",
|
|
63942
65251
|
required: true,
|
|
@@ -63964,6 +65273,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
63964
65273
|
const result = await engine.test(model, {
|
|
63965
65274
|
upstreams: Array.isArray(args.upstreams) ? args.upstreams.map(String) : void 0,
|
|
63966
65275
|
exclude: Array.isArray(args.exclude) ? args.exclude.map(String) : void 0,
|
|
65276
|
+
has: Array.isArray(args.has) ? args.has.map(String) : void 0,
|
|
63967
65277
|
signal: exec.signal
|
|
63968
65278
|
});
|
|
63969
65279
|
return {
|
|
@@ -63975,6 +65285,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
63975
65285
|
pipeline: result.pipeline,
|
|
63976
65286
|
account: result.account,
|
|
63977
65287
|
adherence: result.adherence,
|
|
65288
|
+
has: result.has,
|
|
63978
65289
|
warnings: result.warnings,
|
|
63979
65290
|
trace: result.trace.map((entry) => ({
|
|
63980
65291
|
upstream: entry.upstream,
|
|
@@ -64018,6 +65329,11 @@ function registerClinePassTools(ctx, options) {
|
|
|
64018
65329
|
"none"
|
|
64019
65330
|
],
|
|
64020
65331
|
description: "Reorder candidates by this metric; \"none\" clears it."
|
|
65332
|
+
},
|
|
65333
|
+
has: {
|
|
65334
|
+
type: "array",
|
|
65335
|
+
items: { type: "string" },
|
|
65336
|
+
description: "Vercel gateway capability filter; omit to keep the stored value, or pass an empty array to clear it."
|
|
64021
65337
|
}
|
|
64022
65338
|
},
|
|
64023
65339
|
timeoutMs: 3e4,
|
|
@@ -64052,6 +65368,11 @@ function registerClinePassTools(ctx, options) {
|
|
|
64052
65368
|
type: "string",
|
|
64053
65369
|
required: true
|
|
64054
65370
|
},
|
|
65371
|
+
has: {
|
|
65372
|
+
type: "array",
|
|
65373
|
+
required: true,
|
|
65374
|
+
items: { type: "string" }
|
|
65375
|
+
},
|
|
64055
65376
|
warnings: {
|
|
64056
65377
|
type: "array",
|
|
64057
65378
|
required: true,
|
|
@@ -64063,7 +65384,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
64063
65384
|
`pinned ${value.model}`,
|
|
64064
65385
|
`upstreams: ${value.pinned.length === 0 ? "(auto)" : value.pinned.join(" > ")}`,
|
|
64065
65386
|
`exclude: ${value.excluded.length === 0 ? "(none)" : value.excluded.join(", ")}`,
|
|
64066
|
-
`pinMode=${value.pinMode} sort=${value.sort === "" ? "none" : value.sort}`,
|
|
65387
|
+
`pinMode=${value.pinMode} sort=${value.sort === "" ? "none" : value.sort} has=${value.has.length === 0 ? "(none)" : value.has.join(",")}`,
|
|
64067
65388
|
...value.warnings.map((warning) => `⚠ ${warning}`)
|
|
64068
65389
|
].join("\n"))
|
|
64069
65390
|
},
|
|
@@ -64074,7 +65395,8 @@ function registerClinePassTools(ctx, options) {
|
|
|
64074
65395
|
upstreams: Array.isArray(args.upstreams) ? args.upstreams.map(String) : current.upstreams,
|
|
64075
65396
|
exclude: Array.isArray(args.exclude) ? args.exclude.map(String) : current.exclude,
|
|
64076
65397
|
pinMode: args.pinMode !== void 0 ? args.pinMode === "preferred" ? "preferred" : "strict" : current.pinMode,
|
|
64077
|
-
sort: args.sort === void 0 ? current.sort : args.sort === "none" ? "" : String(args.sort)
|
|
65398
|
+
sort: args.sort === void 0 ? current.sort : args.sort === "none" ? "" : String(args.sort),
|
|
65399
|
+
has: Array.isArray(args.has) ? args.has.map(String).map((name) => name.trim()).filter((name) => name !== "") : current.has
|
|
64078
65400
|
};
|
|
64079
65401
|
await settings.setPin(model, next);
|
|
64080
65402
|
const meta = store.metaOf(model);
|
|
@@ -64096,6 +65418,7 @@ function registerClinePassTools(ctx, options) {
|
|
|
64096
65418
|
excluded: next.exclude,
|
|
64097
65419
|
pinMode: normalizePinMode(next.pinMode),
|
|
64098
65420
|
sort: next.sort,
|
|
65421
|
+
has: next.has,
|
|
64099
65422
|
warnings
|
|
64100
65423
|
};
|
|
64101
65424
|
}
|
|
@@ -64328,16 +65651,15 @@ function registerClinePassTools(ctx, options) {
|
|
|
64328
65651
|
/** Every id the visibility toggle may act on, including shipped ones. */
|
|
64329
65652
|
function currentModelsForVisibility(settings) {
|
|
64330
65653
|
const ids = new Set(DEFAULT_MODELS);
|
|
64331
|
-
for (const model of settings
|
|
65654
|
+
for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
|
|
64332
65655
|
return [...ids];
|
|
64333
65656
|
}
|
|
64334
65657
|
//#endregion
|
|
64335
65658
|
//#region src/host/cline-pass/line.ts
|
|
64336
65659
|
/** Every id the line knows, before the visibility selection is applied. */
|
|
64337
|
-
function clinePassKnownModelIds(settings,
|
|
65660
|
+
function clinePassKnownModelIds(settings, _catalogIds = []) {
|
|
64338
65661
|
const ids = new Set(DEFAULT_MODELS);
|
|
64339
|
-
for (const model of settings
|
|
64340
|
-
if (settings.exposeCatalog) for (const id of catalogIds) ids.add(id);
|
|
65662
|
+
for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
|
|
64341
65663
|
return [...ids];
|
|
64342
65664
|
}
|
|
64343
65665
|
/** Every id the line advertises right now. */
|
|
@@ -64361,7 +65683,7 @@ function createClinePassLine(ctx, options) {
|
|
|
64361
65683
|
accountPool,
|
|
64362
65684
|
store,
|
|
64363
65685
|
pinFor: (model) => ({
|
|
64364
|
-
...modelSettings.status().pins[model] ?? defaultPin(),
|
|
65686
|
+
...modelSettings.status().pins[model] ?? defaultPin(model),
|
|
64365
65687
|
spelling: modelSettings.status().pinSpelling
|
|
64366
65688
|
}),
|
|
64367
65689
|
baseURL: () => modelSettings.status().baseURL,
|
|
@@ -66360,13 +67682,44 @@ async function fetchQoderUserInfo(accessToken, region, options = {}) {
|
|
|
66360
67682
|
};
|
|
66361
67683
|
}
|
|
66362
67684
|
/**
|
|
66363
|
-
*
|
|
67685
|
+
* 一次 PAT 兑换的失败,带 HTTP 状态(调用方据此分类「token 被拒」与「瞬时故障」)。
|
|
67686
|
+
*/
|
|
67687
|
+
var QoderPatExchangeError = class extends Error {
|
|
67688
|
+
status;
|
|
67689
|
+
/** 兑换打的是哪个区(区域猜错是这类 400 最常见的真因)。 */
|
|
67690
|
+
region;
|
|
67691
|
+
constructor(message, status, region) {
|
|
67692
|
+
super(message);
|
|
67693
|
+
this.name = "QoderPatExchangeError";
|
|
67694
|
+
this.status = status;
|
|
67695
|
+
this.region = region;
|
|
67696
|
+
}
|
|
67697
|
+
};
|
|
67698
|
+
/**
|
|
67699
|
+
* 上游对这个状态的回答是否在说「这把 token 不认」。
|
|
67700
|
+
*
|
|
67701
|
+
* 四个区共用一套语义:400 BadRequest 是服务端读完了 `personal_token` 才发现它
|
|
67702
|
+
* 不被接受(缺字段、写错字段名、空体也是 400,但那种请求来自别的实现,不来自这里);
|
|
67703
|
+
* 401/403 是鉴权拒绝;404 是这套部署上没有这个 token。
|
|
67704
|
+
*/
|
|
67705
|
+
function qoderPatRejectionStatus(status) {
|
|
67706
|
+
return status === 400 || status === 401 || status === 403 || status === 404;
|
|
67707
|
+
}
|
|
67708
|
+
/**
|
|
67709
|
+
* POST {openapi}/api/v1/jobToken/exchange,体 \`{"personal_token":"<pt-...>","machine_id":…}\`。
|
|
66364
67710
|
*
|
|
66365
67711
|
* PAT 不能直接鉴权,必须先兑换(docs.qoder.com/cli/authentication + pi-provider pat.ts)。
|
|
66366
|
-
*
|
|
67712
|
+
* 失败抛 {@link QoderPatExchangeError}(登录路径要如实告诉用户 PAT 被拒)。
|
|
67713
|
+
*
|
|
67714
|
+
* **`machine_id` 是官方 CLI 1.1.74 的请求体字段**(qodercli2api `docs/oauth.md` §3:
|
|
67715
|
+
* `getMachineIdentityRequestFields` 为 exchange/refresh 补上既有 machine_id,
|
|
67716
|
+
* 没有 UMID 时不补 machine_token,也不拿 machine_id 冒充它)。早先按 pi-provider
|
|
67717
|
+
* 只发 `personal_token` 的形状在实测里仍可能被服务端接受,但没有理由不发这个官方字段。
|
|
66367
67718
|
*/
|
|
66368
67719
|
async function exchangeQoderJobToken(personalToken, region, options = {}) {
|
|
66369
|
-
const
|
|
67720
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
67721
|
+
const env = options.env ?? process.env;
|
|
67722
|
+
const response = await fetchFn(openapiBaseForRegion(region, env) + JOB_TOKEN_EXCHANGE_PATH, {
|
|
66370
67723
|
method: "POST",
|
|
66371
67724
|
headers: {
|
|
66372
67725
|
"content-type": "application/json",
|
|
@@ -66375,12 +67728,15 @@ async function exchangeQoderJobToken(personalToken, region, options = {}) {
|
|
|
66375
67728
|
"cosy-clienttype": "5",
|
|
66376
67729
|
"user-agent": USER_AGENT
|
|
66377
67730
|
},
|
|
66378
|
-
body: JSON.stringify({
|
|
67731
|
+
body: JSON.stringify({
|
|
67732
|
+
personal_token: personalToken,
|
|
67733
|
+
machine_id: resolveQoderMachineId(env)
|
|
67734
|
+
}),
|
|
66379
67735
|
signal: timeoutSignal(options.signal, DISCOVERY_TIMEOUT_MS)
|
|
66380
67736
|
});
|
|
66381
67737
|
if (!response.ok) {
|
|
66382
67738
|
const detail = qoderErrorDetail(await response.text().catch(() => ""), 160);
|
|
66383
|
-
throw new
|
|
67739
|
+
throw new QoderPatExchangeError("Qoder(订阅) PAT 兑换失败(HTTP " + response.status + ")" + (detail === "" ? "" : ":" + detail), response.status, region);
|
|
66384
67740
|
}
|
|
66385
67741
|
const payload = await response.json().catch(() => void 0);
|
|
66386
67742
|
if (!isRecord$7(payload)) throw new Error("Qoder(订阅) PAT 兑换响应不是 JSON 对象");
|
|
@@ -67823,11 +69179,42 @@ async function qoderCredentialsFromDevicePoll(ready, machineId, options = {}) {
|
|
|
67823
69179
|
* 兑换 + userinfo 补齐;**PAT 本体留在凭据里**(host-only),因为 PAT 的「刷新」
|
|
67824
69180
|
* 就是拿它重新兑换一把 job token(pi-provider 把 PAT 编进 refresh 字段,
|
|
67825
69181
|
* 本线路把它放进一个具名字段——同一件事,更好读)。
|
|
69182
|
+
*
|
|
69183
|
+
* ---------------------------------------------------------------------------
|
|
69184
|
+
* 自动补认区域
|
|
69185
|
+
* ---------------------------------------------------------------------------
|
|
69186
|
+
* 区域是**凭据的属性**,但 PAT 字符串本身不自述区域,而卡片上的区域选择器默认停在
|
|
69187
|
+
* 全球区。中国站的 PAT 拿去全球区端点兑换,上游回的是 400 `BadRequest`——没有
|
|
69188
|
+
* 任何一句说明「你打错了区」,用户看到的只有一串 requestId。这一步因此不是
|
|
69189
|
+
* 「多试一次」,而是**把选错区域的这个必然失败翻译成一次成功的登录**。
|
|
69190
|
+
*
|
|
69191
|
+
* 只在明确的 token 被拒状态下换区({@link qoderPatRejectionStatus}):HTTP 429、
|
|
69192
|
+
* 5xx、网络错误都不换——那些是瞬时故障,换区重发只会把一个可重试的问题变成两次
|
|
69193
|
+
* 打上游。首次命中的区域写进凭据,之后刷新照它走。
|
|
67826
69194
|
*/
|
|
67827
69195
|
async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
67828
69196
|
const pat = personalToken.trim();
|
|
67829
69197
|
if (pat === "") throw new Error("Qoder(订阅) 的 PAT 是空的。");
|
|
67830
|
-
|
|
69198
|
+
if (!pat.startsWith("pt-")) throw new Error("Qoder(订阅) 的 PAT 格式不对:它必须以 `pt-` 开头,粘贴时不要带引号或多余空白。令牌在 https://qoder.com/account/integrations(中国站是 https://qoder.com.cn/account/integrations)生成。");
|
|
69199
|
+
const candidates = [region, ...QODER_REGIONS.filter((other) => other !== region)];
|
|
69200
|
+
let resolved = region;
|
|
69201
|
+
let exchanged = null;
|
|
69202
|
+
let rejection = null;
|
|
69203
|
+
for (const candidate of candidates) try {
|
|
69204
|
+
exchanged = await exchangeQoderJobToken(pat, candidate, options);
|
|
69205
|
+
resolved = candidate;
|
|
69206
|
+
rejection = null;
|
|
69207
|
+
break;
|
|
69208
|
+
} catch (error) {
|
|
69209
|
+
rejection = error;
|
|
69210
|
+
if (!(error instanceof QoderPatExchangeError) || !qoderPatRejectionStatus(error.status)) throw error;
|
|
69211
|
+
}
|
|
69212
|
+
if (exchanged === null) {
|
|
69213
|
+
const primary = rejection instanceof Error ? rejection.message : String(rejection);
|
|
69214
|
+
const tried = candidates.length > 1 ? regionLabel(candidates[0]) + "与" + regionLabel(candidates[1]) + "两个区域都试过" : regionLabel(region) + "区域试过";
|
|
69215
|
+
const status = rejection instanceof QoderPatExchangeError ? rejection.status : 400;
|
|
69216
|
+
throw new QoderPatExchangeError("这把 PAT 被上游拒了(" + tried + ",都会在兑换时被拒)。最可能的原因是它已经被撤销或过期——请到 Integrations 页重新生成一个再粘贴。技术详情:" + primary, status, region);
|
|
69217
|
+
}
|
|
67831
69218
|
let uid = null;
|
|
67832
69219
|
let email = null;
|
|
67833
69220
|
let name = null;
|
|
@@ -67835,7 +69222,7 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
|
67835
69222
|
let organizationTags = [];
|
|
67836
69223
|
let dataPolicyAgreed = false;
|
|
67837
69224
|
try {
|
|
67838
|
-
const info = await fetchQoderUserInfo(exchanged.token,
|
|
69225
|
+
const info = await fetchQoderUserInfo(exchanged.token, resolved, options);
|
|
67839
69226
|
uid = info.userID === "" ? null : info.userID;
|
|
67840
69227
|
email = info.email === "" ? null : info.email;
|
|
67841
69228
|
name = info.name === "" ? null : info.name;
|
|
@@ -67847,7 +69234,7 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
|
67847
69234
|
accessToken: exchanged.token,
|
|
67848
69235
|
refreshToken: exchanged.refreshToken,
|
|
67849
69236
|
expiresAt: exchanged.expiresAt,
|
|
67850
|
-
region,
|
|
69237
|
+
region: resolved,
|
|
67851
69238
|
origin: "pat",
|
|
67852
69239
|
uid,
|
|
67853
69240
|
email,
|
|
@@ -67859,6 +69246,10 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
|
67859
69246
|
authenticatedAt: Date.now()
|
|
67860
69247
|
};
|
|
67861
69248
|
}
|
|
69249
|
+
/** 区域的短名(只用于拼给人看的一句话,卡片另有自己的文案)。 */
|
|
69250
|
+
function regionLabel(region) {
|
|
69251
|
+
return REGION_HOSTS$1[region].title;
|
|
69252
|
+
}
|
|
67862
69253
|
/**
|
|
67863
69254
|
* 刷新一份凭据(号池 refresh 钩子的实现)。
|
|
67864
69255
|
*
|
|
@@ -67870,6 +69261,11 @@ async function qoderCredentialsFromPat(personalToken, region, options = {}) {
|
|
|
67870
69261
|
*
|
|
67871
69262
|
* 失败带上 \`status\`(有 HTTP 状态时),号池据此分类「这个账号要重登」还是
|
|
67872
69263
|
* 「瞬时故障,别判死」。
|
|
69264
|
+
*
|
|
69265
|
+
* PAT 分支走的是 {@link qoderCredentialsFromPat},因此**也和首次登录一样会补认区域**:
|
|
69266
|
+
* 一份区域记错了的旧凭据,会在下一次刷新时自己搬到真正认它的那一区(区域本来就是
|
|
69267
|
+
* 凭据的属性,而 PAT 是唯一能问出它归属的原料)。这是修好「区域选错 → 永远 400」
|
|
69268
|
+
* 的第二半——只修首次登录的话,落盘里已有的错区账号会一直刷不上。
|
|
67873
69269
|
*/
|
|
67874
69270
|
async function refreshQoderCredential(credentials, fetchFn = fetch) {
|
|
67875
69271
|
if (credentials.origin === "pat" && credentials.personalToken !== null) return {
|
|
@@ -68156,8 +69552,13 @@ function qoderPoolQuota(snapshot) {
|
|
|
68156
69552
|
*
|
|
68157
69553
|
* 401/403 → \`'expired'\`(上游明说这把 token 不行了,这个账号要重登);
|
|
68158
69554
|
* 其余(网络故障、5xx、形状变了)→ undefined:瞬时故障不该把一个账号判死。
|
|
69555
|
+
*
|
|
69556
|
+
* PAT 来源的刷新是「拿存下的 PAT 重兑换」,它的 400 同样是终局:上游读完了
|
|
69557
|
+
* `personal_token` 才拒绝(见 client.ts 的 {@link QoderPatExchangeError}),
|
|
69558
|
+
* 重试不会变好,只有重新生成 PAT 能救这个账号。
|
|
68159
69559
|
*/
|
|
68160
69560
|
function qoderRefreshFailureStatus(error) {
|
|
69561
|
+
if (error instanceof QoderPatExchangeError && qoderPatRejectionStatus(error.status)) return "expired";
|
|
68161
69562
|
const status = error?.status;
|
|
68162
69563
|
if (status === 401 || status === 403) return "expired";
|
|
68163
69564
|
const message = error instanceof Error ? error.message : "";
|
|
@@ -68631,7 +70032,13 @@ function patchFromBody(body) {
|
|
|
68631
70032
|
if (body.selectedAccountId !== void 0) patch.selectedAccountId = typeof body.selectedAccountId === "string" ? body.selectedAccountId : null;
|
|
68632
70033
|
return patch;
|
|
68633
70034
|
}
|
|
68634
|
-
/**
|
|
70035
|
+
/**
|
|
70036
|
+
* 请求体里的区域。
|
|
70037
|
+
*
|
|
70038
|
+
* 认不出时回落全球区(它是唯一支持设备流的,且是卡片默认值),但**只对设备流成立**:
|
|
70039
|
+
* 设备流本来就只有全球区。PAT 粘贴的兜底不靠这一层——`qoderCredentialsFromPat`
|
|
70040
|
+
* 会把「区域猜错」这件事自己修好(见该函数的注释),所以这里不需要猜得更聪明。
|
|
70041
|
+
*/
|
|
68635
70042
|
function regionFromBody(body) {
|
|
68636
70043
|
return isQoderRegion(body.region) ? body.region : "global";
|
|
68637
70044
|
}
|
|
@@ -74227,9 +75634,7 @@ function effectiveAllowedRoutes(agent, preference, sessions, scope = "session")
|
|
|
74227
75634
|
return authorizedRoutesFor(agent, sessions) ?? (scope === "preference" ? preference?.allowedModels ?? [] : []);
|
|
74228
75635
|
}
|
|
74229
75636
|
/**
|
|
74230
|
-
*
|
|
74231
|
-
* `list_subagent_models` result when the calling Session is governed by an
|
|
74232
|
-
* allowlist.
|
|
75637
|
+
* State a Session's routing posture in the `list_subagent_models` result.
|
|
74233
75638
|
*
|
|
74234
75639
|
* Calling `list_subagent_models({})` upstream returns only provider names
|
|
74235
75640
|
* (`minimax-code — MiniMax Code`), and calling it with `provider` prints
|
|
@@ -74238,18 +75643,38 @@ function effectiveAllowedRoutes(agent, preference, sessions, scope = "session")
|
|
|
74238
75643
|
* here lets even a single `list_subagent_models({})` call hand the model every
|
|
74239
75644
|
* authorized route in the form `subagent` expects, eliminating any reason to
|
|
74240
75645
|
* search local config files or source code for the allowlist.
|
|
75646
|
+
*
|
|
75647
|
+
* The empty case states the opposite fact rather than staying silent, because
|
|
75648
|
+
* silence is what makes a model infer that a mounted tool implies enforcement.
|
|
75649
|
+
* It does not: the tool row and the route parameters are static, while whether
|
|
75650
|
+
* anything is enforced is decided by the allowlist the Session recorded. The
|
|
75651
|
+
* two facts differ, so the model has to be able to read the second one instead
|
|
75652
|
+
* of deriving it from the first.
|
|
74241
75653
|
* @param output - Original text returned by `list_subagent_models`.
|
|
74242
|
-
* @param allowed - Routes the calling Session authorizes.
|
|
74243
|
-
* @returns the output with the Session
|
|
75654
|
+
* @param allowed - Routes the calling Session authorizes, empty when none is in force.
|
|
75655
|
+
* @returns the output with the Session routing posture appended.
|
|
74244
75656
|
*/
|
|
74245
75657
|
function enrichListSubagentModelsOutput(output, allowed) {
|
|
74246
|
-
if (allowed.length === 0)
|
|
75658
|
+
if (allowed.length === 0) {
|
|
75659
|
+
if (output.includes("No allowlist is in force for this Session: subagent routing is unrestricted, no route is validated against any list, and omitting provider/model is legitimate rather than a rule breach. To govern routing per task, enable subagent model selection under \"Settings -> Subagents\" and start a new session; this one keeps its current posture.")) return output;
|
|
75660
|
+
return output.trim().length === 0 ? UNRESTRICTED_ROUTING_NOTICE : `${output}\n\n${UNRESTRICTED_ROUTING_NOTICE}`;
|
|
75661
|
+
}
|
|
74247
75662
|
const header = "Authorized child routes for this Session (already filtered by the Session allowlist; only these routes are accepted, and `provider` and `model` must be passed as separate fields):";
|
|
74248
75663
|
if (output.includes(header)) return output;
|
|
74249
75664
|
const suffix = `${header}\n${allowed.map((route) => `- ${route.provider}/${route.model} -> provider: "${route.provider}", model: "${route.model}"`).join("\n")}`;
|
|
74250
75665
|
return output.trim().length === 0 ? suffix : `${output}\n\n${suffix}`;
|
|
74251
75666
|
}
|
|
74252
75667
|
/**
|
|
75668
|
+
* The statement appended to `list_subagent_models` when no allowlist governs the
|
|
75669
|
+
* calling Session.
|
|
75670
|
+
*
|
|
75671
|
+
* It names the three consequences a reading model would otherwise have to guess
|
|
75672
|
+
* at: nothing is enforced, omitting the route is legitimate, and "authorized" is
|
|
75673
|
+
* not a word that can be used about any route here. It also names the one action
|
|
75674
|
+
* that changes the state, without asking the user to stop and do it.
|
|
75675
|
+
*/
|
|
75676
|
+
const UNRESTRICTED_ROUTING_NOTICE = "No allowlist is in force for this Session: subagent routing is unrestricted, no route is validated against any list, and omitting provider/model is legitimate rather than a rule breach. To govern routing per task, enable subagent model selection under \"Settings -> Subagents\" and start a new session; this one keeps its current posture.";
|
|
75677
|
+
/**
|
|
74253
75678
|
* Rewrite the upstream `list_subagent_models` and `subagent` tool/parameter
|
|
74254
75679
|
* descriptions when the calling Session enforces an allowlist.
|
|
74255
75680
|
*
|
|
@@ -74450,7 +75875,6 @@ function installSubagentModelAuthorization(ctx, sessions, config = {}) {
|
|
|
74450
75875
|
const decision = await next();
|
|
74451
75876
|
if (exec.name !== "list_subagent_models" || result.isError === true || decision?.kind !== "accept") return decision;
|
|
74452
75877
|
const allowed = effectiveAllowedRoutes(exec.agent, subagentModelSelectionPreference(settings), sessions, scope);
|
|
74453
|
-
if (allowed.length === 0) return decision;
|
|
74454
75878
|
const currentValue = decision.value !== void 0 ? decision.value : result.value;
|
|
74455
75879
|
if (typeof currentValue === "string") {
|
|
74456
75880
|
const enriched = enrichListSubagentModelsOutput(currentValue, allowed);
|
|
@@ -74621,15 +76045,25 @@ function candidatePackageRoots(entryScript = process.argv[1]) {
|
|
|
74621
76045
|
return [...roots];
|
|
74622
76046
|
}
|
|
74623
76047
|
/**
|
|
74624
|
-
* Whether a package is installed in any candidate node_modules tree.
|
|
74625
|
-
*
|
|
76048
|
+
* Whether a package or exported subpath is installed in any candidate node_modules tree.
|
|
76049
|
+
* A subpath needs its own exact `exports` key; the package root alone is not enough.
|
|
74626
76050
|
*/
|
|
74627
76051
|
function packageInstalled(specifier, roots = candidatePackageRoots()) {
|
|
74628
76052
|
const pkg = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
76053
|
+
const subpath = specifier.slice(pkg.length);
|
|
74629
76054
|
for (const root of roots) {
|
|
74630
76055
|
let dir = root;
|
|
74631
76056
|
for (;;) {
|
|
74632
|
-
|
|
76057
|
+
const manifestPath = join(dir, "node_modules", pkg, "package.json");
|
|
76058
|
+
if (existsSync(manifestPath)) {
|
|
76059
|
+
if (subpath === "") return true;
|
|
76060
|
+
try {
|
|
76061
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
76062
|
+
if (manifest.exports !== void 0 && Object.prototype.hasOwnProperty.call(manifest.exports, `.${subpath}`)) return true;
|
|
76063
|
+
} catch {
|
|
76064
|
+
continue;
|
|
76065
|
+
}
|
|
76066
|
+
}
|
|
74633
76067
|
const parent = dirname(dir);
|
|
74634
76068
|
if (parent === dir) break;
|
|
74635
76069
|
dir = parent;
|
|
@@ -74984,6 +76418,12 @@ function installBundledPresets(host, install) {
|
|
|
74984
76418
|
*/
|
|
74985
76419
|
/** The row package whose presence marks a harness that takes presets from a row. */
|
|
74986
76420
|
const PRESET_REGISTRY_PACKAGE = "@deepseek-ai/dsh-agent-preset";
|
|
76421
|
+
/**
|
|
76422
|
+
* The package whose presence marks a harness (>= 0.2.1) that injects the
|
|
76423
|
+
* session working directory via `WORKING_DIRECTORY` user context and no longer
|
|
76424
|
+
* registers `{{cwd}}` as a `systemPrompt` variable.
|
|
76425
|
+
*/
|
|
76426
|
+
const WORKING_DIRECTORY_PACKAGE = "@deepseek-ai/dsh-working-directory";
|
|
74987
76427
|
/** The id the `dispatch` preset is selectable under. */
|
|
74988
76428
|
const DISPATCH_PRESET_ID = "dispatch";
|
|
74989
76429
|
/**
|
|
@@ -75031,7 +76471,7 @@ Criterion: whether spawn_teammate and wait_agent exist (in PTC mode: tools.spawn
|
|
|
75031
76471
|
## R0 · Complexity Triage (Triage First, Then Choose the Workflow)
|
|
75032
76472
|
Upon receiving a task, determine its tier first, write one line stating "Tier + Rationale", and then decide which phases to run.
|
|
75033
76473
|
- L0 Simple / Single-point: single-file or single-point edit; unambiguous requirements; reversible change; no parallelism value; you already hold all required context. -> Do not spawn subagents or run an interview; execute directly and report.
|
|
75034
|
-
- L1 Moderate:
|
|
76474
|
+
- L1 Moderate: edits at most 5 files (5 is a cap, not a range and not a target); requirements mostly clear with at most 2 open points; decomposable but with limited parallelism gain. -> Ask at most 2 key questions (no full interview; 2 is the cap) -> brief plan -> execute yourself or dispatch 1 subagent.
|
|
75035
76475
|
- L2 Complex / Multi-step: cross-module or multi-deliverable; ambiguity or hidden assumptions; requires exploring unknowns; independent subtasks suitable for parallel execution. -> Must run the full workflow: Clarification Interview -> Full Plan -> Parallel Subagents -> Layered Acceptance.
|
|
75036
76476
|
When in doubt, clarify before escalating; if a task turns out more complex than expected, stop, re-triage, and inform the user. Never open an interview or multi-agent orchestration for L0/L1 tasks.
|
|
75037
76477
|
|
|
@@ -75048,21 +76488,25 @@ For L2 tasks, never execute work yourself outside these six categories: (1) Plan
|
|
|
75048
76488
|
For L0/L1 tasks, execute directly—do not spawn subagents just to look like you are orchestrating.
|
|
75049
76489
|
|
|
75050
76490
|
## R2 · Model Routing (Check Availability First, Then Decide How to Dispatch)
|
|
75051
|
-
Before dispatching, check whether subagent model selection is enabled in this session: check whether list_subagent_models exists (in PTC mode: whether tools.list_subagent_models is available). Its presence and the
|
|
76491
|
+
Before dispatching, check whether subagent model selection is enabled in this session: check whether list_subagent_models exists (in PTC mode: whether tools.list_subagent_models is available). Its presence tells you the tool is mounted, and nothing more: the tool row and the provider / model / reasoning_effort parameters are mounted statically, so they are present even in a session that authorized no route at all. Whether the route guard enforces anything is decided by the allowlist this session recorded, which is a separate fact. Read that fact instead of inferring it: the tool's output appends the session's authorized routes, and appends an explicit statement when no allowlist is in force. Then dispatch in one of three states.
|
|
75052
76492
|
|
|
75053
|
-
[
|
|
76493
|
+
[State: Listed] The output ends with "Authorized child routes for this Session" and lists at least one route—enforcement is active. Dispatch with explicit routing; this is the default operating mode.
|
|
75054
76494
|
- The sole source of truth for the allowlist is list_subagent_models: before your first dispatch, call list_subagent_models({}) to list authorized providers (its output also appends the full breakdown of every authorized provider/model pair for this session); if you need per-model capabilities or reasoning effort levels under a provider, call list_subagent_models({ provider }) to list authorized models under that provider (or pass { provider, model }). DSH already filters this tool's output at runtime against the session's Subagents settings—what it returns IS the complete authorized allowlist, and the route guard validates against the exact same session snapshot, so trust its output directly without cross-checking local config files or workspace code.
|
|
75055
76495
|
- list_subagent_models({ provider }) formats entries as <provider>/<model> — <Name>; on every dispatch, pass the split provider and model fields explicitly (model without the <provider>/ prefix, plus reasoning_effort when needed), and the route must be in the allowlist returned by the tool.
|
|
75056
76496
|
- Before every dispatch, write out the routing decision line: route: <provider>/<model> (+ reasoning_effort when applicable). Do not dispatch without writing this line first. The runtime route guard (enabled by default, covering tools.subagent(...) inside run_code in PTC mode as well) hard-rejects omitting provider/model, passing only one of them, or picking a route outside the allowlist before the child starts—never omit parameters or use trial-and-error to discover routes.
|
|
75057
76497
|
- If a tool's generic upstream DSH description calls model selection "optional" or states "Catalog membership is advisory", this rule takes precedence: the runtime output is already filtered to this session's allowlist and needs no cross-checking elsewhere.
|
|
75058
76498
|
|
|
75059
|
-
[
|
|
75060
|
-
-
|
|
76499
|
+
[State: Unlisted] The tool is present but its output states no allowlist is in force—the guard authorizes every route and enforces nothing. This is the second legitimate state of this mode, not a violation, and not the same thing as "model selection is off".
|
|
76500
|
+
- Dispatch per R3–R6 as usual. Passing provider/model is still legal and still needed for the child to differ from your route; omitting it is a genuine choice rather than a rule breach, because nothing is being enforced.
|
|
76501
|
+
- Say it once, in one sentence, in your delivery summary: subagent routing is unrestricted in this session because no allowlist was recorded. Only if the task clearly wants per-task routing (mechanical refactoring vs. deep reasoning) should you also point the user at "Settings -> Subagents" plus a new session—do not stop and wait.
|
|
76502
|
+
- Never claim per-task routing is in force here, and never report a route as "authorized" when the session recorded no allowlist—there was nothing to authorize it against.
|
|
76503
|
+
|
|
76504
|
+
[State: Unmounted] list_subagent_models does not exist and subagent exposes no provider/model/reasoning_effort at all—the guard is inactive and subagents silently inherit your route. Do not error, complain, or ask the user to change settings; continue with DSH's default behavior.
|
|
75061
76505
|
- Dispatch and verify per R3–R6 as usual, simply omitting the route: line.
|
|
75062
76506
|
- Only exception worth a brief note: if a task is clearly better suited for a different model (e.g., mechanical refactoring vs. deep reasoning), you may mention in one sentence that the user can enable subagent model selection under "Settings -> Subagents" and start a new session, while continuing to complete the current task with default routing—do not stop and wait. That setting only affects newly created sessions.
|
|
75063
76507
|
|
|
75064
76508
|
subagent_fork deliberately shares the parent agent's route (reusing conversation history and KV cache); use it only for reviews that require the coordinator's context, never as a shortcut around explicit routing.
|
|
75065
|
-
It has no provider/model parameters, so the explicit-route rule under [
|
|
76509
|
+
It has no provider/model parameters, so the explicit-route rule under [State: Listed] does not apply to it; however, the guard still validates the route it inherits: if your current model is not in the allowlist, subagent_fork will be hard-rejected—switch to subagent with an explicit allowed route, or switch this session to an allowed model so subagent_fork becomes available again.
|
|
75066
76510
|
Choose models based on task nature and model capability; keep model selection consistent across tasks of the same kind within a batch.
|
|
75067
76511
|
|
|
75068
76512
|
## R3 · Subagent Autonomy
|
|
@@ -75072,21 +76516,26 @@ A subagent has full authority to plan, execute, and self-verify within its assig
|
|
|
75072
76516
|
Subagents run with DSH's default subagent role and environment; do not inject custom personas. Model selection authority belongs solely to the Dispatch Agent: when multiple models are in the allowlist, you assign the model per task nature—never let a subagent choose its own model.
|
|
75073
76517
|
|
|
75074
76518
|
## R5 · Dispatch Discipline
|
|
75075
|
-
Every task brief must contain
|
|
76519
|
+
Every task brief must contain six elements: Goal / Boundary (which files or modules may be touched) / Constraints / Limits (see below) / Acceptance Criteria / Required Evidence to Report.
|
|
76520
|
+
- [Limits] One line per numeric or quantitative limit, each spelling out four things: **what it bounds, in what unit, the cap, and whether it accumulates**. A subagent is a fresh instance of the same model and reads your brief exactly the way an ambiguous rule gets read—taking the strictest available meaning and not checking—so a loose limit is spent twice: once when you write it, once when the child under-delivers against it. Write "at most 3 attempts", not "a few attempts"; write "edits at most 5 files", not "2–5 files". Where a dimension is genuinely unbounded, say so explicitly ("test runs: unlimited"); silence is read as a limit of zero. Cover at minimum: attempts/retries, files edited, commands run, and any per-cycle quota with its reset rule (a quota that resets per cycle is lost, not banked—say that).
|
|
76521
|
+
- Do not ask the child to restate your rules back as a correctness check: restating an ambiguous rule reproduces the misreading rather than catching it. If a limit is genuinely unclear, the child should spend one minimal tool call to verify it; write that escaping instruction into the brief when the ambiguity is unavoidable.
|
|
75076
76522
|
Issue mutually independent dispatches in parallel within the same message rather than waiting serially. A subagent's report is evidence, not a final verdict.
|
|
75077
76523
|
Precondition: never dispatch implementation subagents before the clarification interview has converged.
|
|
75078
76524
|
In PTC mode, dispatch via tools.subagent(...) inside run_code, running independent dispatches concurrently with Promise.all / asyncio.gather; parameters match the native tool.
|
|
75079
76525
|
|
|
75080
76526
|
## R6 · Review & Acceptance
|
|
75081
76527
|
Two layers: subagent self-check -> your independent review. Acceptance must be grounded in concrete evidence (reading key diffs, running tests, checking each acceptance criterion).
|
|
76528
|
+
The completion notification is a post-hoc signal: it says the child stopped, not that the work is correct—and it arrives after the work is already done. Your pre-hoc control is the acceptance criteria you wrote into the brief under R5, not the notification and not the child's self-report.
|
|
75082
76529
|
If work fails review, re-dispatch with specific evidence—do not step in to fix the code yourself, and never declare completion based solely on a subagent's self-report.
|
|
75083
|
-
Re-dispatch
|
|
76530
|
+
Re-dispatch budget, counted per task brief: 1 first attempt + at most 2 re-dispatches = at most 3 runs total. Only if it still fails after the third run, or if re-dispatch is hard-rejected (tool unavailable or repeated errors), transition to R8 Fallback and state which condition triggered it. Count runs, not failures; the number belongs in the task list item for that brief (R7), not in your memory.
|
|
76531
|
+
[Noticed but not handled] Every review that finds a defect must end one of two ways: the defect goes back out on a re-dispatch with evidence, or it goes to the user through ask_user_question as a scoping decision (R-W). Writing it into the delivery summary and shipping anyway is not an available outcome—a risk you named but never routed is exactly the failure this rule exists to stop. When the final delivery summary is written, it carries the list of risks you noticed and routed, each with what you did about it; an empty list means you found none, which is a claim in its own right.
|
|
75084
76532
|
|
|
75085
76533
|
## R-W · Turn Boundary (Waiting / Goals / Questions)
|
|
75086
76534
|
|
|
75087
76535
|
All three rules govern the boundary of a turn: after dispatching, before continuing, and before stopping.
|
|
75088
76536
|
|
|
75089
76537
|
[Waiting for Subagents] By default, end your turn without calling any more tools and let the subagent completion notification wake you—notifications are built-in; do not manufacture your own wait.
|
|
76538
|
+
- The notification is a post-hoc signal. It tells you a child stopped running; it does not tell you the work is correct, and nothing about it arrives before the work is done. Treat it as "there is now something to review", never as "this passed". The pre-hoc control is the acceptance criteria in the brief (R5); the review itself is R6.
|
|
75090
76539
|
- Never run sleep, polling loops, watch, or any long-running placeholder command just to "hang around": they burn context and budget for zero information.
|
|
75091
76540
|
- If you have independent coordinator work in hand (the six categories in R1, reviewing another subagent's output, reading evidence you need), do it first and then end your turn.
|
|
75092
76541
|
- Only when you are genuinely blocked and must collect a result immediately should you call job_output({ wait: true }) for a bounded wait; that is a collection tool, not a timer.
|
|
@@ -75102,11 +76551,12 @@ All three rules govern the boundary of a turn: after dispatching, before continu
|
|
|
75102
76551
|
- Only two exceptions: plan-mode approval still goes through exit_plan_mode (which has its own channel); and when the task is finished and only the delivery summary remains, do not ask a question.
|
|
75103
76552
|
|
|
75104
76553
|
## R7 · Documentation
|
|
75105
|
-
Maintain the task list (in todo_write, not create_goal; see R-W), decision log, and interview conclusions (which decisions are settled and why). Subagents never report directly to the user.
|
|
76554
|
+
Maintain the task list (in todo_write, not create_goal; see R-W), decision log, and interview conclusions (which decisions are settled and why). Each task item carries its own run count (R6), so the re-dispatch budget is read rather than remembered. Subagents never report directly to the user.
|
|
76555
|
+
[Before every turn] Scan the task list for items that will escalate, expire, or time out if you do nothing this turn, and handle those before the items that are merely large. Concretely, in this mode: a result that came back and has not been reviewed; an acceptance that failed with the rework scope still undecided; a question the user has not answered and that blocks a dispatch; a brief sitting at its run count. An item that has waited is not the same as an item that is fine—a low-priority backlog is what eventually turns into the incident, so every item still open at the end of a turn is listed with its age in turns, and any item older than the brief's own limit is handled or escalated instead of carried forward once more.
|
|
75106
76556
|
|
|
75107
76557
|
## R8 · Fallback
|
|
75108
76558
|
Fallback is a task-scoped, non-session-wide exception: it covers only the single task that triggered it and never alters the default operating mode for subsequent tasks in this session.
|
|
75109
|
-
You may execute a task yourself only when at least one of the following holds, and you must state which condition triggered it and the supporting evidence: L0/L1 task; subagent tools
|
|
76559
|
+
You may execute a task yourself only when at least one of the following holds, and you must state which condition triggered it and the supporting evidence: L0/L1 task; subagent tools not mounted at all (R2 [State: Unmounted]); task is non-delegable (pure Q&A, tiny single-point edit, or an operation that must share your immediate context); or the same task brief has still not passed review after its third run under R6. An unmounted tool is the only routing condition that permits fallback: an empty, unlisted allowlist (R2 [State: Unlisted]) is not one, because delegation still works there—it is simply unrestricted.
|
|
75110
76560
|
Fix the trigger condition on hard rejections instead of treating them as fallback grounds: on active child limit -> wait for existing children to settle and re-dispatch; on subagent model selection -> specify an explicit provider/model from the authorized routes listed in the error; on route-inheritance rejection -> switch to an explicit-route subagent tool or switch this session to an allowed model. Unless the condition is fixed, the same rejection will recur.
|
|
75111
76561
|
Reset: fallback applies only to the current task. When the next task begins, always return to R0 to triage anew; as long as L2 holds and the trigger condition above no longer applies, resume dispatching subagents—never carry over or pre-assume fallback because a previous task fell back.
|
|
75112
76562
|
|
|
@@ -75114,7 +76564,7 @@ Reset: fallback applies only to the current task. When the next task begins, alw
|
|
|
75114
76564
|
1. Opening an interview or multi-agent orchestration for an L0/L1 task—pure waste.
|
|
75115
76565
|
2. Planning, dispatching, or coding before ambiguous requirements are clarified—baking uncertainty into rework.
|
|
75116
76566
|
3. Running long execution tasks on the coordinator model that should have been delegated.
|
|
75117
|
-
4. Omitting provider/model
|
|
76567
|
+
4. Omitting provider/model while [State: Listed] holds, relying on the guard to catch it or letting it silently inherit the coordinator's route (omitting it under [State: Unlisted] or [State: Unmounted] is a genuine choice, not a violation).
|
|
75118
76568
|
5. Directly editing a module assigned to a subagent.
|
|
75119
76569
|
6. Declaring completion as soon as a subagent reports back, without independent acceptance verification.
|
|
75120
76570
|
7. Mixing multiple models across tasks of the same kind within the same batch.
|
|
@@ -75127,6 +76577,10 @@ Reset: fallback applies only to the current task. When the next task begins, alw
|
|
|
75127
76577
|
14. Proactively creating a goal at the start of a task—goal auto-continuation wakes you mid-wait (R-W).
|
|
75128
76578
|
15. Ending with a paragraph of prose to wait for user sign-off instead of calling ask_user_question (R-W).
|
|
75129
76579
|
16. Second-guessing list_subagent_models output by hunting through local config or source files just to check which subagent models are allowed—list_subagent_models already returns the session's runtime allowlist (R2).
|
|
76580
|
+
17. Shipping a defect you already named in the review, on the grounds that it is recorded in the summary—unrouted risk is not handled risk (R6).
|
|
76581
|
+
18. Writing a task brief with a range where a cap belongs ("2–5 files", "1–2 questions", "a few attempts")—the strictest reading wins and the budget shrinks to its lower bound (R0, R5).
|
|
76582
|
+
19. Asking a subagent to restate its rules as a correctness check instead of verifying the ambiguous ones with one tool call—restatement reproduces the misreading (R5).
|
|
76583
|
+
20. Reporting a route as "authorized", or claiming per-task routing is in force, when the session recorded no allowlist (R2 [State: Unlisted]).
|
|
75130
76584
|
`
|
|
75131
76585
|
}
|
|
75132
76586
|
},
|
|
@@ -75333,15 +76787,26 @@ Reset: fallback applies only to the current task. When the next task begins, alw
|
|
|
75333
76787
|
* `unknown prompt variable "{{cwd}}"` at render time. On 0.1.7–0.2.0 that package
|
|
75334
76788
|
* is absent and `{{cwd}}` is still the only way the persona states the workspace,
|
|
75335
76789
|
* so the unmodified {@link DISPATCH_PRESET} is returned unchanged.
|
|
76790
|
+
*
|
|
76791
|
+
* Newer harnesses also register `list_agents` from the main subagent-control entry
|
|
76792
|
+
* and remove its standalone `./list-agents` export. That row is omitted only when
|
|
76793
|
+
* the export is absent; older harnesses keep it because their main entry does not
|
|
76794
|
+
* register the tool.
|
|
75336
76795
|
* @param resolves - whether a bare specifier resolves in this installation.
|
|
75337
76796
|
* @returns the preset definition for the running harness generation.
|
|
75338
76797
|
*/
|
|
75339
76798
|
function resolveDispatchPreset(resolves = resolvesFromHere) {
|
|
75340
|
-
|
|
76799
|
+
const hasWorkingDirectory = resolves(WORKING_DIRECTORY_PACKAGE);
|
|
76800
|
+
const hasLegacyListAgents = resolves("@deepseek-ai/dsh-tool-subagent-control/list-agents");
|
|
76801
|
+
if (!hasWorkingDirectory && hasLegacyListAgents) return DISPATCH_PRESET;
|
|
75341
76802
|
return {
|
|
75342
76803
|
...DISPATCH_PRESET,
|
|
75343
76804
|
plugins: DISPATCH_PRESET.plugins.map((row) => {
|
|
75344
|
-
if (row.id
|
|
76805
|
+
if (row.id === "delegation" && !hasLegacyListAgents && Array.isArray(row.config)) return {
|
|
76806
|
+
...row,
|
|
76807
|
+
config: row.config.filter((nested) => nested.id !== "tool-subagent-list-agents")
|
|
76808
|
+
};
|
|
76809
|
+
if (row.id !== "persona" || !hasWorkingDirectory || typeof row.config !== "object" || row.config === null) return row;
|
|
75345
76810
|
const { suffix: _omitted, ...rest } = row.config;
|
|
75346
76811
|
return {
|
|
75347
76812
|
...row,
|
|
@@ -77743,6 +79208,7 @@ const Config = z.object({
|
|
|
77743
79208
|
reasoningCollapseGuard: z.union([z.object({}), z.const(false)]).default(false),
|
|
77744
79209
|
noAnswerResume: z.union([z.object({}), z.const(false)]).default({}),
|
|
77745
79210
|
codexStreamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).step(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS$2),
|
|
79211
|
+
poolStorage: z.union([z.const("keyring"), z.const("credentials")]).default("keyring").description("号池存储后端:keyring(默认,桌面平台的系统密钥环)或 credentials(DSH 凭据库,服务器推荐)。"),
|
|
77746
79212
|
progressUpdates: z.boolean().default(true).volatile().description("启用进展更新:允许 agent 用 progress_update 工具主动汇报多步任务的进展,并在侧栏展示。"),
|
|
77747
79213
|
progressUpdatesNudgeThreshold: z.number().step(1).min(1).max(50).default(8).volatile().description("催促阈值:连续这么多轮带工具调用却没有进展更新时,提醒 agent 发一条进展。"),
|
|
77748
79214
|
progressUpdatesLanguage: z.union([
|
|
@@ -77761,6 +79227,14 @@ const inject = [
|
|
|
77761
79227
|
];
|
|
77762
79228
|
function apply(ctx, pluginConfig = {}) {
|
|
77763
79229
|
const fetchConfiguration = resolveFetchConfiguration(pluginConfig);
|
|
79230
|
+
const poolStorageSettings = new PoolStorageSettings();
|
|
79231
|
+
poolStorageSettings.hydrate().catch(() => void 0);
|
|
79232
|
+
configurePoolStorage({
|
|
79233
|
+
mode: () => poolStorageSettings.effective(pluginConfig.poolStorage),
|
|
79234
|
+
ready: () => poolStorageSettings.hydrate().catch(() => void 0),
|
|
79235
|
+
credentials: () => asCredentialsSeam(ctx.get("credentials")),
|
|
79236
|
+
logger: ctx.logger
|
|
79237
|
+
});
|
|
77764
79238
|
if (pluginConfig.syncAgentPresets !== false) ctx.effect(() => installBundledPresets(ctx, installDispatchPreset(ctx)), "dsh-chatgpt-subscription: agent preset sync");
|
|
77765
79239
|
const store = createPlatformTokenStore();
|
|
77766
79240
|
const preferences = registerPreferenceStore(ctx.settings);
|
|
@@ -78132,7 +79606,7 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
78132
79606
|
}))
|
|
78133
79607
|
});
|
|
78134
79608
|
};
|
|
78135
|
-
const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, readRouteAudit, codexAccountPool, fetchConfiguration);
|
|
79609
|
+
const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, readRouteAudit, codexAccountPool, fetchConfiguration, { storage: poolStorageSettings });
|
|
78136
79610
|
const routable = (accounts) => accounts.some((account) => account.authStatus === void 0 || account.authStatus === "ok");
|
|
78137
79611
|
const disposeHubOverview = registerHubOverviewRoutes(ctx, [
|
|
78138
79612
|
{
|