mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-21 08:03:33 +00:00
* 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>
107 lines
3.9 KiB
Svelte
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}
|