Bläddra i källkod

feat(openai): add custom model info configuration

Adds support for configuring custom OpenAI-compatible model capabilities and pricing, including:

Max output tokens
Context window size
Image/computer use support
Input/output token pricing
Cache read/write pricing
sam hoang 1 år sedan
förälder
incheckning
d50e075c75

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

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

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

@@ -68,6 +68,7 @@ type GlobalStateKey =
 	| "taskHistory"
 	| "openAiBaseUrl"
 	| "openAiModelId"
+	| "openAiCusModelInfo"
 	| "ollamaModelId"
 	| "ollamaBaseUrl"
 	| "lmStudioModelId"
@@ -1198,6 +1199,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			openAiBaseUrl,
 			openAiApiKey,
 			openAiModelId,
+			openAiCusModelInfo,
 			ollamaModelId,
 			ollamaBaseUrl,
 			lmStudioModelId,
@@ -1231,6 +1233,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
 		await this.storeSecret("openAiApiKey", openAiApiKey)
 		await this.updateGlobalState("openAiModelId", openAiModelId)
+		await this.updateGlobalState("openAiCusModelInfo", openAiCusModelInfo)
 		await this.updateGlobalState("ollamaModelId", ollamaModelId)
 		await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
 		await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
@@ -1847,6 +1850,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			openAiBaseUrl,
 			openAiApiKey,
 			openAiModelId,
+			openAiCusModelInfo,
 			ollamaModelId,
 			ollamaBaseUrl,
 			lmStudioModelId,
@@ -1910,6 +1914,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
 			this.getSecret("openAiApiKey") as Promise<string | undefined>,
 			this.getGlobalState("openAiModelId") as Promise<string | undefined>,
+			this.getGlobalState("openAiCusModelInfo") as Promise<ModelInfo | undefined>,
 			this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
 			this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
 			this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
@@ -1990,6 +1995,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 				openAiBaseUrl,
 				openAiApiKey,
 				openAiModelId,
+				openAiCusModelInfo,
 				ollamaModelId,
 				ollamaBaseUrl,
 				lmStudioModelId,

+ 1 - 0
src/shared/WebviewMessage.ts

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

+ 1 - 0
src/shared/api.ts

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

+ 179 - 1
webview-ui/src/components/settings/ApiOptions.tsx

@@ -550,6 +550,184 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 							placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
 						/>
 					)}
+
+					{/* Model Info Configuration */}
+					<div style={{ marginTop: 15, padding: 10, border: '1px solid var(--vscode-input-border)', borderRadius: 4 }}>
+						<div style={{ marginBottom: 10 }}>
+							<span style={{ fontWeight: 500, fontSize: '14px' }}>Model Configuration</span>
+							<p style={{ fontSize: '12px', color: 'var(--vscode-descriptionForeground)', margin: '5px 0' }}>
+								Configure the capabilities and pricing for your custom OpenAI-compatible model
+							</p>
+						</div>
+
+						{/* Capabilities Section */}
+						<div style={{ marginBottom: 15 }}>
+							<span style={{ fontWeight: 500, fontSize: '12px', color: 'var(--vscode-descriptionForeground)' }}>Capabilities</span>
+							<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 5 }}>
+								<VSCodeTextField
+									value={apiConfiguration?.openAiCusModelInfo?.maxTokens?.toString() || openAiModelInfoSaneDefaults.maxTokens?.toString() || ""}
+									type="text"
+									style={{ width: "100%" }}
+									title="Maximum number of tokens the model can generate in a single response"
+									onInput={(e: any) => {
+										const value = parseInt(e.target.value)
+										setApiConfiguration({
+											...apiConfiguration,
+											openAiCusModelInfo: {
+												...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults),
+												maxTokens: isNaN(value) ? undefined : value
+											}
+										})
+									}}
+									placeholder="e.g. 4096">
+									<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
+								</VSCodeTextField>
+
+								<VSCodeTextField
+									value={apiConfiguration?.openAiCusModelInfo?.contextWindow?.toString() || openAiModelInfoSaneDefaults.contextWindow?.toString() || ""}
+									type="text"
+									style={{ width: "100%" }}
+									title="Total number of tokens (input + output) the model can process in a single request"
+									onInput={(e: any) => {
+										const parsed = parseInt(e.target.value)
+										setApiConfiguration({
+											...apiConfiguration,
+											openAiCusModelInfo: {
+												...(apiConfiguration?.openAiCusModelInfo || 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={{ display: "flex", gap: 20, marginTop: 5 }}>
+									<VSCodeCheckbox
+										checked={apiConfiguration?.openAiCusModelInfo?.supportsImages ?? openAiModelInfoSaneDefaults.supportsImages}
+										title="Enable if the model can process and understand images in the input"
+										onChange={(e: any) => {
+											setApiConfiguration({
+												...apiConfiguration,
+												openAiCusModelInfo: {
+													...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults),
+													supportsImages: e.target.checked
+												}
+											})
+										}}>
+										Supports Images
+									</VSCodeCheckbox>
+
+									<VSCodeCheckbox
+										checked={apiConfiguration?.openAiCusModelInfo?.supportsComputerUse ?? false}
+										title="Enable if the model can interact with the computer (execute commands, modify files, etc.)"
+										onChange={(e: any) => {
+											setApiConfiguration({
+												...apiConfiguration,
+												openAiCusModelInfo: {
+													...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults),
+													supportsComputerUse: e.target.checked
+												}
+											})
+										}}>
+										Supports Computer Use
+									</VSCodeCheckbox>
+								</div>
+							</div>
+						</div>
+
+						{/* Pricing Section */}
+						<div>
+							<span style={{ fontWeight: 500, fontSize: '12px', color: 'var(--vscode-descriptionForeground)' }}>Pricing (USD per million tokens)</span>
+							<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 5 }}>
+								{/* Input/Output Prices */}
+								<div style={{ display: "flex", gap: 10 }}>
+									<VSCodeTextField
+										value={apiConfiguration?.openAiCusModelInfo?.inputPrice?.toString() || openAiModelInfoSaneDefaults.inputPrice?.toString() || ""}
+										type="text"
+										style={{ width: "100%" }}
+										title="Cost per million tokens in the input/prompt"
+										onChange={(e: any) => {
+											const parsed = parseFloat(e.target.value)
+											setApiConfiguration({
+												...apiConfiguration,
+												openAiCusModelInfo: {
+													...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults),
+													inputPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.inputPrice : parsed)
+												}
+											})
+										}}
+										placeholder="e.g. 0.0001">
+										<span style={{ fontWeight: 500 }}>Input Price</span>
+									</VSCodeTextField>
+
+									<VSCodeTextField
+										value={apiConfiguration?.openAiCusModelInfo?.outputPrice?.toString() || openAiModelInfoSaneDefaults.outputPrice?.toString() || ""}
+										type="text"
+										style={{ width: "100%" }}
+										title="Cost per million tokens in the model's response"
+										onChange={(e: any) => {
+											const parsed = parseFloat(e.target.value)
+											setApiConfiguration({
+												...apiConfiguration,
+												openAiCusModelInfo: {
+													...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults),
+													outputPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.outputPrice : parsed)
+												}
+											})
+										}}
+										placeholder="e.g. 0.0002">
+										<span style={{ fontWeight: 500 }}>Output Price</span>
+									</VSCodeTextField>
+								</div>
+
+								{/* Cache Prices */}
+								<div style={{ display: "flex", gap: 10 }}>
+									<VSCodeTextField
+										value={apiConfiguration?.openAiCusModelInfo?.cacheWritesPrice?.toString() || openAiModelInfoSaneDefaults.cacheWritesPrice?.toString() || ""}
+										type="text"
+										style={{ width: "100%" }}
+										title="Cost per million tokens when writing to the prompt cache"
+										onChange={(e: any) => {
+											const parsed = parseFloat(e.target.value)
+											setApiConfiguration({
+												...apiConfiguration,
+												openAiCusModelInfo: {
+													...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults),
+													cacheWritesPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.cacheWritesPrice : parsed)
+												}
+											})
+										}}
+										placeholder="e.g. 0.0001">
+										<span style={{ fontWeight: 500 }}>Cache Write Price</span>
+									</VSCodeTextField>
+
+									<VSCodeTextField
+										value={apiConfiguration?.openAiCusModelInfo?.cacheReadsPrice?.toString() || openAiModelInfoSaneDefaults.cacheReadsPrice?.toString() || ""}
+										type="text"
+										style={{ width: "100%" }}
+										title="Cost per million tokens when reading from the prompt cache"
+										onChange={(e: any) => {
+											const parsed = parseFloat(e.target.value)
+											setApiConfiguration({
+												...apiConfiguration,
+												openAiCusModelInfo: {
+													...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults),
+													cacheReadsPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.cacheReadsPrice : parsed)
+												}
+											})
+										}}
+										placeholder="e.g. 0.00001">
+										<span style={{ fontWeight: 500 }}>Cache Read Price</span>
+									</VSCodeTextField>
+								</div>
+							</div>
+						</div>
+					</div>
+
+					{ /* TODO: model info here */}
+
+
 					<p
 						style={{
 							fontSize: "12px",
@@ -1031,7 +1209,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
 			return {
 				selectedProvider: provider,
 				selectedModelId: apiConfiguration?.openAiModelId || "",
-				selectedModelInfo: openAiModelInfoSaneDefaults,
+				selectedModelInfo: apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults,
 			}
 		case "ollama":
 			return {