Skip to content
Merged
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
46 changes: 46 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Bug report
description: Report something that isn't working
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to file a bug report. The more specific you
can be, the faster I can fix it.
- type: input
id: version
attributes:
label: ctx-opt version
description: Output of `npm ls ctx-opt`
placeholder: e.g. 0.3.0
validations:
required: true
- type: input
id: node
attributes:
label: Node version
placeholder: e.g. 20.10.0
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: A clear description of the bug.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Minimal reproduction
description: A short, self-contained code snippet that triggers the bug.
render: ts
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What you expected to happen instead.
validations:
required: true
23 changes: 23 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Feature request
description: Suggest a new feature or improvement
labels: [enhancement]
body:
- type: textarea
id: problem
attributes:
label: The problem
description: What are you trying to do that ctx-opt doesn't currently support? Or what's painful about how it works today?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: How would you like it to work?
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Other approaches you've thought about or workarounds you're using.
21 changes: 21 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Summary

<!-- One or two sentences describing the change and why. -->

## Type of change

- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation
- [ ] Refactor / chore

## Test plan

<!-- How did you verify this? -->

- [ ] `npm run typecheck` passes
- [ ] `npm test` passes
- [ ] `npm run build` succeeds
- [ ] Added or updated tests
- [ ] Updated `CHANGELOG.md` (for user-facing changes)
38 changes: 36 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2026-05-14

### Added
- **Vercel AI SDK adapter** at `ctx-opt/ai-sdk`. `withOptimizer(fn, config)`
wraps any AI SDK function (`generateText`, `streamText`, `generateObject`,
`streamObject`) so its `messages` array is trimmed before forwarding.
Also ships `trimMessages(messages, config)` for callers that prefer a
one-shot preprocessor.
- **Streaming support** in the OpenAI and Anthropic adapters. `stream: true`
on `chat.completions.create` / `messages.create` now correctly returns
the SDK's async-iterable stream after optimization. The Anthropic
adapter also wraps `client.messages.stream()`.
- **Native Anthropic token counting** via `countMessageTokensWithAnthropic`,
which delegates to `client.messages.countTokens` for exact counts on
`claude-*` models. Tradeoff is a network round-trip per call versus
tiktoken's free local approximation.
- **Per-strategy `recentWindow` overrides**. `relevance.recentWindow` and
`summarizer.recentWindow` can now be set independently of the top-level
`recentWindow`. This unblocks the hybrid strategy (see below).
- `CONTRIBUTING.md`, GitHub issue templates, and a PR template.

### Fixed
- **Hybrid strategy now actually summarizes**. Previously the relevance
and summarizer phases shared `recentWindow`, so the post-relevance set
was entirely "system + recent" with nothing left for the summarizer to
compress. Setting `relevance.recentWindow` larger than
`summarizer.recentWindow` gives the summarizer real material. The
known-limitation note from 0.2.0 is resolved.

### Changed
- New peer dep: `ai` (optional, for the Vercel AI SDK adapter).
- `tsup` now builds four entry points instead of three.

## [0.2.0] - 2026-05-14

### Added
Expand Down Expand Up @@ -38,8 +71,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- In the `hybrid` strategy, `applyRelevance` and `applySummarizer`
share `recentWindow`, so the post-relevance set rarely contains
"compressible" messages for the summarizer to operate on, and the
pipeline falls through to a sliding-window pass. A future release
will let each phase have its own window.
pipeline falls through to a sliding-window pass. *Fixed in 0.3.0
via per-strategy `recentWindow` overrides.*

## [0.1.0] - 2026-05-01

Expand All @@ -52,5 +85,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Tool-pair preservation across boundary trims.
- ESM + CJS builds, TypeScript types, Node 18+.

[0.3.0]: https://github.com/EvanPaules/ctx-opt/releases/tag/v0.3.0
[0.2.0]: https://github.com/EvanPaules/ctx-opt/releases/tag/v0.2.0
[0.1.0]: https://github.com/EvanPaules/ctx-opt/releases/tag/v0.1.0
62 changes: 62 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Contributing to ctx-opt

Thanks for your interest. This is a small library, so I try to keep the
contribution surface simple.

## Quick start

```bash
git clone https://github.com/EvanPaules/ctx-opt.git
cd ctx-opt
npm install
npm test
```

## Useful commands

| Command | What it does |
|---|---|
| `npm test` | Run the vitest suite once. |
| `npm run test:watch` | Run vitest in watch mode while iterating. |
| `npm run typecheck` | Strict TS typecheck without emitting. |
| `npm run build` | Produce the ESM + CJS + .d.ts dist. |
| `npm run bench` | Run the strategy benchmark and write `benchmarks/RESULTS.md`. |

CI runs typecheck, test, and build on Node 18, 20, and 22. All three must
pass before a PR is mergeable.

## What I'm looking for

- **Bug fixes** with a regression test.
- **New strategies** that fit the existing `Strategy` interface and come
with their own test file.
- **SDK adapters** for new ecosystems (LangChain, Mastra, etc). Use the
existing `src/adapters/openai.ts` and `src/adapters/anthropic.ts` as
templates and add a subpath export to `package.json`.
- **Docs and examples** that make a real workflow easier to get started with.

## What I'm probably going to push back on

- Refactors with no user-facing benefit.
- Optional features behind flags that aren't requested by users.
- Removing the zero-required-dependency property of the core package.

## Style

- TypeScript strict mode is on. No `any` without an explanatory comment.
- Prefer named exports over default exports (except `ContextOptimizer`).
- Tests use vitest. Mock SDKs over real network calls.
- Keep public API surface small. Add to `src/index.ts` deliberately.

## Releasing (maintainer notes)

1. Land all changes on `main` via PR.
2. Bump `version` in `package.json`.
3. Update `CHANGELOG.md`.
4. Tag and push: `git tag v0.x.0 && git push --tags`.
5. `npm publish`.

## License

By contributing you agree your contributions are licensed under MIT,
matching the project license.
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,23 @@ const res = await ai.messages.create({
});
```

`openai` and `@anthropic-ai/sdk` are **optional peer deps** — install only
the one you use. See [`examples/`](./examples) for OpenAI, Anthropic,
```ts
import { generateText } from 'ai';
import { withOptimizer } from 'ctx-opt/ai-sdk';

const trimmedGenerate = withOptimizer(generateText, {
maxTokens: 8_000,
strategy: 'sliding-window',
});

const { text } = await trimmedGenerate({
model: openai('gpt-4o'),
messages: longHistory,
});
```

`openai`, `@anthropic-ai/sdk`, and `ai` are **optional peer deps** — install
only the one(s) you use. See [`examples/`](./examples) for OpenAI, Anthropic,
summarizer-with-real-LLM, and LangChain.js integrations.

## Strategies
Expand Down Expand Up @@ -240,8 +255,20 @@ Every call to `optimize()` returns a `meta` describing what happened:
- A **per-message overhead** of 4 tokens is added to each message to approximate the
role and formatting tokens (per OpenAI's chat-completion cookbook formula).

For exact Anthropic counts, call Anthropic's `messages.countTokens` API and pass that
through your own wrapper.
For exact Anthropic counts, use the built-in helper that delegates to
Anthropic's `messages.countTokens` endpoint:

```ts
import Anthropic from '@anthropic-ai/sdk';
import { countMessageTokensWithAnthropic } from 'ctx-opt';

const client = new Anthropic();
const tokens = await countMessageTokensWithAnthropic(
client,
messages,
'claude-haiku-4-5-20251001'
);
```

## Changelog

Expand Down
Loading
Loading