feat: add Azure AI models support in context and API handling

This commit is contained in:
pacnpal
2025-02-03 13:08:13 -05:00
parent 412eb9ef92
commit 8f0548ec97
5 changed files with 574 additions and 293 deletions

View File

@@ -754,6 +754,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.postMessageToWebview({ type: "openAiModels", openAiModels })
}
break
case "refreshAzureAiModels":
if (message?.values?.endpoint && message?.values?.key) {
const azureAiModels = await this.getAzureAiModels(
message?.values?.endpoint,
message?.values?.key,
)
this.postMessageToWebview({ type: "azureAiModels", azureAiModels })
}
break
case "openImage":
openImage(message.text!)
break
@@ -1645,6 +1654,28 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
// Azure AI
async getAzureAiModels(endpoint?: string, key?: string) {
try {
if (!endpoint || !key) {
return []
}
if (!URL.canParse(endpoint)) {
return []
}
const response = await axios.get(`${endpoint}/deployments`, {
headers: { "api-key": key },
})
const modelsArray = response.data?.value?.map((deployment: any) => deployment.name) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
return []
}
}
// OpenRouter
async handleOpenRouterCallback(code: string) {

View File

@@ -27,6 +27,8 @@ export interface ExtensionMessage {
| "glamaModels"
| "openRouterModels"
| "openAiModels"
| "refreshAzureAiModels"
| "azureAiModels"
| "mcpServers"
| "enhancedPrompt"
| "commitSearchResults"
@@ -63,6 +65,7 @@ export interface ExtensionMessage {
glamaModels?: Record<string, ModelInfo>
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
azureAiModels?: string[]
mcpServers?: McpServer[]
commits?: GitCommit[]
listApiConfig?: ApiConfigMeta[]

View File

@@ -16,6 +16,7 @@ export function checkExistKey(config: ApiConfiguration | undefined) {
config.deepSeekApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.azureAiKey,
].some((key) => key !== undefined)
: false
}

View File

@@ -1,19 +1,170 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import debounce from "debounce"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { highlightFzfMatch } from "../../utils/highlight"
import { Pane } from "vscrui"
import { azureAiModelInfoSaneDefaults } from "../../../../src/shared/api"
import styled from "styled-components"
const AzureAiModelPicker: React.FC = () => {
const { apiConfiguration, handleInputChange } = useExtensionState()
const { apiConfiguration, setApiConfiguration, azureAiModels, onUpdateApiConfig } = useExtensionState()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.apiModelId || "")
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const dropdownListRef = useRef<HTMLDivElement>(null)
const handleModelChange = (newModelId: string) => {
const apiConfig = {
...apiConfiguration,
apiModelId: newModelId,
}
setApiConfiguration(apiConfig)
onUpdateApiConfig(apiConfig)
setSearchTerm(newModelId)
}
useEffect(() => {
if (apiConfiguration?.apiModelId && apiConfiguration?.apiModelId !== searchTerm) {
setSearchTerm(apiConfiguration?.apiModelId)
}
}, [apiConfiguration, searchTerm])
const debouncedRefreshModels = useMemo(
() =>
debounce((endpoint: string, key: string) => {
vscode.postMessage({
type: "refreshAzureAiModels",
values: {
endpoint,
key,
},
})
}, 50),
[],
)
useEffect(() => {
if (!apiConfiguration?.azureAiEndpoint || !apiConfiguration?.azureAiKey) {
return
}
debouncedRefreshModels(apiConfiguration.azureAiEndpoint, apiConfiguration.azureAiKey)
return () => {
debouncedRefreshModels.clear()
}
}, [apiConfiguration?.azureAiEndpoint, apiConfiguration?.azureAiKey, debouncedRefreshModels])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownVisible(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [])
const modelIds = useMemo(() => {
return azureAiModels.sort((a, b) => a.localeCompare(b))
}, [azureAiModels])
const searchableItems = useMemo(() => {
return modelIds.map((id) => ({
id,
html: id,
}))
}, [modelIds])
const fzf = useMemo(() => {
return new Fzf(searchableItems, {
selector: (item) => item.html,
})
}, [searchableItems])
const modelSearchResults = useMemo(() => {
if (!searchTerm) return searchableItems
const searchResults = fzf.find(searchTerm)
return searchResults.map((result) => ({
...result.item,
html: highlightFzfMatch(result.item.html, Array.from(result.positions), "model-item-highlight"),
}))
}, [searchableItems, searchTerm, fzf])
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (!isDropdownVisible) return
switch (event.key) {
case "ArrowDown":
event.preventDefault()
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
break
case "ArrowUp":
event.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
case "Enter":
event.preventDefault()
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
handleModelChange(modelSearchResults[selectedIndex].id)
setIsDropdownVisible(false)
}
break
case "Escape":
setIsDropdownVisible(false)
setSelectedIndex(-1)
break
}
}
useEffect(() => {
setSelectedIndex(-1)
if (dropdownListRef.current) {
dropdownListRef.current.scrollTop = 0
}
}, [searchTerm])
useEffect(() => {
if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
itemRefs.current[selectedIndex]?.scrollIntoView({
block: "nearest",
behavior: "smooth",
})
}
}, [selectedIndex])
return (
<>
<style>
{`
.model-item-highlight {
background-color: var(--vscode-editor-findMatchHighlightBackground);
color: inherit;
}
`}
</style>
<div style={{ display: "flex", flexDirection: "column", rowGap: "5px" }}>
<VSCodeTextField
value={apiConfiguration?.azureAiEndpoint || ""}
style={{ width: "100%" }}
type="url"
onChange={handleInputChange("azureAiEndpoint")}
onChange={(e) => {
const apiConfig = {
...apiConfiguration,
azureAiEndpoint: (e.target as HTMLInputElement).value,
}
setApiConfiguration(apiConfig)
onUpdateApiConfig(apiConfig)
}}
placeholder="https://your-endpoint.region.inference.ai.azure.com">
<span style={{ fontWeight: 500 }}>Base URL</span>
</VSCodeTextField>
@@ -22,19 +173,69 @@ const AzureAiModelPicker: React.FC = () => {
value={apiConfiguration?.azureAiKey || ""}
style={{ width: "100%" }}
type="password"
onChange={handleInputChange("azureAiKey")}
onChange={(e) => {
const apiConfig = {
...apiConfiguration,
azureAiKey: (e.target as HTMLInputElement).value,
}
setApiConfiguration(apiConfig)
onUpdateApiConfig(apiConfig)
}}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<DropdownWrapper ref={dropdownRef}>
<VSCodeTextField
value={apiConfiguration?.apiModelId || ""}
style={{ width: "100%" }}
type="text"
onChange={handleInputChange("apiModelId")}
placeholder="Enter model deployment name...">
id="model-search"
placeholder="Search and select a model..."
value={searchTerm}
onInput={(e) => {
handleModelChange((e.target as HTMLInputElement)?.value)
setIsDropdownVisible(true)
}}
onFocus={() => setIsDropdownVisible(true)}
onKeyDown={handleKeyDown}
style={{ width: "100%", zIndex: AZURE_MODEL_PICKER_Z_INDEX, position: "relative" }}>
<span style={{ fontWeight: 500 }}>Model Deployment Name</span>
{searchTerm && (
<div
className="input-icon-button codicon codicon-close"
aria-label="Clear search"
onClick={() => {
handleModelChange("")
setIsDropdownVisible(true)
}}
slot="end"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
/>
)}
</VSCodeTextField>
{isDropdownVisible && (
<DropdownList ref={dropdownListRef}>
{modelSearchResults.map((item, index) => (
<DropdownItem
key={item.id}
ref={(el) => (itemRefs.current[index] = el)}
isSelected={index === selectedIndex}
onMouseEnter={() => setSelectedIndex(index)}
onClick={() => {
handleModelChange(item.id)
setIsDropdownVisible(false)
}}
dangerouslySetInnerHTML={{
__html: item.html,
}}
/>
))}
</DropdownList>
)}
</DropdownWrapper>
<Pane
title="Model Configuration"
@@ -42,10 +243,14 @@ const AzureAiModelPicker: React.FC = () => {
actions={[
{
iconName: "refresh",
onClick: () =>
handleInputChange("azureAiModelConfig")({
target: { value: azureAiModelInfoSaneDefaults },
}),
onClick: () => {
const apiConfig = {
...apiConfiguration,
azureAiModelConfig: azureAiModelInfoSaneDefaults,
}
setApiConfiguration(apiConfig)
onUpdateApiConfig(apiConfig)
},
},
]}>
<div
@@ -92,9 +297,9 @@ const AzureAiModelPicker: React.FC = () => {
style={{ width: "100%" }}
onChange={(e: any) => {
const parsed = parseInt(e.target.value)
handleInputChange("azureAiModelConfig")({
target: {
value: {
const apiConfig = {
...apiConfiguration,
azureAiModelConfig: {
...(apiConfiguration?.azureAiModelConfig ||
azureAiModelInfoSaneDefaults),
contextWindow:
@@ -104,8 +309,9 @@ const AzureAiModelPicker: React.FC = () => {
? azureAiModelInfoSaneDefaults.contextWindow
: parsed,
},
},
})
}
setApiConfiguration(apiConfig)
onUpdateApiConfig(apiConfig)
}}
placeholder="e.g. 128000">
<span style={{ fontWeight: 500 }}>Context Window Size</span>
@@ -140,7 +346,44 @@ const AzureAiModelPicker: React.FC = () => {
)}
</p>
</div>
</>
)
}
export default memo(AzureAiModelPicker)
// Dropdown
const DropdownWrapper = styled.div`
position: relative;
width: 100%;
`
export const AZURE_MODEL_PICKER_Z_INDEX = 1_000
const DropdownList = styled.div`
position: absolute;
top: calc(100% - 3px);
left: 0;
width: calc(100% - 2px);
max-height: 200px;
overflow-y: auto;
background-color: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-list-activeSelectionBackground);
z-index: ${AZURE_MODEL_PICKER_Z_INDEX - 1};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
`
const DropdownItem = styled.div<{ isSelected: boolean }>`
padding: 5px 10px;
cursor: pointer;
word-break: break-all;
white-space: normal;
background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")};
&:hover {
background-color: var(--vscode-list-activeSelectionBackground);
}
`

View File

@@ -25,6 +25,7 @@ export interface ExtensionStateContextType extends ExtensionState {
glamaModels: Record<string, ModelInfo>
openRouterModels: Record<string, ModelInfo>
openAiModels: string[]
azureAiModels: string[]
mcpServers: McpServer[]
filePaths: string[]
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
@@ -123,6 +124,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
})
const [openAiModels, setOpenAiModels] = useState<string[]>([])
const [azureAiModels, setAzureAiModels] = useState<string[]>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const setListApiConfigMeta = useCallback(
@@ -247,6 +249,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
glamaModels,
openRouterModels,
openAiModels,
azureAiModels,
mcpServers,
filePaths,
openedTabs,