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

feat: custom model info in openai compatible
This commit is contained in:
Matt Rubens
2025-01-21 11:44:49 -05:00
committed by GitHub
5 changed files with 448 additions and 6 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.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults,
} }
} }

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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 {