@bitkyc08/opencodex 2.59.0 → 2.60.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/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
package/src/oauth/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
|
|
2
2
|
import { initializeProviderModelSelection } from "../providers/initial-model-selection";
|
|
3
|
-
import { parseCallbackInput } from "./callback-server";
|
|
4
3
|
import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
|
|
5
4
|
import { ConfigMutationLockError, loadConfig, mutatePersistedConfig, saveConfig } from "../config";
|
|
6
5
|
import { resolveProviderApiKey } from "../providers/key-store";
|
|
@@ -51,8 +50,9 @@ import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../provider
|
|
|
51
50
|
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
52
51
|
import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
53
52
|
import { logOAuthEvent } from "./log";
|
|
54
|
-
import { captureConfigGeneration, sweepExpiredOnWrite
|
|
55
|
-
import {
|
|
53
|
+
import { captureConfigGeneration, sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
54
|
+
import { clearManualCodeSlot, ensureManualCodeSlot, kiroLoginSettling, loginAbort, loginState, waitForManualLoginCode } from "./login-flow-state";
|
|
55
|
+
export { reconcileOAuthFlowState, submitManualLoginCode } from "./login-flow-state";
|
|
56
56
|
import { randomUUID } from "node:crypto";
|
|
57
57
|
export {
|
|
58
58
|
CODEX_HEALTH_AUTH_FAILED_NOTE,
|
|
@@ -1710,110 +1710,6 @@ export async function runLogin(
|
|
|
1710
1710
|
* localhost), the GUI can POST the final redirect URL or authorization code via
|
|
1711
1711
|
* submitManualLoginCode(), which feeds OAuthController.onManualCodeInput.
|
|
1712
1712
|
*/
|
|
1713
|
-
const loginState = new Map<string, { error?: string; done: boolean }>();
|
|
1714
|
-
const loginAbort = new Map<string, AbortController>();
|
|
1715
|
-
const kiroLoginSettling = new Set<string>();
|
|
1716
|
-
|
|
1717
|
-
/** Pending paste for a login in progress: either a waiter or a stashed early submission. */
|
|
1718
|
-
interface ManualCodeSlot {
|
|
1719
|
-
pendingInput?: string;
|
|
1720
|
-
resolve?: (value: string) => void;
|
|
1721
|
-
/** Registered by the callback flow so submits can validate state synchronously. */
|
|
1722
|
-
expectedState?: string;
|
|
1723
|
-
}
|
|
1724
|
-
const loginManual = new Map<string, ManualCodeSlot>();
|
|
1725
|
-
const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
|
|
1726
|
-
let lastOAuthFlowReconciledGeneration = 0;
|
|
1727
|
-
|
|
1728
|
-
export function reconcileOAuthFlowState(context: GenerationContext): number {
|
|
1729
|
-
if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
|
|
1730
|
-
let removed = 0;
|
|
1731
|
-
for (const [provider, state] of loginState) {
|
|
1732
|
-
if (context.providerNames.has(provider) || !state.done || loginAbort.has(provider)) continue;
|
|
1733
|
-
if (loginState.delete(provider)) removed += 1;
|
|
1734
|
-
if (loginManual.delete(provider)) removed += 1;
|
|
1735
|
-
if (loginAbort.delete(provider)) removed += 1;
|
|
1736
|
-
}
|
|
1737
|
-
lastOAuthFlowReconciledGeneration = context.generation;
|
|
1738
|
-
return removed;
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
function clearManualCodeSlot(provider: string): void {
|
|
1742
|
-
loginManual.delete(provider);
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
function ensureManualCodeSlot(provider: string): ManualCodeSlot {
|
|
1746
|
-
let slot = loginManual.get(provider);
|
|
1747
|
-
if (!slot) {
|
|
1748
|
-
slot = {};
|
|
1749
|
-
loginManual.set(provider, slot);
|
|
1750
|
-
}
|
|
1751
|
-
return slot;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
/** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
|
|
1755
|
-
function waitForManualLoginCode(provider: string, signal: AbortSignal, expectedState?: string): Promise<string> {
|
|
1756
|
-
if (signal.aborted) {
|
|
1757
|
-
return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
1758
|
-
}
|
|
1759
|
-
const slot = ensureManualCodeSlot(provider);
|
|
1760
|
-
if (expectedState !== undefined) slot.expectedState = expectedState;
|
|
1761
|
-
if (slot.pendingInput !== undefined) {
|
|
1762
|
-
const value = slot.pendingInput;
|
|
1763
|
-
slot.pendingInput = undefined;
|
|
1764
|
-
return Promise.resolve(value);
|
|
1765
|
-
}
|
|
1766
|
-
return new Promise<string>((resolve, reject) => {
|
|
1767
|
-
const onAbort = () => {
|
|
1768
|
-
if (slot.resolve === resolve) slot.resolve = undefined;
|
|
1769
|
-
reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
1770
|
-
};
|
|
1771
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1772
|
-
slot.resolve = (value: string) => {
|
|
1773
|
-
signal.removeEventListener("abort", onAbort);
|
|
1774
|
-
if (slot.resolve === resolve) slot.resolve = undefined;
|
|
1775
|
-
resolve(value);
|
|
1776
|
-
};
|
|
1777
|
-
});
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
/**
|
|
1781
|
-
* Feed a pasted redirect URL or authorization code into an in-progress GUI login.
|
|
1782
|
-
* Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
|
|
1783
|
-
* here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
|
|
1784
|
-
*/
|
|
1785
|
-
export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
|
|
1786
|
-
const trimmed = input.trim();
|
|
1787
|
-
if (!trimmed) return { ok: false, error: "empty code" };
|
|
1788
|
-
if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) return { ok: false, error: "code too large" };
|
|
1789
|
-
const st = loginState.get(provider);
|
|
1790
|
-
if (!st || st.done) return { ok: false, error: "no login in progress" };
|
|
1791
|
-
const slot = ensureManualCodeSlot(provider);
|
|
1792
|
-
// Synchronous validation (validated request/ack): reject un-parseable input and
|
|
1793
|
-
// authorization responses (url/query kind) whose state is missing or mismatched
|
|
1794
|
-
// once the flow has registered its expected state. Raw codes stay in-session-PKCE
|
|
1795
|
-
// protected. Early posts (flow not yet waiting, no expectedState) are stashed and
|
|
1796
|
-
// re-validated by the callback loop.
|
|
1797
|
-
const parsed = parseCallbackInput(trimmed);
|
|
1798
|
-
// Command Code's manual fallback accepts a pasted JSON callback payload
|
|
1799
|
-
// (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
|
|
1800
|
-
// shared gate so the provider-specific parser can validate it.
|
|
1801
|
-
const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
|
|
1802
|
-
if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
|
|
1803
|
-
if (parsed.kind !== "raw" && slot.expectedState !== undefined) {
|
|
1804
|
-
if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
|
|
1805
|
-
if (parsed.state !== slot.expectedState) return { ok: false, error: "state mismatch — paste the redirect URL from THIS login attempt" };
|
|
1806
|
-
}
|
|
1807
|
-
if (slot.resolve) {
|
|
1808
|
-
const resolve = slot.resolve;
|
|
1809
|
-
slot.resolve = undefined;
|
|
1810
|
-
resolve(trimmed);
|
|
1811
|
-
} else {
|
|
1812
|
-
// Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
|
|
1813
|
-
slot.pendingInput = trimmed;
|
|
1814
|
-
}
|
|
1815
|
-
return { ok: true };
|
|
1816
|
-
}
|
|
1817
1713
|
|
|
1818
1714
|
export interface OAuthAccountSummary {
|
|
1819
1715
|
id: string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { parseCallbackInput } from "./callback-server";
|
|
2
|
+
import { retainedUtf8Bytes } from "../lib/admission";
|
|
3
|
+
import type { GenerationContext } from "../lib/state-store-sweeper";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-flight login flow state and the manual paste slot that feeds it.
|
|
7
|
+
*
|
|
8
|
+
* Split out of `index.ts` when that file crossed the repository file-size ratchet at 2009
|
|
9
|
+
* lines. The cut follows a seam rather than a line count: everything here is bookkeeping for
|
|
10
|
+
* a login that has started and not yet settled, and none of it reads or writes a stored
|
|
11
|
+
* credential. `index.ts` re-exports the two public names, so existing importers are unaffected.
|
|
12
|
+
*/
|
|
13
|
+
export const loginState = new Map<string, { error?: string; done: boolean }>();
|
|
14
|
+
export const loginAbort = new Map<string, AbortController>();
|
|
15
|
+
export const kiroLoginSettling = new Set<string>();
|
|
16
|
+
|
|
17
|
+
/** Pending paste for a login in progress: either a waiter or a stashed early submission. */
|
|
18
|
+
export interface ManualCodeSlot {
|
|
19
|
+
pendingInput?: string;
|
|
20
|
+
resolve?: (value: string) => void;
|
|
21
|
+
/** Registered by the callback flow so submits can validate state synchronously. */
|
|
22
|
+
expectedState?: string;
|
|
23
|
+
}
|
|
24
|
+
const loginManual = new Map<string, ManualCodeSlot>();
|
|
25
|
+
const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
|
|
26
|
+
let lastOAuthFlowReconciledGeneration = 0;
|
|
27
|
+
|
|
28
|
+
export function reconcileOAuthFlowState(context: GenerationContext): number {
|
|
29
|
+
if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
|
|
30
|
+
let removed = 0;
|
|
31
|
+
for (const [provider, state] of loginState) {
|
|
32
|
+
if (context.providerNames.has(provider) || !state.done || loginAbort.has(provider)) continue;
|
|
33
|
+
if (loginState.delete(provider)) removed += 1;
|
|
34
|
+
if (loginManual.delete(provider)) removed += 1;
|
|
35
|
+
if (loginAbort.delete(provider)) removed += 1;
|
|
36
|
+
}
|
|
37
|
+
lastOAuthFlowReconciledGeneration = context.generation;
|
|
38
|
+
return removed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clearManualCodeSlot(provider: string): void {
|
|
42
|
+
loginManual.delete(provider);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ensureManualCodeSlot(provider: string): ManualCodeSlot {
|
|
46
|
+
let slot = loginManual.get(provider);
|
|
47
|
+
if (!slot) {
|
|
48
|
+
slot = {};
|
|
49
|
+
loginManual.set(provider, slot);
|
|
50
|
+
}
|
|
51
|
+
return slot;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
|
|
55
|
+
export function waitForManualLoginCode(provider: string, signal: AbortSignal, expectedState?: string): Promise<string> {
|
|
56
|
+
if (signal.aborted) {
|
|
57
|
+
return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
58
|
+
}
|
|
59
|
+
const slot = ensureManualCodeSlot(provider);
|
|
60
|
+
if (expectedState !== undefined) slot.expectedState = expectedState;
|
|
61
|
+
if (slot.pendingInput !== undefined) {
|
|
62
|
+
const value = slot.pendingInput;
|
|
63
|
+
slot.pendingInput = undefined;
|
|
64
|
+
return Promise.resolve(value);
|
|
65
|
+
}
|
|
66
|
+
return new Promise<string>((resolve, reject) => {
|
|
67
|
+
const onAbort = () => {
|
|
68
|
+
if (slot.resolve === resolve) slot.resolve = undefined;
|
|
69
|
+
reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
|
|
70
|
+
};
|
|
71
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
72
|
+
slot.resolve = (value: string) => {
|
|
73
|
+
signal.removeEventListener("abort", onAbort);
|
|
74
|
+
if (slot.resolve === resolve) slot.resolve = undefined;
|
|
75
|
+
resolve(value);
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Feed a pasted redirect URL or authorization code into an in-progress GUI login.
|
|
82
|
+
* Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
|
|
83
|
+
* here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
|
|
84
|
+
*/
|
|
85
|
+
export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
|
|
86
|
+
const trimmed = input.trim();
|
|
87
|
+
if (!trimmed) return { ok: false, error: "empty code" };
|
|
88
|
+
if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) return { ok: false, error: "code too large" };
|
|
89
|
+
const st = loginState.get(provider);
|
|
90
|
+
if (!st || st.done) return { ok: false, error: "no login in progress" };
|
|
91
|
+
const slot = ensureManualCodeSlot(provider);
|
|
92
|
+
// Synchronous validation (validated request/ack): reject un-parseable input and
|
|
93
|
+
// authorization responses (url/query kind) whose state is missing or mismatched
|
|
94
|
+
// once the flow has registered its expected state. Raw codes stay in-session-PKCE
|
|
95
|
+
// protected — but a raw paste carrying an explicit #state suffix is state-bearing
|
|
96
|
+
// and checked too. Early posts (flow not yet waiting, no expectedState) are
|
|
97
|
+
// stashed and re-validated by the callback loop.
|
|
98
|
+
const parsed = parseCallbackInput(trimmed);
|
|
99
|
+
// Command Code's manual fallback accepts a pasted JSON callback payload
|
|
100
|
+
// (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
|
|
101
|
+
// shared gate so the provider-specific parser can validate it.
|
|
102
|
+
const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
|
|
103
|
+
if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
|
|
104
|
+
// A raw paste carrying an explicit code#state suffix is state-bearing too: it
|
|
105
|
+
// must match the expected state rather than bypass validation.
|
|
106
|
+
const stateBearing = parsed.kind !== "raw" || parsed.state !== undefined;
|
|
107
|
+
if (stateBearing && slot.expectedState !== undefined) {
|
|
108
|
+
if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
|
|
109
|
+
if (parsed.state !== slot.expectedState) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: parsed.kind === "raw"
|
|
113
|
+
? "state mismatch — paste the bare code, or the correct code#state from THIS login attempt"
|
|
114
|
+
: "state mismatch — paste the redirect URL from THIS login attempt",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (slot.resolve) {
|
|
119
|
+
const resolve = slot.resolve;
|
|
120
|
+
slot.resolve = undefined;
|
|
121
|
+
resolve(trimmed);
|
|
122
|
+
} else {
|
|
123
|
+
// Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
|
|
124
|
+
slot.pendingInput = trimmed;
|
|
125
|
+
}
|
|
126
|
+
return { ok: true };
|
|
127
|
+
}
|
package/src/providers/derive.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
2
|
import { cloneFastWire } from "./fastwire";
|
|
3
|
+
import { resolveModelPolicy } from "./resolved-model-policy";
|
|
3
4
|
import {
|
|
4
5
|
PROVIDER_REGISTRY,
|
|
5
6
|
registryEntryForProviderDestination,
|
|
@@ -220,6 +221,14 @@ export function applyDirectReasoningEffortContracts(
|
|
|
220
221
|
*/
|
|
221
222
|
export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderConfig {
|
|
222
223
|
const liveModels = registryEntrySupportsLiveModelDiscovery(entry) ? entry.liveModels : false;
|
|
224
|
+
const staticPolicy = resolveModelPolicy({
|
|
225
|
+
providerName: entry.id,
|
|
226
|
+
modelId: entry.defaultModel ?? entry.models?.[0] ?? "__provider_seed__",
|
|
227
|
+
provider: { adapter: entry.adapter, baseUrl: entry.baseUrl, authMode: entry.authKind },
|
|
228
|
+
registryEntry: entry,
|
|
229
|
+
transportMatchedRegistry: true,
|
|
230
|
+
effectiveAuth: { authMode: entry.authKind },
|
|
231
|
+
}).provider;
|
|
223
232
|
return {
|
|
224
233
|
adapter: entry.adapter,
|
|
225
234
|
baseUrl: entry.baseUrl,
|
|
@@ -239,17 +248,17 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
239
248
|
...(entry.models ? { models: [...entry.models] } : {}),
|
|
240
249
|
...(liveModels !== undefined ? { liveModels } : {}),
|
|
241
250
|
...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
|
|
242
|
-
...(
|
|
243
|
-
...(
|
|
244
|
-
...(
|
|
245
|
-
...(
|
|
251
|
+
...(staticPolicy.modelContextWindows ? { modelContextWindows: { ...staticPolicy.modelContextWindows } } : {}),
|
|
252
|
+
...(staticPolicy.modelDisplayNames ? { modelDisplayNames: { ...staticPolicy.modelDisplayNames } } : {}),
|
|
253
|
+
...(staticPolicy.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(staticPolicy.modelInputModalities) } : {}),
|
|
254
|
+
...(staticPolicy.modelMaxInputTokens ? { modelMaxInputTokens: { ...staticPolicy.modelMaxInputTokens } } : {}),
|
|
246
255
|
...(entry.defaultMaxOutputTokens !== undefined ? { defaultMaxOutputTokens: entry.defaultMaxOutputTokens } : {}),
|
|
247
|
-
...(
|
|
256
|
+
...(staticPolicy.modelMaxOutputTokens ? { modelMaxOutputTokens: { ...staticPolicy.modelMaxOutputTokens } } : {}),
|
|
248
257
|
...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
|
|
249
|
-
...(
|
|
250
|
-
...(
|
|
251
|
-
...(
|
|
252
|
-
...(
|
|
258
|
+
...(staticPolicy.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(staticPolicy.modelReasoningEfforts) } : {}),
|
|
259
|
+
...(staticPolicy.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...staticPolicy.modelDefaultReasoningEfforts } } : {}),
|
|
260
|
+
...(staticPolicy.reasoningEffortMap ? { reasoningEffortMap: { ...staticPolicy.reasoningEffortMap } } : {}),
|
|
261
|
+
...(staticPolicy.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(staticPolicy.modelReasoningEffortMap) } : {}),
|
|
253
262
|
...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}),
|
|
254
263
|
...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
|
|
255
264
|
...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
|
|
@@ -447,13 +456,16 @@ function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistry
|
|
|
447
456
|
* was skipped and the reasoning ladder was advertised without summary support — exactly the
|
|
448
457
|
* inconsistency that makes Codex drop the inbound reasoning object.
|
|
449
458
|
*
|
|
450
|
-
* Deliberately narrow: reasoning-summary and
|
|
459
|
+
* Deliberately narrow: reasoning-summary, effort, and replay-compatibility metadata only, via
|
|
451
460
|
* `registryEntryForProviderDestination`, which matches fixed key destinations and refuses
|
|
452
461
|
* templated or overridable base URLs. A custom row keeps its own identity for everything else.
|
|
453
462
|
*/
|
|
454
463
|
function enrichReasoningMetadataByDestination(prov: OcxProviderConfig): void {
|
|
455
464
|
const destination = registryEntryForProviderDestination(prov);
|
|
456
465
|
applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries);
|
|
466
|
+
if (prov.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined) {
|
|
467
|
+
prov.dropResponsesReasoningItems = destination.dropResponsesReasoningItems;
|
|
468
|
+
}
|
|
457
469
|
if (destination?.modelReasoningEfforts) {
|
|
458
470
|
prov.modelReasoningEfforts = fillRecordOfArrays(destination.modelReasoningEfforts, prov.modelReasoningEfforts);
|
|
459
471
|
}
|
|
@@ -491,6 +503,14 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
491
503
|
modelReasoningEffortMap: prov.modelReasoningEffortMap,
|
|
492
504
|
};
|
|
493
505
|
const seed = providerConfigSeed(entry);
|
|
506
|
+
const resolvedStatic = resolveModelPolicy({
|
|
507
|
+
providerName: name,
|
|
508
|
+
modelId: prov.defaultModel ?? entry.defaultModel ?? "__provider_enrich__",
|
|
509
|
+
provider: prov,
|
|
510
|
+
registryEntry: entry,
|
|
511
|
+
transportMatchedRegistry: true,
|
|
512
|
+
...(prov.authMode ? { effectiveAuth: { authMode: prov.authMode } } : {}),
|
|
513
|
+
}).provider;
|
|
494
514
|
repairStaticModelCatalogProvider(name, prov);
|
|
495
515
|
if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
|
|
496
516
|
if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
|
|
@@ -504,10 +524,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
504
524
|
if (!prov.modelContextWindows && seed.modelContextWindows) prov.modelContextWindows = { ...seed.modelContextWindows };
|
|
505
525
|
// Per-model fill, not all-or-nothing: an operator who renamed ONE model must still receive
|
|
506
526
|
// labels for the rest, and an existing install must pick up newly seeded rows on enrich.
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
if (seed.modelInputModalities) prov.modelInputModalities = fillRecordOfArrays(seed.modelInputModalities, prov.modelInputModalities);
|
|
527
|
+
if (resolvedStatic.modelDisplayNames) prov.modelDisplayNames = { ...resolvedStatic.modelDisplayNames };
|
|
528
|
+
if (resolvedStatic.modelInputModalities) prov.modelInputModalities = cloneRecordOfArrays(resolvedStatic.modelInputModalities);
|
|
511
529
|
if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
|
|
512
530
|
if (!prov.modelMaxOutputTokens && seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens };
|
|
513
531
|
if ((!prov.reasoningEfforts || hasLegacyClinePassReasoningEfforts(name, prov)) && seed.reasoningEfforts) {
|
|
@@ -518,9 +536,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
518
536
|
// provider. That split the two planes apart — routing merges these maps per key
|
|
519
537
|
// (mergeRecordFill in src/router.ts), so the wire honored the effort while /v1/models and
|
|
520
538
|
// every client export showed no effort control at all.
|
|
521
|
-
if (
|
|
522
|
-
prov.modelReasoningEfforts = fillRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts);
|
|
523
|
-
}
|
|
539
|
+
if (resolvedStatic.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(resolvedStatic.modelReasoningEfforts);
|
|
524
540
|
if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
|
|
525
541
|
if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
|
|
526
542
|
if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap);
|
|
@@ -563,6 +579,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
563
579
|
prov.supportsResponsesCustomTools = entry.supportsResponsesCustomTools;
|
|
564
580
|
}
|
|
565
581
|
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
|
|
582
|
+
if (prov.dropResponsesReasoningItems === undefined && entry.dropResponsesReasoningItems !== undefined) prov.dropResponsesReasoningItems = entry.dropResponsesReasoningItems;
|
|
566
583
|
applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
|
|
567
584
|
applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
|
|
568
585
|
applyVerbosityDefaults(prov, entry);
|
|
@@ -12,14 +12,12 @@
|
|
|
12
12
|
* key: the `devin-cli` registry entry is gone, so the PROVIDER_REGISTRY lookup
|
|
13
13
|
* that gated the rewrite here could never fire again.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* adapter is rewritten to `devin`, whatever the row is called, and each
|
|
22
|
-
* rewrite is reported.
|
|
15
|
+
* Custom-named rows fail closed. The retired ACP transport used the installed
|
|
16
|
+
* CLI's identity and ignored both configured and caller credentials, so
|
|
17
|
+
* silently changing one of those rows into a cloud provider could send a
|
|
18
|
+
* credential with different provenance to Cognition. The warning tells the
|
|
19
|
+
* operator to reconfigure it explicitly; retaining the unknown adapter id
|
|
20
|
+
* prevents requests from reaching any upstream in the meantime.
|
|
23
21
|
*
|
|
24
22
|
* A row carrying the retired ACP identity URL is repointed at the api-server
|
|
25
23
|
* in the same pass. That URL was never a destination — it existed only so
|
|
@@ -55,10 +53,16 @@ export function projectDevinCliAuthMode(config: OcxConfig): DevinCliAuthModeProj
|
|
|
55
53
|
const warnings: string[] = [];
|
|
56
54
|
let changed = false;
|
|
57
55
|
|
|
58
|
-
// Every row, not only the registry id: a custom-named row had no registry pin,
|
|
59
|
-
// so after the ACP removal it is the one that cannot construct an adapter.
|
|
60
56
|
for (const [name, row] of Object.entries(config.providers ?? {})) {
|
|
61
57
|
if (!row || row.adapter !== RETIRED_ACP_ADAPTER) continue;
|
|
58
|
+
if (name !== RETIRED_ACP_ADAPTER) {
|
|
59
|
+
warnings.push(
|
|
60
|
+
`left custom provider "${name}" unchanged because adapter ${RETIRED_ACP_ADAPTER} was removed: `
|
|
61
|
+
+ "the row was not migrated; switch its adapter to devin and configure Devin authentication "
|
|
62
|
+
+ "explicitly before using this provider.",
|
|
63
|
+
);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
62
66
|
row.adapter = "devin";
|
|
63
67
|
changed = true;
|
|
64
68
|
let detail = "";
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { commitProviderApiKeySelection } from "./api-key-selection";
|
|
12
12
|
import type { ProviderApiKeySelection } from "../types/provider";
|
|
13
13
|
import { routedProviderConfig } from "../router";
|
|
14
|
+
import { getProviderRegistryEntry } from "./registry";
|
|
15
|
+
import { normalizedBaseUrl } from "./quota/vendor-probes-key";
|
|
14
16
|
import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, TransientRetryPolicy } from "../types";
|
|
15
17
|
import { OPENCODE_GO_SESSION_HEADER } from "./opencode-go-transport";
|
|
16
18
|
import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
|
|
@@ -278,6 +280,43 @@ const DEFAULT_RATE_LIMIT_RETRY = {
|
|
|
278
280
|
respectRetryAfter: true,
|
|
279
281
|
} as const satisfies Required<RateLimitRetryPolicy>;
|
|
280
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Patient same-target 429 fallback for the OpenCode Go destination
|
|
285
|
+
* (https://opencode.ai/zen/go/v1), which serves subscription traffic such as Muse Spark.
|
|
286
|
+
* Single-key pools cannot fail over, so without this a burst 429 surfaces immediately and
|
|
287
|
+
* the client retry budget aborts the goal. Six 10s-paced attempts absorb a short burst
|
|
288
|
+
* window (effective replays are additionally bounded by the shared per-request send
|
|
289
|
+
* budget); Retry-After is still honored and capped. An explicit `retryOn429` (including
|
|
290
|
+
* `enabled: false`) always overrides this fallback.
|
|
291
|
+
*/
|
|
292
|
+
const OPENCODE_GO_RATE_LIMIT_RETRY = {
|
|
293
|
+
enabled: true,
|
|
294
|
+
attempts: 6,
|
|
295
|
+
intervalMs: 10_000,
|
|
296
|
+
maxIntervalMs: 60_000,
|
|
297
|
+
respectRetryAfter: true,
|
|
298
|
+
} as const satisfies Required<RateLimitRetryPolicy>;
|
|
299
|
+
|
|
300
|
+
/** True when the provider row points at the OpenCode Go destination. */
|
|
301
|
+
export function isOpenCodeGoDestination(
|
|
302
|
+
provider: Partial<Pick<OcxProviderConfig, "baseUrl" | "authMode">>,
|
|
303
|
+
): boolean {
|
|
304
|
+
const raw = typeof provider.baseUrl === "string" ? provider.baseUrl : "";
|
|
305
|
+
if (!raw.trim()) return false;
|
|
306
|
+
// Endpoint identity, not adapter identity: the runtime adapter is already overridden
|
|
307
|
+
// per model by the time the recovery loop runs (muse-spark rides `openai-responses`
|
|
308
|
+
// while the preset declares `openai-chat`), so an adapter-strict lookup misses it.
|
|
309
|
+
// Canonicalize with the shared quota-probe normalizer so host case and explicit
|
|
310
|
+
// default ports compare equal; userinfo, query, and fragment never match
|
|
311
|
+
// (follow-up to the review on #5067).
|
|
312
|
+
const endpoint = normalizedBaseUrl(raw.trim());
|
|
313
|
+
if (!endpoint) return false;
|
|
314
|
+
const entry = getProviderRegistryEntry("opencode-go");
|
|
315
|
+
if (!entry) return false;
|
|
316
|
+
const candidates = [entry.baseUrl, ...(entry.destinationAliases ?? []).map(alias => alias.baseUrl)];
|
|
317
|
+
return candidates.some(url => normalizedBaseUrl(url.trim()) === endpoint);
|
|
318
|
+
}
|
|
319
|
+
|
|
281
320
|
/**
|
|
282
321
|
* Default transient-5xx retry used when a provider opts in with a bare
|
|
283
322
|
* `transientRetryOn5xx: {}`. `attempts` is a TOTAL send budget, not extra retries.
|
|
@@ -513,29 +552,38 @@ export function selectProactiveApiKeyTransport(
|
|
|
513
552
|
}
|
|
514
553
|
|
|
515
554
|
/**
|
|
516
|
-
* Normalize a provider's `retryOn429` policy
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
555
|
+
* Normalize a provider's `retryOn429` policy. An explicit object always wins (including
|
|
556
|
+
* `enabled: false` to opt out). When the knob is absent, the OpenCode Go destination
|
|
557
|
+
* (subscription traffic such as Muse Spark) falls back to a patient same-key policy so a
|
|
558
|
+
* burst 429 waits and replays instead of surfacing to the client and aborting a long
|
|
559
|
+
* session; every other provider without the knob keeps today's fail-fast behavior.
|
|
560
|
+
* OAuth/forward/local credentials are never replayed on the same token. The returned
|
|
561
|
+
* policy is fully defaulted so callers never re-check fields.
|
|
521
562
|
*/
|
|
522
563
|
export function rateLimitRetryPolicyFor(
|
|
523
|
-
provider: Pick<OcxProviderConfig, "retryOn429" | "authMode"
|
|
564
|
+
provider: Pick<OcxProviderConfig, "retryOn429" | "authMode"> &
|
|
565
|
+
Partial<Pick<OcxProviderConfig, "baseUrl" | "adapter">>,
|
|
524
566
|
): Required<RateLimitRetryPolicy> | null {
|
|
525
567
|
const policy = provider.retryOn429;
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
568
|
+
if (policy) {
|
|
569
|
+
if (policy.enabled === false) return null;
|
|
570
|
+
// Fail closed: only explicit key auth or the documented omitted-default (undefined == key for
|
|
571
|
+
// custom API-key providers) may use same-key replays. OAuth/forward are never replayed on the
|
|
572
|
+
// same token, local runtimes have no remote key to preserve, and unknown/custom values are
|
|
573
|
+
// rejected rather than guessed at.
|
|
574
|
+
if (provider.authMode !== undefined && provider.authMode !== "key") return null;
|
|
575
|
+
return {
|
|
576
|
+
enabled: policy.enabled ?? DEFAULT_RATE_LIMIT_RETRY.enabled,
|
|
577
|
+
attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
|
|
578
|
+
intervalMs: policy.intervalMs ?? DEFAULT_RATE_LIMIT_RETRY.intervalMs,
|
|
579
|
+
maxIntervalMs: policy.maxIntervalMs ?? DEFAULT_RATE_LIMIT_RETRY.maxIntervalMs,
|
|
580
|
+
respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
// No explicit knob: patient fallback for the OpenCode Go destination only.
|
|
531
584
|
if (provider.authMode !== undefined && provider.authMode !== "key") return null;
|
|
532
|
-
return
|
|
533
|
-
|
|
534
|
-
attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
|
|
535
|
-
intervalMs: policy.intervalMs ?? DEFAULT_RATE_LIMIT_RETRY.intervalMs,
|
|
536
|
-
maxIntervalMs: policy.maxIntervalMs ?? DEFAULT_RATE_LIMIT_RETRY.maxIntervalMs,
|
|
537
|
-
respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
|
|
538
|
-
};
|
|
585
|
+
if (!isOpenCodeGoDestination(provider)) return null;
|
|
586
|
+
return { ...OPENCODE_GO_RATE_LIMIT_RETRY };
|
|
539
587
|
}
|
|
540
588
|
|
|
541
589
|
/**
|
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
// This migration is deliberately NOT general reconciliation. It rewrites exactly
|
|
15
15
|
// one thing: an id this file declares retired, on a provider that still carries
|
|
16
16
|
// the registry's transport, and only when the registry currently seeds the
|
|
17
|
-
// replacement
|
|
17
|
+
// replacement — and it skips a field where the retired entry is registry residue
|
|
18
|
+
// with nothing left to carry forward (see `isRegistryResidue`, issue #5066).
|
|
19
|
+
// Everything else in the row is left alone.
|
|
18
20
|
|
|
19
21
|
import { PROVIDER_REGISTRY } from "./registry";
|
|
22
|
+
import { providerConfigSeed } from "./derive";
|
|
20
23
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
21
24
|
|
|
22
25
|
export interface ModelRename {
|
|
@@ -194,6 +197,54 @@ function registrySeedsTarget(rename: ModelRename): boolean {
|
|
|
194
197
|
return !!entry?.models?.includes(rename.to);
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
/** The row the registry contributes to a saved config, or `undefined` when it dropped the id. */
|
|
201
|
+
function registrySeed(provider: string): Record<string, unknown> | undefined {
|
|
202
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === provider);
|
|
203
|
+
return entry ? (providerConfigSeed(entry) as unknown as Record<string, unknown>) : undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Whether a saved list or model-keyed record names this id at all. */
|
|
207
|
+
function publishes(container: unknown, id: string): boolean {
|
|
208
|
+
if (Array.isArray(container)) return container.includes(id);
|
|
209
|
+
if (!container || typeof container !== "object") return false;
|
|
210
|
+
return id in (container as Record<string, unknown>);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Whether this field’s retired entry is registry residue rather than something to repair.
|
|
215
|
+
*
|
|
216
|
+
* Two things have to be true. The registry’s own seed — `providerConfigSeed`, the row both the
|
|
217
|
+
* OAuth preset (`deriveOAuthProviderConfig`) and `enrichProviderFromRegistry` contribute — must
|
|
218
|
+
* still publish the retired id in this field, so the entry arrives in the config on its own. And
|
|
219
|
+
* the saved field must already carry the supported id, so the rename would only DELETE a key,
|
|
220
|
+
* with nothing to carry forward.
|
|
221
|
+
*
|
|
222
|
+
* Under those two conditions the rewrite is worse than a no-op, because it never converges.
|
|
223
|
+
* `startServer` runs this migration and then `reconcileOAuthProviders`, and
|
|
224
|
+
* `applyOAuthPresetCatalog` copies the preset’s record over the saved one whenever the two
|
|
225
|
+
* differ. The deletion is undone inside the same boot, the config is written twice, and the user
|
|
226
|
+
* is told about a rename that never sticks on every single start. Issue #5066 is exactly that:
|
|
227
|
+
* `ANTIGRAVITY_MODEL_CONTEXT_WINDOWS` derives a window for every compatibility alias, so
|
|
228
|
+
* `modelContextWindows` holds a key for all nine retired Flash ids beside the live ones — the
|
|
229
|
+
* nine `[model-rename-migration]` lines the reporter saw at every `ocx start`, none of which
|
|
230
|
+
* required them to have selected any of those nine models.
|
|
231
|
+
*
|
|
232
|
+
* Those registry entries are deliberate and stay: a request that still names `gemini-3.6-flash`
|
|
233
|
+
* routes to 3.7 and needs a context window under the id it asked for. The second condition is
|
|
234
|
+
* what keeps this narrow. A row that saved `{ "gemini-3.6-flash": 1048576 }` and nothing else
|
|
235
|
+
* still gets the value carried onto the supported id, because dropping it there would blank the
|
|
236
|
+
* record instead of moving it (`tests/adapters/google/gemini-37-flash-migration.test.ts`).
|
|
237
|
+
*/
|
|
238
|
+
function isRegistryResidue(
|
|
239
|
+
seed: Record<string, unknown> | undefined,
|
|
240
|
+
field: string,
|
|
241
|
+
saved: unknown,
|
|
242
|
+
rename: ModelRename,
|
|
243
|
+
): boolean {
|
|
244
|
+
if (!seed) return false;
|
|
245
|
+
return publishes(seed[field], rename.from) && publishes(saved, rename.to);
|
|
246
|
+
}
|
|
247
|
+
|
|
197
248
|
export interface ModelRenameProjection {
|
|
198
249
|
config: OcxConfig;
|
|
199
250
|
changed: boolean;
|
|
@@ -226,14 +277,17 @@ export function projectModelRenames(
|
|
|
226
277
|
// Provider config is a closed interface, so index through one unknown-cast
|
|
227
278
|
// view rather than casting at each assignment.
|
|
228
279
|
const row = prov as unknown as Record<string, unknown>;
|
|
280
|
+
const seed = registrySeed(rename.provider);
|
|
229
281
|
let touched = false;
|
|
230
282
|
for (const field of MODEL_ID_LISTS) {
|
|
283
|
+
if (isRegistryResidue(seed, field, row[field], rename)) continue;
|
|
231
284
|
const next = renameInList(row[field], rename.from, rename.to);
|
|
232
285
|
if (!next) continue;
|
|
233
286
|
row[field] = next;
|
|
234
287
|
touched = true;
|
|
235
288
|
}
|
|
236
289
|
for (const field of MODEL_KEYED_RECORDS) {
|
|
290
|
+
if (isRegistryResidue(seed, field, row[field], rename)) continue;
|
|
237
291
|
const next = rename.dropReasoningEffortMap && field === "modelReasoningEffortMap"
|
|
238
292
|
? dropFromRecord(row[field], rename.from)
|
|
239
293
|
: renameInRecord(row[field], rename.from, rename.to);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { mutatePersistedConfig } from "../config";
|
|
2
2
|
import { projectModelRenames } from "./model-rename-migration";
|
|
3
3
|
import { projectStaleContextWindows } from "./stale-context-window-migration";
|
|
4
|
+
import { projectStaleVisionClassifications } from "./stale-vision-classification-migration";
|
|
4
5
|
import { projectDevinCliAuthMode } from "./devin-cli-authmode-migration";
|
|
5
6
|
import type { OcxConfig } from "../types";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* The startup projection: registry model renames, then the context-window
|
|
9
|
-
*
|
|
9
|
+
* The startup projection: registry model renames, then the context-window and
|
|
10
|
+
* vision-classification repairs. All three fix a saved row the registry can no longer reach on its own —
|
|
10
11
|
* `enrichProviderFromRegistry` backfills a missing field and never rewrites a
|
|
11
12
|
* present one — so they share this pass rather than adding a second boot step
|
|
12
13
|
* with its own persistence, adopt, and failure handling.
|
|
@@ -14,11 +15,12 @@ import type { OcxConfig } from "../types";
|
|
|
14
15
|
export function projectStartupConfigRepairs(config: OcxConfig): ReturnType<typeof projectModelRenames> {
|
|
15
16
|
const renames = projectModelRenames(config);
|
|
16
17
|
const windows = projectStaleContextWindows(renames.config);
|
|
17
|
-
const
|
|
18
|
+
const vision = projectStaleVisionClassifications(windows.config);
|
|
19
|
+
const devinCli = projectDevinCliAuthMode(vision.config);
|
|
18
20
|
return {
|
|
19
21
|
config: devinCli.config,
|
|
20
|
-
changed: renames.changed || windows.changed || devinCli.changed,
|
|
21
|
-
warnings: [...renames.warnings, ...windows.warnings, ...devinCli.warnings],
|
|
22
|
+
changed: renames.changed || windows.changed || vision.changed || devinCli.changed,
|
|
23
|
+
warnings: [...renames.warnings, ...windows.warnings, ...vision.warnings, ...devinCli.warnings],
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
|