From cb3c12b1c0a8929fb8da99e8f5b48dcdf549dd10 Mon Sep 17 00:00:00 2001 From: Eason Liang Date: Sun, 30 Aug 2026 01:43:16 +0800 Subject: [PATCH] fix(openrouter): pin telemetry provider casing against display name (#1302) PR #1165 canonicalized the ApiProviderError provider to providerIdentifiers.openrouter (openrouter) while handleOpenAIError kept the display label this.providerName (OpenRouter), so one error event carried two spellings. Document the providerName field as display-only and add a regression test that pins the serialized telemetry value to the literal lowercase identifier and the user-facing message to the display label, so future drift on either sink fails locally. --- .../providers/__tests__/openrouter.spec.ts | 25 ++++++++++++++++++- src/api/providers/openrouter.ts | 2 ++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index 18b6286d09..3d004177ea 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -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" @@ -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 diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index f61e007214..d9b3b2cee1 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -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[] = []