@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- 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 +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- 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/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- 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/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- 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-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- 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/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- 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/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- 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/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -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 +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
// This migration is deliberately NOT general reconciliation. It rewrites exactly
|
|
15
15
|
// one thing: an id this file declares retired, on a provider that still carries
|
|
16
16
|
// the registry's transport, and only when the registry currently seeds the
|
|
17
|
-
// replacement
|
|
17
|
+
// replacement — and it skips a field where the retired entry is registry residue
|
|
18
|
+
// with nothing left to carry forward (see `isRegistryResidue`, issue #5066).
|
|
19
|
+
// Everything else in the row is left alone.
|
|
18
20
|
|
|
19
21
|
import { PROVIDER_REGISTRY } from "./registry";
|
|
22
|
+
import { MODEL_KEYED_RECORDS, MODEL_ID_LISTS, MODEL_ID_SCALARS, MODEL_NESTED_RECORDS } from "./model-rename-fields";
|
|
23
|
+
import { providerConfigSeed } from "./derive";
|
|
20
24
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
21
25
|
|
|
22
26
|
export interface ModelRename {
|
|
@@ -42,6 +46,52 @@ export interface ModelRename {
|
|
|
42
46
|
* but lose the reasoning picker entirely.
|
|
43
47
|
*/
|
|
44
48
|
dropReasoningEffortMap?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Remove both the retired id and its replacement from `noReasoningModels`.
|
|
51
|
+
*
|
|
52
|
+
* Use this only when the rename also marks a capability change: carrying the old
|
|
53
|
+
* no-reasoning classification onto a newly adjustable alias would keep the picker
|
|
54
|
+
* disabled after the model id itself was repaired.
|
|
55
|
+
*/
|
|
56
|
+
dropNoReasoningModels?: boolean;
|
|
57
|
+
/** Refresh exact registry defaults already saved under the replacement id. */
|
|
58
|
+
targetSeedRefresh?: {
|
|
59
|
+
contextWindow?: { from: number; to: number };
|
|
60
|
+
reasoning?: {
|
|
61
|
+
fromEfforts: readonly string[];
|
|
62
|
+
toEfforts: readonly string[];
|
|
63
|
+
defaultEffort: string;
|
|
64
|
+
effortMap: Readonly<Record<string, string>>;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const KIMI_K28_ALIAS = "kimi-for-coding";
|
|
70
|
+
const KIMI_RETIRED_CODING_IDS = [
|
|
71
|
+
"kimi-k2.7-code",
|
|
72
|
+
"kimi-k2.7-code-highspeed",
|
|
73
|
+
"kimi-k2.6",
|
|
74
|
+
"kimi-k2.5",
|
|
75
|
+
] as const;
|
|
76
|
+
const KIMI_K28_TARGET_SEED_REFRESH: NonNullable<ModelRename["targetSeedRefresh"]> = {
|
|
77
|
+
contextWindow: { from: 262_144, to: 1_048_576 },
|
|
78
|
+
reasoning: {
|
|
79
|
+
fromEfforts: [],
|
|
80
|
+
toEfforts: ["low", "high", "max"],
|
|
81
|
+
defaultEffort: "max",
|
|
82
|
+
effortMap: { none: "none", low: "low", medium: "high", high: "high", xhigh: "max", max: "max" },
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function kimiCodingRenames(provider: "kimi" | "kimi-code", endpoint: string): ModelRename[] {
|
|
87
|
+
return KIMI_RETIRED_CODING_IDS.map(from => ({
|
|
88
|
+
provider,
|
|
89
|
+
from,
|
|
90
|
+
to: KIMI_K28_ALIAS,
|
|
91
|
+
reason: `Moonshot retired the k2.x coding ids from the ${endpoint}; kimi-for-coding is the stable alias the endpoint still serves`,
|
|
92
|
+
dropNoReasoningModels: true,
|
|
93
|
+
targetSeedRefresh: KIMI_K28_TARGET_SEED_REFRESH,
|
|
94
|
+
}));
|
|
45
95
|
}
|
|
46
96
|
|
|
47
97
|
/**
|
|
@@ -62,6 +112,14 @@ export const MODEL_RENAMES: readonly ModelRename[] = [
|
|
|
62
112
|
to: "qwen3.8-max",
|
|
63
113
|
reason: "Alibaba shipped Qwen3.8-Max as stable and documents the preview endpoint as liable to be taken offline once preview concludes",
|
|
64
114
|
},
|
|
115
|
+
// Kimi coding renames. Moonshot retired the k2.x ids from the subscription/coding
|
|
116
|
+
// endpoint when K2.8 Preview shipped (live /coding/v1/models lists only
|
|
117
|
+
// kimi-for-coding[-highspeed], k3, k3-256k); kimi-for-coding is the stable alias the
|
|
118
|
+
// endpoint still serves and currently routes to K2.8 Preview. The registry picker no
|
|
119
|
+
// longer seeds the retired ids, so a saved defaultModel naming one is a dead selection
|
|
120
|
+
// rather than a merely outdated one.
|
|
121
|
+
...kimiCodingRenames("kimi", "subscription endpoint after K2.8 Preview shipped"),
|
|
122
|
+
...kimiCodingRenames("kimi-code", "coding endpoint after K2.8 Preview shipped"),
|
|
65
123
|
// Antigravity Flash generations. Google takes the previous Flash model off Cloud Code
|
|
66
124
|
// Assist almost immediately when the next ships, so a saved 3.6 (or older 3.5) id is a
|
|
67
125
|
// dead selection rather than a merely outdated one. Routing already redirects these ids
|
|
@@ -80,40 +138,6 @@ export const MODEL_RENAMES: readonly ModelRename[] = [
|
|
|
80
138
|
})),
|
|
81
139
|
];
|
|
82
140
|
|
|
83
|
-
/** Provider fields that key metadata by model id. */
|
|
84
|
-
const MODEL_KEYED_RECORDS = [
|
|
85
|
-
"modelContextWindows",
|
|
86
|
-
"modelMaxOutputTokens",
|
|
87
|
-
"modelInputModalities",
|
|
88
|
-
"modelReasoningEfforts",
|
|
89
|
-
"modelSuppressSyntheticMax",
|
|
90
|
-
"modelDefaultReasoningEfforts",
|
|
91
|
-
"modelReasoningEffortMap",
|
|
92
|
-
] as const;
|
|
93
|
-
|
|
94
|
-
/** Provider fields that are flat lists of model ids. */
|
|
95
|
-
const MODEL_ID_LISTS = [
|
|
96
|
-
"models",
|
|
97
|
-
// A retired id left here is worse than a stale label: `filterCatalogModels` treats
|
|
98
|
-
// `selectedModels` as an exact-match allowlist, so a user who allowlisted only the
|
|
99
|
-
// retired model gets NO replacement row at all — the model silently vanishes from
|
|
100
|
-
// their catalog instead of being renamed. OAuth reconciliation does not cover this
|
|
101
|
-
// field, so the rename has to.
|
|
102
|
-
"selectedModels",
|
|
103
|
-
// Same reasoning as `selectedModels`: a retired id pinned here would be resurrected as a
|
|
104
|
-
// ghost row on every discovery instead of following the rename (#1690).
|
|
105
|
-
"retainModels",
|
|
106
|
-
"noVisionModels",
|
|
107
|
-
"noReasoningModels",
|
|
108
|
-
"noTemperatureModels",
|
|
109
|
-
"noTopPModels",
|
|
110
|
-
"noPenaltyModels",
|
|
111
|
-
"autoToolChoiceOnlyModels",
|
|
112
|
-
"preserveReasoningContentModels",
|
|
113
|
-
"thinkingBudgetModels",
|
|
114
|
-
"directReasoningEffortModels",
|
|
115
|
-
] as const;
|
|
116
|
-
|
|
117
141
|
function renameInList(value: unknown, from: string, to: string): string[] | null {
|
|
118
142
|
if (!Array.isArray(value) || !value.includes(from)) return null;
|
|
119
143
|
const seen = new Set<string>();
|
|
@@ -130,6 +154,18 @@ function renameInList(value: unknown, from: string, to: string): string[] | null
|
|
|
130
154
|
return next;
|
|
131
155
|
}
|
|
132
156
|
|
|
157
|
+
function dropRenamedIdsFromList(
|
|
158
|
+
value: unknown,
|
|
159
|
+
from: string,
|
|
160
|
+
to: string,
|
|
161
|
+
dropStaleTarget: boolean,
|
|
162
|
+
): string[] | null {
|
|
163
|
+
if (!Array.isArray(value)) return null;
|
|
164
|
+
const hasRetired = value.includes(from);
|
|
165
|
+
if (!hasRetired && !(dropStaleTarget && value.includes(to))) return null;
|
|
166
|
+
return value.filter(entry => typeof entry === "string" && entry !== from && entry !== to);
|
|
167
|
+
}
|
|
168
|
+
|
|
133
169
|
function renameInRecord(value: unknown, from: string, to: string): Record<string, unknown> | null {
|
|
134
170
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
135
171
|
const record = value as Record<string, unknown>;
|
|
@@ -157,6 +193,42 @@ function dropFromRecord(value: unknown, from: string): Record<string, unknown> |
|
|
|
157
193
|
return next;
|
|
158
194
|
}
|
|
159
195
|
|
|
196
|
+
function sameStringArray(value: unknown, expected: readonly string[]): boolean {
|
|
197
|
+
return Array.isArray(value)
|
|
198
|
+
&& value.length === expected.length
|
|
199
|
+
&& value.every((entry, index) => entry === expected[index]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function targetReasoningMatchesStaleSeed(row: Record<string, unknown>, rename: ModelRename): boolean {
|
|
203
|
+
const reasoning = rename.targetSeedRefresh?.reasoning;
|
|
204
|
+
const efforts = row.modelReasoningEfforts as Record<string, unknown> | undefined;
|
|
205
|
+
return !!reasoning && !!efforts && sameStringArray(efforts[rename.to], reasoning.fromEfforts);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Refresh only exact defaults emitted by the previous registry; preserve user overrides. */
|
|
209
|
+
function refreshTargetSeed(row: Record<string, unknown>, rename: ModelRename): boolean {
|
|
210
|
+
const refresh = rename.targetSeedRefresh;
|
|
211
|
+
if (!refresh) return false;
|
|
212
|
+
let changed = false;
|
|
213
|
+
|
|
214
|
+
const windows = row.modelContextWindows as Record<string, unknown> | undefined;
|
|
215
|
+
if (windows && refresh.contextWindow && windows[rename.to] === refresh.contextWindow.from) {
|
|
216
|
+
windows[rename.to] = refresh.contextWindow.to;
|
|
217
|
+
changed = true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const reasoning = refresh.reasoning;
|
|
221
|
+
const efforts = row.modelReasoningEfforts as Record<string, unknown> | undefined;
|
|
222
|
+
if (!reasoning || !efforts || !sameStringArray(efforts[rename.to], reasoning.fromEfforts)) return changed;
|
|
223
|
+
efforts[rename.to] = [...reasoning.toEfforts];
|
|
224
|
+
|
|
225
|
+
const defaults = (row.modelDefaultReasoningEfforts ??= {}) as Record<string, unknown>;
|
|
226
|
+
if (!(rename.to in defaults)) defaults[rename.to] = reasoning.defaultEffort;
|
|
227
|
+
const maps = (row.modelReasoningEffortMap ??= {}) as Record<string, unknown>;
|
|
228
|
+
if (!(rename.to in maps)) maps[rename.to] = { ...reasoning.effortMap };
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
160
232
|
/**
|
|
161
233
|
* `provider/model` rows in the top-level `disabledModels` list.
|
|
162
234
|
*
|
|
@@ -194,6 +266,54 @@ function registrySeedsTarget(rename: ModelRename): boolean {
|
|
|
194
266
|
return !!entry?.models?.includes(rename.to);
|
|
195
267
|
}
|
|
196
268
|
|
|
269
|
+
/** The row the registry contributes to a saved config, or `undefined` when it dropped the id. */
|
|
270
|
+
function registrySeed(provider: string): Record<string, unknown> | undefined {
|
|
271
|
+
const entry = PROVIDER_REGISTRY.find(row => row.id === provider);
|
|
272
|
+
return entry ? (providerConfigSeed(entry) as unknown as Record<string, unknown>) : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Whether a saved list or model-keyed record names this id at all. */
|
|
276
|
+
function publishes(container: unknown, id: string): boolean {
|
|
277
|
+
if (Array.isArray(container)) return container.includes(id);
|
|
278
|
+
if (!container || typeof container !== "object") return false;
|
|
279
|
+
return id in (container as Record<string, unknown>);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Whether this field’s retired entry is registry residue rather than something to repair.
|
|
284
|
+
*
|
|
285
|
+
* Two things have to be true. The registry’s own seed — `providerConfigSeed`, the row both the
|
|
286
|
+
* OAuth preset (`deriveOAuthProviderConfig`) and `enrichProviderFromRegistry` contribute — must
|
|
287
|
+
* still publish the retired id in this field, so the entry arrives in the config on its own. And
|
|
288
|
+
* the saved field must already carry the supported id, so the rename would only DELETE a key,
|
|
289
|
+
* with nothing to carry forward.
|
|
290
|
+
*
|
|
291
|
+
* Under those two conditions the rewrite is worse than a no-op, because it never converges.
|
|
292
|
+
* `startServer` runs this migration and then `reconcileOAuthProviders`, and
|
|
293
|
+
* `applyOAuthPresetCatalog` copies the preset’s record over the saved one whenever the two
|
|
294
|
+
* differ. The deletion is undone inside the same boot, the config is written twice, and the user
|
|
295
|
+
* is told about a rename that never sticks on every single start. Issue #5066 is exactly that:
|
|
296
|
+
* `ANTIGRAVITY_MODEL_CONTEXT_WINDOWS` derives a window for every compatibility alias, so
|
|
297
|
+
* `modelContextWindows` holds a key for all nine retired Flash ids beside the live ones — the
|
|
298
|
+
* nine `[model-rename-migration]` lines the reporter saw at every `ocx start`, none of which
|
|
299
|
+
* required them to have selected any of those nine models.
|
|
300
|
+
*
|
|
301
|
+
* Those registry entries are deliberate and stay: a request that still names `gemini-3.6-flash`
|
|
302
|
+
* routes to 3.7 and needs a context window under the id it asked for. The second condition is
|
|
303
|
+
* what keeps this narrow. A row that saved `{ "gemini-3.6-flash": 1048576 }` and nothing else
|
|
304
|
+
* still gets the value carried onto the supported id, because dropping it there would blank the
|
|
305
|
+
* record instead of moving it (`tests/adapters/google/gemini-37-flash-migration.test.ts`).
|
|
306
|
+
*/
|
|
307
|
+
function isRegistryResidue(
|
|
308
|
+
seed: Record<string, unknown> | undefined,
|
|
309
|
+
field: string,
|
|
310
|
+
saved: unknown,
|
|
311
|
+
rename: ModelRename,
|
|
312
|
+
): boolean {
|
|
313
|
+
if (!seed) return false;
|
|
314
|
+
return publishes(seed[field], rename.from) && publishes(saved, rename.to);
|
|
315
|
+
}
|
|
316
|
+
|
|
197
317
|
export interface ModelRenameProjection {
|
|
198
318
|
config: OcxConfig;
|
|
199
319
|
changed: boolean;
|
|
@@ -226,14 +346,24 @@ export function projectModelRenames(
|
|
|
226
346
|
// Provider config is a closed interface, so index through one unknown-cast
|
|
227
347
|
// view rather than casting at each assignment.
|
|
228
348
|
const row = prov as unknown as Record<string, unknown>;
|
|
349
|
+
const seed = registrySeed(rename.provider);
|
|
229
350
|
let touched = false;
|
|
230
351
|
for (const field of MODEL_ID_LISTS) {
|
|
231
|
-
|
|
352
|
+
if (isRegistryResidue(seed, field, row[field], rename)) continue;
|
|
353
|
+
const next = rename.dropNoReasoningModels && field === "noReasoningModels"
|
|
354
|
+
? dropRenamedIdsFromList(
|
|
355
|
+
row[field],
|
|
356
|
+
rename.from,
|
|
357
|
+
rename.to,
|
|
358
|
+
targetReasoningMatchesStaleSeed(row, rename),
|
|
359
|
+
)
|
|
360
|
+
: renameInList(row[field], rename.from, rename.to);
|
|
232
361
|
if (!next) continue;
|
|
233
362
|
row[field] = next;
|
|
234
363
|
touched = true;
|
|
235
364
|
}
|
|
236
365
|
for (const field of MODEL_KEYED_RECORDS) {
|
|
366
|
+
if (isRegistryResidue(seed, field, row[field], rename)) continue;
|
|
237
367
|
const next = rename.dropReasoningEffortMap && field === "modelReasoningEffortMap"
|
|
238
368
|
? dropFromRecord(row[field], rename.from)
|
|
239
369
|
: renameInRecord(row[field], rename.from, rename.to);
|
|
@@ -241,11 +371,22 @@ export function projectModelRenames(
|
|
|
241
371
|
row[field] = next;
|
|
242
372
|
touched = true;
|
|
243
373
|
}
|
|
244
|
-
|
|
245
|
-
|
|
374
|
+
for (const field of MODEL_NESTED_RECORDS) {
|
|
375
|
+
const container = row[field];
|
|
376
|
+
if (!container || typeof container !== "object" || Array.isArray(container)) continue;
|
|
377
|
+
const nested = container as Record<string, unknown>;
|
|
378
|
+
const next = renameInRecord(nested.models, rename.from, rename.to);
|
|
379
|
+
if (!next) continue;
|
|
380
|
+
row[field] = { ...nested, models: next };
|
|
381
|
+
touched = true;
|
|
382
|
+
}
|
|
383
|
+
for (const field of MODEL_ID_SCALARS) {
|
|
384
|
+
if (row[field] !== rename.from) continue;
|
|
385
|
+
row[field] = rename.to;
|
|
246
386
|
touched = true;
|
|
247
387
|
}
|
|
248
388
|
if (renameDisabledModels(config, rename)) touched = true;
|
|
389
|
+
if (touched && refreshTargetSeed(row, rename)) touched = true;
|
|
249
390
|
|
|
250
391
|
if (touched) {
|
|
251
392
|
changed = true;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { mutatePersistedConfig } from "../config";
|
|
2
2
|
import { projectModelRenames } from "./model-rename-migration";
|
|
3
3
|
import { projectStaleContextWindows } from "./stale-context-window-migration";
|
|
4
|
+
import { projectStaleVisionClassifications } from "./stale-vision-classification-migration";
|
|
4
5
|
import { projectDevinCliAuthMode } from "./devin-cli-authmode-migration";
|
|
5
6
|
import type { OcxConfig } from "../types";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* The startup projection: registry model renames, then the context-window
|
|
9
|
-
*
|
|
9
|
+
* The startup projection: registry model renames, then the context-window and
|
|
10
|
+
* vision-classification repairs. All three fix a saved row the registry can no longer reach on its own —
|
|
10
11
|
* `enrichProviderFromRegistry` backfills a missing field and never rewrites a
|
|
11
12
|
* present one — so they share this pass rather than adding a second boot step
|
|
12
13
|
* with its own persistence, adopt, and failure handling.
|
|
@@ -14,11 +15,12 @@ import type { OcxConfig } from "../types";
|
|
|
14
15
|
export function projectStartupConfigRepairs(config: OcxConfig): ReturnType<typeof projectModelRenames> {
|
|
15
16
|
const renames = projectModelRenames(config);
|
|
16
17
|
const windows = projectStaleContextWindows(renames.config);
|
|
17
|
-
const
|
|
18
|
+
const vision = projectStaleVisionClassifications(windows.config);
|
|
19
|
+
const devinCli = projectDevinCliAuthMode(vision.config);
|
|
18
20
|
return {
|
|
19
21
|
config: devinCli.config,
|
|
20
|
-
changed: renames.changed || windows.changed || devinCli.changed,
|
|
21
|
-
warnings: [...renames.warnings, ...windows.warnings, ...devinCli.warnings],
|
|
22
|
+
changed: renames.changed || windows.changed || vision.changed || devinCli.changed,
|
|
23
|
+
warnings: [...renames.warnings, ...windows.warnings, ...vision.warnings, ...devinCli.warnings],
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -3,6 +3,9 @@ import { OPENAI_API_PROVIDER_ID } from "./openai-tiers";
|
|
|
3
3
|
import type { OcxParsedRequest } from "../types";
|
|
4
4
|
import type { RouteResult } from "../router";
|
|
5
5
|
import type { RequestLogContext } from "../server/request-log";
|
|
6
|
+
import type { InboundWire } from "./registry/types";
|
|
7
|
+
import { providerMatchesRegistryTransportWithStaticGuards } from "./static-model-discovery";
|
|
8
|
+
import { resolveModelPolicy } from "./resolved-model-policy";
|
|
6
9
|
|
|
7
10
|
export interface OpenAiVirtualModelResolution {
|
|
8
11
|
selectedModelId: string;
|
|
@@ -49,18 +52,55 @@ export function resolveOpenAiVirtualModel(
|
|
|
49
52
|
return validateOpenAiVirtualModelDefinition(selectedModelId, definition);
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
export function captureOpenAiVirtualWirePolicy(
|
|
56
|
+
route: RouteResult,
|
|
57
|
+
resolution: OpenAiVirtualModelResolution,
|
|
58
|
+
inboundWire: InboundWire = "responses",
|
|
59
|
+
): void {
|
|
60
|
+
// applyOpenAiVirtualModel is also a public mutation helper with older focused callers that
|
|
61
|
+
// construct only providerName/modelId/provider. Production RouteResult builders always capture
|
|
62
|
+
// staticPolicy; a legacy partial shape has no authority to recapture and keeps the old rewrite.
|
|
63
|
+
if (!route.staticPolicy) return;
|
|
64
|
+
const entry = PROVIDER_REGISTRY.find(candidate => candidate.id === route.providerName);
|
|
65
|
+
route.staticPolicy = resolveModelPolicy({
|
|
66
|
+
providerName: route.providerName,
|
|
67
|
+
modelId: resolution.wireModelId,
|
|
68
|
+
provider: route.provider,
|
|
69
|
+
registryEntry: entry,
|
|
70
|
+
transportMatchedRegistry: !!entry
|
|
71
|
+
&& providerMatchesRegistryTransportWithStaticGuards(route.providerName, route.provider),
|
|
72
|
+
inboundWire,
|
|
73
|
+
modelCapabilities: route.provider.modelCapabilities?.[resolution.wireModelId],
|
|
74
|
+
...(route.provider.authMode ? { effectiveAuth: { authMode: route.provider.authMode } } : {}),
|
|
75
|
+
...(route.staticPolicy?.effectiveAlias !== undefined
|
|
76
|
+
? { effectiveAlias: route.staticPolicy.effectiveAlias }
|
|
77
|
+
: {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
export function applyOpenAiVirtualModel(
|
|
53
82
|
parsed: OcxParsedRequest,
|
|
54
83
|
route: RouteResult,
|
|
55
84
|
logCtx: RequestLogContext,
|
|
85
|
+
inboundWire: InboundWire = "responses",
|
|
56
86
|
): OpenAiVirtualModelResolution | undefined {
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
// Routing has already removed the provider namespace and resolved model aliases. Never use
|
|
88
|
+
// logCtx.model for identity: it is reporting state and can still contain a namespaced selector.
|
|
89
|
+
const resolution = resolveOpenAiVirtualModel(route.providerName, route.modelId)
|
|
90
|
+
?? (() => {
|
|
91
|
+
// Preserve the public helper's idempotent second-call contract from its own mutation
|
|
92
|
+
// provenance, without promoting reporting state back into routing authority.
|
|
93
|
+
const selected = parsed._openAiVirtualSelectedModelId;
|
|
94
|
+
if (typeof selected !== "string") return undefined;
|
|
95
|
+
const remembered = resolveOpenAiVirtualModel(route.providerName, selected);
|
|
96
|
+
return remembered?.wireModelId === route.modelId ? remembered : undefined;
|
|
97
|
+
})();
|
|
59
98
|
if (!resolution) return undefined;
|
|
60
99
|
|
|
61
100
|
logCtx.model = resolution.selectedModelId;
|
|
62
101
|
logCtx.resolvedModel = resolution.wireModelId;
|
|
63
102
|
route.modelId = resolution.wireModelId;
|
|
103
|
+
captureOpenAiVirtualWirePolicy(route, resolution, inboundWire);
|
|
64
104
|
parsed.modelId = resolution.wireModelId;
|
|
65
105
|
parsed._openAiVirtualSelectedModelId = resolution.selectedModelId;
|
|
66
106
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { antigravityUserAgent } from "../../adapters/client-fingerprint";
|
|
2
2
|
import { DestinationDnsResolutionError } from "../../lib/destination-policy";
|
|
3
|
-
import { PinnedHttpError } from "../../lib/pinned-http";
|
|
3
|
+
import { PinnedHttpError, type PinnedHttpErrorCode } from "../../lib/pinned-http";
|
|
4
4
|
import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../../lib/provider-outbound";
|
|
5
5
|
import { getValidAccessToken } from "../../oauth";
|
|
6
6
|
import { getAccountCredential, getCredential } from "../../oauth/store";
|
|
@@ -193,10 +193,30 @@ type AntigravityQuotaProbeResult =
|
|
|
193
193
|
| { kind: "available"; quota: ProviderQuota; source: "google-antigravity:retrieveUserQuotaSummary" | "google-antigravity:fetchAvailableModels" }
|
|
194
194
|
| { kind: "unavailable"; failure: QuotaFailureCode; legacy: { kind: "null" } | { kind: "throw"; error: unknown } };
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Every pinned-transport failure code, classified once.
|
|
198
|
+
*
|
|
199
|
+
* A conditional that named one code and sent the rest to `timeout` was correct only for as long
|
|
200
|
+
* as the union held exactly the codes it was written against. When the transport learned to
|
|
201
|
+
* report a coding it cannot undo, that answer was reported as a timeout, which is a different
|
|
202
|
+
* operational story entirely. A total map makes a new code a compile error here rather than a
|
|
203
|
+
* quiet misdiagnosis.
|
|
204
|
+
*/
|
|
205
|
+
const PINNED_QUOTA_FAILURES = {
|
|
206
|
+
connect_timeout: "timeout",
|
|
207
|
+
first_byte_timeout: "timeout",
|
|
208
|
+
inactivity_timeout: "timeout",
|
|
209
|
+
// The response arrived and cannot be used: too large, coded in a format this transport cannot
|
|
210
|
+
// undo, or coded bytes that did not decode. None of these is a timing failure.
|
|
211
|
+
output_byte_limit: "response_unusable",
|
|
212
|
+
unsupported_content_encoding: "response_unusable",
|
|
213
|
+
content_decode_failed: "response_unusable",
|
|
214
|
+
} satisfies Record<PinnedHttpErrorCode, QuotaFailureCode>;
|
|
215
|
+
|
|
196
216
|
function quotaTransportFailure(error: unknown): QuotaFailureCode {
|
|
197
217
|
if (error instanceof ProviderOutboundPolicyError) return "destination_blocked";
|
|
198
218
|
if (error instanceof DestinationDnsResolutionError) return "dns_failed";
|
|
199
|
-
if (error instanceof PinnedHttpError) return error.code
|
|
219
|
+
if (error instanceof PinnedHttpError) return PINNED_QUOTA_FAILURES[error.code];
|
|
200
220
|
if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
|
|
201
221
|
return "transport_error";
|
|
202
222
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolveProviderApiKey } from "../key-store";
|
|
2
2
|
import { getProviderRegistryEntry, registryEntryForProviderDestination } from "../registry";
|
|
3
3
|
import { isCanonicalOllamaCloudUrl } from "../../adapters/ollama-native-url";
|
|
4
|
+
import { providerEgressFetchInit, resolveProviderEgress } from "../../lib/provider-egress";
|
|
5
|
+
import { configuredOutboundFetch } from "../../lib/proxy-env";
|
|
4
6
|
import { QUOTA_JSON_READ_FAILURE, asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
|
|
5
7
|
import {
|
|
6
8
|
AUTHORITATIVE_EMPTY_QUOTA,
|
|
@@ -15,6 +17,12 @@ import { getTokenForAccountQuotaProbe } from "./account-cache";
|
|
|
15
17
|
import type { AccountQuotaMode, ProviderQuota, ProviderQuotaCreditsUsd } from "../quota-types";
|
|
16
18
|
import type { OcxProviderConfig } from "../../types";
|
|
17
19
|
|
|
20
|
+
// A quota probe must use the provider's inference route so it neither reports a false healthy path nor leaks a key through another exit.
|
|
21
|
+
async function quotaFetch(providerName: string, config: OcxProviderConfig, url: string, init: RequestInit): Promise<Response> {
|
|
22
|
+
const egress = resolveProviderEgress({ providerName, provider: config, url });
|
|
23
|
+
return configuredOutboundFetch(url, { ...init, ...providerEgressFetchInit(egress) });
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
19
27
|
const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
|
|
20
28
|
const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
|
|
@@ -144,10 +152,10 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro
|
|
|
144
152
|
if (!apiKey) return null;
|
|
145
153
|
const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
|
|
146
154
|
const [subscriptionResponse, tokenResponse] = await Promise.all([
|
|
147
|
-
|
|
155
|
+
quotaFetch(provider, config, `${A6API_BASE_URL}/dashboard/billing/subscription`, {
|
|
148
156
|
headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
149
157
|
}),
|
|
150
|
-
|
|
158
|
+
quotaFetch(provider, config, `${A6API_BASE_URL}/api/usage/token/`, {
|
|
151
159
|
headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
152
160
|
}),
|
|
153
161
|
]);
|
|
@@ -241,7 +249,7 @@ async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig)
|
|
|
241
249
|
if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null;
|
|
242
250
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
243
251
|
if (!apiKey) return null;
|
|
244
|
-
const response = await
|
|
252
|
+
const response = await quotaFetch(provider, config, OPENCODE_GO_USAGE_URL, {
|
|
245
253
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
246
254
|
redirect: "error",
|
|
247
255
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -287,7 +295,7 @@ async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig)
|
|
|
287
295
|
if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null;
|
|
288
296
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
289
297
|
if (!apiKey) return null;
|
|
290
|
-
const response = await
|
|
298
|
+
const response = await quotaFetch(provider, config, `${OPENROUTER_BASE_URL}/key`, {
|
|
291
299
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
292
300
|
redirect: "error",
|
|
293
301
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -337,7 +345,7 @@ async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig):
|
|
|
337
345
|
if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null;
|
|
338
346
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
339
347
|
if (!apiKey) return null;
|
|
340
|
-
const response = await
|
|
348
|
+
const response = await quotaFetch(provider, config, `${DEEPSEEK_BASE_URL}/user/balance`, {
|
|
341
349
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
342
350
|
redirect: "error",
|
|
343
351
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -382,7 +390,7 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
|
|
|
382
390
|
if (!isCanonicalClineBaseUrl(config.baseUrl)) return null;
|
|
383
391
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
384
392
|
if (!apiKey) return null;
|
|
385
|
-
const response = await
|
|
393
|
+
const response = await quotaFetch(provider, config, `${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
|
|
386
394
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
387
395
|
redirect: "error",
|
|
388
396
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -480,7 +488,7 @@ async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig
|
|
|
480
488
|
if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
|
|
481
489
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
482
490
|
if (!apiKey) return null;
|
|
483
|
-
const response = await
|
|
491
|
+
const response = await quotaFetch(provider, config, OLLAMA_CLOUD_USAGE_URL, {
|
|
484
492
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
485
493
|
redirect: "error",
|
|
486
494
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -604,7 +612,7 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
|
|
|
604
612
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
605
613
|
if (!apiKey) return null;
|
|
606
614
|
const authorization = monitorHost === ZAI_CN_BASE_URL ? apiKey : `Bearer ${apiKey}`;
|
|
607
|
-
const response = await
|
|
615
|
+
const response = await quotaFetch(provider, config, `${monitorHost}/api/monitor/usage/quota/limit`, {
|
|
608
616
|
headers: { Accept: "application/json", Authorization: authorization },
|
|
609
617
|
redirect: "error",
|
|
610
618
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -652,7 +660,7 @@ async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): P
|
|
|
652
660
|
if (!apiKey) return null;
|
|
653
661
|
const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com");
|
|
654
662
|
const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL;
|
|
655
|
-
const response = await
|
|
663
|
+
const response = await quotaFetch(provider, config, remainsUrl, {
|
|
656
664
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
657
665
|
redirect: "error",
|
|
658
666
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -695,7 +703,7 @@ async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig):
|
|
|
695
703
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
696
704
|
if (!apiKey) return null;
|
|
697
705
|
const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL;
|
|
698
|
-
const response = await
|
|
706
|
+
const response = await quotaFetch(provider, config, `${host}/users/me/balance`, {
|
|
699
707
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
700
708
|
redirect: "error",
|
|
701
709
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -738,7 +746,7 @@ async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Pr
|
|
|
738
746
|
if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null;
|
|
739
747
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
740
748
|
if (!apiKey) return null;
|
|
741
|
-
const response = await
|
|
749
|
+
const response = await quotaFetch(provider, config, `${VENICE_BASE_URL}/billing/balance`, {
|
|
742
750
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
743
751
|
redirect: "error",
|
|
744
752
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -781,7 +789,7 @@ async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig):
|
|
|
781
789
|
if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null;
|
|
782
790
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
783
791
|
if (!apiKey) return null;
|
|
784
|
-
const response = await
|
|
792
|
+
const response = await quotaFetch(provider, config, `${SYNTHETIC_BASE_URL}/quotas`, {
|
|
785
793
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
786
794
|
redirect: "error",
|
|
787
795
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -831,7 +839,7 @@ async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig):
|
|
|
831
839
|
if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null;
|
|
832
840
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
833
841
|
if (!apiKey) return null;
|
|
834
|
-
const response = await
|
|
842
|
+
const response = await quotaFetch(provider, config, `${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
|
|
835
843
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
836
844
|
redirect: "error",
|
|
837
845
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -873,7 +881,7 @@ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig)
|
|
|
873
881
|
if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null;
|
|
874
882
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
875
883
|
if (!apiKey) return null;
|
|
876
|
-
const response = await
|
|
884
|
+
const response = await quotaFetch(provider, config, `${NEURALWATT_BASE_URL}/quota`, {
|
|
877
885
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
878
886
|
redirect: "error",
|
|
879
887
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -915,7 +923,7 @@ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig)
|
|
|
915
923
|
}
|
|
916
924
|
|
|
917
925
|
|
|
918
|
-
function normalizedBaseUrl(value: string): string | null {
|
|
926
|
+
export function normalizedBaseUrl(value: string): string | null {
|
|
919
927
|
try {
|
|
920
928
|
const url = new URL(value);
|
|
921
929
|
if (url.username || url.password || url.search || url.hash) return null;
|
|
@@ -1050,7 +1058,7 @@ export async function fetchKimiQuota(provider: string, config: OcxProviderConfig
|
|
|
1050
1058
|
// Never release credentials to a user-edited or lookalike provider host.
|
|
1051
1059
|
if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
|
|
1052
1060
|
if (!accessToken) return null;
|
|
1053
|
-
const response = await
|
|
1061
|
+
const response = await quotaFetch(provider, config, KIMI_CODE_USAGE_URL, {
|
|
1054
1062
|
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
|
|
1055
1063
|
redirect: "error",
|
|
1056
1064
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -1077,9 +1085,14 @@ function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: nu
|
|
|
1077
1085
|
}
|
|
1078
1086
|
|
|
1079
1087
|
/** Soft-fail GET returning a parsed record, or null when unavailable. */
|
|
1080
|
-
async function fetchCommandCodeJson(
|
|
1088
|
+
async function fetchCommandCodeJson(
|
|
1089
|
+
provider: string,
|
|
1090
|
+
config: OcxProviderConfig,
|
|
1091
|
+
url: string,
|
|
1092
|
+
bearer: string,
|
|
1093
|
+
): Promise<Record<string, unknown> | null> {
|
|
1081
1094
|
try {
|
|
1082
|
-
const response = await
|
|
1095
|
+
const response = await quotaFetch(provider, config, url, {
|
|
1083
1096
|
headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
|
|
1084
1097
|
redirect: "error",
|
|
1085
1098
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -1097,12 +1110,14 @@ async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record
|
|
|
1097
1110
|
* pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
|
|
1098
1111
|
*/
|
|
1099
1112
|
async function fetchCommandCodeSpend(
|
|
1113
|
+
provider: string,
|
|
1114
|
+
config: OcxProviderConfig,
|
|
1100
1115
|
bearer: string,
|
|
1101
1116
|
credits: Record<string, unknown> | null,
|
|
1102
1117
|
orgQuery: string,
|
|
1103
1118
|
): Promise<ProviderQuotaCreditsUsd | undefined> {
|
|
1104
1119
|
if (!credits) return undefined;
|
|
1105
|
-
const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
|
|
1120
|
+
const subscriptionBody = await fetchCommandCodeJson(provider, config, `${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
|
|
1106
1121
|
const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
|
|
1107
1122
|
const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
|
|
1108
1123
|
// Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
|
|
@@ -1110,7 +1125,7 @@ async function fetchCommandCodeSpend(
|
|
|
1110
1125
|
if (!periodStart) return undefined;
|
|
1111
1126
|
const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
|
|
1112
1127
|
const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
|
|
1113
|
-
const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
|
|
1128
|
+
const summaryBody = await fetchCommandCodeJson(provider, config, `${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
|
|
1114
1129
|
const summary = asRecord(summaryBody?.data) ?? summaryBody;
|
|
1115
1130
|
const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
|
|
1116
1131
|
if (used === undefined || used < 0) return undefined;
|
|
@@ -1161,12 +1176,12 @@ export async function fetchCommandCodeQuota(provider: string, config: OcxProvide
|
|
|
1161
1176
|
// Never release credentials to a user-edited or lookalike provider host.
|
|
1162
1177
|
if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
|
|
1163
1178
|
if (!bearer) return null;
|
|
1164
|
-
const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
|
|
1179
|
+
const whoamiBody = await fetchCommandCodeJson(provider, config, COMMAND_CODE_WHOAMI_URL, bearer);
|
|
1165
1180
|
const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
|
|
1166
1181
|
const org = asRecord(whoami?.org);
|
|
1167
1182
|
const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
|
|
1168
1183
|
const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
|
|
1169
|
-
const response = await
|
|
1184
|
+
const response = await quotaFetch(provider, config, `${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
|
|
1170
1185
|
headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
|
|
1171
1186
|
redirect: "error",
|
|
1172
1187
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -1183,7 +1198,7 @@ export async function fetchCommandCodeQuota(provider: string, config: OcxProvide
|
|
|
1183
1198
|
if (!credits && !limits) return null;
|
|
1184
1199
|
const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
|
|
1185
1200
|
const weekly = parseCommandCodeWindow(limits?.weekly);
|
|
1186
|
-
const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
|
|
1201
|
+
const creditsUsd = await fetchCommandCodeSpend(provider, config, bearer, credits, orgQuery);
|
|
1187
1202
|
const quota: ProviderQuota = {
|
|
1188
1203
|
...(fiveHour ? {
|
|
1189
1204
|
fiveHourPercent: fiveHour.percent,
|