@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,124 @@
|
|
|
1
|
+
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
|
|
2
|
+
import { CliUsageError, RuntimeApiError, printData, terminalSafeText } from "./runtime-api";
|
|
3
|
+
|
|
4
|
+
const ERRORS: Readonly<Record<string, string>> = {
|
|
5
|
+
stale_provider_editor_baseline: "Provider baseline is stale. Read a fresh snapshot and review the changes before applying again.",
|
|
6
|
+
provider_config_conflict: "Provider configuration changed before the write. Inspect the target before retrying.",
|
|
7
|
+
provider_config_unavailable: "Provider configuration is unavailable on the target.",
|
|
8
|
+
invalid_provider_editor_body: "Expected a provider editor document containing only defaultProvider and providers.",
|
|
9
|
+
invalid_provider_editor_field: "The provider editor document contains a non-editable field.",
|
|
10
|
+
invalid_provider_editor_config: "The proposed provider configuration is invalid.",
|
|
11
|
+
invalid_provider_destination: "The target refused a provider destination.",
|
|
12
|
+
invalid_provider: "The target refused the provider configuration.",
|
|
13
|
+
invalid_provider_name: "The provider name is invalid.",
|
|
14
|
+
invalid_default_provider: "The default must name a configured provider.",
|
|
15
|
+
default_provider_disabled: "A disabled provider cannot be the default.",
|
|
16
|
+
provider_namespace_conflict: "The provider conflicts with an account namespace.",
|
|
17
|
+
provider_has_dependent_combos: "A routing combo still depends on this provider. Inspect its dependencies before removal.",
|
|
18
|
+
last_provider: "The last provider cannot be removed.",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
23
|
+
&& [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Read only own data values; never invoke response getters or stringify a body. */
|
|
27
|
+
function own(value: unknown, key: string): unknown {
|
|
28
|
+
if (!record(value)) return undefined;
|
|
29
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
30
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Provider commands return numeric partial outcomes rather than losing them in runCliAction. */
|
|
34
|
+
export async function runProviderAction(action: () => Promise<number | void>): Promise<number> {
|
|
35
|
+
try {
|
|
36
|
+
return (await action()) ?? 0;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof CliUsageError) {
|
|
39
|
+
// Domain/input parsers supply static diagnostics; no raw parser/OS error reaches here.
|
|
40
|
+
console.error(`Error: ${terminalSafeText(error.message)}`);
|
|
41
|
+
if (error.usage) for (const line of error.usage.split("\n")) console.error(terminalSafeText(line));
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
if (error instanceof RuntimeApiError) {
|
|
45
|
+
const code = own(error.body, "code") ?? own(own(error.body, "error"), "code");
|
|
46
|
+
const known = typeof code === "string" && Object.hasOwn(ERRORS, code) ? ERRORS[code] : undefined;
|
|
47
|
+
const fallback = error.status === 404 ? "Provider or management operation was not found on the selected target."
|
|
48
|
+
: error.status === 409 ? "The target refused a conflicting provider change. Read back before trying again."
|
|
49
|
+
: error.status === 503 ? "Management API is unavailable. Start the intended proxy; connected clients must make provider changes on their Hub. The write outcome may be unknown."
|
|
50
|
+
: "The provider request failed. Inspect the selected target before retrying; the write outcome may be unknown.";
|
|
51
|
+
console.error(`Error: ${known ?? fallback}`);
|
|
52
|
+
return error.status === 404 ? 4 : error.status === 409 ? 5 : 1;
|
|
53
|
+
}
|
|
54
|
+
console.error("Error: Provider operation did not return a usable outcome. Inspect the selected target before retrying; no rollback is implied.");
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const RECEIPT_FIELDS = new Set([
|
|
60
|
+
"success", "name", "defaultProvider", "droppedCustomModels", "disabled", "hasApiKey",
|
|
61
|
+
"xaiResponsesOptInState", "dependentShadowIntercept", "catalogRefresh",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function invalidReceipt(): never { throw new Error("Invalid provider receipt"); }
|
|
65
|
+
function safeLabel(value: unknown): value is string { return typeof value === "string" && value.length > 0 && value.length <= 4096; }
|
|
66
|
+
|
|
67
|
+
/** Copy the public receipt vocabulary. Unexpected bodies never become raw CLI output. */
|
|
68
|
+
export function printProviderReceipt(value: unknown, wantsJson: boolean, action: string): number {
|
|
69
|
+
if (!record(value) || own(value, "success") !== true
|
|
70
|
+
|| Object.keys(value).some(key => !RECEIPT_FIELDS.has(key))) invalidReceipt();
|
|
71
|
+
const result: Record<string, unknown> = { success: true };
|
|
72
|
+
for (const key of ["name", "defaultProvider"] as const) {
|
|
73
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
74
|
+
const label = own(value, key);
|
|
75
|
+
if (!safeLabel(label)) invalidReceipt();
|
|
76
|
+
result[key] = label;
|
|
77
|
+
}
|
|
78
|
+
for (const key of ["disabled", "hasApiKey"] as const) {
|
|
79
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
80
|
+
const flag = own(value, key);
|
|
81
|
+
if (typeof flag !== "boolean") invalidReceipt();
|
|
82
|
+
result[key] = flag;
|
|
83
|
+
}
|
|
84
|
+
if (Object.hasOwn(value, "xaiResponsesOptInState")) {
|
|
85
|
+
const state = own(value, "xaiResponsesOptInState");
|
|
86
|
+
if (typeof state !== "boolean" && state !== "mixed") invalidReceipt();
|
|
87
|
+
result.xaiResponsesOptInState = state;
|
|
88
|
+
}
|
|
89
|
+
if (Object.hasOwn(value, "droppedCustomModels")) {
|
|
90
|
+
const count = own(value, "droppedCustomModels");
|
|
91
|
+
if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) invalidReceipt();
|
|
92
|
+
result.droppedCustomModels = count;
|
|
93
|
+
}
|
|
94
|
+
if (Object.hasOwn(value, "dependentShadowIntercept")) {
|
|
95
|
+
const shadow = own(value, "dependentShadowIntercept");
|
|
96
|
+
const model = own(shadow, "model"), enabled = own(shadow, "enabled");
|
|
97
|
+
if (!record(shadow) || !safeLabel(model) || typeof enabled !== "boolean"
|
|
98
|
+
|| Object.keys(shadow).some(key => key !== "model" && key !== "enabled")) invalidReceipt();
|
|
99
|
+
result.dependentShadowIntercept = { model, enabled };
|
|
100
|
+
}
|
|
101
|
+
let pending = false;
|
|
102
|
+
let outcome = "No client catalog refresh was requested by this operation.";
|
|
103
|
+
if (Object.hasOwn(value, "catalogRefresh")) {
|
|
104
|
+
const raw = own(value, "catalogRefresh");
|
|
105
|
+
const refresh = raw === null ? null : normalizeCatalogDisposition(raw);
|
|
106
|
+
if (raw !== null && refresh === null) invalidReceipt();
|
|
107
|
+
result.catalogRefresh = refresh;
|
|
108
|
+
if (refresh?.status === "committed") {
|
|
109
|
+
outcome = refresh.degraded ? "Catalog committed with degraded provider observations." : "Catalog committed.";
|
|
110
|
+
} else if (refresh && refresh.reason !== "not-requested") {
|
|
111
|
+
pending = true;
|
|
112
|
+
outcome = `Configuration saved, but catalog did not converge (${refresh.reason}). Read back before retrying.`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const lines = [`${action}: provider configuration saved.`, outcome];
|
|
116
|
+
if (result.defaultProvider) lines.push(`Default provider: ${result.defaultProvider}`);
|
|
117
|
+
if (result.droppedCustomModels !== undefined) lines.push(`Removed custom models: ${result.droppedCustomModels}`);
|
|
118
|
+
if (result.dependentShadowIntercept) {
|
|
119
|
+
const shadow = result.dependentShadowIntercept as { model: string; enabled: boolean };
|
|
120
|
+
lines.push(`Shadow-call dependency: ${shadow.model} (enabled: ${shadow.enabled}). Inspect its target.`);
|
|
121
|
+
}
|
|
122
|
+
printData(result, wantsJson, lines);
|
|
123
|
+
return pending ? 1 : 0;
|
|
124
|
+
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
takeOption,
|
|
13
13
|
type RuntimeApiDeps,
|
|
14
14
|
} from "./runtime-api";
|
|
15
|
+
import { takeProviderEditSettings } from "./provider-settings";
|
|
16
|
+
import { printProviderReceipt, runProviderAction } from "./provider-result";
|
|
15
17
|
import { providerQuotaLine } from "./account-extended";
|
|
16
18
|
import { pinSponsorRows } from "../providers/sponsor-order";
|
|
17
19
|
import type { ProviderQuotaReportDto } from "./account-api";
|
|
@@ -42,7 +44,7 @@ const USAGE = `Usage:
|
|
|
42
44
|
[--api-key-transport <x-api-key|bearer|->]
|
|
43
45
|
[--headers <json>] [--enabled <on|off>] [--live-models <on|off>]
|
|
44
46
|
[--retain-models <id,id|->] [--model <id> --text-only]
|
|
45
|
-
[--xai-chat <on|off>]
|
|
47
|
+
[--xai-chat <on|off>] [--upstream-http-version <http1.1|->] [--fast <on|off>] [--context-window <tokens|->]
|
|
46
48
|
[--allow-private-network <on|off>] [--model-context-tier <model=default|long_context>] [--json]
|
|
47
49
|
ocx provider test <name> [--json]
|
|
48
50
|
ocx provider quota [--refresh] [--json]
|
|
@@ -56,12 +58,12 @@ function cleared(value: string | undefined): string | undefined {
|
|
|
56
58
|
return value === "-" ? "" : value;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
async function edit(argv: string[], deps: RuntimeApiDeps): Promise<
|
|
61
|
+
async function edit(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
60
62
|
const args = [...argv];
|
|
61
63
|
const name = args.shift()?.trim();
|
|
62
64
|
if (!name) throw new CliUsageError("provider name is required", USAGE);
|
|
63
65
|
const wantsJson = takeFlag(args, "--json");
|
|
64
|
-
const patch: Record<string, unknown> =
|
|
66
|
+
const patch: Record<string, unknown> = takeProviderEditSettings(args);
|
|
65
67
|
const adapter = takeOption(args, "--adapter");
|
|
66
68
|
const baseUrl = takeOption(args, "--base-url");
|
|
67
69
|
const defaultModel = cleared(takeOption(args, "--default-model"));
|
|
@@ -139,13 +141,13 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
139
141
|
if (allowPrivateNetwork !== undefined) patch.allowPrivateNetwork = allowPrivateNetwork;
|
|
140
142
|
if (Object.keys(patch).length === 0) throw new CliUsageError("at least one edit option is required", USAGE);
|
|
141
143
|
const result = await runtimeRequest(`/api/providers?name=${encodeURIComponent(name)}`, {
|
|
142
|
-
method: "PATCH",
|
|
144
|
+
method: "PATCH", redirect: "error",
|
|
143
145
|
body: JSON.stringify(patch),
|
|
144
146
|
}, deps);
|
|
145
|
-
|
|
147
|
+
return printProviderReceipt(result, wantsJson, "Provider edit");
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<
|
|
150
|
+
async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
149
151
|
const args = [...argv];
|
|
150
152
|
const name = args.shift()?.trim();
|
|
151
153
|
const wantsJson = takeFlag(args, "--json");
|
|
@@ -159,7 +161,7 @@ async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<void>
|
|
|
159
161
|
`${name}: not applicable`,
|
|
160
162
|
"Static catalog; no live model-discovery endpoint to test.",
|
|
161
163
|
]);
|
|
162
|
-
return;
|
|
164
|
+
return 0;
|
|
163
165
|
}
|
|
164
166
|
const ok = result.ok === true;
|
|
165
167
|
printData(result, wantsJson, [
|
|
@@ -167,7 +169,7 @@ async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<void>
|
|
|
167
169
|
String(result.message ?? result.error ?? "No detail"),
|
|
168
170
|
`Latency: ${String(result.latencyMs ?? "?")} ms`,
|
|
169
171
|
]);
|
|
170
|
-
|
|
172
|
+
return ok ? 0 : 1;
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
async function quota(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
@@ -310,9 +312,8 @@ async function keychain(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
310
312
|
}
|
|
311
313
|
|
|
312
314
|
export async function handleProviderRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
update: edit,
|
|
315
|
+
if (sub === "edit" || sub === "update") return runProviderAction(() => edit(argv, deps));
|
|
316
|
+
const handlers: Record<string, (args: string[], deps: RuntimeApiDeps) => Promise<number | void>> = {
|
|
316
317
|
test: testProvider,
|
|
317
318
|
quota,
|
|
318
319
|
resets,
|
|
@@ -323,7 +324,9 @@ export async function handleProviderRuntimeCommand(sub: string, argv: string[],
|
|
|
323
324
|
};
|
|
324
325
|
const handler = handlers[sub];
|
|
325
326
|
if (!handler) return null;
|
|
326
|
-
|
|
327
|
+
let outcome: number | void = 0;
|
|
328
|
+
const exit = await runCliAction(async () => { outcome = await handler(argv, deps); });
|
|
329
|
+
return exit || outcome || 0;
|
|
327
330
|
}
|
|
328
331
|
|
|
329
332
|
export const PROVIDER_RUNTIME_USAGE = USAGE;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { isValidProviderName } from "../config/provider-name";
|
|
2
|
+
import { requestPacingConfigError } from "../config/schema/leaf-validators";
|
|
3
|
+
import { readJsonInput, serializeManagementJson } from "./json-input";
|
|
4
|
+
import { printProviderReceipt, runProviderAction } from "./provider-result";
|
|
5
|
+
import {
|
|
6
|
+
CliUsageError, RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest,
|
|
7
|
+
takeFlag, takeOptionWithSyntax, type RuntimeApiDeps,
|
|
8
|
+
} from "./runtime-api";
|
|
9
|
+
|
|
10
|
+
const PACING_USAGE = "Usage: ocx provider pacing <name> [--file <FILE|-> | --enabled <on|off> --rpm <number> --min-interval-ms <integer> --max-concurrent <integer>] [--json]";
|
|
11
|
+
|
|
12
|
+
function onOff(value: string, flag: string): boolean {
|
|
13
|
+
if (value !== "on" && value !== "off") throw new CliUsageError(`${flag} must be on or off`);
|
|
14
|
+
return value === "on";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function positiveInteger(value: string, flag: string): number {
|
|
18
|
+
const number = Number(value);
|
|
19
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(number) || number <= 0) {
|
|
20
|
+
throw new CliUsageError(`${flag} must be a positive safe integer`);
|
|
21
|
+
}
|
|
22
|
+
return number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Consume only the additional edit options; the existing edit parser owns all others. */
|
|
26
|
+
export function takeProviderEditSettings(args: string[]): Record<string, unknown> {
|
|
27
|
+
const patch: Record<string, unknown> = {};
|
|
28
|
+
const version = takeOptionWithSyntax(args, "--upstream-http-version")?.value;
|
|
29
|
+
const fast = takeOptionWithSyntax(args, "--fast")?.value;
|
|
30
|
+
const context = takeOptionWithSyntax(args, "--context-window")?.value;
|
|
31
|
+
if (version !== undefined) {
|
|
32
|
+
if (version !== "http1.1" && version !== "-") {
|
|
33
|
+
throw new CliUsageError("--upstream-http-version must be http1.1 or -");
|
|
34
|
+
}
|
|
35
|
+
patch.upstreamHttpVersion = version === "-" ? null : version;
|
|
36
|
+
}
|
|
37
|
+
if (fast !== undefined) patch.fastEnabled = onOff(fast, "--fast");
|
|
38
|
+
if (context !== undefined) patch.contextWindow = context === "-" ? null : positiveInteger(context, "--context-window");
|
|
39
|
+
return patch;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validRules(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return record(value) && requestPacingConfigError(value) === null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function takePacingScalars(args: string[]): Record<string, unknown> {
|
|
51
|
+
const patch: Record<string, unknown> = {};
|
|
52
|
+
const enabled = takeOptionWithSyntax(args, "--enabled")?.value;
|
|
53
|
+
const rpm = takeOptionWithSyntax(args, "--rpm")?.value;
|
|
54
|
+
const interval = takeOptionWithSyntax(args, "--min-interval-ms")?.value;
|
|
55
|
+
const concurrent = takeOptionWithSyntax(args, "--max-concurrent")?.value;
|
|
56
|
+
if (enabled !== undefined) patch.enabled = onOff(enabled, "--enabled");
|
|
57
|
+
if (rpm !== undefined) {
|
|
58
|
+
const number = Number(rpm);
|
|
59
|
+
if (!rpm.trim() || !Number.isFinite(number)) throw new CliUsageError("--rpm must be a finite number");
|
|
60
|
+
patch.requestsPerMinute = number;
|
|
61
|
+
}
|
|
62
|
+
if (interval !== undefined) patch.minIntervalMs = positiveInteger(interval, "--min-interval-ms");
|
|
63
|
+
if (concurrent !== undefined) patch.maxConcurrentRequests = positiveInteger(concurrent, "--max-concurrent");
|
|
64
|
+
// Validate numeric bounds before target resolution; enabling alone may use stored rules.
|
|
65
|
+
if (!validRules({ ...patch, enabled: false })) throw new CliUsageError("Invalid pacing limits", PACING_USAGE);
|
|
66
|
+
return patch;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function configuredRules(name: string, deps: RuntimeApiDeps): Promise<Record<string, unknown> | null> {
|
|
70
|
+
const config = await runtimeRequest("/api/config", { redirect: "error" }, deps);
|
|
71
|
+
if (!record(config) || !record(config.providers)) throw new Error("Invalid provider configuration response");
|
|
72
|
+
if (!Object.hasOwn(config.providers, name)) throw new RuntimeApiError("Provider not found", 404, null);
|
|
73
|
+
const provider = config.providers[name];
|
|
74
|
+
if (!record(provider)) throw new Error("Invalid provider configuration response");
|
|
75
|
+
if (!Object.hasOwn(provider, "requestPacing")) return null;
|
|
76
|
+
if (!validRules(provider.requestPacing)) throw new Error("Invalid stored pacing rules");
|
|
77
|
+
return provider.requestPacing;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pacingStatus(value: unknown, name: string): Record<string, unknown> {
|
|
81
|
+
const keys = ["provider", "enabled", "queued", "nextSlotInMs", "inFlight", "lastStartedAt", "lastModelId"];
|
|
82
|
+
if (!record(value) || Object.keys(value).some(key => !keys.includes(key))
|
|
83
|
+
|| value.provider !== name || typeof value.enabled !== "boolean") {
|
|
84
|
+
throw new Error("Invalid pacing status response");
|
|
85
|
+
}
|
|
86
|
+
for (const key of ["queued", "nextSlotInMs", "inFlight", "lastStartedAt"]) {
|
|
87
|
+
if ((key === "inFlight" || key === "lastStartedAt") && !Object.hasOwn(value, key)) continue;
|
|
88
|
+
const number = value[key];
|
|
89
|
+
if (typeof number !== "number" || !Number.isFinite(number) || number < 0
|
|
90
|
+
|| (key !== "lastStartedAt" && !Number.isSafeInteger(number))) {
|
|
91
|
+
throw new Error("Invalid pacing status response");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (Object.hasOwn(value, "lastModelId") && typeof value.lastModelId !== "string") {
|
|
95
|
+
throw new Error("Invalid pacing status response");
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function handleProviderPacingCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
101
|
+
return runProviderAction(async () => {
|
|
102
|
+
const args = [...argv];
|
|
103
|
+
const name = args.shift()?.trim();
|
|
104
|
+
if (!name || !isValidProviderName(name)) throw new CliUsageError("A valid provider name is required", PACING_USAGE);
|
|
105
|
+
const wantsJson = takeFlag(args, "--json");
|
|
106
|
+
const file = takeOptionWithSyntax(args, "--file")?.value;
|
|
107
|
+
const scalars = takePacingScalars(args);
|
|
108
|
+
if (args.length) throw new CliUsageError("Unexpected pacing argument(s)", PACING_USAGE);
|
|
109
|
+
const editing = Object.keys(scalars).length > 0;
|
|
110
|
+
if (file !== undefined && editing) throw new CliUsageError("--file cannot be combined with pacing scalar options", PACING_USAGE);
|
|
111
|
+
let fileBody: string | undefined;
|
|
112
|
+
if (file !== undefined) {
|
|
113
|
+
const rules = await readJsonInput(file, deps, "Pacing JSON input");
|
|
114
|
+
if (!validRules(rules)) throw new CliUsageError("Invalid pacing rules", PACING_USAGE);
|
|
115
|
+
fileBody = serializeManagementJson({ requestPacing: rules });
|
|
116
|
+
}
|
|
117
|
+
const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
118
|
+
if (fileBody !== undefined || editing) {
|
|
119
|
+
let body = fileBody;
|
|
120
|
+
if (body === undefined) {
|
|
121
|
+
const observed = await configuredRules(name, pinned);
|
|
122
|
+
const rules = { ...(observed ?? { enabled: false }), ...scalars };
|
|
123
|
+
if (!validRules(rules)) throw new CliUsageError("Invalid resulting pacing rules", PACING_USAGE);
|
|
124
|
+
// PATCH replaces the observed block, including its model rules. This is not CAS.
|
|
125
|
+
body = serializeManagementJson({ requestPacing: rules });
|
|
126
|
+
}
|
|
127
|
+
const result = await runtimeRequest(`/api/providers?name=${encodeURIComponent(name)}`, {
|
|
128
|
+
method: "PATCH", redirect: "error", body,
|
|
129
|
+
}, pinned);
|
|
130
|
+
return printProviderReceipt(result, wantsJson, "Updated provider pacing");
|
|
131
|
+
}
|
|
132
|
+
const rules = await configuredRules(name, pinned);
|
|
133
|
+
const status = pacingStatus(await runtimeRequest(`/api/provider-request-pacing?name=${encodeURIComponent(name)}`, {
|
|
134
|
+
redirect: "error",
|
|
135
|
+
}, pinned), name);
|
|
136
|
+
printData({ provider: name, rules, status }, wantsJson, [
|
|
137
|
+
`${name}: ${rules === null ? "no pacing rules configured" : rules.enabled ? "pacing rules enabled" : "pacing rules disabled"}`,
|
|
138
|
+
...(rules === null ? [] : [JSON.stringify(rules)]),
|
|
139
|
+
`Runtime: ${status.enabled ? "enabled" : "disabled"}; queued: ${status.queued}; next slot: ${status.nextSlotInMs} ms`,
|
|
140
|
+
]);
|
|
141
|
+
return 0;
|
|
142
|
+
});
|
|
143
|
+
}
|
package/src/cli/provider.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* show <name> Show provider config details (secrets masked)
|
|
9
9
|
* set-default <name> Change the default provider
|
|
10
10
|
*/
|
|
11
|
-
import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig } from "../config";
|
|
11
|
+
import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate } from "../config";
|
|
12
12
|
import { apiKeyTransportConfigError, modelCapabilitiesConfigError, mergeModelCapabilities } from "../config/provider-validation";
|
|
13
13
|
import { hasHelpFlag, printSubcommandUsage } from "./help";
|
|
14
14
|
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
|
|
@@ -19,6 +19,16 @@ import { findLiveProxy } from "../server/proxy-liveness";
|
|
|
19
19
|
import { syncModelsToCodex } from "../codex/sync";
|
|
20
20
|
import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
|
|
21
21
|
import { modelSelectionGuidance, modelSelectionNextSteps } from "./model-selection-guidance";
|
|
22
|
+
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination";
|
|
23
|
+
import { providerRelativeSendPathConfigError } from "../config/provider-relative-send-path";
|
|
24
|
+
import type { RuntimeApiDeps } from "./runtime-api";
|
|
25
|
+
import { redactSecretArgs } from "./secret-args";
|
|
26
|
+
import { projectLocalSyncResult, type LocalSyncResult } from "./local-sync-result";
|
|
27
|
+
import { providerManagementConfigError } from "../server/auth-cors";
|
|
28
|
+
|
|
29
|
+
export interface ProviderCommandDeps extends RuntimeApiDeps {
|
|
30
|
+
syncModels?: typeof syncModelsToCodex;
|
|
31
|
+
}
|
|
22
32
|
|
|
23
33
|
// ---------------------------------------------------------------------------
|
|
24
34
|
// Arg helpers
|
|
@@ -42,11 +52,13 @@ function consumeFlagValue(args: string[], flag: string): string | undefined {
|
|
|
42
52
|
/** Reject any leftover args (unknown flags or trailing values). */
|
|
43
53
|
function rejectUnknownArgs(args: string[], usage: string): void {
|
|
44
54
|
if (args.length === 0) return;
|
|
45
|
-
const
|
|
55
|
+
const shown = redactSecretArgs(args);
|
|
56
|
+
// Flags plus redaction markers only: a stray positional may be a credential operand.
|
|
57
|
+
const unknown = shown.filter(a => a.startsWith("-") || a === "<redacted>");
|
|
46
58
|
if (unknown.length > 0) {
|
|
47
59
|
console.error(`Unknown flag(s): ${unknown.join(", ")}`);
|
|
48
60
|
} else {
|
|
49
|
-
console.error(`Unexpected argument(s): ${
|
|
61
|
+
console.error(`Unexpected argument(s): ${shown.join(", ")}`);
|
|
50
62
|
}
|
|
51
63
|
console.error(usage);
|
|
52
64
|
process.exit(1);
|
|
@@ -70,6 +82,16 @@ function validateAndSave(config: ReturnType<typeof loadConfig>): void {
|
|
|
70
82
|
console.error(`Error: defaultProvider "${config.defaultProvider}" does not exist in providers. Aborting.`);
|
|
71
83
|
process.exit(1);
|
|
72
84
|
}
|
|
85
|
+
const result = validateConfigCandidate(config);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
console.error(`Error: ${result.error}`);
|
|
88
|
+
if (result.error.includes("set allowPrivateNetwork:true")) {
|
|
89
|
+
console.error("For an intentionally local provider, add --allow-private-network.");
|
|
90
|
+
} else {
|
|
91
|
+
console.error("Nothing was saved. Fix the setting named above; if this command did not set it, run ocx config validate and repair it with ocx config set/unset.");
|
|
92
|
+
}
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
73
95
|
saveConfig(config);
|
|
74
96
|
}
|
|
75
97
|
|
|
@@ -139,9 +161,9 @@ function handleList(args: string[]): void {
|
|
|
139
161
|
// provider add
|
|
140
162
|
// ---------------------------------------------------------------------------
|
|
141
163
|
|
|
142
|
-
const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--api-key-transport <x-api-key|bearer>] [--default-model <model>] [--model <id> --text-only] [--google-tool-schema-policy <compatible|reject-lossy>] [--allow-private-network] [--set-default] [--force] [--json] [--sync]";
|
|
164
|
+
const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--responses-path <path>] [--auth-mode <key|forward|oauth|local>] [--api-key <key>] [--api-key-transport <x-api-key|bearer>] [--default-model <model>] [--model <id> --text-only] [--google-tool-schema-policy <compatible|reject-lossy>] [--allow-private-network] [--set-default] [--force] [--json] [--sync | --live]";
|
|
143
165
|
|
|
144
|
-
async function handleAdd(args: string[]): Promise<void> {
|
|
166
|
+
async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<void> {
|
|
145
167
|
const name = args[0];
|
|
146
168
|
if (!name || name.startsWith("-")) {
|
|
147
169
|
console.error(ADD_USAGE);
|
|
@@ -164,6 +186,8 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
164
186
|
const adapter = consumeFlagValue(restArgs, "--adapter");
|
|
165
187
|
const baseUrl = consumeFlagValue(restArgs, "--base-url");
|
|
166
188
|
const defaultModel = consumeFlagValue(restArgs, "--default-model");
|
|
189
|
+
const responsesPath = consumeFlagValue(restArgs, "--responses-path");
|
|
190
|
+
const authMode = consumeFlagValue(restArgs, "--auth-mode");
|
|
167
191
|
const googleToolSchemaPolicy = consumeFlagValue(restArgs, "--google-tool-schema-policy");
|
|
168
192
|
const textOnly = consumeFlag(restArgs, "--text-only");
|
|
169
193
|
const capabilityModel = consumeFlagValue(restArgs, "--model");
|
|
@@ -173,6 +197,12 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
173
197
|
process.exit(1);
|
|
174
198
|
}
|
|
175
199
|
|
|
200
|
+
const pathError = providerRelativeSendPathConfigError("responsesPath", responsesPath);
|
|
201
|
+
if (pathError || (authMode !== undefined && !["key", "forward", "oauth", "local"].includes(authMode))) {
|
|
202
|
+
console.error(pathError ? `Error: ${pathError}.` : "Error: --auth-mode must be key, forward, oauth, or local.");
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
176
206
|
const config = loadConfig();
|
|
177
207
|
|
|
178
208
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, name);
|
|
@@ -192,9 +222,9 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
192
222
|
if (registryEntry) {
|
|
193
223
|
provConfig = providerConfigSeed(registryEntry);
|
|
194
224
|
if (apiKey) {
|
|
195
|
-
if (registryEntry.authKind === "forward") {
|
|
225
|
+
if ((authMode ?? registryEntry.authKind) === "forward") {
|
|
196
226
|
console.warn(`Warning: provider "${name}" uses ChatGPT login (forward auth); --api-key is ignored.`);
|
|
197
|
-
} else if (registryEntry.authKind === "oauth") {
|
|
227
|
+
} else if ((authMode ?? registryEntry.authKind) === "oauth") {
|
|
198
228
|
console.warn(`Warning: provider "${name}" uses OAuth auth; --api-key is ignored. Run: ocx login ${name}`);
|
|
199
229
|
} else {
|
|
200
230
|
provConfig.apiKey = apiKey;
|
|
@@ -217,6 +247,15 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
217
247
|
};
|
|
218
248
|
}
|
|
219
249
|
|
|
250
|
+
if (responsesPath !== undefined) provConfig.responsesPath = responsesPath;
|
|
251
|
+
if (authMode !== undefined) provConfig.authMode = authMode as OcxProviderConfig["authMode"];
|
|
252
|
+
if (name === "openai" && (authMode !== undefined || responsesPath !== undefined)
|
|
253
|
+
&& !isCanonicalOpenAiForwardProvider(provConfig)) {
|
|
254
|
+
console.error("Error: Canonical OpenAI must keep its built-in forward destination and authentication. Use a separate provider name for a custom endpoint.");
|
|
255
|
+
process.exitCode = 2;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
220
259
|
if (apiKeyTransport !== undefined) {
|
|
221
260
|
if (apiKeyTransport !== "x-api-key" && apiKeyTransport !== "bearer") {
|
|
222
261
|
console.error('Error: --api-key-transport must be "x-api-key" or "bearer".');
|
|
@@ -259,9 +298,6 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
259
298
|
if (error) { console.error(`Error: ${error}.`); process.exit(1); }
|
|
260
299
|
provConfig.modelCapabilities = mergeModelCapabilities(provConfig.modelCapabilities, declaration);
|
|
261
300
|
}
|
|
262
|
-
const { initializeProviderModelSelection } = await import("../providers/initial-model-selection");
|
|
263
|
-
initializeProviderModelSelection(name, provConfig, existingProvider, config);
|
|
264
|
-
config.providers[name] = provConfig;
|
|
265
301
|
// A --force overwrite rotates the key/endpoint but must not drop a
|
|
266
302
|
// user-configured price overlay (same rule as the /api/providers path and
|
|
267
303
|
// the login paths); there is no explicit clear/replace flag yet.
|
|
@@ -269,10 +305,38 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
269
305
|
provConfig.modelCosts = existingProvider.modelCosts;
|
|
270
306
|
}
|
|
271
307
|
if (allowPrivateNetwork) provConfig.allowPrivateNetwork = true;
|
|
308
|
+
// New auth/path overrides use the management owner's completed-row contract.
|
|
309
|
+
// Validate overrides before registration state changes; the full candidate is
|
|
310
|
+
// validated again by validateAndSave for every local save.
|
|
311
|
+
if ((authMode !== undefined || responsesPath !== undefined)
|
|
312
|
+
&& providerManagementConfigError(name, provConfig)) {
|
|
313
|
+
console.error("Error: Invalid provider configuration. Authentication, destination and provider options must satisfy the provider's management rules.");
|
|
314
|
+
process.exitCode = 2;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const { initializeProviderModelSelection } = await import("../providers/initial-model-selection");
|
|
318
|
+
initializeProviderModelSelection(name, provConfig, existingProvider, config);
|
|
319
|
+
config.providers[name] = provConfig;
|
|
272
320
|
if (setDefault) config.defaultProvider = name;
|
|
273
321
|
|
|
274
322
|
validateAndSave(config);
|
|
275
323
|
|
|
324
|
+
let sync: LocalSyncResult | undefined;
|
|
325
|
+
if (wantsSync) {
|
|
326
|
+
try {
|
|
327
|
+
const live = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
328
|
+
if (!live) sync = { status: "not-running", ok: false };
|
|
329
|
+
else {
|
|
330
|
+
const result = await (deps.syncModels ?? syncModelsToCodex)(live.port, config, null);
|
|
331
|
+
sync = projectLocalSyncResult(result);
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// Dependency errors can contain keys, paths or request details. Keep a fixed outcome.
|
|
335
|
+
sync = { status: "failed", ok: false };
|
|
336
|
+
}
|
|
337
|
+
if (!sync.ok || sync.status === "refused") process.exitCode = 1;
|
|
338
|
+
}
|
|
339
|
+
const synced = sync?.status === "applied" && sync.ok;
|
|
276
340
|
if (wantsJson) {
|
|
277
341
|
console.log(JSON.stringify({
|
|
278
342
|
action: "added",
|
|
@@ -283,26 +347,13 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
283
347
|
defaultModel: provConfig.defaultModel ?? null,
|
|
284
348
|
isDefault: config.defaultProvider === name,
|
|
285
349
|
source: registryEntry ? "registry" : "custom",
|
|
286
|
-
needsSync:
|
|
350
|
+
needsSync: !synced,
|
|
351
|
+
...(sync ? { sync } : {}),
|
|
287
352
|
}, null, 2));
|
|
288
353
|
return;
|
|
289
354
|
}
|
|
290
355
|
|
|
291
|
-
|
|
292
|
-
if (wantsSync) {
|
|
293
|
-
const live = await findLiveProxy();
|
|
294
|
-
if (live) {
|
|
295
|
-
const synced = await syncModelsToCodex(live.port).catch(e => {
|
|
296
|
-
console.error(`Warning: sync failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
297
|
-
return null;
|
|
298
|
-
});
|
|
299
|
-
if (synced?.status === "skipped") {
|
|
300
|
-
codexSyncSkipped = true;
|
|
301
|
-
console.log("Provider saved; Codex integration is OFF, so Codex sync was skipped.");
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
356
|
+
if (sync?.warning) console.log(` Warning: ${sync.warning}`);
|
|
306
357
|
const registryLabel = registryEntry ? ` (${registryEntry.label})` : "";
|
|
307
358
|
console.log(`✅ Provider "${name}"${registryLabel} added.`);
|
|
308
359
|
for (const line of modelSelectionGuidance(name)) console.log(line);
|
|
@@ -315,10 +366,15 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
315
366
|
console.log(` Set API key with: ocx provider add ${name} --api-key <key> --force`);
|
|
316
367
|
console.log(` Or set env var: ${envKey}`);
|
|
317
368
|
}
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
369
|
+
if (synced) console.log(" Models synced to Codex.");
|
|
370
|
+
else {
|
|
371
|
+
if (sync) {
|
|
372
|
+
console.log(sync.configApplied && !sync.ok
|
|
373
|
+
? " Provider saved; sync remains incomplete despite config injection."
|
|
374
|
+
: ` Provider saved; client sync outcome: ${sync.status}.`);
|
|
375
|
+
if (sync.catalog && !sync.catalog.converged) console.log(" Model catalog did not converge; inspect the target before retrying.");
|
|
376
|
+
}
|
|
377
|
+
console.log(" Apply to Codex: ocx sync");
|
|
322
378
|
}
|
|
323
379
|
}
|
|
324
380
|
|
|
@@ -473,7 +529,7 @@ function handleSetDefault(args: string[]): void {
|
|
|
473
529
|
// ---------------------------------------------------------------------------
|
|
474
530
|
|
|
475
531
|
|
|
476
|
-
export async function handleProviderCommand(args: string[]): Promise<void> {
|
|
532
|
+
export async function handleProviderCommand(args: string[], deps: ProviderCommandDeps = {}): Promise<void> {
|
|
477
533
|
const sub = args[0];
|
|
478
534
|
|
|
479
535
|
if (!sub || sub === "help" || hasHelpFlag(args)) {
|
|
@@ -482,13 +538,35 @@ export async function handleProviderCommand(args: string[]): Promise<void> {
|
|
|
482
538
|
}
|
|
483
539
|
|
|
484
540
|
const subArgs = args.slice(1);
|
|
541
|
+
const liveArgs = subArgs.filter(arg => arg === "--live" || arg.startsWith("--live="));
|
|
542
|
+
if (liveArgs.length) {
|
|
543
|
+
if (liveArgs.length !== 1 || liveArgs[0] !== "--live" || !["add", "remove", "set-default"].includes(sub)) {
|
|
544
|
+
console.error("Error: --live is a single boolean flag for provider add, remove, or set-default.");
|
|
545
|
+
process.exitCode = 2;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
subArgs.splice(subArgs.indexOf("--live"), 1);
|
|
549
|
+
const { handleProviderLifecycleRuntimeCommand } = await import("./provider-lifecycle-runtime");
|
|
550
|
+
process.exitCode = await handleProviderLifecycleRuntimeCommand(sub as "add" | "remove" | "set-default", subArgs, deps);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (sub === "snapshot" || sub === "apply") {
|
|
554
|
+
const { handleProviderBatchCommand } = await import("./provider-batch");
|
|
555
|
+
process.exitCode = await handleProviderBatchCommand(sub, subArgs, deps);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (sub === "pacing") {
|
|
559
|
+
const { handleProviderPacingCommand } = await import("./provider-settings");
|
|
560
|
+
process.exitCode = await handleProviderPacingCommand(subArgs, deps);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
485
563
|
|
|
486
564
|
switch (sub) {
|
|
487
565
|
case "list":
|
|
488
566
|
handleList(subArgs);
|
|
489
567
|
break;
|
|
490
568
|
case "add":
|
|
491
|
-
await handleAdd(subArgs);
|
|
569
|
+
await handleAdd(subArgs, deps);
|
|
492
570
|
break;
|
|
493
571
|
case "remove":
|
|
494
572
|
handleRemove(subArgs);
|
|
@@ -501,12 +579,12 @@ export async function handleProviderCommand(args: string[]): Promise<void> {
|
|
|
501
579
|
break;
|
|
502
580
|
default: {
|
|
503
581
|
const { handleProviderRuntimeCommand } = await import("./provider-runtime");
|
|
504
|
-
const code = await handleProviderRuntimeCommand(sub, subArgs);
|
|
582
|
+
const code = await handleProviderRuntimeCommand(sub, subArgs, deps);
|
|
505
583
|
if (code !== null) {
|
|
506
584
|
process.exitCode = code;
|
|
507
585
|
break;
|
|
508
586
|
}
|
|
509
|
-
console.error(`Unknown provider subcommand: ${sub}`);
|
|
587
|
+
console.error(`Unknown provider subcommand: ${redactSecretArgs([sub ?? ""])[0]}`);
|
|
510
588
|
printSubcommandUsage("provider", undefined, { write: console.error });
|
|
511
589
|
process.exit(1);
|
|
512
590
|
}
|