@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -1,10 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mutatePersistedConfig } from "../config";
|
|
2
2
|
import { projectModelRenames } from "./model-rename-migration";
|
|
3
3
|
import type { OcxConfig } from "../types";
|
|
4
4
|
|
|
5
5
|
export interface ModelRenameStartupDeps {
|
|
6
6
|
project: typeof projectModelRenames;
|
|
7
|
-
|
|
7
|
+
/** Injected writer for tests and callers that own their own persistence. */
|
|
8
|
+
save?: (config: OcxConfig) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Recursive key-by-key adopt: descend into a changed container, replace only changed leaves. */
|
|
16
|
+
function adoptRecord(live: Record<string, unknown>, next: Record<string, unknown>): void {
|
|
17
|
+
for (const key of Object.keys(live)) {
|
|
18
|
+
if (!(key in next)) delete live[key];
|
|
19
|
+
}
|
|
20
|
+
for (const [key, value] of Object.entries(next)) {
|
|
21
|
+
const current = live[key];
|
|
22
|
+
if (JSON.stringify(current) === JSON.stringify(value)) continue;
|
|
23
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
24
|
+
adoptRecord(current, value);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
live[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Copy `source` onto `target` in place, touching only the keys that actually differ.
|
|
33
|
+
*
|
|
34
|
+
* A clear-and-reassign preserves the top-level object identity while silently detaching every
|
|
35
|
+
* nested sub-object a caller still holds a live reference to. The renames rewrite one provider
|
|
36
|
+
* row at a time, so every sibling row — and the `providers` container itself — must survive
|
|
37
|
+
* with its identity intact.
|
|
38
|
+
*/
|
|
39
|
+
function adoptConfig(target: OcxConfig, source: OcxConfig): void {
|
|
40
|
+
if (target === source) return;
|
|
41
|
+
adoptRecord(
|
|
42
|
+
target as unknown as Record<string, unknown>,
|
|
43
|
+
structuredClone(source) as unknown as Record<string, unknown>,
|
|
44
|
+
);
|
|
8
45
|
}
|
|
9
46
|
|
|
10
47
|
/**
|
|
@@ -15,14 +52,41 @@ export interface ModelRenameStartupDeps {
|
|
|
15
52
|
* recoverable from the config alone. This one only rewrites model ids that the
|
|
16
53
|
* registry itself no longer seeds, and the pre-migration value is a string this
|
|
17
54
|
* file still names, so the change is reversible by hand.
|
|
55
|
+
*
|
|
56
|
+
* Persistence failure is not fatal. This runs inside `startServer`, so a missing, malformed or
|
|
57
|
+
* contended config degrades to a warning plus the in-memory projection rather than taking the
|
|
58
|
+
* boot path down.
|
|
18
59
|
*/
|
|
19
60
|
export function runModelRenameStartupMigration(
|
|
20
61
|
config: OcxConfig,
|
|
21
|
-
deps: ModelRenameStartupDeps = { project: projectModelRenames
|
|
62
|
+
deps: ModelRenameStartupDeps = { project: projectModelRenames },
|
|
22
63
|
): OcxConfig {
|
|
23
|
-
const projection = deps.project(config);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
64
|
+
const projection = deps.project(structuredClone(config));
|
|
65
|
+
if (!projection.changed) {
|
|
66
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
69
|
+
if (deps.save) {
|
|
70
|
+
deps.save(projection.config);
|
|
71
|
+
adoptConfig(config, projection.config);
|
|
72
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
75
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
76
|
+
const next = deps.project(fresh);
|
|
77
|
+
if (next.changed) adoptConfig(fresh, next.config);
|
|
78
|
+
return { changed: next.changed, value: next };
|
|
79
|
+
});
|
|
80
|
+
if (outcome.status === "unavailable") {
|
|
81
|
+
console.warn(
|
|
82
|
+
`[model-rename-migration] persistence unavailable (${outcome.reason}); applying the renames `
|
|
83
|
+
+ "in memory for this run only.",
|
|
84
|
+
);
|
|
85
|
+
adoptConfig(config, projection.config);
|
|
86
|
+
for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
adoptConfig(config, outcome.value.config);
|
|
90
|
+
for (const warning of outcome.value.warnings) console.warn(`[model-rename-migration] ${warning}`);
|
|
91
|
+
return config;
|
|
28
92
|
}
|
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
resolveCodexAuthContext,
|
|
8
8
|
type CodexAccountSelectionAdmission,
|
|
9
9
|
type CodexAuthContext,
|
|
10
|
+
type CodexAuthPolicyConfig,
|
|
10
11
|
} from "../codex/auth-context";
|
|
11
12
|
import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome } from "../codex/routing";
|
|
12
13
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
13
|
-
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../server/auth-cors";
|
|
14
|
+
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential, type DataPlaneAdmission } from "../server/auth-cors";
|
|
14
15
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
15
16
|
import {
|
|
16
17
|
CODEX_FORWARD_BASE_URL,
|
|
@@ -78,6 +79,8 @@ export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiFor
|
|
|
78
79
|
|
|
79
80
|
function directSidecarHeaders(
|
|
80
81
|
incomingHeaders: Headers,
|
|
82
|
+
config: CodexAuthPolicyConfig,
|
|
83
|
+
admission?: Pick<DataPlaneAdmission, "source">,
|
|
81
84
|
): Headers | undefined {
|
|
82
85
|
const bearer = incomingHeaders.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
83
86
|
if (!bearer) return undefined;
|
|
@@ -89,7 +92,7 @@ function directSidecarHeaders(
|
|
|
89
92
|
// intentional ChatGPT-auth operation instead of silently reclassifying any JWT-shaped
|
|
90
93
|
// provider credential as a Codex bearer.
|
|
91
94
|
if (!requestedAccountId || requestedAccountId !== derivedAccountId) return undefined;
|
|
92
|
-
const selected = headersForCodexAuthContext(incomingHeaders, { kind: "main", accountId: null });
|
|
95
|
+
const selected = headersForCodexAuthContext(incomingHeaders, { kind: "main", accountId: null }, config, undefined, admission);
|
|
93
96
|
return selected;
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -99,10 +102,13 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
99
102
|
config: OcxConfig,
|
|
100
103
|
options: {
|
|
101
104
|
exactAccount?: ExactOpenAiSidecarAccount;
|
|
105
|
+
admission?: Pick<DataPlaneAdmission, "source">;
|
|
106
|
+
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
102
107
|
beginCodexAccountSelection?: () => CodexAccountSelectionAdmission | undefined;
|
|
103
108
|
} = {},
|
|
104
109
|
): Promise<ResolvedOpenAiForwardSidecar | undefined> {
|
|
105
110
|
const { exactAccount } = options;
|
|
111
|
+
const policy = options.codexAuthPolicy ?? config;
|
|
106
112
|
let callerBearerMayBeForwarded = true;
|
|
107
113
|
try {
|
|
108
114
|
validateForwardAdmissionCredential(incomingHeaders, config);
|
|
@@ -116,10 +122,13 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
116
122
|
// credential directly even when the provider is globally Direct, and never
|
|
117
123
|
// consult Pool active state, affinity, probes, or alternates.
|
|
118
124
|
const authContext = await resolveCodexAuthContext(incomingHeaders, config, "pool", {
|
|
125
|
+
codexAuthPolicy: policy,
|
|
119
126
|
accountId: exactAccount.accountId,
|
|
120
127
|
modelId: exactAccount.modelId,
|
|
128
|
+
admission: options.admission,
|
|
121
129
|
beginCodexAccountSelection: options.beginCodexAccountSelection,
|
|
122
130
|
});
|
|
131
|
+
const selectedHeaders = headersForCodexAuthContext(incomingHeaders, authContext, policy, exactAccount.modelId, options.admission);
|
|
123
132
|
if ((authContext.kind !== "pool" && authContext.kind !== "main-pool")
|
|
124
133
|
|| !isCodexAuthContextUsable(authContext, config)) {
|
|
125
134
|
// Exact selection is fail-closed. A generation/runtime-state race must not fall through
|
|
@@ -129,7 +138,7 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
129
138
|
return {
|
|
130
139
|
...candidate,
|
|
131
140
|
authContext,
|
|
132
|
-
headers:
|
|
141
|
+
headers: selectedHeaders,
|
|
133
142
|
recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(
|
|
134
143
|
config,
|
|
135
144
|
authContext.accountId,
|
|
@@ -149,7 +158,7 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
149
158
|
}
|
|
150
159
|
if (candidate.accountMode === "direct") {
|
|
151
160
|
if (!callerBearerMayBeForwarded || !hasCallerCodexBearer(incomingHeaders)) continue;
|
|
152
|
-
const headers = directSidecarHeaders(incomingHeaders);
|
|
161
|
+
const headers = directSidecarHeaders(incomingHeaders, policy, options.admission);
|
|
153
162
|
if (!headers) continue;
|
|
154
163
|
return {
|
|
155
164
|
...candidate,
|
|
@@ -158,13 +167,16 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
158
167
|
};
|
|
159
168
|
}
|
|
160
169
|
const authContext = await resolveCodexAuthContext(incomingHeaders, config, candidate.accountMode, {
|
|
170
|
+
codexAuthPolicy: policy,
|
|
171
|
+
admission: options.admission,
|
|
161
172
|
beginCodexAccountSelection: options.beginCodexAccountSelection,
|
|
162
173
|
});
|
|
174
|
+
const selectedHeaders = headersForCodexAuthContext(incomingHeaders, authContext, policy, undefined, options.admission);
|
|
163
175
|
if (!isCodexAuthContextUsable(authContext, config)) continue;
|
|
164
176
|
return {
|
|
165
177
|
...candidate,
|
|
166
178
|
authContext,
|
|
167
|
-
headers:
|
|
179
|
+
headers: selectedHeaders,
|
|
168
180
|
...(authContext.kind === "pool" || authContext.kind === "main-pool"
|
|
169
181
|
? {
|
|
170
182
|
recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import { openaiResponsesUrl } from "../adapters/openai-responses-url";
|
|
3
|
+
|
|
4
|
+
export const OPENAI_CODEX_PROVIDER_ID = "openai";
|
|
5
|
+
export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi";
|
|
6
|
+
export const OPENAI_API_PROVIDER_ID = "openai-apikey";
|
|
7
|
+
export const LEGACY_CHATGPT_PROVIDER_ID = "chatgpt";
|
|
8
|
+
|
|
9
|
+
export const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
10
|
+
|
|
11
|
+
function normalizedBaseUrl(value: string): string | undefined {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(value.trim());
|
|
14
|
+
if (url.username || url.password || url.search || url.hash) return undefined;
|
|
15
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
16
|
+
return `${url.origin}${path}`;
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): boolean {
|
|
23
|
+
return provider.adapter === "openai-responses"
|
|
24
|
+
&& provider.authMode === "forward"
|
|
25
|
+
&& normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const OPENAI_API_ORIGIN = "https://api.openai.com";
|
|
29
|
+
const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`;
|
|
30
|
+
const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The Responses endpoint the adapter would actually POST key-auth traffic to, normalized.
|
|
34
|
+
*
|
|
35
|
+
* Mirrors the adapter's own construction (`src/adapters/openai-responses.ts`): a configured
|
|
36
|
+
* `responsesPath` is appended to the base verbatim, and only the default branch runs the
|
|
37
|
+
* `/v1/responses` suffix normalization. Classifying on the base URL alone would call
|
|
38
|
+
* `baseUrl: "https://api.openai.com"` with `responsesPath: "/other"` official even though that
|
|
39
|
+
* request never reaches the official Responses endpoint.
|
|
40
|
+
*/
|
|
41
|
+
function resolvedResponsesEndpoint(provider: OcxProviderConfig): string | undefined {
|
|
42
|
+
try {
|
|
43
|
+
const raw = provider.responsesPath === undefined
|
|
44
|
+
? openaiResponsesUrl(provider.baseUrl)
|
|
45
|
+
: `${provider.baseUrl.replace(/\/$/, "")}${provider.responsesPath}`;
|
|
46
|
+
return normalizedBaseUrl(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
53
|
+
// Exact normalized URL keeps lookalike/suffix hosts out of this set: `api.openai.com.evil.test`
|
|
54
|
+
// resolves to its own origin, never to the official one.
|
|
55
|
+
return resolvedResponsesEndpoint(provider) === OPENAI_API_RESPONSES_URL;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT
|
|
60
|
+
* backend can, and so can the official OpenAI API — but an arbitrary gateway that
|
|
61
|
+
* merely speaks the Responses wire cannot, and calling it there fails compaction
|
|
62
|
+
* with an unhelpful error instead of falling back to a routed summary (#422).
|
|
63
|
+
*/
|
|
64
|
+
export function supportsNativeResponsesCompactEndpoint(
|
|
65
|
+
providerName: string,
|
|
66
|
+
provider: OcxProviderConfig,
|
|
67
|
+
): boolean {
|
|
68
|
+
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
69
|
+
return providerName === OPENAI_API_PROVIDER_ID
|
|
70
|
+
&& provider.adapter === "openai-responses"
|
|
71
|
+
&& normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex
|
|
76
|
+
* surface or the official OpenAI API.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately not keyed on `authMode === "forward"`: a noncanonical forward provider does not
|
|
79
|
+
* receive the caller's credentials (see the forward-header gate in the Responses adapter), so
|
|
80
|
+
* forward auth says nothing about which backend is on the other end.
|
|
81
|
+
*/
|
|
82
|
+
export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
83
|
+
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
84
|
+
return provider.adapter === "openai-responses"
|
|
85
|
+
&& isOfficialOpenAiResponsesDestination(provider);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether this destination can decode a native (non-`ocx1:`) compaction blob.
|
|
90
|
+
*
|
|
91
|
+
* Only the backend that minted a blob can decode it. `authMode: "forward"` alone is not a signal:
|
|
92
|
+
* the adapter forwards caller credentials only to the canonical ChatGPT Codex surface, while a
|
|
93
|
+
* noncanonical forward provider receives no caller credentials and may point at any backend.
|
|
94
|
+
*
|
|
95
|
+
* Relay only to an OpenAI-operated destination or a destination whose operator explicitly opts in.
|
|
96
|
+
* Keyed by destination rather than provider id: a blob's issuer is the URL that produced it, not the
|
|
97
|
+
* local config key a replay travels under.
|
|
98
|
+
*/
|
|
99
|
+
export function destinationDecodesNativeCompactionBlob(provider: OcxProviderConfig): boolean {
|
|
100
|
+
return isOpenAiOperatedResponsesDestination(provider)
|
|
101
|
+
|| provider.decodesNativeCompactionBlobs === true;
|
|
102
|
+
}
|
|
@@ -2,13 +2,9 @@ import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverla
|
|
|
2
2
|
import { OPENAI_PROVIDER_TIER_VERSION } from "../types";
|
|
3
3
|
import { openaiResponsesUrl } from "../adapters/openai-responses-url";
|
|
4
4
|
import { MAX_COST4_RATE } from "../usage/expected-prices";
|
|
5
|
+
import { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "./openai-tiers-destination";
|
|
6
|
+
export { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, isOpenAiOperatedResponsesDestination, destinationDecodesNativeCompactionBlob } from "./openai-tiers-destination";
|
|
5
7
|
|
|
6
|
-
export const OPENAI_CODEX_PROVIDER_ID = "openai";
|
|
7
|
-
export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi";
|
|
8
|
-
export const OPENAI_API_PROVIDER_ID = "openai-apikey";
|
|
9
|
-
export const LEGACY_CHATGPT_PROVIDER_ID = "chatgpt";
|
|
10
|
-
|
|
11
|
-
export const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
12
8
|
const LEGACY_OPENAI_MULTI_PREFIX = `${LEGACY_OPENAI_MULTI_PROVIDER_ID}/`;
|
|
13
9
|
|
|
14
10
|
function canonicalCodexForwardProvider(mode: CodexAccountMode): OcxProviderConfig {
|
|
@@ -20,99 +16,6 @@ function canonicalCodexForwardProvider(mode: CodexAccountMode): OcxProviderConfi
|
|
|
20
16
|
};
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
function normalizedBaseUrl(value: string): string | undefined {
|
|
24
|
-
try {
|
|
25
|
-
const url = new URL(value.trim());
|
|
26
|
-
if (url.username || url.password || url.search || url.hash) return undefined;
|
|
27
|
-
const path = url.pathname.replace(/\/+$/, "");
|
|
28
|
-
return `${url.origin}${path}`;
|
|
29
|
-
} catch {
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): boolean {
|
|
35
|
-
return provider.adapter === "openai-responses"
|
|
36
|
-
&& provider.authMode === "forward"
|
|
37
|
-
&& normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const OPENAI_API_ORIGIN = "https://api.openai.com";
|
|
41
|
-
const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`;
|
|
42
|
-
const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The Responses endpoint the adapter would actually POST key-auth traffic to, normalized.
|
|
46
|
-
*
|
|
47
|
-
* Mirrors the adapter's own construction (`src/adapters/openai-responses.ts`): a configured
|
|
48
|
-
* `responsesPath` is appended to the base verbatim, and only the default branch runs the
|
|
49
|
-
* `/v1/responses` suffix normalization. Classifying on the base URL alone would call
|
|
50
|
-
* `baseUrl: "https://api.openai.com"` with `responsesPath: "/other"` official even though that
|
|
51
|
-
* request never reaches the official Responses endpoint.
|
|
52
|
-
*/
|
|
53
|
-
function resolvedResponsesEndpoint(provider: OcxProviderConfig): string | undefined {
|
|
54
|
-
try {
|
|
55
|
-
const raw = provider.responsesPath === undefined
|
|
56
|
-
? openaiResponsesUrl(provider.baseUrl)
|
|
57
|
-
: `${provider.baseUrl.replace(/\/$/, "")}${provider.responsesPath}`;
|
|
58
|
-
return normalizedBaseUrl(raw);
|
|
59
|
-
} catch {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
65
|
-
// Exact normalized URL keeps lookalike/suffix hosts out of this set: `api.openai.com.evil.test`
|
|
66
|
-
// resolves to its own origin, never to the official one.
|
|
67
|
-
return resolvedResponsesEndpoint(provider) === OPENAI_API_RESPONSES_URL;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT
|
|
72
|
-
* backend can, and so can the official OpenAI API — but an arbitrary gateway that
|
|
73
|
-
* merely speaks the Responses wire cannot, and calling it there fails compaction
|
|
74
|
-
* with an unhelpful error instead of falling back to a routed summary (#422).
|
|
75
|
-
*/
|
|
76
|
-
export function supportsNativeResponsesCompactEndpoint(
|
|
77
|
-
providerName: string,
|
|
78
|
-
provider: OcxProviderConfig,
|
|
79
|
-
): boolean {
|
|
80
|
-
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
81
|
-
return providerName === OPENAI_API_PROVIDER_ID
|
|
82
|
-
&& provider.adapter === "openai-responses"
|
|
83
|
-
&& normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex
|
|
88
|
-
* surface or the official OpenAI API.
|
|
89
|
-
*
|
|
90
|
-
* Deliberately not keyed on `authMode === "forward"`: a noncanonical forward provider does not
|
|
91
|
-
* receive the caller's credentials (see the forward-header gate in the Responses adapter), so
|
|
92
|
-
* forward auth says nothing about which backend is on the other end.
|
|
93
|
-
*/
|
|
94
|
-
export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig): boolean {
|
|
95
|
-
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
96
|
-
return provider.adapter === "openai-responses"
|
|
97
|
-
&& isOfficialOpenAiResponsesDestination(provider);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Whether this destination can decode a native (non-`ocx1:`) compaction blob.
|
|
102
|
-
*
|
|
103
|
-
* Only the backend that minted a blob can decode it. `authMode: "forward"` alone is not a signal:
|
|
104
|
-
* the adapter forwards caller credentials only to the canonical ChatGPT Codex surface, while a
|
|
105
|
-
* noncanonical forward provider receives no caller credentials and may point at any backend.
|
|
106
|
-
*
|
|
107
|
-
* Relay only to an OpenAI-operated destination or a destination whose operator explicitly opts in.
|
|
108
|
-
* Keyed by destination rather than provider id: a blob's issuer is the URL that produced it, not the
|
|
109
|
-
* local config key a replay travels under.
|
|
110
|
-
*/
|
|
111
|
-
export function destinationDecodesNativeCompactionBlob(provider: OcxProviderConfig): boolean {
|
|
112
|
-
return isOpenAiOperatedResponsesDestination(provider)
|
|
113
|
-
|| provider.decodesNativeCompactionBlobs === true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
19
|
export interface OpenAiTierMigrationProjection {
|
|
117
20
|
config: OcxConfig;
|
|
118
21
|
changed: boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { OcxProviderConfig } from "../types";
|
|
3
|
+
import { registryEntryForProviderDestination } from "./registry";
|
|
4
|
+
|
|
5
|
+
export const OPENCODE_GO_SESSION_HEADER = "x-opencode-session";
|
|
6
|
+
|
|
7
|
+
function hasHeaderCaseInsensitive(
|
|
8
|
+
headers: Record<string, string> | undefined,
|
|
9
|
+
name: string,
|
|
10
|
+
): boolean {
|
|
11
|
+
const target = name.toLowerCase();
|
|
12
|
+
return Object.keys(headers ?? {}).some(key => key.toLowerCase() === target);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Derive a provider-scoped opaque value without exposing Codex task or subagent ids. */
|
|
16
|
+
export function deriveOpenCodeGoSessionId(sessionLane: string): string {
|
|
17
|
+
const digest = createHash("sha256")
|
|
18
|
+
.update("opencodex/opencode-go/session/v1\0")
|
|
19
|
+
.update(sessionLane)
|
|
20
|
+
.digest("hex")
|
|
21
|
+
.slice(0, 32);
|
|
22
|
+
return `ocx_${digest}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Add per-conversation Go affinity only to the canonical fixed-key destination. */
|
|
26
|
+
export function resolveOpenCodeGoTransport<T extends OcxProviderConfig>(
|
|
27
|
+
provider: T,
|
|
28
|
+
sessionLane: string | undefined,
|
|
29
|
+
): T {
|
|
30
|
+
if (registryEntryForProviderDestination(provider)?.id !== "opencode-go") return provider;
|
|
31
|
+
if (!sessionLane) return provider;
|
|
32
|
+
if (hasHeaderCaseInsensitive(provider.headers, OPENCODE_GO_SESSION_HEADER)) return provider;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...provider,
|
|
36
|
+
headers: {
|
|
37
|
+
...(provider.headers ?? {}),
|
|
38
|
+
[OPENCODE_GO_SESSION_HEADER]: deriveOpenCodeGoSessionId(sessionLane),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** Isolated, process-local API-key quota rows. Never publishes provider/routing caches. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
4
|
+
import { apiKeyPoolEntryId } from "./api-keys";
|
|
5
|
+
import { resolveProviderApiKey } from "./key-store";
|
|
6
|
+
import type { ProviderQuota } from "./quota-types";
|
|
7
|
+
import { ACCOUNT_QUOTA_TTL_MS } from "./quota-wire";
|
|
8
|
+
|
|
9
|
+
const MAX_ENTRIES = 256;
|
|
10
|
+
const LAST_GOOD_MS = 30 * 60_000;
|
|
11
|
+
type Entry = { ts: number; quota: ProviderQuota | null; unavailable?: true };
|
|
12
|
+
export type KeyQuotaProbeOutcome =
|
|
13
|
+
| { kind: "quota"; quota: ProviderQuota }
|
|
14
|
+
| { kind: "empty" | "terminal" | "unavailable" };
|
|
15
|
+
export interface ProviderApiKeyQuota {
|
|
16
|
+
keyId: string;
|
|
17
|
+
quota: ProviderQuota | null;
|
|
18
|
+
unavailable?: true;
|
|
19
|
+
/** Server-only, non-enumerable; call immediately before the safe DTO join. */
|
|
20
|
+
isCurrent: () => boolean;
|
|
21
|
+
}
|
|
22
|
+
const cache = new Map<string, Entry>();
|
|
23
|
+
const flights = new Map<string, Promise<Entry>>();
|
|
24
|
+
let epoch = 0;
|
|
25
|
+
|
|
26
|
+
export function clearProviderApiKeyQuotaCache(): void {
|
|
27
|
+
epoch += 1;
|
|
28
|
+
cache.clear();
|
|
29
|
+
flights.clear();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Four workers per roster, not a process-wide network limit. */
|
|
33
|
+
export async function mapQuotaRoster<T, R>(rows: readonly T[], read: (row: T) => Promise<R>): Promise<R[]> {
|
|
34
|
+
const out = new Array<R>(rows.length);
|
|
35
|
+
let cursor = 0;
|
|
36
|
+
await Promise.all(Array.from({ length: Math.min(4, rows.length) }, async () => {
|
|
37
|
+
while (cursor < rows.length) {
|
|
38
|
+
const index = cursor++;
|
|
39
|
+
out[index] = await read(rows[index]!);
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function roster(provider: OcxProviderConfig) {
|
|
46
|
+
return provider.apiKeyPool?.length ? provider.apiKeyPool
|
|
47
|
+
: provider.apiKey ? [{ id: apiKeyPoolEntryId(provider.apiKey), key: provider.apiKey }] : [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function identity(name: string, provider: OcxProviderConfig, id: string, key: string): string {
|
|
51
|
+
return createHash("sha256").update(JSON.stringify([
|
|
52
|
+
"quota-key", name, provider.adapter, provider.baseUrl, provider.authMode ?? "key",
|
|
53
|
+
provider.disabled === true, id, key,
|
|
54
|
+
])).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function remember(key: string, entry: Entry): void {
|
|
58
|
+
cache.delete(key);
|
|
59
|
+
for (const [id, row] of cache) if (Date.now() - row.ts >= LAST_GOOD_MS) cache.delete(id);
|
|
60
|
+
while (cache.size >= MAX_ENTRIES) cache.delete(cache.keys().next().value!);
|
|
61
|
+
cache.set(key, entry);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readEntry(
|
|
65
|
+
key: string,
|
|
66
|
+
force: boolean,
|
|
67
|
+
isCurrent: () => boolean,
|
|
68
|
+
probe: () => Promise<KeyQuotaProbeOutcome>,
|
|
69
|
+
): Promise<Entry> {
|
|
70
|
+
const previous = cache.get(key);
|
|
71
|
+
if (!force && previous && Date.now() - previous.ts < ACCOUNT_QUOTA_TTL_MS
|
|
72
|
+
&& (!previous.quota || Date.now() - previous.quota.updatedAt < LAST_GOOD_MS)) {
|
|
73
|
+
cache.delete(key);
|
|
74
|
+
cache.set(key, previous);
|
|
75
|
+
return previous;
|
|
76
|
+
}
|
|
77
|
+
const running = flights.get(key);
|
|
78
|
+
if (running) return running;
|
|
79
|
+
const lastGood = previous?.quota && Date.now() - previous.quota.updatedAt < LAST_GOOD_MS
|
|
80
|
+
? previous.quota : null;
|
|
81
|
+
if (flights.size >= MAX_ENTRIES) return { ts: Date.now(), quota: lastGood, unavailable: true };
|
|
82
|
+
const flight = (async (): Promise<Entry> => {
|
|
83
|
+
let result: KeyQuotaProbeOutcome;
|
|
84
|
+
try { result = await probe(); } catch { result = { kind: "unavailable" }; }
|
|
85
|
+
const entry: Entry = result.kind === "quota"
|
|
86
|
+
? { ts: Date.now(), quota: result.quota }
|
|
87
|
+
: result.kind === "empty"
|
|
88
|
+
? { ts: Date.now(), quota: null }
|
|
89
|
+
: {
|
|
90
|
+
ts: Date.now(),
|
|
91
|
+
quota: result.kind !== "terminal" && lastGood && Date.now() - lastGood.updatedAt < LAST_GOOD_MS ? lastGood : null,
|
|
92
|
+
unavailable: true,
|
|
93
|
+
};
|
|
94
|
+
if (isCurrent()) remember(key, entry);
|
|
95
|
+
return entry;
|
|
96
|
+
})().finally(() => { if (flights.get(key) === flight) flights.delete(key); });
|
|
97
|
+
flights.set(key, flight);
|
|
98
|
+
return flight;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function readProviderApiKeyQuotas(
|
|
102
|
+
config: OcxConfig,
|
|
103
|
+
name: string,
|
|
104
|
+
force: boolean,
|
|
105
|
+
probe: (provider: OcxProviderConfig, config: OcxConfig) => Promise<KeyQuotaProbeOutcome>,
|
|
106
|
+
): Promise<ProviderApiKeyQuota[]> {
|
|
107
|
+
const liveProvider = config.providers[name];
|
|
108
|
+
if (!liveProvider) return [];
|
|
109
|
+
const providerSnapshot = { ...liveProvider };
|
|
110
|
+
const rows = roster(liveProvider).map(row => ({ ...row }));
|
|
111
|
+
const generation = epoch;
|
|
112
|
+
return mapQuotaRoster(rows, async row => {
|
|
113
|
+
let resolved: string | undefined;
|
|
114
|
+
try { resolved = resolveProviderApiKey(row.key)?.trim(); } catch { /* unavailable */ }
|
|
115
|
+
const key = resolved ? identity(name, providerSnapshot, row.id, resolved) : null;
|
|
116
|
+
const isCurrent = () => {
|
|
117
|
+
if (epoch !== generation || !key) return false;
|
|
118
|
+
const current = config.providers[name];
|
|
119
|
+
if (!current) return false;
|
|
120
|
+
const entry = roster(current).find(candidate => candidate.id === row.id);
|
|
121
|
+
if (!entry) return false;
|
|
122
|
+
try {
|
|
123
|
+
const currentKey = resolveProviderApiKey(entry.key)?.trim();
|
|
124
|
+
return !!currentKey && identity(name, current, row.id, currentKey) === key;
|
|
125
|
+
} catch { return false; }
|
|
126
|
+
};
|
|
127
|
+
const entry = key && resolved && isCurrent()
|
|
128
|
+
? await readEntry(key, force, isCurrent, () => {
|
|
129
|
+
const isolatedProvider = { ...providerSnapshot, apiKey: resolved, apiKeyPool: undefined };
|
|
130
|
+
const isolatedConfig = { ...config, providers: { [name]: isolatedProvider } };
|
|
131
|
+
return probe(isolatedProvider, isolatedConfig);
|
|
132
|
+
})
|
|
133
|
+
: { ts: Date.now(), quota: null, unavailable: true as const };
|
|
134
|
+
const result = {
|
|
135
|
+
keyId: row.id,
|
|
136
|
+
quota: isCurrent() ? entry.quota : null,
|
|
137
|
+
...(!isCurrent() || entry.unavailable ? { unavailable: true as const } : {}),
|
|
138
|
+
};
|
|
139
|
+
return Object.defineProperty(result, "isCurrent", { value: isCurrent }) as ProviderApiKeyQuota;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -34,3 +34,12 @@ export interface ProviderQuota {
|
|
|
34
34
|
creditsUsd?: ProviderQuotaCreditsUsd;
|
|
35
35
|
updatedAt: number;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
export type AccountQuotaMode = "probe" | "passive" | "unsupported";
|
|
39
|
+
|
|
40
|
+
/** Additive management-row fields; cheap lists emit only quotaMode. */
|
|
41
|
+
export interface AccountQuotaFields {
|
|
42
|
+
quotaMode?: AccountQuotaMode;
|
|
43
|
+
quota?: ProviderQuota | null;
|
|
44
|
+
quotaUnavailable?: boolean;
|
|
45
|
+
}
|