Browse Source

Adds unbound provider to roo cline

Pugazhendhi 11 months ago
parent
commit
62dcfbe549

+ 3 - 0
src/api/index.ts

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

+ 59 - 0
src/api/providers/unbound.ts

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

+ 13 - 0
src/core/webview/ClineProvider.ts

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

+ 11 - 0
src/shared/api.ts

@@ -14,6 +14,7 @@ export type ApiProvider =
 	| "deepseek"
 	| "vscode-lm"
 	| "mistral"
+	| "unbound"
 
 export interface ApiHandlerOptions {
 	apiModelId?: string
@@ -57,6 +58,8 @@ export interface ApiHandlerOptions {
 	deepSeekBaseUrl?: string
 	deepSeekApiKey?: string
 	includeMaxTokens?: boolean
+	unboundApiKey?: string
+	unboundModelId?: string
 }
 
 export type ApiConfiguration = ApiHandlerOptions & {
@@ -593,3 +596,11 @@ export const mistralModels = {
 		outputPrice: 0.9,
 	},
 } 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>

+ 32 - 0
webview-ui/src/components/settings/ApiOptions.tsx

@@ -26,6 +26,8 @@ import {
 	openRouterDefaultModelInfo,
 	vertexDefaultModelId,
 	vertexModels,
+	unboundDefaultModelId,
+	unboundModels,
 } from "../../../../src/shared/api"
 import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
 import { useExtensionState } from "../../context/ExtensionStateContext"
@@ -147,6 +149,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 						{ value: "mistral", label: "Mistral" },
 						{ value: "lmstudio", label: "LM Studio" },
 						{ value: "ollama", label: "Ollama" },
+						{ value: "unbound", label: "Unbound" },
 					]}
 				/>
 			</div>
@@ -1283,6 +1286,27 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 				</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 && (
 				<p
 					style={{
@@ -1315,6 +1339,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 							{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
 							{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
 							{selectedProvider === "mistral" && createDropdown(mistralModels)}
+							{selectedProvider === "unbound" && createDropdown(unboundModels)}
 						</div>
 
 						<ModelInfoView
@@ -1552,6 +1577,13 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
 					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:
 			return getProviderData(anthropicModels, anthropicDefaultModelId)
 	}