From d7e9ead7307ffe99e153b9912db8de8abe925bfd Mon Sep 17 00:00:00 2001 From: Andrew Monostate <165841485+andrewmonostate@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:52:40 -0300 Subject: [PATCH] feat: add X AI provider integration --- .changeset/clean-crabs-do.md | 5 ++ src/api/index.ts | 3 + src/api/providers/xai.ts | 64 +++++++++++++++ src/core/webview/ClineProvider.ts | 7 ++ src/shared/api.ts | 81 +++++++++++++++++++ .../src/components/settings/ApiOptions.tsx | 46 +++++++++++ .../src/context/ExtensionStateContext.tsx | 1 + webview-ui/src/utils/validate.ts | 5 ++ 8 files changed, 212 insertions(+) create mode 100644 .changeset/clean-crabs-do.md create mode 100644 src/api/providers/xai.ts diff --git a/.changeset/clean-crabs-do.md b/.changeset/clean-crabs-do.md new file mode 100644 index 0000000000..4fd7b271e4 --- /dev/null +++ b/.changeset/clean-crabs-do.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Added X AI as a new provider with support for all current models including Grok-2 and Grok Vision. This integration enables users to connect to X AI's API using their API key and access models with context windows up to 131K tokens. The implementation includes proper handling for vision models and accurate pricing information. diff --git a/src/api/index.ts b/src/api/index.ts index 4681ec03af..00321fa504 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -17,6 +17,7 @@ import { QwenHandler } from "./providers/qwen" import { MistralHandler } from "./providers/mistral" import { VsCodeLmHandler } from "./providers/vscode-lm" import { LiteLlmHandler } from "./providers/litellm" +import { XAIHandler } from "./providers/xai" export interface ApiHandler { createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream @@ -62,6 +63,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new VsCodeLmHandler(options) case "litellm": return new LiteLlmHandler(options) + case "xai": + return new XAIHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts new file mode 100644 index 0000000000..f8941b5df6 --- /dev/null +++ b/src/api/providers/xai.ts @@ -0,0 +1,64 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { ApiHandler } from "../" +import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "../../shared/api" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" + +export class XAIHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: "https://api.x.ai/v1", + apiKey: this.options.xaiApiKey, + }) + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const stream = await this.client.chat.completions.create({ + model: this.getModel().id, + max_completion_tokens: this.getModel().info.maxTokens, + temperature: 0, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + }) + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: 0, + outputTokens: chunk.usage.completion_tokens || 0, + // @ts-ignore-next-line + cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0, + // @ts-ignore-next-line + cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0, + } + } + } + } + + getModel(): { id: XAIModelId; info: ModelInfo } { + const modelId = this.options.apiModelId + if (modelId && modelId in xaiModels) { + const id = modelId as XAIModelId + return { id, info: xaiModels[id] } + } + return { + id: xaiDefaultModelId, + info: xaiModels[xaiDefaultModelId], + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index c239930846..a292f235af 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -59,6 +59,7 @@ type SecretKey = | "liteLlmApiKey" | "authToken" | "authNonce" + | "xaiApiKey" type GlobalStateKey = | "apiProvider" | "apiModelId" @@ -593,6 +594,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { liteLlmModelId, liteLlmApiKey, qwenApiLine, + xaiApiKey, } = message.apiConfiguration await this.updateGlobalState("apiProvider", apiProvider) await this.updateGlobalState("apiModelId", apiModelId) @@ -624,6 +626,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret("qwenApiKey", qwenApiKey) await this.storeSecret("mistralApiKey", mistralApiKey) await this.storeSecret("liteLlmApiKey", liteLlmApiKey) + await this.storeSecret("xaiApiKey", xaiApiKey) await this.updateGlobalState("azureApiVersion", azureApiVersion) await this.updateGlobalState("openRouterModelId", openRouterModelId) await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo) @@ -1879,6 +1882,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont qwenApiLine, liteLlmApiKey, telemetrySetting, + xaiApiKey, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1931,6 +1935,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont this.getGlobalState("qwenApiLine") as Promise, this.getSecret("liteLlmApiKey") as Promise, this.getGlobalState("telemetrySetting") as Promise, + this.getSecret("xaiApiKey") as Promise, ]) let apiProvider: ApiProvider @@ -1995,6 +2000,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont liteLlmBaseUrl, liteLlmModelId, liteLlmApiKey, + xaiApiKey, }, lastShownAnnouncementId, customInstructions, @@ -2138,6 +2144,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont "mistralApiKey", "liteLlmApiKey", "authToken", + "xaiApiKey", ] for (const key of secretKeys) { await this.storeSecret(key, undefined) diff --git a/src/shared/api.ts b/src/shared/api.ts index ae57562898..b6d5cfe4a2 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -15,6 +15,7 @@ export type ApiProvider = | "mistral" | "vscode-lm" | "litellm" + | "xai" export interface ApiHandlerOptions { apiModelId?: string @@ -56,6 +57,7 @@ export interface ApiHandlerOptions { vsCodeLmModelSelector?: any o3MiniReasoningEffort?: string qwenApiLine?: string + xaiApiKey?: string } export type ApiConfiguration = ApiHandlerOptions & { @@ -799,3 +801,82 @@ export const liteLlmModelInfoSaneDefaults: ModelInfo = { inputPrice: 0, outputPrice: 0, } + +// X AI +// https://docs.x.ai/docs/api-reference +export type XAIModelId = keyof typeof xaiModels +export const xaiDefaultModelId: XAIModelId = "grok-2-latest" +export const xaiModels = { + "grok-2-latest": { + maxTokens: 8192, + contextWindow: 131072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 2.0, + outputPrice: 10.0, + description: "X AI'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: "X AI'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: "X AI'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: "X AI'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: "X AI'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: "X AI'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: "X AI'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: "X AI's Grok Beta model (legacy) with 131K context window", + }, +} as const satisfies Record diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 4929d43876..5ab7543d4c 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -33,6 +33,8 @@ import { openRouterDefaultModelInfo, vertexDefaultModelId, vertexModels, + xaiDefaultModelId, + xaiModels, } from "../../../../src/shared/api" import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" import { useExtensionState } from "../../context/ExtensionStateContext" @@ -195,6 +197,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is LM Studio Ollama LiteLLM + X AI @@ -1122,6 +1125,46 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is )} + {selectedProvider === "xai" && ( +
+ + X AI API Key + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.xaiApiKey && ( + + You can get an X AI API key by signing up here. + + )} +

+ {/* Note: To fully implement this, you would need to add a handler in ClineProvider.ts */} + {/* {apiConfiguration?.xaiApiKey && ( + + )} */} +
+ )} + {apiErrorMessage && (

key !== undefined) : false setShowWelcome(!hasKey) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index faa0434814..a3d2e4106e 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -38,6 +38,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid API key or choose a different provider." } break + case "xai": + if (!apiConfiguration.xaiApiKey) { + return "You must provide a valid API key or choose a different provider." + } + break case "qwen": if (!apiConfiguration.qwenApiKey) { return "You must provide a valid API key or choose a different provider."