@bitkyc08/opencodex 2.68.0 → 2.69.0-preview.20260928
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/bin/ocx.mjs +25 -17
- package/gui/dist/assets/App-cDPyCb_V.js +51 -0
- package/gui/dist/assets/{Tray-Bn8WhKBH.js → Tray-H1IAB8mN.js} +1 -1
- package/gui/dist/assets/index-BF9QnKO_.css +1 -0
- package/gui/dist/assets/index-CI6YTp8z.js +86 -0
- package/gui/dist/assets/{quota-summary-DL-bNuNL.js → quota-summary-CutOlPu2.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/factory-droid.svg +8 -0
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +109 -11
- package/src/adapters/coding-agent/protocol.ts +106 -11
- package/src/adapters/coding-agent/turn.ts +23 -18
- package/src/adapters/google-errors.ts +16 -1
- package/src/adapters/google-http.ts +14 -0
- package/src/adapters/openai-chat/passthrough.ts +2 -0
- package/src/adapters/openai-chat/summary-budget.ts +72 -0
- package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
- package/src/adapters/openai-chat.ts +6 -9
- package/src/bridge/response-json.ts +3 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/desktop-system-proxy.ts +155 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
- package/src/claude/intercept/picker-ca.ts +283 -83
- package/src/claude/intercept/picker-models.ts +4 -1
- package/src/claude/intercept/picker-trust.ts +15 -2
- package/src/claude/intercept/runtime.ts +99 -32
- package/src/cli/account-api.ts +5 -2
- package/src/cli/account-extended.ts +47 -11
- package/src/cli/account-target.ts +12 -7
- package/src/cli/account.ts +47 -3
- package/src/cli/capabilities.ts +33 -7
- package/src/cli/claude-agent-startup-sync.ts +2 -0
- package/src/cli/claude-desktop.ts +37 -13
- package/src/cli/cross-home-owner.ts +138 -0
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +18 -8
- package/src/cli/ensure-desired-integrations.ts +5 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +15 -18
- package/src/cli/integrations.ts +8 -1
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +106 -4
- package/src/cli/models.ts +6 -1
- package/src/cli/opencode.ts +2 -83
- package/src/cli/registry.ts +3 -2
- package/src/cli/status.ts +86 -7
- package/src/client/connect.ts +24 -10
- package/src/client/link-ingress.ts +3 -1
- package/src/client/link-join.ts +76 -9
- package/src/client/link-relay-transport.ts +184 -0
- package/src/client/link-relay.ts +16 -4
- package/src/client/machine-listener.ts +2 -0
- package/src/clients/config-export/constants.ts +8 -0
- package/src/clients/config-export/contracts.ts +13 -1
- package/src/clients/config-export/droid.ts +71 -0
- package/src/clients/config-export/kilo.ts +116 -0
- package/src/clients/config-export.ts +33 -2
- package/src/codex/auth-api/routes.ts +1 -1
- package/src/codex/catalog/access-programs.ts +14 -2
- package/src/codex/catalog/provider-models.ts +1 -1
- package/src/codex/catalog-auto-refresh.ts +122 -0
- package/src/codex/config-drift-heal.ts +80 -0
- package/src/codex/desktop-switches.ts +7 -5
- package/src/codex/inject/bounded-config-reader.ts +69 -0
- package/src/codex/inject/config-toml.ts +31 -3
- package/src/codex/inject/routing-target.ts +6 -1
- package/src/codex/inject.ts +4 -0
- package/src/codex/model-entitlements.ts +32 -5
- package/src/codex/native-profile-startup.ts +17 -5
- package/src/codex/project-config-warnings.ts +72 -21
- package/src/codex/quota-auto-refresh-state.ts +8 -2
- package/src/codex/quota-auto-refresh.ts +80 -9
- package/src/codex/sync.ts +10 -0
- package/src/combos/failover.ts +4 -1
- package/src/combos/jev.ts +15 -4
- package/src/combos/types.ts +16 -0
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +29 -0
- package/src/config/macos-system-proxy.ts +114 -0
- package/src/config/proxy-env.ts +67 -20
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +23 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +220 -144
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +12 -3
- package/src/integrations/droid-settings.ts +65 -0
- package/src/integrations/kilo-candidates.ts +30 -0
- package/src/integrations/mutation-plan.ts +62 -7
- package/src/integrations/native/ownership-preflight.ts +46 -12
- package/src/integrations/registry.ts +136 -7
- package/src/integrations/state.ts +52 -4
- package/src/integrations/target.ts +3 -0
- package/src/integrations/writer.ts +55 -14
- package/src/lib/account-selection-events.ts +14 -0
- package/src/lib/jsonc.ts +110 -0
- package/src/lib/standalone.ts +9 -1
- package/src/lib/windows-system-proxy.ts +101 -1
- package/src/link/relay-auth.ts +29 -0
- package/src/link/ssh-argv.ts +6 -3
- package/src/link/ssh-runner.ts +5 -5
- package/src/oauth/generic-account-failover.ts +52 -8
- package/src/oauth/index.ts +37 -14
- package/src/oauth/store.ts +96 -11
- package/src/oauth/token-guardian.ts +7 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +5 -3
- package/src/providers/fastwire.ts +3 -0
- package/src/providers/key-failover.ts +13 -5
- package/src/providers/kiro-model-catalog.ts +4 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-tiers-destination.ts +9 -0
- package/src/providers/quota/account-cache.ts +17 -3
- package/src/providers/quota/vendor-probes-oauth.ts +2 -0
- package/src/providers/quota.ts +2 -2
- package/src/responses/code-mode-helper-compat.ts +14 -1
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/images.ts +70 -20
- package/src/server/index/link-listener.ts +20 -3
- package/src/server/index/link-relay-sessions.ts +138 -0
- package/src/server/index/optional-listeners.ts +11 -1
- package/src/server/index/serve-options.ts +12 -0
- package/src/server/inference/client-encoder-delivery.ts +1 -0
- package/src/server/local-management-read-client.ts +21 -2
- package/src/server/management/agent-settings-routes.ts +19 -5
- package/src/server/management/combo-routes.ts +5 -3
- package/src/server/management/config-routes.ts +32 -6
- package/src/server/management/model-routes.ts +232 -7
- package/src/server/management/model-rows.ts +199 -1
- package/src/server/management/oauth-account-routes.ts +63 -3
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/port-reclaim.ts +170 -32
- package/src/server/responses/adapter-delivery.ts +6 -4
- package/src/server/responses/adapter-dispatch.ts +72 -2
- package/src/server/responses/core-combo.ts +6 -1
- package/src/server/responses/core-normalize.ts +19 -4
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/memory-models.ts +169 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +10 -0
- package/src/server/responses/request-prepare.ts +86 -4
- package/src/server/responses/request-transport.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +5 -2
- package/src/server/responses/shadow-target-availability.ts +17 -4
- package/src/server/responses/sidecar-execution.ts +18 -2
- package/src/server/responses/skills-snapshot.ts +239 -0
- package/src/server/responses-undeclared-tool-guard.ts +43 -6
- package/src/server/search.ts +1 -1
- package/src/service/guards.ts +4 -5
- package/src/service/state.ts +39 -2
- package/src/service.ts +2 -2
- package/src/types/config.ts +46 -4
- package/src/types/provider.ts +9 -0
- package/src/types/request.ts +6 -0
- package/src/types/tools.ts +38 -8
- package/src/types.ts +3 -0
- package/src/update/async-check.ts +5 -1
- package/src/update/index.ts +30 -12
- package/src/update/job.ts +3 -1
- package/src/update/pnpm-read-policy.d.mts +4 -0
- package/src/update/pnpm-read-policy.mjs +44 -0
- package/src/update/worker-launch.ts +152 -11
- package/src/usage/cost.ts +6 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/backends.ts +2 -2
- package/src/web-search/devin-executor.ts +1 -1
- package/src/web-search/sidecar-providers.ts +5 -5
- package/gui/dist/assets/App-mpsibsFj.js +0 -51
- package/gui/dist/assets/index-BDUBS8PW.css +0 -1
- package/gui/dist/assets/index-kUE8tuAK.js +0 -86
|
@@ -119,7 +119,7 @@ export async function deliverAdapterResponse(
|
|
|
119
119
|
continuation: fetchGuardedEmptyCompletionRetry,
|
|
120
120
|
})
|
|
121
121
|
: eventStream;
|
|
122
|
-
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
122
|
+
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
123
123
|
// One completion owner for both deliveries: the bridge calls it from its terminal, the
|
|
124
124
|
// direct client encoder from the fold of the same events.
|
|
125
125
|
const onCompletedResponse = (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
@@ -153,7 +153,7 @@ export async function deliverAdapterResponse(
|
|
|
153
153
|
fold: {
|
|
154
154
|
replayCacheScope: parsed._reasoningReplayScope,
|
|
155
155
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
156
|
-
toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames,
|
|
156
|
+
toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames,
|
|
157
157
|
},
|
|
158
158
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
159
159
|
localUpstream,
|
|
@@ -176,8 +176,9 @@ export async function deliverAdapterResponse(
|
|
|
176
176
|
localUpstream,
|
|
177
177
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
178
178
|
declaredToolNames,
|
|
179
|
+
bareCustomToolNames,
|
|
179
180
|
enforceDeclaredToolNames: options.inboundWire !== "chat" && options.inboundWire !== "anthropic",
|
|
180
|
-
|
|
181
|
+
toolParameterSchemas,
|
|
181
182
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
182
183
|
...(routedCompaction ? { compaction: true } : {}),
|
|
183
184
|
// Same grok-surface split as the runTurn branch above.
|
|
@@ -236,7 +237,7 @@ export async function deliverAdapterResponse(
|
|
|
236
237
|
} finally {
|
|
237
238
|
cleanupUpstreamAbort();
|
|
238
239
|
}
|
|
239
|
-
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
240
|
+
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
240
241
|
let providerState: OcxProviderContinuationState | undefined;
|
|
241
242
|
const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
|
|
242
243
|
translatorBudget,
|
|
@@ -244,6 +245,7 @@ export async function deliverAdapterResponse(
|
|
|
244
245
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
245
246
|
toolNsMap,
|
|
246
247
|
declaredToolNames,
|
|
248
|
+
bareCustomToolNames,
|
|
247
249
|
enforceDeclaredToolNames: options.inboundWire !== "chat" && options.inboundWire !== "anthropic",
|
|
248
250
|
toolParameterSchemas,
|
|
249
251
|
freeformToolNames,
|
|
@@ -42,7 +42,8 @@ import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
|
42
42
|
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
43
43
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
44
44
|
import type { OAuthAccessSnapshot } from "../../oauth";
|
|
45
|
-
import { OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
45
|
+
import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
46
|
+
import { getAccountSet } from "../../oauth/store";
|
|
46
47
|
import { tryKiroAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
|
|
47
48
|
import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
|
|
48
49
|
import { normalizeFinalKiroHttpError } from "../../adapters/kiro-retry";
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
rotateGenericOAuthAccountOnRefusal,
|
|
65
66
|
quarantineKiroSuspendedAccount,
|
|
66
67
|
failoverAccountSnapshot,
|
|
68
|
+
rotateAntigravityAccountOnAuthRefusal,
|
|
67
69
|
} from "../../oauth/generic-account-failover";
|
|
68
70
|
import {
|
|
69
71
|
attemptOpaqueBlobRecovery,
|
|
@@ -197,6 +199,8 @@ export async function prepareAdapterExchange(
|
|
|
197
199
|
// Key/OAuth rotation re-resolves credentials, never the origin, so the routed baseUrl is stable.
|
|
198
200
|
const localUpstream = isLocalUpstream(route.provider.baseUrl);
|
|
199
201
|
transportState.activeAdapter = transportState.adapter;
|
|
202
|
+
const antigravityPoolActivated = route.providerName === "google-antigravity"
|
|
203
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName);
|
|
200
204
|
|
|
201
205
|
// One immutable, body-safe outbound request per same-target sequence (URL, serialized body,
|
|
202
206
|
// auth headers, generated compat headers). Same-target 429 replays reuse it verbatim; the
|
|
@@ -409,6 +413,7 @@ export async function prepareAdapterExchange(
|
|
|
409
413
|
// moments later; at most one byte-identical replay is allowed per request.
|
|
410
414
|
const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
|
|
411
415
|
let oauth401ReplayAttempted = false;
|
|
416
|
+
let antigravity401RotationAttempted = false;
|
|
412
417
|
// At most one reasoning-effort downgrade per request. This sits outside the recovery loop
|
|
413
418
|
// below for the same reason the two guards above do: a guard declared inside it is reset by
|
|
414
419
|
// every `continue recovery`, which would let one turn walk the whole ladder down.
|
|
@@ -588,6 +593,54 @@ export async function prepareAdapterExchange(
|
|
|
588
593
|
return { failed: formatErrorResponse(502, "upstream_error", msg) };
|
|
589
594
|
}
|
|
590
595
|
};
|
|
596
|
+
const rotateAntigravityAuth = async (
|
|
597
|
+
failedResponse: Response,
|
|
598
|
+
recovery: AttemptRecoveryKind,
|
|
599
|
+
): Promise<Response | null> => {
|
|
600
|
+
const sent = transportState.replayOAuthCredentialSnapshot ?? transportState.sentOAuthSnapshot;
|
|
601
|
+
if (!antigravityPoolActivated || !sent
|
|
602
|
+
|| transportState.genericFailovers >= transportState.genericFailoverLimit) return null;
|
|
603
|
+
const nextId = rotateAntigravityAccountOnAuthRefusal(
|
|
604
|
+
antigravityPoolActivated, sent.accountId, sent.generation, route.modelId,
|
|
605
|
+
);
|
|
606
|
+
if (!nextId) return null;
|
|
607
|
+
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
608
|
+
const hop = reserveCredentialHop("auth-recovery",
|
|
609
|
+
`${route.providerName}|${route.modelId}|antigravity-auth`,
|
|
610
|
+
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
|
|
611
|
+
if (!hop.allowed) return null;
|
|
612
|
+
try {
|
|
613
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextId);
|
|
614
|
+
const admitted = await applyFailoverSnapshot(snapshot);
|
|
615
|
+
if (admitted?.accountId !== nextId) return null;
|
|
616
|
+
invalidateSameTargetRequest();
|
|
617
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
618
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
619
|
+
config.cacheRetention);
|
|
620
|
+
bindRouteReasoningReplayScope({
|
|
621
|
+
parsed, providerName: route.providerName, provider: route.provider,
|
|
622
|
+
adapterName: transportState.activeAdapter.name,
|
|
623
|
+
oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
|
|
624
|
+
});
|
|
625
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider,
|
|
626
|
+
transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
627
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName,
|
|
628
|
+
route.provider, transportState.activeAdapter.name);
|
|
629
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
630
|
+
const result = await rebuildAndRefetch(recovery, () => {
|
|
631
|
+
if (!adapterOwnsDispatch) hop.permit?.use();
|
|
632
|
+
}, failedResponse);
|
|
633
|
+
if ("failed" in result) return result.failed === failedResponse ? null : result.failed;
|
|
634
|
+
transportState.genericFailovers += 1;
|
|
635
|
+
try { void failedResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
636
|
+
return result;
|
|
637
|
+
} catch {
|
|
638
|
+
return null;
|
|
639
|
+
} finally {
|
|
640
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
641
|
+
hop.permit?.release();
|
|
642
|
+
}
|
|
643
|
+
};
|
|
591
644
|
// Keep recovery kinds in sync with the native Responses `passthroughRecovery:` loop above.
|
|
592
645
|
recovery: for (;;) {
|
|
593
646
|
// Preserve the terminal verdict through adapter and combo error formatting.
|
|
@@ -604,12 +657,18 @@ export async function prepareAdapterExchange(
|
|
|
604
657
|
&& !sendBudgetExhausted()
|
|
605
658
|
) {
|
|
606
659
|
oauth401ReplayAttempted = true;
|
|
607
|
-
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
608
660
|
let refreshed: OAuthAccessSnapshot;
|
|
609
661
|
try {
|
|
610
662
|
refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
|
|
611
663
|
} catch (err) {
|
|
612
664
|
const failed = transportState.sentOAuthSnapshot;
|
|
665
|
+
if (route.providerName === "google-antigravity" && err instanceof OAuthLoginRequiredError && failed
|
|
666
|
+
&& getAccountSet(route.providerName)?.accounts.some(row =>
|
|
667
|
+
row.id === failed.accountId && row.needsReauth === true)) {
|
|
668
|
+
antigravity401RotationAttempted = true;
|
|
669
|
+
const rotated = await rotateAntigravityAuth(upstreamResponse, "oauth-401");
|
|
670
|
+
if (rotated) { upstreamResponse = rotated; continue recovery; }
|
|
671
|
+
}
|
|
613
672
|
if (route.providerName === "kiro" && err instanceof OAuthLoginRequiredError && failed
|
|
614
673
|
&& transportState.genericFailovers < transportState.genericFailoverLimit) {
|
|
615
674
|
const alternate = await tryKiroAlternateAfterTerminalRefresh(config, failed.accountId, failed.generation);
|
|
@@ -650,6 +709,9 @@ export async function prepareAdapterExchange(
|
|
|
650
709
|
}
|
|
651
710
|
}
|
|
652
711
|
cleanupUpstreamAbort();
|
|
712
|
+
if (err instanceof OAuthAccountPausedError) {
|
|
713
|
+
return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(err));
|
|
714
|
+
}
|
|
653
715
|
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
|
|
654
716
|
}
|
|
655
717
|
if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
|
|
@@ -695,6 +757,14 @@ export async function prepareAdapterExchange(
|
|
|
695
757
|
continue recovery;
|
|
696
758
|
}
|
|
697
759
|
|
|
760
|
+
if (route.providerName === "google-antigravity" && upstreamResponse.status === 401
|
|
761
|
+
&& oauth401ReplayAttempted && !antigravity401RotationAttempted
|
|
762
|
+
&& !isNonReplayableResponse(upstreamResponse)) {
|
|
763
|
+
antigravity401RotationAttempted = true;
|
|
764
|
+
const rotated = await rotateAntigravityAuth(upstreamResponse, "oauth-401");
|
|
765
|
+
if (rotated) { upstreamResponse = rotated; continue recovery; }
|
|
766
|
+
}
|
|
767
|
+
|
|
698
768
|
// Static API-key pools can recover a credential-scoped 401 without abandoning the
|
|
699
769
|
// provider: one revoked or mistyped key says nothing about its siblings. OAuth providers
|
|
700
770
|
// refresh above and never enter here — `hasKeyPoolFailover` rejects oauth/forward modes.
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from "../../responses/state";
|
|
46
46
|
import { hasUnreadableEncryptedAgentTask } from "./encrypted-payload";
|
|
47
47
|
import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
|
|
48
|
+
import { memoryModelRouteReason } from "./memory-models";
|
|
48
49
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
49
50
|
import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
|
|
50
51
|
import {
|
|
@@ -226,6 +227,7 @@ function eligibleJevComboChoices(
|
|
|
226
227
|
provider: pick.target.provider,
|
|
227
228
|
model: pick.target.model,
|
|
228
229
|
reasoningEfforts,
|
|
230
|
+
modelProfile: pick.target.modelProfile,
|
|
229
231
|
},
|
|
230
232
|
});
|
|
231
233
|
}
|
|
@@ -558,7 +560,10 @@ export async function executeComboResponses(
|
|
|
558
560
|
}
|
|
559
561
|
// One immutable combo selection trace, before any child dispatch; child
|
|
560
562
|
// adoption below must never replace it with a concrete child route trace.
|
|
561
|
-
|
|
563
|
+
const decision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
|
|
564
|
+
logCtx.routeDecision = options.memoryModelPhase
|
|
565
|
+
? { ...decision, selected: { ...decision.selected, reason: memoryModelRouteReason(options.memoryModelPhase) } }
|
|
566
|
+
: decision;
|
|
562
567
|
|
|
563
568
|
const originalReasoning = body && typeof body === "object" && !Array.isArray(body)
|
|
564
569
|
? (body as { reasoning?: unknown }).reasoning
|
|
@@ -17,6 +17,7 @@ import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transpor
|
|
|
17
17
|
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
18
18
|
import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
19
19
|
import { hasValidatedActiveReasoningEffort } from "../../responses/parser";
|
|
20
|
+
import { responseTierAuthorityForProvider } from "../../providers/openai-tiers-destination";
|
|
20
21
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
21
22
|
import { applyOpenAiVirtualModel } from "../../providers/openai-virtual-models";
|
|
22
23
|
import { renameRoutedIdentityInContext } from "../../adapters/identity";
|
|
@@ -220,14 +221,12 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
220
221
|
);
|
|
221
222
|
const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
|
|
222
223
|
const callerTier = parsed.options.serviceTier;
|
|
223
|
-
//
|
|
224
|
-
// scheduled as priority, so its echo cannot confirm OR deny Fast. Believing it reported every
|
|
225
|
-
// Fast request as `response-declined` (#2558). The public API's echo stays authoritative.
|
|
224
|
+
// Capture destination evidence policy separately from the unchanged outbound Fast decision.
|
|
226
225
|
parsed.options.tierObservation = tierObservationContext(
|
|
227
226
|
fastPolicy,
|
|
228
227
|
config.fastMode,
|
|
229
228
|
callerTier,
|
|
230
|
-
|
|
229
|
+
responseTierAuthorityForProvider(route.provider),
|
|
231
230
|
);
|
|
232
231
|
parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
|
|
233
232
|
parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
|
|
@@ -279,6 +278,22 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
279
278
|
}
|
|
280
279
|
}
|
|
281
280
|
|
|
281
|
+
{
|
|
282
|
+
// Last word on a memory turn's effort: the phase setting is more specific than a provider-wide
|
|
283
|
+
// pin, and Codex hard-codes the phase effort with no config key of its own.
|
|
284
|
+
const phase = parsed._memoryModelPhase;
|
|
285
|
+
if (phase) {
|
|
286
|
+
const { applyMemoryModelEffort } = await import("./memory-models");
|
|
287
|
+
const applied = applyMemoryModelEffort(parsed, config, phase);
|
|
288
|
+
if (applied) {
|
|
289
|
+
logCtx.requestedEffort = applied.from ? `${applied.from}->${applied.to}` : applied.to;
|
|
290
|
+
if (isInjectionDebugEnabled()) {
|
|
291
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: memory ${phase} effort applied (${applied.from ?? "none"} -> ${applied.to})`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
282
297
|
{
|
|
283
298
|
const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
|
|
284
299
|
const surface = collabSurface(parsed);
|
|
@@ -145,6 +145,8 @@ export interface HandleResponsesOptions {
|
|
|
145
145
|
comboAttempt?: boolean;
|
|
146
146
|
/** Internal handoff: this combo was selected by shadow-call interception. */
|
|
147
147
|
shadowCallIntercepted?: boolean;
|
|
148
|
+
/** Internal handoff: the memory phase this turn belongs to, so combo children keep its routing. */
|
|
149
|
+
memoryModelPhase?: "extract" | "consolidation";
|
|
148
150
|
compactionRoutingOverride?: CompactionRoutingOverride | null;
|
|
149
151
|
/** Internal combo handoff for one parent-validated continuation snapshot. */
|
|
150
152
|
comboReplaySnapshot?: {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model routing for Codex's own memory pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Codex writes memories in two background phases, and both ask the provider for a bare native
|
|
5
|
+
* model: Phase 1 ("extract") summarizes one finished thread per call and asks for
|
|
6
|
+
* `gpt-5.6-luna` at effort `low`; Phase 2 ("consolidation") is one agent run that merges those
|
|
7
|
+
* summaries into the files under `$CODEX_HOME/memories` and asks for `gpt-5.6-terra` at effort
|
|
8
|
+
* `medium`. Without a configured target each keeps its existing route; Phase 1 additionally looks like the app's
|
|
9
|
+
* title/commit helper traffic, because the app uses the same model id for those.
|
|
10
|
+
*
|
|
11
|
+
* A phase is therefore recognized from Codex's own turn metadata, never inferred from the model
|
|
12
|
+
* id, the timing, or the token counts. Phase 1 sends `request_kind: "memory"`; both phases carry
|
|
13
|
+
* `thread_source: "memory_consolidation"`, and Phase 2 additionally arrives with
|
|
14
|
+
* `x-openai-subagent: memory_consolidation` (codex-rs `core/src/responses_metadata.rs`).
|
|
15
|
+
*/
|
|
16
|
+
import type { OcxConfig, OcxParsedRequest } from "../../types";
|
|
17
|
+
import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
18
|
+
|
|
19
|
+
/** The two phases Codex runs, in the order it runs them. */
|
|
20
|
+
export type MemoryModelPhase = "extract" | "consolidation";
|
|
21
|
+
|
|
22
|
+
/** codex-rs serializes both keys below into the JSON `x-codex-turn-metadata` header. */
|
|
23
|
+
const TURN_METADATA_HEADER = "x-codex-turn-metadata";
|
|
24
|
+
const REQUEST_KIND_KEY = "request_kind";
|
|
25
|
+
const THREAD_SOURCE_KEY = "thread_source";
|
|
26
|
+
/** `CodexResponsesRequestKind::Memory` (codex-rs `core/src/responses_metadata.rs`). */
|
|
27
|
+
const MEMORY_REQUEST_KIND = "memory";
|
|
28
|
+
/** `ThreadSource::MemoryConsolidation` / `InternalSessionSource::MemoryConsolidation`. */
|
|
29
|
+
const MEMORY_THREAD_SOURCE = "memory_consolidation";
|
|
30
|
+
const SUBAGENT_HEADER = "x-openai-subagent";
|
|
31
|
+
|
|
32
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
33
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
34
|
+
? value as Record<string, unknown>
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One metadata copy's verdict. `"none"` is a well-formed copy that is not a memory turn. */
|
|
39
|
+
type CopyVerdict = MemoryModelPhase | "none";
|
|
40
|
+
|
|
41
|
+
function verdictOf(parsed: Record<string, unknown>): CopyVerdict {
|
|
42
|
+
// Phase 1's detached request names the memory kind explicitly. Phase 2 is an ordinary turn
|
|
43
|
+
// inside the `memory_consolidation` thread, so its thread source is the only signal there.
|
|
44
|
+
if (parsed[REQUEST_KIND_KEY] === MEMORY_REQUEST_KIND) return "extract";
|
|
45
|
+
if (parsed[THREAD_SOURCE_KEY] === MEMORY_THREAD_SOURCE) return "consolidation";
|
|
46
|
+
return "none";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Recognize a memory-pipeline turn, or null.
|
|
51
|
+
*
|
|
52
|
+
* Every copy of the turn metadata the request carries must agree — the same rule
|
|
53
|
+
* `applyCompactionRoutingOverride` applies to compaction turns: a request that contradicts itself
|
|
54
|
+
* is not a memory turn, so neither copy can widen what the setting covers. On HTTP the sub-agent
|
|
55
|
+
* header is accepted on its own because Codex may deliver only that copy; on websocket it is not,
|
|
56
|
+
* because the bridge re-attaches the handshake's header to every frame, so there it marks the
|
|
57
|
+
* connection rather than the turn and the per-frame metadata decides alone.
|
|
58
|
+
*/
|
|
59
|
+
export function detectMemoryModelPhase(
|
|
60
|
+
body: unknown,
|
|
61
|
+
headers: Headers,
|
|
62
|
+
options: { transport?: "websocket" } = {},
|
|
63
|
+
): MemoryModelPhase | null {
|
|
64
|
+
const metadata: unknown[] = [];
|
|
65
|
+
const header = headers.get(TURN_METADATA_HEADER);
|
|
66
|
+
if (options.transport !== "websocket" && header !== null) metadata.push(header);
|
|
67
|
+
const bodyRecord = record(body);
|
|
68
|
+
const rawClient = bodyRecord?.["client_metadata"];
|
|
69
|
+
// Present but malformed client metadata is a turn that failed validation, not an absent
|
|
70
|
+
// copy; it must not fall through to the connection-level sub-agent header below.
|
|
71
|
+
if (bodyRecord && Object.hasOwn(bodyRecord, "client_metadata") && !record(rawClient)) return null;
|
|
72
|
+
const client = record(rawClient);
|
|
73
|
+
if (client && Object.hasOwn(client, TURN_METADATA_HEADER)) metadata.push(client[TURN_METADATA_HEADER]);
|
|
74
|
+
|
|
75
|
+
let verdict: CopyVerdict | null = null;
|
|
76
|
+
for (const value of metadata) {
|
|
77
|
+
if (typeof value !== "string") return null;
|
|
78
|
+
let parsed: Record<string, unknown> | undefined;
|
|
79
|
+
try {
|
|
80
|
+
parsed = record(JSON.parse(value));
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (!parsed) return null;
|
|
85
|
+
const copy = verdictOf(parsed);
|
|
86
|
+
if (verdict !== null && verdict !== copy) return null;
|
|
87
|
+
verdict = copy;
|
|
88
|
+
}
|
|
89
|
+
if (verdict === "extract" || verdict === "consolidation") return verdict;
|
|
90
|
+
// A validated non-memory turn is an explicit decision. The connection-level sub-agent
|
|
91
|
+
// header is only a compatibility signal when turn metadata is wholly absent.
|
|
92
|
+
if (verdict === "none") return null;
|
|
93
|
+
// The websocket bridge rebuilds internal requests from a header allowlist and re-attaches the
|
|
94
|
+
// handshake's sub-agent header to every frame. Trusting it here would sweep the connection's
|
|
95
|
+
// later ordinary turns into the consolidation phase, so websocket frames rely on the per-frame
|
|
96
|
+
// turn metadata above and nothing else.
|
|
97
|
+
if (options.transport === "websocket") return null;
|
|
98
|
+
return headers.get(SUBAGENT_HEADER) === MEMORY_THREAD_SOURCE ? "consolidation" : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The configured destination for one phase, or undefined while the phase keeps Codex's choice. */
|
|
102
|
+
export function configuredMemoryModel(
|
|
103
|
+
config: Pick<OcxConfig, "memoryModels"> | undefined,
|
|
104
|
+
phase: MemoryModelPhase,
|
|
105
|
+
): { model: string; reasoningEffort?: string } | undefined {
|
|
106
|
+
const setting = config?.memoryModels?.[phase];
|
|
107
|
+
if (!setting) return undefined;
|
|
108
|
+
const model = typeof setting.model === "string" ? setting.model.trim() : "";
|
|
109
|
+
if (!model) return undefined;
|
|
110
|
+
const effort = typeof setting.reasoningEffort === "string" ? setting.reasoningEffort : undefined;
|
|
111
|
+
return { model, ...(effort ? { reasoningEffort: effort } : {}) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Force the configured effort onto a memory turn.
|
|
116
|
+
*
|
|
117
|
+
* Codex hard-codes the phase effort (`low` for Phase 1, `medium` for Phase 2) and has no config
|
|
118
|
+
* key for it, so this is the only place the operator's choice can land. Both wire shapes are
|
|
119
|
+
* written: `parsed.options.reasoning` feeds the routed adapters, `_rawBody.reasoning.effort` feeds
|
|
120
|
+
* the ChatGPT passthrough serializer — the same dual-shape contract `applyPinnedEffort` uses.
|
|
121
|
+
*/
|
|
122
|
+
export function applyMemoryModelEffort(
|
|
123
|
+
parsed: OcxParsedRequest,
|
|
124
|
+
config: Pick<OcxConfig, "memoryModels"> | undefined,
|
|
125
|
+
phase: MemoryModelPhase,
|
|
126
|
+
): { from: string | undefined; to: string } | null {
|
|
127
|
+
const effort = configuredMemoryModel(config, phase)?.reasoningEffort;
|
|
128
|
+
if (!effort || !isDeclaredReasoningEffort(effort)) return null;
|
|
129
|
+
const requested = parsed.options.reasoning;
|
|
130
|
+
if (requested === effort) return null;
|
|
131
|
+
parsed.options.reasoning = effort;
|
|
132
|
+
const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
|
|
133
|
+
if (raw && typeof raw === "object") {
|
|
134
|
+
raw.reasoning = { ...(record(raw.reasoning) ?? {}), effort } as { effort?: string };
|
|
135
|
+
}
|
|
136
|
+
return { from: requested, to: effort };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Route reason recorded for a routed memory turn, so the request log names the phase. */
|
|
140
|
+
export function memoryModelRouteReason(phase: MemoryModelPhase): string {
|
|
141
|
+
return phase === "extract" ? "memory-extract" : "memory-consolidation";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Non-retryable: the target stays unavailable until the operator changes the setting. */
|
|
145
|
+
export const MEMORY_MODEL_TARGET_UNAVAILABLE_CODE = "memory_model_target_unavailable";
|
|
146
|
+
export const MEMORY_MODEL_TARGET_UNAVAILABLE_STATUS = 409;
|
|
147
|
+
|
|
148
|
+
const warnedPhases = new Set<MemoryModelPhase>();
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A configured phase destination that stopped resolving fails its call once, clearly, instead of
|
|
152
|
+
* silently falling back to the native model the operator routed away from — the same contract the
|
|
153
|
+
* shadow intercept uses for its single target.
|
|
154
|
+
*/
|
|
155
|
+
export function memoryModelTargetUnavailableResponse(
|
|
156
|
+
phase: MemoryModelPhase,
|
|
157
|
+
): Response {
|
|
158
|
+
// A hand-edited model id or resolver detail may contain a credential-bearing URL. Neither
|
|
159
|
+
// belongs in logs or the client error; the settings panel already displays the saved target.
|
|
160
|
+
const message = `Memory ${phase} model is unavailable. Choose another model in Memory routing or re-enable its provider.`;
|
|
161
|
+
if (!warnedPhases.has(phase)) {
|
|
162
|
+
warnedPhases.add(phase);
|
|
163
|
+
console.warn(`memory-models: ${message}`);
|
|
164
|
+
}
|
|
165
|
+
return new Response(
|
|
166
|
+
JSON.stringify({ error: { message, type: "invalid_request_error", code: MEMORY_MODEL_TARGET_UNAVAILABLE_CODE } }),
|
|
167
|
+
{ status: MEMORY_MODEL_TARGET_UNAVAILABLE_STATUS, headers: { "Content-Type": "application/json" } },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -313,6 +313,7 @@ export async function deliverPassthroughResponse(
|
|
|
313
313
|
| "declaredNamelessClientCallTypes"
|
|
314
314
|
| "providerExecutedCallTypes"
|
|
315
315
|
| "declaredBareWireToolNames"
|
|
316
|
+
| "recoverableBareCustomWireToolNames"
|
|
316
317
|
| "rememberPassthroughResponse"
|
|
317
318
|
| "noteInspectedPayload"
|
|
318
319
|
| "normalizeFunctionCompletionJson"
|
|
@@ -336,6 +337,7 @@ export async function deliverPassthroughResponse(
|
|
|
336
337
|
declaredNamelessClientCallTypes,
|
|
337
338
|
providerExecutedCallTypes,
|
|
338
339
|
declaredBareWireToolNames,
|
|
340
|
+
recoverableBareCustomWireToolNames,
|
|
339
341
|
rememberPassthroughResponse,
|
|
340
342
|
noteInspectedPayload,
|
|
341
343
|
normalizeFunctionCompletionJson,
|
|
@@ -732,6 +734,7 @@ export async function deliverPassthroughResponse(
|
|
|
732
734
|
declaredNamelessClientCallTypes,
|
|
733
735
|
providerExecutedCallTypes,
|
|
734
736
|
declaredBareWireToolNames,
|
|
737
|
+
recoverableBareCustomWireToolNames,
|
|
735
738
|
)
|
|
736
739
|
: undefined,
|
|
737
740
|
grokUpstreamEchoEnabled
|
|
@@ -998,6 +1001,7 @@ export async function deliverPassthroughResponse(
|
|
|
998
1001
|
declaredNamelessClientCallTypes,
|
|
999
1002
|
providerExecutedCallTypes,
|
|
1000
1003
|
declaredBareWireToolNames,
|
|
1004
|
+
recoverableBareCustomWireToolNames,
|
|
1001
1005
|
);
|
|
1002
1006
|
} catch {
|
|
1003
1007
|
return undefined;
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
hasExplicitWireToolCatalog,
|
|
23
23
|
collectDeclaredWireToolNames,
|
|
24
24
|
collectDeclaredBareWireToolNames,
|
|
25
|
+
collectDeclaredBareCustomWireToolNames,
|
|
25
26
|
collectDeclaredNamelessClientCallTypes,
|
|
26
27
|
collectProviderExecutedCallTypes,
|
|
27
28
|
undeclaredToolCallName,
|
|
@@ -314,6 +315,7 @@ export async function preparePassthroughExchange(
|
|
|
314
315
|
const clientExplicitWireToolCatalog = hasExplicitWireToolCatalog(clientToolAuthorizationBody);
|
|
315
316
|
const clientDeclaredWireToolNames = collectDeclaredWireToolNames(clientToolAuthorizationBody);
|
|
316
317
|
const clientDeclaredBareWireToolNames = collectDeclaredBareWireToolNames(clientToolAuthorizationBody);
|
|
318
|
+
const clientDeclaredBareCustomWireToolNames = collectDeclaredBareCustomWireToolNames(clientToolAuthorizationBody);
|
|
317
319
|
const clientDeclaredNamelessCallTypes = collectDeclaredNamelessClientCallTypes(
|
|
318
320
|
clientToolAuthorizationBody,
|
|
319
321
|
);
|
|
@@ -361,6 +363,11 @@ export async function preparePassthroughExchange(
|
|
|
361
363
|
) routedCustomToolRepairNames.add(name);
|
|
362
364
|
}
|
|
363
365
|
}
|
|
366
|
+
// Only grant direct MCP recovery when this delivery actually restores converted custom
|
|
367
|
+
// calls. Native forward and injection paths have no such rewrite.
|
|
368
|
+
const recoverableBareCustomWireToolNames = new Set(
|
|
369
|
+
[...clientDeclaredBareCustomWireToolNames].filter(name => routedCustomToolNames.has(name)),
|
|
370
|
+
);
|
|
364
371
|
for (const name of request.convertedRoutedToolSearchNames ?? []) {
|
|
365
372
|
// The adapter already keeps this set empty when tool_choice forbids the private search.
|
|
366
373
|
// Its wire name may be collision-aliased, so comparing it to the caller-facing name here
|
|
@@ -560,6 +567,7 @@ export async function preparePassthroughExchange(
|
|
|
560
567
|
declaredNamelessClientCallTypes,
|
|
561
568
|
providerExecutedCallTypes,
|
|
562
569
|
declaredBareWireToolNames,
|
|
570
|
+
recoverableBareCustomWireToolNames,
|
|
563
571
|
) !== undefined) {
|
|
564
572
|
inspectionSawUndeclaredTool = true;
|
|
565
573
|
}
|
|
@@ -605,6 +613,7 @@ export async function preparePassthroughExchange(
|
|
|
605
613
|
declaredNamelessClientCallTypes,
|
|
606
614
|
providerExecutedCallTypes,
|
|
607
615
|
declaredBareWireToolNames,
|
|
616
|
+
recoverableBareCustomWireToolNames,
|
|
608
617
|
) !== undefined
|
|
609
618
|
) {
|
|
610
619
|
return;
|
|
@@ -1815,6 +1824,7 @@ export async function preparePassthroughExchange(
|
|
|
1815
1824
|
},
|
|
1816
1825
|
declaredWireToolNames,
|
|
1817
1826
|
declaredBareWireToolNames,
|
|
1827
|
+
recoverableBareCustomWireToolNames,
|
|
1818
1828
|
declaredNamelessClientCallTypes,
|
|
1819
1829
|
authorizedBareNamespaceToolAliases,
|
|
1820
1830
|
normalizeFunctionCompletionJson,
|