-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathxai.ts
More file actions
166 lines (142 loc) · 5.45 KB
/
Copy pathxai.ts
File metadata and controls
166 lines (142 loc) · 5.45 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
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { type XAIModelId, xaiDefaultModelId, xaiModels, ApiProviderError } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import type { ApiHandlerOptions } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { convertToResponsesApiInput } from "../transform/responses-api-input"
import { processResponsesApiStream, createUsageNormalizer } from "../transform/responses-api-stream"
import { getModelParams } from "../transform/model-params"
import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { handleOpenAIError } from "./utils/error-handler"
import { isMcpTool } from "../../utils/mcp-name"
const XAI_DEFAULT_TEMPERATURE = 0
export class XAIHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: OpenAI
private readonly providerName = "xAI"
constructor(options: ApiHandlerOptions) {
super()
this.options = options
const apiKey = this.options.xaiApiKey ?? "not-provided"
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: apiKey,
defaultHeaders: DEFAULT_HEADERS,
timeout: this.timeoutMs,
})
}
override getModel() {
const id =
this.options.apiModelId && this.options.apiModelId in xaiModels
? (this.options.apiModelId as XAIModelId)
: xaiDefaultModelId
const info = xaiModels[id]
const params = getModelParams({
format: "openai",
modelId: id,
model: info,
settings: this.options,
defaultTemperature: XAI_DEFAULT_TEMPERATURE,
})
return { id, info, ...params }
}
/**
* Convert tools from OpenAI Chat Completions format to Responses API format.
* Chat Completions: { type: "function", function: { name, description, parameters } }
* Responses API: { type: "function", name, description, parameters }
*
* Uses base provider's convertToolSchemaForOpenAI() for schema hardening
* (additionalProperties: false, ensureAllRequired) and handles MCP tools.
*/
private mapResponseTools(tools?: any[]): any[] | undefined {
const converted = this.convertToolsForOpenAI(tools)
if (!converted?.length) {
return undefined
}
return converted
.filter((tool) => tool?.type === "function")
.map((tool) => {
const isMcp = isMcpTool(tool.function.name)
return {
type: "function",
name: tool.function.name,
description: tool.function.description,
parameters: isMcp
? tool.function.parameters
: this.convertToolSchemaForOpenAI(tool.function.parameters),
strict: !isMcp,
}
})
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const model = this.getModel()
// Convert directly from Anthropic format to Responses API input format
const input = convertToResponsesApiInput(messages)
const responseTools = this.mapResponseTools(metadata?.tools)
// Build request options
const requestBody: Record<string, any> = {
model: model.id,
instructions: systemPrompt,
input: input,
stream: true,
store: false, // Don't store responses server-side for privacy
include: ["reasoning.encrypted_content"],
}
if (model.maxTokens) {
requestBody.max_output_tokens = model.maxTokens
}
if (model.temperature !== undefined) {
requestBody.temperature = model.temperature
}
if (responseTools) {
requestBody.tools = responseTools
// Cast tool_choice since metadata uses Chat Completions types but Responses API has its own type
requestBody.tool_choice = (metadata?.tool_choice ?? "auto") as any
requestBody.parallel_tool_calls = metadata?.parallelToolCalls ?? true
}
// Pass reasoning effort for models that support it (e.g., grok-4.5, grok-3-mini).
// The xAI Responses API uses `reasoning: { effort }` format (not `reasoning_effort`
// which is the Chat Completions format), so we convert from the OpenAI params shape.
if (model.reasoning) {
requestBody.reasoning = { effort: model.reasoning.reasoning_effort }
}
let stream: AsyncIterable<any>
try {
stream = (await this.client.responses.create({
...requestBody,
stream: true,
} as any)) as unknown as AsyncIterable<any>
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
}
const normalizeUsage = createUsageNormalizer()
yield* processResponsesApiStream(stream, normalizeUsage)
}
async completePrompt(prompt: string): Promise<string> {
const model = this.getModel()
try {
const response = await this.client.responses.create({
model: model.id,
input: [{ role: "user", content: [{ type: "input_text", text: prompt }] }],
store: false,
})
// output_text is a convenience field on the Responses API response
return response.output_text || ""
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "completePrompt")
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
}
}
}