@bitkyc08/opencodex 2.86.0 → 2.88.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-CHC89EXQ.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +151 -33
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +52 -9
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +7 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +59 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +77 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +46 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +33 -16
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +14 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +53 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +657 -297
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +116 -27
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-failure-model.ts +2 -0
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +40 -11
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +6 -5
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/opencode-zen-rate-limit.ts +7 -2
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +74 -6
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/analytics.ts +7 -15
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +56 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +20 -6
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +8 -8
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +117 -24
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +47 -19
- package/src/server/responses/adapter-delivery.ts +72 -12
- package/src/server/responses/adapter-dispatch.ts +46 -3
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +20 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +121 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +8 -4
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +7 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +23 -3
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +77 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +2 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +63 -9
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +35 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +31 -2
- package/src/types/wire.ts +31 -37
- package/src/types.ts +2 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +30 -11
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +236 -73
- package/src/web-search/progress-stream.ts +15 -2
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -52,6 +52,15 @@ import {
|
|
|
52
52
|
setDebugSettings,
|
|
53
53
|
type DebugFlag,
|
|
54
54
|
} from "../../lib/debug-settings";
|
|
55
|
+
import { mergeModelCapabilities, modelCapabilitiesConfigError } from "../../config/provider-validation";
|
|
56
|
+
import { modelRecordValue } from "../../reasoning-effort";
|
|
57
|
+
import { commitProviderPatch } from "./provider-patch-transaction";
|
|
58
|
+
import { ConfigWritePublishedError } from "../../config/persist-unlocked";
|
|
59
|
+
import {
|
|
60
|
+
customRowInputModalities,
|
|
61
|
+
isVisionSidecarConsumer,
|
|
62
|
+
modelAcceptsImageInput,
|
|
63
|
+
} from "../../vision/eligibility";
|
|
55
64
|
import type { OcxClaudeCodeConfig, OcxComboConfig, OcxConfig, OcxCustomModel, OcxProviderConfig, OcxComboCooldownWaitPolicy } from "../../types";
|
|
56
65
|
import { drainAndShutdown } from "../lifecycle";
|
|
57
66
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
@@ -116,6 +125,43 @@ function sparseComboConfig<T extends {
|
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
127
|
|
|
128
|
+
interface VisionSidecarTarget {
|
|
129
|
+
provider: string;
|
|
130
|
+
model: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Request-only PUT field: combo members to declare text-only so the Vision Sidecar
|
|
135
|
+
* covers them when the combo accepts images. Every entry must be an exact target of
|
|
136
|
+
* the submitted combo, and the whole field is validated before any config mutation.
|
|
137
|
+
* Nothing from here is persisted under config.combos.
|
|
138
|
+
*/
|
|
139
|
+
function parseVisionSidecarTargets(
|
|
140
|
+
raw: unknown,
|
|
141
|
+
comboTargetKeys: ReadonlySet<string>,
|
|
142
|
+
): { targets?: VisionSidecarTarget[]; error?: string } {
|
|
143
|
+
if (raw === undefined) return {};
|
|
144
|
+
if (!Array.isArray(raw)) return { error: "visionSidecarTargets must be an array" };
|
|
145
|
+
const seen = new Set<string>();
|
|
146
|
+
const targets: VisionSidecarTarget[] = [];
|
|
147
|
+
for (const entry of raw) {
|
|
148
|
+
if (!isPlainRecord(entry)) return { error: "visionSidecarTargets entries must be objects" };
|
|
149
|
+
const provider = typeof entry.provider === "string" ? entry.provider.trim() : "";
|
|
150
|
+
const model = typeof entry.model === "string" ? entry.model.trim() : "";
|
|
151
|
+
if (!provider || !model) {
|
|
152
|
+
return { error: "visionSidecarTargets entries must have nonblank provider and model" };
|
|
153
|
+
}
|
|
154
|
+
const key = `${provider}/${model}`;
|
|
155
|
+
if (!comboTargetKeys.has(key)) {
|
|
156
|
+
return { error: `visionSidecarTargets entry "${key}" is not a target of this combo` };
|
|
157
|
+
}
|
|
158
|
+
if (seen.has(key)) continue;
|
|
159
|
+
seen.add(key);
|
|
160
|
+
targets.push({ provider, model });
|
|
161
|
+
}
|
|
162
|
+
return { targets };
|
|
163
|
+
}
|
|
164
|
+
|
|
119
165
|
export async function handleComboRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
120
166
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
121
167
|
|
|
@@ -176,6 +222,9 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
176
222
|
const requestedCombo: Record<string, unknown> = body.combo;
|
|
177
223
|
const effectiveCombo = {
|
|
178
224
|
...requestedCombo,
|
|
225
|
+
...(!Object.hasOwn(requestedCombo, "allowMeteredTargets") && previous?.allowMeteredTargets === true
|
|
226
|
+
? { allowMeteredTargets: true }
|
|
227
|
+
: {}),
|
|
179
228
|
...(!Object.hasOwn(requestedCombo, "cooldownMs") && previous?.cooldownMs !== undefined
|
|
180
229
|
? { cooldownMs: previous.cooldownMs }
|
|
181
230
|
: {}),
|
|
@@ -203,7 +252,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
203
252
|
// A non-record entry stays as the client sent it so comboConfigError reports it.
|
|
204
253
|
// Reading `lastResort` off it here would throw in place of that structured 400.
|
|
205
254
|
if (!isPlainRecord(target)) return target;
|
|
206
|
-
if (Object.hasOwn(target,
|
|
255
|
+
if (["lastResort", "metered"].every(key => Object.hasOwn(target, key))) return target;
|
|
207
256
|
const rawProvider = target.provider;
|
|
208
257
|
const rawModel = target.model;
|
|
209
258
|
if (typeof rawProvider !== "string" || typeof rawModel !== "string") return target;
|
|
@@ -214,7 +263,13 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
214
263
|
const before = previous.targets.find(
|
|
215
264
|
candidate => candidate.provider === provider && candidate.model === model,
|
|
216
265
|
);
|
|
217
|
-
|
|
266
|
+
// `metered` is carried the same way: dropping it on a GUI round-trip would open a
|
|
267
|
+
// pay-per-use target to every issued key.
|
|
268
|
+
return {
|
|
269
|
+
...target,
|
|
270
|
+
...(!Object.hasOwn(target, "lastResort") && before?.lastResort ? { lastResort: true } : {}),
|
|
271
|
+
...(!Object.hasOwn(target, "metered") && before?.metered ? { metered: true } : {}),
|
|
272
|
+
};
|
|
218
273
|
}),
|
|
219
274
|
}
|
|
220
275
|
: {}),
|
|
@@ -280,6 +335,54 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
280
335
|
if (routeError) return jsonResponse({ error: issue ? `combo "${otherId}": ${routeError}` : routeError }, 400);
|
|
281
336
|
}
|
|
282
337
|
const normalized = normalizeComboConfig(effectiveCombo as unknown as OcxComboConfig);
|
|
338
|
+
const sidecarParsed = parseVisionSidecarTargets(
|
|
339
|
+
body.visionSidecarTargets,
|
|
340
|
+
new Set(normalized.targets.map((target) => `${target.provider}/${target.model}`)),
|
|
341
|
+
);
|
|
342
|
+
if (sidecarParsed.error) return jsonResponse({ error: sidecarParsed.error }, 400);
|
|
343
|
+
const sidecarPatches = new Map<string, Record<string, { inputModalities: string[] }>>();
|
|
344
|
+
if (sidecarParsed.targets?.length) {
|
|
345
|
+
if (normalized.imageInput === "disabled") {
|
|
346
|
+
return jsonResponse({ error: "visionSidecarTargets requires imageInput not disabled" }, 400);
|
|
347
|
+
}
|
|
348
|
+
for (const { provider, model } of sidecarParsed.targets) {
|
|
349
|
+
const providerRow = config.providers?.[provider];
|
|
350
|
+
if (!providerRow) {
|
|
351
|
+
return jsonResponse({ error: `visionSidecarTargets entry "${provider}/${model}" has no configured provider` }, 400);
|
|
352
|
+
}
|
|
353
|
+
// A text-only declaration would HIDE a real capability; never overwrite one.
|
|
354
|
+
if (modelAcceptsImageInput(config, { provider, id: model }) === true) {
|
|
355
|
+
return jsonResponse({ error: `visionSidecarTargets entry "${provider}/${model}" already accepts image input` }, 400);
|
|
356
|
+
}
|
|
357
|
+
// Already a consumer through any runtime source (exact declaration, custom row,
|
|
358
|
+
// noVisionModels, legacy record, registry enrichment): the catalog already
|
|
359
|
+
// advertises image for it. Writing the declaration again would be a no-op write.
|
|
360
|
+
if (isVisionSidecarConsumer(config, provider, model)) continue;
|
|
361
|
+
// Read the declaration the way the runtime does: exact capability axis, then the
|
|
362
|
+
// operator's custom row, then the provider's per-model modality hint. A row without
|
|
363
|
+
// text (audio-only) cannot be widened by the sidecar, and writing ["text"] on the
|
|
364
|
+
// capability axis would silently override that declaration.
|
|
365
|
+
const capabilityRow = Object.hasOwn(providerRow.modelCapabilities ?? {}, model)
|
|
366
|
+
? providerRow.modelCapabilities?.[model] : undefined;
|
|
367
|
+
const declared = capabilityRow?.inputModalities
|
|
368
|
+
?? customRowInputModalities(config, provider, model)
|
|
369
|
+
?? modelRecordValue(providerRow.modelInputModalities, model);
|
|
370
|
+
if (declared !== undefined && !declared.includes("text")) {
|
|
371
|
+
return jsonResponse({ error: `visionSidecarTargets entry "${provider}/${model}" is declared without text input; the Vision Sidecar cannot cover it` }, 400);
|
|
372
|
+
}
|
|
373
|
+
const patch = sidecarPatches.get(provider) ?? {};
|
|
374
|
+
patch[model] = { inputModalities: ["text"] };
|
|
375
|
+
sidecarPatches.set(provider, patch);
|
|
376
|
+
}
|
|
377
|
+
for (const [providerName, patch] of sidecarPatches) {
|
|
378
|
+
// Validate the map that will be persisted, not just the patch: a provider already at
|
|
379
|
+
// the modelCapabilities cap would otherwise save an oversized map that load-time
|
|
380
|
+
// sanitization truncates, silently dropping the new text-only declaration.
|
|
381
|
+
const merged = mergeModelCapabilities(config.providers?.[providerName]?.modelCapabilities, patch);
|
|
382
|
+
const error = modelCapabilitiesConfigError(patch) ?? (merged ? modelCapabilitiesConfigError(merged) : null);
|
|
383
|
+
if (error) return jsonResponse({ error: `visionSidecarTargets for provider "${providerName}": ${error}` }, 400);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
283
386
|
// Persist only non-default identity/capability fields so config stays sparse.
|
|
284
387
|
// Capability defaults (`imageInput`, `reasoningEffortMode`) go through the same
|
|
285
388
|
// helper the GET/PUT responses use, so the wire shape and the stored shape cannot drift.
|
|
@@ -295,9 +398,10 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
295
398
|
// would add a noise key to every target of every combo, including ones that never use
|
|
296
399
|
// the policy (#5736). Only the opt-in value is stored, matching how the combo-level
|
|
297
400
|
// policy is handled in sparseComboConfig.
|
|
298
|
-
targets: normalizedBase.targets.map(({ lastResort, modelProfile, ...target }) => ({
|
|
401
|
+
targets: normalizedBase.targets.map(({ lastResort, metered, modelProfile, ...target }) => ({
|
|
299
402
|
...target,
|
|
300
403
|
...(lastResort ? { lastResort: true } : {}),
|
|
404
|
+
...(metered ? { metered: true } : {}),
|
|
301
405
|
...(modelProfile ? { modelProfile } : {}),
|
|
302
406
|
})),
|
|
303
407
|
...(normalizedAlias ? { alias: normalizedAlias } : {}),
|
|
@@ -340,58 +444,80 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
340
444
|
const targetError = shadowCallTargetError({ ...config, combos: nextCombos }, migratedShadowTarget);
|
|
341
445
|
if (targetError) return jsonResponse({ error: targetError }, 400);
|
|
342
446
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
447
|
+
// One atomic transaction: a failed save must restore the combo, the capability
|
|
448
|
+
// declarations, and every migrated reference together, never partially.
|
|
449
|
+
try {
|
|
450
|
+
commitProviderPatch(config, () => {
|
|
451
|
+
config.combos = nextCombos;
|
|
452
|
+
if (migratedModels.size > 0) {
|
|
453
|
+
const migrateReference = (model: string): string => migratedModels.get(model) ?? model;
|
|
454
|
+
const migrateAgentReference = (model: string): string => {
|
|
455
|
+
const migrated = migrateReference(model);
|
|
456
|
+
if (migrated !== model) shouldSyncClaudeAgentDefs = true;
|
|
457
|
+
return migrated;
|
|
458
|
+
};
|
|
459
|
+
if (config.subagentModels) {
|
|
460
|
+
config.subagentModels = [...new Set(config.subagentModels.map(migrateAgentReference))];
|
|
461
|
+
}
|
|
462
|
+
if (config.injectionModel && migratedModels.has(config.injectionModel)) {
|
|
463
|
+
config.injectionModel = migrateReference(config.injectionModel);
|
|
464
|
+
}
|
|
465
|
+
if (config.shadowCallIntercept?.model && migratedModels.has(config.shadowCallIntercept.model)) {
|
|
466
|
+
config.shadowCallIntercept = {
|
|
467
|
+
...config.shadowCallIntercept,
|
|
468
|
+
model: migrateReference(config.shadowCallIntercept.model),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (config.claudeCode) {
|
|
472
|
+
const claudeCode = { ...config.claudeCode };
|
|
473
|
+
for (const field of ["model", "smallFastModel"] as const) {
|
|
474
|
+
if (claudeCode[field]) claudeCode[field] = migrateAgentReference(claudeCode[field]);
|
|
475
|
+
}
|
|
476
|
+
if (claudeCode.tierModels) {
|
|
477
|
+
claudeCode.tierModels = Object.fromEntries(
|
|
478
|
+
Object.entries(claudeCode.tierModels).map(([tier, model]) => [tier, migrateAgentReference(model)]),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (claudeCode.modelMap) {
|
|
482
|
+
claudeCode.modelMap = Object.fromEntries(
|
|
483
|
+
Object.entries(claudeCode.modelMap).map(([source, model]) => [source, migrateAgentReference(model)]),
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (claudeCode.intercept?.modelMap) {
|
|
487
|
+
claudeCode.intercept = {
|
|
488
|
+
...claudeCode.intercept,
|
|
489
|
+
modelMap: Object.fromEntries(
|
|
490
|
+
Object.entries(claudeCode.intercept.modelMap).map(([pickerId, route]) => [pickerId, migrateAgentReference(route)]),
|
|
491
|
+
),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
config.claudeCode = claudeCode;
|
|
495
|
+
}
|
|
372
496
|
}
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
);
|
|
497
|
+
if (oldDisabledSelectors.size > 0 && config.disabledModels) {
|
|
498
|
+
config.disabledModels = [...new Set(config.disabledModels.map(model => (
|
|
499
|
+
oldDisabledSelectors.has(model) ? newDisabledModel : model
|
|
500
|
+
)))];
|
|
377
501
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
502
|
+
for (const [providerName, patch] of sidecarPatches) {
|
|
503
|
+
const provider = config.providers?.[providerName];
|
|
504
|
+
if (!provider) continue;
|
|
505
|
+
const capabilities = mergeModelCapabilities(provider.modelCapabilities, patch);
|
|
506
|
+
// Re-check under the config lock: a concurrent edit may have grown the map since
|
|
507
|
+
// the pre-check, and an oversized map must never be published.
|
|
508
|
+
if (capabilities && modelCapabilitiesConfigError(capabilities)) {
|
|
509
|
+
throw new Error("modelCapabilities would exceed its limit");
|
|
510
|
+
}
|
|
511
|
+
if (capabilities === undefined) delete provider.modelCapabilities;
|
|
512
|
+
else provider.modelCapabilities = capabilities;
|
|
385
513
|
}
|
|
386
|
-
|
|
514
|
+
}, deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (!(error instanceof ConfigWritePublishedError)) {
|
|
517
|
+
return jsonResponse({ error: "combo could not be saved" }, 500);
|
|
387
518
|
}
|
|
519
|
+
throw error;
|
|
388
520
|
}
|
|
389
|
-
if (oldDisabledSelectors.size > 0 && config.disabledModels) {
|
|
390
|
-
config.disabledModels = [...new Set(config.disabledModels.map(model => (
|
|
391
|
-
oldDisabledSelectors.has(model) ? newDisabledModel : model
|
|
392
|
-
)))];
|
|
393
|
-
}
|
|
394
|
-
saveConfigPreservingClaudeCode(config);
|
|
395
521
|
reconcileLiveStateStores();
|
|
396
522
|
clearComboSelectionState(id);
|
|
397
523
|
clearComboTargetCooldowns(id);
|
|
@@ -27,7 +27,7 @@ function response(): Response {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export async function handleCompanionRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
30
|
-
if (ctx.url.pathname === "/api/advisor/settings") {
|
|
30
|
+
if (ctx.url.pathname === "/api/advisor/settings" || ctx.url.pathname === "/api/advisor/activity") {
|
|
31
31
|
const { handleAdvisorRoutes } = await import("./advisor-routes");
|
|
32
32
|
return handleAdvisorRoutes(ctx);
|
|
33
33
|
}
|
|
@@ -2,8 +2,9 @@ import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
|
|
|
2
2
|
import { resolveMainAccountHardLockThresholds } from "../../codex/main-account-hard-lock";
|
|
3
3
|
import { getMainAccountExternalUsageWarning } from "../../codex/main-account-external-usage";
|
|
4
4
|
import { getObservedMainQuotaIdentityKey } from "../../codex/main-account-cache";
|
|
5
|
-
import { compactionRoutingSchema, memoryModelsSchema } from "../../config/schema/leaf-validators";
|
|
5
|
+
import { compactionRoutingSchema, memoryModelsSchema, reasoningRetentionSchema } from "../../config/schema/leaf-validators";
|
|
6
6
|
import { compactionRecoverySchema } from "../../config/schema/compaction-recovery";
|
|
7
|
+
import { mainAccountHardLockFallbackSchema } from "../../config/schema/main-account-hard-lock-fallback";
|
|
7
8
|
import { anthropicSidecarPatchError } from "../../config/schema/anthropic-account-pool";
|
|
8
9
|
import { isAnthropicInstanceId } from "../../providers/anthropic-instance-id";
|
|
9
10
|
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
@@ -121,6 +122,8 @@ import {
|
|
|
121
122
|
} from "../../lib/debug-settings";
|
|
122
123
|
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
123
124
|
import { shadowCallTargetError } from "./shadow-call-validation";
|
|
125
|
+
import { autoReviewOverrideConfigError } from "../../config/provider-validation";
|
|
126
|
+
import { ConfigWritePublishedError } from "../../config/persist-unlocked";
|
|
124
127
|
import { drainAndShutdown } from "../lifecycle";
|
|
125
128
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
126
129
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
@@ -268,7 +271,6 @@ export async function syncEnabledClientIntegrations(
|
|
|
268
271
|
try {
|
|
269
272
|
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
270
273
|
const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
|
|
271
|
-
const { fetchAllModels } = await import("../management-api");
|
|
272
274
|
const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
|
|
273
275
|
// Serialized with Desktop mode transitions (picker lock): a first-party switch cannot interleave.
|
|
274
276
|
const { runPickerTransition } = await import("./claude-desktop-picker-routes");
|
|
@@ -353,6 +355,7 @@ function publicVisionSidecarSettings(
|
|
|
353
355
|
};
|
|
354
356
|
}
|
|
355
357
|
|
|
358
|
+
/** Serve validated settings and publish catalog-affecting changes after persistence. */
|
|
356
359
|
export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
357
360
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
358
361
|
const readStartupHealth = deps.getCachedStartupHealth ?? getCachedStartupHealth;
|
|
@@ -424,6 +427,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
424
427
|
// Absent means on by default: the GUI renders a switch enabled unless explicit false.
|
|
425
428
|
fastRows: config.fastRows !== false,
|
|
426
429
|
codexMainAccountHardLock: isMainAccountHardLockEnabled(config),
|
|
430
|
+
// Absent means off: the fallback is an extension of the lock, so the GUI renders
|
|
431
|
+
// the extended fields empty unless an explicit section exists.
|
|
432
|
+
codexMainAccountHardLockFallback: config.codexMainAccountHardLockFallback ?? null,
|
|
427
433
|
mainAccountHardLock: { ...getMainAccountHardLockStatus(config),
|
|
428
434
|
externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
|
|
429
435
|
// Absent means the historical auto-open, so the GUI can render the toggle
|
|
@@ -437,6 +443,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
437
443
|
codexDesktopSwitches: describeCodexDesktopSwitches(config, await observedCodexDesktopSwitchApply()),
|
|
438
444
|
compactionRouting: config.compactionRouting ?? null,
|
|
439
445
|
compactionRecovery: config.compactionRecovery ?? null,
|
|
446
|
+
reasoningRetention: config.reasoningRetention ?? null,
|
|
440
447
|
// Absent means both phases keep their existing routes; the GUI renders that as "Off".
|
|
441
448
|
memoryModels: config.memoryModels ?? null,
|
|
442
449
|
startupHealth: await readStartupHealthSnapshot(config),
|
|
@@ -530,11 +537,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
530
537
|
fastRows?: unknown;
|
|
531
538
|
codexMainAccountHardLock?: unknown;
|
|
532
539
|
codexMainAccountHardLockThresholds?: unknown;
|
|
540
|
+
codexMainAccountHardLockFallback?: unknown;
|
|
533
541
|
codexDesktopAuthless?: unknown;
|
|
534
542
|
codexClientCompaction?: unknown;
|
|
535
543
|
compactionRouting?: unknown;
|
|
536
544
|
compactionRecovery?: unknown;
|
|
537
545
|
memoryModels?: unknown;
|
|
546
|
+
reasoningRetention?: unknown;
|
|
538
547
|
};
|
|
539
548
|
if (body.codexAutoStart === undefined
|
|
540
549
|
&& body.streamMode === undefined
|
|
@@ -547,12 +556,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
547
556
|
&& body.fastRows === undefined
|
|
548
557
|
&& body.codexMainAccountHardLock === undefined
|
|
549
558
|
&& body.codexMainAccountHardLockThresholds === undefined
|
|
559
|
+
&& body.codexMainAccountHardLockFallback === undefined
|
|
550
560
|
&& body.codexDesktopAuthless === undefined
|
|
551
561
|
&& body.codexClientCompaction === undefined
|
|
552
562
|
&& body.compactionRouting === undefined
|
|
553
563
|
&& body.compactionRecovery === undefined
|
|
564
|
+
&& body.reasoningRetention === undefined
|
|
554
565
|
&& body.memoryModels === undefined) {
|
|
555
|
-
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexCredits, ultraFastTier, fastRows, codexMainAccountHardLock, codexMainAccountHardLockThresholds, codexDesktopAuthless, codexClientCompaction, compactionRouting, compactionRecovery, or memoryModels" }, 400);
|
|
566
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexCredits, ultraFastTier, fastRows, codexMainAccountHardLock, codexMainAccountHardLockThresholds, codexMainAccountHardLockFallback, codexDesktopAuthless, codexClientCompaction, compactionRouting, compactionRecovery, reasoningRetention, or memoryModels" }, 400);
|
|
556
567
|
}
|
|
557
568
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
558
569
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -597,6 +608,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
597
608
|
return jsonResponse({ error: "codexMainAccountHardLockThresholds short must not exceed long" }, 400);
|
|
598
609
|
}
|
|
599
610
|
}
|
|
611
|
+
// null clears the section, like compactionRouting; a present section must fully parse.
|
|
612
|
+
const hardLockFallback = body.codexMainAccountHardLockFallback == null
|
|
613
|
+
? body.codexMainAccountHardLockFallback
|
|
614
|
+
: mainAccountHardLockFallbackSchema.safeParse(body.codexMainAccountHardLockFallback);
|
|
615
|
+
if (hardLockFallback != null && !hardLockFallback.success) {
|
|
616
|
+
return jsonResponse({ error: "codexMainAccountHardLockFallback requires a nonblank model selector and an optional declared reasoningEffort" }, 400);
|
|
617
|
+
}
|
|
600
618
|
if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") {
|
|
601
619
|
return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400);
|
|
602
620
|
}
|
|
@@ -614,6 +632,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
614
632
|
const memoryModels = body.memoryModels == null
|
|
615
633
|
? body.memoryModels
|
|
616
634
|
: memoryModelsSchema.safeParse(body.memoryModels);
|
|
635
|
+
const reasoningRetention = body.reasoningRetention == null ? body.reasoningRetention : reasoningRetentionSchema.safeParse(body.reasoningRetention);
|
|
636
|
+
if (reasoningRetention != null && !reasoningRetention.success) return jsonResponse({ error: "reasoningRetention requires optional boolean enabled, maxContextPercent greater than 0 and at most 100 and/or positive integer maxTokens; no other fields are allowed" }, 400);
|
|
617
637
|
if (memoryModels != null && !memoryModels.success) {
|
|
618
638
|
return jsonResponse({ error: "memoryModels requires a nonblank model and an optional declared reasoningEffort per configured phase, and no other fields" }, 400);
|
|
619
639
|
}
|
|
@@ -646,7 +666,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
646
666
|
)) {
|
|
647
667
|
return jsonResponse({ error: `appOwnedMemoryBudgetMb must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}` }, 400);
|
|
648
668
|
}
|
|
649
|
-
const restoreCompactionRouting = captureConfigTopLevelRollback(config, ["compactionRouting", "compactionRecovery", "memoryModels"]);
|
|
669
|
+
const restoreCompactionRouting = captureConfigTopLevelRollback(config, ["compactionRouting", "compactionRecovery", "memoryModels", "reasoningRetention"]);
|
|
650
670
|
const previousSettings = {
|
|
651
671
|
codexAutoStart: config.codexAutoStart,
|
|
652
672
|
hasCodexAutoStart: Object.hasOwn(config, "codexAutoStart"),
|
|
@@ -672,6 +692,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
672
692
|
hasCodexMainAccountHardLockThresholds: Object.hasOwn(config, "codexMainAccountHardLockThresholds"),
|
|
673
693
|
codexMainAccountHardLock: config.codexMainAccountHardLock,
|
|
674
694
|
hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
|
|
695
|
+
codexMainAccountHardLockFallback: config.codexMainAccountHardLockFallback,
|
|
696
|
+
hasCodexMainAccountHardLockFallback: Object.hasOwn(config, "codexMainAccountHardLockFallback"),
|
|
675
697
|
codexDesktopAuthless: config.codexDesktopAuthless,
|
|
676
698
|
hasCodexDesktopAuthless: Object.hasOwn(config, "codexDesktopAuthless"),
|
|
677
699
|
codexClientCompaction: config.codexClientCompaction,
|
|
@@ -717,6 +739,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
717
739
|
if (hardLockThresholds !== undefined) config.codexMainAccountHardLockThresholds = { ...hardLockThresholds };
|
|
718
740
|
if (body.codexMainAccountHardLock === false) config.codexMainAccountHardLock = false;
|
|
719
741
|
else if (body.codexMainAccountHardLock === true) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
742
|
+
if (hardLockFallback === null) deleteConfigTopLevelKey(config, "codexMainAccountHardLockFallback");
|
|
743
|
+
else if (hardLockFallback?.success) config.codexMainAccountHardLockFallback = hardLockFallback.data;
|
|
720
744
|
if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
|
|
721
745
|
else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
722
746
|
if (body.codexClientCompaction === true) config.codexClientCompaction = true;
|
|
@@ -725,6 +749,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
725
749
|
else if (compactionRouting?.success) config.compactionRouting = compactionRouting.data;
|
|
726
750
|
if (compactionRecovery === null) deleteConfigTopLevelKey(config, "compactionRecovery");
|
|
727
751
|
else if (compactionRecovery?.success) config.compactionRecovery = compactionRecovery.data;
|
|
752
|
+
if (reasoningRetention === null) deleteConfigTopLevelKey(config, "reasoningRetention");
|
|
753
|
+
else if (reasoningRetention?.success) config.reasoningRetention = reasoningRetention.data;
|
|
728
754
|
// Null clears both phases; the GUI sends that when neither row names a model.
|
|
729
755
|
if (memoryModels === null) deleteConfigTopLevelKey(config, "memoryModels");
|
|
730
756
|
else if (memoryModels?.success) config.memoryModels = memoryModels.data;
|
|
@@ -775,6 +801,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
775
801
|
if (previousSettings.hasCodexMainAccountHardLock) {
|
|
776
802
|
config.codexMainAccountHardLock = previousSettings.codexMainAccountHardLock;
|
|
777
803
|
} else deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
804
|
+
if (previousSettings.hasCodexMainAccountHardLockFallback) {
|
|
805
|
+
config.codexMainAccountHardLockFallback = previousSettings.codexMainAccountHardLockFallback;
|
|
806
|
+
} else deleteConfigTopLevelKey(config, "codexMainAccountHardLockFallback");
|
|
778
807
|
if (previousSettings.hasCodexDesktopAuthless) {
|
|
779
808
|
config.codexDesktopAuthless = previousSettings.codexDesktopAuthless;
|
|
780
809
|
} else deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
@@ -836,10 +865,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
836
865
|
codexDesktopSwitches,
|
|
837
866
|
compactionRouting: config.compactionRouting ?? null,
|
|
838
867
|
compactionRecovery: config.compactionRecovery ?? null,
|
|
868
|
+
reasoningRetention: config.reasoningRetention ?? null,
|
|
839
869
|
// The panel re-reads its own save response, so a missing block would render both
|
|
840
870
|
// phases as "Off" while the server kept them.
|
|
841
871
|
memoryModels: config.memoryModels ?? null,
|
|
842
872
|
codexMainAccountHardLock: isMainAccountHardLockEnabled(config),
|
|
873
|
+
codexMainAccountHardLockFallback: config.codexMainAccountHardLockFallback ?? null,
|
|
843
874
|
mainAccountHardLock: { ...getMainAccountHardLockStatus(config),
|
|
844
875
|
externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
|
|
845
876
|
startupHealth: await readStartupHealthSnapshot(config),
|
|
@@ -1058,6 +1089,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
1058
1089
|
// Read BEFORE the mutation below: the Codex-side key follows the switch, and a flip is the
|
|
1059
1090
|
// only case that owes a config.toml rewrite.
|
|
1060
1091
|
const webSearchEnabledBefore = config.webSearchSidecar?.enabled !== false;
|
|
1092
|
+
const visionEnabledBefore = config.visionSidecar?.enabled !== false;
|
|
1061
1093
|
if (body.webSearch) {
|
|
1062
1094
|
const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined || body.webSearch.anthropicInstance !== undefined;
|
|
1063
1095
|
// Validate against the backend the caller SUBMITTED, across the whole
|
|
@@ -1193,6 +1225,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
1193
1225
|
}
|
|
1194
1226
|
}
|
|
1195
1227
|
saveConfigPreservingClaudeCode(config);
|
|
1228
|
+
// Publish the changed attachment affordance to both the managed catalog and Codex cache.
|
|
1229
|
+
// Use the existing bounded catalog-only path after persistence, outside the config lock.
|
|
1230
|
+
const catalogRefresh = visionEnabledBefore !== (config.visionSidecar?.enabled !== false)
|
|
1231
|
+
? await convergeCodexCatalog()
|
|
1232
|
+
: undefined;
|
|
1196
1233
|
// The sidecar's own switch is only half of what "off" means: Codex keeps declaring its native
|
|
1197
1234
|
// hosted `web_search` tool until its own mode says otherwise, and the tool a client advertises
|
|
1198
1235
|
// is the one the model reaches for — which is why an MCP search server needs the client-side
|
|
@@ -1218,6 +1255,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
1218
1255
|
...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
|
|
1219
1256
|
},
|
|
1220
1257
|
codexWebSearch,
|
|
1258
|
+
catalogRefreshPending: catalogRefresh ? catalogRefreshIsPending(catalogRefresh) : false,
|
|
1221
1259
|
vision: publicVisionSidecarSettings(config, vision),
|
|
1222
1260
|
visionModels: vision.models,
|
|
1223
1261
|
// Echoed for the same reason GET always carries it: the dashboard rebuilds
|
|
@@ -1269,5 +1307,38 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
1269
1307
|
sourceModels: shadowSourceModels(sci.sourceModels),
|
|
1270
1308
|
});
|
|
1271
1309
|
}
|
|
1310
|
+
|
|
1311
|
+
if (url.pathname === "/api/auto-review-settings" && req.method === "GET") {
|
|
1312
|
+
const override = config.autoReviewOverride ?? {};
|
|
1313
|
+
return jsonResponse({ enabled: override.enabled === true, model: override.model?.trim() ?? "" });
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (url.pathname === "/api/auto-review-settings" && req.method === "PUT") {
|
|
1317
|
+
let raw: unknown;
|
|
1318
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
1319
|
+
if (!isPlainRecord(raw)) return jsonResponse({ error: "body must be a JSON object" }, 400);
|
|
1320
|
+
const body = raw as { enabled?: unknown; model?: unknown };
|
|
1321
|
+
const shapeError = autoReviewOverrideConfigError(body, "body");
|
|
1322
|
+
if (shapeError) return jsonResponse({ error: shapeError }, 400);
|
|
1323
|
+
const rollback = captureConfigTopLevelRollback(config, ["autoReviewOverride"]);
|
|
1324
|
+
try {
|
|
1325
|
+
config.autoReviewOverride = { ...config.autoReviewOverride };
|
|
1326
|
+
if (typeof body.enabled === "boolean") config.autoReviewOverride.enabled = body.enabled;
|
|
1327
|
+
if (typeof body.model === "string") {
|
|
1328
|
+
const trimmed = body.model.trim();
|
|
1329
|
+
if (trimmed === "") delete config.autoReviewOverride.model;
|
|
1330
|
+
else config.autoReviewOverride.model = trimmed;
|
|
1331
|
+
}
|
|
1332
|
+
(deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
if (!(error instanceof ConfigWritePublishedError)) rollback();
|
|
1335
|
+
throw error;
|
|
1336
|
+
}
|
|
1337
|
+
// The catalog carries the reviewer stamps, so the new selector has to reach it now; the
|
|
1338
|
+
// disposition travels in the body like every other catalog-affecting route.
|
|
1339
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
1340
|
+
const override = config.autoReviewOverride;
|
|
1341
|
+
return jsonResponse({ ok: true, enabled: override.enabled === true, model: override.model ?? "", catalogRefresh });
|
|
1342
|
+
}
|
|
1272
1343
|
return null;
|
|
1273
1344
|
}
|
|
@@ -16,7 +16,7 @@ import { buildCursorLocalInstallerHint, type CursorLocalInstallerHint } from "..
|
|
|
16
16
|
import { loadCursorEffortTable } from "../../integrations/cursor-effort-table";
|
|
17
17
|
import { configuredApiAuthToken, isApiAuthRequired, jsonResponse } from "../auth-cors";
|
|
18
18
|
import { localInferenceDestination } from "../../lib/local-destinations";
|
|
19
|
-
import { fetchAllModels } from "
|
|
19
|
+
import { fetchAllModels } from "./shared";
|
|
20
20
|
import { predictCursorEffort } from "../models-capabilities";
|
|
21
21
|
import { expandCursorEffortRow, knownEffortRowIds } from "../effort-row";
|
|
22
22
|
import type { ManagementContext } from "./context";
|
|
@@ -109,6 +109,7 @@ import {
|
|
|
109
109
|
import { removeCredential } from "../../oauth/store";
|
|
110
110
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
111
111
|
import { redactSecretString } from "../../lib/redact";
|
|
112
|
+
import { withModelCostRowReplacement } from "../../config/live-reconcile";
|
|
112
113
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
113
114
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
114
115
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
@@ -408,7 +409,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
408
409
|
const submitted = { [modelId]: body.cost };
|
|
409
410
|
const validationError = body.cost === null ? null : providerModelCostsConfigError(submitted);
|
|
410
411
|
if (validationError) return jsonResponse({ error: validationError }, 400, req, config);
|
|
411
|
-
// Copy
|
|
412
|
+
// Copy validated rates and policy; never echo a secret-shaped model key that the
|
|
412
413
|
// shared display boundary suppresses. Model IDs remain exact, including slashes.
|
|
413
414
|
const cost = body.cost === null ? null : sanitizeModelCostsForDisplay(submitted)?.[modelId];
|
|
414
415
|
if (cost === undefined) return jsonResponse({ error: "modelId cannot be displayed safely" }, 400, req, config);
|
|
@@ -423,10 +424,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
423
424
|
const previousModelCosts = provider.modelCosts;
|
|
424
425
|
const nextModelCosts = Object.assign(
|
|
425
426
|
Object.create(null) as Record<string, ProviderCostOverlay>,
|
|
426
|
-
previousModelCosts ?? {},
|
|
427
|
+
structuredClone(previousModelCosts ?? {}),
|
|
427
428
|
);
|
|
428
429
|
if (cost === null) delete nextModelCosts[modelId];
|
|
429
|
-
else nextModelCosts[modelId] = cost;
|
|
430
|
+
else nextModelCosts[modelId] = structuredClone(cost);
|
|
430
431
|
const mergedError = providerModelCostsConfigError(nextModelCosts);
|
|
431
432
|
if (mergedError) return jsonResponse({ error: mergedError }, 400, req, config);
|
|
432
433
|
// Keep even an empty map until persistence reconciles individual model keys.
|
|
@@ -435,10 +436,14 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
435
436
|
try {
|
|
436
437
|
// The persistence owner refreshes usage overlays after its atomic write.
|
|
437
438
|
// Price-only edits do not change routing or require catalog convergence.
|
|
438
|
-
persistConfig(config);
|
|
439
|
+
withModelCostRowReplacement(config, name, modelId, cost, () => persistConfig(config));
|
|
439
440
|
} catch (error) {
|
|
440
|
-
if
|
|
441
|
-
|
|
441
|
+
// A published write is durable even if bookkeeping failed: recovery GET
|
|
442
|
+
// must not serve old prices or let a later save undo the committed policy.
|
|
443
|
+
if (!(error instanceof ConfigWritePublishedError)) {
|
|
444
|
+
if (hadModelCosts) provider.modelCosts = previousModelCosts;
|
|
445
|
+
else delete provider.modelCosts;
|
|
446
|
+
}
|
|
442
447
|
throw error;
|
|
443
448
|
}
|
|
444
449
|
return jsonResponse({ ok: true, provider: name, modelId, cost }, 200, req, config);
|
|
@@ -52,6 +52,7 @@ import { reconcileSuccessfulModelDiscoveries } from "../../providers/new-model-p
|
|
|
52
52
|
import { initialModelSelectionPending, pendingModelSelectionProviders } from "../../providers/initial-model-selection";
|
|
53
53
|
import { catalogFastRowEligible, fastRowId } from "../fast-row";
|
|
54
54
|
import { knownEffortRowIds } from "../effort-row";
|
|
55
|
+
import { visionSidecarConsumerClassifier } from "../../vision/eligibility";
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
|
|
@@ -78,6 +79,15 @@ export type ManagementModelRow = Partial<CatalogModel> & {
|
|
|
78
79
|
* one thing and saves another, so its own writes never appear to take.
|
|
79
80
|
*/
|
|
80
81
|
inputModalitiesDeclared?: string[];
|
|
82
|
+
/**
|
|
83
|
+
* The runtime's verdict on whether the Vision Sidecar describes this model's images, from
|
|
84
|
+
* the same predicate the request path consults (legacy `noVisionModels`, registry
|
|
85
|
+
* enrichment, explicit declarations, custom rows). `inputModalitiesDeclared` cannot answer
|
|
86
|
+
* this: a model covered ONLY by legacy `noVisionModels` has no declaration while its
|
|
87
|
+
* advertised modalities are widened to image, so a client inferring consumer status from
|
|
88
|
+
* declared-vs-advertised modalities misclassifies the row after reload.
|
|
89
|
+
*/
|
|
90
|
+
visionSidecarConsumer?: boolean;
|
|
81
91
|
/** Exact stored context override, distinct from the effective catalog window. */
|
|
82
92
|
contextWindowDeclared?: number;
|
|
83
93
|
/**
|
|
@@ -496,6 +506,7 @@ export async function listManagementModelRows(
|
|
|
496
506
|
// a hidden real `x--fast` must never become a synthetic selector for another model.
|
|
497
507
|
const knownIds = config.fastRows === false ? new Set<string>() : knownEffortRowIds(config);
|
|
498
508
|
for (const row of rows) knownIds.add(row.namespaced);
|
|
509
|
+
const isVisionSidecarConsumer = visionSidecarConsumerClassifier(config);
|
|
499
510
|
return rows.map(row => {
|
|
500
511
|
const pending = initialModelSelectionPending(config.providers[row.provider]);
|
|
501
512
|
const modelCosts = Object.hasOwn(config.providers, row.provider)
|
|
@@ -519,6 +530,9 @@ export async function listManagementModelRows(
|
|
|
519
530
|
});
|
|
520
531
|
return {
|
|
521
532
|
...row,
|
|
533
|
+
// One shared definition with the request path: the GUI must not re-derive consumer
|
|
534
|
+
// status from declared-vs-advertised modalities (see the field doc above).
|
|
535
|
+
visionSidecarConsumer: isVisionSidecarConsumer(row.provider, row.id),
|
|
522
536
|
...(!row.native && modelCosts !== undefined && Object.hasOwn(modelCosts, row.id)
|
|
523
537
|
? { manualPricing: true } : {}),
|
|
524
538
|
...(pending ? { disabled: true, initialSelectionPending: true } : {}),
|