ApiOptions.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
  2. import { convertHeadersToObject } from "./utils/headers"
  3. import { useDebounce } from "react-use"
  4. import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
  5. import { ExternalLinkIcon } from "@radix-ui/react-icons"
  6. import {
  7. type ProviderName,
  8. type ProviderSettings,
  9. isRetiredProvider,
  10. DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
  11. openRouterDefaultModelId,
  12. requestyDefaultModelId,
  13. litellmDefaultModelId,
  14. openAiNativeDefaultModelId,
  15. openAiCodexDefaultModelId,
  16. anthropicDefaultModelId,
  17. qwenCodeDefaultModelId,
  18. geminiDefaultModelId,
  19. deepSeekDefaultModelId,
  20. moonshotDefaultModelId,
  21. mistralDefaultModelId,
  22. xaiDefaultModelId,
  23. basetenDefaultModelId,
  24. bedrockDefaultModelId,
  25. vertexDefaultModelId,
  26. sambaNovaDefaultModelId,
  27. internationalZAiDefaultModelId,
  28. mainlandZAiDefaultModelId,
  29. fireworksDefaultModelId,
  30. rooDefaultModelId,
  31. vercelAiGatewayDefaultModelId,
  32. minimaxDefaultModelId,
  33. } from "@roo-code/types"
  34. import {
  35. getProviderServiceConfig,
  36. getDefaultModelIdForProvider,
  37. getStaticModelsForProvider,
  38. shouldUseGenericModelPicker,
  39. handleModelChangeSideEffects,
  40. } from "./utils/providerModelConfig"
  41. import { vscode } from "@src/utils/vscode"
  42. import { validateApiConfigurationExcludingModelErrors, getModelValidationError } from "@src/utils/validate"
  43. import { useAppTranslation } from "@src/i18n/TranslationContext"
  44. import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
  45. import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
  46. import { useExtensionState } from "@src/context/ExtensionStateContext"
  47. import {
  48. useOpenRouterModelProviders,
  49. OPENROUTER_DEFAULT_PROVIDER_NAME,
  50. } from "@src/components/ui/hooks/useOpenRouterModelProviders"
  51. import { filterProviders, filterModels } from "./utils/organizationFilters"
  52. import {
  53. Select,
  54. SelectTrigger,
  55. SelectValue,
  56. SelectContent,
  57. SelectItem,
  58. SearchableSelect,
  59. Collapsible,
  60. CollapsibleTrigger,
  61. CollapsibleContent,
  62. } from "@src/components/ui"
  63. import {
  64. Anthropic,
  65. Azure,
  66. Baseten,
  67. Bedrock,
  68. DeepSeek,
  69. Gemini,
  70. LMStudio,
  71. LiteLLM,
  72. Mistral,
  73. Moonshot,
  74. Ollama,
  75. OpenAI,
  76. OpenAICompatible,
  77. OpenAICodex,
  78. OpenRouter,
  79. QwenCode,
  80. Requesty,
  81. Roo,
  82. SambaNova,
  83. Vertex,
  84. VSCodeLM,
  85. XAI,
  86. ZAi,
  87. Fireworks,
  88. VercelAiGateway,
  89. MiniMax,
  90. } from "./providers"
  91. import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants"
  92. import { inputEventTransform, noTransform } from "./transforms"
  93. import { ModelPicker } from "./ModelPicker"
  94. import { ApiErrorMessage } from "./ApiErrorMessage"
  95. import { ThinkingBudget } from "./ThinkingBudget"
  96. import { Verbosity } from "./Verbosity"
  97. import { TodoListSettingsControl } from "./TodoListSettingsControl"
  98. import { TemperatureControl } from "./TemperatureControl"
  99. import { RateLimitSecondsControl } from "./RateLimitSecondsControl"
  100. import { ConsecutiveMistakeLimitControl } from "./ConsecutiveMistakeLimitControl"
  101. import { BedrockCustomArn } from "./providers/BedrockCustomArn"
  102. import { RooBalanceDisplay } from "./providers/RooBalanceDisplay"
  103. import { buildDocLink } from "@src/utils/docLinks"
  104. import { BookOpenText } from "lucide-react"
  105. export interface ApiOptionsProps {
  106. uriScheme: string | undefined
  107. apiConfiguration: ProviderSettings
  108. setApiConfigurationField: <K extends keyof ProviderSettings>(
  109. field: K,
  110. value: ProviderSettings[K],
  111. isUserAction?: boolean,
  112. ) => void
  113. fromWelcomeView?: boolean
  114. errorMessage: string | undefined
  115. setErrorMessage: React.Dispatch<React.SetStateAction<string | undefined>>
  116. }
  117. const ApiOptions = ({
  118. uriScheme,
  119. apiConfiguration,
  120. setApiConfigurationField,
  121. fromWelcomeView,
  122. errorMessage,
  123. setErrorMessage,
  124. }: ApiOptionsProps) => {
  125. const { t } = useAppTranslation()
  126. const { organizationAllowList, cloudIsAuthenticated, openAiCodexIsAuthenticated } = useExtensionState()
  127. const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
  128. const headers = apiConfiguration?.openAiHeaders || {}
  129. return Object.entries(headers)
  130. })
  131. useEffect(() => {
  132. const propHeaders = apiConfiguration?.openAiHeaders || {}
  133. if (JSON.stringify(customHeaders) !== JSON.stringify(Object.entries(propHeaders))) {
  134. setCustomHeaders(Object.entries(propHeaders))
  135. }
  136. }, [apiConfiguration?.openAiHeaders, customHeaders])
  137. // Helper to convert array of tuples to object (filtering out empty keys).
  138. // Debounced effect to update the main configuration when local
  139. // customHeaders state stabilizes.
  140. useDebounce(
  141. () => {
  142. const currentConfigHeaders = apiConfiguration?.openAiHeaders || {}
  143. const newHeadersObject = convertHeadersToObject(customHeaders)
  144. // Only update if the processed object is different from the current config.
  145. if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(newHeadersObject)) {
  146. setApiConfigurationField("openAiHeaders", newHeadersObject, false)
  147. }
  148. },
  149. 300,
  150. [customHeaders, apiConfiguration?.openAiHeaders, setApiConfigurationField],
  151. )
  152. const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false)
  153. const handleInputChange = useCallback(
  154. <K extends keyof ProviderSettings, E>(
  155. field: K,
  156. transform: (event: E) => ProviderSettings[K] = inputEventTransform,
  157. ) =>
  158. (event: E | Event) => {
  159. setApiConfigurationField(field, transform(event as E))
  160. },
  161. [setApiConfigurationField],
  162. )
  163. const {
  164. provider: selectedProvider,
  165. id: selectedModelId,
  166. info: selectedModelInfo,
  167. } = useSelectedModel(apiConfiguration)
  168. const activeSelectedProvider: ProviderName | undefined = isRetiredProvider(selectedProvider)
  169. ? undefined
  170. : selectedProvider
  171. const isRetiredSelectedProvider =
  172. typeof apiConfiguration.apiProvider === "string" && isRetiredProvider(apiConfiguration.apiProvider)
  173. const { data: routerModels, refetch: refetchRouterModels } = useRouterModels()
  174. const { data: openRouterModelProviders } = useOpenRouterModelProviders(
  175. apiConfiguration?.openRouterModelId,
  176. apiConfiguration?.openRouterBaseUrl,
  177. {
  178. enabled:
  179. !!apiConfiguration?.openRouterModelId &&
  180. routerModels?.openrouter &&
  181. Object.keys(routerModels.openrouter).length > 1 &&
  182. apiConfiguration.openRouterModelId in routerModels.openrouter,
  183. },
  184. )
  185. // Update `apiModelId` whenever `selectedModelId` changes.
  186. useEffect(() => {
  187. if (isRetiredSelectedProvider) {
  188. return
  189. }
  190. if (selectedModelId && apiConfiguration.apiModelId !== selectedModelId) {
  191. // Pass false as third parameter to indicate this is not a user action
  192. // This is an internal sync, not a user-initiated change
  193. setApiConfigurationField("apiModelId", selectedModelId, false)
  194. }
  195. }, [selectedModelId, setApiConfigurationField, apiConfiguration.apiModelId, isRetiredSelectedProvider])
  196. // Debounced refresh model updates, only executed 250ms after the user
  197. // stops typing.
  198. useDebounce(
  199. () => {
  200. if (selectedProvider === "openai") {
  201. // Use our custom headers state to build the headers object.
  202. const headerObject = convertHeadersToObject(customHeaders)
  203. vscode.postMessage({
  204. type: "requestOpenAiModels",
  205. values: {
  206. baseUrl: apiConfiguration?.openAiBaseUrl,
  207. apiKey: apiConfiguration?.openAiApiKey,
  208. customHeaders: {}, // Reserved for any additional headers.
  209. openAiHeaders: headerObject,
  210. },
  211. })
  212. } else if (selectedProvider === "ollama") {
  213. vscode.postMessage({ type: "requestOllamaModels" })
  214. } else if (selectedProvider === "lmstudio") {
  215. vscode.postMessage({ type: "requestLmStudioModels" })
  216. } else if (selectedProvider === "vscode-lm") {
  217. vscode.postMessage({ type: "requestVsCodeLmModels" })
  218. } else if (selectedProvider === "litellm" || selectedProvider === "roo") {
  219. vscode.postMessage({ type: "requestRouterModels" })
  220. }
  221. },
  222. 250,
  223. [
  224. selectedProvider,
  225. apiConfiguration?.requestyApiKey,
  226. apiConfiguration?.openAiBaseUrl,
  227. apiConfiguration?.openAiApiKey,
  228. apiConfiguration?.ollamaBaseUrl,
  229. apiConfiguration?.lmStudioBaseUrl,
  230. apiConfiguration?.litellmBaseUrl,
  231. apiConfiguration?.litellmApiKey,
  232. customHeaders,
  233. ],
  234. )
  235. useEffect(() => {
  236. if (isRetiredSelectedProvider) {
  237. setErrorMessage(undefined)
  238. return
  239. }
  240. const apiValidationResult = validateApiConfigurationExcludingModelErrors(
  241. apiConfiguration,
  242. routerModels,
  243. organizationAllowList,
  244. )
  245. setErrorMessage(apiValidationResult)
  246. }, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage, isRetiredSelectedProvider])
  247. const onProviderChange = useCallback(
  248. (value: ProviderName) => {
  249. setApiConfigurationField("apiProvider", value)
  250. // It would be much easier to have a single attribute that stores
  251. // the modelId, but we have a separate attribute for each of
  252. // OpenRouter and Requesty.
  253. // If you switch to one of these providers and the corresponding
  254. // modelId is not set then you immediately end up in an error state.
  255. // To address that we set the modelId to the default value for th
  256. // provider if it's not already set.
  257. const validateAndResetModel = (
  258. provider: ProviderName,
  259. modelId: string | undefined,
  260. field: keyof ProviderSettings,
  261. defaultValue?: string,
  262. ) => {
  263. // in case we haven't set a default value for a provider
  264. if (!defaultValue) return
  265. // 1) If nothing is set, initialize to the provider default.
  266. if (!modelId) {
  267. setApiConfigurationField(field, defaultValue, false)
  268. return
  269. }
  270. // 2) If something *is* set, ensure it's valid for the newly selected provider.
  271. //
  272. // Without this, switching providers can leave the UI showing a model from the
  273. // previously selected provider (including model IDs that don't exist for the
  274. // newly selected provider).
  275. //
  276. // Note: We only validate providers with static model lists.
  277. const staticModels = MODELS_BY_PROVIDER[provider]
  278. if (!staticModels) {
  279. return
  280. }
  281. // Bedrock has a special “custom-arn” pseudo-model that isn't part of MODELS_BY_PROVIDER.
  282. if (provider === "bedrock" && modelId === "custom-arn") {
  283. return
  284. }
  285. const filteredModels = filterModels(staticModels, provider, organizationAllowList)
  286. const isValidModel = !!filteredModels && Object.prototype.hasOwnProperty.call(filteredModels, modelId)
  287. if (!isValidModel) {
  288. setApiConfigurationField(field, defaultValue, false)
  289. }
  290. }
  291. // Define a mapping object that associates each provider with its model configuration
  292. const PROVIDER_MODEL_CONFIG: Partial<
  293. Record<
  294. ProviderName,
  295. {
  296. field: keyof ProviderSettings
  297. default?: string
  298. }
  299. >
  300. > = {
  301. openrouter: { field: "openRouterModelId", default: openRouterDefaultModelId },
  302. requesty: { field: "requestyModelId", default: requestyDefaultModelId },
  303. litellm: { field: "litellmModelId", default: litellmDefaultModelId },
  304. anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
  305. "openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId },
  306. "qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
  307. "openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
  308. gemini: { field: "apiModelId", default: geminiDefaultModelId },
  309. deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
  310. moonshot: { field: "apiModelId", default: moonshotDefaultModelId },
  311. minimax: { field: "apiModelId", default: minimaxDefaultModelId },
  312. mistral: { field: "apiModelId", default: mistralDefaultModelId },
  313. xai: { field: "apiModelId", default: xaiDefaultModelId },
  314. baseten: { field: "apiModelId", default: basetenDefaultModelId },
  315. bedrock: { field: "apiModelId", default: bedrockDefaultModelId },
  316. vertex: { field: "apiModelId", default: vertexDefaultModelId },
  317. sambanova: { field: "apiModelId", default: sambaNovaDefaultModelId },
  318. zai: {
  319. field: "apiModelId",
  320. default:
  321. apiConfiguration.zaiApiLine === "china_coding"
  322. ? mainlandZAiDefaultModelId
  323. : internationalZAiDefaultModelId,
  324. },
  325. fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
  326. roo: { field: "apiModelId", default: rooDefaultModelId },
  327. "vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
  328. openai: { field: "openAiModelId" },
  329. ollama: { field: "ollamaModelId" },
  330. lmstudio: { field: "lmStudioModelId" },
  331. }
  332. const config = PROVIDER_MODEL_CONFIG[value]
  333. if (config) {
  334. validateAndResetModel(
  335. value,
  336. apiConfiguration[config.field] as string | undefined,
  337. config.field,
  338. config.default,
  339. )
  340. }
  341. },
  342. [setApiConfigurationField, apiConfiguration, organizationAllowList],
  343. )
  344. const modelValidationError = useMemo(() => {
  345. return getModelValidationError(apiConfiguration, routerModels, organizationAllowList)
  346. }, [apiConfiguration, routerModels, organizationAllowList])
  347. const docs = useMemo(() => {
  348. const provider = PROVIDERS.find(({ value }) => value === selectedProvider)
  349. const name = provider?.label
  350. if (!name) {
  351. return undefined
  352. }
  353. // Get the URL slug - use custom mapping if available, otherwise use the provider key.
  354. const slugs: Record<string, string> = {
  355. "openai-native": "openai",
  356. openai: "openai-compatible",
  357. }
  358. const slug = slugs[selectedProvider] || selectedProvider
  359. return {
  360. url: buildDocLink(`providers/${slug}`, "provider_docs"),
  361. name,
  362. }
  363. }, [selectedProvider])
  364. // Convert providers to SearchableSelect options
  365. const providerOptions = useMemo(() => {
  366. // First filter by organization allow list
  367. const allowedProviders = filterProviders(PROVIDERS, organizationAllowList)
  368. // Then filter out static providers that have no models (unless currently selected)
  369. const providersWithModels = allowedProviders.filter(({ value }) => {
  370. // Always show the currently selected provider to avoid breaking existing configurations
  371. // Use apiConfiguration.apiProvider directly since that's what's actually selected
  372. if (value === apiConfiguration.apiProvider) {
  373. return true
  374. }
  375. // Check if this is a static provider (has models in MODELS_BY_PROVIDER)
  376. const staticModels = MODELS_BY_PROVIDER[value as ProviderName]
  377. // If it's a static provider, check if it has any models after filtering
  378. if (staticModels) {
  379. const filteredModels = filterModels(staticModels, value as ProviderName, organizationAllowList)
  380. // Hide the provider if it has no models after filtering
  381. return filteredModels && Object.keys(filteredModels).length > 0
  382. }
  383. // If it's a dynamic provider (not in MODELS_BY_PROVIDER), always show it
  384. // to avoid race conditions with async model fetching
  385. return true
  386. })
  387. const options = providersWithModels.map(({ value, label }) => ({
  388. value,
  389. label,
  390. }))
  391. // Pin "roo" to the top if not on welcome screen
  392. if (!fromWelcomeView) {
  393. const rooIndex = options.findIndex((opt) => opt.value === "roo")
  394. if (rooIndex > 0) {
  395. const [rooOption] = options.splice(rooIndex, 1)
  396. options.unshift(rooOption)
  397. }
  398. } else {
  399. // Filter out roo from the welcome view
  400. const filteredOptions = options.filter((opt) => opt.value !== "roo")
  401. options.length = 0
  402. options.push(...filteredOptions)
  403. const openRouterIndex = options.findIndex((opt) => opt.value === "openrouter")
  404. if (openRouterIndex > 0) {
  405. const [openRouterOption] = options.splice(openRouterIndex, 1)
  406. options.unshift(openRouterOption)
  407. }
  408. }
  409. return options
  410. }, [organizationAllowList, apiConfiguration.apiProvider, fromWelcomeView])
  411. return (
  412. <div className="flex flex-col gap-3">
  413. <div className="flex flex-col gap-1 relative">
  414. <div className="flex justify-between items-center">
  415. <label className="block font-medium">{t("settings:providers.apiProvider")}</label>
  416. {selectedProvider === "roo" && cloudIsAuthenticated ? (
  417. <RooBalanceDisplay />
  418. ) : (
  419. docs && (
  420. <VSCodeLink href={docs.url} target="_blank" className="flex gap-2">
  421. {t("settings:providers.apiProviderDocs")}
  422. <BookOpenText className="size-4 inline ml-2" />
  423. </VSCodeLink>
  424. )
  425. )}
  426. </div>
  427. <SearchableSelect
  428. value={selectedProvider}
  429. onValueChange={(value) => onProviderChange(value as ProviderName)}
  430. options={providerOptions}
  431. placeholder={t("settings:common.select")}
  432. searchPlaceholder={t("settings:providers.searchProviderPlaceholder")}
  433. emptyMessage={t("settings:providers.noProviderMatchFound")}
  434. className="w-full"
  435. data-testid="provider-select"
  436. />
  437. </div>
  438. {errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
  439. {isRetiredSelectedProvider ? (
  440. <div
  441. className="rounded-md border border-vscode-panel-border px-3 py-2 text-sm text-vscode-descriptionForeground"
  442. data-testid="retired-provider-message">
  443. {t("settings:providers.retiredProviderMessage")}
  444. </div>
  445. ) : (
  446. <>
  447. {selectedProvider === "openrouter" && (
  448. <OpenRouter
  449. apiConfiguration={apiConfiguration}
  450. setApiConfigurationField={setApiConfigurationField}
  451. routerModels={routerModels}
  452. selectedModelId={selectedModelId}
  453. uriScheme={uriScheme}
  454. simplifySettings={fromWelcomeView}
  455. organizationAllowList={organizationAllowList}
  456. modelValidationError={modelValidationError}
  457. />
  458. )}
  459. {selectedProvider === "requesty" && (
  460. <Requesty
  461. uriScheme={uriScheme}
  462. apiConfiguration={apiConfiguration}
  463. setApiConfigurationField={setApiConfigurationField}
  464. routerModels={routerModels}
  465. refetchRouterModels={refetchRouterModels}
  466. organizationAllowList={organizationAllowList}
  467. modelValidationError={modelValidationError}
  468. simplifySettings={fromWelcomeView}
  469. />
  470. )}
  471. {selectedProvider === "anthropic" && (
  472. <Anthropic
  473. apiConfiguration={apiConfiguration}
  474. setApiConfigurationField={setApiConfigurationField}
  475. simplifySettings={fromWelcomeView}
  476. />
  477. )}
  478. {selectedProvider === "azure" && (
  479. <Azure
  480. apiConfiguration={apiConfiguration}
  481. setApiConfigurationField={setApiConfigurationField}
  482. simplifySettings={fromWelcomeView}
  483. />
  484. )}
  485. {selectedProvider === "openai-codex" && (
  486. <OpenAICodex
  487. apiConfiguration={apiConfiguration}
  488. setApiConfigurationField={setApiConfigurationField}
  489. simplifySettings={fromWelcomeView}
  490. openAiCodexIsAuthenticated={openAiCodexIsAuthenticated}
  491. />
  492. )}
  493. {selectedProvider === "openai-native" && (
  494. <OpenAI
  495. apiConfiguration={apiConfiguration}
  496. setApiConfigurationField={setApiConfigurationField}
  497. selectedModelInfo={selectedModelInfo}
  498. simplifySettings={fromWelcomeView}
  499. />
  500. )}
  501. {selectedProvider === "mistral" && (
  502. <Mistral
  503. apiConfiguration={apiConfiguration}
  504. setApiConfigurationField={setApiConfigurationField}
  505. simplifySettings={fromWelcomeView}
  506. />
  507. )}
  508. {selectedProvider === "baseten" && (
  509. <Baseten
  510. apiConfiguration={apiConfiguration}
  511. setApiConfigurationField={setApiConfigurationField}
  512. simplifySettings={fromWelcomeView}
  513. />
  514. )}
  515. {selectedProvider === "bedrock" && (
  516. <Bedrock
  517. apiConfiguration={apiConfiguration}
  518. setApiConfigurationField={setApiConfigurationField}
  519. selectedModelInfo={selectedModelInfo}
  520. simplifySettings={fromWelcomeView}
  521. />
  522. )}
  523. {selectedProvider === "vertex" && (
  524. <Vertex
  525. apiConfiguration={apiConfiguration}
  526. setApiConfigurationField={setApiConfigurationField}
  527. />
  528. )}
  529. {selectedProvider === "gemini" && (
  530. <Gemini
  531. apiConfiguration={apiConfiguration}
  532. setApiConfigurationField={setApiConfigurationField}
  533. />
  534. )}
  535. {selectedProvider === "openai" && (
  536. <OpenAICompatible
  537. apiConfiguration={apiConfiguration}
  538. setApiConfigurationField={setApiConfigurationField}
  539. organizationAllowList={organizationAllowList}
  540. modelValidationError={modelValidationError}
  541. simplifySettings={fromWelcomeView}
  542. />
  543. )}
  544. {selectedProvider === "lmstudio" && (
  545. <LMStudio
  546. apiConfiguration={apiConfiguration}
  547. setApiConfigurationField={setApiConfigurationField}
  548. />
  549. )}
  550. {selectedProvider === "deepseek" && (
  551. <DeepSeek
  552. apiConfiguration={apiConfiguration}
  553. setApiConfigurationField={setApiConfigurationField}
  554. simplifySettings={fromWelcomeView}
  555. />
  556. )}
  557. {selectedProvider === "qwen-code" && (
  558. <QwenCode
  559. apiConfiguration={apiConfiguration}
  560. setApiConfigurationField={setApiConfigurationField}
  561. simplifySettings={fromWelcomeView}
  562. />
  563. )}
  564. {selectedProvider === "moonshot" && (
  565. <Moonshot
  566. apiConfiguration={apiConfiguration}
  567. setApiConfigurationField={setApiConfigurationField}
  568. simplifySettings={fromWelcomeView}
  569. />
  570. )}
  571. {selectedProvider === "minimax" && (
  572. <MiniMax
  573. apiConfiguration={apiConfiguration}
  574. setApiConfigurationField={setApiConfigurationField}
  575. />
  576. )}
  577. {selectedProvider === "vscode-lm" && (
  578. <VSCodeLM
  579. apiConfiguration={apiConfiguration}
  580. setApiConfigurationField={setApiConfigurationField}
  581. />
  582. )}
  583. {selectedProvider === "ollama" && (
  584. <Ollama
  585. apiConfiguration={apiConfiguration}
  586. setApiConfigurationField={setApiConfigurationField}
  587. />
  588. )}
  589. {selectedProvider === "xai" && (
  590. <XAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
  591. )}
  592. {selectedProvider === "litellm" && (
  593. <LiteLLM
  594. apiConfiguration={apiConfiguration}
  595. setApiConfigurationField={setApiConfigurationField}
  596. organizationAllowList={organizationAllowList}
  597. modelValidationError={modelValidationError}
  598. simplifySettings={fromWelcomeView}
  599. />
  600. )}
  601. {selectedProvider === "sambanova" && (
  602. <SambaNova
  603. apiConfiguration={apiConfiguration}
  604. setApiConfigurationField={setApiConfigurationField}
  605. />
  606. )}
  607. {selectedProvider === "zai" && (
  608. <ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
  609. )}
  610. {selectedProvider === "vercel-ai-gateway" && (
  611. <VercelAiGateway
  612. apiConfiguration={apiConfiguration}
  613. setApiConfigurationField={setApiConfigurationField}
  614. routerModels={routerModels}
  615. organizationAllowList={organizationAllowList}
  616. modelValidationError={modelValidationError}
  617. simplifySettings={fromWelcomeView}
  618. />
  619. )}
  620. {selectedProvider === "fireworks" && (
  621. <Fireworks
  622. apiConfiguration={apiConfiguration}
  623. setApiConfigurationField={setApiConfigurationField}
  624. />
  625. )}
  626. {selectedProvider === "roo" && (
  627. <Roo
  628. apiConfiguration={apiConfiguration}
  629. setApiConfigurationField={setApiConfigurationField}
  630. routerModels={routerModels}
  631. cloudIsAuthenticated={cloudIsAuthenticated}
  632. organizationAllowList={organizationAllowList}
  633. modelValidationError={modelValidationError}
  634. simplifySettings={fromWelcomeView}
  635. />
  636. )}
  637. {/* Generic model picker for providers with static models */}
  638. {activeSelectedProvider && shouldUseGenericModelPicker(activeSelectedProvider) && (
  639. <>
  640. <ModelPicker
  641. apiConfiguration={apiConfiguration}
  642. setApiConfigurationField={setApiConfigurationField}
  643. defaultModelId={getDefaultModelIdForProvider(activeSelectedProvider, apiConfiguration)}
  644. models={getStaticModelsForProvider(
  645. activeSelectedProvider,
  646. t("settings:labels.useCustomArn"),
  647. )}
  648. modelIdKey="apiModelId"
  649. serviceName={getProviderServiceConfig(activeSelectedProvider).serviceName}
  650. serviceUrl={getProviderServiceConfig(activeSelectedProvider).serviceUrl}
  651. organizationAllowList={organizationAllowList}
  652. errorMessage={modelValidationError}
  653. simplifySettings={fromWelcomeView}
  654. onModelChange={(modelId) =>
  655. handleModelChangeSideEffects(
  656. activeSelectedProvider,
  657. modelId,
  658. setApiConfigurationField,
  659. )
  660. }
  661. />
  662. {selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
  663. <BedrockCustomArn
  664. apiConfiguration={apiConfiguration}
  665. setApiConfigurationField={setApiConfigurationField}
  666. />
  667. )}
  668. </>
  669. )}
  670. {!fromWelcomeView && (
  671. <ThinkingBudget
  672. key={`${selectedProvider}-${selectedModelId}`}
  673. apiConfiguration={apiConfiguration}
  674. setApiConfigurationField={setApiConfigurationField}
  675. modelInfo={selectedModelInfo}
  676. />
  677. )}
  678. {/* Gate Verbosity UI by capability flag */}
  679. {!fromWelcomeView && selectedModelInfo?.supportsVerbosity && (
  680. <Verbosity
  681. apiConfiguration={apiConfiguration}
  682. setApiConfigurationField={setApiConfigurationField}
  683. modelInfo={selectedModelInfo}
  684. />
  685. )}
  686. {!fromWelcomeView && (
  687. <Collapsible open={isAdvancedSettingsOpen} onOpenChange={setIsAdvancedSettingsOpen}>
  688. <CollapsibleTrigger className="flex items-center gap-1 w-full cursor-pointer hover:opacity-80 mb-2">
  689. <span
  690. className={`codicon codicon-chevron-${isAdvancedSettingsOpen ? "down" : "right"}`}></span>
  691. <span className="font-medium">{t("settings:advancedSettings.title")}</span>
  692. </CollapsibleTrigger>
  693. <CollapsibleContent className="space-y-3">
  694. <TodoListSettingsControl
  695. todoListEnabled={apiConfiguration.todoListEnabled}
  696. onChange={(field, value) => setApiConfigurationField(field, value)}
  697. />
  698. {selectedModelInfo?.supportsTemperature !== false && (
  699. <TemperatureControl
  700. value={apiConfiguration.modelTemperature}
  701. onChange={handleInputChange("modelTemperature", noTransform)}
  702. maxValue={2}
  703. defaultValue={selectedModelInfo?.defaultTemperature}
  704. />
  705. )}
  706. <RateLimitSecondsControl
  707. value={apiConfiguration.rateLimitSeconds || 0}
  708. onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
  709. />
  710. <ConsecutiveMistakeLimitControl
  711. value={
  712. apiConfiguration.consecutiveMistakeLimit !== undefined
  713. ? apiConfiguration.consecutiveMistakeLimit
  714. : DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
  715. }
  716. onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
  717. />
  718. {selectedProvider === "openrouter" &&
  719. openRouterModelProviders &&
  720. Object.keys(openRouterModelProviders).length > 0 && (
  721. <div>
  722. <div className="flex items-center gap-1">
  723. <label className="block font-medium mb-1">
  724. {t("settings:providers.openRouter.providerRouting.title")}
  725. </label>
  726. <a href={`https://openrouter.ai/${selectedModelId}/providers`}>
  727. <ExternalLinkIcon className="w-4 h-4" />
  728. </a>
  729. </div>
  730. <Select
  731. value={
  732. apiConfiguration?.openRouterSpecificProvider ||
  733. OPENROUTER_DEFAULT_PROVIDER_NAME
  734. }
  735. onValueChange={(value) =>
  736. setApiConfigurationField("openRouterSpecificProvider", value)
  737. }>
  738. <SelectTrigger className="w-full">
  739. <SelectValue placeholder={t("settings:common.select")} />
  740. </SelectTrigger>
  741. <SelectContent>
  742. <SelectItem value={OPENROUTER_DEFAULT_PROVIDER_NAME}>
  743. {OPENROUTER_DEFAULT_PROVIDER_NAME}
  744. </SelectItem>
  745. {Object.entries(openRouterModelProviders).map(
  746. ([value, { label }]) => (
  747. <SelectItem key={value} value={value}>
  748. {label}
  749. </SelectItem>
  750. ),
  751. )}
  752. </SelectContent>
  753. </Select>
  754. <div className="text-sm text-vscode-descriptionForeground mt-1">
  755. {t("settings:providers.openRouter.providerRouting.description")}{" "}
  756. <a href="https://openrouter.ai/docs/features/provider-routing">
  757. {t("settings:providers.openRouter.providerRouting.learnMore")}.
  758. </a>
  759. </div>
  760. </div>
  761. )}
  762. </CollapsibleContent>
  763. </Collapsible>
  764. )}
  765. </>
  766. )}
  767. </div>
  768. )
  769. }
  770. export default memo(ApiOptions)