Skip to content

Commit d1a097c

Browse files
dtrugmandcbartlett
andauthored
Requesty dynamic model selection (RooCodeInc#1836)
* Extract reuseable ModelDescriptionMarkdown from OpenRouter model picker * Requesty: Add model picker component * Refactor readOpenRouterModels to allow any dynamic list filename * Extract parsePrice to allow reuse by other providers * Simplify model display name switch case * Requesty: Add dynamic model list fetching from API * Requesty: Add default model selection * Requesty: Specify max_tokens when sending request * Add changeset --------- Co-authored-by: Dennis Bartlett <[email protected]>
1 parent a02eb40 commit d1a097c

File tree

14 files changed

+612
-211
lines changed

14 files changed

+612
-211
lines changed

.changeset/red-cars-cry.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"claude-dev": patch
3+
---
4+
5+
Add dynamic model fetching for the Requesty provider.
6+
7+
Instead of manually typing the model name, the extension dynamically fetches
8+
all the supported model names from Requesty's /v1/models API.
9+
10+
This allows users to use a fuzzy search logic when choosing the models and
11+
also guarantees the information for each model is up to date.

src/api/providers/requesty.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
22
import OpenAI from "openai"
33
import { withRetry } from "../retry"
44
import { calculateApiCostOpenAI } from "../../utils/cost"
5-
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
5+
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults, requestyDefaultModelId, requestyDefaultModelInfo } from "../../shared/api"
66
import { ApiHandler } from "../index"
77
import { convertToOpenAiMessages } from "../transform/openai-format"
88
import { ApiStream } from "../transform/stream"
@@ -25,7 +25,7 @@ export class RequestyHandler implements ApiHandler {
2525

2626
@withRetry()
2727
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
28-
const modelId = this.options.requestyModelId ?? ""
28+
const model = this.getModel()
2929

3030
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
3131
{ role: "system", content: systemPrompt },
@@ -34,12 +34,13 @@ export class RequestyHandler implements ApiHandler {
3434

3535
// @ts-ignore-next-line
3636
const stream = await this.client.chat.completions.create({
37-
model: modelId,
37+
model: model.id,
38+
max_tokens: model.info.maxTokens || undefined,
3839
messages: openAiMessages,
3940
temperature: 0,
4041
stream: true,
4142
stream_options: { include_usage: true },
42-
...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
43+
...(model.id === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
4344
})
4445

4546
for await (const chunk of stream) {
@@ -89,9 +90,11 @@ export class RequestyHandler implements ApiHandler {
8990
}
9091

9192
getModel(): { id: string; info: ModelInfo } {
92-
return {
93-
id: this.options.requestyModelId ?? "",
94-
info: openAiModelInfoSaneDefaults,
93+
const modelId = this.options.requestyModelId
94+
const modelInfo = this.options.requestyModelInfo
95+
if (modelId && modelInfo) {
96+
return { id: modelId, info: modelInfo }
9597
}
98+
return { id: requestyDefaultModelId, info: requestyDefaultModelInfo }
9699
}
97100
}

src/core/webview/ClineProvider.ts

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ type GlobalStateKey =
9595
| "liteLlmModelId"
9696
| "qwenApiLine"
9797
| "requestyModelId"
98+
| "requestyModelInfo"
9899
| "togetherModelId"
99100
| "mcpMarketplaceCatalog"
100101
| "telemetrySetting"
@@ -103,6 +104,7 @@ export const GlobalFileNames = {
103104
apiConversationHistory: "api_conversation_history.json",
104105
uiMessages: "ui_messages.json",
105106
openRouterModels: "openrouter_models.json",
107+
requestyModels: "requesty_models.json",
106108
mcpSettings: "cline_mcp_settings.json",
107109
clineRules: ".clinerules",
108110
}
@@ -497,7 +499,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
497499
}),
498500
)
499501
// post last cached models in case the call to endpoint fails
500-
this.readOpenRouterModels().then((openRouterModels) => {
502+
this.readDynamicProviderModels(GlobalFileNames.openRouterModels).then((openRouterModels) => {
501503
if (openRouterModels) {
502504
this.postMessageToWebview({
503505
type: "openRouterModels",
@@ -540,6 +542,34 @@ export class ClineProvider implements vscode.WebviewViewProvider {
540542
telemetryService.updateTelemetryState(isOptedIn)
541543
})
542544

545+
546+
// post last cached models in case the call to endpoint fails
547+
this.readDynamicProviderModels(GlobalFileNames.requestyModels).then((requestyModels) => {
548+
if (requestyModels) {
549+
this.postMessageToWebview({
550+
type: "requestyModels",
551+
requestyModels,
552+
})
553+
}
554+
})
555+
556+
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
557+
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
558+
// (see normalizeApiConfiguration > openrouter)
559+
this.refreshRequestyModels().then(async (requestyModels) => {
560+
if (requestyModels) {
561+
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
562+
const { apiConfiguration } = await this.getState()
563+
if (apiConfiguration.requestyModelId) {
564+
await this.updateGlobalState(
565+
"requestyModelInfo",
566+
requestyModels[apiConfiguration.requestyModelId],
567+
)
568+
await this.postStateToWebview()
569+
}
570+
}
571+
})
572+
543573
break
544574
case "newTask":
545575
// Code that should run in response to the hello message command
@@ -582,6 +612,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
582612
deepSeekApiKey,
583613
requestyApiKey,
584614
requestyModelId,
615+
requestyModelInfo,
585616
togetherApiKey,
586617
togetherModelId,
587618
qwenApiKey,
@@ -635,6 +666,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
635666
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
636667
await this.updateGlobalState("qwenApiLine", qwenApiLine)
637668
await this.updateGlobalState("requestyModelId", requestyModelId)
669+
await this.updateGlobalState("requestyModelInfo", requestyModelInfo)
638670
await this.updateGlobalState("togetherModelId", togetherModelId)
639671
if (this.cline) {
640672
this.cline.api = buildApiHandler(message.apiConfiguration)
@@ -731,6 +763,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
731763
case "refreshOpenRouterModels":
732764
await this.refreshOpenRouterModels()
733765
break
766+
case "refreshRequestyModels":
767+
await this.refreshRequestyModels()
768+
break
734769
case "refreshOpenAiModels":
735770
const { apiConfiguration } = await this.getState()
736771
const openAiModels = await this.getOpenAiModels(
@@ -996,6 +1031,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
9961031
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
9971032
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
9981033
break
1034+
case "requesty":
1035+
await this.updateGlobalState("previousModeModelId", apiConfiguration.requestyModelId)
1036+
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.requestyModelInfo)
1037+
break
9991038
case "vscode-lm":
10001039
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
10011040
break
@@ -1028,6 +1067,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
10281067
await this.updateGlobalState("openRouterModelId", newModelId)
10291068
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
10301069
break
1070+
case "requesty":
1071+
await this.updateGlobalState("requestyModelId", newModelId)
1072+
await this.updateGlobalState("requestyModelInfo", newModelInfo)
1073+
break
10311074
case "vscode-lm":
10321075
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
10331076
break
@@ -1500,16 +1543,61 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
15001543
return cacheDir
15011544
}
15021545

1503-
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
1504-
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
1505-
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
1546+
async readDynamicProviderModels(filename: string): Promise<Record<string, ModelInfo> | undefined> {
1547+
const filePath = path.join(await this.ensureCacheDirectoryExists(), filename)
1548+
const fileExists = await fileExistsAtPath(filePath)
15061549
if (fileExists) {
1507-
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
1550+
const fileContents = await fs.readFile(filePath, "utf8")
15081551
return JSON.parse(fileContents)
15091552
}
15101553
return undefined
15111554
}
15121555

1556+
adjustPriceToMillionTokens(price: any) {
1557+
if (price) {
1558+
return parseFloat(price) * 1_000_000
1559+
}
1560+
return undefined
1561+
}
1562+
1563+
async refreshRequestyModels() {
1564+
const requestyModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.requestyModels)
1565+
1566+
let models: Record<string, ModelInfo> = {}
1567+
try {
1568+
const response = await axios.get("https://router.requesty.ai/v1/models")
1569+
if (response.data?.data) {
1570+
for (const model of response.data.data) {
1571+
const modelInfo: ModelInfo = {
1572+
maxTokens: model.max_output_tokens,
1573+
contextWindow: model.context_window,
1574+
supportsImages: model.supports_images || undefined,
1575+
supportsComputerUse: model.supports_computer_use || undefined,
1576+
supportsPromptCache: model.supports_caching || undefined,
1577+
inputPrice: this.adjustPriceToMillionTokens(model.input_price),
1578+
outputPrice: this.adjustPriceToMillionTokens(model.output_price),
1579+
cacheWritesPrice: this.adjustPriceToMillionTokens(model.caching_price),
1580+
cacheReadsPrice: this.adjustPriceToMillionTokens(model.cached_price),
1581+
description: model.description,
1582+
}
1583+
models[model.id] = modelInfo
1584+
}
1585+
await fs.writeFile(requestyModelsFilePath, JSON.stringify(models))
1586+
console.log("Requesty models fetched and saved", models)
1587+
} else {
1588+
console.error("Invalid response from Requesty API")
1589+
}
1590+
} catch (error) {
1591+
console.error("Error fetching Requesty models:", error)
1592+
}
1593+
1594+
await this.postMessageToWebview({
1595+
type: "requestyModels",
1596+
requestyModels: models,
1597+
})
1598+
return models
1599+
}
1600+
15131601
async refreshOpenRouterModels() {
15141602
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
15151603

@@ -1544,20 +1632,14 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
15441632
*/
15451633
if (response.data?.data) {
15461634
const rawModels = response.data.data
1547-
const parsePrice = (price: any) => {
1548-
if (price) {
1549-
return parseFloat(price) * 1_000_000
1550-
}
1551-
return undefined
1552-
}
15531635
for (const rawModel of rawModels) {
15541636
const modelInfo: ModelInfo = {
15551637
maxTokens: rawModel.top_provider?.max_completion_tokens,
15561638
contextWindow: rawModel.context_length,
15571639
supportsImages: rawModel.architecture?.modality?.includes("image"),
15581640
supportsPromptCache: false,
1559-
inputPrice: parsePrice(rawModel.pricing?.prompt),
1560-
outputPrice: parsePrice(rawModel.pricing?.completion),
1641+
inputPrice: this.adjustPriceToMillionTokens(rawModel.pricing?.prompt),
1642+
outputPrice: this.adjustPriceToMillionTokens(rawModel.pricing?.completion),
15611643
description: rawModel.description,
15621644
}
15631645

@@ -1858,6 +1940,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
18581940
deepSeekApiKey,
18591941
requestyApiKey,
18601942
requestyModelId,
1943+
requestyModelInfo,
18611944
togetherApiKey,
18621945
togetherModelId,
18631946
qwenApiKey,
@@ -1911,6 +1994,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
19111994
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
19121995
this.getSecret("requestyApiKey") as Promise<string | undefined>,
19131996
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
1997+
this.getGlobalState("requestyModelInfo") as Promise<ModelInfo | undefined>,
19141998
this.getSecret("togetherApiKey") as Promise<string | undefined>,
19151999
this.getGlobalState("togetherModelId") as Promise<string | undefined>,
19162000
this.getSecret("qwenApiKey") as Promise<string | undefined>,
@@ -1987,6 +2071,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
19872071
deepSeekApiKey,
19882072
requestyApiKey,
19892073
requestyModelId,
2074+
requestyModelInfo,
19902075
togetherApiKey,
19912076
togetherModelId,
19922077
qwenApiKey,

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ExtensionMessage {
2222
| "invoke"
2323
| "partialMessage"
2424
| "openRouterModels"
25+
| "requestyModels"
2526
| "openAiModels"
2627
| "mcpServers"
2728
| "relinquishControl"
@@ -51,6 +52,7 @@ export interface ExtensionMessage {
5152
filePaths?: string[]
5253
partialMessage?: ClineMessage
5354
openRouterModels?: Record<string, ModelInfo>
55+
requestyModels?: Record<string, ModelInfo>
5456
openAiModels?: string[]
5557
mcpServers?: McpServer[]
5658
mcpMarketplaceCatalog?: McpMarketplaceCatalog

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface WebviewMessage {
2727
| "openMention"
2828
| "cancelTask"
2929
| "refreshOpenRouterModels"
30+
| "refreshRequestyModels"
3031
| "refreshOpenAiModels"
3132
| "openMcpSettings"
3233
| "restartMcpServer"

src/shared/api.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface ApiHandlerOptions {
4949
deepSeekApiKey?: string
5050
requestyApiKey?: string
5151
requestyModelId?: string
52+
requestyModelInfo?: ModelInfo
5253
togetherApiKey?: string
5354
togetherModelId?: string
5455
qwenApiKey?: string
@@ -802,6 +803,22 @@ export const liteLlmModelInfoSaneDefaults: ModelInfo = {
802803
outputPrice: 0,
803804
}
804805

806+
// Requesty
807+
// https://requesty.ai/models
808+
export const requestyDefaultModelId = "anthropic/claude-3-5-sonnet-latest"
809+
export const requestyDefaultModelInfo: ModelInfo = {
810+
maxTokens: 8192,
811+
contextWindow: 200_000,
812+
supportsImages: true,
813+
supportsComputerUse: false,
814+
supportsPromptCache: true,
815+
inputPrice: 3.0,
816+
outputPrice: 15.0,
817+
cacheWritesPrice: 3.75,
818+
cacheReadsPrice: 0.3,
819+
description: "Anthropic's most intelligent model. Highest level of intelligence and capability.",
820+
}
821+
805822
// X AI
806823
// https://docs.x.ai/docs/api-reference
807824
export type XAIModelId = keyof typeof xaiModels
@@ -880,3 +897,4 @@ export const xaiModels = {
880897
description: "X AI's Grok Beta model (legacy) with 131K context window",
881898
},
882899
} as const satisfies Record<string, ModelInfo>
900+

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
214214
},
215215
ref,
216216
) => {
217-
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
217+
const { filePaths, chatSettings, apiConfiguration, openRouterModels, requestyModels, platform } = useExtensionState()
218218
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
219219
const [gitCommits, setGitCommits] = useState<any[]>([])
220220

@@ -635,14 +635,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
635635
// Separate the API config submission logic
636636
const submitApiConfig = useCallback(() => {
637637
const apiValidationResult = validateApiConfiguration(apiConfiguration)
638-
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
638+
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels, requestyModels)
639639

640640
if (!apiValidationResult && !modelIdValidationResult) {
641641
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
642642
} else {
643643
vscode.postMessage({ type: "getLatestState" })
644644
}
645-
}, [apiConfiguration, openRouterModels])
645+
}, [apiConfiguration, openRouterModels, requestyModels])
646646

647647
const onModeToggle = useCallback(() => {
648648
// if (textAreaDisabled) return
@@ -742,9 +742,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
742742
const unknownModel = "unknown"
743743
if (!apiConfiguration) return unknownModel
744744
switch (selectedProvider) {
745-
case "anthropic":
746-
case "openrouter":
747-
return `${selectedProvider}:${selectedModelId}`
748745
case "openai":
749746
return `openai-compat:${selectedModelId}`
750747
case "vscode-lm":
@@ -758,7 +755,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
758755
case "litellm":
759756
return `${selectedProvider}:${apiConfiguration.liteLlmModelId}`
760757
case "requesty":
761-
return `${selectedProvider}:${apiConfiguration.requestyModelId}`
758+
case "anthropic":
759+
case "openrouter":
762760
default:
763761
return `${selectedProvider}:${selectedModelId}`
764762
}

0 commit comments

Comments
 (0)