Files
windmill/frontend/src/lib/components/UserSettings.svelte
T
hugocasaandClaude Opus 5 b6e059116a feat: track token cost in AI sessions and chats (#10688)
* feat: track token cost in AI sessions and chats

* fix: address review findings on AI cost tracking

* fix: price inherited and overridden models at their real rates

* fix: stop newer model revisions inheriting an older price

* fix: stop a sub-model inheriting its family's price

* fix: keep alias suffixes resolving to their model's price

* fix: count OpenRouter cache writes and drop unverifiable rates

* refactor: move AI spend out of the chat into workspace and user settings

* fix: pin the usage workspace per turn and stop inventing cache rates

* fix: leave Sonnet 5 unpriced while its promotional rate runs

* docs: record the new table in the schema summary and tighten comments

* fix: mark estimated AI costs with ~ and drop session grouping

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

* fix: name the workspace in the self-scoped AI usage title

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

* docs: state that overrides never replace a provider-returned cost

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

* fix: let a cleared cache rate inherit again and flag partial totals

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

* fix: clear a refused rate's error when the input snaps back

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

* fix: stop a revision variant inheriting its base family's rate

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

* fix: report AI usage before tools run and price self usage consistently

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

* fix: key pricing rows on the model id usage is reported under

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

* fix: surface Bedrock and Gemini usage the chat proxy was dropping

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

* fix: count Gemini tool-use prompt tokens as input

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

* feat: price flat-rate Gemini Flash models

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

* docs: state the tool-use token invariant once

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 11:08:13 +02:00

136 lines
4.0 KiB
Svelte

<script lang="ts">
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import Version from './Version.svelte'
import DarkModeToggle from './sidebar/DarkModeToggle.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import TokensTable from './settings/TokensTable.svelte'
import { createEventDispatcher } from 'svelte'
import UserInfoSettings from './settings/UserInfoSettings.svelte'
import AIUserSettings from './settings/AIUserSettings.svelte'
import AiUsagePanel from './workspaceSettings/AiUsagePanel.svelte'
import { copilotInfo, copilotWorkspace } from '$lib/aiStore'
import {
getDarkModeVariant,
setDarkModeVariant,
type DarkModeVariant
} from '$lib/darkModeVariant'
interface Props {
scopes?: string[] | undefined
newTokenLabel?: string | undefined
newTokenWorkspace?: string | undefined
newToken?: string | undefined
showMcpMode?: boolean
disableChatOffset?: boolean
}
let {
scopes = undefined,
newTokenLabel = undefined,
newTokenWorkspace = undefined,
newToken = $bindable(undefined),
showMcpMode = false,
disableChatOffset = false
}: Props = $props()
let drawer: Drawer | undefined = $state()
let openWithMcpMode = $state(false)
let darkVariant = $state<DarkModeVariant>(getDarkModeVariant())
const dispatch = createEventDispatcher()
export function openDrawer(mcpMode: boolean = false) {
openWithMcpMode = mcpMode
drawer?.openDrawer()
}
export function closeDrawer() {
drawer?.closeDrawer()
removeHash()
}
function removeHash() {
window.location.hash = ''
}
function handleTokenCreated(token: string) {
newToken = token
dispatch('tokenCreated', token)
}
</script>
<Drawer
bind:this={drawer}
size="900px"
on:open={() => (darkVariant = getDarkModeVariant())}
on:close={removeHash}
{disableChatOffset}
>
<DrawerContent title="User settings" on:close={closeDrawer}>
<div class="flex flex-col gap-6 pb-8">
{#if scopes == undefined}
<div
class="flex flex-row justify-between items-start gap-2 border border-border-light p-4 rounded-md"
>
<div class="flex flex-col gap-2">
<div class="font-semibold text-emphasis text-xs flex items-center">
Theme <DarkModeToggle forcedDarkMode={false} />
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-secondary">Dark variant</span>
<ToggleButtonGroup
selected={darkVariant}
class="w-fit"
onSelected={(v) => {
darkVariant = v
setDarkModeVariant(v)
}}
>
{#snippet children({ item })}
<ToggleButton value="default" label="Default" size="sm" {item} />
<ToggleButton value="github" label="GitHub" size="sm" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
</div>
<div class="text-xs text-emphasis flex-col flex">
Windmill <Version />
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 w-full gap-4">
<div class="min-w-0">
<UserInfoSettings />
</div>
<div class="min-w-0">
<AIUserSettings />
</div>
</div>
<!-- Keyed on the workspace `copilotInfo` reflects, not on `$workspaceStore`:
a session acting on another workspace loads that workspace's AI config
while navigation stays put, and pricing usage from one workspace with
another's rates would silently misstate it. -->
{#if $copilotWorkspace}
<AiUsagePanel
workspace={$copilotWorkspace}
modelPricing={$copilotInfo.modelPricing ?? {}}
scope="self"
/>
{/if}
{/if}
<div class="grow min-h-0">
<TokensTable
{showMcpMode}
{openWithMcpMode}
defaultNewTokenLabel={newTokenLabel}
defaultNewTokenWorkspace={newTokenWorkspace}
{scopes}
onTokenCreated={handleTokenCreated}
/>
</div>
</div>
</DrawerContent>
</Drawer>