mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: resolve OpenRouter 401 authentication errors
- Add Authorization header with Bearer token to OpenRouter API requests - Include API key in getOpenRouterModels and getOpenRouterModelEndpoints functions - Add proper 401 error handling with descriptive error messages - Update GetModelsOptions type to include apiKey and baseUrl for OpenRouter - Add API key validation in OpenRouter handler constructor - Add comprehensive tests for authentication scenarios Fixes #6459
This commit is contained in:
parent
b71cd44c1c
commit
d186eb08de
6 changed files with 133 additions and 5 deletions
|
|
@ -68,6 +68,30 @@ describe("OpenRouterHandler", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should warn when API key is missing or invalid", () => {
|
||||
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
// Test with missing API key
|
||||
new OpenRouterHandler({})
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"OpenRouter API key is missing or invalid. This may cause authentication errors.",
|
||||
)
|
||||
|
||||
// Test with empty API key
|
||||
new OpenRouterHandler({ openRouterApiKey: "" })
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"OpenRouter API key is missing or invalid. This may cause authentication errors.",
|
||||
)
|
||||
|
||||
// Test with whitespace-only API key
|
||||
new OpenRouterHandler({ openRouterApiKey: " " })
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"OpenRouter API key is missing or invalid. This may cause authentication errors.",
|
||||
)
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
describe("fetchModel", () => {
|
||||
it("returns correct model info when options are provided", async () => {
|
||||
const handler = new OpenRouterHandler(mockOptions)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from "@roo-code/types"
|
||||
|
||||
import { getOpenRouterModelEndpoints, getOpenRouterModels } from "../openrouter"
|
||||
import axios from "axios"
|
||||
|
||||
nockBack.fixtures = path.join(__dirname, "fixtures")
|
||||
nockBack.setMode("lockdown")
|
||||
|
|
@ -250,5 +251,73 @@ describe("OpenRouter API", () => {
|
|||
|
||||
nockDone()
|
||||
})
|
||||
|
||||
it("should include authorization header when API key is provided", async () => {
|
||||
const mockAxiosGet = vi.spyOn(axios, "get").mockResolvedValue({
|
||||
data: { data: [] },
|
||||
})
|
||||
|
||||
await getOpenRouterModels({ openRouterApiKey: "test-api-key" })
|
||||
|
||||
expect(mockAxiosGet).toHaveBeenCalledWith("https://openrouter.ai/api/v1/models", {
|
||||
headers: { Authorization: "Bearer test-api-key" },
|
||||
})
|
||||
|
||||
mockAxiosGet.mockRestore()
|
||||
})
|
||||
|
||||
it("should not include authorization header when API key is not provided", async () => {
|
||||
const mockAxiosGet = vi.spyOn(axios, "get").mockResolvedValue({
|
||||
data: { data: [] },
|
||||
})
|
||||
|
||||
await getOpenRouterModels()
|
||||
|
||||
expect(mockAxiosGet).toHaveBeenCalledWith("https://openrouter.ai/api/v1/models", { headers: {} })
|
||||
|
||||
mockAxiosGet.mockRestore()
|
||||
})
|
||||
|
||||
it("should throw authentication error on 401 response", async () => {
|
||||
const mockAxiosGet = vi.spyOn(axios, "get").mockRejectedValue({
|
||||
isAxiosError: true,
|
||||
response: { status: 401 },
|
||||
})
|
||||
|
||||
await expect(getOpenRouterModels({ openRouterApiKey: "invalid-key" })).rejects.toThrow(
|
||||
"OpenRouter API authentication failed. Please check your API key.",
|
||||
)
|
||||
|
||||
mockAxiosGet.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOpenRouterModelEndpoints", () => {
|
||||
it("should include authorization header when API key is provided", async () => {
|
||||
const mockAxiosGet = vi.spyOn(axios, "get").mockResolvedValue({
|
||||
data: { data: { id: "test", name: "test", endpoints: [] } },
|
||||
})
|
||||
|
||||
await getOpenRouterModelEndpoints("test-model", { openRouterApiKey: "test-api-key" })
|
||||
|
||||
expect(mockAxiosGet).toHaveBeenCalledWith("https://openrouter.ai/api/v1/models/test-model/endpoints", {
|
||||
headers: { Authorization: "Bearer test-api-key" },
|
||||
})
|
||||
|
||||
mockAxiosGet.mockRestore()
|
||||
})
|
||||
|
||||
it("should throw authentication error on 401 response", async () => {
|
||||
const mockAxiosGet = vi.spyOn(axios, "get").mockRejectedValue({
|
||||
isAxiosError: true,
|
||||
response: { status: 401 },
|
||||
})
|
||||
|
||||
await expect(
|
||||
getOpenRouterModelEndpoints("test-model", { openRouterApiKey: "invalid-key" }),
|
||||
).rejects.toThrow("OpenRouter API authentication failed. Please check your API key.")
|
||||
|
||||
mockAxiosGet.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
|
|||
try {
|
||||
switch (provider) {
|
||||
case "openrouter":
|
||||
models = await getOpenRouterModels()
|
||||
models = await getOpenRouterModels({
|
||||
openRouterApiKey: options.apiKey,
|
||||
openRouterBaseUrl: options.baseUrl,
|
||||
})
|
||||
break
|
||||
case "requesty":
|
||||
// Requesty models endpoint requires an API key for per-user custom policies
|
||||
|
|
|
|||
|
|
@ -97,8 +97,14 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
|
|||
const models: Record<string, ModelInfo> = {}
|
||||
const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1"
|
||||
|
||||
// Prepare headers with API key if available
|
||||
const headers: Record<string, string> = {}
|
||||
if (options?.openRouterApiKey) {
|
||||
headers.Authorization = `Bearer ${options.openRouterApiKey}`
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get<OpenRouterModelsResponse>(`${baseURL}/models`)
|
||||
const response = await axios.get<OpenRouterModelsResponse>(`${baseURL}/models`, { headers })
|
||||
const result = openRouterModelsResponseSchema.safeParse(response.data)
|
||||
const data = result.success ? result.data.data : response.data.data
|
||||
|
||||
|
|
@ -118,6 +124,10 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
|
|||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
console.error("OpenRouter API authentication failed. Please check your API key.")
|
||||
throw new Error("OpenRouter API authentication failed. Please check your API key.")
|
||||
}
|
||||
console.error(
|
||||
`Error fetching OpenRouter models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
|
|
@ -137,8 +147,16 @@ export async function getOpenRouterModelEndpoints(
|
|||
const models: Record<string, ModelInfo> = {}
|
||||
const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1"
|
||||
|
||||
// Prepare headers with API key if available
|
||||
const headers: Record<string, string> = {}
|
||||
if (options?.openRouterApiKey) {
|
||||
headers.Authorization = `Bearer ${options.openRouterApiKey}`
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get<OpenRouterModelEndpointsResponse>(`${baseURL}/models/${modelId}/endpoints`)
|
||||
const response = await axios.get<OpenRouterModelEndpointsResponse>(`${baseURL}/models/${modelId}/endpoints`, {
|
||||
headers,
|
||||
})
|
||||
const result = openRouterModelEndpointsResponseSchema.safeParse(response.data)
|
||||
const data = result.success ? result.data.data : response.data.data
|
||||
|
||||
|
|
@ -157,6 +175,10 @@ export async function getOpenRouterModelEndpoints(
|
|||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
console.error("OpenRouter API authentication failed. Please check your API key.")
|
||||
throw new Error("OpenRouter API authentication failed. Please check your API key.")
|
||||
}
|
||||
console.error(
|
||||
`Error fetching OpenRouter model endpoints: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
|
||||
const apiKey = this.options.openRouterApiKey ?? "not-provided"
|
||||
|
||||
// Validate API key format
|
||||
if (apiKey === "not-provided" || !apiKey || apiKey.trim() === "") {
|
||||
console.warn("OpenRouter API key is missing or invalid. This may cause authentication errors.")
|
||||
}
|
||||
|
||||
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS })
|
||||
}
|
||||
|
||||
|
|
@ -175,11 +180,16 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
|
||||
public async fetchModel() {
|
||||
const [models, endpoints] = await Promise.all([
|
||||
getModels({ provider: "openrouter" }),
|
||||
getModels({
|
||||
provider: "openrouter",
|
||||
apiKey: this.options.openRouterApiKey,
|
||||
baseUrl: this.options.openRouterBaseUrl,
|
||||
}),
|
||||
getModelEndpoints({
|
||||
router: "openrouter",
|
||||
modelId: this.options.openRouterModelId,
|
||||
endpoint: this.options.openRouterSpecificProvider,
|
||||
...this.options,
|
||||
}),
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const getModelMaxOutputTokens = ({
|
|||
// GetModelsOptions
|
||||
|
||||
export type GetModelsOptions =
|
||||
| { provider: "openrouter" }
|
||||
| { provider: "openrouter"; apiKey?: string; baseUrl?: string }
|
||||
| { provider: "glama" }
|
||||
| { provider: "requesty"; apiKey?: string }
|
||||
| { provider: "unbound"; apiKey?: string }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue