diff --git a/.changeset/big-plums-wave.md b/.changeset/big-plums-wave.md new file mode 100644 index 0000000000..a3c836e45f --- /dev/null +++ b/.changeset/big-plums-wave.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Adding Requesty API Provider diff --git a/src/api/index.ts b/src/api/index.ts index 680eb53232..09598680c7 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -11,6 +11,7 @@ import { GeminiHandler } from "./providers/gemini" import { OpenAiNativeHandler } from "./providers/openai-native" import { ApiStream } from "./transform/stream" import { DeepSeekHandler } from "./providers/deepseek" +import { RequestyHandler } from "./providers/requesty" import { QwenHandler } from "./providers/qwen" import { MistralHandler } from "./providers/mistral" import { VsCodeLmHandler } from "./providers/vscode-lm" @@ -48,6 +49,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new OpenAiNativeHandler(options) case "deepseek": return new DeepSeekHandler(options) + case "requesty": + return new RequestyHandler(options) case "qwen": return new QwenHandler(options) case "mistral": diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts new file mode 100644 index 0000000000..34dd2b4d67 --- /dev/null +++ b/src/api/providers/requesty.ts @@ -0,0 +1,75 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { withRetry } from "../retry" +import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" +import { ApiHandler } from "../index" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" +import { convertToR1Format } from "../transform/r1-format" + +export class RequestyHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: "https://router.requesty.ai/v1", + apiKey: this.options.requestyApiKey, + }) + } + + @withRetry() + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const modelId = this.options.requestyModelId ?? "" + const isDeepseekReasoner = modelId.includes("deepseek-reasoner") + + let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + if (isDeepseekReasoner) { + openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) + } + + const stream = await this.client.chat.completions.create({ + model: modelId, + messages: openAiMessages, + temperature: 0, + 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 (delta && "reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + reasoning: (delta.reasoning_content as string | undefined) || "", + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } + } + } + } + + getModel(): { id: string; info: ModelInfo } { + return { + id: this.options.requestyModelId ?? "", + info: openAiModelInfoSaneDefaults, + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 415656ffe6..2961d8953a 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -45,6 +45,7 @@ type SecretKey = | "geminiApiKey" | "openAiNativeApiKey" | "deepSeekApiKey" + | "requestyApiKey" | "qwenApiKey" | "mistralApiKey" | "authToken" @@ -80,6 +81,7 @@ type GlobalStateKey = | "liteLlmBaseUrl" | "liteLlmModelId" | "qwenApiLine" + | "requestyModelId" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -442,6 +444,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + requestyApiKey, + requestyModelId, qwenApiKey, mistralApiKey, azureApiVersion, @@ -474,6 +478,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret("geminiApiKey", geminiApiKey) await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey) await this.storeSecret("deepSeekApiKey", deepSeekApiKey) + await this.storeSecret("requestyApiKey", requestyApiKey) await this.storeSecret("qwenApiKey", qwenApiKey) await this.storeSecret("mistralApiKey", mistralApiKey) await this.updateGlobalState("azureApiVersion", azureApiVersion) @@ -483,6 +488,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl) await this.updateGlobalState("liteLlmModelId", liteLlmModelId) await this.updateGlobalState("qwenApiLine", qwenApiLine) + await this.updateGlobalState("requestyModelId", requestyModelId) if (this.cline) { this.cline.api = buildApiHandler(message.apiConfiguration) } @@ -1371,6 +1377,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + requestyApiKey, + requestyModelId, qwenApiKey, mistralApiKey, azureApiVersion, @@ -1414,6 +1422,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getSecret("geminiApiKey") as Promise, this.getSecret("openAiNativeApiKey") as Promise, this.getSecret("deepSeekApiKey") as Promise, + this.getSecret("requestyApiKey") as Promise, + this.getGlobalState("requestyModelId") as Promise, this.getSecret("qwenApiKey") as Promise, this.getSecret("mistralApiKey") as Promise, this.getGlobalState("azureApiVersion") as Promise, @@ -1474,6 +1484,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + requestyApiKey, + requestyModelId, qwenApiKey, qwenApiLine, mistralApiKey, @@ -1571,6 +1583,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { "geminiApiKey", "openAiNativeApiKey", "deepSeekApiKey", + "requestyApiKey", "qwenApiKey", "mistralApiKey", "authToken", diff --git a/src/shared/api.ts b/src/shared/api.ts index a640a1c9d7..ea9d42c25e 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -8,6 +8,7 @@ export type ApiProvider = | "lmstudio" | "gemini" | "openai-native" + | "requesty" | "deepseek" | "qwen" | "mistral" @@ -40,6 +41,8 @@ export interface ApiHandlerOptions { geminiApiKey?: string openAiNativeApiKey?: string deepSeekApiKey?: string + requestyApiKey?: string + requestyModelId?: string qwenApiKey?: string mistralApiKey?: string azureApiVersion?: string diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index cbbf18198d..9a815f9735 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -187,6 +187,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is AWS Bedrock OpenAI OpenAI Compatible + Requesty VS Code LM API LM Studio Ollama @@ -673,6 +674,37 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is )} + {selectedProvider === "requesty" && ( +
+ + API Key + + + Model ID + +

+ + (Note: Cline uses complex prompts and works best with Claude + models. Less capable models may not work as expected.) + +

+
+ )} + {selectedProvider === "vscode-lm" && (
diff --git a/webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx new file mode 100644 index 0000000000..47c662acfe --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx @@ -0,0 +1,51 @@ +import { render, screen, fireEvent } from "@testing-library/react" +import { describe, it, expect, vi } from "vitest" +import ApiOptions from "../ApiOptions" +import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext" + +vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + useExtensionState: vi.fn(() => ({ + apiConfiguration: { + apiProvider: "requesty", + requestyApiKey: "", + requestyModelId: "", + }, + setApiConfiguration: vi.fn(), + uriScheme: "vscode", + })), + } +}) + +describe("ApiOptions Component", () => { + vi.clearAllMocks() + const mockPostMessage = vi.fn() + const mockSetApiConfiguration = vi.fn() + + beforeEach(() => { + global.vscode = { postMessage: mockPostMessage } as any + }) + + it("renders Requesty API Key input", () => { + render( + + + , + ) + const apiKeyInput = screen.getByPlaceholderText("Enter API Key...") + expect(apiKeyInput).toBeInTheDocument() + }) + + it("renders Requesty Model ID input", () => { + render( + + + , + ) + const modelIdInput = screen.getByPlaceholderText("Enter Model ID...") + expect(modelIdInput).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 99a8cca3f9..34abc21619 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -67,6 +67,7 @@ export const ExtensionStateContextProvider: React.FC<{ config.geminiApiKey, config.openAiNativeApiKey, config.deepSeekApiKey, + config.requestyApiKey, config.qwenApiKey, config.mistralApiKey, config.vsCodeLmModelSelector, diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 4617fb2c70..698a778f3c 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -53,6 +53,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid base URL, API key, and model ID." } break + case "requesty": + if (!apiConfiguration.requestyApiKey || !apiConfiguration.requestyModelId) { + return "You must provide a valid API key or choose a different provider." + } + break case "ollama": if (!apiConfiguration.ollamaModelId) { return "You must provide a valid model ID."