mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
fix: report AI usage before tools run and price self usage consistently
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,7 @@
|
||||
import UserInfoSettings from './settings/UserInfoSettings.svelte'
|
||||
import AIUserSettings from './settings/AIUserSettings.svelte'
|
||||
import AiUsagePanel from './workspaceSettings/AiUsagePanel.svelte'
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { copilotInfo, copilotWorkspace } from '$lib/aiStore'
|
||||
import {
|
||||
getDarkModeVariant,
|
||||
setDarkModeVariant,
|
||||
@@ -108,9 +107,13 @@
|
||||
<AIUserSettings />
|
||||
</div>
|
||||
</div>
|
||||
{#if $workspaceStore}
|
||||
<!-- 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={$workspaceStore}
|
||||
workspace={$copilotWorkspace}
|
||||
modelPricing={$copilotInfo.modelPricing ?? {}}
|
||||
scope="self"
|
||||
/>
|
||||
|
||||
@@ -193,7 +193,7 @@ export async function parseAnthropicCompletion(
|
||||
tools: Tool<any>[],
|
||||
helpers: any,
|
||||
abortController?: AbortController,
|
||||
options?: { workspace?: string }
|
||||
options?: { workspace?: string; onTokenUsage?: (usage: ChatTokenUsage) => void }
|
||||
): Promise<ParsedCompletionResult> {
|
||||
let toolCallsToProcess: ChatCompletionMessageFunctionToolCall[] = []
|
||||
let error = null
|
||||
@@ -415,6 +415,7 @@ export async function parseAnthropicCompletion(
|
||||
|
||||
const finalMessage = await completion.finalMessage()
|
||||
const tokenUsage = anthropicUsageToChatTokenUsage(finalMessage.usage)
|
||||
options?.onTokenUsage?.(tokenUsage)
|
||||
|
||||
// Process tool calls if any
|
||||
if (toolCallsToProcess.length > 0) {
|
||||
|
||||
@@ -338,11 +338,18 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
// actually served it rather than to whatever is selected when the loop ends.
|
||||
let iterationModel: ReasoningProviderModel | undefined
|
||||
|
||||
const trackUsage = (usage: ChatTokenUsage | null | undefined) => {
|
||||
tokenUsage = addChatTokenUsage(tokenUsage, usage)
|
||||
// Reported as the provider's usage arrives, not when the parser returns: a parser
|
||||
// waits on tool execution, which can wait on a person, and a tab closed in that
|
||||
// gap would drop a response that was already billed. Accounting for the turn's
|
||||
// own totals stays on the return path, where every parser reports uniformly.
|
||||
const reportUsage = (usage: ChatTokenUsage | null | undefined) => {
|
||||
if (usage && iterationModel) {
|
||||
config.onUsage?.(usage, iterationModel)
|
||||
}
|
||||
}
|
||||
|
||||
const trackUsage = (usage: ChatTokenUsage | null | undefined) => {
|
||||
tokenUsage = addChatTokenUsage(tokenUsage, usage)
|
||||
// Some providers/paths report no usage (prompt 0); keep the last real one.
|
||||
if (usage && usage.prompt > 0) {
|
||||
lastIterationUsage = usage
|
||||
@@ -399,7 +406,14 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
...(pendingUserMessage ? [pendingUserMessage] : [])
|
||||
]
|
||||
const toolDefs = tools.map((t) => t.def)
|
||||
const parseOptions = { workspace, provider: modelProvider.provider }
|
||||
// Report each response as its usage arrives rather than after the parser
|
||||
// returns: a parser waits on tool execution, which can wait on a person, and
|
||||
// a tab closed in that gap would drop a response the provider already billed.
|
||||
const parseOptions = {
|
||||
workspace,
|
||||
provider: modelProvider.provider,
|
||||
onTokenUsage: reportUsage
|
||||
}
|
||||
|
||||
if (isOpenAI) {
|
||||
const reasoningSummaryCacheKey = getReasoningSummaryCacheKey(workspace, modelProvider)
|
||||
|
||||
@@ -392,7 +392,7 @@ export async function parseOpenAIResponsesCompletion(
|
||||
addedMessages: ChatCompletionMessageParam[],
|
||||
tools: Tool<any>[],
|
||||
helpers: any,
|
||||
options?: { workspace?: string }
|
||||
options?: { workspace?: string; onTokenUsage?: (usage: ChatTokenUsage) => void }
|
||||
): Promise<ParsedCompletionResult> {
|
||||
let toolCallsToProcess: ChatCompletionMessageFunctionToolCall[] = []
|
||||
let error: OpenAIError | ResponseErrorEvent | null = null
|
||||
@@ -566,6 +566,7 @@ export async function parseOpenAIResponsesCompletion(
|
||||
|
||||
const finalResponse = await runner.finalResponse()
|
||||
const tokenUsage = openAIResponsesUsageToChatTokenUsage(finalResponse.usage)
|
||||
options?.onTokenUsage?.(tokenUsage)
|
||||
|
||||
for (const item of finalResponse.output ?? []) {
|
||||
if (item.type === 'web_search_call' && !surfacedWebSearchCalls.has(item.id)) {
|
||||
|
||||
@@ -1162,7 +1162,11 @@ export async function parseOpenAICompletion(
|
||||
tools: Tool<any>[],
|
||||
helpers: any,
|
||||
_abortController?: AbortController, // unused, for signature compatibility with parseAnthropicCompletion
|
||||
options?: { workspace?: string; provider?: string }
|
||||
options?: {
|
||||
workspace?: string
|
||||
provider?: string
|
||||
onTokenUsage?: (usage: ChatTokenUsage) => void
|
||||
}
|
||||
): Promise<{ shouldContinue: boolean; tokenUsage: ChatTokenUsage }> {
|
||||
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
|
||||
// The tool call currently receiving argument deltas; when the stream moves on
|
||||
@@ -1312,6 +1316,7 @@ export async function parseOpenAICompletion(
|
||||
|
||||
callbacks.onMessageEnd()
|
||||
|
||||
options?.onTokenUsage?.(tokenUsage)
|
||||
// Stream over: every parsed call is queued until its turn in processToolCall.
|
||||
for (const toolCall of Object.values(finalToolCalls)) {
|
||||
if (toolCall.id) {
|
||||
|
||||
@@ -96,8 +96,8 @@ const MODEL_PRICES: [name: string, price: PriceEntry | null][] = [
|
||||
// one, so the write rate never applies. The -mini/-nano entries must precede
|
||||
// the family entry, which would otherwise claim them.
|
||||
// Revisions past gpt-5 are priced separately by OpenAI and are not tracked here.
|
||||
// `strictVariants` does not cover these: a version bump is digits, not a name
|
||||
// segment, so `gpt-5-6` would otherwise still match `gpt-5`.
|
||||
// The matcher's revision guard already keeps them off the family rate; these
|
||||
// entries stay so a revision the guard admits still resolves to no rate.
|
||||
['gpt-5.6', null],
|
||||
['gpt-5.5', null],
|
||||
['gpt-5.4', null],
|
||||
|
||||
Reference in New Issue
Block a user