/** * `ocx provider` subcommand — non-interactive provider management. * * Subcommands: * list List configured and available registry providers * add Add a provider from the registry or with custom flags * remove Remove a configured provider * show Show provider config details (secrets masked) * set-default Change the default provider */ import { hasOwnProvider, isValidProviderName, loadConfig, mutatePersistedConfig, sanitizeModelCostsForDisplay } from "../config"; import { apiKeyTransportConfigError } from "../config/provider-validation"; import { hasHelpFlag } from "./help"; import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry"; import { providerConfigSeed } from "../providers/derive"; import { dropProviderCustomModels } from "../providers/provider-id-rewrite"; import type { OcxProviderConfig } from "../types"; import { findLiveProxy } from "../server/proxy-liveness"; import { syncModelsToCodex } from "../codex/sync"; import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match"; import { modelSelectionGuidance, modelSelectionNextSteps } from "./model-selection-guidance"; // --------------------------------------------------------------------------- // Arg helpers // --------------------------------------------------------------------------- function consumeFlag(args: string[], flag: string): boolean { const idx = args.indexOf(flag); if (idx === -1) return false; args.splice(idx, 1); return true; } function consumeFlagValue(args: string[], flag: string): string | undefined { const idx = args.indexOf(flag); if (idx === -1 || idx + 1 >= args.length) return undefined; const value = args[idx + 1]; args.splice(idx, 2); return value; } /** Reject any leftover args (unknown flags or trailing values). */ function rejectUnknownArgs(args: string[], usage: string): void { if (args.length === 0) return; const unknown = args.filter(a => a.startsWith("-")); if (unknown.length > 0) { console.error(`Unknown flag(s): ${unknown.join(", ")}`); } else { console.error(`Unexpected argument(s): ${args.join(", ")}`); } console.error(usage); process.exit(1); } function maskSecret(value: string): string { if (value.length <= 8) return "****"; return `${value.slice(0, 4)}****${value.slice(-4)}`; } // --------------------------------------------------------------------------- // Validation helper // --------------------------------------------------------------------------- function validateProviderConfig(config: ReturnType): void { if (!config.providers || Object.keys(config.providers).length === 0) { throw new Error("config would have no providers"); } if (!hasOwnProvider(config.providers, config.defaultProvider)) { throw new Error(`defaultProvider "${config.defaultProvider}" does not exist in providers`); } } function mutateProviderConfig(mutate: (config: ReturnType) => { changed: boolean; value: T }): T { const outcome = mutatePersistedConfig(mutate); if (outcome.status === "unavailable") { throw new Error(outcome.reason === "conflict" ? "config changed while applying this provider update; retry" : `config is ${outcome.reason}`); } return outcome.value; } // --------------------------------------------------------------------------- // provider list // --------------------------------------------------------------------------- function handleList(args: string[]): void { const wantsJson = consumeFlag(args, "--json"); const wantsJsonl = consumeFlag(args, "--jsonl"); rejectUnknownArgs(args, "Usage: ocx provider list [--json|--jsonl]"); if (wantsJson && wantsJsonl) { console.error("Use only one of --json or --jsonl."); process.exit(1); } const config = loadConfig(); const configured = Object.keys(config.providers); const entries = configured.map(name => { const prov = config.providers[name]; const registryEntry = getProviderRegistryEntry(name); return { name, adapter: prov.adapter, baseUrl: prov.baseUrl, authMode: prov.authMode ?? "key", defaultModel: prov.defaultModel ?? null, isDefault: name === config.defaultProvider, source: registryEntry ? "registry" : "custom", models: prov.models ?? [], }; }); if (wantsJsonl) { for (const entry of entries) console.log(JSON.stringify(entry)); return; } if (wantsJson) { console.log(JSON.stringify({ configured: entries, registryCount: PROVIDER_REGISTRY.length }, null, 2)); return; } console.log("Configured providers:\n"); for (const name of configured) { const prov = config.providers[name]; const isDefault = name === config.defaultProvider ? " (default)" : ""; const registryEntry = getProviderRegistryEntry(name); const source = registryEntry ? "" : " [custom]"; const model = prov.defaultModel ? ` model=${prov.defaultModel}` : ""; console.log(` ${name}${isDefault}${source} adapter=${prov.adapter}${model}`); } const available = PROVIDER_REGISTRY.filter(e => !configured.includes(e.id)); if (available.length > 0) { console.log(`\nAvailable from registry (${available.length}):\n`); for (const entry of available) { const auth = entry.authKind === "forward" ? "chatgpt-login" : entry.authKind; console.log(` ${entry.id.padEnd(24)} ${entry.label} (${auth})`); } console.log(`\nAdd with: ocx provider add [--api-key ]`); } } // --------------------------------------------------------------------------- // provider add // --------------------------------------------------------------------------- const ADD_USAGE = "Usage: ocx provider add [--adapter ] [--base-url ] [--api-key ] [--api-key-transport ] [--default-model ] [--allow-private-network] [--set-default] [--force] [--json] [--sync]"; async function handleAdd(args: string[]): Promise { const name = args[0]; if (!name || name.startsWith("-")) { console.error(ADD_USAGE); process.exit(1); } if (!isValidProviderName(name)) { console.error(`Invalid provider name: "${name}". Use letters, numbers, dots, underscores, or hyphens.`); process.exit(1); } const restArgs = args.slice(1); const force = consumeFlag(restArgs, "--force"); const setDefault = consumeFlag(restArgs, "--set-default"); const wantsJson = consumeFlag(restArgs, "--json"); const wantsSync = consumeFlag(restArgs, "--sync"); const allowPrivateNetwork = consumeFlag(restArgs, "--allow-private-network"); const apiKey = consumeFlagValue(restArgs, "--api-key"); const apiKeyTransport = consumeFlagValue(restArgs, "--api-key-transport"); const adapter = consumeFlagValue(restArgs, "--adapter"); const baseUrl = consumeFlagValue(restArgs, "--base-url"); const defaultModel = consumeFlagValue(restArgs, "--default-model"); rejectUnknownArgs(restArgs, ADD_USAGE); let provConfig: OcxProviderConfig; const registryEntry = getProviderRegistryEntry(name); if (registryEntry) { provConfig = providerConfigSeed(registryEntry); if (apiKey) { if (registryEntry.authKind === "forward") { console.warn(`Warning: provider "${name}" uses ChatGPT login (forward auth); --api-key is ignored.`); } else if (registryEntry.authKind === "oauth") { console.warn(`Warning: provider "${name}" uses OAuth auth; --api-key is ignored. Run: ocx login ${name}`); } else { provConfig.apiKey = apiKey; } } if (defaultModel) provConfig.defaultModel = defaultModel; if (adapter) provConfig.adapter = adapter; if (baseUrl) provConfig.baseUrl = baseUrl; } else { if (!adapter || !baseUrl) { console.error(`Provider "${name}" is not in the registry. --adapter and --base-url are required.`); console.error("Usage: ocx provider add --adapter --base-url [--api-key ]"); process.exit(1); } provConfig = { adapter, baseUrl, ...(apiKey ? { apiKey } : {}), ...(defaultModel ? { defaultModel } : {}), }; } if (apiKeyTransport !== undefined) { if (apiKeyTransport !== "x-api-key" && apiKeyTransport !== "bearer") { console.error('Error: --api-key-transport must be "x-api-key" or "bearer".'); process.exit(1); } const transportError = apiKeyTransportConfigError({ ...provConfig, apiKeyTransport }); if (transportError) { console.error(`Error: ${transportError}.`); process.exit(1); } provConfig.apiKeyTransport = apiKeyTransport; } const { initializeProviderModelSelection } = await import("../providers/initial-model-selection"); if (allowPrivateNetwork) provConfig.allowPrivateNetwork = true; const saved = mutateProviderConfig(config => { const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, name); if (namespaceCollision) throw new Error(`${namespaceCollision}.`); const existingProvider = config.providers[name]; if (existingProvider && !force) throw new Error(`Provider "${name}" already exists. Use --force to overwrite.`); const next = structuredClone(provConfig); // A --force overwrite rotates the key/endpoint but must not drop a user-configured price overlay. if (existingProvider?.modelCosts !== undefined && next.modelCosts === undefined) next.modelCosts = existingProvider.modelCosts; initializeProviderModelSelection(name, next, existingProvider, config); config.providers[name] = next; if (setDefault) config.defaultProvider = name; validateProviderConfig(config); return { changed: JSON.stringify(existingProvider) !== JSON.stringify(next) || (setDefault && config.defaultProvider === name), value: { provider: next, defaultProvider: config.defaultProvider }, }; }); provConfig = saved.provider; if (wantsJson) { console.log(JSON.stringify({ action: "added", modelSelection: modelSelectionNextSteps(name), provider: name, adapter: provConfig.adapter, baseUrl: provConfig.baseUrl, defaultModel: provConfig.defaultModel ?? null, isDefault: saved.defaultProvider === name, source: registryEntry ? "registry" : "custom", needsSync: true, }, null, 2)); return; } let codexSyncSkipped = false; if (wantsSync) { const live = await findLiveProxy(); if (live) { const synced = await syncModelsToCodex(live.port).catch(e => { console.error(`Warning: sync failed: ${e instanceof Error ? e.message : String(e)}`); return null; }); if (synced?.status === "skipped") { codexSyncSkipped = true; console.log("Provider saved; Codex integration is OFF, so Codex sync was skipped."); } } } const registryLabel = registryEntry ? ` (${registryEntry.label})` : ""; console.log(`✅ Provider "${name}"${registryLabel} added.`); for (const line of modelSelectionGuidance(name)) console.log(line); if (setDefault) console.log(` Set as default provider.`); if (registryEntry?.authKind === "oauth") { console.log(` Authenticate with: ocx login ${name}`); } if (registryEntry?.authKind === "key" && !apiKey) { const envKey = `${name.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`; console.log(` Set API key with: ocx provider add ${name} --api-key --force`); console.log(` Or set env var: ${envKey}`); } if (wantsSync && !codexSyncSkipped) { console.log(` Models synced to Codex.`); } else { console.log(` Apply to Codex: ocx sync`); } } // --------------------------------------------------------------------------- // provider remove // --------------------------------------------------------------------------- function handleRemove(args: string[]): void { const restArgs = [...args]; const wantsJson = consumeFlag(restArgs, "--json"); const name = restArgs[0]; if (!name || name.startsWith("-")) { console.error("Usage: ocx provider remove [--json]"); process.exit(1); } rejectUnknownArgs(restArgs.slice(1), "Usage: ocx provider remove [--json]"); const saved = mutateProviderConfig(config => { if (!hasOwnProvider(config.providers, name)) throw new Error(`Provider "${name}" is not configured.`); if (name === config.defaultProvider) throw new Error(`Cannot remove "${name}" — it is the default provider. Change the default first: ocx provider set-default `); if (Object.keys(config.providers).length <= 1) throw new Error("Cannot remove the last provider."); const dependentCombos = Object.entries(config.combos ?? {}) .filter(([, combo]) => combo.targets.some(target => target.provider === name)) .map(([id]) => id).sort(); if (dependentCombos.length > 0) throw new Error(`Cannot remove "${name}" — combo(s) depend on it: ${dependentCombos.join(", ")}`); const dependentProfiles = Object.entries(config.routingProfiles ?? {}) .filter(([, profile]) => profile.candidates.some(candidate => candidate.provider === name)) .map(([id]) => id).sort(); if (dependentProfiles.length > 0) throw new Error(`Cannot remove "${name}" — routing profile(s) depend on it: ${dependentProfiles.join(", ")}`); delete config.providers[name]; const droppedCustomModels = dropProviderCustomModels(config, name); validateProviderConfig(config); return { changed: true, value: { droppedCustomModels, providers: Object.keys(config.providers), defaultProvider: config.defaultProvider }, }; }); const { droppedCustomModels } = saved; if (wantsJson) { console.log(JSON.stringify({ action: "removed", provider: name, remainingProviders: saved.providers, defaultProvider: saved.defaultProvider, needsSync: true, ...(droppedCustomModels > 0 ? { droppedCustomModels } : {}), }, null, 2)); return; } console.log(`✅ Provider "${name}" removed.`); if (droppedCustomModels > 0) { const plural = droppedCustomModels === 1 ? "model" : "models"; console.log(` Also removed ${droppedCustomModels} custom ${plural} that belonged to it.`); } } // --------------------------------------------------------------------------- // provider show // --------------------------------------------------------------------------- function handleShow(args: string[]): void { const restArgs = [...args]; const wantsJson = consumeFlag(restArgs, "--json"); const name = restArgs[0]; if (!name || name.startsWith("-")) { console.error("Usage: ocx provider show [--json]"); process.exit(1); } rejectUnknownArgs(restArgs.slice(1), "Usage: ocx provider show [--json]"); const config = loadConfig(); if (!hasOwnProvider(config.providers, name)) { console.error(`Provider "${name}" is not configured.`); process.exit(1); } const prov = config.providers[name]; const display = { ...prov, ...(prov.modelCosts !== undefined ? { modelCosts: sanitizeModelCostsForDisplay(prov.modelCosts) } : {}), ...(prov.apiKey ? { apiKey: maskSecret(prov.apiKey) } : {}), ...(prov.apiKeyPool ? { apiKeyPool: prov.apiKeyPool.map(e => ({ ...e, key: maskSecret(e.key) })) } : {}), ...(prov.azureCredential ? { azureCredential: { type: prov.azureCredential.type, hasManagedIdentityClientId: Boolean(prov.azureCredential.managedIdentityClientId?.trim()), }, } : {}), }; if (wantsJson) { console.log(JSON.stringify({ name, isDefault: name === config.defaultProvider, ...display }, null, 2)); return; } console.log(`Provider: ${name}${name === config.defaultProvider ? " (default)" : ""}`); console.log(` adapter: ${display.adapter}`); console.log(` baseUrl: ${display.baseUrl}`); if (display.authMode) console.log(` authMode: ${display.authMode}`); if (display.apiKey) console.log(` apiKey: ${display.apiKey}`); if (display.defaultModel) console.log(` defaultModel: ${display.defaultModel}`); if (display.models?.length) console.log(` models: ${display.models.join(", ")}`); } // --------------------------------------------------------------------------- // provider set-default // --------------------------------------------------------------------------- function handleSetDefault(args: string[]): void { const restArgs = [...args]; const wantsJson = consumeFlag(restArgs, "--json"); const name = restArgs[0]; if (!name || name.startsWith("-")) { console.error("Usage: ocx provider set-default [--json]"); process.exit(1); } rejectUnknownArgs(restArgs.slice(1), "Usage: ocx provider set-default [--json]"); const changed = mutateProviderConfig(config => { if (!hasOwnProvider(config.providers, name)) throw new Error(`Provider "${name}" is not configured. Add it first: ocx provider add ${name}`); if (config.defaultProvider === name) return { changed: false, value: false }; config.defaultProvider = name; validateProviderConfig(config); return { changed: true, value: true }; }); if (!changed) { if (wantsJson) { console.log(JSON.stringify({ action: "noop", provider: name, defaultProvider: name, needsSync: false }, null, 2)); } else { console.log(`"${name}" is already the default provider.`); } return; } if (wantsJson) { console.log(JSON.stringify({ action: "set-default", provider: name, defaultProvider: name, needsSync: true }, null, 2)); return; } console.log(`✅ Default provider set to "${name}".`); } // --------------------------------------------------------------------------- // Router (F2 fix: handle help flags internally, like service/codex-shim) // --------------------------------------------------------------------------- const PROVIDER_USAGE = `Usage: ocx provider Subcommands: list List configured and available providers add Add a provider (registry or custom) install-replit Install the paired Replit gateway providers edit Edit live provider fields test Test the provider's upstream model endpoint remove Remove a configured provider show Show provider config details set-default Change the default provider selected Show or set the provider model allowlist quota Show provider quota reports resets Show recently detected quota resets presets List GUI provider presets account-mode Set OpenAI Codex pool/direct mode Examples: ocx provider list ocx provider list --jsonl ocx provider add anthropic --api-key sk-ant-... ocx provider add my-ollama --adapter openai-chat --base-url http://localhost:11434/v1 ocx provider install-replit --origin https://my-app.replit.app (gateway key via REPLIT_GATEWAY_KEY, --stdin, or --gateway-key-file; never argv) ocx provider show anthropic --json ocx provider set-default anthropic ocx provider edit xai --xai-chat on # opt Grok 4.5/4.6 into Chat Completions ocx provider edit xai --xai-chat off # use Responses again ocx provider remove my-ollama`; export async function handleProviderCommand(args: string[]): Promise { const sub = args[0]; if (!sub || sub === "help" || hasHelpFlag(args)) { console.log(PROVIDER_USAGE); process.exit(0); } const subArgs = args.slice(1); switch (sub) { case "list": handleList(subArgs); break; case "add": await handleAdd(subArgs); break; case "install-replit": { const { handleInstallReplit } = await import("./provider-replit"); await handleInstallReplit(subArgs); break; } case "remove": handleRemove(subArgs); break; case "show": handleShow(subArgs); break; case "set-default": handleSetDefault(subArgs); break; default: { const { handleProviderRuntimeCommand } = await import("./provider-runtime"); const code = await handleProviderRuntimeCommand(sub, subArgs); if (code !== null) { process.exitCode = code; break; } console.error(`Unknown provider subcommand: ${sub}`); console.error(PROVIDER_USAGE); process.exit(1); } } }