@bitkyc08/opencodex 2.75.0 → 2.76.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 +6 -1
- package/bin/ocx.mjs +12 -1
- package/gui/dist/assets/App-CddorHdA.js +51 -0
- package/gui/dist/assets/App-DMudtISW.css +1 -0
- package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
- package/gui/dist/assets/index-Btze1I3T.css +1 -0
- package/gui/dist/assets/index-CvnY7E6i.js +86 -0
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opengateway.svg +12 -0
- package/package.json +1 -1
- package/src/adapters/anthropic/account-metadata.ts +17 -0
- package/src/adapters/anthropic/client-identity.ts +69 -0
- package/src/adapters/anthropic/passthrough.ts +23 -4
- package/src/adapters/anthropic.ts +18 -1
- package/src/adapters/command-code-project-context.ts +7 -11
- package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
- package/src/adapters/kiro-retry.ts +23 -8
- package/src/adapters/openai-chat/messages.ts +1 -1
- package/src/adapters/registry.ts +8 -1
- package/src/adapters/zed.ts +377 -0
- package/src/bridge/sse.ts +4 -2
- package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
- package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
- package/src/chatgpt/app-server-shim/filter.ts +157 -0
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
- package/src/chatgpt/app-server-shim/launcher.ts +91 -0
- package/src/claude/desktop-3p-startup.ts +62 -0
- package/src/claude/desktop-3p.ts +5 -0
- package/src/claude/first-party-settings.ts +14 -0
- package/src/claude/intercept/cli-catalog.ts +150 -0
- package/src/claude/intercept/cli-picker.ts +82 -0
- package/src/claude/intercept/listener.ts +21 -6
- package/src/claude/intercept/picker-bootstrap.ts +23 -5
- package/src/claude/intercept/picker-models.ts +57 -13
- package/src/claude/intercept/runtime.ts +30 -2
- package/src/claude/intercept/settings.ts +3 -0
- package/src/cli/agent.ts +172 -0
- package/src/cli/alias.ts +2 -1
- package/src/cli/capabilities-command.ts +3 -2
- package/src/cli/capabilities.ts +87 -0
- package/src/cli/chatgpt-command.ts +140 -0
- package/src/cli/combo.ts +144 -19
- package/src/cli/config-command.ts +3 -1
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -24
- package/src/cli/init.ts +16 -2
- package/src/cli/integrations.ts +13 -0
- package/src/cli/internal-command.ts +12 -5
- package/src/cli/observe.ts +6 -1
- package/src/cli/registry.ts +6 -0
- package/src/cli/route-policy.ts +3 -1
- package/src/cli/runtime-api.ts +5 -2
- package/src/cli/status-oauth.ts +5 -0
- package/src/cli/status.ts +8 -1
- package/src/cli/stop-approval.ts +55 -16
- package/src/cli/stop-report.ts +5 -0
- package/src/clients/lazycodex.ts +48 -0
- package/src/clients/omo-role-models.ts +111 -0
- package/src/codex/account-credit-use.ts +99 -0
- package/src/codex/account-lifecycle.ts +2 -0
- package/src/codex/agent-role-models.ts +171 -0
- package/src/codex/auth-api/account-list.ts +10 -3
- package/src/codex/auth-api/routes.ts +34 -0
- package/src/codex/auth-context.ts +52 -11
- package/src/codex/catalog/discovered-natives.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +6 -1
- package/src/codex/catalog/model-hints.ts +9 -1
- package/src/codex/catalog/provider-models.ts +60 -0
- package/src/codex/catalog/routed-gather.ts +30 -5
- package/src/codex/convergence-types.ts +2 -1
- package/src/codex/main-account-external-usage.ts +62 -0
- package/src/codex/main-account-hard-lock.ts +24 -13
- package/src/codex/native-profile-processes.ts +53 -16
- package/src/codex/prompt-layers/toml-edit.ts +1 -1
- package/src/codex/quota-types.ts +3 -7
- package/src/codex/quota.ts +19 -4
- package/src/codex/role-auto-assign.ts +159 -0
- package/src/codex/role-sizing-limits.ts +2 -0
- package/src/codex/role-sizing.ts +216 -0
- package/src/codex/routing/selection.ts +36 -0
- package/src/codex/routing/thread-affinity.ts +2 -0
- package/src/codex/routing.ts +4 -4
- package/src/codex/subagent-model-fallback.ts +82 -26
- package/src/codex/subagent-selectable-models.ts +32 -0
- package/src/combos/index.ts +18 -0
- package/src/combos/jev-decision-contract.ts +20 -0
- package/src/combos/jev-dispatch.ts +18 -0
- package/src/combos/jev-model-backend.ts +127 -0
- package/src/combos/jev.ts +249 -33
- package/src/combos/types.ts +125 -2
- package/src/config/diagnostics.ts +18 -0
- package/src/config/macos-system-proxy.ts +72 -15
- package/src/config/owner-registry.ts +13 -4
- package/src/config/process-state.ts +17 -8
- package/src/config/proxy-env.ts +35 -5
- package/src/config/schema/config-schema.ts +30 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config/serving-runtimes.ts +95 -1
- package/src/generated/compatibility-version.json +336 -156
- package/src/lib/errors.ts +7 -7
- package/src/lib/jsonc.ts +13 -0
- package/src/lib/local-chat-completion.ts +144 -0
- package/src/lib/provider-outbound.ts +87 -1
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +221 -0
- package/src/lib/request-execution-budget.ts +16 -6
- package/src/lib/spend-reservation-ledger.ts +54 -20
- package/src/lib/sse-decoder.ts +20 -17
- package/src/lib/synced-state-location.ts +96 -0
- package/src/lib/winsw.ts +14 -5
- package/src/oauth/anthropic-account-refusal.ts +53 -4
- package/src/oauth/anthropic-model-quota.ts +83 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
- package/src/oauth/anthropic-routing.ts +112 -58
- package/src/oauth/health.ts +39 -3
- package/src/oauth/index.ts +14 -0
- package/src/oauth/kiro-account-load.ts +10 -0
- package/src/oauth/zed.ts +134 -0
- package/src/providers/model-discovery.ts +19 -8
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/provider-id-rewrite.ts +9 -2
- package/src/providers/quota/account-cache.ts +29 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
- package/src/providers/quota/anthropic-family-headers.ts +25 -0
- package/src/providers/quota/vendor-probes-oauth.ts +5 -1
- package/src/providers/quota-types.ts +4 -0
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +55 -0
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/xai-fast-model.ts +12 -4
- package/src/providers/zed.ts +598 -0
- package/src/responses/state/snapshot-select.ts +22 -9
- package/src/responses/state.ts +2 -3
- package/src/router.ts +9 -0
- package/src/server/anthropic-rate-limit-headers.ts +7 -0
- package/src/server/auth-cors.ts +7 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/claude-messages.ts +59 -21
- package/src/server/index/claude-intercept-lifecycle.ts +71 -25
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/index/spend-ledger-lifecycle.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +74 -37
- package/src/server/management/claude-desktop-picker-routes.ts +7 -0
- package/src/server/management/claude-intercept-routes.ts +35 -0
- package/src/server/management/codex-agent-role-routes.ts +122 -0
- package/src/server/management/codex-role-auto-assign.ts +208 -0
- package/src/server/management/combo-routes.ts +62 -5
- package/src/server/management/config-routes.ts +35 -3
- package/src/server/management/context.ts +4 -1
- package/src/server/management/decision-discovery.ts +74 -0
- package/src/server/management/decision-model-validation.ts +80 -0
- package/src/server/management/decision-routes.ts +160 -0
- package/src/server/management/native-integration-routes.ts +16 -0
- package/src/server/management/provider-routes.ts +30 -34
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management/zed-provider-probe.ts +38 -0
- package/src/server/management-api.ts +12 -1
- package/src/server/messages-native-oauth.ts +7 -2
- package/src/server/messages-native.ts +7 -3
- package/src/server/relay.ts +3 -1
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +18 -3
- package/src/server/responses/compact.ts +14 -1
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-combo.ts +67 -11
- package/src/server/responses/core-normalize.ts +21 -1
- package/src/server/responses/core-options.ts +11 -1
- package/src/server/responses/core.ts +5 -3
- package/src/server/responses/fetch-helpers.ts +6 -3
- package/src/server/responses/jev-model-invoke.ts +190 -0
- package/src/server/responses/passthrough-delivery.ts +16 -0
- package/src/server/responses/policy-fallback.ts +55 -3
- package/src/server/responses/policy-request-scope.ts +80 -0
- package/src/server/responses/request-prepare.ts +70 -18
- package/src/server/responses/request-transport.ts +54 -23
- package/src/server/responses/run-turn-execution.ts +1 -0
- package/src/server/responses/sidecar-execution.ts +9 -3
- package/src/server/responses-hosted-image-display.ts +225 -0
- package/src/service/managing-cli.ts +34 -4
- package/src/service/systemd-env.ts +75 -0
- package/src/service/windows-guarded-manager.ts +60 -19
- package/src/service/windows-scheduler.ts +22 -3
- package/src/service/windows-taskxml.ts +32 -10
- package/src/service-manager-probe.ts +72 -19
- package/src/types/config.ts +31 -1
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +2 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +63 -63
- package/src/update/npm-cache-preflight.d.mts +33 -1
- package/src/update/npm-cache-preflight.mjs +131 -18
- package/src/update/ownership-transaction.ts +19 -1
- package/src/update/restart-ownership.ts +80 -6
- package/src/update/transactional-install.d.mts +2 -0
- package/src/update/transactional-install.mjs +6 -1
- package/src/usage/jev-stats.ts +28 -1
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +5 -3
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +2 -1
- package/src/vision/routed-describe.ts +29 -121
- package/src/web-search/alpha-search.ts +3 -3
- package/src/web-search/anthropic-executor.ts +6 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +3 -3
- package/src/web-search/passthrough-bridge.ts +2 -1
- package/src/web-search/run-turn-loop.ts +1 -1
- package/src/web-search/sidecar-providers.ts +2 -1
- package/gui/dist/assets/App-ia4U0ufY.css +0 -1
- package/gui/dist/assets/App-xm0zGNOe.js +0 -51
- package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
- package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
- package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
package/src/combos/jev.ts
CHANGED
|
@@ -3,21 +3,38 @@ import {
|
|
|
3
3
|
providerOutboundPost,
|
|
4
4
|
providerRedirectError,
|
|
5
5
|
} from "../lib/provider-outbound";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isKeychainReference,
|
|
8
|
+
keychainReferenceBelongsToProvider,
|
|
9
|
+
resolveProviderApiKey,
|
|
10
|
+
} from "../providers/api-key-resolve";
|
|
7
11
|
import { providerMatchesRegistryTransport } from "../providers/registry";
|
|
8
12
|
import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
|
|
9
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
isSystemOneEndpoint,
|
|
15
|
+
JEV_DECISION_TIMEOUT_DEFAULT_MS,
|
|
16
|
+
JEV_DECISION_TIMEOUT_MAX_MS,
|
|
17
|
+
JEV_DECISION_TIMEOUT_MIN_MS,
|
|
18
|
+
JEV_MAX_CANDIDATE_FIELD_CHARS,
|
|
19
|
+
} from "./types";
|
|
10
20
|
|
|
11
21
|
export const JEV_PROVIDER_ID = "jev";
|
|
12
22
|
export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
|
|
13
23
|
export const JEV_MODEL = "jev-latest";
|
|
14
24
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
25
|
+
export const JEV_MAX_CANDIDATES = 64;
|
|
26
|
+
/** Ollama's System One choice questions accept 2..26 options; other services share the floor. */
|
|
27
|
+
const SELF_HOSTED_MIN_OPTIONS = 2;
|
|
28
|
+
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;
|
|
19
33
|
const JEV_OUTBOUND_DEPENDENCIES = {
|
|
20
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,
|
|
21
38
|
};
|
|
22
39
|
|
|
23
40
|
const TASK_CHARS = 500;
|
|
@@ -74,7 +91,23 @@ export interface JevCandidate {
|
|
|
74
91
|
modelProfile?: string;
|
|
75
92
|
}
|
|
76
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Which kind of service answered a decision. `typesafe` is the canonical System One endpoint,
|
|
96
|
+
* `systemone` a configured System One-compatible row, `model` an opencodex-routed model.
|
|
97
|
+
*/
|
|
98
|
+
export type JevDecisionBackend = "typesafe" | "systemone" | "model";
|
|
99
|
+
|
|
100
|
+
/** The backend a combo's decision settings select. A decision model wins over a provider row. */
|
|
101
|
+
export function jevDecisionBackendFor(
|
|
102
|
+
combo: { decisionProvider?: string | null; decisionModel?: string | null },
|
|
103
|
+
): JevDecisionBackend {
|
|
104
|
+
if (typeof combo.decisionModel === "string" && combo.decisionModel.trim()) return "model";
|
|
105
|
+
const provider = typeof combo.decisionProvider === "string" ? combo.decisionProvider.trim() : "";
|
|
106
|
+
return provider && provider !== JEV_PROVIDER_ID ? "systemone" : "typesafe";
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
export interface JevDecision {
|
|
110
|
+
backend: JevDecisionBackend;
|
|
78
111
|
targetKey: string;
|
|
79
112
|
effort: OcxComboDefaultEffort | null;
|
|
80
113
|
gate: "apply" | "missing_key" | "no_choices" | "no_state" | "timeout" | "network" | "redirect" | "http" | "malformed" | "invalid";
|
|
@@ -89,6 +122,13 @@ export interface ResolveJevDecisionOptions {
|
|
|
89
122
|
candidates: readonly JevCandidate[];
|
|
90
123
|
fallback: { targetKey: string; effort: OcxComboDefaultEffort | null };
|
|
91
124
|
config: OcxConfig;
|
|
125
|
+
/**
|
|
126
|
+
* Decision service provider id. Omitted or `"jev"` keeps the canonical TypeSafe destination;
|
|
127
|
+
* any other id names a configured `jev-decision` row (for example a self-hosted Ollama `tev1`).
|
|
128
|
+
*/
|
|
129
|
+
decisionProvider?: string;
|
|
130
|
+
/** Decision deadline; values outside 1000..120000 ms keep the four-second default. */
|
|
131
|
+
timeoutMs?: number;
|
|
92
132
|
signal?: AbortSignal;
|
|
93
133
|
post?: typeof providerOutboundPost;
|
|
94
134
|
now?: () => number;
|
|
@@ -400,7 +440,7 @@ export function buildJevState(body: unknown, candidates: readonly JevCandidate[]
|
|
|
400
440
|
};
|
|
401
441
|
}
|
|
402
442
|
|
|
403
|
-
function hasJevDecisionState(state: Record<string, unknown>): boolean {
|
|
443
|
+
export function hasJevDecisionState(state: Record<string, unknown>): boolean {
|
|
404
444
|
if (typeof state.task === "string" && state.task.trim()) return true;
|
|
405
445
|
if (isRecord(state.signals) && state.signals.has_image === true) return true;
|
|
406
446
|
return isRecord(state.step)
|
|
@@ -431,7 +471,7 @@ function candidateOptions(candidates: readonly JevCandidate[]): Map<string, JevR
|
|
|
431
471
|
return options;
|
|
432
472
|
}
|
|
433
473
|
|
|
434
|
-
function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
|
|
474
|
+
export function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
|
|
435
475
|
if (candidates.length > JEV_MAX_CANDIDATES) return false;
|
|
436
476
|
return candidates.every(candidate => [candidate.key, candidate.provider, candidate.model]
|
|
437
477
|
.every(value => value.length > 0 && value.length <= JEV_MAX_CANDIDATE_FIELD_CHARS)
|
|
@@ -445,10 +485,47 @@ function modelProfile(candidate: JevCandidate): string {
|
|
|
445
485
|
?? "Configured target with capability unspecified by JEV; judge it only from the supplied request evidence.";
|
|
446
486
|
}
|
|
447
487
|
|
|
448
|
-
|
|
449
|
-
const
|
|
488
|
+
function criterionDescription(criterion: JevRouteOption["criterion"]): string {
|
|
489
|
+
const effort = criterion.reasoning_effort
|
|
490
|
+
? `${criterion.reasoning_effort} reasoning effort`
|
|
491
|
+
: "no reasoning-effort control";
|
|
492
|
+
return `Target ${criterion.target} (provider ${criterion.provider}, model ${criterion.model}) with ${effort}.`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** One allowlisted target/effort option, shared by every decision backend. */
|
|
496
|
+
export interface JevRouteOptionDescriptor {
|
|
497
|
+
key: string;
|
|
498
|
+
targetKey: string;
|
|
499
|
+
effort: OcxComboDefaultEffort | null;
|
|
500
|
+
description: string;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Every allowlisted option in question order; throws on a duplicate choice like the builder. */
|
|
504
|
+
export function jevRouteOptions(candidates: readonly JevCandidate[]): JevRouteOptionDescriptor[] {
|
|
505
|
+
return [...candidateOptions(candidates)].map(([key, option]) => ({
|
|
506
|
+
key,
|
|
507
|
+
targetKey: option.targetKey,
|
|
508
|
+
effort: option.effort,
|
|
509
|
+
description: criterionDescription(option.criterion),
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Build the single `route` choice question.
|
|
515
|
+
*
|
|
516
|
+
* TypeSafe receives each option's structured criterion object. `descriptiveCriteria` renders the
|
|
517
|
+
* same facts as one plain description string per option instead, the portable System One shape
|
|
518
|
+
* that self-hosted services such as Ollama require ("option keys to descriptions or null").
|
|
519
|
+
*/
|
|
520
|
+
export function buildJevRouteQuestion(
|
|
521
|
+
candidates: readonly JevCandidate[],
|
|
522
|
+
options: { descriptiveCriteria?: boolean } = {},
|
|
523
|
+
): Record<string, unknown> {
|
|
524
|
+
const routeOptions = candidateOptions(candidates);
|
|
450
525
|
const criteria: Record<string, unknown> = {};
|
|
451
|
-
for (const [choice, option] of
|
|
526
|
+
for (const [choice, option] of routeOptions) {
|
|
527
|
+
criteria[choice] = options.descriptiveCriteria ? criterionDescription(option.criterion) : option.criterion;
|
|
528
|
+
}
|
|
452
529
|
const modelProfiles: Record<string, string> = {};
|
|
453
530
|
for (const candidate of candidates) modelProfiles[candidate.key] = modelProfile(candidate);
|
|
454
531
|
return {
|
|
@@ -468,7 +545,7 @@ export function buildJevRouteQuestion(candidates: readonly JevCandidate[]): Reco
|
|
|
468
545
|
};
|
|
469
546
|
}
|
|
470
547
|
|
|
471
|
-
function jevUsage(payload: Record<string, unknown>): Record<string, number> | undefined {
|
|
548
|
+
export function jevUsage(payload: Record<string, unknown>): Record<string, number> | undefined {
|
|
472
549
|
if (!isRecord(payload.usage)) return undefined;
|
|
473
550
|
const usage: Record<string, number> = {};
|
|
474
551
|
for (const [key, value] of Object.entries(payload.usage)) {
|
|
@@ -530,12 +607,23 @@ export function parseJevDecision(
|
|
|
530
607
|
};
|
|
531
608
|
}
|
|
532
609
|
|
|
533
|
-
function fallbackDecision(
|
|
610
|
+
export function fallbackDecision(
|
|
534
611
|
fallback: ResolveJevDecisionOptions["fallback"],
|
|
535
612
|
gate: Exclude<JevDecision["gate"], "apply">,
|
|
536
613
|
latencyMs: number,
|
|
614
|
+
backend: JevDecisionBackend,
|
|
537
615
|
): JevDecision {
|
|
538
|
-
return { ...fallback, gate, latencyMs };
|
|
616
|
+
return { backend, ...fallback, gate, latencyMs };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
|
|
620
|
+
export function jevDecisionTimeoutMs(value: number | undefined): number {
|
|
621
|
+
return value !== undefined
|
|
622
|
+
&& Number.isInteger(value)
|
|
623
|
+
&& value >= JEV_DECISION_TIMEOUT_MIN_MS
|
|
624
|
+
&& value <= JEV_DECISION_TIMEOUT_MAX_MS
|
|
625
|
+
? value
|
|
626
|
+
: JEV_DECISION_TIMEOUT_DEFAULT_MS;
|
|
539
627
|
}
|
|
540
628
|
|
|
541
629
|
function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
|
|
@@ -549,8 +637,87 @@ function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
|
|
|
549
637
|
};
|
|
550
638
|
}
|
|
551
639
|
|
|
640
|
+
interface JevDecisionEndpoint {
|
|
641
|
+
name: string;
|
|
642
|
+
provider: OcxProviderConfig;
|
|
643
|
+
url: string;
|
|
644
|
+
model: string;
|
|
645
|
+
apiKey: string | undefined;
|
|
646
|
+
/** Self-hosted System One services accept only string option descriptions. */
|
|
647
|
+
descriptiveCriteria: boolean;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function envReferenceName(value: string): string | undefined {
|
|
651
|
+
const braced = /^\$\{(\w+)\}$/.exec(value);
|
|
652
|
+
if (braced) return braced[1];
|
|
653
|
+
return value.startsWith("$") ? value.slice(1) : undefined;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
|
|
658
|
+
* keys, and never a keychain entry that belongs to another provider. `null` means refused.
|
|
659
|
+
*/
|
|
660
|
+
function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
|
|
661
|
+
if (!apiKey) return undefined;
|
|
662
|
+
const envName = envReferenceName(apiKey);
|
|
663
|
+
if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
|
|
664
|
+
if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
|
|
665
|
+
return resolveProviderApiKey(apiKey)?.trim() || undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Resolve where one decision request goes and which credential it may carry.
|
|
670
|
+
*
|
|
671
|
+
* The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
|
|
672
|
+
* only while the row still matches the registry transport, and the environment fallbacks exist
|
|
673
|
+
* only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
|
|
674
|
+
* custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
|
|
675
|
+
* `/systemone` endpoint and which names its own model; only its own key may accompany it, so no
|
|
676
|
+
* TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
|
|
677
|
+
* service (reported through the existing `missing_key` gate).
|
|
678
|
+
*/
|
|
679
|
+
function jevDecisionEndpoint(config: OcxConfig, decisionProvider: string): JevDecisionEndpoint | undefined {
|
|
680
|
+
const configured = Object.hasOwn(config.providers, decisionProvider)
|
|
681
|
+
? config.providers[decisionProvider]
|
|
682
|
+
: undefined;
|
|
683
|
+
if (configured?.disabled === true) return undefined;
|
|
684
|
+
if (decisionProvider === JEV_PROVIDER_ID) {
|
|
685
|
+
const configuredOwnsJev = configured
|
|
686
|
+
&& providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
|
|
687
|
+
const apiKey = (
|
|
688
|
+
configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
|
|
689
|
+
) || process.env.TYPESAFE_API_KEY?.trim()
|
|
690
|
+
|| process.env.JEV_API_KEY?.trim();
|
|
691
|
+
if (!apiKey) return undefined;
|
|
692
|
+
return {
|
|
693
|
+
name: JEV_PROVIDER_ID,
|
|
694
|
+
provider: canonicalJevProvider(config),
|
|
695
|
+
url: JEV_API_URL,
|
|
696
|
+
model: JEV_MODEL,
|
|
697
|
+
apiKey,
|
|
698
|
+
descriptiveCriteria: false,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
|
|
702
|
+
const url = configured.baseUrl.trim().replace(/\/+$/, "");
|
|
703
|
+
if (!url || !isSystemOneEndpoint(url)) return undefined;
|
|
704
|
+
// `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
|
|
705
|
+
const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
|
|
706
|
+
if (!model) return undefined;
|
|
707
|
+
const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
|
|
708
|
+
if (apiKey === null) return undefined;
|
|
709
|
+
return {
|
|
710
|
+
name: decisionProvider,
|
|
711
|
+
provider: configured,
|
|
712
|
+
url,
|
|
713
|
+
model,
|
|
714
|
+
apiKey,
|
|
715
|
+
descriptiveCriteria: true,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
552
719
|
/**
|
|
553
|
-
* Ask
|
|
720
|
+
* Ask the configured JEV decision service for one allowlisted target/effort decision.
|
|
554
721
|
*
|
|
555
722
|
* Every operational or response failure returns the supplied first-eligible fallback. A caller
|
|
556
723
|
* abort is the exception: request cancellation remains cancellation and is rethrown by identity.
|
|
@@ -558,38 +725,40 @@ function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
|
|
|
558
725
|
export async function resolveJevDecision(options: ResolveJevDecisionOptions): Promise<JevDecision> {
|
|
559
726
|
const now = options.now ?? Date.now;
|
|
560
727
|
const startedAt = now();
|
|
728
|
+
const backend = jevDecisionBackendFor({ decisionProvider: options.decisionProvider });
|
|
561
729
|
const failed = (gate: Exclude<JevDecision["gate"], "apply">): JevDecision =>
|
|
562
|
-
fallbackDecision(options.fallback, gate, Math.max(0, now() - startedAt));
|
|
730
|
+
fallbackDecision(options.fallback, gate, Math.max(0, now() - startedAt), backend);
|
|
563
731
|
|
|
564
732
|
if (options.signal?.aborted) throw options.signal.reason;
|
|
565
733
|
if (options.candidates.length === 0) return failed("no_choices");
|
|
566
734
|
if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
|
|
567
735
|
|
|
568
|
-
const
|
|
569
|
-
if (
|
|
570
|
-
const configuredOwnsJev = configured
|
|
571
|
-
&& providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
|
|
572
|
-
const apiKey = (
|
|
573
|
-
configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
|
|
574
|
-
) || process.env.TYPESAFE_API_KEY?.trim()
|
|
575
|
-
|| process.env.JEV_API_KEY?.trim();
|
|
576
|
-
if (!apiKey) return failed("missing_key");
|
|
736
|
+
const endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID);
|
|
737
|
+
if (!endpoint) return failed("missing_key");
|
|
577
738
|
|
|
578
739
|
let requestBody: string;
|
|
579
740
|
try {
|
|
741
|
+
if (endpoint.descriptiveCriteria) {
|
|
742
|
+
// Self-hosted choice questions accept 2..26 options; decide locally instead of spending a
|
|
743
|
+
// round-trip on a request the service will refuse.
|
|
744
|
+
const optionCount = candidateOptions(options.candidates).size;
|
|
745
|
+
if (optionCount < SELF_HOSTED_MIN_OPTIONS) return failed("no_choices");
|
|
746
|
+
if (optionCount > SELF_HOSTED_MAX_OPTIONS) return failed("invalid");
|
|
747
|
+
}
|
|
580
748
|
const state = buildJevState(options.body, options.candidates);
|
|
581
749
|
if (!hasJevDecisionState(state)) return failed("no_state");
|
|
582
750
|
requestBody = JSON.stringify({
|
|
583
|
-
model:
|
|
751
|
+
model: endpoint.model,
|
|
584
752
|
state,
|
|
585
|
-
questions: buildJevRouteQuestion(options.candidates),
|
|
753
|
+
questions: buildJevRouteQuestion(options.candidates, { descriptiveCriteria: endpoint.descriptiveCriteria }),
|
|
586
754
|
});
|
|
587
755
|
if (new TextEncoder().encode(requestBody).byteLength > JEV_MAX_REQUEST_BYTES) return failed("invalid");
|
|
588
756
|
} catch {
|
|
589
757
|
return failed("invalid");
|
|
590
758
|
}
|
|
591
759
|
|
|
592
|
-
const
|
|
760
|
+
const timeoutMs = jevDecisionTimeoutMs(options.timeoutMs);
|
|
761
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
593
762
|
const signal = options.signal
|
|
594
763
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
595
764
|
: timeoutSignal;
|
|
@@ -597,12 +766,12 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
|
|
|
597
766
|
|
|
598
767
|
try {
|
|
599
768
|
const response = await post(
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
769
|
+
endpoint.name,
|
|
770
|
+
endpoint.provider,
|
|
771
|
+
endpoint.url,
|
|
603
772
|
{
|
|
604
773
|
headers: {
|
|
605
|
-
Authorization: `Bearer ${apiKey}
|
|
774
|
+
...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
|
|
606
775
|
"Content-Type": "application/json",
|
|
607
776
|
},
|
|
608
777
|
body: requestBody,
|
|
@@ -612,7 +781,7 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
|
|
|
612
781
|
);
|
|
613
782
|
if (options.signal?.aborted) throw options.signal.reason;
|
|
614
783
|
|
|
615
|
-
const redirectError = await providerRedirectError(response,
|
|
784
|
+
const redirectError = await providerRedirectError(response, endpoint.url);
|
|
616
785
|
if (redirectError) return failed("redirect");
|
|
617
786
|
if (!response.ok) {
|
|
618
787
|
try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
|
|
@@ -642,6 +811,7 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
|
|
|
642
811
|
}
|
|
643
812
|
if (options.signal?.aborted) throw options.signal.reason;
|
|
644
813
|
return {
|
|
814
|
+
backend,
|
|
645
815
|
...parsed,
|
|
646
816
|
gate: "apply",
|
|
647
817
|
latencyMs: Math.max(0, now() - startedAt),
|
|
@@ -655,3 +825,49 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
|
|
|
655
825
|
return failed("network");
|
|
656
826
|
}
|
|
657
827
|
}
|
|
828
|
+
|
|
829
|
+
export type JevDecisionProbeResult =
|
|
830
|
+
| { ok: true; latencyMs: number; message: string }
|
|
831
|
+
| { ok: false; latencyMs: number; error: string };
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Management connection test for one `jev-decision` provider row. It sends a fixed probe task and
|
|
835
|
+
* no user prompt, and reports only a sanitized gate. Canonical TypeSafe wording is kept for `jev`.
|
|
836
|
+
*/
|
|
837
|
+
export async function probeJevDecisionProvider(
|
|
838
|
+
config: OcxConfig,
|
|
839
|
+
name: string,
|
|
840
|
+
options: Pick<ResolveJevDecisionOptions, "signal" | "post"> = {},
|
|
841
|
+
): Promise<JevDecisionProbeResult> {
|
|
842
|
+
const canonical = name === JEV_PROVIDER_ID;
|
|
843
|
+
const label = canonical ? "TypeSafe JEV" : "JEV decision service";
|
|
844
|
+
const probe = { targetKey: "jev/probe", effort: null } as const;
|
|
845
|
+
const decision = await resolveJevDecision({
|
|
846
|
+
body: {
|
|
847
|
+
input: canonical
|
|
848
|
+
? "Verify the configured TypeSafe JEV decision service."
|
|
849
|
+
: "Verify the configured JEV decision service.",
|
|
850
|
+
},
|
|
851
|
+
candidates: [{
|
|
852
|
+
key: probe.targetKey,
|
|
853
|
+
provider: "jev",
|
|
854
|
+
model: "jev-latest",
|
|
855
|
+
// Self-hosted System One choice questions need at least two options.
|
|
856
|
+
reasoningEfforts: canonical ? [] : ["low", "high"],
|
|
857
|
+
}],
|
|
858
|
+
fallback: probe,
|
|
859
|
+
config,
|
|
860
|
+
decisionProvider: name,
|
|
861
|
+
...options,
|
|
862
|
+
});
|
|
863
|
+
if (decision.gate === "apply") {
|
|
864
|
+
return { ok: true, latencyMs: decision.latencyMs, message: `Connected. ${label} answered a decision probe.` };
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
ok: false,
|
|
868
|
+
latencyMs: decision.latencyMs,
|
|
869
|
+
error: decision.gate === "missing_key"
|
|
870
|
+
? (canonical ? "TypeSafe JEV API key is not configured" : "JEV decision service is disabled or not configured")
|
|
871
|
+
: `${canonical ? "TypeSafe JEV decision probe" : "JEV decision service probe"} failed (${decision.gate})`,
|
|
872
|
+
};
|
|
873
|
+
}
|
package/src/combos/types.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { isCodexReasoningEffort } from "../reasoning-effort";
|
|
1
|
+
import { isCodexReasoningEffort, isDeclaredReasoningEffort } from "../reasoning-effort";
|
|
2
2
|
import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
|
|
3
3
|
import type { OcxComboConfig, OcxComboCooldownWaitPolicy, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
|
|
4
|
-
import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers";
|
|
4
|
+
import { COMBO_NAMESPACE, isValidComboId, resolveComboId, targetKey } from "./identifiers";
|
|
5
|
+
import {
|
|
6
|
+
CANONICAL_JEV_DECISION_PROVIDER,
|
|
7
|
+
JEV_DECISION_TIMEOUT_MAX_MS,
|
|
8
|
+
JEV_DECISION_TIMEOUT_MIN_MS,
|
|
9
|
+
isSystemOneEndpoint,
|
|
10
|
+
} from "./jev-decision-contract";
|
|
5
11
|
|
|
6
12
|
export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0;
|
|
7
13
|
export const JEV_MAX_CANDIDATE_FIELD_CHARS = 512;
|
|
14
|
+
export {
|
|
15
|
+
JEV_DECISION_TIMEOUT_DEFAULT_MS,
|
|
16
|
+
JEV_DECISION_TIMEOUT_MAX_MS,
|
|
17
|
+
JEV_DECISION_TIMEOUT_MIN_MS,
|
|
18
|
+
isSystemOneEndpoint,
|
|
19
|
+
} from "./jev-decision-contract";
|
|
8
20
|
export { COMBO_NAMESPACE, preservesPhysicalComboProvider, isNativeAliasCombo, targetKey, parseComboModelId, comboModelId, comboPublicModelId, comboDisabledModelId, comboDisabledModelSelectors, resolveComboId, isValidComboId } from "./identifiers";
|
|
9
21
|
|
|
10
22
|
/**
|
|
@@ -52,6 +64,12 @@ export interface NormalizedComboConfig {
|
|
|
52
64
|
nativeAlias: boolean;
|
|
53
65
|
/** Display-only label for the catalog row, or null when unset. */
|
|
54
66
|
displayName: string | null;
|
|
67
|
+
/** JEV decision service provider id; absent means the canonical `jev` service. */
|
|
68
|
+
decisionProvider?: string;
|
|
69
|
+
/** Ordinary inference route used for JEV decisions. */
|
|
70
|
+
decisionModel?: string;
|
|
71
|
+
/** JEV decision deadline override; absent keeps the default four-second deadline. */
|
|
72
|
+
decisionTimeoutMs?: number;
|
|
55
73
|
targets: NormalizedComboTarget[];
|
|
56
74
|
}
|
|
57
75
|
|
|
@@ -102,8 +120,15 @@ export function comboAliasIssues(
|
|
|
102
120
|
|
|
103
121
|
export interface ComboValidationOptions {
|
|
104
122
|
requireEnabledTarget?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Save-time only: also reject a `decisionProvider` row the runtime would skip (disabled, or no
|
|
125
|
+
* model). Config-file load stays lenient so disabling a referenced row never breaks startup.
|
|
126
|
+
*/
|
|
127
|
+
requireUsableDecisionService?: boolean;
|
|
105
128
|
/** Full combos map for alias uniqueness checks; omitted during early config load. */
|
|
106
129
|
combos?: Record<string, OcxComboConfig>;
|
|
130
|
+
/** Ingress selector grammar, injected so combo validation never imports server routing. */
|
|
131
|
+
normalizeDecisionModel?: (model: string) => string;
|
|
107
132
|
/** Combo being renamed — its stored alias is excluded from uniqueness checks. */
|
|
108
133
|
excludeComboId?: string;
|
|
109
134
|
}
|
|
@@ -256,6 +281,72 @@ export function comboConfigIssues(
|
|
|
256
281
|
if (nativeAlias && (typeof body.displayName !== "string" || body.displayName.trim().length === 0)) {
|
|
257
282
|
issues.push({ path: ["displayName"], message: "displayName is required for native aliases" });
|
|
258
283
|
}
|
|
284
|
+
if (body.decisionProvider !== undefined && body.decisionProvider !== null) {
|
|
285
|
+
const decisionProvider = typeof body.decisionProvider === "string" ? body.decisionProvider.trim() : "";
|
|
286
|
+
if (!decisionProvider) {
|
|
287
|
+
issues.push({ path: ["decisionProvider"], message: "decisionProvider must be a non-empty provider name" });
|
|
288
|
+
} else if (body.strategy !== "jev") {
|
|
289
|
+
issues.push({ path: ["decisionProvider"], message: 'decisionProvider is only valid with strategy "jev"' });
|
|
290
|
+
} else if (decisionProvider === CANONICAL_JEV_DECISION_PROVIDER) {
|
|
291
|
+
// Explicit "jev" means exactly what omission means: the canonical TypeSafe service.
|
|
292
|
+
} else if (!Object.hasOwn(providers, decisionProvider)) {
|
|
293
|
+
issues.push({
|
|
294
|
+
path: ["decisionProvider"],
|
|
295
|
+
message: `decisionProvider "${decisionProvider}" is not configured`,
|
|
296
|
+
});
|
|
297
|
+
} else if (providers[decisionProvider]?.adapter !== "jev-decision") {
|
|
298
|
+
issues.push({
|
|
299
|
+
path: ["decisionProvider"],
|
|
300
|
+
message: `decisionProvider "${decisionProvider}" is not a decision service (adapter must be "jev-decision")`,
|
|
301
|
+
});
|
|
302
|
+
} else if (!isSystemOneEndpoint(String(providers[decisionProvider]?.baseUrl ?? ""))) {
|
|
303
|
+
issues.push({
|
|
304
|
+
path: ["decisionProvider"],
|
|
305
|
+
message: `decisionProvider "${decisionProvider}" baseUrl must be the full decision endpoint ending in /systemone`,
|
|
306
|
+
});
|
|
307
|
+
} else if (options.requireUsableDecisionService && providers[decisionProvider]?.disabled === true) {
|
|
308
|
+
issues.push({
|
|
309
|
+
path: ["decisionProvider"],
|
|
310
|
+
message: `decisionProvider "${decisionProvider}" is disabled`,
|
|
311
|
+
});
|
|
312
|
+
} else if (options.requireUsableDecisionService
|
|
313
|
+
&& !providers[decisionProvider]?.defaultModel?.trim()
|
|
314
|
+
&& !providers[decisionProvider]?.models?.[0]?.trim()) {
|
|
315
|
+
issues.push({
|
|
316
|
+
path: ["decisionProvider"],
|
|
317
|
+
message: `decisionProvider "${decisionProvider}" has no model (set defaultModel or models)`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (body.decisionModel !== undefined && body.decisionModel !== null) {
|
|
322
|
+
const model = typeof body.decisionModel === "string" ? body.decisionModel.trim() : "";
|
|
323
|
+
if (!model || model.length > JEV_MAX_CANDIDATE_FIELD_CHARS) {
|
|
324
|
+
issues.push({ path: ["decisionModel"], message: `decisionModel must be a non-empty string of at most ${JEV_MAX_CANDIDATE_FIELD_CHARS} characters` });
|
|
325
|
+
} else {
|
|
326
|
+
if (body.strategy !== "jev") {
|
|
327
|
+
issues.push({ path: ["decisionModel"], message: 'decisionModel is only valid with strategy "jev"' });
|
|
328
|
+
}
|
|
329
|
+
if (body.decisionProvider !== undefined && body.decisionProvider !== null) {
|
|
330
|
+
issues.push({ path: ["decisionModel"], message: "decisionModel cannot coexist with decisionProvider" });
|
|
331
|
+
}
|
|
332
|
+
const comboId = resolveComboId({ combos: options.combos }, options.normalizeDecisionModel?.(model) ?? model);
|
|
333
|
+
if (comboId === id || (comboId && options.combos?.[comboId]?.strategy === "jev")) {
|
|
334
|
+
issues.push({ path: ["decisionModel"], message: `decisionModel must not reference ${comboId === id ? "itself" : "a JEV combo"} (combo "${comboId}")` });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (body.decisionTimeoutMs !== undefined && body.decisionTimeoutMs !== null) {
|
|
339
|
+
if (typeof body.decisionTimeoutMs !== "number" || !Number.isInteger(body.decisionTimeoutMs)
|
|
340
|
+
|| body.decisionTimeoutMs < JEV_DECISION_TIMEOUT_MIN_MS
|
|
341
|
+
|| body.decisionTimeoutMs > JEV_DECISION_TIMEOUT_MAX_MS) {
|
|
342
|
+
issues.push({
|
|
343
|
+
path: ["decisionTimeoutMs"],
|
|
344
|
+
message: `decisionTimeoutMs must be an integer from ${JEV_DECISION_TIMEOUT_MIN_MS} to ${JEV_DECISION_TIMEOUT_MAX_MS}`,
|
|
345
|
+
});
|
|
346
|
+
} else if (body.strategy !== "jev") {
|
|
347
|
+
issues.push({ path: ["decisionTimeoutMs"], message: 'decisionTimeoutMs is only valid with strategy "jev"' });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
259
350
|
|
|
260
351
|
if (!Array.isArray(body.targets) || body.targets.length === 0) {
|
|
261
352
|
issues.push({ path: ["targets"], message: "targets must be a non-empty array" });
|
|
@@ -379,6 +470,8 @@ export function comboConfigError(
|
|
|
379
470
|
export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig {
|
|
380
471
|
const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
|
|
381
472
|
const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
|
|
473
|
+
const decisionProvider = typeof raw.decisionProvider === "string" ? raw.decisionProvider.trim() : "";
|
|
474
|
+
const decisionModel = typeof raw.decisionModel === "string" ? raw.decisionModel.trim() : "";
|
|
382
475
|
const defaultEffort = typeof raw.defaultEffort === "string" && isCodexReasoningEffort(raw.defaultEffort)
|
|
383
476
|
? raw.defaultEffort
|
|
384
477
|
: null;
|
|
@@ -395,6 +488,10 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
395
488
|
alias: alias || null,
|
|
396
489
|
nativeAlias: raw.nativeAlias === true,
|
|
397
490
|
displayName: displayName || null,
|
|
491
|
+
// Explicit "jev" is the default and stays sparse.
|
|
492
|
+
...(decisionProvider && decisionProvider !== CANONICAL_JEV_DECISION_PROVIDER ? { decisionProvider } : {}),
|
|
493
|
+
...(decisionModel ? { decisionModel } : {}),
|
|
494
|
+
...(typeof raw.decisionTimeoutMs === "number" ? { decisionTimeoutMs: raw.decisionTimeoutMs } : {}),
|
|
398
495
|
targets: raw.targets.map(target => ({
|
|
399
496
|
provider: target.provider.trim(),
|
|
400
497
|
model: target.model.trim(),
|
|
@@ -410,6 +507,32 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
410
507
|
};
|
|
411
508
|
}
|
|
412
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Load-time stand-in for the ingress synthetic-row grammar: strip a `--fast` suffix, and a
|
|
512
|
+
* `--<effort>` suffix when Cursor effort rows are on. It reads only the string, so schema
|
|
513
|
+
* validation needs no server module, model inventory, or Cursor install detection. The
|
|
514
|
+
* management save path still resolves the exact grammar against the live inventory.
|
|
515
|
+
*/
|
|
516
|
+
export function lexicalDecisionModelBase(model: string, cursorEffortRows: boolean): string {
|
|
517
|
+
if (model.endsWith("--fast")) return model.slice(0, -"--fast".length);
|
|
518
|
+
if (!cursorEffortRows) return model;
|
|
519
|
+
const separator = model.lastIndexOf("--");
|
|
520
|
+
if (separator <= 0) return model;
|
|
521
|
+
const effort = model.slice(separator + 2);
|
|
522
|
+
return effort !== "none" && isDeclaredReasoningEffort(effort) ? model.slice(0, separator) : model;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Whether removing `provider` would leave this stored combo invalid: a target uses it, or the
|
|
527
|
+
* combo names it as its JEV decision service. The canonical `jev` id stays valid without a row.
|
|
528
|
+
*/
|
|
529
|
+
export function comboDependsOnProvider(combo: OcxComboConfig, provider: string): boolean {
|
|
530
|
+
if (combo.targets.some(target => target.provider === provider)) return true;
|
|
531
|
+
if (typeof combo.decisionModel === "string" && combo.decisionModel.trim().startsWith(`${provider}/`)) return true;
|
|
532
|
+
const decisionProvider = typeof combo.decisionProvider === "string" ? combo.decisionProvider.trim() : "";
|
|
533
|
+
return decisionProvider === provider && provider !== CANONICAL_JEV_DECISION_PROVIDER;
|
|
534
|
+
}
|
|
535
|
+
|
|
413
536
|
export function comboDefaultEffort(
|
|
414
537
|
config: { combos?: Record<string, OcxComboConfig> },
|
|
415
538
|
id: string,
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
remoteGuiConfigSchema,
|
|
65
65
|
runtimeRoleSchema,
|
|
66
66
|
spendSchema,
|
|
67
|
+
chatgptDesktopSchema,
|
|
67
68
|
compactionRoutingSchema,
|
|
68
69
|
skillsConfigSchema,
|
|
69
70
|
memoryModelsSchema,
|
|
@@ -102,6 +103,9 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
|
|
|
102
103
|
const rawEffort = rawClaudeSubagentEffort(rawParsed);
|
|
103
104
|
const normalized = normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, rawParsed), rawParsed);
|
|
104
105
|
const warnings = configPlaceholderWarnings(normalized);
|
|
106
|
+
if (normalized.chatgptDesktop?.appServerShim === true && process.platform !== "darwin") {
|
|
107
|
+
warnings.push("chatgptDesktop.appServerShim is experimental and macOS only; ignored on this platform");
|
|
108
|
+
}
|
|
105
109
|
warnings.push(...inheritedFastWireConflictProviderNames(normalized).map(inheritedFastWireConflictWarning));
|
|
106
110
|
warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
|
|
107
111
|
warnings.push(...degradedListenerWarnings(rawParsed, normalized));
|
|
@@ -463,6 +467,15 @@ function showCodexCreditsError(value: unknown): string | null {
|
|
|
463
467
|
return "schema_invalid: showCodexCredits: must be a boolean or omitted";
|
|
464
468
|
}
|
|
465
469
|
|
|
470
|
+
function creditCodexAccountIdsError(value: unknown): string | null {
|
|
471
|
+
const raw = rawConfigRecord(value);
|
|
472
|
+
if (!raw || !Object.hasOwn(raw, "creditCodexAccountIds")) return null;
|
|
473
|
+
const ids = raw.creditCodexAccountIds;
|
|
474
|
+
if (ids === undefined) return null;
|
|
475
|
+
if (Array.isArray(ids) && ids.every(id => typeof id === "string" && /^[a-zA-Z0-9._-]{1,64}$/.test(id))) return null;
|
|
476
|
+
return "schema_invalid: creditCodexAccountIds: must be an array of account ids or omitted";
|
|
477
|
+
}
|
|
478
|
+
|
|
466
479
|
function oauthOpenBrowserError(value: unknown): string | null {
|
|
467
480
|
const raw = rawConfigRecord(value);
|
|
468
481
|
if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null;
|
|
@@ -619,6 +632,10 @@ function skillsConfigError(value: unknown): string | null {
|
|
|
619
632
|
}
|
|
620
633
|
|
|
621
634
|
export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } {
|
|
635
|
+
const chatgptDesktop = rawConfigRecord(value)?.chatgptDesktop;
|
|
636
|
+
if (chatgptDesktop !== undefined && !chatgptDesktopSchema.safeParse(chatgptDesktop).success) {
|
|
637
|
+
return { ok: false, error: "schema_invalid: chatgptDesktop: requires an optional boolean appServerShim and no other fields" };
|
|
638
|
+
}
|
|
622
639
|
const compactionRouting = rawConfigRecord(value)?.compactionRouting;
|
|
623
640
|
if (compactionRouting !== undefined && !compactionRoutingSchema.safeParse(compactionRouting).success) {
|
|
624
641
|
return { ok: false, error: "schema_invalid: compactionRouting: requires a nonblank model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" };
|
|
@@ -654,6 +671,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
654
671
|
?? dropCodexSafetyBufferingError(value)
|
|
655
672
|
?? oauthOpenBrowserError(value)
|
|
656
673
|
?? showCodexCreditsError(value)
|
|
674
|
+
?? creditCodexAccountIdsError(value)
|
|
657
675
|
?? runtimeRoleError(value)
|
|
658
676
|
?? remoteGuiConfigError(value)
|
|
659
677
|
?? clientConnectionConfigError(value)
|