@bitkyc08/opencodex 2.10.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/providers/quota.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
effectiveCodexAuthAccountId,
|
|
4
|
+
fetchMainAccountInfoSnapshot,
|
|
5
|
+
listCodexAuthAccountsSnapshot,
|
|
6
|
+
} from "../codex/auth-api";
|
|
7
|
+
import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
|
|
2
8
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
3
9
|
import { resolveEnvValue } from "../config";
|
|
4
10
|
import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
|
|
5
11
|
import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
|
|
6
12
|
import { antigravityUserAgent } from "../adapters/client-fingerprint";
|
|
13
|
+
import { apiKeyPoolEntryId } from "./api-keys";
|
|
7
14
|
import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
|
|
8
15
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
9
16
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
|
|
@@ -12,6 +19,12 @@ import {
|
|
|
12
19
|
sweepExpiredOnWrite,
|
|
13
20
|
type GenerationContext,
|
|
14
21
|
} from "../lib/state-store-sweeper";
|
|
22
|
+
import {
|
|
23
|
+
aggregateCodexPoolCapacity,
|
|
24
|
+
CODEX_CAPACITY_MAX_QUOTA_AGE_MS,
|
|
25
|
+
type CodexCapacityAggregation,
|
|
26
|
+
type CodexCapacityQuota,
|
|
27
|
+
} from "./codex-capacity";
|
|
15
28
|
|
|
16
29
|
/** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
|
|
17
30
|
const ACCOUNT_TOKEN_SKEW_MS = 60_000;
|
|
@@ -20,8 +33,20 @@ const CACHE_TTL_MS = 5 * 60_000;
|
|
|
20
33
|
const REQUEST_TIMEOUT_MS = 8_000;
|
|
21
34
|
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
22
35
|
const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
|
|
36
|
+
const A6API_BASE_URL = "https://api.a6api.com";
|
|
23
37
|
/** Keep a failed probe's previous row at most this long before dropping it. */
|
|
24
|
-
const LAST_GOOD_MAX_AGE_MS =
|
|
38
|
+
const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
39
|
+
const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
|
|
40
|
+
let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
|
|
41
|
+
|
|
42
|
+
/** Test-only seam for identity/config invalidation after probes but before publication. */
|
|
43
|
+
export function setProviderQuotaBeforePublishForTests(
|
|
44
|
+
hook: (() => void | Promise<void>) | null,
|
|
45
|
+
): void {
|
|
46
|
+
providerQuotaBeforePublishForTests = hook;
|
|
47
|
+
}
|
|
48
|
+
const TERMINAL_QUOTA_FAILURE = Symbol("terminal-quota-failure");
|
|
49
|
+
type ProviderQuotaProbeResult = ProviderQuotaReport | null | typeof TERMINAL_QUOTA_FAILURE;
|
|
25
50
|
|
|
26
51
|
export interface ProviderQuotaWindow {
|
|
27
52
|
label: string;
|
|
@@ -47,6 +72,7 @@ export interface ProviderQuotaReport {
|
|
|
47
72
|
quota: ProviderQuota;
|
|
48
73
|
updatedAt: number;
|
|
49
74
|
reverseEngineered?: boolean;
|
|
75
|
+
aggregation?: CodexCapacityAggregation;
|
|
50
76
|
}
|
|
51
77
|
|
|
52
78
|
export interface ProviderQuotaResponse {
|
|
@@ -67,10 +93,99 @@ export function clearProviderQuotaCache(): void {
|
|
|
67
93
|
|
|
68
94
|
function cacheKey(config: OcxConfig): string {
|
|
69
95
|
const providers = Object.entries(config.providers)
|
|
70
|
-
.map(([name, provider]) =>
|
|
96
|
+
.map(([name, provider]) => {
|
|
97
|
+
const resolvedKey = typeof provider.apiKey === "string"
|
|
98
|
+
? resolveEnvValue(provider.apiKey)?.trim()
|
|
99
|
+
: undefined;
|
|
100
|
+
const activeKeyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "none";
|
|
101
|
+
return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}`;
|
|
102
|
+
})
|
|
71
103
|
.sort()
|
|
72
104
|
.join("|");
|
|
73
|
-
return `${config.defaultProvider}|${
|
|
105
|
+
return `${config.defaultProvider}|${providers}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type CodexAuthAccountsSnapshotPromise = ReturnType<typeof listCodexAuthAccountsSnapshot>;
|
|
109
|
+
|
|
110
|
+
function hasCodexPoolProvider(config: OcxConfig): boolean {
|
|
111
|
+
return Object.entries(config.providers).some(([name, provider]) => (
|
|
112
|
+
provider.disabled !== true
|
|
113
|
+
&& isBuiltInChatGptForwardProvider(name, provider)
|
|
114
|
+
&& providerCodexAccountMode(name, provider) !== "direct"
|
|
115
|
+
));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
|
|
119
|
+
if (!quota) return null;
|
|
120
|
+
return {
|
|
121
|
+
fiveHourPercent: quota.fiveHourPercent,
|
|
122
|
+
fiveHourResetAt: quota.fiveHourResetAt,
|
|
123
|
+
weeklyPercent: quota.weeklyPercent,
|
|
124
|
+
weeklyResetAt: quota.weeklyResetAt,
|
|
125
|
+
monthlyPercent: quota.monthlyPercent,
|
|
126
|
+
monthlyResetAt: quota.monthlyResetAt,
|
|
127
|
+
updatedAt: quota.updatedAt,
|
|
128
|
+
customWindows: [...(quota.customWindows ?? [])]
|
|
129
|
+
.map(window => ({ label: window.label, percent: window.percent, resetAt: window.resetAt }))
|
|
130
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
|
|
135
|
+
function cacheKeyWithAggregationState(
|
|
136
|
+
config: OcxConfig,
|
|
137
|
+
prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
|
|
138
|
+
): string | Promise<string> {
|
|
139
|
+
const base = cacheKey(config);
|
|
140
|
+
if (!hasCodexPoolProvider(config)) return base;
|
|
141
|
+
return (async () => {
|
|
142
|
+
try {
|
|
143
|
+
const activeId = effectiveCodexAuthAccountId(config);
|
|
144
|
+
const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, false));
|
|
145
|
+
const rows = snapshot.accounts.map(account => ({
|
|
146
|
+
isMain: account.isMain,
|
|
147
|
+
active: account.id === activeId,
|
|
148
|
+
plan: account.plan?.trim().toLowerCase() ?? null,
|
|
149
|
+
paused: account.paused,
|
|
150
|
+
needsReauth: account.needsReauth === true,
|
|
151
|
+
quota: quotaSignatureValue(account.quota as CodexCapacityQuota | null),
|
|
152
|
+
}));
|
|
153
|
+
const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
|
|
154
|
+
const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
|
|
155
|
+
return `${base}|codex-pool:${digest}`;
|
|
156
|
+
} catch {
|
|
157
|
+
return `${base}|codex-pool:unavailable`;
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function publicCapacityWindow(window: import("./codex-capacity").CodexCapacityWindowAggregation) {
|
|
163
|
+
const { totalWeight: _totalWeight, consumedWeight: _consumedWeight, remainingWeight: _remainingWeight, ...safe } = window;
|
|
164
|
+
return safe;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Management API metadata intentionally omits configured/weighted unit counts. */
|
|
168
|
+
function publicCapacityAggregation(
|
|
169
|
+
aggregation: CodexCapacityAggregation,
|
|
170
|
+
presentation: NonNullable<CodexCapacityAggregation["presentation"]>,
|
|
171
|
+
): CodexCapacityAggregation {
|
|
172
|
+
const safeCurrentAccount = presentation === "coverage-only" && aggregation.currentAccount
|
|
173
|
+
? { ...aggregation.currentAccount, quota: null }
|
|
174
|
+
: aggregation.currentAccount;
|
|
175
|
+
return {
|
|
176
|
+
...aggregation,
|
|
177
|
+
presentation,
|
|
178
|
+
...(safeCurrentAccount ? { currentAccount: safeCurrentAccount } : {}),
|
|
179
|
+
...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
|
|
180
|
+
...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
|
|
181
|
+
...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
|
|
182
|
+
...(aggregation.customWindows ? {
|
|
183
|
+
customWindows: aggregation.customWindows.map(window => ({
|
|
184
|
+
label: window.label,
|
|
185
|
+
...publicCapacityWindow(window),
|
|
186
|
+
})),
|
|
187
|
+
} : {}),
|
|
188
|
+
};
|
|
74
189
|
}
|
|
75
190
|
|
|
76
191
|
function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
|
|
@@ -123,7 +238,83 @@ function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConf
|
|
|
123
238
|
return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
|
|
124
239
|
}
|
|
125
240
|
|
|
126
|
-
function
|
|
241
|
+
function isCanonicalA6apiBaseUrl(baseUrl: string): boolean {
|
|
242
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
243
|
+
return normalized === A6API_BASE_URL || normalized === `${A6API_BASE_URL}/v1`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function a6apiPayload(value: unknown): Record<string, unknown> | null {
|
|
247
|
+
const body = asRecord(value);
|
|
248
|
+
return asRecord(body?.data) ?? body;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function firstFinite(record: Record<string, unknown> | null, names: string[]): number | undefined {
|
|
252
|
+
if (!record) return undefined;
|
|
253
|
+
for (const name of names) {
|
|
254
|
+
const value = toFiniteNumber(record[name]);
|
|
255
|
+
if (value !== undefined) return value;
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
261
|
+
// Never send a configured API key to a lookalike host or through a redirect.
|
|
262
|
+
if (!isCanonicalA6apiBaseUrl(config.baseUrl)) return null;
|
|
263
|
+
const apiKey = resolveEnvValue(config.apiKey)?.trim();
|
|
264
|
+
if (!apiKey) return null;
|
|
265
|
+
const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
|
|
266
|
+
const [subscriptionResponse, tokenResponse] = await Promise.all([
|
|
267
|
+
fetch(`${A6API_BASE_URL}/dashboard/billing/subscription`, {
|
|
268
|
+
headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
269
|
+
}),
|
|
270
|
+
fetch(`${A6API_BASE_URL}/api/usage/token/`, {
|
|
271
|
+
headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
272
|
+
}),
|
|
273
|
+
]);
|
|
274
|
+
if (!subscriptionResponse.ok || !tokenResponse.ok) {
|
|
275
|
+
const statuses = [subscriptionResponse.status, tokenResponse.status];
|
|
276
|
+
// 408/429 are transient (timeout/throttle), not invalid-account signals: keep the
|
|
277
|
+
// last-good row like 5xx/network failures. 401/403 (bad key) and 404 (contract change)
|
|
278
|
+
// stay terminal.
|
|
279
|
+
return statuses.some(status => status >= 400 && status < 500 && status !== 429 && status !== 408)
|
|
280
|
+
? TERMINAL_QUOTA_FAILURE
|
|
281
|
+
: null;
|
|
282
|
+
}
|
|
283
|
+
const subscription = a6apiPayload(await subscriptionResponse.json().catch(() => null));
|
|
284
|
+
const token = a6apiPayload(await tokenResponse.json().catch(() => null));
|
|
285
|
+
const limitUsd = firstFinite(subscription, ["hard_limit_usd"]);
|
|
286
|
+
const grantedUnits = firstFinite(token, ["total_granted"]);
|
|
287
|
+
const usedUnits = firstFinite(token, ["total_used"]);
|
|
288
|
+
const availableUnits = firstFinite(token, ["total_available"]);
|
|
289
|
+
const reconciledUnits = usedUnits !== undefined && availableUnits !== undefined
|
|
290
|
+
? usedUnits + availableUnits
|
|
291
|
+
: undefined;
|
|
292
|
+
const reconciliationTolerance = grantedUnits !== undefined
|
|
293
|
+
? Math.abs(grantedUnits) * 1e-9
|
|
294
|
+
: 0;
|
|
295
|
+
if (limitUsd === undefined || grantedUnits === undefined || usedUnits === undefined
|
|
296
|
+
|| availableUnits === undefined || limitUsd <= 0 || grantedUnits <= 0
|
|
297
|
+
|| usedUnits < 0 || availableUnits < 0
|
|
298
|
+
|| reconciledUnits === undefined
|
|
299
|
+
|| Math.abs(reconciledUnits - grantedUnits) > reconciliationTolerance) return TERMINAL_QUOTA_FAILURE;
|
|
300
|
+
const usdPerUnit = limitUsd / grantedUnits;
|
|
301
|
+
const usedUsd = usedUnits * usdPerUnit;
|
|
302
|
+
const remainingUsd = Math.max(0, availableUnits * usdPerUnit);
|
|
303
|
+
const percent = normalizePercent((usedUsd / limitUsd) * 100);
|
|
304
|
+
if (percent === undefined) return TERMINAL_QUOTA_FAILURE;
|
|
305
|
+
const label = `API credits ($${remainingUsd.toFixed(2)} of $${limitUsd.toFixed(2)} remaining)`;
|
|
306
|
+
return report(provider, "a6api:billing", {
|
|
307
|
+
customWindows: [{ label, percent }],
|
|
308
|
+
updatedAt: Date.now(),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function report(
|
|
313
|
+
provider: string,
|
|
314
|
+
source: string,
|
|
315
|
+
quota: ProviderQuota,
|
|
316
|
+
aggregation?: CodexCapacityAggregation,
|
|
317
|
+
): ProviderQuotaReport | null {
|
|
127
318
|
if (!hasQuotaRows(quota)) return null;
|
|
128
319
|
return {
|
|
129
320
|
provider,
|
|
@@ -131,27 +322,92 @@ function report(provider: string, source: string, quota: ProviderQuota): Provide
|
|
|
131
322
|
source,
|
|
132
323
|
quota,
|
|
133
324
|
updatedAt: quota.updatedAt,
|
|
325
|
+
...(aggregation ? { aggregation } : {}),
|
|
134
326
|
};
|
|
135
327
|
}
|
|
136
328
|
|
|
329
|
+
function tagNativeMainReport(
|
|
330
|
+
value: ProviderQuotaReport | null,
|
|
331
|
+
generation: number,
|
|
332
|
+
): ProviderQuotaReport | null {
|
|
333
|
+
if (value) nativeMainReportGenerations.set(value, generation);
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolean {
|
|
338
|
+
const generation = nativeMainReportGenerations.get(value);
|
|
339
|
+
return generation === undefined || isMainAccountIdentityGenerationLive(generation);
|
|
340
|
+
}
|
|
341
|
+
|
|
137
342
|
async function fetchChatGptForwardQuota(
|
|
138
343
|
config: OcxConfig,
|
|
139
344
|
provider: string,
|
|
140
345
|
providerConfig: OcxProviderConfig,
|
|
141
346
|
forceRefresh: boolean,
|
|
347
|
+
prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
|
|
142
348
|
): Promise<ProviderQuotaReport | null> {
|
|
143
349
|
if (providerCodexAccountMode(provider, providerConfig) === "direct") {
|
|
144
|
-
const
|
|
145
|
-
const quota =
|
|
146
|
-
|
|
350
|
+
const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
|
|
351
|
+
const quota = snapshot.info.quota
|
|
352
|
+
? { ...snapshot.info.quota, updatedAt: Date.now() } as ProviderQuota
|
|
353
|
+
: null;
|
|
354
|
+
return quota
|
|
355
|
+
? tagNativeMainReport(report(provider, "chatgpt:wham", quota), snapshot.mainIdentityGeneration)
|
|
356
|
+
: null;
|
|
147
357
|
}
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
358
|
+
const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, forceRefresh));
|
|
359
|
+
const accounts = snapshot.accounts;
|
|
360
|
+
const activeId = effectiveCodexAuthAccountId(config);
|
|
361
|
+
const capacityAccounts = accounts.map(account => ({ ...account, active: account.id === activeId }));
|
|
362
|
+
const active = capacityAccounts.find(account => account.active)
|
|
151
363
|
?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
|
|
152
364
|
?? accounts[0];
|
|
153
|
-
const
|
|
154
|
-
|
|
365
|
+
const now = Date.now();
|
|
366
|
+
const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
|
|
367
|
+
if (capacity.aggregation && capacity.quota) {
|
|
368
|
+
return tagNativeMainReport(
|
|
369
|
+
report(
|
|
370
|
+
provider,
|
|
371
|
+
"chatgpt:wham",
|
|
372
|
+
capacity.quota as ProviderQuota,
|
|
373
|
+
publicCapacityAggregation(capacity.aggregation, "aggregate"),
|
|
374
|
+
),
|
|
375
|
+
snapshot.mainIdentityGeneration,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const activeUsable = !!active && !active.paused && active.needsReauth !== true;
|
|
379
|
+
const quota = activeUsable && active?.quota
|
|
380
|
+
? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
|
|
381
|
+
: null;
|
|
382
|
+
const quotaFresh = !!quota
|
|
383
|
+
&& Number.isFinite(quota.updatedAt)
|
|
384
|
+
&& now - quota.updatedAt < CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
385
|
+
if (quota && quotaFresh) {
|
|
386
|
+
const fallback = report(
|
|
387
|
+
provider,
|
|
388
|
+
"chatgpt:wham",
|
|
389
|
+
quota as ProviderQuota,
|
|
390
|
+
capacity.aggregation
|
|
391
|
+
? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
|
|
392
|
+
: undefined,
|
|
393
|
+
);
|
|
394
|
+
return tagNativeMainReport(fallback, snapshot.mainIdentityGeneration);
|
|
395
|
+
}
|
|
396
|
+
if (capacity.aggregation) {
|
|
397
|
+
const updatedAt = Date.now();
|
|
398
|
+
return tagNativeMainReport(
|
|
399
|
+
{
|
|
400
|
+
provider,
|
|
401
|
+
label: providerLabel(provider),
|
|
402
|
+
source: "chatgpt:wham",
|
|
403
|
+
quota: { updatedAt },
|
|
404
|
+
updatedAt,
|
|
405
|
+
aggregation: publicCapacityAggregation(capacity.aggregation, "coverage-only"),
|
|
406
|
+
},
|
|
407
|
+
snapshot.mainIdentityGeneration,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
155
411
|
}
|
|
156
412
|
|
|
157
413
|
function centsValue(value: unknown): number | undefined {
|
|
@@ -371,6 +627,13 @@ export function reconcileProviderAccountQuotaRows(context: GenerationContext): n
|
|
|
371
627
|
return removed;
|
|
372
628
|
}
|
|
373
629
|
|
|
630
|
+
/** Test-only reset so a direct reconcile call in one file cannot leak across files. */
|
|
631
|
+
export function resetProviderQuotaReconcileStateForTests(): void {
|
|
632
|
+
lastReconciledGeneration = 0;
|
|
633
|
+
liveAccountQuotaKeys = new Set();
|
|
634
|
+
liveProviderQuotaKeys = new Set();
|
|
635
|
+
}
|
|
636
|
+
|
|
374
637
|
/** Drop cached per-account rows (all, or just one provider's). */
|
|
375
638
|
export function clearAccountQuotaCache(provider?: string): void {
|
|
376
639
|
if (!provider) {
|
|
@@ -489,7 +752,7 @@ export async function fetchProviderAccountQuotas(
|
|
|
489
752
|
function normalizedBaseUrl(value: string): string | null {
|
|
490
753
|
try {
|
|
491
754
|
const url = new URL(value);
|
|
492
|
-
if (url.search || url.hash) return null;
|
|
755
|
+
if (url.username || url.password || url.search || url.hash) return null;
|
|
493
756
|
return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
|
|
494
757
|
} catch {
|
|
495
758
|
return null;
|
|
@@ -896,10 +1159,13 @@ async function maybeFetchProviderQuota(
|
|
|
896
1159
|
provider: OcxProviderConfig,
|
|
897
1160
|
config: OcxConfig,
|
|
898
1161
|
forceRefresh: boolean,
|
|
899
|
-
|
|
1162
|
+
prefetchedCodexSnapshot?: CodexAuthAccountsSnapshotPromise,
|
|
1163
|
+
): Promise<ProviderQuotaProbeResult> {
|
|
900
1164
|
if (provider.disabled === true) return null;
|
|
901
1165
|
try {
|
|
902
|
-
if (isBuiltInChatGptForwardProvider(name, provider))
|
|
1166
|
+
if (isBuiltInChatGptForwardProvider(name, provider)) {
|
|
1167
|
+
return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexSnapshot);
|
|
1168
|
+
}
|
|
903
1169
|
if (provider.authMode === "oauth" && name === "xai") return fetchXaiQuota(name);
|
|
904
1170
|
if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
|
|
905
1171
|
if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
|
|
@@ -910,6 +1176,9 @@ async function maybeFetchProviderQuota(
|
|
|
910
1176
|
if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
|
|
911
1177
|
return fetchKimiQuota(name, provider);
|
|
912
1178
|
}
|
|
1179
|
+
if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {
|
|
1180
|
+
return fetchA6apiQuota(name, provider);
|
|
1181
|
+
}
|
|
913
1182
|
return null;
|
|
914
1183
|
} catch {
|
|
915
1184
|
return null;
|
|
@@ -917,13 +1186,20 @@ async function maybeFetchProviderQuota(
|
|
|
917
1186
|
}
|
|
918
1187
|
|
|
919
1188
|
export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
|
|
920
|
-
|
|
1189
|
+
// A Pool report's cache signature and provider fetch must share one account snapshot.
|
|
1190
|
+
// Preserve force semantics when deciding whether that snapshot refreshes upstream data.
|
|
1191
|
+
const prefetchedCodexSnapshot = hasCodexPoolProvider(config)
|
|
1192
|
+
? listCodexAuthAccountsSnapshot(config, forceRefresh)
|
|
1193
|
+
: undefined;
|
|
1194
|
+
const keyCandidate = cacheKeyWithAggregationState(config, prefetchedCodexSnapshot);
|
|
1195
|
+
const key = typeof keyCandidate === "string" ? keyCandidate : await keyCandidate;
|
|
921
1196
|
const writerGeneration = captureConfigGeneration();
|
|
922
1197
|
const now = Date.now();
|
|
923
1198
|
// The cache fast path must not extend a preserved last-good row past its 30-minute bound:
|
|
924
1199
|
// a row preserved at age 29:59 plus a full 5-minute TTL would otherwise serve until ~35min.
|
|
925
1200
|
const cacheFresh = cache && cache.key === key && now - cache.ts < CACHE_TTL_MS
|
|
926
|
-
&& cache.response.reports.every(item =>
|
|
1201
|
+
&& cache.response.reports.every(item =>
|
|
1202
|
+
now - item.updatedAt < LAST_GOOD_MAX_AGE_MS && isProviderQuotaReportCurrent(item));
|
|
927
1203
|
if (!forceRefresh && cacheFresh) return cache!.response;
|
|
928
1204
|
const joinable = inflight.get(key);
|
|
929
1205
|
if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) return joinable.promise;
|
|
@@ -933,25 +1209,57 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
|
|
|
933
1209
|
|
|
934
1210
|
const promise = (async (): Promise<ProviderQuotaResponse> => {
|
|
935
1211
|
const previous = cache && cache.key === key ? cache.response.reports : [];
|
|
936
|
-
const
|
|
937
|
-
Object.entries(config.providers).map(([name, provider]) =>
|
|
938
|
-
|
|
1212
|
+
const probeResults = await Promise.all(
|
|
1213
|
+
Object.entries(config.providers).map(([name, provider]) => (
|
|
1214
|
+
maybeFetchProviderQuota(name, provider, config, forceRefresh, prefetchedCodexSnapshot)
|
|
1215
|
+
)),
|
|
1216
|
+
);
|
|
1217
|
+
const fresh = probeResults.filter((item): item is ProviderQuotaReport => item !== null && item !== TERMINAL_QUOTA_FAILURE);
|
|
1218
|
+
const terminalFailures = new Set(
|
|
1219
|
+
Object.keys(config.providers).filter((_, index) => probeResults[index] === TERMINAL_QUOTA_FAILURE),
|
|
1220
|
+
);
|
|
1221
|
+
await providerQuotaBeforePublishForTests?.();
|
|
1222
|
+
let commitKey: string | null = null;
|
|
1223
|
+
if (epoch === invalidationEpoch) {
|
|
1224
|
+
const commitKeyCandidate = cacheKeyWithAggregationState(config);
|
|
1225
|
+
commitKey = typeof commitKeyCandidate === "string" ? commitKeyCandidate : await commitKeyCandidate;
|
|
1226
|
+
}
|
|
939
1227
|
|
|
940
|
-
// Keep bounded last-good rows when a probe fails
|
|
941
|
-
//
|
|
1228
|
+
// Keep bounded last-good rows when a probe fails transiently; terminal-invalid provider
|
|
1229
|
+
// responses explicitly suppress their old row. Never re-stamp preserved timestamps.
|
|
942
1230
|
// Note: the cache key encodes the provider set (name/adapter/authMode/disabled/baseUrl),
|
|
943
1231
|
// so previous rows always correspond to currently configured, enabled providers — a
|
|
944
1232
|
// disabled or removed provider changes the key and starts from an empty previous set.
|
|
945
1233
|
const cutoff = Date.now() - LAST_GOOD_MAX_AGE_MS;
|
|
946
1234
|
const byProvider = new Map<string, ProviderQuotaReport>();
|
|
1235
|
+
const generationMismatchedProviders = new Set<string>();
|
|
947
1236
|
for (const item of previous) {
|
|
948
|
-
if (item.updatedAt
|
|
1237
|
+
if (item.updatedAt < cutoff) continue;
|
|
1238
|
+
if (isProviderQuotaReportCurrent(item)) byProvider.set(item.provider, item);
|
|
1239
|
+
else generationMismatchedProviders.add(item.provider);
|
|
1240
|
+
}
|
|
1241
|
+
for (const item of fresh) {
|
|
1242
|
+
if (isProviderQuotaReportCurrent(item)) {
|
|
1243
|
+
byProvider.set(item.provider, item);
|
|
1244
|
+
generationMismatchedProviders.delete(item.provider);
|
|
1245
|
+
} else {
|
|
1246
|
+
byProvider.delete(item.provider);
|
|
1247
|
+
generationMismatchedProviders.add(item.provider);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
// Terminal-invalid probes suppress their previous row (transient failures keep it).
|
|
1251
|
+
for (const provider of terminalFailures) {
|
|
1252
|
+
byProvider.delete(provider);
|
|
1253
|
+
generationMismatchedProviders.delete(provider);
|
|
949
1254
|
}
|
|
950
|
-
for (const item of fresh) byProvider.set(item.provider, item);
|
|
951
1255
|
|
|
952
1256
|
const response = { generatedAt: Date.now(), reports: [...byProvider.values()] };
|
|
953
1257
|
// Commit only when this probe still holds authority (no clear/force superseded it).
|
|
954
|
-
if (
|
|
1258
|
+
if (
|
|
1259
|
+
epoch === invalidationEpoch
|
|
1260
|
+
&& commitKey === key
|
|
1261
|
+
&& generationMismatchedProviders.size === 0
|
|
1262
|
+
) {
|
|
955
1263
|
const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
|
|
956
1264
|
cache = { key, ts: Date.now(), response: { ...response, reports } };
|
|
957
1265
|
}
|