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

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

import { OpenRouterHandler } from "../openrouter"
import { Package } from "../../../shared/package"
import { makeApiHandlerOptions } from "../../../test-utils/api"
Expand Down Expand Up @@ -349,7 +351,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "API Error",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
errorCode: 500,
Expand All @@ -371,7 +373,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Connection failed",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
}),
Expand All @@ -394,7 +396,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rate limit exceeded: free-models-per-day",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
}),
Expand All @@ -415,7 +417,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "429 Rate limit exceeded: free-models-per-day",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
}),
Expand All @@ -436,7 +438,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Request failed due to rate limit",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
}),
Expand All @@ -458,7 +460,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rate limit exceeded",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "createMessage",
errorCode: 429,
Expand Down Expand Up @@ -585,7 +587,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "API Error",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
errorCode: 500,
Expand All @@ -608,7 +610,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Unexpected error",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
}),
Expand All @@ -630,7 +632,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rate limit exceeded: free-models-per-day",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
}),
Expand All @@ -651,7 +653,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "429 Rate limit exceeded: free-models-per-day",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
}),
Expand All @@ -672,7 +674,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Request failed due to rate limit",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
}),
Expand Down Expand Up @@ -701,7 +703,7 @@ describe("OpenRouterHandler", () => {
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rate limit exceeded",
provider: "OpenRouter",
provider: providerIdentifiers.openrouter,
modelId: mockOptions.openRouterModelId,
operation: "completePrompt",
errorCode: 429,
Expand Down
161 changes: 124 additions & 37 deletions src/api/providers/__tests__/poe.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,55 @@
const mockStreamText = vitest.fn()
const mockGenerateText = vitest.fn()
const mockCreatePoe = vitest.fn()
import { poeDefaultModelId, providerIdentifiers } from "@roo-code/types"

import { PoeHandler } from "../poe"
import { getModelsFromCache } from "../fetchers/modelCache"

import { clearAllMocks } from "../../../test-utils/reset"

const { mockStreamText, mockGenerateText, mockCreatePoe, mockGetModelsFromCache, mockCaptureException } =
vitest.hoisted(() => ({
mockStreamText: vitest.fn(),
mockGenerateText: vitest.fn(),
mockCreatePoe: vitest.fn(),
mockCaptureException: vitest.fn(),
mockGetModelsFromCache: vitest.fn(),
}))

const cachedModels = {
"anthropic/claude-sonnet-4": {
maxTokens: 10_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
inputPrice: 3,
outputPrice: 15,
},
"openai/gpt-4o": {
maxTokens: 16_384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.5,
outputPrice: 10,
},
"openai/o3": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
supportsReasoningEffort: ["low", "medium", "high"],
inputPrice: 10,
outputPrice: 40,
},
}

vitest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureException: (...args: unknown[]) => mockCaptureException(...args),
},
},
}))

vitest.mock("ai-sdk-provider-poe", () => ({
createPoe: (...args: unknown[]) => mockCreatePoe(...args),
Expand Down Expand Up @@ -41,48 +90,17 @@ vitest.mock("ai", async (importOriginal) => {
})

vitest.mock("../fetchers/modelCache", () => ({
getModelsFromCache: vitest.fn().mockReturnValue({
"anthropic/claude-sonnet-4": {
maxTokens: 10_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
inputPrice: 3,
outputPrice: 15,
},
"openai/gpt-4o": {
maxTokens: 16_384,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.5,
outputPrice: 10,
},
"openai/o3": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
supportsReasoningEffort: ["low", "medium", "high"],
inputPrice: 10,
outputPrice: 40,
},
}),
getModelsFromCache: mockGetModelsFromCache,
}))

import { poeDefaultModelId } from "@roo-code/types"
import { PoeHandler } from "../poe"

import { clearAllMocks } from "../../../test-utils/reset"

describe("PoeHandler", () => {
const mockLanguageModel = { modelId: "test-model" }
const mockPoeProvider = vitest.fn().mockReturnValue(mockLanguageModel)

beforeEach(() => {
clearAllMocks()
mockCreatePoe.mockReturnValue(mockPoeProvider)
mockGetModelsFromCache.mockReturnValue(cachedModels)
})

describe("constructor", () => {
Expand Down Expand Up @@ -116,9 +134,19 @@ describe("PoeHandler", () => {

describe("getModel", () => {
it("returns model info from cache", () => {
const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "anthropic/claude-sonnet-4" })
const options = {
poeApiKey: "key",
poeBaseUrl: "https://custom.poe.com/v1",
apiModelId: "anthropic/claude-sonnet-4",
}
const handler = new PoeHandler(options)
const result = handler.getModel()

expect(getModelsFromCache).toHaveBeenCalledWith({
provider: providerIdentifiers.poe,
Comment thread
WebMad marked this conversation as resolved.
apiKey: options.poeApiKey,
baseUrl: options.poeBaseUrl,
})
expect(result.id).toBe("anthropic/claude-sonnet-4")
expect(result.info.contextWindow).toBe(200_000)
expect(result.info.maxTokens).toBe(10_000)
Expand Down Expand Up @@ -166,6 +194,49 @@ describe("PoeHandler", () => {
expect(chunks).toContainEqual({ type: "text", text: "world!" })
expect(chunks).toContainEqual(expect.objectContaining({ type: "usage", inputTokens: 10, outputTokens: 5 }))
})

it("reports synchronous completion failures with the canonical provider identifier", async () => {
const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" })
mockStreamText.mockImplementationOnce(() => {
throw new Error("request failed")
})

await expect(
handler.createMessage("system", [{ role: "user" as const, content: "hello" }]).next(),
).rejects.toThrow("Poe completion error: request failed")
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
provider: providerIdentifiers.poe,
modelId: "openai/gpt-4o",
operation: "createMessage",
}),
)
})

it("reports asynchronous stream failures with the canonical provider identifier", async () => {
const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" })
const failedStream = {
[Symbol.asyncIterator]() {
return this
},
next: vitest.fn().mockRejectedValueOnce(new Error("stream failed")),
}
mockStreamText.mockReturnValueOnce({
fullStream: failedStream,
usage: Promise.resolve(undefined),
})

await expect(
handler.createMessage("system", [{ role: "user" as const, content: "hello" }]).next(),
).rejects.toThrow("Poe streaming error: stream failed")
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
provider: providerIdentifiers.poe,
modelId: "openai/gpt-4o",
operation: "createMessage",
}),
)
})
})

describe("reasoning", () => {
Expand Down Expand Up @@ -311,5 +382,21 @@ describe("PoeHandler", () => {
}),
)
})

it("reports failures with the canonical provider identifier", async () => {
const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" })
mockGenerateText.mockRejectedValueOnce(new Error("generation failed"))

await expect(handler.completePrompt("complete this")).rejects.toThrow(
"Poe completion error: generation failed",
)
expect(mockCaptureException).toHaveBeenCalledWith(
expect.objectContaining({
provider: providerIdentifiers.poe,
modelId: "openai/gpt-4o",
operation: "completePrompt",
}),
)
})
})
})
3 changes: 2 additions & 1 deletion src/api/providers/anthropic-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "../../core/prompts/tools/native-tools/converters"

import { BaseProvider } from "./base-provider"
import { NOT_PROVIDED } from "./constants"
import { parseVertexJsonCredentials } from "./utils/vertex-credentials"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index"

Expand All @@ -38,7 +39,7 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
this.options = options

// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
const projectId = this.options.vertexProjectId ?? "not-provided"
const projectId = this.options.vertexProjectId ?? NOT_PROVIDED
const region = this.options.vertexRegion ?? "us-east5"

const parsedVertexCredentials = parseVertexJsonCredentials(this.options.vertexJsonCredentials)
Expand Down
2 changes: 2 additions & 0 deletions src/api/providers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const DEFAULT_HEADERS = {
"X-Title": "Zoo Code",
"User-Agent": `ZooCode/${Package.version}`,
}

export const NOT_PROVIDED = "not-provided"
3 changes: 2 additions & 1 deletion src/api/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { getModelParams } from "../transform/model-params"
import { convertToR1Format } from "../transform/r1-format"

import { OpenAiHandler } from "./openai"
import { NOT_PROVIDED } from "./constants"
import { extractReasoningFromDelta } from "./utils/extract-reasoning"
import type { ApiHandlerCreateMessageMetadata } from "../index"
import { handleOpenAIError } from "./utils/error-handler"
Expand Down Expand Up @@ -84,7 +85,7 @@ export class DeepSeekHandler extends OpenAiHandler {
constructor(options: ApiHandlerOptions) {
super({
...options,
openAiApiKey: options.deepSeekApiKey ?? "not-provided",
openAiApiKey: options.deepSeekApiKey ?? NOT_PROVIDED,
openAiModelId: options.apiModelId ?? deepSeekDefaultModelId,
openAiBaseUrl: options.deepSeekBaseUrl || "https://api.deepseek.com",
openAiStreamingEnabled: true,
Expand Down
Loading
Loading