Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
688e3b21c5
  1. 4
      .env.example
  2. 2
      src/app/api/env-status/route.ts
  3. 286
      src/app/api/llm/__tests__/route.test.ts
  4. 102
      src/app/api/llm/route.ts
  5. 3
      src/components/CostDialog.tsx
  6. 67
      src/components/ProjectSetupModal.tsx
  7. 22
      src/components/nodes/LLMGenerateNode.tsx
  8. 1
      src/store/execution/__tests__/llmGenerateExecutor.test.ts
  9. 6
      src/store/utils/buildApiHeaders.ts
  10. 1
      src/store/utils/localStorage.ts
  11. 10
      src/types/providers.ts

4
.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

2
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,

286
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,
}),
})
);
});
});
});

102
src/app/api/llm/route.ts

@ -15,6 +15,7 @@ const GOOGLE_MODEL_MAP: Record<string, string> = {
"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<string, string> = {
@ -22,6 +23,12 @@ const OPENAI_MODEL_MAP: Record<string, string> = {
"gpt-4.1-nano": "gpt-4.1-nano",
};
const ANTHROPIC_MODEL_MAP: Record<string, string> = {
"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<string> {
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<LLMGenerateResponse>(

3
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",
};

67
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<LLMProvider, { value: LLMModelType; label: string }[]> = {
@ -20,11 +21,17 @@ const LLM_MODELS: Record<LLMProvider, { value: LLMModelType; label: string }[]>
{ 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<Record<ProviderType, boolean>>({
gemini: false,
openai: false,
anthropic: false,
replicate: false,
fal: false,
kie: false,
@ -157,6 +165,7 @@ export function ProjectSetupModal({
const [overrideActive, setOverrideActive] = useState<Record<ProviderType, boolean>>({
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({
</div>
</div>
{/* Anthropic Provider */}
<div className="p-3 bg-neutral-900 rounded-lg border border-neutral-700">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-neutral-100">Anthropic</span>
{envStatus?.anthropic && !overrideActive.anthropic ? (
<div className="flex items-center gap-2">
<span className="text-xs text-green-400">Configured via .env</span>
<button
type="button"
onClick={() => setOverrideActive((prev) => ({ ...prev, anthropic: true }))}
className="px-2 py-1 text-xs text-neutral-400 hover:text-neutral-200 transition-colors"
>
Override
</button>
</div>
) : (
<div className="flex items-center gap-2">
<input
type={showApiKey.anthropic ? "text" : "password"}
value={localProviders.providers.anthropic?.apiKey || ""}
onChange={(e) => 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"
/>
<button
type="button"
onClick={() => setShowApiKey((prev) => ({ ...prev, anthropic: !prev.anthropic }))}
className="text-xs text-neutral-400 hover:text-neutral-200"
>
{showApiKey.anthropic ? "Hide" : "Show"}
</button>
{envStatus?.anthropic && (
<button
type="button"
onClick={() => {
setOverrideActive((prev) => ({ ...prev, anthropic: false }));
updateLocalProvider("anthropic", { apiKey: null });
}}
className="text-xs text-neutral-500 hover:text-neutral-300"
>
Cancel
</button>
)}
</div>
)}
</div>
</div>
{/* Replicate Provider */}
<div className="p-3 bg-neutral-900 rounded-lg border border-neutral-700">
<div className="flex items-center justify-between">
@ -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({
<input
type="range"
min="0"
max="2"
max={(localNodeDefaults.llm?.provider || "google") === "anthropic" ? "1" : "2"}
step="0.1"
value={localNodeDefaults.llm?.temperature ?? 0.7}
onChange={(e) => {

22
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<LLMProvider, { value: LLMModelType; label: string }[]> = {
@ -17,11 +18,17 @@ const MODELS: Record<LLMProvider, { value: LLMModelType; label: string }[]> = {
{ 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<LLMGenerateNodeData, "llmGenerate">;
@ -35,12 +42,17 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
(e: React.ChangeEvent<HTMLSelectElement>) => {
const newProvider = e.target.value as LLMProvider;
const firstModelForProvider = MODELS[newProvider][0].value;
updateNodeData(id, {
const updates: Partial<LLMGenerateNodeData> = {
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<LLMGenerateNod
<input
type="range"
min="0"
max="2"
max={provider === "anthropic" ? "1" : "2"}
step="0.1"
value={nodeData.temperature}
onChange={handleTemperatureChange}

1
src/store/execution/__tests__/llmGenerateExecutor.test.ts

@ -14,6 +14,7 @@ const defaultProviderSettings = {
kie: { apiKey: "" },
wavespeed: { apiKey: "" },
openai: { apiKey: "okey" },
anthropic: { apiKey: "" },
},
} as any;

6
src/store/utils/buildApiHeaders.ts

@ -17,6 +17,7 @@ const PROVIDER_HEADER_MAP: Record<ProviderType, string> = {
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;

1
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" },

10
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 {

Loading…
Cancel
Save