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:
Roo Code 2025-07-31 02:25:45 +00:00
parent b71cd44c1c
commit d186eb08de
6 changed files with 133 additions and 5 deletions

View file

@ -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)

View file

@ -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()
})
})
})

View file

@ -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

View file

@ -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)}`,
)

View file

@ -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,
}),
])

View file

@ -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 }