@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
|
@@ -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 {
|
|
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]) =>
|
|
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
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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]) =>
|
|
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
|
+
}
|