@bitkyc08/opencodex 2.77.0 → 2.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { isValidProviderName } from "../config/provider-name";
|
|
2
|
+
import { isDeclaredReasoningEffort } from "../reasoning-effort";
|
|
3
|
+
import { resolveSlugSelection } from "../providers/slug-codec";
|
|
4
|
+
import { printCatalogResult, runCatalogAction } from "./catalog-command-result";
|
|
5
|
+
import { serializeManagementJson } from "./json-input";
|
|
6
|
+
import { parseCustomModelAddInput } from "./models-custom-input";
|
|
7
|
+
import {
|
|
8
|
+
CliUsageError, RuntimeApiError, runtimeBaseUrl, runtimeRequest, takeFlag,
|
|
9
|
+
type RuntimeApiDeps,
|
|
10
|
+
} from "./runtime-api";
|
|
11
|
+
|
|
12
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function identity(value: unknown): value is string {
|
|
17
|
+
return typeof value === "string" && value.trim().length > 0 && value.trim() === value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function modelIdentity(value: unknown): { id: string; provider: string; modelId: string } {
|
|
21
|
+
if (!record(value) || !identity(value.id) || !identity(value.provider)
|
|
22
|
+
|| !isValidProviderName(value.provider) || !identity(value.modelId)) {
|
|
23
|
+
throw new Error("Invalid custom model identity");
|
|
24
|
+
}
|
|
25
|
+
return { id: value.id, provider: value.provider, modelId: value.modelId };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Rebuild the public stored-entry DTO; never copy arbitrary response properties. */
|
|
29
|
+
function addedModel(value: unknown, provider: string, modelId: string): Record<string, unknown> {
|
|
30
|
+
const result: Record<string, unknown> = modelIdentity(value);
|
|
31
|
+
if (!record(value) || result.provider !== provider || result.modelId !== modelId
|
|
32
|
+
|| !identity(value.addedAt) || !Number.isFinite(Date.parse(value.addedAt))) {
|
|
33
|
+
throw new Error("Invalid custom model response");
|
|
34
|
+
}
|
|
35
|
+
result.addedAt = value.addedAt;
|
|
36
|
+
if (value.displayName !== undefined) {
|
|
37
|
+
if (!identity(value.displayName) || value.displayName.includes("/")) throw new Error("Invalid display name response");
|
|
38
|
+
result.displayName = value.displayName;
|
|
39
|
+
}
|
|
40
|
+
if (value.contextWindow !== undefined) {
|
|
41
|
+
if (typeof value.contextWindow !== "number" || !Number.isSafeInteger(value.contextWindow) || value.contextWindow <= 0) {
|
|
42
|
+
throw new Error("Invalid context response");
|
|
43
|
+
}
|
|
44
|
+
result.contextWindow = value.contextWindow;
|
|
45
|
+
}
|
|
46
|
+
for (const key of ["inputModalities", "reasoningEfforts"] as const) {
|
|
47
|
+
const values = value[key];
|
|
48
|
+
if (values === undefined) continue;
|
|
49
|
+
if (!Array.isArray(values) || !values.every(item => typeof item === "string"
|
|
50
|
+
&& (key === "inputModalities" ? ["text", "image", "audio"].includes(item) : isDeclaredReasoningEffort(item)))
|
|
51
|
+
|| new Set(values).size !== values.length || (key === "inputModalities" && values.length === 0)) {
|
|
52
|
+
throw new Error("Invalid model metadata response");
|
|
53
|
+
}
|
|
54
|
+
result[key] = [...values];
|
|
55
|
+
}
|
|
56
|
+
if (value.defaultReasoningEffort !== undefined) {
|
|
57
|
+
if (typeof value.defaultReasoningEffort !== "string" || !Array.isArray(result.reasoningEfforts)
|
|
58
|
+
|| !result.reasoningEfforts.includes(value.defaultReasoningEffort)) throw new Error("Invalid default effort response");
|
|
59
|
+
result.defaultReasoningEffort = value.defaultReasoningEffort;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveRemoval(value: unknown, target: string): { id: string; provider: string; modelId: string } {
|
|
65
|
+
if (!Array.isArray(value)) throw new Error("Invalid custom model roster");
|
|
66
|
+
const rows = value.map(modelIdentity);
|
|
67
|
+
if (new Set(rows.map(row => row.id)).size !== rows.length) throw new Error("Duplicate stored model IDs");
|
|
68
|
+
const byId = rows.find(row => row.id === target);
|
|
69
|
+
if (byId) return byId;
|
|
70
|
+
const separator = target.indexOf("/");
|
|
71
|
+
const provider = separator >= 0 ? target.slice(0, separator) : undefined;
|
|
72
|
+
const roster = provider === undefined ? [] : rows.filter(row => row.provider === provider);
|
|
73
|
+
const admitted = new Set(provider === undefined ? []
|
|
74
|
+
: resolveSlugSelection(provider, target, roster.map(row => row.modelId)).matched);
|
|
75
|
+
const matches = roster.filter(row => admitted.has(row.modelId));
|
|
76
|
+
if (matches.length === 0) throw new RuntimeApiError("Custom model not found", 404, null);
|
|
77
|
+
if (matches.length !== 1) throw new CliUsageError("Ambiguous custom model selector; use the complete stored ID");
|
|
78
|
+
return matches[0]!;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function handleModelsCustomRuntimeCommand(
|
|
82
|
+
sub: "add" | "remove", argv: string[], deps: RuntimeApiDeps = {},
|
|
83
|
+
): Promise<number> {
|
|
84
|
+
return runCatalogAction(async () => {
|
|
85
|
+
const args = [...argv];
|
|
86
|
+
const wantsJson = takeFlag(args, "--json");
|
|
87
|
+
if (sub === "add") {
|
|
88
|
+
const input = parseCustomModelAddInput(args);
|
|
89
|
+
const body = serializeManagementJson(input);
|
|
90
|
+
const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
91
|
+
const response = await runtimeRequest("/api/custom-models", { method: "POST", redirect: "error", body }, pinned);
|
|
92
|
+
const result = addedModel(response, input.provider, input.modelId);
|
|
93
|
+
return printCatalogResult(result, record(response) ? response.catalogRefresh : undefined, wantsJson,
|
|
94
|
+
[`Saved custom model ${input.provider}/${input.modelId} (${result.id}).`]);
|
|
95
|
+
}
|
|
96
|
+
const confirmed = takeFlag(args, "--yes");
|
|
97
|
+
if (!confirmed) throw new CliUsageError("Live custom model removal requires --yes");
|
|
98
|
+
if (args.length !== 1 || !args[0]?.trim() || args[0].startsWith("-")) {
|
|
99
|
+
throw new CliUsageError("A complete stored ID or provider/model selector is required");
|
|
100
|
+
}
|
|
101
|
+
const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
102
|
+
const roster = await runtimeRequest("/api/custom-models", { redirect: "error" }, pinned);
|
|
103
|
+
const selected = resolveRemoval(roster, args[0].trim());
|
|
104
|
+
if (selected.id === "." || selected.id === "..") throw new Error("Stored ID is not addressable as a route segment");
|
|
105
|
+
const response = await runtimeRequest(`/api/custom-models/${encodeURIComponent(selected.id)}`, {
|
|
106
|
+
method: "DELETE", redirect: "error",
|
|
107
|
+
}, pinned);
|
|
108
|
+
if (!record(response) || response.ok !== true) throw new Error("Invalid custom model removal response");
|
|
109
|
+
return printCatalogResult({ ok: true, ...selected }, response.catalogRefresh, wantsJson,
|
|
110
|
+
[`Removed custom model ${selected.provider}/${selected.modelId} (${selected.id}).`]);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { isValidProviderName } from "../config/provider-name";
|
|
2
|
+
import { modelDisplayNamesConfigError } from "../config/provider-validation";
|
|
3
|
+
import { isValidModelDiscoveryModelId } from "../providers/model-discovery-limits";
|
|
4
|
+
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
|
|
5
|
+
import { printCatalogResult, runCatalogAction } from "./catalog-command-result";
|
|
6
|
+
import { serializeManagementJson } from "./json-input";
|
|
7
|
+
import {
|
|
8
|
+
customPickerRows, isModelPickerUsage, isPickerOrderSaved, isPickerOrderSettings,
|
|
9
|
+
modelPickerOrder, normalizePickerIds, pickerIdentityCoverage,
|
|
10
|
+
type PickerModelIdentity, type PickerOrderSettings, type ModelPickerUsage,
|
|
11
|
+
} from "./model-picker-ordering";
|
|
12
|
+
import {
|
|
13
|
+
CliUsageError, RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest,
|
|
14
|
+
takeFlag, takeOptionWithSyntax, type RuntimeApiDeps,
|
|
15
|
+
} from "./runtime-api";
|
|
16
|
+
|
|
17
|
+
const ORDER_USAGE = "Usage: ocx models order status|reset|set (--models CSV | --mode default|alphabetical|provider|most-used) [--json]";
|
|
18
|
+
const DISPLAY_USAGE = "Usage: ocx models display-name PROVIDER/MODEL (--set TEXT | --clear) [--json]";
|
|
19
|
+
const SETTINGS = "/api/subagent-models";
|
|
20
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function stringList(value: unknown): value is string[] {
|
|
24
|
+
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
25
|
+
}
|
|
26
|
+
function unusable(): never { throw new Error("Invalid model management response"); }
|
|
27
|
+
|
|
28
|
+
async function readSettings(deps: RuntimeApiDeps): Promise<PickerOrderSettings> {
|
|
29
|
+
const value = await runtimeRequest(SETTINGS, { redirect: "error" }, deps);
|
|
30
|
+
if (!isPickerOrderSettings(value) || value.pickerAvailable.some(id => !id.includes("/"))
|
|
31
|
+
|| new Set(value.pickerAvailable).size !== value.pickerAvailable.length) unusable();
|
|
32
|
+
return { pickerAvailable: value.pickerAvailable, pickerOrder: value.pickerOrder,
|
|
33
|
+
pickerOrderMode: value.pickerOrderMode, ...(value.chosen === undefined ? {} : { chosen: value.chosen }) };
|
|
34
|
+
}
|
|
35
|
+
async function readIdentities(deps: RuntimeApiDeps): Promise<PickerModelIdentity[]> {
|
|
36
|
+
const rows = await runtimeRequest("/api/models", { redirect: "error" }, deps);
|
|
37
|
+
if (!Array.isArray(rows)) unusable();
|
|
38
|
+
return rows.map(row => {
|
|
39
|
+
if (!record(row) || typeof row.provider !== "string" || !row.provider.trim()
|
|
40
|
+
|| !isValidModelDiscoveryModelId(row.id) || !isValidModelDiscoveryModelId(row.namespaced)) unusable();
|
|
41
|
+
return { provider: row.provider, id: row.id, namespaced: row.namespaced };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function snapshot(settings: PickerOrderSettings, identities: PickerModelIdentity[]): string {
|
|
45
|
+
return JSON.stringify([settings, identities]);
|
|
46
|
+
}
|
|
47
|
+
function assertRouted(settings: PickerOrderSettings): void {
|
|
48
|
+
if (settings.pickerOrder.some(id => !id.includes("/"))) {
|
|
49
|
+
throw new CliUsageError("Saved order includes native models. Run ocx models order reset before replacing it.");
|
|
50
|
+
}
|
|
51
|
+
if (settings.pickerAvailable.length === 0) throw new CliUsageError("No routed picker candidates are available.");
|
|
52
|
+
}
|
|
53
|
+
function manualOrder(csv: string, settings: PickerOrderSettings, identities: PickerModelIdentity[]): string[] {
|
|
54
|
+
const tokens = csv.split(",").map(id => id.trim());
|
|
55
|
+
const order = tokens.map(token => {
|
|
56
|
+
if (!token) throw new CliUsageError("--models cannot contain blank entries", ORDER_USAGE);
|
|
57
|
+
const result = normalizePickerIds([token], settings.pickerAvailable, identities);
|
|
58
|
+
if (result.length !== 1) throw new CliUsageError("--models contains an unknown or ambiguous model", ORDER_USAGE);
|
|
59
|
+
return result[0]!;
|
|
60
|
+
});
|
|
61
|
+
if (new Set(order).size !== order.length || order.length !== settings.pickerAvailable.length) {
|
|
62
|
+
throw new CliUsageError("--models must contain every routed candidate exactly once", ORDER_USAGE);
|
|
63
|
+
}
|
|
64
|
+
const draft = customPickerRows(settings, identities);
|
|
65
|
+
if (!draft) throw new CliUsageError("Featured state or model identities are incomplete. Read status before trying again.");
|
|
66
|
+
if (!draft.fixed.every((id, index) => order[index] === id)) {
|
|
67
|
+
throw new CliUsageError("--models must keep the current featured models as its exact leading prefix.");
|
|
68
|
+
}
|
|
69
|
+
return order;
|
|
70
|
+
}
|
|
71
|
+
async function readUsage(deps: RuntimeApiDeps): Promise<ModelPickerUsage[]> {
|
|
72
|
+
const value = await runtimeRequest("/api/usage?range=all&surface=all", { redirect: "error" }, deps);
|
|
73
|
+
if (!record(value) || (value.usageIncomplete !== undefined && value.usageIncomplete !== false)
|
|
74
|
+
|| !isModelPickerUsage(value.models)) unusable();
|
|
75
|
+
return value.models;
|
|
76
|
+
}
|
|
77
|
+
function printOrderSave(value: unknown, expected: string[] | null, mode: string | null, wantsJson: boolean): number {
|
|
78
|
+
if (!record(value) || value.ok !== true || !isPickerOrderSaved(value)
|
|
79
|
+
|| !stringList(value.applied) || (value.force !== null && typeof value.force !== "string")
|
|
80
|
+
|| JSON.stringify(value.pickerOrder) !== JSON.stringify(expected ?? []) || value.pickerOrderMode !== mode) unusable();
|
|
81
|
+
return printCatalogResult({ ok: true, pickerOrder: value.pickerOrder, pickerOrderMode: value.pickerOrderMode,
|
|
82
|
+
applied: value.applied, force: value.force }, value.catalogRefresh, wantsJson,
|
|
83
|
+
["Picker order saved.", `Order: ${value.pickerOrder.join(", ") || "default"}`]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function handleModelsOrderCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
87
|
+
return runCatalogAction(async () => {
|
|
88
|
+
const args = [...argv];
|
|
89
|
+
const sub = args.shift();
|
|
90
|
+
const wantsJson = takeFlag(args, "--json");
|
|
91
|
+
const csv = takeOptionWithSyntax(args, "--models")?.value;
|
|
92
|
+
const mode = takeOptionWithSyntax(args, "--mode")?.value;
|
|
93
|
+
if (args.length || !["status", "reset", "set"].includes(sub ?? "")
|
|
94
|
+
|| (sub !== "set" && (csv !== undefined || mode !== undefined))
|
|
95
|
+
|| (sub === "set" && ((csv === undefined) === (mode === undefined)))
|
|
96
|
+
|| (mode !== undefined && !["default", "alphabetical", "provider", "most-used"].includes(mode))) {
|
|
97
|
+
throw new CliUsageError("Invalid picker order arguments", ORDER_USAGE);
|
|
98
|
+
}
|
|
99
|
+
// Refuse lexical manual-input failures before discovering a target.
|
|
100
|
+
if (csv !== undefined) {
|
|
101
|
+
const tokens = csv.split(",").map(id => id.trim());
|
|
102
|
+
if (tokens.some(id => !id) || new Set(tokens).size !== tokens.length) {
|
|
103
|
+
throw new CliUsageError("--models requires unique nonblank entries", ORDER_USAGE);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
107
|
+
if (sub === "status") {
|
|
108
|
+
const settings = await readSettings(pinned);
|
|
109
|
+
printData(settings, wantsJson, [
|
|
110
|
+
`Saved mode: ${settings.pickerOrderMode ?? (settings.pickerOrder.length ? "custom" : "default")}`,
|
|
111
|
+
`Saved order: ${settings.pickerOrder.join(", ") || "default"}`,
|
|
112
|
+
`Routed candidates: ${settings.pickerAvailable.join(", ") || "none"}`,
|
|
113
|
+
`Featured: ${settings.chosen === undefined ? "unknown" : settings.chosen.join(", ") || "none"}`,
|
|
114
|
+
]);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
let order: string[] | null = null;
|
|
118
|
+
let savedMode: string | null = null;
|
|
119
|
+
if (sub !== "reset" && mode !== "default") {
|
|
120
|
+
const settings = await readSettings(pinned);
|
|
121
|
+
assertRouted(settings);
|
|
122
|
+
const identities = await readIdentities(pinned);
|
|
123
|
+
if (!pickerIdentityCoverage(settings.pickerAvailable, identities)) {
|
|
124
|
+
throw new CliUsageError("Model identities are incomplete or ambiguous; picker order was not changed.");
|
|
125
|
+
}
|
|
126
|
+
if (csv !== undefined) {
|
|
127
|
+
order = manualOrder(csv, settings, identities);
|
|
128
|
+
const freshSettings = await readSettings(pinned);
|
|
129
|
+
const freshIdentities = await readIdentities(pinned);
|
|
130
|
+
if (snapshot(settings, identities) !== snapshot(freshSettings, freshIdentities)) {
|
|
131
|
+
throw new RuntimeApiError("Picker state changed", 409, null);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
const preset = mode as "alphabetical" | "provider" | "most-used";
|
|
135
|
+
order = modelPickerOrder(preset, settings.pickerAvailable, preset === "most-used" ? await readUsage(pinned) : [], identities);
|
|
136
|
+
savedMode = preset;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const value = await runtimeRequest(SETTINGS, { method: "PUT", redirect: "error",
|
|
140
|
+
body: serializeManagementJson({ pickerOrder: order, pickerOrderMode: savedMode }) }, pinned);
|
|
141
|
+
return printOrderSave(value, order, savedMode, wantsJson);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function displayReceipt(value: unknown, provider: string, modelId: string, label: string | null, partial: boolean): Record<string, unknown> {
|
|
146
|
+
if (!record(value) || value.provider !== provider || value.modelId !== modelId
|
|
147
|
+
|| value.displayNameOverride !== label || (partial ? value.saved !== true : value.ok !== true)) unusable();
|
|
148
|
+
const projected: Record<string, unknown> = { ...(partial ? { saved: true } : { ok: true }), provider, modelId,
|
|
149
|
+
displayNameOverride: value.displayNameOverride };
|
|
150
|
+
if (partial) {
|
|
151
|
+
if (normalizeCatalogDisposition(value.catalogRefresh)?.status !== "failed") unusable();
|
|
152
|
+
} else {
|
|
153
|
+
if (typeof value.displayName !== "string" || !value.displayName.trim()
|
|
154
|
+
|| typeof value.displayNameSource !== "string"
|
|
155
|
+
|| !["operator", "provider", "fallback"].includes(value.displayNameSource)) unusable();
|
|
156
|
+
projected.displayName = value.displayName;
|
|
157
|
+
projected.displayNameSource = value.displayNameSource;
|
|
158
|
+
}
|
|
159
|
+
return projected;
|
|
160
|
+
}
|
|
161
|
+
export async function handleModelsDisplayNameCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
162
|
+
return runCatalogAction(async () => {
|
|
163
|
+
const args = [...argv];
|
|
164
|
+
const selector = args.shift() ?? "";
|
|
165
|
+
const wantsJson = takeFlag(args, "--json");
|
|
166
|
+
const set = takeOptionWithSyntax(args, "--set")?.value;
|
|
167
|
+
const clear = takeFlag(args, "--clear");
|
|
168
|
+
const slash = selector.indexOf("/");
|
|
169
|
+
const provider = selector.slice(0, slash), modelId = selector.slice(slash + 1);
|
|
170
|
+
if (args.length || slash < 1 || !isValidProviderName(provider) || !isValidModelDiscoveryModelId(modelId)
|
|
171
|
+
|| (set !== undefined) === clear) throw new CliUsageError("Invalid display-name arguments", DISPLAY_USAGE);
|
|
172
|
+
const label = set === undefined ? null : set.trim();
|
|
173
|
+
if (label !== null && modelDisplayNamesConfigError({ [modelId]: label }) !== null) {
|
|
174
|
+
throw new CliUsageError("Display name must be a valid nonblank label without slashes or control characters", DISPLAY_USAGE);
|
|
175
|
+
}
|
|
176
|
+
const body = serializeManagementJson({ modelId, displayName: label });
|
|
177
|
+
const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
178
|
+
let value: unknown;
|
|
179
|
+
let partial = false;
|
|
180
|
+
try {
|
|
181
|
+
value = await runtimeRequest(`/api/providers/${encodeURIComponent(provider)}/model-display-names`, {
|
|
182
|
+
method: "PUT", redirect: "error", body,
|
|
183
|
+
}, pinned);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (!(error instanceof RuntimeApiError) || error.status !== 503) throw error;
|
|
186
|
+
value = error.body;
|
|
187
|
+
partial = true;
|
|
188
|
+
}
|
|
189
|
+
const result = displayReceipt(value, provider, modelId, label, partial);
|
|
190
|
+
const refresh = (value as Record<string, unknown>).catalogRefresh;
|
|
191
|
+
return printCatalogResult(result, refresh, wantsJson,
|
|
192
|
+
[`Display name saved for ${provider}/${modelId}: ${label ?? "override cleared"}.`]);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -24,6 +24,9 @@ import { MAX_COST4_RATE } from "../usage/expected-prices";
|
|
|
24
24
|
import { isValidCost4Rate } from "../usage/user-cost-overlays";
|
|
25
25
|
|
|
26
26
|
const USAGE = `Usage:
|
|
27
|
+
ocx models display-name <provider/raw-model> (--set <text> | --clear) [--json]
|
|
28
|
+
ocx models order status|reset [--json]
|
|
29
|
+
ocx models order set (--models <csv> | --mode <default|alphabetical|provider|most-used>) [--json]
|
|
27
30
|
ocx models live [--provider <name>] [--free-only] [--json]
|
|
28
31
|
ocx models price <provider/model> [--json]
|
|
29
32
|
ocx models set-price <provider/model> --input N --output N [--cache-read N] [--cache-write N] [--json]
|
|
@@ -564,6 +567,10 @@ async function shadow(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
564
567
|
}
|
|
565
568
|
|
|
566
569
|
export async function handleModelsRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
|
|
570
|
+
if (sub === "order" || sub === "display-name") {
|
|
571
|
+
const { handleModelsOrderCommand, handleModelsDisplayNameCommand } = await import("./models-order");
|
|
572
|
+
return sub === "order" ? handleModelsOrderCommand(argv, deps) : handleModelsDisplayNameCommand(argv, deps);
|
|
573
|
+
}
|
|
567
574
|
// The dispatch below and MODELS_RUNTIME_SUBCOMMANDS must name the same set;
|
|
568
575
|
// tests/cli/cli-models-runtime-dispatch.test.ts fails if they drift (#3094).
|
|
569
576
|
if (!isModelsRuntimeSubcommand(sub)) return null;
|
package/src/cli/models.ts
CHANGED
|
@@ -8,9 +8,7 @@ import { syncModelsToCodex } from "../codex/sync";
|
|
|
8
8
|
import { configuredContextWindow } from "../codex/catalog/provider-fetch";
|
|
9
9
|
import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
|
|
10
10
|
import {
|
|
11
|
-
canonicalizeReasoningEfforts,
|
|
12
11
|
configuredReasoningEfforts,
|
|
13
|
-
isDeclaredReasoningEffort,
|
|
14
12
|
modelRecordValue,
|
|
15
13
|
} from "../reasoning-effort";
|
|
16
14
|
import { encodedModelIdCollides, resolveSlugSelection, routedSlug } from "../providers/slug-codec";
|
|
@@ -18,66 +16,19 @@ import { isModelsRuntimeSubcommand } from "./models-runtime-subcommands";
|
|
|
18
16
|
import { knownModelIdsForProvider } from "../router";
|
|
19
17
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
20
18
|
import { modelInList, type OcxConfig, type OcxCustomModel } from "../types";
|
|
19
|
+
import type { RuntimeApiDeps } from "./runtime-api";
|
|
20
|
+
import { projectLocalSyncResult, type LocalSyncResult } from "./local-sync-result";
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
export interface ModelsCommandDeps extends RuntimeApiDeps { syncModels?: typeof syncModelsToCodex; }
|
|
23
|
+
|
|
24
|
+
const ADD_USAGE = "Usage: ocx models add <provider> <modelId> [--display-name <name>] [--context-window <tokens>] [--modalities text,image,audio] [--reasoning-efforts <none,minimal,low,medium,high,xhigh,max,ultra>] [--default-reasoning-effort <level>] [--live] [--json]";
|
|
25
|
+
const REMOVE_USAGE = "Usage: ocx models remove <customId|provider/modelId> [--yes] [--live] [--json]";
|
|
24
26
|
const LIST_CUSTOM_USAGE = "Usage: ocx models list-custom [--json]";
|
|
25
27
|
const ALLOWED_MODALITIES = new Set(["text", "image", "audio"]);
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* ("no reasoning" override, the same state the dashboard stores for the toggle-off
|
|
31
|
-
* checkbox set). Malformed CSV like `low,,high` or `,,` is rejected instead of being
|
|
32
|
-
* silently normalized. Values are canonicalized into Codex ladder order so the stored
|
|
33
|
-
* config matches what the API stores.
|
|
34
|
-
*/
|
|
35
|
-
export function parseReasoningArgs(
|
|
36
|
-
reasoningEffortsValue: string | undefined,
|
|
37
|
-
defaultEffortValue: string | undefined,
|
|
38
|
-
): { reasoningEfforts?: string[]; defaultReasoningEffort?: string; error?: string } {
|
|
39
|
-
if (reasoningEffortsValue === undefined && defaultEffortValue === undefined) return {};
|
|
40
|
-
let reasoningEfforts: string[] | undefined;
|
|
41
|
-
if (reasoningEffortsValue !== undefined) {
|
|
42
|
-
const trimmed = reasoningEffortsValue.trim();
|
|
43
|
-
if (trimmed === "-") {
|
|
44
|
-
reasoningEfforts = undefined;
|
|
45
|
-
} else if (trimmed === "") {
|
|
46
|
-
// Explicit no-reasoning override, exactly like the API's [] / the dashboard's
|
|
47
|
-
// uncheck-all state.
|
|
48
|
-
reasoningEfforts = [];
|
|
49
|
-
} else {
|
|
50
|
-
const parts = trimmed.split(",").map(value => value.trim());
|
|
51
|
-
if (parts.some(part => part === "")) {
|
|
52
|
-
return { error: "--reasoning-efforts must be comma-separated values from none, minimal, low, medium, high, xhigh, max, ultra (\"\" for no reasoning, \"-\" to inherit)" };
|
|
53
|
-
}
|
|
54
|
-
const invalid = parts.filter(value => !isDeclaredReasoningEffort(value));
|
|
55
|
-
if (invalid.length > 0) {
|
|
56
|
-
return { error: `unsupported reasoning effort: ${invalid.join(", ")} (allowed: none, minimal, low, medium, high, xhigh, max, ultra)` };
|
|
57
|
-
}
|
|
58
|
-
reasoningEfforts = canonicalizeReasoningEfforts(parts);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
let defaultReasoningEffort: string | undefined;
|
|
62
|
-
if (defaultEffortValue !== undefined) {
|
|
63
|
-
const trimmed = defaultEffortValue.trim();
|
|
64
|
-
if (trimmed === "-") {
|
|
65
|
-
defaultReasoningEffort = undefined;
|
|
66
|
-
} else {
|
|
67
|
-
if (!isDeclaredReasoningEffort(trimmed)) {
|
|
68
|
-
return { error: `unsupported reasoning effort: ${trimmed} (allowed: none, minimal, low, medium, high, xhigh, max, ultra)` };
|
|
69
|
-
}
|
|
70
|
-
if (!reasoningEfforts || reasoningEfforts.length === 0) {
|
|
71
|
-
return { error: "--default-reasoning-effort requires --reasoning-efforts" };
|
|
72
|
-
}
|
|
73
|
-
if (!reasoningEfforts.includes(trimmed)) {
|
|
74
|
-
return { error: `--default-reasoning-effort "${trimmed}" is not in the declared reasoning efforts` };
|
|
75
|
-
}
|
|
76
|
-
defaultReasoningEffort = trimmed;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return { reasoningEfforts, defaultReasoningEffort };
|
|
80
|
-
}
|
|
29
|
+
import { parseReasoningArgs } from "./models-custom-input";
|
|
30
|
+
import { redactSecretArgs } from "./secret-args";
|
|
31
|
+
export { parseReasoningArgs } from "./models-custom-input";
|
|
81
32
|
|
|
82
33
|
interface ModelEntry {
|
|
83
34
|
provider: string;
|
|
@@ -173,29 +124,44 @@ function fail(message: string, usage?: string): never {
|
|
|
173
124
|
|
|
174
125
|
function rejectUnexpectedArgs(args: string[], usage: string): void {
|
|
175
126
|
if (args.length === 0) return;
|
|
176
|
-
const
|
|
127
|
+
const shown = redactSecretArgs(args);
|
|
128
|
+
const unknown = shown.filter(arg => arg.startsWith("-"));
|
|
177
129
|
fail(
|
|
178
130
|
unknown.length > 0
|
|
179
131
|
? `Unknown flag(s): ${unknown.join(", ")}`
|
|
180
|
-
: `Unexpected argument(s): ${
|
|
132
|
+
: `Unexpected argument(s): ${shown.join(", ")}`,
|
|
181
133
|
usage,
|
|
182
134
|
);
|
|
183
135
|
}
|
|
184
136
|
|
|
185
|
-
async function syncCustomModelsIfLive(): Promise<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (synced?.status === "skipped") {
|
|
193
|
-
console.log("Custom model saved; Codex integration is OFF, so its catalog was not changed.");
|
|
137
|
+
async function syncCustomModelsIfLive(config: OcxConfig, deps: ModelsCommandDeps): Promise<LocalSyncResult> {
|
|
138
|
+
try {
|
|
139
|
+
const live = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
140
|
+
if (!live) return { status: "not-attempted", ok: false };
|
|
141
|
+
return projectLocalSyncResult(await (deps.syncModels ?? syncModelsToCodex)(live.port, config, null));
|
|
142
|
+
} catch {
|
|
143
|
+
return { status: "failed", ok: false };
|
|
194
144
|
}
|
|
195
145
|
}
|
|
196
146
|
|
|
197
|
-
|
|
147
|
+
function printCustomMutation(action: "added" | "removed", model: OcxCustomModel, sync: LocalSyncResult, wantsJson: boolean): void {
|
|
148
|
+
const complete = sync.configApplied === true && sync.ok;
|
|
149
|
+
if (sync.status !== "not-attempted" && (!sync.ok || sync.status === "refused")) process.exitCode = 1;
|
|
150
|
+
if (wantsJson) {
|
|
151
|
+
console.log(JSON.stringify({ action,
|
|
152
|
+
model: action === "added" ? model : { id: model.id, provider: model.provider, modelId: model.modelId },
|
|
153
|
+
needsSync: !complete, sync }, null, 2));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (sync.warning) console.log(`Warning: ${sync.warning}`);
|
|
157
|
+
console.log(`${action === "added" ? "Added" : "Removed"} custom model ${routedSlug(model.provider, model.modelId)}${action === "added" ? ` (${model.id})` : ""}.`);
|
|
158
|
+
if (sync.status === "skipped") console.log("Custom model saved; client sync was skipped by the integration policy.");
|
|
159
|
+
else if (!complete && sync.status !== "not-attempted") console.log("Custom model saved; client/catalog sync remains incomplete. Inspect the target before retrying.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function handleCustomAdd(args: string[], deps: ModelsCommandDeps): Promise<void> {
|
|
198
163
|
const rest = [...args];
|
|
164
|
+
const wantsJson = consumeFlag(rest, "--json");
|
|
199
165
|
const provider = rest.shift()?.trim() ?? "";
|
|
200
166
|
const modelId = rest.shift()?.trim() ?? "";
|
|
201
167
|
const displayNameValue = consumeFlagValue(rest, "--display-name");
|
|
@@ -260,8 +226,8 @@ async function handleCustomAdd(args: string[]): Promise<void> {
|
|
|
260
226
|
};
|
|
261
227
|
config.customModels = [...existing, entry];
|
|
262
228
|
saveConfig(config);
|
|
263
|
-
await syncCustomModelsIfLive();
|
|
264
|
-
|
|
229
|
+
const sync = await syncCustomModelsIfLive(config, deps);
|
|
230
|
+
printCustomMutation("added", entry, sync, wantsJson);
|
|
265
231
|
}
|
|
266
232
|
|
|
267
233
|
async function confirmCustomRemoval(model: OcxCustomModel): Promise<boolean> {
|
|
@@ -277,12 +243,18 @@ async function confirmCustomRemoval(model: OcxCustomModel): Promise<boolean> {
|
|
|
277
243
|
}
|
|
278
244
|
}
|
|
279
245
|
|
|
280
|
-
async function handleCustomRemove(args: string[]): Promise<void> {
|
|
246
|
+
async function handleCustomRemove(args: string[], deps: ModelsCommandDeps): Promise<void> {
|
|
281
247
|
const rest = [...args];
|
|
248
|
+
const wantsJson = consumeFlag(rest, "--json");
|
|
282
249
|
const confirmed = consumeFlag(rest, "--yes");
|
|
283
250
|
const target = rest.shift()?.trim() ?? "";
|
|
284
251
|
rejectUnexpectedArgs(rest, REMOVE_USAGE);
|
|
285
252
|
if (!target) fail("custom model id or provider/modelId is required", REMOVE_USAGE);
|
|
253
|
+
if (wantsJson && !confirmed) {
|
|
254
|
+
console.error("Error: JSON removal requires --yes; it does not prompt.");
|
|
255
|
+
process.exitCode = 2;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
286
258
|
|
|
287
259
|
const config = loadConfig();
|
|
288
260
|
const existing = config.customModels ?? [];
|
|
@@ -329,8 +301,8 @@ async function handleCustomRemove(args: string[]): Promise<void> {
|
|
|
329
301
|
const next = existing.filter((_, modelIndex) => modelIndex !== index);
|
|
330
302
|
config.customModels = next.length > 0 ? next : undefined;
|
|
331
303
|
saveConfig(config);
|
|
332
|
-
await syncCustomModelsIfLive();
|
|
333
|
-
|
|
304
|
+
const sync = await syncCustomModelsIfLive(config, deps);
|
|
305
|
+
printCustomMutation("removed", model, sync, wantsJson);
|
|
334
306
|
}
|
|
335
307
|
|
|
336
308
|
function customModelCells(model: OcxCustomModel): string[] {
|
|
@@ -384,11 +356,12 @@ function handleConfiguredModels(args: string[]): void {
|
|
|
384
356
|
const providerFilter = consumeFlagValue(restArgs, "--provider");
|
|
385
357
|
|
|
386
358
|
if (restArgs.length > 0) {
|
|
387
|
-
const
|
|
359
|
+
const shown = redactSecretArgs(restArgs);
|
|
360
|
+
const unknown = shown.filter(a => a.startsWith("-"));
|
|
388
361
|
if (unknown.length > 0) {
|
|
389
362
|
console.error(`Unknown flag(s): ${unknown.join(", ")}`);
|
|
390
363
|
} else {
|
|
391
|
-
console.error(`Unexpected argument(s): ${
|
|
364
|
+
console.error(`Unexpected argument(s): ${shown.join(", ")}`);
|
|
392
365
|
}
|
|
393
366
|
console.error("Usage: ocx models [--provider <name>] [--json]");
|
|
394
367
|
process.exit(1);
|
|
@@ -442,14 +415,26 @@ function handleConfiguredModels(args: string[]): void {
|
|
|
442
415
|
console.log("Note: providers with liveModels may have additional models at runtime.");
|
|
443
416
|
}
|
|
444
417
|
|
|
445
|
-
export async function handleModels(args: string[]): Promise<void> {
|
|
418
|
+
export async function handleModels(args: string[], deps: ModelsCommandDeps = {}): Promise<void> {
|
|
446
419
|
const [subcommand, ...rest] = args;
|
|
420
|
+
const live = rest.filter(arg => arg === "--live" || arg.startsWith("--live="));
|
|
421
|
+
if (live.length) {
|
|
422
|
+
if (live.length !== 1 || live[0] !== "--live" || (subcommand !== "add" && subcommand !== "remove")) {
|
|
423
|
+
console.error("Error: --live is a single boolean flag for models add or remove.");
|
|
424
|
+
process.exitCode = 2;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
rest.splice(rest.indexOf("--live"), 1);
|
|
428
|
+
const { handleModelsCustomRuntimeCommand } = await import("./models-custom-runtime");
|
|
429
|
+
process.exitCode = await handleModelsCustomRuntimeCommand(subcommand, rest, deps);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
447
432
|
if (subcommand === "add") {
|
|
448
|
-
await handleCustomAdd(rest);
|
|
433
|
+
await handleCustomAdd(rest, deps);
|
|
449
434
|
return;
|
|
450
435
|
}
|
|
451
436
|
if (subcommand === "remove") {
|
|
452
|
-
await handleCustomRemove(rest);
|
|
437
|
+
await handleCustomRemove(rest, deps);
|
|
453
438
|
return;
|
|
454
439
|
}
|
|
455
440
|
if (subcommand === "list-custom") {
|
|
@@ -458,7 +443,7 @@ export async function handleModels(args: string[]): Promise<void> {
|
|
|
458
443
|
}
|
|
459
444
|
if (isModelsRuntimeSubcommand(subcommand)) {
|
|
460
445
|
const { handleModelsRuntimeCommand } = await import("./models-runtime");
|
|
461
|
-
const code = await handleModelsRuntimeCommand(subcommand!, rest);
|
|
446
|
+
const code = await handleModelsRuntimeCommand(subcommand!, rest, deps);
|
|
462
447
|
if (code !== null) process.exitCode = code;
|
|
463
448
|
return;
|
|
464
449
|
}
|