@spinabot/brigade 0.1.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -0
  4. package/SECURITY.md +208 -0
  5. package/assets/brigade-wordmark-on-black.png +0 -0
  6. package/assets/brigade-wordmark.png +0 -0
  7. package/brigade.mjs +96 -0
  8. package/dist/cli/chat-cmd.js +120 -0
  9. package/dist/cli/config-cmd.js +132 -0
  10. package/dist/cli/connect-cmd.js +447 -0
  11. package/dist/cli/doctor-cmd.js +317 -0
  12. package/dist/cli/gateway-cmd.js +92 -0
  13. package/dist/cli.js +287 -0
  14. package/dist/core/agent.js +1123 -0
  15. package/dist/core/config.js +80 -0
  16. package/dist/core/console-stream.js +188 -0
  17. package/dist/core/error-classifier.js +354 -0
  18. package/dist/core/event-logger.js +122 -0
  19. package/dist/core/model-caps.js +185 -0
  20. package/dist/core/provider-payload-mutators.js +517 -0
  21. package/dist/core/provider-quirks.js +285 -0
  22. package/dist/core/server.js +459 -0
  23. package/dist/core/smart-compaction.js +209 -0
  24. package/dist/core/system-prompt-defaults.js +88 -0
  25. package/dist/core/system-prompt-guidance.js +269 -0
  26. package/dist/core/system-prompt.js +884 -0
  27. package/dist/index.js +30 -0
  28. package/dist/integrations/ollama.js +140 -0
  29. package/dist/protocol.js +49 -0
  30. package/dist/providers/catalog.js +100 -0
  31. package/dist/providers/validate-key.js +197 -0
  32. package/dist/tui/client.js +263 -0
  33. package/dist/ui/brand-frames-cli.js +20 -0
  34. package/dist/ui/brand-frames.js +36 -0
  35. package/dist/ui/brand.js +402 -0
  36. package/dist/ui/chat.js +929 -0
  37. package/dist/ui/onboarding.js +400 -0
  38. package/dist/ui/theme.js +51 -0
  39. package/package.json +92 -0
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Backward-compat shim. The real entry point is `src/cli.ts`; this file
4
+ * stays so anything that previously executed `dist/index.js` (the old
5
+ * `bin.brigade` target) keeps working without a re-install.
6
+ *
7
+ * For new development, prefer `src/cli.ts` directly.
8
+ */
9
+ import process from "node:process";
10
+ import { pathToFileURL } from "node:url";
11
+ import { runCli } from "./cli.js";
12
+ const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
13
+ if (import.meta.url === entry) {
14
+ runCli(process.argv.slice(2))
15
+ .then((code) => {
16
+ if (typeof code === "number")
17
+ process.exit(code);
18
+ })
19
+ .catch((err) => {
20
+ try {
21
+ process.stdout.write("\x1b[?25h\x1b[?1049l");
22
+ }
23
+ catch {
24
+ /* terminal already gone */
25
+ }
26
+ console.error(`Brigade crashed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
27
+ process.exit(1);
28
+ });
29
+ }
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Ollama integration.
3
+ *
4
+ * Ollama runs locally and exposes two parallel APIs:
5
+ * 1. Native Ollama API (`/api/tags`, `/api/generate`) — easiest for discovery.
6
+ * 2. OpenAI-compatible API at `/v1/...` — what we point Pi-AI at for streaming.
7
+ *
8
+ * Pi-AI doesn't ship Ollama as a built-in provider. We register it dynamically
9
+ * via the `~/.brigade/models.json` mechanism Pi exposes for custom providers.
10
+ * Each model the user has pulled becomes a Pi `Model<"openai-completions">`.
11
+ *
12
+ * Capability inference is best-effort — Ollama doesn't tell us a model's
13
+ * context window or whether it reasons. We pattern-match the model name
14
+ * against well-known families and fall back to safe defaults.
15
+ */
16
+ import * as fs from "node:fs/promises";
17
+ const DEFAULT_BASE_URL = "http://localhost:11434";
18
+ const TIMEOUT_MS = 5000;
19
+ /**
20
+ * Hit Ollama's `/api/tags` and return the user's locally-installed models.
21
+ * Throws a friendly error if the server isn't running or returns no models.
22
+ */
23
+ export async function discoverOllamaModels(baseUrl = DEFAULT_BASE_URL) {
24
+ const url = `${baseUrl.replace(/\/$/, "")}/api/tags`;
25
+ const controller = new AbortController();
26
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
27
+ let response;
28
+ try {
29
+ response = await fetch(url, { signal: controller.signal });
30
+ }
31
+ catch (err) {
32
+ if (err instanceof Error && err.name === "AbortError") {
33
+ throw new Error(`Couldn't reach Ollama within ${TIMEOUT_MS / 1000} seconds. Make sure it's running.`);
34
+ }
35
+ throw new Error(`Couldn't reach Ollama. Is it running on this machine?`);
36
+ }
37
+ finally {
38
+ clearTimeout(timer);
39
+ }
40
+ if (!response.ok) {
41
+ throw new Error(`Ollama replied with an unexpected error (status ${response.status}).`);
42
+ }
43
+ const body = (await response.json());
44
+ const models = body.models ?? [];
45
+ if (models.length === 0) {
46
+ throw new Error(`Ollama is running but no models are installed yet. Install one (for example: ollama pull llama3.2) and try again.`);
47
+ }
48
+ return models.map((m) => ({
49
+ id: m.model ?? m.name,
50
+ name: m.name,
51
+ sizeBytes: m.size,
52
+ family: m.details?.family,
53
+ parameterSize: m.details?.parameter_size,
54
+ }));
55
+ }
56
+ export function inferOllamaModelCapabilities(modelId) {
57
+ const id = modelId.toLowerCase();
58
+ // Reasoning families. EXTREMELY conservative — only mark a model as
59
+ // reasoning when the name UNAMBIGUOUSLY signals it. False positives here
60
+ // are user-visible failures: marking a non-reasoning model as reasoning
61
+ // makes the loop send `thinking: low` which Ollama rejects with
62
+ // `400 "<model>" does not support thinking`. The runtime has a
63
+ // runWithThinkingFallback wrapper that auto-downgrades, but it's a
64
+ // wasted round trip on the first turn.
65
+ //
66
+ // Specifically EXCLUDED from previous heuristics (the regression case):
67
+ // - `qwen3-coder*` — code model, no thinking
68
+ // - `qwen3.5*` — base chat, not the reasoning fork
69
+ // Originally `^qwen3\b` matched all of these and triggered the bug.
70
+ //
71
+ // Reasoning patterns that ARE safe to mark:
72
+ // - `deepseek-r1*` — explicit r1 (reasoning) line
73
+ // - `qwq*` — Qwen with Questions (reasoning fork)
74
+ // - `*-thinking*` — explicit thinking variant naming convention
75
+ // - `o1*` / `o3*` — OpenAI o-series via Ollama (rare)
76
+ const reasoning = /^deepseek-r1\b/.test(id) ||
77
+ /^qwq\b/.test(id) ||
78
+ /-thinking\b/.test(id) ||
79
+ /\bo1\b/.test(id) ||
80
+ /\bo3\b/.test(id);
81
+ // Vision-capable families — text+image input.
82
+ const vision = /^llava\b/.test(id) ||
83
+ /^bakllava\b/.test(id) ||
84
+ /^moondream\b/.test(id) ||
85
+ /^llama3\.2-vision\b/.test(id) ||
86
+ /^minicpm-v\b/.test(id) ||
87
+ /-vision\b/.test(id) ||
88
+ /^gemma[34](?:\.\d+)?[-:]/.test(id); // Gemma 3+ has vision variants
89
+ // Context window — Ollama's defaults vary wildly. Common ranges:
90
+ // - Most modern open models: 32k-128k
91
+ // - Older models: 4k-8k
92
+ // We use 32k as a safe middle ground — Pi only uses this for display, not
93
+ // for actual API limits (Ollama itself enforces those server-side).
94
+ const contextWindow = 32_768;
95
+ const maxTokens = 8_192;
96
+ const input = vision ? ["text", "image"] : ["text"];
97
+ return { reasoning, contextWindow, maxTokens, input };
98
+ }
99
+ /**
100
+ * Write Brigade's Ollama provider entry into Pi's `~/.brigade/models.json`.
101
+ * Pi's `ModelRegistry.refresh()` picks this up and exposes the models as
102
+ * regular Pi models from then on.
103
+ *
104
+ * We MERGE rather than overwrite — the user (or other providers) may have
105
+ * existing entries in the file we shouldn't clobber.
106
+ */
107
+ export async function writeOllamaToModelsJson(modelsJsonPath, baseUrl, models) {
108
+ let existing = { providers: {} };
109
+ try {
110
+ const raw = await fs.readFile(modelsJsonPath, "utf8");
111
+ existing = JSON.parse(raw);
112
+ if (!existing.providers)
113
+ existing.providers = {};
114
+ }
115
+ catch {
116
+ // File missing or unparseable — start fresh. Pi treats an absent file as no config.
117
+ }
118
+ const modelDefs = models.map((m) => {
119
+ const caps = inferOllamaModelCapabilities(m.id);
120
+ return {
121
+ id: m.id,
122
+ name: m.name + (m.parameterSize ? ` (${m.parameterSize})` : ""),
123
+ reasoning: caps.reasoning,
124
+ input: caps.input,
125
+ contextWindow: caps.contextWindow,
126
+ maxTokens: caps.maxTokens,
127
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, // local = free
128
+ };
129
+ });
130
+ existing.providers["ollama"] = {
131
+ baseUrl: `${baseUrl.replace(/\/$/, "")}/v1`,
132
+ api: "openai-completions",
133
+ // Ollama ignores the API key but Pi requires apiKey to be set when defining
134
+ // custom models for non-built-in providers. Use a sentinel value.
135
+ apiKey: "ollama-local-no-auth-required",
136
+ models: modelDefs,
137
+ };
138
+ await fs.writeFile(modelsJsonPath, JSON.stringify(existing, null, 2), "utf8");
139
+ }
140
+ //# sourceMappingURL=ollama.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Brigade gateway wire protocol.
3
+ *
4
+ * Single WebSocket connection per client. Three frame types travel over
5
+ * the same connection:
6
+ *
7
+ * 1. REQUEST (client → server) — caller expects a Response with the same id
8
+ * 2. RESPONSE (server → client) — answers a Request, ok+payload OR error
9
+ * 3. EVENT (server → client) — push, no id, broadcast to all clients
10
+ *
11
+ * The req/res shape is for commands that need a reply (e.g. `list-models`
12
+ * returns the list). The event shape is for streaming the live agent
13
+ * state (every Pi event becomes a Brigade event with `event === "pi"`).
14
+ *
15
+ * Every frame is a JSON object with a discriminator `type` field so the
16
+ * server and client can route without sniffing payload shapes. ID format
17
+ * is `r{counter}` — opaque string, server treats as bytes.
18
+ *
19
+ * Compatible with: any WebSocket client speaking this JSON shape.
20
+ */
21
+ export function modelToSummary(model) {
22
+ return {
23
+ provider: model.provider,
24
+ id: model.id,
25
+ name: model.name ?? model.id,
26
+ reasoning: !!model.reasoning,
27
+ contextWindow: model.contextWindow ?? 0,
28
+ costInputPerMtok: model.cost?.input ?? 0,
29
+ hasVision: Array.isArray(model.input) && model.input.includes("image"),
30
+ };
31
+ }
32
+ /* ─────────────────────────── shared constants ─────────────────────────── */
33
+ /** Default port. Configurable via BRIGADE_PORT env var. */
34
+ export const DEFAULT_PORT = 7777;
35
+ /**
36
+ * Tick interval for the heartbeat. Server pushes a `pi`-wrapped tick event
37
+ * every TICK_INTERVAL_MS so the client can detect a stalled connection.
38
+ * Client closes if no frame received in 2× this interval.
39
+ */
40
+ export const TICK_INTERVAL_MS = 30_000;
41
+ /* ─────────────────────────── tiny runtime guards ─────────────────────────── */
42
+ /** Cheap shape check before routing. Avoids dragging in AJV for v1's small surface. */
43
+ export function isFrame(value) {
44
+ if (!value || typeof value !== "object")
45
+ return false;
46
+ const t = value.type;
47
+ return t === "req" || t === "res" || t === "event";
48
+ }
49
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Curated list of LLM providers Brigade exposes in the onboarding picker.
3
+ *
4
+ * Pi-AI knows ~25 providers internally — we surface the ~10 most popular for
5
+ * UX. Power users can edit `~/.brigade/models.json` to add anything else Pi
6
+ * supports (Bedrock, Vertex, ZAI, Moonshot, etc.) — see Pi's docs.
7
+ *
8
+ * `id` MUST match Pi-AI's KnownProvider names from
9
+ * pi-mono/packages/ai/src/types.ts:18
10
+ *
11
+ * `envVar` MUST match Pi's findEnvKeys() conventions from
12
+ * pi-mono/packages/ai/src/env-api-keys.ts:101
13
+ */
14
+ export const PROVIDERS = [
15
+ {
16
+ id: "anthropic",
17
+ name: "Anthropic",
18
+ description: "Claude — best for tool use & long context",
19
+ keyUrl: "https://console.anthropic.com/settings/keys",
20
+ envVar: "ANTHROPIC_API_KEY",
21
+ },
22
+ {
23
+ id: "openai",
24
+ name: "OpenAI",
25
+ description: "GPT-5, GPT-4o",
26
+ keyUrl: "https://platform.openai.com/api-keys",
27
+ envVar: "OPENAI_API_KEY",
28
+ },
29
+ {
30
+ id: "google",
31
+ name: "Google Gemini",
32
+ description: "Gemini 2.5 — generous free tier",
33
+ keyUrl: "https://aistudio.google.com/apikey",
34
+ envVar: "GEMINI_API_KEY",
35
+ },
36
+ {
37
+ id: "openrouter",
38
+ name: "OpenRouter",
39
+ description: "300+ models from one key",
40
+ keyUrl: "https://openrouter.ai/settings/keys",
41
+ envVar: "OPENROUTER_API_KEY",
42
+ },
43
+ {
44
+ id: "groq",
45
+ name: "Groq",
46
+ description: "Very fast inference (Llama, Qwen, Kimi)",
47
+ keyUrl: "https://console.groq.com/keys",
48
+ envVar: "GROQ_API_KEY",
49
+ },
50
+ {
51
+ id: "cerebras",
52
+ name: "Cerebras",
53
+ description: "Extremely fast inference",
54
+ keyUrl: "https://cloud.cerebras.ai",
55
+ envVar: "CEREBRAS_API_KEY",
56
+ },
57
+ {
58
+ id: "xai",
59
+ name: "xAI",
60
+ description: "Grok models",
61
+ keyUrl: "https://console.x.ai",
62
+ envVar: "XAI_API_KEY",
63
+ },
64
+ {
65
+ id: "deepseek",
66
+ name: "DeepSeek",
67
+ description: "Cheap reasoning models",
68
+ keyUrl: "https://platform.deepseek.com/api_keys",
69
+ envVar: "DEEPSEEK_API_KEY",
70
+ },
71
+ {
72
+ id: "mistral",
73
+ name: "Mistral",
74
+ description: "European, strong open-weight roots",
75
+ keyUrl: "https://console.mistral.ai/api-keys",
76
+ envVar: "MISTRAL_API_KEY",
77
+ },
78
+ {
79
+ id: "ollama",
80
+ name: "Ollama (local)",
81
+ description: "Run models locally — no API key, fully private",
82
+ keyUrl: "https://ollama.com/download",
83
+ envVar: "", // no env key
84
+ noAuth: true,
85
+ local: true,
86
+ baseUrl: "http://localhost:11434",
87
+ },
88
+ {
89
+ id: "custom",
90
+ name: "Custom (OpenAI-compatible)",
91
+ description: "Together, Fireworks, vLLM, LM Studio, any /v1/chat/completions endpoint",
92
+ keyUrl: "—",
93
+ envVar: "",
94
+ custom: true,
95
+ },
96
+ ];
97
+ export function findProvider(id) {
98
+ return PROVIDERS.find((p) => p.id === id);
99
+ }
100
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Live API key validation against each provider's `/v1/models` endpoint.
3
+ *
4
+ * **Why this matters.** A format-only check (length, prefix, no whitespace)
5
+ * catches typos, but it cannot tell you whether the key is *active* — the user
6
+ * could have pasted a revoked, deleted, or rate-limited key. Until they fire a
7
+ * real chat turn, the bug stays hidden.
8
+ *
9
+ * **Why `/v1/models`.** It's the universally cheapest auth-checking endpoint:
10
+ * - Returns the catalogue of models the key can access
11
+ * - No tokens consumed, no billing event
12
+ * - Returns 200 quickly when auth works, 401/403 instantly when it doesn't
13
+ * - Same surface across every OpenAI-compatible provider (Groq, OpenRouter,
14
+ * xAI, Cerebras, DeepSeek, Mistral). Anthropic and Google have their own
15
+ * conventions, handled below.
16
+ *
17
+ * **Failure modes.**
18
+ * - 401/403 → key is invalid/revoked/wrong-provider. Hard reject.
19
+ * - 429 → rate limited (key probably valid). Show warning, accept anyway.
20
+ * - 5xx → provider-side outage. Soft accept.
21
+ * - timeout/network → no internet or DNS. Hard reject (the agent loop won't
22
+ * work either if the network is down).
23
+ */
24
+ const TIMEOUT_MS = 8000;
25
+ /**
26
+ * Build the validation request for a given provider.
27
+ * Returns `null` when we have no validation endpoint for this provider —
28
+ * caller should treat that as "skip online validation" (offline-only check).
29
+ */
30
+ function buildRequest(providerId, apiKey) {
31
+ switch (providerId) {
32
+ case "ollama":
33
+ // Ollama runs locally; `/api/tags` is auth-free and lists installed models.
34
+ return {
35
+ url: "http://localhost:11434/api/tags",
36
+ init: { method: "GET" },
37
+ };
38
+ case "anthropic":
39
+ return {
40
+ url: "https://api.anthropic.com/v1/models?limit=1",
41
+ init: {
42
+ method: "GET",
43
+ headers: {
44
+ "x-api-key": apiKey,
45
+ "anthropic-version": "2023-06-01",
46
+ },
47
+ },
48
+ };
49
+ case "openai":
50
+ return {
51
+ url: "https://api.openai.com/v1/models",
52
+ init: { method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
53
+ };
54
+ case "google":
55
+ // Google Gemini puts the key in the query string, no auth header.
56
+ return {
57
+ url: `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}&pageSize=1`,
58
+ init: { method: "GET" },
59
+ };
60
+ case "openrouter":
61
+ return {
62
+ url: "https://openrouter.ai/api/v1/auth/key", // returns key info; cheaper than /models
63
+ init: { method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
64
+ };
65
+ case "groq":
66
+ return {
67
+ url: "https://api.groq.com/openai/v1/models",
68
+ init: { method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
69
+ };
70
+ case "cerebras":
71
+ return {
72
+ url: "https://api.cerebras.ai/v1/models",
73
+ init: { method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
74
+ };
75
+ case "xai":
76
+ return {
77
+ url: "https://api.x.ai/v1/models",
78
+ init: { method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
79
+ };
80
+ case "deepseek":
81
+ return {
82
+ url: "https://api.deepseek.com/v1/models",
83
+ init: { method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
84
+ };
85
+ case "mistral":
86
+ return {
87
+ url: "https://api.mistral.ai/v1/models",
88
+ init: { method: "GET", headers: { Authorization: `Bearer ${apiKey}` } },
89
+ };
90
+ default:
91
+ return null;
92
+ }
93
+ }
94
+ /**
95
+ * Hit the provider's models endpoint with the supplied key and report back.
96
+ * Never throws — always returns a typed result.
97
+ */
98
+ export async function validateApiKeyOnline(providerId, apiKey) {
99
+ const request = buildRequest(providerId, apiKey);
100
+ if (!request) {
101
+ // Unknown provider: we can't validate online, but the format check above
102
+ // already passed, so let it through. The first chat turn will surface
103
+ // any auth issues with a real error message.
104
+ return { ok: true, warning: `No validation endpoint configured for "${providerId}" — will be tested on first message.` };
105
+ }
106
+ const controller = new AbortController();
107
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
108
+ try {
109
+ const response = await fetch(request.url, {
110
+ ...request.init,
111
+ signal: controller.signal,
112
+ });
113
+ // Try to count models from the body for friendlier success messaging.
114
+ // Best-effort only — don't fail validation if parsing fails.
115
+ let modelCount;
116
+ if (response.ok) {
117
+ try {
118
+ const body = (await response.json());
119
+ if (Array.isArray(body.data))
120
+ modelCount = body.data.length;
121
+ else if (Array.isArray(body.models))
122
+ modelCount = body.models.length;
123
+ }
124
+ catch {
125
+ /* parse failure is fine — auth still verified */
126
+ }
127
+ // Ollama-specific: server is reachable but has zero models pulled — surface
128
+ // that as a hard error so the user knows the next step.
129
+ if (providerId === "ollama" && modelCount === 0) {
130
+ return {
131
+ ok: false,
132
+ reason: `Ollama is running but no models are installed yet. Install one (for example: ollama pull llama3.2) and try again.`,
133
+ };
134
+ }
135
+ return modelCount === undefined ? { ok: true } : { ok: true, modelCount };
136
+ }
137
+ // Auth-style failures: hard reject. Use the human-friendly provider name
138
+ // (Anthropic / OpenAI / Google Gemini) rather than the internal id.
139
+ const providerName = providerDisplayName(providerId);
140
+ if (response.status === 401 || response.status === 403) {
141
+ return {
142
+ ok: false,
143
+ reason: `${providerName} didn't accept this key. Double-check that it's correct and active.`,
144
+ };
145
+ }
146
+ // Rate limited — key probably fine, just over quota right now.
147
+ if (response.status === 429) {
148
+ return { ok: true, warning: `${providerName} is busy right now — connecting anyway.` };
149
+ }
150
+ // 5xx — provider outage, not a key problem. Soft accept.
151
+ if (response.status >= 500) {
152
+ return { ok: true, warning: `${providerName} is having a temporary issue — connecting anyway.` };
153
+ }
154
+ // Anything else (404, 400, …) — surface the status and refuse.
155
+ return {
156
+ ok: false,
157
+ reason: `${providerName} couldn't be reached (status ${response.status}). The key may be incorrect.`,
158
+ };
159
+ }
160
+ catch (err) {
161
+ const providerName = providerDisplayName(providerId);
162
+ if (err instanceof Error && err.name === "AbortError") {
163
+ return {
164
+ ok: false,
165
+ reason: `Couldn't reach ${providerName} within ${TIMEOUT_MS / 1000} seconds. Check your internet connection.`,
166
+ };
167
+ }
168
+ return {
169
+ ok: false,
170
+ reason: `Couldn't reach ${providerName}: ${err instanceof Error ? err.message : String(err)}`,
171
+ };
172
+ }
173
+ finally {
174
+ clearTimeout(timer);
175
+ }
176
+ }
177
+ /**
178
+ * Map raw provider id to a friendly display name. Avoids surfacing raw ids
179
+ * like "openai" or "anthropic" in user-facing error text — enterprise users
180
+ * expect "OpenAI", "Anthropic", "Google Gemini" instead.
181
+ */
182
+ function providerDisplayName(providerId) {
183
+ const map = {
184
+ anthropic: "Anthropic",
185
+ openai: "OpenAI",
186
+ google: "Google Gemini",
187
+ openrouter: "OpenRouter",
188
+ groq: "Groq",
189
+ cerebras: "Cerebras",
190
+ xai: "xAI",
191
+ deepseek: "DeepSeek",
192
+ mistral: "Mistral",
193
+ ollama: "Ollama",
194
+ };
195
+ return map[providerId] ?? providerId;
196
+ }
197
+ //# sourceMappingURL=validate-key.js.map