@bitkyc08/opencodex 2.76.0 → 2.77.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/gui/dist/assets/App-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -30,7 +30,7 @@ import { probeHostname } from "../server/proxy-liveness";
|
|
|
30
30
|
import type { OcxConfig } from "../types";
|
|
31
31
|
|
|
32
32
|
export type { ConfigFormat } from "../integrations/serialize";
|
|
33
|
-
export type { ManagedFragment, ManagedContribution, BuildContribution, OpencodeLaunchEnv, OpencodeCatalogModel, ExportModel, ExportContext, ExportClientId, ExportClientSpec, PiModelEntry } from "./config-export/contracts";
|
|
33
|
+
export type { ManagedFragment, ManagedContribution, BuildContribution, OpencodeLaunchEnv, OpencodeCatalogModel, ExportModel, ExportContext, ExportClientId, ExportClientSpec, PiModelEntry, DroidReasoningDefaults } from "./config-export/contracts";
|
|
34
34
|
export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPENCODE_API_KEY_ENV_REF, KILO_API_KEY_ENV, KILO_API_KEY_ENV_REF, KILO_CONFIG_SCHEMA, HERMES_API_KEY_ENV, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV, OPENCLAW_API_KEY_ENV_REF, LOOPBACK_API_KEY_PLACEHOLDER, GAJAE_API_KEY_ENV, SCHEMA_REQUIRED_OUTPUT_BUDGET, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG } from "./config-export/constants";
|
|
35
35
|
export { normalizeExportModels } from "./config-export/model-metadata";
|
|
36
36
|
export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./config-export/omp";
|
|
@@ -51,12 +51,13 @@ export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProv
|
|
|
51
51
|
export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode";
|
|
52
52
|
export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProviderEntry, RaycastGeneratedConfig } from "./config-export/raycast";
|
|
53
53
|
export { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
|
|
54
|
-
export { droidHomeDir, droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid";
|
|
54
|
+
export { droidHomeDir, droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution, droidReasoningModels, droidDefaultsFromOwnedRows, validateDroidReasoningDefaults } from "./config-export/droid";
|
|
55
55
|
export type { DroidModelEntry, DroidGeneratedConfig } from "./config-export/droid";
|
|
56
56
|
|
|
57
57
|
import type { OpencodeLaunchEnv, OpencodeCatalogModel, ExportContext, PiModelEntry, ManagedContribution, ManagedFragment, ExportClientId, ExportClientSpec } from "./config-export/contracts";
|
|
58
58
|
import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV, KILO_API_KEY_ENV } from "./config-export/constants";
|
|
59
|
-
import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, normalizeExportModels, inputModalitiesForClient, opencodeModelCapabilities, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata";
|
|
59
|
+
import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, inputBudgetFor, normalizeExportModels, inputModalitiesForClient, opencodeModelCapabilities, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata";
|
|
60
|
+
import { exportReasoningEfforts, exportDefaultReasoningEffort, legacyReasoningMetadata, type LegacyEffortVariant } from "./config-export/reasoning-metadata";
|
|
60
61
|
import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp";
|
|
61
62
|
import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh";
|
|
62
63
|
import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from "./config-export/mcode";
|
|
@@ -72,7 +73,7 @@ import { droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidCont
|
|
|
72
73
|
|
|
73
74
|
export interface OpencodeModelEntry {
|
|
74
75
|
name: string;
|
|
75
|
-
limit?: { context: number; output: number };
|
|
76
|
+
limit?: { context: number; input?: number; output: number };
|
|
76
77
|
/**
|
|
77
78
|
* opencode's own capability fields, derived from the catalog row's declared input
|
|
78
79
|
* modalities. Written only when the row declares at least one — an entry without them is
|
|
@@ -81,22 +82,30 @@ export interface OpencodeModelEntry {
|
|
|
81
82
|
*/
|
|
82
83
|
attachment?: boolean;
|
|
83
84
|
modalities?: { input: string[]; output: string[] };
|
|
85
|
+
tool_call?: boolean;
|
|
86
|
+
reasoning?: boolean;
|
|
87
|
+
interleaved?: { field: "reasoning_content" };
|
|
88
|
+
options?: { reasoningEffort: string };
|
|
89
|
+
variants?: Record<string, LegacyEffortVariant>;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
87
93
|
* One selectable reasoning effort.
|
|
88
94
|
*
|
|
89
|
-
*
|
|
90
|
-
* legacy `provider` block is parsed and then dropped, so the V1 block stays variant-free
|
|
91
|
-
* rather than carrying fields that look configured but never reach a request.
|
|
95
|
+
* Native V2 variants use settings; legacy clients receive a separate options map.
|
|
92
96
|
*/
|
|
93
97
|
export interface OpencodeModelVariant {
|
|
94
98
|
id: string;
|
|
95
99
|
settings: { reasoningEffort: string };
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
export interface OpencodeV2ModelEntry
|
|
99
|
-
|
|
102
|
+
export interface OpencodeV2ModelEntry {
|
|
103
|
+
name: string;
|
|
104
|
+
limit?: { context: number; input?: number; output: number };
|
|
105
|
+
capabilities?: { tools: boolean; input?: string[]; output?: string[] };
|
|
106
|
+
compatibility?: { reasoningField: "reasoning_content" };
|
|
107
|
+
settings?: { reasoningEffort: string };
|
|
108
|
+
variants: OpencodeModelVariant[];
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
/** Endpoint and admission, spelled once and shared by both block generations. */
|
|
@@ -114,7 +123,7 @@ export interface OpencodeProviderBlock {
|
|
|
114
123
|
models: Record<string, OpencodeModelEntry>;
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
/** opencode V2 provider block: `package` + `settings
|
|
126
|
+
/** opencode V2 provider block: `package` + `settings` and native variant arrays. */
|
|
118
127
|
export interface OpencodeV2ProviderBlock {
|
|
119
128
|
package: string;
|
|
120
129
|
name: string;
|
|
@@ -147,16 +156,10 @@ export interface OpencodeGeneratedConfig {
|
|
|
147
156
|
const OPENCODE_PROVIDER_NPM = "@ai-sdk/openai-compatible";
|
|
148
157
|
|
|
149
158
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* or the provider is not loaded at all.
|
|
153
|
-
*
|
|
154
|
-
* Verified end-to-end against opencode 0.0.0-beta-18684: `GET /api/model` resolves this
|
|
155
|
-
* package for the provider and applies the per-model `variants`. opencode changes its
|
|
156
|
-
* provider package table between releases, so re-verify the supported range whenever it
|
|
157
|
-
* moves; a stale string breaks only the V2 block, silently.
|
|
159
|
+
* Canonical V2 runtime spelling. Current clients alias the old @opencode-ai scope,
|
|
160
|
+
* but exported documents use the documented package rather than depending on that alias.
|
|
158
161
|
*/
|
|
159
|
-
const OPENCODE_V2_PROVIDER_PACKAGE = "@opencode
|
|
162
|
+
const OPENCODE_V2_PROVIDER_PACKAGE = "@opencode/ai/providers/openai-compatible";
|
|
160
163
|
|
|
161
164
|
/** Display name for the provider block, identical in both generations. */
|
|
162
165
|
const OPENCODE_PROVIDER_NAME = "OpenCodex";
|
|
@@ -672,35 +675,11 @@ function opencodeProviderConnection(baseURL: string, config: OcxConfig): Opencod
|
|
|
672
675
|
}
|
|
673
676
|
|
|
674
677
|
/**
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
* No model-level `settings.reasoningEffort` default is emitted: the proxy already applies
|
|
678
|
-
* its own configured default when a request carries no effort, and pinning one here would
|
|
679
|
-
* override a default the user controls in opencodex. Variants are opt-in per selection,
|
|
680
|
-
* which is the same reason we never emit `defaultModel` for MCode.
|
|
681
|
-
*
|
|
682
|
-
* `none` is dropped even when a ladder declares it.
|
|
683
|
-
*
|
|
684
|
-
* The original reason no longer holds and is recorded here so it is not repeated: the chat
|
|
685
|
-
* ingress `OUTPUT_CONFIG_EFFORTS` allowlist DID omit `none`, so selecting it sent no effort
|
|
686
|
-
* at all and fell back to the proxy default. That allowlist now accepts `none` (audit F7),
|
|
687
|
-
* because it is the runtime's disable sentinel and dropping it let a provider default
|
|
688
|
-
* re-enable thinking a caller had turned off.
|
|
689
|
-
*
|
|
690
|
-
* The variant stays filtered anyway, deliberately and narrowly: emitting it would change
|
|
691
|
-
* what this exporter writes into a user's opencode config, and whether opencode's own
|
|
692
|
-
* picker round-trips `reasoningEffort: "none"` to the wire this proxy reads has not been
|
|
693
|
-
* verified here. Re-enabling it is a scoped follow-up that needs that check first, not a
|
|
694
|
-
* side effect of an ingress fix. MCode and ZCode filter `none` for their own separate
|
|
695
|
-
* reasons, documented at their call sites.
|
|
678
|
+
* Exact declared choices. An empty array is intentional: omitting variants lets OpenCode
|
|
679
|
+
* synthesize low/medium/high, even for a model whose upstream declares no adjustable effort.
|
|
696
680
|
*/
|
|
697
|
-
function opencodeEffortVariants(model: OpencodeCatalogModel): OpencodeModelVariant[]
|
|
698
|
-
|
|
699
|
-
// Canonical order (none, minimal, then low..ultra) and dedupe, so the picker order does
|
|
700
|
-
// not depend on whatever order a provider listed its efforts in.
|
|
701
|
-
const efforts = canonicalizeReasoningEfforts(model.reasoningEfforts).filter(effort => effort !== "none");
|
|
702
|
-
if (efforts.length === 0) return undefined;
|
|
703
|
-
return efforts.map(effort => ({ id: effort, settings: { reasoningEffort: effort } }));
|
|
681
|
+
function opencodeEffortVariants(model: OpencodeCatalogModel): OpencodeModelVariant[] {
|
|
682
|
+
return (exportReasoningEfforts(model) ?? []).map(effort => ({ id: effort, settings: { reasoningEffort: effort } }));
|
|
704
683
|
}
|
|
705
684
|
|
|
706
685
|
/**
|
|
@@ -712,7 +691,7 @@ function opencodeEffortVariants(model: OpencodeCatalogModel): OpencodeModelVaria
|
|
|
712
691
|
* the pair) clamped to the context window.
|
|
713
692
|
*
|
|
714
693
|
* Two blocks instead of one because opencode V2 reads the `providers` map and V1 reads
|
|
715
|
-
* `provider`,
|
|
694
|
+
* `provider`, with native arrays versus legacy maps for variants. Emitting both keeps V1 installs
|
|
716
695
|
* working: V2 merges them by provider id and model id, so a model listed in both blocks
|
|
717
696
|
* appears once, with the V2 entry's name, connection, and variants.
|
|
718
697
|
*/
|
|
@@ -725,36 +704,37 @@ export function opencodeProviderBlocks(
|
|
|
725
704
|
const v2Models: Record<string, OpencodeV2ModelEntry> = {};
|
|
726
705
|
for (const model of expandFastExportModels(catalogModels)) {
|
|
727
706
|
const key = model.namespaced;
|
|
728
|
-
const entry: OpencodeModelEntry = { name: exportModelLabel(model) };
|
|
707
|
+
const entry: OpencodeModelEntry = { name: exportModelLabel(model), ...legacyReasoningMetadata(model) };
|
|
729
708
|
const context = authoritativeContextWindow(model.contextWindow);
|
|
730
709
|
if (context !== undefined) {
|
|
731
710
|
entry.limit = { context, output: outputBudgetFor(context, model) };
|
|
711
|
+
const input = inputBudgetFor(context, model);
|
|
712
|
+
if (input !== undefined) entry.limit.input = input;
|
|
732
713
|
}
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
// 1.18.30, src/provider/provider.ts: `model.attachment ?? …` / `model.modalities?.input`).
|
|
736
|
-
// They ride on both generations anyway: the two blocks are two spellings of one model list,
|
|
737
|
-
// and the V2 model schema (capabilities.{tools,input,output}, which opencode fills by
|
|
738
|
-
// migrating this same `modalities` field) ignores keys it does not define — its loader
|
|
739
|
-
// decodes with `onExcessProperty: "ignore"`. Same values on both, so a merge cannot make
|
|
740
|
-
// the two entries disagree.
|
|
714
|
+
// Native V2 decoders ignore legacy attachment/modalities fields. Emit each generation's
|
|
715
|
+
// actual capability shape rather than relying on migration of the other provider block.
|
|
741
716
|
const capabilities = opencodeModelCapabilities(model.inputModalities);
|
|
742
717
|
if (capabilities) {
|
|
743
718
|
entry.attachment = capabilities.attachment;
|
|
744
719
|
entry.modalities = capabilities.modalities;
|
|
745
720
|
}
|
|
721
|
+
if (typeof model.supportsTools === "boolean") entry.tool_call = model.supportsTools;
|
|
746
722
|
v1Models[key] = entry;
|
|
747
|
-
const
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
// the
|
|
723
|
+
const defaultEffort = exportDefaultReasoningEffort(model);
|
|
724
|
+
// V2 requires tools whenever capabilities exists. A partial object discards the entire
|
|
725
|
+
// native block, losing explicit variants. Unknown tools use the legacy modality migration
|
|
726
|
+
// rather than inventing a tools flag to make the native schema accept the declaration.
|
|
727
|
+
const v2Capabilities: OpencodeV2ModelEntry["capabilities"] = typeof model.supportsTools === "boolean" ? {
|
|
728
|
+
tools: model.supportsTools,
|
|
729
|
+
...(capabilities ? { input: [...capabilities.modalities.input], output: [...capabilities.modalities.output] } : {}),
|
|
730
|
+
} : undefined;
|
|
751
731
|
v2Models[key] = {
|
|
752
|
-
|
|
732
|
+
name: entry.name,
|
|
753
733
|
...(entry.limit ? { limit: { ...entry.limit } } : {}),
|
|
754
|
-
...(
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
734
|
+
...(v2Capabilities ? { capabilities: v2Capabilities } : {}),
|
|
735
|
+
...(entry.interleaved ? { compatibility: { reasoningField: "reasoning_content" } } : {}),
|
|
736
|
+
...(defaultEffort !== undefined ? { settings: { reasoningEffort: defaultEffort } } : {}),
|
|
737
|
+
variants: opencodeEffortVariants(model),
|
|
758
738
|
};
|
|
759
739
|
}
|
|
760
740
|
return {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* dashboard or CLI pick, never creates the file, and refuses a file with comments: the write
|
|
7
7
|
* re-serializes JSON, and a comment the user wrote would be lost without a word.
|
|
8
8
|
*/
|
|
9
|
-
import { lstatSync, readFileSync } from "node:fs";
|
|
9
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { atomicWriteFileNoFollowUnclaimed } from "../config/atomic-write";
|
|
@@ -40,12 +40,26 @@ type Loaded =
|
|
|
40
40
|
|
|
41
41
|
function load(path: string): Loaded {
|
|
42
42
|
let text: string;
|
|
43
|
+
let fd: number | undefined;
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// POSIX FIFOs must not block before fstat can reject them. Windows omits
|
|
46
|
+
// these flags and retains the descriptor/path identity checks below.
|
|
47
|
+
const flags = process.platform === "win32"
|
|
48
|
+
? constants.O_RDONLY
|
|
49
|
+
: constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
50
|
+
fd = openSync(path, flags);
|
|
51
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
52
|
+
const current = lstatSync(path, { bigint: true });
|
|
53
|
+
if (!opened.isFile() || !current.isFile() || opened.dev !== current.dev || opened.ino !== current.ino) {
|
|
54
|
+
return { kind: "invalid" };
|
|
55
|
+
}
|
|
56
|
+
text = readFileSync(fd, "utf8");
|
|
46
57
|
} catch (error) {
|
|
47
58
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { kind: "absent" };
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP") return { kind: "invalid" };
|
|
48
60
|
throw error;
|
|
61
|
+
} finally {
|
|
62
|
+
if (fd !== undefined) closeSync(fd);
|
|
49
63
|
}
|
|
50
64
|
if (hasJsoncComments(text)) return { kind: "comments" };
|
|
51
65
|
try {
|
|
@@ -4,6 +4,7 @@ import { isThirtyDayOnlyCodexPlan } from "./plan";
|
|
|
4
4
|
import {
|
|
5
5
|
CODEX_EXHAUSTED_USAGE_PERCENT,
|
|
6
6
|
isTerminalShortWindow,
|
|
7
|
+
hasSpendableCodexCredits,
|
|
7
8
|
resetAtToMs,
|
|
8
9
|
type StoredAccountQuota,
|
|
9
10
|
} from "./quota-types";
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
* Upstream does not refuse an account that holds ChatGPT credits at 100%: it serves the request
|
|
15
16
|
* and draws the balance. Selection only takes an account off on quota after a refusal, so an
|
|
16
17
|
* account with credits was never moved off. Spending is opt-in: only the ids in
|
|
17
|
-
* `creditCodexAccountIds` may keep serving from credits, and every other account leaves rotation
|
|
18
|
+
* `creditCodexAccountIds` may keep serving from fresh spendable credits, and every other account leaves rotation
|
|
18
19
|
* at 100% and comes back after its reset. Pool accounts and the `__main__` login both carry the
|
|
19
20
|
* switch; the main login is also checked where the main-account hard lock is.
|
|
20
21
|
*/
|
|
@@ -95,5 +96,6 @@ export function isCodexAccountHeldForCredits(
|
|
|
95
96
|
plan: unknown,
|
|
96
97
|
now: number,
|
|
97
98
|
): boolean {
|
|
98
|
-
return !codexAccountUsesCreditsAfterLimit(config, accountId) &&
|
|
99
|
+
return !(codexAccountUsesCreditsAfterLimit(config, accountId) && hasSpendableCodexCredits(quota, now))
|
|
100
|
+
&& isCodexUsageLimitReached(quota, plan, now);
|
|
99
101
|
}
|
|
@@ -18,6 +18,27 @@ const reauthAccounts = new Map<string, number | undefined>();
|
|
|
18
18
|
let lastReconciledGeneration = 0;
|
|
19
19
|
let liveAccountIds = new Set<string>();
|
|
20
20
|
|
|
21
|
+
// Refusal evidence is independent of the ordinary quarantine flag. Profile switches make a key
|
|
22
|
+
// inapplicable, not forgotten; the oldest of at most 64 records is evicted to bound process state.
|
|
23
|
+
const refusedMainRefreshGrants = new Set<string>();
|
|
24
|
+
const MAX_REFUSED_MAIN_REFRESH_GRANTS = 64;
|
|
25
|
+
|
|
26
|
+
export function markMainRefreshGrantRejected(key: string): void {
|
|
27
|
+
refusedMainRefreshGrants.add(key);
|
|
28
|
+
if (refusedMainRefreshGrants.size > MAX_REFUSED_MAIN_REFRESH_GRANTS) {
|
|
29
|
+
refusedMainRefreshGrants.delete(refusedMainRefreshGrants.values().next().value!);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isMainRefreshGrantRejected(key: string): boolean {
|
|
34
|
+
return refusedMainRefreshGrants.has(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearMainRefreshGrantRejection(key: string): void {
|
|
38
|
+
refusedMainRefreshGrants.delete(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
21
42
|
export function markAccountNeedsReauth(
|
|
22
43
|
id: string,
|
|
23
44
|
writerGeneration = captureConfigGeneration(),
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { classifyChatgptRefreshFailure, noteChatgptRefreshFailure, type ChatgptRefreshDiagnosticCode } from "./chatgpt-refresh-failure";
|
|
2
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
3
|
+
export { classifyChatgptRefreshFailure } from "./chatgpt-refresh-failure";
|
|
1
4
|
import { readOrcaAuthSource } from "./orca-auth-source";
|
|
2
5
|
import { createHash } from "node:crypto";
|
|
3
6
|
import { closeSync, existsSync, fstatSync, readFileSync, mkdirSync, openSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -526,7 +529,8 @@ const CHATGPT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
|
526
529
|
|
|
527
530
|
export class TokenRefreshError extends Error {
|
|
528
531
|
reason: "expired" | "revoked" | "unknown";
|
|
529
|
-
constructor(reason: "expired" | "revoked" | "unknown", message: string
|
|
532
|
+
constructor(reason: "expired" | "revoked" | "unknown", message: string,
|
|
533
|
+
readonly status?: number, readonly code?: ChatgptRefreshDiagnosticCode) {
|
|
530
534
|
super(message);
|
|
531
535
|
this.name = "TokenRefreshError";
|
|
532
536
|
this.reason = reason;
|
|
@@ -1322,52 +1326,12 @@ async function resolveCodexToken(
|
|
|
1322
1326
|
signal,
|
|
1323
1327
|
});
|
|
1324
1328
|
if (!res.ok) {
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
error_description?: string;
|
|
1332
|
-
};
|
|
1333
|
-
if (typeof parsed.error === "string") {
|
|
1334
|
-
errCodeExact = parsed.error.trim();
|
|
1335
|
-
errDesc = [parsed.error, parsed.error_description].filter(Boolean).join(": ");
|
|
1336
|
-
} else if (parsed.error && typeof parsed.error === "object") {
|
|
1337
|
-
errCodeExact = typeof parsed.error.code === "string" ? parsed.error.code.trim() : undefined;
|
|
1338
|
-
errDesc = [parsed.error.code, parsed.error.message, parsed.error_description].filter(Boolean).join(": ");
|
|
1339
|
-
} else {
|
|
1340
|
-
errDesc = parsed.error_description || `HTTP ${res.status}`;
|
|
1341
|
-
}
|
|
1342
|
-
if (!errDesc) errDesc = `HTTP ${res.status}`;
|
|
1343
|
-
} catch { errDesc = `HTTP ${res.status}`; }
|
|
1344
|
-
// `invalid_grant` is the standard OAuth code for a refresh token that is no longer
|
|
1345
|
-
// usable, and upstream sends it bare with no description. Without it here the dead
|
|
1346
|
-
// grant is classified "unknown", which callers treat as transient — so the account
|
|
1347
|
-
// is never retired and every request repeats the same doomed refresh (#2887).
|
|
1348
|
-
//
|
|
1349
|
-
// Matched on the exact `error` CODE, not anywhere in the combined text: a transient
|
|
1350
|
-
// `server_error` whose description happens to mention invalid_grant would otherwise
|
|
1351
|
-
// retire a healthy account, which is the failure this whole change exists to remove.
|
|
1352
|
-
//
|
|
1353
|
-
// That rule binds the DESCRIPTION words too. "invalidated", "revoked" and "expired" read
|
|
1354
|
-
// as terminal prose, but upstream puts arbitrary text there: a `server_error` whose
|
|
1355
|
-
// description says "token was revoked" or "session expired" is still a 5xx blip, and
|
|
1356
|
-
// retiring the account on it is exactly the false quarantine #2887 exists to prevent.
|
|
1357
|
-
// So a body that carries a structured code is classified by that code ALONE. The
|
|
1358
|
-
// substring fallback survives only where there is no structured code to read at all --
|
|
1359
|
-
// a description-only body, or one this parser could not decode -- because there the
|
|
1360
|
-
// prose is the only signal upstream gave us.
|
|
1361
|
-
const structuredCode = errCodeExact ? errCodeExact : undefined;
|
|
1362
|
-
const proseIsOnlySignal = structuredCode === undefined;
|
|
1363
|
-
const reason = structuredCode === "invalid_grant"
|
|
1364
|
-
|| structuredCode === "refresh_token_invalidated"
|
|
1365
|
-
|| (proseIsOnlySignal
|
|
1366
|
-
&& (errDesc.includes("invalidated") || errDesc.includes("revoked"))) ? "revoked" as const
|
|
1367
|
-
: structuredCode === "refresh_token_expired"
|
|
1368
|
-
|| (proseIsOnlySignal && errDesc.includes("expired")) ? "expired" as const
|
|
1369
|
-
: "unknown" as const;
|
|
1370
|
-
throw new TokenRefreshError(reason, `Codex token refresh failed (${reason}); reauthenticate the account.`);
|
|
1329
|
+
const body = await readBoundedResponseBody(res, { signal, maxBytes: 16_384, fatalUtf8: true })
|
|
1330
|
+
.catch(() => undefined);
|
|
1331
|
+
const failure = classifyChatgptRefreshFailure(res.status, body?.displaySafe ? body.text : "");
|
|
1332
|
+
const { reason } = failure;
|
|
1333
|
+
noteChatgptRefreshFailure("pool", res.status, failure);
|
|
1334
|
+
throw new TokenRefreshError(reason, `Codex token refresh failed (${reason}); reauthenticate the account.`, res.status, failure.code);
|
|
1371
1335
|
}
|
|
1372
1336
|
const data = (await res.json()) as { access_token: string; refresh_token?: string; expires_in: number };
|
|
1373
1337
|
// Guard against a missing/non-finite/negative expires_in (malformed upstream
|
|
@@ -2,8 +2,7 @@ import { readCodexAccountRecord } from "./account-store";
|
|
|
2
2
|
import { isAccountNeedsReauth } from "./account-runtime-state";
|
|
3
3
|
import {
|
|
4
4
|
MAIN_CODEX_ACCOUNT_ID,
|
|
5
|
-
|
|
6
|
-
isMainAccountCredentialUsable,
|
|
5
|
+
getMainAccountCredentialStatus,
|
|
7
6
|
isMainAccountTokenLive,
|
|
8
7
|
} from "./main-account";
|
|
9
8
|
import { hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount } from "./account-id";
|
|
@@ -14,7 +13,7 @@ import { isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
|
14
13
|
export interface CodexAccountUsabilityOptions {
|
|
15
14
|
/** Route using cached runtime state only; the caller must reject selected main before auth. */
|
|
16
15
|
nativeMainSelectionOnly?: boolean;
|
|
17
|
-
/**
|
|
16
|
+
/** Liveness override; ownership/read fences independently control physical credential reads. */
|
|
18
17
|
isMainAccountTokenLive?: typeof isMainAccountTokenLive;
|
|
19
18
|
/** Confirmed account ids for an account-gated model; omitted for ordinary native models. */
|
|
20
19
|
modelEligibleAccountIds?: ReadonlySet<string>;
|
|
@@ -88,15 +87,19 @@ export function codexAccountUnusableReason(
|
|
|
88
87
|
// A legacy pool row with the sentinel makes an active `__main__` ambiguous.
|
|
89
88
|
// Fail closed until the authenticated compatibility-delete path removes it.
|
|
90
89
|
if (hasLegacyMainCodexPoolAccount(config.codexAccounts)) return "legacy_pool_sentinel";
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
// before reservation or token materialization. Treat cached main as a routing
|
|
94
|
-
// candidate without touching the credential file so affinity is not rebound.
|
|
90
|
+
// These scopes cannot consume physical stored-main state, even indirectly through an
|
|
91
|
+
// ordinary quarantine plus hasMainAccountRefreshGrant(). Caller liveness is request-owned.
|
|
95
92
|
if (options.nativeMainSelectionOnly) return undefined;
|
|
93
|
+
if (options.requestOwnedMainCredential) {
|
|
94
|
+
return options.isMainAccountTokenLive?.() === false ? "main_credential_unavailable" : undefined;
|
|
95
|
+
}
|
|
96
|
+
const credential = getMainAccountCredentialStatus();
|
|
97
|
+
if (credential.refreshGrantRejected) return "needs_reauth";
|
|
98
|
+
if (isAccountNeedsReauth(accountId) && !credential.hasRefreshGrant) return "needs_reauth";
|
|
96
99
|
// Main account: a refresh grant is enough to route; materialization refreshes before I/O.
|
|
97
100
|
const mainLive = options.isMainAccountTokenLive
|
|
98
101
|
? options.isMainAccountTokenLive()
|
|
99
|
-
:
|
|
102
|
+
: credential.usable;
|
|
100
103
|
return mainLive ? undefined : "main_credential_unavailable";
|
|
101
104
|
}
|
|
102
105
|
const exists = (config.codexAccounts ?? [])
|
|
@@ -15,7 +15,7 @@ import { getCodexAccountAutoSwitchThresholdOverride } from "../account-auto-swit
|
|
|
15
15
|
import { clearThreadAccountMapForAccount, isCodexAccountPlanExcluded, reconcileCodexActiveAfterExclusion } from "../routing";
|
|
16
16
|
import { codexPlanValue, isThirtyDayOnlyCodexPlan } from "../plan";
|
|
17
17
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "../account-runtime-state";
|
|
18
|
-
import { getValidMainAccountToken,
|
|
18
|
+
import { getValidMainAccountToken, isMainAccountRefreshGrantRejected, MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
19
19
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
20
20
|
import { captureMainAccountIdentityGeneration, getMainAccountCredentialPresence, getMainAccountInfoCache, getObservedMainQuotaIdentityKey, isMainAccountIdentityGenerationLive } from "../main-account-cache";
|
|
21
21
|
import type { CodexQuotaRefreshOutcome } from "../quota-refresh-outcome";
|
|
@@ -342,60 +342,76 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
342
342
|
maskEmails,
|
|
343
343
|
)];
|
|
344
344
|
});
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
359
|
-
needsReauth: mainNeedsReauth,
|
|
360
|
-
});
|
|
361
|
-
// The main row carries the same attribution as a pool row. Reaching this point without
|
|
362
|
-
// `mainMissingCredential` means the runtime reauth flag is what set `mainNeedsReauth`, so the
|
|
363
|
-
// cause is a refresh that did not complete.
|
|
364
|
-
const mainReauthReason: CodexAccountReauthReason | undefined = mainMissingCredential
|
|
365
|
-
? "missing_credential"
|
|
366
|
-
: mainNeedsReauth
|
|
367
|
-
? "refresh_failed"
|
|
368
|
-
: mainHealth.status === "reauth_required" ? mainHealth.reason : undefined;
|
|
369
|
-
const main: CodexAuthAccountDto = {
|
|
370
|
-
id: MAIN_CODEX_ACCOUNT_ID,
|
|
371
|
-
email: projectEmail(mainInfo.email, maskEmails) ?? "Codex App login",
|
|
372
|
-
plan: mainInfo.plan,
|
|
373
|
-
...(mainSnapshotLive && mainResult.quotaRefresh && mainResult.quotaRefreshGeneration !== undefined
|
|
345
|
+
const projectSnapshot = (mainRefreshRefused = false): CodexAuthAccountsSnapshot => {
|
|
346
|
+
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
347
|
+
const mainSnapshotLive = isMainAccountIdentityGenerationLive(fetchedMainGeneration);
|
|
348
|
+
// An ordinary same-account return can be parsed after its credential was replaced.
|
|
349
|
+
// The card and hard-lock status must describe the same published quota snapshot.
|
|
350
|
+
const mainInfo = mainSnapshotLive
|
|
351
|
+
? mainResult.infoUnpublished ? getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO : mainResult.info
|
|
352
|
+
: EMPTY_MAIN_ACCOUNT_INFO;
|
|
353
|
+
const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
|
|
354
|
+
? mainResult.hasCredential
|
|
355
|
+
: getMainAccountCredentialPresence() ?? false;
|
|
356
|
+
const mainMissingCredential = mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential;
|
|
357
|
+
const liveQuotaRefresh = mainSnapshotLive && mainResult.quotaRefreshGeneration !== undefined
|
|
374
358
|
&& isMainAccountIdentityGenerationLive(mainResult.quotaRefreshGeneration)
|
|
375
|
-
?
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
:
|
|
359
|
+
? mainResult.quotaRefresh : undefined;
|
|
360
|
+
const mainNeedsReauth = mainMissingCredential || mainRefreshRefused || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
361
|
+
const mainHealth = projectCodexAccountHealth({
|
|
362
|
+
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
363
|
+
needsReauth: mainNeedsReauth,
|
|
364
|
+
});
|
|
365
|
+
// The main row carries the same attribution as a pool row. Reaching this point without
|
|
366
|
+
// Only a current terminal usage probe can attribute the reauth cause. A transient status
|
|
367
|
+
// never replaces a refresh failure, including a separately recorded refused grant.
|
|
368
|
+
const mainReauthReason: CodexAccountReauthReason | undefined = mainMissingCredential
|
|
369
|
+
? "missing_credential"
|
|
370
|
+
: mainNeedsReauth
|
|
371
|
+
? mainResult.terminalAuthFailure === true && liveQuotaRefresh?.status === "http_error"
|
|
372
|
+
? "unauthorized" : "refresh_failed"
|
|
373
|
+
: mainHealth.status === "reauth_required" ? mainHealth.reason : undefined;
|
|
374
|
+
const main: CodexAuthAccountDto = {
|
|
375
|
+
id: MAIN_CODEX_ACCOUNT_ID,
|
|
376
|
+
email: projectEmail(mainInfo.email, maskEmails) ?? "Codex App login",
|
|
377
|
+
plan: mainInfo.plan,
|
|
378
|
+
...(liveQuotaRefresh ? { quotaRefresh: liveQuotaRefresh } : {}),
|
|
379
|
+
logLabel: "main",
|
|
380
|
+
isMain: true,
|
|
381
|
+
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
382
|
+
mainAccountHardLock: { ...getMainAccountHardLockStatus(runtimeConfig),
|
|
383
|
+
externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
|
|
384
|
+
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
385
|
+
autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
386
|
+
creditsAfterLimit: codexAccountUsesCreditsAfterLimit(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
387
|
+
hasCredential: hasMainCredential,
|
|
388
|
+
needsReauth: mainNeedsReauth,
|
|
389
|
+
...(mainReauthReason !== undefined ? { reauthReason: mainReauthReason } : {}),
|
|
390
|
+
quota: mainInfo.quota
|
|
391
|
+
? quotaForPlan(mainQuotaWithCarriedResetCredits(mainInfo.quota), mainInfo.plan)
|
|
392
|
+
: null,
|
|
393
|
+
...codexCreditsDtoField(runtimeConfig, MAIN_CODEX_ACCOUNT_ID, getMainChatgptAccountId()),
|
|
394
|
+
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
accounts: [main, ...withQuota],
|
|
398
|
+
mainIdentityGeneration: mainSnapshotLive
|
|
399
|
+
? fetchedMainGeneration
|
|
400
|
+
: captureMainAccountIdentityGeneration(),
|
|
401
|
+
};
|
|
398
402
|
};
|
|
403
|
+
// Project the refusal and DTO in the same owned synchronous observation. A same-account grant
|
|
404
|
+
// replacement during earlier pool probes must not inherit the previous grant's refusal.
|
|
405
|
+
const projectionLease = tryAcquireNativeMainProfileClaim();
|
|
406
|
+
if (!projectionLease) return projectSnapshot();
|
|
407
|
+
try {
|
|
408
|
+
return await withNativeMainCredentialClaim(async () => projectSnapshot(isMainAccountRefreshGrantRejected()));
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (isNativeMainClaimUnavailable(error)) return projectSnapshot();
|
|
411
|
+
throw error;
|
|
412
|
+
} finally {
|
|
413
|
+
projectionLease.release();
|
|
414
|
+
}
|
|
399
415
|
}
|
|
400
416
|
|
|
401
417
|
/** One opted-in account's metadata; reuse the bounded WHAM 401 recovery and generation fence. */
|
|
@@ -406,16 +422,10 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
|
|
|
406
422
|
try {
|
|
407
423
|
reconcileMainCodexAccountRuntimeState();
|
|
408
424
|
if (isAccountNeedsReauth(accountId)) return;
|
|
409
|
-
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
410
|
-
const writerGeneration = captureConfigGeneration();
|
|
411
425
|
try {
|
|
412
|
-
// Refresh
|
|
426
|
+
// Refresh owns credential-scoped refusal evidence; do not recreate a global quarantine.
|
|
413
427
|
if (!await getValidMainAccountToken({ preserveReauth: true })) return;
|
|
414
|
-
} catch
|
|
415
|
-
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth"
|
|
416
|
-
&& isMainAccountIdentityGenerationLive(identityGeneration)) {
|
|
417
|
-
markAccountNeedsReauth(accountId, writerGeneration);
|
|
418
|
-
}
|
|
428
|
+
} catch {
|
|
419
429
|
return;
|
|
420
430
|
}
|
|
421
431
|
if (isAccountNeedsReauth(accountId)) return;
|
|
@@ -476,7 +486,7 @@ export async function pauseExhaustedCodexAccounts(
|
|
|
476
486
|
}
|
|
477
487
|
return {
|
|
478
488
|
shouldPause: !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
479
|
-
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan),
|
|
489
|
+
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan, codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)),
|
|
480
490
|
checkedAccountCount: 1,
|
|
481
491
|
failedAccountCount: 0,
|
|
482
492
|
};
|
|
@@ -511,7 +521,7 @@ export async function pauseExhaustedCodexAccounts(
|
|
|
511
521
|
continue;
|
|
512
522
|
}
|
|
513
523
|
checkedAccountCount += 1;
|
|
514
|
-
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
524
|
+
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan, codexAccountUsesCreditsAfterLimit(config, account.id))) {
|
|
515
525
|
exhaustedIds.push(account.id);
|
|
516
526
|
}
|
|
517
527
|
}
|