{"repo": "aaronqlf/alfredo", "files": {"src/providers/index.ts": {"bf10fc0a895e8603aad43c64386f81c88b3ffef4": {"t": 1785745601.2904744, "n": 7775, "h": "3da7ffda94683825", "c": "import type { AlfredConfig, ProviderCredentials } from \"../config.js\";\nimport { AnthropicProvider } from \"./anthropic.js\";\nimport { OpenAICompatibleProvider } from \"./openai-compatible.js\";\nimport type { Provider } from \"./types.js\";\n\ninterface CompatPreset {\n baseURL?: string;\n keyEnv: string;\n}\n\n/** Built-in OpenAI-compatible providers. */\nconst COMPAT_PRESETS: Record = {\n openai: { keyEnv: \"OPENAI_API_KEY\" },\n google: {\n baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n keyEnv: \"GEMINI_API_KEY\",\n },\n openrouter: {\n baseURL: \"https://openrouter.ai/api/v1\",\n keyEnv: \"OPENROUTER_API_KEY\",\n },\n groq: { baseURL: \"https://api.groq.com/openai/v1\", keyEnv: \"GROQ_API_KEY\" },\n xai: { baseURL: \"https://api.x.ai/v1\", keyEnv: \"XAI_API_KEY\" },\n mistral: { baseURL: \"https://api.mistral.ai/v1\", keyEnv: \"MISTRAL_API_KEY\" },\n deepseek: {\n baseURL: \"https://api.deepseek.com/v1\",\n keyEnv: \"DEEPSEEK_API_KEY\",\n },\n together: {\n baseURL: \"https://api.together.xyz/v1\",\n keyEnv: \"TOGETHER_API_KEY\",\n },\n fireworks: {\n baseURL: \"https://api.fireworks.ai/inference/v1\",\n keyEnv: \"FIREWORKS_API_KEY\",\n },\n cerebras: {\n baseURL: \"https://api.cerebras.ai/v1\",\n keyEnv: \"CEREBRAS_API_KEY\",\n },\n ollama: { baseURL: \"http://localhost:11434/v1\", keyEnv: \"OLLAMA_API_KEY\" },\n lmstudio: { baseURL: \"http://localhost:1234/v1\", keyEnv: \"LMSTUDIO_API_KEY\" },\n};\n\nexport const DEFAULT_MODEL = \"anthropic/claude-opus-4-8\";\n\n/** Short names that expand to a full provider/model spec. */\nconst BUILTIN_ALIASES: Record = {\n opus: \"anthropic/claude-opus-4-8\",\n sonnet: \"anthropic/claude-sonnet-4-6\",\n haiku: \"anthropic/claude-haiku-4-5\",\n fable: \"anthropic/claude-fable-5\",\n gpt: \"openai/gpt-5\",\n \"gpt-5\": \"openai/gpt-5\",\n o3: \"openai/o3\",\n gemini: \"google/gemini-2.5-pro\",\n deepseek: \"deepseek/deepseek-chat\",\n};\n\n/**\n * Expand a model alias (\"opus\", \"sonnet\") to its full provider/model spec.\n * config.modelAliases override/extend the built-ins. A spec that already names\n * a provider (contains \"/\") or is unknown passes through unchanged.\n */\nexport function applyModelAlias(spec: string, config?: AlfredConfig): string {\n if (spec.includes(\"/\")) return spec;\n const aliases = { ...BUILTIN_ALIASES, ...(config?.modelAliases ?? {}) };\n return aliases[spec.toLowerCase()] ?? spec;\n}\n\nexport interface ResolvedProvider {\n provider: Provider;\n providerId: string;\n /** Default model for this provider, if one is configured. */\n defaultModel?: string;\n}\n\nexport interface ResolvedModel {\n provider: Provider;\n providerId: string;\n model: string;\n}\n\nfunction envName(providerId: string): string {\n return providerId.toUpperCase().replace(/[^A-Z0-9]/g, \"_\");\n}\n\n/** First value that is neither undefined nor empty — so apiKey:\"\" falls through. */\nfunction firstNonEmpty(...vals: (string | undefined)[]): string | undefined {\n for (const v of vals) if (v !== undefined && v !== \"\") return v;\n return undefined;\n}\n\nfunction isAnthropicProvider(id: string, type?: string): boolean {\n return type === \"anthropic\" || (id === \"anthropic\" && type !== \"openai\");\n}\n\n/**\n * Build a provider adapter for an id, drawing credentials (in priority order)\n * from the config `providers` map, then preset env vars / built-in base URLs,\n * then `ALFRED__BASE_URL` / `ALFRED__API_KEY` for custom endpoints.\n */\nexport function resolveProvider(\n providerId: string,\n config?: AlfredConfig,\n): ResolvedProvider {\n const id = providerId.toLowerCase();\n const cfg: ProviderCredentials | undefined = config?.providers?.[id];\n const preset = COMPAT_PRESETS[id];\n\n // Anthropic native: when the id is \"anthropic\", or config explicitly types it so.\n if (isAnthropicProvider(id, cfg?.type)) {\n const baseUrl = firstNonEmpty(cfg?.baseUrl);\n // A custom id typed \"anthropic\" with no baseUrl would silently hit the\n // official api.anthropic.com — almost never the intent. Require it.\n if (id !== \"anthropic\" && !baseUrl) {\n throw new Error(\n `Provider \"${id}\" is type \"anthropic\" but has no baseUrl. Set ` +\n `providers.${id}.baseUrl to your Anthropic-compatible endpoint, or use ` +\n `the built-in \"anthropic\" provider for the official API.`,\n );\n }\n return {\n provider: new AnthropicProvider({ apiKey: firstNonEmpty(cfg?.apiKey), baseURL: baseUrl }),\n providerId: id,\n defaultModel: cfg?.model,\n };\n }\n\n const baseURL = firstNonEmpty(\n cfg?.baseUrl,\n preset?.baseURL,\n process.env[`ALFRED_${envName(id)}_BASE_URL`],\n );\n const apiKey = firstNonEmpty(\n cfg?.apiKey,\n preset ? process.env[preset.keyEnv] : undefined,\n process.env[`ALFRED_${envName(id)}_API_KEY`],\n );\n\n if (!preset && !baseURL) {\n throw new Error(\n `Unknown provider \"${id}\". Built-ins: ${listProviders().join(\", \")}.\\n` +\n `Configure it under \"providers\" in ~/.alfred/config.json (baseUrl + apiKey), ` +\n `or set ALFRED_${envName(id)}_BASE_URL (and ALFRED_${envName(id)}_API_KEY).`,\n );\n }\n\n const keyEnvHint = preset?.keyEnv ?? `ALFRED_${envName(id)}_API_KEY`;\n return {\n provider: new OpenAICompatibleProvider(id, { apiKey, baseURL, keyEnvHint }),\n providerId: id,\n defaultModel: cfg?.model,\n };\n}\n\n/**\n * Resolve a \"provider/model\" string into a provider adapter + model id.\n * The model may itself contain slashes (e.g. \"openrouter/qwen/qwen3-coder\").\n */\nexport function resolveModel(rawSpec: string, config?: AlfredConfig): ResolvedModel {\n const spec = applyModelAlias(rawSpec, config);\n const slash = spec.indexOf(\"/\");\n if (slash === -1) {\n throw new Error(\n `Model must be \"provider/model\" (e.g. \"${DEFAULT_MODEL}\"), got \"${spec}\".`,\n );\n }\n const providerId = spec.slice(0, slash).toLowerCase();\n const model = spec.slice(slash + 1);\n if (!model) throw new Error(`Missing model name in \"${spec}\".`);\n const resolved = resolveProvider(providerId, config);\n return { provider: resolved.provider, providerId: resolved.providerId, model };\n}\n\n/** All known provider ids: built-ins plus any defined under config.providers. */\nexport function listProviders(config?: AlfredConfig): string[] {\n const ids = new Set([\"anthropic\", ...Object.keys(COMPAT_PRESETS)]);\n for (const id of Object.keys(config?.providers ?? {})) ids.add(id.toLowerCase());\n return [...ids];\n}\n\n/**\n * True if a provider has usable credentials right now (drives the ● / ○ mark\n * in the picker). Mirrors resolveProvider so the indicator never disagrees with\n * what actually happens on switch. (An `ant auth login` profile can't be\n * detected here, so anthropic may show ○ yet still work — the safe direction.)\n */\nexport function providerIsConfigured(providerId: string, config?: AlfredConfig): boolean {\n const id = providerId.toLowerCase();\n const cfg = config?.providers?.[id];\n\n if (isAnthropicProvider(id, cfg?.type)) {\n // A custom anthropic-typed id needs a baseUrl or resolveProvider throws.\n if (id !== \"anthropic\" && !firstNonEmpty(cfg?.baseUrl)) return false;\n if (firstNonEmpty(cfg?.apiKey)) return true;\n return Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN);\n }\n\n const preset = COMPAT_PRESETS[id];\n const baseURL = firstNonEmpty(\n cfg?.baseUrl,\n preset?.baseURL,\n process.env[`ALFRED_${envName(id)}_BASE_URL`],\n );\n if (!preset && !baseURL) return false; // resolveProvider would throw\n if (baseURL && (baseURL.includes(\"localhost\") || baseURL.includes(\"127.0.0.1\"))) {\n return true; // local endpoints need no key\n }\n return Boolean(\n firstNonEmpty(\n cfg?.apiKey,\n preset ? process.env[preset.keyEnv] : undefined,\n process.env[`ALFRED_${envName(id)}_API_KEY`],\n ),\n );\n}\n"}, "1007cd268f29dd7b2cd494146885d5553de029b3": {"t": 1785754664.3869383, "n": 7775, "h": "3da7ffda94683825", "c": "import type { AlfredConfig, ProviderCredentials } from \"../config.js\";\nimport { AnthropicProvider } from \"./anthropic.js\";\nimport { OpenAICompatibleProvider } from \"./openai-compatible.js\";\nimport type { Provider } from \"./types.js\";\n\ninterface CompatPreset {\n baseURL?: string;\n keyEnv: string;\n}\n\n/** Built-in OpenAI-compatible providers. */\nconst COMPAT_PRESETS: Record = {\n openai: { keyEnv: \"OPENAI_API_KEY\" },\n google: {\n baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n keyEnv: \"GEMINI_API_KEY\",\n },\n openrouter: {\n baseURL: \"https://openrouter.ai/api/v1\",\n keyEnv: \"OPENROUTER_API_KEY\",\n },\n groq: { baseURL: \"https://api.groq.com/openai/v1\", keyEnv: \"GROQ_API_KEY\" },\n xai: { baseURL: \"https://api.x.ai/v1\", keyEnv: \"XAI_API_KEY\" },\n mistral: { baseURL: \"https://api.mistral.ai/v1\", keyEnv: \"MISTRAL_API_KEY\" },\n deepseek: {\n baseURL: \"https://api.deepseek.com/v1\",\n keyEnv: \"DEEPSEEK_API_KEY\",\n },\n together: {\n baseURL: \"https://api.together.xyz/v1\",\n keyEnv: \"TOGETHER_API_KEY\",\n },\n fireworks: {\n baseURL: \"https://api.fireworks.ai/inference/v1\",\n keyEnv: \"FIREWORKS_API_KEY\",\n },\n cerebras: {\n baseURL: \"https://api.cerebras.ai/v1\",\n keyEnv: \"CEREBRAS_API_KEY\",\n },\n ollama: { baseURL: \"http://localhost:11434/v1\", keyEnv: \"OLLAMA_API_KEY\" },\n lmstudio: { baseURL: \"http://localhost:1234/v1\", keyEnv: \"LMSTUDIO_API_KEY\" },\n};\n\nexport const DEFAULT_MODEL = \"anthropic/claude-opus-4-8\";\n\n/** Short names that expand to a full provider/model spec. */\nconst BUILTIN_ALIASES: Record = {\n opus: \"anthropic/claude-opus-4-8\",\n sonnet: \"anthropic/claude-sonnet-4-6\",\n haiku: \"anthropic/claude-haiku-4-5\",\n fable: \"anthropic/claude-fable-5\",\n gpt: \"openai/gpt-5\",\n \"gpt-5\": \"openai/gpt-5\",\n o3: \"openai/o3\",\n gemini: \"google/gemini-2.5-pro\",\n deepseek: \"deepseek/deepseek-chat\",\n};\n\n/**\n * Expand a model alias (\"opus\", \"sonnet\") to its full provider/model spec.\n * config.modelAliases override/extend the built-ins. A spec that already names\n * a provider (contains \"/\") or is unknown passes through unchanged.\n */\nexport function applyModelAlias(spec: string, config?: AlfredConfig): string {\n if (spec.includes(\"/\")) return spec;\n const aliases = { ...BUILTIN_ALIASES, ...(config?.modelAliases ?? {}) };\n return aliases[spec.toLowerCase()] ?? spec;\n}\n\nexport interface ResolvedProvider {\n provider: Provider;\n providerId: string;\n /** Default model for this provider, if one is configured. */\n defaultModel?: string;\n}\n\nexport interface ResolvedModel {\n provider: Provider;\n providerId: string;\n model: string;\n}\n\nfunction envName(providerId: string): string {\n return providerId.toUpperCase().replace(/[^A-Z0-9]/g, \"_\");\n}\n\n/** First value that is neither undefined nor empty — so apiKey:\"\" falls through. */\nfunction firstNonEmpty(...vals: (string | undefined)[]): string | undefined {\n for (const v of vals) if (v !== undefined && v !== \"\") return v;\n return undefined;\n}\n\nfunction isAnthropicProvider(id: string, type?: string): boolean {\n return type === \"anthropic\" || (id === \"anthropic\" && type !== \"openai\");\n}\n\n/**\n * Build a provider adapter for an id, drawing credentials (in priority order)\n * from the config `providers` map, then preset env vars / built-in base URLs,\n * then `ALFRED__BASE_URL` / `ALFRED__API_KEY` for custom endpoints.\n */\nexport function resolveProvider(\n providerId: string,\n config?: AlfredConfig,\n): ResolvedProvider {\n const id = providerId.toLowerCase();\n const cfg: ProviderCredentials | undefined = config?.providers?.[id];\n const preset = COMPAT_PRESETS[id];\n\n // Anthropic native: when the id is \"anthropic\", or config explicitly types it so.\n if (isAnthropicProvider(id, cfg?.type)) {\n const baseUrl = firstNonEmpty(cfg?.baseUrl);\n // A custom id typed \"anthropic\" with no baseUrl would silently hit the\n // official api.anthropic.com — almost never the intent. Require it.\n if (id !== \"anthropic\" && !baseUrl) {\n throw new Error(\n `Provider \"${id}\" is type \"anthropic\" but has no baseUrl. Set ` +\n `providers.${id}.baseUrl to your Anthropic-compatible endpoint, or use ` +\n `the built-in \"anthropic\" provider for the official API.`,\n );\n }\n return {\n provider: new AnthropicProvider({ apiKey: firstNonEmpty(cfg?.apiKey), baseURL: baseUrl }),\n providerId: id,\n defaultModel: cfg?.model,\n };\n }\n\n const baseURL = firstNonEmpty(\n cfg?.baseUrl,\n preset?.baseURL,\n process.env[`ALFRED_${envName(id)}_BASE_URL`],\n );\n const apiKey = firstNonEmpty(\n cfg?.apiKey,\n preset ? process.env[preset.keyEnv] : undefined,\n process.env[`ALFRED_${envName(id)}_API_KEY`],\n );\n\n if (!preset && !baseURL) {\n throw new Error(\n `Unknown provider \"${id}\". Built-ins: ${listProviders().join(\", \")}.\\n` +\n `Configure it under \"providers\" in ~/.alfred/config.json (baseUrl + apiKey), ` +\n `or set ALFRED_${envName(id)}_BASE_URL (and ALFRED_${envName(id)}_API_KEY).`,\n );\n }\n\n const keyEnvHint = preset?.keyEnv ?? `ALFRED_${envName(id)}_API_KEY`;\n return {\n provider: new OpenAICompatibleProvider(id, { apiKey, baseURL, keyEnvHint }),\n providerId: id,\n defaultModel: cfg?.model,\n };\n}\n\n/**\n * Resolve a \"provider/model\" string into a provider adapter + model id.\n * The model may itself contain slashes (e.g. \"openrouter/qwen/qwen3-coder\").\n */\nexport function resolveModel(rawSpec: string, config?: AlfredConfig): ResolvedModel {\n const spec = applyModelAlias(rawSpec, config);\n const slash = spec.indexOf(\"/\");\n if (slash === -1) {\n throw new Error(\n `Model must be \"provider/model\" (e.g. \"${DEFAULT_MODEL}\"), got \"${spec}\".`,\n );\n }\n const providerId = spec.slice(0, slash).toLowerCase();\n const model = spec.slice(slash + 1);\n if (!model) throw new Error(`Missing model name in \"${spec}\".`);\n const resolved = resolveProvider(providerId, config);\n return { provider: resolved.provider, providerId: resolved.providerId, model };\n}\n\n/** All known provider ids: built-ins plus any defined under config.providers. */\nexport function listProviders(config?: AlfredConfig): string[] {\n const ids = new Set([\"anthropic\", ...Object.keys(COMPAT_PRESETS)]);\n for (const id of Object.keys(config?.providers ?? {})) ids.add(id.toLowerCase());\n return [...ids];\n}\n\n/**\n * True if a provider has usable credentials right now (drives the ● / ○ mark\n * in the picker). Mirrors resolveProvider so the indicator never disagrees with\n * what actually happens on switch. (An `ant auth login` profile can't be\n * detected here, so anthropic may show ○ yet still work — the safe direction.)\n */\nexport function providerIsConfigured(providerId: string, config?: AlfredConfig): boolean {\n const id = providerId.toLowerCase();\n const cfg = config?.providers?.[id];\n\n if (isAnthropicProvider(id, cfg?.type)) {\n // A custom anthropic-typed id needs a baseUrl or resolveProvider throws.\n if (id !== \"anthropic\" && !firstNonEmpty(cfg?.baseUrl)) return false;\n if (firstNonEmpty(cfg?.apiKey)) return true;\n return Boolean(process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN);\n }\n\n const preset = COMPAT_PRESETS[id];\n const baseURL = firstNonEmpty(\n cfg?.baseUrl,\n preset?.baseURL,\n process.env[`ALFRED_${envName(id)}_BASE_URL`],\n );\n if (!preset && !baseURL) return false; // resolveProvider would throw\n if (baseURL && (baseURL.includes(\"localhost\") || baseURL.includes(\"127.0.0.1\"))) {\n return true; // local endpoints need no key\n }\n return Boolean(\n firstNonEmpty(\n cfg?.apiKey,\n preset ? process.env[preset.keyEnv] : undefined,\n process.env[`ALFRED_${envName(id)}_API_KEY`],\n ),\n );\n}\n"}}}}