feat: add asksage support (#2011)

* feat: add asksage support

* chore: create changeset for asksage support

* Fix Typo

Fix Typo

* chore: fix lint

* Fixes

* Validate asksage API key

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
This commit is contained in:
Steven White 2025-03-01 18:20:58 -05:00 committed by GitHub
parent 3d34de4908
commit d103f8ee62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 216 additions and 0 deletions

View file

@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add support for AskSage as model provider.

View file

@ -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:

View file

@ -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],
}
}
}

View file

@ -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<string | undefined>,
this.getSecret("liteLlmApiKey") as Promise<string | undefined>,
this.getGlobalState("telemetrySetting") as Promise<TelemetrySetting | undefined>,
this.getSecret("asksageApiKey") as Promise<string | undefined>,
this.getGlobalState("asksageApiUrl") as Promise<string | undefined>,
this.getSecret("xaiApiKey") as Promise<string | undefined>,
this.getGlobalState("thinkingBudgetTokens") as Promise<number | undefined>,
])
@ -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) {

View file

@ -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

View file

@ -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
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
<VSCodeOption value="asksage">AskSage</VSCodeOption>
<VSCodeOption value="xai">X AI</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
{selectedProvider === "asksage" && (
<div>
<VSCodeTextField
value={apiConfiguration?.asksageApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("asksageApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>AskSage API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API requests from this extension.
</p>
<VSCodeTextField
value={apiConfiguration?.asksageApiUrl || askSageDefaultURL}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("asksageApiUrl")}
placeholder="Enter AskSage API URL...">
<span style={{ fontWeight: 500 }}>AskSage API URL</span>
</VSCodeTextField>
</div>
)}
{selectedProvider === "anthropic" && (
<div>
<VSCodeTextField
@ -1216,6 +1249,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "qwen" && createDropdown(qwenModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
{selectedProvider === "asksage" && createDropdown(askSageModels)}
{selectedProvider === "xai" && createDropdown(xaiModels)}
</DropdownContainer>
@ -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,

View file

@ -79,6 +79,7 @@ export const ExtensionStateContextProvider: React.FC<{
config.qwenApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.asksageApiKey,
config.xaiApiKey,
].some((key) => key !== undefined)
: false

View file

@ -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