Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions src/api/providers/fetchers/__tests__/openrouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ describe("OpenRouter API", () => {
input_modalities: ["text", "image"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "max_tokens", "temperature", "reasoning"],
endpoints: [
{
provider_name: "Google Vertex",
Expand Down Expand Up @@ -196,6 +197,7 @@ describe("OpenRouter API", () => {
input_modalities: ["text", "image"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "max_tokens", "temperature", "reasoning"],
endpoints: [
{
provider_name: "Anthropic",
Expand Down Expand Up @@ -265,6 +267,132 @@ describe("OpenRouter API", () => {
})
})

describe("getOpenRouterModels - tool support filtering", () => {
it("filters out models without tool support in supported_parameters", async () => {
const mockResponse = {
data: {
data: [
{
id: "test/with-tools",
name: "Model With Tools",
context_length: 128000,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
top_provider: { max_completion_tokens: 8192 },
supported_parameters: ["tools", "max_tokens", "temperature"],
},
{
id: "test/without-tools",
name: "Model Without Tools",
context_length: 128000,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
top_provider: { max_completion_tokens: 8192 },
supported_parameters: ["max_tokens", "temperature"],
},
{
id: "test/no-params",
name: "Model With No Params",
context_length: 128000,
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
top_provider: { max_completion_tokens: 8192 },
},
],
},
}

const axios = await import("axios")
const getSpy = vi.spyOn(axios.default, "get").mockResolvedValue(mockResponse)

const models = await getOpenRouterModels()

// Only the model with tools support should be included
expect(models["test/with-tools"]).toBeDefined()
expect(models["test/without-tools"]).toBeUndefined()
expect(models["test/no-params"]).toBeUndefined()

getSpy.mockRestore()
})
})

describe("getOpenRouterModelEndpoints - tool support filtering", () => {
it("returns empty when model does not support tools", async () => {
const mockEndpointsResponse = {
data: {
data: {
id: "test/no-tools-model",
name: "No Tools Model",
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["max_tokens", "temperature"],
endpoints: [
{
provider_name: "Provider A",
name: "No Tools Model",
context_length: 128000,
max_completion_tokens: 8192,
pricing: { prompt: "0.000003", completion: "0.000015" },
},
],
},
},
}

const axios = await import("axios")
const getSpy = vi.spyOn(axios.default, "get").mockResolvedValue(mockEndpointsResponse)

const endpoints = await getOpenRouterModelEndpoints("test/no-tools-model")

expect(Object.keys(endpoints).length).toBe(0)

getSpy.mockRestore()
})

it("returns endpoints when model supports tools", async () => {
const mockEndpointsResponse = {
data: {
data: {
id: "test/tools-model",
name: "Tools Model",
architecture: {
input_modalities: ["text"],
output_modalities: ["text"],
},
supported_parameters: ["tools", "max_tokens", "temperature"],
endpoints: [
{
provider_name: "Provider A",
name: "Tools Model",
context_length: 128000,
max_completion_tokens: 8192,
pricing: { prompt: "0.000003", completion: "0.000015" },
},
],
},
},
}

const axios = await import("axios")
const getSpy = vi.spyOn(axios.default, "get").mockResolvedValue(mockEndpointsResponse)

const endpoints = await getOpenRouterModelEndpoints("test/tools-model")

expect(Object.keys(endpoints).length).toBe(1)
expect(endpoints["Provider A"]).toBeDefined()

getSpy.mockRestore()
})
})

describe("parseOpenRouterModel", () => {
it("sets claude-sonnet-4.6 model to Anthropic max tokens", () => {
const mockModel = {
Expand Down
15 changes: 14 additions & 1 deletion src/api/providers/fetchers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
continue
}

// Skip models that don't support tool calling. Models without tool
// capability produce broken output (e.g. wrapping code in [{...}]
// formatting) because Roo Code relies on native tool calling.
// This matches the filtering behavior used by the Ollama provider.
if (!supported_parameters.includes("tools")) {
continue
}

const parsedModel = parseOpenRouterModel({
id,
model,
Expand Down Expand Up @@ -155,13 +163,18 @@ export async function getOpenRouterModelEndpoints(
console.error("OpenRouter model endpoints response is invalid", result.error.format())
}

const { id, architecture, endpoints } = data
const { id, architecture, supported_parameters, endpoints } = data

// Skip image generation models (models that output images)
if (architecture?.output_modalities?.includes("image")) {
return models
}

// Skip models that don't support tool calling (same rationale as getOpenRouterModels)
if (!supported_parameters?.includes("tools")) {
return models
}

for (const endpoint of endpoints) {
models[endpoint.tag ?? endpoint.provider_name] = parseOpenRouterModel({
id,
Expand Down
Loading