Enhance LiteLLM provider configuration and default handling

- Updated ApiOptions to ensure model-specific IDs are set correctly when switching to the LiteLLM provider.
- Added useEffect in SettingsView to set default values for LiteLLM configuration fields if they are missing.
- Improved error handling in LiteLLM component for missing API key and base URL during model refresh.
- Added missing configuration error messages in multiple languages for better user feedback.
This commit is contained in:
slytechnical 2025-05-19 17:56:48 -05:00
parent 9546dc7ea0
commit f92e4b9233
22 changed files with 202 additions and 41 deletions

View file

@ -263,16 +263,29 @@ const ApiOptions = ({
setApiConfigurationField("requestyModelId", requestyDefaultModelId)
}
break
case "litellm":
if (!apiConfiguration.litellmModelId) {
case "litellm": {
let currentLitellmModelId = apiConfiguration.litellmModelId
if (!currentLitellmModelId) {
setApiConfigurationField("litellmModelId", litellmDefaultModelId)
currentLitellmModelId = litellmDefaultModelId // Use the default for the next step
}
// Ensure apiModelId is also set to the specific litellm model id
if (apiConfiguration.apiModelId !== currentLitellmModelId) {
setApiConfigurationField("apiModelId", currentLitellmModelId)
}
break
}
}
setApiConfigurationField("apiProvider", value)
// Only update the apiProvider if it's actually changing.
// This should be called after model-specific IDs are handled for the new provider.
if (apiConfiguration.apiProvider !== value) {
setApiConfigurationField("apiProvider", value)
}
},
[
apiConfiguration.apiProvider,
apiConfiguration.apiModelId, // Add apiModelId as it's read and potentially set
setApiConfigurationField,
apiConfiguration.openRouterModelId,
apiConfiguration.glamaModelId,

View file

@ -27,7 +27,7 @@ import {
import { ExperimentId } from "@roo/shared/experiments"
import { TelemetrySetting } from "@roo/shared/TelemetrySetting"
import { ProviderSettings } from "@roo/shared/api"
import { ProviderSettings, litellmDefaultModelId } from "@roo/shared/api"
import { vscode } from "@/utils/vscode"
import { ExtensionStateContextType, useExtensionState } from "@/context/ExtensionStateContext"
@ -219,6 +219,51 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
})
}, [])
useEffect(() => {
// This effect ensures that if LiteLLM is the provider,
// its specific configuration fields have default values in the cached state.
const config = cachedState.apiConfiguration
if (config && config.apiProvider === "litellm") {
const baseUrlMissing = config.litellmBaseUrl === undefined
const apiKeyMissing = config.litellmApiKey === undefined
if (baseUrlMissing || apiKeyMissing) {
setCachedState((prevState) => {
// Ensure we are working with the latest apiConfiguration from prevState
const currentApiConfig = prevState.apiConfiguration ?? {}
let updatedApiConfig = { ...currentApiConfig } // Clone to modify
let madeChanges = false
// Check and apply defaults based on the fresh currentApiConfig
if (currentApiConfig.litellmBaseUrl === undefined) {
updatedApiConfig.litellmBaseUrl = "http://localhost:4000"
madeChanges = true
}
if (currentApiConfig.litellmApiKey === undefined) {
updatedApiConfig.litellmApiKey = "sk-1234"
madeChanges = true
}
if (currentApiConfig.litellmModelId === undefined) {
updatedApiConfig.litellmModelId = litellmDefaultModelId
madeChanges = true
}
if (madeChanges) {
// setChangeDetected is not directly available here unless passed to setCachedState's scope or handled differently
// For now, we focus on updating apiConfiguration correctly.
// The parent component (SettingsView) already calls setChangeDetected(true) when setApiConfigurationField is used.
// If we bypass setApiConfigurationField, we need to call setChangeDetected here.
setChangeDetected(true) // Call setChangeDetected as we are modifying the state that tracks changes.
return { ...prevState, apiConfiguration: updatedApiConfig }
}
return prevState // No changes were actually needed based on the fresh check
})
}
}
// Adding setCachedState and setChangeDetected to dependencies as they are used directly or indirectly.
// setApiConfigurationField is removed as we are not calling it from here for these specific defaults.
}, [cachedState.apiConfiguration, setCachedState, setChangeDetected])
const setTelemetrySetting = useCallback((setting: TelemetrySetting) => {
setCachedState((prevState) => {
if (prevState.telemetrySetting === setting) {

View file

@ -1,4 +1,4 @@
import { useCallback, useState } from "react"
import { useCallback, useState, useEffect, useRef } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useEvent } from "react-use"
@ -24,6 +24,7 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, routerMode
const { t } = useAppTranslation()
const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
const [refreshError, setRefreshError] = useState<string | undefined>()
const initialRefreshPerformedRef = useRef(false)
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
@ -36,14 +37,18 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, routerMode
[setApiConfigurationField],
)
const handleRefreshModels = () => {
const handleRefreshModels = useCallback(() => {
setRefreshStatus("loading")
setRefreshError(undefined)
// Due to the button's disabled state logic, litellmApiKey and litellmBaseUrl are guaranteed to be non-empty strings here.
// We use non-null assertions (!) to reflect this guarantee for type safety.
const key = apiConfiguration.litellmApiKey!
const url = apiConfiguration.litellmBaseUrl!
const key = apiConfiguration.litellmApiKey
const url = apiConfiguration.litellmBaseUrl
if (!key || !url) {
setRefreshStatus("error")
setRefreshError(t("settings:providers.refreshModels.missingConfig"))
return
}
const message: WebviewMessage = {
type: "requestProviderModels",
@ -54,9 +59,34 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, routerMode
},
}
vscode.postMessage(message)
}
}, [apiConfiguration.litellmApiKey, apiConfiguration.litellmBaseUrl, setRefreshStatus, setRefreshError, t])
// Effect to trigger initial model refresh, once per component instance when conditions are met
useEffect(() => {
// Only proceed if the initial refresh for this component instance hasn't been done
if (initialRefreshPerformedRef.current) {
return
}
// Check if the necessary configuration is available
if (apiConfiguration.litellmApiKey && apiConfiguration.litellmBaseUrl) {
// Mark that we are performing the refresh for this instance
initialRefreshPerformedRef.current = true
// Directly execute refresh logic
setRefreshStatus("loading")
setRefreshError(undefined)
const message: WebviewMessage = {
type: "requestProviderModels",
payload: {
provider: "litellm",
apiKey: apiConfiguration.litellmApiKey,
baseUrl: apiConfiguration.litellmBaseUrl,
},
}
vscode.postMessage(message)
}
}, [apiConfiguration.litellmApiKey, apiConfiguration.litellmBaseUrl])
// Listen for model refresh responses using useEvent
useEvent("message", (event: MessageEvent<ExtensionMessage>) => {
const message = event.data
if (message.type === "providerModelsResponse") {
@ -67,7 +97,6 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, routerMode
setRefreshError(message.payload.error)
} else {
setRefreshStatus("success")
// Parent (ApiOptions.tsx) will handle updating the routerModels prop for ModelPicker
}
} else {
console.log(
@ -77,7 +106,6 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, routerMode
}
}
})
console.log("apiconfig1212", apiConfiguration)
return (
<>

View file

@ -1,4 +1,4 @@
import { useCallback, useState } from "react"
import { useCallback, useState, useEffect } from "react"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { validateApiConfiguration } from "@src/utils/validate"
@ -10,12 +10,67 @@ import { useAppTranslation } from "@src/i18n/TranslationContext"
import { getRequestyAuthUrl, getOpenRouterAuthUrl } from "@src/oauth/urls"
import RooHero from "./RooHero"
import knuthShuffle from "knuth-shuffle-seeded"
import { ProviderSettings, litellmDefaultModelId } from "@roo/shared/api"
const WelcomeView = () => {
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, machineId } = useExtensionState()
const { t } = useAppTranslation()
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
// Memoize the setApiConfigurationField function to pass to ApiOptions
const setApiConfigurationFieldForApiOptions = useCallback(
<K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) => {
setApiConfiguration({ [field]: value })
},
[setApiConfiguration], // setApiConfiguration from context is stable
)
useEffect(() => {
if (!apiConfiguration) {
// If no apiConfig at all, nothing to default yet.
// This can happen before the initial state hydration from the extension.
return
}
const currentProvider = apiConfiguration.apiProvider
// Needs provider default if apiProvider is undefined, null, or an empty string.
const needsProviderDefault = !currentProvider
const isLiteLLMSelectedOrShouldBeDefault = currentProvider === "litellm" || needsProviderDefault
if (isLiteLLMSelectedOrShouldBeDefault) {
const updates: Partial<ProviderSettings> = {}
let madeChanges = false
if (apiConfiguration.litellmBaseUrl === undefined) {
updates.litellmBaseUrl = "http://localhost:4000"
madeChanges = true
}
if (apiConfiguration.litellmApiKey === undefined) {
updates.litellmApiKey = "sk-1234"
madeChanges = true
}
if (apiConfiguration.litellmModelId === undefined) {
updates.litellmModelId = litellmDefaultModelId
madeChanges = true
}
// If apiProvider was initially missing or falsy, set it to "litellm".
if (needsProviderDefault) {
updates.apiProvider = "litellm"
madeChanges = true
}
if (madeChanges) {
// This log helps confirm if we are about to call setApiConfiguration
setApiConfiguration(updates)
} else {
// This log helps confirm that on subsequent runs, no changes are deemed necessary.
}
}
}, [apiConfiguration, setApiConfiguration])
useEffect(() => {}, [apiConfiguration])
const handleSubmit = useCallback(() => {
const error = apiConfiguration ? validateApiConfiguration(apiConfiguration) : undefined
@ -106,7 +161,7 @@ const WelcomeView = () => {
fromWelcomeView
apiConfiguration={apiConfiguration || {}}
uriScheme={uriScheme}
setApiConfigurationField={(field, value) => setApiConfiguration({ [field]: value })}
setApiConfigurationField={setApiConfigurationFieldForApiOptions}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>

View file

@ -188,6 +188,16 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
[],
)
const setApiConfiguration = useCallback((value: ProviderSettings) => {
setState((prevState) => ({
...prevState,
apiConfiguration: {
...prevState.apiConfiguration,
...value,
},
}))
}, [])
const handleMessage = useCallback(
(event: MessageEvent) => {
const message: ExtensionMessage = event.data
@ -266,14 +276,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
screenshotQuality: state.screenshotQuality,
setExperimentEnabled: (id, enabled) =>
setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
setApiConfiguration: (value) =>
setState((prevState) => ({
...prevState,
apiConfiguration: {
...prevState.apiConfiguration,
...value,
},
})),
setApiConfiguration,
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
setAlwaysAllowReadOnlyOutsideWorkspace: (value) =>

View file

@ -121,7 +121,8 @@
"hint": "Si us plau, torneu a obrir la configuració per veure els models més recents.",
"loading": "Actualitzant models...",
"success": "Models actualitzats correctament.",
"error": "No s'han pogut actualitzar els models. Si us plau, comproveu la vostra configuració i torneu-ho a provar."
"error": "No s'han pogut actualitzar els models. Si us plau, comproveu la vostra configuració i torneu-ho a provar.",
"missingConfig": "Falta la clau API o l'URL base. Si us plau, proporcioneu ambdós per actualitzar els models."
},
"getRequestyApiKey": "Obtenir clau API de Requesty",
"openRouterTransformsText": "Comprimir prompts i cadenes de missatges a la mida del context (<a>Transformacions d'OpenRouter</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Bitte öffne die Einstellungen erneut, um die neuesten Modelle zu sehen.",
"loading": "Modelle werden aktualisiert...",
"success": "Modelle erfolgreich aktualisiert.",
"error": "Fehler beim Aktualisieren der Modelle. Bitte überprüfe deine Konfiguration und versuche es erneut."
"error": "Fehler beim Aktualisieren der Modelle. Bitte überprüfe deine Konfiguration und versuche es erneut.",
"missingConfig": "API-Schlüssel oder Basis-URL fehlt. Bitte gib beides an, um Modelle zu aktualisieren."
},
"getRequestyApiKey": "Requesty API-Schlüssel erhalten",
"openRouterTransformsText": "Prompts und Nachrichtenketten auf Kontextgröße komprimieren (<a>OpenRouter Transformationen</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Please reopen the settings to see the latest models.",
"loading": "Refreshing models...",
"success": "Models refreshed successfully.",
"error": "Failed to refresh models. Please check your configuration and try again."
"error": "Failed to refresh models. Please check your configuration and try again.",
"missingConfig": "API key or base URL missing. Please provide both to refresh models."
},
"getRequestyApiKey": "Get Requesty API Key",
"openRouterTransformsText": "Compress prompts and message chains to the context size (<a>OpenRouter Transforms</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Por favor, vuelve a abrir la configuración para ver los modelos más recientes.",
"loading": "Actualizando modelos...",
"success": "Modelos actualizados correctamente.",
"error": "Error al actualizar los modelos. Por favor, verifica tu configuración e inténtalo de nuevo."
"error": "Error al actualizar los modelos. Por favor, verifica tu configuración e inténtalo de nuevo.",
"missingConfig": "Falta la clave API o la URL base. Por favor, proporciona ambos para actualizar los modelos."
},
"getRequestyApiKey": "Obtener clave API de Requesty",
"openRouterTransformsText": "Comprimir prompts y cadenas de mensajes al tamaño del contexto (<a>Transformaciones de OpenRouter</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Veuillez rouvrir les paramètres pour voir les modèles les plus récents.",
"loading": "Actualisation des modèles...",
"success": "Modèles actualisés avec succès.",
"error": "Échec de l'actualisation des modèles. Veuillez vérifier votre configuration et réessayer."
"error": "Échec de l'actualisation des modèles. Veuillez vérifier votre configuration et réessayer.",
"missingConfig": "Clé API ou URL de base manquante. Veuillez fournir les deux pour actualiser les modèles."
},
"getRequestyApiKey": "Obtenir la clé API Requesty",
"openRouterTransformsText": "Compresser les prompts et chaînes de messages à la taille du contexte (<a>Transformations OpenRouter</a>)",

View file

@ -121,7 +121,8 @@
"hint": "नवीनतम मॉडल देखने के लिए कृपया सेटिंग्स को फिर से खोलें।",
"loading": "मॉडल रिफ्रेश हो रहे हैं...",
"success": "मॉडल सफलतापूर्वक रिफ्रेश हो गए।",
"error": "मॉडल रिफ्रेश करने में विफल। कृपया अपनी कॉन्फ़िगरेशन जांचें और पुनः प्रयास करें।"
"error": "मॉडल रिफ्रेश करने में विफल। कृपया अपनी कॉन्फ़िगरेशन जांचें और पुनः प्रयास करें।",
"missingConfig": "API कुंजी या बेस URL अनुपलब्ध है। मॉडल रिफ्रेश करने के लिए कृपया दोनों प्रदान करें।"
},
"getRequestyApiKey": "Requesty API कुंजी प्राप्त करें",
"openRouterTransformsText": "संदर्भ आकार के लिए प्रॉम्प्ट और संदेश श्रृंखलाओं को संपीड़ित करें (<a>OpenRouter ट्रांसफॉर्म</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Riapri le impostazioni per vedere i modelli più recenti.",
"loading": "Aggiornamento modelli in corso...",
"success": "Modelli aggiornati con successo.",
"error": "Impossibile aggiornare i modelli. Verifica la tua configurazione e riprova."
"error": "Impossibile aggiornare i modelli. Verifica la tua configurazione e riprova.",
"missingConfig": "Chiave API o URL base mancante. Fornisci entrambi per aggiornare i modelli."
},
"getRequestyApiKey": "Ottieni chiave API Requesty",
"openRouterTransformsText": "Comprimi prompt e catene di messaggi alla dimensione del contesto (<a>Trasformazioni OpenRouter</a>)",

View file

@ -121,7 +121,8 @@
"hint": "最新のモデルを表示するには設定を再度開いてください。",
"loading": "モデルを更新中...",
"success": "モデルが正常に更新されました。",
"error": "モデルの更新に失敗しました。設定を確認して再試行してください。"
"error": "モデルの更新に失敗しました。設定を確認して再試行してください。",
"missingConfig": "APIキーまたはベースURLが不足しています。モデルを更新するには両方を提供してください。"
},
"getRequestyApiKey": "Requesty APIキーを取得",
"openRouterTransformsText": "プロンプトとメッセージチェーンをコンテキストサイズに圧縮 (<a>OpenRouter Transforms</a>)",

View file

@ -121,7 +121,8 @@
"hint": "최신 모델을 보려면 설정을 다시 열어주세요.",
"loading": "모델 새로고침 중...",
"success": "모델이 성공적으로 새로고침되었습니다.",
"error": "모델 새로고침에 실패했습니다. 설정을 확인하고 다시 시도해주세요."
"error": "모델 새로고침에 실패했습니다. 설정을 확인하고 다시 시도해주세요.",
"missingConfig": "API 키 또는 기본 URL이 누락되었습니다. 모델을 새로고침하려면 둘 다 제공해주세요."
},
"getRequestyApiKey": "Requesty API 키 받기",
"openRouterTransformsText": "프롬프트와 메시지 체인을 컨텍스트 크기로 압축 (<a>OpenRouter Transforms</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Open de instellingen opnieuw om de nieuwste modellen te zien.",
"loading": "Modellen verversen...",
"success": "Modellen succesvol ververst.",
"error": "Modellen verversen mislukt. Controleer je configuratie en probeer het opnieuw."
"error": "Modellen verversen mislukt. Controleer je configuratie en probeer het opnieuw.",
"missingConfig": "API-sleutel of basis-URL ontbreekt. Geef beide op om modellen te verversen."
},
"getRequestyApiKey": "Requesty API-sleutel ophalen",
"openRouterTransformsText": "Comprimeer prompts en berichtreeksen tot de contextgrootte (<a>OpenRouter Transforms</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Proszę ponownie otworzyć ustawienia, aby zobaczyć najnowsze modele.",
"loading": "Odświeżanie modeli...",
"success": "Modele zostały pomyślnie odświeżone.",
"error": "Nie udało się odświeżyć modeli. Sprawdź konfigurację i spróbuj ponownie."
"error": "Nie udało się odświeżyć modeli. Sprawdź konfigurację i spróbuj ponownie.",
"missingConfig": "Brak klucza API lub podstawowego URL. Podaj oba, aby odświeżyć modele."
},
"getRequestyApiKey": "Uzyskaj klucz API Requesty",
"openRouterTransformsText": "Kompresuj podpowiedzi i łańcuchy wiadomości do rozmiaru kontekstu (<a>Transformacje OpenRouter</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Por favor, reabra as configurações para ver os modelos mais recentes.",
"loading": "Atualizando modelos...",
"success": "Modelos atualizados com sucesso.",
"error": "Falha ao atualizar modelos. Verifique sua configuração e tente novamente."
"error": "Falha ao atualizar modelos. Verifique sua configuração e tente novamente.",
"missingConfig": "Chave API ou URL base ausente. Por favor, forneça ambos para atualizar os modelos."
},
"getRequestyApiKey": "Obter chave de API Requesty",
"openRouterTransformsText": "Comprimir prompts e cadeias de mensagens para o tamanho do contexto (<a>Transformações OpenRouter</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Пожалуйста, откройте настройки заново, чтобы увидеть последние модели.",
"loading": "Обновление моделей...",
"success": "Модели успешно обновлены.",
"error": "Не удалось обновить модели. Пожалуйста, проверьте вашу конфигурацию и попробуйте снова."
"error": "Не удалось обновить модели. Пожалуйста, проверьте вашу конфигурацию и попробуйте снова.",
"missingConfig": "Отсутствует API-ключ или базовый URL. Пожалуйста, укажите оба параметра для обновления моделей."
},
"getRequestyApiKey": "Получить Requesty API-ключ",
"openRouterTransformsText": "Сжимать подсказки и цепочки сообщений до размера контекста (<a>OpenRouter Transforms</a>)",

View file

@ -121,7 +121,8 @@
"hint": "En son modelleri görmek için lütfen ayarları yeniden açın.",
"loading": "Modeller yenileniyor...",
"success": "Modeller başarıyla yenilendi.",
"error": "Modeller yenilenemedi. Lütfen yapılandırmanızı kontrol edin ve tekrar deneyin."
"error": "Modeller yenilenemedi. Lütfen yapılandırmanızı kontrol edin ve tekrar deneyin.",
"missingConfig": "API anahtarı veya temel URL eksik. Modelleri yenilemek için lütfen her ikisini de sağlayın."
},
"getRequestyApiKey": "Requesty API Anahtarı Al",
"openRouterTransformsText": "İstem ve mesaj zincirlerini bağlam boyutuna sıkıştır (<a>OpenRouter Dönüşümleri</a>)",

View file

@ -121,7 +121,8 @@
"hint": "Vui lòng mở lại cài đặt để xem các mô hình mới nhất.",
"loading": "Đang làm mới mô hình...",
"success": "Làm mới mô hình thành công.",
"error": "Không thể làm mới mô hình. Vui lòng kiểm tra cấu hình của bạn và thử lại."
"error": "Không thể làm mới mô hình. Vui lòng kiểm tra cấu hình của bạn và thử lại.",
"missingConfig": "Thiếu khóa API hoặc URL cơ sở. Vui lòng cung cấp cả hai để làm mới mô hình."
},
"getRequestyApiKey": "Lấy khóa API Requesty",
"openRouterTransformsText": "Nén lời nhắc và chuỗi tin nhắn theo kích thước ngữ cảnh (<a>OpenRouter Transforms</a>)",

View file

@ -121,7 +121,8 @@
"hint": "请重新打开设置以查看最新模型。",
"loading": "正在刷新模型...",
"success": "模型刷新成功。",
"error": "刷新模型失败。请检查您的配置并重试。"
"error": "刷新模型失败。请检查您的配置并重试。",
"missingConfig": "缺少 API 密钥或基础 URL。请提供两者以刷新模型。"
},
"getRequestyApiKey": "获取 Requesty API 密钥",
"openRouterTransformsText": "自动压缩提示词和消息链到上下文长度限制内 (<a>OpenRouter转换</a>)",

View file

@ -121,7 +121,8 @@
"hint": "請重新開啟設定以查看最新模型。",
"loading": "正在重新整理模型...",
"success": "模型重新整理成功。",
"error": "重新整理模型失敗。請檢查您的設定並重試。"
"error": "重新整理模型失敗。請檢查您的設定並重試。",
"missingConfig": "缺少 API 金鑰或基礎 URL。請提供兩者以重新整理模型。"
},
"getRequestyApiKey": "取得 Requesty API 金鑰",
"openRouterTransformsText": "將提示和訊息鏈壓縮到上下文大小 (<a>OpenRouter 轉換</a>)",