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
This commit is contained in:
sam hoang
2025-01-12 19:28:25 +07:00
parent 9a2bfcce64
commit d50e075c75
5 changed files with 188 additions and 2 deletions

View File

@@ -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.openAiCusModelInfo ?? openAiModelInfoSaneDefaults,
} }
} }

View File

@@ -68,6 +68,7 @@ type GlobalStateKey =
| "taskHistory" | "taskHistory"
| "openAiBaseUrl" | "openAiBaseUrl"
| "openAiModelId" | "openAiModelId"
| "openAiCusModelInfo"
| "ollamaModelId" | "ollamaModelId"
| "ollamaBaseUrl" | "ollamaBaseUrl"
| "lmStudioModelId" | "lmStudioModelId"
@@ -1198,6 +1199,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openAiBaseUrl, openAiBaseUrl,
openAiApiKey, openAiApiKey,
openAiModelId, openAiModelId,
openAiCusModelInfo,
ollamaModelId, ollamaModelId,
ollamaBaseUrl, ollamaBaseUrl,
lmStudioModelId, lmStudioModelId,
@@ -1231,6 +1233,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("openAiCusModelInfo", openAiCusModelInfo)
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)
@@ -1847,6 +1850,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openAiBaseUrl, openAiBaseUrl,
openAiApiKey, openAiApiKey,
openAiModelId, openAiModelId,
openAiCusModelInfo,
ollamaModelId, ollamaModelId,
ollamaBaseUrl, ollamaBaseUrl,
lmStudioModelId, lmStudioModelId,
@@ -1910,6 +1914,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("openAiCusModelInfo") 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>,
@@ -1990,6 +1995,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openAiBaseUrl, openAiBaseUrl,
openAiApiKey, openAiApiKey,
openAiModelId, openAiModelId,
openAiCusModelInfo,
ollamaModelId, ollamaModelId,
ollamaBaseUrl, ollamaBaseUrl,
lmStudioModelId, lmStudioModelId,

View File

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

View File

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

View File

@@ -550,6 +550,184 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`} 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 <p
style={{ style={{
fontSize: "12px", fontSize: "12px",
@@ -1031,7 +1209,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return { return {
selectedProvider: provider, selectedProvider: provider,
selectedModelId: apiConfiguration?.openAiModelId || "", selectedModelId: apiConfiguration?.openAiModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults, selectedModelInfo: apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults,
} }
case "ollama": case "ollama":
return { return {