Compare commits

...
Author SHA1 Message Date
Saoud Rizwan dd970b7258 Add missing API provider implementation 2025-03-12 18:53:28 -07:00
celestial-vault f53d7a9011 remove unused imports 2025-03-12 17:51:05 -07:00
celestial-vault 04ec9be993 support deepseek and qwen models 2025-03-12 17:45:16 -07:00
0829f23d38 Feat/add sambanova provider (#2142)
* feat(api): Add SambaNova provider type definitions

- Add 'sambanova' to ApiProvider type
- Add sambanovaApiKey to ApiHandlerOptions interface
- Add SambaNova model definitions

* feat(api): Implement SambaNova provider handler

- Create SambanovaHandler class
- Implement createMessage and getModel methods
- Register the provider in the API factory

* feat(ui): Add SambaNova provider UI components

- Add SambaNova to provider dropdown
- Add UI components for API key and model selection

* feat(storage): Update secret storage for SambaNova API key

- Update SecretKey type to include sambanovaApiKey
- Update getState method to include the SambaNova API key
- Update API configuration handler to store the SambaNova API key

* feat(sambanova): format fix

* feat(sambanova): adding previously untracked files

* fix: will ensure that when no model ID is explicitly selected for the Sambanova provider, it will use the default model ID that's already defined in the codebase (Meta-Llama-3.3-70B-Instruct)

* fix: changing how the dropdown is created by following existing pattern

* fix: changing how the dropdown is created by following existing pattern

* fix: added SamanovaHandler to buildApiHandler

---------

Co-authored-by: saad.khan.hd <saad.khan@hd.digital>
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-03-12 15:08:08 -07:00
celestial-vault f2267128b3 Set up branch 2025-03-12 12:32:15 -07:00
8 changed files with 222 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add SambaNova as a new API provider with support for text-based models. Users can now connect to SambaNova's API using their API key and access their hosted LLM services directly from within Cline.
+3
View File
@@ -20,6 +20,7 @@ import { ClineHandler } from "./providers/cline"
import { LiteLlmHandler } from "./providers/litellm"
import { AskSageHandler } from "./providers/asksage"
import { XAIHandler } from "./providers/xai"
import { SambanovaHandler } from "./providers/sambanova"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -71,6 +72,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new AskSageHandler(options)
case "xai":
return new XAIHandler(options)
case "sambanova":
return new SambanovaHandler(options)
default:
return new AnthropicHandler(options)
}
+75
View File
@@ -0,0 +1,75 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } 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 SambanovaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const modelId = model.id.toLowerCase()
if (modelId.includes("deepseek") || modelId.includes("qwen") || modelId.includes("qwq")) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
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 (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in sambanovaModels) {
const id = modelId as SambanovaModelId
return { id, info: sambanovaModels[id] }
}
return {
id: sambanovaDefaultModelId,
info: sambanovaModels[sambanovaDefaultModelId],
}
}
}
+7
View File
@@ -62,6 +62,7 @@ type SecretKey =
| "authNonce"
| "asksageApiKey"
| "xaiApiKey"
| "sambanovaApiKey"
type GlobalStateKey =
| "apiProvider"
| "apiModelId"
@@ -1138,6 +1139,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
xaiApiKey,
thinkingBudgetTokens,
clineApiKey,
sambanovaApiKey,
} = apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
@@ -1185,6 +1187,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("asksageApiUrl", asksageApiUrl)
await this.updateGlobalState("thinkingBudgetTokens", thinkingBudgetTokens)
await this.storeSecret("clineApiKey", clineApiKey)
await this.storeSecret("sambanovaApiKey", sambanovaApiKey)
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
}
@@ -1983,6 +1986,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
sambanovaApiKey,
planActSeparateModelsSettingRaw,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
@@ -2043,6 +2047,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getGlobalState("asksageApiUrl") as Promise<string | undefined>,
this.getSecret("xaiApiKey") as Promise<string | undefined>,
this.getGlobalState("thinkingBudgetTokens") as Promise<number | undefined>,
this.getSecret("sambanovaApiKey") as Promise<string | undefined>,
this.getGlobalState("planActSeparateModelsSetting") as Promise<boolean | undefined>,
])
@@ -2133,6 +2138,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
asksageApiKey,
asksageApiUrl,
xaiApiKey,
sambanovaApiKey,
},
lastShownAnnouncementId,
customInstructions,
@@ -2279,6 +2285,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
"liteLlmApiKey",
"asksageApiKey",
"xaiApiKey",
"sambanovaApiKey",
]
for (const key of secretKeys) {
await this.storeSecret(key, undefined)
+89
View File
@@ -18,6 +18,7 @@ export type ApiProvider =
| "litellm"
| "asksage"
| "xai"
| "sambanova"
export interface ApiHandlerOptions {
apiModelId?: string
@@ -66,6 +67,7 @@ export interface ApiHandlerOptions {
asksageApiKey?: string
xaiApiKey?: string
thinkingBudgetTokens?: number
sambanovaApiKey?: string
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -1258,3 +1260,90 @@ export const xaiModels = {
description: "X AI's Grok Beta model (legacy) with 131K context window",
},
} as const satisfies Record<string, ModelInfo>
// SambaNova
// https://docs.sambanova.ai/cloud/docs/get-started/supported-models
export type SambanovaModelId = keyof typeof sambanovaModels
export const sambanovaDefaultModelId: SambanovaModelId = "Meta-Llama-3.3-70B-Instruct"
export const sambanovaModels = {
"Meta-Llama-3.3-70B-Instruct": {
maxTokens: 4096,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"DeepSeek-R1-Distill-Llama-70B": {
maxTokens: 4096,
contextWindow: 32_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Llama-3.1-Swallow-70B-Instruct-v0.3": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Llama-3.1-Swallow-8B-Instruct-v0.3": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Meta-Llama-3.1-405B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Meta-Llama-3.1-8B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Meta-Llama-3.2-1B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Qwen2.5-72B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Qwen2.5-Coder-32B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"QwQ-32B-Preview": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
} as const satisfies Record<string, ModelInfo>
@@ -43,6 +43,8 @@ import {
askSageDefaultURL,
xaiDefaultModelId,
xaiModels,
sambanovaModels,
sambanovaDefaultModelId,
} from "../../../../src/shared/api"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../../context/ExtensionStateContext"
@@ -207,6 +209,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
<VSCodeOption value="asksage">AskSage</VSCodeOption>
<VSCodeOption value="xai">X AI</VSCodeOption>
<VSCodeOption value="sambanova">SambaNova</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
@@ -1266,6 +1269,37 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "sambanova" && (
<div>
<VSCodeTextField
value={apiConfiguration?.sambanovaApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("sambanovaApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>SambaNova 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.
{!apiConfiguration?.sambanovaApiKey && (
<VSCodeLink
href="https://docs.sambanova.ai/cloud/docs/get-started/overview"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a SambaNova API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)}
{apiErrorMessage && (
<p
style={{
@@ -1304,6 +1338,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "mistral" && createDropdown(mistralModels)}
{selectedProvider === "asksage" && createDropdown(askSageModels)}
{selectedProvider === "xai" && createDropdown(xaiModels)}
{selectedProvider === "sambanova" && createDropdown(sambanovaModels)}
</DropdownContainer>
{((selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219") ||
@@ -1576,6 +1611,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
case "sambanova":
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
@@ -83,6 +83,7 @@ export const ExtensionStateContextProvider: React.FC<{
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
+5
View File
@@ -93,6 +93,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
return "You must provide a valid API key or choose a different provider."
}
break
case "sambanova":
if (!apiConfiguration.sambanovaApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
}
}
return undefined