Просмотр исходного кода

Merge pull request #277 from samhvw8/feat/roo-custom-model-info-openai-compatible

feat: custom model info in openai compatible
Matt Rubens 11 месяцев назад
Родитель
Сommit
5dcb96739b

+ 1 - 1
src/api/providers/openai.ts

@@ -108,7 +108,7 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler {
 	getModel(): { id: string; info: ModelInfo } {
 	getModel(): { id: string; info: ModelInfo } {
 		return {
 		return {
 			id: this.options.openAiModelId ?? "",
 			id: this.options.openAiModelId ?? "",
-			info: openAiModelInfoSaneDefaults,
+			info: this.options.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults,
 		}
 		}
 	}
 	}
 
 

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

@@ -69,6 +69,7 @@ type GlobalStateKey =
 	| "taskHistory"
 	| "taskHistory"
 	| "openAiBaseUrl"
 	| "openAiBaseUrl"
 	| "openAiModelId"
 	| "openAiModelId"
+	| "openAiCustomModelInfo"
 	| "ollamaModelId"
 	| "ollamaModelId"
 	| "ollamaBaseUrl"
 	| "ollamaBaseUrl"
 	| "lmStudioModelId"
 	| "lmStudioModelId"
@@ -1208,6 +1209,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			openAiBaseUrl,
 			openAiBaseUrl,
 			openAiApiKey,
 			openAiApiKey,
 			openAiModelId,
 			openAiModelId,
+			openAiCustomModelInfo,
 			ollamaModelId,
 			ollamaModelId,
 			ollamaBaseUrl,
 			ollamaBaseUrl,
 			lmStudioModelId,
 			lmStudioModelId,
@@ -1241,6 +1243,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
 		await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
 		await this.storeSecret("openAiApiKey", openAiApiKey)
 		await this.storeSecret("openAiApiKey", openAiApiKey)
 		await this.updateGlobalState("openAiModelId", openAiModelId)
 		await this.updateGlobalState("openAiModelId", openAiModelId)
+		await this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo)
 		await this.updateGlobalState("ollamaModelId", ollamaModelId)
 		await this.updateGlobalState("ollamaModelId", ollamaModelId)
 		await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
 		await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
 		await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
 		await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
@@ -1857,6 +1860,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			openAiBaseUrl,
 			openAiBaseUrl,
 			openAiApiKey,
 			openAiApiKey,
 			openAiModelId,
 			openAiModelId,
+			openAiCustomModelInfo,
 			ollamaModelId,
 			ollamaModelId,
 			ollamaBaseUrl,
 			ollamaBaseUrl,
 			lmStudioModelId,
 			lmStudioModelId,
@@ -1920,6 +1924,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
 			this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
 			this.getSecret("openAiApiKey") as Promise<string | undefined>,
 			this.getSecret("openAiApiKey") as Promise<string | undefined>,
 			this.getGlobalState("openAiModelId") as Promise<string | undefined>,
 			this.getGlobalState("openAiModelId") as Promise<string | undefined>,
+			this.getGlobalState("openAiCustomModelInfo") as Promise<ModelInfo | undefined>,
 			this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
 			this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
 			this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
 			this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
 			this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
 			this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
@@ -2000,6 +2005,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 				openAiBaseUrl,
 				openAiBaseUrl,
 				openAiApiKey,
 				openAiApiKey,
 				openAiModelId,
 				openAiModelId,
+				openAiCustomModelInfo,
 				ollamaModelId,
 				ollamaModelId,
 				ollamaBaseUrl,
 				ollamaBaseUrl,
 				lmStudioModelId,
 				lmStudioModelId,

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -76,6 +76,7 @@ export interface WebviewMessage {
 		| "autoApprovalEnabled"
 		| "autoApprovalEnabled"
 		| "updateCustomMode"
 		| "updateCustomMode"
 		| "deleteCustomMode"
 		| "deleteCustomMode"
+		| "setopenAiCustomModelInfo"
 	text?: string
 	text?: string
 	disabled?: boolean
 	disabled?: boolean
 	askResponse?: ClineAskResponse
 	askResponse?: ClineAskResponse

+ 1 - 0
src/shared/api.ts

@@ -38,6 +38,7 @@ export interface ApiHandlerOptions {
 	openAiBaseUrl?: string
 	openAiBaseUrl?: string
 	openAiApiKey?: string
 	openAiApiKey?: string
 	openAiModelId?: string
 	openAiModelId?: string
+	openAiCustomModelInfo?: ModelInfo
 	ollamaModelId?: string
 	ollamaModelId?: string
 	ollamaBaseUrl?: string
 	ollamaBaseUrl?: string
 	lmStudioModelId?: string
 	lmStudioModelId?: string

+ 439 - 5
webview-ui/src/components/settings/ApiOptions.tsx

@@ -1,4 +1,4 @@
-import { Checkbox, Dropdown } from "vscrui"
+import { Checkbox, Dropdown, Pane } from "vscrui"
 import type { DropdownOption } from "vscrui"
 import type { DropdownOption } from "vscrui"
 import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
 import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
@@ -45,7 +45,7 @@ interface ApiOptionsProps {
 }
 }
 
 
 const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => {
 const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => {
-	const { apiConfiguration, setApiConfiguration, uriScheme, handleInputChange } = useExtensionState()
+	const { apiConfiguration, uriScheme, handleInputChange } = useExtensionState()
 	const [ollamaModels, setOllamaModels] = useState<string[]>([])
 	const [ollamaModels, setOllamaModels] = useState<string[]>([])
 	const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
 	const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
 	const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
 	const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
@@ -166,7 +166,11 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 						onChange={(checked: boolean) => {
 						onChange={(checked: boolean) => {
 							setAnthropicBaseUrlSelected(checked)
 							setAnthropicBaseUrlSelected(checked)
 							if (!checked) {
 							if (!checked) {
-								setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" })
+								handleInputChange("anthropicBaseUrl")({
+									target: {
+										value: "",
+									},
+								})
 							}
 							}
 						}}>
 						}}>
 						Use custom base URL
 						Use custom base URL
@@ -537,7 +541,11 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 						onChange={(checked: boolean) => {
 						onChange={(checked: boolean) => {
 							setAzureApiVersionSelected(checked)
 							setAzureApiVersionSelected(checked)
 							if (!checked) {
 							if (!checked) {
-								setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" })
+								handleInputChange("azureApiVersion")({
+									target: {
+										value: "",
+									},
+								})
 							}
 							}
 						}}>
 						}}>
 						Set Azure API version
 						Set Azure API version
@@ -550,6 +558,432 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 							placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
 							placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
 						/>
 						/>
 					)}
 					)}
+
+					<div
+						style={{
+							marginTop: 15,
+						}}
+					/>
+					<Pane
+						title="Model Configuration"
+						open={false}
+						actions={[
+							{
+								iconName: "refresh",
+								onClick: () =>
+									handleInputChange("openAiCustomModelInfo")({
+										target: { value: openAiModelInfoSaneDefaults },
+									}),
+							},
+						]}>
+						<div
+							style={{
+								padding: 15,
+								backgroundColor: "var(--vscode-editor-background)",
+							}}>
+							<p
+								style={{
+									fontSize: "12px",
+									color: "var(--vscode-descriptionForeground)",
+									margin: "0 0 15px 0",
+									lineHeight: "1.4",
+								}}>
+								Configure the capabilities and pricing for your custom OpenAI-compatible model. <br />
+								Be careful for the model capabilities, as they can affect how Roo Code can work.
+							</p>
+
+							{/* Capabilities Section */}
+							<div
+								style={{
+									marginBottom: 20,
+									padding: 12,
+									backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
+									borderRadius: 4,
+								}}>
+								<span
+									style={{
+										fontWeight: 500,
+										fontSize: "12px",
+										display: "block",
+										marginBottom: 12,
+										color: "var(--vscode-editor-foreground)",
+									}}>
+									Model Capabilities
+								</span>
+								<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
+									<div className="token-config-field">
+										<VSCodeTextField
+											value={
+												apiConfiguration?.openAiCustomModelInfo?.maxTokens?.toString() ||
+												openAiModelInfoSaneDefaults.maxTokens?.toString() ||
+												""
+											}
+											type="text"
+											style={{
+												width: "100%",
+												borderColor: (() => {
+													const value = apiConfiguration?.openAiCustomModelInfo?.maxTokens
+													if (!value) return "var(--vscode-input-border)"
+													return value > 0
+														? "var(--vscode-charts-green)"
+														: "var(--vscode-errorForeground)"
+												})(),
+											}}
+											title="Maximum number of tokens the model can generate in a single response"
+											onChange={(e: any) => {
+												const value = parseInt(e.target.value)
+												handleInputChange("openAiCustomModelInfo")({
+													target: {
+														value: {
+															...(apiConfiguration?.openAiCustomModelInfo ||
+																openAiModelInfoSaneDefaults),
+															maxTokens: isNaN(value) ? undefined : value,
+														},
+													},
+												})
+											}}
+											placeholder="e.g. 4096">
+											<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
+										</VSCodeTextField>
+										<div
+											style={{
+												fontSize: "11px",
+												color: "var(--vscode-descriptionForeground)",
+												marginTop: 4,
+												display: "flex",
+												alignItems: "center",
+												gap: 4,
+											}}>
+											<i className="codicon codicon-info" style={{ fontSize: "12px" }}></i>
+											<span>
+												Maximum number of tokens the model can generate in a response. <br />
+												(-1 is depend on server)
+											</span>
+										</div>
+									</div>
+
+									<div className="token-config-field">
+										<VSCodeTextField
+											value={
+												apiConfiguration?.openAiCustomModelInfo?.contextWindow?.toString() ||
+												openAiModelInfoSaneDefaults.contextWindow?.toString() ||
+												""
+											}
+											type="text"
+											style={{
+												width: "100%",
+												borderColor: (() => {
+													const value = apiConfiguration?.openAiCustomModelInfo?.contextWindow
+													if (!value) return "var(--vscode-input-border)"
+													return value > 0
+														? "var(--vscode-charts-green)"
+														: "var(--vscode-errorForeground)"
+												})(),
+											}}
+											title="Total number of tokens (input + output) the model can process in a single request"
+											onChange={(e: any) => {
+												const parsed = parseInt(e.target.value)
+												handleInputChange("openAiCustomModelInfo")({
+													target: {
+														value: {
+															...(apiConfiguration?.openAiCustomModelInfo ||
+																openAiModelInfoSaneDefaults),
+															contextWindow:
+																e.target.value === ""
+																	? undefined
+																	: isNaN(parsed)
+																		? openAiModelInfoSaneDefaults.contextWindow
+																		: parsed,
+														},
+													},
+												})
+											}}
+											placeholder="e.g. 128000">
+											<span style={{ fontWeight: 500 }}>Context Window Size</span>
+										</VSCodeTextField>
+										<div
+											style={{
+												fontSize: "11px",
+												color: "var(--vscode-descriptionForeground)",
+												marginTop: 4,
+												display: "flex",
+												alignItems: "center",
+												gap: 4,
+											}}>
+											<i className="codicon codicon-info" style={{ fontSize: "12px" }}></i>
+											<span>
+												Total tokens (input + output) the model can process. This will help Roo
+												Code run correctly.
+											</span>
+										</div>
+									</div>
+
+									<div
+										style={{
+											backgroundColor: "var(--vscode-editor-background)",
+											padding: "12px",
+											borderRadius: "4px",
+											marginTop: "8px",
+											border: "1px solid var(--vscode-input-border)",
+											transition: "background-color 0.2s ease",
+										}}>
+										<span
+											style={{
+												fontSize: "11px",
+												fontWeight: 500,
+												color: "var(--vscode-editor-foreground)",
+												display: "block",
+												marginBottom: "10px",
+											}}>
+											Model Features
+										</span>
+
+										<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
+											<div className="feature-toggle">
+												<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
+													<Checkbox
+														checked={
+															apiConfiguration?.openAiCustomModelInfo?.supportsImages ??
+															openAiModelInfoSaneDefaults.supportsImages
+														}
+														onChange={(checked: boolean) => {
+															handleInputChange("openAiCustomModelInfo")({
+																target: {
+																	value: {
+																		...(apiConfiguration?.openAiCustomModelInfo ||
+																			openAiModelInfoSaneDefaults),
+																		supportsImages: checked,
+																	},
+																},
+															})
+														}}>
+														<span style={{ fontWeight: 500 }}>Image Support</span>
+													</Checkbox>
+													<i
+														className="codicon codicon-info"
+														title="Enable if the model can process and understand images in the input. Required for image-based assistance and visual code understanding."
+														style={{
+															fontSize: "12px",
+															color: "var(--vscode-descriptionForeground)",
+															cursor: "help",
+														}}
+													/>
+												</div>
+												<p
+													style={{
+														fontSize: "11px",
+														color: "var(--vscode-descriptionForeground)",
+														marginLeft: "24px",
+														marginTop: "4px",
+														lineHeight: "1.4",
+													}}>
+													Allows the model to analyze and understand images, essential for
+													visual code assistance
+												</p>
+											</div>
+
+											<div
+												className="feature-toggle"
+												style={{
+													borderTop: "1px solid var(--vscode-input-border)",
+													paddingTop: "12px",
+												}}>
+												<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
+													<Checkbox
+														checked={
+															apiConfiguration?.openAiCustomModelInfo
+																?.supportsComputerUse ?? false
+														}
+														onChange={(checked: boolean) => {
+															handleInputChange("openAiCustomModelInfo")({
+																target: {
+																	value: {
+																		...(apiConfiguration?.openAiCustomModelInfo ||
+																			openAiModelInfoSaneDefaults),
+																		supportsComputerUse: checked,
+																	},
+																},
+															})
+														}}>
+														<span style={{ fontWeight: 500 }}>Computer Use</span>
+													</Checkbox>
+													<i
+														className="codicon codicon-info"
+														title="Enable if the model can interact with your computer through commands and file operations. Required for automated tasks and file modifications."
+														style={{
+															fontSize: "12px",
+															color: "var(--vscode-descriptionForeground)",
+															cursor: "help",
+														}}
+													/>
+												</div>
+												<p
+													style={{
+														fontSize: "11px",
+														color: "var(--vscode-descriptionForeground)",
+														marginLeft: "24px",
+														marginTop: "4px",
+														lineHeight: "1.4",
+													}}>
+													This model feature is for computer use like sonnet 3.5 support
+												</p>
+											</div>
+										</div>
+									</div>
+								</div>
+							</div>
+
+							{/* Pricing Section */}
+							<div
+								style={{
+									backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
+									padding: "12px",
+									borderRadius: "4px",
+									marginTop: "15px",
+								}}>
+								<div style={{ marginBottom: "12px" }}>
+									<span
+										style={{
+											fontWeight: 500,
+											fontSize: "12px",
+											color: "var(--vscode-editor-foreground)",
+											display: "block",
+											marginBottom: "4px",
+										}}>
+										Model Pricing
+									</span>
+									<span
+										style={{
+											fontSize: "11px",
+											color: "var(--vscode-descriptionForeground)",
+											display: "block",
+										}}>
+										Configure token-based pricing in USD per million tokens
+									</span>
+								</div>
+
+								<div
+									style={{
+										display: "grid",
+										gridTemplateColumns: "1fr 1fr",
+										gap: "12px",
+										backgroundColor: "var(--vscode-editor-background)",
+										padding: "12px",
+										borderRadius: "4px",
+									}}>
+									<div className="price-input">
+										<VSCodeTextField
+											value={
+												apiConfiguration?.openAiCustomModelInfo?.inputPrice?.toString() ??
+												openAiModelInfoSaneDefaults.inputPrice?.toString() ??
+												""
+											}
+											type="text"
+											style={{
+												width: "100%",
+												borderColor: (() => {
+													const value = apiConfiguration?.openAiCustomModelInfo?.inputPrice
+													if (!value && value !== 0) return "var(--vscode-input-border)"
+													return value >= 0
+														? "var(--vscode-charts-green)"
+														: "var(--vscode-errorForeground)"
+												})(),
+											}}
+											onChange={(e: any) => {
+												const parsed = parseFloat(e.target.value)
+												handleInputChange("openAiCustomModelInfo")({
+													target: {
+														value: {
+															...(apiConfiguration?.openAiCustomModelInfo ??
+																openAiModelInfoSaneDefaults),
+															inputPrice:
+																e.target.value === ""
+																	? undefined
+																	: isNaN(parsed)
+																		? openAiModelInfoSaneDefaults.inputPrice
+																		: parsed,
+														},
+													},
+												})
+											}}
+											placeholder="e.g. 0.0001">
+											<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
+												<span style={{ fontWeight: 500 }}>Input Price</span>
+												<i
+													className="codicon codicon-info"
+													title="Cost per million tokens in the input/prompt. This affects the cost of sending context and instructions to the model."
+													style={{
+														fontSize: "12px",
+														color: "var(--vscode-descriptionForeground)",
+														cursor: "help",
+													}}
+												/>
+											</div>
+										</VSCodeTextField>
+									</div>
+
+									<div className="price-input">
+										<VSCodeTextField
+											value={
+												apiConfiguration?.openAiCustomModelInfo?.outputPrice?.toString() ||
+												openAiModelInfoSaneDefaults.outputPrice?.toString() ||
+												""
+											}
+											type="text"
+											style={{
+												width: "100%",
+												borderColor: (() => {
+													const value = apiConfiguration?.openAiCustomModelInfo?.outputPrice
+													if (!value && value !== 0) return "var(--vscode-input-border)"
+													return value >= 0
+														? "var(--vscode-charts-green)"
+														: "var(--vscode-errorForeground)"
+												})(),
+											}}
+											onChange={(e: any) => {
+												const parsed = parseFloat(e.target.value)
+												handleInputChange("openAiCustomModelInfo")({
+													target: {
+														value: {
+															...(apiConfiguration?.openAiCustomModelInfo ||
+																openAiModelInfoSaneDefaults),
+															outputPrice:
+																e.target.value === ""
+																	? undefined
+																	: isNaN(parsed)
+																		? openAiModelInfoSaneDefaults.outputPrice
+																		: parsed,
+														},
+													},
+												})
+											}}
+											placeholder="e.g. 0.0002">
+											<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
+												<span style={{ fontWeight: 500 }}>Output Price</span>
+												<i
+													className="codicon codicon-info"
+													title="Cost per million tokens in the model's response. This affects the cost of generated content and completions."
+													style={{
+														fontSize: "12px",
+														color: "var(--vscode-descriptionForeground)",
+														cursor: "help",
+													}}
+												/>
+											</div>
+										</VSCodeTextField>
+									</div>
+								</div>
+							</div>
+						</div>
+					</Pane>
+					<div
+						style={{
+							marginTop: 15,
+						}}
+					/>
+
+					{/* end Model Info Configuration */}
+
 					<p
 					<p
 						style={{
 						style={{
 							fontSize: "12px",
 							fontSize: "12px",
@@ -1031,7 +1465,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
 			return {
 			return {
 				selectedProvider: provider,
 				selectedProvider: provider,
 				selectedModelId: apiConfiguration?.openAiModelId || "",
 				selectedModelId: apiConfiguration?.openAiModelId || "",
-				selectedModelInfo: openAiModelInfoSaneDefaults,
+				selectedModelInfo: apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults,
 			}
 			}
 		case "ollama":
 		case "ollama":
 			return {
 			return {