@eddyskywalker/dsh-chatgpt-subscription 0.13.1 → 0.14.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 +42 -0
- package/README.md +75 -8
- package/lib/client.js +183 -115
- package/lib/client.js.map +1 -1
- package/lib/index.js +888 -115
- package/lib/types/client/CodexSubscriptionSection.d.ts +1 -1
- package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
- package/lib/types/client/ProviderHubSection.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +12 -0
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/claude/ClaudeSection.d.ts.map +1 -1
- package/lib/types/client/claude/locales.d.ts +2 -11
- package/lib/types/client/claude/locales.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +13 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/host/claude/model-catalog.d.ts +42 -20
- package/lib/types/host/claude/model-catalog.d.ts.map +1 -1
- package/lib/types/host/claude/types.d.ts +7 -2
- package/lib/types/host/claude/types.d.ts.map +1 -1
- package/lib/types/host/codex-account-pool.d.ts +129 -1
- package/lib/types/host/codex-account-pool.d.ts.map +1 -1
- package/lib/types/host/codex-adopt.d.ts +255 -0
- package/lib/types/host/codex-adopt.d.ts.map +1 -0
- package/lib/types/host/common/request-images.d.ts +22 -1
- package/lib/types/host/common/request-images.d.ts.map +1 -1
- package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
- package/lib/types/host/kimi-code/mapper.d.ts +22 -0
- package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
- package/lib/types/host/routes.d.ts +11 -1
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/host/token-store.d.ts +26 -1
- package/lib/types/host/token-store.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +22 -0
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -993,7 +993,7 @@ function parseStoredCredentials(value) {
|
|
|
993
993
|
if (typeof candidate !== "string") throw new Error(`${key} is invalid`);
|
|
994
994
|
return candidate;
|
|
995
995
|
};
|
|
996
|
-
|
|
996
|
+
const credentials = {
|
|
997
997
|
accessToken: record.accessToken,
|
|
998
998
|
refreshToken: record.refreshToken,
|
|
999
999
|
expiresAt: record.expiresAt,
|
|
@@ -1002,6 +1002,16 @@ function parseStoredCredentials(value) {
|
|
|
1002
1002
|
email: optional("email"),
|
|
1003
1003
|
planType: optional("planType")
|
|
1004
1004
|
};
|
|
1005
|
+
if (record.adopted === true) {
|
|
1006
|
+
const source = optional("source");
|
|
1007
|
+
if (source !== void 0) {
|
|
1008
|
+
credentials.adopted = true;
|
|
1009
|
+
credentials.source = source;
|
|
1010
|
+
const sourcePath = optional("sourcePath");
|
|
1011
|
+
if (sourcePath !== void 0) credentials.sourcePath = sourcePath;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return credentials;
|
|
1005
1015
|
}
|
|
1006
1016
|
//#endregion
|
|
1007
1017
|
//#region src/host/token-store-windows.ts
|
|
@@ -2895,7 +2905,433 @@ function poolQuota(fetchedAt, windows) {
|
|
|
2895
2905
|
};
|
|
2896
2906
|
}
|
|
2897
2907
|
//#endregion
|
|
2908
|
+
//#region src/host/codex-adopt.ts
|
|
2909
|
+
/**
|
|
2910
|
+
* OPT-IN adoption of a pre-existing Codex CLI sign-in.
|
|
2911
|
+
*
|
|
2912
|
+
* WHAT THIS IS FOR. A user who already runs the unmodified Codex CLI is already
|
|
2913
|
+
* signed in to the very subscription this provider serves. Asking them to sign in
|
|
2914
|
+
* a second time is pure friction, so this module offers the one thing that
|
|
2915
|
+
* removes it: reading back the credential the CLI itself stored in
|
|
2916
|
+
* `$CODEX_HOME/auth.json` (`~/.codex/auth.json` when the variable is unset). It
|
|
2917
|
+
* is opt-in — nothing here runs until the user asks for it.
|
|
2918
|
+
*
|
|
2919
|
+
* ---------------------------------------------------------------------------
|
|
2920
|
+
* RULE 1 — THIS MODULE NEVER WRITES. IT IS A READER, AND ONLY A READER.
|
|
2921
|
+
* ---------------------------------------------------------------------------
|
|
2922
|
+
* It must not create, modify, move or delete any file belonging to the Codex CLI.
|
|
2923
|
+
* No file this module touches is ever opened for writing, no directory is
|
|
2924
|
+
* created, nothing is renamed and nothing is unlinked. The guarantee is
|
|
2925
|
+
* structural rather than a promise in prose: the module imports exactly two
|
|
2926
|
+
* functions BY NAME from 'node:fs/promises' — readFile and stat — instead of the
|
|
2927
|
+
* whole namespace, so introducing a write would require a visible change to the
|
|
2928
|
+
* import list, which is the line a reviewer reads first. Nothing exported here
|
|
2929
|
+
* writes. The tests assert the stronger, observable form of the rule: a
|
|
2930
|
+
* credential file's bytes, size and mtime are identical before and after both
|
|
2931
|
+
* entry points run, and its directory gains no entries.
|
|
2932
|
+
*
|
|
2933
|
+
* ---------------------------------------------------------------------------
|
|
2934
|
+
* RULE 2 — PRESENCE AND CONTENT ARE TWO SEPARATE FUNCTIONS
|
|
2935
|
+
* ---------------------------------------------------------------------------
|
|
2936
|
+
* codexCliCredentialPresence() is cheap enough to call on plugin startup and
|
|
2937
|
+
* answers only yes/no: it stats the candidate and never opens it for reading. It
|
|
2938
|
+
* exists so the settings card can offer the import before the user has agreed to
|
|
2939
|
+
* it, and so a user who never opts in never has their token read at all.
|
|
2940
|
+
*
|
|
2941
|
+
* readCodexCliCredentials() is the full read, and it is called only AFTER the
|
|
2942
|
+
* user explicitly opts in. Keeping the two apart is the whole point: a startup
|
|
2943
|
+
* path that slurped the file "just in case" would make the opt-in decorative.
|
|
2944
|
+
*
|
|
2945
|
+
* ---------------------------------------------------------------------------
|
|
2946
|
+
* RULE 3 — AN ADOPTED CREDENTIAL IS A SNAPSHOT, AND THIS PLUGIN NEVER REFRESHES IT
|
|
2947
|
+
* ---------------------------------------------------------------------------
|
|
2948
|
+
* An adopted credential is used only while its own accessToken is still valid.
|
|
2949
|
+
* Once it is expired the pool reports it as expired and tells the user to sign in
|
|
2950
|
+
* again in the Codex CLI and re-adopt. That is deliberate, not a gap: the
|
|
2951
|
+
* ChatGPT refresh token the CLI stores ROTATES, and the CLI refreshes its own
|
|
2952
|
+
* file in place. Two processes refreshing the same grant therefore invalidate
|
|
2953
|
+
* each other — whoever loses the race holds a token the server has already
|
|
2954
|
+
* retired, and the user ends up signed OUT of the Codex CLI by this plugin's good
|
|
2955
|
+
* intentions. An in-process single-flight cannot fix that, because the race is
|
|
2956
|
+
* across PROCESSES: this plugin and the CLI are separate programs with separate
|
|
2957
|
+
* memories, and the only shared state between them is the file — which this
|
|
2958
|
+
* module must not write back into (Rule 1).
|
|
2959
|
+
*
|
|
2960
|
+
* So the honest contract is: borrow the access token for as long as the CLI itself
|
|
2961
|
+
* vouched for it, and never touch the refresh token. Expiry is therefore part of
|
|
2962
|
+
* the adoption decision rather than a detail — see
|
|
2963
|
+
* {@link isAdoptedCodexCredentialExpired}.
|
|
2964
|
+
*
|
|
2965
|
+
* The marker the pool chunk must honour is the literal 'adopted: true' plus
|
|
2966
|
+
* {@link isAdoptedCodexCredential}; an account carrying it must be treated as
|
|
2967
|
+
* read-only, non-refreshable and disposable. There is deliberately no refresh
|
|
2968
|
+
* function in this module to find in the first place.
|
|
2969
|
+
*
|
|
2970
|
+
* ---------------------------------------------------------------------------
|
|
2971
|
+
* RULE 4 — AN UNRECOGNISED SHAPE YIELDS undefined, NEVER AN EXCEPTION
|
|
2972
|
+
* ---------------------------------------------------------------------------
|
|
2973
|
+
* The card shows 'unrecognised local sign-in format' for undefined, which is a far
|
|
2974
|
+
* better outcome than a thrown error escaping into the settings route. Nothing is
|
|
2975
|
+
* guessed: a document in another auth_mode, a missing or non-object tokens
|
|
2976
|
+
* block, a missing access or refresh token, an access token that is not a JWT
|
|
2977
|
+
* with a usable 'exp', an unreadable or unparsable file — all of them are
|
|
2978
|
+
* undefined. Extra and unknown fields are ignored rather than rejected, so a
|
|
2979
|
+
* future CLI that adds one does not break adoption.
|
|
2980
|
+
*
|
|
2981
|
+
* THE TRAP THIS PARSER EXISTS TO AVOID. A Codex auth.json is NOT a flat bag of
|
|
2982
|
+
* tokens. It is a document that may carry an API key alongside the OAuth tokens,
|
|
2983
|
+
* and a "find the first accessToken anywhere in the JSON" scan would pick
|
|
2984
|
+
* whichever one the serializer happened to emit first — which is how an API key
|
|
2985
|
+
* or an unrelated token ends up presented to the Responses API as the user's
|
|
2986
|
+
* subscription credential. The read is therefore a direct, specific lookup of
|
|
2987
|
+
* tokens.access_token and its siblings — never a tree walk — and the test suite
|
|
2988
|
+
* holds a fixture carrying an unrelated token beside the real one and asserts
|
|
2989
|
+
* which comes back.
|
|
2990
|
+
*
|
|
2991
|
+
* ---------------------------------------------------------------------------
|
|
2992
|
+
* THE CLAIMS ARE DERIVED LOCALLY, NEVER ASKED OF ANYBODY
|
|
2993
|
+
* ---------------------------------------------------------------------------
|
|
2994
|
+
* expiresAt comes from the access token's own 'exp' claim, email and planType
|
|
2995
|
+
* from the id token's claims. The JWT payload is decoded in process; no token is
|
|
2996
|
+
* ever sent anywhere to be inspected, and no token value is logged or returned
|
|
2997
|
+
* from anything but the credential object the caller asked for.
|
|
2998
|
+
*
|
|
2999
|
+
* WHY EXPIRY IS REFUSED RATHER THAN DEFAULTED. A snapshot whose validity cannot
|
|
3000
|
+
* be established must not be used. Rather than stamping "now" or "never" and
|
|
3001
|
+
* hoping, this module yields undefined: the card then says the local sign-in
|
|
3002
|
+
* format is unrecognised, which is true, instead of importing a credential that
|
|
3003
|
+
* would 401 on the first request.
|
|
3004
|
+
*/
|
|
3005
|
+
/**
|
|
3006
|
+
* Environment variable that relocates the Codex CLI's home directory.
|
|
3007
|
+
*
|
|
3008
|
+
* Highest precedence, and honoured exactly as the CLI honours it: when it is set
|
|
3009
|
+
* to a non-blank value it names the home that holds auth.json, whatever the
|
|
3010
|
+
* platform's conventional location happens to be. This is also what makes the
|
|
3011
|
+
* whole module testable without touching a developer's real profile.
|
|
3012
|
+
*/
|
|
3013
|
+
const CODEX_HOME_ENV = "CODEX_HOME";
|
|
3014
|
+
/** File name of the credential document inside the Codex home. */
|
|
3015
|
+
const CODEX_CLI_CREDENTIAL_FILE = "auth.json";
|
|
3016
|
+
/**
|
|
3017
|
+
* Top-level key holding the OAuth tokens.
|
|
3018
|
+
*
|
|
3019
|
+
* Named as a constant because the specificity of this key IS the defence
|
|
3020
|
+
* described in the module comment: the read addresses this key, not the first
|
|
3021
|
+
* token it can find anywhere in the document.
|
|
3022
|
+
*/
|
|
3023
|
+
const CODEX_CLI_TOKENS_KEY = "tokens";
|
|
3024
|
+
/** Where an adopted snapshot came from, for the card and for the pool record. */
|
|
3025
|
+
const CODEX_CLI_CREDENTIAL_SOURCE = "codex";
|
|
3026
|
+
/** Namespace the id token carries the ChatGPT account claims under. */
|
|
3027
|
+
const CODEX_AUTH_CLAIMS_KEY = "https://api.openai.com/auth";
|
|
3028
|
+
/**
|
|
3029
|
+
* What the user is told when an adopted credential has aged out.
|
|
3030
|
+
*
|
|
3031
|
+
* Exported so the pool report and the settings card say the same sentence, and
|
|
3032
|
+
* so the instruction is stated once: the re-sign-in happens in the CODEX CLI, not
|
|
3033
|
+
* here, and the import has to be repeated afterwards.
|
|
3034
|
+
*/
|
|
3035
|
+
const ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT = "The sign-in imported from the Codex CLI has expired. Sign in again in the Codex CLI, then import it again from this settings card.";
|
|
3036
|
+
function isRecord$31(value) {
|
|
3037
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3038
|
+
}
|
|
3039
|
+
/** A trimmed, non-empty string, or nothing. */
|
|
3040
|
+
function nonEmptyString$5(value) {
|
|
3041
|
+
if (typeof value !== "string") return void 0;
|
|
3042
|
+
const trimmed = value.trim();
|
|
3043
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* The Codex CLI's home directory.
|
|
3047
|
+
*
|
|
3048
|
+
* The environment override wins whenever it is set to a non-blank value — a blank
|
|
3049
|
+
* or whitespace-only variable is treated as unset rather than as "the directory
|
|
3050
|
+
* whose name is empty", which on Windows would resolve to a drive-relative path.
|
|
3051
|
+
* Otherwise the conventional home/.codex directory is used, os.homedir() being
|
|
3052
|
+
* the same %USERPROFILE% on Windows and ~ on POSIX that the CLI itself uses.
|
|
3053
|
+
*
|
|
3054
|
+
* Read at CALL time rather than captured in a module constant: a test (or an
|
|
3055
|
+
* embedder) that sets the variable after import must see the change.
|
|
3056
|
+
*/
|
|
3057
|
+
function codexHomeDir() {
|
|
3058
|
+
const override = process.env[CODEX_HOME_ENV]?.trim();
|
|
3059
|
+
if (override) return override;
|
|
3060
|
+
return path.join(os.homedir(), ".codex");
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Every path this module will look at, most authoritative first.
|
|
3064
|
+
*
|
|
3065
|
+
* A single candidate, which is a judgement call stated plainly. The Claude reader
|
|
3066
|
+
* in this package also falls back to the conventional home when the environment
|
|
3067
|
+
* override points somewhere else; here the override IS the user's own statement of
|
|
3068
|
+
* where this CLI keeps its sign-in, and honouring a stale scratch directory is
|
|
3069
|
+
* more dangerous than reporting nothing: a snapshot belonging to a profile the CLI
|
|
3070
|
+
* is not currently using would be adopted silently. The path actually consulted is
|
|
3071
|
+
* returned and printed by the caller, so "not found" is always accompanied by the
|
|
3072
|
+
* exact place that was looked at.
|
|
3073
|
+
*
|
|
3074
|
+
* A pure function — no filesystem access at all — so it is safe to call just to
|
|
3075
|
+
* render the card, and so its precedence is testable without touching a disk.
|
|
3076
|
+
*/
|
|
3077
|
+
function codexCliCredentialPaths() {
|
|
3078
|
+
return [path.join(codexHomeDir(), CODEX_CLI_CREDENTIAL_FILE)];
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Whether a plain file exists at one path.
|
|
3082
|
+
*
|
|
3083
|
+
* stat, not lstat: a credential file reached through a symlink is a legitimate
|
|
3084
|
+
* layout — a user may keep profiles behind links — and the question asked here is
|
|
3085
|
+
* whether the client has a file there, not how it is reached.
|
|
3086
|
+
*
|
|
3087
|
+
* Any error (ENOENT, EACCES, ENOTDIR, an unsupported path shape) answers "no". A
|
|
3088
|
+
* failure to establish existence is not evidence of existence, and this path runs
|
|
3089
|
+
* on startup where throwing would take the plugin down over a missing file.
|
|
3090
|
+
*/
|
|
3091
|
+
async function existsAsFile$1(candidate) {
|
|
3092
|
+
try {
|
|
3093
|
+
return (await stat(candidate)).isFile();
|
|
3094
|
+
} catch {
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Is there a local Codex CLI sign-in to offer?
|
|
3100
|
+
*
|
|
3101
|
+
* Safe on plugin startup: this NEVER opens a candidate for reading, so a user who
|
|
3102
|
+
* never opts in never has the credential's contents touched — not by this
|
|
3103
|
+
* function, not by the route that renders the card. Only stat runs, which reveals
|
|
3104
|
+
* existence and timestamps and nothing else.
|
|
3105
|
+
*
|
|
3106
|
+
* Deliberately NOT reported here: whether the file is valid, expired, or even
|
|
3107
|
+
* JSON. Answering that would require the read this function exists to avoid, and
|
|
3108
|
+
* the card does not need it — validity is answered by the read, after opt-in, and
|
|
3109
|
+
* lands on the card as either a credential or 'unrecognised local sign-in
|
|
3110
|
+
* format'.
|
|
3111
|
+
*
|
|
3112
|
+
* A boolean, rather than the record the Claude reader returns, because the two
|
|
3113
|
+
* callers want different things and neither needs the other's shape: the status
|
|
3114
|
+
* route wants a flag, and reports the path it consulted beside it. paths
|
|
3115
|
+
* is injectable so tests drive it against fixtures instead of a real profile;
|
|
3116
|
+
* production callers take the default.
|
|
3117
|
+
*/
|
|
3118
|
+
async function codexCliCredentialPresence(paths = codexCliCredentialPaths()) {
|
|
3119
|
+
for (const candidate of paths) if (await existsAsFile$1(candidate)) return true;
|
|
3120
|
+
return false;
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* The claims of a JWT, decoded in process, or nothing.
|
|
3124
|
+
*
|
|
3125
|
+
* The payload is the second base64url segment; the signature is never touched and
|
|
3126
|
+
* the result is used only for the three non-secret facts below. Never sent
|
|
3127
|
+
* anywhere: a token handed to a decoder service is a token handed away.
|
|
3128
|
+
*
|
|
3129
|
+
* No verification is claimed and none is possible here — the signing key is the
|
|
3130
|
+
* issuer's, and this module has no business holding it. What it does instead is
|
|
3131
|
+
* refuse to invent: a token that does not decode is treated as stating nothing.
|
|
3132
|
+
*/
|
|
3133
|
+
function decodeJwtClaims(token) {
|
|
3134
|
+
const parts = token.split(".");
|
|
3135
|
+
if (parts.length !== 3) return void 0;
|
|
3136
|
+
try {
|
|
3137
|
+
const value = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
3138
|
+
return isRecord$31(value) ? value : void 0;
|
|
3139
|
+
} catch {
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* The access token's expiry, in EPOCH MILLISECONDS, or nothing.
|
|
3145
|
+
*
|
|
3146
|
+
* RFC 7519 states 'exp' in SECONDS since the epoch, and the store's
|
|
3147
|
+
* StoredOAuthCredentials states milliseconds — a thousand-fold difference that is
|
|
3148
|
+
* silent when it goes the wrong way (a credential stamped a thousand times too
|
|
3149
|
+
* late reads as unexpired for centuries) and catastrophic the other. The
|
|
3150
|
+
* conversion is therefore done here, once, with a comment on it.
|
|
3151
|
+
*
|
|
3152
|
+
* Only a finite, non-negative number is accepted. A string, null, NaN, a
|
|
3153
|
+
* negative value or a missing claim are all "this document states no usable
|
|
3154
|
+
* expiry", and the caller's answer to that is to refuse the whole document rather
|
|
3155
|
+
* than import a credential whose validity nothing established.
|
|
3156
|
+
*/
|
|
3157
|
+
function expiryFromAccessToken(accessToken) {
|
|
3158
|
+
const claims = decodeJwtClaims(accessToken);
|
|
3159
|
+
if (claims === void 0) return void 0;
|
|
3160
|
+
const exp = claims.exp;
|
|
3161
|
+
if (typeof exp !== "number" || !Number.isFinite(exp) || exp < 0) return void 0;
|
|
3162
|
+
return Math.round(exp * 1e3);
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Map one parsed credential document onto the store's credential shape.
|
|
3166
|
+
*
|
|
3167
|
+
* Pure and exported so the format rules can be tested without a filesystem, and
|
|
3168
|
+
* so a later chunk that reads the document from somewhere else (a backup, a pasted
|
|
3169
|
+
* blob) reuses the one set of rules instead of growing a second, drifting copy.
|
|
3170
|
+
*
|
|
3171
|
+
* Returns undefined for every shape it does not recognise, and never throws — see
|
|
3172
|
+
* Rule 4 in the module comment. The gate is deliberately narrow: the document must
|
|
3173
|
+
* be a JSON object in the ChatGPT auth mode, its tokens block must be an object,
|
|
3174
|
+
* and it must carry BOTH a non-empty access token and a non-empty refresh token.
|
|
3175
|
+
* An access token alone is not adopted because the store's record is a sign-in,
|
|
3176
|
+
* not a bare token, and a record with an empty one could never be re-recognised.
|
|
3177
|
+
*
|
|
3178
|
+
* The unrelated-token trap is handled by construction: value.tokens is addressed
|
|
3179
|
+
* directly and no other key of the document is ever traversed, so whatever else
|
|
3180
|
+
* the file carries cannot reach the result.
|
|
3181
|
+
*/
|
|
3182
|
+
function parseAdoptedCodexCredential(value, sourcePath) {
|
|
3183
|
+
if (!isRecord$31(value)) return void 0;
|
|
3184
|
+
if (value.auth_mode !== "chatgpt") return void 0;
|
|
3185
|
+
const tokens = value[CODEX_CLI_TOKENS_KEY];
|
|
3186
|
+
if (!isRecord$31(tokens)) return void 0;
|
|
3187
|
+
const accessToken = nonEmptyString$5(tokens.access_token);
|
|
3188
|
+
if (accessToken === void 0) return void 0;
|
|
3189
|
+
const refreshToken = nonEmptyString$5(tokens.refresh_token);
|
|
3190
|
+
if (refreshToken === void 0) return void 0;
|
|
3191
|
+
const expiresAt = expiryFromAccessToken(accessToken);
|
|
3192
|
+
if (expiresAt === void 0) return void 0;
|
|
3193
|
+
const credentials = {
|
|
3194
|
+
accessToken,
|
|
3195
|
+
refreshToken,
|
|
3196
|
+
expiresAt
|
|
3197
|
+
};
|
|
3198
|
+
const idToken = nonEmptyString$5(tokens.id_token);
|
|
3199
|
+
if (idToken !== void 0) {
|
|
3200
|
+
credentials.idToken = idToken;
|
|
3201
|
+
const claims = decodeJwtClaims(idToken);
|
|
3202
|
+
const email = claims === void 0 ? void 0 : nonEmptyString$5(claims.email);
|
|
3203
|
+
if (email !== void 0) credentials.email = email;
|
|
3204
|
+
const authClaims = claims === void 0 ? void 0 : claims[CODEX_AUTH_CLAIMS_KEY];
|
|
3205
|
+
const planType = isRecord$31(authClaims) ? nonEmptyString$5(authClaims.chatgpt_plan_type) : void 0;
|
|
3206
|
+
if (planType !== void 0) credentials.planType = planType;
|
|
3207
|
+
}
|
|
3208
|
+
const accountId = nonEmptyString$5(tokens.account_id);
|
|
3209
|
+
if (accountId !== void 0) credentials.accountId = accountId;
|
|
3210
|
+
return {
|
|
3211
|
+
adopted: true,
|
|
3212
|
+
source: CODEX_CLI_CREDENTIAL_SOURCE,
|
|
3213
|
+
sourcePath,
|
|
3214
|
+
credentials
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Read the local Codex CLI sign-in; the first usable candidate wins.
|
|
3219
|
+
*
|
|
3220
|
+
* ONLY call this after the user has explicitly opted in — see the module comment.
|
|
3221
|
+
*
|
|
3222
|
+
* Never throws and never rejects: an absent file, an unreadable file, a file that
|
|
3223
|
+
* is not JSON, and a JSON document of an unrecognised shape all produce undefined,
|
|
3224
|
+
* which the card renders as 'unrecognised local sign-in format'. A settings route
|
|
3225
|
+
* must not be able to fail because a user's file was mid-write.
|
|
3226
|
+
*/
|
|
3227
|
+
async function readCodexCliCredentials(paths = codexCliCredentialPaths()) {
|
|
3228
|
+
for (const candidate of paths) try {
|
|
3229
|
+
const raw = await readFile(candidate, "utf8");
|
|
3230
|
+
const adopted = parseAdoptedCodexCredential(JSON.parse(raw), candidate);
|
|
3231
|
+
if (adopted !== void 0) return adopted;
|
|
3232
|
+
} catch {}
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Whether an adopted credential's access token is already unusable.
|
|
3236
|
+
*
|
|
3237
|
+
* A structural mirror of the expiry test the OAuth module applies to its own
|
|
3238
|
+
* credentials (expiresAt - Date.now() > 0), asked of a pooled credential so the
|
|
3239
|
+
* pool can refuse it WITHOUT ever reaching for a refresh this module deliberately
|
|
3240
|
+
* does not perform. A non-finite expiresAt counts as expired, matching the
|
|
3241
|
+
* refusal in the reader: no established validity, no use.
|
|
3242
|
+
*
|
|
3243
|
+
* now is injectable so a test can reason about a fixed clock.
|
|
3244
|
+
*/
|
|
3245
|
+
function isAdoptedCodexCredentialExpired(credential, now = Date.now()) {
|
|
3246
|
+
const expiresAt = credential.expiresAt;
|
|
3247
|
+
return !(typeof expiresAt === "number" && Number.isFinite(expiresAt) && expiresAt > now);
|
|
3248
|
+
}
|
|
3249
|
+
//#endregion
|
|
2898
3250
|
//#region src/host/codex-account-pool.ts
|
|
3251
|
+
/**
|
|
3252
|
+
* The ChatGPT account pool.
|
|
3253
|
+
*
|
|
3254
|
+
* TWO KINDS OF ACCOUNT, AND ONLY ONE OF THEM MAY BE REFRESHED.
|
|
3255
|
+
*
|
|
3256
|
+
* - managed — signed in through this plugin. Its credential lives in the pool's
|
|
3257
|
+
* encrypted storage AND in the pre-pool credential document beside it, it is
|
|
3258
|
+
* refreshable, and deleting it is this plugin's business.
|
|
3259
|
+
* - adopted — a SNAPSHOT read out of a local Codex CLI sign-in by 'codex-adopt.ts'
|
|
3260
|
+
* (marker 'adopted: true', source 'codex'). It is used only while its own access
|
|
3261
|
+
* token is still valid.
|
|
3262
|
+
*
|
|
3263
|
+
* An adopted snapshot is NEVER refreshed, and the reason is not an optimisation:
|
|
3264
|
+
* the ChatGPT refresh token the CLI stores rotates, and the CLI refreshes its own
|
|
3265
|
+
* file in place. Two processes spending one grant invalidate each other, and
|
|
3266
|
+
* whoever loses the race holds a token the server has already retired — which
|
|
3267
|
+
* leaves the user signed OUT of the Codex CLI by this plugin's good intentions. An
|
|
3268
|
+
* in-process single-flight cannot fix that, because the race is across PROCESSES
|
|
3269
|
+
* and the only state they share is a file 'codex-adopt.ts' must never write. Once
|
|
3270
|
+
* such a snapshot has expired the pool takes it out of rotation and surfaces
|
|
3271
|
+
* ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT — sign in again in the CODEX CLI and
|
|
3272
|
+
* re-import, not here.
|
|
3273
|
+
*
|
|
3274
|
+
* The rule is enforced twice on purpose: the needsRefresh hook answers false for
|
|
3275
|
+
* an adopted credential no matter what its expiry says, and the refresh hook
|
|
3276
|
+
* itself refuses one, so a future caller that forgets the first fails loudly
|
|
3277
|
+
* instead of silently spending a grant that is not ours to spend.
|
|
3278
|
+
*
|
|
3279
|
+
* MANAGED BEATS ADOPTED on a conflict, and the marker is therefore written on
|
|
3280
|
+
* EVERY account this file creates, including the explicit false case: the core
|
|
3281
|
+
* merges a re-authorization as { ...existing, ...created }, so an omitted key
|
|
3282
|
+
* would leave a stored 'adopted: true' — and its never-refresh consequence — on a
|
|
3283
|
+
* record that is now plugin-owned.
|
|
3284
|
+
*/
|
|
3285
|
+
/**
|
|
3286
|
+
* Whether a pooled credential is an adopted snapshot.
|
|
3287
|
+
*
|
|
3288
|
+
* The two fields, and only those two: a plain credential carries no 'adopted'
|
|
3289
|
+
* field and must never be mistaken for a snapshot, because treating a
|
|
3290
|
+
* plugin-owned account as a disposable one would freeze it out of refreshing and
|
|
3291
|
+
* make deleteAccount refuse to remove an account the user signed in here.
|
|
3292
|
+
*/
|
|
3293
|
+
function isAdoptedCodexPoolCredential(credentials) {
|
|
3294
|
+
return credentials.adopted === true && credentials.source === "codex";
|
|
3295
|
+
}
|
|
3296
|
+
/**
|
|
3297
|
+
* Whether a credential must be refreshed before the next request.
|
|
3298
|
+
*
|
|
3299
|
+
* FALSE for every adopted snapshot, valid or expired. That is this pool's core
|
|
3300
|
+
* rule: an adopted credential is borrowed for exactly as long as the Codex CLI
|
|
3301
|
+
* vouched for it, and this pool has no business rotating a token another process
|
|
3302
|
+
* is rotating too. See the module comment.
|
|
3303
|
+
*/
|
|
3304
|
+
function codexNeedsRefresh(credentials, now) {
|
|
3305
|
+
if (isAdoptedCodexPoolCredential(credentials)) return false;
|
|
3306
|
+
return credentials.expiresAt - now <= TOKEN_REFRESH_MARGIN_MS;
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Why a stored credential is already known to be unusable, or nothing.
|
|
3310
|
+
*
|
|
3311
|
+
* Reported purely — nothing is written here — and consulted by the core for both
|
|
3312
|
+
* routing eligibility and credential usability, which is what makes an expired
|
|
3313
|
+
* snapshot non-routable WITHOUT a write on every read. The hint names the remedy
|
|
3314
|
+
* in the Codex CLI, because that is where the user has to go: re-adopting here is
|
|
3315
|
+
* only possible after the CLI itself signed in again.
|
|
3316
|
+
*/
|
|
3317
|
+
function codexAuthRejectedReason(credentials) {
|
|
3318
|
+
if (!isAdoptedCodexPoolCredential(credentials)) return void 0;
|
|
3319
|
+
return isAdoptedCodexCredentialExpired(credentials) ? ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT : void 0;
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Message raised if a refresh of a still-valid adopted snapshot were attempted.
|
|
3323
|
+
*
|
|
3324
|
+
* Unreachable through this pool's own paths (see the module comment); it exists
|
|
3325
|
+
* because 'unreachable' is a claim the guard has to be able to make good on.
|
|
3326
|
+
*/
|
|
3327
|
+
const ADOPTED_NEVER_REFRESHED_MESSAGE$1 = "The sign-in imported from the Codex CLI is never refreshed by this plugin: the Codex CLI refreshes the same rotating token, and two refreshers would invalidate each other. Sign in again in the Codex CLI, then import the new sign-in from this settings card.";
|
|
3328
|
+
/** Raised when a refresh of an adopted snapshot is attempted. Never a 401. */
|
|
3329
|
+
var CodexAdoptedCredentialError = class extends Error {
|
|
3330
|
+
constructor(message) {
|
|
3331
|
+
super(message);
|
|
3332
|
+
this.name = "CodexAdoptedCredentialError";
|
|
3333
|
+
}
|
|
3334
|
+
};
|
|
2899
3335
|
/** Encrypted pool file the ChatGPT line owns; the single-credential store stays the mirror. */
|
|
2900
3336
|
function codexPoolPath() {
|
|
2901
3337
|
return path.join(dshHomeDir(), "storages", "codex-pool.json");
|
|
@@ -2916,13 +3352,18 @@ function parseCodexPoolData(value) {
|
|
|
2916
3352
|
const raw = item;
|
|
2917
3353
|
if (typeof raw.id !== "string") continue;
|
|
2918
3354
|
const credentials = parseStoredCredentials(raw.credentials);
|
|
3355
|
+
const adopted = isAdoptedCodexPoolCredential(credentials);
|
|
2919
3356
|
const account = {
|
|
2920
3357
|
id: raw.id,
|
|
2921
3358
|
alias: typeof raw.alias === "string" ? raw.alias : credentials.email || "账号",
|
|
2922
3359
|
credentials,
|
|
2923
3360
|
addedAt: typeof raw.addedAt === "number" ? raw.addedAt : Date.now(),
|
|
2924
|
-
isPrimary: raw.isPrimary === true
|
|
3361
|
+
isPrimary: raw.isPrimary === true,
|
|
3362
|
+
adopted,
|
|
3363
|
+
source: adopted ? CODEX_CLI_CREDENTIAL_SOURCE : "managed"
|
|
2925
3364
|
};
|
|
3365
|
+
const sourcePath = optionalString$9(raw, "sourcePath") ?? credentials.sourcePath;
|
|
3366
|
+
if (adopted && sourcePath !== void 0) account.sourcePath = sourcePath;
|
|
2926
3367
|
const email = optionalString$9(raw, "email") ?? credentials.email;
|
|
2927
3368
|
if (email !== void 0) account.email = email;
|
|
2928
3369
|
const planType = optionalString$9(raw, "planType") ?? credentials.planType;
|
|
@@ -2945,14 +3386,6 @@ function parseCodexPoolData(value) {
|
|
|
2945
3386
|
return result;
|
|
2946
3387
|
}
|
|
2947
3388
|
/**
|
|
2948
|
-
* ChatGPT's account pool.
|
|
2949
|
-
*
|
|
2950
|
-
* Thin wrapper over {@link AccountPoolCore}: it only supplies the credential
|
|
2951
|
-
* shape, the identity a duplicate sign-in collapses onto, and the two provider
|
|
2952
|
-
* hooks the core cannot know — how to refresh one account's tokens and which
|
|
2953
|
-
* accounts a cached quota window already rules out.
|
|
2954
|
-
*/
|
|
2955
|
-
/**
|
|
2956
3389
|
* Per-account in-flight requests, shared by every ChatGPT request this host
|
|
2957
3390
|
* makes. A fan-out of subagents is what reaches a plan's concurrency bound, so
|
|
2958
3391
|
* the bound is enforced here rather than discovered by cooling every account at
|
|
@@ -2997,23 +3430,31 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
2997
3430
|
parsePoolData: parseCodexPoolData,
|
|
2998
3431
|
dedupeKey: (credentials) => credentials.accountId ?? credentials.email,
|
|
2999
3432
|
defaultAlias: (credentials, position) => credentials.email ?? `账号 ${position}`,
|
|
3000
|
-
createAccount: ({ id, alias, credentials, addedAt, isPrimary }) =>
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3433
|
+
createAccount: ({ id, alias, credentials, addedAt, isPrimary }) => {
|
|
3434
|
+
const adopted = isAdoptedCodexPoolCredential(credentials);
|
|
3435
|
+
return {
|
|
3436
|
+
id,
|
|
3437
|
+
alias,
|
|
3438
|
+
credentials,
|
|
3439
|
+
addedAt,
|
|
3440
|
+
isPrimary,
|
|
3441
|
+
adopted,
|
|
3442
|
+
source: adopted ? CODEX_CLI_CREDENTIAL_SOURCE : "managed",
|
|
3443
|
+
sourcePath: adopted ? credentials.sourcePath : void 0,
|
|
3444
|
+
...credentials.email === void 0 ? {} : { email: credentials.email },
|
|
3445
|
+
...credentials.planType === void 0 ? {} : { planType: credentials.planType },
|
|
3446
|
+
...credentials.accountId === void 0 ? {} : { accountId: credentials.accountId }
|
|
3447
|
+
};
|
|
3448
|
+
},
|
|
3010
3449
|
expiresAt: (credentials) => credentials.expiresAt,
|
|
3011
|
-
needsRefresh: (credentials, now) => credentials
|
|
3450
|
+
needsRefresh: (credentials, now) => codexNeedsRefresh(credentials, now),
|
|
3012
3451
|
refresh: async (credentials) => {
|
|
3452
|
+
if (isAdoptedCodexPoolCredential(credentials)) throw new CodexAdoptedCredentialError(isAdoptedCodexCredentialExpired(credentials) ? ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT : ADOPTED_NEVER_REFRESHED_MESSAGE$1);
|
|
3013
3453
|
const refresher = ref.current;
|
|
3014
3454
|
if (refresher === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
|
|
3015
3455
|
return refresher.refreshAccount(credentials);
|
|
3016
3456
|
},
|
|
3457
|
+
authRejectedReason: (credentials) => codexAuthRejectedReason(credentials),
|
|
3017
3458
|
refreshFailureStatus: (error) => error instanceof OAuthServiceError && (error.status === 400 || error.status === 401) ? "expired" : void 0,
|
|
3018
3459
|
legacyAccount: async () => {
|
|
3019
3460
|
const stored = await store.load();
|
|
@@ -3024,6 +3465,8 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
3024
3465
|
credentials: stored,
|
|
3025
3466
|
addedAt: Date.now(),
|
|
3026
3467
|
isPrimary: true,
|
|
3468
|
+
adopted: false,
|
|
3469
|
+
source: "managed",
|
|
3027
3470
|
...stored.email === void 0 ? {} : { email: stored.email },
|
|
3028
3471
|
...stored.planType === void 0 ? {} : { planType: stored.planType },
|
|
3029
3472
|
...stored.accountId === void 0 ? {} : { accountId: stored.accountId }
|
|
@@ -3034,17 +3477,30 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
3034
3477
|
await store.clear();
|
|
3035
3478
|
return;
|
|
3036
3479
|
}
|
|
3480
|
+
if (isAdoptedCodexPoolCredential(credentials)) return;
|
|
3037
3481
|
await store.save(credentials);
|
|
3038
3482
|
},
|
|
3039
|
-
extendSummary: (account, base) =>
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3483
|
+
extendSummary: (account, base) => {
|
|
3484
|
+
const adopted = account.adopted === true;
|
|
3485
|
+
const expired = adopted && isAdoptedCodexCredentialExpired(account.credentials);
|
|
3486
|
+
return {
|
|
3487
|
+
...base,
|
|
3488
|
+
...account.email === void 0 ? {} : { email: account.email },
|
|
3489
|
+
...account.planType === void 0 ? {} : { planLabel: account.planType },
|
|
3490
|
+
adopted,
|
|
3491
|
+
source: account.source,
|
|
3492
|
+
removable: !adopted,
|
|
3493
|
+
...account.sourcePath === void 0 ? {} : { sourcePath: account.sourcePath },
|
|
3494
|
+
...(() => {
|
|
3495
|
+
const quota = quotaSnapshotRef.current?.(account);
|
|
3496
|
+
return quota === void 0 ? {} : { quota };
|
|
3497
|
+
})(),
|
|
3498
|
+
...expired ? {
|
|
3499
|
+
authStatus: "expired",
|
|
3500
|
+
authFailedReason: ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT
|
|
3501
|
+
} : {}
|
|
3502
|
+
};
|
|
3503
|
+
},
|
|
3048
3504
|
emptyMessage: "未登录 ChatGPT 账号,请在「设置 → 订阅服务 → ChatGPT」中添加并登录账号。",
|
|
3049
3505
|
...options.backend === void 0 ? {} : { backend: options.backend },
|
|
3050
3506
|
...options.maxAccounts === void 0 ? {} : { maxAccounts: options.maxAccounts }
|
|
@@ -3098,6 +3554,35 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
3098
3554
|
if (this.refresherRef.current === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
|
|
3099
3555
|
return this.renewCredential(accountId, fetch, account.credentials);
|
|
3100
3556
|
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Remove one account inside a transaction, refusing a borrowed snapshot.
|
|
3559
|
+
*
|
|
3560
|
+
* The check lives HERE rather than in the public delete method so it cannot be
|
|
3561
|
+
* separated from the removal by a concurrent change, and so every caller of the
|
|
3562
|
+
* core's own deletion inherits it. The remedy is a separate action
|
|
3563
|
+
* ({@link removeImportedAccount}): a card must not destroy an account this plugin
|
|
3564
|
+
* did not create with the same button that deletes its own.
|
|
3565
|
+
*/
|
|
3566
|
+
deleteAccountFromPool(data, accountId) {
|
|
3567
|
+
if (data.accounts.find((account) => account.id === accountId)?.adopted === true) throw new Error("The sign-in imported from the Codex CLI is not an account this plugin owns; remove the import instead.");
|
|
3568
|
+
super.deleteAccountFromPool(data, accountId);
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Drop an imported snapshot from the pool — the card's 'remove import' action.
|
|
3572
|
+
*
|
|
3573
|
+
* Separate from {@link deleteAccount} because that refuses one, and because the
|
|
3574
|
+
* user is asking to stop borrowing a sign-in, not to sign out of an account. The
|
|
3575
|
+
* Codex CLI's own file is touched by neither path: not deleted, not rewritten,
|
|
3576
|
+
* not moved. This only forgets the copy.
|
|
3577
|
+
*/
|
|
3578
|
+
async removeImportedAccount(accountId) {
|
|
3579
|
+
return this.updatePool((data) => {
|
|
3580
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
3581
|
+
if (target === void 0) return;
|
|
3582
|
+
if (target.adopted !== true) throw new Error("That account was signed in through this plugin; delete it instead of removing an import.");
|
|
3583
|
+
super.deleteAccountFromPool(data, accountId);
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3101
3586
|
/** The single-credential store this pool mirrors its primary account into. */
|
|
3102
3587
|
mirrorStore() {
|
|
3103
3588
|
return this.store;
|
|
@@ -7147,12 +7632,16 @@ function identityKey(credentials) {
|
|
|
7147
7632
|
//#endregion
|
|
7148
7633
|
//#region src/host/routes.ts
|
|
7149
7634
|
const MAX_BODY_BYTES$6 = 64 * 1024;
|
|
7150
|
-
function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, routeAudit, accountPool, fetchConfiguration) {
|
|
7635
|
+
function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, routeAudit, accountPool, fetchConfiguration, adopt) {
|
|
7636
|
+
const adoptPaths = adopt?.adoptPaths;
|
|
7151
7637
|
const handler = async (request, response) => {
|
|
7152
7638
|
const url = new URL(request.url ?? "/", "http://dsh.local");
|
|
7153
7639
|
if (request.method === "GET" && url.pathname === `/api/dsh-chatgpt-subscription/status`) {
|
|
7154
7640
|
const oauthStatus = await oauth.status();
|
|
7155
7641
|
const quota = await usage.status(oauthStatus.authenticated, false, { backgroundRefresh: true });
|
|
7642
|
+
const codexCliPaths = adoptPaths ?? codexCliCredentialPaths();
|
|
7643
|
+
const codexCliSignInAvailable = await codexCliCredentialPresence(codexCliPaths).catch(() => false);
|
|
7644
|
+
const codexCliSignInPath = codexCliPaths[0];
|
|
7156
7645
|
json(response, {
|
|
7157
7646
|
ok: true,
|
|
7158
7647
|
value: {
|
|
@@ -7160,6 +7649,8 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
7160
7649
|
quota,
|
|
7161
7650
|
quotaRefreshing: usage.refreshing,
|
|
7162
7651
|
preferences: preferences.status(),
|
|
7652
|
+
codexCliSignInAvailable,
|
|
7653
|
+
codexCliSignInPath,
|
|
7163
7654
|
detectedProxy: proxyManager?.getSystemProxy() ?? null,
|
|
7164
7655
|
activeProxy: proxyManager?.resolveActiveProxyUrl() ?? null,
|
|
7165
7656
|
switcher: searchSwitcher?.status() ?? null,
|
|
@@ -7238,6 +7729,14 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
7238
7729
|
});
|
|
7239
7730
|
return;
|
|
7240
7731
|
}
|
|
7732
|
+
const refreshedStatus = async () => {
|
|
7733
|
+
const afterAction = await oauth.status();
|
|
7734
|
+
return {
|
|
7735
|
+
...afterAction,
|
|
7736
|
+
quota: await usage.status(afterAction.authenticated),
|
|
7737
|
+
preferences: preferences.status()
|
|
7738
|
+
};
|
|
7739
|
+
};
|
|
7241
7740
|
try {
|
|
7242
7741
|
switch (url.pathname) {
|
|
7243
7742
|
case `${ROUTE_PREFIX$5}/login/start`:
|
|
@@ -7267,14 +7766,60 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
7267
7766
|
else if (action === "clear-cooldown" && accountId !== null) await accountPool.clearCooldown(accountId);
|
|
7268
7767
|
else if (action === "strategy" && (body.strategy === "sequential" || body.strategy === "round-robin" || body.strategy === "sticky")) await accountPool.setStrategy(body.strategy);
|
|
7269
7768
|
else if (action === "relogin" && accountId !== null) await accountPool.clearAuthFailed(accountId);
|
|
7270
|
-
const afterAction = await oauth.status();
|
|
7271
7769
|
json(response, {
|
|
7272
7770
|
ok: true,
|
|
7273
|
-
value:
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7771
|
+
value: await refreshedStatus()
|
|
7772
|
+
});
|
|
7773
|
+
return;
|
|
7774
|
+
}
|
|
7775
|
+
case `${ROUTE_PREFIX$5}/adopt`: {
|
|
7776
|
+
if (accountPool === void 0) {
|
|
7777
|
+
jsonError(response, 400, {
|
|
7778
|
+
code: "bad-request",
|
|
7779
|
+
message: "Importing a local Codex CLI sign-in needs the account pool, which is not installed."
|
|
7780
|
+
});
|
|
7781
|
+
return;
|
|
7782
|
+
}
|
|
7783
|
+
if (field(body, "source") !== "codex") {
|
|
7784
|
+
jsonError(response, 400, {
|
|
7785
|
+
code: "bad-request",
|
|
7786
|
+
message: "That local sign-in is not imported here. Claude Code and MiniMax Code are imported from their own provider settings."
|
|
7787
|
+
});
|
|
7788
|
+
return;
|
|
7789
|
+
}
|
|
7790
|
+
const adopted = await readCodexCliCredentials(adoptPaths ?? codexCliCredentialPaths());
|
|
7791
|
+
if (adopted === void 0) {
|
|
7792
|
+
jsonError(response, 400, {
|
|
7793
|
+
code: "bad-request",
|
|
7794
|
+
message: "No usable local Codex CLI sign-in was found (unrecognised local sign-in format)."
|
|
7795
|
+
});
|
|
7796
|
+
return;
|
|
7797
|
+
}
|
|
7798
|
+
await accountPool.addAccount({
|
|
7799
|
+
...adopted.credentials,
|
|
7800
|
+
adopted: true,
|
|
7801
|
+
source: CODEX_CLI_CREDENTIAL_SOURCE,
|
|
7802
|
+
sourcePath: adopted.sourcePath
|
|
7803
|
+
});
|
|
7804
|
+
json(response, {
|
|
7805
|
+
ok: true,
|
|
7806
|
+
value: await refreshedStatus()
|
|
7807
|
+
});
|
|
7808
|
+
return;
|
|
7809
|
+
}
|
|
7810
|
+
case `${ROUTE_PREFIX$5}/adopt/disable`: {
|
|
7811
|
+
if (accountPool === void 0) {
|
|
7812
|
+
jsonError(response, 400, {
|
|
7813
|
+
code: "bad-request",
|
|
7814
|
+
message: "The ChatGPT account pool is not installed."
|
|
7815
|
+
});
|
|
7816
|
+
return;
|
|
7817
|
+
}
|
|
7818
|
+
const imported = ((await accountPool.read().catch(() => null))?.accounts ?? []).filter((account) => account.adopted === true);
|
|
7819
|
+
for (const account of imported) await accountPool.removeImportedAccount(account.id);
|
|
7820
|
+
json(response, {
|
|
7821
|
+
ok: true,
|
|
7822
|
+
value: await refreshedStatus()
|
|
7278
7823
|
});
|
|
7279
7824
|
return;
|
|
7280
7825
|
}
|
|
@@ -9272,17 +9817,17 @@ function base64Length$4(bytes) {
|
|
|
9272
9817
|
return Math.ceil(bytes / 3) * 4;
|
|
9273
9818
|
}
|
|
9274
9819
|
/** Request payload one inline image block occupies, when it can be measured. */
|
|
9275
|
-
function requestImageBytes$
|
|
9820
|
+
function requestImageBytes$3(block) {
|
|
9276
9821
|
const attachment = attachmentOf$5(block);
|
|
9277
9822
|
if (attachment) return base64Length$4(attachment.bytes);
|
|
9278
9823
|
const inline = asString$17(block.data) || asString$17(block.base64);
|
|
9279
9824
|
return inline ? inline.length : void 0;
|
|
9280
9825
|
}
|
|
9281
|
-
function collectRequestImageBytes$
|
|
9826
|
+
function collectRequestImageBytes$3(content, lengths) {
|
|
9282
9827
|
if (!Array.isArray(content)) return;
|
|
9283
9828
|
for (const block of content) {
|
|
9284
9829
|
if (!isRecord$27(block) || block.type !== "image") continue;
|
|
9285
|
-
const bytes = requestImageBytes$
|
|
9830
|
+
const bytes = requestImageBytes$3(block);
|
|
9286
9831
|
if (bytes !== void 0) lengths.push(bytes);
|
|
9287
9832
|
}
|
|
9288
9833
|
}
|
|
@@ -9301,7 +9846,7 @@ function collectRequestImageBytes$4(content, lengths) {
|
|
|
9301
9846
|
*/
|
|
9302
9847
|
function offloadOldestRequestImages$4(options) {
|
|
9303
9848
|
const lengths = [];
|
|
9304
|
-
for (const message of options.messages) collectRequestImageBytes$
|
|
9849
|
+
for (const message of options.messages) collectRequestImageBytes$3(message.content, lengths);
|
|
9305
9850
|
const excess = lengths.reduce((sum, bytes) => sum + bytes, 0) - MAX_REQUEST_IMAGE_BYTES$4;
|
|
9306
9851
|
if (excess <= 0) return options;
|
|
9307
9852
|
let omitted = 0;
|
|
@@ -9317,7 +9862,7 @@ function offloadOldestRequestImages$4(options) {
|
|
|
9317
9862
|
let replaced = false;
|
|
9318
9863
|
const content = message.content.map((block) => {
|
|
9319
9864
|
if (remaining.count === 0 || !isRecord$27(block) || block.type !== "image") return block;
|
|
9320
|
-
if (requestImageBytes$
|
|
9865
|
+
if (requestImageBytes$3(block) === void 0) return block;
|
|
9321
9866
|
remaining.count -= 1;
|
|
9322
9867
|
replaced = true;
|
|
9323
9868
|
return {
|
|
@@ -13216,17 +13761,17 @@ function collectImageRefs$3(content, refs) {
|
|
|
13216
13761
|
function base64Length$3(bytes) {
|
|
13217
13762
|
return Math.ceil(bytes / 3) * 4;
|
|
13218
13763
|
}
|
|
13219
|
-
function requestImageBytes$
|
|
13764
|
+
function requestImageBytes$2(block) {
|
|
13220
13765
|
const attachment = attachmentOf$4(block);
|
|
13221
13766
|
if (attachment) return base64Length$3(attachment.bytes);
|
|
13222
13767
|
const inline = asString$15(block.data) || asString$15(block.base64);
|
|
13223
13768
|
return inline ? inline.length : void 0;
|
|
13224
13769
|
}
|
|
13225
|
-
function collectRequestImageBytes$
|
|
13770
|
+
function collectRequestImageBytes$2(content, lengths) {
|
|
13226
13771
|
if (!Array.isArray(content)) return;
|
|
13227
13772
|
for (const block of content) {
|
|
13228
13773
|
if (!isRecord$25(block) || block.type !== "image") continue;
|
|
13229
|
-
const bytes = requestImageBytes$
|
|
13774
|
+
const bytes = requestImageBytes$2(block);
|
|
13230
13775
|
if (bytes !== void 0) lengths.push(bytes);
|
|
13231
13776
|
}
|
|
13232
13777
|
}
|
|
@@ -13237,7 +13782,7 @@ function collectRequestImageBytes$3(content, lengths) {
|
|
|
13237
13782
|
*/
|
|
13238
13783
|
function offloadOldestRequestImages$3(options) {
|
|
13239
13784
|
const lengths = [];
|
|
13240
|
-
for (const message of options.messages) collectRequestImageBytes$
|
|
13785
|
+
for (const message of options.messages) collectRequestImageBytes$2(message.content, lengths);
|
|
13241
13786
|
const excess = lengths.reduce((sum, bytes) => sum + bytes, 0) - MAX_REQUEST_IMAGE_BYTES$3;
|
|
13242
13787
|
if (excess <= 0) return options;
|
|
13243
13788
|
let omitted = 0;
|
|
@@ -13253,7 +13798,7 @@ function offloadOldestRequestImages$3(options) {
|
|
|
13253
13798
|
let replaced = false;
|
|
13254
13799
|
const content = message.content.map((block) => {
|
|
13255
13800
|
if (remaining.count === 0 || !isRecord$25(block) || block.type !== "image") return block;
|
|
13256
|
-
if (requestImageBytes$
|
|
13801
|
+
if (requestImageBytes$2(block) === void 0) return block;
|
|
13257
13802
|
remaining.count -= 1;
|
|
13258
13803
|
replaced = true;
|
|
13259
13804
|
return {
|
|
@@ -20612,19 +21157,135 @@ function collectImageRefs$2(content, refs) {
|
|
|
20612
21157
|
function base64Length$2(bytes) {
|
|
20613
21158
|
return Math.ceil(bytes / 3) * 4;
|
|
20614
21159
|
}
|
|
20615
|
-
|
|
21160
|
+
/**
|
|
21161
|
+
* Base64 payload one image block contributes to the wire, or undefined when it
|
|
21162
|
+
* contributes none.
|
|
21163
|
+
*
|
|
21164
|
+
* This is the length of what is SENT, which is not the length of what is
|
|
21165
|
+
* STORED. Kimi's own client downsamples and re-encodes an oversized image before
|
|
21166
|
+
* delivery "to avoid the supplier erroring on an oversized image", and this route
|
|
21167
|
+
* does the same through `requestImageTarget`: a 5 MB screenshot reaches the
|
|
21168
|
+
* wire as a ~256 KiB request version. Measuring `attachment.bytes` therefore
|
|
21169
|
+
* over-counts every image that was scaled, and an image that fits comfortably
|
|
21170
|
+
* after scaling was reported as several megabytes over budget and dropped.
|
|
21171
|
+
*
|
|
21172
|
+
* With `images` supplied the measurement is exact. Without it the stored size is
|
|
21173
|
+
* the only estimate available, so it stays the fallback and every call site that
|
|
21174
|
+
* has no resolved map behaves exactly as before.
|
|
21175
|
+
*/
|
|
21176
|
+
function requestImagePayloadLength(block, images) {
|
|
21177
|
+
let data = asString$11(block.data) || asString$11(block.base64);
|
|
21178
|
+
const source = isRecord$19(block.source) ? block.source : void 0;
|
|
21179
|
+
if (!data && source) data = asString$11(source.data) || asString$11(source.base64);
|
|
21180
|
+
if (data?.startsWith("data:")) {
|
|
21181
|
+
const matched = data.match(/^data:([^;,]+);base64,(.*)$/s);
|
|
21182
|
+
if (matched) data = matched[2] ?? "";
|
|
21183
|
+
}
|
|
21184
|
+
if (data) return data.length;
|
|
20616
21185
|
const attachment = attachmentOf$3(block);
|
|
20617
|
-
if (attachment) return
|
|
20618
|
-
|
|
20619
|
-
|
|
21186
|
+
if (!attachment) return void 0;
|
|
21187
|
+
if (images === void 0) return base64Length$2(attachment.bytes);
|
|
21188
|
+
const resolved = images.get(attachment.attachmentId);
|
|
21189
|
+
return resolved?.kind === "inline" ? resolved.data.length : void 0;
|
|
20620
21190
|
}
|
|
20621
|
-
|
|
21191
|
+
/**
|
|
21192
|
+
* Collect every image occurrence in model request order, recursing into tool
|
|
21193
|
+
* results the way `collectImageRefs` does.
|
|
21194
|
+
*
|
|
21195
|
+
* RECURSION IS NOT OPTIONAL. A `read_image` result or a screenshot inside a tool
|
|
21196
|
+
* result carries its pixels to the wire — `toolResultBlocks` sends them — so an
|
|
21197
|
+
* earlier version that only looked at top-level blocks under-counted exactly the
|
|
21198
|
+
* images a tool-using session produces most of. The budget has to describe the
|
|
21199
|
+
* body that is actually built.
|
|
21200
|
+
*/
|
|
21201
|
+
function collectRequestImagePayloads(content, images, lengths) {
|
|
20622
21202
|
if (!Array.isArray(content)) return;
|
|
20623
21203
|
for (const block of content) {
|
|
20624
|
-
if (!isRecord$19(block)
|
|
20625
|
-
|
|
20626
|
-
|
|
21204
|
+
if (!isRecord$19(block)) continue;
|
|
21205
|
+
if (block.type === "image") {
|
|
21206
|
+
const bytes = requestImagePayloadLength(block, images);
|
|
21207
|
+
if (bytes !== void 0) lengths.push(bytes);
|
|
21208
|
+
continue;
|
|
21209
|
+
}
|
|
21210
|
+
if (block.type === "tool-result") collectRequestImagePayloads(block.content, images, lengths);
|
|
21211
|
+
}
|
|
21212
|
+
}
|
|
21213
|
+
/**
|
|
21214
|
+
* Wire-level base64 payload of each image occurrence one request carries, in the
|
|
21215
|
+
* order the request sends them, so index N is the Nth oldest image.
|
|
21216
|
+
*/
|
|
21217
|
+
function requestImagePayloadLengths(options, images) {
|
|
21218
|
+
const lengths = [];
|
|
21219
|
+
for (const message of options.messages) collectRequestImagePayloads(message.content, images, lengths);
|
|
21220
|
+
return lengths;
|
|
21221
|
+
}
|
|
21222
|
+
/** How many of the oldest occurrences must go to bring the total under `maxBytes`. */
|
|
21223
|
+
function imagesToOffloadCount(lengths, maxBytes) {
|
|
21224
|
+
const excess = lengths.reduce((sum, bytes) => sum + bytes, 0) - maxBytes;
|
|
21225
|
+
if (excess <= 0) return 0;
|
|
21226
|
+
let freed = 0;
|
|
21227
|
+
let omitted = 0;
|
|
21228
|
+
for (const bytes of lengths) {
|
|
21229
|
+
if (freed >= excess) break;
|
|
21230
|
+
freed += bytes;
|
|
21231
|
+
omitted += 1;
|
|
20627
21232
|
}
|
|
21233
|
+
return omitted;
|
|
21234
|
+
}
|
|
21235
|
+
/**
|
|
21236
|
+
* Replace the first `count` image occurrences with a visible placeholder,
|
|
21237
|
+
* recursing into tool results. Durable history is untouched; only the request
|
|
21238
|
+
* about to be sent changes.
|
|
21239
|
+
*/
|
|
21240
|
+
function replaceOldestRequestImages(options, count) {
|
|
21241
|
+
if (count <= 0) return options;
|
|
21242
|
+
const remaining = { count };
|
|
21243
|
+
const replaceIn = (content) => {
|
|
21244
|
+
if (!Array.isArray(content) || remaining.count === 0) return {
|
|
21245
|
+
content,
|
|
21246
|
+
replaced: false
|
|
21247
|
+
};
|
|
21248
|
+
let replaced = false;
|
|
21249
|
+
return {
|
|
21250
|
+
content: content.map((block) => {
|
|
21251
|
+
if (remaining.count === 0 || !isRecord$19(block)) return block;
|
|
21252
|
+
if (block.type === "image") {
|
|
21253
|
+
if (requestImagePayloadLength(block) === void 0) return block;
|
|
21254
|
+
remaining.count -= 1;
|
|
21255
|
+
replaced = true;
|
|
21256
|
+
return {
|
|
21257
|
+
type: "text",
|
|
21258
|
+
text: OMITTED_IMAGE_TEXT$2
|
|
21259
|
+
};
|
|
21260
|
+
}
|
|
21261
|
+
if (block.type === "tool-result" && Array.isArray(block.content)) {
|
|
21262
|
+
const inner = replaceIn(block.content);
|
|
21263
|
+
if (!inner.replaced) return block;
|
|
21264
|
+
replaced = true;
|
|
21265
|
+
return {
|
|
21266
|
+
...block,
|
|
21267
|
+
content: inner.content
|
|
21268
|
+
};
|
|
21269
|
+
}
|
|
21270
|
+
return block;
|
|
21271
|
+
}),
|
|
21272
|
+
replaced
|
|
21273
|
+
};
|
|
21274
|
+
};
|
|
21275
|
+
let changed = false;
|
|
21276
|
+
const messages = options.messages.map((message) => {
|
|
21277
|
+
const result = replaceIn(message.content);
|
|
21278
|
+
if (!result.replaced) return message;
|
|
21279
|
+
changed = true;
|
|
21280
|
+
return {
|
|
21281
|
+
...message,
|
|
21282
|
+
content: result.content
|
|
21283
|
+
};
|
|
21284
|
+
});
|
|
21285
|
+
return changed ? {
|
|
21286
|
+
...options,
|
|
21287
|
+
messages
|
|
21288
|
+
} : options;
|
|
20628
21289
|
}
|
|
20629
21290
|
/**
|
|
20630
21291
|
* Longest edge any image of this request may keep.
|
|
@@ -20684,41 +21345,22 @@ function requestImageTarget(ref, edge = REQUEST_IMAGE_MAX_EDGE) {
|
|
|
20684
21345
|
*
|
|
20685
21346
|
* Durable history is untouched; only the request about to be sent changes.
|
|
20686
21347
|
*/
|
|
20687
|
-
function offloadOldestRequestImages$2(options, maxBytes = MAX_REQUEST_IMAGE_BYTES$2) {
|
|
20688
|
-
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
20692
|
-
|
|
20693
|
-
|
|
20694
|
-
|
|
20695
|
-
|
|
20696
|
-
|
|
20697
|
-
|
|
20698
|
-
|
|
20699
|
-
|
|
20700
|
-
|
|
20701
|
-
|
|
20702
|
-
let replaced = false;
|
|
20703
|
-
const content = message.content.map((block) => {
|
|
20704
|
-
if (remaining.count === 0 || !isRecord$19(block) || block.type !== "image") return block;
|
|
20705
|
-
if (requestImageBytes$2(block) === void 0) return block;
|
|
20706
|
-
remaining.count -= 1;
|
|
20707
|
-
replaced = true;
|
|
20708
|
-
return {
|
|
20709
|
-
type: "text",
|
|
20710
|
-
text: OMITTED_IMAGE_TEXT$2
|
|
20711
|
-
};
|
|
20712
|
-
});
|
|
20713
|
-
return replaced ? {
|
|
20714
|
-
...message,
|
|
20715
|
-
content
|
|
20716
|
-
} : message;
|
|
21348
|
+
function offloadOldestRequestImages$2(options, maxBytes = MAX_REQUEST_IMAGE_BYTES$2, images) {
|
|
21349
|
+
return replaceOldestRequestImages(options, imagesToOffloadCount(requestImagePayloadLengths(options, images), maxBytes));
|
|
21350
|
+
}
|
|
21351
|
+
/**
|
|
21352
|
+
* Whether a request still carries at least one image the mapper can send.
|
|
21353
|
+
*
|
|
21354
|
+
* A request whose images have all been replaced by placeholders can no longer
|
|
21355
|
+
* shrink, which is what separates "drop more images" from "this body is too
|
|
21356
|
+
* large for a reason no image budget can fix".
|
|
21357
|
+
*/
|
|
21358
|
+
function requestHasSendableImage(options) {
|
|
21359
|
+
return options.messages.some((message) => {
|
|
21360
|
+
const content = message.content;
|
|
21361
|
+
if (!Array.isArray(content)) return false;
|
|
21362
|
+
return content.some((block) => isRecord$19(block) && block.type === "image");
|
|
20717
21363
|
});
|
|
20718
|
-
return {
|
|
20719
|
-
...options,
|
|
20720
|
-
messages
|
|
20721
|
-
};
|
|
20722
21364
|
}
|
|
20723
21365
|
/**
|
|
20724
21366
|
* Read every durable `{ type: 'image', attachment }` block one request carries.
|
|
@@ -21917,7 +22559,48 @@ function assertRequestBodyFits$1(body, carriesVideo = false) {
|
|
|
21917
22559
|
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
21918
22560
|
const limit = carriesVideo ? MAX_VIDEO_MESSAGE_BODY_BYTES : MAX_MESSAGE_BODY_BYTES;
|
|
21919
22561
|
if (bytes <= limit) return serialized;
|
|
21920
|
-
throw new LlmError$1(
|
|
22562
|
+
throw new LlmError$1(oversizedBodyMessage(bytes, limit, body), "PROVIDER_ERROR");
|
|
22563
|
+
}
|
|
22564
|
+
function walkJson(value, visit) {
|
|
22565
|
+
if (Array.isArray(value)) {
|
|
22566
|
+
for (const item of value) walkJson(item, visit);
|
|
22567
|
+
return;
|
|
22568
|
+
}
|
|
22569
|
+
if (!isRecord$17(value)) return;
|
|
22570
|
+
visit(value);
|
|
22571
|
+
for (const nested of Object.values(value)) walkJson(nested, visit);
|
|
22572
|
+
}
|
|
22573
|
+
/**
|
|
22574
|
+
* Measure one already-serialized body by composition.
|
|
22575
|
+
*
|
|
22576
|
+
* Image payload is found structurally (a base64 `data:` URL or an Anthropic
|
|
22577
|
+
* base64 source) rather than by searching for a marker substring, so a user
|
|
22578
|
+
* message that happens to contain that text cannot inflate the image share.
|
|
22579
|
+
*/
|
|
22580
|
+
function requestBodyBreakdown(body, carriesVideo = false) {
|
|
22581
|
+
const serialized = JSON.stringify(body);
|
|
22582
|
+
const totalBytes = Buffer.byteLength(serialized, "utf8");
|
|
22583
|
+
let imageBytes = 0;
|
|
22584
|
+
walkJson(body, (node) => {
|
|
22585
|
+
const url = isRecord$17(node.image_url) ? asString$9(node.image_url.url) : void 0;
|
|
22586
|
+
if (url?.startsWith("data:")) {
|
|
22587
|
+
imageBytes += Buffer.byteLength(url, "utf8");
|
|
22588
|
+
return;
|
|
22589
|
+
}
|
|
22590
|
+
const source = isRecord$17(node.source) ? node.source : void 0;
|
|
22591
|
+
if (source && asString$9(source.data) !== void 0 && asString$9(source.media_type) !== void 0) imageBytes += Buffer.byteLength(asString$9(source.data) ?? "", "utf8");
|
|
22592
|
+
});
|
|
22593
|
+
const toolSchemaBytes = Buffer.byteLength(JSON.stringify(body.tools ?? []), "utf8");
|
|
22594
|
+
return {
|
|
22595
|
+
totalBytes,
|
|
22596
|
+
imageBytes,
|
|
22597
|
+
toolSchemaBytes,
|
|
22598
|
+
otherBytes: Math.max(0, totalBytes - imageBytes - toolSchemaBytes)
|
|
22599
|
+
};
|
|
22600
|
+
}
|
|
22601
|
+
function oversizedBodyMessage(bytes, limit, body) {
|
|
22602
|
+
const parts = requestBodyBreakdown(body);
|
|
22603
|
+
return `Kimi Code rejected the request before sending: the serialized body is ${bytes} bytes, above the ${limit}-byte limit this route enforces. Composition: ${`images ${parts.imageBytes} bytes, tool schemas ${parts.toolSchemaBytes} bytes, conversation text and system prompt ${parts.otherBytes} bytes`}. ${parts.imageBytes >= parts.otherBytes ? "Older images are omitted first; drop or re-attach fewer images, or start a new session." : "The conversation text and tool schemas dominate, so no image budget can fix this: compact the conversation or start a new session."}`;
|
|
21921
22604
|
}
|
|
21922
22605
|
function createStreamState$3(wire, sessionId) {
|
|
21923
22606
|
return {
|
|
@@ -22509,6 +23192,34 @@ function assertStreamComplete$3(state) {
|
|
|
22509
23192
|
//#endregion
|
|
22510
23193
|
//#region src/host/kimi-code/adapter.ts
|
|
22511
23194
|
/**
|
|
23195
|
+
* Rebuild attempts allowed while shrinking a request into its body ceiling.
|
|
23196
|
+
*
|
|
23197
|
+
* Each attempt drops at least one image, so the loop is bounded by the number of
|
|
23198
|
+
* images the request carries; this cap only exists so a future change that fails
|
|
23199
|
+
* to make progress fails fast instead of spinning.
|
|
23200
|
+
*/
|
|
23201
|
+
const MAX_BODY_FIT_ATTEMPTS = 64;
|
|
23202
|
+
/**
|
|
23203
|
+
* Drop the fewest oldest images that free at least `excessBytes`.
|
|
23204
|
+
*
|
|
23205
|
+
* `current` is the options the over-large body was built from, and `resolved`
|
|
23206
|
+
* supplies the post-scaling payload length of each occurrence, so the arithmetic
|
|
23207
|
+
* is made of the bytes the wire actually carries rather than of stored sizes.
|
|
23208
|
+
*/
|
|
23209
|
+
function dropOldestImagesUntilFits(current, resolved, excessBytes, baseline) {
|
|
23210
|
+
const lengths = requestImagePayloadLengths(baseline, resolved);
|
|
23211
|
+
const alreadyDropped = countOmitted(current, baseline);
|
|
23212
|
+
const additional = imagesToOffloadCount(lengths.slice(alreadyDropped), excessBytes);
|
|
23213
|
+
if (additional === 0) return current;
|
|
23214
|
+
return replaceOldestRequestImages(current, additional);
|
|
23215
|
+
}
|
|
23216
|
+
/** How many image occurrences `current` has already had replaced, versus `baseline`. */
|
|
23217
|
+
function countOmitted(current, baseline) {
|
|
23218
|
+
const before = requestImagePayloadLengths(baseline).length;
|
|
23219
|
+
const after = requestImagePayloadLengths(current).length;
|
|
23220
|
+
return Math.max(0, before - after);
|
|
23221
|
+
}
|
|
23222
|
+
/**
|
|
22512
23223
|
* Transient-failure retry policy for the `kimi-code` route.
|
|
22513
23224
|
*
|
|
22514
23225
|
* Kimi Code fronts the model providers, so a call can fail with a 502/503/504
|
|
@@ -22846,8 +23557,10 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
22846
23557
|
const catalog = await this.catalog().catch(() => []);
|
|
22847
23558
|
const wire = wireForCatalogEntry(options.model, catalog);
|
|
22848
23559
|
const entry = catalog.find((model) => model.id === options.model);
|
|
22849
|
-
const
|
|
22850
|
-
const
|
|
23560
|
+
const normalized = normalizeGenerateOptions(options);
|
|
23561
|
+
const images = await resolveRequestImages$2(normalized, this.options.attachments, signal);
|
|
23562
|
+
const requestOptions = offloadOldestRequestVideos(offloadOldestRequestImages$2(normalized, MAX_REQUEST_IMAGE_BYTES$2, images), MAX_REQUEST_VIDEO_BYTES$1);
|
|
23563
|
+
const videos = await resolveRequestVideos(requestOptions, this.options.videos, signal);
|
|
22851
23564
|
const settings = await this.settings();
|
|
22852
23565
|
const media = {
|
|
22853
23566
|
videos,
|
|
@@ -22859,10 +23572,25 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
22859
23572
|
const requestedMax = options.maxTokens ?? maxOutputTokensFor$3(options.model, contextWindow);
|
|
22860
23573
|
const inputCap = maxInputTokensForEntry(options.model, catalog);
|
|
22861
23574
|
const promptLimit = inputCap === void 0 ? contextWindow : Math.min(contextWindow, inputCap);
|
|
22862
|
-
const
|
|
23575
|
+
const boundedOptions = {
|
|
22863
23576
|
...requestOptions,
|
|
22864
23577
|
maxTokens: clampOutputToContext$1(requestedMax, promptLimit, estimatedInputTokens(requestOptions))
|
|
22865
|
-
}
|
|
23578
|
+
};
|
|
23579
|
+
const carriesVideo = requestHasVideo(requestOptions);
|
|
23580
|
+
const bodyLimit = carriesVideo ? MAX_VIDEO_MESSAGE_BODY_BYTES : MAX_MESSAGE_BODY_BYTES;
|
|
23581
|
+
let buildOptions = boundedOptions;
|
|
23582
|
+
let body = "";
|
|
23583
|
+
for (let attempt = 0;; attempt++) {
|
|
23584
|
+
const built = buildRequest(buildOptions, wire, images, void 0, media);
|
|
23585
|
+
body = JSON.stringify(built);
|
|
23586
|
+
const bytes = Buffer.byteLength(body, "utf8");
|
|
23587
|
+
if (bytes <= bodyLimit) break;
|
|
23588
|
+
if (attempt > MAX_BODY_FIT_ATTEMPTS || !requestHasSendableImage(buildOptions)) {
|
|
23589
|
+
assertRequestBodyFits$1(built, carriesVideo);
|
|
23590
|
+
throw new Error("unreachable: assertRequestBodyFits returns or throws");
|
|
23591
|
+
}
|
|
23592
|
+
buildOptions = dropOldestImagesUntilFits(buildOptions, images, bytes - bodyLimit, requestOptions);
|
|
23593
|
+
}
|
|
22866
23594
|
const pool = this.accountPool;
|
|
22867
23595
|
const tried = /* @__PURE__ */ new Set();
|
|
22868
23596
|
let servedByAccountId;
|
|
@@ -33791,14 +34519,19 @@ const ANTHROPIC_VERSION = "2023-06-01";
|
|
|
33791
34519
|
* table, whether or not a floor for that model has been observed: upstream has
|
|
33792
34520
|
* so far gated a new model on the release that added it (Opus 5.5 arrived in
|
|
33793
34521
|
* 2.1.280 and is refused below 2.1.280). Claude Sonnet 5.5 arrived in 2.1.284,
|
|
33794
|
-
* so 2.1.283 could not be relied on to reach it
|
|
34522
|
+
* so 2.1.283 could not be relied on to reach it. Claude Haiku 5.5 arrived in
|
|
34523
|
+
* 2.1.293, so 2.1.292 could not be relied on to reach it either — and that is
|
|
34524
|
+
* not hypothetical: when this row was added, npm's `stable` dist-tag was STILL
|
|
34525
|
+
* sitting at 2.1.285, five releases below the one that ships the model, while
|
|
34526
|
+
* `latest` was already 2.1.293. The warning above about reading `stable` is
|
|
34527
|
+
* therefore about this table's own newest model, not a hypothetical.
|
|
33795
34528
|
*
|
|
33796
34529
|
* It is still not a literal that only moves when this package is republished: a
|
|
33797
34530
|
* user whose installed baseline has aged past the server's floor has to be able
|
|
33798
34531
|
* to raise it without waiting for a release. Read the effective value with
|
|
33799
34532
|
* {@link claudeCliVersion}, pin one with {@link setClaudeCliVersion}.
|
|
33800
34533
|
*/
|
|
33801
|
-
const CLAUDE_CLI_VERSION = "2.1.
|
|
34534
|
+
const CLAUDE_CLI_VERSION = "2.1.293";
|
|
33802
34535
|
/**
|
|
33803
34536
|
* Loopback callback port the subscription flow defaults to.
|
|
33804
34537
|
*
|
|
@@ -34159,10 +34892,11 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
|
|
|
34159
34892
|
* TWO KINDS OF ROW ARE NOT FULLY TRANSCRIBED, and they are different things.
|
|
34160
34893
|
* Do not collapse them.
|
|
34161
34894
|
*
|
|
34162
|
-
* 1. `claude-sonnet-5-5` — the snapshot this table
|
|
34163
|
-
* entry for
|
|
34164
|
-
* the vendor's own documentation and no
|
|
34165
|
-
* mirror list in the test is
|
|
34895
|
+
* 1. `claude-sonnet-5-5` and `claude-haiku-5-5` — the snapshot this table
|
|
34896
|
+
* was copied from has no entry for either, so each whole row is CURATED:
|
|
34897
|
+
* every value below comes from the vendor's own documentation and no
|
|
34898
|
+
* snapshot field can check it. The mirror list in the test is
|
|
34899
|
+
* `LOCALLY_CURATED_MODEL_IDS`.
|
|
34166
34900
|
*
|
|
34167
34901
|
* 2. `claude-opus-5-5` — a NEWER snapshot (pi-ai >= 0.87.1) does carry it, so
|
|
34168
34902
|
* the row sits in the snapshot's own position and EVERY field is checked
|
|
@@ -34175,22 +34909,35 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
|
|
|
34175
34909
|
* stands. The test asserts the row still disagrees with the snapshot, so a
|
|
34176
34910
|
* future snapshot that agrees fails loudly and retires the entry.
|
|
34177
34911
|
*
|
|
34178
|
-
*
|
|
34179
|
-
*
|
|
34180
|
-
* is
|
|
34181
|
-
*
|
|
34912
|
+
* 3. `claude-haiku-5-5` (released 2026-10-07) is the second fully curated
|
|
34913
|
+
* row, and the first one whose documented `canDisableThinking` is TRUE. That
|
|
34914
|
+
* flag is not a free choice here and the row's own comment records why the
|
|
34915
|
+
* documented answer is safe to send: `thinking: { type: 'disabled' }` is
|
|
34916
|
+
* accepted at `low`/`medium`/`high` and is a 400 at `xhigh`/`max`, and this
|
|
34917
|
+
* line only sends that form when the caller asked for thinking OFF, in which
|
|
34918
|
+
* case it names no effort at all and the server's own default (`medium`) is
|
|
34919
|
+
* what the request runs at. Its documented default effort is MEDIUM, so it is
|
|
34920
|
+
* 'adaptive' rather than 'mid-convo' for exactly the reason Opus 5.5 is —
|
|
34921
|
+
* 'mid-convo' would force `high` — and it runs the preserved-thinking prefix
|
|
34922
|
+
* check, so `bindsThinkingToPrefix` is set even though the form is adaptive.
|
|
34923
|
+
*
|
|
34924
|
+
* All three must be changed together with the mirror list in the test. An
|
|
34925
|
+
* invented row that mimics the format of a checked one is worse than a missing
|
|
34926
|
+
* row: it is unverifiable and it looks verified. So each row is marked at the
|
|
34927
|
+
* row itself, the test asserts the id is on the curated list, and the test
|
|
34182
34928
|
* asserts the curated list is exactly the ids the snapshot lacks — which keeps
|
|
34183
34929
|
* the lock narrow instead of merely weaker.
|
|
34184
34930
|
*
|
|
34185
|
-
*
|
|
34186
|
-
* (the model overview page plus the extended-thinking
|
|
34187
|
-
* snapshot, and they are the values a person read off
|
|
34931
|
+
* `claude-sonnet-5-5`'s fields come from the vendor's own published
|
|
34932
|
+
* documentation for the model (the model overview page plus the extended-thinking
|
|
34933
|
+
* effort page), NOT from the snapshot, and they are the values a person read off
|
|
34934
|
+
* those pages. That is a
|
|
34188
34935
|
* weaker source than the snapshot and it is labelled as such: nothing here
|
|
34189
34936
|
* proves the account is entitled to the model, and the server's own
|
|
34190
34937
|
* `GET /v1/models` still outranks it.
|
|
34191
34938
|
*
|
|
34192
|
-
* Two of
|
|
34193
|
-
* like mistakes against their neighbours:
|
|
34939
|
+
* Two of that row's fields are the ones worth recording a WHY for, because both
|
|
34940
|
+
* look like mistakes against their neighbours:
|
|
34194
34941
|
*
|
|
34195
34942
|
* 1. `canDisableThinking: false`. The vendor documents that thinking cannot be
|
|
34196
34943
|
* turned off on this model at all: sending `thinking: { type: 'disabled' }`
|
|
@@ -34216,17 +34963,18 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
|
|
|
34216
34963
|
* A future reader who notices the shared `supportsTemperature: false` and
|
|
34217
34964
|
* "fixes" item 2 to 'mid-convo' has reintroduced exactly that bug.
|
|
34218
34965
|
*
|
|
34219
|
-
* One more field on
|
|
34220
|
-
* floor is the number upstream itself states when it refuses
|
|
34221
|
-
* (`claude_code_version_too_old`, "version 2.1.280 or newer is
|
|
34966
|
+
* One more field on `claude-opus-5-5` comes from neither source: its
|
|
34967
|
+
* `minCliVersion` floor is the number upstream itself states when it refuses
|
|
34968
|
+
* the model (`claude_code_version_too_old`, "version 2.1.280 or newer is
|
|
34969
|
+
* required"). That
|
|
34222
34970
|
* is a stronger witness than a documentation page — the server said it — and it
|
|
34223
34971
|
* is recorded here so a claim below it is refused locally, before the request is
|
|
34224
34972
|
* sent, instead of as an upstream 400 that names neither the model nor the number
|
|
34225
34973
|
* the user has to reach.
|
|
34226
34974
|
*/
|
|
34227
34975
|
/**
|
|
34228
|
-
*
|
|
34229
|
-
* followed by the
|
|
34976
|
+
* Seventeen rows: the snapshot's own 15, in the snapshot's own declaration order,
|
|
34977
|
+
* followed by the two rows the snapshot still predates (see LOCAL ADDITIONS above).
|
|
34230
34978
|
*
|
|
34231
34979
|
* The order matters and is not cosmetic. The test asserts the snapshot's ids
|
|
34232
34980
|
* appear in the table as a SUBSEQUENCE, so a curated row may be appended or
|
|
@@ -34514,6 +35262,24 @@ const CLAUDE_MODELS = Object.freeze([
|
|
|
34514
35262
|
],
|
|
34515
35263
|
canDisableThinking: false,
|
|
34516
35264
|
bindsThinkingToPrefix: true
|
|
35265
|
+
},
|
|
35266
|
+
{
|
|
35267
|
+
id: "claude-haiku-5-5",
|
|
35268
|
+
name: "Claude Haiku 5.5",
|
|
35269
|
+
contextWindow: 1e6,
|
|
35270
|
+
maxTokens: 128e3,
|
|
35271
|
+
supportsImage: true,
|
|
35272
|
+
supportsTemperature: false,
|
|
35273
|
+
thinkingMode: "adaptive",
|
|
35274
|
+
reasoningEfforts: [
|
|
35275
|
+
"low",
|
|
35276
|
+
"medium",
|
|
35277
|
+
"high",
|
|
35278
|
+
"xhigh",
|
|
35279
|
+
"max"
|
|
35280
|
+
],
|
|
35281
|
+
canDisableThinking: true,
|
|
35282
|
+
bindsThinkingToPrefix: true
|
|
34517
35283
|
}
|
|
34518
35284
|
]);
|
|
34519
35285
|
/** Every id the fallback table knows, in declaration order. */
|
|
@@ -34538,6 +35304,13 @@ const FALLBACK_MODELS = CLAUDE_MODELS;
|
|
|
34538
35304
|
* than one that leads with the general-purpose ids. The rest stay selectable
|
|
34539
35305
|
* from the model card and are one click away.
|
|
34540
35306
|
*
|
|
35307
|
+
* The list tracks ONE current model per family rather than the newest of each,
|
|
35308
|
+
* so a slot follows its family's newest release in place instead of growing: the
|
|
35309
|
+
* Haiku slot is `claude-haiku-5-5`, which replaced `claude-haiku-4-5` when that
|
|
35310
|
+
* model shipped. That is a deliberate swap and not a silent append — a list that
|
|
35311
|
+
* offered both generations of every family would be longer than the picker it
|
|
35312
|
+
* feeds, and the older Haiku stays one click away in the card.
|
|
35313
|
+
*
|
|
34541
35314
|
* This list is the SHIPPED DEFAULT, and edits to it are a deliberate onboarding
|
|
34542
35315
|
* decision rather than a no-op: a stored list that still equals the shipped
|
|
34543
35316
|
* default counts as "never edited", and the routes and adapter read it that way
|
|
@@ -34551,7 +35324,7 @@ const DEFAULT_VISIBLE_MODEL_IDS = [
|
|
|
34551
35324
|
"claude-opus-5-5",
|
|
34552
35325
|
"claude-opus-4-6",
|
|
34553
35326
|
"claude-sonnet-4-6",
|
|
34554
|
-
"claude-haiku-
|
|
35327
|
+
"claude-haiku-5-5",
|
|
34555
35328
|
"claude-opus-4-5"
|
|
34556
35329
|
];
|
|
34557
35330
|
new Map(CLAUDE_MODELS.map((model) => [model.id, model]));
|