Files
windmill/frontend/src/lib/components/AIReasoningEffortPicker.svelte
T
GuilhemandClaude Fable 5.1 189793c2e4 feat: flow chat model picker on a shared model-settings component (#11187)
* refactor: render the session chat model menu from a shared ChatModelSettings config

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat: pick the flow chat's model and thinking from the provider fields the flow exposes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: name only the thinking level the flow run will send on the model button

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: let the flow chat take a typed model id and keep a shared thinking input editable

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: promote a flow input to the model button only where its control can edit it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop any reasoning token the chosen model rejects before a flow chat run

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 09:59:05 +02:00

107 lines
3.9 KiB
Svelte

<script lang="ts">
import { Brain, ChevronDown } from 'lucide-svelte'
import DropdownV2 from './DropdownV2.svelte'
import Button from './common/button/Button.svelte'
import type { Item } from '$lib/utils'
import type { AIProvider } from '$lib/gen'
import { getReasoningCapability, explicitOffToken } from './copilot/reasoningRegistry'
interface Props {
// The provider-native reasoning token sent to the model (e.g. `high`,
// `none`), or undefined to leave the provider default untouched.
value: string | undefined
// The selected provider config; only `kind` and `model` are read.
providerConfig: { kind?: AIProvider; model?: string } | string | undefined
disabled?: boolean
}
let { value = $bindable(), providerConfig, disabled = false }: Props = $props()
let provider = $derived(
typeof providerConfig === 'object'
? (providerConfig?.kind as AIProvider | undefined)
: undefined
)
let model = $derived(typeof providerConfig === 'object' ? providerConfig?.model : undefined)
let capability = $derived(
provider && model
? getReasoningCapability(provider, model)
: { supported: false, levels: [], canDisable: false, known: false }
)
// The token that turns reasoning off on a model that reasons by default
// (e.g. Gemini/OpenAI 'none'). Undefined means omission already disables it,
// so leaving the effort unset is itself off.
let offToken = $derived(provider && model ? explicitOffToken(provider, model) : undefined)
// When the model reasons only on request (Anthropic/Bedrock), an unset effort
// already means off, so the default choice is labelled "off". Models that
// reason by default keep a distinct "Model default" plus an explicit off.
let offViaOmission = $derived(capability.canDisable && offToken === undefined)
// Label shown on the trigger for the current selection.
let currentLabel = $derived(
!value ? (offViaOmission ? 'off' : 'Model default') : value === offToken ? 'off' : value
)
let items = $derived.by((): Item[] => {
const opts: Item[] = [
{
displayName: offViaOmission ? 'off' : 'Model default',
selected: !value,
action: () => (value = undefined)
}
]
if (capability.canDisable && offToken !== undefined) {
opts.push({
displayName: 'off',
selected: value === offToken,
action: () => (value = offToken)
})
}
for (const level of capability.levels) {
opts.push({ displayName: level, selected: value === level, action: () => (value = level) })
}
return opts
})
// Clear a stale selection once the model no longer accepts it — either it
// can't reason at all, or the token isn't a valid level/off for this model
// (e.g. carrying `xhigh` from Opus onto a model that tops out at `high`).
// Sending an unsupported token would be rejected by the provider.
$effect(() => {
if (!provider || !model || value === undefined) return
// Valid values are exactly the selectable options: the levels, plus the
// explicit off token only when this model can actually disable reasoning.
const valid =
capability.supported &&
(capability.levels.includes(value) ||
(capability.canDisable && offToken !== undefined && value === offToken))
if (!valid) value = undefined
})
</script>
{#if !provider || !model}
<div class="text-xs text-tertiary">Select a model to configure reasoning effort.</div>
{:else if !capability.supported}
<div class="text-xs text-tertiary">The selected model does not support reasoning effort.</div>
{:else}
<DropdownV2 {items} {disabled} placement="bottom-start" fixedHeight={false} class="justify-start">
{#snippet buttonReplacement()}
<Button
nonCaptureEvent
{disabled}
unifiedSize="xs"
variant="default"
startIcon={{ icon: Brain }}
endIcon={{ icon: ChevronDown }}
btnClasses="w-full justify-between font-normal text-secondary"
title="Reasoning effort"
>
<span class="truncate">{currentLabel}</span>
</Button>
{/snippet}
</DropdownV2>
{/if}