import { CliUsageError, csv, printData, rejectArgs, runCliAction, runtimeRequest, summaryLines, takeBooleanOption, takeFlag, takeOption, type RuntimeApiDeps, } from "./runtime-api"; import { providerQuotaLine } from "./account-extended"; import type { ProviderQuotaReportDto } from "./account-api"; interface ProviderQuotasDto { generatedAt?: number; reports?: ProviderQuotaReportDto[]; } interface QuotaResetEventDto { kind?: string; scope?: string; window?: string; percentBefore?: number; percentAfter?: number; resetAt?: number; detectedAt?: number; } interface QuotaResetsDto { enabled?: boolean; events?: QuotaResetEventDto[]; } const USAGE = `Usage: ocx provider edit [--adapter ] [--base-url ] [--default-model ] [--auth-mode ] [--note ] [--api-key-transport ] [--headers ] [--enabled ] [--live-models ] [--retain-models ] [--xai-chat ] [--allow-private-network ] [--json] ocx provider test [--json] ocx provider quota [--refresh] [--json] ocx provider resets [--limit ] [--json] ocx provider presets [--json] ocx provider account-mode [--json] ocx provider selected [--set ] [--clear] [--json] ocx provider keychain [status|store|restore] [--json]`; function cleared(value: string | undefined): string | undefined { return value === "-" ? "" : value; } async function edit(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const name = args.shift()?.trim(); if (!name) throw new CliUsageError("provider name is required", USAGE); const wantsJson = takeFlag(args, "--json"); const patch: Record = {}; const adapter = takeOption(args, "--adapter"); const baseUrl = takeOption(args, "--base-url"); const defaultModel = cleared(takeOption(args, "--default-model")); const authMode = cleared(takeOption(args, "--auth-mode")); const note = cleared(takeOption(args, "--note")); const apiKeyTransport = cleared(takeOption(args, "--api-key-transport")); const headers = takeOption(args, "--headers"); const retainModelsRaw = takeOption(args, "--retain-models"); const enabled = takeBooleanOption(args, "--enabled"); const liveModels = takeBooleanOption(args, "--live-models"); const allowPrivateNetwork = takeBooleanOption(args, "--allow-private-network"); const xaiChat = takeBooleanOption(args, "--xai-chat"); rejectArgs(args, USAGE); if (xaiChat !== undefined) { if (name !== "xai") throw new CliUsageError("--xai-chat is valid only for provider xai", USAGE); patch.xaiResponsesOptIn = !xaiChat; } if (adapter !== undefined) patch.adapter = adapter; if (baseUrl !== undefined) patch.baseUrl = baseUrl; if (defaultModel !== undefined) patch.defaultModel = defaultModel; if (authMode !== undefined) patch.authMode = authMode; if (note !== undefined) patch.note = note; if (apiKeyTransport !== undefined) patch.apiKeyTransport = apiKeyTransport; if (headers !== undefined) { if (headers === "-") { patch.headers = null; } else { let parsed: unknown; try { parsed = JSON.parse(headers); } catch { throw new CliUsageError("--headers must be valid JSON"); } if (parsed === null) { patch.headers = null; } else if (typeof parsed !== "object" || Array.isArray(parsed)) { throw new CliUsageError("--headers must be a JSON object like {\"X-Custom\":\"value\"}"); } else { patch.headers = parsed; } } } if (enabled !== undefined) patch.disabled = !enabled; if (retainModelsRaw !== undefined) { // `-` clears, matching the other `edit` scalars; test before csv() or it becomes ["-"]. patch.retainModels = retainModelsRaw.trim() === "-" ? null : csv(retainModelsRaw); } if (liveModels !== undefined) patch.liveModels = liveModels; if (allowPrivateNetwork !== undefined) patch.allowPrivateNetwork = allowPrivateNetwork; if (Object.keys(patch).length === 0) throw new CliUsageError("at least one edit option is required", USAGE); const result = await runtimeRequest(`/api/providers?name=${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch), }, deps); printData(result, wantsJson, [`Updated provider ${name}.`]); } async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const name = args.shift()?.trim(); const wantsJson = takeFlag(args, "--json"); if (!name) throw new CliUsageError("provider name is required", USAGE); rejectArgs(args, USAGE); const result = await runtimeRequest>(`/api/providers/test?name=${encodeURIComponent(name)}`, { method: "POST", }, deps); if (result.applicable === false) { printData(result, wantsJson, [ `${name}: not applicable`, "Static catalog; no live model-discovery endpoint to test.", ]); return; } const ok = result.ok === true; printData(result, wantsJson, [ `${name}: ${ok ? "connected" : "failed"}`, String(result.message ?? result.error ?? "No detail"), `Latency: ${String(result.latencyMs ?? "?")} ms`, ]); if (!ok) process.exitCode = 1; } async function quota(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const wantsJson = takeFlag(args, "--json"); const refresh = takeFlag(args, "--refresh"); rejectArgs(args, USAGE); const result = await runtimeRequest(`/api/provider-quotas${refresh ? "?refresh=1" : ""}`, {}, deps); // `summaryLines` is a depth-1 flattener: it renders a non-scalar array as "N item(s)", which // collapsed the whole report to a count and made the command useless for its stated purpose // (#2565). Render one line per report with the same formatter `ocx account refresh` uses. const reports = Array.isArray(result?.reports) ? result.reports : []; const lines = reports.length > 0 ? reports.map(report => providerQuotaLine(report.provider, report)) : ["no quota reports available"]; printData(result, wantsJson, lines); } /** * Recently detected quota resets, newest first. * * `accountTag` is deliberately NOT rendered: it is a salted hash that means nothing to a human * reading a terminal, and printing it invites treating an opaque tag as an account identifier. */ function quotaResetLine(event: QuotaResetEventDto): string { const when = typeof event.detectedAt === "number" ? new Date(event.detectedAt).toISOString() : "unknown time"; const movement = typeof event.percentBefore === "number" && typeof event.percentAfter === "number" ? `${event.percentBefore}% -> ${event.percentAfter}%` : "usage unknown"; const next = typeof event.resetAt === "number" ? `, next ${new Date(event.resetAt).toISOString()}` : ""; return `${when} ${event.scope ?? "?"} ${event.window ?? "?"} ${event.kind ?? "?"}: ${movement}${next}`; } async function resets(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const wantsJson = takeFlag(args, "--json"); const limitRaw = takeOption(args, "--limit"); rejectArgs(args, USAGE); if (limitRaw !== undefined && !/^\d+$/.test(limitRaw)) { throw new CliUsageError("--limit must be a non-negative integer", USAGE); } const query = limitRaw === undefined ? "" : `?limit=${limitRaw}`; const result = await runtimeRequest(`/api/quota-resets${query}`, {}, deps); const events = Array.isArray(result?.events) ? result.events : []; // One line per event, NOT summaryLines: that helper is a depth-1 flattener and renders a // non-scalar array as "N item(s)", which is what made `ocx provider quota` useless in #2565. const lines = events.length > 0 ? events.map(quotaResetLine) // An empty list is ambiguous, so say which kind of empty it is. Without this an operator // cannot tell "nothing has reset yet" from "I never turned this on". : [result?.enabled === true ? "no resets detected yet" : "quota-reset notifications are disabled (set quotaResetNotify.enabled)"]; printData(result, wantsJson, lines); } async function presets(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE); const result = await runtimeRequest<{ providers?: unknown[] } | unknown[]>("/api/provider-presets", {}, deps); const rows = Array.isArray(result) ? result : result.providers ?? []; printData(result, wantsJson, rows.map(row => { const record = row as Record; return `${String(record.id ?? record.name ?? "?")} ${String(record.label ?? record.adapter ?? "")}`.trimEnd(); })); } async function accountMode(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const mode = args.shift(); const wantsJson = takeFlag(args, "--json"); if (mode !== "pool" && mode !== "direct") throw new CliUsageError("mode must be pool or direct", USAGE); rejectArgs(args, USAGE); const result = await runtimeRequest("/api/providers?name=openai", { method: "PATCH", body: JSON.stringify({ codexAccountMode: mode }), }, deps); printData(result, wantsJson, [`OpenAI Codex account mode: ${mode}`]); } async function selected(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const name = args.shift()?.trim(); const wantsJson = takeFlag(args, "--json"); const set = csv(takeOption(args, "--set")); const clear = takeFlag(args, "--clear"); if (!name) throw new CliUsageError("provider name is required", USAGE); if (set !== undefined && clear) throw new CliUsageError("--set and --clear cannot be combined", USAGE); rejectArgs(args, USAGE); if (set === undefined && !clear) { const result = await runtimeRequest>("/api/selected-models", {}, deps); const selectedMap = (result.selected ?? {}) as Record; const availableMap = (result.available ?? {}) as Record; printData({ provider: name, selected: selectedMap[name] ?? [], available: availableMap[name] ?? [] }, wantsJson, [`Selected models for ${name}: ${(selectedMap[name] ?? []).join(", ") || "all"}`]); return; } const models = clear ? [] : (set ?? []); const result = await runtimeRequest("/api/selected-models", { method: "PUT", body: JSON.stringify({ provider: name, models }), }, deps); printData(result, wantsJson, [`${name}: ${models.length ? models.join(", ") : "all models"}`]); } async function keychain(argv: string[], deps: RuntimeApiDeps): Promise { const args = [...argv]; const name = args.shift()?.trim(); const wantsJson = takeFlag(args, "--json"); const action = (args.shift() ?? "status").toLowerCase(); if (!name) throw new CliUsageError("provider name is required", USAGE); if (!["status", "store", "restore"].includes(action)) throw new CliUsageError(`unknown keychain action ${action}`, USAGE); rejectArgs(args, USAGE); if (action === "status") { const result = await runtimeRequest>(`/api/providers/keychain?name=${encodeURIComponent(name)}`, {}, deps); printData(result, wantsJson, summaryLines(result)); return; } const result = await runtimeRequest>("/api/providers/keychain", { method: "POST", body: JSON.stringify({ name, action }), }, deps); printData(result, wantsJson, [action === "store" ? `${name}: API key moved to the OS keychain; config.json now holds a keychain: reference.` : `${name}: API key restored to config.json; keychain entries removed.`]); } export async function handleProviderRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise { const handlers: Record Promise> = { edit, update: edit, test: testProvider, quota, resets, presets, "account-mode": accountMode, selected, keychain, }; const handler = handlers[sub]; if (!handler) return null; return runCliAction(() => handler(argv, deps)); } export const PROVIDER_RUNTIME_USAGE = USAGE;