@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
package/src/types/provider.ts
CHANGED
|
@@ -356,6 +356,22 @@ export interface OcxProviderConfig {
|
|
|
356
356
|
* `ocxr1` envelopes are still stripped because no upstream can decrypt them.
|
|
357
357
|
*/
|
|
358
358
|
preserveResponsesReasoningContent?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* Treat this provider's `modelReasoningEfforts` as authoritative at the wire, not only in the
|
|
361
|
+
* catalog. Adapters that ship their own per-model effort table (currently `command-code`)
|
|
362
|
+
* otherwise let that table win for models it knows, so a widened row is advertised in the
|
|
363
|
+
* picker and then stripped on the way out. Opt-in because presets are SEEDED with the shipped
|
|
364
|
+
* table: without a declared flag there is no way to tell an operator's row from a copy an
|
|
365
|
+
* older release persisted. A rung the upstream then refuses is returned as that error rather
|
|
366
|
+
* than silently retried without the effort, since the operator asked for it.
|
|
367
|
+
*/
|
|
368
|
+
modelReasoningEffortsAuthoritative?: boolean;
|
|
369
|
+
/**
|
|
370
|
+
* Drop replayed Responses `reasoning` items from input history before forwarding.
|
|
371
|
+
* Some OpenAI-compatible Responses upstreams accept tool-call replay but reject
|
|
372
|
+
* reasoning output items when they are sent back on a continuation.
|
|
373
|
+
*/
|
|
374
|
+
dropResponsesReasoningItems?: boolean;
|
|
359
375
|
/**
|
|
360
376
|
* Explicit opt-in for a relay that genuinely fronts OpenAI and can decode native
|
|
361
377
|
* compaction blobs. Absent or false degrades foreign blobs to an opaque note.
|
|
@@ -618,6 +634,8 @@ export interface OcxProviderConfig {
|
|
|
618
634
|
reasoningEfforts?: string[];
|
|
619
635
|
/** Model-specific Codex-visible reasoning tiers. An empty array means “do not expose effort”. */
|
|
620
636
|
modelReasoningEfforts?: Record<string, string[]>;
|
|
637
|
+
/** Catalog-only: do not synthesize a missing max rung for matching routed models. */
|
|
638
|
+
modelSuppressSyntheticMax?: Record<string, boolean>;
|
|
621
639
|
/** Model-specific default Codex reasoning tier; must also be present in the visible tier list. */
|
|
622
640
|
modelDefaultReasoningEfforts?: Record<string, string>;
|
|
623
641
|
/** Operator-owned effort override; none omits effort and uses the provider default. */
|
|
@@ -677,6 +695,23 @@ export interface OcxProviderConfig {
|
|
|
677
695
|
* apply_patch passthrough compatibility for OpenAI and unclassified gateways.
|
|
678
696
|
*/
|
|
679
697
|
supportsResponsesCustomTools?: boolean;
|
|
698
|
+
/**
|
|
699
|
+
* Hosted tool declarations this Responses destination rejects, so they are stripped from
|
|
700
|
+
* the request instead of being forwarded and 400'd.
|
|
701
|
+
*
|
|
702
|
+
* This is how an OpenAI-compatible gateway with a narrower capability set than OpenAI
|
|
703
|
+
* describes itself. Before it existed, a destination that accepted plain Responses and
|
|
704
|
+
* `function` tools but rejected hosted `web_search` could only be handled by adding a
|
|
705
|
+
* hard-coded baseUrl rule to `src/responses/hosted-tool-policy.ts`, so every such gateway
|
|
706
|
+
* needed a proxy release; a text-only prompt like "Reply exactly with OK" failed before
|
|
707
|
+
* the model answered because the hosted declaration travelled with it (#5002).
|
|
708
|
+
*
|
|
709
|
+
* Values come from `DECLARABLE_HOSTED_TOOL_TYPES`. Spelling variants of one capability
|
|
710
|
+
* are aliased, so `["web_search"]` also denies `web_search_preview`. Pair this with
|
|
711
|
+
* `supportsResponsesCustomTools: false` for a gateway that also rejects native custom
|
|
712
|
+
* tools; the two capabilities are independent and denied independently.
|
|
713
|
+
*/
|
|
714
|
+
unsupportedHostedTools?: string[];
|
|
680
715
|
/**
|
|
681
716
|
* Provider-local repair for Responses gateways whose lifecycle snapshots omit canonical
|
|
682
717
|
* fields or closing events (#893). Disabled by default and applied only to client-facing
|
|
@@ -882,6 +917,8 @@ export interface OcxProviderConfig {
|
|
|
882
917
|
* "cloud-code-assist" = Google Antigravity (Cloud Code Assist) OAuth + CCA envelope.
|
|
883
918
|
*/
|
|
884
919
|
googleMode?: "ai-studio" | "vertex" | "cloud-code-assist";
|
|
920
|
+
/** Google tool-schema compatibility policy. Omitted preserves compatible report-only behavior. */
|
|
921
|
+
googleToolSchemaPolicy?: "compatible" | "reject-lossy";
|
|
885
922
|
/** Vertex AI GCP project id (or GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT env). */
|
|
886
923
|
project?: string;
|
|
887
924
|
/** Vertex AI location, e.g. "us-central1" or "global" (or GOOGLE_CLOUD_LOCATION env). */
|
package/src/types/request.ts
CHANGED
|
@@ -68,6 +68,12 @@ export interface OcxParsedRequest {
|
|
|
68
68
|
_cursorConversationId?: string;
|
|
69
69
|
/** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
|
|
70
70
|
_clientThreadId?: string;
|
|
71
|
+
/**
|
|
72
|
+
* This request's OWN Codex thread id (`thread-id`), as opposed to `_clientThreadId`, which
|
|
73
|
+
* carries `x-codex-parent-thread-id` and is therefore shared by every parallel child of one
|
|
74
|
+
* parent. Only a surface that must distinguish siblings should read it.
|
|
75
|
+
*/
|
|
76
|
+
_codexOwnThreadId?: string;
|
|
71
77
|
/** True when promptCacheKey identifies a shared cache cohort rather than one conversation. */
|
|
72
78
|
_promptCacheKeyIsSharedCohort?: boolean;
|
|
73
79
|
/** Cursor-only thread owner; may be an opaque process-local Desktop session/thread identity. */
|
|
@@ -120,6 +126,8 @@ export interface OcxParsedRequest {
|
|
|
120
126
|
* (see src/responses/compaction.ts).
|
|
121
127
|
*/
|
|
122
128
|
_compactionRequest?: boolean;
|
|
129
|
+
/** Manual compaction moved to another provider: summarize portably even on a canonical ChatGPT target. */
|
|
130
|
+
_portableCompaction?: boolean;
|
|
123
131
|
/**
|
|
124
132
|
* True when the current request newly introduced a stored compaction summary/marker. Historical
|
|
125
133
|
* markers restored by previous_response_id expansion were already acknowledged and do not reset
|
|
@@ -312,7 +320,7 @@ export interface OcxProviderContinuationState {
|
|
|
312
320
|
}
|
|
313
321
|
|
|
314
322
|
export type AdapterEvent =
|
|
315
|
-
| { type: "heartbeat" }
|
|
323
|
+
| { type: "heartbeat"; replayUnsafe?: true }
|
|
316
324
|
| { type: "text_delta"; text: string; phase?: OcxMessagePhase }
|
|
317
325
|
| { type: "thinking_delta"; thinking: string }
|
|
318
326
|
// Anthropic extended-thinking round-trip: signature_delta for the current thinking block, and
|
package/src/types/tools.ts
CHANGED
|
@@ -67,6 +67,20 @@ const CODE_MODE_HELPER_TOOL_NAMES = [
|
|
|
67
67
|
*/
|
|
68
68
|
export const CODE_MODE_EXEC_TOOL_NAME = "exec";
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The nested-helper spellings, as a membership view of the same list.
|
|
72
|
+
*
|
|
73
|
+
* A code-mode catalog never DECLARES any of them — they exist only as `tools.<helper>(...)` inside
|
|
74
|
+
* `exec` — so a recorded call under one of these names can only have come from a provider echoing
|
|
75
|
+
* the helper, which is what makes the set usable as a bounded recovery vocabulary for stored
|
|
76
|
+
* history (#5095). Kept beside the tuple it is built from so the two can never drift; this is a
|
|
77
|
+
* different question from `NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES` below, which also covers `exec`
|
|
78
|
+
* itself because declaring THAT name is what turns normalization on.
|
|
79
|
+
*/
|
|
80
|
+
export const CODE_MODE_HELPER_WIRE_NAMES: ReadonlySet<string> = new Set<string>(
|
|
81
|
+
CODE_MODE_HELPER_TOOL_NAMES,
|
|
82
|
+
);
|
|
83
|
+
|
|
70
84
|
/**
|
|
71
85
|
* Spellings that may never be MANUFACTURED as a bare alias for a namespaced tool.
|
|
72
86
|
*
|
package/src/types/wire.ts
CHANGED
|
@@ -46,7 +46,15 @@ export const MODEL_ADAPTER_OVERRIDE_ALLOWED: ReadonlySet<string> = new Set([
|
|
|
46
46
|
* Anthropic for these models.
|
|
47
47
|
*/
|
|
48
48
|
const ANTHROPIC_WIRE_MODELS: Record<string, ReadonlySet<string>> = {
|
|
49
|
-
"opencode-go": new Set([
|
|
49
|
+
"opencode-go": new Set([
|
|
50
|
+
"minimax-m2.5",
|
|
51
|
+
"minimax-m2.7",
|
|
52
|
+
"minimax-m3",
|
|
53
|
+
// OpenCode's catalog identifies Union Alpha as @ai-sdk/anthropic while the
|
|
54
|
+
// provider defaults to OpenAI-compatible; direct Chat returns 500 and direct
|
|
55
|
+
// Messages reaches the session check (#4847).
|
|
56
|
+
"union-alpha",
|
|
57
|
+
]),
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
function anthropicWireModelsForProvider(providerName: string): ReadonlySet<string> | undefined {
|
package/src/types.ts
CHANGED
|
@@ -124,6 +124,16 @@ const META_MUSE_SPARK_13_CONTRIBUTOR: Cost4 = { input: 0.1, output: 0.2, cacheRe
|
|
|
124
124
|
const META_SPARK_SOURCE = `Meta Model API published price ${META_MODEL_PRICING}`;
|
|
125
125
|
const META_SPARK_CONTRIBUTOR_SOURCE = `Meta Model API published Contributor-tier price ${META_MODEL_PRICING}; data-sharing discount tier`;
|
|
126
126
|
const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after";
|
|
127
|
+
/*
|
|
128
|
+
* DeepSeek V4.1-Flash list prices (USD / 1M tokens), verified 2026-09-17 against
|
|
129
|
+
* https://api-docs.deepseek.com/quick_start/pricing. The page prices a peak window
|
|
130
|
+
* (09:30-24:00 Beijing) and an off-peak window; the tuple below is the peak-window
|
|
131
|
+
* list rate and the off-peak discount (0.15 / 0.60, cache-hit 0.003) is deliberately
|
|
132
|
+
* not baked in — the same rule as the Devin time-boxed promos. cacheWrite=0 follows
|
|
133
|
+
* the existing deepseek-chat / deepseek-reasoner rows: DeepSeek publishes no
|
|
134
|
+
* cache-write charge.
|
|
135
|
+
*/
|
|
136
|
+
const DEEPSEEK_V41_FLASH: Cost4 = { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 };
|
|
127
137
|
// Kimi official tables publish input/output/cache-hit only; cacheWrite is mapped to the
|
|
128
138
|
// cache-miss input price (Kimi auto-caches with no separate write billing). 2026-07-20 re-verified.
|
|
129
139
|
const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cacheWrite derived = input, Kimi auto-cache has no write billing)";
|
|
@@ -142,6 +152,14 @@ const BIGMODEL_NOTE = "z.ai international list price shown as estimate; domestic
|
|
|
142
152
|
// anywhere. Cache stays 0 rather than inheriting the reseller's 0.15 — a reseller number
|
|
143
153
|
// under a vendor-price label would be a wrong value wearing a verified badge.
|
|
144
154
|
const QWEN38_MAX_PRICING = "https://qwen.ai/blog?id=qwen3.8 (Qwen release announcement; no Model Studio billing row yet; cache rates unpublished -> 0)";
|
|
155
|
+
// Qwen-published qwen3.8-flash rate ($0.16 in / $0.47 out per 1M tokens) via the
|
|
156
|
+
// Qwen3.8 release announcement, corroborated by API-vendor price tables. No cache
|
|
157
|
+
// rate is published anywhere, so cache stays 0 rather than borrowing a reseller's
|
|
158
|
+
// number — the same hold QWEN38_MAX takes. The announcement marks API availability
|
|
159
|
+
// as coming soon, but the id is already served (and logged) on OpenCode Go, so the
|
|
160
|
+
// estimate applies to real usage rows now.
|
|
161
|
+
const QWEN38_FLASH: Cost4 = { input: 0.16, output: 0.47, cacheRead: 0, cacheWrite: 0 };
|
|
162
|
+
const QWEN38_FLASH_PRICING = "https://qwen.ai/blog?id=qwen3.8-2026 (Qwen release announcement; API marked coming soon at announcement; cache rates unpublished -> 0; input/output corroborated by https://docs.b.ai/guides/models/qwen/qwen3.8-flash)";
|
|
145
163
|
|
|
146
164
|
/*
|
|
147
165
|
* Cognition/Devin list prices (USD / 1M tokens), verified 2026-09-13 against the
|
|
@@ -289,6 +307,16 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
|
|
|
289
307
|
// for what that source does and does not cover.
|
|
290
308
|
{ provider: "alibaba-token-plan", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: QWEN38_MAX_PRICING, verifiedAt: "2026-08-04", status: "verified" },
|
|
291
309
|
{ provider: "alibaba-token-plan-intl", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: QWEN38_MAX_PRICING, verifiedAt: "2026-08-04", status: "verified" },
|
|
310
|
+
// OpenCode Go — five served ids with no jawcode bundle row and no vendor-level
|
|
311
|
+
// fallback (the fallback only searches jawcode metadata, never overlays), so the
|
|
312
|
+
// Usage estimated-cost column and the per-model breakdown rendered an em dash for
|
|
313
|
+
// every request through them. Each row reuses the vendor's own published list
|
|
314
|
+
// price as an estimate: Go itself is subscription-billed, hence verified-derived.
|
|
315
|
+
{ provider: "opencode-go", modelId: "qwen3.8-max", cost4: QWEN38_MAX, source: `vendor list price applied to the OpenCode Go surface; ${QWEN38_MAX_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
316
|
+
{ provider: "opencode-go", modelId: "qwen3.8-flash", cost4: QWEN38_FLASH, source: `vendor list price applied to the OpenCode Go surface; ${QWEN38_FLASH_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
317
|
+
{ provider: "opencode-go", modelId: "deepseek-v4.1-flash", cost4: DEEPSEEK_V41_FLASH, source: `peak-window list rate applied to the OpenCode Go surface (off-peak 0.15/0.60 + cache-hit 0.003 not baked in); ${DEEPSEEK_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
318
|
+
{ provider: "opencode-go", modelId: "glm-5.3-flash", cost4: GLM_53_FLASH, source: `z.ai list price applied to the OpenCode Go surface as an estimate; ${ZAI_PRICING}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
319
|
+
{ provider: "opencode-go", modelId: "muse-spark-1.3-contributor", cost4: META_MUSE_SPARK_13_CONTRIBUTOR, source: `Meta Model API Contributor-tier price applied to the OpenCode Go surface as an estimate; ${META_SPARK_CONTRIBUTOR_SOURCE}`, verifiedAt: "2026-09-17", status: "verified-derived" },
|
|
292
320
|
// Cursor Auto router — Cursor's published fixed token price (verified).
|
|
293
321
|
{ provider: "cursor", modelId: "auto", cost4: { input: 1.25, output: 6, cacheRead: 0.25, cacheWrite: 1.25 }, source: "https://docs.cursor.com/account/pricing + https://cursor.com/blog/aug-2025-pricing", verifiedAt: "2026-07-20", status: "verified" },
|
|
294
322
|
// Z.AI GLM family — the zai bundle's rows are all-zero upstream, and the four
|
package/src/usage/log.ts
CHANGED
|
@@ -77,6 +77,26 @@ export type AttemptRecoveryKind =
|
|
|
77
77
|
| "empty-completion"
|
|
78
78
|
| "reasoning-effort-downgrade";
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Why a recovery this request was otherwise willing to make did not happen.
|
|
82
|
+
*
|
|
83
|
+
* Recorded separately from `recoveryKinds` and from `sendCount`, because the question it
|
|
84
|
+
* answers is different from either. A log showing one physical send and no recovery kind used
|
|
85
|
+
* to be ambiguous: it could mean nothing was eligible, or that something was eligible and the
|
|
86
|
+
* send budget withheld it. Those need opposite follow-ups, and the second one was invisible
|
|
87
|
+
* (#5044).
|
|
88
|
+
*
|
|
89
|
+
* `sendCount` deliberately does not move for these. A refused attempt is not a physical send,
|
|
90
|
+
* and inflating the count to signal the refusal would corrupt the one number that means
|
|
91
|
+
* "requests this proxy actually made".
|
|
92
|
+
*
|
|
93
|
+
* Bounded vocabulary on purpose: it is a wire value a maintainer reads, never a credential, an
|
|
94
|
+
* account id, an upstream body, prompt content, or exception text.
|
|
95
|
+
*/
|
|
96
|
+
export type AttemptRecoveryWithheld =
|
|
97
|
+
| "retry-send-budget"
|
|
98
|
+
| "rotation-send-budget";
|
|
99
|
+
|
|
80
100
|
/** Request-time upstream credential class, never a credential or account identifier. */
|
|
81
101
|
export type UsageCredentialSource = "grok-oauth" | "xai-api-key";
|
|
82
102
|
|
|
@@ -139,6 +159,11 @@ export interface PersistedUsageAttempt {
|
|
|
139
159
|
firstOutputMs?: number;
|
|
140
160
|
sendCount: number;
|
|
141
161
|
recoveryKinds: AttemptRecoveryKind[];
|
|
162
|
+
/**
|
|
163
|
+
* Recoveries this attempt was eligible for and did not make. Absent on ordinary attempts so
|
|
164
|
+
* old rows keep their exact shape.
|
|
165
|
+
*/
|
|
166
|
+
recoveryWithheld?: AttemptRecoveryWithheld[];
|
|
142
167
|
usageStatus: UsageStatus;
|
|
143
168
|
/**
|
|
144
169
|
* True when the proxy answered this turn locally and issued no upstream request. It travels on
|
|
@@ -487,6 +512,10 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
|
|
|
487
512
|
"empty-completion",
|
|
488
513
|
"reasoning-effort-downgrade",
|
|
489
514
|
]);
|
|
515
|
+
const ATTEMPT_RECOVERY_WITHHELD = new Set<AttemptRecoveryWithheld>([
|
|
516
|
+
"retry-send-budget",
|
|
517
|
+
"rotation-send-budget",
|
|
518
|
+
]);
|
|
490
519
|
const USAGE_STATUSES = new Set<UsageStatus>([
|
|
491
520
|
"reported",
|
|
492
521
|
"unreported",
|
|
@@ -620,6 +649,14 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
620
649
|
&& ATTEMPT_RECOVERY_KINDS.has(value as AttemptRecoveryKind),
|
|
621
650
|
))]
|
|
622
651
|
: [];
|
|
652
|
+
// Same shape as `recoveryKinds`: unknown values are dropped rather than failing the row, so a
|
|
653
|
+
// log written by a newer build stays readable by an older one.
|
|
654
|
+
const recoveryWithheld = Array.isArray(attempt.recoveryWithheld)
|
|
655
|
+
? [...new Set(attempt.recoveryWithheld.filter(
|
|
656
|
+
(value): value is AttemptRecoveryWithheld => typeof value === "string"
|
|
657
|
+
&& ATTEMPT_RECOVERY_WITHHELD.has(value as AttemptRecoveryWithheld),
|
|
658
|
+
))]
|
|
659
|
+
: [];
|
|
623
660
|
return {
|
|
624
661
|
ordinal: attempt.ordinal as number,
|
|
625
662
|
provider: attempt.provider,
|
|
@@ -639,6 +676,7 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
639
676
|
: {}),
|
|
640
677
|
sendCount: attempt.sendCount as number,
|
|
641
678
|
recoveryKinds,
|
|
679
|
+
...(recoveryWithheld.length ? { recoveryWithheld } : {}),
|
|
642
680
|
usageStatus: attempt.usageStatus as UsageStatus,
|
|
643
681
|
...(isCodexUsageAccountLogLabel(attempt.accountLogLabel)
|
|
644
682
|
? { accountLogLabel: attempt.accountLogLabel }
|
|
@@ -852,18 +890,61 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
852
890
|
};
|
|
853
891
|
}
|
|
854
892
|
|
|
855
|
-
|
|
893
|
+
// Bound hot-path filesystem hardening to once per second while ensuring an external mode
|
|
894
|
+
// widening cannot suppress write-triggered repair for the lifetime of the process.
|
|
895
|
+
const USAGE_LOG_PERMISSION_RECHECK_MS = 1_000;
|
|
896
|
+
|
|
897
|
+
type UsageLogPermissionCheck = {
|
|
898
|
+
path: string;
|
|
899
|
+
checkedAt: number;
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
let ensuredUsageLogDir: UsageLogPermissionCheck | null = null;
|
|
903
|
+
let ensuredUsageLogFile: UsageLogPermissionCheck | null = null;
|
|
904
|
+
|
|
905
|
+
function usageLogPermissionCheckIsCurrent(
|
|
906
|
+
check: UsageLogPermissionCheck | null,
|
|
907
|
+
path: string,
|
|
908
|
+
now: number,
|
|
909
|
+
): boolean {
|
|
910
|
+
return check?.path === path
|
|
911
|
+
&& now >= check.checkedAt
|
|
912
|
+
&& now - check.checkedAt < USAGE_LOG_PERMISSION_RECHECK_MS;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function ensureUsageLogDir(now: number): void {
|
|
856
916
|
const dir = getConfigDir();
|
|
917
|
+
if (usageLogPermissionCheckIsCurrent(ensuredUsageLogDir, dir, now)) return;
|
|
857
918
|
recordOwnedConfigPath(dir, usageLogPath());
|
|
858
919
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
859
920
|
try { chmodSync(dir, 0o700); } catch { /* best-effort on platforms that ignore chmod */ }
|
|
921
|
+
ensuredUsageLogDir = { path: dir, checkedAt: now };
|
|
860
922
|
}
|
|
861
923
|
|
|
862
924
|
export function appendUsageEntry(entry: PersistedUsageEntry): void {
|
|
863
|
-
|
|
925
|
+
const line = `${JSON.stringify(normalizeUsageEntry(entry))}\n`;
|
|
864
926
|
const path = usageLogPath();
|
|
865
|
-
|
|
866
|
-
|
|
927
|
+
const now = Date.now();
|
|
928
|
+
const doAppend = (): void => {
|
|
929
|
+
ensureUsageLogDir(now);
|
|
930
|
+
const filePermissionsCurrent = usageLogPermissionCheckIsCurrent(ensuredUsageLogFile, path, now);
|
|
931
|
+
appendFileSync(path, line, { encoding: "utf-8", mode: 0o600 });
|
|
932
|
+
if (!filePermissionsCurrent) {
|
|
933
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort on platforms that ignore chmod */ }
|
|
934
|
+
ensuredUsageLogFile = { path, checkedAt: now };
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
try {
|
|
938
|
+
doAppend();
|
|
939
|
+
} catch (error: any) {
|
|
940
|
+
if (error?.code === "ENOENT") {
|
|
941
|
+
ensuredUsageLogDir = null;
|
|
942
|
+
ensuredUsageLogFile = null;
|
|
943
|
+
doAppend();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
867
948
|
}
|
|
868
949
|
|
|
869
950
|
export type UsageLogRevision = {
|
|
@@ -1057,6 +1138,8 @@ export function resetUsageReadCacheForTests(): void {
|
|
|
1057
1138
|
managementUsageReadInflight?.abort.abort();
|
|
1058
1139
|
managementUsageReadInflight = null;
|
|
1059
1140
|
retainedUsageSnapshot = null;
|
|
1141
|
+
ensuredUsageLogDir = null;
|
|
1142
|
+
ensuredUsageLogFile = null;
|
|
1060
1143
|
}
|
|
1061
1144
|
|
|
1062
1145
|
function readExactly(fd: number, length: number, position: number): Buffer | null {
|
|
@@ -35,6 +35,54 @@ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-dest
|
|
|
35
35
|
*/
|
|
36
36
|
export type VisionSidecarBackend = "openai" | "anthropic" | "routed";
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Input modalities an explicit custom row declares for one routed model.
|
|
40
|
+
*
|
|
41
|
+
* A custom row is the operator's own definition of that model, so its declaration outranks the
|
|
42
|
+
* provider-level hints the predicates below read. Without this the two halves of one config
|
|
43
|
+
* disagreed in production: the catalog overlay in `src/codex/catalog/routed-gather.ts` copies
|
|
44
|
+
* `customModels[].inputModalities` onto the row and the dashboard showed "text, image", while
|
|
45
|
+
* the request path consulted only `providers[].noVisionModels` / `modelInputModalities` and
|
|
46
|
+
* stripped the image before dispatch. A model the operator had declared image-capable therefore
|
|
47
|
+
* received an omission marker instead of its attachment.
|
|
48
|
+
*
|
|
49
|
+
* Precedence, highest first: `modelCapabilities` (the documented per-model capability
|
|
50
|
+
* declaration), then this custom-row declaration, then `noVisionModels`, then
|
|
51
|
+
* `modelInputModalities`, then registry/vendor metadata. `modelCapabilities` stays on top
|
|
52
|
+
* because it is the dedicated capability axis and the CLI `--text-only` flag writes it; when the
|
|
53
|
+
* two explicit forms contradict each other the more specific axis wins.
|
|
54
|
+
*
|
|
55
|
+
* Matching is exact on the routed identity — provider name and native model id, the pair the
|
|
56
|
+
* custom-model API keys rows by. A row that declares no modalities returns `undefined` rather
|
|
57
|
+
* than `["text"]`, so it stays silent instead of turning into a text-only claim.
|
|
58
|
+
*/
|
|
59
|
+
export function customRowInputModalities(
|
|
60
|
+
config: Pick<OcxConfig, "customModels">,
|
|
61
|
+
providerName: string,
|
|
62
|
+
modelId: string,
|
|
63
|
+
): string[] | undefined {
|
|
64
|
+
for (const row of config.customModels ?? []) {
|
|
65
|
+
if (row.provider !== providerName || row.modelId !== modelId) continue;
|
|
66
|
+
if (Array.isArray(row.inputModalities) && row.inputModalities.length > 0) {
|
|
67
|
+
return [...row.inputModalities];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The custom row's verdict for one model, in the shape the predicates need:
|
|
75
|
+
* `true`/`false` when the row declares modalities, `undefined` when it declares none.
|
|
76
|
+
*/
|
|
77
|
+
function customRowAcceptsImageInput(
|
|
78
|
+
config: Pick<OcxConfig, "customModels">,
|
|
79
|
+
providerName: string,
|
|
80
|
+
modelId: string,
|
|
81
|
+
): boolean | undefined {
|
|
82
|
+
const declared = customRowInputModalities(config, providerName, modelId);
|
|
83
|
+
return declared === undefined ? undefined : declared.includes("image");
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
/** The two sides every deployment has; also the empty-auth fallback set. */
|
|
39
87
|
export type UniversalVisionBackend = "openai" | "anthropic";
|
|
40
88
|
|
|
@@ -60,6 +108,14 @@ export interface VisionCandidateModel {
|
|
|
60
108
|
native?: boolean;
|
|
61
109
|
}
|
|
62
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The config slice the capability predicates need. `customModels` is optional so provider-only
|
|
113
|
+
* callers and unit tests keep compiling; a caller that omits it is simply silent about custom
|
|
114
|
+
* rows rather than wrong about them.
|
|
115
|
+
*/
|
|
116
|
+
export type VisionCapabilityConfig =
|
|
117
|
+
Pick<OcxConfig, "providers"> & { customModels?: OcxConfig["customModels"] };
|
|
118
|
+
|
|
63
119
|
export interface VisionModelOption {
|
|
64
120
|
value: string;
|
|
65
121
|
label: string;
|
|
@@ -142,20 +198,31 @@ function enrichedProviderForVision(
|
|
|
142
198
|
}
|
|
143
199
|
|
|
144
200
|
function isVisionSidecarConsumerWithCache(
|
|
145
|
-
config:
|
|
201
|
+
config: VisionCapabilityConfig,
|
|
146
202
|
providerName: string,
|
|
147
203
|
modelId: string,
|
|
148
204
|
cache: EnrichedProviderCache,
|
|
149
205
|
): boolean {
|
|
150
206
|
const provider = enrichedProviderForVision(config, providerName, cache);
|
|
151
|
-
|
|
207
|
+
if (provider === undefined) return false;
|
|
208
|
+
// Documented precedence, highest first: the dedicated per-model capability axis, then the
|
|
209
|
+
// operator's own custom row, then the provider-level hints. Reading the custom row ahead of
|
|
210
|
+
// `modelCapabilities` would let it override the more specific axis.
|
|
211
|
+
const capabilityDeclared = Object.hasOwn(provider.modelCapabilities ?? {}, modelId)
|
|
212
|
+
? provider.modelCapabilities?.[modelId]?.inputModalities : undefined;
|
|
213
|
+
if (capabilityDeclared !== undefined) {
|
|
214
|
+
return capabilityDeclared.includes("text") && !capabilityDeclared.includes("image");
|
|
215
|
+
}
|
|
216
|
+
const customDeclared = customRowInputModalities(config, providerName, modelId);
|
|
217
|
+
if (customDeclared !== undefined) return customDeclared.includes("text") && !customDeclared.includes("image");
|
|
218
|
+
return isModelVisionSidecarConsumer(provider, modelId);
|
|
152
219
|
}
|
|
153
220
|
|
|
154
221
|
/**
|
|
155
222
|
* Is this model listed as one the sidecar describes FOR? Such a model cannot be
|
|
156
223
|
* the describer, and its advertised modalities are untrustworthy.
|
|
157
224
|
*/
|
|
158
|
-
export function isVisionSidecarConsumer(config:
|
|
225
|
+
export function isVisionSidecarConsumer(config: VisionCapabilityConfig, providerName: string, modelId: string): boolean {
|
|
159
226
|
return isVisionSidecarConsumerWithCache(config, providerName, modelId, new Map());
|
|
160
227
|
}
|
|
161
228
|
|
|
@@ -165,23 +232,30 @@ export function isVisionSidecarConsumer(config: Pick<OcxConfig, "providers">, pr
|
|
|
165
232
|
* which callers treat as eligible.
|
|
166
233
|
*/
|
|
167
234
|
export function modelAcceptsImageInput(
|
|
168
|
-
config:
|
|
235
|
+
config: VisionCapabilityConfig,
|
|
169
236
|
candidate: VisionCandidateModel,
|
|
170
237
|
): boolean | undefined {
|
|
171
238
|
return modelAcceptsImageInputWithCache(config, candidate, new Map());
|
|
172
239
|
}
|
|
173
240
|
|
|
174
241
|
function modelAcceptsImageInputWithCache(
|
|
175
|
-
config:
|
|
242
|
+
config: VisionCapabilityConfig,
|
|
176
243
|
candidate: VisionCandidateModel,
|
|
177
244
|
cache: EnrichedProviderCache,
|
|
178
245
|
): boolean | undefined {
|
|
179
246
|
if (candidate.native === true || (candidate.provider === "openai" && SUPPORTED_NATIVE_OPENAI_SLUGS.has(candidate.id))) {
|
|
180
247
|
const nativeProvider = enrichedProviderForVision(config, candidate.provider, cache);
|
|
181
|
-
if (nativeProvider && isModelVisionSidecarConsumer(nativeProvider, candidate.id)) return false;
|
|
182
248
|
const declared = Object.hasOwn(nativeProvider?.modelCapabilities ?? {}, candidate.id)
|
|
183
249
|
? nativeProvider?.modelCapabilities?.[candidate.id]?.inputModalities : undefined;
|
|
184
250
|
if (declared !== undefined) return declared.includes("image");
|
|
251
|
+
// The catalog already lets an explicit custom row replace the native modality list for the
|
|
252
|
+
// same slug; consult it here too or the row would advertise what the request path ignores.
|
|
253
|
+
const fromCustomRow = customRowAcceptsImageInput(config, candidate.provider, candidate.id);
|
|
254
|
+
if (fromCustomRow !== undefined) return fromCustomRow;
|
|
255
|
+
// The sidecar hints come after both explicit declarations, matching the documented order.
|
|
256
|
+
// Ahead of them a `noVisionModels` membership would short-circuit to text-only before the
|
|
257
|
+
// operator's own row was read.
|
|
258
|
+
if (nativeProvider && isModelVisionSidecarConsumer(nativeProvider, candidate.id)) return false;
|
|
185
259
|
return advertisesImageInput(nativeInputModalities(candidate.id)) ?? true;
|
|
186
260
|
}
|
|
187
261
|
if (isVisionSidecarConsumerWithCache(config, candidate.provider, candidate.id, cache)) return false;
|
|
@@ -189,6 +263,11 @@ function modelAcceptsImageInputWithCache(
|
|
|
189
263
|
const declared = Object.hasOwn(provider?.modelCapabilities ?? {}, candidate.id)
|
|
190
264
|
? provider?.modelCapabilities?.[candidate.id]?.inputModalities : undefined;
|
|
191
265
|
if (declared !== undefined) return declared.includes("image");
|
|
266
|
+
// Ahead of the provider's own hints: this row is the operator's definition of this exact model,
|
|
267
|
+
// and the catalog overlay reads the same field. Behind modelCapabilities, which is the dedicated
|
|
268
|
+
// capability axis the CLI `--text-only` writes.
|
|
269
|
+
const fromCustomRow = customRowAcceptsImageInput(config, candidate.provider, candidate.id);
|
|
270
|
+
if (fromCustomRow !== undefined) return fromCustomRow;
|
|
192
271
|
const configuredModalities = provider ? modelRecordValue(provider.modelInputModalities, candidate.id) : undefined;
|
|
193
272
|
const fromConfiguredModalities = advertisesImageInput(configuredModalities);
|
|
194
273
|
if (fromConfiguredModalities !== undefined) return fromConfiguredModalities;
|
|
@@ -206,14 +285,14 @@ function modelAcceptsImageInputWithCache(
|
|
|
206
285
|
|
|
207
286
|
/** Eligible = not a sidecar consumer, and not positively known to be text-only. */
|
|
208
287
|
export function isVisionEligibleModel(
|
|
209
|
-
config:
|
|
288
|
+
config: VisionCapabilityConfig,
|
|
210
289
|
candidate: VisionCandidateModel,
|
|
211
290
|
): boolean {
|
|
212
291
|
return isVisionEligibleModelWithCache(config, candidate, new Map());
|
|
213
292
|
}
|
|
214
293
|
|
|
215
294
|
function isVisionEligibleModelWithCache(
|
|
216
|
-
config:
|
|
295
|
+
config: VisionCapabilityConfig,
|
|
217
296
|
candidate: VisionCandidateModel,
|
|
218
297
|
cache: EnrichedProviderCache,
|
|
219
298
|
): boolean {
|
|
@@ -274,7 +353,7 @@ function baselineCandidate(
|
|
|
274
353
|
* backend, and only `value` reaches the client, so first-wins costs nothing.
|
|
275
354
|
*/
|
|
276
355
|
export function visionEligibleModelOptions(
|
|
277
|
-
config:
|
|
356
|
+
config: VisionCapabilityConfig,
|
|
278
357
|
candidates: readonly VisionCandidateModel[],
|
|
279
358
|
enabledBackends: readonly VisionSidecarBackend[],
|
|
280
359
|
anthropicProviderName?: string,
|
package/src/vision/plan.ts
CHANGED
|
@@ -5,7 +5,11 @@ import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
|
|
|
5
5
|
import type { CodexAuthPolicyConfig } from "../codex/auth-context";
|
|
6
6
|
import { isCodexReserveRequestEligible } from "../codex/loopback-target";
|
|
7
7
|
import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
customRowInputModalities,
|
|
10
|
+
isModelVisionSidecarConsumer as isModelTextOnly,
|
|
11
|
+
modelAcceptsImageInput,
|
|
12
|
+
} from "./eligibility";
|
|
9
13
|
import { normalizeVisionReasoningForModel } from "./reasoning";
|
|
10
14
|
import { resolveSidecarAuth } from "../sidecar/auth";
|
|
11
15
|
import { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds";
|
|
@@ -95,23 +99,40 @@ function messagesHaveImage(parsed: OcxParsedRequest): boolean {
|
|
|
95
99
|
/**
|
|
96
100
|
* Direct-image admission for a routed target. Returns true when capability evidence proves the
|
|
97
101
|
* target cannot accept image input, so the caller must describe or strip the image first.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
102
|
+
* Evidence is consulted highest-first: `modelCapabilities` (the dedicated per-model capability
|
|
103
|
+
* axis), an explicit custom row for the same routed identity, `noVisionModels`, an explicit
|
|
104
|
+
* per-model modality list without `image`, and finally proven-negative registry/vendor metadata.
|
|
105
|
+
* Any of them can require the vision preprocessor. A genuinely unknown custom model is NOT
|
|
106
|
+
* guessed blind: it keeps the established pass-through behaviour. The provider-only fallback
|
|
107
|
+
* keeps legacy unit callers stable; production dispatch always supplies providerName so the
|
|
108
|
+
* complete capability chain is consulted.
|
|
103
109
|
*/
|
|
104
110
|
export function requiresVisionPreprocessing(
|
|
105
|
-
config: Pick<OcxConfig, "providers"
|
|
111
|
+
config: Pick<OcxConfig, "providers"> & { customModels?: OcxConfig["customModels"] },
|
|
106
112
|
provider: Pick<OcxProviderConfig, "noVisionModels" | "modelInputModalities" | "modelCapabilities">,
|
|
107
113
|
modelId: string,
|
|
108
114
|
providerName?: string,
|
|
109
115
|
): boolean {
|
|
110
|
-
if (isModelTextOnly(provider, modelId)) return true;
|
|
111
116
|
const runtimeDeclared = Object.hasOwn(provider.modelCapabilities ?? {}, modelId)
|
|
112
117
|
? provider.modelCapabilities?.[modelId]?.inputModalities
|
|
113
118
|
: undefined;
|
|
119
|
+
// `modelCapabilities` is the dedicated per-model capability axis, so it outranks everything
|
|
120
|
+
// below — including an explicit custom row that disagrees. The CLI `--text-only` flag writes it.
|
|
114
121
|
if (runtimeDeclared !== undefined) return !runtimeDeclared.includes("image");
|
|
122
|
+
// An explicit custom row outranks the provider-level hints below, mirroring the catalog overlay
|
|
123
|
+
// in `src/codex/catalog/routed-gather.ts` that already copies this same declaration onto the
|
|
124
|
+
// advertised row. Without it the dashboard advertised "text, image" from the operator's own row
|
|
125
|
+
// while this predicate stripped the image before dispatch.
|
|
126
|
+
const customDeclared = providerName === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: customRowInputModalities(config, providerName, modelId);
|
|
129
|
+
// Same rule as the `modelCapabilities` branch above: the declaration answers "can this model
|
|
130
|
+
// take an image", not "is it a text model". A row that lists only `audio` or `video` excludes
|
|
131
|
+
// image input just as `["text"]` does, and `modelAcceptsImageInput` already answers "no image"
|
|
132
|
+
// for it; the narrower `includes("text")` test here made the two predicates disagree and sent
|
|
133
|
+
// the attachment to a model that cannot read it.
|
|
134
|
+
if (customDeclared !== undefined) return !customDeclared.includes("image");
|
|
135
|
+
if (isModelTextOnly(provider, modelId)) return true;
|
|
115
136
|
const runtimeModalities = modelRecordValue(provider.modelInputModalities, modelId);
|
|
116
137
|
if (Array.isArray(runtimeModalities) && runtimeModalities.length > 0) {
|
|
117
138
|
return !runtimeModalities.includes("image");
|
|
@@ -129,9 +150,12 @@ function usableRoutedVisionModel(config: OcxConfig): string | undefined {
|
|
|
129
150
|
if (!routedModel || sep <= 0) return undefined;
|
|
130
151
|
const targetProvider = routedModel.slice(0, sep);
|
|
131
152
|
const targetId = routedModel.slice(sep + 1);
|
|
132
|
-
|
|
153
|
+
// `modelAcceptsImageInput` is the one predicate here: it resolves the whole capability chain,
|
|
154
|
+
// custom row included. The provider-only `isModelTextOnly` that used to be ANDed in could only
|
|
155
|
+
// subtract — it never saw a custom row, so a describer the operator had declared image-capable
|
|
156
|
+
// was refused for a provider hint that same row overrides.
|
|
133
157
|
return modelAcceptsImageInput(config, { provider: targetProvider, id: targetId }) !== false
|
|
134
|
-
|
|
158
|
+
? routedModel : undefined;
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
export function shouldResolveOpenAiVisionSidecar(
|