@bitkyc08/opencodex 2.65.0 → 2.66.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 +14 -0
- package/gui/dist/assets/App-DyHlFeNB.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
- package/gui/dist/assets/index-BiwnVAUm.js +86 -0
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn the account argument of a Codex pool command into a stored account id.
|
|
3
|
+
*
|
|
4
|
+
* `main` is the Codex App login. Anything else is tried as an id first and then as the
|
|
5
|
+
* display alias `ocx account alias` recorded, so an operator can type the name they gave an
|
|
6
|
+
* account instead of its generated id. An alias matches exactly, or case-insensitively when
|
|
7
|
+
* that still names one account; two accounts sharing a name is an error, not a guess.
|
|
8
|
+
*/
|
|
9
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
|
|
10
|
+
import { apiError, apiJson, fetchRows, type AccountDeps } from "./account-api";
|
|
11
|
+
|
|
12
|
+
/** `ocx account use <provider> auto`: clear the manual selection instead of making one. */
|
|
13
|
+
export const AUTO_ACCOUNT_ARGUMENT = "auto";
|
|
14
|
+
const MAIN_ALIAS = "main";
|
|
15
|
+
|
|
16
|
+
export type CodexAccountTarget =
|
|
17
|
+
| { id: string }
|
|
18
|
+
| { error: string; kind: "not_found" | "ambiguous" | "reserved" }
|
|
19
|
+
| { networkDown: true; transportError?: string };
|
|
20
|
+
|
|
21
|
+
/** Built-in selectors cannot be reused as Codex account aliases. */
|
|
22
|
+
export function isReservedCodexAccountWord(value: string): boolean {
|
|
23
|
+
const word = value.toLowerCase();
|
|
24
|
+
return word === AUTO_ACCOUNT_ARGUMENT || word === MAIN_ALIAS || word === MAIN_CODEX_ACCOUNT_ID;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Keep a local resolution miss consistent with the account API's not-found exit code. */
|
|
28
|
+
export function reportCodexAccountTargetError(target: Extract<CodexAccountTarget, { error: string }>): number {
|
|
29
|
+
return apiError({ error: target.error }, target.error, target.kind === "not_found" ? 404 : 400);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function resolveCodexAccountTarget(
|
|
33
|
+
deps: AccountDeps,
|
|
34
|
+
baseUrl: string,
|
|
35
|
+
requested: string,
|
|
36
|
+
): Promise<CodexAccountTarget> {
|
|
37
|
+
if (requested === MAIN_ALIAS || requested === MAIN_CODEX_ACCOUNT_ID) return { id: MAIN_CODEX_ACCOUNT_ID };
|
|
38
|
+
if (isReservedCodexAccountWord(requested)) {
|
|
39
|
+
return { error: `"${requested}" is reserved; it clears the selection with \`ocx account use\` and names no account`, kind: "reserved" };
|
|
40
|
+
}
|
|
41
|
+
const res = await apiJson(deps, baseUrl, "GET", "/api/codex-auth/accounts");
|
|
42
|
+
if (res.status === 0) return { networkDown: true, transportError: res.transportError };
|
|
43
|
+
// The list is only needed to turn an alias into an id. If the proxy cannot produce it, send
|
|
44
|
+
// the argument as the id it may already be and let the route answer, as the CLI always did.
|
|
45
|
+
if (res.status !== 200) return { id: requested };
|
|
46
|
+
const accounts = (Array.isArray(res.json.accounts) ? res.json.accounts : [])
|
|
47
|
+
.filter((entry): entry is { id: string; alias?: unknown } =>
|
|
48
|
+
typeof entry === "object" && entry !== null && typeof (entry as { id?: unknown }).id === "string");
|
|
49
|
+
if (accounts.some(account => account.id === requested)) return { id: requested };
|
|
50
|
+
const exact = accounts.filter(account => account.alias === requested);
|
|
51
|
+
const matches = exact.length > 0
|
|
52
|
+
? exact
|
|
53
|
+
: accounts.filter(account => typeof account.alias === "string" && account.alias.toLowerCase() === requested.toLowerCase());
|
|
54
|
+
if (matches.length === 1) return { id: matches[0].id };
|
|
55
|
+
if (matches.length > 1) {
|
|
56
|
+
return { error: `alias "${requested}" names ${matches.length} accounts; use the account id`, kind: "ambiguous" };
|
|
57
|
+
}
|
|
58
|
+
return { error: `Account not found: no Codex account has the id or alias "${requested}"`, kind: "not_found" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type CodexUseTarget =
|
|
62
|
+
| { accountId: string | null }
|
|
63
|
+
| Extract<CodexAccountTarget, { error: string }>
|
|
64
|
+
| { networkDown: true; transportError?: string };
|
|
65
|
+
|
|
66
|
+
/** The `use` argument: `auto` clears the selection, everything else resolves like any other verb. */
|
|
67
|
+
export async function resolveCodexUseTarget(
|
|
68
|
+
deps: AccountDeps,
|
|
69
|
+
baseUrl: string,
|
|
70
|
+
requested: string,
|
|
71
|
+
): Promise<CodexUseTarget> {
|
|
72
|
+
if (requested === AUTO_ACCOUNT_ARGUMENT) return { accountId: null };
|
|
73
|
+
const target = await resolveCodexAccountTarget(deps, baseUrl, requested);
|
|
74
|
+
if ("networkDown" in target) return target;
|
|
75
|
+
if ("error" in target) return target;
|
|
76
|
+
return { accountId: target.id };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function displayCodexId(id: string): string {
|
|
80
|
+
return id === MAIN_CODEX_ACCOUNT_ID ? MAIN_ALIAS : id;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What the switch means for running work, printed to stderr after the route accepted it. The
|
|
85
|
+
* route reports `pinDrainReason` only when routing would drop the pin it just recorded, so an
|
|
86
|
+
* absent field means the pin survives (#4521); `null` means the pin was cleared and the pool
|
|
87
|
+
* decides from here.
|
|
88
|
+
*/
|
|
89
|
+
export async function explainCodexUseOutcome(
|
|
90
|
+
deps: AccountDeps,
|
|
91
|
+
baseUrl: string,
|
|
92
|
+
name: string,
|
|
93
|
+
activeId: string | null,
|
|
94
|
+
pinDrainReason: string | undefined,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
if (activeId === null) {
|
|
97
|
+
console.error("Takes effect from the next unbound request; running threads keep their account until it cannot serve.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.error("Takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.");
|
|
101
|
+
const state = await fetchRows(deps, baseUrl, name, "codex");
|
|
102
|
+
const selected = state.rows.find(row => row.id === activeId);
|
|
103
|
+
const threshold = selected?.autoSwitchThresholdOverride ?? state.autoSwitchThreshold;
|
|
104
|
+
if (pinDrainReason !== undefined) {
|
|
105
|
+
// "may override" is the right caveat for a pin that is currently fine and could be
|
|
106
|
+
// overtaken later. It is the wrong sentence for one the next request will discard, and
|
|
107
|
+
// printing only that is what left the operator believing the account was pinned.
|
|
108
|
+
const because = pinDrainReason === "quota_threshold"
|
|
109
|
+
? `is at or above the auto-switch threshold${threshold !== undefined ? ` (${threshold}%)` : ""}`
|
|
110
|
+
: `cannot currently be selected (${pinDrainReason})`;
|
|
111
|
+
console.error(`Note: ${displayCodexId(activeId)} ${because}, so routing releases this pin on its next request.`);
|
|
112
|
+
} else if (state.status === 200 && typeof threshold === "number" && threshold > 0) {
|
|
113
|
+
console.error(`Note: auto-switch (threshold ${threshold}%) may override this pin.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/cli/account.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** `ocx account` — list and switch provider credentials (issue #180). */
|
|
2
2
|
import { loadConfig } from "../config";
|
|
3
|
+
import { explainCodexUseOutcome, reportCodexAccountTargetError, resolveCodexUseTarget } from "./account-target";
|
|
3
4
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
4
5
|
import type { OcxConfig } from "../types";
|
|
5
6
|
import {
|
|
@@ -43,18 +44,18 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
43
44
|
ocx account list [provider] [--json] [--all] [--quota [--refresh]]
|
|
44
45
|
ocx account history openai <pool-account-id> [--limit <1-200>] [--json]
|
|
45
46
|
ocx account current <provider> [--json]
|
|
46
|
-
ocx account use <provider> <account-or-key-id|main> [--json]
|
|
47
|
+
ocx account use <provider> <account-or-key-id|alias|main|auto> [--json]
|
|
47
48
|
ocx account refresh <provider> [--json]
|
|
48
49
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
49
|
-
ocx account alias <provider> <account-or-key-id> <display-name|-> [--json]
|
|
50
|
-
ocx account priority <provider> <account-id|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
|
|
51
|
-
ocx account pause <provider> <account-id|main> [--json]
|
|
52
|
-
ocx account resume <provider> <account-id|main> [--json]
|
|
50
|
+
ocx account alias <provider> <account-or-key-id|alias> <display-name|-> [--json]
|
|
51
|
+
ocx account priority <provider> <account-id|alias|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
|
|
52
|
+
ocx account pause <provider> <account-id|alias|main> [--json]
|
|
53
|
+
ocx account resume <provider> <account-id|alias|main> [--json]
|
|
53
54
|
ocx account pause-exhausted <provider> [--json]
|
|
54
55
|
ocx account strategy <provider> [<quota|round-robin|fill-first|reset-first>] [--json]
|
|
55
56
|
ocx account sticky <provider> [<1-100>] [--json]
|
|
56
|
-
ocx account remove <provider> <account-or-key-id|main> --yes [--json]
|
|
57
|
-
ocx account clear-cooldown <provider> <account-id|main> [--json]
|
|
57
|
+
ocx account remove <provider> <account-or-key-id|alias|main> --yes [--json]
|
|
58
|
+
ocx account clear-cooldown <provider> <account-id|alias|main> [--json]
|
|
58
59
|
ocx account add-key <provider> [--label <label>] [--json]
|
|
59
60
|
ocx account import <provider> --format <format> (--file <path>|--stdin) [--json]
|
|
60
61
|
ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]
|
|
@@ -66,7 +67,9 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
66
67
|
ocx account main <doctor|list|register|add|reauth|switch|recover> ...
|
|
67
68
|
|
|
68
69
|
List and switch provider accounts and API-key pools (masked output only).
|
|
69
|
-
'main' selects the Codex App login for the openai account pool
|
|
70
|
+
'main' selects the Codex App login for the openai account pool; 'auto' clears the
|
|
71
|
+
selection so the pool places work by its own strategy. A Codex account can be named
|
|
72
|
+
by the alias set with 'ocx account alias' wherever an id is accepted.`;
|
|
70
73
|
|
|
71
74
|
function consumeFlag(args: string[], flag: string): boolean {
|
|
72
75
|
const idx = args.indexOf(flag);
|
|
@@ -310,9 +313,12 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
310
313
|
if (!baseUrl) return proxyUnreachable();
|
|
311
314
|
|
|
312
315
|
let res: ApiResult;
|
|
313
|
-
let activeId: string;
|
|
316
|
+
let activeId: string | null;
|
|
314
317
|
if (c.type === "codex") {
|
|
315
|
-
|
|
318
|
+
const target = await resolveCodexUseTarget(deps, baseUrl, id);
|
|
319
|
+
if ("networkDown" in target) return proxyUnreachable(target.transportError);
|
|
320
|
+
if ("error" in target) return reportCodexAccountTargetError(target);
|
|
321
|
+
activeId = target.accountId;
|
|
316
322
|
res = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/active", { accountId: activeId });
|
|
317
323
|
} else if (c.type === "oauth") {
|
|
318
324
|
activeId = id;
|
|
@@ -336,25 +342,11 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
336
342
|
...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
|
|
337
343
|
}, null, 2));
|
|
338
344
|
} else {
|
|
339
|
-
console.log(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
console.error("Takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.");
|
|
343
|
-
const state = await fetchRows(deps, baseUrl, name, "codex");
|
|
344
|
-
const selected = state.rows.find(row => row.id === activeId);
|
|
345
|
-
const threshold = selected?.autoSwitchThresholdOverride ?? state.autoSwitchThreshold;
|
|
346
|
-
if (pinDrainReason !== undefined) {
|
|
347
|
-
// "may override" is the right caveat for a pin that is currently fine and could be
|
|
348
|
-
// overtaken later. It is the wrong sentence for one the next request will discard, and
|
|
349
|
-
// printing only that is what left the operator believing the account was pinned.
|
|
350
|
-
const because = pinDrainReason === "quota_threshold"
|
|
351
|
-
? `is at or above the auto-switch threshold${threshold !== undefined ? ` (${threshold}%)` : ""}`
|
|
352
|
-
: `cannot currently be selected (${pinDrainReason})`;
|
|
353
|
-
console.error(`Note: ${displayId(activeId)} ${because}, so routing releases this pin on its next request.`);
|
|
354
|
-
} else if (state.status === 200 && typeof threshold === "number" && threshold > 0) {
|
|
355
|
-
console.error(`Note: auto-switch (threshold ${threshold}%) may override this pin.`);
|
|
356
|
-
}
|
|
345
|
+
console.log(activeId === null
|
|
346
|
+
? `${name}: automatic account selection (pin cleared)`
|
|
347
|
+
: `${name}: active ${c.type === "api-key" ? "key" : "account"} is now ${displayId(activeId)}`);
|
|
357
348
|
}
|
|
349
|
+
if (c.type === "codex") await explainCodexUseOutcome(deps, baseUrl, name, activeId, pinDrainReason);
|
|
358
350
|
return 0;
|
|
359
351
|
}
|
|
360
352
|
|
package/src/cli/agent.ts
CHANGED
|
@@ -220,9 +220,18 @@ async function sidecar(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
220
220
|
const apply = (result as { codexWebSearch?: { applied?: boolean; reason?: string; detail?: string } } | null)?.codexWebSearch;
|
|
221
221
|
if (apply && apply.reason !== "not_requested") {
|
|
222
222
|
const detail = typeof apply.detail === "string" && apply.detail.length > 0 ? ` Details: ${apply.detail}` : "";
|
|
223
|
+
// `ocx sync` re-runs the same injection the external provider owns — the retry
|
|
224
|
+
// advice is meaningless on that outcome, same as the Desktop-switch report.
|
|
225
|
+
const retry = apply.reason === "external_provider"
|
|
226
|
+
? ""
|
|
227
|
+
: apply.reason === "ownership_undetermined"
|
|
228
|
+
? " Resolve the reported config.toml read error, then inspect 'ocx system settings --json'."
|
|
229
|
+
: apply.reason === "integration_disabled"
|
|
230
|
+
? " Enable Codex integration before applying the stored settings."
|
|
231
|
+
: " Run 'ocx sync' to apply the stored settings.";
|
|
223
232
|
lines.push(apply.applied === true
|
|
224
233
|
? "Codex config: ~/.codex/config.toml was rewritten."
|
|
225
|
-
: `Codex config: ~/.codex/config.toml was not rewritten because ${desktopSwitchApplyReason(apply.reason)}.${detail}
|
|
234
|
+
: `Codex config: ~/.codex/config.toml was not rewritten because ${desktopSwitchApplyReason(apply.reason)}.${detail}${retry}`);
|
|
226
235
|
}
|
|
227
236
|
printData(result, wantsJson, lines);
|
|
228
237
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ocx api protocols | explain | policy`: the CLI side of the protocol management routes
|
|
3
|
+
* (`src/server/management/protocol-routes.ts`).
|
|
4
|
+
*
|
|
5
|
+
* - `protocols` reads `GET /api/protocols` (optionally `?provider=<name>`).
|
|
6
|
+
* - `explain` posts `POST /api/protocols/plan`: a preview computed from config that sends
|
|
7
|
+
* nothing upstream.
|
|
8
|
+
* - `policy` with no setting flag reads the same GET; with one it sends
|
|
9
|
+
* `PATCH /api/protocols/settings`, which changes the operator's config. It is only ever
|
|
10
|
+
* invoked explicitly: nothing in the CLI calls it on the operator's behalf.
|
|
11
|
+
*
|
|
12
|
+
* Validation of values the server owns (rollout switch names, cross-field rules) stays on the
|
|
13
|
+
* server, so the two cannot drift; the CLI rejects only malformed argv.
|
|
14
|
+
*/
|
|
15
|
+
import { isProtocol } from "../protocols/contract";
|
|
16
|
+
import { isProtocolFeature } from "../protocols/features";
|
|
17
|
+
import {
|
|
18
|
+
CliUsageError,
|
|
19
|
+
csv,
|
|
20
|
+
printData,
|
|
21
|
+
rejectArgs,
|
|
22
|
+
runCliAction,
|
|
23
|
+
runtimeRequest,
|
|
24
|
+
takeFlag,
|
|
25
|
+
takeOption,
|
|
26
|
+
type RuntimeApiDeps,
|
|
27
|
+
} from "./runtime-api";
|
|
28
|
+
|
|
29
|
+
const USAGE = `Usage:
|
|
30
|
+
ocx api protocols [--provider <name>] [--json]
|
|
31
|
+
ocx api explain --model <id> --inbound <responses|chat|messages> [--feature <key>[,<key>]]... [--json]
|
|
32
|
+
ocx api policy [--messages <on|off>] [--unrepresentable <legacy|reject>]
|
|
33
|
+
[--rollout <switch>=<on|off>]... [--json]`;
|
|
34
|
+
|
|
35
|
+
type Rec = Record<string, unknown>;
|
|
36
|
+
function isRec(value: unknown): value is Rec {
|
|
37
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every value of a repeatable option, in argv order. */
|
|
41
|
+
function takeRepeated(args: string[], flag: string): string[] {
|
|
42
|
+
const values: string[] = [];
|
|
43
|
+
for (let value = takeOption(args, flag); value !== undefined; value = takeOption(args, flag)) values.push(value);
|
|
44
|
+
return values;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function onOff(flag: string, raw: string): boolean {
|
|
48
|
+
if (raw === "on") return true;
|
|
49
|
+
if (raw === "off") return false;
|
|
50
|
+
throw new CliUsageError(`${flag} must be on or off`, USAGE);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function text(value: unknown): string {
|
|
54
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "-";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function list(value: unknown): string {
|
|
58
|
+
return Array.isArray(value) && value.length > 0 ? value.map(text).join(", ") : "none";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Human view of the `GET /api/protocols` body (also the PATCH answer). */
|
|
62
|
+
export function protocolInfoLines(payload: unknown): string[] {
|
|
63
|
+
if (!isRec(payload)) return [text(payload)];
|
|
64
|
+
const lines = [
|
|
65
|
+
`Contract: ${text(payload.contractVersion)} policy revision: ${text(payload.policyRevision)}`,
|
|
66
|
+
];
|
|
67
|
+
if (isRec(payload.surfaces)) {
|
|
68
|
+
for (const [name, surface] of Object.entries(payload.surfaces)) {
|
|
69
|
+
if (!isRec(surface)) continue;
|
|
70
|
+
lines.push(`API ${name}: ${surface.enabled === true ? "open" : "closed"} (${text(surface.source)})`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (isRec(payload.settings)) {
|
|
74
|
+
lines.push(`Unrepresentable features: ${text(payload.settings.unrepresentable)}`);
|
|
75
|
+
if (isRec(payload.settings.rollout)) {
|
|
76
|
+
for (const [name, on] of Object.entries(payload.settings.rollout)) {
|
|
77
|
+
lines.push(`Rollout ${name}: ${on === true ? "on" : "off"}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(payload.features)) lines.push(`Features: ${payload.features.length} known (--json lists them)`);
|
|
82
|
+
if (isRec(payload.provider)) {
|
|
83
|
+
const provider = payload.provider;
|
|
84
|
+
lines.push(
|
|
85
|
+
`Provider ${text(provider.name)}: adapter ${text(provider.adapter)} (${text(provider.adapterSource)}), `
|
|
86
|
+
+ `upstream ${text(provider.upstream)}, auth ${text(provider.authMode)}`,
|
|
87
|
+
);
|
|
88
|
+
const overrides = Array.isArray(provider.modelOverrides) ? provider.modelOverrides : [];
|
|
89
|
+
for (const override of overrides) {
|
|
90
|
+
if (!isRec(override)) continue;
|
|
91
|
+
lines.push(` model ${text(override.model)}: adapter ${text(override.adapter)} (${text(override.source)})`);
|
|
92
|
+
}
|
|
93
|
+
if (provider.modelOverridesTruncated === true) lines.push(" more model overrides exist; --json has the same capped list");
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Human view of a `ProtocolPlanV1`. */
|
|
99
|
+
export function protocolPlanLines(plan: unknown): string[] {
|
|
100
|
+
if (!isRec(plan)) return [text(plan)];
|
|
101
|
+
const lines = [
|
|
102
|
+
`${text(plan.inbound)} ${text(plan.requestedModel)}: ${text(plan.mode)} (${text(plan.routeKind)} route, ${text(plan.basis)})`,
|
|
103
|
+
`Reasons: ${list(plan.reasonCodes)}`,
|
|
104
|
+
];
|
|
105
|
+
const candidates = Array.isArray(plan.candidates) ? plan.candidates : [];
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
if (!isRec(candidate)) continue;
|
|
108
|
+
const path = Array.isArray(candidate.requestPath) && candidate.requestPath.length > 0
|
|
109
|
+
? candidate.requestPath.map(text).join(" > ")
|
|
110
|
+
: "no path";
|
|
111
|
+
lines.push(
|
|
112
|
+
` ${text(candidate.provider)}/${text(candidate.model)}: ${text(candidate.mode)} ${path}`
|
|
113
|
+
+ `${candidate.eligible === false ? " (refused under reject)" : ""}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
lines.push(`Guaranteed features: ${list(plan.guaranteedFeatures)}`);
|
|
117
|
+
lines.push(`Partial features: ${list(plan.partialFeatures)}`);
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function protocols(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
122
|
+
const args = [...argv];
|
|
123
|
+
const wantsJson = takeFlag(args, "--json");
|
|
124
|
+
const provider = takeOption(args, "--provider");
|
|
125
|
+
rejectArgs(args, USAGE);
|
|
126
|
+
const path = provider === undefined ? "/api/protocols" : `/api/protocols?provider=${encodeURIComponent(provider)}`;
|
|
127
|
+
const result = await runtimeRequest(path, {}, deps);
|
|
128
|
+
printData(result, wantsJson, protocolInfoLines(result));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function explain(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
132
|
+
const args = [...argv];
|
|
133
|
+
const wantsJson = takeFlag(args, "--json");
|
|
134
|
+
const model = takeOption(args, "--model");
|
|
135
|
+
const inbound = takeOption(args, "--inbound");
|
|
136
|
+
const features = takeRepeated(args, "--feature").flatMap(value => csv(value) ?? []);
|
|
137
|
+
rejectArgs(args, USAGE);
|
|
138
|
+
if (!model) throw new CliUsageError("--model is required", USAGE);
|
|
139
|
+
if (!isProtocol(inbound)) throw new CliUsageError("--inbound must be responses, chat or messages", USAGE);
|
|
140
|
+
const unknown = features.filter(feature => !isProtocolFeature(feature));
|
|
141
|
+
if (unknown.length > 0) throw new CliUsageError("--feature names a feature this CLI does not know; `ocx api protocols --json` lists them", USAGE);
|
|
142
|
+
const body = { model, inbound, ...(features.length > 0 ? { features: [...new Set(features)] } : {}) };
|
|
143
|
+
const result = await runtimeRequest("/api/protocols/plan", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "content-type": "application/json" },
|
|
146
|
+
body: JSON.stringify(body),
|
|
147
|
+
}, deps);
|
|
148
|
+
printData(result, wantsJson, protocolPlanLines(result));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The `PATCH /api/protocols/settings` body the flags describe, or `undefined` for a read. */
|
|
152
|
+
export function protocolPolicyPatch(args: string[]): Rec | undefined {
|
|
153
|
+
const messages = takeOption(args, "--messages");
|
|
154
|
+
const unrepresentable = takeOption(args, "--unrepresentable");
|
|
155
|
+
const rolloutArgs = takeRepeated(args, "--rollout");
|
|
156
|
+
const body: Rec = {};
|
|
157
|
+
if (messages !== undefined) body.messagesEnabled = onOff("--messages", messages);
|
|
158
|
+
if (unrepresentable !== undefined) {
|
|
159
|
+
if (unrepresentable !== "legacy" && unrepresentable !== "reject") {
|
|
160
|
+
throw new CliUsageError("--unrepresentable must be legacy or reject", USAGE);
|
|
161
|
+
}
|
|
162
|
+
body.unrepresentable = unrepresentable;
|
|
163
|
+
}
|
|
164
|
+
if (rolloutArgs.length > 0) {
|
|
165
|
+
const rollout: Rec = {};
|
|
166
|
+
for (const entry of rolloutArgs) {
|
|
167
|
+
const match = /^([A-Za-z]+)=(on|off)$/.exec(entry);
|
|
168
|
+
if (!match) throw new CliUsageError("--rollout takes <switch>=<on|off>", USAGE);
|
|
169
|
+
if (Object.hasOwn(rollout, match[1]!)) throw new CliUsageError("--rollout names the same switch twice", USAGE);
|
|
170
|
+
rollout[match[1]!] = match[2] === "on";
|
|
171
|
+
}
|
|
172
|
+
body.rollout = rollout;
|
|
173
|
+
}
|
|
174
|
+
return Object.keys(body).length > 0 ? body : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
178
|
+
const args = [...argv];
|
|
179
|
+
const wantsJson = takeFlag(args, "--json");
|
|
180
|
+
const patch = protocolPolicyPatch(args);
|
|
181
|
+
rejectArgs(args, USAGE);
|
|
182
|
+
// No setting flag means show. A read must not write the value it is reporting.
|
|
183
|
+
if (!patch) {
|
|
184
|
+
const result = await runtimeRequest("/api/protocols", {}, deps);
|
|
185
|
+
printData(result, wantsJson, protocolInfoLines(result));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const result = await runtimeRequest("/api/protocols/settings", {
|
|
189
|
+
method: "PATCH",
|
|
190
|
+
headers: { "content-type": "application/json" },
|
|
191
|
+
body: JSON.stringify(patch),
|
|
192
|
+
}, deps);
|
|
193
|
+
printData(result, wantsJson, ["Protocol settings updated.", ...protocolInfoLines(result)]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function handleApiCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
197
|
+
return runCliAction(async () => {
|
|
198
|
+
const [sub, ...rest] = argv;
|
|
199
|
+
if (sub === "protocols") await protocols(rest, deps);
|
|
200
|
+
else if (sub === "explain") await explain(rest, deps);
|
|
201
|
+
else if (sub === "policy") await policy(rest, deps);
|
|
202
|
+
else throw new CliUsageError(sub ? `unknown api command ${sub}` : "an api command is required", USAGE);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const API_USAGE = USAGE;
|
package/src/cli/capabilities.ts
CHANGED
|
@@ -95,6 +95,47 @@ export const HEAD_CAPABILITIES: readonly HeadCapability[] = [
|
|
|
95
95
|
* A capability must not name a route the command does not actually fetch.
|
|
96
96
|
*/
|
|
97
97
|
export const CAPABILITIES: readonly Capability[] = [
|
|
98
|
+
{
|
|
99
|
+
command: ["link", "port"],
|
|
100
|
+
summary: "Allocate a free loopback port for a remote home link.",
|
|
101
|
+
routes: [],
|
|
102
|
+
flags: [{ name: "--json", value: "boolean", summary: "Emit the selected port as JSON." }],
|
|
103
|
+
mutates: false,
|
|
104
|
+
json: "payload",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
command: ["link", "issue"],
|
|
108
|
+
summary: "Issue one link credential and record its tunnel metadata.",
|
|
109
|
+
routes: [{ method: "POST", path: "/api/link/issue" }],
|
|
110
|
+
flags: [
|
|
111
|
+
{ name: "--alias", value: "string", required: true, summary: "SSH host alias for the linked machine." },
|
|
112
|
+
{ name: "--tunnel-port", value: "number", required: true, summary: "Remote loopback port for the reverse tunnel." },
|
|
113
|
+
{ name: "--json", value: "boolean", summary: "Emit the issue result as JSON." },
|
|
114
|
+
],
|
|
115
|
+
mutates: true,
|
|
116
|
+
json: "payload",
|
|
117
|
+
details: ["Requires the running proxy's admin token on loopback; the one-time data key is printed only on stdout."],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
command: ["link", "status"],
|
|
121
|
+
summary: "Read link listener and tunnel status.",
|
|
122
|
+
routes: [{ method: "GET", path: "/api/link/status" }],
|
|
123
|
+
flags: [{ name: "--json", value: "boolean", summary: "Emit the K16 status payload as JSON." }],
|
|
124
|
+
mutates: false,
|
|
125
|
+
json: "payload",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
command: ["link", "revoke"],
|
|
129
|
+
summary: "Revoke a link credential and remove its link record.",
|
|
130
|
+
routes: [{ method: "DELETE", path: "/api/link/{id}" }],
|
|
131
|
+
flags: [
|
|
132
|
+
{ name: "--link-id", value: "string", required: true, summary: "Link id to revoke." },
|
|
133
|
+
{ name: "--json", value: "boolean", summary: "Emit the revoked link id as JSON." },
|
|
134
|
+
],
|
|
135
|
+
mutates: true,
|
|
136
|
+
json: "payload",
|
|
137
|
+
details: ["Requires the running proxy's admin token on loopback."],
|
|
138
|
+
},
|
|
98
139
|
{
|
|
99
140
|
"command": [
|
|
100
141
|
"remote-workspace",
|
|
@@ -946,6 +987,51 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
946
987
|
json: "payload",
|
|
947
988
|
details: ["A bare invocation reads and never writes."],
|
|
948
989
|
},
|
|
990
|
+
{
|
|
991
|
+
command: ["api", "protocols"],
|
|
992
|
+
summary: "Read the protocol contract version, API surfaces, protocol settings and feature vocabulary.",
|
|
993
|
+
routes: [{ method: "GET", path: "/api/protocols" }],
|
|
994
|
+
flags: [
|
|
995
|
+
{ name: "--provider", value: "string", summary: "Add one configured provider's upstream wire and who decided it." },
|
|
996
|
+
{ name: "--json", value: "boolean", summary: "Emit the GET /api/protocols body." },
|
|
997
|
+
],
|
|
998
|
+
mutates: false,
|
|
999
|
+
json: "payload",
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
command: ["api", "explain"],
|
|
1003
|
+
summary: "Preview the request path a model would take from one inbound API, computed from config.",
|
|
1004
|
+
routes: [{ method: "POST", path: "/api/protocols/plan" }],
|
|
1005
|
+
flags: [
|
|
1006
|
+
{ name: "--model", value: "string", required: true, summary: "Model selector as a client would send it." },
|
|
1007
|
+
{ name: "--inbound", value: "string", required: true, summary: "Inbound API: responses, chat or messages." },
|
|
1008
|
+
{ name: "--feature", value: "string", summary: "Request feature key to judge; repeatable or comma-separated." },
|
|
1009
|
+
{ name: "--json", value: "boolean", summary: "Emit the ProtocolPlanV1 preview." },
|
|
1010
|
+
],
|
|
1011
|
+
mutates: false,
|
|
1012
|
+
json: "payload",
|
|
1013
|
+
details: ["A read-only POST: nothing is sent upstream, no combo state advances and the input is not logged."],
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
command: ["api", "policy"],
|
|
1017
|
+
summary: "Read the protocol policy, or change the Messages surface, unrepresentable policy and rollout switches.",
|
|
1018
|
+
routes: [
|
|
1019
|
+
{ method: "GET", path: "/api/protocols" },
|
|
1020
|
+
{ method: "PATCH", path: "/api/protocols/settings" },
|
|
1021
|
+
],
|
|
1022
|
+
flags: [
|
|
1023
|
+
{ name: "--messages", value: "string", summary: "Open or close the Messages API: on or off. Off also turns the Claude integration off." },
|
|
1024
|
+
{ name: "--unrepresentable", value: "string", summary: "legacy keeps today's behavior; reject refuses a request its path cannot carry." },
|
|
1025
|
+
{ name: "--rollout", value: "string", summary: "One switch as name=on or name=off; repeatable. Every switch defaults off." },
|
|
1026
|
+
{ name: "--json", value: "boolean", summary: "Emit the resulting GET /api/protocols body." },
|
|
1027
|
+
],
|
|
1028
|
+
mutates: true,
|
|
1029
|
+
json: "payload",
|
|
1030
|
+
details: [
|
|
1031
|
+
"A bare invocation reads and never writes.",
|
|
1032
|
+
"A setting flag changes the operator's config; run it only when the operator asks for that change.",
|
|
1033
|
+
],
|
|
1034
|
+
},
|
|
949
1035
|
];
|
|
950
1036
|
|
|
951
1037
|
/** Capabilities that drive `route`, for `ocx capabilities --route`. */
|
package/src/cli/combo.ts
CHANGED
|
@@ -14,7 +14,7 @@ const USAGE = `Usage:
|
|
|
14
14
|
ocx combo [list] [--json]
|
|
15
15
|
ocx combo show <id> [--json]
|
|
16
16
|
ocx combo set <id> --targets <provider/model[:weight],...>
|
|
17
|
-
[--strategy <failover|round-robin|random|least-used|reset-window>] [--sticky <1-100>]
|
|
17
|
+
[--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100>]
|
|
18
18
|
[--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force>]
|
|
19
19
|
(force overrides valid client effort and can increase cost/latency) [--alias <name|->]
|
|
20
20
|
[--native-alias] [--display-name <label|->]
|
|
@@ -74,7 +74,7 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
74
74
|
const targetsRaw = takeOption(args, "--targets");
|
|
75
75
|
if (!targetsRaw) throw new CliUsageError("--targets is required", USAGE);
|
|
76
76
|
const strategy = takeOption(args, "--strategy") ?? "failover";
|
|
77
|
-
if (strategy !== "failover" && strategy !== "round-robin" && strategy !== "random" && strategy !== "least-used" && strategy !== "reset-window") throw new CliUsageError("--strategy must be failover, round-robin, random, least-used,
|
|
77
|
+
if (strategy !== "failover" && strategy !== "round-robin" && strategy !== "random" && strategy !== "least-used" && strategy !== "reset-window" && strategy !== "jev") throw new CliUsageError("--strategy must be failover, round-robin, random, least-used, reset-window, or jev", USAGE);
|
|
78
78
|
const stickyLimit = takeIntegerOption(args, "--sticky", { min: 1 });
|
|
79
79
|
if (stickyLimit !== undefined) {
|
|
80
80
|
if (stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE);
|