Merge remote-tracking branch 'origin/dev' into kai/review/pr-1655-retry-followup

This commit is contained in:
Tam Nhu Tran committed 2026-07-22 15:08:11 -04:00
commit 2d909a162f
43 files changed
+953 -218

No files matched your search

@@ -122,6 +122,7 @@ export function ProviderEditor({
opusModel,
sonnetModel,
haikuModel,
fableModel,
extendedContextEnabled,
toggleExtendedContext,
handleRawJsonChange,
@@ -271,6 +272,7 @@ export function ProviderEditor({
opusModel={opusModel}
sonnetModel={sonnetModel}
haikuModel={haikuModel}
fableModel={fableModel}
providerModels={providerModels}
routing={routing}
extendedContextEnabled={extendedContextEnabled}
@@ -43,6 +43,7 @@ export function ModelConfigSection({
opusModel,
sonnetModel,
haikuModel,
fableModel,
providerModels,
routing,
provider,
@@ -67,7 +68,7 @@ export function ModelConfigSection({
const extendedContextModels = useMemo(() => {
if (!catalog) return [];
const selectedModels = [currentModel, opusModel, sonnetModel, haikuModel]
const selectedModels = [currentModel, opusModel, sonnetModel, haikuModel, fableModel]
.filter((modelId): modelId is string => Boolean(modelId))
.map((modelId) => stripExtendedContextSuffix(modelId));
@@ -75,7 +76,7 @@ export function ModelConfigSection({
return uniqueIds
.map((modelId) => findCatalogModel(catalog.provider, modelId, catalog))
.filter((model): model is NonNullable<typeof model> => Boolean(model?.extendedContext));
}, [catalog, currentModel, opusModel, sonnetModel, haikuModel]);
}, [catalog, currentModel, opusModel, sonnetModel, haikuModel, fableModel]);
const resolvedCatalogModels = useMemo(
() => getResolvedCatalogModels(catalog, providerModels),
@@ -253,6 +254,15 @@ export function ModelConfigSection({
onToggle={onExtendedContextToggle}
/>
)}
<FlexibleModelSelector
label="Fable (Frontier)"
description="Maps the fable subagent tier; leave empty to keep the native Claude Fable model"
value={fableModel}
onChange={(model) => onUpdateEnvValue('ANTHROPIC_DEFAULT_FABLE_MODEL', model)}
catalog={catalog}
allModels={providerModels}
routing={routing}
/>
<FlexibleModelSelector
label="Opus (Most capable)"
description="For complex reasoning tasks"
@@ -27,6 +27,7 @@ interface ModelConfigTabProps {
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;
fableModel?: string;
providerModels: Array<{ id: string; owned_by: string }>;
routing?: CliproxyProviderRoutingHints;
/** Whether extended context (1M tokens) is enabled */
@@ -72,6 +73,7 @@ export function ModelConfigTab({
opusModel,
sonnetModel,
haikuModel,
fableModel,
providerModels,
routing,
extendedContextEnabled,
@@ -163,6 +165,7 @@ export function ModelConfigTab({
opusModel={opusModel}
sonnetModel={sonnetModel}
haikuModel={haikuModel}
fableModel={fableModel}
providerModels={providerModels}
routing={routing}
provider={provider}
@@ -126,6 +126,7 @@ export interface ModelConfigSectionProps {
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;
fableModel?: string;
providerModels: Array<{ id: string; owned_by: string }>;
routing?: CliproxyProviderRoutingHints;
/** Provider name for display */
@@ -154,6 +155,7 @@ export interface UseProviderEditorReturn {
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;
fableModel?: string;
/** Whether extended context (1M tokens) is enabled */
extendedContextEnabled: boolean;
/** Toggle extended context on/off */
@@ -85,6 +85,7 @@ export function useProviderEditor(
const opusModel = currentSettings?.env?.ANTHROPIC_DEFAULT_OPUS_MODEL;
const sonnetModel = currentSettings?.env?.ANTHROPIC_DEFAULT_SONNET_MODEL;
const haikuModel = currentSettings?.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL;
const fableModel = currentSettings?.env?.ANTHROPIC_DEFAULT_FABLE_MODEL;
// Extended context is enabled if any model has [1m] suffix
const extendedContextEnabled = useMemo(() => {
@@ -224,6 +225,7 @@ export function useProviderEditor(
opusModel,
sonnetModel,
haikuModel,
fableModel,
extendedContextEnabled,
toggleExtendedContext,
handleRawJsonChange,
@@ -10,16 +10,31 @@ import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { SearchableSelect } from '@/components/ui/searchable-select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { Switch } from '@/components/ui/switch';
import type { CliproxyProviderRoutingHints } from '@/lib/api-client';
import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort';
import {
applyCodexServiceTierSuffix,
getCodexEffortDisplay,
getCodexEffortVariants,
parseCodexEffort,
parseCodexServiceTier,
stripCodexEffortSuffix,
} from '@/lib/codex-effort';
import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs';
import {
findCatalogModel,
getResolvedCatalogModels,
getSupplementalCatalogModels,
} from '@/lib/model-catalogs';
import { getReasoningAdapter } from '@/lib/reasoning-control';
import { cn } from '@/lib/utils';
/** Model entry from catalog */
@@ -47,6 +62,8 @@ export interface ModelEntry {
codexEfforts?: CodexEffort[];
/** Additional Codex service-tier suffixes supported by this model in the dashboard UI. */
codexServiceTiers?: CodexServiceTier[];
/** Reasoning-level suffixes this model supports via paren syntax in the dashboard UI. */
reasoningLevels?: string[];
}
/** Provider catalog */
@@ -561,6 +578,34 @@ export function FlexibleModelSelector({
}
: null;
const hasAvailableModels = recommendedOptions.length + allModelOptions.length > 0;
const reasoningAdapter = getReasoningAdapter(catalog?.provider);
const currentReasoningLevel = reasoningAdapter?.parse(value);
const selectableReasoningLevels =
reasoningAdapter && catalog
? reasoningAdapter.getOptions(
reasoningAdapter.strip(normalizeModelValue(value, routing)),
catalog
)
: null;
const reasoningOptions =
selectableReasoningLevels &&
currentReasoningLevel &&
!selectableReasoningLevels.includes(currentReasoningLevel)
? [...selectableReasoningLevels, currentReasoningLevel]
: selectableReasoningLevels;
const currentServiceTier = isCodexProvider ? parseCodexServiceTier(value) : undefined;
const selectedCodexBaseModel =
isCodexProvider && catalog && value
? findCatalogModel(
catalog.provider,
stripCodexEffortSuffix(normalizeModelValue(value, routing)),
catalog
)
: undefined;
const showFastToggle =
isCodexProvider &&
(selectedCodexBaseModel?.codexServiceTiers?.includes('fast') === true ||
currentServiceTier === 'fast');
return (
<div className="space-y-1.5">
@@ -568,87 +613,129 @@ export function FlexibleModelSelector({
<label className="text-xs font-medium">{label}</label>
{description && <p className="text-[10px] text-muted-foreground">{description}</p>}
</div>
<SearchableSelect
value={value || undefined}
onChange={onChange}
disabled={disabled}
placeholder={t('providerModelSelector.selectModel')}
searchPlaceholder={t('searchableSelect.searchModels')}
emptyText={
hasAvailableModels
? t('searchableSelect.noResults')
: t('providerModelSelector.noModelsAvailable')
}
triggerClassName="h-9"
createOption={createCustomModelOption}
groups={[
...(selectedValueMissing && legacySelectedOption
? [
{
key: 'current',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.currentValue')}
</span>
),
},
]
: []),
{
key: 'recommended',
label: (
<span className="text-xs text-primary">{t('providerModelSelector.recommended')}</span>
),
},
...(isCodexProvider
? [
{
key: 'codex-reasoning',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.codexReasoningVariants')}
</span>
),
},
{
key: 'codex-fast',
label: (
<span className="text-xs text-amber-600">
{t('providerModelSelector.codexFastVariants')}
</span>
),
},
]
: []),
...(allModelOptions.length > 0
? [
{
key: 'all',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.allModelsCount', {
count: allModelOptions.length,
})}
</span>
),
},
]
: []),
{
key: 'custom',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.customModel')}
</span>
),
},
]}
options={[
...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
...recommendedOptions,
...allModelOptions,
]}
/>
<div className="flex gap-2">
<SearchableSelect
value={value || undefined}
onChange={onChange}
disabled={disabled}
placeholder={t('providerModelSelector.selectModel')}
searchPlaceholder={t('searchableSelect.searchModels')}
emptyText={
hasAvailableModels
? t('searchableSelect.noResults')
: t('providerModelSelector.noModelsAvailable')
}
className="min-w-0 flex-1"
triggerClassName="h-9"
createOption={createCustomModelOption}
groups={[
...(selectedValueMissing && legacySelectedOption
? [
{
key: 'current',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.currentValue')}
</span>
),
},
]
: []),
{
key: 'recommended',
label: (
<span className="text-xs text-primary">
{t('providerModelSelector.recommended')}
</span>
),
},
...(isCodexProvider
? [
{
key: 'codex-reasoning',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.codexReasoningVariants')}
</span>
),
},
{
key: 'codex-fast',
label: (
<span className="text-xs text-amber-600">
{t('providerModelSelector.codexFastVariants')}
</span>
),
},
]
: []),
...(allModelOptions.length > 0
? [
{
key: 'all',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.allModelsCount', {
count: allModelOptions.length,
})}
</span>
),
},
]
: []),
{
key: 'custom',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.customModel')}
</span>
),
},
]}
options={[
...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []),
...recommendedOptions,
...allModelOptions,
]}
/>
{reasoningAdapter && reasoningOptions && (
<Select
value={currentReasoningLevel ?? 'auto'}
onValueChange={(level) =>
onChange(reasoningAdapter.apply(value, level === 'auto' ? undefined : level))
}
disabled={disabled || !value}
>
<SelectTrigger
className="h-9 w-28 shrink-0"
aria-label={t('providerModelSelector.effortLabel')}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">{t('providerModelSelector.effortAuto')}</SelectItem>
{reasoningOptions.map((level) => (
<SelectItem key={level} value={level}>
{level}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{showFastToggle && (
<label className="flex h-9 shrink-0 items-center gap-1.5">
<Switch
checked={currentServiceTier === 'fast'}
onCheckedChange={(checked) => onChange(applyCodexServiceTierSuffix(value, checked))}
disabled={disabled || !value}
aria-label={t('providerModelSelector.fastTier')}
/>
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.fastTier')}
</span>
</label>
)}
</div>
{selectedRoutingHint ? (
<div
className={cn(