@bitkyc08/opencodex 2.58.0 → 2.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
malformedQuotaResetNotifyWarning,
|
|
35
35
|
malformedCatalogAutoRefreshWarning,
|
|
36
36
|
malformedCodexPoolWarning,
|
|
37
|
+
malformedSpendWarning,
|
|
37
38
|
rawConfigRecord,
|
|
38
39
|
malformedNativeSubagentFields,
|
|
39
40
|
malformedNativeSubagentFieldWarning,
|
|
@@ -58,6 +59,8 @@ import {
|
|
|
58
59
|
quotaResetNotifySchema,
|
|
59
60
|
remoteGuiConfigSchema,
|
|
60
61
|
runtimeRoleSchema,
|
|
62
|
+
spendSchema,
|
|
63
|
+
compactionRoutingSchema,
|
|
61
64
|
} from "./schema/leaf-validators";
|
|
62
65
|
|
|
63
66
|
export type ConfigDiagnostics = {
|
|
@@ -122,6 +125,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
|
|
|
122
125
|
if (catalogRefreshWarning) warnings.push(catalogRefreshWarning);
|
|
123
126
|
const codexPoolWarning = malformedCodexPoolWarning(rawParsed);
|
|
124
127
|
if (codexPoolWarning) warnings.push(codexPoolWarning);
|
|
128
|
+
const spendWarning = malformedSpendWarning(rawParsed);
|
|
129
|
+
if (spendWarning) warnings.push(spendWarning);
|
|
125
130
|
const plaintextWarning = malformedPlaintextV2AgentMessagesWarning(rawParsed);
|
|
126
131
|
if (plaintextWarning) warnings.push(plaintextWarning);
|
|
127
132
|
if (syncDisabledReason) {
|
|
@@ -295,6 +300,22 @@ function catalogAutoRefreshError(value: unknown): string | null {
|
|
|
295
300
|
return `schema_invalid: catalogAutoRefresh${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
|
|
296
301
|
}
|
|
297
302
|
|
|
303
|
+
/**
|
|
304
|
+
* The read path degrades a malformed spend section to undefined, which means no ceiling is
|
|
305
|
+
* enforced. Reject it on write so `ocx config set` cannot store a budget that reads back as
|
|
306
|
+
* configured and refuses nothing -- a ceiling that is not enforced looks identical to one
|
|
307
|
+
* nothing has reached.
|
|
308
|
+
*/
|
|
309
|
+
function spendError(value: unknown): string | null {
|
|
310
|
+
const raw = rawConfigRecord(value);
|
|
311
|
+
if (!raw || !Object.hasOwn(raw, "spend") || raw.spend === undefined) return null;
|
|
312
|
+
const result = spendSchema.safeParse(raw.spend);
|
|
313
|
+
if (result.success) return null;
|
|
314
|
+
const issue = result.error.issues[0];
|
|
315
|
+
const field = issue?.path.join(".");
|
|
316
|
+
return `schema_invalid: spend${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
298
319
|
/**
|
|
299
320
|
* The read path degrades a malformed pool policy to undefined, which for an exclusion policy means
|
|
300
321
|
* the excluded accounts quietly keep serving traffic. Reject it on write so `ocx config set` cannot
|
|
@@ -541,7 +562,26 @@ function managementIngressConfigError(value: unknown): string | null {
|
|
|
541
562
|
return null;
|
|
542
563
|
}
|
|
543
564
|
|
|
565
|
+
/** Load degrades malformed metrics export config to off; live writes reject the same shape. */
|
|
566
|
+
export function metricsExportConfigError(value: unknown): string | null {
|
|
567
|
+
const raw = rawConfigRecord(value);
|
|
568
|
+
if (!raw || !Object.hasOwn(raw, "metricsExport") || raw.metricsExport === undefined) return null;
|
|
569
|
+
const metricsExport = rawConfigRecord(raw.metricsExport);
|
|
570
|
+
if (!metricsExport) return "schema_invalid: metricsExport: must be an object or omitted";
|
|
571
|
+
if (Object.keys(metricsExport).some(key => key !== "enabled")) {
|
|
572
|
+
return "schema_invalid: metricsExport: contains an unsupported field";
|
|
573
|
+
}
|
|
574
|
+
if (metricsExport.enabled !== undefined && typeof metricsExport.enabled !== "boolean") {
|
|
575
|
+
return "schema_invalid: metricsExport.enabled: must be a boolean";
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
544
580
|
export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } {
|
|
581
|
+
const compactionRouting = rawConfigRecord(value)?.compactionRouting;
|
|
582
|
+
if (compactionRouting !== undefined && !compactionRoutingSchema.safeParse(compactionRouting).success) {
|
|
583
|
+
return { ok: false, error: "schema_invalid: compactionRouting: requires a nonblank model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" };
|
|
584
|
+
}
|
|
545
585
|
const boundaryError = configReasoningPinsConfigError(value)
|
|
546
586
|
?? blankHostnameError(value)
|
|
547
587
|
?? claudeSubagentEffortError(value)
|
|
@@ -551,6 +591,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
551
591
|
?? agentTaskRecoveryError(value)
|
|
552
592
|
?? quotaResetNotifyError(value)
|
|
553
593
|
?? catalogAutoRefreshError(value)
|
|
594
|
+
?? spendError(value)
|
|
554
595
|
?? codexPoolError(value)
|
|
555
596
|
?? googleAntigravityStaticCatalogVersionError(value)
|
|
556
597
|
?? codexAccountPrioritiesError(value)
|
|
@@ -565,7 +606,8 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
565
606
|
?? clientConnectionConfigError(value)
|
|
566
607
|
?? clientRolePairError(value)
|
|
567
608
|
?? loopbackListenerPortError(value)
|
|
568
|
-
?? managementIngressConfigError(value)
|
|
609
|
+
?? managementIngressConfigError(value)
|
|
610
|
+
?? metricsExportConfigError(value);
|
|
569
611
|
if (boundaryError) return { ok: false, error: boundaryError };
|
|
570
612
|
const result = configSchema.safeParse(value);
|
|
571
613
|
if (result.success) {
|
|
@@ -12,6 +12,11 @@ export function ultraFastTierEnabled(config: Pick<OcxConfig, "ultraFastTier">):
|
|
|
12
12
|
return config.ultraFastTier === true;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Default-off aggregate request metrics; activation is fixed for one server process lifetime. */
|
|
16
|
+
export function metricsExportEnabled(config: Pick<OcxConfig, "metricsExport">): boolean {
|
|
17
|
+
return config.metricsExport?.enabled === true;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* Default cadence for the opt-in catalog auto-refresh (issue #3630): one converge pass
|
|
17
22
|
* per hour. Each pass spends a live /models call against every enabled provider, and
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
remoteGuiConfigSchema,
|
|
34
34
|
retryOn429PolicySchema,
|
|
35
35
|
runtimeRoleSchema,
|
|
36
|
+
spendSchema,
|
|
36
37
|
} from "./schema/leaf-validators";
|
|
37
38
|
import { hasWarnedInheritedFastWireConflict, markWarnedInheritedFastWireConflict } from "./warn-memo";
|
|
38
39
|
|
|
@@ -100,6 +101,24 @@ export function warnDegradedStreamMode(rawParsed: unknown, validated: OcxConfig)
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
export function warnDegradedCompactionRouting(rawParsed: unknown, validated: OcxConfig): void {
|
|
105
|
+
if (!rawParsed || typeof rawParsed !== "object") return;
|
|
106
|
+
const raw = (rawParsed as Record<string, unknown>).compactionRouting;
|
|
107
|
+
if (raw !== undefined && validated.compactionRouting === undefined) {
|
|
108
|
+
console.warn("⚠️ config.json compactionRouting is invalid (expected { model, reasoningEffort?, triggers? } with a nonblank model, a declared effort, and triggers drawn without repetition from \"manual\" and \"auto\") — compaction keeps the conversation model");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Top-level opt-in blocks whose hand-edited form degrades to "off" instead of failing the whole
|
|
114
|
+
* schema. Grouped behind one entry point because `src/config.ts` sits at its file-size cap, and
|
|
115
|
+
* the ratchet only ever moves down: a per-block call there costs a line the file does not have.
|
|
116
|
+
*/
|
|
117
|
+
export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxConfig): void {
|
|
118
|
+
warnDegradedStreamMode(rawParsed, validated);
|
|
119
|
+
warnDegradedCompactionRouting(rawParsed, validated);
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
/**
|
|
104
123
|
* Load-time degradation for `retryOn429` (loadConfig only): one hand-edited invalid optional
|
|
105
124
|
* field (e.g. `attempts: 0` or a string) must not trip the whole provider schema and hide every
|
|
@@ -636,6 +655,20 @@ export function malformedCatalogAutoRefreshWarning(rawParsed: unknown): string |
|
|
|
636
655
|
return `catalogAutoRefresh${field ? `.${field}` : ""} ignored: invalid catalog auto-refresh configuration`;
|
|
637
656
|
}
|
|
638
657
|
|
|
658
|
+
/**
|
|
659
|
+
* The same silent-in-the-wrong-direction failure, and the most expensive instance of it here:
|
|
660
|
+
* a dropped spend section means the ceilings are not enforced, and an unenforced ceiling is
|
|
661
|
+
* indistinguishable from one nothing has reached. The operator finds out from the bill.
|
|
662
|
+
*/
|
|
663
|
+
export function malformedSpendWarning(rawParsed: unknown): string | null {
|
|
664
|
+
const raw = rawConfigRecord(rawParsed);
|
|
665
|
+
if (!raw || !Object.hasOwn(raw, "spend")) return null;
|
|
666
|
+
const result = spendSchema.safeParse(raw.spend);
|
|
667
|
+
if (result.success) return null;
|
|
668
|
+
const field = result.error.issues[0]?.path.join(".");
|
|
669
|
+
return `spend${field ? `.${field}` : ""} ignored: invalid spend ceiling configuration, so no token ceiling is enforced`;
|
|
670
|
+
}
|
|
671
|
+
|
|
639
672
|
/**
|
|
640
673
|
* Same silent-in-the-wrong-direction failure as the notification block: a dropped pool policy means
|
|
641
674
|
* the accounts the operator meant to exclude keep taking traffic, and the only visible symptom is
|
|
@@ -295,6 +295,14 @@ export function quarantinePendingTeardown(nonce: string): string | null {
|
|
|
295
295
|
* alone. Only an abandoned obligation is a candidate, and a VALID one still has to prove
|
|
296
296
|
* its endpoint is down before anything is restored — an invalid one never can, which is
|
|
297
297
|
* what {@link quarantinePendingTeardown} exists for.
|
|
298
|
+
*
|
|
299
|
+
* `isAlive` must answer "is the OWNER still running", which is not "is this PID alive".
|
|
300
|
+
* PIDs are reused: after the owner exits its number can be handed to an unrelated process,
|
|
301
|
+
* and a bare liveness probe then reports the owner as running for as long as that process
|
|
302
|
+
* lives. The receipt is neither recovered nor quarantined nor reported while both updater
|
|
303
|
+
* gates keep refusing on it, which is a permanent fail-closed with no way forward (#4897).
|
|
304
|
+
* `handleStop` therefore passes an identity-aware predicate; do not substitute a bare
|
|
305
|
+
* `process.kill(pid, 0)` here for cheapness.
|
|
298
306
|
*/
|
|
299
307
|
export function isPendingTeardownAbandoned(
|
|
300
308
|
read: PendingTeardownRead | TeardownScanFailure,
|
|
@@ -110,16 +110,29 @@ export function removeRuntimePortIfPidIs(snapshotPid: number | null): void {
|
|
|
110
110
|
try { unlinkSync(getRuntimePortPath()); } catch { /* ignore */ }
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Does this command line belong to an opencodex process at all, whatever it is doing?
|
|
115
|
+
*
|
|
116
|
+
* {@link isOcxStartCommandLine} answers the narrower question the pidfile needs — "is this
|
|
117
|
+
* the proxy?" — by additionally requiring the `start` verb. Ownership of a pending-teardown
|
|
118
|
+
* receipt is the broader question: the owner is whichever invocation claimed it, which is an
|
|
119
|
+
* `ocx stop` or the `ocx update` worker that drove it, never an `ocx start`. Asking the
|
|
120
|
+
* start-shaped question there would call every real owner foreign.
|
|
121
|
+
*/
|
|
122
|
+
export function isOcxCommandLine(commandLine: string): boolean {
|
|
114
123
|
const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
|
|
115
124
|
// Keep legacy source launches and npm's in-place Windows rename recognizable:
|
|
116
125
|
// a service wrapper may respawn from `.opencodex-*` during a global update.
|
|
117
|
-
|
|
126
|
+
return normalized.includes("src/cli.ts")
|
|
118
127
|
|| normalized.includes("src/cli/index.ts")
|
|
119
128
|
|| normalized.includes("@bitkyc08/opencodex")
|
|
120
129
|
|| /@bitkyc08\/\.opencodex-/.test(normalized)
|
|
121
130
|
|| /(?:^|[\s/"'])(?:ocx|opencodex)(?:\.cmd)?(?:$|[\s"'])/.test(normalized);
|
|
122
|
-
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isOcxStartCommandLine(commandLine: string): boolean {
|
|
134
|
+
const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
|
|
135
|
+
return isOcxCommandLine(commandLine) && /(?:^|[\s"'])start(?:$|[\s"'])/.test(normalized);
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
/** Avoid spawning WMIC/PowerShell on every short liveness poll. */
|
|
@@ -227,6 +240,26 @@ export function verifyPidIdentity(candidatePid: number): number | null {
|
|
|
227
240
|
return isLikelyOcxStartProcess(candidatePid) ? candidatePid : null;
|
|
228
241
|
}
|
|
229
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Is this PID an opencodex process, rather than merely a live one?
|
|
245
|
+
*
|
|
246
|
+
* `process.kill(pid, 0)` answers "does this number name a process", which is not the same
|
|
247
|
+
* question. PIDs are reused, so a recorded owner that exited can have its number handed to
|
|
248
|
+
* an unrelated process, and bare liveness then reports the owner as still running forever.
|
|
249
|
+
*
|
|
250
|
+
* Deliberately uncached, unlike {@link isLikelyOcxStartProcess}. That cache exists because
|
|
251
|
+
* liveness polling asks about the same PID many times a second; this is asked once per
|
|
252
|
+
* outstanding teardown receipt in a short-lived `ocx stop`, so the cache would only add an
|
|
253
|
+
* unswept map. The Windows WMIC/PowerShell probe cost is the point rather than a regression:
|
|
254
|
+
* the runtime contract requires the identity check, not the cheap one, before acting on
|
|
255
|
+
* stale state.
|
|
256
|
+
*/
|
|
257
|
+
export function isLikelyOcxProcess(pid: number): boolean {
|
|
258
|
+
const commandLine = readProcessCommandLine(pid);
|
|
259
|
+
if (commandLine === undefined) return false;
|
|
260
|
+
return isOcxCommandLine(commandLine);
|
|
261
|
+
}
|
|
262
|
+
|
|
230
263
|
type ProcessCommandLineExec = (
|
|
231
264
|
executable: string,
|
|
232
265
|
args: string[],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Shared, initialization-independent shape check for provider send-path overrides. */
|
|
2
|
+
export function providerRelativeSendPathConfigError(
|
|
3
|
+
field: "responsesPath" | "chatCompletionsPath",
|
|
4
|
+
value: unknown,
|
|
5
|
+
): string | null {
|
|
6
|
+
if (value === undefined) return null;
|
|
7
|
+
if (typeof value !== "string") return `${field} must be a string`;
|
|
8
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.includes("://")) {
|
|
9
|
+
return `${field} must be a relative path without a URL scheme`;
|
|
10
|
+
}
|
|
11
|
+
if (!value.startsWith("/")) return `${field} must start with /`;
|
|
12
|
+
if (value.includes("?") || value.includes("#")) {
|
|
13
|
+
return `${field} must not include query strings or fragments`;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
package/src/config/proxy-env.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { configureSocks5Fetch } from "../lib/proxy-env";
|
|
2
|
+
import { redactUrlForLog } from "../lib/redact";
|
|
1
3
|
import { join } from "node:path";
|
|
2
4
|
import { DEFAULT_SUBAGENT_MODELS, SUBAGENT_MODELS_VERSION } from "./subagent-models";
|
|
3
5
|
import { MULTI_AGENT_SURFACE_ADVISORY_VERSION } from "./multi-agent-surface";
|
|
@@ -102,10 +104,15 @@ function warnProxyConfigDiscardOnce(kind: "proxy" | "noProxy" | "noProxyElements
|
|
|
102
104
|
* variables win; config fills missing scheme proxies, which take precedence over ALL_PROXY for WS.
|
|
103
105
|
* localhost/127.0.0.1 are appended to NO_PROXY so the CLI's own health checks and
|
|
104
106
|
* running-proxy API calls stay direct. Call once per process entry that makes outbound provider
|
|
105
|
-
* requests (server start, catalog sync).
|
|
107
|
+
* requests (server start, catalog sync). `announce` prints the one-line startup banner naming
|
|
108
|
+
* the outbound proxy actually in effect; only server start passes it, because on catalog sync
|
|
109
|
+
* that line is noise. The URL is redacted because a proxy URL can carry credentials.
|
|
106
110
|
*/
|
|
107
|
-
export function applyProxyEnv(config: OcxConfig): void {
|
|
111
|
+
export function applyProxyEnv(config: OcxConfig, announce = false): void {
|
|
108
112
|
applyProxyEnvWith(config);
|
|
113
|
+
if (!announce) return;
|
|
114
|
+
const outbound = process.env.ALL_PROXY?.trim() || process.env.HTTPS_PROXY?.trim() || process.env.HTTP_PROXY?.trim();
|
|
115
|
+
if (outbound) console.log(` outbound proxy: ${redactUrlForLog(outbound)}`);
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
/** Test seam for `proxy: "auto"`: the registry reader and platform are injectable. */
|
|
@@ -123,6 +130,7 @@ export function applyProxyEnvWith(
|
|
|
123
130
|
let proxy = typeof rawProxy === "string" ? resolveEnvValue(rawProxy) : undefined;
|
|
124
131
|
if (!proxy) {
|
|
125
132
|
if (rawProxy !== undefined) warnProxyConfigDiscardOnce("proxy");
|
|
133
|
+
configureSocks5Fetch();
|
|
126
134
|
return;
|
|
127
135
|
}
|
|
128
136
|
if (proxy.trim().toLowerCase() === "auto") {
|
|
@@ -135,8 +143,14 @@ export function applyProxyEnvWith(
|
|
|
135
143
|
} else {
|
|
136
144
|
const found = readWindowsSystemProxy(auto.reader, auto.platform);
|
|
137
145
|
if (found.kind === "proxy") {
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
const origins = [
|
|
147
|
+
found.httpUrl && `HTTP ${describeProxyForLog(found.httpUrl)}`,
|
|
148
|
+
found.httpsUrl && `HTTPS ${describeProxyForLog(found.httpsUrl)}`,
|
|
149
|
+
].filter(Boolean).join(", ");
|
|
150
|
+
console.log(`[opencodex] proxy "auto": using Windows system proxy ${origins}`);
|
|
151
|
+
if (found.httpUrl) process.env.HTTP_PROXY = found.httpUrl;
|
|
152
|
+
if (found.httpsUrl) process.env.HTTPS_PROXY = found.httpsUrl;
|
|
153
|
+
proxy = undefined;
|
|
140
154
|
} else {
|
|
141
155
|
const reason = found.kind === "unsupported"
|
|
142
156
|
? "only Windows system proxy discovery is supported; using direct egress on this OS"
|
|
@@ -151,8 +165,18 @@ export function applyProxyEnvWith(
|
|
|
151
165
|
}
|
|
152
166
|
}
|
|
153
167
|
if (proxy) {
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
if (/^socks/i.test(proxy.trim()) && !/^socks5h?:\/\//i.test(proxy.trim())) {
|
|
169
|
+
throw new Error("Only SOCKS5 proxy URLs are supported; use socks5://host:port");
|
|
170
|
+
}
|
|
171
|
+
if (/^socks5h?:\/\//i.test(proxy.trim())) {
|
|
172
|
+
for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy"] as const) {
|
|
173
|
+
delete process.env[key];
|
|
174
|
+
}
|
|
175
|
+
process.env.ALL_PROXY = proxy;
|
|
176
|
+
} else {
|
|
177
|
+
if (!process.env.HTTP_PROXY?.trim() && !process.env.http_proxy?.trim()) process.env.HTTP_PROXY = proxy;
|
|
178
|
+
if (!process.env.HTTPS_PROXY?.trim() && !process.env.https_proxy?.trim()) process.env.HTTPS_PROXY = proxy;
|
|
179
|
+
}
|
|
156
180
|
}
|
|
157
181
|
const existing = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
158
182
|
const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
|
|
@@ -184,5 +208,5 @@ export function applyProxyEnvWith(
|
|
|
184
208
|
}
|
|
185
209
|
}
|
|
186
210
|
process.env.NO_PROXY = entries.join(",");
|
|
211
|
+
configureSocks5Fetch();
|
|
187
212
|
}
|
|
188
|
-
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `compaction.trigger` values Codex labels a compaction turn with (`CompactionTrigger`
|
|
3
|
+
* in codex-rs: a manual `/compact` versus automatic compaction). `compactionRouting.triggers`
|
|
4
|
+
* selects which of them an override applies to.
|
|
5
|
+
*
|
|
6
|
+
* This is its own leaf module and imports nothing on purpose. The request path reads it from
|
|
7
|
+
* `src/server/responses/compaction-routing.ts`; importing `leaf-validators.ts` there instead
|
|
8
|
+
* closes a cycle with `config-schema.ts` that survives typecheck and then fails at run time
|
|
9
|
+
* with "Cannot access 'runtimeRoleSchema' before initialization".
|
|
10
|
+
*/
|
|
11
|
+
export const COMPACTION_TRIGGERS = ["manual", "auto"] as const;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
quotaResetNotifySchema,
|
|
14
14
|
remoteGuiConfigSchema,
|
|
15
15
|
runtimeRoleSchema,
|
|
16
|
+
spendSchema,
|
|
16
17
|
configuredCodexPoolAccountIds,
|
|
17
18
|
apiKeyEntrySchema,
|
|
18
19
|
asideProfileSyncSchema,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
CODEX_ACCOUNT_NAMESPACE_ACCOUNT_ID_COLLISION_ERROR,
|
|
21
22
|
codexAccountNamespacesSchema,
|
|
22
23
|
modelPinnedEffortsSchema,
|
|
24
|
+
compactionRoutingSchema,
|
|
23
25
|
modelPreferHostedToolsConfigError,
|
|
24
26
|
providerModelCostsConfigError,
|
|
25
27
|
providerRelativeSendPathConfigError,
|
|
@@ -72,6 +74,8 @@ export const configSchema = z.object({
|
|
|
72
74
|
// A malformed privacy block must never be read as "unmask": .catch(undefined) drops it and
|
|
73
75
|
// emailMaskingEnabled then falls back to masked, which is also what an absent block means.
|
|
74
76
|
privacy: z.object({ maskEmails: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
77
|
+
// Malformed hand edits disable this opt-in exporter. Live writes reject them in diagnostics.ts.
|
|
78
|
+
metricsExport: z.object({ enabled: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
75
79
|
// A malformed present client block must remain diagnosable from raw config and
|
|
76
80
|
// fail closed through src/client/state.ts; unrelated provider state still loads.
|
|
77
81
|
client: clientConnectionSchema.optional().catch(undefined),
|
|
@@ -124,6 +128,7 @@ export const configSchema = z.object({
|
|
|
124
128
|
]).optional().catch(undefined),
|
|
125
129
|
providers: z.record(z.string(), providerConfigSchema),
|
|
126
130
|
modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
|
|
131
|
+
compactionRouting: compactionRoutingSchema.optional().catch(undefined),
|
|
127
132
|
defaultProvider: z.string().min(1).default("openai"),
|
|
128
133
|
defaultModelAliases: z.boolean().optional(),
|
|
129
134
|
// Malformed hand edits disable this opt-in projection without rejecting providers.
|
|
@@ -166,6 +171,11 @@ export const configSchema = z.object({
|
|
|
166
171
|
quotaResetNotify: quotaResetNotifySchema.optional().catch(undefined),
|
|
167
172
|
// Same rationale: a bad auto-refresh section must not cost the operator their providers.
|
|
168
173
|
catalogAutoRefresh: catalogAutoRefreshSchema.optional().catch(undefined),
|
|
174
|
+
// Same rationale again, with the failure direction stated: a malformed spend section
|
|
175
|
+
// degrades to "no ceiling", which means observe-only accounting rather than an outage. That
|
|
176
|
+
// is the safe degrade for traffic and the dangerous one for the operator, so the write path
|
|
177
|
+
// rejects it and loadConfig warns -- the same pair codexPool uses below.
|
|
178
|
+
spend: spendSchema.optional().catch(undefined),
|
|
169
179
|
// These selections pre-date schema validation and used to pass through as
|
|
170
180
|
// unknown fields. Invalid hand edits must disable only the optional
|
|
171
181
|
// delegation/native-default feature, not reject the whole config and hide
|
|
@@ -182,6 +192,10 @@ export const configSchema = z.object({
|
|
|
182
192
|
codexShimAutoRestore: z.boolean().optional(),
|
|
183
193
|
codexDesktopAuthless: z.boolean().optional().catch(undefined),
|
|
184
194
|
codexClientCompaction: z.boolean().optional().catch(undefined),
|
|
195
|
+
// Presentation-only label for the injected provider. A malformed value degrades to undefined
|
|
196
|
+
// and the default label is emitted, rather than failing the parse or writing a config Codex
|
|
197
|
+
// would refuse to load — the provider id routing depends on is never derived from it.
|
|
198
|
+
codexProviderDisplayName: z.string().trim().min(1).max(128).optional().catch(undefined),
|
|
185
199
|
pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
|
|
186
200
|
// A malformed policy degrades to "no policy" rather than failing the parse, so a hand-edited
|
|
187
201
|
// typo cannot trip the backup-and-defaults repair path and wipe providers or pool accounts.
|
|
@@ -464,6 +478,17 @@ export const configSchema = z.object({
|
|
|
464
478
|
message: reasoningSummariesError,
|
|
465
479
|
});
|
|
466
480
|
}
|
|
481
|
+
const suppressSyntheticMaxError = booleanRecordConfigError(
|
|
482
|
+
(provider as { modelSuppressSyntheticMax?: unknown }).modelSuppressSyntheticMax,
|
|
483
|
+
"modelSuppressSyntheticMax",
|
|
484
|
+
);
|
|
485
|
+
if (suppressSyntheticMaxError) {
|
|
486
|
+
ctx.addIssue({
|
|
487
|
+
code: "custom",
|
|
488
|
+
path: ["providers", redactSecretString(name), "modelSuppressSyntheticMax"],
|
|
489
|
+
message: suppressSyntheticMaxError,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
467
492
|
const verbositySupportError = booleanRecordConfigError(
|
|
468
493
|
(provider as { modelSupportsVerbosity?: unknown }).modelSupportsVerbosity,
|
|
469
494
|
"modelSupportsVerbosity",
|
|
@@ -32,14 +32,27 @@ import { getProviderRegistryEntry, providerMatchesRegistryTransport, providerMod
|
|
|
32
32
|
import { resolveOpenAiVirtualModel } from "../../providers/openai-virtual-models";
|
|
33
33
|
import { COST4_RATE_KEYS, isValidCost4Rate } from "../../usage/user-cost-overlays";
|
|
34
34
|
import { MAX_COST4_RATE } from "../../usage/expected-prices";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
DECLARABLE_HOSTED_TOOL_TYPES,
|
|
37
|
+
declaredUnsupportedHostedTools,
|
|
38
|
+
isHostedToolUnsupportedForModel,
|
|
39
|
+
} from "../../responses/hosted-tool-policy";
|
|
36
40
|
import { getConfigDir } from "../paths";
|
|
41
|
+
import { COMPACTION_TRIGGERS } from "./compaction-triggers";
|
|
37
42
|
|
|
38
43
|
/** One definition of "usable secret", shared by the schema and the warnings. */
|
|
39
44
|
export function isUsableApiKeySecret(value: unknown): value is string {
|
|
40
45
|
return typeof value === "string" && value.length > 0 && value === value.trim();
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
export const compactionRoutingSchema = z.object({
|
|
49
|
+
model: z.string().trim().min(1),
|
|
50
|
+
reasoningEffort: z.string().refine(value => pinnedReasoningEffortConfigError(value) === null).optional(),
|
|
51
|
+
triggers: z.array(z.enum(COMPACTION_TRIGGERS)).nonempty()
|
|
52
|
+
.refine(values => new Set(values).size === values.length, "triggers must not repeat a value")
|
|
53
|
+
.optional(),
|
|
54
|
+
}).strict();
|
|
55
|
+
|
|
43
56
|
/**
|
|
44
57
|
* Bounds for the opt-in same-target 429 wait-and-retry policy. Single source of truth
|
|
45
58
|
* shared by the config schema, the load-time sanitizer, and the management write
|
|
@@ -248,7 +261,10 @@ export const providerConfigSchema = z.object({
|
|
|
248
261
|
fastWire: fastWireSchema.nullable().optional(),
|
|
249
262
|
supportsServiceTier: z.boolean().optional(),
|
|
250
263
|
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
|
|
264
|
+
modelSuppressSyntheticMax: z.record(z.string().min(1), z.boolean()).optional(),
|
|
251
265
|
preserveResponsesReasoningContent: z.boolean().optional(),
|
|
266
|
+
dropResponsesReasoningItems: z.boolean().optional(),
|
|
267
|
+
modelReasoningEffortsAuthoritative: z.boolean().optional(),
|
|
252
268
|
decodesNativeCompactionBlobs: z.boolean().optional(),
|
|
253
269
|
allowEncryptedV2AgentTasks: z.boolean().optional(),
|
|
254
270
|
allowPrivateNetwork: z.boolean().optional(),
|
|
@@ -263,6 +279,7 @@ export const providerConfigSchema = z.object({
|
|
|
263
279
|
// canonical ChatGPT backend WS selection is independent of this flag.
|
|
264
280
|
upstreamWebsocket: z.boolean().optional(),
|
|
265
281
|
directGeminiWireRenames: z.boolean().optional(),
|
|
282
|
+
googleToolSchemaPolicy: z.enum(["compatible", "reject-lossy"]).optional(),
|
|
266
283
|
noStructuredOutputModels: z.array(z.string().min(1))
|
|
267
284
|
.transform(normalizeNonBlankStringArray)
|
|
268
285
|
.optional(),
|
|
@@ -275,6 +292,18 @@ export const providerConfigSchema = z.object({
|
|
|
275
292
|
omitReasoningEffortWithToolsModels: z.array(z.string().min(1))
|
|
276
293
|
.transform(normalizeNonBlankStringArray)
|
|
277
294
|
.optional(),
|
|
295
|
+
// Validated against a closed vocabulary rather than accepted as free strings. This
|
|
296
|
+
// schema ends in `.passthrough()`, so a misspelled `web_serch` would otherwise be
|
|
297
|
+
// accepted, persisted, and strip nothing -- leaving the operator with the upstream 400
|
|
298
|
+
// the field was set to prevent, and no message saying why (the `codexToolMode` lesson,
|
|
299
|
+
// #2106).
|
|
300
|
+
unsupportedHostedTools: z.array(z.string().min(1))
|
|
301
|
+
.transform(normalizeNonBlankStringArray)
|
|
302
|
+
.refine(
|
|
303
|
+
tools => tools.every(tool => DECLARABLE_HOSTED_TOOL_TYPES.has(tool)),
|
|
304
|
+
{ message: `unsupportedHostedTools accepts only hosted tool types: ${[...DECLARABLE_HOSTED_TOOL_TYPES].join(", ")}` },
|
|
305
|
+
)
|
|
306
|
+
.optional(),
|
|
278
307
|
retryOn429: retryOn429PolicySchema.optional(),
|
|
279
308
|
transientRetryOn5xx: transientRetryOn5xxPolicySchema.optional(),
|
|
280
309
|
codexAccountMode: z.enum(["pool", "direct"]).optional(),
|
|
@@ -300,21 +329,7 @@ export const providerConfigSchema = z.object({
|
|
|
300
329
|
}).passthrough();
|
|
301
330
|
|
|
302
331
|
|
|
303
|
-
|
|
304
|
-
* Shared shape check for the two relative send-path overrides. `field` names the
|
|
305
|
-
* offending key so the message stays specific to what the user actually wrote.
|
|
306
|
-
*/
|
|
307
|
-
export function providerRelativeSendPathConfigError(field: string, value: string | undefined): string | null {
|
|
308
|
-
if (value === undefined) return null;
|
|
309
|
-
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.includes("://")) {
|
|
310
|
-
return `${field} must be a relative path without a URL scheme`;
|
|
311
|
-
}
|
|
312
|
-
if (!value.startsWith("/")) return `${field} must start with /`;
|
|
313
|
-
if (value.includes("?") || value.includes("#")) {
|
|
314
|
-
return `${field} must not include query strings or fragments`;
|
|
315
|
-
}
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
332
|
+
export { providerRelativeSendPathConfigError } from "../provider-relative-send-path";
|
|
318
333
|
|
|
319
334
|
/**
|
|
320
335
|
* Validate `providers.<name>.modelCosts`: a plain object keyed by exact model
|
|
@@ -391,7 +406,13 @@ export function modelPreferHostedToolsConfigError(
|
|
|
391
406
|
value: unknown,
|
|
392
407
|
field: string,
|
|
393
408
|
providerName: string,
|
|
394
|
-
provider: {
|
|
409
|
+
provider: {
|
|
410
|
+
adapter?: unknown;
|
|
411
|
+
authMode?: unknown;
|
|
412
|
+
modelAdapters?: unknown;
|
|
413
|
+
baseUrl?: unknown;
|
|
414
|
+
unsupportedHostedTools?: unknown;
|
|
415
|
+
},
|
|
395
416
|
): string | null {
|
|
396
417
|
if (value === undefined) return null;
|
|
397
418
|
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
|
|
@@ -399,6 +420,12 @@ export function modelPreferHostedToolsConfigError(
|
|
|
399
420
|
if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`;
|
|
400
421
|
const entries = Object.entries(value);
|
|
401
422
|
const registry = getProviderRegistryEntry(providerName);
|
|
423
|
+
// A provider that denies a hosted tool cannot also prefer it. Both keys are
|
|
424
|
+
// provider-owned capability statements about the same tool, and the denial wins at
|
|
425
|
+
// request time, so accepting the pair would silently ignore the preference.
|
|
426
|
+
const declaredUnsupported = declaredUnsupportedHostedTools(
|
|
427
|
+
provider as { unsupportedHostedTools?: readonly string[] },
|
|
428
|
+
);
|
|
402
429
|
// Effective transport: a `preserveCustomDestination` registry row reused under a
|
|
403
430
|
// different endpoint keeps its own adapter AND its own auth at runtime, because
|
|
404
431
|
// `routedProviderConfig()` honors `providerMatchesRegistryTransport()`. Both the
|
|
@@ -459,6 +486,9 @@ export function modelPreferHostedToolsConfigError(
|
|
|
459
486
|
if (typeof tool !== "string" || !SUPPORTED_PREFERRED_HOSTED_TOOLS.has(tool)) {
|
|
460
487
|
return `${field}.${key} supports only image_generation`;
|
|
461
488
|
}
|
|
489
|
+
if (declaredUnsupported.has(tool)) {
|
|
490
|
+
return `${field}.${key} cannot prefer ${tool}: unsupportedHostedTools declares it unsupported`;
|
|
491
|
+
}
|
|
462
492
|
if (isHostedToolUnsupportedForModel(key, tool)) {
|
|
463
493
|
return `${field}.${key} cannot prefer ${tool}: the model does not support it`;
|
|
464
494
|
}
|
|
@@ -854,3 +884,32 @@ export const catalogAutoRefreshSchema = z.object({
|
|
|
854
884
|
enabled: z.boolean().optional(),
|
|
855
885
|
intervalMinutes: z.number().int().min(0).max(1440).optional(),
|
|
856
886
|
}).strict();
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* One spend scope's ceiling.
|
|
890
|
+
*
|
|
891
|
+
* `.strict()` for the usual reason and one sharper one. Elsewhere a silently ignored key
|
|
892
|
+
* leaves a feature off that the operator believed was on; here it leaves a BUDGET off, and a
|
|
893
|
+
* budget nobody is enforcing looks exactly like a budget nobody has exceeded. That is #2106 --
|
|
894
|
+
* an undeclared option accepted, persisted, and then read as its default -- aimed at spend.
|
|
895
|
+
*
|
|
896
|
+
* Only positive integers: 0 would read as "no tokens at all" and refuse every request under
|
|
897
|
+
* the scope, which is never what writing a budget means. An operator who wants no ceiling
|
|
898
|
+
* removes the key.
|
|
899
|
+
*/
|
|
900
|
+
const spendScopeSchema = z.object({
|
|
901
|
+
maxTokens: z.number().int().positive().optional(),
|
|
902
|
+
}).strict();
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Durable spend ceilings (#4546).
|
|
906
|
+
*
|
|
907
|
+
* Absent, empty, and all-scopes-absent are the same thing: observe-only accounting. There is
|
|
908
|
+
* no default ceiling anywhere in this section, deliberately.
|
|
909
|
+
*/
|
|
910
|
+
export const spendSchema = z.object({
|
|
911
|
+
root: spendScopeSchema.optional(),
|
|
912
|
+
identity: spendScopeSchema.optional(),
|
|
913
|
+
pool: spendScopeSchema.optional(),
|
|
914
|
+
retentionDays: z.number().int().min(1).max(365).optional(),
|
|
915
|
+
}).strict();
|
package/src/config.ts
CHANGED
|
@@ -170,7 +170,7 @@ import {
|
|
|
170
170
|
sanitizeModelCostsForLoad,
|
|
171
171
|
sanitizeCapabilityDeclarationsForLoad,
|
|
172
172
|
warnInheritedFastWireConflicts,
|
|
173
|
-
|
|
173
|
+
warnDegradedTopLevelOptIns,
|
|
174
174
|
warnDegradedHostname,
|
|
175
175
|
warnDegradedListeners,
|
|
176
176
|
warnDegradedApiKeys,
|
|
@@ -227,7 +227,7 @@ export function loadConfig(): OcxConfig {
|
|
|
227
227
|
if (result.success) {
|
|
228
228
|
const config = normalizeApiKeyIds(result.data as OcxConfig);
|
|
229
229
|
warnInheritedFastWireConflicts(configPath, config);
|
|
230
|
-
|
|
230
|
+
warnDegradedTopLevelOptIns(parsed, config);
|
|
231
231
|
warnDegradedHostname(parsed, config);
|
|
232
232
|
warnDegradedListeners(parsed, config);
|
|
233
233
|
warnDegradedApiKeys(parsed, config);
|