Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

contextfit

Fit any conversation into any context window β€” deterministically, with a full audit of what you dropped.

npm MIT zero dependencies bundle size types included runtimes live demo

Quickstart Β· Audit trail Β· Live demo Β· Strategies Β· Providers Β· Tokenizers Β· Contributing


contextfit takes an over-budget conversation and a token budget, and returns messages that fit plus a structured audit of every drop, shrink, and summary.

You give it a list of messages (system + history + tool results) and a token budget; it returns a pruned/compacted list that fits β€” plus structured metadata about what it dropped and why. Zero runtime dependencies, deterministic by default, and never a silent cut.

Why

Token counting is solved (js-tiktoken, gpt-tokenizer, provider count endpoints). Agent frameworks solve compaction internally β€” but lock you in. Memory services are heavy and often hosted. There is no small, dependency-light, framework-agnostic library whose one job is "here are my messages and my budget β€” make them fit, and tell me what you cut." Every agent developer writes this by hand in their loop. contextfit is that missing piece.

  • Zero required runtime dependencies. A fast built-in heuristic estimator; exact tokenizers are optional and injected by you.
  • Deterministic by default. The default pipeline never calls an LLM and never hits the network. Same input + budget β†’ byte-identical output.
  • Never drops silently. Every removal, shrink, and summary is recorded in result.dropped with the message ref, the strategy, and token deltas.
  • Provider-agnostic. One normalized Message model in and out; adapters for OpenAI, Anthropic, and Gemini.
  • Runs everywhere. Node β‰₯ 20, Bun, Deno, Cloudflare Workers, Vercel Edge. No node:* in core.

Install

npm install contextfit        # or: pnpm add contextfit / bun add contextfit

Quickstart

import { type Message, fit, pinSystem, slidingWindow, truncateToolOutput } from "contextfit";

const messages: Message[] = [
  { role: "system", content: "You are a helpful assistant." },
  { role: "user", content: "Explain context windows in one paragraph." },
];

const result = await fit(messages, {
  budget: { context: 128_000, reserveForOutput: 4_096 },
  strategies: [pinSystem(), truncateToolOutput({ maxTokensPerResult: 2_000 }), slidingWindow()],
});

console.log(result.messages, result.tokens, result.fits);

The audit trail

The headline feature: know exactly what was cut and why.

import { type Message, fit } from "contextfit";

const messages: Message[] = [
  { role: "system", content: "You are a helpful assistant." },
  {
    role: "assistant",
    content: [{ type: "tool_call", id: "c1", name: "search", arguments: { q: "rust" } }],
  },
  {
    role: "tool",
    content: [{ type: "tool_result", toolCallId: "c1", content: "…10k tokens of results…" }],
  },
  { role: "user", content: "Thanks!" },
];

const result = await fit(messages, { budget: { context: 200 } });

// Every removal, shrink, and summary is recorded β€” nothing is dropped silently.
for (const decision of result.dropped) {
  console.log(
    `${decision.strategy} ${decision.action} ${decision.messageId}: -${decision.tokensSaved} tokens`,
  );
}

fit throws BudgetUnsatisfiableError when the mandatory keep-set (system + pinned) alone exceeds the budget. Use fitSafe for a non-throwing, discriminated result:

const outcome = await fitSafe(messages, { budget: { context: 4_096 } });
if (outcome.ok) {
  send(outcome.result.messages);
} else {
  console.warn(outcome.error.message); // requiredTokens vs availableTokens
}

Try it live

β†’ contextfit.vercel.app β€” an interactive playground: drag the budget slider or edit the JSON and watch the conversation re-fit in real time, with the full audit trail.

Deploy your own copy in one click, or run it locally with cd demo && npm install && npm run dev:

Deploy with Vercel

Strategies

Strategies are pure, composable, independently testable functions over (messages, ctx). The default pipeline is pin-system β†’ truncate-tool-output β†’ sliding-window: protect the system/pins, reclaim tokens from oversized tool output, then drop the oldest turns only if still over budget.

Strategy Keeps Cuts Fidelity Network In default pipeline
pin-system system + pinned + user-specified nothing lossless no βœ…
truncate-tool-output every message; non-tool content tool results over the cap lossy no βœ…
sliding-window protected + recent suffix older turns lossy no βœ…
drop-middle protected + head + tail middle turns lossy no β€”
summarize-spans protected + recent; a summary of the rest old turns (folded into a summary) lossy (summary) yes (hook) β€”

Each strategy lives in its own folder under src/strategies/ with tests and a README describing what it keeps, what it cuts, and its fidelity/latency trade-off. Order is explicit and yours to choose.

Providers

One normalized Message shape in, the same shape out. Adapters convert to/from each vendor's format so the same pipeline serves all three. Tool-call and tool-result blocks survive the round trip.

Provider Import Exports Reference
OpenAI contextfit/openai toOpenAI, fromOpenAI docs
Anthropic contextfit/anthropic toAnthropic, fromAnthropic docs
Gemini contextfit/gemini toGemini, fromGemini docs
import { type Message, fit } from "contextfit";
import { toOpenAI } from "contextfit/openai";

const messages: Message[] = [
  { role: "system", content: "You are helpful." },
  { role: "user", content: "Hi!" },
];

const { messages: fitted } = await fit(messages, { budget: { context: 128_000 } });

// The same normalized messages, ready for the OpenAI SDK.
const openaiMessages = toOpenAI(fitted);
console.log(openaiMessages.length);

Bring your own tokenizer

The default counter is a fast heuristic β€” an estimate, good enough for budgeting. Inject an exact counter when you need precision. Exact counts for Claude and Gemini require an API call (there is no local exact tokenizer for those models); OpenAI-family models have local exact tokenizers.

Counter Kind Exactness Factory
built-in heuristic sync (local) estimate heuristicCounter (default)
js-tiktoken / gpt-tokenizer sync (local) exact (OpenAI family) encodeCounter(encode)
Anthropic count_tokens async (API) exact (Claude) anthropicApiCounter({ apiKey, model })
Gemini countTokens async (API) exact (Gemini) geminiApiCounter({ apiKey, model })
import { type Message, fit } from "contextfit";
import { encodeCounter } from "contextfit/tokenizers";
import { encode } from "gpt-tokenizer";

const messages: Message[] = [{ role: "user", content: "Count me exactly." }];

const result = await fit(messages, {
  counter: encodeCounter(encode), // exact OpenAI-family counts, injected by you
  budget: { context: 8_192, reserveForOutput: 1_024 },
});

console.log(result.tokens.before);

Bring your own summarizer

For lossy compaction that keeps meaning, pass a summarize hook. contextfit orchestrates when to summarize and which span; you own the model call β€” it imports no SDK.

import { type Message, fit, pinSystem, slidingWindow, summarizeSpans } from "contextfit";

const messages: Message[] = [
  { role: "system", content: "You are a helpful assistant." },
  // ...a long history...
  { role: "user", content: "What did we decide earlier?" },
];

const result = await fit(messages, {
  budget: { context: 128_000, reserveForOutput: 4_096 },
  // contextfit decides which span to collapse; you own the model call.
  summarize: async (span) => `Summary of ${span.length} earlier messages.`,
  strategies: [pinSystem(), summarizeSpans({ keepRecent: 6 }), slidingWindow()],
});

console.log(result.dropped.filter((d) => d.action === "summarize").length);

API

Export What it is
fit(messages, options) Fit into the budget; throws BudgetUnsatisfiableError if the mandatory set can't fit.
fitSafe(messages, options) Same, but returns a discriminated { ok, result, error? }.
defaultStrategies() The default pin-system β†’ truncate-tool-output β†’ sliding-window pipeline.
Message, ContentPart, Decision, Budget, Strategy, TokenCounter The core types.
pinSystem, slidingWindow, truncateToolOutput, dropMiddle, summarizeSpans Strategy factories.

FitResult is { messages, tokens: { before, after, budget, available }, dropped: Decision[], fits }.

Contributing

Adding a strategy or a provider is meant to take an afternoon:

pnpm new-strategy recency-decay   # scaffolds folder + tests + README, and registers it
pnpm new-provider cohere          # scaffolds a provider adapter

See CONTRIBUTING.md for the quality gate and the authoring cheat-sheet, BACKLOG.md for good-first-issues, and CLAUDE.md for the repo conventions your editor/agent should follow.

License

MIT

About

🌾 LLM context window management, done right. Fit any conversation into any model's token limit β€” deterministic, zero-dependency, and it tells you exactly what it dropped.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages