mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix(chutes): add graceful fallback for model parsing (#10279)
Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
This commit is contained in:
parent
7fae76ec86
commit
f462eeb80c
2 changed files with 171 additions and 24 deletions
|
|
@ -212,4 +212,132 @@ describe("getChutesModels", () => {
|
|||
expect(models["test/no-tools-model"].supportsNativeTools).toBe(false)
|
||||
expect(models["test/no-tools-model"].defaultToolProtocol).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should skip empty objects in API response and still process valid models", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "test/valid-model",
|
||||
object: "model",
|
||||
owned_by: "test",
|
||||
created: 1234567890,
|
||||
context_length: 128000,
|
||||
max_model_len: 8192,
|
||||
input_modalities: ["text"],
|
||||
},
|
||||
{}, // Empty object - should be skipped
|
||||
{
|
||||
id: "test/another-valid-model",
|
||||
object: "model",
|
||||
context_length: 64000,
|
||||
max_model_len: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
mockedAxios.get.mockResolvedValue(mockResponse)
|
||||
|
||||
const models = await getChutesModels("test-api-key")
|
||||
|
||||
// Valid models should be processed
|
||||
expect(models["test/valid-model"]).toBeDefined()
|
||||
expect(models["test/valid-model"].contextWindow).toBe(128000)
|
||||
expect(models["test/another-valid-model"]).toBeDefined()
|
||||
expect(models["test/another-valid-model"].contextWindow).toBe(64000)
|
||||
})
|
||||
|
||||
it("should skip models without id field", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
// Missing id field
|
||||
object: "model",
|
||||
context_length: 128000,
|
||||
max_model_len: 8192,
|
||||
},
|
||||
{
|
||||
id: "test/valid-model",
|
||||
context_length: 64000,
|
||||
max_model_len: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
mockedAxios.get.mockResolvedValue(mockResponse)
|
||||
|
||||
const models = await getChutesModels("test-api-key")
|
||||
|
||||
// Only the valid model should be added
|
||||
expect(models["test/valid-model"]).toBeDefined()
|
||||
// Hardcoded models should still exist
|
||||
expect(Object.keys(models).length).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it("should calculate maxTokens fallback when max_model_len is missing", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "test/no-max-len-model",
|
||||
object: "model",
|
||||
context_length: 100000,
|
||||
// max_model_len is missing
|
||||
input_modalities: ["text"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
mockedAxios.get.mockResolvedValue(mockResponse)
|
||||
|
||||
const models = await getChutesModels("test-api-key")
|
||||
|
||||
// Should calculate maxTokens as 20% of contextWindow
|
||||
expect(models["test/no-max-len-model"]).toBeDefined()
|
||||
expect(models["test/no-max-len-model"].maxTokens).toBe(20000) // 100000 * 0.2
|
||||
expect(models["test/no-max-len-model"].contextWindow).toBe(100000)
|
||||
})
|
||||
|
||||
it("should gracefully handle response with mixed valid and invalid items", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: "test/valid-1",
|
||||
context_length: 128000,
|
||||
max_model_len: 8192,
|
||||
},
|
||||
{}, // Empty - will be skipped
|
||||
null, // Null - will be skipped
|
||||
{
|
||||
id: "", // Empty string id - will be skipped
|
||||
context_length: 64000,
|
||||
},
|
||||
{
|
||||
id: "test/valid-2",
|
||||
context_length: 256000,
|
||||
max_model_len: 16384,
|
||||
supported_features: ["tools"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
mockedAxios.get.mockResolvedValue(mockResponse)
|
||||
|
||||
const models = await getChutesModels("test-api-key")
|
||||
|
||||
// Both valid models should be processed
|
||||
expect(models["test/valid-1"]).toBeDefined()
|
||||
expect(models["test/valid-2"]).toBeDefined()
|
||||
expect(models["test/valid-2"].supportsNativeTools).toBe(true)
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,19 +6,22 @@ import { type ModelInfo, chutesModels } from "@roo-code/types"
|
|||
import { DEFAULT_HEADERS } from "../constants"
|
||||
|
||||
// Chutes models endpoint follows OpenAI /models shape with additional fields.
|
||||
// All fields are optional to allow graceful handling of incomplete API responses.
|
||||
const ChutesModelSchema = z.object({
|
||||
id: z.string(),
|
||||
id: z.string().optional(),
|
||||
object: z.literal("model").optional(),
|
||||
owned_by: z.string().optional(),
|
||||
created: z.number().optional(),
|
||||
context_length: z.number().optional(),
|
||||
max_model_len: z.number(),
|
||||
max_model_len: z.number().optional(),
|
||||
input_modalities: z.array(z.string()).optional(),
|
||||
supported_features: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
const ChutesModelsResponseSchema = z.object({ data: z.array(ChutesModelSchema) })
|
||||
|
||||
type ChutesModelsResponse = z.infer<typeof ChutesModelsResponseSchema>
|
||||
|
||||
export async function getChutesModels(apiKey?: string): Promise<Record<string, ModelInfo>> {
|
||||
const headers: Record<string, string> = { ...DEFAULT_HEADERS }
|
||||
|
||||
|
|
@ -32,33 +35,49 @@ export async function getChutesModels(apiKey?: string): Promise<Record<string, M
|
|||
const models: Record<string, ModelInfo> = { ...chutesModels }
|
||||
|
||||
try {
|
||||
const response = await axios.get(url, { headers })
|
||||
const parsed = ChutesModelsResponseSchema.safeParse(response.data)
|
||||
const response = await axios.get<ChutesModelsResponse>(url, { headers })
|
||||
const result = ChutesModelsResponseSchema.safeParse(response.data)
|
||||
|
||||
if (parsed.success) {
|
||||
for (const m of parsed.data.data) {
|
||||
const contextWindow = m.context_length
|
||||
// Graceful fallback: use parsed data if valid, otherwise fall back to raw response data.
|
||||
// This mirrors the OpenRouter pattern for handling API responses with some invalid items.
|
||||
const data = result.success ? result.data.data : response.data?.data
|
||||
|
||||
if (!contextWindow) {
|
||||
continue
|
||||
}
|
||||
if (!result.success) {
|
||||
console.error(`Error parsing Chutes models response: ${JSON.stringify(result.error.format(), null, 2)}`)
|
||||
}
|
||||
|
||||
const info: ModelInfo = {
|
||||
maxTokens: m.max_model_len,
|
||||
contextWindow,
|
||||
supportsImages: (m.input_modalities || []).includes("image"),
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: (m.supported_features || []).includes("tools"),
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: `Chutes AI model: ${m.id}`,
|
||||
}
|
||||
if (!data || !Array.isArray(data)) {
|
||||
console.error("Chutes models response missing data array")
|
||||
return models
|
||||
}
|
||||
|
||||
// Union: dynamic models override hardcoded ones if they have the same ID.
|
||||
models[m.id] = info
|
||||
for (const m of data) {
|
||||
// Skip items missing required fields (e.g., empty objects from API)
|
||||
if (!m || typeof m.id !== "string" || !m.id) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
console.error(`Error parsing Chutes models: ${JSON.stringify(parsed.error.format(), null, 2)}`)
|
||||
|
||||
const contextWindow = typeof m.context_length === "number" && Number.isFinite(m.context_length) ? m.context_length : undefined
|
||||
const maxModelLen = typeof m.max_model_len === "number" && Number.isFinite(m.max_model_len) ? m.max_model_len : undefined
|
||||
|
||||
// Skip models without valid context window information
|
||||
if (!contextWindow) {
|
||||
continue
|
||||
}
|
||||
|
||||
const info: ModelInfo = {
|
||||
maxTokens: maxModelLen ?? Math.ceil(contextWindow * 0.2),
|
||||
contextWindow,
|
||||
supportsImages: (m.input_modalities || []).includes("image"),
|
||||
supportsPromptCache: false,
|
||||
supportsNativeTools: (m.supported_features || []).includes("tools"),
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: `Chutes AI model: ${m.id}`,
|
||||
}
|
||||
|
||||
// Union: dynamic models override hardcoded ones if they have the same ID.
|
||||
models[m.id] = info
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Chutes models: ${error instanceof Error ? error.message : String(error)}`)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue