mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 00:03:07 +00:00
feat(frontend): show AI chat context-size usage and fix trim gate
Surface the AI chat's current context-window usage as a "45k / 200k" badge next to the model selector, and make the conversation-trim gate accurate. The provider-reported token usage was already computed end-to-end but dropped. Capture the final loop iteration's usage (prompt + completion) as an accurate "current context size" anchor, persist it per chat in IndexedDB, and display it. Re-base the trim gate on the same anchor: the crude chars-per-4 estimate now includes the system message and tool schemas (previously ignored, which let real context overflow the model window), and is calibrated against the accurate anchor when one exists. The badge colour shifts neutral->amber->red as usage approaches the threshold where older messages start being dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
92c21bbe65
commit
1ccf2b7e72
@@ -32,8 +32,10 @@
|
||||
import Tooltip from '$lib/components/meltComponents/Tooltip.svelte'
|
||||
import Markdown from 'svelte-exmarkdown'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { AIAutonomyMode, AIMode } from './AIChatManager.svelte'
|
||||
import { AIAutonomyMode, AIMode, getTrimThreshold } from './AIChatManager.svelte'
|
||||
import { getAiChatManager } from './aiChatManagerContext'
|
||||
import { getModelContextWindow } from '../lib'
|
||||
import { tryGetCurrentModel } from '$lib/aiStore'
|
||||
import ChatTypingIndicator from './ChatTypingIndicator.svelte'
|
||||
import AIChatInput from './AIChatInput.svelte'
|
||||
import { getModifierKey } from '$lib/utils'
|
||||
@@ -288,6 +290,34 @@
|
||||
return aiChatManager.appAiChatHelpers.getSelectedContext()
|
||||
})
|
||||
|
||||
function formatTokenCount(n: number): string {
|
||||
if (n < 1000) return `${Math.round(n)}`
|
||||
const k = n / 1000
|
||||
if (k < 1000) return `${k < 10 ? k.toFixed(1) : Math.round(k)}k`
|
||||
const m = n / 1_000_000
|
||||
return `${m < 10 ? m.toFixed(1) : Math.round(m)}M`
|
||||
}
|
||||
|
||||
// Accurate "current context size" badge. Hidden until a real provider-reported count
|
||||
// exists (fresh chat, or a chat saved before this feature). Colour shifts as the
|
||||
// context fills toward the threshold where older messages start being dropped.
|
||||
const contextUsage = $derived.by(() => {
|
||||
const tokens = aiChatManager.contextTokens
|
||||
if (tokens == null) return undefined
|
||||
const window = getModelContextWindow(tryGetCurrentModel()?.model ?? '')
|
||||
const threshold = getTrimThreshold(window)
|
||||
const colorClass =
|
||||
tokens >= threshold
|
||||
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||
: tokens >= threshold * 0.8
|
||||
? 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300'
|
||||
: 'bg-surface-secondary text-tertiary'
|
||||
return {
|
||||
label: `${formatTokenCount(tokens)} / ${formatTokenCount(window)}`,
|
||||
colorClass
|
||||
}
|
||||
})
|
||||
|
||||
const yoloBypassedTools = $derived.by(() => {
|
||||
return aiChatManager.tools
|
||||
.filter((tool) => tool.requiresConfirmation === true)
|
||||
@@ -643,6 +673,18 @@
|
||||
{/if}
|
||||
<ProviderModelSelector />
|
||||
|
||||
{#if contextUsage}
|
||||
<div
|
||||
class={twMerge(
|
||||
'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-2xs tabular-nums',
|
||||
contextUsage.colorClass
|
||||
)}
|
||||
title="Context used — older messages are dropped as this fills"
|
||||
>
|
||||
{contextUsage.label}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if aiChatManager.mode === AIMode.APP && appContext && (appContext.inspectorElement || appContext.codeSelection)}
|
||||
{#if appContext.inspectorElement}
|
||||
<div
|
||||
|
||||
@@ -70,8 +70,18 @@ import {
|
||||
import { isGlobalAiEnabled } from './global/gate'
|
||||
|
||||
// If the estimated token usage is greater than the model context window - the threshold, we delete the oldest message
|
||||
const MAX_TOKENS_THRESHOLD_PERCENTAGE = 0.05
|
||||
const MAX_TOKENS_HARD_LIMIT = 5000
|
||||
export const MAX_TOKENS_THRESHOLD_PERCENTAGE = 0.05
|
||||
export const MAX_TOKENS_HARD_LIMIT = 5000
|
||||
|
||||
// The token count above which the oldest messages start being dropped for a given
|
||||
// context window. Shared with the UI so the context-size badge can colour itself to
|
||||
// match when trimming actually kicks in.
|
||||
export function getTrimThreshold(modelContextWindow: number): number {
|
||||
return (
|
||||
modelContextWindow -
|
||||
Math.max(modelContextWindow * MAX_TOKENS_THRESHOLD_PERCENTAGE, MAX_TOKENS_HARD_LIMIT)
|
||||
)
|
||||
}
|
||||
const AI_AUTONOMY_MODE_STORAGE_KEY = 'ai-chat-autonomy-mode'
|
||||
const LEGACY_AUTO_ACCEPT_TOOL_CONFIRMATIONS_STORAGE_KEY = 'ai-chat-yolo-mode'
|
||||
|
||||
@@ -169,6 +179,14 @@ export class AIChatManager {
|
||||
currentReply = $state<string>('')
|
||||
displayMessages = $state<DisplayMessage[]>([])
|
||||
messages = $state<ChatCompletionMessageParam[]>([])
|
||||
// Accurate "current context size" of the conversation, taken from the provider's
|
||||
// reported usage on the last turn's final iteration (prompt + completion).
|
||||
// Undefined until a turn completes (or a chat saved before this existed is restored).
|
||||
// Doubles as the trim gate's accurate baseline (see checkTokenUsageOverLimit).
|
||||
contextTokens = $state<number | undefined>(undefined)
|
||||
// Crude chars÷4 estimate measured over the message set at the moment contextTokens
|
||||
// was last set. Used to calibrate the crude estimator against the real API count.
|
||||
#anchorCrude: number | undefined = undefined
|
||||
autonomyMode = $state<AIAutonomyMode>(getPersistedAutonomyMode())
|
||||
autoAcceptEditsAvailable = $derived(supportsAutoAcceptEdits(this.mode))
|
||||
autoAcceptEditsActive = $derived(
|
||||
@@ -239,27 +257,44 @@ export class AIChatManager {
|
||||
|
||||
open = $derived(chatState.size > 0)
|
||||
|
||||
checkTokenUsageOverLimit = (messages: ChatCompletionMessageParam[]) => {
|
||||
const estimatedTokens = messages.reduce((acc, message) => {
|
||||
// one token is ~ 4 characters
|
||||
const tokenPerCharacter = 4
|
||||
// handle content
|
||||
// Crude chars÷4 estimate of the tokens a request carries: every message's content +
|
||||
// tool_calls, PLUS the system message and tool schemas (which the API counts but the
|
||||
// per-message loop above used to ignore — the source of context overflows in agentic mode).
|
||||
#crudeEstimate = (messages: ChatCompletionMessageParam[]) => {
|
||||
const tokenPerCharacter = 4
|
||||
let estimate = messages.reduce((acc, message) => {
|
||||
if (message.content) {
|
||||
acc += message.content.length / tokenPerCharacter
|
||||
}
|
||||
// Handle tool calls
|
||||
if (message.role === 'assistant' && message.tool_calls) {
|
||||
acc += JSON.stringify(message.tool_calls).length / tokenPerCharacter
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
const model = getCurrentModel()
|
||||
const modelContextWindow = getModelContextWindow(model.model)
|
||||
return (
|
||||
estimatedTokens >
|
||||
modelContextWindow -
|
||||
Math.max(modelContextWindow * MAX_TOKENS_THRESHOLD_PERCENTAGE, MAX_TOKENS_HARD_LIMIT)
|
||||
)
|
||||
estimate += (this.systemMessage?.content?.length ?? 0) / tokenPerCharacter
|
||||
if (this.tools.length > 0) {
|
||||
estimate += JSON.stringify(this.tools.map((t) => t.def)).length / tokenPerCharacter
|
||||
}
|
||||
return estimate
|
||||
}
|
||||
|
||||
// Best estimate of the context size for a given message set. When we have a real
|
||||
// provider-reported anchor (contextTokens) we scale the crude estimate by the
|
||||
// anchor/crude ratio measured at anchor time — so the figure is accurate at the
|
||||
// anchor and shrinks proportionally as messages are dropped during trimming.
|
||||
// Falls back to the crude estimate (which now includes system + tools) on the first
|
||||
// turn or for chats restored without a stored count.
|
||||
#estimateContextTokens = (messages: ChatCompletionMessageParam[]) => {
|
||||
const crude = this.#crudeEstimate(messages)
|
||||
if (this.contextTokens != null && this.#anchorCrude && this.#anchorCrude > 0) {
|
||||
return crude * (this.contextTokens / this.#anchorCrude)
|
||||
}
|
||||
return crude
|
||||
}
|
||||
|
||||
checkTokenUsageOverLimit = (messages: ChatCompletionMessageParam[]) => {
|
||||
const modelContextWindow = getModelContextWindow(tryGetCurrentModel()?.model ?? '')
|
||||
return this.#estimateContextTokens(messages) > getTrimThreshold(modelContextWindow)
|
||||
}
|
||||
|
||||
deleteOldestMessage = (messages: ChatCompletionMessageParam[], maxDepth: number = 10) => {
|
||||
@@ -519,8 +554,7 @@ export class AIChatManager {
|
||||
this.tools = globalToolsFor({ sessionPreview: this.isSessionChat })
|
||||
this.helpers = {
|
||||
...(this.isSessionChat ? { sessionId: this.sessionId } : {}),
|
||||
testActiveFlow: async (args?: Record<string, any>) =>
|
||||
this.flowAiChatHelpers?.testFlow(args)
|
||||
testActiveFlow: async (args?: Record<string, any>) => this.flowAiChatHelpers?.testFlow(args)
|
||||
} satisfies GlobalToolHelpers
|
||||
} else if (mode === AIMode.APP) {
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
@@ -721,6 +755,14 @@ export class AIChatManager {
|
||||
}
|
||||
}
|
||||
})
|
||||
// Anchor the accurate context size on the provider-reported usage of the final
|
||||
// iteration (prompt already holds the fully-grown context; completion is the
|
||||
// final reply). Record the crude estimate over the same message set so the trim
|
||||
// gate can calibrate against this anchor.
|
||||
if (result.lastIterationUsage?.total) {
|
||||
this.contextTokens = result.lastIterationUsage.total
|
||||
this.#anchorCrude = this.#crudeEstimate([...messages, ...result.addedMessages])
|
||||
}
|
||||
return result.addedMessages
|
||||
} catch (err) {
|
||||
console.log('chatRequest error', err)
|
||||
@@ -966,7 +1008,7 @@ export class AIChatManager {
|
||||
}
|
||||
|
||||
this.messages.push(userMessage)
|
||||
await this.historyManager.saveChat(this.displayMessages, this.messages)
|
||||
await this.historyManager.saveChat(this.displayMessages, this.messages, this.contextTokens)
|
||||
|
||||
this.currentReply = ''
|
||||
|
||||
@@ -1054,7 +1096,7 @@ export class AIChatManager {
|
||||
if (this.autoAcceptEditsActive) {
|
||||
this.acceptPendingFlowEdits()
|
||||
}
|
||||
await this.historyManager.saveChat(this.displayMessages, this.messages)
|
||||
await this.historyManager.saveChat(this.displayMessages, this.messages, this.contextTokens)
|
||||
if (isFirstUserTurn && this.afterFirstTurnSaved) {
|
||||
void Promise.resolve(this.afterFirstTurnSaved()).catch((e) => {
|
||||
console.error('AIChatManager afterFirstTurnSaved hook failed', e)
|
||||
@@ -1152,9 +1194,11 @@ export class AIChatManager {
|
||||
|
||||
saveAndClear = async () => {
|
||||
this.cancel('saveAndClear')
|
||||
await this.historyManager.save(this.displayMessages, this.messages)
|
||||
await this.historyManager.save(this.displayMessages, this.messages, this.contextTokens)
|
||||
this.displayMessages = []
|
||||
this.messages = []
|
||||
this.contextTokens = undefined
|
||||
this.#anchorCrude = undefined
|
||||
}
|
||||
|
||||
loadPastChat = async (id: string) => {
|
||||
@@ -1162,6 +1206,11 @@ export class AIChatManager {
|
||||
if (chat) {
|
||||
this.displayMessages = chat.displayMessages
|
||||
this.messages = chat.actualMessages
|
||||
this.contextTokens = chat.contextTokens
|
||||
// Re-derive the calibration baseline so the trim gate is accurate on the next
|
||||
// check even before a new turn. Undefined for chats saved before this existed.
|
||||
this.#anchorCrude =
|
||||
chat.contextTokens != null ? this.#crudeEstimate(chat.actualMessages) : undefined
|
||||
this.#automaticScroll = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,3 +217,72 @@ describe('AIChatManager persisted autonomy default', () => {
|
||||
expect(new AIChatManager().autonomyMode).toBe(AIAutonomyMode.DEFAULT)
|
||||
})
|
||||
})
|
||||
|
||||
describe('AIChatManager context trim gate', () => {
|
||||
// Mocked window is 128000; threshold = 128000 - max(128000*0.05, 5000) = 121600 tokens.
|
||||
// chars÷4, so ~486400 chars to exceed.
|
||||
const big = (chars: number) => 'x'.repeat(chars)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('counts the system message in the estimate (closes the overflow blind spot)', () => {
|
||||
const manager = new AIChatManager()
|
||||
const messages = [{ role: 'user', content: 'hi' }] as any
|
||||
|
||||
// Small messages alone are well under the limit...
|
||||
expect(manager.checkTokenUsageOverLimit(messages)).toBe(false)
|
||||
|
||||
// ...but a large system message must push it over (the old gate ignored it).
|
||||
manager.systemMessage = { role: 'system', content: big(520_000) }
|
||||
expect(manager.checkTokenUsageOverLimit(messages)).toBe(true)
|
||||
})
|
||||
|
||||
it('counts the tool schemas in the estimate', () => {
|
||||
const manager = new AIChatManager()
|
||||
const messages = [{ role: 'user', content: 'hi' }] as any
|
||||
|
||||
expect(manager.checkTokenUsageOverLimit(messages)).toBe(false)
|
||||
|
||||
manager.tools = [{ def: { name: 'huge', schema: big(520_000) } }] as any
|
||||
expect(manager.checkTokenUsageOverLimit(messages)).toBe(true)
|
||||
})
|
||||
|
||||
it('drops an assistant tool-call message together with its tool response', () => {
|
||||
const manager = new AIChatManager()
|
||||
const messages = [
|
||||
{ role: 'assistant', content: null, tool_calls: [{ id: 't1', function: { name: 'f' } }] },
|
||||
{ role: 'tool', tool_call_id: 't1', content: big(300_000) },
|
||||
{ role: 'user', content: big(300_000) }
|
||||
] as any
|
||||
|
||||
// 3 messages (~600k chars = 150k tokens) exceed the 121.6k threshold; the lone
|
||||
// user message (~300k chars / 4 = 75k tokens) is under it.
|
||||
expect(manager.checkTokenUsageOverLimit(messages)).toBe(true)
|
||||
|
||||
const trimmed = manager.deleteOldestMessage([...messages])
|
||||
|
||||
// The assistant+tool pair is removed together — never a dangling tool at the front.
|
||||
expect(trimmed.map((m: any) => m.role)).toEqual(['user'])
|
||||
expect(manager.checkTokenUsageOverLimit(trimmed)).toBe(false)
|
||||
})
|
||||
|
||||
it('calibrates the estimate to the accurate anchor when one exists', () => {
|
||||
const manager = new AIChatManager()
|
||||
const messages = [{ role: 'user', content: big(200_000) }] as any // crude ≈ 50k tokens
|
||||
|
||||
// Without an anchor the crude estimate (~50k) is under the 121.6k threshold.
|
||||
expect(manager.checkTokenUsageOverLimit(messages)).toBe(false)
|
||||
|
||||
// Restoring a chat whose real context was ~130k tokens sets the anchor; the gate
|
||||
// scales the crude estimate by anchor/crude and now reports over-limit.
|
||||
;(manager.historyManager as any).loadPastChat = () => ({
|
||||
displayMessages: [],
|
||||
actualMessages: messages,
|
||||
contextTokens: 130_000
|
||||
})
|
||||
manager.loadPastChat('id')
|
||||
expect(manager.checkTokenUsageOverLimit(messages)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,10 @@ interface ChatSchema extends IDBSchema {
|
||||
title: string
|
||||
lastModified: number
|
||||
sessionId?: string
|
||||
// Accurate context size (tokens) at the last saved turn. Optional: chats saved
|
||||
// before this field existed read back as undefined (no migration needed — IndexedDB
|
||||
// stores plain objects).
|
||||
contextTokens?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +32,7 @@ export default class HistoryManager {
|
||||
id: string
|
||||
lastModified: number
|
||||
sessionId?: string
|
||||
contextTokens?: number
|
||||
}
|
||||
> = $state({})
|
||||
|
||||
@@ -104,7 +109,11 @@ export default class HistoryManager {
|
||||
return Object.values(this.savedChats)
|
||||
}
|
||||
|
||||
async saveChat(displayMessages: DisplayMessage[], messages: ChatCompletionMessageParam[]) {
|
||||
async saveChat(
|
||||
displayMessages: DisplayMessage[],
|
||||
messages: ChatCompletionMessageParam[],
|
||||
contextTokens?: number
|
||||
) {
|
||||
if (displayMessages.length > 0) {
|
||||
// we don't want to save the snapshot in the history
|
||||
const updatedChat = {
|
||||
@@ -116,6 +125,7 @@ export default class HistoryManager {
|
||||
title: displayMessages[0].content.slice(0, 50),
|
||||
id: this.currentChatId,
|
||||
lastModified: Date.now(),
|
||||
...(contextTokens != null ? { contextTokens } : {}),
|
||||
...(this.sessionId ? { sessionId: this.sessionId } : {})
|
||||
}
|
||||
this.savedChats = {
|
||||
@@ -129,8 +139,12 @@ export default class HistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
async save(displayMessages: DisplayMessage[], messages: ChatCompletionMessageParam[]) {
|
||||
await this.saveChat(displayMessages, messages)
|
||||
async save(
|
||||
displayMessages: DisplayMessage[],
|
||||
messages: ChatCompletionMessageParam[],
|
||||
contextTokens?: number
|
||||
) {
|
||||
await this.saveChat(displayMessages, messages, contextTokens)
|
||||
this.currentChatId = createLongHash()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,9 @@ import type {
|
||||
import type { AIProviderModel } from '$lib/gen'
|
||||
import { getCompletion, parseOpenAICompletion } from '../lib'
|
||||
import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic'
|
||||
import {
|
||||
getOpenAIResponsesCompletion,
|
||||
parseOpenAIResponsesCompletion
|
||||
} from './openai-responses'
|
||||
import { getOpenAIResponsesCompletion, parseOpenAIResponsesCompletion } from './openai-responses'
|
||||
import type { Tool, ToolCallbacks } from './shared'
|
||||
import {
|
||||
addChatTokenUsage,
|
||||
emptyChatTokenUsage,
|
||||
type ChatTokenUsage
|
||||
} from './tokenUsage'
|
||||
import { addChatTokenUsage, emptyChatTokenUsage, type ChatTokenUsage } from './tokenUsage'
|
||||
|
||||
export interface ChatClients {
|
||||
openai: OpenAI
|
||||
@@ -55,6 +48,15 @@ export interface ChatLoopConfig {
|
||||
export interface ChatLoopResult {
|
||||
addedMessages: ChatCompletionMessageParam[]
|
||||
tokenUsage: ChatTokenUsage
|
||||
/**
|
||||
* Usage of the final loop iteration only (last write wins). Its prompt already
|
||||
* includes the fully-grown context (original messages + every tool result this
|
||||
* turn), and its completion is the final assistant reply now appended — so
|
||||
* `prompt + completion` approximates the whole conversation's token size after
|
||||
* the turn. Used as the accurate "current context size" anchor, distinct from
|
||||
* the cumulative `tokenUsage` above (which sums every iteration = total billed).
|
||||
*/
|
||||
lastIterationUsage: ChatTokenUsage
|
||||
hitMaxIterations: boolean
|
||||
}
|
||||
|
||||
@@ -74,6 +76,7 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
|
||||
const addedMessages: ChatCompletionMessageParam[] = []
|
||||
let tokenUsage = emptyChatTokenUsage()
|
||||
let lastIterationUsage = emptyChatTokenUsage()
|
||||
let iterations = 0
|
||||
let hitMaxIterations = false
|
||||
|
||||
@@ -133,14 +136,12 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
parseOptions
|
||||
)
|
||||
tokenUsage = addChatTokenUsage(tokenUsage, continueCompletion.tokenUsage)
|
||||
lastIterationUsage = continueCompletion.tokenUsage
|
||||
if (!continueCompletion.shouldContinue) {
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'OpenAI Responses API failed, falling back to Completions API:',
|
||||
err
|
||||
)
|
||||
console.warn('OpenAI Responses API failed, falling back to Completions API:', err)
|
||||
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||
if (errorMessage.includes('Responses API is not enabled')) {
|
||||
skipResponsesApi = true
|
||||
@@ -167,20 +168,16 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
parseOptions
|
||||
)
|
||||
tokenUsage = addChatTokenUsage(tokenUsage, continueCompletion.tokenUsage)
|
||||
lastIterationUsage = continueCompletion.tokenUsage
|
||||
if (!continueCompletion.shouldContinue) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (isAnthropic) {
|
||||
const completion = await getAnthropicCompletion(
|
||||
messageParams,
|
||||
abortController,
|
||||
toolDefs,
|
||||
{
|
||||
forceModelProvider: modelProvider,
|
||||
anthropicClient: clients.anthropic
|
||||
}
|
||||
)
|
||||
const completion = await getAnthropicCompletion(messageParams, abortController, toolDefs, {
|
||||
forceModelProvider: modelProvider,
|
||||
anthropicClient: clients.anthropic
|
||||
})
|
||||
if (completion) {
|
||||
const continueCompletion = await parseAnthropicCompletion(
|
||||
completion,
|
||||
@@ -193,6 +190,7 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
parseOptions
|
||||
)
|
||||
tokenUsage = addChatTokenUsage(tokenUsage, continueCompletion.tokenUsage)
|
||||
lastIterationUsage = continueCompletion.tokenUsage
|
||||
if (!continueCompletion.shouldContinue) {
|
||||
break
|
||||
}
|
||||
@@ -214,6 +212,7 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
parseOptions
|
||||
)
|
||||
tokenUsage = addChatTokenUsage(tokenUsage, continueCompletion.tokenUsage)
|
||||
lastIterationUsage = continueCompletion.tokenUsage
|
||||
if (!continueCompletion.shouldContinue) {
|
||||
break
|
||||
}
|
||||
@@ -221,5 +220,5 @@ export async function runChatLoop(config: ChatLoopConfig): Promise<ChatLoopResul
|
||||
}
|
||||
}
|
||||
|
||||
return { addedMessages, tokenUsage, hitMaxIterations }
|
||||
return { addedMessages, tokenUsage, lastIterationUsage, hitMaxIterations }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user