diff --git a/components/ApiKeyInput.tsx b/components/ApiKeyInput.tsx
index 2579abf..6ff9091 100644
--- a/components/ApiKeyInput.tsx
+++ b/components/ApiKeyInput.tsx
@@ -49,6 +49,15 @@ export function ApiKeyInput({
return false
}
+ // Validate key format - OpenRouter keys must start with "sk-or-v1-"
+ if (!keyToTest.startsWith('sk-or-v1-')) {
+ setTestResult({
+ isValid: false,
+ error: 'Invalid API key format. OpenRouter keys must start with "sk-or-v1-"'
+ })
+ return false
+ }
+
setIsTestingKey(true)
setTestResult(null)
onLoadingChange?.(true)
@@ -67,7 +76,7 @@ export function ApiKeyInput({
} else {
setTestResult({
isValid: false,
- error: 'Invalid API key'
+ error: 'Invalid API key - could not connect to OpenRouter. Please check your API key.'
})
// Clear invalid key from sessionStorage
clearAPIKey()
@@ -75,11 +84,26 @@ export function ApiKeyInput({
return false
}
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : 'Failed to test API key'
+ let errorMessage = 'Failed to test API key'
+
+ if (error instanceof Error) {
+ if (error.message.includes('401') || error.message.includes('403')) {
+ errorMessage = 'Invalid API key - authentication failed. Please check your OpenRouter API key.'
+ } else if (error.message.includes('429')) {
+ errorMessage = 'Rate limit exceeded. Please wait before trying again.'
+ } else if (error.message.includes('Network') || error.message.includes('fetch')) {
+ errorMessage = 'Network error - could not connect to OpenRouter. Please check your internet connection.'
+ } else {
+ errorMessage = `Validation failed: ${error.message}`
+ }
+ }
+
setTestResult({
isValid: false,
- error: `Validation failed: ${errorMessage}`
+ error: errorMessage
})
+ // Clear invalid key from sessionStorage
+ clearAPIKey()
onApiKeyValidated?.(false)
return false
} finally {
@@ -112,11 +136,11 @@ export function ApiKeyInput({
return
}
- // Validate key format
- if (!trimmedValue.startsWith('sk-or-') && !trimmedValue.startsWith('sk-')) {
+ // Validate key format - OpenRouter keys must start with "sk-or-v1-"
+ if (!trimmedValue.startsWith('sk-or-v1-')) {
setTestResult({
isValid: false,
- error: 'OpenRouter API keys should start with "sk-or-" or "sk-"'
+ error: 'OpenRouter API keys must start with "sk-or-v1-"'
})
return
}
@@ -146,11 +170,11 @@ export function ApiKeyInput({
return
}
- // Validate key format
- if (!keyToTest.startsWith('sk-or-') && !keyToTest.startsWith('sk-')) {
+ // Validate key format - OpenRouter keys must start with "sk-or-v1-"
+ if (!keyToTest.startsWith('sk-or-v1-')) {
setTestResult({
isValid: false,
- error: 'Invalid API key format'
+ error: 'Invalid API key format. OpenRouter keys must start with "sk-or-v1-"'
})
onApiKeyValidated?.(false, keyToTest)
return
@@ -216,7 +240,7 @@ export function ApiKeyInput({
([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
@@ -66,6 +66,13 @@ export function ModelSelector({
return
}
+ // Check if API key has valid format
+ if (!isApiKeyValidFormat) {
+ setError('Invalid API key format. OpenRouter API keys must start with "sk-or-v1-"')
+ onError?.()
+ return
+ }
+
setIsLoading(true)
setError(null)
onLoadingChange?.(true)
@@ -81,14 +88,27 @@ export function ModelSelector({
setModels(fetchedModels)
}
} catch (err) {
- const errorMessage = err instanceof Error ? err.message : 'Failed to load models'
+ let errorMessage = 'Failed to load models'
+
+ if (err instanceof Error) {
+ if (err.message.includes('401') || err.message.includes('403')) {
+ errorMessage = 'Invalid API key - authentication failed. Please check your OpenRouter API key.'
+ } else if (err.message.includes('429')) {
+ errorMessage = 'Rate limit exceeded. Please wait before trying again.'
+ } else if (err.message.includes('Network') || err.message.includes('fetch')) {
+ errorMessage = 'Network error - could not connect to OpenRouter. Please check your internet connection.'
+ } else {
+ errorMessage = err.message
+ }
+ }
+
setError(errorMessage)
onError?.()
} finally {
setIsLoading(false)
onLoadingChange?.(false)
}
- }, [apiKey])
+ }, [apiKey, isApiKeyValidFormat, onError, onLoadingChange])
// Load models when component mounts or API key changes
useEffect(() => {
diff --git a/hooks/useSimpleApiKeyStorage.ts b/hooks/useSimpleApiKeyStorage.ts
index 64fa9f7..ca409ce 100644
--- a/hooks/useSimpleApiKeyStorage.ts
+++ b/hooks/useSimpleApiKeyStorage.ts
@@ -77,13 +77,14 @@ export function useSimpleApiKeyStorage() {
}, [])
// Simple validation check
- const isValidFormat = apiKey ? apiKey.startsWith('sk-or-') || apiKey.startsWith('sk-') : false
+ const isValidFormat = apiKey ? apiKey.startsWith('sk-or-v1-') : false
const hasValidKey = Boolean(apiKey && isValidFormat && isValidated)
return {
value: apiKey,
hasValidKey,
setAPIKey,
- clearAPIKey
+ clearAPIKey,
+ isValidFormat
}
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index d99175d..8f31c53 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "openspec-temp",
- "version": "0.1.0",
+ "version": "0.5.5-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openspec-temp",
- "version": "0.1.0",
+ "version": "0.5.5-beta",
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
@@ -144,6 +144,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -794,6 +795,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -817,6 +819,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -2943,7 +2946,6 @@
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
@@ -3033,8 +3035,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -3508,6 +3509,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -3519,6 +3521,7 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -3623,6 +3626,7 @@
"integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.43.0",
"@typescript-eslint/types": "8.43.0",
@@ -4128,6 +4132,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4674,6 +4679,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.2",
"caniuse-lite": "^1.0.30001741",
@@ -4897,6 +4903,7 @@
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
@@ -5223,6 +5230,7 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10"
}
@@ -5632,6 +5640,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
+ "peer": true,
"engines": {
"node": ">=12"
}
@@ -5989,8 +5998,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/dompurify": {
"version": "3.2.6",
@@ -6278,6 +6286,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -6447,6 +6456,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -9353,6 +9363,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -9722,7 +9733,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -11502,6 +11512,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11623,7 +11634,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -11639,7 +11649,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -11652,8 +11661,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/prismjs": {
"version": "1.30.0",
@@ -11760,6 +11768,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -11772,6 +11781,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -13166,6 +13176,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -13354,6 +13365,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -13613,6 +13625,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"