Skip to content

Commit 2e32d9f

Browse files
committed
feat: add Venice AI as a provider
Adds Venice AI (https://venice.ai) as a new OpenAI-compatible provider for private inference. Includes modern models like glm-4-32b, trinity-v1, deepseek-r1-671b, deepseek-v3-0324, qwen-2.5-coder-32b, and llama-3.3-70b. Closes #12106
1 parent 8b12f21 commit 2e32d9f

File tree

16 files changed

+341
-0
lines changed

16 files changed

+341
-0
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ export const SECRET_STATE_KEYS = [
276276
"codebaseIndexVercelAiGatewayApiKey",
277277
"codebaseIndexOpenRouterApiKey",
278278
"sambaNovaApiKey",
279+
"veniceApiKey",
279280
"zaiApiKey",
280281
"fireworksApiKey",
281282
"vercelAiGatewayApiKey",

packages/types/src/provider-settings.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
xaiModels,
2121
internationalZAiModels,
2222
minimaxModels,
23+
veniceModels,
2324
} from "./providers/index.js"
2425

2526
/**
@@ -126,6 +127,7 @@ export const providerNames = [
126127
"roo",
127128
"sambanova",
128129
"vertex",
130+
"venice",
129131
"xai",
130132
"zai",
131133
] as const
@@ -372,6 +374,10 @@ const zaiSchema = apiModelIdProviderModelSchema.extend({
372374
zaiApiLine: zaiApiLineSchema.optional(),
373375
})
374376

377+
const veniceSchema = apiModelIdProviderModelSchema.extend({
378+
veniceApiKey: z.string().optional(),
379+
})
380+
375381
const fireworksSchema = apiModelIdProviderModelSchema.extend({
376382
fireworksApiKey: z.string().optional(),
377383
})
@@ -427,6 +433,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
427433
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
428434
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
429435
rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
436+
veniceSchema.merge(z.object({ apiProvider: z.literal("venice") })),
430437
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
431438
defaultSchema,
432439
])
@@ -461,6 +468,7 @@ export const providerSettingsSchema = z.object({
461468
...fireworksSchema.shape,
462469
...qwenCodeSchema.shape,
463470
...rooSchema.shape,
471+
...veniceSchema.shape,
464472
...vercelAiGatewaySchema.shape,
465473
...codebaseIndexProviderSchema.shape,
466474
})
@@ -529,6 +537,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
529537
"qwen-code": "apiModelId",
530538
requesty: "requestyModelId",
531539
unbound: "unboundModelId",
540+
venice: "apiModelId",
532541
xai: "apiModelId",
533542
baseten: "apiModelId",
534543
litellm: "litellmModelId",
@@ -643,6 +652,7 @@ export const MODELS_BY_PROVIDER: Record<
643652
label: "VS Code LM API",
644653
models: Object.keys(vscodeLlmModels),
645654
},
655+
venice: { id: "venice", label: "Venice AI", models: Object.keys(veniceModels) },
646656
xai: { id: "xai", label: "xAI (Grok)", models: Object.keys(xaiModels) },
647657
zai: { id: "zai", label: "Z.ai", models: Object.keys(internationalZAiModels) },
648658
baseten: { id: "baseten", label: "Baseten", models: Object.keys(basetenModels) },

packages/types/src/providers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from "./requesty.js"
1919
export * from "./roo.js"
2020
export * from "./sambanova.js"
2121
export * from "./unbound.js"
22+
export * from "./venice.js"
2223
export * from "./vertex.js"
2324
export * from "./vscode-llm.js"
2425
export * from "./xai.js"
@@ -43,6 +44,7 @@ import { requestyDefaultModelId } from "./requesty.js"
4344
import { rooDefaultModelId } from "./roo.js"
4445
import { sambaNovaDefaultModelId } from "./sambanova.js"
4546
import { unboundDefaultModelId } from "./unbound.js"
47+
import { veniceDefaultModelId } from "./venice.js"
4648
import { vertexDefaultModelId } from "./vertex.js"
4749
import { vscodeLlmDefaultModelId } from "./vscode-llm.js"
4850
import { xaiDefaultModelId } from "./xai.js"
@@ -113,6 +115,8 @@ export function getProviderDefaultModelId(
113115
return poeDefaultModelId
114116
case "unbound":
115117
return unboundDefaultModelId
118+
case "venice":
119+
return veniceDefaultModelId
116120
case "vercel-ai-gateway":
117121
return vercelAiGatewayDefaultModelId
118122
case "anthropic":
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { ModelInfo } from "../model.js"
2+
3+
// Venice AI
4+
// https://docs.venice.ai/api-reference/chat-completions
5+
export type VeniceModelId =
6+
| "glm-4-32b"
7+
| "trinity-v1"
8+
| "deepseek-r1-671b"
9+
| "deepseek-v3-0324"
10+
| "qwen-2.5-coder-32b"
11+
| "llama-3.3-70b"
12+
13+
export const veniceDefaultModelId: VeniceModelId = "glm-4-32b"
14+
15+
export const veniceDefaultModelInfo: ModelInfo = {
16+
maxTokens: 8192,
17+
contextWindow: 32768,
18+
supportsImages: false,
19+
supportsPromptCache: false,
20+
inputPrice: 0,
21+
outputPrice: 0,
22+
description: "GLM-4 32B model via Venice AI with private inference.",
23+
}
24+
25+
export const veniceModels = {
26+
"glm-4-32b": {
27+
maxTokens: 8192,
28+
contextWindow: 32768,
29+
supportsImages: false,
30+
supportsPromptCache: false,
31+
inputPrice: 0,
32+
outputPrice: 0,
33+
description: "GLM-4 32B model via Venice AI with private inference.",
34+
},
35+
"trinity-v1": {
36+
maxTokens: 8192,
37+
contextWindow: 32768,
38+
supportsImages: false,
39+
supportsPromptCache: false,
40+
inputPrice: 0,
41+
outputPrice: 0,
42+
description: "Venice Trinity V1 model with private inference.",
43+
},
44+
"deepseek-r1-671b": {
45+
maxTokens: 8192,
46+
contextWindow: 65536,
47+
supportsImages: false,
48+
supportsPromptCache: false,
49+
supportsReasoningBudget: true,
50+
inputPrice: 0,
51+
outputPrice: 0,
52+
description: "DeepSeek R1 671B reasoning model via Venice AI.",
53+
},
54+
"deepseek-v3-0324": {
55+
maxTokens: 8192,
56+
contextWindow: 65536,
57+
supportsImages: false,
58+
supportsPromptCache: false,
59+
inputPrice: 0,
60+
outputPrice: 0,
61+
description: "DeepSeek V3 0324 model via Venice AI.",
62+
},
63+
"qwen-2.5-coder-32b": {
64+
maxTokens: 8192,
65+
contextWindow: 32768,
66+
supportsImages: false,
67+
supportsPromptCache: false,
68+
inputPrice: 0,
69+
outputPrice: 0,
70+
description: "Qwen 2.5 Coder 32B model via Venice AI.",
71+
},
72+
"llama-3.3-70b": {
73+
maxTokens: 8192,
74+
contextWindow: 131072,
75+
supportsImages: false,
76+
supportsPromptCache: false,
77+
inputPrice: 0,
78+
outputPrice: 0,
79+
description: "Meta Llama 3.3 70B model via Venice AI.",
80+
},
81+
} as const satisfies Record<string, ModelInfo>

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
VsCodeLmHandler,
2424
RequestyHandler,
2525
UnboundHandler,
26+
VeniceHandler,
2627
FakeAIHandler,
2728
XAIHandler,
2829
LiteLLMHandler,
@@ -177,6 +178,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
177178
return new MiniMaxHandler(options)
178179
case "baseten":
179180
return new BasetenHandler(options)
181+
case "venice":
182+
return new VeniceHandler(options)
180183
case "poe":
181184
return new PoeHandler(options)
182185
default:
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// npx vitest run src/api/providers/__tests__/venice.spec.ts
2+
3+
import OpenAI from "openai"
4+
import { Anthropic } from "@anthropic-ai/sdk"
5+
6+
import { type VeniceModelId, veniceDefaultModelId, veniceModels } from "@roo-code/types"
7+
8+
import { VeniceHandler } from "../venice"
9+
10+
vitest.mock("openai", () => {
11+
const createMock = vitest.fn()
12+
return {
13+
default: vitest.fn(() => ({ chat: { completions: { create: createMock } } })),
14+
}
15+
})
16+
17+
describe("VeniceHandler", () => {
18+
let handler: VeniceHandler
19+
let mockCreate: any
20+
21+
beforeEach(() => {
22+
vitest.clearAllMocks()
23+
mockCreate = (OpenAI as unknown as any)().chat.completions.create
24+
handler = new VeniceHandler({ veniceApiKey: "test-venice-api-key" })
25+
})
26+
27+
it("should use the correct Venice base URL", () => {
28+
new VeniceHandler({ veniceApiKey: "test-venice-api-key" })
29+
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.venice.ai/api/v1" }))
30+
})
31+
32+
it("should use the provided API key", () => {
33+
const veniceApiKey = "test-venice-api-key"
34+
new VeniceHandler({ veniceApiKey })
35+
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: veniceApiKey }))
36+
})
37+
38+
it("should return default model when no model is specified", () => {
39+
const model = handler.getModel()
40+
expect(model.id).toBe(veniceDefaultModelId)
41+
expect(model.info).toEqual(veniceModels[veniceDefaultModelId])
42+
})
43+
44+
it("should return specified model when valid model is provided", () => {
45+
const testModelId: VeniceModelId = "deepseek-r1-671b"
46+
const handlerWithModel = new VeniceHandler({
47+
apiModelId: testModelId,
48+
veniceApiKey: "test-venice-api-key",
49+
})
50+
const model = handlerWithModel.getModel()
51+
expect(model.id).toBe(testModelId)
52+
expect(model.info).toEqual(veniceModels[testModelId])
53+
})
54+
55+
it("completePrompt method should return text from Venice API", async () => {
56+
const expectedResponse = "This is a test response from Venice"
57+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
58+
const result = await handler.completePrompt("test prompt")
59+
expect(result).toBe(expectedResponse)
60+
})
61+
62+
it("should handle errors in completePrompt", async () => {
63+
const errorMessage = "Venice API error"
64+
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
65+
await expect(handler.completePrompt("test prompt")).rejects.toThrow(`Venice completion error: ${errorMessage}`)
66+
})
67+
68+
it("createMessage should yield text content from stream", async () => {
69+
const testContent = "This is test content from Venice stream"
70+
71+
mockCreate.mockImplementationOnce(() => {
72+
return {
73+
[Symbol.asyncIterator]: () => ({
74+
next: vitest
75+
.fn()
76+
.mockResolvedValueOnce({
77+
done: false,
78+
value: { choices: [{ delta: { content: testContent } }] },
79+
})
80+
.mockResolvedValueOnce({ done: true }),
81+
}),
82+
}
83+
})
84+
85+
const stream = handler.createMessage("system prompt", [])
86+
const firstChunk = await stream.next()
87+
88+
expect(firstChunk.done).toBe(false)
89+
expect(firstChunk.value).toEqual({ type: "text", text: testContent })
90+
})
91+
92+
it("createMessage should yield usage data from stream", async () => {
93+
mockCreate.mockImplementationOnce(() => {
94+
return {
95+
[Symbol.asyncIterator]: () => ({
96+
next: vitest
97+
.fn()
98+
.mockResolvedValueOnce({
99+
done: false,
100+
value: {
101+
choices: [{ delta: { content: "" } }],
102+
usage: { prompt_tokens: 10, completion_tokens: 5 },
103+
},
104+
})
105+
.mockResolvedValueOnce({ done: true }),
106+
}),
107+
}
108+
})
109+
110+
const stream = handler.createMessage("system prompt", [])
111+
const firstChunk = await stream.next()
112+
113+
expect(firstChunk.done).toBe(false)
114+
expect(firstChunk.value).toEqual(
115+
expect.objectContaining({
116+
type: "usage",
117+
inputTokens: 10,
118+
outputTokens: 5,
119+
}),
120+
)
121+
})
122+
123+
it("should pass the correct parameters to OpenAI API", async () => {
124+
mockCreate.mockImplementationOnce(() => {
125+
return {
126+
[Symbol.asyncIterator]: () => ({
127+
next: vitest.fn().mockResolvedValueOnce({ done: true }),
128+
}),
129+
}
130+
})
131+
132+
const systemPrompt = "You are a helpful assistant"
133+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
134+
135+
const stream = handler.createMessage(systemPrompt, messages)
136+
137+
// Consume the stream
138+
const results = []
139+
for await (const chunk of stream) {
140+
results.push(chunk)
141+
}
142+
143+
const callArgs = mockCreate.mock.calls[0][0]
144+
expect(callArgs.model).toBe(veniceDefaultModelId)
145+
expect(callArgs.stream).toBe(true)
146+
})
147+
})

src/api/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export { QwenCodeHandler } from "./qwen-code"
1919
export { RequestyHandler } from "./requesty"
2020
export { SambaNovaHandler } from "./sambanova"
2121
export { UnboundHandler } from "./unbound"
22+
export { VeniceHandler } from "./venice"
2223
export { VertexHandler } from "./vertex"
2324
export { VsCodeLmHandler } from "./vscode-lm"
2425
export { XAIHandler } from "./xai"

src/api/providers/venice.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type VeniceModelId, veniceDefaultModelId, veniceModels } from "@roo-code/types"
2+
3+
import type { ApiHandlerOptions } from "../../shared/api"
4+
5+
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
6+
7+
export class VeniceHandler extends BaseOpenAiCompatibleProvider<VeniceModelId> {
8+
constructor(options: ApiHandlerOptions) {
9+
super({
10+
...options,
11+
providerName: "Venice",
12+
baseURL: "https://api.venice.ai/api/v1",
13+
apiKey: options.veniceApiKey,
14+
defaultProviderModelId: veniceDefaultModelId,
15+
providerModels: veniceModels,
16+
})
17+
}
18+
}

src/shared/ProfileValidator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class ProfileValidator {
6262
case "deepseek":
6363
case "xai":
6464
case "sambanova":
65+
case "venice":
6566
case "fireworks":
6667
return profile.apiModelId
6768
case "litellm":

0 commit comments

Comments
 (0)