diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 288f6c2118c..418bec5c9c5 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -276,6 +276,7 @@ export const SECRET_STATE_KEYS = [ "codebaseIndexVercelAiGatewayApiKey", "codebaseIndexOpenRouterApiKey", "sambaNovaApiKey", + "veniceApiKey", "zaiApiKey", "fireworksApiKey", "vercelAiGatewayApiKey", diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 43135577e16..b2ca793e506 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -20,6 +20,7 @@ import { xaiModels, internationalZAiModels, minimaxModels, + veniceModels, } from "./providers/index.js" /** @@ -126,6 +127,7 @@ export const providerNames = [ "roo", "sambanova", "vertex", + "venice", "xai", "zai", ] as const @@ -372,6 +374,10 @@ const zaiSchema = apiModelIdProviderModelSchema.extend({ zaiApiLine: zaiApiLineSchema.optional(), }) +const veniceSchema = apiModelIdProviderModelSchema.extend({ + veniceApiKey: z.string().optional(), +}) + const fireworksSchema = apiModelIdProviderModelSchema.extend({ fireworksApiKey: z.string().optional(), }) @@ -427,6 +433,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })), qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), + veniceSchema.merge(z.object({ apiProvider: z.literal("venice") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), defaultSchema, ]) @@ -461,6 +468,7 @@ export const providerSettingsSchema = z.object({ ...fireworksSchema.shape, ...qwenCodeSchema.shape, ...rooSchema.shape, + ...veniceSchema.shape, ...vercelAiGatewaySchema.shape, ...codebaseIndexProviderSchema.shape, }) @@ -529,6 +537,7 @@ export const modelIdKeysByProvider: Record = { "qwen-code": "apiModelId", requesty: "requestyModelId", unbound: "unboundModelId", + venice: "apiModelId", xai: "apiModelId", baseten: "apiModelId", litellm: "litellmModelId", @@ -643,6 +652,7 @@ export const MODELS_BY_PROVIDER: Record< label: "VS Code LM API", models: Object.keys(vscodeLlmModels), }, + venice: { id: "venice", label: "Venice AI", models: Object.keys(veniceModels) }, xai: { id: "xai", label: "xAI (Grok)", models: Object.keys(xaiModels) }, zai: { id: "zai", label: "Z.ai", models: Object.keys(internationalZAiModels) }, baseten: { id: "baseten", label: "Baseten", models: Object.keys(basetenModels) }, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 6c180d5dda4..1e336dd54a0 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -19,6 +19,7 @@ export * from "./requesty.js" export * from "./roo.js" export * from "./sambanova.js" export * from "./unbound.js" +export * from "./venice.js" export * from "./vertex.js" export * from "./vscode-llm.js" export * from "./xai.js" @@ -43,6 +44,7 @@ import { requestyDefaultModelId } from "./requesty.js" import { rooDefaultModelId } from "./roo.js" import { sambaNovaDefaultModelId } from "./sambanova.js" import { unboundDefaultModelId } from "./unbound.js" +import { veniceDefaultModelId } from "./venice.js" import { vertexDefaultModelId } from "./vertex.js" import { vscodeLlmDefaultModelId } from "./vscode-llm.js" import { xaiDefaultModelId } from "./xai.js" @@ -113,6 +115,8 @@ export function getProviderDefaultModelId( return poeDefaultModelId case "unbound": return unboundDefaultModelId + case "venice": + return veniceDefaultModelId case "vercel-ai-gateway": return vercelAiGatewayDefaultModelId case "anthropic": diff --git a/packages/types/src/providers/venice.ts b/packages/types/src/providers/venice.ts new file mode 100644 index 00000000000..384e6feb1db --- /dev/null +++ b/packages/types/src/providers/venice.ts @@ -0,0 +1,81 @@ +import type { ModelInfo } from "../model.js" + +// Venice AI +// https://docs.venice.ai/api-reference/chat-completions +export type VeniceModelId = + | "glm-4-32b" + | "trinity-v1" + | "deepseek-r1-671b" + | "deepseek-v3-0324" + | "qwen-2.5-coder-32b" + | "llama-3.3-70b" + +export const veniceDefaultModelId: VeniceModelId = "glm-4-32b" + +export const veniceDefaultModelInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "GLM-4 32B model via Venice AI with private inference.", +} + +export const veniceModels = { + "glm-4-32b": { + maxTokens: 8192, + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "GLM-4 32B model via Venice AI with private inference.", + }, + "trinity-v1": { + maxTokens: 8192, + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Venice Trinity V1 model with private inference.", + }, + "deepseek-r1-671b": { + maxTokens: 8192, + contextWindow: 65536, + supportsImages: false, + supportsPromptCache: false, + supportsReasoningBudget: true, + inputPrice: 0, + outputPrice: 0, + description: "DeepSeek R1 671B reasoning model via Venice AI.", + }, + "deepseek-v3-0324": { + maxTokens: 8192, + contextWindow: 65536, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "DeepSeek V3 0324 model via Venice AI.", + }, + "qwen-2.5-coder-32b": { + maxTokens: 8192, + contextWindow: 32768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Qwen 2.5 Coder 32B model via Venice AI.", + }, + "llama-3.3-70b": { + maxTokens: 8192, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Meta Llama 3.3 70B model via Venice AI.", + }, +} as const satisfies Record diff --git a/src/api/index.ts b/src/api/index.ts index 1891113c03b..f95ac942fa7 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -23,6 +23,7 @@ import { VsCodeLmHandler, RequestyHandler, UnboundHandler, + VeniceHandler, FakeAIHandler, XAIHandler, LiteLLMHandler, @@ -177,6 +178,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new MiniMaxHandler(options) case "baseten": return new BasetenHandler(options) + case "venice": + return new VeniceHandler(options) case "poe": return new PoeHandler(options) default: diff --git a/src/api/providers/__tests__/venice.spec.ts b/src/api/providers/__tests__/venice.spec.ts new file mode 100644 index 00000000000..39579fc2c3d --- /dev/null +++ b/src/api/providers/__tests__/venice.spec.ts @@ -0,0 +1,147 @@ +// npx vitest run src/api/providers/__tests__/venice.spec.ts + +import OpenAI from "openai" +import { Anthropic } from "@anthropic-ai/sdk" + +import { type VeniceModelId, veniceDefaultModelId, veniceModels } from "@roo-code/types" + +import { VeniceHandler } from "../venice" + +vitest.mock("openai", () => { + const createMock = vitest.fn() + return { + default: vitest.fn(() => ({ chat: { completions: { create: createMock } } })), + } +}) + +describe("VeniceHandler", () => { + let handler: VeniceHandler + let mockCreate: any + + beforeEach(() => { + vitest.clearAllMocks() + mockCreate = (OpenAI as unknown as any)().chat.completions.create + handler = new VeniceHandler({ veniceApiKey: "test-venice-api-key" }) + }) + + it("should use the correct Venice base URL", () => { + new VeniceHandler({ veniceApiKey: "test-venice-api-key" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.venice.ai/api/v1" })) + }) + + it("should use the provided API key", () => { + const veniceApiKey = "test-venice-api-key" + new VeniceHandler({ veniceApiKey }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: veniceApiKey })) + }) + + it("should return default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(veniceDefaultModelId) + expect(model.info).toEqual(veniceModels[veniceDefaultModelId]) + }) + + it("should return specified model when valid model is provided", () => { + const testModelId: VeniceModelId = "deepseek-r1-671b" + const handlerWithModel = new VeniceHandler({ + apiModelId: testModelId, + veniceApiKey: "test-venice-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(veniceModels[testModelId]) + }) + + it("completePrompt method should return text from Venice API", async () => { + const expectedResponse = "This is a test response from Venice" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "Venice API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow(`Venice completion error: ${errorMessage}`) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from Venice stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest + .fn() + .mockResolvedValueOnce({ + done: false, + value: { + choices: [{ delta: { content: "" } }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual( + expect.objectContaining({ + type: "usage", + inputTokens: 10, + outputTokens: 5, + }), + ) + }) + + it("should pass the correct parameters to OpenAI API", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vitest.fn().mockResolvedValueOnce({ done: true }), + }), + } + }) + + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] + + const stream = handler.createMessage(systemPrompt, messages) + + // Consume the stream + const results = [] + for await (const chunk of stream) { + results.push(chunk) + } + + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.model).toBe(veniceDefaultModelId) + expect(callArgs.stream).toBe(true) + }) +}) diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 41aff953d43..c162f741fb1 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -19,6 +19,7 @@ export { QwenCodeHandler } from "./qwen-code" export { RequestyHandler } from "./requesty" export { SambaNovaHandler } from "./sambanova" export { UnboundHandler } from "./unbound" +export { VeniceHandler } from "./venice" export { VertexHandler } from "./vertex" export { VsCodeLmHandler } from "./vscode-lm" export { XAIHandler } from "./xai" diff --git a/src/api/providers/venice.ts b/src/api/providers/venice.ts new file mode 100644 index 00000000000..5acf9b78fdc --- /dev/null +++ b/src/api/providers/venice.ts @@ -0,0 +1,18 @@ +import { type VeniceModelId, veniceDefaultModelId, veniceModels } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" + +export class VeniceHandler extends BaseOpenAiCompatibleProvider { + constructor(options: ApiHandlerOptions) { + super({ + ...options, + providerName: "Venice", + baseURL: "https://api.venice.ai/api/v1", + apiKey: options.veniceApiKey, + defaultProviderModelId: veniceDefaultModelId, + providerModels: veniceModels, + }) + } +} diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 7246a90177a..b19d78ac204 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -62,6 +62,7 @@ export class ProfileValidator { case "deepseek": case "xai": case "sambanova": + case "venice": case "fireworks": return profile.apiModelId case "litellm": diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index a6e4cc3f5f6..893291854d0 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -33,6 +33,7 @@ import { vercelAiGatewayDefaultModelId, minimaxDefaultModelId, unboundDefaultModelId, + veniceDefaultModelId, } from "@roo-code/types" import { @@ -87,6 +88,7 @@ import { Roo, SambaNova, Unbound, + Venice, Vertex, VSCodeLM, XAI, @@ -352,6 +354,7 @@ const ApiOptions = ({ bedrock: { field: "apiModelId", default: bedrockDefaultModelId }, vertex: { field: "apiModelId", default: vertexDefaultModelId }, sambanova: { field: "apiModelId", default: sambaNovaDefaultModelId }, + venice: { field: "apiModelId", default: veniceDefaultModelId }, zai: { field: "apiModelId", default: @@ -686,6 +689,13 @@ const ApiOptions = ({ /> )} + {selectedProvider === "venice" && ( + + )} + {selectedProvider === "zai" && ( )} diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 14f04cb5b22..85fe70cfd5f 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -17,6 +17,7 @@ import { fireworksModels, minimaxModels, basetenModels, + veniceModels, } from "@roo-code/types" export const MODELS_BY_PROVIDER: Partial>> = { @@ -36,6 +37,7 @@ export const MODELS_BY_PROVIDER: Partial a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/Venice.tsx b/webview-ui/src/components/settings/providers/Venice.tsx new file mode 100644 index 00000000000..a62376bc5dc --- /dev/null +++ b/webview-ui/src/components/settings/providers/Venice.tsx @@ -0,0 +1,50 @@ +import { useCallback } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import type { ProviderSettings } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { inputEventTransform } from "../transforms" + +type VeniceProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void +} + +export const Venice = ({ apiConfiguration, setApiConfigurationField }: VeniceProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.veniceApiKey && ( + + {t("settings:providers.getVeniceApiKey")} + + )} + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 4a64ce9586b..f9f167632e2 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -16,6 +16,7 @@ export { Roo } from "./Roo" export { Requesty } from "./Requesty" export { SambaNova } from "./SambaNova" export { Unbound } from "./Unbound" +export { Venice } from "./Venice" export { Vertex } from "./Vertex" export { VSCodeLM } from "./VSCodeLM" export { XAI } from "./XAI" diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index 59f76862b45..ec3c434239c 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -11,6 +11,7 @@ import { vertexDefaultModelId, xaiDefaultModelId, sambaNovaDefaultModelId, + veniceDefaultModelId, internationalZAiDefaultModelId, mainlandZAiDefaultModelId, fireworksDefaultModelId, @@ -37,6 +38,7 @@ export const PROVIDER_SERVICE_CONFIG: Partial> = vertex: vertexDefaultModelId, xai: xaiDefaultModelId, sambanova: sambaNovaDefaultModelId, + venice: veniceDefaultModelId, zai: internationalZAiDefaultModelId, fireworks: fireworksDefaultModelId, minimax: minimaxDefaultModelId, diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 7192d9d4ee4..0e85654fd36 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -19,6 +19,7 @@ import { vscodeLlmDefaultModelId, openAiCodexModels, sambaNovaModels, + veniceModels, internationalZAiModels, mainlandZAiModels, fireworksModels, @@ -332,6 +333,11 @@ function getSelectedModel({ const info = openAiCodexModels[id as keyof typeof openAiCodexModels] return { id, info } } + case "venice": { + const id = apiConfiguration.apiModelId ?? defaultModelId + const info = veniceModels[id as keyof typeof veniceModels] + return { id, info } + } case "vercel-ai-gateway": { const id = getValidatedModelId( apiConfiguration.vercelAiGatewayModelId, diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 183cd663e31..e42c952501d 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -463,6 +463,8 @@ "geminiApiKey": "Gemini API Key", "getSambaNovaApiKey": "Get SambaNova API Key", "sambaNovaApiKey": "SambaNova API Key", + "veniceApiKey": "Venice AI API Key", + "getVeniceApiKey": "Get Venice AI API Key", "getGeminiApiKey": "Get Gemini API Key", "openAiApiKey": "OpenAI API Key", "apiKey": "API Key",