import { describe, expect, test } from "bun:test"import path from "path"import { SessionCompaction } from "../../src/session/compaction"import { Token } from "../../src/util/token"import { Instance } from "../../src/project/instance"import { Log } from "../../src/util/log"import { tmpdir } from "../fixture/fixture"import { Session } from "../../src/session"import type { Provider } from "../../src/provider/provider" Log.init({ print: false }) function createModel(opts: { context: number output: number input?: number cost?: Provider.Model["cost"]}): Provider.Model { return { id: "test-model", providerID: "test", name: "Test", limit: { context: opts.context, input: opts.input, output: opts.output, }, cost: opts.cost ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, capabilities: { toolcall: true, attachment: false, reasoning: false, temperature: true, input: { text: true, image: false, audio: false, video: false }, output: { text: true, image: false, audio: false, video: false }, }, api: { npm: "@ai-sdk/anthropic" }, options: {}, } as Provider.Model} describe("session.compaction.isOverflow", () => { test("returns true when token count exceeds usable context", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const model = createModel({ context: 100_000, output: 32_000 }) const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } } expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true) }, }) }) test("returns false when token count within usable context", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const model = createModel({ context: 200_000, output: 32_000 }) const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } } expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false) }, }) }) test("includes cache.read in token count", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const model = createModel({ context: 100_000, output: 32_000 }) const tokens = { input: 50_000, output: 10_000, reasoning: 0, cache: { read: 10_000, write: 0 } } expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true) }, }) }) test("respects input limit for input caps", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const model = createModel({ context: 400_000, input: 272_000, output: 128_000 }) const tokens = { input: 271_000, output: 1_000, reasoning: 0, cache: { read: 2_000, write: 0 } } expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true) }, }) }) test("returns false when input/output are within input caps", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const model = createModel({ context: 400_000, input: 272_000, output: 128_000 }) const tokens = { input: 200_000, output: 20_000, reasoning: 0, cache: { read: 10_000, write: 0 } } expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false) }, }) }) test("returns false when output within limit with input caps", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const model = createModel({ context: 200_000, input: 120_000, output: 10_000 }) const tokens = { input: 50_000, output: 9_999, reasoning: 0, cache: { read: 0, write: 0 } } expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false) }, }) }) test("returns false when model context limit is 0", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const model = createModel({ context: 0, output: 32_000 }) const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } } expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false) }, }) }) test("returns false when compaction.auto is disabled", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( path.join(dir, "openscience.json"), JSON.stringify({ compaction: { auto: false }, }), ) }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const model = createModel({ context: 100_000, output: 32_000 }) const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } } expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false) }, }) })}) describe("util.token.estimate", () => { test("estimates tokens from text (4 chars per token)", () => { const text = "x".repeat(4000) expect(Token.estimate(text)).toBe(1000) }) test("estimates tokens from larger text", () => { const text = "y".repeat(20_000) expect(Token.estimate(text)).toBe(5000) }) test("returns 0 for empty string", () => { expect(Token.estimate("")).toBe(0) })}) describe("session.getUsage", () => { test("normalizes standard usage to token format", () => { const model = createModel({ context: 100_000, output: 32_000 }) const result = Session.getUsage({ model, usage: { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, }, }) expect(result.tokens.input).toBe(1000) expect(result.tokens.output).toBe(500) expect(result.tokens.reasoning).toBe(0) expect(result.tokens.cache.read).toBe(0) expect(result.tokens.cache.write).toBe(0) }) test("extracts cached tokens to cache.read", () => { const model = createModel({ context: 100_000, output: 32_000 }) const result = Session.getUsage({ model, usage: { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, cachedInputTokens: 200, }, }) expect(result.tokens.input).toBe(800) expect(result.tokens.cache.read).toBe(200) }) test("handles anthropic cache write metadata", () => { const model = createModel({ context: 100_000, output: 32_000 }) const result = Session.getUsage({ model, usage: { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, }, metadata: { anthropic: { cacheCreationInputTokens: 300, }, }, }) expect(result.tokens.cache.write).toBe(300) }) test("does not subtract cached tokens for anthropic provider", () => { const model = createModel({ context: 100_000, output: 32_000 }) const result = Session.getUsage({ model, usage: { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, cachedInputTokens: 200, }, metadata: { anthropic: {}, }, }) expect(result.tokens.input).toBe(1000) expect(result.tokens.cache.read).toBe(200) }) test("handles reasoning tokens", () => { const model = createModel({ context: 100_000, output: 32_000 }) const result = Session.getUsage({ model, usage: { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, reasoningTokens: 100, }, }) expect(result.tokens.reasoning).toBe(100) }) test("handles undefined optional values gracefully", () => { const model = createModel({ context: 100_000, output: 32_000 }) const result = Session.getUsage({ model, usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, }, }) expect(result.tokens.input).toBe(0) expect(result.tokens.output).toBe(0) expect(result.tokens.reasoning).toBe(0) expect(result.tokens.cache.read).toBe(0) expect(result.tokens.cache.write).toBe(0) expect(Number.isNaN(result.cost)).toBe(false) }) test("calculates cost correctly", () => { const model = createModel({ context: 100_000, output: 32_000, cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 }, }, }) const result = Session.getUsage({ model, usage: { inputTokens: 1_000_000, outputTokens: 100_000, totalTokens: 1_100_000, }, }) expect(result.cost).toBe(3 + 1.5) })})
import { describe, expect, test } from "bun:test"
import path from "path"
import { SessionCompaction } from "../../src/session/compaction"
import { Token } from "../../src/util/token"
import { Instance } from "../../src/project/instance"
import { Log } from "../../src/util/log"
import { tmpdir } from "../fixture/fixture"
import { Session } from "../../src/session"
import type { Provider } from "../../src/provider/provider"
Log.init({ print: false })
function createModel(opts: {
context: number
output: number
input?: number
cost?: Provider.Model["cost"]
}): Provider.Model {
return {
id: "test-model",
providerID: "test",
name: "Test",
limit: {
context: opts.context,
input: opts.input,
output: opts.output,
},
cost: opts.cost ?? { input: 0, output: 0, cache: { read: 0, write: 0 } },
capabilities: {
toolcall: true,
attachment: false,
reasoning: false,
temperature: true,
input: { text: true, image: false, audio: false, video: false },
output: { text: true, image: false, audio: false, video: false },
},
api: { npm: "@ai-sdk/anthropic" },
options: {},
} as Provider.Model
}
describe("session.compaction.isOverflow", () => {
test("returns true when token count exceeds usable context", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = createModel({ context: 100_000, output: 32_000 })
const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } }
expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
},
})
})
test("returns false when token count within usable context", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = createModel({ context: 200_000, output: 32_000 })
const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } }
expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
},
})
})
test("includes cache.read in token count", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = createModel({ context: 100_000, output: 32_000 })
const tokens = { input: 50_000, output: 10_000, reasoning: 0, cache: { read: 10_000, write: 0 } }
expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
},
})
})
test("respects input limit for input caps", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
const tokens = { input: 271_000, output: 1_000, reasoning: 0, cache: { read: 2_000, write: 0 } }
expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
},
})
})
test("returns false when input/output are within input caps", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = createModel({ context: 400_000, input: 272_000, output: 128_000 })
const tokens = { input: 200_000, output: 20_000, reasoning: 0, cache: { read: 10_000, write: 0 } }
expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
},
})
})
test("returns false when output within limit with input caps", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = createModel({ context: 200_000, input: 120_000, output: 10_000 })
const tokens = { input: 50_000, output: 9_999, reasoning: 0, cache: { read: 0, write: 0 } }
expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
},
})
})
test("returns false when model context limit is 0", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = createModel({ context: 0, output: 32_000 })
const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } }
expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
},
})
})
test("returns false when compaction.auto is disabled", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "openscience.json"),
JSON.stringify({
compaction: { auto: false },
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = createModel({ context: 100_000, output: 32_000 })
const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } }
expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
},
})
})
})
describe("util.token.estimate", () => {
test("estimates tokens from text (4 chars per token)", () => {
const text = "x".repeat(4000)
expect(Token.estimate(text)).toBe(1000)
})
test("estimates tokens from larger text", () => {
const text = "y".repeat(20_000)
expect(Token.estimate(text)).toBe(5000)
})
test("returns 0 for empty string", () => {
expect(Token.estimate("")).toBe(0)
})
})
describe("session.getUsage", () => {
test("normalizes standard usage to token format", () => {
const model = createModel({ context: 100_000, output: 32_000 })
const result = Session.getUsage({
model,
usage: {
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
},
})
expect(result.tokens.input).toBe(1000)
expect(result.tokens.output).toBe(500)
expect(result.tokens.reasoning).toBe(0)
expect(result.tokens.cache.read).toBe(0)
expect(result.tokens.cache.write).toBe(0)
})
test("extracts cached tokens to cache.read", () => {
const model = createModel({ context: 100_000, output: 32_000 })
const result = Session.getUsage({
model,
usage: {
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
cachedInputTokens: 200,
},
})
expect(result.tokens.input).toBe(800)
expect(result.tokens.cache.read).toBe(200)
})
test("handles anthropic cache write metadata", () => {
const model = createModel({ context: 100_000, output: 32_000 })
const result = Session.getUsage({
model,
usage: {
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
},
metadata: {
anthropic: {
cacheCreationInputTokens: 300,
},
},
})
expect(result.tokens.cache.write).toBe(300)
})
test("does not subtract cached tokens for anthropic provider", () => {
const model = createModel({ context: 100_000, output: 32_000 })
const result = Session.getUsage({
model,
usage: {
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
cachedInputTokens: 200,
},
metadata: {
anthropic: {},
},
})
expect(result.tokens.input).toBe(1000)
expect(result.tokens.cache.read).toBe(200)
})
test("handles reasoning tokens", () => {
const model = createModel({ context: 100_000, output: 32_000 })
const result = Session.getUsage({
model,
usage: {
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
reasoningTokens: 100,
},
})
expect(result.tokens.reasoning).toBe(100)
})
test("handles undefined optional values gracefully", () => {
const model = createModel({ context: 100_000, output: 32_000 })
const result = Session.getUsage({
model,
usage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
},
})
expect(result.tokens.input).toBe(0)
expect(result.tokens.output).toBe(0)
expect(result.tokens.reasoning).toBe(0)
expect(result.tokens.cache.read).toBe(0)
expect(result.tokens.cache.write).toBe(0)
expect(Number.isNaN(result.cost)).toBe(false)
})
test("calculates cost correctly", () => {
const model = createModel({
context: 100_000,
output: 32_000,
cost: {
input: 3,
output: 15,
cache: { read: 0.3, write: 3.75 },
},
})
const result = Session.getUsage({
model,
usage: {
inputTokens: 1_000_000,
outputTokens: 100_000,
totalTokens: 1_100_000,
},
})
expect(result.cost).toBe(3 + 1.5)
})
})