Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app/components/ProviderSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Button } from "@convex-dev/design-system";
import { useStore } from "@nanostores/react";
import { $selectedProvider, setProvider } from "../stores/providerStore";

export const ProviderSelector = () => {
const selectedProvider = useStore($selectedProvider);

return (
<div className="flex gap-2">
<Button
variant={selectedProvider === 'openai' ? 'primary' : 'secondary'}
onClick={() => setProvider('openai')}
>
OpenAI
</Button>
<Button
variant={selectedProvider === 'anthropic' ? 'primary' : 'secondary'}
onClick={() => setProvider('anthropic')}
>
Anthropic
</Button>
<Button
variant={selectedProvider === 'openrouter' ? 'primary' : 'secondary'}
onClick={() => setProvider('openrouter')}
>
OpenRouter
</Button>
</div>
);
};
33 changes: 33 additions & 0 deletions app/components/chat/ProviderSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// This is a new component to handle provider selection
// Based on existing patterns in the codebase

import { Button } from "@convex-dev/design-system";
import { useStore } from "@nanostores/react";
import { $selectedProvider, setProvider } from "../stores/providerStore";

export const ProviderSelector = () => {
const selectedProvider = useStore($selectedProvider);

return (
<div className="flex gap-2">
<Button
variant={selectedProvider === 'openai' ? 'primary' : 'secondary'}
onClick={() => setProvider('openai')}
>
OpenAI
</Button>
<Button
variant={selectedProvider === 'anthropic' ? 'primary' : 'secondary'}
onClick={() => setProvider('anthropic')}
>
Anthropic
</Button>
<Button
variant={selectedProvider === 'openrouter' ? 'primary' : 'secondary'}
onClick={() => setProvider('openrouter')}
>
OpenRouter
</Button>
</div>
);
};
18 changes: 18 additions & 0 deletions app/config/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const SUPPORTED_PROVIDERS = [
{
id: 'openai',
name: 'OpenAI',
apiKeyEnvVar: 'OPENAI_API_KEY',
},
{
id: 'anthropic',
name: 'Anthropic',
apiKeyEnvVar: 'ANTHROPIC_API_KEY',
},
{
id: 'openrouter',
name: 'OpenRouter',
apiKeyEnvVar: 'OPENROUTER_API_KEY',
isAggregator: true,
},
] as const;
49 changes: 20 additions & 29 deletions app/routes/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
import { useTeamsInitializer } from '~/lib/stores/startup/useTeamsInitializer';
import { ChefAuthProvider } from '~/components/chat/ChefAuthWrapper';
import { json } from '@vercel/remix';
import type { LoaderFunctionArgs, MetaFunction } from '@vercel/remix';
import { SettingsContent } from '~/components/SettingsContent.client';
import { ClientOnly } from 'remix-utils/client-only';

export const meta: MetaFunction = () => {
return [{ title: 'Settings | Chef' }];
};

export const loader = async (args: LoaderFunctionArgs) => {
const url = new URL(args.request.url);
let code: string | null = url.searchParams.get('code');
const state = url.searchParams.get('state');
// If state is also set, this is probably the GitHub OAuth login flow finishing.
// The code is probably not for us.
if (state) {
code = null;
}
return json({ code });
};

export default function Settings() {
useTeamsInitializer();
import { ApiKeyForm } from "../components/ApiKeyForm";
import { $apiKeys } from "../stores/apiKeyStore";

export const SettingsRoute = () => {
return (
<ChefAuthProvider redirectIfUnauthenticated={true}>
<ClientOnly>{() => <SettingsContent />}</ClientOnly>
</ChefAuthProvider>
<div>
<h2>API Keys</h2>
<ApiKeyForm
provider="openai"
label="OpenAI"
/>
<ApiKeyForm
provider="anthropic"
label="Anthropic"
/>
<ApiKeyForm
provider="openrouter"
providerName="OpenRouter"
label="OpenRouter"
/>
</div>
);
}
};
31 changes: 31 additions & 0 deletions app/services/openrouterService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createOpenRouter } from '@ai-sdk/openrouter';
import { ApiKeyManager } from '../utils/apiKeyManager';

export class OpenRouterService {
private apiKey: string;

constructor(apiKey: string) {
this.apiKey = apiKey;
}

async createCompletion(messages: any[]) {
const response = await fetch('https://openrouter.com/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: 'openrouter',
messages,
temperature: 0.7,
}),
});

if (!response.ok) {
throw new Error('OpenRouter API request failed');
}

return response.json();
}
}
7 changes: 7 additions & 0 deletions app/stores/providerStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { atom } from 'nanostores';

export const $selectedProvider = atom<'openai' | 'anthropic' | 'openrouter'>('openai');

export const setProvider = (provider: 'openai' | 'anthropic' | 'openrouter') => {
$selectedProvider.set(provider);
};