@bitkyc08/opencodex 2.83.0 → 2.84.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-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +19 -0
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export.ts +46 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +311 -171
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +24 -15
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +14 -14
- package/src/providers/registry/entries-extended.ts +0 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +13 -0
- package/src/server/lifecycle.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +5 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/responses/adapter-dispatch.ts +232 -79
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -4
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +6 -8
- package/src/server/responses/sidecar-execution.ts +11 -2
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/usage/expected-prices.ts +4 -0
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
|
@@ -44,6 +44,8 @@ interface OperationRecord {
|
|
|
44
44
|
status: "open" | "settled";
|
|
45
45
|
code?: string;
|
|
46
46
|
resetsLeft?: number | null;
|
|
47
|
+
/** Windows the settled claim reported cleared; absent on records written before this field. */
|
|
48
|
+
cleared?: string[];
|
|
47
49
|
attempts: number;
|
|
48
50
|
leaseUntil: number;
|
|
49
51
|
createdAt: number;
|
|
@@ -64,7 +66,7 @@ export interface AnthropicResetOperationIdentity {
|
|
|
64
66
|
|
|
65
67
|
export type AnthropicResetBegin =
|
|
66
68
|
| { kind: "execute"; attempt: number }
|
|
67
|
-
| { kind: "replay"; code: string; resetsLeft: number | null; settledAt: number }
|
|
69
|
+
| { kind: "replay"; code: string; resetsLeft: number | null; settledAt: number; claimedAt: number; cleared: string[] }
|
|
68
70
|
| { kind: "identity-mismatch" }
|
|
69
71
|
| { kind: "in-flight" }
|
|
70
72
|
| { kind: "expired" }
|
|
@@ -102,7 +104,9 @@ function isRecordShape(value: unknown): value is OperationRecord {
|
|
|
102
104
|
return typeof record.accountId === "string" && typeof record.grantId === "string"
|
|
103
105
|
&& typeof record.orgDigest === "string" && (record.status === "open" || record.status === "settled")
|
|
104
106
|
&& typeof record.attempts === "number" && typeof record.leaseUntil === "number"
|
|
105
|
-
&& typeof record.createdAt === "number" && typeof record.updatedAt === "number"
|
|
107
|
+
&& typeof record.createdAt === "number" && typeof record.updatedAt === "number"
|
|
108
|
+
&& (record.cleared === undefined
|
|
109
|
+
|| Array.isArray(record.cleared) && record.cleared.every(window => typeof window === "string"));
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
/** Missing file → empty journal. Present but unreadable → throws; never an empty fallback. */
|
|
@@ -197,7 +201,10 @@ export function beginAnthropicResetOperation(
|
|
|
197
201
|
return { kind: "identity-mismatch" };
|
|
198
202
|
}
|
|
199
203
|
if (existing.status === "settled") {
|
|
200
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
kind: "replay", code: existing.code ?? "unavailable", resetsLeft: existing.resetsLeft ?? null,
|
|
206
|
+
settledAt: existing.updatedAt, claimedAt: existing.createdAt, cleared: existing.cleared ?? [],
|
|
207
|
+
};
|
|
201
208
|
}
|
|
202
209
|
if (existing.leaseUntil > now) return { kind: "in-flight" };
|
|
203
210
|
if (now - existing.createdAt >= ANTHROPIC_RESET_RETRY_WINDOW_MS) return { kind: "expired" };
|
|
@@ -232,6 +239,12 @@ export function beginAnthropicResetOperation(
|
|
|
232
239
|
export interface AnthropicResetSettlement {
|
|
233
240
|
code: string;
|
|
234
241
|
resetsLeft: number | null;
|
|
242
|
+
/** When the canonical (first) settlement was journaled, not when this caller arrived. */
|
|
243
|
+
settledAt: number;
|
|
244
|
+
/** When the operation was first journaled, before its first claim was sent. */
|
|
245
|
+
claimedAt: number;
|
|
246
|
+
/** Windows the canonical settlement reported cleared. */
|
|
247
|
+
cleared: string[];
|
|
235
248
|
}
|
|
236
249
|
|
|
237
250
|
/**
|
|
@@ -241,7 +254,7 @@ export interface AnthropicResetSettlement {
|
|
|
241
254
|
* throws, because an answer with no journal entry cannot be reported as durable.
|
|
242
255
|
*/
|
|
243
256
|
export function settleAnthropicResetOperation(
|
|
244
|
-
settlement: { operationId: string; code: string; resetsLeft: number | null },
|
|
257
|
+
settlement: { operationId: string; code: string; resetsLeft: number | null; cleared?: readonly string[] },
|
|
245
258
|
options: AnthropicResetLedgerOptions = {},
|
|
246
259
|
): AnthropicResetSettlement {
|
|
247
260
|
const now = options.now ?? Date.now();
|
|
@@ -250,14 +263,23 @@ export function settleAnthropicResetOperation(
|
|
|
250
263
|
const journal = readJournal(filePath);
|
|
251
264
|
const existing = journal.operations[settlement.operationId];
|
|
252
265
|
if (!existing) throw new AnthropicResetLedgerError("unavailable");
|
|
253
|
-
if (existing.status === "settled")
|
|
266
|
+
if (existing.status === "settled") {
|
|
267
|
+
return {
|
|
268
|
+
code: existing.code ?? "unavailable", resetsLeft: existing.resetsLeft ?? null,
|
|
269
|
+
settledAt: existing.updatedAt, claimedAt: existing.createdAt, cleared: existing.cleared ?? [],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
254
272
|
existing.status = "settled";
|
|
255
273
|
existing.code = settlement.code;
|
|
256
274
|
existing.resetsLeft = settlement.resetsLeft;
|
|
275
|
+
existing.cleared = [...(settlement.cleared ?? [])];
|
|
257
276
|
existing.leaseUntil = 0;
|
|
258
277
|
existing.updatedAt = now;
|
|
259
278
|
writeJournal(filePath, journal, now);
|
|
260
|
-
return {
|
|
279
|
+
return {
|
|
280
|
+
code: settlement.code, resetsLeft: settlement.resetsLeft, settledAt: now,
|
|
281
|
+
claimedAt: existing.createdAt, cleared: [...existing.cleared],
|
|
282
|
+
};
|
|
261
283
|
});
|
|
262
284
|
}
|
|
263
285
|
|
|
@@ -39,17 +39,25 @@ function defaultEntryFactory(service: string, account: string): ProviderKeychain
|
|
|
39
39
|
let entryFactory: ProviderKeychainEntryFactory = defaultEntryFactory;
|
|
40
40
|
const resolvedCache = new Map<string, string>();
|
|
41
41
|
const warnedAccounts = new Set<string>();
|
|
42
|
+
let resolutionGeneration = 0;
|
|
43
|
+
|
|
44
|
+
/** Advances whenever cached secrets may have changed, so derived snapshots can rebuild. */
|
|
45
|
+
export function providerKeyResolutionGeneration(): number {
|
|
46
|
+
return resolutionGeneration;
|
|
47
|
+
}
|
|
42
48
|
|
|
43
49
|
/** Test seam: swap the OS entry for an in-memory one and drop caches. */
|
|
44
50
|
export function setProviderKeychainEntryFactoryForTests(factory: ProviderKeychainEntryFactory | null): void {
|
|
45
51
|
entryFactory = factory ?? defaultEntryFactory;
|
|
46
52
|
resolvedCache.clear();
|
|
53
|
+
resolutionGeneration++;
|
|
47
54
|
warnedAccounts.clear();
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
/** Write-path seam: a store/restore mutated secrets, so cached reads and warnings are stale. */
|
|
51
58
|
export function invalidateResolvedProviderKeyCache(): void {
|
|
52
59
|
resolvedCache.clear();
|
|
60
|
+
resolutionGeneration++;
|
|
53
61
|
warnedAccounts.clear();
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -76,7 +84,7 @@ export function providerKeychainEntry(account: string): ProviderKeychainEntry {
|
|
|
76
84
|
return entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
function readKeychain(account: string): string | undefined {
|
|
87
|
+
function readKeychain(account: string, quiet = false): string | undefined {
|
|
80
88
|
const cached = resolvedCache.get(account);
|
|
81
89
|
if (cached !== undefined) return cached;
|
|
82
90
|
try {
|
|
@@ -88,7 +96,7 @@ function readKeychain(account: string): string | undefined {
|
|
|
88
96
|
} catch {
|
|
89
97
|
// fall through to the single warning below
|
|
90
98
|
}
|
|
91
|
-
if (!warnedAccounts.has(account)) {
|
|
99
|
+
if (!quiet && !warnedAccounts.has(account)) {
|
|
92
100
|
warnedAccounts.add(account);
|
|
93
101
|
console.warn(`[opencodex] provider key reference keychain:${account} could not be read from the OS keychain; requests for this provider have no credential until the keychain is available (no plaintext fallback)`);
|
|
94
102
|
}
|
|
@@ -105,6 +113,13 @@ export function resolveProviderApiKey(value: string | undefined): string | undef
|
|
|
105
113
|
return resolveEnvValue(value);
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
/** The same resolution without the unreadable-reference warning, for observe-only callers. */
|
|
117
|
+
export function resolveProviderApiKeyQuietly(value: string | undefined): string | undefined {
|
|
118
|
+
if (!value) return undefined;
|
|
119
|
+
if (isKeychainReference(value)) return readKeychain(keychainAccount(value), true);
|
|
120
|
+
return resolveEnvValue(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
export type ProviderKeyStoreKind = "keychain" | "env" | "file" | "none";
|
|
109
124
|
|
|
110
125
|
export function providerKeyStoreKind(provider: Pick<OcxProviderConfig, "apiKey"> | undefined): ProviderKeyStoreKind {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
|
|
10
10
|
*/
|
|
11
|
-
import { commitProviderApiKeySelection } from "./api-key-selection";
|
|
11
|
+
import { commitProviderApiKeySelection, providerApiKeySelectionIsCurrent } from "./api-key-selection";
|
|
12
12
|
import type { ProviderApiKeySelection } from "../types/provider";
|
|
13
13
|
import { routedProviderConfig } from "../router";
|
|
14
14
|
import { getProviderRegistryEntry } from "./registry";
|
|
@@ -30,6 +30,16 @@ interface KeyCooldown {
|
|
|
30
30
|
|
|
31
31
|
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
32
32
|
const MAX_COOLDOWN_MS = 10 * 60_000; // cap at 10 min for api-key rotation
|
|
33
|
+
/**
|
|
34
|
+
* Anthropic credit-balance refusal (#6958). An empty prepaid balance is account state, not a
|
|
35
|
+
* rate signal: it clears only when someone buys credits or auto-reload fires, so the
|
|
36
|
+
* ten-minute credential cap would hand a dead key back to rotation six times an hour. One hour
|
|
37
|
+
* keeps it out of replacement and proactive selection while a funded sibling exists, and still
|
|
38
|
+
* notices a top-up without a restart. When every key is held, requests keep using the active
|
|
39
|
+
* key and return Anthropic's original refusal. The hold is in memory; dashboard key management
|
|
40
|
+
* clears it.
|
|
41
|
+
*/
|
|
42
|
+
export const ANTHROPIC_BILLING_COOLDOWN_MS = 60 * 60_000;
|
|
33
43
|
|
|
34
44
|
/**
|
|
35
45
|
* Cap for a cooldown the upstream itself dated, as opposed to one we inferred.
|
|
@@ -688,12 +698,13 @@ export function rateLimitRetryDelayMs(
|
|
|
688
698
|
function rotateKeyAfterFailure(
|
|
689
699
|
config: OcxConfig,
|
|
690
700
|
providerName: string,
|
|
691
|
-
failureStatus: 401 | 429,
|
|
701
|
+
failureStatus: 401 | 429 | "billing",
|
|
692
702
|
retryAfterHeader: string | null | undefined,
|
|
693
703
|
now = Date.now(),
|
|
694
704
|
attemptedKey?: string,
|
|
695
705
|
attemptedSelection?: ProviderApiKeySelection,
|
|
696
706
|
quotaResetAt?: number,
|
|
707
|
+
acceptsFreshProvider?: (provider: OcxProviderConfig) => boolean,
|
|
697
708
|
): OcxProviderConfig | null {
|
|
698
709
|
const provider = config.providers[providerName];
|
|
699
710
|
if (!provider) return null;
|
|
@@ -704,6 +715,11 @@ function rotateKeyAfterFailure(
|
|
|
704
715
|
| { failedId?: string; candidateId?: string }
|
|
705
716
|
| { exhaustedCount: number; failedId?: string };
|
|
706
717
|
const outcome = commitProviderApiKeySelection<Rotation | null>(config, providerName, freshProvider => {
|
|
718
|
+
// Run on every persisted transaction attempt, including a freshness rebase.
|
|
719
|
+
if (acceptsFreshProvider) {
|
|
720
|
+
try { if (!acceptsFreshProvider(freshProvider)) return { changed: false, value: null }; }
|
|
721
|
+
catch { return { changed: false, value: null }; }
|
|
722
|
+
}
|
|
707
723
|
const pool = freshProvider.apiKeyPool;
|
|
708
724
|
if (!pool || pool.length < 2) return { changed: false, value: null };
|
|
709
725
|
|
|
@@ -741,6 +757,8 @@ function rotateKeyAfterFailure(
|
|
|
741
757
|
}, attemptedSelection);
|
|
742
758
|
if (outcome.status === "unavailable") return null;
|
|
743
759
|
if (outcome.status === "superseded") {
|
|
760
|
+
// Billing authority is bound to the sent selection; a newer manual choice grants no replay.
|
|
761
|
+
if (acceptsFreshProvider) return null;
|
|
744
762
|
// A newer manual selection (including A→B→A) owns subsequent dispatch. Reusing the
|
|
745
763
|
// same failed key here would loop forever; preserve its original failure instead.
|
|
746
764
|
return outcome.provider.apiKey !== failedKey ? structuredClone(outcome.provider) : null;
|
|
@@ -750,7 +768,9 @@ function rotateKeyAfterFailure(
|
|
|
750
768
|
// A 401 is a verdict about the credential itself, not a timing signal: the key is rejected
|
|
751
769
|
// until an operator replaces it, and upstreams send no Retry-After for it. Hold it for the
|
|
752
770
|
// full cap instead of the 429 default so a dead key is not re-tried once a minute.
|
|
753
|
-
const cooldownMs = failureStatus ===
|
|
771
|
+
const cooldownMs = failureStatus === "billing"
|
|
772
|
+
? ANTHROPIC_BILLING_COOLDOWN_MS
|
|
773
|
+
: failureStatus === 401
|
|
754
774
|
? MAX_COOLDOWN_MS
|
|
755
775
|
// A reset instant the upstream dated outranks both the header and the
|
|
756
776
|
// default: it is the only one of the three that knows when the quota
|
|
@@ -762,7 +782,7 @@ function rotateKeyAfterFailure(
|
|
|
762
782
|
sweepExpiredOnWrite(now);
|
|
763
783
|
}
|
|
764
784
|
if ("exhaustedCount" in outcome.value) {
|
|
765
|
-
console.warn(`[key-failover] ${providerName}: all ${outcome.value.exhaustedCount} keys in cooldown after ${failureStatus}; returning the upstream status to the client`);
|
|
785
|
+
console.warn(`[key-failover] ${providerName}: all ${outcome.value.exhaustedCount} keys in cooldown after ${failureLabel(failureStatus)}; returning the upstream status to the client`);
|
|
766
786
|
return null;
|
|
767
787
|
}
|
|
768
788
|
|
|
@@ -771,12 +791,16 @@ function rotateKeyAfterFailure(
|
|
|
771
791
|
if (outcome.value.candidateId) {
|
|
772
792
|
console.warn(
|
|
773
793
|
// Log ids only — labels are user-supplied free text and could carry secret material.
|
|
774
|
-
`[key-failover] ${providerName}: ${failureStatus} on key ${outcome.value.failedId ?? "?"}; rotating to key ${outcome.value.candidateId}`,
|
|
794
|
+
`[key-failover] ${providerName}: ${failureLabel(failureStatus)} on key ${outcome.value.failedId ?? "?"}; rotating to key ${outcome.value.candidateId}`,
|
|
775
795
|
);
|
|
776
796
|
}
|
|
777
797
|
return structuredClone(committed);
|
|
778
798
|
}
|
|
779
799
|
|
|
800
|
+
function failureLabel(failureStatus: 401 | 429 | "billing"): string {
|
|
801
|
+
return failureStatus === "billing" ? "credit-balance refusal" : String(failureStatus);
|
|
802
|
+
}
|
|
803
|
+
|
|
780
804
|
export function rotateKeyOn429(
|
|
781
805
|
config: OcxConfig,
|
|
782
806
|
providerName: string,
|
|
@@ -865,6 +889,29 @@ export function rotateProviderTransportOn401(
|
|
|
865
889
|
return applyRotatedTransport(providerName, routedProvider, rotated, options.promptCacheKey);
|
|
866
890
|
}
|
|
867
891
|
|
|
892
|
+
/** Exact Anthropic billing refusal: cool the key for ANTHROPIC_BILLING_COOLDOWN_MS. */
|
|
893
|
+
export function rotateProviderTransportOnAnthropicBilling(
|
|
894
|
+
config: OcxConfig,
|
|
895
|
+
providerName: string,
|
|
896
|
+
routedProvider: OcxProviderTransport,
|
|
897
|
+
options: Omit<RotateProviderTransportOptions, "retryAfter"> = {},
|
|
898
|
+
): OcxProviderTransport | null {
|
|
899
|
+
if (routedProvider.adapter !== "anthropic"
|
|
900
|
+
|| (routedProvider.authMode !== undefined && routedProvider.authMode !== "key")
|
|
901
|
+
|| !hasKeyPoolFailover(routedProvider)) return null;
|
|
902
|
+
const headerShape = (provider: OcxProviderConfig) => JSON.stringify(Object.entries(provider.headers ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
903
|
+
const sentHeaders = headerShape(routedProvider);
|
|
904
|
+
const acceptsFreshProvider = (fresh: OcxProviderConfig): boolean => fresh.adapter === routedProvider.adapter
|
|
905
|
+
&& (fresh.authMode === undefined || fresh.authMode === "key") && hasKeyPoolFailover(fresh)
|
|
906
|
+
&& (fresh.apiKeyTransport ?? "x-api-key") === (routedProvider.apiKeyTransport ?? "x-api-key")
|
|
907
|
+
&& headerShape(fresh) === sentHeaders
|
|
908
|
+
&& providerApiKeySelectionIsCurrent({ ...config, providers: { ...config.providers, [providerName]: fresh } },
|
|
909
|
+
providerName, routedProvider);
|
|
910
|
+
const rotated = rotateKeyAfterFailure(config, providerName, "billing", null, options.now,
|
|
911
|
+
options.attemptedKey, options.attemptedSelection ?? routedProvider._apiKeyAttempt, undefined, acceptsFreshProvider);
|
|
912
|
+
return rotated ? applyRotatedTransport(providerName, routedProvider, rotated, options.promptCacheKey) : null;
|
|
913
|
+
}
|
|
914
|
+
|
|
868
915
|
function applyRotatedTransport(
|
|
869
916
|
providerName: string,
|
|
870
917
|
routedProvider: OcxProviderTransport,
|
|
@@ -3,6 +3,7 @@ import { projectModelRenames } from "./model-rename-migration";
|
|
|
3
3
|
import { projectStaleContextWindows } from "./stale-context-window-migration";
|
|
4
4
|
import { projectStaleModelRosters } from "./stale-model-roster-migration";
|
|
5
5
|
import { projectStaleVisionClassifications } from "./stale-vision-classification-migration";
|
|
6
|
+
import { projectStaleNoVisionSeeds } from "./stale-no-vision-seed-migration";
|
|
6
7
|
import { projectDevinCliAuthMode } from "./devin-cli-authmode-migration";
|
|
7
8
|
import type { OcxConfig } from "../types";
|
|
8
9
|
|
|
@@ -17,12 +18,13 @@ export function projectStartupConfigRepairs(config: OcxConfig): ReturnType<typeo
|
|
|
17
18
|
const renames = projectModelRenames(config);
|
|
18
19
|
const windows = projectStaleContextWindows(renames.config);
|
|
19
20
|
const vision = projectStaleVisionClassifications(windows.config);
|
|
20
|
-
const
|
|
21
|
+
const noVision = projectStaleNoVisionSeeds(vision.config);
|
|
22
|
+
const rosters = projectStaleModelRosters(noVision.config);
|
|
21
23
|
const devinCli = projectDevinCliAuthMode(rosters.config);
|
|
22
24
|
return {
|
|
23
25
|
config: devinCli.config,
|
|
24
|
-
changed: renames.changed || windows.changed || vision.changed || rosters.changed || devinCli.changed,
|
|
25
|
-
warnings: [...renames.warnings, ...windows.warnings, ...vision.warnings, ...rosters.warnings, ...devinCli.warnings],
|
|
26
|
+
changed: renames.changed || windows.changed || vision.changed || noVision.changed || rosters.changed || devinCli.changed,
|
|
27
|
+
warnings: [...renames.warnings, ...windows.warnings, ...vision.warnings, ...noVision.warnings, ...rosters.warnings, ...devinCli.warnings],
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -473,6 +473,17 @@ export function clearAccountQuotaCache(provider?: string): void {
|
|
|
473
473
|
persistAccountQuotaCache();
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
/** Drop one account's cached bars and any joined probe so a reset is not ranked from the old window. */
|
|
477
|
+
export function forgetCachedProviderAccountQuota(provider: string, accountId: string): void {
|
|
478
|
+
hydrateAccountQuotaCache();
|
|
479
|
+
const key = accountCacheKey(provider, accountId);
|
|
480
|
+
accountQuotaCache.delete(key);
|
|
481
|
+
for (const flight of [...accountQuotaInflight.keys()]) {
|
|
482
|
+
if (flight === key || flight.startsWith(key + "\u0000")) accountQuotaInflight.delete(flight);
|
|
483
|
+
}
|
|
484
|
+
persistAccountQuotaCache();
|
|
485
|
+
}
|
|
486
|
+
|
|
476
487
|
/**
|
|
477
488
|
* Resolve a bearer for quota probing without silently adopting a newer global
|
|
478
489
|
* Claude CLI credential into a background multiauth slot.
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
*/
|
|
21
21
|
export async function fetchAnthropicAccountQuota(
|
|
22
22
|
instance: AnthropicInstanceId, accountId: string, force: boolean, config: OcxConfig = loadConfig(),
|
|
23
|
+
/** Never join an older account or vendor flight; a reset reread needs post-grant evidence. */
|
|
24
|
+
freshDispatch = false,
|
|
23
25
|
): Promise<AccountQuotaCacheEntry> {
|
|
24
26
|
const unavailable = (): AccountQuotaCacheEntry => ({ ts: Date.now(), quota: null, unavailable: true });
|
|
25
27
|
const target = config.providers[instance];
|
|
@@ -52,7 +54,7 @@ export async function fetchAnthropicAccountQuota(
|
|
|
52
54
|
const flightKey = recovery.anthropicCredentialQuotaFlightKey(key, accountId);
|
|
53
55
|
const flightCurrent = () => ownerCurrent()
|
|
54
56
|
&& recovery.anthropicCredentialQuotaFlightKey(key, accountId) === flightKey;
|
|
55
|
-
const joinable = accountQuotaInflight.get(flightKey);
|
|
57
|
+
const joinable = freshDispatch ? undefined : accountQuotaInflight.get(flightKey);
|
|
56
58
|
if (joinable) {
|
|
57
59
|
const joined = await joinable;
|
|
58
60
|
return ownerCurrent() && (joined.isCurrent?.() ?? flightCurrent()) ? joined : unavailable();
|
|
@@ -78,7 +80,7 @@ export async function fetchAnthropicAccountQuota(
|
|
|
78
80
|
const result = await recovery.probeAnthropicQuotaWithRecovery(accountId, token, fresh => {
|
|
79
81
|
if (!flightCurrent()) throw new AnthropicQuotaProbeOwnershipError("quota flight changed");
|
|
80
82
|
recovery.assertAnthropicQuotaSendAllowed(accountId, token);
|
|
81
|
-
return fetchAnthropicUsageQuotaForInstance(instance, token, fresh);
|
|
83
|
+
return fetchAnthropicUsageQuotaForInstance(instance, token, fresh || freshDispatch);
|
|
82
84
|
}, () => ownerCurrent() && mayCommitAccountQuotaKey(key, writerGeneration));
|
|
83
85
|
if (result && !result.isCurrent()) return unavailable();
|
|
84
86
|
if (!result && !flightCurrent()) return unavailable();
|
|
@@ -344,3 +344,17 @@ export function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolea
|
|
|
344
344
|
return (generation === undefined || isMainAccountIdentityGenerationLive(generation))
|
|
345
345
|
&& (accountReportCurrent.get(value)?.() ?? true);
|
|
346
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The reports this process already holds for the configured providers, for a passive read (#6959):
|
|
350
|
+
* no probe, token refresh, account snapshot or publication into routing evidence. The Home's own
|
|
351
|
+
* dashboard and traffic keep the cache current.
|
|
352
|
+
*/
|
|
353
|
+
export function heldProviderQuotaReports(configuredProviders: readonly string[]): ProviderQuotaResponse {
|
|
354
|
+
const held = getProviderQuotaReportCache()?.response;
|
|
355
|
+
const configured = new Set(configuredProviders);
|
|
356
|
+
return {
|
|
357
|
+
generatedAt: held?.generatedAt ?? Date.now(),
|
|
358
|
+
reports: (held?.reports ?? []).filter(item => configured.has(item.provider) && isProviderQuotaReportCurrent(item)),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
@@ -327,9 +327,9 @@ async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuo
|
|
|
327
327
|
* This endpoint reports quota only. Its body carries `five_hour`, `seven_day`, the
|
|
328
328
|
* model-scoped weekly buckets (`seven_day_fable`/`_opus`/`_sonnet`) and a `limits` array,
|
|
329
329
|
* and **no subscription or tier field** — nor does the OAuth token response, which yields only
|
|
330
|
-
* `account.uuid` and `account.email_address` (`src/oauth/anthropic.ts`).
|
|
331
|
-
*
|
|
332
|
-
*
|
|
330
|
+
* `account.uuid` and `account.email_address` (`src/oauth/anthropic.ts`). Subscription labels
|
|
331
|
+
* instead come from the authenticated profile in `src/oauth/anthropic-identity.ts`, never
|
|
332
|
+
* from this quota response.
|
|
333
333
|
*
|
|
334
334
|
* A tier must not be inferred from what is here. Percentages are normalized per account, so a
|
|
335
335
|
* Max x5 seat at 50% is byte-identical to a Max x20 seat at 50%, and the presence of a
|
package/src/providers/quota.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./kiro-usage";
|
|
16
16
|
import { mapQuotaRoster, readProviderApiKeyQuotas, type ProviderApiKeyQuota } from "./quota-key-accounts";
|
|
17
17
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
18
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
18
19
|
import type { ProviderQuota, QuotaFailureCode } from "./quota-types";
|
|
19
20
|
import {
|
|
20
21
|
accountReportCurrent,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
captureProviderReportEpochs, providerReportEpochChanged,
|
|
24
25
|
cacheKeyWithAggregationState,
|
|
25
26
|
getProviderQuotaReportCache,
|
|
26
|
-
hasCodexPoolProvider,
|
|
27
|
+
hasCodexPoolProvider, heldProviderQuotaReports,
|
|
27
28
|
inflight,
|
|
28
29
|
invalidationEpoch,
|
|
29
30
|
isBuiltInChatGptForwardProvider,
|
|
@@ -229,6 +230,8 @@ export function flushProviderQuotaObservationsForTests(): Promise<void> {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
|
|
233
|
+
// A passive read (a Remote Link Child's, #6959) shows the held reports and starts nothing.
|
|
234
|
+
if (isPassiveRead()) return heldProviderQuotaReports(Object.keys(config.providers));
|
|
232
235
|
// A Pool report's cache signature and provider fetch must share one account snapshot.
|
|
233
236
|
// Preserve force semantics when deciding whether that snapshot refreshes upstream data.
|
|
234
237
|
const prefetchedCodexSnapshot = hasCodexPoolProvider(config)
|
|
@@ -32,12 +32,12 @@ import {
|
|
|
32
32
|
OPENAI_API_GPT56_MAX_INPUT_TOKENS,
|
|
33
33
|
OPENAI_API_GPT56_VIRTUAL_MODELS,
|
|
34
34
|
OPENAI_API_GPT56_REASONING_EFFORTS,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
META_MODEL_API_REASONING_EFFORTS,
|
|
36
|
+
META_MODEL_API_REASONING_EFFORT_MAPS,
|
|
37
37
|
META_MUSE_CODE_REASONING_EFFORTS,
|
|
38
38
|
META_MUSE_CODE_REASONING_EFFORT_MAP,
|
|
39
39
|
META_MUSE_CONTEXT_WINDOW,
|
|
40
|
-
|
|
40
|
+
META_MODEL_API_MODELS,
|
|
41
41
|
OPENAI_DAYBREAK_MODELS,
|
|
42
42
|
OPENAI_DAYBREAK_CONTEXT_WINDOWS,
|
|
43
43
|
OPENAI_DAYBREAK_MAX_INPUT_TOKENS,
|
|
@@ -736,7 +736,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
736
736
|
authKind: "key",
|
|
737
737
|
dashboardUrl: "https://dev.meta.ai/docs/authentication",
|
|
738
738
|
defaultModel: "muse-spark-1.3",
|
|
739
|
-
models:
|
|
739
|
+
models: META_MODEL_API_MODELS,
|
|
740
740
|
// Static roster: no authenticated /v1/models payload was ever observed (the only
|
|
741
741
|
// contact was an unauthenticated GET returning 401 invalid_api_key), and Meta serves
|
|
742
742
|
// non-agent families on this same base URL. Turning discovery on would publish an
|
|
@@ -746,20 +746,20 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
746
746
|
// without this, registry transport canonicalization would retarget it and send their
|
|
747
747
|
// saved key to Meta.
|
|
748
748
|
preserveCustomDestination: true,
|
|
749
|
-
modelContextWindows: Object.fromEntries(
|
|
749
|
+
modelContextWindows: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
|
|
750
750
|
// text+image only. Meta also documents video, audio (degraded on 1.3), and PDF, but
|
|
751
751
|
// the catalog modality enum is text/image and over-advertising poisons the exported
|
|
752
752
|
// client config (see tests/codex-integration/catalog-input-modality-enum.test.ts).
|
|
753
|
-
modelInputModalities: Object.fromEntries(
|
|
754
|
-
modelReasoningEfforts:
|
|
755
|
-
modelReasoningEffortMap:
|
|
753
|
+
modelInputModalities: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
|
|
754
|
+
modelReasoningEfforts: META_MODEL_API_REASONING_EFFORTS,
|
|
755
|
+
modelReasoningEffortMap: META_MODEL_API_REASONING_EFFORT_MAPS,
|
|
756
756
|
// No defaultMaxOutputTokens: Meta publishes none. The only number in its docs
|
|
757
757
|
// (131072) appears inside a third-party config sample, and the protocol pages call
|
|
758
758
|
// the real limit "model-dependent".
|
|
759
759
|
// Meta names its variable MODEL_API_KEY, but the env var opencodex reads is derived
|
|
760
760
|
// from the provider id (META_MODEL_API_KEY). Saying only Meta's name would send a
|
|
761
761
|
// user to export a variable this proxy never reads.
|
|
762
|
-
note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai (Meta calls it MODEL_API_KEY; export it here as META_MODEL_API_KEY) — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is cheap because Meta trains on your prompts — about 92% off input, 95% off output, 99% off cached input; do not send confidential material through it. Muse Spark is also reachable through resellers: command-code carries both tiers, opencode-go serves only muse-spark-1.3-contributor.",
|
|
762
|
+
note: "Pay-as-you-go Meta Model API. Get a key at https://dev.meta.ai (Meta calls it MODEL_API_KEY; export it here as META_MODEL_API_KEY) — a Meta developer account needs a payment method before it can serve requests, and every call is metered per token. A Muse Code subscription does NOT work here: Meta scopes that credential to the Muse Code CLI and bills any other key pay-as-you-go (dev.meta.ai/docs/muse-code/subscriptions). The Contributor tier (muse-spark-1.3-contributor) is cheap because Meta trains on your prompts — about 92% off input, 95% off output, 99% off cached input; do not send confidential material through it. Muse Wonder (muse-wonder-1.0, preview) is Standard mode only on this API ($4 input / $20 output per 1M tokens, no Contributor tier) and accepts reasoning effort up to max; the Spark rows stop at xhigh. Muse Spark is also reachable through resellers: command-code carries both tiers, opencode-go serves only muse-spark-1.3-contributor.",
|
|
763
763
|
},
|
|
764
764
|
/* [Decision Log]
|
|
765
765
|
- 목적과 의도: Let an operator who already signed the Muse Code CLI in reach Muse Spark with that credential, instead of provisioning a second key.
|
|
@@ -787,14 +787,14 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
787
787
|
oauthId: "meta-muse",
|
|
788
788
|
dashboardUrl: "https://dev.meta.ai",
|
|
789
789
|
defaultModel: "muse-spark-1.3",
|
|
790
|
-
models:
|
|
790
|
+
models: META_MODEL_API_MODELS,
|
|
791
791
|
// Same reason as meta-model: the authenticated roster carries muse-image-1.0 and
|
|
792
792
|
// muse-voice-transcribe-1.0, which this Responses-agent provider cannot drive.
|
|
793
793
|
liveModels: false,
|
|
794
|
-
modelContextWindows: Object.fromEntries(
|
|
795
|
-
modelInputModalities: Object.fromEntries(
|
|
796
|
-
modelReasoningEfforts: Object.fromEntries(
|
|
797
|
-
modelReasoningEffortMap: Object.fromEntries(
|
|
794
|
+
modelContextWindows: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, META_MUSE_CONTEXT_WINDOW])),
|
|
795
|
+
modelInputModalities: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, ["text", "image"] as ["text", "image"]])),
|
|
796
|
+
modelReasoningEfforts: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, META_MUSE_CODE_REASONING_EFFORTS])),
|
|
797
|
+
modelReasoningEffortMap: Object.fromEntries(META_MODEL_API_MODELS.map(id => [id, META_MUSE_CODE_REASONING_EFFORT_MAP])),
|
|
798
798
|
note: "Signs in to Meta with a browser device code on any platform, then mints the Muse Code subscription key. That grant is reimplemented from the one the Muse Code CLI performs and has NOT been exercised against Meta from OpenCodex, so treat the first login as unverified. If the Muse Code CLI is already signed in on macOS, the existing key is imported instead of starting a new grant. A pasted key from https://dev.meta.ai still works as a fallback when a device login cannot complete, and faces the same format check and live validation. A device login authenticates as Meta own Muse Code client, which is a stronger claim than reusing a key the CLI already minted. Meta scopes that credential to the Muse Code CLI, so this is an UNSUPPORTED use: Meta does not authorize subscription coverage outside its own CLI, how these calls settle is not observable from the API, and you should treat every call as billable against your account. The key, imported or pasted, is copied into OpenCodex's auth store. For an account signed in with the device login, OpenCodex refreshes Meta's subscription windows on demand from the same key endpoint the login uses, at most once every five minutes. For an imported or pasted key there is no endpoint to query them on demand, so OpenCodex reads them from streaming responses and shows the last observed value with its age; refreshing one then requires another streaming turn, and translated (non-passthrough) turns report none. Rate limits apply per team, not per key. For a supported path use the meta-model provider with your own key (export it as META_MODEL_API_KEY).",
|
|
799
799
|
},
|
|
800
800
|
{
|
|
@@ -1468,7 +1468,6 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
|
|
|
1468
1468
|
models: [...QODER_GLOBAL_MODELS],
|
|
1469
1469
|
liveModels: true,
|
|
1470
1470
|
reasoningEfforts: [...QODER_REASONING_EFFORTS],
|
|
1471
|
-
noVisionModels: [...QODER_GLOBAL_MODELS],
|
|
1472
1471
|
note: "Official Qoder Global CLI using QODER_PERSONAL_ACCESS_TOKEN. Models are discovered per account with `qoder --list-models`; the documented roster is a degraded fallback. The CLI runs single-turn with tools, MCP, settings hooks, and session persistence disabled. Requires `npm install -g @qoder-ai/qodercli`.",
|
|
1473
1472
|
},
|
|
1474
1473
|
{
|
|
@@ -255,6 +255,23 @@ export const META_MUSE_CODE_REASONING_EFFORT_MAP: Record<string, string> = Objec
|
|
|
255
255
|
/** Both Muse Spark 1.3 tiers publish a 1,048,576-token window (dev.meta.ai/docs/models). */
|
|
256
256
|
export const META_MUSE_CONTEXT_WINDOW = 1_048_576;
|
|
257
257
|
export const META_MUSE_MODELS = ["muse-spark-1.3", "muse-spark-1.3-contributor"];
|
|
258
|
+
/*
|
|
259
|
+
* Muse Wonder (preview), announced 2026-10-12 (research.meta.ai/blog/muse-wonder). Per
|
|
260
|
+
* dev.meta.ai/docs/models and dev.meta.ai/docs/pricing-rate-limits, verified 2026-10-13:
|
|
261
|
+
* id muse-wonder-1.0, a 1,048,576-token window, reasoning effort "minimal" through "max",
|
|
262
|
+
* and Standard mode only on the Model API (no Contributor variant). Muse Wonder Full is
|
|
263
|
+
* reachable only inside Muse Code subscriptions and has no Model API id, so it is absent.
|
|
264
|
+
*/
|
|
265
|
+
export const META_MUSE_WONDER_MODEL = "muse-wonder-1.0";
|
|
266
|
+
/** Roster both direct Meta providers serve: the Spark 1.3 tiers plus Muse Wonder. */
|
|
267
|
+
export const META_MODEL_API_MODELS = [...META_MUSE_MODELS, META_MUSE_WONDER_MODEL];
|
|
268
|
+
/** Model API ladders: Spark stops at xhigh; Wonder documents minimal..max. */
|
|
269
|
+
export const META_MODEL_API_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
|
|
270
|
+
META_MODEL_API_MODELS.map(id => [id, id === META_MUSE_WONDER_MODEL ? META_MUSE_CODE_REASONING_EFFORTS : META_MUSE_REASONING_EFFORTS]),
|
|
271
|
+
);
|
|
272
|
+
export const META_MODEL_API_REASONING_EFFORT_MAPS: Record<string, Record<string, string>> = Object.fromEntries(
|
|
273
|
+
META_MODEL_API_MODELS.map(id => [id, id === META_MUSE_WONDER_MODEL ? META_MUSE_CODE_REASONING_EFFORT_MAP : META_MUSE_REASONING_EFFORT_MAP]),
|
|
274
|
+
);
|
|
258
275
|
/**
|
|
259
276
|
* Daybreak program aliases. These `-latest` ids are the stable contract: OpenAI repoints
|
|
260
277
|
* them at newer snapshots over time (red -> gpt-5.6-cyber, blue -> gpt-5.6-sol as of
|
|
@@ -33,6 +33,12 @@ export const REQUEST_PACING_CONCURRENCY_RETRY_AFTER_SECONDS = 5;
|
|
|
33
33
|
let maxQueueDepth = REQUEST_PACING_MAX_QUEUE_DEPTH;
|
|
34
34
|
let maxQueueAgeMs = REQUEST_PACING_MAX_QUEUE_AGE_MS;
|
|
35
35
|
|
|
36
|
+
// Optional owners register internal response markers; only this owned body rewrap carries them.
|
|
37
|
+
const responseProvenance = new Set<WeakMap<Response, unknown>>();
|
|
38
|
+
export function registerProviderResponseProvenance(markers: WeakMap<Response, unknown>): void {
|
|
39
|
+
responseProvenance.add(markers);
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
export type RequestPacingQueueOverloadReason = "queue_full" | "queue_expired";
|
|
37
43
|
|
|
38
44
|
export class RequestPacingQueueOverloadError extends Error {
|
|
@@ -588,6 +594,9 @@ export function trackProviderRequestSlotBody(
|
|
|
588
594
|
// Retry helpers mark the response they return from, and recovery decisions key on these
|
|
589
595
|
// identity markers; the rewrap must not make a non-replayable response look replayable.
|
|
590
596
|
if (isNonReplayableResponse(response)) markResponseNonReplayable(wrapped);
|
|
597
|
+
for (const markers of responseProvenance) {
|
|
598
|
+
if (markers.has(response)) markers.set(wrapped, markers.get(response));
|
|
599
|
+
}
|
|
591
600
|
return carryReplayRefusal(response, wrapped);
|
|
592
601
|
}
|
|
593
602
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop a saved `noVisionModels` list that is still, byte for byte, a retired registry seed.
|
|
3
|
+
*
|
|
4
|
+
* `enrichProviderFromRegistry` fills `noVisionModels` all-or-nothing, so a provider row saved
|
|
5
|
+
* while the registry seeded a blanket list keeps that list after the registry stops seeding it.
|
|
6
|
+
* Qoder Global seeded its whole static discovery roster as text-only until the Global adapter
|
|
7
|
+
* began forwarding image input to the Qoder CLI (#6906). The saved copy would keep every image
|
|
8
|
+
* on the vision sidecar path, stripped or captioned before the adapter could forward it.
|
|
9
|
+
*
|
|
10
|
+
* This projection removes one thing: a saved list identical, in order, to a seed this file names,
|
|
11
|
+
* on a row the registry still owns (same adapter, and the registry transport rule enrichment uses),
|
|
12
|
+
* and only while the registry no longer seeds the list at all. Any edited list does not match and
|
|
13
|
+
* is left alone. `modelCapabilities` and `modelInputModalities` are untouched; they are where a
|
|
14
|
+
* deliberate per-model text-only declaration belongs.
|
|
15
|
+
*/
|
|
16
|
+
import { PROVIDER_REGISTRY, providerMatchesRegistryTransport } from "./registry";
|
|
17
|
+
import type { OcxConfig } from "../types";
|
|
18
|
+
|
|
19
|
+
export interface StaleNoVisionSeed {
|
|
20
|
+
/** Registry provider id whose saved rows may carry the retired list. */
|
|
21
|
+
provider: string;
|
|
22
|
+
/** Every list the registry ever seeded, in seed order; only an exact match is removed. */
|
|
23
|
+
seeds: readonly (readonly string[])[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StaleNoVisionSeedProjection {
|
|
27
|
+
config: OcxConfig;
|
|
28
|
+
changed: boolean;
|
|
29
|
+
warnings: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const STALE_NO_VISION_SEEDS: readonly StaleNoVisionSeed[] = [
|
|
33
|
+
{
|
|
34
|
+
provider: "qoder",
|
|
35
|
+
seeds: [
|
|
36
|
+
// 2026-09-03 (Qoder Global PAT provider) until the DeepSeek V4.1 transition (#4282).
|
|
37
|
+
["Qwen3.8-Max", "Qwen3.7-Max", "Qwen3.7-Plus", "Kimi-K3", "Kimi-K2.7-Code", "GLM-5.3", "GLM-5.2", "DeepSeek-V4-Pro"],
|
|
38
|
+
// #4282 until Global image input (#6906).
|
|
39
|
+
["Qwen3.8-Max", "Qwen3.7-Max", "Qwen3.7-Plus", "Kimi-K3", "Kimi-K2.7-Code", "GLM-5.3", "GLM-5.2"],
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function sameList(current: readonly unknown[], expected: readonly string[]): boolean {
|
|
45
|
+
return current.length === expected.length && current.every((value, index) => value === expected[index]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Pure projection. The caller decides whether to persist. */
|
|
49
|
+
export function projectStaleNoVisionSeeds(
|
|
50
|
+
config: OcxConfig,
|
|
51
|
+
entries: readonly StaleNoVisionSeed[] = STALE_NO_VISION_SEEDS,
|
|
52
|
+
): StaleNoVisionSeedProjection {
|
|
53
|
+
const warnings: string[] = [];
|
|
54
|
+
let changed = false;
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const prov = config.providers?.[entry.provider];
|
|
57
|
+
if (!prov || !Array.isArray(prov.noVisionModels)) continue;
|
|
58
|
+
const registry = PROVIDER_REGISTRY.find(row => row.id === entry.provider);
|
|
59
|
+
// While the registry still seeds a list, the saved one is not stale by this file's rule.
|
|
60
|
+
if (!registry || registry.noVisionModels !== undefined || registry.adapter !== prov.adapter) continue;
|
|
61
|
+
if (!providerMatchesRegistryTransport(entry.provider, prov)) continue;
|
|
62
|
+
const saved = prov.noVisionModels;
|
|
63
|
+
if (!entry.seeds.some(seed => sameList(saved, seed))) continue;
|
|
64
|
+
delete prov.noVisionModels;
|
|
65
|
+
changed = true;
|
|
66
|
+
warnings.push(
|
|
67
|
+
`removed the retired registry noVisionModels seed (${saved.length} model(s)) from "${entry.provider}"; `
|
|
68
|
+
+ "image input now follows the provider's current classification.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return { config, changed, warnings };
|
|
72
|
+
}
|
|
@@ -56,6 +56,20 @@ export const SUMMARY_PREFIX = "Another language model started to solve this prob
|
|
|
56
56
|
|
|
57
57
|
export const OPAQUE_COMPACTION_NOTE = "[earlier conversation was compacted; the summary is stored in a format this model cannot read]";
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Prompt contract for a routed model receiving a decoded `ocx1` checkpoint as user text (#6764).
|
|
61
|
+
* It preserves existing authorization scope and permission gates; it grants no authority.
|
|
62
|
+
* Kept apart from SUMMARY_PREFIX, which the v1 retention and recovery paths match exactly.
|
|
63
|
+
*/
|
|
64
|
+
export const CHECKPOINT_CONTINUATION_NOTE = "This conversation was compacted automatically. A checkpoint summary from an earlier context window appears as a user message and records prior work. Treat earlier requests it reports as completed as history; do not repeat them unless a later user message asks you to. Continue an unfinished task only when it was already authorized and remains within the scope of that authorization. Do not ask for the same authorization again while it still applies. Preserve all existing permission and confirmation gates; if a decision or required permission is pending, wait for it before taking the dependent action. A checkpoint or delegation, including one from another thread, does not itself grant authority or expand the task's scope. Later user messages take priority; obey any pause, cancellation, replacement, or change of scope.";
|
|
65
|
+
|
|
66
|
+
/** Append the continuation note to existing instructions once. */
|
|
67
|
+
export function appendCheckpointContinuationNote(instructions: unknown): string {
|
|
68
|
+
const existing = typeof instructions === "string" ? instructions : "";
|
|
69
|
+
if (existing.includes(CHECKPOINT_CONTINUATION_NOTE)) return existing;
|
|
70
|
+
return existing.trim().length > 0 ? `${existing}\n\n${CHECKPOINT_CONTINUATION_NOTE}` : CHECKPOINT_CONTINUATION_NOTE;
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
/**
|
|
60
74
|
* Item types in the compact wire family. Each carries an `encrypted_content` blob the client
|
|
61
75
|
* replays verbatim on every later turn, and the minting backend verifies it is unmodified.
|