diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4621ec9de9..bc71d79765 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -776,9 +776,6 @@ importers:
ansi-to-html:
specifier: ^0.7.2
version: 0.7.2
- axios:
- specifier: ^1.18.1
- version: 1.20.0
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
diff --git a/webview-ui/eslint.config.mjs b/webview-ui/eslint.config.mjs
index e7b18babca..c8b5b49bfb 100644
--- a/webview-ui/eslint.config.mjs
+++ b/webview-ui/eslint.config.mjs
@@ -40,6 +40,23 @@ export default [
"no-case-declarations": "off",
},
},
+ {
+ files: ["src/**/*.{ts,tsx}"],
+ rules: {
+ "no-restricted-imports": [
+ "error",
+ {
+ paths: [
+ {
+ name: "axios",
+ message:
+ "axios was removed from the webview bundle in favour of global fetch. Use fetch() for network calls. The dependency is no longer declared in webview-ui/package.json, so an import here only resolves through the hoisted store and would fail at build time.",
+ },
+ ],
+ },
+ ],
+ },
+ },
{
files: ["src/__mocks__/**/*.js"],
rules: {
@@ -63,6 +80,11 @@ export default [
"error",
{
paths: [
+ {
+ name: "axios",
+ message:
+ "axios was removed from the webview bundle in favour of global fetch. Use fetch() for network calls. The dependency is no longer declared in webview-ui/package.json, so an import here only resolves through the hoisted store and would fail at build time.",
+ },
{
name: "@tanstack/react-query",
importNames: ["QueryClientProvider"],
diff --git a/webview-ui/package.json b/webview-ui/package.json
index c8cecf5d4d..2ea7e5eea3 100644
--- a/webview-ui/package.json
+++ b/webview-ui/package.json
@@ -38,7 +38,6 @@
"@vscode/codicons": "^0.0.45",
"@vscode/webview-ui-toolkit": "^1.4.0",
"ansi-to-html": "^0.7.2",
- "axios": "^1.18.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
diff --git a/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterKeyInfo.spec.tsx b/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterKeyInfo.spec.tsx
new file mode 100644
index 0000000000..2e7bf65c24
--- /dev/null
+++ b/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterKeyInfo.spec.tsx
@@ -0,0 +1,123 @@
+import { renderHook, waitFor } from "@testing-library/react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+import React from "react"
+
+import { stubTransport } from "@src/utils/test-utils"
+
+import { useOpenRouterKeyInfo } from "../useOpenRouterKeyInfo"
+
+const KEY_INFO = {
+ label: "sk-or-v1-...abc",
+ usage: 12.5,
+ is_free_tier: false,
+ is_provisioning_key: false,
+ rate_limit: { requests: 60, interval: "1m" },
+ limit: 100,
+}
+
+const wrapper = ({ children }: React.PropsWithChildren) => (
+
+ {children}
+
+)
+
+describe("useOpenRouterKeyInfo", () => {
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it("returns undefined and issues no request when apiKey is missing", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: { data: KEY_INFO } }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useOpenRouterKeyInfo(undefined), { wrapper })
+
+ await waitFor(() => expect(result.current.fetchStatus).toBe("idle"))
+ expect(result.current.data).toBeUndefined()
+ expect(calls).toHaveLength(0)
+ expect(consoleError).not.toHaveBeenCalled()
+ })
+
+ it("requests the default key endpoint with a bearer token and returns the parsed key info", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: { data: KEY_INFO } }))
+
+ const { result } = renderHook(() => useOpenRouterKeyInfo("sk-or-v1-test"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toEqual(KEY_INFO)
+ expect(calls).toEqual([
+ {
+ url: "https://openrouter.ai/api/v1/key",
+ method: "GET",
+ headers: { authorization: "Bearer sk-or-v1-test" },
+ },
+ ])
+ })
+
+ it("uses a caller-supplied baseUrl verbatim", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: { data: KEY_INFO } }))
+
+ const { result } = renderHook(() => useOpenRouterKeyInfo("sk-or-v1-test", "https://proxy.example.com/api/v1"), {
+ wrapper,
+ })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(calls[0].url).toBe("https://proxy.example.com/api/v1/key")
+ })
+
+ it("returns null and logs a validation failure when the payload does not match the schema", async () => {
+ stubTransport(() => ({ status: 200, body: { data: { label: "only-label" } } }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useOpenRouterKeyInfo("sk-or-v1-test"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toBeNull()
+ expect(consoleError).toHaveBeenCalledWith(
+ "OpenRouter API key info validation failed:",
+ expect.objectContaining({
+ issues: expect.arrayContaining([expect.objectContaining({ path: ["data", "usage"] })]),
+ }),
+ )
+ })
+
+ it("returns null and logs a fetch failure when the response is not 2xx", async () => {
+ stubTransport(() => ({ status: 401, body: { error: { message: "No auth credentials found" } } }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useOpenRouterKeyInfo("sk-or-v1-test"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toBeNull()
+ expect(consoleError).toHaveBeenCalledWith(
+ "Error fetching OpenRouter key info:",
+ expect.objectContaining({ message: "Request failed with status code 401" }),
+ )
+ })
+
+ it("returns null and logs a fetch failure when the request rejects", async () => {
+ const failure = new Error("network down")
+ stubTransport(() => {
+ throw failure
+ })
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useOpenRouterKeyInfo("sk-or-v1-test"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toBeNull()
+ expect(consoleError).toHaveBeenCalledWith("Error fetching OpenRouter key info:", failure)
+ })
+
+ it("returns null and logs a fetch failure when a 2xx body is not valid JSON", async () => {
+ const parseFailure = new SyntaxError("Unexpected token '<', \"\" is not valid JSON")
+ stubTransport(() => ({ status: 200, body: undefined, jsonRejects: parseFailure }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useOpenRouterKeyInfo("sk-or-v1-test"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toBeNull()
+ expect(consoleError).toHaveBeenCalledWith("Error fetching OpenRouter key info:", parseFailure)
+ })
+})
diff --git a/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx b/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx
index dcf0b5829d..f16edbd8a8 100644
--- a/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx
+++ b/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx
@@ -1,14 +1,39 @@
-import React from "react"
-import axios from "axios"
-import { QueryClientProvider } from "@tanstack/react-query"
import { renderHook, waitFor } from "@testing-library/react"
-import { z } from "zod"
+import { QueryClientProvider } from "@tanstack/react-query"
+import React from "react"
-import { createTestQueryClient } from "@src/utils/test-utils"
+import { createTestQueryClient, stubTransport } from "@src/utils/test-utils"
import { useOpenRouterModelProviders } from "../useOpenRouterModelProviders"
-vi.mock("axios")
+const endpointsPayload = (overrides: Record = {}) => ({
+ data: {
+ id: "vendor/model",
+ name: "Vendor Model",
+ description: "A test model",
+ architecture: { input_modalities: ["text", "image"], output_modalities: ["text"] },
+ endpoints: [
+ {
+ name: "Provider A",
+ tag: "provider-a",
+ context_length: 128000,
+ max_completion_tokens: 4096,
+ pricing: {
+ prompt: "0.000003",
+ completion: "0.000015",
+ input_cache_read: "0.0000003",
+ input_cache_write: "0.0000036",
+ },
+ },
+ {
+ name: "Provider B",
+ context_length: 32000,
+ pricing: { prompt: "0.000001", completion: "0.000002" },
+ },
+ ],
+ ...overrides,
+ },
+})
const wrapper = ({ children }: React.PropsWithChildren) => (
{children}
@@ -19,21 +44,129 @@ describe("useOpenRouterModelProviders", () => {
vi.restoreAllMocks()
})
- it("logs the zod issues and returns no providers when validation throws", async () => {
- const zodError = new z.ZodError([{ code: "custom", path: ["data"], message: "bad data" }])
- vi.mocked(axios.get).mockRejectedValue(zodError)
+ it("returns an empty record without a request when modelId is missing", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: endpointsPayload() }))
+
+ const { result } = renderHook(() => useOpenRouterModelProviders(undefined), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toEqual({})
+ expect(calls).toHaveLength(0)
+ })
+
+ it("requests the endpoints URL and maps each endpoint to provider model info", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: endpointsPayload() }))
+
+ const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+
+ expect(calls).toEqual([
+ { url: "https://openrouter.ai/api/v1/models/vendor/model/endpoints", method: "GET", headers: {} },
+ ])
+
+ expect(result.current.data).toEqual({
+ "provider-a": {
+ maxTokens: 4096,
+ contextWindow: 128000,
+ supportsImages: true,
+ supportsPromptCache: true,
+ cacheReadsPrice: 0.3,
+ cacheWritesPrice: expect.closeTo(3.6, 10),
+ inputPrice: 3,
+ outputPrice: 15,
+ description: "A test model",
+ label: "provider-a",
+ },
+ "Provider B": {
+ maxTokens: 32000,
+ contextWindow: 32000,
+ supportsImages: true,
+ supportsPromptCache: false,
+ cacheReadsPrice: undefined,
+ cacheWritesPrice: undefined,
+ inputPrice: 1,
+ outputPrice: 2,
+ description: "A test model",
+ label: "Provider B",
+ },
+ })
+ })
+
+ it.each([
+ { name: "text-only architecture", architecture: { input_modalities: ["text"], output_modalities: ["text"] } },
+ { name: "omitted architecture", architecture: undefined },
+ ])("disables image support for $name", async ({ architecture }) => {
+ stubTransport(() => ({ status: 200, body: endpointsPayload({ architecture }) }))
+
+ const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toMatchObject({
+ "provider-a": { supportsImages: false },
+ "Provider B": { supportsImages: false },
+ })
+ })
+
+ it("uses a caller-supplied baseUrl", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: endpointsPayload() }))
+
+ const { result } = renderHook(
+ () => useOpenRouterModelProviders("vendor/model", "https://proxy.example.com/api/v1"),
+ { wrapper },
+ )
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(calls[0].url).toBe("https://proxy.example.com/api/v1/models/vendor/model/endpoints")
+ })
+
+ it("returns no providers for image-generation models", async () => {
+ stubTransport(() => ({
+ status: 200,
+ body: endpointsPayload({ architecture: { input_modalities: ["text"], output_modalities: ["image"] } }),
+ }))
+
+ const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toEqual({})
+ })
+
+ it("logs a validation failure and returns no providers when the payload does not match the schema", async () => {
+ stubTransport(() => ({ status: 200, body: { data: { id: "vendor/model" } } }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toEqual({})
+ expect(consoleError).toHaveBeenCalledWith(
+ "OpenRouter API response validation failed:",
+ expect.objectContaining({
+ issues: expect.arrayContaining([expect.objectContaining({ path: ["data", "endpoints"] })]),
+ }),
+ )
+ })
+
+ it("logs a fetch failure and returns no providers when the response is not 2xx", async () => {
+ stubTransport(() => ({ status: 500, body: { error: "boom" } }))
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual({})
- expect(consoleError).toHaveBeenCalledWith("OpenRouter API response validation failed:", zodError.issues)
+ expect(consoleError).toHaveBeenCalledWith(
+ "Error fetching OpenRouter providers:",
+ expect.objectContaining({ message: "Request failed with status code 500" }),
+ )
})
- it("logs other errors and returns no providers", async () => {
+ it("logs a fetch failure and returns no providers when the request rejects", async () => {
const failure = new Error("network down")
- vi.mocked(axios.get).mockRejectedValue(failure)
+ stubTransport(() => {
+ throw failure
+ })
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper })
@@ -42,4 +175,16 @@ describe("useOpenRouterModelProviders", () => {
expect(result.current.data).toEqual({})
expect(consoleError).toHaveBeenCalledWith("Error fetching OpenRouter providers:", failure)
})
+
+ it("logs a fetch failure and returns no providers when a 2xx body is not valid JSON", async () => {
+ const parseFailure = new SyntaxError("Unexpected token '<', \"\" is not valid JSON")
+ stubTransport(() => ({ status: 200, body: undefined, jsonRejects: parseFailure }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useOpenRouterModelProviders("vendor/model"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toEqual({})
+ expect(consoleError).toHaveBeenCalledWith("Error fetching OpenRouter providers:", parseFailure)
+ })
})
diff --git a/webview-ui/src/components/ui/hooks/__tests__/useRequestyKeyInfo.spec.tsx b/webview-ui/src/components/ui/hooks/__tests__/useRequestyKeyInfo.spec.tsx
new file mode 100644
index 0000000000..22688db698
--- /dev/null
+++ b/webview-ui/src/components/ui/hooks/__tests__/useRequestyKeyInfo.spec.tsx
@@ -0,0 +1,122 @@
+import { renderHook, waitFor } from "@testing-library/react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+import React from "react"
+
+import { stubTransport } from "@src/utils/test-utils"
+
+import { useRequestyKeyInfo } from "../useRequestyKeyInfo"
+
+const KEY_INFO = {
+ name: "my-key",
+ monthly_limit: "20",
+ monthly_spend: "1.25",
+ org_balance: "18.75",
+ config: { aliases: { "gpt-4o": "openai/gpt-4o" } },
+}
+
+const wrapper = ({ children }: React.PropsWithChildren) => (
+
+ {children}
+
+)
+
+describe("useRequestyKeyInfo", () => {
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it("returns undefined and issues no request when apiKey is missing", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: KEY_INFO }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useRequestyKeyInfo(undefined, undefined), { wrapper })
+
+ await waitFor(() => expect(result.current.fetchStatus).toBe("idle"))
+ expect(result.current.data).toBeUndefined()
+ expect(calls).toHaveLength(0)
+ expect(consoleError).not.toHaveBeenCalled()
+ })
+
+ it("requests the default api key endpoint with bearer auth and returns the parsed key info", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: KEY_INFO }))
+
+ const { result } = renderHook(() => useRequestyKeyInfo(undefined, "test-key"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toEqual(KEY_INFO)
+ expect(calls).toEqual([
+ {
+ url: "https://api.requesty.ai/x/apikey",
+ method: "GET",
+ headers: { authorization: "Bearer test-key", "content-type": "application/json" },
+ },
+ ])
+ })
+
+ it("keeps a caller-supplied host that does not contain 'router' and drops the version suffix", async () => {
+ const { calls } = stubTransport(() => ({ status: 200, body: KEY_INFO }))
+
+ const { result } = renderHook(() => useRequestyKeyInfo("https://custom.requesty.ai/v1", "test-key"), {
+ wrapper,
+ })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(calls[0].url).toBe("https://custom.requesty.ai/x/apikey")
+ })
+
+ it("returns null and logs a validation failure when the payload does not match the schema", async () => {
+ stubTransport(() => ({ status: 200, body: { name: "my-key" } }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useRequestyKeyInfo(undefined, "test-key"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toBeNull()
+ expect(consoleError).toHaveBeenCalledWith(
+ "Requesty API key info validation failed:",
+ expect.objectContaining({
+ issues: expect.arrayContaining([expect.objectContaining({ path: ["monthly_limit"] })]),
+ }),
+ )
+ })
+
+ it("returns null and logs a fetch failure when the response is not 2xx", async () => {
+ stubTransport(() => ({ status: 403, body: { detail: "forbidden" } }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useRequestyKeyInfo(undefined, "test-key"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toBeNull()
+ expect(consoleError).toHaveBeenCalledWith(
+ "Error fetching Requesty key info:",
+ expect.objectContaining({ message: "Request failed with status code 403" }),
+ )
+ })
+
+ it("returns null and logs a fetch failure when the request rejects", async () => {
+ const failure = new Error("network down")
+ stubTransport(() => {
+ throw failure
+ })
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useRequestyKeyInfo(undefined, "test-key"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toBeNull()
+ expect(consoleError).toHaveBeenCalledWith("Error fetching Requesty key info:", failure)
+ })
+
+ it("returns null and logs a fetch failure when a 2xx body is not valid JSON", async () => {
+ const parseFailure = new SyntaxError("Unexpected token '<', \"\" is not valid JSON")
+ stubTransport(() => ({ status: 200, body: undefined, jsonRejects: parseFailure }))
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
+
+ const { result } = renderHook(() => useRequestyKeyInfo(undefined, "test-key"), { wrapper })
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toBeNull()
+ expect(consoleError).toHaveBeenCalledWith("Error fetching Requesty key info:", parseFailure)
+ })
+})
diff --git a/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts b/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts
index 44b7aaade5..406da24eb9 100644
--- a/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts
+++ b/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts
@@ -1,4 +1,3 @@
-import axios from "axios"
import { z } from "zod"
import { useQuery, UseQueryOptions } from "@tanstack/react-query"
@@ -28,13 +27,18 @@ async function getOpenRouterKeyInfo(apiKey?: string, baseUrl?: string) {
const keyEndpoint = `${apiBaseUrl}/key`
- const response = await axios.get(keyEndpoint, {
+ const response = await fetch(keyEndpoint, {
+ method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
- const result = openRouterKeyInfoSchema.safeParse(response.data)
+ if (!response.ok) {
+ throw new Error(`Request failed with status code ${response.status}`)
+ }
+
+ const result = openRouterKeyInfoSchema.safeParse(await response.json())
if (!result.success) {
console.error("OpenRouter API key info validation failed:", result.error)
return null
diff --git a/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts b/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts
index 1994bdb9a8..175ee7b8df 100644
--- a/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts
+++ b/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts
@@ -1,4 +1,3 @@
-import axios from "axios"
import { z } from "zod"
import { useQuery, UseQueryOptions } from "@tanstack/react-query"
@@ -48,8 +47,13 @@ async function getOpenRouterProvidersForModel(modelId: string, baseUrl?: string)
try {
const apiBaseUrl = baseUrl || "https://openrouter.ai/api/v1"
- const response = await axios.get(`${apiBaseUrl}/models/${modelId}/endpoints`)
- const result = openRouterEndpointsSchema.safeParse(response.data)
+ const response = await fetch(`${apiBaseUrl}/models/${modelId}/endpoints`, { method: "GET" })
+
+ if (!response.ok) {
+ throw new Error(`Request failed with status code ${response.status}`)
+ }
+
+ const result = openRouterEndpointsSchema.safeParse(await response.json())
if (!result.success) {
console.error("OpenRouter API response validation failed:", result.error)
@@ -86,11 +90,7 @@ async function getOpenRouterProvidersForModel(modelId: string, baseUrl?: string)
models[providerName] = modelInfo
}
} catch (error) {
- if (error instanceof z.ZodError) {
- console.error(`OpenRouter API response validation failed:`, error.issues)
- } else {
- console.error(`Error fetching OpenRouter providers:`, error)
- }
+ console.error(`Error fetching OpenRouter providers:`, error)
}
return models
diff --git a/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts b/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts
index dd61c17eee..defc295173 100644
--- a/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts
+++ b/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts
@@ -1,4 +1,3 @@
-import axios from "axios"
import { z } from "zod"
import { useQuery, UseQueryOptions } from "@tanstack/react-query"
import { toRequestyServiceUrl } from "@roo/utils/requesty"
@@ -22,14 +21,19 @@ async function getRequestyKeyInfo(baseUrl?: string, apiKey?: string) {
const apiKeyUrl = new URL("x/apikey", url)
try {
- const response = await axios.get(apiKeyUrl.toString(), {
+ const response = await fetch(apiKeyUrl.toString(), {
+ method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
})
- const result = requestyKeyInfoSchema.safeParse(response.data)
+ if (!response.ok) {
+ throw new Error(`Request failed with status code ${response.status}`)
+ }
+
+ const result = requestyKeyInfoSchema.safeParse(await response.json())
if (!result.success) {
console.error("Requesty API key info validation failed:", result.error)
return null
diff --git a/webview-ui/src/utils/test-utils.tsx b/webview-ui/src/utils/test-utils.tsx
index 847c401f2c..b64d88e0b6 100644
--- a/webview-ui/src/utils/test-utils.tsx
+++ b/webview-ui/src/utils/test-utils.tsx
@@ -84,3 +84,36 @@ export * from "@testing-library/react"
// override render method
export { customRender as render }
+
+type TransportCall = { url: string; method: string; headers: Record }
+type TransportResult = { status: number; body: unknown; jsonRejects?: Error }
+
+// Stubs global fetch and records the calls.
+export const stubTransport = (handler: (call: TransportCall) => TransportResult | Promise) => {
+ const calls: TransportCall[] = []
+
+ vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
+ const request = typeof input === "object" && "url" in input ? input : undefined
+ const call: TransportCall = {
+ url: request?.url ?? String(input),
+ method: init?.method ?? request?.method ?? "GET",
+ headers: Object.fromEntries(new Headers(init?.headers ?? request?.headers).entries()),
+ }
+ calls.push(call)
+ const result = await handler(call)
+
+ // The hooks consume only ok, status, and json from this response double.
+ return {
+ ok: result.status >= 200 && result.status < 300,
+ status: result.status,
+ json: async () => {
+ if (result.jsonRejects) {
+ throw result.jsonRejects
+ }
+ return result.body
+ },
+ } as Response
+ })
+
+ return { calls }
+}