mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add model routing UI settings, fix api handler restoration bug
- Add translation keys for MODEL_ROUTING in locales/en/settings.json - Create ModelRoutingSettings component with toggle and light model ID input - Wire up MODEL_ROUTING special-case in ExperimentalSettings.tsx - Add modelRoutingLightModelId state binding in SettingsView.tsx - Fix bug: restore this.api to primary handler on all paths (stream failure, empty-response retry, catch) not just the happy path - Fix nit: remove unnecessary as any cast in ModelRouter.isEnabled()
This commit is contained in:
parent
6e090e7112
commit
37c8e2ae3f
6 changed files with 111 additions and 7 deletions
|
|
@ -187,7 +187,7 @@ export class ModelRouter {
|
|||
if (!experimentsConfig || !lightModelId || lightModelId.trim() === "") {
|
||||
return false
|
||||
}
|
||||
return experiments.isEnabled(experimentsConfig, EXPERIMENT_IDS.MODEL_ROUTING as any)
|
||||
return experiments.isEnabled(experimentsConfig, EXPERIMENT_IDS.MODEL_ROUTING)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2795,6 +2795,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
await this.saveClineMessages()
|
||||
await this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory()
|
||||
|
||||
// Model routing: declare outside try block so catch can restore it
|
||||
let primaryApiHandler: typeof this.api | undefined
|
||||
|
||||
try {
|
||||
let cacheWriteTokens = 0
|
||||
let cacheReadTokens = 0
|
||||
|
|
@ -2898,7 +2901,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
await this.diffViewProvider.reset()
|
||||
|
||||
// Model routing: temporarily swap to light model if heuristics say so
|
||||
let primaryApiHandler: typeof this.api | undefined
|
||||
{
|
||||
const routingState = await this.providerRef.deref()?.getState()
|
||||
if (
|
||||
|
|
@ -3604,6 +3606,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
presentAssistantMessage(this)
|
||||
}
|
||||
|
||||
// Model routing: always restore primary API handler after streaming completes,
|
||||
// regardless of whether the turn had content or not. This prevents permanently
|
||||
// losing the primary model on empty-response retries or error paths.
|
||||
if (primaryApiHandler) {
|
||||
this.api = primaryApiHandler
|
||||
primaryApiHandler = undefined
|
||||
}
|
||||
|
||||
if (hasTextContent || hasToolUses) {
|
||||
// NOTE: This comment is here for future reference - this was a
|
||||
// workaround for `userMessageContent` not getting set to true.
|
||||
|
|
@ -3623,12 +3633,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
|
||||
await pWaitFor(() => this.userMessageContentReady)
|
||||
|
||||
// Model routing: end current turn and restore primary handler
|
||||
// Model routing: end current turn classification
|
||||
this.modelRouter.endTurn()
|
||||
if (primaryApiHandler) {
|
||||
this.api = primaryApiHandler
|
||||
primaryApiHandler = undefined
|
||||
}
|
||||
|
||||
// If the model did not tool use, then we need to tell it to
|
||||
// either use a tool or attempt_completion.
|
||||
|
|
@ -3770,6 +3776,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// If we reach here without continuing, return false (will always be false for now)
|
||||
return false
|
||||
} catch (error) {
|
||||
// Model routing: restore primary API handler on error paths
|
||||
if (primaryApiHandler) {
|
||||
this.api = primaryApiHandler
|
||||
primaryApiHandler = undefined
|
||||
}
|
||||
// This should never happen since the only thing that can throw an
|
||||
// error is the attemptApiRequest, which is wrapped in a try catch
|
||||
// that sends an ask where if noButtonClicked, will clear current
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { SearchableSetting } from "./SearchableSetting"
|
|||
import { ExperimentalFeature } from "./ExperimentalFeature"
|
||||
import { ImageGenerationSettings } from "./ImageGenerationSettings"
|
||||
import { CustomToolsSettings } from "./CustomToolsSettings"
|
||||
import { ModelRoutingSettings } from "./ModelRoutingSettings"
|
||||
|
||||
type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
||||
experiments: Experiments
|
||||
|
|
@ -26,6 +27,8 @@ type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
setImageGenerationProvider?: (provider: ImageGenerationProvider) => void
|
||||
setOpenRouterImageApiKey?: (apiKey: string) => void
|
||||
setImageGenerationSelectedModel?: (model: string) => void
|
||||
modelRoutingLightModelId?: string
|
||||
setModelRoutingLightModelId?: (modelId: string) => void
|
||||
}
|
||||
|
||||
export const ExperimentalSettings = ({
|
||||
|
|
@ -39,6 +42,8 @@ export const ExperimentalSettings = ({
|
|||
setImageGenerationProvider,
|
||||
setOpenRouterImageApiKey,
|
||||
setImageGenerationSelectedModel,
|
||||
modelRoutingLightModelId,
|
||||
setModelRoutingLightModelId,
|
||||
className,
|
||||
...props
|
||||
}: ExperimentalSettingsProps) => {
|
||||
|
|
@ -83,6 +88,24 @@ export const ExperimentalSettings = ({
|
|||
</SearchableSetting>
|
||||
)
|
||||
}
|
||||
if (config[0] === "MODEL_ROUTING" && setModelRoutingLightModelId) {
|
||||
return (
|
||||
<SearchableSetting
|
||||
key={config[0]}
|
||||
settingId={`experimental-${config[0].toLowerCase()}`}
|
||||
section="experimental"
|
||||
label={label}>
|
||||
<ModelRoutingSettings
|
||||
enabled={experiments[EXPERIMENT_IDS.MODEL_ROUTING] ?? false}
|
||||
onChange={(enabled) =>
|
||||
setExperimentEnabled(EXPERIMENT_IDS.MODEL_ROUTING, enabled)
|
||||
}
|
||||
modelRoutingLightModelId={modelRoutingLightModelId}
|
||||
setModelRoutingLightModelId={setModelRoutingLightModelId}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
)
|
||||
}
|
||||
if (config[0] === "CUSTOM_TOOLS") {
|
||||
return (
|
||||
<SearchableSetting
|
||||
|
|
|
|||
50
webview-ui/src/components/settings/ModelRoutingSettings.tsx
Normal file
50
webview-ui/src/components/settings/ModelRoutingSettings.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
|
||||
interface ModelRoutingSettingsProps {
|
||||
enabled: boolean
|
||||
onChange: (enabled: boolean) => void
|
||||
modelRoutingLightModelId: string | undefined
|
||||
setModelRoutingLightModelId: (modelId: string) => void
|
||||
}
|
||||
|
||||
export const ModelRoutingSettings = ({
|
||||
enabled,
|
||||
onChange,
|
||||
modelRoutingLightModelId,
|
||||
setModelRoutingLightModelId,
|
||||
}: ModelRoutingSettingsProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
|
||||
<span className="font-medium">{t("settings:experimental.MODEL_ROUTING.name")}</span>
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
<p className="text-vscode-descriptionForeground text-sm mt-0">
|
||||
{t("settings:experimental.MODEL_ROUTING.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<div className="ml-2 space-y-3">
|
||||
<div>
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:experimental.MODEL_ROUTING.lightModelIdLabel")}
|
||||
</label>
|
||||
<VSCodeTextField
|
||||
value={modelRoutingLightModelId ?? ""}
|
||||
placeholder={t("settings:experimental.MODEL_ROUTING.lightModelIdPlaceholder")}
|
||||
onInput={(e: any) => setModelRoutingLightModelId(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -207,6 +207,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
imageGenerationProvider,
|
||||
openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel,
|
||||
modelRoutingLightModelId,
|
||||
reasoningBlockCollapsed,
|
||||
enterBehavior,
|
||||
includeCurrentTime,
|
||||
|
|
@ -338,6 +339,16 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
})
|
||||
}, [])
|
||||
|
||||
const setModelRoutingLightModelId = useCallback((modelId: string) => {
|
||||
setCachedState((prevState) => {
|
||||
if (prevState.modelRoutingLightModelId !== modelId) {
|
||||
setChangeDetected(true)
|
||||
}
|
||||
|
||||
return { ...prevState, modelRoutingLightModelId: modelId }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setCustomSupportPromptsField = useCallback((prompts: Record<string, string | undefined>) => {
|
||||
setCachedState((prevState) => {
|
||||
const previousStr = JSON.stringify(prevState.customSupportPrompts)
|
||||
|
|
@ -422,6 +433,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
imageGenerationProvider,
|
||||
openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel,
|
||||
modelRoutingLightModelId,
|
||||
experiments,
|
||||
customSupportPrompts,
|
||||
},
|
||||
|
|
@ -927,6 +939,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
setImageGenerationProvider={setImageGenerationProvider}
|
||||
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
|
||||
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
|
||||
modelRoutingLightModelId={modelRoutingLightModelId}
|
||||
setModelRoutingLightModelId={setModelRoutingLightModelId}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -941,6 +941,12 @@
|
|||
"refreshSuccess": "Tools refreshed successfully",
|
||||
"refreshError": "Failed to refresh tools",
|
||||
"toolParameters": "Parameters"
|
||||
},
|
||||
"MODEL_ROUTING": {
|
||||
"name": "Enable model routing",
|
||||
"description": "When enabled, Roo dynamically routes API calls to a lighter (cheaper) model during information-gathering phases. The light model must be from the same provider as your primary model.",
|
||||
"lightModelIdLabel": "Light Model ID",
|
||||
"lightModelIdPlaceholder": "e.g. claude-3-haiku-20241022"
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue