Skip to content

Commit 4c47d33

Browse files
committed
feat: add back grok 3 mini and API key management functionality
- Integrated the XAI provider into the application, allowing for model interactions. - Implemented an API Key Manager component for managing API keys for OpenAI, Anthropic, Groq, and XAI. - Updated the Chat Sidebar to include an option for accessing the API Key Manager. - Enhanced the providers.ts file to utilize the new XAI client and manage API keys securely.
1 parent 6adcd00 commit 4c47d33

File tree

6 files changed

+273
-19
lines changed

6 files changed

+273
-19
lines changed

ai/providers.ts

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import { openai } from "@ai-sdk/openai";
2-
import { google } from "@ai-sdk/google";
3-
import { groq } from "@ai-sdk/groq";
4-
import { customProvider, wrapLanguageModel, extractReasoningMiddleware } from "ai";
5-
import { anthropic } from "@ai-sdk/anthropic";
1+
import { createOpenAI } from "@ai-sdk/openai";
2+
import { createGroq } from "@ai-sdk/groq";
3+
import { createAnthropic } from "@ai-sdk/anthropic";
4+
import { createXai } from "@ai-sdk/xai";
5+
6+
import {
7+
customProvider,
8+
wrapLanguageModel,
9+
extractReasoningMiddleware
10+
} from "ai";
11+
612
export interface ModelInfo {
713
provider: string;
814
name: string;
@@ -15,26 +21,51 @@ const middleware = extractReasoningMiddleware({
1521
tagName: 'think',
1622
});
1723

24+
// Helper to get API keys from environment variables first, then localStorage
25+
const getApiKey = (key: string): string | undefined => {
26+
// Check for environment variables first
27+
if (process.env[key]) {
28+
return process.env[key] || undefined;
29+
}
30+
31+
// Fall back to localStorage if available
32+
if (typeof window !== 'undefined') {
33+
return window.localStorage.getItem(key) || undefined;
34+
}
35+
36+
return undefined;
37+
};
38+
39+
// Create provider instances with API keys from localStorage
40+
const openaiClient = createOpenAI({
41+
apiKey: getApiKey('OPENAI_API_KEY'),
42+
});
43+
44+
const anthropicClient = createAnthropic({
45+
apiKey: getApiKey('ANTHROPIC_API_KEY'),
46+
});
47+
48+
const groqClient = createGroq({
49+
apiKey: getApiKey('GROQ_API_KEY'),
50+
});
51+
52+
const xaiClient = createXai({
53+
apiKey: getApiKey('XAI_API_KEY'),
54+
});
55+
1856
const languageModels = {
19-
"gemini-2.5-flash": google("gemini-2.5-flash-preview-04-17"),
20-
"gpt-4.1-mini": openai("gpt-4.1-mini"),
21-
"claude-3-7-sonnet": anthropic('claude-3-7-sonnet-20250219'),
57+
"gpt-4.1-mini": openaiClient("gpt-4.1-mini"),
58+
"claude-3-7-sonnet": anthropicClient('claude-3-7-sonnet-20250219'),
2259
"qwen-qwq": wrapLanguageModel(
2360
{
24-
model: groq("qwen-qwq-32b"),
61+
model: groqClient("qwen-qwq-32b"),
2562
middleware
2663
}
2764
),
65+
"grok-3-mini": xaiClient("grok-3-mini-latest"),
2866
};
2967

3068
export const modelDetails: Record<keyof typeof languageModels, ModelInfo> = {
31-
"gemini-2.5-flash": {
32-
provider: "Google",
33-
name: "Gemini 2.5 Flash",
34-
description: "Latest version of Google's Gemini 2.5 Flash with strong reasoning and coding capabilities.",
35-
apiVersion: "gemini-2.5-flash-preview-04-17",
36-
capabilities: ["Balance", "Efficient", "Agentic"]
37-
},
3869
"gpt-4.1-mini": {
3970
provider: "OpenAI",
4071
name: "GPT-4.1 Mini",
@@ -56,8 +87,25 @@ export const modelDetails: Record<keyof typeof languageModels, ModelInfo> = {
5687
apiVersion: "qwen-qwq",
5788
capabilities: ["Reasoning", "Efficient", "Agentic"]
5889
},
90+
"grok-3-mini": {
91+
provider: "XAI",
92+
name: "Grok 3 Mini",
93+
description: "Latest version of XAI's Grok 3 Mini with strong reasoning and coding capabilities.",
94+
apiVersion: "grok-3-mini-latest",
95+
capabilities: ["Reasoning", "Efficient", "Agentic"]
96+
},
5997
};
6098

99+
// Update API keys when localStorage changes (for runtime updates)
100+
if (typeof window !== 'undefined') {
101+
window.addEventListener('storage', (event) => {
102+
// Reload the page if any API key changed to refresh the providers
103+
if (event.key?.includes('API_KEY')) {
104+
window.location.reload();
105+
}
106+
});
107+
}
108+
61109
export const model = customProvider({
62110
languageModels,
63111
});

components/api-key-manager.tsx

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { useState, useEffect } from "react";
2+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
3+
import { Button } from "@/components/ui/button";
4+
import { Input } from "@/components/ui/input";
5+
import { Label } from "@/components/ui/label";
6+
import { toast } from "sonner";
7+
8+
// API key configuration
9+
interface ApiKeyConfig {
10+
name: string;
11+
key: string;
12+
storageKey: string;
13+
label: string;
14+
placeholder: string;
15+
}
16+
17+
// Available API keys configuration
18+
const API_KEYS_CONFIG: ApiKeyConfig[] = [
19+
{
20+
name: "OpenAI",
21+
key: "openai",
22+
storageKey: "OPENAI_API_KEY",
23+
label: "OpenAI API Key",
24+
placeholder: "sk-..."
25+
},
26+
{
27+
name: "Anthropic",
28+
key: "anthropic",
29+
storageKey: "ANTHROPIC_API_KEY",
30+
label: "Anthropic API Key",
31+
placeholder: "sk-ant-..."
32+
},
33+
{
34+
name: "Groq",
35+
key: "groq",
36+
storageKey: "GROQ_API_KEY",
37+
label: "Groq API Key",
38+
placeholder: "gsk_..."
39+
},
40+
{
41+
name: "XAI",
42+
key: "xai",
43+
storageKey: "XAI_API_KEY",
44+
label: "XAI API Key",
45+
placeholder: "xai-..."
46+
}
47+
];
48+
49+
interface ApiKeyManagerProps {
50+
open: boolean;
51+
onOpenChange: (open: boolean) => void;
52+
}
53+
54+
export function ApiKeyManager({ open, onOpenChange }: ApiKeyManagerProps) {
55+
// State to store API keys
56+
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
57+
58+
// Load API keys from localStorage on initial mount
59+
useEffect(() => {
60+
const storedKeys: Record<string, string> = {};
61+
62+
API_KEYS_CONFIG.forEach(config => {
63+
const value = localStorage.getItem(config.storageKey);
64+
if (value) {
65+
storedKeys[config.key] = value;
66+
}
67+
});
68+
69+
setApiKeys(storedKeys);
70+
}, []);
71+
72+
// Update API key in state
73+
const handleApiKeyChange = (key: string, value: string) => {
74+
setApiKeys(prev => ({
75+
...prev,
76+
[key]: value
77+
}));
78+
};
79+
80+
// Save API keys to localStorage
81+
const handleSaveApiKeys = () => {
82+
try {
83+
API_KEYS_CONFIG.forEach(config => {
84+
const value = apiKeys[config.key];
85+
86+
if (value && value.trim()) {
87+
localStorage.setItem(config.storageKey, value.trim());
88+
} else {
89+
localStorage.removeItem(config.storageKey);
90+
}
91+
});
92+
93+
toast.success("API keys saved successfully");
94+
onOpenChange(false);
95+
} catch (error) {
96+
console.error("Error saving API keys:", error);
97+
toast.error("Failed to save API keys");
98+
}
99+
};
100+
101+
// Clear all API keys
102+
const handleClearApiKeys = () => {
103+
try {
104+
API_KEYS_CONFIG.forEach(config => {
105+
localStorage.removeItem(config.storageKey);
106+
});
107+
108+
setApiKeys({});
109+
toast.success("All API keys cleared");
110+
} catch (error) {
111+
console.error("Error clearing API keys:", error);
112+
toast.error("Failed to clear API keys");
113+
}
114+
};
115+
116+
return (
117+
<Dialog open={open} onOpenChange={onOpenChange}>
118+
<DialogContent className="sm:max-w-[500px]">
119+
<DialogHeader>
120+
<DialogTitle>API Key Settings</DialogTitle>
121+
<DialogDescription>
122+
Enter your own API keys for different AI providers. Keys are stored securely in your browser&apos;s local storage.
123+
</DialogDescription>
124+
</DialogHeader>
125+
126+
<div className="grid gap-4 py-4">
127+
{API_KEYS_CONFIG.map(config => (
128+
<div key={config.key} className="grid gap-2">
129+
<Label htmlFor={config.key}>{config.label}</Label>
130+
<Input
131+
id={config.key}
132+
type="password"
133+
value={apiKeys[config.key] || ""}
134+
onChange={(e) => handleApiKeyChange(config.key, e.target.value)}
135+
placeholder={config.placeholder}
136+
/>
137+
</div>
138+
))}
139+
</div>
140+
141+
<DialogFooter className="flex justify-between sm:justify-between">
142+
<Button
143+
variant="destructive"
144+
onClick={handleClearApiKeys}
145+
>
146+
Clear All Keys
147+
</Button>
148+
<div className="flex gap-2">
149+
<Button
150+
variant="outline"
151+
onClick={() => onOpenChange(false)}
152+
>
153+
Cancel
154+
</Button>
155+
<Button onClick={handleSaveApiKeys}>
156+
Save Keys
157+
</Button>
158+
</div>
159+
</DialogFooter>
160+
</DialogContent>
161+
</Dialog>
162+
);
163+
}

components/chat-sidebar.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useEffect } from "react";
44
import { useRouter, usePathname } from "next/navigation";
5-
import { MessageSquare, PlusCircle, Trash2, ServerIcon, Settings, Sparkles, ChevronsUpDown, Copy, Pencil, Github } from "lucide-react";
5+
import { MessageSquare, PlusCircle, Trash2, ServerIcon, Settings, Sparkles, ChevronsUpDown, Copy, Pencil, Github, Key } from "lucide-react";
66
import {
77
Sidebar,
88
SidebarContent,
@@ -23,6 +23,7 @@ import { Badge } from "@/components/ui/badge";
2323
import { toast } from "sonner";
2424
import Image from "next/image";
2525
import { MCPServerManager } from "./mcp-server-manager";
26+
import { ApiKeyManager } from "./api-key-manager";
2627
import { ThemeToggle } from "./theme-toggle";
2728
import { getUserId, updateUserId } from "@/lib/user-id";
2829
import { useChats } from "@/lib/hooks/use-chats";
@@ -57,6 +58,7 @@ export function ChatSidebar() {
5758
const pathname = usePathname();
5859
const [userId, setUserId] = useState<string>('');
5960
const [mcpSettingsOpen, setMcpSettingsOpen] = useState(false);
61+
const [apiKeySettingsOpen, setApiKeySettingsOpen] = useState(false);
6062
const { state } = useSidebar();
6163
const isCollapsed = state === "collapsed";
6264
const [editUserIdOpen, setEditUserIdOpen] = useState(false);
@@ -384,6 +386,13 @@ export function ChatSidebar() {
384386
<Settings className="mr-2 h-4 w-4 hover:text-sidebar-accent" />
385387
MCP Settings
386388
</DropdownMenuItem>
389+
<DropdownMenuItem onSelect={(e) => {
390+
e.preventDefault();
391+
setApiKeySettingsOpen(true);
392+
}}>
393+
<Key className="mr-2 h-4 w-4 hover:text-sidebar-accent" />
394+
API Keys
395+
</DropdownMenuItem>
387396
<DropdownMenuItem onSelect={(e) => {
388397
e.preventDefault();
389398
window.open("https://git.new/s-mcp", "_blank");
@@ -413,6 +422,11 @@ export function ChatSidebar() {
413422
open={mcpSettingsOpen}
414423
onOpenChange={setMcpSettingsOpen}
415424
/>
425+
426+
<ApiKeyManager
427+
open={apiKeySettingsOpen}
428+
onOpenChange={setApiKeySettingsOpen}
429+
/>
416430
</SidebarFooter>
417431

418432
<Dialog open={editUserIdOpen} onOpenChange={(open) => {

components/model-picker.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ export const ModelPicker = ({ selectedModel, setSelectedModel }: ModelPickerProp
3535
const getProviderIcon = (provider: string) => {
3636
switch (provider.toLowerCase()) {
3737
case 'anthropic':
38-
return <Sparkles className="h-3 w-3 text-yellow-500" />;
38+
return <Sparkles className="h-3 w-3 text-orange-600" />;
3939
case 'openai':
4040
return <Zap className="h-3 w-3 text-green-500" />;
4141
case 'google':
4242
return <Zap className="h-3 w-3 text-red-500" />;
4343
case 'groq':
4444
return <Sparkles className="h-3 w-3 text-blue-500" />;
45-
case 'cohere':
45+
case 'xai':
4646
return <Sparkles className="h-3 w-3 text-yellow-500" />;
4747
default:
4848
return <Info className="h-3 w-3 text-blue-500" />;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@ai-sdk/groq": "^1.2.8",
2020
"@ai-sdk/openai": "^1.3.16",
2121
"@ai-sdk/react": "^1.2.9",
22+
"@ai-sdk/xai": "^1.2.14",
2223
"@neondatabase/serverless": "^1.0.0",
2324
"@radix-ui/react-accordion": "^1.2.7",
2425
"@radix-ui/react-avatar": "^1.1.6",

0 commit comments

Comments
 (0)