Skip to content
Merged
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
78 changes: 78 additions & 0 deletions app/api/api-keys/check/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { NextRequest, NextResponse } from 'next/server'
import { getUserApiKey } from '@/lib/api-keys/user-keys'

type Provider = 'openai' | 'gemini' | 'cursor' | 'anthropic' | 'aigateway'

// Map agents to their required providers
const AGENT_PROVIDER_MAP: Record<string, Provider> = {
claude: 'anthropic',
codex: 'aigateway', // Codex uses Vercel AI Gateway
cursor: 'cursor',
gemini: 'gemini',
opencode: 'openai', // OpenCode can use OpenAI or Anthropic, but primarily OpenAI
}

// Check if a model is an Anthropic model
function isAnthropicModel(model: string): boolean {
const anthropicPatterns = ['claude', 'sonnet', 'opus']
const lowerModel = model.toLowerCase()
return anthropicPatterns.some((pattern) => lowerModel.includes(pattern))
}

// Check if a model is an OpenAI model
function isOpenAIModel(model: string): boolean {
const openaiPatterns = ['gpt', 'openai']
const lowerModel = model.toLowerCase()
return openaiPatterns.some((pattern) => lowerModel.includes(pattern))
}

// Check if a model is a Gemini model
function isGeminiModel(model: string): boolean {
const geminiPatterns = ['gemini']
const lowerModel = model.toLowerCase()
return geminiPatterns.some((pattern) => lowerModel.includes(pattern))
}

export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const agent = searchParams.get('agent')
const model = searchParams.get('model')

if (!agent) {
return NextResponse.json({ error: 'Agent parameter is required' }, { status: 400 })
}

let provider = AGENT_PROVIDER_MAP[agent]
if (!provider) {
return NextResponse.json({ error: 'Invalid agent' }, { status: 400 })
}

// Override provider based on model for multi-provider agents
if (model && (agent === 'cursor' || agent === 'opencode')) {
if (isAnthropicModel(model)) {
provider = 'anthropic'
} else if (isGeminiModel(model)) {
provider = 'gemini'
} else if (isOpenAIModel(model)) {
// For OpenAI models, prefer AI Gateway if available, otherwise use OpenAI
provider = 'aigateway'
}
// For cursor with no recognizable pattern, keep the default 'cursor' provider
}

// Check if API key is available (either user's or system)
const apiKey = await getUserApiKey(provider)
const hasKey = !!apiKey

return NextResponse.json({
success: true,
hasKey,
provider,
agentName: agent.charAt(0).toUpperCase() + agent.slice(1),
})
} catch (error) {
console.error('Error checking API key:', error)
return NextResponse.json({ error: 'Failed to check API key' }, { status: 500 })
}
}
37 changes: 36 additions & 1 deletion components/task-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { Claude, Codex, Cursor, Gemini, OpenCode } from '@/components/logos'
import { setInstallDependencies, setMaxDuration } from '@/lib/utils/cookies'
import { useConnectors } from '@/components/connectors-provider'
import { ConnectorDialog } from '@/components/connectors/manage-connectors'
import { ApiKeysDialog } from '@/components/api-keys-dialog'
import { toast } from 'sonner'

interface GitHubRepo {
name: string
Expand Down Expand Up @@ -122,6 +124,7 @@ export function TaskForm({
const [maxDuration, setMaxDurationState] = useState(initialMaxDuration)
const [showOptionsDialog, setShowOptionsDialog] = useState(false)
const [showMcpServersDialog, setShowMcpServersDialog] = useState(false)
const [showApiKeysDialog, setShowApiKeysDialog] = useState(false)

// Connectors state
const { connectors } = useConnectors()
Expand Down Expand Up @@ -262,9 +265,40 @@ export function TaskForm({
fetchRepos()
}, [selectedOwner])

const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (prompt.trim() && selectedOwner && selectedRepo) {
// Check if API key is required and available for the selected agent and model
try {
const response = await fetch(`/api/api-keys/check?agent=${selectedAgent}&model=${selectedModel}`)
const data = await response.json()

if (!data.hasKey) {
// Show error message with provider name
const providerNames: Record<string, string> = {
anthropic: 'Anthropic',
openai: 'OpenAI',
cursor: 'Cursor',
gemini: 'Gemini',
aigateway: 'AI Gateway',
}
const providerName = providerNames[data.provider] || data.provider

toast.error(`${providerName} API key required`, {
description: `Please add your ${providerName} API key to use the ${data.agentName} agent with this model.`,
action: {
label: 'Add API Key',
onClick: () => setShowApiKeysDialog(true),
},
})
return
}
Comment on lines +273 to +295
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing response status check before accessing API response data, causing error responses to show "undefined API key required" instead of proper error messages.

View Details
📝 Patch Details
diff --git a/components/task-form.tsx b/components/task-form.tsx
index dfe5c76..bab118f 100644
--- a/components/task-form.tsx
+++ b/components/task-form.tsx
@@ -271,6 +271,15 @@ export function TaskForm({
       // Check if API key is required and available for the selected agent and model
       try {
         const response = await fetch(`/api/api-keys/check?agent=${selectedAgent}&model=${selectedModel}`)
+        
+        if (!response.ok) {
+          const error = await response.json()
+          toast.error('Failed to validate API key availability', {
+            description: error.error || 'An error occurred while checking API key availability',
+          })
+          return
+        }
+
         const data = await response.json()
 
         if (!data.hasKey) {

Analysis

Missing response status check in API key validation causes misleading error messages

What fails: components/task-form.tsx handleSubmit() calls response.json() without checking response.ok, causing error responses to be processed as valid data

How to reproduce:

# API returns error: { error: 'Invalid agent' } with status 400
# Current code accesses data.hasKey, data.provider, data.agentName without checking response.ok

Result: When API returns { error: 'Invalid agent' } (status 400), data.hasKey is undefined, so the code shows toast: "undefined API key required" with description "Please add your undefined API key to use the undefined agent with this model."

Expected: Should check response.ok first (following Fetch API pattern used in 21 other locations in codebase including line 251 of same file), show proper error message from API response

} catch (error) {
console.error('Error checking API key:', error)
toast.error('Failed to validate API key availability')
return
}

const selectedRepoData = repos.find((repo) => repo.name === selectedRepo)
if (selectedRepoData) {
// Clear the saved prompt since we're submitting it
Expand Down Expand Up @@ -529,6 +563,7 @@ export function TaskForm({
</form>

<ConnectorDialog open={showMcpServersDialog} onOpenChange={setShowMcpServersDialog} />
<ApiKeysDialog open={showApiKeysDialog} onOpenChange={setShowApiKeysDialog} />
</div>
)
}
Loading