diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index cc2471a527..d2a2fb8b2c 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -14,7 +14,7 @@ both `--adapter` and `--base-url`. | Subcommand | Supported flags | Action | | --- | --- | --- | -| `list` | `--json` | List configured providers and the remaining registry entries. | +| `list` | `--json`, `--jsonl` | List configured providers and the remaining registry entries; `--jsonl` emits one configured provider object per line. | | `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | Add a registry/custom provider. `--force` overwrites; `--sync` refreshes a running proxy in human-output mode. | | `edit ` | provider field flags, `--headers `, `--json` | Edit validated live provider fields without replacing key pools. `--headers` merges custom request headers; pass `{}` or `-` to clear them. | | `test ` | `--json` | Probe the real upstream model endpoint. | @@ -29,6 +29,7 @@ both `--adapter` and `--base-url`. ```bash ocx provider list --json +ocx provider list --jsonl # one configured provider object per line ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -37,6 +38,10 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl` writes only configured providers, one JSON object per line, and omits the +`registryCount` summary from `--json`. Use it for line-oriented scripts that should not +buffer the whole provider list. + :::caution[Custom headers are not a credential channel] `--headers` is for non-secret request metadata — routing hints, tenant or project selectors, tracing ids. It is **not** a place to put authentication diff --git a/skills/ocx/references/01_management_surface.md b/skills/ocx/references/01_management_surface.md index d5711f3cac..10b0cd9e89 100644 --- a/skills/ocx/references/01_management_surface.md +++ b/skills/ocx/references/01_management_surface.md @@ -67,6 +67,7 @@ Drives no management route. | Flag | Value | Meaning | |---|---|---| | `--json` | boolean | Emit the provider list as JSON. | +| `--jsonl` | boolean | Emit one configured provider per JSON line. | JSON mode: `envelope`. diff --git a/skills/ocx/references/02_json_shapes.md b/skills/ocx/references/02_json_shapes.md index a91e35a2e1..261c8be5a2 100644 --- a/skills/ocx/references/02_json_shapes.md +++ b/skills/ocx/references/02_json_shapes.md @@ -52,6 +52,11 @@ to `requestedModel` is how you get a wrong answer about which provider served it `displayMetrics.cost.estimate.estimateReasons` lists why — for example `usage_estimated`, `cache_detail_missing`, `expected_price_overlay`. +## `ocx provider list --jsonl` + +One configured provider per line. Each object has the same fields as an item in the +`configured` array from `ocx provider list --json`; the `registryCount` summary is omitted. + ## `ocx logs explain ` ```json diff --git a/skills/ocx/references/03_recipes.md b/skills/ocx/references/03_recipes.md index 85b734a9ce..d4a6c1631b 100644 --- a/skills/ocx/references/03_recipes.md +++ b/skills/ocx/references/03_recipes.md @@ -116,6 +116,7 @@ exist for them — do not attribute usage to either. ```bash ocx provider list --json +ocx provider list --jsonl # one configured provider per line ocx provider add --json # registry providers auto-configure by name ocx provider test --json ocx provider set-default --json diff --git a/src/cli/capabilities.ts b/src/cli/capabilities.ts index 1b5cfd6283..ff1f5fb9a2 100644 --- a/src/cli/capabilities.ts +++ b/src/cli/capabilities.ts @@ -148,7 +148,10 @@ export const CAPABILITIES: readonly Capability[] = [ summary: "Configured providers with connectivity and selected models.", // Local config + PROVIDER_REGISTRY. Does not call GET /api/providers. routes: [], - flags: [{ name: "--json", value: "boolean", summary: "Emit the provider list as JSON." }], + flags: [ + { name: "--json", value: "boolean", summary: "Emit the provider list as JSON." }, + { name: "--jsonl", value: "boolean", summary: "Emit one configured provider per JSON line." }, + ], mutates: false, json: "envelope", details: ["Reads local config; drives no management API route."], diff --git a/src/cli/provider.ts b/src/cli/provider.ts index 55c654d8d7..47f23fee62 100644 --- a/src/cli/provider.ts +++ b/src/cli/provider.ts @@ -79,26 +79,37 @@ function validateAndSave(config: ReturnType): void { function handleList(args: string[]): void { const wantsJson = consumeFlag(args, "--json"); - rejectUnknownArgs(args, "Usage: ocx provider list [--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) { - 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 ?? [], - }; - }); console.log(JSON.stringify({ configured: entries, registryCount: PROVIDER_REGISTRY.length }, null, 2)); return; } @@ -444,6 +455,7 @@ Subcommands: 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 show anthropic --json diff --git a/tests/cli/cli-provider.test.ts b/tests/cli/cli-provider.test.ts index b83bc8d514..ef70dcac1a 100644 --- a/tests/cli/cli-provider.test.ts +++ b/tests/cli/cli-provider.test.ts @@ -109,6 +109,32 @@ describe("ocx provider", () => { } }); + test("provider list --jsonl emits one configured provider per line", () => { + const { dir } = freshConfig(); + try { + const result = runCli(["provider", "list", "--jsonl"], { OPENCODEX_HOME: dir }); + expect(result.status).toBe(0); + const lines = result.stdout.trim().split(/\r?\n/); + expect(lines).toHaveLength(1); + const parsed = JSON.parse(lines[0] ?? ""); + expect(parsed).toMatchObject({ name: "openai", isDefault: true }); + expect(parsed).not.toHaveProperty("registryCount"); + } finally { + removeTreeWithRetry(dir); + } + }); + + test("provider list rejects --json and --jsonl together", () => { + const { dir } = freshConfig(); + try { + const result = runCli(["provider", "list", "--json", "--jsonl"], { OPENCODEX_HOME: dir }); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Use only one of --json or --jsonl"); + } finally { + removeTreeWithRetry(dir); + } + }); + test("provider add registry provider seeds config", () => { const { dir } = freshConfig(); try {