Adds unbound provider to roo cline

This commit is contained in:
Pugazhendhi
2025-01-22 16:15:25 +05:30
committed by Vignesh Subbiah
parent db0ec64d1c
commit 62dcfbe549
5 changed files with 118 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ import { DeepSeekHandler } from "./providers/deepseek"
import { MistralHandler } from "./providers/mistral" import { MistralHandler } from "./providers/mistral"
import { VsCodeLmHandler } from "./providers/vscode-lm" import { VsCodeLmHandler } from "./providers/vscode-lm"
import { ApiStream } from "./transform/stream" import { ApiStream } from "./transform/stream"
import { UnboundHandler } from "./providers/unbound"
export interface SingleCompletionHandler { export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string> completePrompt(prompt: string): Promise<string>
@@ -53,6 +54,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new VsCodeLmHandler(options) return new VsCodeLmHandler(options)
case "mistral": case "mistral":
return new MistralHandler(options) return new MistralHandler(options)
case "unbound":
return new UnboundHandler(options)
default: default:
return new AnthropicHandler(options) return new AnthropicHandler(options)
} }

View File

@@ -0,0 +1,59 @@
import { ApiHandlerOptions, unboundModels, UnboundModelId, unboundDefaultModelId, ModelInfo } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from "../index"
export class UnboundHandler implements ApiHandler {
private unboundApiKey: string
private unboundModelId: string
private unboundBaseUrl: string = "https://ai-gateway-43843357113.us-west1.run.app/v1"
private options: ApiHandlerOptions
constructor(options: ApiHandlerOptions) {
this.options = options
this.unboundApiKey = options.unboundApiKey || ""
this.unboundModelId = options.unboundModelId || ""
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const response = await fetch(`${this.unboundBaseUrl}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.unboundApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: this.unboundModelId,
messages: [{ role: "system", content: systemPrompt }, ...messages],
}),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
yield {
type: "text",
text: data.choices[0]?.message?.content || "",
}
yield {
type: "usage",
inputTokens: data.usage?.prompt_tokens || 0,
outputTokens: data.usage?.completion_tokens || 0,
}
}
getModel(): { id: UnboundModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in unboundModels) {
const id = modelId as UnboundModelId
return { id, info: unboundModels[id] }
}
return {
id: unboundDefaultModelId,
info: unboundModels[unboundDefaultModelId],
}
}
}

View File

@@ -62,6 +62,7 @@ type SecretKey =
| "openAiNativeApiKey" | "openAiNativeApiKey"
| "deepSeekApiKey" | "deepSeekApiKey"
| "mistralApiKey" | "mistralApiKey"
| "unboundApiKey"
type GlobalStateKey = type GlobalStateKey =
| "apiProvider" | "apiProvider"
| "apiModelId" | "apiModelId"
@@ -120,6 +121,7 @@ type GlobalStateKey =
| "experimentalDiffStrategy" | "experimentalDiffStrategy"
| "autoApprovalEnabled" | "autoApprovalEnabled"
| "customModes" // Array of custom modes | "customModes" // Array of custom modes
| "unboundModelId"
export const GlobalFileNames = { export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json", apiConversationHistory: "api_conversation_history.json",
@@ -1309,6 +1311,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterUseMiddleOutTransform, openRouterUseMiddleOutTransform,
vsCodeLmModelSelector, vsCodeLmModelSelector,
mistralApiKey, mistralApiKey,
unboundApiKey,
unboundModelId,
} = apiConfiguration } = apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider) await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId) await this.updateGlobalState("apiModelId", apiModelId)
@@ -1347,6 +1351,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform) await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector) await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
await this.storeSecret("mistralApiKey", mistralApiKey) await this.storeSecret("mistralApiKey", mistralApiKey)
await this.storeSecret("unboundApiKey", unboundApiKey)
await this.updateGlobalState("unboundModelId", unboundModelId)
if (this.cline) { if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration) this.cline.api = buildApiHandler(apiConfiguration)
} }
@@ -2001,6 +2007,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
experimentalDiffStrategy, experimentalDiffStrategy,
autoApprovalEnabled, autoApprovalEnabled,
customModes, customModes,
unboundApiKey,
unboundModelId,
] = await Promise.all([ ] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>, this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>, this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -2070,6 +2078,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>, this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>, this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
this.customModesManager.getCustomModes(), this.customModesManager.getCustomModes(),
this.getSecret("unboundApiKey") as Promise<string | undefined>,
this.getGlobalState("unboundModelId") as Promise<string | undefined>,
]) ])
let apiProvider: ApiProvider let apiProvider: ApiProvider
@@ -2125,6 +2135,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterBaseUrl, openRouterBaseUrl,
openRouterUseMiddleOutTransform, openRouterUseMiddleOutTransform,
vsCodeLmModelSelector, vsCodeLmModelSelector,
unboundApiKey,
unboundModelId,
}, },
lastShownAnnouncementId, lastShownAnnouncementId,
customInstructions, customInstructions,
@@ -2273,6 +2285,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
"openAiNativeApiKey", "openAiNativeApiKey",
"deepSeekApiKey", "deepSeekApiKey",
"mistralApiKey", "mistralApiKey",
"unboundApiKey",
] ]
for (const key of secretKeys) { for (const key of secretKeys) {
await this.storeSecret(key, undefined) await this.storeSecret(key, undefined)

View File

@@ -14,6 +14,7 @@ export type ApiProvider =
| "deepseek" | "deepseek"
| "vscode-lm" | "vscode-lm"
| "mistral" | "mistral"
| "unbound"
export interface ApiHandlerOptions { export interface ApiHandlerOptions {
apiModelId?: string apiModelId?: string
@@ -57,6 +58,8 @@ export interface ApiHandlerOptions {
deepSeekBaseUrl?: string deepSeekBaseUrl?: string
deepSeekApiKey?: string deepSeekApiKey?: string
includeMaxTokens?: boolean includeMaxTokens?: boolean
unboundApiKey?: string
unboundModelId?: string
} }
export type ApiConfiguration = ApiHandlerOptions & { export type ApiConfiguration = ApiHandlerOptions & {
@@ -593,3 +596,11 @@ export const mistralModels = {
outputPrice: 0.9, outputPrice: 0.9,
}, },
} as const satisfies Record<string, ModelInfo> } as const satisfies Record<string, ModelInfo>
// Unbound Security
export type UnboundModelId = keyof typeof unboundModels
export const unboundDefaultModelId = "gpt-4o"
export const unboundModels = {
"gpt-4o": openAiNativeModels["gpt-4o"],
"claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"],
} as const satisfies Record<string, ModelInfo>

View File

@@ -26,6 +26,8 @@ import {
openRouterDefaultModelInfo, openRouterDefaultModelInfo,
vertexDefaultModelId, vertexDefaultModelId,
vertexModels, vertexModels,
unboundDefaultModelId,
unboundModels,
} from "../../../../src/shared/api" } from "../../../../src/shared/api"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../../context/ExtensionStateContext" import { useExtensionState } from "../../context/ExtensionStateContext"
@@ -147,6 +149,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ value: "mistral", label: "Mistral" }, { value: "mistral", label: "Mistral" },
{ value: "lmstudio", label: "LM Studio" }, { value: "lmstudio", label: "LM Studio" },
{ value: "ollama", label: "Ollama" }, { value: "ollama", label: "Ollama" },
{ value: "unbound", label: "Unbound" },
]} ]}
/> />
</div> </div>
@@ -1283,6 +1286,27 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
</div> </div>
)} )}
{selectedProvider === "unbound" && (
<div>
<VSCodeTextField
value={apiConfiguration?.unboundApiKey || ""}
style={{ width: "100%" }}
type="password"
onChange={handleInputChange("unboundApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>Unbound 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>
</div>
)}
{apiErrorMessage && ( {apiErrorMessage && (
<p <p
style={{ style={{
@@ -1315,6 +1339,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)} {selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)} {selectedProvider === "mistral" && createDropdown(mistralModels)}
{selectedProvider === "unbound" && createDropdown(unboundModels)}
</div> </div>
<ModelInfoView <ModelInfoView
@@ -1552,6 +1577,13 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
supportsImages: false, // VSCode LM API currently doesn't support images supportsImages: false, // VSCode LM API currently doesn't support images
}, },
} }
case "unbound":
return getProviderData(unboundModels, unboundDefaultModelId)
// return {
// selectedProvider: provider,
// selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
// selectedModelInfo: openAiModelInfoSaneDefaults,
// }
default: default:
return getProviderData(anthropicModels, anthropicDefaultModelId) return getProviderData(anthropicModels, anthropicDefaultModelId)
} }