This repository was archived by the owner on May 15, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathopenai-compatible.ts
More file actions
231 lines (201 loc) · 6.93 KB
/
Copy pathopenai-compatible.ts
File metadata and controls
231 lines (201 loc) · 6.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/**
* OpenAI-compatible provider base class using Vercel AI SDK.
* This provides a parallel implementation to OpenAiHandler using @ai-sdk/openai-compatible.
*/
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import { streamText, generateText, LanguageModel, ToolSet } from "ai"
import type { ModelInfo } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import { convertToAiSdkMessages, convertToolsForAiSdk, processAiSdkStreamPart } from "../transform/ai-sdk"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
/**
* Configuration options for creating an OpenAI-compatible provider.
*/
export interface OpenAICompatibleConfig {
/** Provider name for identification */
providerName: string
/** Base URL for the API endpoint */
baseURL: string
/** API key for authentication */
apiKey: string
/** Model ID to use */
modelId: string
/** Model information */
modelInfo: ModelInfo
/** Optional custom headers */
headers?: Record<string, string>
/** Whether to include max_tokens in requests (default: false uses max_completion_tokens) */
useMaxTokens?: boolean
/** User-configured max tokens override */
modelMaxTokens?: number
/** Temperature setting */
temperature?: number
}
/**
* Base class for OpenAI-compatible API providers using Vercel AI SDK.
* Extends BaseProvider and implements SingleCompletionHandler.
*/
export abstract class OpenAICompatibleHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
protected config: OpenAICompatibleConfig
protected provider: ReturnType<typeof createOpenAICompatible>
constructor(options: ApiHandlerOptions, config: OpenAICompatibleConfig) {
super()
this.options = options
this.config = config
// Create the OpenAI-compatible provider using AI SDK
this.provider = createOpenAICompatible({
name: config.providerName,
baseURL: config.baseURL,
apiKey: config.apiKey,
headers: {
...DEFAULT_HEADERS,
...(config.headers || {}),
},
})
}
/**
* Get the language model for the configured model ID.
*/
protected getLanguageModel(): LanguageModel {
return this.provider(this.config.modelId)
}
/**
* Get the model information. Must be implemented by subclasses.
*/
abstract override getModel(): { id: string; info: ModelInfo; maxTokens?: number; temperature?: number }
/**
* Process usage metrics from the AI SDK response.
* Can be overridden by subclasses to handle provider-specific usage formats.
*/
protected processUsageMetrics(usage: {
inputTokens?: number
outputTokens?: number
details?: {
cachedInputTokens?: number
reasoningTokens?: number
}
raw?: Record<string, unknown>
}): ApiStreamUsageChunk {
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
cacheReadTokens: usage.details?.cachedInputTokens,
reasoningTokens: usage.details?.reasoningTokens,
}
}
/**
* Map OpenAI tool_choice to AI SDK toolChoice format.
*/
protected mapToolChoice(
toolChoice: OpenAI.Chat.ChatCompletionCreateParams["tool_choice"],
): "auto" | "none" | "required" | { type: "tool"; toolName: string } | undefined {
if (!toolChoice) {
return undefined
}
// Handle string values
if (typeof toolChoice === "string") {
switch (toolChoice) {
case "auto":
return "auto"
case "none":
return "none"
case "required":
return "required"
default:
return "auto"
}
}
// Handle object values (OpenAI ChatCompletionNamedToolChoice format)
if (typeof toolChoice === "object" && "type" in toolChoice) {
if (toolChoice.type === "function" && "function" in toolChoice && toolChoice.function?.name) {
return { type: "tool", toolName: toolChoice.function.name }
}
}
return undefined
}
/**
* Get the max tokens parameter to include in the request.
*/
protected getMaxOutputTokens(): number | undefined {
const modelInfo = this.config.modelInfo
const maxTokens = this.config.modelMaxTokens || modelInfo.maxTokens
return maxTokens ?? undefined
}
/**
* Create a message stream using the AI SDK.
*/
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const model = this.getModel()
const languageModel = this.getLanguageModel()
// Convert messages to AI SDK format
const aiSdkMessages = convertToAiSdkMessages(messages)
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
// Build provider options for reasoning_effort (supported by @ai-sdk/openai-compatible)
const modelReasoning = (model as any).reasoning as { reasoning_effort?: string } | undefined
const openaiCompatibleOptions = modelReasoning?.reasoning_effort
? { reasoningEffort: modelReasoning.reasoning_effort }
: undefined
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
messages: aiSdkMessages,
temperature: model.temperature ?? this.config.temperature ?? 0,
maxOutputTokens: this.getMaxOutputTokens(),
tools: aiSdkTools,
toolChoice: this.mapToolChoice(metadata?.tool_choice),
...(openaiCompatibleOptions
? { providerOptions: { openaiCompatible: openaiCompatibleOptions } as any }
: {}),
}
// Use streamText for streaming responses
const result = streamText(requestOptions)
// Process the full stream to get all events
for await (const part of result.fullStream) {
// Use the processAiSdkStreamPart utility to convert stream parts
for (const chunk of processAiSdkStreamPart(part)) {
yield chunk
}
}
// Yield usage metrics at the end
const usage = await result.usage
if (usage) {
yield this.processUsageMetrics(usage)
}
}
/**
* Complete a prompt using the AI SDK generateText.
*/
async completePrompt(prompt: string): Promise<string> {
const languageModel = this.getLanguageModel()
const model = this.getModel()
// Build provider options for reasoning_effort
const modelReasoning = (model as any).reasoning as { reasoning_effort?: string } | undefined
const openaiCompatibleOptions = modelReasoning?.reasoning_effort
? { reasoningEffort: modelReasoning.reasoning_effort }
: undefined
const { text } = await generateText({
model: languageModel,
prompt,
maxOutputTokens: this.getMaxOutputTokens(),
temperature: this.config.temperature ?? 0,
...(openaiCompatibleOptions
? { providerOptions: { openaiCompatible: openaiCompatibleOptions } as any }
: {}),
})
return text
}
}