close
Skip to content

Add model context window metadata and proactive token-limit check - #264

Open
the-hercules wants to merge 2 commits into
WordPress:trunkfrom
the-hercules:add/feature-context-window-token-check
Open

Add model context window metadata and proactive token-limit check#264
the-hercules wants to merge 2 commits into
WordPress:trunkfrom
the-hercules:add/feature-context-window-token-check

Conversation

@the-hercules

@the-hercules the-hercules commented Jul 24, 2026

Copy link
Copy Markdown

Closes - #260

Summary

Adds an input-side counterpart to the existing output-side maxTokens cap. Today the SDK has no concept of a model's context window, and TokenLimitReachedException is declared but never thrown. This PR lets callers (chat UIs, agents, plugins) discover a model's context window and be told — with a typed, catchable exception, before a request is sent — when an assembled prompt is estimated to be too large, instead of relying on an opaque provider-side HTTP error.

Implements the input-side of #260 (context-window metadata + proactive budgeting). The reactive/output-side (#193) is intentionally left as a follow-up; both can share the same TokenLimitReachedException.

What changed

Metadata

  • ModelMetadata gains an optional contextWindow (?int, total input + output tokens). Nullable, defaults to null, backward compatible: constructor param is trailing/optional, included in toArray()/schema only when set, read in fromArray() without becoming required. Rejected if < 1.

Token counting (BYOB)

  • New TokenCounterInterface (countTokens(list<Message>, ModelMetadata): int).
  • New default HeuristicTokenCounter: a rough, dependency-free strlen / 4 estimate over text parts, documented as advisory. Non-text parts (files, tool calls/responses) are not counted.
  • New PromptBuilder::usingTokenCounter() to inject an accurate, model-specific counter per request.

Proactive check

  • PromptBuilder::generateResult() now estimates the prompt (message text + system instruction, plus the configured output maxTokens when set) and throws TokenLimitReachedException when the total exceeds the resolved model's contextWindow. No-op when the context window is unknown, so existing behavior is preserved until providers populate the field.

Docs

  • Clarified contextWindow (input budget) vs ModelConfig::maxTokens (output cap) in docblocks, docs/ARCHITECTURE.md, and docs/GLOSSARY.md.

Design notes / assumptions

  • contextWindow is the total window (input + output), matching published specs — not input-only.
  • The check is always-on when a window is known, but advisory: a heuristic can be wrong, so it can false-positive; usingTokenCounter() is the escape hatch, and the docblock states a passing check is not a guarantee.
  • Override is per-call only; registry-level/provider propagation (the HttpTransporter-style parity) is deliberately out of scope here.
  • No real per-model context-window numbers are shipped — those belong in the provider packages; core stays null.
  • The check is not capability-gated (runs for any model exposing a window; only text is counted). Easy to restrict to text generation if preferred.

Relationship to #193

#193 covers the reactive/output side (truncation via finish_reason == length). This PR is the proactive/input side. The exception and its docblock cover both cases so #193 can build on this; the length finish reason already flows through to Candidate::getFinishReason()->isLength().

Testing

  • composer lint (PHPCS PSR-12/PER + Slevomat, and PHPStan level max): clean.
  • composer test:unit: full suite passes, including new tests for the contextWindow field, HeuristicTokenCounter, and the PromptBuilder check (throws when over, no-op when the window is unknown, honors reserved output, honors an injected counter).

AI usage disclosure

This PR was developed collaboratively with Claude, in a Claude Code session, under human direction and review.

  • Model used: Claude Opus 4.8 (claude-opus-4-8), via Claude Code.
  • How it was used:
    • Investigated the SDK to confirm the gap (ModelMetadata, ModelConfig, TokenLimitReachedException, PromptBuilder) and reviewed related issue Missing Required Parameters in Tool Calls When Token Limit Reached #193.
    • Studied the repo's conventions (DTO patterns, the HttpTransporter-style override mechanism, exception/finish_reason handling, test structure, PHP 7.4 / PHPStan-max / PSR-12 constraints) before writing code.
    • Drafted the implementation, unit tests, and the doc updates (ARCHITECTURE.md, GLOSSARY.md), and wrote this PR description and the issue comment.
    • Ran composer lint (PHPCS + PHPStan level max) and composer test:unit, plus an offline demo script, to verify behavior.
  • Human involvement: A human maintainer directed the scope, clarified the approach with the issue author, reviewed every change.
  • Caveats:
    • Verification was done via unit tests and an offline demo; no live provider API calls were made.
    • No real per-model context-window values are included — those are left to the provider packages.

- Introduced TokenCounterInterface and HeuristicTokenCounter for token estimation.
- Implemented context window checks in PromptBuilder to prevent exceeding model limits.
- Enhanced ModelMetadata to include context window property.
- Updated tests to validate new functionality and ensure proper exception handling.
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: the-hercules <thehercules@git.wordpress.org>
Co-authored-by: georgestephanis <georgestephanis@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.08197% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.60%. Comparing base (a31b0ec) to head (737aaa8).

Files with missing lines Patch % Lines
...ders/Models/Tokenization/HeuristicTokenCounter.php 84.61% 2 Missing ⚠️
src/Builders/PromptBuilder.php 96.77% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##              trunk     #264      +/-   ##
============================================
+ Coverage     86.49%   86.60%   +0.11%     
- Complexity     1327     1344      +17     
============================================
  Files            68       69       +1     
  Lines          4295     4353      +58     
============================================
+ Hits           3715     3770      +55     
- Misses          580      583       +3     
Flag Coverage Δ
unit 86.60% <95.08%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@georgestephanis

Copy link
Copy Markdown

@the-hercules Is this missing an AI Use disclosure in the PR? (What models were used, how, caveats, etc)

@the-hercules

Copy link
Copy Markdown
Author

@georgestephanis Yes, it was missing AI disclosure, it was my first time raising PR on this repo so didn't know that we write a disclosure at the end. Will keep in mind from next time.

@georgestephanis

Copy link
Copy Markdown

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants