@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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 +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
package/src/codex/sync.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { currentExternalCodexModelProvider, injectCodexConfig } from "./inject";
|
|
2
|
+
import { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE, unbackedRoutedRemovalMessage } from "./catalog/routed-removal";
|
|
2
3
|
import { printProjectCodexConfigWarnings, groupProjectCodexConfigWarningsByPath, type ProjectCodexConfigWarning } from "./project-config-warnings";
|
|
3
4
|
import { refreshCodexModelCatalog } from "./refresh";
|
|
4
5
|
import { applyProxyEnv, loadConfig } from "../config";
|
|
@@ -35,8 +36,8 @@ export interface CodexSyncResult {
|
|
|
35
36
|
cacheSynced: boolean;
|
|
36
37
|
/**
|
|
37
38
|
* Whether the catalog owner committed a validated catalog or refused the
|
|
38
|
-
* refresh.
|
|
39
|
-
*
|
|
39
|
+
* refresh. Available on applied and catalog-only results; `ok` on applied
|
|
40
|
+
* describes config injection, so callers must also inspect catalog evidence.
|
|
40
41
|
*/
|
|
41
42
|
refreshOutcome?: "committed" | "refused";
|
|
42
43
|
message: string;
|
|
@@ -262,17 +263,27 @@ export async function syncModelsToCodex(
|
|
|
262
263
|
let cacheSynced = false;
|
|
263
264
|
let warning: string | undefined;
|
|
264
265
|
let comboOmissions: ComboCatalogOmission[] = [];
|
|
266
|
+
let refreshOutcome: "committed" | "refused" | undefined;
|
|
265
267
|
|
|
266
268
|
try {
|
|
267
269
|
const cat = await deps.refreshCodexModelCatalog(config);
|
|
268
270
|
added = cat.added;
|
|
271
|
+
refreshOutcome = cat.refreshOutcome;
|
|
269
272
|
catalogExists = cat.catalogExists;
|
|
270
273
|
catalogWritten = cat.catalogWritten;
|
|
271
274
|
cacheSynced = cat.cacheSynced;
|
|
272
275
|
catalogPathForInjection = cat.catalogExists ? cat.path : null;
|
|
273
276
|
catalogPath = catalogPathForInjection;
|
|
274
277
|
comboOmissions = cat.comboOmissions ?? [];
|
|
275
|
-
if (cat.
|
|
278
|
+
if (cat.skippedReason === "unbacked_routed_removal" || cat.skippedReason === "foreign_owner" || cat.skippedReason === "owner_unknown") {
|
|
279
|
+
// The catalog on disk is kept as it was; say so, or a native-only refresh refused for safety
|
|
280
|
+
// reads exactly like a successful sync (#6529).
|
|
281
|
+
warning = cat.skippedReason === "foreign_owner"
|
|
282
|
+
? FOREIGN_CODEX_HOME_OWNER_MESSAGE
|
|
283
|
+
: cat.skippedReason === "owner_unknown" ? UNKNOWN_CODEX_HOME_OWNER_MESSAGE
|
|
284
|
+
: unbackedRoutedRemovalMessage(cat.protectedRoutedNamespaces ?? 1);
|
|
285
|
+
log?.error(`[opencodex] ${warning}`);
|
|
286
|
+
} else if (cat.added > 0) {
|
|
276
287
|
log?.log(` + ${cat.added} models appended to Codex catalog (${cat.path})`);
|
|
277
288
|
} else if (!cat.catalogExists) {
|
|
278
289
|
warning = "catalog sync skipped: no Codex catalog source found; keeping Codex's native catalog.";
|
|
@@ -322,6 +333,7 @@ export async function syncModelsToCodex(
|
|
|
322
333
|
status: "applied",
|
|
323
334
|
ok: result.success,
|
|
324
335
|
added,
|
|
336
|
+
...(refreshOutcome ? { refreshOutcome } : {}),
|
|
325
337
|
catalogPath,
|
|
326
338
|
catalogExists,
|
|
327
339
|
catalogWritten,
|
|
@@ -370,7 +382,15 @@ async function refreshCatalogForSync(
|
|
|
370
382
|
cacheSynced = cat.cacheSynced;
|
|
371
383
|
catalogPath = cat.catalogExists ? cat.path : null;
|
|
372
384
|
comboOmissions = cat.comboOmissions ?? [];
|
|
373
|
-
if (cat.
|
|
385
|
+
if (cat.skippedReason === "unbacked_routed_removal" || cat.skippedReason === "foreign_owner" || cat.skippedReason === "owner_unknown") {
|
|
386
|
+
// The catalog on disk is kept as it was; say so, or a native-only refresh refused for safety
|
|
387
|
+
// reads exactly like a successful sync (#6529).
|
|
388
|
+
warning = cat.skippedReason === "foreign_owner"
|
|
389
|
+
? FOREIGN_CODEX_HOME_OWNER_MESSAGE
|
|
390
|
+
: cat.skippedReason === "owner_unknown" ? UNKNOWN_CODEX_HOME_OWNER_MESSAGE
|
|
391
|
+
: unbackedRoutedRemovalMessage(cat.protectedRoutedNamespaces ?? 1);
|
|
392
|
+
log?.error(`[opencodex] ${warning}`);
|
|
393
|
+
} else if (cat.added > 0) {
|
|
374
394
|
log?.log(` + ${cat.added} models appended to Codex catalog (${cat.path})`);
|
|
375
395
|
} else if (!cat.catalogExists) {
|
|
376
396
|
warning = "catalog sync skipped: no Codex catalog source found; keeping Codex's native catalog.";
|
package/src/combos/failover.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
interface TargetCooldown {
|
|
13
13
|
cooldownUntil: number;
|
|
14
|
+
status?: number;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
@@ -195,6 +196,23 @@ export function remainingComboCooldownMs(comboId: string, now = Date.now()): num
|
|
|
195
196
|
return soonest;
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
/** Snapshot active quota cooldowns without inspecting request eligibility or changing state. */
|
|
200
|
+
export function snapshotComboQuotaCooldowns<T extends Pick<OcxComboTarget, "provider" | "model">>(
|
|
201
|
+
comboId: string,
|
|
202
|
+
targets: Iterable<T>,
|
|
203
|
+
now = Date.now(),
|
|
204
|
+
): Array<{ target: T; cooldownUntil: number }> {
|
|
205
|
+
const snapshot: Array<{ target: T; cooldownUntil: number }> = [];
|
|
206
|
+
for (const target of targets) {
|
|
207
|
+
const key = cooldownMapKey(comboId, target);
|
|
208
|
+
const cooldown = targetCooldowns.get(key);
|
|
209
|
+
if (!cooldown || cooldown.cooldownUntil <= now) continue;
|
|
210
|
+
if (cooldown.status !== 429 && cooldown.status !== 402) continue;
|
|
211
|
+
snapshot.push({ target, cooldownUntil: cooldown.cooldownUntil });
|
|
212
|
+
}
|
|
213
|
+
return snapshot;
|
|
214
|
+
}
|
|
215
|
+
|
|
198
216
|
export function comboCooldownRetryAfterSeconds(comboId: string, now = Date.now()): string | undefined {
|
|
199
217
|
const remainingMs = remainingComboCooldownMs(comboId, now);
|
|
200
218
|
if (remainingMs === undefined) return undefined;
|
|
@@ -244,6 +262,7 @@ export function coolComboTarget(
|
|
|
244
262
|
targetCooldowns.set(cooldownMapKey(comboId, target), {
|
|
245
263
|
// Local fallbacks are capped at ten minutes; explicit server delays at one day.
|
|
246
264
|
cooldownUntil: now + (serverDelayMs ?? Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS)),
|
|
265
|
+
status: options?.status,
|
|
247
266
|
});
|
|
248
267
|
sweepExpiredOnWrite(now);
|
|
249
268
|
return true;
|
|
@@ -670,10 +689,84 @@ function isDefiniteContextOverflow(status: number, message: string): boolean {
|
|
|
670
689
|
return false;
|
|
671
690
|
}
|
|
672
691
|
|
|
692
|
+
const CODEX_ACCOUNT_MODEL_REFUSAL = /^The '[^']{1,256}' model is not supported when using Codex with a ChatGPT account\.$/;
|
|
693
|
+
|
|
694
|
+
export type CodexAccountModelRefusal = "other" | "refusal" | "ambiguous";
|
|
695
|
+
|
|
696
|
+
/** Inspect only the original root and its own response record, before carrier selection. */
|
|
697
|
+
export function hasConflictingCodexModelRefusalEnvelopes(status: number, payload: unknown): boolean {
|
|
698
|
+
if (status !== 400 || !payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
699
|
+
const response = Object.hasOwn(payload, "response") ? (payload as Record<string, unknown>).response : undefined;
|
|
700
|
+
const nested = response && typeof response === "object" && !Array.isArray(response) ? response : undefined;
|
|
701
|
+
return (Object.hasOwn(payload, "detail") || !!nested && Object.hasOwn(nested, "detail"))
|
|
702
|
+
&& (Object.hasOwn(payload, "error") || !!nested && Object.hasOwn(nested, "error"));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Inspect an already parsed, bounded error frame without copying or walking its body. */
|
|
706
|
+
export function codexAccountModelRefusalPayload(status: number, payload: unknown): CodexAccountModelRefusal {
|
|
707
|
+
if (status !== 400 || !payload || typeof payload !== "object" || Array.isArray(payload)) return "other";
|
|
708
|
+
if (Object.hasOwn(payload, "detail") && Object.hasOwn(payload, "error")) return "ambiguous";
|
|
709
|
+
const { detail, error } = payload as { detail?: unknown; error?: unknown };
|
|
710
|
+
const field = typeof detail === "string" ? detail
|
|
711
|
+
: error && typeof error === "object" && !Array.isArray(error)
|
|
712
|
+
&& typeof (error as Record<string, unknown>).message === "string"
|
|
713
|
+
? (error as { message: string }).message : undefined;
|
|
714
|
+
return field !== undefined && CODEX_ACCOUNT_MODEL_REFUSAL.test(field) ? "refusal" : "other";
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** One complete, bounded HTTP envelope; accept only a single exact status prefix. */
|
|
718
|
+
function parseCodexRefusalEnvelope(status: number, message: string): { text: string; payload: unknown } | undefined {
|
|
719
|
+
if (status !== 400 || message.length > 16_384) return undefined;
|
|
720
|
+
let text = message.trim();
|
|
721
|
+
if (text.startsWith("Provider error 400: ")) text = text.slice("Provider error 400: ".length);
|
|
722
|
+
let payload: unknown;
|
|
723
|
+
try { payload = JSON.parse(text); } catch { /* Bare refusal text is also a supported carrier. */ }
|
|
724
|
+
return { text, payload };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Only allowlisted hard stops cross the same HTTP boundary as positive refusal evidence. */
|
|
728
|
+
export function codexAccountModelRefusalHardStopCode(status: number, message: string): string | undefined {
|
|
729
|
+
const payload = parseCodexRefusalEnvelope(status, message)?.payload;
|
|
730
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
731
|
+
const root = payload as Record<string, unknown>;
|
|
732
|
+
const response = Object.hasOwn(root, "response") ? root.response : undefined;
|
|
733
|
+
const records = response && typeof response === "object" && !Array.isArray(response)
|
|
734
|
+
? [root, response as Record<string, unknown>] : [root];
|
|
735
|
+
for (const record of records) {
|
|
736
|
+
const error = record.error && typeof record.error === "object" && !Array.isArray(record.error)
|
|
737
|
+
? record.error as Record<string, unknown> : undefined;
|
|
738
|
+
// Diagnostic type is never code evidence, even when a code is absent or malformed.
|
|
739
|
+
for (const code of [error?.code, record.code]) {
|
|
740
|
+
if (typeof code !== "string") continue;
|
|
741
|
+
if (isNonReplayableUpstreamCode(code) || isCyberPolicyCode(code)) return code;
|
|
742
|
+
if (normalizedFailureCode(code) === "origin_rejected") return "origin_rejected";
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** A bounded fallback signal, never credential or account-entitlement evidence. */
|
|
749
|
+
export function codexAccountModelRefusal(
|
|
750
|
+
status: number, message: string, options?: { allowNestedResponse?: boolean },
|
|
751
|
+
): CodexAccountModelRefusal {
|
|
752
|
+
const envelope = parseCodexRefusalEnvelope(status, message);
|
|
753
|
+
if (!envelope) return "other";
|
|
754
|
+
if (CODEX_ACCOUNT_MODEL_REFUSAL.test(envelope.text)) return "refusal";
|
|
755
|
+
const payload = envelope.payload;
|
|
756
|
+
if (hasConflictingCodexModelRefusalEnvelopes(status, payload)) return "ambiguous";
|
|
757
|
+
// A present root carrier stays authoritative, even when malformed or nonmatching.
|
|
758
|
+
if (options?.allowNestedResponse !== false && payload && typeof payload === "object" && !Array.isArray(payload)
|
|
759
|
+
&& !Object.hasOwn(payload, "detail") && !Object.hasOwn(payload, "error")
|
|
760
|
+
&& Object.hasOwn(payload, "response")) {
|
|
761
|
+
return codexAccountModelRefusalPayload(status, (payload as Record<string, unknown>).response);
|
|
762
|
+
}
|
|
763
|
+
return codexAccountModelRefusalPayload(status, payload);
|
|
764
|
+
}
|
|
765
|
+
|
|
673
766
|
export function comboFailureDecision(
|
|
674
767
|
status: number,
|
|
675
768
|
message: string,
|
|
676
|
-
options?: { code?: string | null },
|
|
769
|
+
options?: { code?: string | null; codexModelRefusal?: CodexAccountModelRefusal },
|
|
677
770
|
): ComboFailureDecision {
|
|
678
771
|
if (status === 499) return "stop";
|
|
679
772
|
if (message.toLowerCase().includes("origin_rejected")) return "stop";
|
|
@@ -688,6 +781,12 @@ export function comboFailureDecision(
|
|
|
688
781
|
// Cyber policy is a hard non-retryable refusal — honor structured code even when
|
|
689
782
|
// classificationText was truncated before the JSON code field.
|
|
690
783
|
if (isCyberPolicyCode(options?.code)) return "stop";
|
|
784
|
+
// HTTP consumers retain hard codes before truncation; SSE metadata owns its selected carrier.
|
|
785
|
+
if (options?.codexModelRefusal === undefined && codexAccountModelRefusalHardStopCode(status, message)) return "stop";
|
|
786
|
+
const modelRefusal = status === 400
|
|
787
|
+
? options?.codexModelRefusal ?? codexAccountModelRefusal(status, message) : "other";
|
|
788
|
+
// Competing envelopes cannot grant a hop through an earlier structured-code rule either.
|
|
789
|
+
if (modelRefusal === "ambiguous") return "stop";
|
|
691
790
|
// HTTP 410 is normally terminal. A model-specific lifecycle verdict is target-local,
|
|
692
791
|
// however: another provider/model in the declared combo can still serve the request.
|
|
693
792
|
// Require structured lifecycle code or explicit model+lifecycle prose so unrelated
|
|
@@ -738,6 +837,7 @@ export function comboFailureDecision(
|
|
|
738
837
|
if (["model_not_found", "model_unavailable", "unsupported_model"].includes(failureCode)) {
|
|
739
838
|
return "hop";
|
|
740
839
|
}
|
|
840
|
+
if (modelRefusal === "refusal") return "hop";
|
|
741
841
|
// `free_rate_limited` no longer routes through `isProviderScopedQuotaCap` (it is a
|
|
742
842
|
// per-request cap, not provider-wide evidence), so keep its hop verdict explicit here.
|
|
743
843
|
if (failureCode === "free_rate_limited") return "hop";
|
package/src/combos/index.ts
CHANGED
package/src/combos/jev.ts
CHANGED
|
@@ -127,6 +127,8 @@ export interface ResolveJevDecisionOptions {
|
|
|
127
127
|
* any other id names a configured `jev-decision` row (for example a self-hosted Ollama `tev1`).
|
|
128
128
|
*/
|
|
129
129
|
decisionProvider?: string;
|
|
130
|
+
/** Request-local authorization of the concrete decision destination, before credential access. */
|
|
131
|
+
isDestinationAllowed?: (providerName: string, modelId: string) => boolean;
|
|
130
132
|
/** Decision deadline; values outside 1000..120000 ms keep the four-second default. */
|
|
131
133
|
timeoutMs?: number;
|
|
132
134
|
signal?: AbortSignal;
|
|
@@ -674,14 +676,20 @@ function selfHostedApiKey(name: string, apiKey: string | undefined): string | un
|
|
|
674
676
|
* custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
|
|
675
677
|
* `/systemone` endpoint and which names its own model; only its own key may accompany it, so no
|
|
676
678
|
* TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
|
|
677
|
-
* service (reported through the existing `missing_key` gate)
|
|
679
|
+
* service (reported through the existing `missing_key` gate); `null` means the request's
|
|
680
|
+
* destination scope refused it before any credential access.
|
|
678
681
|
*/
|
|
679
|
-
function jevDecisionEndpoint(
|
|
682
|
+
function jevDecisionEndpoint(
|
|
683
|
+
config: OcxConfig,
|
|
684
|
+
decisionProvider: string,
|
|
685
|
+
isDestinationAllowed?: ResolveJevDecisionOptions["isDestinationAllowed"],
|
|
686
|
+
): JevDecisionEndpoint | undefined | null {
|
|
680
687
|
const configured = Object.hasOwn(config.providers, decisionProvider)
|
|
681
688
|
? config.providers[decisionProvider]
|
|
682
689
|
: undefined;
|
|
683
690
|
if (configured?.disabled === true) return undefined;
|
|
684
691
|
if (decisionProvider === JEV_PROVIDER_ID) {
|
|
692
|
+
if (isDestinationAllowed?.(JEV_PROVIDER_ID, JEV_MODEL) === false) return null;
|
|
685
693
|
const configuredOwnsJev = configured
|
|
686
694
|
&& providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
|
|
687
695
|
const apiKey = (
|
|
@@ -704,6 +712,7 @@ function jevDecisionEndpoint(config: OcxConfig, decisionProvider: string): JevDe
|
|
|
704
712
|
// `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
|
|
705
713
|
const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
|
|
706
714
|
if (!model) return undefined;
|
|
715
|
+
if (isDestinationAllowed?.(decisionProvider, model) === false) return null;
|
|
707
716
|
const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
|
|
708
717
|
if (apiKey === null) return undefined;
|
|
709
718
|
return {
|
|
@@ -733,7 +742,8 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
|
|
|
733
742
|
if (options.candidates.length === 0) return failed("no_choices");
|
|
734
743
|
if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
|
|
735
744
|
|
|
736
|
-
const endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID);
|
|
745
|
+
const endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID, options.isDestinationAllowed);
|
|
746
|
+
if (endpoint === null) return failed("invalid");
|
|
737
747
|
if (!endpoint) return failed("missing_key");
|
|
738
748
|
|
|
739
749
|
let requestBody: string;
|
package/src/combos/resolve.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
coolComboTarget,
|
|
7
7
|
earliestComboCooldown,
|
|
8
8
|
isComboTargetInCooldown,
|
|
9
|
+
snapshotComboQuotaCooldowns as snapshotTargetQuotaCooldowns,
|
|
9
10
|
type ComboFailureCooldownScope,
|
|
10
11
|
} from "./failover";
|
|
11
12
|
import { quotaResetRemainingMs } from "./reset-window";
|
|
@@ -67,6 +68,17 @@ function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now:
|
|
|
67
68
|
return !cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now, target.model);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Capture usable providers' quota cooldowns; request eligibility is deferred to exhaustion. */
|
|
72
|
+
export function snapshotComboQuotaCooldowns(
|
|
73
|
+
config: OcxConfig,
|
|
74
|
+
comboId: string,
|
|
75
|
+
now = Date.now(),
|
|
76
|
+
): Array<{ target: NormalizedComboTarget; cooldownUntil: number }> {
|
|
77
|
+
const targets = getCombo(config, comboId)?.targets.filter(target =>
|
|
78
|
+
targetProviderIsUsable(config, target, now)) ?? [];
|
|
79
|
+
return snapshotTargetQuotaCooldowns(comboId, targets, now);
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
function quotaWindowExhausted(percent: number | undefined, resetAt: number | undefined, now: number): boolean {
|
|
71
83
|
if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 100) return false;
|
|
72
84
|
return typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt > now;
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { basename, dirname, join } from "node:path";
|
|
15
15
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
16
|
-
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
16
|
+
import { assertNotRealCodexHomeUnderTest, assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
17
17
|
import {
|
|
18
18
|
forgetEphemeralSecretPath,
|
|
19
19
|
hardenSecretPath,
|
|
@@ -114,14 +114,19 @@ export function resolveWriteTarget(path: string): string {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function assertResolvedTargetAllowed(path: string, target: string): void {
|
|
117
|
+
// Codex-home files (catalog, models cache, journal, config.toml, profile) are addressed through
|
|
118
|
+
// CODEX_HOME, which falls back to os.homedir() when a test's teardown deletes it, and os.homedir()
|
|
119
|
+
// ignores HOME. That is how test runs rewrote the real catalog (#6529), so every atomic write
|
|
120
|
+
// whose directory is the real Codex home is refused under an armed test process.
|
|
121
|
+
let realParent: string | null = null;
|
|
122
|
+
try {
|
|
123
|
+
realParent = realpathSync(dirname(target));
|
|
124
|
+
} catch {
|
|
125
|
+
realParent = null;
|
|
126
|
+
}
|
|
127
|
+
assertNotRealCodexHomeUnderTest(realParent ?? dirname(target));
|
|
117
128
|
if (target === path) {
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
realParent = realpathSync(dirname(target));
|
|
121
|
-
} catch {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (realParent !== dirname(target)) assertNotRealHomeUnderTest(realParent);
|
|
129
|
+
if (realParent !== null && realParent !== dirname(target)) assertNotRealHomeUnderTest(realParent);
|
|
125
130
|
return;
|
|
126
131
|
}
|
|
127
132
|
assertNotRealHomeUnderTest(dirname(target));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
|
|
2
|
+
import { protocolConfigSchema } from "./schema/config-schema";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
2
4
|
import { lstatSync, readFileSync } from "node:fs";
|
|
3
5
|
import { join } from "node:path";
|
|
@@ -65,6 +67,7 @@ import {
|
|
|
65
67
|
runtimeRoleSchema,
|
|
66
68
|
spendSchema,
|
|
67
69
|
chatgptDesktopSchema,
|
|
70
|
+
chatgptDesktopConfigIssue,
|
|
68
71
|
compactionRoutingSchema,
|
|
69
72
|
skillsConfigSchema,
|
|
70
73
|
memoryModelsSchema,
|
|
@@ -100,12 +103,16 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
|
|
|
100
103
|
// the entire config, which would hide unrelated providers/accounts. The next
|
|
101
104
|
// ordinary save persists the normalized absence.
|
|
102
105
|
const syncDisabledReason = nativeSubagentSyncDisabledReason(config, rawParsed);
|
|
106
|
+
const rawForce = rawSubagentModelForce(rawParsed);
|
|
103
107
|
const rawEffort = rawClaudeSubagentEffort(rawParsed);
|
|
104
108
|
const normalized = normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, rawParsed), rawParsed);
|
|
105
109
|
const warnings = configPlaceholderWarnings(normalized);
|
|
110
|
+
if (rawForce !== undefined && !isSubagentModelEntry(rawForce)) warnings.push("claudeCode.subagentModelForce ignored: expected a safe roster-style model id");
|
|
106
111
|
if (normalized.chatgptDesktop?.appServerShim === true && process.platform !== "darwin") {
|
|
107
112
|
warnings.push("chatgptDesktop.appServerShim is experimental and macOS only; ignored on this platform");
|
|
108
113
|
}
|
|
114
|
+
const chatgptDesktopIssue = chatgptDesktopConfigIssue(rawParsed);
|
|
115
|
+
if (chatgptDesktopIssue) warnings.push(`${chatgptDesktopIssue}; the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
|
|
109
116
|
warnings.push(...inheritedFastWireConflictProviderNames(normalized).map(inheritedFastWireConflictWarning));
|
|
110
117
|
warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
|
|
111
118
|
warnings.push(...degradedListenerWarnings(rawParsed, normalized));
|
|
@@ -644,7 +651,19 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
644
651
|
if (memoryModels !== undefined && !memoryModelsSchema.safeParse(memoryModels).success) {
|
|
645
652
|
return { ok: false, error: "schema_invalid: memoryModels: requires a nonblank model and an optional declared reasoningEffort per configured phase, and no other fields" };
|
|
646
653
|
}
|
|
647
|
-
const
|
|
654
|
+
const protocols = rawConfigRecord(value)?.protocols;
|
|
655
|
+
if (protocols !== undefined && !protocolConfigSchema.safeParse(protocols).success) {
|
|
656
|
+
return { ok: false, error: "schema_invalid: protocols: expected valid protocol policy and boolean rollout flags" };
|
|
657
|
+
}
|
|
658
|
+
const rawAnthropicPool = rawConfigRecord(value)?.anthropicAccountPool;
|
|
659
|
+
const anthropicPool = rawConfigRecord(rawAnthropicPool);
|
|
660
|
+
if (rawAnthropicPool !== undefined && !anthropicPool) {
|
|
661
|
+
return { ok: false, error: "schema_invalid: anthropicAccountPool: must be an object" };
|
|
662
|
+
}
|
|
663
|
+
if (anthropicPool && Object.hasOwn(anthropicPool, "nativeMessages") && typeof anthropicPool.nativeMessages !== "boolean") {
|
|
664
|
+
return { ok: false, error: "schema_invalid: anthropicAccountPool.nativeMessages: must be a boolean" };
|
|
665
|
+
}
|
|
666
|
+
const routeValue = anthropicPool?.routes;
|
|
648
667
|
if (routeValue !== undefined) {
|
|
649
668
|
const parsed = parseAnthropicModelRoutes(routeValue);
|
|
650
669
|
if (!parsed.ok) return { ok: false, error: `schema_invalid: anthropicAccountPool.routes: ${parsed.error}` };
|
|
@@ -652,6 +671,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
652
671
|
const boundaryError = blockedModelRedirectsError(value)
|
|
653
672
|
?? compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
|
|
654
673
|
?? blankHostnameError(value)
|
|
674
|
+
?? (rawSubagentModelForce(value) !== undefined && !isSubagentModelEntry(rawSubagentModelForce(value)) ? "schema_invalid: claudeCode.subagentModelForce: expected a safe roster-style model id" : null)
|
|
655
675
|
?? claudeSubagentEffortError(value)
|
|
656
676
|
?? appOwnedMemoryBudgetError(value)
|
|
657
677
|
?? upstreamHostCircuitThresholdError(value)
|
|
@@ -726,6 +746,8 @@ export function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
|
|
|
726
746
|
if (salvaged) {
|
|
727
747
|
const config = normalizeApiKeyIds(salvaged.parsed);
|
|
728
748
|
const warnings = degradedListenerWarnings(parsed, config);
|
|
749
|
+
const chatgptDesktopIssue = chatgptDesktopConfigIssue(parsed);
|
|
750
|
+
if (chatgptDesktopIssue) warnings.push(`${chatgptDesktopIssue}; the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
|
|
729
751
|
return {
|
|
730
752
|
config,
|
|
731
753
|
source: "fallback",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
|
|
1
2
|
import { chmodSync, existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
transientRetryOn5xxPolicySchema,
|
|
41
42
|
runtimeRoleSchema,
|
|
42
43
|
spendSchema,
|
|
44
|
+
chatgptDesktopConfigIssue,
|
|
43
45
|
} from "./schema/leaf-validators";
|
|
44
46
|
import { hasWarnedInheritedFastWireConflict, markWarnedInheritedFastWireConflict } from "./warn-memo";
|
|
45
47
|
|
|
@@ -123,6 +125,8 @@ export function warnDegradedCompactionRouting(rawParsed: unknown, validated: Ocx
|
|
|
123
125
|
export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxConfig): void {
|
|
124
126
|
if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
|
|
125
127
|
if (blockedModelRedirectsError(rawParsed)) console.warn("⚠️ invalid blockedModelRedirects ignored; provider routing remains available");
|
|
128
|
+
const chatgptDesktop = chatgptDesktopConfigIssue(rawParsed);
|
|
129
|
+
if (chatgptDesktop) console.warn(`⚠️ config.json ${chatgptDesktop} — the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
|
|
126
130
|
warnDegradedStreamMode(rawParsed, validated);
|
|
127
131
|
warnDegradedCompactionRouting(rawParsed, validated);
|
|
128
132
|
warnDegradedMemoryModels(rawParsed, validated);
|
|
@@ -592,6 +596,7 @@ export function normalizePersistedClaudeCode(claudeCode: unknown): OcxConfig["cl
|
|
|
592
596
|
return claudeCode as OcxConfig["claudeCode"];
|
|
593
597
|
}
|
|
594
598
|
const normalized = { ...claudeCode } as Record<string, unknown>;
|
|
599
|
+
if (Object.hasOwn(normalized, "subagentModelForce") && !isSubagentModelEntry(normalized.subagentModelForce)) delete normalized.subagentModelForce;
|
|
595
600
|
// A malformed hand edit must not arm CLI interception or discard the whole config.
|
|
596
601
|
if (Object.hasOwn(normalized, "cliFirstParty") && typeof normalized.cliFirstParty !== "boolean") {
|
|
597
602
|
delete normalized.cliFirstParty;
|
|
@@ -634,6 +639,8 @@ export function normalizeClaudeSubagentEffort(config: OcxConfig, _rawParsed: unk
|
|
|
634
639
|
}
|
|
635
640
|
|
|
636
641
|
export function warnDegradedClaudeSubagentEffort(rawParsed: unknown): void {
|
|
642
|
+
const force = rawSubagentModelForce(rawParsed);
|
|
643
|
+
if (force !== undefined && !isSubagentModelEntry(force)) console.warn("⚠️ config.json claudeCode.subagentModelForce is invalid — ignoring it. Other settings were preserved.");
|
|
637
644
|
const rawEffort = rawClaudeSubagentEffort(rawParsed);
|
|
638
645
|
if (rawEffort !== undefined && !isClaudeSubagentEffort(rawEffort)) {
|
|
639
646
|
console.warn(`⚠️ config.json claudeCode.subagentEffort is invalid (expected ${CLAUDE_SUBAGENT_EFFORTS.join(", ")}) — ignoring it. Other settings were preserved.`);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../providers/model-discovery-limits";
|
|
7
7
|
import { isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
8
8
|
import { encodeRoutedModelId } from "../providers/slug-codec";
|
|
9
|
+
import { providerForwardClientHeadersConfigError } from "../lib/provider-client-headers";
|
|
9
10
|
import {
|
|
10
11
|
isWirePinnedModel,
|
|
11
12
|
MODEL_ADAPTER_OVERRIDE_ALLOWED,
|
|
@@ -125,6 +126,8 @@ export function providerHeadersConfigError(headers: unknown): string | null {
|
|
|
125
126
|
return null;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
export { providerForwardClientHeadersConfigError };
|
|
130
|
+
|
|
128
131
|
/** Keep the configured API-key header style scoped to Anthropic-compatible key auth. */
|
|
129
132
|
export function apiKeyTransportConfigError(
|
|
130
133
|
provider: Pick<OcxProviderConfig, "adapter" | "authMode" | "apiKeyTransport">,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { redactSecretString } from "../../lib/redact";
|
|
3
|
+
|
|
4
|
+
/** Experimental macOS ChatGPT app-server shim; only a strict optional boolean is accepted. */
|
|
5
|
+
export const chatgptDesktopSchema = z.object({ appServerShim: z.boolean().optional() }).strict();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Why a present `chatgptDesktop` block fails the schema, or null when it is valid or absent. The
|
|
9
|
+
* read path degrades a failing block to absent, so the integration reads as off while the file
|
|
10
|
+
* still says `appServerShim: true`; this names the field so that is never silent. A leftover key
|
|
11
|
+
* from an older or ported config (for example `unblockSend`) is the usual cause.
|
|
12
|
+
*/
|
|
13
|
+
export function chatgptDesktopConfigIssue(rawParsed: unknown): string | null {
|
|
14
|
+
if (!rawParsed || typeof rawParsed !== "object" || Array.isArray(rawParsed)) return null;
|
|
15
|
+
const block = (rawParsed as Record<string, unknown>).chatgptDesktop;
|
|
16
|
+
if (block === undefined) return null;
|
|
17
|
+
const result = chatgptDesktopSchema.safeParse(block);
|
|
18
|
+
if (result.success) return null;
|
|
19
|
+
const issue = result.error.issues[0];
|
|
20
|
+
const field = issue?.path.join(".");
|
|
21
|
+
return redactSecretString(`chatgptDesktop${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`);
|
|
22
|
+
}
|
|
@@ -9,3 +9,11 @@
|
|
|
9
9
|
* with "Cannot access 'runtimeRoleSchema' before initialization".
|
|
10
10
|
*/
|
|
11
11
|
export const COMPACTION_TRIGGERS = ["manual", "auto"] as const;
|
|
12
|
+
|
|
13
|
+
/** Exact selectors or a provider-qualified trailing wildcard; never a global wildcard. */
|
|
14
|
+
export function validCompactionSourceModels(value: unknown): value is string[] {
|
|
15
|
+
return Array.isArray(value) && value.length > 0 && new Set(value).size === value.length
|
|
16
|
+
&& value.every(entry => typeof entry === "string" && entry.length > 0
|
|
17
|
+
&& entry === entry.trim() && !/\s/.test(entry)
|
|
18
|
+
&& (!entry.includes("*") || /^[^/*]+\/\*$/.test(entry)));
|
|
19
|
+
}
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
positiveIntegerRecordConfigError,
|
|
46
46
|
providerBaseUrlConfigError,
|
|
47
47
|
providerHeadersConfigError,
|
|
48
|
+
providerForwardClientHeadersConfigError,
|
|
48
49
|
reasoningSummaryDeliveryRecordConfigError,
|
|
49
50
|
} from "../provider-validation";
|
|
50
51
|
import {
|
|
@@ -73,6 +74,18 @@ import { parseDesktopProfile } from "../../claude/desktop-profile";
|
|
|
73
74
|
import { isInterceptBindingId, isInterceptBindingRoute } from "../../claude/intercept/model-bindings";
|
|
74
75
|
import { DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES, MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../../lib/app-owned-memory";
|
|
75
76
|
|
|
77
|
+
/** Strict write contract; file-load recovery is applied only by the enclosing schema. */
|
|
78
|
+
export const protocolConfigSchema = z.object({
|
|
79
|
+
unrepresentable: z.enum(["legacy", "reject"]).optional(),
|
|
80
|
+
rollout: z.object({
|
|
81
|
+
nativeChatCombos: z.boolean().optional(),
|
|
82
|
+
managedMessagesNative: z.boolean().optional(),
|
|
83
|
+
managedMessagesNativeOAuth: z.boolean().optional(),
|
|
84
|
+
directEncoders: z.boolean().optional(),
|
|
85
|
+
shadowPlan: z.boolean().optional(),
|
|
86
|
+
}).strict().optional(),
|
|
87
|
+
}).strict();
|
|
88
|
+
|
|
76
89
|
export const configSchema = z.object({
|
|
77
90
|
chatgptDesktop: chatgptDesktopSchema.optional().catch(undefined),
|
|
78
91
|
codexNativeSteering: z.boolean().optional().catch(false),
|
|
@@ -95,18 +108,23 @@ export const configSchema = z.object({
|
|
|
95
108
|
// which can reopen a surface the operator meant to close. src/protocols/settings.ts parses it
|
|
96
109
|
// and fails closed instead.
|
|
97
110
|
apiSurfaces: z.unknown().optional(),
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
rollout
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
// Keep malformed native policy disabled even when an enabled pool supplies defaults.
|
|
112
|
+
protocols: protocolConfigSchema.optional().catch(ctx => {
|
|
113
|
+
const raw = ctx.input;
|
|
114
|
+
const protocols = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
115
|
+
const rollout = protocols.rollout;
|
|
116
|
+
const fields = rollout && typeof rollout === "object" && !Array.isArray(rollout) ? rollout as Record<string, unknown> : {};
|
|
117
|
+
return {
|
|
118
|
+
unrepresentable: protocols.unrepresentable === "reject" ? "reject" as const : "legacy" as const,
|
|
119
|
+
rollout: {
|
|
120
|
+
nativeChatCombos: fields.nativeChatCombos === true,
|
|
121
|
+
managedMessagesNative: false,
|
|
122
|
+
managedMessagesNativeOAuth: false,
|
|
123
|
+
directEncoders: fields.directEncoders === true,
|
|
124
|
+
shadowPlan: fields.shadowPlan === true,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}),
|
|
110
128
|
// A malformed present client block must remain diagnosable from raw config and
|
|
111
129
|
// fail closed through src/client/state.ts; unrelated provider state still loads.
|
|
112
130
|
client: clientConnectionSchema.optional().catch(undefined),
|
|
@@ -307,8 +325,9 @@ export const configSchema = z.object({
|
|
|
307
325
|
// path below and wipe providers/pool accounts. Warning emitted in loadConfig.
|
|
308
326
|
streamMode: z.enum(["auto", "legacy-tee", "eager-relay"]).optional().catch(undefined),
|
|
309
327
|
blockedModelRedirects: blockedModelRedirectsSchema.optional().catch(undefined),
|
|
310
|
-
//
|
|
311
|
-
|
|
328
|
+
// Degrade malformed hand edits locally; candidate writes reject them before parsing.
|
|
329
|
+
// An invalid native preference retains the legacy route instead of enabling native by default.
|
|
330
|
+
anthropicAccountPool: z.object({ nativeMessages: z.boolean().optional().catch(false) }).passthrough().optional().catch(undefined),
|
|
312
331
|
// Same degrade-don't-reject rationale as the fields above: a hand-edited
|
|
313
332
|
// non-string must not trip the backup-and-defaults repair path. Unset then
|
|
314
333
|
// takes the canonical sideband path (src/server/live.ts normalizeSidebandRoot).
|
|
@@ -506,6 +525,16 @@ export const configSchema = z.object({
|
|
|
506
525
|
message: headersError,
|
|
507
526
|
});
|
|
508
527
|
}
|
|
528
|
+
const forwardClientHeadersError = providerForwardClientHeadersConfigError(
|
|
529
|
+
(provider as { forwardClientHeaders?: unknown }).forwardClientHeaders,
|
|
530
|
+
);
|
|
531
|
+
if (forwardClientHeadersError) {
|
|
532
|
+
ctx.addIssue({
|
|
533
|
+
code: "custom",
|
|
534
|
+
path: ["providers", redactSecretString(name), "forwardClientHeaders"],
|
|
535
|
+
message: forwardClientHeadersError,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
509
538
|
const modelCostsError = providerModelCostsConfigError((provider as { modelCosts?: unknown }).modelCosts);
|
|
510
539
|
if (modelCostsError) {
|
|
511
540
|
ctx.addIssue({
|
|
@@ -41,10 +41,11 @@ import {
|
|
|
41
41
|
isHostedToolUnsupportedForModel,
|
|
42
42
|
} from "../../responses/hosted-tool-policy";
|
|
43
43
|
import { getConfigDir } from "../paths";
|
|
44
|
-
import { COMPACTION_TRIGGERS } from "./compaction-triggers";
|
|
44
|
+
import { COMPACTION_TRIGGERS, validCompactionSourceModels } from "./compaction-triggers";
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// The chatgptDesktop leaf lives in its own zod-only module so the `ocx chatgpt` command can explain
|
|
47
|
+
// a dropped block without loading this file's provider and account validators.
|
|
48
|
+
export { chatgptDesktopConfigIssue, chatgptDesktopSchema } from "./chatgpt-desktop";
|
|
48
49
|
|
|
49
50
|
/** One definition of "usable secret", shared by the schema and the warnings. */
|
|
50
51
|
export function isUsableApiKeySecret(value: unknown): value is string {
|
|
@@ -53,6 +54,8 @@ export function isUsableApiKeySecret(value: unknown): value is string {
|
|
|
53
54
|
|
|
54
55
|
export const compactionRoutingSchema = z.object({
|
|
55
56
|
model: z.string().trim().min(1),
|
|
57
|
+
sourceModels: z.array(z.string()).refine(validCompactionSourceModels,
|
|
58
|
+
"sourceModels requires unique exact selectors or provider/* patterns").optional(),
|
|
56
59
|
reasoningEffort: z.string().refine(value => pinnedReasoningEffortConfigError(value) === null).optional(),
|
|
57
60
|
triggers: z.array(z.enum(COMPACTION_TRIGGERS)).nonempty()
|
|
58
61
|
.refine(values => new Set(values).size === values.length, "triggers must not repeat a value")
|
|
@@ -327,6 +330,8 @@ export const providerConfigSchema = z.object({
|
|
|
327
330
|
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
|
|
328
331
|
modelSuppressSyntheticMax: z.record(z.string().min(1), z.boolean()).optional(),
|
|
329
332
|
preserveResponsesReasoningContent: z.boolean().optional(),
|
|
333
|
+
preserveResponsesInputItemIds: z.boolean().optional(),
|
|
334
|
+
preserveResponsesMessageMetadata: z.boolean().optional(),
|
|
330
335
|
dropResponsesReasoningItems: z.boolean().optional(),
|
|
331
336
|
modelReasoningEffortsAuthoritative: z.boolean().optional(),
|
|
332
337
|
decodesNativeCompactionBlobs: z.boolean().optional(),
|
|
@@ -75,3 +75,15 @@ export function migrateSubagentModels(config: OcxConfig): boolean {
|
|
|
75
75
|
config.subagentModelsVersion = SUBAGENT_MODELS_VERSION;
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
export const SAFE_AGENT_MODEL_ID = /^[a-z0-9][a-z0-9._:/@+\[\]~-]*$/i;
|
|
80
|
+
|
|
81
|
+
export function isSubagentModelEntry(value: unknown): value is string {
|
|
82
|
+
return typeof value === "string" && SAFE_AGENT_MODEL_ID.test(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function rawSubagentModelForce(value: unknown): unknown {
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
87
|
+
const cc = (value as Record<string, unknown>).claudeCode;
|
|
88
|
+
return cc && typeof cc === "object" && !Array.isArray(cc) ? (cc as Record<string, unknown>).subagentModelForce : undefined;
|
|
89
|
+
}
|