@bitkyc08/opencodex 2.81.0 → 2.83.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- 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-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- 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/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- 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/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -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/main-account.ts +6 -7
- 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-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- 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/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- 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/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/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- 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 +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- 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 +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -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/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/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -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/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -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,42 +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 { jevDecisionEndpointUrl } from "./jev-decision-contract";
|
|
14
|
-
import {
|
|
15
|
-
isSystemOneEndpoint,
|
|
16
|
-
JEV_DECISION_TIMEOUT_DEFAULT_MS,
|
|
17
|
-
JEV_DECISION_TIMEOUT_MAX_MS,
|
|
18
|
-
JEV_DECISION_TIMEOUT_MIN_MS,
|
|
19
4
|
JEV_MAX_CANDIDATE_FIELD_CHARS,
|
|
20
5
|
} from "./types";
|
|
21
6
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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";
|
|
25
19
|
|
|
26
20
|
export const JEV_MAX_CANDIDATES = 64;
|
|
27
21
|
/** Ollama's System One choice questions accept 2..26 options; other services share the floor. */
|
|
28
22
|
const SELF_HOSTED_MIN_OPTIONS = 2;
|
|
29
23
|
const SELF_HOSTED_MAX_OPTIONS = 26;
|
|
30
|
-
/** Environment names that hold TypeSafe credentials; a self-hosted row may never reference them. */
|
|
31
|
-
const TYPESAFE_ENV_KEYS = new Set(["TYPESAFE_API_KEY", "JEV_API_KEY"]);
|
|
32
|
-
export const JEV_MAX_REQUEST_BYTES = 65_536;
|
|
33
|
-
export const JEV_MAX_RESPONSE_BYTES = 65_536;
|
|
34
|
-
const JEV_OUTBOUND_DEPENDENCIES = {
|
|
35
|
-
isCanonicalUrl: (name: string, url: string) => name === JEV_PROVIDER_ID && url === JEV_API_URL,
|
|
36
|
-
// Self-hosted decision models (Ollama tev1) listen on plain HTTP loopback. The outbound wrapper
|
|
37
|
-
// still requires the row's own allowPrivateNetwork and a literal local address for that.
|
|
38
|
-
allowLocalCleartextPost: true,
|
|
39
|
-
};
|
|
40
24
|
|
|
41
25
|
const TASK_CHARS = 500;
|
|
42
26
|
const TASK_HEAD_CHARS = 320;
|
|
@@ -384,6 +368,7 @@ function hasImageContent(item: Record<string, unknown>): boolean {
|
|
|
384
368
|
return item.content.some(part => isRecord(part) && (part.type === "input_image" || part.type === "image_url"));
|
|
385
369
|
}
|
|
386
370
|
|
|
371
|
+
/** Extract bounded task/assistant/tool evidence, omit protected envelopes, and attach configured target notes. */
|
|
387
372
|
export function buildJevState(body: unknown, candidates: readonly JevCandidate[] = []): Record<string, unknown> {
|
|
388
373
|
const input = isRecord(body) ? body.input : undefined;
|
|
389
374
|
let task = "";
|
|
@@ -445,6 +430,7 @@ export function buildJevState(body: unknown, candidates: readonly JevCandidate[]
|
|
|
445
430
|
};
|
|
446
431
|
}
|
|
447
432
|
|
|
433
|
+
/** Require task text, an image signal, or a tool-output tail; assistant context and operator notes alone do not suffice. */
|
|
448
434
|
export function hasJevDecisionState(state: Record<string, unknown>): boolean {
|
|
449
435
|
if (typeof state.task === "string" && state.task.trim()) return true;
|
|
450
436
|
if (isRecord(state.signals) && state.signals.has_image === true) return true;
|
|
@@ -476,6 +462,7 @@ function candidateOptions(candidates: readonly JevCandidate[]): Map<string, JevR
|
|
|
476
462
|
return options;
|
|
477
463
|
}
|
|
478
464
|
|
|
465
|
+
/** Check candidate count and field lengths before state extraction; the exchange separately caps serialized bytes. */
|
|
479
466
|
export function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
|
|
480
467
|
if (candidates.length > JEV_MAX_CANDIDATES) return false;
|
|
481
468
|
return candidates.every(candidate => [candidate.key, candidate.provider, candidate.model]
|
|
@@ -550,6 +537,7 @@ export function buildJevRouteQuestion(
|
|
|
550
537
|
};
|
|
551
538
|
}
|
|
552
539
|
|
|
540
|
+
/** Project only allowlisted, nonnegative safe-integer usage counters; omit missing or wholly invalid usage. */
|
|
553
541
|
export function jevUsage(payload: Record<string, unknown>): Record<string, number> | undefined {
|
|
554
542
|
if (!isRecord(payload.usage)) return undefined;
|
|
555
543
|
const usage: Record<string, number> = {};
|
|
@@ -560,6 +548,7 @@ export function jevUsage(payload: Record<string, unknown>): Record<string, numbe
|
|
|
560
548
|
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
561
549
|
}
|
|
562
550
|
|
|
551
|
+
/** Validate an allowlisted route choice and any complete probability distribution; throw on invalid answers. */
|
|
563
552
|
export function parseJevDecision(
|
|
564
553
|
payload: unknown,
|
|
565
554
|
candidates: readonly JevCandidate[],
|
|
@@ -612,6 +601,7 @@ export function parseJevDecision(
|
|
|
612
601
|
};
|
|
613
602
|
}
|
|
614
603
|
|
|
604
|
+
/** Preserve the caller's eligible target/effort fallback while recording the failed gate, backend, and latency. */
|
|
615
605
|
export function fallbackDecision(
|
|
616
606
|
fallback: ResolveJevDecisionOptions["fallback"],
|
|
617
607
|
gate: Exclude<JevDecision["gate"], "apply">,
|
|
@@ -621,113 +611,6 @@ export function fallbackDecision(
|
|
|
621
611
|
return { backend, ...fallback, gate, latencyMs };
|
|
622
612
|
}
|
|
623
613
|
|
|
624
|
-
/** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
|
|
625
|
-
export function jevDecisionTimeoutMs(value: number | undefined): number {
|
|
626
|
-
return value !== undefined
|
|
627
|
-
&& Number.isInteger(value)
|
|
628
|
-
&& value >= JEV_DECISION_TIMEOUT_MIN_MS
|
|
629
|
-
&& value <= JEV_DECISION_TIMEOUT_MAX_MS
|
|
630
|
-
? value
|
|
631
|
-
: JEV_DECISION_TIMEOUT_DEFAULT_MS;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
|
|
635
|
-
const configured = config.providers[JEV_PROVIDER_ID];
|
|
636
|
-
if (configured && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured)) return configured;
|
|
637
|
-
return {
|
|
638
|
-
adapter: "jev-decision",
|
|
639
|
-
baseUrl: JEV_API_URL,
|
|
640
|
-
authMode: "key",
|
|
641
|
-
liveModels: false,
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
interface JevDecisionEndpoint {
|
|
646
|
-
name: string;
|
|
647
|
-
provider: OcxProviderConfig;
|
|
648
|
-
url: string;
|
|
649
|
-
model: string;
|
|
650
|
-
apiKey: string | undefined;
|
|
651
|
-
/** Self-hosted System One services accept only string option descriptions. */
|
|
652
|
-
descriptiveCriteria: boolean;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function envReferenceName(value: string): string | undefined {
|
|
656
|
-
const braced = /^\$\{(\w+)\}$/.exec(value);
|
|
657
|
-
if (braced) return braced[1];
|
|
658
|
-
return value.startsWith("$") ? value.slice(1) : undefined;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
|
|
663
|
-
* keys, and never a keychain entry that belongs to another provider. `null` means refused.
|
|
664
|
-
*/
|
|
665
|
-
function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
|
|
666
|
-
if (!apiKey) return undefined;
|
|
667
|
-
const envName = envReferenceName(apiKey);
|
|
668
|
-
if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
|
|
669
|
-
if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
|
|
670
|
-
return resolveProviderApiKey(apiKey)?.trim() || undefined;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Resolve where one decision request goes and which credential it may carry.
|
|
675
|
-
*
|
|
676
|
-
* The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
|
|
677
|
-
* only while the row still matches the registry transport, and the environment fallbacks exist
|
|
678
|
-
* only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
|
|
679
|
-
* custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
|
|
680
|
-
* full HTTPS decision endpoint (or a local HTTP `/systemone` endpoint) and names its own model; only its own key may accompany it, so no
|
|
681
|
-
* TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
|
|
682
|
-
* service (reported through the existing `missing_key` gate); `null` means the request's
|
|
683
|
-
* destination scope refused it before any credential access.
|
|
684
|
-
*/
|
|
685
|
-
function jevDecisionEndpoint(
|
|
686
|
-
config: OcxConfig,
|
|
687
|
-
decisionProvider: string,
|
|
688
|
-
isDestinationAllowed?: ResolveJevDecisionOptions["isDestinationAllowed"],
|
|
689
|
-
): JevDecisionEndpoint | undefined | null {
|
|
690
|
-
const configured = Object.hasOwn(config.providers, decisionProvider)
|
|
691
|
-
? config.providers[decisionProvider]
|
|
692
|
-
: undefined;
|
|
693
|
-
if (configured?.disabled === true) return undefined;
|
|
694
|
-
if (decisionProvider === JEV_PROVIDER_ID) {
|
|
695
|
-
if (isDestinationAllowed?.(JEV_PROVIDER_ID, JEV_MODEL) === false) return null;
|
|
696
|
-
const configuredOwnsJev = configured
|
|
697
|
-
&& providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
|
|
698
|
-
const apiKey = (
|
|
699
|
-
configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
|
|
700
|
-
) || process.env.TYPESAFE_API_KEY?.trim()
|
|
701
|
-
|| process.env.JEV_API_KEY?.trim();
|
|
702
|
-
if (!apiKey) return undefined;
|
|
703
|
-
return {
|
|
704
|
-
name: JEV_PROVIDER_ID,
|
|
705
|
-
provider: canonicalJevProvider(config),
|
|
706
|
-
url: JEV_API_URL,
|
|
707
|
-
model: JEV_MODEL,
|
|
708
|
-
apiKey,
|
|
709
|
-
descriptiveCriteria: false,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
|
|
713
|
-
const url = jevDecisionEndpointUrl(configured.baseUrl);
|
|
714
|
-
if (!url || !isSystemOneEndpoint(url)) return undefined;
|
|
715
|
-
// `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
|
|
716
|
-
const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
|
|
717
|
-
if (!model) return undefined;
|
|
718
|
-
if (isDestinationAllowed?.(decisionProvider, model) === false) return null;
|
|
719
|
-
const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
|
|
720
|
-
if (apiKey === null) return undefined;
|
|
721
|
-
return {
|
|
722
|
-
name: decisionProvider,
|
|
723
|
-
provider: configured,
|
|
724
|
-
url,
|
|
725
|
-
model,
|
|
726
|
-
apiKey,
|
|
727
|
-
descriptiveCriteria: true,
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
|
|
731
614
|
/**
|
|
732
615
|
* Ask the configured JEV decision service for one allowlisted target/effort decision.
|
|
733
616
|
*
|
|
@@ -745,98 +628,29 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
|
|
|
745
628
|
if (options.candidates.length === 0) return failed("no_choices");
|
|
746
629
|
if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
|
|
747
630
|
|
|
748
|
-
const
|
|
749
|
-
if (endpoint === null) return failed("invalid");
|
|
750
|
-
if (!endpoint) return failed("missing_key");
|
|
751
|
-
|
|
752
|
-
let requestBody: string;
|
|
753
|
-
try {
|
|
631
|
+
const exchanged = await exchangeJevDecision(options, (endpoint) => {
|
|
754
632
|
if (endpoint.descriptiveCriteria) {
|
|
755
633
|
// Self-hosted choice questions accept 2..26 options; decide locally instead of spending a
|
|
756
634
|
// round-trip on a request the service will refuse.
|
|
757
635
|
const optionCount = candidateOptions(options.candidates).size;
|
|
758
|
-
if (optionCount < SELF_HOSTED_MIN_OPTIONS) return
|
|
759
|
-
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";
|
|
760
638
|
}
|
|
761
639
|
const state = buildJevState(options.body, options.candidates);
|
|
762
|
-
if (!hasJevDecisionState(state)) return
|
|
763
|
-
|
|
640
|
+
if (!hasJevDecisionState(state)) return "no_state";
|
|
641
|
+
return { body: JSON.stringify({
|
|
764
642
|
model: endpoint.model,
|
|
765
643
|
state,
|
|
766
644
|
questions: buildJevRouteQuestion(options.candidates, { descriptiveCriteria: endpoint.descriptiveCriteria }),
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
? AbortSignal.any([options.signal, timeoutSignal])
|
|
777
|
-
: timeoutSignal;
|
|
778
|
-
const post = options.post ?? providerOutboundPost;
|
|
779
|
-
|
|
780
|
-
try {
|
|
781
|
-
const response = await post(
|
|
782
|
-
endpoint.name,
|
|
783
|
-
endpoint.provider,
|
|
784
|
-
endpoint.url,
|
|
785
|
-
{
|
|
786
|
-
headers: {
|
|
787
|
-
...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
|
|
788
|
-
"Content-Type": "application/json",
|
|
789
|
-
},
|
|
790
|
-
body: requestBody,
|
|
791
|
-
signal,
|
|
792
|
-
},
|
|
793
|
-
JEV_OUTBOUND_DEPENDENCIES,
|
|
794
|
-
);
|
|
795
|
-
if (options.signal?.aborted) throw options.signal.reason;
|
|
796
|
-
|
|
797
|
-
const redirectError = await providerRedirectError(response, endpoint.url);
|
|
798
|
-
if (redirectError) return failed("redirect");
|
|
799
|
-
if (!response.ok) {
|
|
800
|
-
try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
|
|
801
|
-
return failed("http");
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const bounded = await readBoundedResponseBytes(response, {
|
|
805
|
-
maxBytes: JEV_MAX_RESPONSE_BYTES,
|
|
806
|
-
signal,
|
|
807
|
-
});
|
|
808
|
-
if (options.signal?.aborted) throw options.signal.reason;
|
|
809
|
-
if (bounded.oversized) return failed("malformed");
|
|
810
|
-
|
|
811
|
-
let payload: unknown;
|
|
812
|
-
try {
|
|
813
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
|
|
814
|
-
payload = JSON.parse(text);
|
|
815
|
-
} catch {
|
|
816
|
-
return failed("malformed");
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
let parsed: ReturnType<typeof parseJevDecision>;
|
|
820
|
-
try {
|
|
821
|
-
parsed = parseJevDecision(payload, options.candidates);
|
|
822
|
-
} catch {
|
|
823
|
-
return failed("invalid");
|
|
824
|
-
}
|
|
825
|
-
if (options.signal?.aborted) throw options.signal.reason;
|
|
826
|
-
return {
|
|
827
|
-
backend,
|
|
828
|
-
...parsed,
|
|
829
|
-
gate: "apply",
|
|
830
|
-
latencyMs: Math.max(0, now() - startedAt),
|
|
831
|
-
};
|
|
832
|
-
} catch (error) {
|
|
833
|
-
if (options.signal?.aborted) throw options.signal.reason;
|
|
834
|
-
if (timeoutSignal.aborted
|
|
835
|
-
|| (error instanceof DOMException && error.name === "TimeoutError")) {
|
|
836
|
-
return failed("timeout");
|
|
837
|
-
}
|
|
838
|
-
return failed("network");
|
|
839
|
-
}
|
|
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
|
+
};
|
|
840
654
|
}
|
|
841
655
|
|
|
842
656
|
export type JevDecisionProbeResult =
|
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
clearPendingConfigObjectChildDeletions,
|
|
9
9
|
prepareConfigObjectChildDeletionRebase,
|
|
10
10
|
clearPendingConfigTopLevelDeletions,
|
|
11
|
+
clearConfigTopLevelDeletionIntent,
|
|
11
12
|
configHasRebaseProvenance,
|
|
12
13
|
configRebaseDeletionKeys,
|
|
13
14
|
CONFIG_REBASE_PROVENANCE_KEY,
|
|
@@ -42,6 +43,22 @@ const claudeCodeBaseline = new WeakMap<OcxConfig, unknown>();
|
|
|
42
43
|
*/
|
|
43
44
|
const liveConfigBaseline = new WeakMap<OcxConfig, OcxConfig>();
|
|
44
45
|
|
|
46
|
+
/** Adopt a committed manual selection without advancing unrelated live merge baselines. */
|
|
47
|
+
export function adoptPersistedCodexAccountSelection(
|
|
48
|
+
config: OcxConfig,
|
|
49
|
+
persisted: Pick<OcxConfig, "activeCodexAccountId" | "activeCodexAccountPinned">,
|
|
50
|
+
): void {
|
|
51
|
+
const keys = ["activeCodexAccountId", "activeCodexAccountPinned"] as const;
|
|
52
|
+
const baseline = liveConfigBaseline.get(config);
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
config[key] = persisted[key];
|
|
55
|
+
// Retain known absent keys: a later disk-only selection is a new edit, not stale intent.
|
|
56
|
+
if (baseline) baseline[key] = persisted[key];
|
|
57
|
+
}
|
|
58
|
+
clearConfigTopLevelDeletionIntent(config, keys);
|
|
59
|
+
if (baseline) clearConfigTopLevelDeletionIntent(baseline, keys);
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
/**
|
|
46
63
|
* Adopt a committed discovery decision and its merge baseline as one synchronous step.
|
|
47
64
|
* Visibility writers assign and save synchronously; any future writer yielding between
|