Skip to content

Commit 19625b4

Browse files
committed
fix: correct Requesty model fetching URL construction
- Fix URL construction in getRequestyModels to properly append /models path - Ensure base URL with /v1 correctly resolves to /v1/models instead of /models - Add comprehensive tests for URL construction scenarios Fixes #7377
1 parent 8367b1a commit 19625b4

File tree

2 files changed

+187
-1
lines changed

2 files changed

+187
-1
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import axios from "axios"
3+
import { getRequestyModels } from "../requesty"
4+
5+
vi.mock("axios")
6+
7+
describe("getRequestyModels", () => {
8+
const mockAxios = axios as any
9+
10+
beforeEach(() => {
11+
vi.clearAllMocks()
12+
})
13+
14+
const mockModelsResponse = {
15+
data: {
16+
data: [
17+
{
18+
id: "test-model",
19+
max_output_tokens: 4096,
20+
context_window: 128000,
21+
supports_caching: true,
22+
supports_vision: true,
23+
supports_computer_use: false,
24+
supports_reasoning: false,
25+
input_price: 3,
26+
output_price: 15,
27+
caching_price: 3.75,
28+
cached_price: 0.3,
29+
description: "Test model",
30+
},
31+
],
32+
},
33+
}
34+
35+
describe("URL construction", () => {
36+
it("should correctly append /models to default base URL", async () => {
37+
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
38+
39+
await getRequestyModels()
40+
41+
expect(mockAxios.get).toHaveBeenCalledWith("https://router.requesty.ai/v1/models", { headers: {} })
42+
})
43+
44+
it("should correctly append /models to custom base URL with /v1", async () => {
45+
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
46+
47+
await getRequestyModels("https://custom.requesty.ai/v1")
48+
49+
expect(mockAxios.get).toHaveBeenCalledWith("https://custom.requesty.ai/v1/models", { headers: {} })
50+
})
51+
52+
it("should correctly append /models to custom base URL with trailing slash", async () => {
53+
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
54+
55+
await getRequestyModels("https://custom.requesty.ai/v1/")
56+
57+
expect(mockAxios.get).toHaveBeenCalledWith("https://custom.requesty.ai/v1/models", { headers: {} })
58+
})
59+
60+
it("should correctly append /models to custom base URL without /v1", async () => {
61+
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
62+
63+
await getRequestyModels("https://custom.requesty.ai")
64+
65+
expect(mockAxios.get).toHaveBeenCalledWith("https://custom.requesty.ai/models", { headers: {} })
66+
})
67+
68+
it("should include authorization header when API key is provided", async () => {
69+
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
70+
71+
await getRequestyModels("https://custom.requesty.ai/v1", "test-api-key")
72+
73+
expect(mockAxios.get).toHaveBeenCalledWith("https://custom.requesty.ai/v1/models", {
74+
headers: { Authorization: "Bearer test-api-key" },
75+
})
76+
})
77+
})
78+
79+
describe("model parsing", () => {
80+
it("should correctly parse model information", async () => {
81+
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
82+
83+
const models = await getRequestyModels()
84+
85+
expect(models["test-model"]).toEqual({
86+
maxTokens: 4096,
87+
contextWindow: 128000,
88+
supportsPromptCache: true,
89+
supportsImages: true,
90+
supportsComputerUse: false,
91+
supportsReasoningBudget: false,
92+
supportsReasoningEffort: false,
93+
inputPrice: 3000000, // parseApiPrice multiplies by 1,000,000
94+
outputPrice: 15000000, // parseApiPrice multiplies by 1,000,000
95+
description: "Test model",
96+
cacheWritesPrice: 3750000, // parseApiPrice multiplies by 1,000,000
97+
cacheReadsPrice: 300000, // parseApiPrice multiplies by 1,000,000
98+
})
99+
})
100+
101+
it("should handle reasoning support for Claude models", async () => {
102+
const claudeResponse = {
103+
data: {
104+
data: [
105+
{
106+
id: "claude-3-opus",
107+
max_output_tokens: 4096,
108+
context_window: 200000,
109+
supports_caching: true,
110+
supports_vision: true,
111+
supports_computer_use: true,
112+
supports_reasoning: true,
113+
input_price: 15,
114+
output_price: 75,
115+
caching_price: 18.75,
116+
cached_price: 1.5,
117+
description: "Claude 3 Opus",
118+
},
119+
],
120+
},
121+
}
122+
123+
mockAxios.get = vi.fn().mockResolvedValue(claudeResponse)
124+
125+
const models = await getRequestyModels()
126+
127+
expect(models["claude-3-opus"]).toBeDefined()
128+
expect(models["claude-3-opus"].supportsReasoningBudget).toBe(true)
129+
expect(models["claude-3-opus"].supportsReasoningEffort).toBe(false)
130+
})
131+
132+
it("should handle reasoning support for OpenAI models", async () => {
133+
const openaiResponse = {
134+
data: {
135+
data: [
136+
{
137+
id: "openai/gpt-4",
138+
max_output_tokens: 4096,
139+
context_window: 128000,
140+
supports_caching: false,
141+
supports_vision: true,
142+
supports_computer_use: false,
143+
supports_reasoning: true,
144+
input_price: 10,
145+
output_price: 30,
146+
caching_price: 0,
147+
cached_price: 0,
148+
description: "GPT-4",
149+
},
150+
],
151+
},
152+
}
153+
154+
mockAxios.get = vi.fn().mockResolvedValue(openaiResponse)
155+
156+
const models = await getRequestyModels()
157+
158+
expect(models["openai/gpt-4"]).toBeDefined()
159+
expect(models["openai/gpt-4"].supportsReasoningBudget).toBe(false)
160+
expect(models["openai/gpt-4"].supportsReasoningEffort).toBe(true)
161+
})
162+
})
163+
164+
describe("error handling", () => {
165+
it("should return empty object on API error", async () => {
166+
mockAxios.get = vi.fn().mockRejectedValue(new Error("API Error"))
167+
168+
const models = await getRequestyModels()
169+
170+
expect(models).toEqual({})
171+
})
172+
173+
it("should log error details", async () => {
174+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
175+
mockAxios.get = vi.fn().mockRejectedValue(new Error("API Error"))
176+
177+
await getRequestyModels()
178+
179+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Error fetching Requesty models:"))
180+
181+
consoleErrorSpy.mockRestore()
182+
})
183+
})
184+
})

src/api/providers/fetchers/requesty.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export async function getRequestyModels(baseUrl?: string, apiKey?: string): Prom
1616
}
1717

1818
const resolvedBaseUrl = toRequestyServiceUrl(baseUrl)
19-
const modelsUrl = new URL("models", resolvedBaseUrl)
19+
// Ensure the base URL ends with a slash so "models" is appended correctly
20+
const baseWithSlash = resolvedBaseUrl.endsWith("/") ? resolvedBaseUrl : `${resolvedBaseUrl}/`
21+
const modelsUrl = new URL("models", baseWithSlash)
2022

2123
const response = await axios.get(modelsUrl.toString(), { headers })
2224
const rawModels = response.data.data

0 commit comments

Comments
 (0)