@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
|
@@ -16,14 +16,15 @@ import { getMainAccountPlan, isMainAccountTokenVerifiablyLive, MAIN_CODEX_ACCOUN
|
|
|
16
16
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
17
17
|
import { captureMainAccountIdentityGeneration, clearMainAccountInfoCache, getMainAccountInfoCache, getMainQuotaCredentialGeneration, isMainAccountIdentityGenerationLive, isMainQuotaWriterLive, matchesMainQuotaCredential, observeMainQuotaCredential, setMainAccountCredentialPresence, setMainAccountInfoCache } from "../main-account-cache";
|
|
18
18
|
import type { MainQuotaWriter, MainAccountInfo } from "../main-account-cache";
|
|
19
|
-
import
|
|
19
|
+
import { isCodexTerminalAuthCode } from "../quota-refresh-outcome";
|
|
20
|
+
import type { CodexQuotaRefreshOutcome, CodexTerminalAuthCode } from "../quota-refresh-outcome";
|
|
20
21
|
import { observeMainReserveRevocation } from "../reserve-availability";
|
|
21
22
|
import type { AdmissionLease } from "../../lib/admission";
|
|
22
23
|
import { nonEmptyPlan } from "./runtime-config";
|
|
23
24
|
import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
|
|
24
25
|
import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
|
|
25
26
|
import { withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
|
|
26
|
-
import {
|
|
27
|
+
import { readMainAuthErrorCode, currentQuotaDispatchSequence, nextQuotaDispatchSequence, isQuotaDispatchCurrent, publishQuotaDispatch } from "./pool-quota-probe";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Last reset-credit count this process parsed for the main account, tagged with the
|
|
@@ -79,14 +80,18 @@ function sharedMainQuotaPacingEnabled(config: OcxConfig): boolean {
|
|
|
79
80
|
* itself would make a real 401 permanently transient.
|
|
80
81
|
*/
|
|
81
82
|
export async function isTerminalMainAuthResponse(resp: Response, accessTokenLive: boolean): Promise<boolean> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
return (await classifyMainAuthResponse(resp, accessTokenLive)).terminal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** As `isTerminalMainAuthResponse`, also naming the provider code that made it terminal. */
|
|
87
|
+
async function classifyMainAuthResponse(
|
|
88
|
+
resp: Response,
|
|
89
|
+
accessTokenLive: boolean,
|
|
90
|
+
): Promise<{ terminal: boolean; code?: CodexTerminalAuthCode }> {
|
|
91
|
+
if (resp.status !== 401 && resp.status !== 403) return { terminal: false };
|
|
88
92
|
const code = await readMainAuthErrorCode(resp);
|
|
89
|
-
|
|
93
|
+
if (isCodexTerminalAuthCode(code)) return { terminal: true, code };
|
|
94
|
+
return { terminal: resp.status === 401 && !accessTokenLive };
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
export interface MainResetQuotaProof {
|
|
@@ -103,6 +108,8 @@ export interface MainAccountInfoFetchResult {
|
|
|
103
108
|
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
104
109
|
/** Internal dispatch fence for diagnostics only; never copied into a public DTO or cache. */
|
|
105
110
|
quotaRefreshGeneration?: number;
|
|
111
|
+
/** This current-credential usage response supplied terminal auth evidence; never public. */
|
|
112
|
+
terminalAuthFailure?: true;
|
|
106
113
|
/** Whether this attempt safely inspected the physical native-main credential. */
|
|
107
114
|
credentialChecked: boolean;
|
|
108
115
|
/** Meaningful only when credentialChecked is true. */
|
|
@@ -288,7 +295,8 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
288
295
|
const resp = admission.response;
|
|
289
296
|
quotaPhase = "publish";
|
|
290
297
|
if (!resp.ok) {
|
|
291
|
-
const
|
|
298
|
+
const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
299
|
+
const terminalAuthFailure = authFailure.terminal;
|
|
292
300
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
|
|
293
301
|
if (retried) return retried;
|
|
294
302
|
if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
|
|
@@ -302,10 +310,15 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
302
310
|
if (diagnosticStillLive) quotaRefreshGeneration = captureMainAccountIdentityGeneration();
|
|
303
311
|
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
304
312
|
}
|
|
313
|
+
// Keep the last-known plan visible: after a plan change revokes the session, the plan the
|
|
314
|
+
// account HAD is what tells the operator why it stopped working.
|
|
305
315
|
return {
|
|
306
|
-
info: EMPTY_MAIN_ACCOUNT_INFO,
|
|
307
|
-
|
|
316
|
+
info: { ...EMPTY_MAIN_ACCOUNT_INFO, plan: nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan()) },
|
|
317
|
+
credentialChecked: true, hasCredential: true,
|
|
318
|
+
quotaRefresh: { status: "http_error", httpStatus: resp.status,
|
|
319
|
+
...(authFailure.code ? { code: authFailure.code } : {}) },
|
|
308
320
|
quotaRefreshGeneration,
|
|
321
|
+
...(terminalAuthFailure ? { terminalAuthFailure: true as const } : {}),
|
|
309
322
|
};
|
|
310
323
|
}
|
|
311
324
|
quotaPhase = "body";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
|
|
1
2
|
import { nextCodexUsageQueryAt, nextQuotaQueryDelay, pruneRemovedCodexPoolUsageAccounts } from "../quota-query-backoff";
|
|
2
3
|
import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
|
|
3
4
|
import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
|
|
@@ -6,8 +7,8 @@ import { getAccountQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota"
|
|
|
6
7
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
7
8
|
import { claimDueCodexQuotaRecoveryProbes, settleCodexQuotaRecoveryProbe } from "../routing";
|
|
8
9
|
import { readCodexTokens } from "../auth-collision";
|
|
9
|
-
import { isAccountNeedsReauth
|
|
10
|
-
import { getValidMainAccountToken,
|
|
10
|
+
import { isAccountNeedsReauth } from "../account-runtime-state";
|
|
11
|
+
import { getValidMainAccountToken, MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "../main-account";
|
|
11
12
|
import { captureConfigGeneration, registerStateSweepAfterTick } from "../../lib/state-store-sweeper";
|
|
12
13
|
import { observeMainQuotaCredential, getMainQuotaCredentialGeneration, captureMainAccountIdentityGeneration, isMainAccountIdentityGenerationLive } from "../main-account-cache";
|
|
13
14
|
import { getMainAccountHardLockStatus } from "../main-account-hard-lock";
|
|
@@ -58,7 +59,7 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
|
|
|
58
59
|
// Defence in depth: independent scopes are already excluded at the claim site.
|
|
59
60
|
// Generic WHAM must never clear Reserve even if claim selection changes.
|
|
60
61
|
const recovered = (claim.scope === undefined || claim.scope === "shared")
|
|
61
|
-
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
62
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan, codexAccountUsesCreditsAfterLimit(config, claim.accountId));
|
|
62
63
|
settleCodexQuotaRecoveryProbe(claim, recovered, {
|
|
63
64
|
credentialGeneration: result.freshCredentialGeneration,
|
|
64
65
|
}, now);
|
|
@@ -100,18 +101,14 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
|
|
|
100
101
|
if (getMainAccountHardLockStatus(config).state !== "blocked"
|
|
101
102
|
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
102
103
|
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
103
|
-
const writerGeneration = captureConfigGeneration();
|
|
104
104
|
try {
|
|
105
105
|
// Refresh can require an exclusive credential claim: never hold WHAM's shared
|
|
106
106
|
// claim while obtaining a valid token. The runtime lease spans both operations.
|
|
107
107
|
const prepared = await getValidMainAccountToken({ preserveReauth: true });
|
|
108
108
|
if (!prepared) return;
|
|
109
109
|
observeMainQuotaCredential(prepared.accessToken, prepared.chatgptAccountId);
|
|
110
|
-
} catch
|
|
111
|
-
|
|
112
|
-
&& isMainAccountIdentityGenerationLive(identityGeneration)) {
|
|
113
|
-
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
114
|
-
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Native refresh retains its own grant-scoped refusal; global quarantine would outlive it.
|
|
115
112
|
return;
|
|
116
113
|
}
|
|
117
114
|
if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
@@ -15,14 +15,12 @@ import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
|
|
|
15
15
|
import { claimQuotaRecovery, fencePropagatedQuotaRecovery, quotaRecoveryTerminalFor, releaseQuotaRecovery, settleQuotaRecovery, settleQuotaRecoveryTerminal } from "../quota-401-recovery";
|
|
16
16
|
import { seedLoginRowsForTests } from "./login-state";
|
|
17
17
|
import { nonEmptyPlan } from "./runtime-config";
|
|
18
|
+
import { CODEX_TERMINAL_AUTH_CODES } from "../quota-refresh-outcome";
|
|
18
19
|
|
|
19
20
|
export const POOL_CACHE_TTL = 5 * 60_000;
|
|
20
21
|
export const POOL_QUOTA_REFRESH_CONCURRENCY = 4;
|
|
21
22
|
|
|
22
|
-
export const MAIN_TERMINAL_AUTH_CODES = new Set(
|
|
23
|
-
"invalid_workspace_selected",
|
|
24
|
-
"invalid_refresh_token",
|
|
25
|
-
]);
|
|
23
|
+
export const MAIN_TERMINAL_AUTH_CODES: ReadonlySet<string> = new Set(CODEX_TERMINAL_AUTH_CODES);
|
|
26
24
|
|
|
27
25
|
export async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
|
|
28
26
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
|
|
1
2
|
import { getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord, CodexCredentialGenerationConflictError } from "../account-store";
|
|
2
3
|
import { isCompleteCodexQuotaRecoverySnapshot } from "../quota";
|
|
3
4
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
@@ -233,7 +234,7 @@ async function refreshAfterManualReset(
|
|
|
233
234
|
&& proof.credentialGeneration === auth.mainProof.credentialGeneration
|
|
234
235
|
&& proof.writer.identityKey === auth.mainProof.writer.identityKey
|
|
235
236
|
&& proof.writer.identityGeneration === auth.mainProof.writer.identityGeneration
|
|
236
|
-
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan);
|
|
237
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan, codexAccountUsesCreditsAfterLimit(getRuntimeConfig(config), accountId));
|
|
237
238
|
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered);
|
|
238
239
|
return manualResetAuthStillLive(accountId, auth) ? result.freshResetCredits : undefined;
|
|
239
240
|
}
|
|
@@ -255,7 +256,7 @@ async function refreshAfterManualReset(
|
|
|
255
256
|
const record = readCodexAccountRecord(accountId);
|
|
256
257
|
const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId
|
|
257
258
|
&& (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence
|
|
258
|
-
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
259
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan, codexAccountUsesCreditsAfterLimit(getRuntimeConfig(config), accountId));
|
|
259
260
|
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered, {
|
|
260
261
|
credentialGeneration: result.freshCredentialGeneration,
|
|
261
262
|
refreshLineage: result.resetRefreshLineage,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasSpendableCodexCredits } from "./quota-types";
|
|
1
2
|
import { noteMainAccountActivity } from "./main-account-external-usage";
|
|
2
3
|
import { codexAccountPriorityFailbackEnabled } from "./account-priority";
|
|
3
4
|
import type { PoolQuotaWriter } from "./quota-types";
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
17
18
|
import { ConfigMutationLockError } from "../config";
|
|
18
19
|
import { NativeProfileError } from "./native-profile-types";
|
|
19
|
-
import { isCodexAccountUsable } from "./account-usability";
|
|
20
|
+
import { codexAccountUnusableReason, isCodexAccountUsable } from "./account-usability";
|
|
20
21
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
21
22
|
import {
|
|
22
23
|
MAIN_CODEX_ACCOUNT_ID,
|
|
@@ -102,7 +103,7 @@ import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
|
102
103
|
function requestOwnedMainPinHasQuotaHeadroom(config: OcxConfig): boolean {
|
|
103
104
|
const threshold = getEffectiveCodexAutoSwitchThreshold(config, MAIN_CODEX_ACCOUNT_ID);
|
|
104
105
|
if (threshold <= 0) return true;
|
|
105
|
-
const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID));
|
|
106
|
+
const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID), undefined, Date.now(), codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID));
|
|
106
107
|
return usage >= CODEX_UNKNOWN_USAGE_SCORE || usage < threshold;
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -378,9 +379,11 @@ export class CodexAuthContextError extends Error {
|
|
|
378
379
|
}
|
|
379
380
|
|
|
380
381
|
export class CodexPoolAuthenticationError extends Error {
|
|
381
|
-
|
|
382
|
+
readonly quarantinedMain: boolean;
|
|
383
|
+
constructor(message = "OpenAI account pool has no usable account credential", options?: { quarantinedMain?: boolean }) {
|
|
382
384
|
super(message);
|
|
383
385
|
this.name = "CodexPoolAuthenticationError";
|
|
386
|
+
this.quarantinedMain = options?.quarantinedMain === true;
|
|
384
387
|
}
|
|
385
388
|
}
|
|
386
389
|
|
|
@@ -530,7 +533,7 @@ export class CodexMainAccountCreditsOffError extends CodexAccountCooldownError {
|
|
|
530
533
|
super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
|
|
531
534
|
this.name = "CodexMainAccountCreditsOffError";
|
|
532
535
|
this.resetAt = resetAt;
|
|
533
|
-
this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off
|
|
536
|
+
this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available."
|
|
534
537
|
+ " Choose another account, wait for the limit to reset, or allow the main account under \"Use credits\" in Codex Auth.";
|
|
535
538
|
}
|
|
536
539
|
}
|
|
@@ -723,7 +726,8 @@ export function unwrapUpstreamRetryEvidenceError(error: unknown): unknown {
|
|
|
723
726
|
* file, which several callers are forbidden to open, so every long window counts instead.
|
|
724
727
|
*/
|
|
725
728
|
function mainCreditsHoldResetAt(config: Pick<OcxConfig, "creditCodexAccountIds">): number | undefined {
|
|
726
|
-
if (codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
|
|
729
|
+
if (codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
|
|
730
|
+
&& hasSpendableCodexCredits(getMainPolicyQuota())) return undefined;
|
|
727
731
|
return codexUsageLimitResetAt(getMainPolicyQuota(), undefined, Date.now());
|
|
728
732
|
}
|
|
729
733
|
|
|
@@ -920,7 +924,7 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
920
924
|
&& !(cause instanceof CodexCredentialRefreshBusyError)
|
|
921
925
|
&& !(cause instanceof CodexCredentialRefreshStaleError)
|
|
922
926
|
&& !(cause instanceof MainAuthJsonChangedDuringRefreshError)
|
|
923
|
-
&& !(cause instanceof MainAccountTokenRefreshError
|
|
927
|
+
&& !(cause instanceof MainAccountTokenRefreshError)
|
|
924
928
|
&& !(cause instanceof NativeProfileError && cause.retryable)
|
|
925
929
|
&& !(cause instanceof DOMException && cause.name === "AbortError")
|
|
926
930
|
&& !(cause instanceof ConfigMutationLockError);
|
|
@@ -1314,7 +1318,12 @@ export async function resolveCodexAuthContext(
|
|
|
1314
1318
|
"Selected Codex account does not support this model",
|
|
1315
1319
|
);
|
|
1316
1320
|
}
|
|
1317
|
-
|
|
1321
|
+
const mainReason = fixedAccountId === MAIN_CODEX_ACCOUNT_ID && !nativeMainReadsForbidden
|
|
1322
|
+
&& !policy.pausedCodexAccountIds?.includes(fixedAccountId)
|
|
1323
|
+
? codexAccountUnusableReason(config, fixedAccountId, selectionOptions) : undefined;
|
|
1324
|
+
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable", {
|
|
1325
|
+
quarantinedMain: mainReason === "needs_reauth",
|
|
1326
|
+
});
|
|
1318
1327
|
}
|
|
1319
1328
|
// Recovery or a turn drain deliberately makes physical main unobservable.
|
|
1320
1329
|
// If no healthy pool route is available, report the temporary fence rather
|
|
@@ -1339,7 +1348,12 @@ export async function resolveCodexAuthContext(
|
|
|
1339
1348
|
: "Codex accounts that support this model are currently unavailable",
|
|
1340
1349
|
);
|
|
1341
1350
|
}
|
|
1342
|
-
throw new CodexPoolAuthenticationError(
|
|
1351
|
+
throw new CodexPoolAuthenticationError(undefined, {
|
|
1352
|
+
quarantinedMain: !nativeMainReadsForbidden
|
|
1353
|
+
&& options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
|
|
1354
|
+
&& !policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID)
|
|
1355
|
+
&& codexAccountUnusableReason(config, MAIN_CODEX_ACCOUNT_ID, selectionOptions) === "needs_reauth",
|
|
1356
|
+
});
|
|
1343
1357
|
}
|
|
1344
1358
|
accountId = selected;
|
|
1345
1359
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) assertMainAccountPolicy(policy);
|
|
@@ -1370,10 +1384,15 @@ export async function resolveCodexAuthContext(
|
|
|
1370
1384
|
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
1371
1385
|
}
|
|
1372
1386
|
if (isAccountNeedsReauth(accountId)) {
|
|
1373
|
-
throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication"
|
|
1387
|
+
throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication", {
|
|
1388
|
+
quarantinedMain: accountId === MAIN_CODEX_ACCOUNT_ID && !nativeMainReadsForbidden,
|
|
1389
|
+
});
|
|
1374
1390
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1391
|
+
const unusableReason = codexAccountUnusableReason(config, accountId, selectionOptions);
|
|
1392
|
+
if (unusableReason !== undefined) {
|
|
1393
|
+
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable", {
|
|
1394
|
+
quarantinedMain: accountId === MAIN_CODEX_ACCOUNT_ID && unusableReason === "needs_reauth",
|
|
1395
|
+
});
|
|
1377
1396
|
}
|
|
1378
1397
|
}
|
|
1379
1398
|
} catch (cause) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { collapseAntigravityPublicModels } from "../../providers/antigravity-effort-families";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
4
5
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
@@ -504,6 +505,7 @@ export function resolveSlugAliasCollisions(goModels: CatalogModel[]): Set<Catalo
|
|
|
504
505
|
}
|
|
505
506
|
|
|
506
507
|
export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): CatalogModel[] {
|
|
508
|
+
goModels = collapseAntigravityPublicModels(goModels);
|
|
507
509
|
const collisionSkipped = resolveSlugAliasCollisions(goModels);
|
|
508
510
|
const comboPublicSlugs = new Set(goModels
|
|
509
511
|
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
@@ -525,6 +527,7 @@ export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): Cata
|
|
|
525
527
|
}
|
|
526
528
|
|
|
527
529
|
export function uniqueCatalogModelsForRawPublicList(goModels: CatalogModel[]): CatalogModel[] {
|
|
530
|
+
goModels = collapseAntigravityPublicModels(goModels);
|
|
528
531
|
const publicId = (model: CatalogModel): string => model.alias ?? `${model.provider}/${model.id}`;
|
|
529
532
|
const comboPublicIds = new Set(goModels
|
|
530
533
|
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
@@ -295,7 +295,8 @@ export function applyProviderConfigHints(
|
|
|
295
295
|
inputModalities = base.includes("image") ? [...base] : [...base, "image"];
|
|
296
296
|
}
|
|
297
297
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
298
|
-
const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id)
|
|
298
|
+
const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id)
|
|
299
|
+
?? (model.antigravityEffortWireModelIds !== undefined);
|
|
299
300
|
const defaultReasoningEffort = staticPolicy.model.defaultReasoningEffort ?? model.defaultReasoningEffort;
|
|
300
301
|
const supportsReasoningSummaries = staticPolicy.model.supportsReasoningSummaries;
|
|
301
302
|
const supportsVerbosity = staticPolicy.model.supportsVerbosity;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
|
|
2
|
+
import { antigravityFamilyDisabled, collapseAntigravityPublicModels, projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
|
|
2
3
|
import { XAI_OAUTH_FAST_VARIANT_IDS } from "../../providers/xai-fast-model";
|
|
3
4
|
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
4
5
|
import { execFileSync } from "node:child_process";
|
|
@@ -267,7 +268,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
|
|
|
267
268
|
|
|
268
269
|
export function filterCatalogVisibleModels(
|
|
269
270
|
models: CatalogModel[],
|
|
270
|
-
config: Pick<OcxConfig, "disabledModels" | "providers">,
|
|
271
|
+
config: Pick<OcxConfig, "disabledModels" | "providers" | "modelDiscovery">,
|
|
271
272
|
): CatalogModel[] {
|
|
272
273
|
const disabled = new Set(config.disabledModels ?? []);
|
|
273
274
|
const allowByProvider = new Map<string, Set<string>>();
|
|
@@ -289,12 +290,14 @@ export function filterCatalogVisibleModels(
|
|
|
289
290
|
// Two catalog stages with different equivalence relations is the exact bug class
|
|
290
291
|
// this change exists to remove.
|
|
291
292
|
if (Array.isArray(sel) && sel.length > 0) {
|
|
292
|
-
allowByProvider.set(name, new Set(
|
|
293
|
+
allowByProvider.set(name, new Set(projectAntigravitySelectedModels(name, sel, models)
|
|
294
|
+
.map(model => slugEquivalenceKey(routedSlug(name, model)))));
|
|
293
295
|
}
|
|
294
296
|
}
|
|
295
|
-
return models.filter(m => {
|
|
297
|
+
return collapseAntigravityPublicModels(models).filter(m => {
|
|
296
298
|
if (initialModelSelectionPending(config.providers[m.provider])) return false;
|
|
297
299
|
if (config.providers[m.provider]?.disabled === true) return false;
|
|
300
|
+
if (antigravityFamilyDisabled(config, m, models)) return false;
|
|
298
301
|
const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
|
|
299
302
|
// disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
|
|
300
303
|
for (const stored of disabled) {
|
|
@@ -113,6 +113,8 @@ export interface CatalogModel {
|
|
|
113
113
|
displayName?: string;
|
|
114
114
|
owned_by?: string;
|
|
115
115
|
reasoningEfforts?: string[];
|
|
116
|
+
/** Exact CCA family evidence, retained through the in-memory discovery cache. */
|
|
117
|
+
antigravityEffortWireModelIds?: Partial<Record<"low" | "medium" | "high", string>>;
|
|
116
118
|
/** Suppress only catalog synthesis of a missing max rung; provider-declared max survives. */
|
|
117
119
|
suppressSyntheticMax?: boolean;
|
|
118
120
|
defaultReasoningEffort?: string;
|
|
@@ -751,20 +751,29 @@ export async function fetchProviderModelsWithAuth(
|
|
|
751
751
|
const live = antigravity.map(model => applyProviderConfigHints(name, prov, {
|
|
752
752
|
id: model.id,
|
|
753
753
|
provider: name,
|
|
754
|
-
//
|
|
755
|
-
|
|
756
|
-
|
|
754
|
+
// Only an exact discovered wire map proves a new model's selectable effort ladder.
|
|
755
|
+
reasoningEfforts: Object.keys(model.effortWireModelIds ?? {}),
|
|
756
|
+
...(model.effortWireModelIds ? {
|
|
757
|
+
antigravityEffortWireModelIds: model.effortWireModelIds,
|
|
758
|
+
suppressSyntheticMax: true,
|
|
759
|
+
defaultReasoningEffort: model.effortWireModelIds.medium ? "medium" : "high",
|
|
760
|
+
} : {}),
|
|
757
761
|
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
|
|
758
762
|
...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
|
|
759
763
|
}, contextCap, metadataModelIdCaseFold, captured.effectiveAlias));
|
|
760
764
|
const forCache = withConfiguredRetention(live, { retainComboTargets: false });
|
|
761
|
-
if (!
|
|
765
|
+
if (!isCurrentCacheGeneration()) {
|
|
762
766
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
763
767
|
}
|
|
768
|
+
// Save exact family routes before publishing synthetic IDs. These synchronous writes
|
|
769
|
+
// cannot interleave with a credential-generation change; failure keeps the prior cache.
|
|
764
770
|
registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, {
|
|
765
771
|
provider: name,
|
|
766
772
|
cacheGeneration,
|
|
767
773
|
});
|
|
774
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
775
|
+
return observed(withConfiguredRetention(configured), "degraded");
|
|
776
|
+
}
|
|
768
777
|
markProviderDiscoveryOk(name, live.length);
|
|
769
778
|
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
770
779
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { initializeConfigOwnership } from "../../lib/config-ownership";
|
|
@@ -465,7 +466,7 @@ function writeRetainedCatalogSync({
|
|
|
465
466
|
provider.disabled !== true
|
|
466
467
|
&& Array.isArray(provider.selectedModels)
|
|
467
468
|
&& provider.selectedModels.length > 0
|
|
468
|
-
? [[name, new Set(provider.selectedModels)] as const]
|
|
469
|
+
? [[name, new Set(projectAntigravitySelectedModels(name, provider.selectedModels, goModels))] as const]
|
|
469
470
|
: []
|
|
470
471
|
)),
|
|
471
472
|
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Closed diagnostic vocabulary; provider descriptions and unknown codes never escape. */
|
|
2
|
+
const REFRESH_DIAGNOSTIC_CODES = [
|
|
3
|
+
"invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused",
|
|
4
|
+
"refresh_token_expired", "invalid_client", "invalid_request", "server_error",
|
|
5
|
+
"temporarily_unavailable", "rate_limit_exceeded", "unsupported_grant_type", "invalid_scope",
|
|
6
|
+
] as const;
|
|
7
|
+
export type ChatgptRefreshDiagnosticCode = typeof REFRESH_DIAGNOSTIC_CODES[number];
|
|
8
|
+
export type ChatgptRefreshFailure = {
|
|
9
|
+
reason: "expired" | "revoked" | "unknown";
|
|
10
|
+
code?: ChatgptRefreshDiagnosticCode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** HTTP availability failures cannot establish that a refresh grant was revoked. */
|
|
14
|
+
export function classifyChatgptRefreshFailure(status: number, body: string): ChatgptRefreshFailure {
|
|
15
|
+
if (body.length > 16_384) return { reason: "unknown" };
|
|
16
|
+
let payload: unknown;
|
|
17
|
+
try { payload = JSON.parse(body); } catch { return { reason: "unknown" }; }
|
|
18
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { reason: "unknown" };
|
|
19
|
+
const row = payload as Record<string, unknown>;
|
|
20
|
+
let exactCode: string | undefined;
|
|
21
|
+
let description = typeof row.error_description === "string" ? row.error_description : "";
|
|
22
|
+
if (typeof row.error === "string") {
|
|
23
|
+
exactCode = row.error.trim() || undefined;
|
|
24
|
+
} else if (row.error !== undefined) {
|
|
25
|
+
if (!row.error || typeof row.error !== "object" || Array.isArray(row.error)) return { reason: "unknown" };
|
|
26
|
+
const error = row.error as Record<string, unknown>;
|
|
27
|
+
if (error.code !== undefined && typeof error.code !== "string") return { reason: "unknown" };
|
|
28
|
+
exactCode = typeof error.code === "string" ? error.code.trim() || undefined : undefined;
|
|
29
|
+
if (typeof error.message === "string") description = `${error.message} ${description}`;
|
|
30
|
+
}
|
|
31
|
+
const code = (REFRESH_DIAGNOSTIC_CODES as readonly unknown[]).includes(exactCode)
|
|
32
|
+
? exactCode as ChatgptRefreshDiagnosticCode : undefined;
|
|
33
|
+
let reason: ChatgptRefreshFailure["reason"] = "unknown";
|
|
34
|
+
if (status === 400 || status === 401 || status === 403) {
|
|
35
|
+
if (["invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused"].includes(exactCode ?? "")) {
|
|
36
|
+
reason = "revoked";
|
|
37
|
+
} else if (exactCode === "refresh_token_expired") {
|
|
38
|
+
reason = "expired";
|
|
39
|
+
} else if (status === 400 && exactCode === undefined) {
|
|
40
|
+
// Preserve description-only OAuth 400 compatibility; a named code always wins.
|
|
41
|
+
if (description.includes("invalidated") || description.includes("revoked")) reason = "revoked";
|
|
42
|
+
else if (description.includes("expired")) reason = "expired";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { reason, ...(code ? { code } : {}) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Endpoint verdict only: no credential identity, body, or arbitrary upstream string. */
|
|
49
|
+
export function noteChatgptRefreshFailure(
|
|
50
|
+
kind: "pool" | "native main",
|
|
51
|
+
status: number,
|
|
52
|
+
failure: ChatgptRefreshFailure,
|
|
53
|
+
): void {
|
|
54
|
+
console.warn(`[codex] ${kind} refresh: ${failure.reason === "unknown" ? "transient" : "reauth"}`
|
|
55
|
+
+ ` status=${status} code=${failure.code ?? "none"}`);
|
|
56
|
+
}
|
package/src/codex/convergence.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectAntigravitySelectedModels } from "../providers/antigravity-effort-families";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
|
|
3
4
|
import { getConfigDir, saveConfigPreservingClaudeCode, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
@@ -347,7 +348,7 @@ function prepareCatalog(
|
|
|
347
348
|
const selectedModelsByProvider = new Map<string, ReadonlySet<string>>(
|
|
348
349
|
enabledProviders.flatMap(([name, provider]) => (
|
|
349
350
|
Array.isArray(provider.selectedModels) && provider.selectedModels.length > 0
|
|
350
|
-
? [[name, new Set(provider.selectedModels)] as const]
|
|
351
|
+
? [[name, new Set(projectAntigravitySelectedModels(name, provider.selectedModels, routedModels))] as const]
|
|
351
352
|
: []
|
|
352
353
|
)),
|
|
353
354
|
);
|