Skip to content
Open
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
25 changes: 24 additions & 1 deletion src/api/providers/__tests__/openrouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const MOCK_TIMEOUT_MS = 300_000
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"

import { providerIdentifiers } from "@roo-code/types"
import { ApiProviderError, extractApiProviderErrorProperties, providerIdentifiers } from "@roo-code/types"

import { OpenRouterHandler } from "../openrouter"
import { Package } from "../../../shared/package"
Expand Down Expand Up @@ -384,6 +384,29 @@ describe("OpenRouterHandler", () => {
)
})

it("pins provider casing at the telemetry/user boundary (#1302)", async () => {
const handler = new OpenRouterHandler(mockOptions)
const mockCreate = vitest.fn().mockRejectedValue(new Error("casing drift"))
// Structural cast (not `as any`): the auto-mocked OpenAI client exposes no
// typed surface for overriding the shared prototype every test here uses.
;(OpenAI as { prototype: { chat: { completions: { create: unknown } } } }).prototype.chat = {
completions: { create: mockCreate },
}

const generator = handler.createMessage("test", [])
// The user-facing message keeps the display label (providerName).
await expect(generator.next()).rejects.toThrow("OpenRouter completion error: casing drift")

// The serialized telemetry event must carry the canonical lowercase
// identifier. This is a literal pin (not providerIdentifiers.openrouter
// on both sides) so a future casing drift on either sink fails here.
const captured = mockCaptureException.mock.calls.at(-1)?.[0]
expect(captured).toBeInstanceOf(ApiProviderError)
const properties = extractApiProviderErrorProperties(captured as ApiProviderError)
expect(properties.provider).toBe("openrouter")
expect(properties.provider).not.toBe("OpenRouter")
})

it("passes SDK exceptions with status 429 to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
const handler = new OpenRouterHandler(mockOptions)
const error = new Error("Rate limit exceeded: free-models-per-day") as any
Expand Down
2 changes: 2 additions & 0 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
private client: OpenAI
protected models: ModelRecord = {}
protected endpoints: ModelRecord = {}
// Display name for user-facing error messages only; telemetry uses
// providerIdentifiers.openrouter as the canonical provider identifier.
private readonly providerName = "OpenRouter"
private currentReasoningDetails: any[] = []

Expand Down
Loading