@bitkyc08/opencodex 2.8.0 → 2.9.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/README.md +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
package/src/oauth/store.ts
CHANGED
|
@@ -21,10 +21,28 @@ import { join } from "node:path";
|
|
|
21
21
|
import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
|
|
22
22
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
23
23
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
24
|
+
import { MAX_PENDING_OAUTH_MUTATIONS } from "../lib/translator-budget";
|
|
25
|
+
import {
|
|
26
|
+
captureConfigGeneration,
|
|
27
|
+
type GenerationContext,
|
|
28
|
+
} from "../lib/state-store-sweeper";
|
|
24
29
|
import { validateCopilotApiBaseUrl } from "./github-copilot";
|
|
25
30
|
import type { OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
|
|
26
31
|
|
|
27
32
|
type AuthStore = Record<string, ProviderAccountSet>;
|
|
33
|
+
let lastReconciledGeneration = 0;
|
|
34
|
+
let liveOAuthAccountKeys = new Set<string>();
|
|
35
|
+
|
|
36
|
+
function oauthAccountKey(provider: string, accountId: string): string {
|
|
37
|
+
return `${provider}\0${accountId}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function reconcileOAuthReauthState(context: GenerationContext): number {
|
|
41
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
42
|
+
liveOAuthAccountKeys = new Set(context.oauthAccountKeys);
|
|
43
|
+
lastReconciledGeneration = context.generation;
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
28
46
|
|
|
29
47
|
/** Providers whose account set is pinned to a single slot (see module doc). */
|
|
30
48
|
const SINGLE_SLOT_PROVIDERS = new Set(["chatgpt"]);
|
|
@@ -41,7 +59,7 @@ export function getAuthRefreshIntentLockPath(provider: string, accountId: string
|
|
|
41
59
|
export function getAuthRefreshIntentPath(provider: string, accountId: string): string {
|
|
42
60
|
return `${getAuthRefreshIntentLockPath(provider, accountId)}.json`;
|
|
43
61
|
}
|
|
44
|
-
export interface OAuthRefreshIntent { version: 1; provider: string; accountId: string; generation: string; createdAt: number; uncertain?: true }
|
|
62
|
+
export interface OAuthRefreshIntent { version: 1; provider: string; accountId: string; generation: string; createdAt: number; flightId?: string; staleOwner?: true; uncertain?: true }
|
|
45
63
|
function parseOAuthRefreshIntent(
|
|
46
64
|
provider: string,
|
|
47
65
|
accountId: string,
|
|
@@ -54,6 +72,8 @@ function parseOAuthRefreshIntent(
|
|
|
54
72
|
|| value.accountId !== accountId
|
|
55
73
|
|| typeof value.generation !== "string"
|
|
56
74
|
|| typeof value.createdAt !== "number"
|
|
75
|
+
|| (value.flightId !== undefined && typeof value.flightId !== "string")
|
|
76
|
+
|| (value.staleOwner !== undefined && value.staleOwner !== true)
|
|
57
77
|
) {
|
|
58
78
|
return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
|
|
59
79
|
}
|
|
@@ -82,13 +102,19 @@ export function peekOAuthRefreshIntent(provider: string, accountId: string): OAu
|
|
|
82
102
|
return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
|
-
export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now()): void {
|
|
105
|
+
export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string): void {
|
|
86
106
|
const dir = getConfigDir();
|
|
87
107
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
88
108
|
hardenConfigDir();
|
|
89
|
-
const intent: OAuthRefreshIntent = { version: 1, provider, accountId, generation, createdAt };
|
|
109
|
+
const intent: OAuthRefreshIntent = { version: 1, provider, accountId, generation, createdAt, ...(flightId ? { flightId } : {}) };
|
|
90
110
|
atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify(intent)}\n`);
|
|
91
111
|
}
|
|
112
|
+
export function markOAuthRefreshIntentStaleOwner(provider: string, accountId: string, generation: string, flightId: string): boolean {
|
|
113
|
+
const current = readOAuthRefreshIntent(provider, accountId);
|
|
114
|
+
if (current?.uncertain || current?.generation !== generation || current.flightId !== flightId) return false;
|
|
115
|
+
atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify({ ...current, staleOwner: true })}\n`);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
92
118
|
export function clearOAuthRefreshIntent(provider: string, accountId: string, generation: string): boolean {
|
|
93
119
|
const current = readOAuthRefreshIntent(provider, accountId);
|
|
94
120
|
if (!current || current.generation !== generation) return false;
|
|
@@ -294,15 +320,118 @@ function normalizeAuthStore(raw: unknown): { store: AuthStore; hadLegacy: boolea
|
|
|
294
320
|
* a guardian refresh persisting a non-active account cannot roll back a concurrent
|
|
295
321
|
* active-account switch (lost update). Cross-process races are accepted (single proxy).
|
|
296
322
|
*/
|
|
323
|
+
const OAUTH_MUTATION_WAIT_MS = 30_000;
|
|
324
|
+
const oauthMutationEncoder = new TextEncoder();
|
|
297
325
|
let mutationTail: Promise<void> = Promise.resolve();
|
|
298
|
-
|
|
299
|
-
|
|
326
|
+
let pendingMutations = 0;
|
|
327
|
+
let mutationCurrentBytes = 0;
|
|
328
|
+
let mutationHighWaterBytes = 0;
|
|
329
|
+
interface QueuedOAuthMutation {
|
|
330
|
+
started: boolean;
|
|
331
|
+
settled: boolean;
|
|
332
|
+
timeout?: ReturnType<typeof setTimeout>;
|
|
333
|
+
run(): Promise<void>;
|
|
334
|
+
}
|
|
335
|
+
const mutationWaiters: QueuedOAuthMutation[] = [];
|
|
336
|
+
let mutationRunning = false;
|
|
337
|
+
|
|
338
|
+
export class OAuthMutationBusyError extends Error {
|
|
339
|
+
readonly code = "oauth_mutation_busy";
|
|
340
|
+
constructor(message = "OAuth mutation queue is busy") {
|
|
341
|
+
super(message);
|
|
342
|
+
this.name = "OAuthMutationBusyError";
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function oauthMutationTailSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } {
|
|
347
|
+
return { currentBytes: mutationCurrentBytes, highWaterBytes: mutationHighWaterBytes, active: pendingMutations };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function retainedClosureStringBytes(values: readonly unknown[]): number {
|
|
351
|
+
const visit = (value: unknown): number => {
|
|
352
|
+
if (typeof value === "string") return oauthMutationEncoder.encode(value).byteLength;
|
|
353
|
+
if (Array.isArray(value)) return value.reduce((sum, entry) => sum + visit(entry), 0);
|
|
354
|
+
if (value && typeof value === "object") {
|
|
355
|
+
return Object.entries(value as Record<string, unknown>)
|
|
356
|
+
.reduce((sum, [key, entry]) => sum + oauthMutationEncoder.encode(key).byteLength + visit(entry), 0);
|
|
357
|
+
}
|
|
358
|
+
return 0;
|
|
359
|
+
};
|
|
360
|
+
return values.reduce<number>((sum, value) => sum + visit(value), 0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function drainOAuthMutations(): void {
|
|
364
|
+
if (mutationRunning) return;
|
|
365
|
+
const next = mutationWaiters.shift();
|
|
366
|
+
if (!next) return;
|
|
367
|
+
if (next.settled) {
|
|
368
|
+
drainOAuthMutations();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
next.started = true;
|
|
372
|
+
if (next.timeout) clearTimeout(next.timeout);
|
|
373
|
+
mutationRunning = true;
|
|
374
|
+
mutationTail = next.run().finally(() => {
|
|
375
|
+
mutationRunning = false;
|
|
376
|
+
drainOAuthMutations();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function serializeMutation<T>(work: () => Promise<T>, retainedValues: readonly unknown[], waitMs = OAUTH_MUTATION_WAIT_MS): Promise<T> {
|
|
381
|
+
if (pendingMutations >= MAX_PENDING_OAUTH_MUTATIONS) return Promise.reject(new OAuthMutationBusyError());
|
|
382
|
+
pendingMutations += 1;
|
|
383
|
+
const retainedBytes = retainedClosureStringBytes(retainedValues);
|
|
384
|
+
mutationCurrentBytes += retainedBytes;
|
|
385
|
+
mutationHighWaterBytes = Math.max(mutationHighWaterBytes, mutationCurrentBytes);
|
|
386
|
+
|
|
387
|
+
let resolveResult!: (value: T | PromiseLike<T>) => void;
|
|
388
|
+
let rejectResult!: (reason?: unknown) => void;
|
|
389
|
+
const result = new Promise<T>((resolve, reject) => {
|
|
390
|
+
resolveResult = resolve;
|
|
391
|
+
rejectResult = reject;
|
|
392
|
+
});
|
|
393
|
+
const entry: QueuedOAuthMutation = {
|
|
394
|
+
started: false,
|
|
395
|
+
settled: false,
|
|
396
|
+
async run() {
|
|
397
|
+
try {
|
|
398
|
+
resolveResult(await work());
|
|
399
|
+
} catch (error) {
|
|
400
|
+
rejectResult(error);
|
|
401
|
+
} finally {
|
|
402
|
+
release();
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
const release = () => {
|
|
407
|
+
if (entry.settled) return;
|
|
408
|
+
entry.settled = true;
|
|
409
|
+
pendingMutations -= 1;
|
|
410
|
+
mutationCurrentBytes = Math.max(0, mutationCurrentBytes - retainedBytes);
|
|
411
|
+
};
|
|
412
|
+
entry.timeout = setTimeout(() => {
|
|
413
|
+
if (entry.started || entry.settled) return;
|
|
414
|
+
const index = mutationWaiters.indexOf(entry);
|
|
415
|
+
if (index >= 0) mutationWaiters.splice(index, 1);
|
|
416
|
+
release();
|
|
417
|
+
rejectResult(new OAuthMutationBusyError("OAuth mutation queue wait timed out"));
|
|
418
|
+
}, waitMs);
|
|
419
|
+
// Only unref the long default wait. Short waitMs (tests) must stay ref'd:
|
|
420
|
+
// on Windows Bun under `bun test --isolate`, an unref'd timer can fail to
|
|
421
|
+
// fire while the head mutation holds an unresolved Promise, hanging the
|
|
422
|
+
// waiter forever (#827 / full admission queue hang on windows-latest).
|
|
423
|
+
if (waitMs >= OAUTH_MUTATION_WAIT_MS) entry.timeout.unref?.();
|
|
424
|
+
mutationWaiters.push(entry);
|
|
425
|
+
drainOAuthMutations();
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValues: readonly unknown[] = [], options?: { waitMs?: number }):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
|
|
300
429
|
const { store, hadLegacy } = loadAuthStoreInternal();
|
|
301
430
|
if (hadLegacy) backupLegacyOnce();
|
|
302
431
|
const result = await fn(store);
|
|
303
432
|
persist(store);
|
|
304
433
|
return result;
|
|
305
|
-
}finally{guard.release();}});
|
|
434
|
+
}finally{guard.release();}}, retainedValues, options?.waitMs);
|
|
306
435
|
}
|
|
307
436
|
|
|
308
437
|
/** The ACTIVE account's credential for a provider (what requests should use). */
|
|
@@ -366,7 +495,7 @@ export async function saveCredential(
|
|
|
366
495
|
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
367
496
|
set.activeAccountId = id;
|
|
368
497
|
}
|
|
369
|
-
});
|
|
498
|
+
}, [provider, safe]);
|
|
370
499
|
}
|
|
371
500
|
|
|
372
501
|
/** Remove the ACTIVE account; remaining accounts promote the first one. */
|
|
@@ -380,7 +509,7 @@ export async function removeCredential(provider: string): Promise<void> {
|
|
|
380
509
|
return;
|
|
381
510
|
}
|
|
382
511
|
set.activeAccountId = set.accounts[0]!.id;
|
|
383
|
-
});
|
|
512
|
+
}, [provider]);
|
|
384
513
|
}
|
|
385
514
|
|
|
386
515
|
// ---------------------------------------------------------------------------
|
|
@@ -395,6 +524,17 @@ export function listAccounts(provider: string): ProviderAccount[] {
|
|
|
395
524
|
return loadAuthStore()[provider]?.accounts ?? [];
|
|
396
525
|
}
|
|
397
526
|
|
|
527
|
+
export function listLiveOAuthAccountKeys(
|
|
528
|
+
providerNames: ReadonlySet<string>,
|
|
529
|
+
): ReadonlySet<string> {
|
|
530
|
+
const keys = new Set<string>();
|
|
531
|
+
for (const [provider, accountSet] of Object.entries(loadAuthStore())) {
|
|
532
|
+
if (!providerNames.has(provider)) continue;
|
|
533
|
+
for (const account of accountSet.accounts) keys.add(`${provider}\0${account.id}`);
|
|
534
|
+
}
|
|
535
|
+
return keys;
|
|
536
|
+
}
|
|
537
|
+
|
|
398
538
|
export function getAccountCredential(provider: string, accountId: string): OAuthCredentials | null {
|
|
399
539
|
return loadAuthStore()[provider]?.accounts.find(a => a.id === accountId)?.credential ?? null;
|
|
400
540
|
}
|
|
@@ -408,7 +548,7 @@ export async function saveAccountCredential(provider: string, accountId: string,
|
|
|
408
548
|
if (!account) return;
|
|
409
549
|
account.credential = safe;
|
|
410
550
|
delete account.needsReauth;
|
|
411
|
-
});
|
|
551
|
+
}, [provider, accountId, safe]);
|
|
412
552
|
}
|
|
413
553
|
|
|
414
554
|
export async function setActiveAccount(provider: string, accountId: string): Promise<boolean> {
|
|
@@ -417,7 +557,7 @@ export async function setActiveAccount(provider: string, accountId: string): Pro
|
|
|
417
557
|
if (!set || !set.accounts.some(a => a.id === accountId)) return false;
|
|
418
558
|
set.activeAccountId = accountId;
|
|
419
559
|
return true;
|
|
420
|
-
});
|
|
560
|
+
}, [provider, accountId]);
|
|
421
561
|
}
|
|
422
562
|
|
|
423
563
|
export async function setAccountAlias(provider: string, accountId: string, alias: string | undefined): Promise<boolean> {
|
|
@@ -427,12 +567,12 @@ export async function setAccountAlias(provider: string, accountId: string, alias
|
|
|
427
567
|
if (alias) account.alias = alias;
|
|
428
568
|
else delete account.alias;
|
|
429
569
|
return true;
|
|
430
|
-
});
|
|
570
|
+
}, [provider, accountId, alias]);
|
|
431
571
|
}
|
|
432
572
|
|
|
433
573
|
/** Remove one account by id; active removal promotes the first remaining account. */
|
|
434
574
|
export async function removeAccount(provider: string, accountId: string): Promise<boolean> {
|
|
435
|
-
|
|
575
|
+
const removed = await mutateStore(store => {
|
|
436
576
|
const set = store[provider];
|
|
437
577
|
if (!set) return false;
|
|
438
578
|
const before = set.accounts.length;
|
|
@@ -444,7 +584,8 @@ export async function removeAccount(provider: string, accountId: string): Promis
|
|
|
444
584
|
}
|
|
445
585
|
if (set.activeAccountId === accountId) set.activeAccountId = set.accounts[0]!.id;
|
|
446
586
|
return true;
|
|
447
|
-
});
|
|
587
|
+
}, [provider, accountId]);
|
|
588
|
+
return removed;
|
|
448
589
|
}
|
|
449
590
|
|
|
450
591
|
/** Replace or clear a provider account set (used for transactional Kiro add-account rollback). */
|
|
@@ -467,17 +608,23 @@ export async function replaceProviderAccountSet(
|
|
|
467
608
|
...(account.addedAt !== undefined ? { addedAt: account.addedAt } : {}),
|
|
468
609
|
})),
|
|
469
610
|
};
|
|
470
|
-
});
|
|
611
|
+
}, [provider, set]);
|
|
471
612
|
}
|
|
472
613
|
|
|
473
|
-
export async function markAccountNeedsReauth(
|
|
614
|
+
export async function markAccountNeedsReauth(
|
|
615
|
+
provider: string,
|
|
616
|
+
accountId: string,
|
|
617
|
+
needsReauth: boolean,
|
|
618
|
+
writerGeneration = captureConfigGeneration(),
|
|
619
|
+
): Promise<void> {
|
|
620
|
+
if (writerGeneration < lastReconciledGeneration && !liveOAuthAccountKeys.has(oauthAccountKey(provider, accountId))) return;
|
|
474
621
|
await mutateStore(store => {
|
|
475
622
|
const account = store[provider]?.accounts.find(a => a.id === accountId);
|
|
476
623
|
if (!account) return;
|
|
477
624
|
if (needsReauth) account.needsReauth = true;
|
|
478
625
|
else delete account.needsReauth;
|
|
479
|
-
});
|
|
626
|
+
}, [provider, accountId]);
|
|
480
627
|
}
|
|
481
628
|
|
|
482
|
-
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};});}
|
|
483
|
-
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;});}
|
|
629
|
+
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};},[provider,accountId,safe,opts.expectedGeneration]);}
|
|
630
|
+
export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration()):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;return true;},[provider,accountId,generation]);}
|
|
@@ -22,11 +22,14 @@ import {
|
|
|
22
22
|
markCodexAccountValidationFailed,
|
|
23
23
|
readCodexAccountRecord,
|
|
24
24
|
TokenRefreshError,
|
|
25
|
+
CodexCredentialRefreshBusyError,
|
|
26
|
+
CodexCredentialRefreshStaleError,
|
|
25
27
|
} from "../codex/account-store";
|
|
26
28
|
import { codexWarmupFailureReason, warmCodexAccount } from "../codex/warmup";
|
|
27
29
|
import { getMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
28
30
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
29
31
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
32
|
+
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
30
33
|
|
|
31
34
|
export interface TokenGuardianHandle {
|
|
32
35
|
stop(): void;
|
|
@@ -58,6 +61,8 @@ interface BackoffEntry {
|
|
|
58
61
|
|
|
59
62
|
// Module-scoped so backoff survives across sweeps within one process (keyed "oauth:<p>" / "codex:<id>").
|
|
60
63
|
const backoff = new Map<string, BackoffEntry>();
|
|
64
|
+
let lastReconciledGeneration = 0;
|
|
65
|
+
let liveBackoffKeys = new Set<string>();
|
|
61
66
|
|
|
62
67
|
/** Test hook: clear backoff state between cases. */
|
|
63
68
|
export function __resetGuardianState(): void {
|
|
@@ -87,7 +92,15 @@ function inBackoff(key: string, nowMs: number): boolean {
|
|
|
87
92
|
return entry !== undefined && entry.retryAfterMs > nowMs;
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
function recordFailure(
|
|
95
|
+
function recordFailure(
|
|
96
|
+
key: string,
|
|
97
|
+
nowMs: number,
|
|
98
|
+
baseSeconds: number,
|
|
99
|
+
maxSeconds: number,
|
|
100
|
+
permanent: boolean,
|
|
101
|
+
writerGeneration = captureConfigGeneration(),
|
|
102
|
+
): void {
|
|
103
|
+
if (writerGeneration < lastReconciledGeneration && !liveBackoffKeys.has(key)) return;
|
|
91
104
|
const prev = backoff.get(key);
|
|
92
105
|
const attempts = (prev?.attempts ?? 0) + 1;
|
|
93
106
|
// Permanent failures (revoked/expired refresh token) wait the full ceiling — nothing but a
|
|
@@ -116,6 +129,7 @@ async function runWithConcurrency(tasks: Array<() => Promise<void>>, limit: numb
|
|
|
116
129
|
* ids only, never tokens).
|
|
117
130
|
*/
|
|
118
131
|
export async function guardianSweep(nowMs: number = Date.now()): Promise<GuardianSweepResult> {
|
|
132
|
+
const writerGeneration = captureConfigGeneration();
|
|
119
133
|
const config: OcxConfig = loadConfig();
|
|
120
134
|
const g = config.tokenGuardian;
|
|
121
135
|
const result: GuardianSweepResult = { enabled: !!g?.enabled, refreshed: [], warmed: [], failed: [], skippedBackoff: [] };
|
|
@@ -143,7 +157,7 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
143
157
|
// Terminal grant failures surface as OAuthLoginRequiredError (account marked
|
|
144
158
|
// needsReauth by the resolver) — back off at the ceiling; transient errors backoff exponentially.
|
|
145
159
|
const permanent = err instanceof OAuthLoginRequiredError;
|
|
146
|
-
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, permanent);
|
|
160
|
+
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, permanent, writerGeneration);
|
|
147
161
|
result.failed.push(key);
|
|
148
162
|
}
|
|
149
163
|
});
|
|
@@ -174,7 +188,7 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
174
188
|
backoff.delete(key);
|
|
175
189
|
result.warmed.push(key);
|
|
176
190
|
} catch (err) {
|
|
177
|
-
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, false);
|
|
191
|
+
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, false, writerGeneration);
|
|
178
192
|
result.failed.push(key);
|
|
179
193
|
}
|
|
180
194
|
});
|
|
@@ -210,11 +224,16 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
210
224
|
}
|
|
211
225
|
backoff.delete(key);
|
|
212
226
|
} catch (err) {
|
|
227
|
+
if (err instanceof CodexCredentialRefreshBusyError || err instanceof CodexCredentialRefreshStaleError) {
|
|
228
|
+
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, false, writerGeneration);
|
|
229
|
+
result.skippedBackoff.push(key);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
213
232
|
const permanent = err instanceof TokenRefreshError && (err.reason === "revoked" || err.reason === "expired");
|
|
214
233
|
if (needsWarmup && !(err instanceof TokenRefreshError)) {
|
|
215
234
|
markCodexAccountValidationFailed(id, codexWarmupFailureReason(err));
|
|
216
235
|
}
|
|
217
|
-
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, permanent);
|
|
236
|
+
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, permanent, writerGeneration);
|
|
218
237
|
result.failed.push(key);
|
|
219
238
|
}
|
|
220
239
|
});
|
|
@@ -225,6 +244,26 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
225
244
|
return result;
|
|
226
245
|
}
|
|
227
246
|
|
|
247
|
+
export function reconcileGuardianBackoff(context: GenerationContext): number {
|
|
248
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
249
|
+
const valid = new Set<string>();
|
|
250
|
+
for (const id of context.codexAccountIds) valid.add(`codex:${id}`);
|
|
251
|
+
for (const key of context.oauthAccountKeys) {
|
|
252
|
+
const separator = key.indexOf("\0");
|
|
253
|
+
if (separator <= 0) continue;
|
|
254
|
+
valid.add(`oauth:${key.slice(0, separator)}:${key.slice(separator + 1)}`);
|
|
255
|
+
}
|
|
256
|
+
let removed = 0;
|
|
257
|
+
for (const key of backoff.keys()) {
|
|
258
|
+
if (valid.has(key)) continue;
|
|
259
|
+
backoff.delete(key);
|
|
260
|
+
removed += 1;
|
|
261
|
+
}
|
|
262
|
+
liveBackoffKeys = valid;
|
|
263
|
+
lastReconciledGeneration = context.generation;
|
|
264
|
+
return removed;
|
|
265
|
+
}
|
|
266
|
+
|
|
228
267
|
/**
|
|
229
268
|
* Start the background sweep loop. Returns a handle whose stop() clears the pending timer (in-flight
|
|
230
269
|
* refreshes settle on their own). Schedules recursively so each interval gets fresh jitter. The loop
|
package/src/oauth/types.ts
CHANGED
|
@@ -13,7 +13,8 @@ export interface KiroOAuthMetadata {
|
|
|
13
13
|
export type OAuthCredentials = {
|
|
14
14
|
refresh: string;
|
|
15
15
|
access: string;
|
|
16
|
-
|
|
16
|
+
/** Epoch ms after any small provider-specific early-refresh margin; the shared gate adds 1 minute. */
|
|
17
|
+
expires: number;
|
|
17
18
|
email?: string;
|
|
18
19
|
accountId?: string;
|
|
19
20
|
source?: OAuthCredentialSource;
|
|
@@ -39,6 +39,16 @@ export const ALIBABA_INTL_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
|
|
|
39
39
|
{ id: "custom", label: "Custom" },
|
|
40
40
|
];
|
|
41
41
|
|
|
42
|
+
/** Alibaba Coding Plan endpoint presets (international default; China mainland selectable). */
|
|
43
|
+
export const ALIBABA_CODING_INTL_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
|
|
44
|
+
export const ALIBABA_CODING_CN_BASE_URL = "https://coding.dashscope.aliyuncs.com/v1";
|
|
45
|
+
|
|
46
|
+
export const ALIBABA_CODING_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
|
|
47
|
+
{ id: "intl", label: "International", baseUrl: ALIBABA_CODING_INTL_BASE_URL },
|
|
48
|
+
{ id: "china", label: "China", baseUrl: ALIBABA_CODING_CN_BASE_URL },
|
|
49
|
+
{ id: "custom", label: "Custom" },
|
|
50
|
+
];
|
|
51
|
+
|
|
42
52
|
/** Match a saved baseUrl to a known choice id (`custom` when it does not match). */
|
|
43
53
|
export function matchBaseUrlChoice(
|
|
44
54
|
choices: readonly ProviderBaseUrlChoice[],
|
package/src/providers/derive.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { PROVIDER_REGISTRY, providerMatchesRegistryTransport, type ProviderRegis
|
|
|
4
4
|
export interface DerivedKeyLoginProvider {
|
|
5
5
|
label: string;
|
|
6
6
|
baseUrl: string;
|
|
7
|
+
responsesPath?: string;
|
|
7
8
|
adapter: string;
|
|
8
9
|
apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
|
|
9
10
|
dashboardUrl: string;
|
|
@@ -53,6 +54,7 @@ export interface DerivedProviderPreset {
|
|
|
53
54
|
label: string;
|
|
54
55
|
adapter: string;
|
|
55
56
|
baseUrl: string;
|
|
57
|
+
responsesPath?: string;
|
|
56
58
|
defaultModel?: string;
|
|
57
59
|
auth: "oauth" | "forward" | "key" | "local";
|
|
58
60
|
codexAccountMode?: CodexAccountMode;
|
|
@@ -105,6 +107,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
105
107
|
adapter: entry.adapter,
|
|
106
108
|
baseUrl: entry.baseUrl,
|
|
107
109
|
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
|
|
110
|
+
...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
|
|
108
111
|
authMode: entry.authKind === "local" ? undefined : entry.authKind,
|
|
109
112
|
...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
|
|
110
113
|
...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
|
|
@@ -132,6 +135,8 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
132
135
|
...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
|
|
133
136
|
...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}),
|
|
134
137
|
...(entry.promptCacheKey !== undefined ? { promptCacheKey: entry.promptCacheKey } : {}),
|
|
138
|
+
...(entry.responsesPath !== undefined ? { responsesPath: entry.responsesPath } : {}),
|
|
139
|
+
...(entry.statelessResponses !== undefined ? { statelessResponses: entry.statelessResponses } : {}),
|
|
135
140
|
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
|
|
136
141
|
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
|
|
137
142
|
...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
|
|
@@ -152,6 +157,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
152
157
|
out[entry.id] = {
|
|
153
158
|
label: entry.label,
|
|
154
159
|
baseUrl: entry.baseUrl,
|
|
160
|
+
...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
|
|
155
161
|
adapter: entry.adapter,
|
|
156
162
|
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
|
|
157
163
|
dashboardUrl: entry.dashboardUrl,
|
|
@@ -227,6 +233,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
227
233
|
const seed = providerConfigSeed(entry);
|
|
228
234
|
if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
|
|
229
235
|
if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
|
|
236
|
+
if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
|
|
230
237
|
// Fill mode only when absent: an explicit persisted `direct` must never be overwritten.
|
|
231
238
|
if (prov.codexAccountMode === undefined && seed.codexAccountMode !== undefined) prov.codexAccountMode = seed.codexAccountMode;
|
|
232
239
|
if (!prov.models && seed.models) prov.models = [...seed.models];
|
|
@@ -248,6 +255,10 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
248
255
|
if (!prov.noPenaltyModels && seed.noPenaltyModels) prov.noPenaltyModels = [...seed.noPenaltyModels];
|
|
249
256
|
if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls;
|
|
250
257
|
if (prov.promptCacheKey === undefined && seed.promptCacheKey !== undefined) prov.promptCacheKey = seed.promptCacheKey;
|
|
258
|
+
// Fill-only: a hand-edited path must survive, and a config saved before the registry
|
|
259
|
+
// learned this route still gets backfilled.
|
|
260
|
+
if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
|
|
261
|
+
if (prov.statelessResponses === undefined && seed.statelessResponses !== undefined) prov.statelessResponses = seed.statelessResponses;
|
|
251
262
|
if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
|
|
252
263
|
if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
|
|
253
264
|
if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels];
|
|
@@ -287,6 +298,7 @@ function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
|
|
|
287
298
|
label: entry.label,
|
|
288
299
|
adapter: entry.adapter,
|
|
289
300
|
baseUrl: entry.baseUrl,
|
|
301
|
+
...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
|
|
290
302
|
auth: entry.authKind === "forward" ? "forward" : entry.authKind === "oauth" ? "oauth" : entry.authKind === "local" ? "local" : "key",
|
|
291
303
|
...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
|
|
292
304
|
...(entry.codexAccountMode ? { provider: providerConfigSeed(entry) } : {}),
|
|
@@ -18,7 +18,7 @@ export const FREE_PROVIDER_ACCESS_GROUPS = {
|
|
|
18
18
|
],
|
|
19
19
|
"recurring-credit": ["bytez", "nous-research"],
|
|
20
20
|
"signup-credit": [
|
|
21
|
-
"agentrouter", "ai21", "baichuan", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
|
|
21
|
+
"agentrouter", "ai21", "baichuan", "baseten", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
|
|
22
22
|
"hyperbolic", "longcat", "monsterapi", "nebius", "novita", "nscale", "nvidia", "predibase", "publicai", "qoder",
|
|
23
23
|
"scaleway", "sensenova", "stepfun", "together", "vertex",
|
|
24
24
|
],
|
|
@@ -119,6 +119,9 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
|
|
|
119
119
|
agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true },
|
|
120
120
|
ai21: openAi("https://api.ai21.com/studio/v1", "https://studio.ai21.com/account/api-key", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.ai21.com/reference/models" }),
|
|
121
121
|
baichuan: openAi("https://api.baichuan-ai.com/v1", "https://platform.baichuan-ai.com/console/apikey", { verification: "official" }),
|
|
122
|
+
// Verified end-to-end 2026-07-30: /v1/models returns the OpenAI-shaped live catalog (13 models),
|
|
123
|
+
// and a chat completion against moonshotai/Kimi-K3 returned a standard chat.completion payload.
|
|
124
|
+
baseten: openAi("https://inference.baseten.co/v1", "https://app.baseten.co/settings/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.baseten.co/inference/model-apis/overview", modelsUrl: "https://inference.baseten.co/v1/models", lastVerified: "2026-07-30" }),
|
|
122
125
|
deepinfra: openAi("https://api.deepinfra.com/v1/openai", "https://deepinfra.com/dash/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://deepinfra.com/docs/openai_api" }),
|
|
123
126
|
deepseek: openAi("https://api.deepseek.com", "https://platform.deepseek.com/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://api-docs.deepseek.com/api/list-models" }),
|
|
124
127
|
doubao: openAi("https://ark.cn-beijing.volces.com/api/v3", "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey", { verification: "official" }),
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { saveConfigPreservingClaudeCode } from "../config";
|
|
12
12
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
13
13
|
import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
|
|
14
|
+
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
14
15
|
|
|
15
16
|
// ---- cooldown state (in-memory, same as codex/routing.ts) ----
|
|
16
17
|
|
|
@@ -99,6 +100,7 @@ export function rotateKeyOn429(
|
|
|
99
100
|
keyCooldowns.set(cooldownKey(providerName, currentEntry.id), {
|
|
100
101
|
cooldownUntil: now + cooldownMs,
|
|
101
102
|
});
|
|
103
|
+
sweepExpiredOnWrite(now);
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
// Lost the race: someone already rotated away from the failed key. If the live key is healthy,
|
|
@@ -131,6 +133,16 @@ export function rotateKeyOn429(
|
|
|
131
133
|
return null;
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
export function sweepExpiredApiKeyCooldowns(now = Date.now()): number {
|
|
137
|
+
let removed = 0;
|
|
138
|
+
for (const [key, cooldown] of keyCooldowns) {
|
|
139
|
+
if (cooldown.cooldownUntil > now) continue;
|
|
140
|
+
keyCooldowns.delete(key);
|
|
141
|
+
removed += 1;
|
|
142
|
+
}
|
|
143
|
+
return removed;
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
interface RotateProviderTransportOptions {
|
|
135
147
|
retryAfter?: string | null;
|
|
136
148
|
now?: number;
|
|
@@ -101,7 +101,10 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
101
101
|
config,
|
|
102
102
|
authContext.accountId,
|
|
103
103
|
outcome,
|
|
104
|
-
{
|
|
104
|
+
{
|
|
105
|
+
probeLeaseId: authContext.probeLeaseId,
|
|
106
|
+
writerGeneration: authContext.writerGeneration,
|
|
107
|
+
},
|
|
105
108
|
),
|
|
106
109
|
}
|
|
107
110
|
: {}),
|