Skip to content

Commit 4d37e8c

Browse files
committed
functional helicone integration with hardcoded model
1 parent 4e6c717 commit 4d37e8c

File tree

13 files changed

+314
-1
lines changed

13 files changed

+314
-1
lines changed

packages/types/src/provider-settings.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
fireworksModels,
1515
geminiModels,
1616
groqModels,
17+
heliconeModels,
1718
ioIntelligenceModels,
1819
mistralModels,
1920
moonshotModels,
@@ -129,6 +130,7 @@ export const providerNames = [
129130
"gemini",
130131
"gemini-cli",
131132
"groq",
133+
"helicone",
132134
"mistral",
133135
"moonshot",
134136
"openai-native",
@@ -213,6 +215,13 @@ const openRouterSchema = baseProviderSettingsSchema.extend({
213215
openRouterUseMiddleOutTransform: z.boolean().optional(),
214216
})
215217

218+
const heliconeSchema = baseProviderSettingsSchema.extend({
219+
heliconeApiKey: z.string().optional(),
220+
heliconeModelId: z.string().optional(),
221+
heliconeBaseUrl: z.string().optional(),
222+
heliconeSpecificProvider: z.string().optional(),
223+
})
224+
216225
const bedrockSchema = apiModelIdProviderModelSchema.extend({
217226
awsAccessKey: z.string().optional(),
218227
awsSecretKey: z.string().optional(),
@@ -421,6 +430,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
421430
claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })),
422431
glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })),
423432
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
433+
heliconeSchema.merge(z.object({ apiProvider: z.literal("helicone") })),
424434
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
425435
vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })),
426436
openAiSchema.merge(z.object({ apiProvider: z.literal("openai") })),
@@ -462,6 +472,7 @@ export const providerSettingsSchema = z.object({
462472
...claudeCodeSchema.shape,
463473
...glamaSchema.shape,
464474
...openRouterSchema.shape,
475+
...heliconeSchema.shape,
465476
...bedrockSchema.shape,
466477
...vertexSchema.shape,
467478
...openAiSchema.shape,
@@ -517,6 +528,7 @@ export const modelIdKeys = [
517528
"apiModelId",
518529
"glamaModelId",
519530
"openRouterModelId",
531+
"heliconeModelId",
520532
"openAiModelId",
521533
"ollamaModelId",
522534
"lmStudioModelId",
@@ -551,6 +563,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
551563
"claude-code": "apiModelId",
552564
glama: "glamaModelId",
553565
openrouter: "openRouterModelId",
566+
helicone: "apiModelId",
554567
bedrock: "apiModelId",
555568
vertex: "apiModelId",
556569
"openai-native": "openAiModelId",
@@ -656,6 +669,7 @@ export const MODELS_BY_PROVIDER: Record<
656669
models: Object.keys(geminiModels),
657670
},
658671
groq: { id: "groq", label: "Groq", models: Object.keys(groqModels) },
672+
helicone: { id: "helicone", label: "Helicone", models: Object.keys(heliconeModels) },
659673
"io-intelligence": {
660674
id: "io-intelligence",
661675
label: "IO Intelligence",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { ModelInfo } from "../model.js"
2+
3+
// Helicone is OpenAI-compatible and uses name/provider model IDs.
4+
// TODO [HELICONE]: change this to claude-4.5-sonnet/anthropic as Roo Code is optimized for that
5+
export const heliconeDefaultModelId = "gpt-4o/openai"
6+
7+
export const heliconeDefaultModelInfo: ModelInfo = {
8+
maxTokens: 16_384,
9+
contextWindow: 128_000,
10+
supportsImages: true,
11+
supportsPromptCache: true,
12+
inputPrice: 5.0,
13+
outputPrice: 20.0,
14+
cacheReadsPrice: 2.5,
15+
description: "GPT-4o via Helicone AI Gateway.",
16+
}
17+
18+
export const heliconeModels = {
19+
"gpt-4o/openai": heliconeDefaultModelInfo,
20+
} as const

packages/types/src/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from "./moonshot.js"
1919
export * from "./ollama.js"
2020
export * from "./openai.js"
2121
export * from "./openrouter.js"
22+
export * from "./helicone.js"
2223
export * from "./qwen-code.js"
2324
export * from "./requesty.js"
2425
export * from "./roo.js"

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
FeatherlessHandler,
4141
VercelAiGatewayHandler,
4242
DeepInfraHandler,
43+
HeliconeHandler,
4344
} from "./providers"
4445
import { NativeOllamaHandler } from "./providers/native-ollama"
4546

@@ -99,6 +100,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
99100
return new GlamaHandler(options)
100101
case "openrouter":
101102
return new OpenRouterHandler(options)
103+
case "helicone":
104+
return new HeliconeHandler(options)
102105
case "bedrock":
103106
return new AwsBedrockHandler(options)
104107
case "vertex":

src/api/providers/helicone.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { Anthropic } from "@anthropic-ai/sdk"
2+
import OpenAI from "openai"
3+
4+
import {
5+
heliconeDefaultModelId,
6+
heliconeDefaultModelInfo,
7+
heliconeModels,
8+
DEEP_SEEK_DEFAULT_TEMPERATURE,
9+
} from "@roo-code/types"
10+
11+
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
12+
13+
import { convertToOpenAiMessages } from "../transform/openai-format"
14+
import { ApiStreamChunk } from "../transform/stream"
15+
import { convertToR1Format } from "../transform/r1-format"
16+
import { getModelParams } from "../transform/model-params"
17+
18+
import { DEFAULT_HEADERS } from "./constants"
19+
import { BaseProvider } from "./base-provider"
20+
import type { SingleCompletionHandler } from "../index"
21+
import { handleOpenAIError } from "./utils/openai-error-handler"
22+
23+
export class HeliconeHandler extends BaseProvider implements SingleCompletionHandler {
24+
protected options: ApiHandlerOptions
25+
private client: OpenAI
26+
protected models: ModelRecord = {}
27+
private readonly providerName = "Helicone"
28+
29+
constructor(options: ApiHandlerOptions) {
30+
super()
31+
this.options = options
32+
33+
const baseURL = this.options.heliconeBaseUrl || "https://ai-gateway.helicone.ai/v1"
34+
const apiKey = this.options.heliconeApiKey ?? "not-provided"
35+
36+
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS })
37+
}
38+
39+
override async *createMessage(
40+
systemPrompt: string,
41+
messages: Anthropic.Messages.MessageParam[],
42+
): AsyncGenerator<ApiStreamChunk> {
43+
const model = await this.fetchModel()
44+
45+
let { id: modelId, maxTokens, temperature } = model
46+
47+
// Convert Anthropic messages to OpenAI format.
48+
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
49+
{ role: "system", content: systemPrompt },
50+
...convertToOpenAiMessages(messages),
51+
]
52+
53+
// DeepSeek and similar reasoning models recommend using user instead of system role.
54+
if (this.isDeepSeekR1(modelId) || this.isPerplexityReasoning(modelId)) {
55+
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
56+
// DeepSeek recommended default temperature
57+
temperature = this.options.modelTemperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE
58+
}
59+
60+
// TODO [HELICONE]: add automatic gemini/anthropic cache breakpoints
61+
62+
const completionParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
63+
model: modelId,
64+
...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),
65+
temperature,
66+
messages: openAiMessages,
67+
stream: true,
68+
stream_options: { include_usage: true },
69+
}
70+
71+
let stream
72+
try {
73+
stream = await this.client.chat.completions.create(completionParams)
74+
} catch (error) {
75+
throw handleOpenAIError(error, this.providerName)
76+
}
77+
78+
let lastUsage: any | undefined = undefined
79+
80+
for await (const chunk of stream) {
81+
const delta = chunk.choices[0]?.delta
82+
83+
if (
84+
"reasoning" in (delta || {}) &&
85+
(delta as any).reasoning &&
86+
typeof (delta as any).reasoning === "string"
87+
) {
88+
yield { type: "reasoning", text: (delta as any).reasoning as string }
89+
}
90+
91+
if (delta?.content) {
92+
yield { type: "text", text: delta.content }
93+
}
94+
95+
if (chunk.usage) {
96+
lastUsage = chunk.usage
97+
}
98+
}
99+
100+
if (lastUsage) {
101+
yield {
102+
type: "usage",
103+
inputTokens: lastUsage.prompt_tokens || 0,
104+
outputTokens: lastUsage.completion_tokens || 0,
105+
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
106+
reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,
107+
}
108+
}
109+
}
110+
111+
public async fetchModel() {
112+
this.models = heliconeModels as unknown as ModelRecord
113+
return this.getModel()
114+
}
115+
116+
override getModel() {
117+
const id = this.options.apiModelId ?? heliconeDefaultModelId
118+
const info = this.models[id] ?? heliconeDefaultModelInfo
119+
120+
const params = getModelParams({
121+
format: "openai",
122+
modelId: id,
123+
model: info,
124+
settings: this.options,
125+
defaultTemperature:
126+
this.isDeepSeekR1(id) || this.isPerplexityReasoning(id) ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0,
127+
})
128+
129+
// Apply a small topP tweak for DeepSeek-style reasoning models
130+
const topP = this.isDeepSeekR1(id) || this.isPerplexityReasoning(id) ? 0.95 : undefined
131+
return { id, info, topP, ...params }
132+
}
133+
134+
async completePrompt(prompt: string) {
135+
let { id: modelId, maxTokens, temperature } = await this.fetchModel()
136+
137+
const completionParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
138+
model: modelId,
139+
...(maxTokens && maxTokens > 0 && { max_tokens: maxTokens }),
140+
temperature,
141+
messages: [{ role: "user", content: prompt }],
142+
stream: false,
143+
}
144+
145+
let response
146+
try {
147+
response = await this.client.chat.completions.create(completionParams)
148+
} catch (error) {
149+
throw handleOpenAIError(error, this.providerName)
150+
}
151+
152+
if ("error" in (response as any)) {
153+
const error = (response as any).error as { message?: string; code?: number }
154+
throw new Error(`Helicone API Error ${error?.code}: ${error?.message}`)
155+
}
156+
157+
const completion = response as OpenAI.Chat.ChatCompletion
158+
return completion.choices[0]?.message?.content || ""
159+
}
160+
161+
private isDeepSeekR1(modelId: string): boolean {
162+
return modelId.includes("deepseek-r1")
163+
}
164+
165+
private isPerplexityReasoning(modelId: string): boolean {
166+
return modelId.includes("sonar-reasoning")
167+
}
168+
}

src/api/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ export { RooHandler } from "./roo"
3434
export { FeatherlessHandler } from "./featherless"
3535
export { VercelAiGatewayHandler } from "./vercel-ai-gateway"
3636
export { DeepInfraHandler } from "./deepinfra"
37+
export { HeliconeHandler } from "./helicone"

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
rooDefaultModelId,
3838
vercelAiGatewayDefaultModelId,
3939
deepInfraDefaultModelId,
40+
heliconeDefaultModelId,
4041
} from "@roo-code/types"
4142

4243
import { vscode } from "@src/utils/vscode"
@@ -83,6 +84,7 @@ import {
8384
OpenAI,
8485
OpenAICompatible,
8586
OpenRouter,
87+
Helicone,
8688
QwenCode,
8789
Requesty,
8890
SambaNova,
@@ -322,6 +324,7 @@ const ApiOptions = ({
322324
> = {
323325
deepinfra: { field: "deepInfraModelId", default: deepInfraDefaultModelId },
324326
openrouter: { field: "openRouterModelId", default: openRouterDefaultModelId },
327+
helicone: { field: "heliconeModelId", default: heliconeDefaultModelId },
325328
glama: { field: "glamaModelId", default: glamaDefaultModelId },
326329
unbound: { field: "unboundModelId", default: unboundDefaultModelId },
327330
requesty: { field: "requestyModelId", default: requestyDefaultModelId },
@@ -470,6 +473,15 @@ const ApiOptions = ({
470473
/>
471474
)}
472475

476+
{selectedProvider === "helicone" && (
477+
<Helicone
478+
apiConfiguration={apiConfiguration}
479+
setApiConfigurationField={setApiConfigurationField}
480+
// TODO [HELICONE]: add router models, selected model id,
481+
// fromWelcomeView
482+
/>
483+
)}
484+
473485
{selectedProvider === "requesty" && (
474486
<Requesty
475487
uriScheme={uriScheme}

webview-ui/src/components/settings/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
fireworksModels,
2222
rooModels,
2323
featherlessModels,
24+
heliconeModels,
2425
} from "@roo-code/types"
2526

2627
export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, ModelInfo>>> = {
@@ -44,9 +45,11 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
4445
fireworks: fireworksModels,
4546
roo: rooModels,
4647
featherless: featherlessModels,
48+
helicone: heliconeModels,
4749
}
4850

4951
export const PROVIDERS = [
52+
{ value: "helicone", label: "Helicone" },
5053
{ value: "openrouter", label: "OpenRouter" },
5154
{ value: "deepinfra", label: "DeepInfra" },
5255
{ value: "anthropic", label: "Anthropic" },

0 commit comments

Comments
 (0)