Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { type WorkspaceStatus } from "../workspace-label"
import { useCommandPalette } from "../../context/command-palette"
import { useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap"
import { useTuiConfig } from "../../context/tui-config"
import { SessionOverflow } from "@/session/overflow"

export type PromptProps = {
sessionID?: string
Expand Down Expand Up @@ -342,15 +343,14 @@ export function Prompt(props: PromptProps) {
const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
if (!last) return

const tokens =
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
const tokens = SessionOverflow.count(last.tokens)
if (tokens <= 0) return

const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
const percent = model ? SessionOverflow.percent({ cfg: sync.data.config, tokens: last.tokens, model }) : null
const cost = session?.cost ?? 0
return {
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
context: percent === null ? Locale.number(tokens) : `${Locale.number(tokens)} (${percent}%)`,
cost: cost > 0 ? money.format(cost) : undefined,
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AssistantMessage } from "@opencode-ai/sdk/v2"
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
import type { InternalTuiPlugin } from "../../plugin/internal"
import { createMemo } from "solid-js"
import { SessionOverflow } from "@/session/overflow"

const id = "internal:sidebar-context"

Expand All @@ -25,12 +26,11 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
}
}

const tokens =
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
const tokens = SessionOverflow.count(last.tokens)
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
return {
tokens,
percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : null,
percent: model ? SessionOverflow.percent({ cfg: props.api.state.config, tokens: last.tokens, model }) : null,
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Locale } from "@/util/locale"
import { useTerminalDimensions } from "@opentui/solid"
import { useCommandPalette } from "../../context/command-palette"
import { useCommandShortcut } from "../../keymap"
import { SessionOverflow } from "@/session/overflow"

export function SubagentFooter() {
const route = useRouteData("session")
Expand Down Expand Up @@ -36,12 +37,11 @@ export function SubagentFooter() {
const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
if (!last) return

const tokens =
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
const tokens = SessionOverflow.count(last.tokens)
if (tokens <= 0) return

const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
const percent = model ? SessionOverflow.percent({ cfg: sync.data.config, tokens: last.tokens, model }) : null
const cost = session()?.cost ?? 0

const money = new Intl.NumberFormat("en-US", {
Expand All @@ -50,7 +50,7 @@ export function SubagentFooter() {
})

return {
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
context: percent === null ? Locale.number(tokens) : `${Locale.number(tokens)} (${percent}%)`,
cost: cost > 0 ? money.format(cost) : undefined,
}
})
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1250,7 +1250,7 @@ export function providerOptions(model: Provider.Model, options: { [x: string]: a
return { [key]: options }
}

export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_TOKEN_MAX): number {
export function maxOutputTokens(model: { limit: { output: number } }, outputTokenMax = OUTPUT_TOKEN_MAX): number {
return Math.min(model.limit.output, outputTokenMax) || outputTokenMax
}

Expand Down
38 changes: 34 additions & 4 deletions packages/opencode/src/session/overflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@ import type { MessageV2 } from "./message-v2"

const COMPACTION_BUFFER = 20_000

export function usable(input: { cfg: Config.Info; model: Provider.Model; outputTokenMax?: number }) {
type ContextConfig = {
compaction?: {
auto?: boolean
reserved?: number
}
}

type ContextModel = {
limit: {
context: number
input?: number
output: number
}
}

export function usable(input: { cfg: ContextConfig; model: ContextModel; outputTokenMax?: number }) {
const context = input.model.limit.context
if (context === 0) return 0

Expand All @@ -17,6 +32,21 @@ export function usable(input: { cfg: Config.Info; model: Provider.Model; outputT
: Math.max(0, context - ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax))
}

export function count(tokens: MessageV2.Assistant["tokens"]) {
return tokens.total || tokens.input + tokens.output + tokens.cache.read + tokens.cache.write
}

export function percent(input: {
cfg: ContextConfig
tokens: MessageV2.Assistant["tokens"]
model: ContextModel
outputTokenMax?: number
}) {
const limit = usable(input)
if (limit === 0) return null
return Math.round((count(input.tokens) / limit) * 100)
}

export function isOverflow(input: {
cfg: Config.Info
tokens: MessageV2.Assistant["tokens"]
Expand All @@ -26,7 +56,7 @@ export function isOverflow(input: {
if (input.cfg.compaction?.auto === false) return false
if (input.model.limit.context === 0) return false

const count =
input.tokens.total || input.tokens.input + input.tokens.output + input.tokens.cache.read + input.tokens.cache.write
return count >= usable(input)
return count(input.tokens) >= usable(input)
}

export * as SessionOverflow from "./overflow"
33 changes: 33 additions & 0 deletions packages/opencode/test/session/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Image } from "@/image/image"
import { Agent } from "../../src/agent/agent"
import { LLM } from "../../src/session/llm"
import { SessionCompaction } from "../../src/session/compaction"
import { SessionOverflow } from "../../src/session/overflow"
import { Token } from "@/util/token"
import * as Log from "@opencode-ai/core/util/log"
import { Permission } from "../../src/permission"
Expand Down Expand Up @@ -83,6 +84,38 @@ function createModel(opts: {

const wide = () => ProviderTest.fake({ model: createModel({ context: 100_000, output: 32_000 }) })

describe("session.overflow.percent", () => {
test("uses input limit minus default compaction reserve", () => {
const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
const tokens = { input: 126_000, output: 0, reasoning: 50_000, cache: { read: 0, write: 0 } }

expect(SessionOverflow.usable({ cfg: {}, model })).toBe(252_000)
expect(SessionOverflow.percent({ cfg: {}, tokens, model })).toBe(50)
})

test("uses configured compaction reserve for input-limited models", () => {
const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
const tokens = { input: 121_000, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }

expect(SessionOverflow.percent({ cfg: { compaction: { reserved: 30_000 } }, tokens, model })).toBe(50)
})

test("returns null when there is no known context limit", () => {
const model = createModel({ context: 0, output: 32_000 })
const tokens = { input: 10_000, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }

expect(SessionOverflow.percent({ cfg: {}, tokens, model })).toBe(null)
})

test("uses total token count when providers report it", () => {
const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
const tokens = { total: 63_000, input: 126_000, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }

expect(SessionOverflow.count(tokens)).toBe(63_000)
expect(SessionOverflow.percent({ cfg: {}, tokens, model })).toBe(25)
})
})

function createUserMessage(sessionID: SessionID, text: string) {
return Effect.gen(function* () {
const ssn = yield* SessionNs.Service
Expand Down
Loading