From 688e3b21c557f51d025b307704dda179bc3f5b41 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 27 Feb 2026 22:31:39 +1300 Subject: [PATCH] feat: add Anthropic Claude LLM provider and Gemini 3.1 Pro model Add Anthropic as a third LLM provider with Claude Opus 4.6, Sonnet 4.5, and Haiku 4.5 models. Extend Google provider with Gemini 3.1 Pro. Includes full API route support with multimodal content blocks, provider settings UI, temperature clamping (Anthropic max 1.0), header builder plumbing, and comprehensive test coverage. Co-Authored-By: Claude Opus 4.6 --- .env.example | 4 + src/app/api/env-status/route.ts | 2 + src/app/api/llm/__tests__/route.test.ts | 286 ++++++++++++++++++ src/app/api/llm/route.ts | 102 +++++++ src/components/CostDialog.tsx | 3 + src/components/ProjectSetupModal.tsx | 67 +++- src/components/nodes/LLMGenerateNode.tsx | 22 +- .../__tests__/llmGenerateExecutor.test.ts | 1 + src/store/utils/buildApiHeaders.ts | 6 + src/store/utils/localStorage.ts | 1 + src/types/providers.ts | 10 +- 11 files changed, 493 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index f700a2cb..be2fd10e 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,10 @@ GEMINI_API_KEY=your_gemini_api_key_here # Get your API key from: https://platform.openai.com/api-keys OPENAI_API_KEY=your_openai_api_key_here +# Anthropic API Key (Optional - only needed if using Anthropic Claude LLM provider) +# Get your API key from: https://console.anthropic.com/settings/keys +ANTHROPIC_API_KEY=your_anthropic_api_key_here + # Replicate API Key (Optional - only needed if using Replicate models) # Get your API key from: https://replicate.com/account/api-tokens REPLICATE_API_KEY=your_replicate_api_key_here diff --git a/src/app/api/env-status/route.ts b/src/app/api/env-status/route.ts index 038c878c..798328cc 100644 --- a/src/app/api/env-status/route.ts +++ b/src/app/api/env-status/route.ts @@ -3,6 +3,7 @@ import { NextResponse } from "next/server"; export interface EnvStatusResponse { gemini: boolean; openai: boolean; + anthropic: boolean; replicate: boolean; fal: boolean; kie: boolean; @@ -14,6 +15,7 @@ export async function GET() { const status: EnvStatusResponse = { gemini: !!process.env.GEMINI_API_KEY, openai: !!process.env.OPENAI_API_KEY, + anthropic: !!process.env.ANTHROPIC_API_KEY, replicate: !!process.env.REPLICATE_API_KEY, fal: !!process.env.FAL_API_KEY, kie: !!process.env.KIE_API_KEY, diff --git a/src/app/api/llm/__tests__/route.test.ts b/src/app/api/llm/__tests__/route.test.ts index fcb6903f..c942b99b 100644 --- a/src/app/api/llm/__tests__/route.test.ts +++ b/src/app/api/llm/__tests__/route.test.ts @@ -583,4 +583,290 @@ describe("/api/llm route", () => { expect(data.error).toBe("No text in OpenAI response"); }); }); + + describe("Anthropic provider", () => { + beforeEach(() => { + global.fetch = mockFetch; + }); + + it("should generate text successfully with Anthropic", async () => { + process.env.ANTHROPIC_API_KEY = "test-anthropic-key"; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + content: [{ type: "text", text: "Anthropic response text" }], + }), + }); + + const request = createMockPostRequest({ + prompt: "Test prompt", + provider: "anthropic", + model: "claude-sonnet-4.5", + temperature: 0.7, + maxTokens: 1024, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(data.text).toBe("Anthropic response text"); + + // Verify fetch was called with correct parameters + expect(mockFetch).toHaveBeenCalledWith( + "https://api.anthropic.com/v1/messages", + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": "test-anthropic-key", + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify({ + model: "claude-sonnet-4-5-20250929", + messages: [{ role: "user", content: [{ type: "text", text: "Test prompt" }] }], + temperature: 0.7, + max_tokens: 1024, + }), + }) + ); + }); + + it("should handle multimodal input with Anthropic content block structure", async () => { + process.env.ANTHROPIC_API_KEY = "test-anthropic-key"; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + content: [{ type: "text", text: "Image description from Claude" }], + }), + }); + + const request = createMockPostRequest({ + prompt: "Describe this image", + images: ["data:image/png;base64,iVBORw0KGgo="], + provider: "anthropic", + model: "claude-sonnet-4.5", + temperature: 0.7, + maxTokens: 1024, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(data.text).toBe("Image description from Claude"); + + // Verify Anthropic content block structure + expect(mockFetch).toHaveBeenCalledWith( + "https://api.anthropic.com/v1/messages", + expect.objectContaining({ + body: JSON.stringify({ + model: "claude-sonnet-4-5-20250929", + messages: [ + { + role: "user", + content: [ + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "iVBORw0KGgo=" }, + }, + { type: "text", text: "Describe this image" }, + ], + }, + ], + temperature: 0.7, + max_tokens: 1024, + }), + }) + ); + }); + + it("should reject missing Anthropic API key", async () => { + delete process.env.ANTHROPIC_API_KEY; + + const request = createMockPostRequest({ + prompt: "Test prompt", + provider: "anthropic", + model: "claude-sonnet-4.5", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.success).toBe(false); + expect(data.error).toContain("ANTHROPIC_API_KEY not configured"); + }); + + it("should use X-Anthropic-API-Key header over env var", async () => { + process.env.ANTHROPIC_API_KEY = "env-anthropic-key"; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + content: [{ type: "text", text: "Response with header key" }], + }), + }); + + const request = createMockPostRequest( + { + prompt: "Test prompt", + provider: "anthropic", + model: "claude-sonnet-4.5", + }, + { "X-Anthropic-API-Key": "header-anthropic-key" } + ); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + + // Verify fetch was called with header key (takes precedence) + expect(mockFetch).toHaveBeenCalledWith( + "https://api.anthropic.com/v1/messages", + expect.objectContaining({ + headers: { + "Content-Type": "application/json", + "x-api-key": "header-anthropic-key", + "anthropic-version": "2023-06-01", + }, + }) + ); + }); + + it("should return 429 on rate limit errors", async () => { + process.env.ANTHROPIC_API_KEY = "test-anthropic-key"; + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 429, + json: () => + Promise.resolve({ + error: { message: "429 Rate limit exceeded" }, + }), + }); + + const request = createMockPostRequest({ + prompt: "Test prompt", + provider: "anthropic", + model: "claude-sonnet-4.5", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(429); + expect(data.success).toBe(false); + expect(data.error).toBe("Rate limit reached. Please wait and try again."); + }); + + it("should handle Anthropic API error responses", async () => { + process.env.ANTHROPIC_API_KEY = "test-anthropic-key"; + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 401, + json: () => + Promise.resolve({ + error: { message: "Invalid API key" }, + }), + }); + + const request = createMockPostRequest({ + prompt: "Test prompt", + provider: "anthropic", + model: "claude-sonnet-4.5", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.success).toBe(false); + expect(data.error).toBe("Invalid API key"); + }); + + it("should handle no text in Anthropic response", async () => { + process.env.ANTHROPIC_API_KEY = "test-anthropic-key"; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + content: [], + }), + }); + + const request = createMockPostRequest({ + prompt: "Test prompt", + provider: "anthropic", + model: "claude-sonnet-4.5", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.success).toBe(false); + expect(data.error).toBe("No text in Anthropic response"); + }); + + it("should handle image without data URL prefix", async () => { + process.env.ANTHROPIC_API_KEY = "test-anthropic-key"; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + content: [{ type: "text", text: "Image description" }], + }), + }); + + const request = createMockPostRequest({ + prompt: "Describe this", + images: ["iVBORw0KGgoAAAANSUhEUgAAAAUA"], + provider: "anthropic", + model: "claude-sonnet-4.5", + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + + // Verify fallback to PNG mime type + expect(mockFetch).toHaveBeenCalledWith( + "https://api.anthropic.com/v1/messages", + expect.objectContaining({ + body: JSON.stringify({ + model: "claude-sonnet-4-5-20250929", + messages: [ + { + role: "user", + content: [ + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "iVBORw0KGgoAAAANSUhEUgAAAAUA" }, + }, + { type: "text", text: "Describe this" }, + ], + }, + ], + temperature: 0.7, + max_tokens: 1024, + }), + }) + ); + }); + }); }); diff --git a/src/app/api/llm/route.ts b/src/app/api/llm/route.ts index 411495a8..260614ef 100644 --- a/src/app/api/llm/route.ts +++ b/src/app/api/llm/route.ts @@ -15,6 +15,7 @@ const GOOGLE_MODEL_MAP: Record = { "gemini-2.5-flash": "gemini-2.5-flash", "gemini-3-flash-preview": "gemini-3-flash-preview", "gemini-3-pro-preview": "gemini-3-pro-preview", + "gemini-3.1-pro-preview": "gemini-3.1-pro-preview", }; const OPENAI_MODEL_MAP: Record = { @@ -22,6 +23,12 @@ const OPENAI_MODEL_MAP: Record = { "gpt-4.1-nano": "gpt-4.1-nano", }; +const ANTHROPIC_MODEL_MAP: Record = { + "claude-sonnet-4.5": "claude-sonnet-4-5-20250929", + "claude-haiku-4.5": "claude-haiku-4-5-20251001", + "claude-opus-4.6": "claude-opus-4-6", +}; + async function generateWithGoogle( prompt: string, model: LLMModelType, @@ -190,6 +197,98 @@ async function generateWithOpenAI( return text; } +async function generateWithAnthropic( + prompt: string, + model: LLMModelType, + temperature: number, + maxTokens: number, + images?: string[], + requestId?: string, + userApiKey?: string | null +): Promise { + const apiKey = userApiKey || process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + logger.error('api.error', 'ANTHROPIC_API_KEY not configured', { requestId }); + throw new Error("ANTHROPIC_API_KEY not configured. Add it to .env.local or configure in Settings."); + } + + const modelId = ANTHROPIC_MODEL_MAP[model]; + + logger.info('api.llm', 'Calling Anthropic API', { + requestId, + model: modelId, + temperature, + maxTokens, + imageCount: images?.length || 0, + promptLength: prompt.length, + }); + + // Build content blocks + const content: Array<{ type: string; text?: string; source?: { type: string; media_type: string; data: string } }> = []; + + if (images && images.length > 0) { + for (const img of images) { + const matches = img.match(/^data:(.+?);base64,(.+)$/); + if (matches) { + content.push({ + type: "image", + source: { type: "base64", media_type: matches[1], data: matches[2] }, + }); + } else { + content.push({ + type: "image", + source: { type: "base64", media_type: "image/png", data: img }, + }); + } + } + } + + content.push({ type: "text", text: prompt }); + + const startTime = Date.now(); + const response = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify({ + model: modelId, + messages: [{ role: "user", content }], + temperature, + max_tokens: maxTokens, + }), + }); + const duration = Date.now() - startTime; + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + logger.error('api.error', 'Anthropic API request failed', { + requestId, + status: response.status, + error: error.error?.message, + }); + throw new Error(error.error?.message || `Anthropic API error: ${response.status}`); + } + + const data = await response.json(); + const text = data.content?.[0]?.text; + + if (!text) { + logger.error('api.error', 'No text in Anthropic response', { requestId }); + throw new Error("No text in Anthropic response"); + } + + logger.info('api.llm', 'Anthropic API response received', { + requestId, + duration, + responseLength: text.length, + }); + + return text; +} + export async function POST(request: NextRequest) { const requestId = generateRequestId(); @@ -197,6 +296,7 @@ export async function POST(request: NextRequest) { // Get user-provided API keys from headers (override env variables) const geminiApiKey = request.headers.get("X-Gemini-API-Key"); const openaiApiKey = request.headers.get("X-OpenAI-API-Key"); + const anthropicApiKey = request.headers.get("X-Anthropic-API-Key"); const body: LLMGenerateRequest = await request.json(); const { @@ -233,6 +333,8 @@ export async function POST(request: NextRequest) { text = await generateWithGoogle(prompt, model, temperature, maxTokens, images, requestId, geminiApiKey); } else if (provider === "openai") { text = await generateWithOpenAI(prompt, model, temperature, maxTokens, images, requestId, openaiApiKey); + } else if (provider === "anthropic") { + text = await generateWithAnthropic(prompt, model, temperature, maxTokens, images, requestId, anthropicApiKey); } else { logger.warn('api.llm', 'Unknown provider requested', { requestId, provider }); return NextResponse.json( diff --git a/src/components/CostDialog.tsx b/src/components/CostDialog.tsx index 5716df77..28a84d77 100644 --- a/src/components/CostDialog.tsx +++ b/src/components/CostDialog.tsx @@ -20,6 +20,7 @@ function ProviderIcon({ provider }: { provider: ProviderType }) { fal: { bg: "bg-purple-500/20", text: "text-purple-300" }, replicate: { bg: "bg-blue-500/20", text: "text-blue-300" }, openai: { bg: "bg-teal-500/20", text: "text-teal-300" }, + anthropic: { bg: "bg-amber-500/20", text: "text-amber-300" }, kie: { bg: "bg-orange-500/20", text: "text-orange-300" }, wavespeed: { bg: "bg-purple-500/20", text: "text-purple-300" }, }; @@ -29,6 +30,7 @@ function ProviderIcon({ provider }: { provider: ProviderType }) { fal: "f", replicate: "R", openai: "O", + anthropic: "A", kie: "K", wavespeed: "W", }; @@ -51,6 +53,7 @@ function getProviderDisplayName(provider: ProviderType): string { fal: "fal.ai", replicate: "Replicate", openai: "OpenAI", + anthropic: "Anthropic", kie: "Kie.ai", wavespeed: "WaveSpeed", }; diff --git a/src/components/ProjectSetupModal.tsx b/src/components/ProjectSetupModal.tsx index 3e680d04..bf081d99 100644 --- a/src/components/ProjectSetupModal.tsx +++ b/src/components/ProjectSetupModal.tsx @@ -13,6 +13,7 @@ import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [ { value: "google", label: "Google" }, { value: "openai", label: "OpenAI" }, + { value: "anthropic", label: "Anthropic" }, ]; const LLM_MODELS: Record = { @@ -20,11 +21,17 @@ const LLM_MODELS: Record { value: "gemini-3-flash-preview", label: "Gemini 3 Flash" }, { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }, { value: "gemini-3-pro-preview", label: "Gemini 3.0 Pro" }, + { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro" }, ], openai: [ { value: "gpt-4.1-mini", label: "GPT-4.1 Mini" }, { value: "gpt-4.1-nano", label: "GPT-4.1 Nano" }, ], + anthropic: [ + { value: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" }, + { value: "claude-haiku-4.5", label: "Claude Haiku 4.5" }, + { value: "claude-opus-4.6", label: "Claude Opus 4.6" }, + ], }; // Provider icons @@ -149,6 +156,7 @@ export function ProjectSetupModal({ const [showApiKey, setShowApiKey] = useState>({ gemini: false, openai: false, + anthropic: false, replicate: false, fal: false, kie: false, @@ -157,6 +165,7 @@ export function ProjectSetupModal({ const [overrideActive, setOverrideActive] = useState>({ gemini: false, openai: false, + anthropic: false, replicate: false, fal: false, kie: false, @@ -192,11 +201,12 @@ export function ProjectSetupModal({ // Sync local providers state setLocalProviders(providerSettings); - setShowApiKey({ gemini: false, openai: false, replicate: false, fal: false, kie: false, wavespeed: false }); + setShowApiKey({ gemini: false, openai: false, anthropic: false, replicate: false, fal: false, kie: false, wavespeed: false }); // Initialize override as active if user already has a key set setOverrideActive({ gemini: !!providerSettings.providers.gemini?.apiKey, openai: !!providerSettings.providers.openai?.apiKey, + anthropic: !!providerSettings.providers.anthropic?.apiKey, replicate: !!providerSettings.providers.replicate?.apiKey, fal: !!providerSettings.providers.fal?.apiKey, kie: !!providerSettings.providers.kie?.apiKey, @@ -298,7 +308,7 @@ export function ProjectSetupModal({ const handleSaveProviders = () => { // Save each provider's settings - const providerIds: ProviderType[] = ["gemini", "openai", "replicate", "fal", "kie", "wavespeed"]; + const providerIds: ProviderType[] = ["gemini", "openai", "anthropic", "replicate", "fal", "kie", "wavespeed"]; for (const providerId of providerIds) { const local = localProviders.providers[providerId]; const current = providerSettings.providers[providerId]; @@ -572,6 +582,54 @@ export function ProjectSetupModal({ + {/* Anthropic Provider */} +
+
+ Anthropic + {envStatus?.anthropic && !overrideActive.anthropic ? ( +
+ Configured via .env + +
+ ) : ( +
+ updateLocalProvider("anthropic", { apiKey: e.target.value || null })} + placeholder="sk-ant-..." + className="w-48 px-2 py-1 bg-neutral-800 border border-neutral-600 rounded text-neutral-100 text-xs focus:outline-none focus:border-neutral-500" + /> + + {envStatus?.anthropic && ( + + )} +
+ )} +
+
+ {/* Replicate Provider */}
@@ -904,12 +962,15 @@ export function ProjectSetupModal({ onChange={(e) => { const newProvider = e.target.value as LLMProvider; const firstModelForProvider = LLM_MODELS[newProvider][0].value; + const currentTemp = localNodeDefaults.llm?.temperature ?? 0.7; setLocalNodeDefaults(prev => ({ ...prev, llm: { ...prev.llm, provider: newProvider, model: firstModelForProvider, + // Clamp temperature for Anthropic (max 1.0) + ...(newProvider === "anthropic" && currentTemp > 1 ? { temperature: 1 } : {}), } })); }} @@ -948,7 +1009,7 @@ export function ProjectSetupModal({ { diff --git a/src/components/nodes/LLMGenerateNode.tsx b/src/components/nodes/LLMGenerateNode.tsx index 523cb5fe..fb9ad6d2 100644 --- a/src/components/nodes/LLMGenerateNode.tsx +++ b/src/components/nodes/LLMGenerateNode.tsx @@ -10,6 +10,7 @@ import { LLMGenerateNodeData, LLMProvider, LLMModelType } from "@/types"; const PROVIDERS: { value: LLMProvider; label: string }[] = [ { value: "google", label: "Google" }, { value: "openai", label: "OpenAI" }, + { value: "anthropic", label: "Anthropic" }, ]; const MODELS: Record = { @@ -17,11 +18,17 @@ const MODELS: Record = { { value: "gemini-3-flash-preview", label: "Gemini 3 Flash" }, { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }, { value: "gemini-3-pro-preview", label: "Gemini 3.0 Pro" }, + { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro" }, ], openai: [ { value: "gpt-4.1-mini", label: "GPT-4.1 Mini" }, { value: "gpt-4.1-nano", label: "GPT-4.1 Nano" }, ], + anthropic: [ + { value: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" }, + { value: "claude-haiku-4.5", label: "Claude Haiku 4.5" }, + { value: "claude-opus-4.6", label: "Claude Opus 4.6" }, + ], }; type LLMGenerateNodeType = Node; @@ -35,12 +42,17 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps) => { const newProvider = e.target.value as LLMProvider; const firstModelForProvider = MODELS[newProvider][0].value; - updateNodeData(id, { + const updates: Partial = { provider: newProvider, - model: firstModelForProvider - }); + model: firstModelForProvider, + }; + // Anthropic limits temperature to 0-1 + if (newProvider === "anthropic" && nodeData.temperature > 1) { + updates.temperature = 1; + } + updateNodeData(id, updates); }, - [id, updateNodeData] + [id, updateNodeData, nodeData.temperature] ); const handleModelChange = useCallback( @@ -266,7 +278,7 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps = { kie: "X-Kie-Key", wavespeed: "X-WaveSpeed-Key", openai: "X-OpenAI-API-Key", + anthropic: "X-Anthropic-API-Key", }; /** @@ -65,6 +66,11 @@ export function buildLlmHeaders( if (openaiConfig?.apiKey) { headers["X-OpenAI-API-Key"] = openaiConfig.apiKey; } + } else if (llmProvider === "anthropic") { + const anthropicConfig = providerSettings.providers.anthropic; + if (anthropicConfig?.apiKey) { + headers["X-Anthropic-API-Key"] = anthropicConfig.apiKey; + } } return headers; diff --git a/src/store/utils/localStorage.ts b/src/store/utils/localStorage.ts index 41165034..fb5ed9b9 100644 --- a/src/store/utils/localStorage.ts +++ b/src/store/utils/localStorage.ts @@ -45,6 +45,7 @@ export const defaultProviderSettings: ProviderSettings = { providers: { gemini: { id: "gemini", name: "Google Gemini", enabled: true, apiKey: null, apiKeyEnvVar: "GEMINI_API_KEY" }, openai: { id: "openai", name: "OpenAI", enabled: true, apiKey: null, apiKeyEnvVar: "OPENAI_API_KEY" }, + anthropic: { id: "anthropic", name: "Anthropic", enabled: true, apiKey: null, apiKeyEnvVar: "ANTHROPIC_API_KEY" }, replicate: { id: "replicate", name: "Replicate", enabled: false, apiKey: null, apiKeyEnvVar: "REPLICATE_API_KEY" }, fal: { id: "fal", name: "fal.ai", enabled: false, apiKey: null, apiKeyEnvVar: "FAL_API_KEY" }, kie: { id: "kie", name: "Kie.ai", enabled: false, apiKey: null, apiKeyEnvVar: "KIE_API_KEY" }, diff --git a/src/types/providers.ts b/src/types/providers.ts index bfc1d536..a249b5a8 100644 --- a/src/types/providers.ts +++ b/src/types/providers.ts @@ -6,7 +6,7 @@ */ // Provider Types for multi-provider support (image/video generation) -export type ProviderType = "gemini" | "openai" | "replicate" | "fal" | "kie" | "wavespeed"; +export type ProviderType = "gemini" | "openai" | "anthropic" | "replicate" | "fal" | "kie" | "wavespeed"; // Model pricing info (stored when model is selected) export interface SelectedModelPricing { @@ -36,15 +36,19 @@ export interface ProviderSettings { } // LLM Provider Options -export type LLMProvider = "google" | "openai"; +export type LLMProvider = "google" | "openai" | "anthropic"; // LLM Model Options export type LLMModelType = | "gemini-2.5-flash" | "gemini-3-flash-preview" | "gemini-3-pro-preview" + | "gemini-3.1-pro-preview" | "gpt-4.1-mini" - | "gpt-4.1-nano"; + | "gpt-4.1-nano" + | "claude-opus-4.6" + | "claude-sonnet-4.5" + | "claude-haiku-4.5"; // Recently used models tracking export interface RecentModel {