@bitkyc08/opencodex 2.80.0 → 2.82.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 +3 -3
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +17 -5
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +90 -18
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +68 -10
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +85 -14
- package/src/server/management/context.ts +2 -2
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/shared.ts +3 -3
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
2
|
+
import {
|
|
3
|
+
providerOutboundPost,
|
|
4
|
+
providerRedirectError,
|
|
5
|
+
} from "../lib/provider-outbound";
|
|
6
|
+
import {
|
|
7
|
+
isKeychainReference,
|
|
8
|
+
keychainReferenceBelongsToProvider,
|
|
9
|
+
resolveProviderApiKey,
|
|
10
|
+
} from "../providers/api-key-resolve";
|
|
11
|
+
import { providerMatchesRegistryTransport } from "../providers/registry";
|
|
12
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
13
|
+
import type { JevDecision, ResolveJevDecisionOptions } from "./jev";
|
|
14
|
+
import {
|
|
15
|
+
isSystemOneEndpoint,
|
|
16
|
+
jevDecisionEndpointUrl,
|
|
17
|
+
JEV_DECISION_TIMEOUT_DEFAULT_MS,
|
|
18
|
+
JEV_DECISION_TIMEOUT_MAX_MS,
|
|
19
|
+
JEV_DECISION_TIMEOUT_MIN_MS,
|
|
20
|
+
} from "./jev-decision-contract";
|
|
21
|
+
|
|
22
|
+
export const JEV_PROVIDER_ID = "jev";
|
|
23
|
+
export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
|
|
24
|
+
export const JEV_MODEL = "jev-latest";
|
|
25
|
+
|
|
26
|
+
/** Environment names that hold TypeSafe credentials; a self-hosted row may never reference them. */
|
|
27
|
+
const TYPESAFE_ENV_KEYS = new Set(["TYPESAFE_API_KEY", "JEV_API_KEY"]);
|
|
28
|
+
export const JEV_MAX_REQUEST_BYTES = 65_536;
|
|
29
|
+
export const JEV_MAX_RESPONSE_BYTES = 65_536;
|
|
30
|
+
const JEV_OUTBOUND_DEPENDENCIES = {
|
|
31
|
+
isCanonicalUrl: (name: string, url: string) => name === JEV_PROVIDER_ID && url === JEV_API_URL,
|
|
32
|
+
// Self-hosted decision models (Ollama tev1) listen on plain HTTP loopback. The outbound wrapper
|
|
33
|
+
// still requires the row's own allowPrivateNetwork and a literal local address for that.
|
|
34
|
+
allowLocalCleartextPost: true,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Every non-applied outcome of one exchange: a local refusal, a transport failure or an invalid answer. */
|
|
38
|
+
export type JevDecisionFailureGate = Exclude<JevDecision["gate"], "apply">;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Transport options shared with the route resolver. `signal` is the caller's cancellation and always
|
|
42
|
+
* rejects with its own reason; `timeoutMs` is the separate decision deadline that yields a `timeout` gate.
|
|
43
|
+
*/
|
|
44
|
+
export type JevServiceExchangeOptions = Pick<ResolveJevDecisionOptions,
|
|
45
|
+
"config" | "decisionProvider" | "isDestinationAllowed" | "timeoutMs" | "signal" | "post">;
|
|
46
|
+
|
|
47
|
+
/** Request shape only: question builders never receive the destination or credential. */
|
|
48
|
+
export interface JevDecisionEndpointShape {
|
|
49
|
+
model: string;
|
|
50
|
+
descriptiveCriteria: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
|
|
54
|
+
export function jevDecisionTimeoutMs(value: number | undefined): number {
|
|
55
|
+
return value !== undefined
|
|
56
|
+
&& Number.isInteger(value)
|
|
57
|
+
&& value >= JEV_DECISION_TIMEOUT_MIN_MS
|
|
58
|
+
&& value <= JEV_DECISION_TIMEOUT_MAX_MS
|
|
59
|
+
? value
|
|
60
|
+
: JEV_DECISION_TIMEOUT_DEFAULT_MS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Retain a registry-matching JEV row's transport policy; ignore a retargeted row for hosted requests. */
|
|
64
|
+
function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
|
|
65
|
+
const configured = config.providers[JEV_PROVIDER_ID];
|
|
66
|
+
if (configured && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured)) return configured;
|
|
67
|
+
return {
|
|
68
|
+
adapter: "jev-decision",
|
|
69
|
+
baseUrl: JEV_API_URL,
|
|
70
|
+
authMode: "key",
|
|
71
|
+
liveModels: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface JevDecisionEndpoint {
|
|
76
|
+
name: string;
|
|
77
|
+
provider: OcxProviderConfig;
|
|
78
|
+
url: string;
|
|
79
|
+
model: string;
|
|
80
|
+
apiKey: string | undefined;
|
|
81
|
+
/** Self-hosted System One services accept only string option descriptions. */
|
|
82
|
+
descriptiveCriteria: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Recognize the $NAME and ${NAME} credential references before applying self-hosted ownership rules. */
|
|
86
|
+
function envReferenceName(value: string): string | undefined {
|
|
87
|
+
const braced = /^\$\{(\w+)\}$/.exec(value);
|
|
88
|
+
if (braced) return braced[1];
|
|
89
|
+
return value.startsWith("$") ? value.slice(1) : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
|
|
94
|
+
* keys, and never a keychain entry that belongs to another provider. `null` means refused.
|
|
95
|
+
*/
|
|
96
|
+
function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
|
|
97
|
+
if (!apiKey) return undefined;
|
|
98
|
+
const envName = envReferenceName(apiKey);
|
|
99
|
+
if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
|
|
100
|
+
if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
|
|
101
|
+
return resolveProviderApiKey(apiKey)?.trim() || undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve where one decision request goes and which credential it may carry.
|
|
106
|
+
*
|
|
107
|
+
* The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
|
|
108
|
+
* only while the row still matches the registry transport, and the environment fallbacks exist
|
|
109
|
+
* only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
|
|
110
|
+
* custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
|
|
111
|
+
* full HTTPS decision endpoint (or a local HTTP `/systemone` endpoint) and which names its own
|
|
112
|
+
* model; only its own key may accompany it, so no TypeSafe credential can reach a self-hosted
|
|
113
|
+
* service. The URL is `jevDecisionEndpointUrl`, shared with the management surfaces: an HTTPS
|
|
114
|
+
* path is sent exactly as configured and only `/systemone` normalizes trailing slashes.
|
|
115
|
+
* `undefined` means no usable decision service (reported through the existing `missing_key`
|
|
116
|
+
* gate); `null` means the request's destination scope refused it before any credential access.
|
|
117
|
+
*/
|
|
118
|
+
function jevDecisionEndpoint(
|
|
119
|
+
config: OcxConfig,
|
|
120
|
+
decisionProvider: string,
|
|
121
|
+
isDestinationAllowed?: ResolveJevDecisionOptions["isDestinationAllowed"],
|
|
122
|
+
): JevDecisionEndpoint | undefined | null {
|
|
123
|
+
const configured = Object.hasOwn(config.providers, decisionProvider)
|
|
124
|
+
? config.providers[decisionProvider]
|
|
125
|
+
: undefined;
|
|
126
|
+
if (configured?.disabled === true) return undefined;
|
|
127
|
+
if (decisionProvider === JEV_PROVIDER_ID) {
|
|
128
|
+
if (isDestinationAllowed?.(JEV_PROVIDER_ID, JEV_MODEL) === false) return null;
|
|
129
|
+
const configuredOwnsJev = configured
|
|
130
|
+
&& providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
|
|
131
|
+
const apiKey = (
|
|
132
|
+
configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
|
|
133
|
+
) || process.env.TYPESAFE_API_KEY?.trim()
|
|
134
|
+
|| process.env.JEV_API_KEY?.trim();
|
|
135
|
+
if (!apiKey) return undefined;
|
|
136
|
+
return {
|
|
137
|
+
name: JEV_PROVIDER_ID,
|
|
138
|
+
provider: canonicalJevProvider(config),
|
|
139
|
+
url: JEV_API_URL,
|
|
140
|
+
model: JEV_MODEL,
|
|
141
|
+
apiKey,
|
|
142
|
+
descriptiveCriteria: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
|
|
146
|
+
const url = jevDecisionEndpointUrl(configured.baseUrl);
|
|
147
|
+
if (!url || !isSystemOneEndpoint(url)) return undefined;
|
|
148
|
+
// `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
|
|
149
|
+
const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
|
|
150
|
+
if (!model) return undefined;
|
|
151
|
+
if (isDestinationAllowed?.(decisionProvider, model) === false) return null;
|
|
152
|
+
const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
|
|
153
|
+
if (apiKey === null) return undefined;
|
|
154
|
+
return {
|
|
155
|
+
name: decisionProvider,
|
|
156
|
+
provider: configured,
|
|
157
|
+
url,
|
|
158
|
+
model,
|
|
159
|
+
apiKey,
|
|
160
|
+
descriptiveCriteria: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Run the question builder: a refusal keeps its gate; a throw or an over-cap body is `invalid`. */
|
|
165
|
+
function prepareRequestBody(
|
|
166
|
+
prepare: (endpoint: JevDecisionEndpointShape) => { body: string } | JevDecisionFailureGate,
|
|
167
|
+
shape: JevDecisionEndpointShape,
|
|
168
|
+
): { body: string } | { gate: JevDecisionFailureGate } {
|
|
169
|
+
try {
|
|
170
|
+
const prepared = prepare(shape);
|
|
171
|
+
if (typeof prepared === "string") return { gate: prepared };
|
|
172
|
+
const body = prepared.body;
|
|
173
|
+
if (new TextEncoder().encode(body).byteLength > JEV_MAX_REQUEST_BYTES) return { gate: "invalid" };
|
|
174
|
+
return { body };
|
|
175
|
+
} catch {
|
|
176
|
+
return { gate: "invalid" };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* One bounded System One round-trip, independent of the decision question and its choice policy.
|
|
182
|
+
* Destination authorization precedes credential access and `prepare`; request bytes, deadline,
|
|
183
|
+
* redirects and UTF-8 JSON are bounded here. `parse` validates the question's answer inside the
|
|
184
|
+
* cancellation boundary. Local preparation/parser failures are `invalid`.
|
|
185
|
+
*
|
|
186
|
+
* This function owns caller cancellation: an aborted `options.signal` rejects with its reason by
|
|
187
|
+
* identity, never as a gate. It is checked before endpoint, credential or `prepare` work, after
|
|
188
|
+
* endpoint resolution and preparation (including failures), after POST, redirect inspection, HTTP
|
|
189
|
+
* error-body cleanup and response reads, and when `parse` returns or throws. Only the caller's signal counts; expiry of
|
|
190
|
+
* the separate decision deadline remains a `timeout` gate.
|
|
191
|
+
*/
|
|
192
|
+
export async function exchangeJevDecision<T>(
|
|
193
|
+
options: JevServiceExchangeOptions,
|
|
194
|
+
prepare: (endpoint: JevDecisionEndpointShape) => { body: string } | JevDecisionFailureGate,
|
|
195
|
+
parse: (payload: unknown) => T,
|
|
196
|
+
): Promise<{ value: T } | { gate: JevDecisionFailureGate }> {
|
|
197
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
198
|
+
|
|
199
|
+
let endpoint: JevDecisionEndpoint | undefined | null;
|
|
200
|
+
try {
|
|
201
|
+
endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID, options.isDestinationAllowed);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// Authorization and credential callbacks may cancel the caller and then throw; cancellation wins.
|
|
204
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
208
|
+
if (endpoint === null) return { gate: "invalid" };
|
|
209
|
+
if (!endpoint) return { gate: "missing_key" };
|
|
210
|
+
|
|
211
|
+
const request = prepareRequestBody(prepare, { model: endpoint.model, descriptiveCriteria: endpoint.descriptiveCriteria });
|
|
212
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
213
|
+
if ("gate" in request) return request;
|
|
214
|
+
const requestBody = request.body;
|
|
215
|
+
|
|
216
|
+
const timeoutMs = jevDecisionTimeoutMs(options.timeoutMs);
|
|
217
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
218
|
+
const signal = options.signal
|
|
219
|
+
? AbortSignal.any([options.signal, timeoutSignal])
|
|
220
|
+
: timeoutSignal;
|
|
221
|
+
const post = options.post ?? providerOutboundPost;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const response = await post(
|
|
225
|
+
endpoint.name,
|
|
226
|
+
endpoint.provider,
|
|
227
|
+
endpoint.url,
|
|
228
|
+
{
|
|
229
|
+
headers: {
|
|
230
|
+
...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
},
|
|
233
|
+
body: requestBody,
|
|
234
|
+
signal,
|
|
235
|
+
},
|
|
236
|
+
JEV_OUTBOUND_DEPENDENCIES,
|
|
237
|
+
);
|
|
238
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
239
|
+
|
|
240
|
+
const redirectError = await providerRedirectError(response, endpoint.url);
|
|
241
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
242
|
+
if (redirectError) return { gate: "redirect" };
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
|
|
245
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
246
|
+
return { gate: "http" };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const bounded = await readBoundedResponseBytes(response, {
|
|
250
|
+
maxBytes: JEV_MAX_RESPONSE_BYTES,
|
|
251
|
+
signal,
|
|
252
|
+
});
|
|
253
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
254
|
+
if (bounded.oversized) return { gate: "malformed" };
|
|
255
|
+
|
|
256
|
+
let payload: unknown;
|
|
257
|
+
try {
|
|
258
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
|
|
259
|
+
payload = JSON.parse(text);
|
|
260
|
+
} catch {
|
|
261
|
+
return { gate: "malformed" };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let parsed: T;
|
|
265
|
+
try {
|
|
266
|
+
parsed = parse(payload);
|
|
267
|
+
} catch {
|
|
268
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
269
|
+
return { gate: "invalid" };
|
|
270
|
+
}
|
|
271
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
272
|
+
return { value: parsed };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
275
|
+
if (timeoutSignal.aborted
|
|
276
|
+
|| (error instanceof DOMException && error.name === "TimeoutError")) {
|
|
277
|
+
return { gate: "timeout" };
|
|
278
|
+
}
|
|
279
|
+
return { gate: "network" };
|
|
280
|
+
}
|
|
281
|
+
}
|
package/src/combos/jev.ts
CHANGED
|
@@ -1,41 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { providerOutboundPost } from "../lib/provider-outbound";
|
|
2
|
+
import type { OcxComboDefaultEffort, OcxConfig } from "../types";
|
|
2
3
|
import {
|
|
3
|
-
providerOutboundPost,
|
|
4
|
-
providerRedirectError,
|
|
5
|
-
} from "../lib/provider-outbound";
|
|
6
|
-
import {
|
|
7
|
-
isKeychainReference,
|
|
8
|
-
keychainReferenceBelongsToProvider,
|
|
9
|
-
resolveProviderApiKey,
|
|
10
|
-
} from "../providers/api-key-resolve";
|
|
11
|
-
import { providerMatchesRegistryTransport } from "../providers/registry";
|
|
12
|
-
import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
|
|
13
|
-
import {
|
|
14
|
-
isSystemOneEndpoint,
|
|
15
|
-
JEV_DECISION_TIMEOUT_DEFAULT_MS,
|
|
16
|
-
JEV_DECISION_TIMEOUT_MAX_MS,
|
|
17
|
-
JEV_DECISION_TIMEOUT_MIN_MS,
|
|
18
4
|
JEV_MAX_CANDIDATE_FIELD_CHARS,
|
|
19
5
|
} from "./types";
|
|
20
6
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
import {
|
|
8
|
+
exchangeJevDecision,
|
|
9
|
+
JEV_PROVIDER_ID,
|
|
10
|
+
} from "./jev-service-exchange";
|
|
11
|
+
export {
|
|
12
|
+
JEV_API_URL,
|
|
13
|
+
JEV_MODEL,
|
|
14
|
+
JEV_PROVIDER_ID,
|
|
15
|
+
JEV_MAX_REQUEST_BYTES,
|
|
16
|
+
JEV_MAX_RESPONSE_BYTES,
|
|
17
|
+
jevDecisionTimeoutMs,
|
|
18
|
+
} from "./jev-service-exchange";
|
|
24
19
|
|
|
25
20
|
export const JEV_MAX_CANDIDATES = 64;
|
|
26
21
|
/** Ollama's System One choice questions accept 2..26 options; other services share the floor. */
|
|
27
22
|
const SELF_HOSTED_MIN_OPTIONS = 2;
|
|
28
23
|
const SELF_HOSTED_MAX_OPTIONS = 26;
|
|
29
|
-
/** Environment names that hold TypeSafe credentials; a self-hosted row may never reference them. */
|
|
30
|
-
const TYPESAFE_ENV_KEYS = new Set(["TYPESAFE_API_KEY", "JEV_API_KEY"]);
|
|
31
|
-
export const JEV_MAX_REQUEST_BYTES = 65_536;
|
|
32
|
-
export const JEV_MAX_RESPONSE_BYTES = 65_536;
|
|
33
|
-
const JEV_OUTBOUND_DEPENDENCIES = {
|
|
34
|
-
isCanonicalUrl: (name: string, url: string) => name === JEV_PROVIDER_ID && url === JEV_API_URL,
|
|
35
|
-
// Self-hosted decision models (Ollama tev1) listen on plain HTTP loopback. The outbound wrapper
|
|
36
|
-
// still requires the row's own allowPrivateNetwork and a literal local address for that.
|
|
37
|
-
allowLocalCleartextPost: true,
|
|
38
|
-
};
|
|
39
24
|
|
|
40
25
|
const TASK_CHARS = 500;
|
|
41
26
|
const TASK_HEAD_CHARS = 320;
|
|
@@ -383,6 +368,7 @@ function hasImageContent(item: Record<string, unknown>): boolean {
|
|
|
383
368
|
return item.content.some(part => isRecord(part) && (part.type === "input_image" || part.type === "image_url"));
|
|
384
369
|
}
|
|
385
370
|
|
|
371
|
+
/** Extract bounded task/assistant/tool evidence, omit protected envelopes, and attach configured target notes. */
|
|
386
372
|
export function buildJevState(body: unknown, candidates: readonly JevCandidate[] = []): Record<string, unknown> {
|
|
387
373
|
const input = isRecord(body) ? body.input : undefined;
|
|
388
374
|
let task = "";
|
|
@@ -444,6 +430,7 @@ export function buildJevState(body: unknown, candidates: readonly JevCandidate[]
|
|
|
444
430
|
};
|
|
445
431
|
}
|
|
446
432
|
|
|
433
|
+
/** Require task text, an image signal, or a tool-output tail; assistant context and operator notes alone do not suffice. */
|
|
447
434
|
export function hasJevDecisionState(state: Record<string, unknown>): boolean {
|
|
448
435
|
if (typeof state.task === "string" && state.task.trim()) return true;
|
|
449
436
|
if (isRecord(state.signals) && state.signals.has_image === true) return true;
|
|
@@ -475,6 +462,7 @@ function candidateOptions(candidates: readonly JevCandidate[]): Map<string, JevR
|
|
|
475
462
|
return options;
|
|
476
463
|
}
|
|
477
464
|
|
|
465
|
+
/** Check candidate count and field lengths before state extraction; the exchange separately caps serialized bytes. */
|
|
478
466
|
export function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
|
|
479
467
|
if (candidates.length > JEV_MAX_CANDIDATES) return false;
|
|
480
468
|
return candidates.every(candidate => [candidate.key, candidate.provider, candidate.model]
|
|
@@ -549,6 +537,7 @@ export function buildJevRouteQuestion(
|
|
|
549
537
|
};
|
|
550
538
|
}
|
|
551
539
|
|
|
540
|
+
/** Project only allowlisted, nonnegative safe-integer usage counters; omit missing or wholly invalid usage. */
|
|
552
541
|
export function jevUsage(payload: Record<string, unknown>): Record<string, number> | undefined {
|
|
553
542
|
if (!isRecord(payload.usage)) return undefined;
|
|
554
543
|
const usage: Record<string, number> = {};
|
|
@@ -559,6 +548,7 @@ export function jevUsage(payload: Record<string, unknown>): Record<string, numbe
|
|
|
559
548
|
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
560
549
|
}
|
|
561
550
|
|
|
551
|
+
/** Validate an allowlisted route choice and any complete probability distribution; throw on invalid answers. */
|
|
562
552
|
export function parseJevDecision(
|
|
563
553
|
payload: unknown,
|
|
564
554
|
candidates: readonly JevCandidate[],
|
|
@@ -611,6 +601,7 @@ export function parseJevDecision(
|
|
|
611
601
|
};
|
|
612
602
|
}
|
|
613
603
|
|
|
604
|
+
/** Preserve the caller's eligible target/effort fallback while recording the failed gate, backend, and latency. */
|
|
614
605
|
export function fallbackDecision(
|
|
615
606
|
fallback: ResolveJevDecisionOptions["fallback"],
|
|
616
607
|
gate: Exclude<JevDecision["gate"], "apply">,
|
|
@@ -620,113 +611,6 @@ export function fallbackDecision(
|
|
|
620
611
|
return { backend, ...fallback, gate, latencyMs };
|
|
621
612
|
}
|
|
622
613
|
|
|
623
|
-
/** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
|
|
624
|
-
export function jevDecisionTimeoutMs(value: number | undefined): number {
|
|
625
|
-
return value !== undefined
|
|
626
|
-
&& Number.isInteger(value)
|
|
627
|
-
&& value >= JEV_DECISION_TIMEOUT_MIN_MS
|
|
628
|
-
&& value <= JEV_DECISION_TIMEOUT_MAX_MS
|
|
629
|
-
? value
|
|
630
|
-
: JEV_DECISION_TIMEOUT_DEFAULT_MS;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
|
|
634
|
-
const configured = config.providers[JEV_PROVIDER_ID];
|
|
635
|
-
if (configured && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured)) return configured;
|
|
636
|
-
return {
|
|
637
|
-
adapter: "jev-decision",
|
|
638
|
-
baseUrl: JEV_API_URL,
|
|
639
|
-
authMode: "key",
|
|
640
|
-
liveModels: false,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
interface JevDecisionEndpoint {
|
|
645
|
-
name: string;
|
|
646
|
-
provider: OcxProviderConfig;
|
|
647
|
-
url: string;
|
|
648
|
-
model: string;
|
|
649
|
-
apiKey: string | undefined;
|
|
650
|
-
/** Self-hosted System One services accept only string option descriptions. */
|
|
651
|
-
descriptiveCriteria: boolean;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function envReferenceName(value: string): string | undefined {
|
|
655
|
-
const braced = /^\$\{(\w+)\}$/.exec(value);
|
|
656
|
-
if (braced) return braced[1];
|
|
657
|
-
return value.startsWith("$") ? value.slice(1) : undefined;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
|
|
662
|
-
* keys, and never a keychain entry that belongs to another provider. `null` means refused.
|
|
663
|
-
*/
|
|
664
|
-
function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
|
|
665
|
-
if (!apiKey) return undefined;
|
|
666
|
-
const envName = envReferenceName(apiKey);
|
|
667
|
-
if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
|
|
668
|
-
if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
|
|
669
|
-
return resolveProviderApiKey(apiKey)?.trim() || undefined;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Resolve where one decision request goes and which credential it may carry.
|
|
674
|
-
*
|
|
675
|
-
* The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
|
|
676
|
-
* only while the row still matches the registry transport, and the environment fallbacks exist
|
|
677
|
-
* only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
|
|
678
|
-
* custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
|
|
679
|
-
* `/systemone` endpoint and which names its own model; only its own key may accompany it, so no
|
|
680
|
-
* TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
|
|
681
|
-
* service (reported through the existing `missing_key` gate); `null` means the request's
|
|
682
|
-
* destination scope refused it before any credential access.
|
|
683
|
-
*/
|
|
684
|
-
function jevDecisionEndpoint(
|
|
685
|
-
config: OcxConfig,
|
|
686
|
-
decisionProvider: string,
|
|
687
|
-
isDestinationAllowed?: ResolveJevDecisionOptions["isDestinationAllowed"],
|
|
688
|
-
): JevDecisionEndpoint | undefined | null {
|
|
689
|
-
const configured = Object.hasOwn(config.providers, decisionProvider)
|
|
690
|
-
? config.providers[decisionProvider]
|
|
691
|
-
: undefined;
|
|
692
|
-
if (configured?.disabled === true) return undefined;
|
|
693
|
-
if (decisionProvider === JEV_PROVIDER_ID) {
|
|
694
|
-
if (isDestinationAllowed?.(JEV_PROVIDER_ID, JEV_MODEL) === false) return null;
|
|
695
|
-
const configuredOwnsJev = configured
|
|
696
|
-
&& providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
|
|
697
|
-
const apiKey = (
|
|
698
|
-
configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
|
|
699
|
-
) || process.env.TYPESAFE_API_KEY?.trim()
|
|
700
|
-
|| process.env.JEV_API_KEY?.trim();
|
|
701
|
-
if (!apiKey) return undefined;
|
|
702
|
-
return {
|
|
703
|
-
name: JEV_PROVIDER_ID,
|
|
704
|
-
provider: canonicalJevProvider(config),
|
|
705
|
-
url: JEV_API_URL,
|
|
706
|
-
model: JEV_MODEL,
|
|
707
|
-
apiKey,
|
|
708
|
-
descriptiveCriteria: false,
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
|
|
712
|
-
const url = configured.baseUrl.trim().replace(/\/+$/, "");
|
|
713
|
-
if (!url || !isSystemOneEndpoint(url)) return undefined;
|
|
714
|
-
// `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
|
|
715
|
-
const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
|
|
716
|
-
if (!model) return undefined;
|
|
717
|
-
if (isDestinationAllowed?.(decisionProvider, model) === false) return null;
|
|
718
|
-
const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
|
|
719
|
-
if (apiKey === null) return undefined;
|
|
720
|
-
return {
|
|
721
|
-
name: decisionProvider,
|
|
722
|
-
provider: configured,
|
|
723
|
-
url,
|
|
724
|
-
model,
|
|
725
|
-
apiKey,
|
|
726
|
-
descriptiveCriteria: true,
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
|
|
730
614
|
/**
|
|
731
615
|
* Ask the configured JEV decision service for one allowlisted target/effort decision.
|
|
732
616
|
*
|
|
@@ -744,98 +628,29 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
|
|
|
744
628
|
if (options.candidates.length === 0) return failed("no_choices");
|
|
745
629
|
if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
|
|
746
630
|
|
|
747
|
-
const
|
|
748
|
-
if (endpoint === null) return failed("invalid");
|
|
749
|
-
if (!endpoint) return failed("missing_key");
|
|
750
|
-
|
|
751
|
-
let requestBody: string;
|
|
752
|
-
try {
|
|
631
|
+
const exchanged = await exchangeJevDecision(options, (endpoint) => {
|
|
753
632
|
if (endpoint.descriptiveCriteria) {
|
|
754
633
|
// Self-hosted choice questions accept 2..26 options; decide locally instead of spending a
|
|
755
634
|
// round-trip on a request the service will refuse.
|
|
756
635
|
const optionCount = candidateOptions(options.candidates).size;
|
|
757
|
-
if (optionCount < SELF_HOSTED_MIN_OPTIONS) return
|
|
758
|
-
if (optionCount > SELF_HOSTED_MAX_OPTIONS) return
|
|
636
|
+
if (optionCount < SELF_HOSTED_MIN_OPTIONS) return "no_choices";
|
|
637
|
+
if (optionCount > SELF_HOSTED_MAX_OPTIONS) return "invalid";
|
|
759
638
|
}
|
|
760
639
|
const state = buildJevState(options.body, options.candidates);
|
|
761
|
-
if (!hasJevDecisionState(state)) return
|
|
762
|
-
|
|
640
|
+
if (!hasJevDecisionState(state)) return "no_state";
|
|
641
|
+
return { body: JSON.stringify({
|
|
763
642
|
model: endpoint.model,
|
|
764
643
|
state,
|
|
765
644
|
questions: buildJevRouteQuestion(options.candidates, { descriptiveCriteria: endpoint.descriptiveCriteria }),
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
? AbortSignal.any([options.signal, timeoutSignal])
|
|
776
|
-
: timeoutSignal;
|
|
777
|
-
const post = options.post ?? providerOutboundPost;
|
|
778
|
-
|
|
779
|
-
try {
|
|
780
|
-
const response = await post(
|
|
781
|
-
endpoint.name,
|
|
782
|
-
endpoint.provider,
|
|
783
|
-
endpoint.url,
|
|
784
|
-
{
|
|
785
|
-
headers: {
|
|
786
|
-
...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
|
|
787
|
-
"Content-Type": "application/json",
|
|
788
|
-
},
|
|
789
|
-
body: requestBody,
|
|
790
|
-
signal,
|
|
791
|
-
},
|
|
792
|
-
JEV_OUTBOUND_DEPENDENCIES,
|
|
793
|
-
);
|
|
794
|
-
if (options.signal?.aborted) throw options.signal.reason;
|
|
795
|
-
|
|
796
|
-
const redirectError = await providerRedirectError(response, endpoint.url);
|
|
797
|
-
if (redirectError) return failed("redirect");
|
|
798
|
-
if (!response.ok) {
|
|
799
|
-
try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
|
|
800
|
-
return failed("http");
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const bounded = await readBoundedResponseBytes(response, {
|
|
804
|
-
maxBytes: JEV_MAX_RESPONSE_BYTES,
|
|
805
|
-
signal,
|
|
806
|
-
});
|
|
807
|
-
if (options.signal?.aborted) throw options.signal.reason;
|
|
808
|
-
if (bounded.oversized) return failed("malformed");
|
|
809
|
-
|
|
810
|
-
let payload: unknown;
|
|
811
|
-
try {
|
|
812
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
|
|
813
|
-
payload = JSON.parse(text);
|
|
814
|
-
} catch {
|
|
815
|
-
return failed("malformed");
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
let parsed: ReturnType<typeof parseJevDecision>;
|
|
819
|
-
try {
|
|
820
|
-
parsed = parseJevDecision(payload, options.candidates);
|
|
821
|
-
} catch {
|
|
822
|
-
return failed("invalid");
|
|
823
|
-
}
|
|
824
|
-
if (options.signal?.aborted) throw options.signal.reason;
|
|
825
|
-
return {
|
|
826
|
-
backend,
|
|
827
|
-
...parsed,
|
|
828
|
-
gate: "apply",
|
|
829
|
-
latencyMs: Math.max(0, now() - startedAt),
|
|
830
|
-
};
|
|
831
|
-
} catch (error) {
|
|
832
|
-
if (options.signal?.aborted) throw options.signal.reason;
|
|
833
|
-
if (timeoutSignal.aborted
|
|
834
|
-
|| (error instanceof DOMException && error.name === "TimeoutError")) {
|
|
835
|
-
return failed("timeout");
|
|
836
|
-
}
|
|
837
|
-
return failed("network");
|
|
838
|
-
}
|
|
645
|
+
}) };
|
|
646
|
+
}, (payload) => parseJevDecision(payload, options.candidates));
|
|
647
|
+
if ("gate" in exchanged) return failed(exchanged.gate);
|
|
648
|
+
return {
|
|
649
|
+
backend,
|
|
650
|
+
...exchanged.value,
|
|
651
|
+
gate: "apply",
|
|
652
|
+
latencyMs: Math.max(0, now() - startedAt),
|
|
653
|
+
};
|
|
839
654
|
}
|
|
840
655
|
|
|
841
656
|
export type JevDecisionProbeResult =
|
package/src/combos/types.ts
CHANGED
|
@@ -302,7 +302,7 @@ export function comboConfigIssues(
|
|
|
302
302
|
} else if (!isSystemOneEndpoint(String(providers[decisionProvider]?.baseUrl ?? ""))) {
|
|
303
303
|
issues.push({
|
|
304
304
|
path: ["decisionProvider"],
|
|
305
|
-
message: `decisionProvider "${decisionProvider}" baseUrl must be
|
|
305
|
+
message: `decisionProvider "${decisionProvider}" baseUrl must be a full HTTPS decision endpoint or an HTTP /systemone endpoint`,
|
|
306
306
|
});
|
|
307
307
|
} else if (options.requireUsableDecisionService && providers[decisionProvider]?.disabled === true) {
|
|
308
308
|
issues.push({
|
|
@@ -304,7 +304,7 @@ function atomicWriteFileToTarget(
|
|
|
304
304
|
}
|
|
305
305
|
try { chmodSync(tempPath, 0o600); } catch { /* platform may ignore chmod */ }
|
|
306
306
|
},
|
|
307
|
-
rename: renameAtomicFile,
|
|
307
|
+
rename: (source, destination) => renameAtomicFile(source, destination, undefined, "config", hooks),
|
|
308
308
|
truncate: tempPath => truncateSync(tempPath, 0),
|
|
309
309
|
unlink: unlinkSync,
|
|
310
310
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Process-local opt-in for cross-turn Codex WebSocket reuse. Default off. */
|
|
2
|
+
let reuseAcrossTurns = false;
|
|
3
|
+
|
|
4
|
+
export function setCodexWsReuseAcrossTurns(enabled: boolean): void {
|
|
5
|
+
reuseAcrossTurns = enabled;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function codexWsReuseAcrossTurnsEnabled(): boolean {
|
|
9
|
+
return reuseAcrossTurns;
|
|
10
|
+
}
|