Skip to content

Commit ea3d384

Browse files
committed
Fetches all models from gateway during init
1 parent 29dfe11 commit ea3d384

File tree

11 files changed

+11436
-10881
lines changed

11 files changed

+11436
-10881
lines changed

src/api/providers/unbound.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import OpenAI from "openai"
33
import { ApiHandler, SingleCompletionHandler } from "../"
4-
import { ApiHandlerOptions, ModelInfo, UnboundModelId, unboundDefaultModelId, unboundModels } from "../../shared/api"
4+
import { ApiHandlerOptions, ModelInfo, unboundDefaultModelId, unboundDefaultModelInfo } from "../../shared/api"
55
import { convertToOpenAiMessages } from "../transform/openai-format"
66
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
77

@@ -130,15 +130,15 @@ export class UnboundHandler implements ApiHandler, SingleCompletionHandler {
130130
}
131131
}
132132

133-
getModel(): { id: UnboundModelId; info: ModelInfo } {
134-
const modelId = this.options.apiModelId
135-
if (modelId && modelId in unboundModels) {
136-
const id = modelId as UnboundModelId
137-
return { id, info: unboundModels[id] }
133+
getModel(): { id: string; info: ModelInfo } {
134+
const modelId = this.options.unboundModelId
135+
const modelInfo = this.options.unboundModelInfo
136+
if (modelId && modelInfo) {
137+
return { id: modelId, info: modelInfo }
138138
}
139139
return {
140140
id: unboundDefaultModelId,
141-
info: unboundModels[unboundDefaultModelId],
141+
info: unboundDefaultModelInfo,
142142
}
143143
}
144144

src/core/webview/ClineProvider.ts

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,15 @@ type GlobalStateKey =
124124
| "autoApprovalEnabled"
125125
| "customModes" // Array of custom modes
126126
| "unboundModelId"
127+
| "unboundModelInfo"
127128

128129
export const GlobalFileNames = {
129130
apiConversationHistory: "api_conversation_history.json",
130131
uiMessages: "ui_messages.json",
131132
glamaModels: "glama_models.json",
132133
openRouterModels: "openrouter_models.json",
133134
mcpSettings: "cline_mcp_settings.json",
135+
unboundModels: "unbound_models.json",
134136
}
135137

136138
export class ClineProvider implements vscode.WebviewViewProvider {
@@ -529,6 +531,24 @@ export class ClineProvider implements vscode.WebviewViewProvider {
529531
}
530532
})
531533

534+
this.readUnboundModels().then((unboundModels) => {
535+
if (unboundModels) {
536+
this.postMessageToWebview({ type: "unboundModels", unboundModels })
537+
}
538+
})
539+
this.refreshUnboundModels().then(async (unboundModels) => {
540+
if (unboundModels) {
541+
const { apiConfiguration } = await this.getState()
542+
if (apiConfiguration?.unboundModelId) {
543+
await this.updateGlobalState(
544+
"unboundModelInfo",
545+
unboundModels[apiConfiguration.unboundModelId],
546+
)
547+
await this.postStateToWebview()
548+
}
549+
}
550+
})
551+
532552
this.configManager
533553
.listConfig()
534554
.then(async (listApiConfig) => {
@@ -548,7 +568,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
548568
}
549569
}
550570

551-
let currentConfigName = (await this.getGlobalState("currentApiConfigName")) as string
571+
const currentConfigName = (await this.getGlobalState("currentApiConfigName")) as string
552572

553573
if (currentConfigName) {
554574
if (!(await this.configManager.hasConfig(currentConfigName))) {
@@ -687,6 +707,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
687707
this.postMessageToWebview({ type: "openAiModels", openAiModels })
688708
}
689709
break
710+
case "refreshUnboundModels":
711+
await this.refreshUnboundModels()
712+
break
690713
case "openImage":
691714
openImage(message.text!)
692715
break
@@ -1124,7 +1147,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11241147
if (message.text && message.apiConfiguration) {
11251148
try {
11261149
await this.configManager.saveConfig(message.text, message.apiConfiguration)
1127-
let listApiConfig = await this.configManager.listConfig()
1150+
const listApiConfig = await this.configManager.listConfig()
11281151

11291152
await Promise.all([
11301153
this.updateGlobalState("listApiConfigMeta", listApiConfig),
@@ -1149,7 +1172,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11491172
await this.configManager.saveConfig(newName, message.apiConfiguration)
11501173
await this.configManager.deleteConfig(oldName)
11511174

1152-
let listApiConfig = await this.configManager.listConfig()
1175+
const listApiConfig = await this.configManager.listConfig()
11531176
const config = listApiConfig?.find((c) => c.name === newName)
11541177

11551178
// Update listApiConfigMeta first to ensure UI has latest data
@@ -1207,7 +1230,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12071230
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
12081231

12091232
// If this was the current config, switch to first available
1210-
let currentApiConfigName = await this.getGlobalState("currentApiConfigName")
1233+
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
12111234
if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
12121235
const apiConfig = await this.configManager.loadConfig(listApiConfig[0].name)
12131236
await Promise.all([
@@ -1227,7 +1250,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12271250
break
12281251
case "getListApiConfiguration":
12291252
try {
1230-
let listApiConfig = await this.configManager.listConfig()
1253+
const listApiConfig = await this.configManager.listConfig()
12311254
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
12321255
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
12331256
} catch (error) {
@@ -1267,7 +1290,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12671290
this.outputChannel.appendLine(
12681291
`Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
12691292
)
1270-
vscode.window.showErrorMessage(`Failed to update server timeout`)
1293+
vscode.window.showErrorMessage("Failed to update server timeout")
12711294
}
12721295
}
12731296
break
@@ -1395,6 +1418,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13951418
mistralApiKey,
13961419
unboundApiKey,
13971420
unboundModelId,
1421+
unboundModelInfo,
13981422
} = apiConfiguration
13991423
await this.updateGlobalState("apiProvider", apiProvider)
14001424
await this.updateGlobalState("apiModelId", apiModelId)
@@ -1435,6 +1459,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
14351459
await this.storeSecret("mistralApiKey", mistralApiKey)
14361460
await this.storeSecret("unboundApiKey", unboundApiKey)
14371461
await this.updateGlobalState("unboundModelId", unboundModelId)
1462+
await this.updateGlobalState("unboundModelInfo", unboundModelInfo)
14381463
if (this.cline) {
14391464
this.cline.api = buildApiHandler(apiConfiguration)
14401465
}
@@ -1620,7 +1645,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
16201645
async refreshGlamaModels() {
16211646
const glamaModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.glamaModels)
16221647

1623-
let models: Record<string, ModelInfo> = {}
1648+
const models: Record<string, ModelInfo> = {}
16241649
try {
16251650
const response = await axios.get("https://glama.ai/api/gateway/v1/models")
16261651
/*
@@ -1710,7 +1735,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
17101735
GlobalFileNames.openRouterModels,
17111736
)
17121737

1713-
let models: Record<string, ModelInfo> = {}
1738+
const models: Record<string, ModelInfo> = {}
17141739
try {
17151740
const response = await axios.get("https://openrouter.ai/api/v1/models")
17161741
/*
@@ -1816,6 +1841,52 @@ export class ClineProvider implements vscode.WebviewViewProvider {
18161841
return models
18171842
}
18181843

1844+
async readUnboundModels(): Promise<Record<string, ModelInfo> | undefined> {
1845+
const unboundModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.unboundModels)
1846+
const fileExists = await fileExistsAtPath(unboundModelsFilePath)
1847+
if (fileExists) {
1848+
const fileContents = await fs.readFile(unboundModelsFilePath, "utf8")
1849+
return JSON.parse(fileContents)
1850+
}
1851+
return undefined
1852+
}
1853+
1854+
async refreshUnboundModels() {
1855+
const unboundModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.unboundModels)
1856+
1857+
const models: Record<string, ModelInfo> = {}
1858+
try {
1859+
const response = await axios.get("http://localhost:8787/models")
1860+
1861+
if (response.data) {
1862+
const rawModels: Record<string, any> = response.data
1863+
1864+
for (const [modelId, model] of Object.entries(rawModels)) {
1865+
models[modelId] = {
1866+
maxTokens: model.maxTokens ? parseInt(model.maxTokens) : undefined,
1867+
contextWindow: model.contextWindow ? parseInt(model.contextWindow) : 0,
1868+
supportsImages: model.supportsImages ?? false,
1869+
supportsPromptCache: model.supportsPromptCaching ?? false,
1870+
supportsComputerUse: model.supportsComputerUse ?? false,
1871+
inputPrice: model.inputTokenPrice ? parseFloat(model.inputTokenPrice) : undefined,
1872+
outputPrice: model.outputTokenPrice ? parseFloat(model.outputTokenPrice) : undefined,
1873+
cacheWritesPrice: model.cacheWritePrice ? parseFloat(model.cacheWritePrice) : undefined,
1874+
cacheReadsPrice: model.cacheReadPrice ? parseFloat(model.cacheReadPrice) : undefined,
1875+
}
1876+
}
1877+
}
1878+
await fs.writeFile(unboundModelsFilePath, JSON.stringify(models))
1879+
this.outputChannel.appendLine(`Unbound models fetched and saved: ${JSON.stringify(models, null, 2)}`)
1880+
} catch (error) {
1881+
this.outputChannel.appendLine(
1882+
`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
1883+
)
1884+
}
1885+
1886+
await this.postMessageToWebview({ type: "unboundModels", unboundModels: models })
1887+
return models
1888+
}
1889+
18191890
// Task history
18201891

18211892
async getTaskWithId(id: string): Promise<{
@@ -2104,6 +2175,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21042175
experiments,
21052176
unboundApiKey,
21062177
unboundModelId,
2178+
unboundModelInfo,
21072179
] = await Promise.all([
21082180
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
21092181
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -2176,6 +2248,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21762248
this.getGlobalState("experiments") as Promise<Record<ExperimentId, boolean> | undefined>,
21772249
this.getSecret("unboundApiKey") as Promise<string | undefined>,
21782250
this.getGlobalState("unboundModelId") as Promise<string | undefined>,
2251+
this.getGlobalState("unboundModelInfo") as Promise<ModelInfo | undefined>,
21792252
])
21802253

21812254
let apiProvider: ApiProvider
@@ -2233,6 +2306,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
22332306
vsCodeLmModelSelector,
22342307
unboundApiKey,
22352308
unboundModelId,
2309+
unboundModelInfo,
22362310
},
22372311
lastShownAnnouncementId,
22382312
customInstructions,

src/shared/ExtensionMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export interface ExtensionMessage {
4242
| "autoApprovalEnabled"
4343
| "updateCustomMode"
4444
| "deleteCustomMode"
45+
| "unboundModels"
46+
| "refreshUnboundModels"
4547
text?: string
4648
action?:
4749
| "chatButtonClicked"
@@ -61,6 +63,7 @@ export interface ExtensionMessage {
6163
glamaModels?: Record<string, ModelInfo>
6264
openRouterModels?: Record<string, ModelInfo>
6365
openAiModels?: string[]
66+
unboundModels?: Record<string, ModelInfo>
6467
mcpServers?: McpServer[]
6568
commits?: GitCommit[]
6669
listApiConfig?: ApiConfigMeta[]

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface WebviewMessage {
3939
| "refreshGlamaModels"
4040
| "refreshOpenRouterModels"
4141
| "refreshOpenAiModels"
42+
| "refreshUnboundModels"
4243
| "alwaysAllowBrowser"
4344
| "alwaysAllowMcp"
4445
| "alwaysAllowModeSwitch"

src/shared/api.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface ApiHandlerOptions {
6060
includeMaxTokens?: boolean
6161
unboundApiKey?: string
6262
unboundModelId?: string
63+
unboundModelInfo?: ModelInfo
6364
}
6465

6566
export type ApiConfiguration = ApiHandlerOptions & {
@@ -564,7 +565,7 @@ export const deepSeekModels = {
564565
supportsPromptCache: false,
565566
inputPrice: 0.014, // $0.014 per million tokens
566567
outputPrice: 0.28, // $0.28 per million tokens
567-
description: `DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.`,
568+
description: "DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.",
568569
},
569570
"deepseek-reasoner": {
570571
maxTokens: 8192,
@@ -573,7 +574,7 @@ export const deepSeekModels = {
573574
supportsPromptCache: false,
574575
inputPrice: 0.55, // $0.55 per million tokens
575576
outputPrice: 2.19, // $2.19 per million tokens
576-
description: `DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks.`,
577+
description: "DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks.",
577578
},
578579
} as const satisfies Record<string, ModelInfo>
579580

@@ -598,12 +599,20 @@ export const mistralModels = {
598599
} as const satisfies Record<string, ModelInfo>
599600

600601
// Unbound Security
601-
export type UnboundModelId = keyof typeof unboundModels
602+
// export type UnboundModelId = keyof typeof unboundModels
602603
export const unboundDefaultModelId = "openai/gpt-4o"
603-
export const unboundModels = {
604-
"anthropic/claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"],
605-
"openai/gpt-4o": openAiNativeModels["gpt-4o"],
606-
"deepseek/deepseek-chat": deepSeekModels["deepseek-chat"],
607-
"deepseek/deepseek-reasoner": deepSeekModels["deepseek-reasoner"],
608-
"mistral/codestral-latest": mistralModels["codestral-latest"],
609-
} as const satisfies Record<string, ModelInfo>
604+
// export const unboundModels = {
605+
// "anthropic/claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"],
606+
// "openai/gpt-4o": openAiNativeModels["gpt-4o"],
607+
// "deepseek/deepseek-chat": deepSeekModels["deepseek-chat"],
608+
// "deepseek/deepseek-reasoner": deepSeekModels["deepseek-reasoner"],
609+
// "mistral/codestral-latest": mistralModels["codestral-latest"],
610+
// } as const satisfies Record<string, ModelInfo>
611+
export const unboundDefaultModelInfo: ModelInfo = {
612+
maxTokens: 8192,
613+
contextWindow: 64_000,
614+
supportsImages: false,
615+
supportsPromptCache: false,
616+
inputPrice: 0,
617+
outputPrice: 0,
618+
}

0 commit comments

Comments
 (0)