mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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
This commit is contained in:
parent
8367b1ac92
commit
19625b49bb
2 changed files with 187 additions and 1 deletions
184
src/api/providers/fetchers/__tests__/requesty.spec.ts
Normal file
184
src/api/providers/fetchers/__tests__/requesty.spec.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import axios from "axios"
|
||||
import { getRequestyModels } from "../requesty"
|
||||
|
||||
vi.mock("axios")
|
||||
|
||||
describe("getRequestyModels", () => {
|
||||
const mockAxios = axios as any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const mockModelsResponse = {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "test-model",
|
||||
max_output_tokens: 4096,
|
||||
context_window: 128000,
|
||||
supports_caching: true,
|
||||
supports_vision: true,
|
||||
supports_computer_use: false,
|
||||
supports_reasoning: false,
|
||||
input_price: 3,
|
||||
output_price: 15,
|
||||
caching_price: 3.75,
|
||||
cached_price: 0.3,
|
||||
description: "Test model",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
describe("URL construction", () => {
|
||||
it("should correctly append /models to default base URL", async () => {
|
||||
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
|
||||
|
||||
await getRequestyModels()
|
||||
|
||||
expect(mockAxios.get).toHaveBeenCalledWith("https://router.requesty.ai/v1/models", { headers: {} })
|
||||
})
|
||||
|
||||
it("should correctly append /models to custom base URL with /v1", async () => {
|
||||
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
|
||||
|
||||
await getRequestyModels("https://custom.requesty.ai/v1")
|
||||
|
||||
expect(mockAxios.get).toHaveBeenCalledWith("https://custom.requesty.ai/v1/models", { headers: {} })
|
||||
})
|
||||
|
||||
it("should correctly append /models to custom base URL with trailing slash", async () => {
|
||||
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
|
||||
|
||||
await getRequestyModels("https://custom.requesty.ai/v1/")
|
||||
|
||||
expect(mockAxios.get).toHaveBeenCalledWith("https://custom.requesty.ai/v1/models", { headers: {} })
|
||||
})
|
||||
|
||||
it("should correctly append /models to custom base URL without /v1", async () => {
|
||||
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
|
||||
|
||||
await getRequestyModels("https://custom.requesty.ai")
|
||||
|
||||
expect(mockAxios.get).toHaveBeenCalledWith("https://custom.requesty.ai/models", { headers: {} })
|
||||
})
|
||||
|
||||
it("should include authorization header when API key is provided", async () => {
|
||||
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
|
||||
|
||||
await getRequestyModels("https://custom.requesty.ai/v1", "test-api-key")
|
||||
|
||||
expect(mockAxios.get).toHaveBeenCalledWith("https://custom.requesty.ai/v1/models", {
|
||||
headers: { Authorization: "Bearer test-api-key" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("model parsing", () => {
|
||||
it("should correctly parse model information", async () => {
|
||||
mockAxios.get = vi.fn().mockResolvedValue(mockModelsResponse)
|
||||
|
||||
const models = await getRequestyModels()
|
||||
|
||||
expect(models["test-model"]).toEqual({
|
||||
maxTokens: 4096,
|
||||
contextWindow: 128000,
|
||||
supportsPromptCache: true,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: false,
|
||||
supportsReasoningBudget: false,
|
||||
supportsReasoningEffort: false,
|
||||
inputPrice: 3000000, // parseApiPrice multiplies by 1,000,000
|
||||
outputPrice: 15000000, // parseApiPrice multiplies by 1,000,000
|
||||
description: "Test model",
|
||||
cacheWritesPrice: 3750000, // parseApiPrice multiplies by 1,000,000
|
||||
cacheReadsPrice: 300000, // parseApiPrice multiplies by 1,000,000
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle reasoning support for Claude models", async () => {
|
||||
const claudeResponse = {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "claude-3-opus",
|
||||
max_output_tokens: 4096,
|
||||
context_window: 200000,
|
||||
supports_caching: true,
|
||||
supports_vision: true,
|
||||
supports_computer_use: true,
|
||||
supports_reasoning: true,
|
||||
input_price: 15,
|
||||
output_price: 75,
|
||||
caching_price: 18.75,
|
||||
cached_price: 1.5,
|
||||
description: "Claude 3 Opus",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
mockAxios.get = vi.fn().mockResolvedValue(claudeResponse)
|
||||
|
||||
const models = await getRequestyModels()
|
||||
|
||||
expect(models["claude-3-opus"]).toBeDefined()
|
||||
expect(models["claude-3-opus"].supportsReasoningBudget).toBe(true)
|
||||
expect(models["claude-3-opus"].supportsReasoningEffort).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle reasoning support for OpenAI models", async () => {
|
||||
const openaiResponse = {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "openai/gpt-4",
|
||||
max_output_tokens: 4096,
|
||||
context_window: 128000,
|
||||
supports_caching: false,
|
||||
supports_vision: true,
|
||||
supports_computer_use: false,
|
||||
supports_reasoning: true,
|
||||
input_price: 10,
|
||||
output_price: 30,
|
||||
caching_price: 0,
|
||||
cached_price: 0,
|
||||
description: "GPT-4",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
mockAxios.get = vi.fn().mockResolvedValue(openaiResponse)
|
||||
|
||||
const models = await getRequestyModels()
|
||||
|
||||
expect(models["openai/gpt-4"]).toBeDefined()
|
||||
expect(models["openai/gpt-4"].supportsReasoningBudget).toBe(false)
|
||||
expect(models["openai/gpt-4"].supportsReasoningEffort).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should return empty object on API error", async () => {
|
||||
mockAxios.get = vi.fn().mockRejectedValue(new Error("API Error"))
|
||||
|
||||
const models = await getRequestyModels()
|
||||
|
||||
expect(models).toEqual({})
|
||||
})
|
||||
|
||||
it("should log error details", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
mockAxios.get = vi.fn().mockRejectedValue(new Error("API Error"))
|
||||
|
||||
await getRequestyModels()
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Error fetching Requesty models:"))
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -16,7 +16,9 @@ export async function getRequestyModels(baseUrl?: string, apiKey?: string): Prom
|
|||
}
|
||||
|
||||
const resolvedBaseUrl = toRequestyServiceUrl(baseUrl)
|
||||
const modelsUrl = new URL("models", resolvedBaseUrl)
|
||||
// Ensure the base URL ends with a slash so "models" is appended correctly
|
||||
const baseWithSlash = resolvedBaseUrl.endsWith("/") ? resolvedBaseUrl : `${resolvedBaseUrl}/`
|
||||
const modelsUrl = new URL("models", baseWithSlash)
|
||||
|
||||
const response = await axios.get(modelsUrl.toString(), { headers })
|
||||
const rawModels = response.data.data
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue