@bitkyc08/opencodex 2.7.21 → 2.7.23
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/gui/dist/assets/{index-CILVKWmx.css → index-Bk_GgFrh.css} +1 -1
- package/gui/dist/assets/index-DQjt6Hly.js +40 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/claude/outbound.ts +15 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/auth-api.ts +7 -6
- package/src/codex/quota.ts +13 -27
- package/src/codex/routing.ts +1 -2
- package/src/lib/abort.ts +55 -0
- package/src/lib/upstream-retry.ts +62 -0
- package/src/oauth/index.ts +82 -24
- package/src/oauth/local-token-detect.ts +23 -1
- package/src/oauth/store.ts +51 -31
- package/src/oauth/xai.ts +17 -10
- package/src/providers/quota.ts +2 -6
- package/src/providers/registry.ts +38 -13
- package/src/providers/xai-transport.ts +107 -60
- package/src/responses/parser.ts +59 -17
- package/src/server/claude-messages.ts +240 -14
- package/src/server/management-api.ts +3 -3
- package/src/server/request-log.ts +13 -1
- package/src/server/responses.ts +67 -18
- package/src/types.ts +13 -0
- package/src/usage/log.ts +11 -0
- package/gui/dist/assets/index-DYIS0tTL.js +0 -40
package/src/oauth/store.ts
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* same human on every re-login. Cursor login extracts JWT `sub` as accountId so multiauth
|
|
16
16
|
* can append distinct accounts.
|
|
17
17
|
*/
|
|
18
|
-
import { createHash } from "node:crypto";
|
|
19
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync,
|
|
18
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
19
|
+
import { chmodSync, closeSync, copyFileSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
|
|
22
22
|
import type { OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
|
|
@@ -26,12 +26,21 @@ type AuthStore = Record<string, ProviderAccountSet>;
|
|
|
26
26
|
/** Providers whose account set is pinned to a single slot (see module doc). */
|
|
27
27
|
const SINGLE_SLOT_PROVIDERS = new Set(["chatgpt"]);
|
|
28
28
|
|
|
29
|
-
function
|
|
29
|
+
export function getAuthStorePath(): string {
|
|
30
30
|
return join(getConfigDir(), "auth.json");
|
|
31
31
|
}
|
|
32
|
+
export function getAuthStoreLockPath(): string { return join(getConfigDir(), "auth.store.lock"); }
|
|
33
|
+
export function getAuthRefreshIntentLockPath(provider: string, accountId: string): string {
|
|
34
|
+
const safeProvider = provider.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
35
|
+
const accountHash = createHash("sha256").update(accountId).digest("hex").slice(0, 24);
|
|
36
|
+
return join(getConfigDir(), `auth.refresh.${safeProvider}.${accountHash}.lock`);
|
|
37
|
+
}
|
|
38
|
+
export function credentialGeneration(cred: OAuthCredentials): string {
|
|
39
|
+
return createHash("sha256").update(JSON.stringify([cred.refresh, cred.access, cred.expires])).digest("hex");
|
|
40
|
+
}
|
|
32
41
|
|
|
33
42
|
function loadAuthStoreInternal(): { store: AuthStore; hadLegacy: boolean } {
|
|
34
|
-
const path =
|
|
43
|
+
const path = getAuthStorePath();
|
|
35
44
|
hardenConfigDir();
|
|
36
45
|
hardenExistingSecret(path);
|
|
37
46
|
if (!existsSync(path)) return { store: {}, hadLegacy: false };
|
|
@@ -55,8 +64,24 @@ function persist(store: AuthStore): void {
|
|
|
55
64
|
try { chmodSync(dir, 0o700); } catch { /* best-effort on existing dir */ }
|
|
56
65
|
}
|
|
57
66
|
hardenConfigDir();
|
|
58
|
-
atomicWriteFile(
|
|
67
|
+
atomicWriteFile(getAuthStorePath(), JSON.stringify(store, null, 2) + "\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class OAuthFileLockError extends Error { readonly code = "OAUTH_FILE_LOCK_UNAVAILABLE"; constructor(message: string, options?: { cause?: unknown }) { super(message, options); this.name = "OAuthFileLockError"; } }
|
|
71
|
+
interface LockSnapshot { bytes: string; dev: number; ino: number; mtimeMs: number; size: number }
|
|
72
|
+
export interface OAuthFileLockOptions { path: string; waitTimeoutMs?: number; staleAfterMs?: number; pollMinMs?: number; pollMaxMs?: number; sleep?: (ms: number) => Promise<void>; now?: () => number; random?: () => number; beforeStaleUnlink?: () => void; beforeReleaseUnlink?: () => void; beforeFailedCreateUnlink?: () => void; writeMetadata?: (fd: number, bytes: string) => void }
|
|
73
|
+
export interface OAuthFileLockGuard { readonly ownerId: string; release(): void }
|
|
74
|
+
function errorCode(error: unknown): string | undefined { return error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : undefined; }
|
|
75
|
+
function snapshot(path: string): LockSnapshot { const bytes = readFileSync(path, "utf8"); const s = statSync(path); return { bytes, dev:s.dev, ino:s.ino, mtimeMs:s.mtimeMs, size:s.size }; }
|
|
76
|
+
function sameSnapshot(a: LockSnapshot,b: LockSnapshot): boolean { return a.bytes===b.bytes&&a.dev===b.dev&&a.ino===b.ino&&a.mtimeMs===b.mtimeMs&&a.size===b.size; }
|
|
77
|
+
function sameFd(a: LockSnapshot,b: ReturnType<typeof fstatSync>): boolean { return a.dev===b.dev&&a.ino===b.ino&&a.mtimeMs===b.mtimeMs&&a.size===b.size; }
|
|
78
|
+
export function createOAuthFileLock(options: OAuthFileLockOptions): { acquire(): Promise<OAuthFileLockGuard> } {
|
|
79
|
+
const wait=options.waitTimeoutMs??5000, stale=options.staleAfterMs??120000, min=options.pollMinMs??25,max=options.pollMaxMs??100,sleep=options.sleep??(ms=>Bun.sleep(ms)),now=options.now??Date.now,random=options.random??Math.random,write=options.writeMetadata??((fd,b)=>writeFileSync(fd,b,"utf8"));
|
|
80
|
+
if(wait<0||stale<=0||min<0||max<min) throw new OAuthFileLockError("Invalid OAuth file-lock timing options");
|
|
81
|
+
return { async acquire() { hardenConfigDir(); if(!existsSync(getConfigDir())) mkdirSync(getConfigDir(),{recursive:true,mode:0o700}); const ownerId=randomUUID(),started=now(); for(;;){ let fd:number|undefined; try { fd=openSync(options.path,"wx",0o600); const bytes=`${JSON.stringify({version:1,ownerId,pid:process.pid,createdAt:now()})}\n`; write(fd,bytes); const fs=fstatSync(fd); closeSync(fd); fd=undefined; const owned=snapshot(options.path); if(owned.bytes!==bytes||!sameFd(owned,fs)) throw new OAuthFileLockError("OAuth lock changed during creation"); let released=false; return {ownerId,release(){if(released)return;released=true;try{const a=snapshot(options.path);if(!sameSnapshot(owned,a))return;options.beforeReleaseUnlink?.();const b=snapshot(options.path);if(sameSnapshot(owned,b))unlinkSync(options.path);}catch(e){if(errorCode(e)!=="ENOENT")console.warn(`[oauth] lock release failed: ${e instanceof Error?e.message:String(e)}`);}}}; } catch(e) { if(fd!==undefined){let fs;try{fs=fstatSync(fd);}catch{}try{closeSync(fd);}catch{}if(fs)try{const a=snapshot(options.path);if(sameFd(a,fs)){options.beforeFailedCreateUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b)&&sameFd(b,fs))unlinkSync(options.path);}}catch{}} if(errorCode(e)!=="EEXIST")throw e instanceof OAuthFileLockError?e:new OAuthFileLockError("Could not create OAuth file lock",{cause:e}); }
|
|
82
|
+
try{const a=snapshot(options.path);let created=a.mtimeMs;try{const p=JSON.parse(a.bytes);if(typeof p.createdAt==="number")created=Math.max(created,p.createdAt);}catch{}if(now()-created>stale){options.beforeStaleUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b))unlinkSync(options.path);continue;}}catch(e){if(errorCode(e)==="ENOENT")continue;throw new OAuthFileLockError("Could not inspect OAuth file lock",{cause:e});} const elapsed=now()-started;if(elapsed>=wait)throw new OAuthFileLockError(`Timed out after ${wait}ms waiting for OAuth file lock`);await sleep(Math.min(wait-elapsed,min+Math.floor(random()*(max-min+1)))); } } };
|
|
59
83
|
}
|
|
84
|
+
export function createOAuthRefreshIntentLock(provider:string,accountId:string,overrides:Partial<OAuthFileLockOptions>={}) { return createOAuthFileLock({path:getAuthRefreshIntentLockPath(provider,accountId),staleAfterMs:120000,...overrides}); }
|
|
60
85
|
|
|
61
86
|
/**
|
|
62
87
|
* One-time downgrade safety net: the first time we persist the NEW shape over a file that
|
|
@@ -65,7 +90,7 @@ function persist(store: AuthStore): void {
|
|
|
65
90
|
* store, destroying refresh tokens; the backup makes that recoverable.
|
|
66
91
|
*/
|
|
67
92
|
function backupLegacyOnce(): void {
|
|
68
|
-
const path =
|
|
93
|
+
const path = getAuthStorePath();
|
|
69
94
|
const backup = `${path}.pre-multiauth`;
|
|
70
95
|
if (!existsSync(path) || existsSync(backup)) return;
|
|
71
96
|
try {
|
|
@@ -155,23 +180,15 @@ function normalizeAuthStore(raw: unknown): { store: AuthStore; hadLegacy: boolea
|
|
|
155
180
|
* a guardian refresh persisting a non-active account cannot roll back a concurrent
|
|
156
181
|
* active-account switch (lost update). Cross-process races are accepted (single proxy).
|
|
157
182
|
*/
|
|
158
|
-
let
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
// current callers are sync. The queue exists so future async mutators serialize too.
|
|
162
|
-
const result = fn();
|
|
163
|
-
writeQueue = writeQueue.then(() => result);
|
|
164
|
-
return result;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function mutateStore<T>(fn: (store: AuthStore) => T): T {
|
|
168
|
-
return enqueueWrite(() => {
|
|
183
|
+
let mutationTail: Promise<void> = Promise.resolve();
|
|
184
|
+
function serializeMutation<T>(work:()=>Promise<T>):Promise<T>{const result=mutationTail.then(work,work);mutationTail=result.then(()=>undefined,()=>undefined);return result;}
|
|
185
|
+
export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
|
|
169
186
|
const { store, hadLegacy } = loadAuthStoreInternal();
|
|
170
187
|
if (hadLegacy) backupLegacyOnce();
|
|
171
|
-
const result = fn(store);
|
|
188
|
+
const result = await fn(store);
|
|
172
189
|
persist(store);
|
|
173
190
|
return result;
|
|
174
|
-
});
|
|
191
|
+
}finally{guard.release();}});
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
/** The ACTIVE account's credential for a provider (what requests should use). */
|
|
@@ -187,10 +204,10 @@ export function getCredential(provider: string): OAuthCredentials | null {
|
|
|
187
204
|
* (rotating refresh tokens would fabricate duplicates) and single-slot providers replace the
|
|
188
205
|
* active slot / whole set instead.
|
|
189
206
|
*/
|
|
190
|
-
export function saveCredential(provider: string, cred: OAuthCredentials): void {
|
|
207
|
+
export async function saveCredential(provider: string, cred: OAuthCredentials): Promise<void> {
|
|
191
208
|
const safe = normalizeCredential(cred);
|
|
192
209
|
if (!safe) return;
|
|
193
|
-
mutateStore(store => {
|
|
210
|
+
await mutateStore(store => {
|
|
194
211
|
const set = store[provider];
|
|
195
212
|
const identity = safe.accountId ?? safe.email;
|
|
196
213
|
if (!set || SINGLE_SLOT_PROVIDERS.has(provider)) {
|
|
@@ -225,8 +242,8 @@ export function saveCredential(provider: string, cred: OAuthCredentials): void {
|
|
|
225
242
|
}
|
|
226
243
|
|
|
227
244
|
/** Remove the ACTIVE account; remaining accounts promote the first one. */
|
|
228
|
-
export function removeCredential(provider: string): void {
|
|
229
|
-
mutateStore(store => {
|
|
245
|
+
export async function removeCredential(provider: string): Promise<void> {
|
|
246
|
+
await mutateStore(store => {
|
|
230
247
|
const set = store[provider];
|
|
231
248
|
if (!set) return;
|
|
232
249
|
set.accounts = set.accounts.filter(a => a.id !== set.activeAccountId);
|
|
@@ -255,10 +272,10 @@ export function getAccountCredential(provider: string, accountId: string): OAuth
|
|
|
255
272
|
}
|
|
256
273
|
|
|
257
274
|
/** Persist a refreshed credential for a SPECIFIC account without touching activeAccountId. */
|
|
258
|
-
export function saveAccountCredential(provider: string, accountId: string, cred: OAuthCredentials): void {
|
|
275
|
+
export async function saveAccountCredential(provider: string, accountId: string, cred: OAuthCredentials): Promise<void> {
|
|
259
276
|
const safe = normalizeCredential(cred);
|
|
260
277
|
if (!safe) return;
|
|
261
|
-
mutateStore(store => {
|
|
278
|
+
await mutateStore(store => {
|
|
262
279
|
const account = store[provider]?.accounts.find(a => a.id === accountId);
|
|
263
280
|
if (!account) return;
|
|
264
281
|
account.credential = safe;
|
|
@@ -266,8 +283,8 @@ export function saveAccountCredential(provider: string, accountId: string, cred:
|
|
|
266
283
|
});
|
|
267
284
|
}
|
|
268
285
|
|
|
269
|
-
export function setActiveAccount(provider: string, accountId: string): boolean {
|
|
270
|
-
return mutateStore(store => {
|
|
286
|
+
export async function setActiveAccount(provider: string, accountId: string): Promise<boolean> {
|
|
287
|
+
return await mutateStore(store => {
|
|
271
288
|
const set = store[provider];
|
|
272
289
|
if (!set || !set.accounts.some(a => a.id === accountId)) return false;
|
|
273
290
|
set.activeAccountId = accountId;
|
|
@@ -276,8 +293,8 @@ export function setActiveAccount(provider: string, accountId: string): boolean {
|
|
|
276
293
|
}
|
|
277
294
|
|
|
278
295
|
/** Remove one account by id; active removal promotes the first remaining account. */
|
|
279
|
-
export function removeAccount(provider: string, accountId: string): boolean {
|
|
280
|
-
return mutateStore(store => {
|
|
296
|
+
export async function removeAccount(provider: string, accountId: string): Promise<boolean> {
|
|
297
|
+
return await mutateStore(store => {
|
|
281
298
|
const set = store[provider];
|
|
282
299
|
if (!set) return false;
|
|
283
300
|
const before = set.accounts.length;
|
|
@@ -292,11 +309,14 @@ export function removeAccount(provider: string, accountId: string): boolean {
|
|
|
292
309
|
});
|
|
293
310
|
}
|
|
294
311
|
|
|
295
|
-
export function markAccountNeedsReauth(provider: string, accountId: string, needsReauth: boolean): void {
|
|
296
|
-
mutateStore(store => {
|
|
312
|
+
export async function markAccountNeedsReauth(provider: string, accountId: string, needsReauth: boolean): Promise<void> {
|
|
313
|
+
await mutateStore(store => {
|
|
297
314
|
const account = store[provider]?.accounts.find(a => a.id === accountId);
|
|
298
315
|
if (!account) return;
|
|
299
316
|
if (needsReauth) account.needsReauth = true;
|
|
300
317
|
else delete account.needsReauth;
|
|
301
318
|
});
|
|
302
319
|
}
|
|
320
|
+
|
|
321
|
+
export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};account.credential=safe;delete account.needsReauth;return{superseded:false};});}
|
|
322
|
+
export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string):Promise<boolean>{return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||credentialGeneration(account.credential)!==generation)return false;account.needsReauth=true;return true;});}
|
package/src/oauth/xai.ts
CHANGED
|
@@ -12,6 +12,9 @@ const XAI_OAUTH_CALLBACK_PATH = "/callback";
|
|
|
12
12
|
const XAI_OAUTH_REFRESH_SKEW_MS = 2 * 60 * 1000;
|
|
13
13
|
const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
|
|
14
14
|
|
|
15
|
+
export const XAI_LOCAL_CLI_DETACH_WARNING =
|
|
16
|
+
"[oauth:xai] Grok CLI credential was stale; refreshed into OpenCodex ownership. Grok CLI may require login again.";
|
|
17
|
+
|
|
15
18
|
interface XaiDiscovery {
|
|
16
19
|
authorizationEndpoint: string;
|
|
17
20
|
tokenEndpoint: string;
|
|
@@ -22,7 +25,7 @@ interface XaiDiscoveryPayload {
|
|
|
22
25
|
token_endpoint?: unknown;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
interface XaiTokenPayload {
|
|
28
|
+
export interface XaiTokenPayload {
|
|
26
29
|
access_token?: unknown;
|
|
27
30
|
refresh_token?: unknown;
|
|
28
31
|
expires_in?: unknown;
|
|
@@ -89,12 +92,18 @@ function getTokenIdentity(accessToken: string, idToken: string | undefined): { a
|
|
|
89
92
|
return { accountId, email };
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
export class XaiTokenRequestError extends Error { constructor(public readonly status?:number,public readonly oauthError?:string,message="xAI token request failed",options?:{cause?:unknown}){super(message,options);this.name="XaiTokenRequestError";} }
|
|
96
|
+
export interface XaiTokenRetryDeps { sleep?:(ms:number)=>Promise<void>; random?:()=>number }
|
|
97
|
+
function isAbortError(error:unknown):boolean{return error instanceof DOMException&&error.name==="AbortError";}
|
|
98
|
+
function retryDelay(attempt:number,retryAfter:string|null,random:()=>number):number{const base=attempt===1?100:250,j=Math.round(base*(.75+random()*.5)),seconds=retryAfter!==null&&/^\d+$/.test(retryAfter)?Number(retryAfter):0;return Math.min(2000,Math.max(j,seconds*1000));}
|
|
99
|
+
async function readTokenError(response:Response):Promise<XaiTokenRequestError>{let oauthError:string|undefined,detail="";try{const body=await response.json() as {error?:unknown;error_description?:unknown};if(typeof body.error==="string")oauthError=body.error;if(typeof body.error_description==="string")detail=body.error_description;}catch{}const suffix=detail?`: ${detail}`:oauthError?`: ${oauthError}`:"";return new XaiTokenRequestError(response.status,oauthError,`xAI token request failed: ${response.status}${suffix}`);}
|
|
100
|
+
export async function postXaiToken(
|
|
93
101
|
tokenEndpoint: string,
|
|
94
102
|
body: Record<string, string>,
|
|
95
|
-
signal?: AbortSignal,
|
|
103
|
+
signal?: AbortSignal, deps:XaiTokenRetryDeps={},
|
|
96
104
|
): Promise<XaiTokenPayload> {
|
|
97
|
-
|
|
105
|
+
const sleep=deps.sleep??(ms=>Bun.sleep(ms)),random=deps.random??Math.random;let last:unknown;
|
|
106
|
+
for(let attempt=1;attempt<=3;attempt++){let response:Response;try{response=await fetch(tokenEndpoint, {
|
|
98
107
|
method: "POST",
|
|
99
108
|
headers: {
|
|
100
109
|
Accept: "application/json",
|
|
@@ -102,11 +111,7 @@ async function postXaiToken(
|
|
|
102
111
|
},
|
|
103
112
|
body: new URLSearchParams(body).toString(),
|
|
104
113
|
signal: requestSignal(signal),
|
|
105
|
-
});
|
|
106
|
-
if (!response.ok) {
|
|
107
|
-
throw new Error(`xAI token request failed: ${response.status} ${await response.text()}`);
|
|
108
|
-
}
|
|
109
|
-
return (await response.json()) as XaiTokenPayload;
|
|
114
|
+
});}catch(error){if(isAbortError(error)&&signal?.aborted)throw error;last=error;if(attempt===3)throw new XaiTokenRequestError(undefined,undefined,"xAI token request failed: network error",{cause:error});await sleep(retryDelay(attempt,null,random));continue;}if(response.ok)return await response.json() as XaiTokenPayload;const error=await readTokenError(response);last=error;if(!(response.status===429||response.status>=500)||attempt===3)throw error;await sleep(retryDelay(attempt,response.headers.get("retry-after"),random));}throw last;
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
function credentialsFromTokenPayload(payload: XaiTokenPayload, refreshFallback = ""): OAuthCredentials {
|
|
@@ -200,7 +205,9 @@ export async function loginXai(
|
|
|
200
205
|
ctrl.onProgress?.("Found Grok CLI token, importing automatically");
|
|
201
206
|
if (local.expires >= Date.now() + 60_000) return local;
|
|
202
207
|
try {
|
|
203
|
-
|
|
208
|
+
const fresh = await refreshXaiToken(local.refresh, ctrl.signal);
|
|
209
|
+
ctrl.onProgress?.(XAI_LOCAL_CLI_DETACH_WARNING);
|
|
210
|
+
return { ...fresh, source: "oauth" };
|
|
204
211
|
} catch (error) {
|
|
205
212
|
if (importLocal === "only") {
|
|
206
213
|
throw new Error(
|
package/src/providers/quota.ts
CHANGED
|
@@ -17,8 +17,6 @@ export interface ProviderQuotaWindow {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface ProviderQuota {
|
|
20
|
-
fiveHourPercent?: number;
|
|
21
|
-
fiveHourResetAt?: number;
|
|
22
20
|
weeklyPercent?: number;
|
|
23
21
|
weeklyResetAt?: number;
|
|
24
22
|
monthlyPercent?: number;
|
|
@@ -58,8 +56,7 @@ function cacheKey(config: OcxConfig): string {
|
|
|
58
56
|
|
|
59
57
|
function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
|
|
60
58
|
if (!quota) return false;
|
|
61
|
-
return typeof quota.
|
|
62
|
-
|| typeof quota.weeklyPercent === "number"
|
|
59
|
+
return typeof quota.weeklyPercent === "number"
|
|
63
60
|
|| typeof quota.monthlyPercent === "number"
|
|
64
61
|
|| !!quota.customWindows?.some(window => typeof window.percent === "number");
|
|
65
62
|
}
|
|
@@ -188,11 +185,10 @@ async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaRepor
|
|
|
188
185
|
const opus = parseClaudeBucket(body.seven_day_opus);
|
|
189
186
|
const sonnet = parseClaudeBucket(body.seven_day_sonnet);
|
|
190
187
|
const customWindows: ProviderQuotaWindow[] = [];
|
|
188
|
+
if (fiveHour?.percent !== undefined) customWindows.push({ label: "5h", percent: fiveHour.percent, ...(fiveHour.resetAt !== undefined ? { resetAt: fiveHour.resetAt } : {}) });
|
|
191
189
|
if (opus?.percent !== undefined) customWindows.push({ label: "Opus", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
|
|
192
190
|
if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
|
|
193
191
|
const quota: ProviderQuota = {
|
|
194
|
-
...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
|
|
195
|
-
...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
196
192
|
...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
|
|
197
193
|
...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
|
|
198
194
|
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
@@ -146,16 +146,33 @@ const DEEPSEEK_THINKING_REASONING_MAP: Record<string, string> = {
|
|
|
146
146
|
xhigh: "max",
|
|
147
147
|
max: "max",
|
|
148
148
|
};
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
// 260717 Kimi K3: the subscription endpoint uses one upstream id (`k3`) for both
|
|
150
|
+
// entitlement tiers. Bare `k3` advertises the Moderato 256K ceiling; the local `[1m]`
|
|
151
|
+
// alias advertises Allegretto's 1M ceiling and is stripped before the upstream request.
|
|
152
|
+
// The separately billed Moonshot API uses `kimi-k3`.
|
|
153
|
+
// Evidence: https://www.kimi.com/code/docs/en/kimi-code/models.html
|
|
154
|
+
// https://www.kimi.com/code/docs/en/kimi-code/error-reference.html
|
|
155
|
+
const KIMI_K3_STANDARD_CONTEXT_WINDOW = 262_144;
|
|
156
|
+
const KIMI_K3_1M_CONTEXT_WINDOW = 1_048_576;
|
|
157
|
+
const KIMI_CODING_K3_MODELS = ["k3", "k3[1m]"];
|
|
158
|
+
const KIMI_LEGACY_API_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"];
|
|
159
|
+
const KIMI_API_MODELS = ["kimi-k3", ...KIMI_LEGACY_API_MODELS];
|
|
160
|
+
const KIMI_CODING_MODELS = [...KIMI_CODING_K3_MODELS, ...KIMI_LEGACY_API_MODELS, "kimi-for-coding"];
|
|
153
161
|
const KIMI_THINKING_MODELS = KIMI_CODING_MODELS;
|
|
162
|
+
const KIMI_CODING_NO_REASONING_MODELS = KIMI_CODING_MODELS.filter(id => !KIMI_CODING_K3_MODELS.includes(id));
|
|
163
|
+
const KIMI_API_NO_REASONING_MODELS = KIMI_API_MODELS.filter(id => id !== "kimi-k3");
|
|
164
|
+
const KIMI_CODING_REASONING_EFFORTS = Object.fromEntries(
|
|
165
|
+
KIMI_CODING_MODELS.map(id => [id, KIMI_CODING_K3_MODELS.includes(id) ? ["max"] : []]),
|
|
166
|
+
);
|
|
167
|
+
const KIMI_API_REASONING_EFFORTS = Object.fromEntries(
|
|
168
|
+
KIMI_API_MODELS.map(id => [id, id === "kimi-k3" ? ["max"] : []]),
|
|
169
|
+
);
|
|
154
170
|
const KIMI_LOCKED_PARAMETER_MODELS = KIMI_CODING_MODELS;
|
|
155
171
|
const KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS = ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-for-coding"];
|
|
156
172
|
const KIMI_API_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
|
|
157
|
-
KIMI_API_MODELS.map(id => [id, 262_144]),
|
|
173
|
+
KIMI_API_MODELS.map(id => [id, id === "kimi-k3" ? KIMI_K3_1M_CONTEXT_WINDOW : 262_144]),
|
|
158
174
|
);
|
|
175
|
+
const KIMI_API_MODEL_INPUT_MODALITIES = { "kimi-k3": ["text", "image"] };
|
|
159
176
|
|
|
160
177
|
// 260715 NVIDIA NIM kimi family (issue #126): documented served ids on integrate
|
|
161
178
|
// chat/completions per docs.api.nvidia.com/nim/reference/llm-apis; live /v1/models
|
|
@@ -168,7 +185,10 @@ const NVIDIA_NIM_KIMI_MODELS = [
|
|
|
168
185
|
"moonshotai/kimi-k2-instruct", "moonshotai/kimi-k2-instruct-0905",
|
|
169
186
|
];
|
|
170
187
|
const KIMI_CODING_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
|
|
171
|
-
KIMI_CODING_MODELS.map(id => [id,
|
|
188
|
+
KIMI_CODING_MODELS.map(id => [id, id === "k3[1m]" ? KIMI_K3_1M_CONTEXT_WINDOW : KIMI_K3_STANDARD_CONTEXT_WINDOW]),
|
|
189
|
+
);
|
|
190
|
+
const KIMI_CODING_MODEL_INPUT_MODALITIES = Object.fromEntries(
|
|
191
|
+
KIMI_CODING_K3_MODELS.map(id => [id, ["text", "image"]]),
|
|
172
192
|
);
|
|
173
193
|
const NEURALWATT_REASONING_HISTORY_MODELS = [
|
|
174
194
|
"glm-5.2", "glm-5.2-short",
|
|
@@ -306,6 +326,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
306
326
|
adapter: "openai-chat",
|
|
307
327
|
baseUrl: "https://api.kimi.com/coding/v1",
|
|
308
328
|
authKind: "oauth",
|
|
329
|
+
modelSuffixBracketStrip: true,
|
|
309
330
|
featured: true,
|
|
310
331
|
oauthId: "kimi",
|
|
311
332
|
jawcodeBundle: "moonshot",
|
|
@@ -313,9 +334,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
313
334
|
models: KIMI_CODING_MODELS,
|
|
314
335
|
defaultModel: "kimi-k2.7-code",
|
|
315
336
|
modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
337
|
+
modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
|
|
338
|
+
// K3 accepts reasoning_effort=max; older Kimi models use Kimi's private thinking control.
|
|
339
|
+
noReasoningModels: KIMI_CODING_NO_REASONING_MODELS,
|
|
340
|
+
modelReasoningEfforts: KIMI_CODING_REASONING_EFFORTS,
|
|
319
341
|
noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
320
342
|
noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
321
343
|
noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
@@ -502,8 +524,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
502
524
|
dashboardUrl: "https://platform.moonshot.ai/console/api-keys", defaultModel: "kimi-k2.7-code", jawcodeBundle: "moonshot",
|
|
503
525
|
models: KIMI_API_MODELS,
|
|
504
526
|
modelContextWindows: KIMI_API_MODEL_CONTEXT_WINDOWS,
|
|
505
|
-
|
|
506
|
-
|
|
527
|
+
modelInputModalities: KIMI_API_MODEL_INPUT_MODALITIES,
|
|
528
|
+
noReasoningModels: KIMI_API_NO_REASONING_MODELS,
|
|
529
|
+
modelReasoningEfforts: KIMI_API_REASONING_EFFORTS,
|
|
507
530
|
noTemperatureModels: KIMI_API_MODELS,
|
|
508
531
|
noTopPModels: KIMI_API_MODELS,
|
|
509
532
|
noPenaltyModels: KIMI_API_MODELS,
|
|
@@ -599,10 +622,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
599
622
|
{
|
|
600
623
|
id: "kimi-code", label: "Kimi (coding)", baseUrl: "https://api.kimi.com/coding/v1", adapter: "openai-chat", authKind: "key",
|
|
601
624
|
dashboardUrl: "https://platform.moonshot.cn/console/api-keys", defaultModel: "kimi-k2.7-code",
|
|
625
|
+
modelSuffixBracketStrip: true,
|
|
602
626
|
models: KIMI_CODING_MODELS,
|
|
603
627
|
modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
|
|
604
|
-
|
|
605
|
-
|
|
628
|
+
modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
|
|
629
|
+
noReasoningModels: KIMI_CODING_NO_REASONING_MODELS,
|
|
630
|
+
modelReasoningEfforts: KIMI_CODING_REASONING_EFFORTS,
|
|
606
631
|
noTemperatureModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
607
632
|
noTopPModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
608
633
|
noPenaltyModels: KIMI_LOCKED_PARAMETER_MODELS,
|
|
@@ -1,48 +1,79 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import type { OcxProviderConfig } from "../types";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* xAI account OAuth and xAI API keys share a bearer shape but not a billing
|
|
6
|
-
* transport. OAuth represents the Grok CLI subscription entitlement, while a
|
|
7
|
-
* key represents the API team. Keep the saved provider preset compatible with
|
|
8
|
-
* the dashboard's "Use an API key instead" switch and resolve the transport at
|
|
9
|
-
* request time.
|
|
10
|
-
*/
|
|
11
4
|
export const XAI_GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
12
5
|
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
export const XAI_GROK_COMPATIBILITY = {
|
|
7
|
+
version: "0.2.93",
|
|
8
|
+
userAgent: "opencodex-grok/0.2.93",
|
|
9
|
+
headers: {
|
|
10
|
+
clientIdentifier: "x-grok-client-identifier",
|
|
11
|
+
clientVersion: "x-grok-client-version",
|
|
12
|
+
tokenAuth: "x-xai-token-auth",
|
|
13
|
+
authenticateResponse: "x-authenticateresponse",
|
|
14
|
+
conversationId: "x-grok-conv-id",
|
|
15
|
+
requestId: "x-grok-req-id",
|
|
16
|
+
sessionId: "x-grok-session-id",
|
|
17
|
+
userAgent: "User-Agent",
|
|
18
|
+
},
|
|
19
|
+
} as const;
|
|
15
20
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
export const XAI_GROK_CLIENT_VERSION = XAI_GROK_COMPATIBILITY.version;
|
|
22
|
+
export const XAI_CONV_ID_HEADER = XAI_GROK_COMPATIBILITY.headers.conversationId;
|
|
23
|
+
|
|
24
|
+
export type OcxProviderTransport = OcxProviderConfig & {
|
|
25
|
+
/** Request executor used only at runtime; never persisted. */
|
|
26
|
+
fetch?: typeof globalThis.fetch;
|
|
20
27
|
};
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* leaves the proxy.
|
|
29
|
-
*/
|
|
30
|
-
export const XAI_CONV_ID_HEADER = "x-grok-conv-id";
|
|
29
|
+
const XAI_GROK_CLI_HEADERS: Readonly<Record<string, string>> = {
|
|
30
|
+
[XAI_GROK_COMPATIBILITY.headers.clientIdentifier]: "opencodex",
|
|
31
|
+
[XAI_GROK_COMPATIBILITY.headers.clientVersion]: XAI_GROK_CLIENT_VERSION,
|
|
32
|
+
[XAI_GROK_COMPATIBILITY.headers.tokenAuth]: "xai-grok-cli",
|
|
33
|
+
[XAI_GROK_COMPATIBILITY.headers.authenticateResponse]: "authenticate-response",
|
|
34
|
+
};
|
|
31
35
|
|
|
32
|
-
function hasHeaderCaseInsensitive(
|
|
33
|
-
|
|
36
|
+
function hasHeaderCaseInsensitive(
|
|
37
|
+
headers: Record<string, string> | undefined,
|
|
38
|
+
name: string,
|
|
39
|
+
): boolean {
|
|
34
40
|
const target = name.toLowerCase();
|
|
35
|
-
return Object.keys(headers).some(key => key.toLowerCase() === target);
|
|
41
|
+
return Object.keys(headers ?? {}).some(key => key.toLowerCase() === target);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function withoutUserOverridden(
|
|
45
|
+
defaults: Readonly<Record<string, string>>,
|
|
46
|
+
userHeaders: Record<string, string> | undefined,
|
|
47
|
+
): Record<string, string> {
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
Object.entries(defaults).filter(([name]) => !hasHeaderCaseInsensitive(userHeaders, name)),
|
|
50
|
+
);
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
function withGeneratedRequestId(
|
|
54
|
+
init: RequestInit | undefined,
|
|
55
|
+
configuredRequestId: string | undefined,
|
|
56
|
+
stableHeaders: Readonly<Record<string, string>>,
|
|
57
|
+
): RequestInit {
|
|
58
|
+
const headers = new Headers(init?.headers);
|
|
59
|
+
for (const [name, value] of Object.entries(stableHeaders)) {
|
|
60
|
+
if (!headers.has(name)) headers.set(name, value);
|
|
44
61
|
}
|
|
45
|
-
|
|
62
|
+
if (!headers.has(XAI_GROK_COMPATIBILITY.headers.requestId)) {
|
|
63
|
+
headers.set(
|
|
64
|
+
XAI_GROK_COMPATIBILITY.headers.requestId,
|
|
65
|
+
configuredRequestId ?? randomUUID(),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return { ...init, headers };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findHeaderCaseInsensitive(
|
|
72
|
+
headers: Record<string, string> | undefined,
|
|
73
|
+
name: string,
|
|
74
|
+
): string | undefined {
|
|
75
|
+
const target = name.toLowerCase();
|
|
76
|
+
return Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === target)?.[1];
|
|
46
77
|
}
|
|
47
78
|
|
|
48
79
|
export function deriveXaiConvId(promptCacheKey: string): string {
|
|
@@ -50,40 +81,56 @@ export function deriveXaiConvId(promptCacheKey: string): string {
|
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
/**
|
|
53
|
-
* Resolve
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* `promptCacheKey` (the client's stable conversation key) additionally pins
|
|
58
|
-
* cache-affinity routing via `x-grok-conv-id` in BOTH auth modes. Blank or
|
|
59
|
-
* whitespace-only keys are ignored so unrelated requests can never collapse
|
|
60
|
-
* onto one shared conv id, and any user-configured header (any case) wins.
|
|
84
|
+
* Resolve xAI's runtime transport without mutating persisted config. Conversation/session
|
|
85
|
+
* affinity is stable for this resolved transport; request identity is generated per fetch.
|
|
86
|
+
* Agent, deployment, model-override, turn, mode, and user identity headers are intentionally
|
|
87
|
+
* omitted because opencodex has no truthful values for the official fields.
|
|
61
88
|
*/
|
|
62
89
|
export function resolveProviderTransport(
|
|
63
90
|
providerName: string,
|
|
64
|
-
provider:
|
|
91
|
+
provider: OcxProviderTransport,
|
|
65
92
|
promptCacheKey?: string,
|
|
66
|
-
):
|
|
93
|
+
): OcxProviderTransport {
|
|
67
94
|
if (providerName !== "xai") return provider;
|
|
95
|
+
|
|
68
96
|
const cacheKey = promptCacheKey?.trim();
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
}
|
|
97
|
+
const affinity = cacheKey ? deriveXaiConvId(cacheKey) : undefined;
|
|
98
|
+
const stableDefaults: Record<string, string> = {
|
|
99
|
+
[XAI_GROK_COMPATIBILITY.headers.userAgent]: XAI_GROK_COMPATIBILITY.userAgent,
|
|
100
|
+
...(affinity
|
|
101
|
+
? {
|
|
102
|
+
[XAI_GROK_COMPATIBILITY.headers.conversationId]: affinity,
|
|
103
|
+
[XAI_GROK_COMPATIBILITY.headers.sessionId]: affinity,
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
...(provider.authMode === "oauth" ? XAI_GROK_CLI_HEADERS : {}),
|
|
107
|
+
};
|
|
108
|
+
const stableHeaders = {
|
|
109
|
+
...withoutUserOverridden(stableDefaults, provider.headers),
|
|
110
|
+
...(provider.headers ?? {}),
|
|
111
|
+
};
|
|
112
|
+
// Keep API-key provider metadata compatible with key-pool rotation; session/UA defaults
|
|
113
|
+
// remain transport-scoped and are applied by the wrapper immediately below.
|
|
114
|
+
const headers = provider.authMode === "oauth"
|
|
115
|
+
? stableHeaders
|
|
116
|
+
: {
|
|
117
|
+
...(affinity && !hasHeaderCaseInsensitive(provider.headers, XAI_GROK_COMPATIBILITY.headers.conversationId)
|
|
118
|
+
? { [XAI_GROK_COMPATIBILITY.headers.conversationId]: affinity }
|
|
119
|
+
: {}),
|
|
120
|
+
...(provider.headers ?? {}),
|
|
121
|
+
};
|
|
122
|
+
const configuredRequestId = findHeaderCaseInsensitive(
|
|
123
|
+
provider.headers,
|
|
124
|
+
XAI_GROK_COMPATIBILITY.headers.requestId,
|
|
125
|
+
);
|
|
126
|
+
const baseFetch = provider.fetch ?? globalThis.fetch;
|
|
127
|
+
const attemptFetch = ((input, init) =>
|
|
128
|
+
baseFetch(input, withGeneratedRequestId(init, configuredRequestId, stableHeaders))) as typeof globalThis.fetch;
|
|
129
|
+
|
|
80
130
|
return {
|
|
81
131
|
...provider,
|
|
82
|
-
baseUrl: XAI_GROK_CLI_BASE_URL,
|
|
83
|
-
headers
|
|
84
|
-
|
|
85
|
-
...convIdHeaders,
|
|
86
|
-
...(provider.headers ?? {}),
|
|
87
|
-
},
|
|
132
|
+
...(provider.authMode === "oauth" ? { baseUrl: XAI_GROK_CLI_BASE_URL } : {}),
|
|
133
|
+
headers,
|
|
134
|
+
fetch: attemptFetch,
|
|
88
135
|
};
|
|
89
136
|
}
|