Skip to content

Commit 97d88e9

Browse files
committed
require user api key if no system env var
1 parent d607d40 commit 97d88e9

File tree

2 files changed

+124
-1
lines changed

2 files changed

+124
-1
lines changed

app/api/api-keys/check/route.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { getUserApiKey } from '@/lib/api-keys/user-keys'
3+
4+
type Provider = 'openai' | 'gemini' | 'cursor' | 'anthropic' | 'aigateway'
5+
6+
// Map agents to their required providers
7+
const AGENT_PROVIDER_MAP: Record<string, Provider> = {
8+
claude: 'anthropic',
9+
codex: 'aigateway', // Codex uses Vercel AI Gateway
10+
cursor: 'cursor',
11+
gemini: 'gemini',
12+
opencode: 'openai', // OpenCode can use OpenAI or Anthropic, but primarily OpenAI
13+
}
14+
15+
// Check if a model is an Anthropic model
16+
function isAnthropicModel(model: string): boolean {
17+
const anthropicPatterns = [
18+
'claude',
19+
'sonnet',
20+
'opus',
21+
]
22+
const lowerModel = model.toLowerCase()
23+
return anthropicPatterns.some(pattern => lowerModel.includes(pattern))
24+
}
25+
26+
// Check if a model is an OpenAI model
27+
function isOpenAIModel(model: string): boolean {
28+
const openaiPatterns = [
29+
'gpt',
30+
'openai',
31+
]
32+
const lowerModel = model.toLowerCase()
33+
return openaiPatterns.some(pattern => lowerModel.includes(pattern))
34+
}
35+
36+
// Check if a model is a Gemini model
37+
function isGeminiModel(model: string): boolean {
38+
const geminiPatterns = [
39+
'gemini',
40+
]
41+
const lowerModel = model.toLowerCase()
42+
return geminiPatterns.some(pattern => lowerModel.includes(pattern))
43+
}
44+
45+
export async function GET(req: NextRequest) {
46+
try {
47+
const { searchParams } = new URL(req.url)
48+
const agent = searchParams.get('agent')
49+
const model = searchParams.get('model')
50+
51+
if (!agent) {
52+
return NextResponse.json({ error: 'Agent parameter is required' }, { status: 400 })
53+
}
54+
55+
let provider = AGENT_PROVIDER_MAP[agent]
56+
if (!provider) {
57+
return NextResponse.json({ error: 'Invalid agent' }, { status: 400 })
58+
}
59+
60+
// Override provider based on model for multi-provider agents
61+
if (model && (agent === 'cursor' || agent === 'opencode')) {
62+
if (isAnthropicModel(model)) {
63+
provider = 'anthropic'
64+
} else if (isGeminiModel(model)) {
65+
provider = 'gemini'
66+
} else if (isOpenAIModel(model)) {
67+
// For OpenAI models, prefer AI Gateway if available, otherwise use OpenAI
68+
provider = 'aigateway'
69+
}
70+
// For cursor with no recognizable pattern, keep the default 'cursor' provider
71+
}
72+
73+
// Check if API key is available (either user's or system)
74+
const apiKey = await getUserApiKey(provider)
75+
const hasKey = !!apiKey
76+
77+
return NextResponse.json({
78+
success: true,
79+
hasKey,
80+
provider,
81+
agentName: agent.charAt(0).toUpperCase() + agent.slice(1),
82+
})
83+
} catch (error) {
84+
console.error('Error checking API key:', error)
85+
return NextResponse.json({ error: 'Failed to check API key' }, { status: 500 })
86+
}
87+
}
88+

components/task-form.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { Claude, Codex, Cursor, Gemini, OpenCode } from '@/components/logos'
2121
import { setInstallDependencies, setMaxDuration } from '@/lib/utils/cookies'
2222
import { useConnectors } from '@/components/connectors-provider'
2323
import { ConnectorDialog } from '@/components/connectors/manage-connectors'
24+
import { ApiKeysDialog } from '@/components/api-keys-dialog'
25+
import { toast } from 'sonner'
2426

2527
interface GitHubRepo {
2628
name: string
@@ -122,6 +124,7 @@ export function TaskForm({
122124
const [maxDuration, setMaxDurationState] = useState(initialMaxDuration)
123125
const [showOptionsDialog, setShowOptionsDialog] = useState(false)
124126
const [showMcpServersDialog, setShowMcpServersDialog] = useState(false)
127+
const [showApiKeysDialog, setShowApiKeysDialog] = useState(false)
125128

126129
// Connectors state
127130
const { connectors } = useConnectors()
@@ -262,9 +265,40 @@ export function TaskForm({
262265
fetchRepos()
263266
}, [selectedOwner])
264267

265-
const handleSubmit = (e: React.FormEvent) => {
268+
const handleSubmit = async (e: React.FormEvent) => {
266269
e.preventDefault()
267270
if (prompt.trim() && selectedOwner && selectedRepo) {
271+
// Check if API key is required and available for the selected agent and model
272+
try {
273+
const response = await fetch(`/api/api-keys/check?agent=${selectedAgent}&model=${selectedModel}`)
274+
const data = await response.json()
275+
276+
if (!data.hasKey) {
277+
// Show error message with provider name
278+
const providerNames: Record<string, string> = {
279+
anthropic: 'Anthropic',
280+
openai: 'OpenAI',
281+
cursor: 'Cursor',
282+
gemini: 'Gemini',
283+
aigateway: 'AI Gateway',
284+
}
285+
const providerName = providerNames[data.provider] || data.provider
286+
287+
toast.error(`${providerName} API key required`, {
288+
description: `Please add your ${providerName} API key to use the ${data.agentName} agent with this model.`,
289+
action: {
290+
label: 'Add API Key',
291+
onClick: () => setShowApiKeysDialog(true),
292+
},
293+
})
294+
return
295+
}
296+
} catch (error) {
297+
console.error('Error checking API key:', error)
298+
toast.error('Failed to validate API key availability')
299+
return
300+
}
301+
268302
const selectedRepoData = repos.find((repo) => repo.name === selectedRepo)
269303
if (selectedRepoData) {
270304
// Clear the saved prompt since we're submitting it
@@ -529,6 +563,7 @@ export function TaskForm({
529563
</form>
530564

531565
<ConnectorDialog open={showMcpServersDialog} onOpenChange={setShowMcpServersDialog} />
566+
<ApiKeysDialog open={showApiKeysDialog} onOpenChange={setShowApiKeysDialog} />
532567
</div>
533568
)
534569
}

0 commit comments

Comments
 (0)