|
@@ -1,6 +1,5 @@
|
|
|
-// npx vitest run src/api/providers/__tests__/openrouter.spec.ts
|
|
|
|
|
|
|
+// pnpm --filter roo-cline test api/providers/__tests__/openrouter.spec.ts
|
|
|
|
|
|
|
|
-// Mock vscode first to avoid import errors
|
|
|
|
|
vitest.mock("vscode", () => ({}))
|
|
vitest.mock("vscode", () => ({}))
|
|
|
|
|
|
|
|
import { Anthropic } from "@anthropic-ai/sdk"
|
|
import { Anthropic } from "@anthropic-ai/sdk"
|
|
@@ -9,14 +8,12 @@ import OpenAI from "openai"
|
|
|
import { OpenRouterHandler } from "../openrouter"
|
|
import { OpenRouterHandler } from "../openrouter"
|
|
|
import { ApiHandlerOptions } from "../../../shared/api"
|
|
import { ApiHandlerOptions } from "../../../shared/api"
|
|
|
import { Package } from "../../../shared/package"
|
|
import { Package } from "../../../shared/package"
|
|
|
-import { ApiProviderError } from "@roo-code/types"
|
|
|
|
|
|
|
|
|
|
-// Mock dependencies
|
|
|
|
|
vitest.mock("openai")
|
|
vitest.mock("openai")
|
|
|
vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) }))
|
|
vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) }))
|
|
|
|
|
|
|
|
-// Mock TelemetryService
|
|
|
|
|
const mockCaptureException = vitest.fn()
|
|
const mockCaptureException = vitest.fn()
|
|
|
|
|
+
|
|
|
vitest.mock("@roo-code/telemetry", () => ({
|
|
vitest.mock("@roo-code/telemetry", () => ({
|
|
|
TelemetryService: {
|
|
TelemetryService: {
|
|
|
instance: {
|
|
instance: {
|
|
@@ -24,6 +21,7 @@ vitest.mock("@roo-code/telemetry", () => ({
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
}))
|
|
}))
|
|
|
|
|
+
|
|
|
vitest.mock("../fetchers/modelCache", () => ({
|
|
vitest.mock("../fetchers/modelCache", () => ({
|
|
|
getModels: vitest.fn().mockImplementation(() => {
|
|
getModels: vitest.fn().mockImplementation(() => {
|
|
|
return Promise.resolve({
|
|
return Promise.resolve({
|
|
@@ -294,13 +292,16 @@ describe("OpenRouterHandler", () => {
|
|
|
const generator = handler.createMessage("test", [])
|
|
const generator = handler.createMessage("test", [])
|
|
|
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 500: API Error")
|
|
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 500: API Error")
|
|
|
|
|
|
|
|
- // Verify telemetry was captured
|
|
|
|
|
- expect(mockCaptureException).toHaveBeenCalledWith(expect.any(ApiProviderError), {
|
|
|
|
|
- provider: "OpenRouter",
|
|
|
|
|
- modelId: mockOptions.openRouterModelId,
|
|
|
|
|
- operation: "createMessage",
|
|
|
|
|
- errorCode: 500,
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "API Error",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "createMessage",
|
|
|
|
|
+ errorCode: 500,
|
|
|
|
|
+ status: 500,
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it("captures telemetry when createMessage throws an exception", async () => {
|
|
it("captures telemetry when createMessage throws an exception", async () => {
|
|
@@ -313,15 +314,82 @@ describe("OpenRouterHandler", () => {
|
|
|
const generator = handler.createMessage("test", [])
|
|
const generator = handler.createMessage("test", [])
|
|
|
await expect(generator.next()).rejects.toThrow()
|
|
await expect(generator.next()).rejects.toThrow()
|
|
|
|
|
|
|
|
- // Verify telemetry was captured
|
|
|
|
|
- expect(mockCaptureException).toHaveBeenCalledWith(expect.any(ApiProviderError), {
|
|
|
|
|
- provider: "OpenRouter",
|
|
|
|
|
- modelId: mockOptions.openRouterModelId,
|
|
|
|
|
- operation: "createMessage",
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "Connection failed",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "createMessage",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ error.status = 429
|
|
|
|
|
+
|
|
|
|
|
+ const mockCreate = vitest.fn().mockRejectedValue(error)
|
|
|
|
|
+ ;(OpenAI as any).prototype.chat = {
|
|
|
|
|
+ completions: { create: mockCreate },
|
|
|
|
|
+ } as any
|
|
|
|
|
+
|
|
|
|
|
+ const generator = handler.createMessage("test", [])
|
|
|
|
|
+ await expect(generator.next()).rejects.toThrow("Rate limit exceeded")
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "Rate limit exceeded: free-models-per-day",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "createMessage",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it("does NOT capture telemetry for 429 rate limit errors", async () => {
|
|
|
|
|
|
|
+ it("passes SDK exceptions with 429 in message to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
|
|
|
|
|
+ const handler = new OpenRouterHandler(mockOptions)
|
|
|
|
|
+ const error = new Error("429 Rate limit exceeded: free-models-per-day")
|
|
|
|
|
+ const mockCreate = vitest.fn().mockRejectedValue(error)
|
|
|
|
|
+ ;(OpenAI as any).prototype.chat = {
|
|
|
|
|
+ completions: { create: mockCreate },
|
|
|
|
|
+ } as any
|
|
|
|
|
+
|
|
|
|
|
+ const generator = handler.createMessage("test", [])
|
|
|
|
|
+ await expect(generator.next()).rejects.toThrow("429 Rate limit exceeded")
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "429 Rate limit exceeded: free-models-per-day",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "createMessage",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("passes SDK exceptions containing 'rate limit' to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
|
|
|
|
|
+ const handler = new OpenRouterHandler(mockOptions)
|
|
|
|
|
+ const error = new Error("Request failed due to rate limit")
|
|
|
|
|
+ const mockCreate = vitest.fn().mockRejectedValue(error)
|
|
|
|
|
+ ;(OpenAI as any).prototype.chat = {
|
|
|
|
|
+ completions: { create: mockCreate },
|
|
|
|
|
+ } as any
|
|
|
|
|
+
|
|
|
|
|
+ const generator = handler.createMessage("test", [])
|
|
|
|
|
+ await expect(generator.next()).rejects.toThrow("rate limit")
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "Request failed due to rate limit",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "createMessage",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("passes 429 rate limit errors from stream to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
|
|
|
const handler = new OpenRouterHandler(mockOptions)
|
|
const handler = new OpenRouterHandler(mockOptions)
|
|
|
const mockStream = {
|
|
const mockStream = {
|
|
|
async *[Symbol.asyncIterator]() {
|
|
async *[Symbol.asyncIterator]() {
|
|
@@ -337,8 +405,16 @@ describe("OpenRouterHandler", () => {
|
|
|
const generator = handler.createMessage("test", [])
|
|
const generator = handler.createMessage("test", [])
|
|
|
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 429: Rate limit exceeded")
|
|
await expect(generator.next()).rejects.toThrow("OpenRouter API Error 429: Rate limit exceeded")
|
|
|
|
|
|
|
|
- // Verify telemetry was NOT captured for 429 errors
|
|
|
|
|
- expect(mockCaptureException).not.toHaveBeenCalled()
|
|
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "Rate limit exceeded",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "createMessage",
|
|
|
|
|
+ errorCode: 429,
|
|
|
|
|
+ status: 429,
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it("yields tool_call_end events when finish_reason is tool_calls", async () => {
|
|
it("yields tool_call_end events when finish_reason is tool_calls", async () => {
|
|
@@ -458,32 +534,104 @@ describe("OpenRouterHandler", () => {
|
|
|
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenRouter API Error 500: API Error")
|
|
await expect(handler.completePrompt("test prompt")).rejects.toThrow("OpenRouter API Error 500: API Error")
|
|
|
|
|
|
|
|
// Verify telemetry was captured
|
|
// Verify telemetry was captured
|
|
|
- expect(mockCaptureException).toHaveBeenCalledWith(expect.any(ApiProviderError), {
|
|
|
|
|
- provider: "OpenRouter",
|
|
|
|
|
- modelId: mockOptions.openRouterModelId,
|
|
|
|
|
- operation: "completePrompt",
|
|
|
|
|
- errorCode: 500,
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "API Error",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "completePrompt",
|
|
|
|
|
+ errorCode: 500,
|
|
|
|
|
+ status: 500,
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it("handles unexpected errors and captures telemetry", async () => {
|
|
it("handles unexpected errors and captures telemetry", async () => {
|
|
|
const handler = new OpenRouterHandler(mockOptions)
|
|
const handler = new OpenRouterHandler(mockOptions)
|
|
|
- const mockCreate = vitest.fn().mockRejectedValue(new Error("Unexpected error"))
|
|
|
|
|
|
|
+ const error = new Error("Unexpected error")
|
|
|
|
|
+ const mockCreate = vitest.fn().mockRejectedValue(error)
|
|
|
;(OpenAI as any).prototype.chat = {
|
|
;(OpenAI as any).prototype.chat = {
|
|
|
completions: { create: mockCreate },
|
|
completions: { create: mockCreate },
|
|
|
} as any
|
|
} as any
|
|
|
|
|
|
|
|
await expect(handler.completePrompt("test prompt")).rejects.toThrow("Unexpected error")
|
|
await expect(handler.completePrompt("test prompt")).rejects.toThrow("Unexpected error")
|
|
|
|
|
|
|
|
- // Verify telemetry was captured
|
|
|
|
|
- expect(mockCaptureException).toHaveBeenCalledWith(expect.any(ApiProviderError), {
|
|
|
|
|
- provider: "OpenRouter",
|
|
|
|
|
- modelId: mockOptions.openRouterModelId,
|
|
|
|
|
- operation: "completePrompt",
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ // Verify telemetry was captured (filtering now happens inside PostHogTelemetryClient)
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "Unexpected error",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "completePrompt",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ error.status = 429
|
|
|
|
|
+ const mockCreate = vitest.fn().mockRejectedValue(error)
|
|
|
|
|
+ ;(OpenAI as any).prototype.chat = {
|
|
|
|
|
+ completions: { create: mockCreate },
|
|
|
|
|
+ } as any
|
|
|
|
|
+
|
|
|
|
|
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow("Rate limit exceeded")
|
|
|
|
|
+
|
|
|
|
|
+ // captureException is called, but PostHogTelemetryClient filters out 429 errors internally
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "Rate limit exceeded: free-models-per-day",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "completePrompt",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("passes SDK exceptions with 429 in message to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
|
|
|
|
|
+ const handler = new OpenRouterHandler(mockOptions)
|
|
|
|
|
+ const error = new Error("429 Rate limit exceeded: free-models-per-day")
|
|
|
|
|
+ const mockCreate = vitest.fn().mockRejectedValue(error)
|
|
|
|
|
+ ;(OpenAI as any).prototype.chat = {
|
|
|
|
|
+ completions: { create: mockCreate },
|
|
|
|
|
+ } as any
|
|
|
|
|
+
|
|
|
|
|
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow("429 Rate limit exceeded")
|
|
|
|
|
+
|
|
|
|
|
+ // captureException is called, but PostHogTelemetryClient filters out 429 errors internally
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "429 Rate limit exceeded: free-models-per-day",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "completePrompt",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it("does NOT capture telemetry for 429 rate limit errors", async () => {
|
|
|
|
|
|
|
+ it("passes SDK exceptions containing 'rate limit' to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
|
|
|
|
|
+ const handler = new OpenRouterHandler(mockOptions)
|
|
|
|
|
+ const error = new Error("Request failed due to rate limit")
|
|
|
|
|
+ const mockCreate = vitest.fn().mockRejectedValue(error)
|
|
|
|
|
+ ;(OpenAI as any).prototype.chat = {
|
|
|
|
|
+ completions: { create: mockCreate },
|
|
|
|
|
+ } as any
|
|
|
|
|
+
|
|
|
|
|
+ await expect(handler.completePrompt("test prompt")).rejects.toThrow("rate limit")
|
|
|
|
|
+
|
|
|
|
|
+ // captureException is called, but PostHogTelemetryClient filters out rate limit errors internally
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "Request failed due to rate limit",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "completePrompt",
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it("passes 429 rate limit errors from response to telemetry (filtering happens in PostHogTelemetryClient)", async () => {
|
|
|
const handler = new OpenRouterHandler(mockOptions)
|
|
const handler = new OpenRouterHandler(mockOptions)
|
|
|
const mockError = {
|
|
const mockError = {
|
|
|
error: {
|
|
error: {
|
|
@@ -501,8 +649,17 @@ describe("OpenRouterHandler", () => {
|
|
|
"OpenRouter API Error 429: Rate limit exceeded",
|
|
"OpenRouter API Error 429: Rate limit exceeded",
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- // Verify telemetry was NOT captured for 429 errors
|
|
|
|
|
- expect(mockCaptureException).not.toHaveBeenCalled()
|
|
|
|
|
|
|
+ // captureException is called, but PostHogTelemetryClient filters out 429 errors internally
|
|
|
|
|
+ expect(mockCaptureException).toHaveBeenCalledWith(
|
|
|
|
|
+ expect.objectContaining({
|
|
|
|
|
+ message: "Rate limit exceeded",
|
|
|
|
|
+ provider: "OpenRouter",
|
|
|
|
|
+ modelId: mockOptions.openRouterModelId,
|
|
|
|
|
+ operation: "completePrompt",
|
|
|
|
|
+ errorCode: 429,
|
|
|
|
|
+ status: 429,
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|