diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index ea850c47be..07246d6982 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -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) diff --git a/src/api/providers/fetchers/__tests__/openrouter.spec.ts b/src/api/providers/fetchers/__tests__/openrouter.spec.ts index f0ebead30f..63a13ef4b4 100644 --- a/src/api/providers/fetchers/__tests__/openrouter.spec.ts +++ b/src/api/providers/fetchers/__tests__/openrouter.spec.ts @@ -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() + }) }) }) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index fef700268d..4fe851653f 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -55,7 +55,10 @@ export const getModels = async (options: GetModelsOptions): Promise 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 diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index 027f8c54fb..58f2a2e0e5 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -97,8 +97,14 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise< const models: Record = {} const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1" + // Prepare headers with API key if available + const headers: Record = {} + if (options?.openRouterApiKey) { + headers.Authorization = `Bearer ${options.openRouterApiKey}` + } + try { - const response = await axios.get(`${baseURL}/models`) + const response = await axios.get(`${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 = {} const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1" + // Prepare headers with API key if available + const headers: Record = {} + if (options?.openRouterApiKey) { + headers.Authorization = `Bearer ${options.openRouterApiKey}` + } + try { - const response = await axios.get(`${baseURL}/models/${modelId}/endpoints`) + const response = await axios.get(`${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)}`, ) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 6565daa238..a3cc699295 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -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, }), ]) diff --git a/src/shared/api.ts b/src/shared/api.ts index 8cbfc72133..4a2cd6465f 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -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 }