diff --git a/.changeset/green-forks-change.md b/.changeset/green-forks-change.md new file mode 100644 index 0000000000..6931023a3f --- /dev/null +++ b/.changeset/green-forks-change.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add support for AskSage as model provider. diff --git a/src/api/index.ts b/src/api/index.ts index 00321fa504..6cdb5aaab7 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 { AskSageHandler } from "./providers/asksage" import { XAIHandler } from "./providers/xai" export interface ApiHandler { @@ -63,6 +64,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new VsCodeLmHandler(options) case "litellm": return new LiteLlmHandler(options) + case "asksage": + return new AskSageHandler(options) case "xai": return new XAIHandler(options) default: diff --git a/src/api/providers/asksage.ts b/src/api/providers/asksage.ts new file mode 100644 index 0000000000..4e70151e18 --- /dev/null +++ b/src/api/providers/asksage.ts @@ -0,0 +1,115 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import { ApiHandler } from ".." +import { + ApiHandlerOptions, + ModelInfo, + AskSageModelId, + askSageModels, + askSageDefaultModelId, + askSageDefaultURL, +} from "../../shared/api" +import { ApiStream } from "../transform/stream" + +type AskSageRequest = { + system_prompt: string + message: { + user: "gpt" | "me" + message: string + }[] + model: string + dataset: "none" +} + +type AskSageResponse = { + uuid: string + status: number + // Response status + response: string + // Generated response message + message: string +} + +export class AskSageHandler implements ApiHandler { + private options: ApiHandlerOptions + private apiUrl: string + private apiKey: string + + constructor(options: ApiHandlerOptions) { + console.log("init api url", options.asksageApiUrl, askSageDefaultURL) + this.options = options + this.apiKey = options.asksageApiKey || "" + this.apiUrl = options.asksageApiUrl || askSageDefaultURL + + if (!this.apiKey) { + throw new Error("AskSage API key is required") + } + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + try { + const model = this.getModel() + + // Transform messages into AskSageRequest format + const formattedMessages = messages.map((msg) => { + const content = Array.isArray(msg.content) + ? msg.content.map((block) => ("text" in block ? block.text : "")).join("") + : msg.content + + return { + user: msg.role === "assistant" ? ("gpt" as const) : ("me" as const), + message: content, + } + }) + + const request: AskSageRequest = { + system_prompt: systemPrompt, + message: formattedMessages, + model: model.id, + dataset: "none", + } + + // Make request to AskSage API + const response = await fetch(`${this.apiUrl}/query`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-access-tokens": this.apiKey, + }, + body: JSON.stringify(request), + }) + + if (!response.ok) { + const error = await response.text() + throw new Error(`AskSage API error: ${error}`) + } + + const result = (await response.json()) as AskSageResponse + + if (!result.message) { + throw new Error("No content in AskSage response") + } + + // Return entire response as a single chunk since streaming is not supported + yield { + type: "text", + text: result.message, + } + } catch (error) { + if (error instanceof Error) { + throw new Error(`AskSage request failed: ${error.message}`) + } + } + } + + getModel(): { id: string; info: ModelInfo } { + const modelId = this.options.apiModelId + if (modelId && modelId in askSageModels) { + const id = modelId as AskSageModelId + return { id, info: askSageModels[id] } + } + return { + id: askSageDefaultModelId, + info: askSageModels[askSageDefaultModelId], + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index cabb80fb08..5705e53b55 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -59,6 +59,7 @@ type SecretKey = | "liteLlmApiKey" | "authToken" | "authNonce" + | "asksageApiKey" | "xaiApiKey" type GlobalStateKey = | "apiProvider" @@ -100,7 +101,9 @@ type GlobalStateKey = | "togetherModelId" | "mcpMarketplaceCatalog" | "telemetrySetting" + | "asksageApiUrl" | "thinkingBudgetTokens" + export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", uiMessages: "ui_messages.json", @@ -596,6 +599,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { liteLlmModelId, liteLlmApiKey, qwenApiLine, + asksageApiKey, + asksageApiUrl, xaiApiKey, thinkingBudgetTokens, } = message.apiConfiguration @@ -640,6 +645,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("qwenApiLine", qwenApiLine) await this.updateGlobalState("requestyModelId", requestyModelId) await this.updateGlobalState("togetherModelId", togetherModelId) + await this.storeSecret("asksageApiKey", asksageApiKey) + await this.updateGlobalState("asksageApiUrl", asksageApiUrl) await this.updateGlobalState("thinkingBudgetTokens", thinkingBudgetTokens) if (this.cline) { this.cline.api = buildApiHandler(message.apiConfiguration) @@ -1007,6 +1014,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "bedrock": case "vertex": case "gemini": + case "asksage": await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId) break case "openrouter": @@ -1043,6 +1051,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "bedrock": case "vertex": case "gemini": + case "asksage": await this.updateGlobalState("apiModelId", newModelId) break case "openrouter": @@ -1904,6 +1913,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont qwenApiLine, liteLlmApiKey, telemetrySetting, + asksageApiKey, + asksageApiUrl, xaiApiKey, thinkingBudgetTokens, ] = await Promise.all([ @@ -1960,6 +1971,8 @@ 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("asksageApiKey") as Promise, + this.getGlobalState("asksageApiUrl") as Promise, this.getSecret("xaiApiKey") as Promise, this.getGlobalState("thinkingBudgetTokens") as Promise, ]) @@ -2028,6 +2041,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont liteLlmBaseUrl, liteLlmModelId, liteLlmApiKey, + asksageApiKey, + asksageApiUrl, xaiApiKey, }, lastShownAnnouncementId, @@ -2173,6 +2188,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont "mistralApiKey", "liteLlmApiKey", "authToken", + "asksageApiKey", "xaiApiKey", ] for (const key of secretKeys) { diff --git a/src/shared/api.ts b/src/shared/api.ts index a57c5887d2..5676584e7e 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -15,6 +15,7 @@ export type ApiProvider = | "mistral" | "vscode-lm" | "litellm" + | "asksage" | "xai" export interface ApiHandlerOptions { @@ -58,6 +59,8 @@ export interface ApiHandlerOptions { vsCodeLmModelSelector?: any o3MiniReasoningEffort?: string qwenApiLine?: string + asksageApiUrl?: string + asksageApiKey?: string xaiApiKey?: string thinkingBudgetTokens?: number } @@ -808,6 +811,38 @@ export const liteLlmModelInfoSaneDefaults: ModelInfo = { outputPrice: 0, } +// AskSage Models +// https://docs.asksage.ai/ +export type AskSageModelId = keyof typeof askSageModels +export const askSageDefaultModelId: AskSageModelId = "claude-35-sonnet" +export const askSageDefaultURL: string = "https://api.asksage.ai/server" +export const askSageModels = { + "gpt-4o": { + maxTokens: 4096, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gpt-4o-gov": { + maxTokens: 4096, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "claude-35-sonnet": { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, +} + // X AI // https://docs.x.ai/docs/api-reference export type XAIModelId = keyof typeof xaiModels diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index baf39369f1..245cebb80f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -36,6 +36,9 @@ import { qwenModels, vertexDefaultModelId, vertexModels, + askSageModels, + askSageDefaultModelId, + askSageDefaultURL, xaiDefaultModelId, xaiModels, } from "../../../../src/shared/api" @@ -198,10 +201,40 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is LM Studio Ollama LiteLLM + AskSage X AI + {selectedProvider === "asksage" && ( +
+ + AskSage API Key + +

+ This key is stored locally and only used to make API requests from this extension. +

+ + AskSage API URL + +
+ )} + {selectedProvider === "anthropic" && (
@@ -1433,6 +1467,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration): return getProviderData(qwenModels, qwenDefaultModelId) case "mistral": return getProviderData(mistralModels, mistralDefaultModelId) + case "asksage": + return getProviderData(askSageModels, askSageDefaultModelId) case "openrouter": return { selectedProvider: provider, diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 58e90e3e84..67be29ddd4 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -79,6 +79,7 @@ export const ExtensionStateContextProvider: React.FC<{ config.qwenApiKey, config.mistralApiKey, config.vsCodeLmModelSelector, + config.asksageApiKey, config.xaiApiKey, ].some((key) => key !== undefined) : false diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index a3d2e4106e..c799ddf5aa 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -83,6 +83,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid model selector." } break + case "asksage": + if (!apiConfiguration.asksageApiKey) { + return "You must provide a valid API key or choose a different provider." + } + break } } return undefined