Add xAI provider (#2667)

* Add xAI provider

* Add model reasoning effort

* DRY this up

* Handle undefined delta

* Cleanup getModel to fix test

* Add missing translations

* Small type cleanup

* Support temperature

---------

Co-authored-by: cte <cestreich@gmail.com>
This commit is contained in:
Matt Rubens 2025-04-15 22:16:52 -04:00 • committed by GitHub
parent 3b19d7a455
commit 37f7d83792
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 621 additions and 23 deletions

View file

@ -21,6 +21,7 @@ import { UnboundHandler } from "./providers/unbound"
import { RequestyHandler } from "./providers/requesty"
import { HumanRelayHandler } from "./providers/human-relay"
import { FakeAIHandler } from "./providers/fake-ai"
import { XAIHandler } from "./providers/xai"
export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
@ -78,6 +79,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new HumanRelayHandler(options)
case "fake-ai":
return new FakeAIHandler(options)
case "xai":
return new XAIHandler(options)
default:
return new AnthropicHandler(options)
}

View file

@ -0,0 +1,292 @@
import { XAIHandler } from "../xai"
import { xaiDefaultModelId, xaiModels } from "../../../shared/api"
import OpenAI from "openai"
import { Anthropic } from "@anthropic-ai/sdk"
// Mock OpenAI client
jest.mock("openai", () => {
const createMock = jest.fn()
return jest.fn(() => ({
chat: {
completions: {
create: createMock,
},
},
}))
})
describe("XAIHandler", () => {
let handler: XAIHandler
let mockCreate: jest.Mock
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks()
// Get the mock create function
mockCreate = (OpenAI as unknown as jest.Mock)().chat.completions.create
// Create handler with mock
handler = new XAIHandler({})
})
test("should use the correct X.AI base URL", () => {
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: "https://api.x.ai/v1",
}),
)
})
test("should use the provided API key", () => {
// Clear mocks before this specific test
jest.clearAllMocks()
// Create a handler with our API key
const xaiApiKey = "test-api-key"
new XAIHandler({ xaiApiKey })
// Verify the OpenAI constructor was called with our API key
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: xaiApiKey,
}),
)
})
test("should return default model when no model is specified", () => {
const model = handler.getModel()
expect(model.id).toBe(xaiDefaultModelId)
expect(model.info).toEqual(xaiModels[xaiDefaultModelId])
})
test("should return specified model when valid model is provided", () => {
const testModelId = "grok-2-latest"
const handlerWithModel = new XAIHandler({ apiModelId: testModelId })
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(xaiModels[testModelId])
})
test("should include reasoning_effort parameter for mini models", async () => {
const miniModelHandler = new XAIHandler({
apiModelId: "grok-3-mini-beta",
reasoningEffort: "high",
})
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
// Start generating a message
const messageGenerator = miniModelHandler.createMessage("test prompt", [])
await messageGenerator.next() // Start the generator
// Check that reasoning_effort was included
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
reasoning_effort: "high",
}),
)
})
test("should not include reasoning_effort parameter for non-mini models", async () => {
const regularModelHandler = new XAIHandler({
apiModelId: "grok-2-latest",
reasoningEffort: "high",
})
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
// Start generating a message
const messageGenerator = regularModelHandler.createMessage("test prompt", [])
await messageGenerator.next() // Start the generator
// Check call args for reasoning_effort
const calls = mockCreate.mock.calls
const lastCall = calls[calls.length - 1][0]
expect(lastCall).not.toHaveProperty("reasoning_effort")
})
test("completePrompt method should return text from OpenAI API", async () => {
const expectedResponse = "This is a test response"
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: expectedResponse,
},
},
],
})
const result = await handler.completePrompt("test prompt")
expect(result).toBe(expectedResponse)
})
test("should handle errors in completePrompt", async () => {
const errorMessage = "API error"
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
await expect(handler.completePrompt("test prompt")).rejects.toThrow(`xAI completion error: ${errorMessage}`)
})
test("createMessage should yield text content from stream", async () => {
const testContent = "This is test content"
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: jest
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [{ delta: { content: testContent } }],
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
// Create and consume the stream
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
// Verify the content
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({
type: "text",
text: testContent,
})
})
test("createMessage should yield reasoning content from stream", async () => {
const testReasoning = "Test reasoning content"
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: jest
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [{ delta: { reasoning_content: testReasoning } }],
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
// Create and consume the stream
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
// Verify the reasoning content
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({
type: "reasoning",
text: testReasoning,
})
})
test("createMessage should yield usage data from stream", async () => {
// Setup mock for streaming response that includes usage data
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: jest
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [{ delta: {} }], // Needs to have choices array to avoid error
usage: {
prompt_tokens: 10,
completion_tokens: 20,
cache_read_input_tokens: 5,
cache_creation_input_tokens: 15,
},
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
// Create and consume the stream
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
// Verify the usage data
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({
type: "usage",
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: 5,
cacheWriteTokens: 15,
})
})
test("createMessage should pass correct parameters to OpenAI client", async () => {
// Setup a handler with specific model
const modelId = "grok-2-latest"
const modelInfo = xaiModels[modelId]
const handlerWithModel = new XAIHandler({ apiModelId: modelId })
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
// System prompt and messages
const systemPrompt = "Test system prompt"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
// Start generating a message
const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages)
await messageGenerator.next() // Start the generator
// Check that all parameters were passed correctly
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },
}),
)
})
})

View file

@ -1,3 +1,12 @@
export const DEFAULT_HEADERS = {
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
"X-Title": "Roo Code",
}
export const ANTHROPIC_DEFAULT_MAX_TOKENS = 8192
export const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6
export const AZURE_AI_INFERENCE_PATH = "/models/chat/completions"
export const REASONING_MODELS = new Set(["x-ai/grok-3-mini-beta", "grok-3-mini-beta", "grok-3-mini-fast-beta"])

View file

@ -15,17 +15,10 @@ import { convertToSimpleMessages } from "../transform/simple-format"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { BaseProvider } from "./base-provider"
import { XmlMatcher } from "../../utils/xml-matcher"
import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
export const defaultHeaders = {
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
"X-Title": "Roo Code",
}
import { DEEP_SEEK_DEFAULT_TEMPERATURE, DEFAULT_HEADERS, AZURE_AI_INFERENCE_PATH } from "./constants"
export interface OpenAiHandlerOptions extends ApiHandlerOptions {}
const AZURE_AI_INFERENCE_PATH = "/models/chat/completions"
export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler {
protected options: OpenAiHandlerOptions
private client: OpenAI
@ -45,7 +38,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
this.client = new OpenAI({
baseURL,
apiKey,
defaultHeaders,
defaultHeaders: DEFAULT_HEADERS,
defaultQuery: { "api-version": this.options.azureApiVersion || "2024-05-01-preview" },
})
} else if (isAzureOpenAi) {
@ -56,7 +49,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
apiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: {
...defaultHeaders,
...DEFAULT_HEADERS,
...(this.options.openAiHostHeader ? { Host: this.options.openAiHostHeader } : {}),
},
})
@ -65,7 +58,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
baseURL,
apiKey,
defaultHeaders: {
...defaultHeaders,
...DEFAULT_HEADERS,
...(this.options.openAiHostHeader ? { Host: this.options.openAiHostHeader } : {}),
},
})

View file

@ -9,10 +9,9 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStreamChunk, ApiStreamUsageChunk } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
import { DEFAULT_HEADERS, DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
import { getModelParams, SingleCompletionHandler } from ".."
import { BaseProvider } from "./base-provider"
import { defaultHeaders } from "./openai"
const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]"
@ -40,7 +39,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
const apiKey = this.options.openRouterApiKey ?? "not-provided"
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders })
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS })
}
override async *createMessage(

110
src/api/providers/xai.ts Normal file
View file

@ -0,0 +1,110 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { DEFAULT_HEADERS, REASONING_MODELS } from "./constants"
import { BaseProvider } from "./base-provider"
import { SingleCompletionHandler } from ".."
const XAI_DEFAULT_TEMPERATURE = 0
export class XAIHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
super()
this.options = options
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey ?? "not-provided",
defaultHeaders: DEFAULT_HEADERS,
})
}
override getModel() {
// Determine which model ID to use (specified or default)
const id =
this.options.apiModelId && this.options.apiModelId in xaiModels
? (this.options.apiModelId as XAIModelId)
: xaiDefaultModelId
// Check if reasoning effort applies to this model
const supportsReasoning = REASONING_MODELS.has(id)
return {
id,
info: xaiModels[id],
reasoningEffort: supportsReasoning ? this.options.reasoningEffort : undefined,
}
}
override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const { id: modelId, info: modelInfo, reasoningEffort } = this.getModel()
// Use the OpenAI-compatible API.
const stream = await this.client.chat.completions.create({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
text: delta.reasoning_content as string,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// X.AI might include these fields in the future, handle them if present.
cacheReadTokens:
"cache_read_input_tokens" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0,
cacheWriteTokens:
"cache_creation_input_tokens" in chunk.usage
? (chunk.usage as any).cache_creation_input_tokens
: 0,
}
}
}
}
async completePrompt(prompt: string): Promise<string> {
const { id: modelId, reasoningEffort } = this.getModel()
try {
const response = await this.client.chat.completions.create({
model: modelId,
messages: [{ role: "user", content: prompt }],
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
})
return response.choices[0]?.message.content || ""
} catch (error) {
if (error instanceof Error) {
throw new Error(`xAI completion error: ${error.message}`)
}
throw error
}
}
}

View file

@ -20,6 +20,7 @@ type ProviderSettings = {
| "requesty"
| "human-relay"
| "fake-ai"
| "xai"
)
| undefined
apiModelId?: string | undefined
@ -176,6 +177,7 @@ type ProviderSettings = {
cachableFields?: string[] | undefined
} | null)
| undefined
xaiApiKey?: string | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
includeMaxTokens?: boolean | undefined
@ -212,6 +214,7 @@ type GlobalSettings = {
| "requesty"
| "human-relay"
| "fake-ai"
| "xai"
)
| undefined
}[]

View file

@ -21,6 +21,7 @@ type ProviderSettings = {
| "requesty"
| "human-relay"
| "fake-ai"
| "xai"
)
| undefined
apiModelId?: string | undefined
@ -177,6 +178,7 @@ type ProviderSettings = {
cachableFields?: string[] | undefined
} | null)
| undefined
xaiApiKey?: string | undefined
modelMaxTokens?: number | undefined
modelMaxThinkingTokens?: number | undefined
includeMaxTokens?: boolean | undefined
@ -215,6 +217,7 @@ type GlobalSettings = {
| "requesty"
| "human-relay"
| "fake-ai"
| "xai"
)
| undefined
}[]

View file

@ -28,6 +28,7 @@ export const providerNames = [
"requesty",
"human-relay",
"fake-ai",
"xai",
] as const
export const providerNamesSchema = z.enum(providerNames)
@ -380,6 +381,8 @@ export const providerSettingsSchema = z.object({
requestyApiKey: z.string().optional(),
requestyModelId: z.string().optional(),
requestyModelInfo: modelInfoSchema.nullish(),
// X.AI (Grok)
xaiApiKey: z.string().optional(),
// Claude 3.7 Sonnet Thinking
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),
@ -483,6 +486,8 @@ const providerSettingsRecord: ProviderSettingsRecord = {
fuzzyMatchThreshold: undefined,
// Fake AI
fakeAi: undefined,
// X.AI (Grok)
xaiApiKey: undefined,
}
export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as Keys<ProviderSettings>[]
@ -672,6 +677,7 @@ export type SecretState = Pick<
| "mistralApiKey"
| "unboundApiKey"
| "requestyApiKey"
| "xaiApiKey"
>
type SecretStateRecord = Record<Keys<SecretState>, undefined>
@ -690,6 +696,7 @@ const secretStateRecord: SecretStateRecord = {
mistralApiKey: undefined,
unboundApiKey: undefined,
requestyApiKey: undefined,
xaiApiKey: undefined,
}
export const SECRET_STATE_KEYS = Object.keys(secretStateRecord) as Keys<SecretState>[]

View file

@ -1,4 +1,7 @@
import { ModelInfo, ProviderName, ProviderSettings } from "../schemas"
import { REASONING_MODELS } from "../api/providers/constants"
export { REASONING_MODELS }
export type { ModelInfo, ProviderName as ApiProvider }
@ -77,6 +80,7 @@ export const anthropicModels = {
cacheReadsPrice: 0.03,
},
} as const satisfies Record<string, ModelInfo> // as const assertion makes the object deeply readonly
// Amazon Bedrock
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
export interface MessageContent {
@ -950,6 +954,7 @@ export const mistralModels = {
} as const satisfies Record<string, ModelInfo>
// Unbound Security
// https://www.unboundsecurity.ai/ai-gateway
export const unboundDefaultModelId = "anthropic/claude-3-5-sonnet-20241022"
export const unboundDefaultModelInfo: ModelInfo = {
maxTokens: 8192,
@ -961,3 +966,118 @@ export const unboundDefaultModelInfo: ModelInfo = {
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
}
// xAI
// https://docs.x.ai/docs/api-reference
export type XAIModelId = keyof typeof xaiModels
export const xaiDefaultModelId: XAIModelId = "grok-3-beta"
export const xaiModels = {
"grok-3-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
description: "xAI's Grok-3 beta model with 131K context window",
},
"grok-3-fast-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 5.0,
outputPrice: 25.0,
description: "xAI's Grok-3 fast beta model with 131K context window",
},
"grok-3-mini-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 0.5,
description: "xAI's Grok-3 mini beta model with 131K context window",
},
"grok-3-mini-fast-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.6,
outputPrice: 4.0,
description: "xAI's Grok-3 mini fast beta model with 131K context window",
},
"grok-2-latest": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 10.0,
description: "xAI's Grok-2 model - latest version with 131K context window",
},
"grok-2": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 10.0,
description: "xAI's Grok-2 model with 131K context window",
},
"grok-2-1212": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 10.0,
description: "xAI's Grok-2 model (version 1212) with 131K context window",
},
"grok-2-vision-latest": {
maxTokens: 8192,
contextWindow: 32768,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 10.0,
description: "xAI's Grok-2 Vision model - latest version with image support and 32K context window",
},
"grok-2-vision": {
maxTokens: 8192,
contextWindow: 32768,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 10.0,
description: "xAI's Grok-2 Vision model with image support and 32K context window",
},
"grok-2-vision-1212": {
maxTokens: 8192,
contextWindow: 32768,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 10.0,
description: "xAI's Grok-2 Vision model (version 1212) with image support and 32K context window",
},
"grok-vision-beta": {
maxTokens: 8192,
contextWindow: 8192,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5.0,
outputPrice: 15.0,
description: "xAI's Grok Vision Beta model with image support and 8K context window",
},
"grok-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 5.0,
outputPrice: 15.0,
description: "xAI's Grok Beta model (legacy) with 131K context window",
},
} as const satisfies Record<string, ModelInfo>

View file

@ -35,6 +35,8 @@ import {
unboundDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
xaiDefaultModelId,
xaiModels,
ApiProvider,
} from "../../../../src/shared/api"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
@ -1444,6 +1446,27 @@ const ApiOptions = ({
</>
)}
{selectedProvider === "xai" && (
<>
<VSCodeTextField
value={apiConfiguration?.xaiApiKey || ""}
type="password"
onInput={handleInputChange("xaiApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.xaiApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.xaiApiKey && (
<VSCodeButtonLink href="https://api.x.ai/docs" appearance="secondary">
{t("settings:providers.getXaiApiKey")}
</VSCodeButtonLink>
)}
</>
)}
{selectedProvider === "unbound" && (
<>
<VSCodeTextField
@ -1539,13 +1562,6 @@ const ApiOptions = ({
</div>
)}
{selectedProvider === "openrouter" && REASONING_MODELS.has(selectedModelId) && (
<ReasoningEffort
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "glama" && (
<ModelPicker
apiConfiguration={apiConfiguration}
@ -1690,6 +1706,13 @@ const ApiOptions = ({
</>
)}
{REASONING_MODELS.has(selectedModelId) && (
<ReasoningEffort
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{!fromWelcomeView && (
<>
<DiffSettingsControl
@ -1734,6 +1757,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
case "bedrock":
// Special case for custom ARN
if (modelId === "custom-arn") {

View file

@ -8,8 +8,12 @@ import {
mistralModels,
openAiNativeModels,
vertexModels,
xaiModels,
REASONING_MODELS,
} from "../../../../src/shared/api"
export { REASONING_MODELS }
export const MODELS_BY_PROVIDER: Partial<Record<ApiProvider, Record<string, ModelInfo>>> = {
anthropic: anthropicModels,
bedrock: bedrockModels,
@ -18,6 +22,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ApiProvider, Record<string, Mode
mistral: mistralModels,
"openai-native": openAiNativeModels,
vertex: vertexModels,
xai: xaiModels,
}
export const PROVIDERS = [
@ -37,6 +42,7 @@ export const PROVIDERS = [
{ value: "unbound", label: "Unbound" },
{ value: "requesty", label: "Requesty" },
{ value: "human-relay", label: "Human Relay" },
{ value: "xai", label: "xAI" },
].sort((a, b) => a.label.localeCompare(b.label))
export const VERTEX_REGIONS = [
@ -46,5 +52,3 @@ export const VERTEX_REGIONS = [
{ value: "europe-west4", label: "europe-west4" },
{ value: "asia-southeast1", label: "asia-southeast1" },
]
export const REASONING_MODELS = new Set(["x-ai/grok-3-mini-beta"])

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Obtenir clau API de Mistral / Codestral",
"codestralBaseUrl": "URL base de Codestral (opcional)",
"codestralBaseUrlDesc": "Establir una URL alternativa per al model Codestral.",
"xaiApiKey": "Clau API de xAI",
"getXaiApiKey": "Obtenir clau API de xAI",
"awsCredentials": "Credencials d'AWS",
"awsProfile": "Perfil d'AWS",
"awsProfileName": "Nom del perfil d'AWS",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Mistral / Codestral API-Schlüssel erhalten",
"codestralBaseUrl": "Codestral Basis-URL (Optional)",
"codestralBaseUrlDesc": "Legen Sie eine alternative URL für das Codestral-Modell fest.",
"xaiApiKey": "xAI API-Schlüssel",
"getXaiApiKey": "xAI API-Schlüssel erhalten",
"awsCredentials": "AWS Anmeldedaten",
"awsProfile": "AWS Profil",
"awsProfileName": "AWS Profilname",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Get Mistral / Codestral API Key",
"codestralBaseUrl": "Codestral Base URL (Optional)",
"codestralBaseUrlDesc": "Set an alternative URL for the Codestral model.",
"xaiApiKey": "xAI API Key",
"getXaiApiKey": "Get xAI API Key",
"awsCredentials": "AWS Credentials",
"awsProfile": "AWS Profile",
"awsProfileName": "AWS Profile Name",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Obtener clave API de Mistral / Codestral",
"codestralBaseUrl": "URL base de Codestral (Opcional)",
"codestralBaseUrlDesc": "Establecer una URL alternativa para el modelo Codestral.",
"xaiApiKey": "Clave API de xAI",
"getXaiApiKey": "Obtener clave API de xAI",
"awsCredentials": "Credenciales de AWS",
"awsProfile": "Perfil de AWS",
"awsProfileName": "Nombre del perfil de AWS",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Obtenir la clé API Mistral / Codestral",
"codestralBaseUrl": "URL de base Codestral (Optionnel)",
"codestralBaseUrlDesc": "Définir une URL alternative pour le modèle Codestral.",
"xaiApiKey": "Clé API xAI",
"getXaiApiKey": "Obtenir la clé API xAI",
"awsCredentials": "Identifiants AWS",
"awsProfile": "Profil AWS",
"awsProfileName": "Nom du profil AWS",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Mistral / Codestral API कुंजी प्राप्त करें",
"codestralBaseUrl": "Codestral बेस URL (वैकल्पिक)",
"codestralBaseUrlDesc": "Codestral मॉडल के लिए वैकल्पिक URL सेट करें।",
"xaiApiKey": "xAI API कुंजी",
"getXaiApiKey": "xAI API कुंजी प्राप्त करें",
"awsCredentials": "AWS क्रेडेंशियल्स",
"awsProfile": "AWS प्रोफाइल",
"awsProfileName": "AWS प्रोफाइल नाम",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Ottieni chiave API Mistral / Codestral",
"codestralBaseUrl": "URL base Codestral (opzionale)",
"codestralBaseUrlDesc": "Imposta un URL opzionale per i modelli Codestral.",
"xaiApiKey": "Chiave API xAI",
"getXaiApiKey": "Ottieni chiave API xAI",
"awsCredentials": "Credenziali AWS",
"awsProfile": "Profilo AWS",
"awsProfileName": "Nome profilo AWS",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Mistral / Codestral APIキーを取得",
"codestralBaseUrl": "Codestral ベースURL(オプション)",
"codestralBaseUrlDesc": "Codestralモデルの代替URLを設定します。",
"xaiApiKey": "xAI APIキー",
"getXaiApiKey": "xAI APIキーを取得",
"awsCredentials": "AWS認証情報",
"awsProfile": "AWSプロファイル",
"awsProfileName": "AWSプロファイル名",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Mistral / Codestral API 키 받기",
"codestralBaseUrl": "Codestral 기본 URL (선택사항)",
"codestralBaseUrlDesc": "Codestral 모델의 대체 URL을 설정합니다.",
"xaiApiKey": "xAI API 키",
"getXaiApiKey": "xAI API 키 받기",
"awsCredentials": "AWS 자격 증명",
"awsProfile": "AWS 프로필",
"awsProfileName": "AWS 프로필 이름",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Uzyskaj klucz API Mistral / Codestral",
"codestralBaseUrl": "URL bazowy Codestral (opcjonalnie)",
"codestralBaseUrlDesc": "Ustaw opcjonalny URL dla modeli Codestral.",
"xaiApiKey": "Klucz API xAI",
"getXaiApiKey": "Uzyskaj klucz API xAI",
"awsCredentials": "Poświadczenia AWS",
"awsProfile": "Profil AWS",
"awsProfileName": "Nazwa profilu AWS",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Obter chave de API Mistral / Codestral",
"codestralBaseUrl": "URL Base Codestral (Opcional)",
"codestralBaseUrlDesc": "Defina uma URL alternativa para o modelo Codestral.",
"xaiApiKey": "Chave de API xAI",
"getXaiApiKey": "Obter chave de API xAI",
"awsCredentials": "Credenciais AWS",
"awsProfile": "Perfil AWS",
"awsProfileName": "Nome do Perfil AWS",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "Mistral / Codestral API Anahtarı Al",
"codestralBaseUrl": "Codestral Temel URL (İsteğe bağlı)",
"codestralBaseUrlDesc": "Codestral modeli için alternatif URL ayarlayın.",
"xaiApiKey": "xAI API Anahtarı",
"getXaiApiKey": "xAI API Anahtarı Al",
"awsCredentials": "AWS Kimlik Bilgileri",
"awsProfile": "AWS Profili",
"awsProfileName": "AWS Profil Adı",

View file

@ -127,6 +127,8 @@
"getMistralApiKey": "Lấy khóa API Mistral / Codestral",
"codestralBaseUrl": "URL cơ sở Codestral (Tùy chọn)",
"codestralBaseUrlDesc": "Đặt URL thay thế cho mô hình Codestral.",
"xaiApiKey": "Khóa API xAI",
"getXaiApiKey": "Lấy khóa API xAI",
"awsCredentials": "Thông tin xác thực AWS",
"awsProfile": "Hồ sơ AWS",
"awsProfileName": "Tên hồ sơ AWS",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "获取 Mistral / Codestral API 密钥",
"codestralBaseUrl": "Codestral 基础 URL(可选)",
"codestralBaseUrlDesc": "为 Codestral 模型设置替代 URL。",
"xaiApiKey": "xAI API 密钥",
"getXaiApiKey": "获取 xAI API 密钥",
"awsCredentials": "AWS 凭证",
"awsProfile": "AWS 配置文件",
"awsProfileName": "AWS 配置文件名称",

View file

@ -128,6 +128,8 @@
"getMistralApiKey": "取得 Mistral/Codestral API 金鑰",
"codestralBaseUrl": "Codestral 基礎 URL(選用)",
"codestralBaseUrlDesc": "設定 Codestral 模型的替代 URL。",
"xaiApiKey": "xAI API 金鑰",
"getXaiApiKey": "取得 xAI API 金鑰",
"awsCredentials": "AWS 認證",
"awsProfile": "AWS Profile",
"awsProfileName": "AWS Profile 名稱",