@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.
Files changed (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -0,0 +1,74 @@
1
+ // Carried from PR #6185 by yxr1995-maker and PR #6275 by codingbooo.
2
+ /**
3
+ * Discovery of decision-capable models in the Codex catalog.
4
+ *
5
+ * A decision destination does not have to be configured by hand: gateways resell the System One
6
+ * contract under their own model ids (the OpenCode zen gateway serves `jev-1.13`, an aggregator may
7
+ * expose `typesafe-ai/jev`). This module answers the two pure questions such a search needs — which
8
+ * catalog rows look like decision models, and where that provider's System One endpoint lives — so
9
+ * the route only has to do the IO and the answer checks stay in one place.
10
+ */
11
+
12
+ export interface DiscoveryModelRow {
13
+ provider?: string;
14
+ id: string;
15
+ namespaced?: string;
16
+ owned_by?: string;
17
+ pricingStatus?: string;
18
+ displayName?: string;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Spellings that name a decision service rather than a generating model.
24
+ *
25
+ * Deliberately conservative: a bare `decision` or `jev` token, `systemone`, and the hyphenated
26
+ * spelling. Substring hits like `jeveux` do not qualify, because a false candidate costs a probe.
27
+ */
28
+ export const DECISION_MODEL_HINT = /(?:^|[^a-z0-9])(?:jev|systemone|system-one|decision)(?:[^a-z0-9]|$)/i;
29
+
30
+ const normalize = (value: string) => value.trim().toLowerCase();
31
+
32
+ /**
33
+ * True when a catalog row is worth probing as a decision model.
34
+ *
35
+ * An explicit query replaces the heuristic: an operator searching for a term knows what they mean,
36
+ * and a search that silently keeps the built-in filter would hide the model they asked for.
37
+ */
38
+ export function isDecisionModelCandidate(row: DiscoveryModelRow, query = ""): boolean {
39
+ const term = normalize(query);
40
+ const haystack = `${row.id} ${row.owned_by ?? ""} ${row.displayName ?? ""}`.toLowerCase();
41
+ if (term.length > 0) return haystack.includes(term);
42
+ return DECISION_MODEL_HINT.test(row.id) || DECISION_MODEL_HINT.test(row.owned_by ?? "");
43
+ }
44
+
45
+ /**
46
+ * The System One endpoint for a provider, derived from its API prefix.
47
+ *
48
+ * TypeSafe serves it at `<base>/v1/systemone` and the OpenCode zen gateway at `<base>/zen/v1/systemone`
49
+ * — in both cases the segment sits directly after the provider's own prefix, so appending is the
50
+ * rule, and a baseUrl that already names it is returned unchanged. Anything that is not an absolute
51
+ * http(s) URL yields `null` rather than a guess.
52
+ */
53
+ export function systemOneEndpoint(baseUrl: string | undefined): string | null {
54
+ const trimmed = (baseUrl ?? "").trim().replace(/\/+$/, "");
55
+ if (!/^https?:\/\//i.test(trimmed)) return null;
56
+ return trimmed.endsWith("/systemone") ? trimmed : `${trimmed}/systemone`;
57
+ }
58
+
59
+ /** Deduplicate by provider+model so a row that appears twice is probed once. */
60
+ export function uniqueDiscoveryCandidates(
61
+ rows: readonly DiscoveryModelRow[],
62
+ query = "",
63
+ ): DiscoveryModelRow[] {
64
+ const seen = new Set<string>();
65
+ const candidates: DiscoveryModelRow[] = [];
66
+ for (const row of rows) {
67
+ if (!row?.provider || typeof row.id !== "string" || !isDecisionModelCandidate(row, query)) continue;
68
+ const key = `${row.provider}/${row.id}`;
69
+ if (seen.has(key)) continue;
70
+ seen.add(key);
71
+ candidates.push(row);
72
+ }
73
+ return candidates;
74
+ }
@@ -0,0 +1,80 @@
1
+ import { resolveComboId } from "../../combos/identifiers";
2
+ import { comboDependsOnProvider } from "../../combos/types";
3
+ import { previewRouteModel } from "../../router";
4
+ import type { OcxComboConfig, OcxConfig, OcxProviderConfig } from "../../types";
5
+ import { parseSyntheticRowId } from "../fast-row";
6
+
7
+ /** Resolve synthetic selectors using the same prospective config as route preview. */
8
+ export function normalizeDecisionModelSelector(config: OcxConfig, model: string): string {
9
+ const parsed = parseSyntheticRowId(model, config);
10
+ return parsed.fastRow?.baseId ?? parsed.effortRow?.baseId ?? model;
11
+ }
12
+
13
+ /**
14
+ * Whether deleting provider `name` would leave `combo` invalid. Beyond the lexical check in
15
+ * `comboDependsOnProvider`, a decision model is resolved the way routing resolves it, so a
16
+ * provider alias (`judge/model`) or an unqualified model served by the default provider still
17
+ * counts as a dependency instead of silently falling through to whatever routes it next.
18
+ */
19
+ export function comboDependsOnProviderRoute(config: OcxConfig, combo: OcxComboConfig, name: string): boolean {
20
+ if (comboDependsOnProvider(combo, name)) return true;
21
+ if (typeof combo.decisionModel !== "string" || !combo.decisionModel.trim()) return false;
22
+ const selector = normalizeDecisionModelSelector(config, combo.decisionModel.trim());
23
+ const slash = selector.indexOf("/");
24
+ const prefix = slash > 0 ? selector.slice(0, slash).toLowerCase() : "";
25
+ if (prefix && (prefix === name.toLowerCase() || prefix === config.providers[name]?.alias?.toLowerCase())) return true;
26
+ try {
27
+ return previewRouteModel(config, selector).providerName === name;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /** Save-time validation uses preview routing, so it never advances combo selection state. */
34
+ export function decisionModelRouteError(config: OcxConfig, comboId: string | undefined, model: string): string | null {
35
+ // Standalone decision-test probes have no saved combo identity yet.
36
+ const subject = comboId ? `combo "${comboId}" decisionModel` : "decisionModel";
37
+ const selector = normalizeDecisionModelSelector(config, model.trim());
38
+ const referenced = resolveComboId(config, selector);
39
+ if (referenced === comboId || (referenced && config.combos?.[referenced]?.strategy === "jev")) {
40
+ return `${subject} must not reference ${referenced === comboId ? "itself" : "a JEV combo"} (combo "${referenced}")`;
41
+ }
42
+ try {
43
+ const route = previewRouteModel(config, selector);
44
+ // Match shadow-call validation: a qualified typo must not silently use the default row.
45
+ if (route.routeKind === "default-provider" && selector.includes("/")) {
46
+ return `${subject} must resolve to a configured provider`;
47
+ }
48
+ if (route.provider.adapter === "jev-decision") {
49
+ return `${subject} must not route to a jev-decision provider`;
50
+ }
51
+ if (route.combo && (route.combo.comboId === comboId || config.combos?.[route.combo.comboId]?.strategy === "jev")) {
52
+ return `${subject} must not route to JEV combo "${route.combo.comboId}"`;
53
+ }
54
+ return null;
55
+ } catch {
56
+ return `${subject} must resolve to a configured provider`;
57
+ }
58
+ }
59
+
60
+ /** Adapter edits cannot turn a referenced inference row into a decision-only service. */
61
+ export function decisionModelProviderPatchError(config: OcxConfig, name: string, candidate: OcxProviderConfig): string | null {
62
+ if (candidate.adapter !== "jev-decision") return null;
63
+ const prospective = { ...config, providers: { ...config.providers, [name]: candidate } };
64
+ for (const [id, combo] of Object.entries(config.combos ?? {})) {
65
+ if (typeof combo.decisionModel !== "string") continue;
66
+ const selector = normalizeDecisionModelSelector(config, combo.decisionModel.trim());
67
+ const slash = selector.indexOf("/");
68
+ const prefix = slash > 0 ? selector.slice(0, slash).toLowerCase() : "";
69
+ const explicit = !!prefix && (prefix === name.toLowerCase() || prefix === config.providers[name]?.alias?.toLowerCase());
70
+ // Unrelated unusable routes remain runtime fail-open cases. A qualified dependency
71
+ // still counts when the old row is disabled and preview cannot resolve it.
72
+ if (!explicit) {
73
+ try { if (previewRouteModel(config, selector).providerName !== name) continue; }
74
+ catch { continue; }
75
+ }
76
+ const error = decisionModelRouteError(prospective, id, combo.decisionModel);
77
+ if (error) return error;
78
+ }
79
+ return null;
80
+ }
@@ -0,0 +1,160 @@
1
+ import { jsonResponse } from "../auth-cors";
2
+ import type { ManagementContext } from "./context";
3
+ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
4
+ import { isPlainRecord } from "./shared";
5
+ import { systemOneEndpoint, uniqueDiscoveryCandidates, type DiscoveryModelRow } from "./decision-discovery";
6
+
7
+ const PROBE_TASK = "Verify the configured decision method with a two-option probe.";
8
+ const PROBE_CANDIDATE = {
9
+ key: "probe/decision",
10
+ provider: "probe",
11
+ model: "decision",
12
+ reasoningEfforts: ["low", "high"],
13
+ } as const;
14
+ const MAX_DECISION_MODEL_CHARS = 512;
15
+
16
+ function optionalString(body: Record<string, unknown>, key: string): string | null | undefined | false {
17
+ const value = body[key];
18
+ if (value === undefined || value === null) return undefined;
19
+ if (typeof value !== "string") return false;
20
+ const trimmed = value.trim();
21
+ return trimmed ? trimmed : null;
22
+ }
23
+
24
+ type DecisionRow = import("../../types").OcxProviderConfig;
25
+
26
+ /** Why a decision-service row cannot answer, shared by discovery and the probe. */
27
+ function decisionServiceIssue(
28
+ id: string,
29
+ provider: DecisionRow,
30
+ isSystemOneEndpoint: (url: string) => boolean,
31
+ ): { url: string; model: string; issue?: "disabled" | "endpoint" | "model" } {
32
+ const url = typeof provider.baseUrl === "string" ? provider.baseUrl.trim().replace(/\/+$/, "") : "";
33
+ const model = provider.defaultModel?.trim() || provider.models?.[0]?.trim() || "";
34
+ const issue = provider.disabled === true
35
+ ? "disabled"
36
+ : id === "jev"
37
+ ? undefined
38
+ : !isSystemOneEndpoint(url)
39
+ ? "endpoint"
40
+ : !model ? "model" : undefined;
41
+ return { url, model, ...(issue ? { issue } : {}) };
42
+ }
43
+
44
+ /**
45
+ * Decision-method management surfaces for JEV combos.
46
+ *
47
+ * - POST /api/combos/decision-test runs one bounded decision with a synthetic two-option probe
48
+ * through the same backend a combo would use, saved or not. It may spend a decision call.
49
+ * - GET /api/combos/decision-discovery lists configured System One rows and catalog models whose
50
+ * names look like decision services, with the System One endpoint their provider would serve.
51
+ * Nothing is probed and nothing is written.
52
+ *
53
+ * The probe selects its method only from the body's `decisionProvider` / `decisionModel`; none
54
+ * means TypeSafe. `comboId` names the combo for the recursion rules and never loads its saved
55
+ * method, because the dashboard probes an unsaved TypeSafe draft by sending `comboId` alone.
56
+ * `ocx combo test --combo` resolves the saved method on the client before calling this.
57
+ */
58
+ export async function handleDecisionRoutes(ctx: ManagementContext): Promise<Response | null> {
59
+ const { req, url, config } = ctx;
60
+
61
+ if (url.pathname === "/api/combos/decision-test" && req.method === "POST") {
62
+ let rawBody: unknown;
63
+ try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
64
+ if (!isPlainRecord(rawBody)) return jsonResponse({ error: "request body must be an object" }, 400);
65
+ const decisionProvider = optionalString(rawBody, "decisionProvider");
66
+ const decisionModel = optionalString(rawBody, "decisionModel");
67
+ const comboId = optionalString(rawBody, "comboId");
68
+ if (decisionProvider === false || decisionModel === false || comboId === false) {
69
+ return jsonResponse({ error: "decisionProvider, decisionModel and comboId must be strings" }, 400);
70
+ }
71
+ if (decisionProvider && decisionModel) {
72
+ return jsonResponse({ error: "choose either decisionProvider or decisionModel, not both" }, 400);
73
+ }
74
+ const timeout = rawBody.decisionTimeoutMs;
75
+ const { JEV_DECISION_TIMEOUT_MAX_MS, JEV_DECISION_TIMEOUT_MIN_MS } = await import("../../combos/types");
76
+ if (timeout !== undefined && timeout !== null && (typeof timeout !== "number" || !Number.isInteger(timeout)
77
+ || timeout < JEV_DECISION_TIMEOUT_MIN_MS || timeout > JEV_DECISION_TIMEOUT_MAX_MS)) {
78
+ return jsonResponse({
79
+ error: `decisionTimeoutMs must be an integer from ${JEV_DECISION_TIMEOUT_MIN_MS} to ${JEV_DECISION_TIMEOUT_MAX_MS}`,
80
+ }, 400);
81
+ }
82
+ if (decisionProvider && decisionProvider !== "jev") {
83
+ const row = Object.hasOwn(config.providers, decisionProvider) ? config.providers[decisionProvider] : undefined;
84
+ if (row?.adapter !== "jev-decision") {
85
+ return jsonResponse({ error: `decisionProvider "${decisionProvider}" is not a configured decision service` }, 400);
86
+ }
87
+ // Match the combo PUT: a disabled or model-less row is refused by name, not probed into a
88
+ // generic fail-open gate.
89
+ const { isSystemOneEndpoint } = await import("../../combos/types");
90
+ const { issue } = decisionServiceIssue(decisionProvider, row, isSystemOneEndpoint);
91
+ if (issue) {
92
+ return jsonResponse({ error: `decisionProvider "${decisionProvider}" is not usable (${issue})`, issue }, 400);
93
+ }
94
+ }
95
+ if (decisionModel) {
96
+ if (decisionModel.length > MAX_DECISION_MODEL_CHARS) {
97
+ return jsonResponse({ error: "decisionModel is too long" }, 400);
98
+ }
99
+ const { decisionModelRouteError } = await import("./decision-model-validation");
100
+ const routeError = decisionModelRouteError(config, comboId || undefined, decisionModel);
101
+ if (routeError) return jsonResponse({ error: routeError }, 400);
102
+ }
103
+ const { resolveJevComboDecision } = await import("../../combos/jev-dispatch");
104
+ let invokeModel: import("../../combos/jev-model-backend").JevModelInvoke | undefined;
105
+ if (decisionModel) {
106
+ const [{ createJevModelInvoker }, { handleResponses }] = await Promise.all([
107
+ import("../responses/jev-model-invoke"),
108
+ import("../responses/core"),
109
+ ]);
110
+ invokeModel = createJevModelInvoker({
111
+ req: new Request("http://localhost/api/combos/decision-test"),
112
+ config,
113
+ options: {},
114
+ handleResponses,
115
+ });
116
+ }
117
+ const fallback = { targetKey: PROBE_CANDIDATE.key, effort: null };
118
+ const decision = await resolveJevComboDecision({
119
+ body: { input: PROBE_TASK },
120
+ candidates: [{ ...PROBE_CANDIDATE, reasoningEfforts: [...PROBE_CANDIDATE.reasoningEfforts] }],
121
+ fallback,
122
+ config,
123
+ ...(decisionProvider ? { decisionProvider } : {}),
124
+ ...(decisionModel && invokeModel ? { decisionModel, invokeModel } : {}),
125
+ ...(typeof timeout === "number" ? { timeoutMs: timeout } : {}),
126
+ signal: req.signal,
127
+ });
128
+ return jsonResponse({
129
+ ok: decision.gate === "apply",
130
+ backend: decision.backend,
131
+ gate: decision.gate,
132
+ latencyMs: decision.latencyMs,
133
+ ...(decision.gate === "apply" && decision.effort ? { effort: decision.effort } : {}),
134
+ });
135
+ }
136
+
137
+ if (url.pathname === "/api/combos/decision-discovery" && req.method === "GET") {
138
+ const query = (url.searchParams.get("q") ?? "").slice(0, 128);
139
+ const { isSystemOneEndpoint } = await import("../../combos/types");
140
+ const configured = Object.entries(config.providers)
141
+ .filter(([, provider]) => provider.adapter === "jev-decision")
142
+ .map(([id, provider]) => {
143
+ const { url, model, issue } = decisionServiceIssue(id, provider, isSystemOneEndpoint);
144
+ return { id, url, ...(model ? { model } : {}), usable: issue === undefined, ...(issue ? { issue } : {}) };
145
+ });
146
+ const { listManagementModelRows } = await import("./model-rows");
147
+ const rows = (await listManagementModelRows(config)) as unknown as DiscoveryModelRow[];
148
+ const discovered = uniqueDiscoveryCandidates(rows, query)
149
+ .flatMap(row => {
150
+ const provider = config.providers[row.provider!];
151
+ if (!provider || provider.disabled === true || provider.adapter === "jev-decision") return [];
152
+ const endpoint = systemOneEndpoint(provider.baseUrl);
153
+ return endpoint ? [{ provider: row.provider!, model: row.id, endpoint }] : [];
154
+ })
155
+ .slice(0, 50);
156
+ return jsonResponse({ configured, discovered });
157
+ }
158
+
159
+ return null;
160
+ }
@@ -1,3 +1,4 @@
1
+ import { ensureManagementClaudeIntercept, interceptStartRefusal } from "./claude-intercept-routes";
1
2
  import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
2
3
  import { commitClaudeCodeBlock } from "../../claude/claude-code-block";
3
4
  /**
@@ -94,6 +95,7 @@ export interface NativeToggleEnvelope {
94
95
  desiredEnabled: boolean;
95
96
  /** Present when the outcome needs more than success/failure to be honest. */
96
97
  reason?: string;
98
+ interceptReason?: string | null;
97
99
  artifacts?: CodexNativeRestoreResult["artifacts"];
98
100
  }
99
101
 
@@ -748,6 +750,13 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
748
750
  }
749
751
 
750
752
  if (resolveClaudeDesktopApplyMode(current, observeClaudeDesktopMode(current)) === "first-party") {
753
+ const startRefusal = interceptStartRefusal(ctx);
754
+ if (startRefusal) return startRefusal;
755
+ const started = await ensureManagementClaudeIntercept(ctx);
756
+ if (!started.ok) return jsonResponse({ ...started, code: started.reason }, 409);
757
+ const { claudeInterceptProxyPort } = await import("../../claude/intercept/runtime");
758
+ const configured = claudeInterceptProxyPort(current, current.port ?? 10100);
759
+ if (started.state.proxyPort !== configured) return jsonResponse({ ok: false, code: "port_mismatch", bound: started.state.proxyPort, configured }, 409);
751
760
  // The whole switch runs under the picker lock, in today's order: env first, then gateway cleanup.
752
761
  return await runPickerTransition(current, async ops => {
753
762
  const rollback = captureDesktopFirstPartyRollback(current);
@@ -924,11 +933,18 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
924
933
  throw error;
925
934
  }
926
935
 
936
+ let interceptReason = enabled && interceptStartRefusal(ctx) ? "intercept_start_forbidden" : null;
937
+ if (enabled && !interceptReason) {
938
+ const outcome = await ensureManagementClaudeIntercept(ctx);
939
+ if (!outcome.ok) interceptReason = outcome.reason;
940
+ }
941
+
927
942
  return jsonResponse({
928
943
  ok: true, clientId: "claude", changed: true,
929
944
  state: enabled ? "current" : "absent",
930
945
  desiredEnabled: enabled,
931
946
  message: enabled ? "Claude inbound enabled" : "Claude inbound disabled",
947
+ interceptReason,
932
948
  } satisfies NativeToggleEnvelope);
933
949
  }
934
950
 
@@ -74,7 +74,8 @@ import { clearAccountQuotaCache, clearProviderQuotaCache, fetchProviderQuotaRepo
74
74
  import { getCachedProviderRoutingQuota } from "../../providers/quota-routing-cache";
75
75
  import { PROVIDER_QUOTA_MAX_AGE_MS, type ProviderRoutingQuota } from "../../providers/quota-types";
76
76
  import { cachedProviderQuotaIsExhausted } from "../../combos/resolve";
77
- import { resolveJevDecision } from "../../combos/jev";
77
+ import { probeJevDecisionProvider } from "../../combos/jev";
78
+ import { comboDependsOnProviderRoute } from "./decision-model-validation";
78
79
  import { clearKeyCooldowns, forgetApiKeyRotationCursor } from "../../providers/key-failover";
79
80
  import { providerRequestPacingStatus } from "../../providers/request-pacing";
80
81
  import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
@@ -137,6 +138,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
137
138
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
138
139
  import type { ManagementContext } from "./context";
139
140
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
141
+ import { providerTlsProfileDiagnostic } from "../../lib/provider-tls-profile";
140
142
 
141
143
  type ProviderPatchApplication =
142
144
  | { error: string }
@@ -245,7 +247,7 @@ function providerEditorCandidate(
245
247
 
246
248
  for (const name of removedProviders) {
247
249
  const dependentCombos = Object.entries(persisted.combos ?? {})
248
- .filter(([, combo]) => combo.targets.some(target => target.provider === name))
250
+ .filter(([, combo]) => comboDependsOnProviderRoute(persisted, combo, name))
249
251
  .map(([id]) => id)
250
252
  .sort((a, b) => a.localeCompare(b));
251
253
  if (dependentCombos.length > 0) {
@@ -930,8 +932,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
930
932
 
931
933
  if (url.pathname === "/api/providers" && req.method === "GET") {
932
934
  return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
933
- name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,
934
- hasApiKey: !!p.apiKey,
935
+ name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel, hasApiKey: !!p.apiKey,
935
936
  // Presence only (#959 review): header names and values never leave the process.
936
937
  hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
937
938
  allowPrivateNetwork: p.allowPrivateNetwork === true,
@@ -962,7 +963,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
962
963
  ...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
963
964
  // Only opt-in Fast lanes (Anthropic fast mode bills usage credits) get a dashboard switch.
964
965
  ...(getProviderRegistryEntry(name)?.fastOptIn === true ? { fastOptIn: { enabled: p.fastEnabled === true } } : {}),
965
- discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name),
966
+ discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name), ...providerTlsProfileDiagnostic(name, p),
966
967
  ...(name === "openai" && isCanonicalOpenAiForwardProvider(p)
967
968
  ? { entitlement: getCodexModelEntitlementStatus(config) }
968
969
  : {}),
@@ -1489,6 +1490,15 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1489
1490
  const next = applied.next;
1490
1491
 
1491
1492
  const pacingOnly = keys.every(key => key === "requestPacing");
1493
+ // A combo naming this row as its decisionProvider is only as valid as the row: an adapter
1494
+ // or baseUrl edit could fail the combo's load-time checks and get it salvaged away on the
1495
+ // next reload. Disabling the row stays allowed (not editorTouched; runtime fails open).
1496
+ const decisionDependent = () => Object.values(config.combos ?? {})
1497
+ .some(combo => (typeof combo.decisionProvider === "string" && combo.decisionProvider.trim() === name)
1498
+ || (typeof combo.decisionModel === "string" && combo.decisionModel.trim().startsWith(`${name}/`)));
1499
+ const { decisionModelProviderPatchError } = await import("./decision-model-validation");
1500
+ const decisionModelError = applied.editorTouched ? decisionModelProviderPatchError(config, name, next) : null;
1501
+ if (decisionModelError) return jsonResponse({ error: decisionModelError }, 400);
1492
1502
  if (applied.editorTouched && !pacingOnly) {
1493
1503
  const providerError = canonicalBudgetOnly
1494
1504
  ? canonicalOpenAiBudgetPatchError(next, rawBody, keys, config)
@@ -1514,6 +1524,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1514
1524
  const resolvedError = await providerDestinationResolvedError(name, next, { allowBenchmarkAddresses });
1515
1525
  if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
1516
1526
  }
1527
+ if (decisionDependent()) {
1528
+ const validation = validateConfigCandidate({ ...config, providers: { ...config.providers, [name]: next } });
1529
+ if (!validation.ok) return jsonResponse({ error: validation.error }, 400);
1530
+ }
1517
1531
  } else if (applied.enablingOpenAi) {
1518
1532
  // Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
1519
1533
  // allowPrivateNetwork on this path — it must not bypass the built-in guard.
@@ -1567,8 +1581,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1567
1581
  // A PATCH that managed headers owns the resulting block: the clear path restores
1568
1582
  // registry static headers, so exact-match stripping must not erase them again.
1569
1583
  const candidate = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
1584
+ const decisionModelError = replay.editorTouched ? decisionModelProviderPatchError(config, name, candidate) : null;
1585
+ if (decisionModelError) { replayError = decisionModelError; return; }
1570
1586
  const pinsTouched = Object.hasOwn(rawBody, "pinnedReasoningEffort") || Object.hasOwn(rawBody, "modelPinnedReasoningEfforts");
1571
- if (pinsTouched) {
1587
+ if (pinsTouched || (replay.editorTouched && !pacingOnly && decisionDependent())) {
1572
1588
  const validation = validateConfigCandidate({ ...config, providers: { ...config.providers, [name]: candidate } });
1573
1589
  if (!validation.ok) { replayError = validation.error; return; }
1574
1590
  }
@@ -1616,34 +1632,13 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1616
1632
  message: "Passthrough provider is configured (forwards your Codex login; no upstream /models).",
1617
1633
  });
1618
1634
  }
1619
- if (name === "jev" && providerMatchesRegistryTransport(name, prov)) {
1620
- const probe = { targetKey: "jev/probe", effort: null } as const;
1621
- const decision = await resolveJevDecision({
1622
- body: { input: "Verify the configured TypeSafe JEV decision service." },
1623
- candidates: [{
1624
- key: probe.targetKey,
1625
- provider: "jev",
1626
- model: "jev-latest",
1627
- reasoningEfforts: [],
1628
- }],
1629
- fallback: probe,
1630
- config,
1631
- signal: req.signal,
1632
- });
1633
- if (decision.gate === "apply") {
1634
- return jsonResponse({
1635
- ok: true,
1636
- latencyMs: decision.latencyMs,
1637
- message: "Connected. TypeSafe JEV answered a decision probe.",
1638
- });
1635
+ // Decision services answer through the bounded JEV client. A retargeted `jev` row is neither
1636
+ // TypeSafe nor a self-hosted service, so it is not probed at all: its key stays unsent.
1637
+ if (prov.adapter === "jev-decision") {
1638
+ if (name === "jev" && !providerMatchesRegistryTransport(name, prov)) {
1639
+ return jsonResponse({ applicable: false, reason: "retargeted_decision_service", latencyMs: 0 });
1639
1640
  }
1640
- return jsonResponse({
1641
- ok: false,
1642
- latencyMs: decision.latencyMs,
1643
- error: decision.gate === "missing_key"
1644
- ? "TypeSafe JEV API key is not configured"
1645
- : `TypeSafe JEV decision probe failed (${decision.gate})`,
1646
- });
1641
+ return jsonResponse(await probeJevDecisionProvider(config, name, { signal: req.signal }));
1647
1642
  }
1648
1643
  if (prov.liveModels === false) {
1649
1644
  // A static catalog has no live discovery endpoint to test. This is neither
@@ -1660,6 +1655,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1660
1655
  if (prov.authMode === "oauth" && !apiKey) {
1661
1656
  return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
1662
1657
  }
1658
+ if (prov.adapter === "zed") return (await import("./zed-provider-probe")).probeZedProvider(prov, apiKey, snapshot?.providerUserId);
1663
1659
  if (prov.adapter === "cursor") {
1664
1660
  const started = Date.now();
1665
1661
  const live = await fetchCursorUsableModels({
@@ -1845,7 +1841,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1845
1841
  }, 409);
1846
1842
  }
1847
1843
  const dependentCombos = Object.entries(config.combos ?? {})
1848
- .filter(([, combo]) => combo.targets.some(target => target.provider === name))
1844
+ .filter(([, combo]) => comboDependsOnProviderRoute(config, combo, name))
1849
1845
  .map(([id]) => id)
1850
1846
  .sort((a, b) => a.localeCompare(b));
1851
1847
  if (dependentCombos.length > 0) {
@@ -121,6 +121,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
121
121
  { method: "PUT", path: "/api/codex-auth/accounts/alias", module: "codex/auth-api/routes", mutates: true },
122
122
  { method: "PUT", path: "/api/codex-auth/accounts/pause", module: "codex/auth-api/routes", mutates: true },
123
123
  { method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted", module: "codex/auth-api/routes", mutates: true },
124
+ { method: "PUT", path: "/api/codex-auth/accounts/credits", module: "codex/auth-api/routes", mutates: true, exempt: { reason: "deferred-verb", why: "The credits switches ship on the dashboard Codex Auth header first; `ocx config set creditCodexAccountIds` covers scripted use until an account verb exists.", owner: "#6334 follow-up", ownerDoc: "structure/codex-account-controls.md" } },
124
125
  { method: "PUT", path: "/api/codex-auth/accounts/priority", module: "codex/auth-api/routes", mutates: true },
125
126
  { method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api/routes", mutates: true },
126
127
  { method: "PUT", path: "/api/codex-auth/auto-switch", module: "codex/auth-api/routes", mutates: true },
@@ -144,6 +145,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
144
145
  { method: "GET", path: "/api/effort-caps", module: "server/management/agent-settings-routes", mutates: false },
145
146
  { method: "GET", path: "/api/grok", module: "server/management/agent-settings-routes", mutates: false },
146
147
  { method: "GET", path: "/api/injection-model", module: "server/management/agent-settings-routes", mutates: false },
148
+ { method: "POST", path: "/api/injection-model/suggest", module: "server/management/agent-settings-routes", mutates: false },
147
149
  { method: "GET", path: "/api/subagent-model-fallback", module: "server/management/agent-settings-routes", mutates: false },
148
150
  { method: "GET", path: "/api/subagent-models", module: "server/management/agent-settings-routes", mutates: false },
149
151
  { method: "GET", path: "/api/v2", module: "server/management/agent-settings-routes", mutates: false },
@@ -156,6 +158,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
156
158
  { method: "PUT", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: true },
157
159
  { method: "PUT", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: true },
158
160
  { method: "PUT", path: "/api/claude-desktop/first-party-bindings", module: "server/management/agent-settings-routes", mutates: true },
161
+ // server/management/claude-intercept-routes
162
+ { method: "POST", path: "/api/claude-intercept/start", module: "server/management/claude-intercept-routes", mutates: true },
163
+
159
164
  // server/management/claude-desktop-picker-routes
160
165
  { method: "GET", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: false },
161
166
  { method: "PUT", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: true },
@@ -166,6 +171,10 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
166
171
  { method: "PUT", path: "/api/subagent-model-fallback", module: "server/management/agent-settings-routes", mutates: true },
167
172
  { method: "PUT", path: "/api/subagent-models", module: "server/management/agent-settings-routes", mutates: true },
168
173
  { method: "PUT", path: "/api/v2", module: "server/management/agent-settings-routes", mutates: true },
174
+ // server/management/codex-agent-role-routes
175
+ { method: "GET", path: "/api/codex-agent-roles", module: "server/management/codex-agent-role-routes", mutates: false },
176
+ { method: "POST", path: "/api/codex-agent-roles/auto-assign", module: "server/management/codex-agent-role-routes", mutates: false, mechanism: "path-constant" },
177
+ { method: "PUT", path: "/api/codex-agent-roles/{role}", module: "server/management/codex-agent-role-routes", mutates: true, mechanism: "prefix-decode" },
169
178
  // server/management/aside-profile-routes
170
179
  { method: "GET", path: "/api/client-integrations/aside", module: "server/management/aside-profile-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "compatibility-alias", why: "Legacy Aside status alias; the current CLI reads the same aggregate through GET /api/client-integrations/aside/profiles." } },
171
180
  { method: "PUT", path: "/api/client-integrations/aside", module: "server/management/aside-profile-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "compatibility-alias", why: "Legacy Aside toggle alias; the current CLI uses PUT /api/client-integrations/aside/profiles so older servers cannot mistake a bulk request for a current-account toggle." } },
@@ -193,6 +202,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
193
202
  { method: "DELETE", path: "/api/combos", module: "server/management/combo-routes", mutates: true },
194
203
  { method: "GET", path: "/api/combos", module: "server/management/combo-routes", mutates: false },
195
204
  { method: "PUT", path: "/api/combos", module: "server/management/combo-routes", mutates: true },
205
+ // server/management/decision-routes
206
+ { method: "GET", path: "/api/combos/decision-discovery", module: "server/management/decision-routes", mutates: false },
207
+ { method: "POST", path: "/api/combos/decision-test", module: "server/management/decision-routes", mutates: true },
196
208
  // server/management/config-routes
197
209
  { method: "GET", path: "/api/config", module: "server/management/config-routes", mutates: false },
198
210
  { method: "GET", path: "/api/diagnostics/project-config", module: "server/management/config-routes", mutates: false },
@@ -34,6 +34,7 @@ export const SIBLING_REFUSED_MANAGEMENT_PATHS: readonly { readonly path: string;
34
34
  { path: "/api/grok/apply", children: false },
35
35
  { path: "/api/grok/selection", children: false },
36
36
  { path: "/api/codex-prompt", children: true },
37
+ { path: "/api/codex-agent-roles", children: true },
37
38
  { path: CODEX_RESTART_PATH, children: false },
38
39
  { path: "/api/link/join", children: false },
39
40
  { path: "/api/native-main-profiles", children: true },
@@ -0,0 +1,38 @@
1
+ import { resolveZedModels } from "../../providers/zed";
2
+ import type { OcxProviderConfig } from "../../types";
3
+ import { jsonResponse } from "../auth-cors";
4
+
5
+ /**
6
+ * Connection test for the experimental Zed Hosted AI provider.
7
+ *
8
+ * Lives outside `provider-routes.ts`, which sits at the file-size threshold, and is reached by a
9
+ * dynamic import so Zed code stays off the module graph until someone tests a Zed row. The probe
10
+ * forces a live roster fetch, which exercises the account lookup, the short-lived LLM token
11
+ * exchange and the model list in one call. Failures return a fixed message: an upstream error can
12
+ * echo account material, and the caller only needs to know that discovery failed.
13
+ */
14
+ export async function probeZedProvider(
15
+ prov: OcxProviderConfig,
16
+ apiKey: string | undefined,
17
+ zedUserId: string | undefined,
18
+ ): Promise<Response> {
19
+ const started = Date.now();
20
+ if (!zedUserId) {
21
+ return jsonResponse({ ok: false, latencyMs: 0, error: "Zed account identity is unavailable — re-run `ocx login zed`" });
22
+ }
23
+ try {
24
+ const zedFetch = (prov as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch;
25
+ const live = await resolveZedModels(
26
+ { userId: zedUserId, accessToken: apiKey ?? "" },
27
+ { forceRefresh: true, signal: AbortSignal.timeout(8_000), ...(zedFetch ? { fetchFn: zedFetch } : {}) },
28
+ );
29
+ return jsonResponse({
30
+ ok: true,
31
+ latencyMs: Date.now() - started,
32
+ models: live.models.length,
33
+ message: `Connected. ${live.models.length} models.`,
34
+ });
35
+ } catch {
36
+ return jsonResponse({ ok: false, latencyMs: Date.now() - started, error: "zed model discovery failed" });
37
+ }
38
+ }