Repository navigation
docs(sdk): document per-call prompt token composition metrics #757
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
georgeglarson
wants to merge
6
commits into
OpenHands:main
Choose a base branch
from
georgeglarson:feat/llm-prompt-composition-metrics
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+92
−1
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
991a59c
docs(sdk): document per-call prompt token composition metrics
georgeglarson 99bcd6e
docs(sdk): fix prompt composition join example and estimator caveats
georgeglarson 3784e95
docs(sdk): note subscription-mode bucket behavior for prompt composition
georgeglarson d62f87e
docs(sdk): make prompt composition opt-in via enable_prompt_composition
georgeglarson 7959896
Merge branch 'main' into feat/llm-prompt-composition-metrics
georgeglarson 9ddf874
docs(sdk): rework prompt composition guide for offline report script
georgeglarson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| --- | ||
| title: Prompt Token Composition | ||
| description: Break down prompt tokens per LLM call into system prompt, tool schemas, conversation history, and the latest message with the offline composition report. | ||
| --- | ||
|
|
||
| ## Overview | ||
|
|
||
| The prompt composition report is an **offline analysis script** in the [software-agent-sdk repository](https://github.com/OpenHands/software-agent-sdk/blob/main/scripts/prompt_composition_report.py). It reads the completion logs written by the SDK during a run and rebuilds, for every LLM call, a decomposition of its prompt tokens, so you can see where the input budget went on each step of an agent run: | ||
|
|
||
| - `system_prompt_tokens` - Tokens in system messages | ||
| - `tool_schema_tokens` - Tokens in the tool schemas included with the call | ||
| - `history_tokens` - Tokens in conversation history (all non-system messages except the latest one) | ||
| - `latest_message_tokens` - Tokens in the latest observation or user message | ||
|
|
||
| Because the analysis runs after the fact on logged payloads, it adds **no runtime cost and no runtime API surface** - there is nothing to enable on the LLM beyond completion logging, which the SDK already supports. On the Responses API path, the decomposition is computed on the finalized logged payload (instructions plus input items), so the report reflects what the provider received. | ||
|
|
||
| A typical question this answers: how many prompt tokens per call are re-sent tool schemas? That number is the baseline for evaluating deferred tool loading (see [software-agent-sdk#4083](https://github.com/OpenHands/software-agent-sdk/issues/4083)). | ||
|
|
||
| ## Enable Completion Logging | ||
|
|
||
| Run your agent with `log_completions=True` so each LLM call is written to a JSON log file: | ||
|
|
||
| ```python icon="python" focus={4-5} | ||
| llm = LLM( | ||
| model="anthropic/claude-sonnet-4-5-20250929", | ||
| api_key=SecretStr(os.getenv("LLM_API_KEY")), | ||
| log_completions=True, | ||
| log_completions_folder="logs/completions", | ||
| ) | ||
| ``` | ||
|
|
||
| `log_completions_folder` defaults to `logs/completions`. Each call lands in its own `*.json` file containing the finalized request payload (messages and OpenAI-format tool schemas on the Chat Completions path; `instructions`, `input`, and Responses-format tool schemas on the Responses API path) plus the provider-reported usage, latency, and timestamp. The report accepts either a single folder of log files or a directory containing several such run folders. | ||
|
|
||
| ## Run the Report | ||
|
|
||
| From the root of the `software-agent-sdk` repository: | ||
|
|
||
| ```bash icon="terminal" | ||
| uv run python scripts/prompt_composition_report.py --root logs/completions | ||
| ``` | ||
|
|
||
| - `--root` (required) - A run folder of `*.json` completion logs, or a root directory containing run folders. | ||
| - `--out DIR` - Also write the per-call rows to `calls.jsonl` and the aggregate numbers to `summary.json` in `DIR`. | ||
| - `--no-chart` - Skip the text visualization on stdout. | ||
|
|
||
| The script prints a stacked bar per call (`S`=system, `T`=tool_schema, `H`=history, `L`=latest) and a trend table over the call sequence: | ||
|
|
||
| ```text icon="terminal" | ||
| seq composition est_total provider | ||
| 0 SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSTTLL 675 702 | ||
| 1 SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSTTHHHHLL 754 784 | ||
| 2 SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSTTHHHHHHHHHHLL 870 904 | ||
| ``` | ||
|
|
||
| Each `calls.jsonl` row carries `seq`, `usage` (provider-reported, joined by response id), `composition` (the four estimated buckets), `latency_s`, and `tool_schema_counted`. The `summary.json` aggregates per-bucket averages and the `est_provider_median_ratio` - the median of estimated-total over provider-reported `prompt_tokens` across calls, which shows how much of the billed prompt the breakdown covers. | ||
|
|
||
| Agent steps that send tools as native function-calling schemas show `tool_schema_tokens > 0`. Auxiliary calls that pass no tools - for example the [context condenser](/sdk/guides/context-condenser) or title generation - show `tool_schema_tokens == 0`. | ||
|
|
||
| <Note> | ||
| On models without native function calling, the SDK renders tool schemas into the prompt text instead of sending them as tool parameters. The report detects this from the log and leaves `tool_schema_tokens` at 0 for those calls; the schema tokens appear in the message buckets instead, with no double counting. | ||
| </Note> | ||
|
|
||
| <Warning> | ||
| Completion logs written by SDK versions before tool schemas were logged in finalized form record only tool names and descriptions, not parameter schemas. The report cannot reconstruct the tool bucket from those and flags the affected rows with `tool_schema_counted=false` (marked `*` in the chart) rather than miscounting them; flagged calls are excluded from the est/provider median ratio. Re-run with a current SDK to populate the bucket. | ||
| </Warning> | ||
|
|
||
| ## Estimates vs Provider-Reported Usage | ||
|
|
||
| Composition counts are **client-side estimates**, computed offline with the model's tokenizer from the logged payload. The provider-reported usage in each log remains the authoritative accounting: | ||
|
|
||
| - `is_estimate` is `True` on records produced by the client-side estimator. | ||
| - Each component is counted independently, so per-message framing overhead is included in every component and the components do not necessarily sum exactly to the provider-reported `prompt_tokens`. | ||
| - Tool schema counts follow litellm's `token_counter` serialization convention for tools, which can differ from the provider's wire-format tokenization. | ||
| - For models litellm has no tokenizer mapping for, counts use litellm's fallback tokenizer and may deviate more from the provider's counts. | ||
| - When token counting fails or is disabled (for example `litellm.disable_token_counter`), the affected log file is skipped and counted under `skipped_files` in the summary; other calls are unaffected. | ||
| - Buckets follow the wire, not the logical prompt: in subscription mode the system prompt is folded into the first user message before transport, so those tokens are counted in `history_tokens` or `latest_message_tokens` rather than `system_prompt_tokens` on that path. | ||
|
|
||
| <Tip> | ||
| Treat the composition as a breakdown of *where* prompt tokens go, and the provider-reported usage as the record of *how many* tokens the provider billed. | ||
| </Tip> | ||
|
|
||
| ## Performance | ||
|
|
||
| The counting cost is paid at analysis time, not during the agent run. It scales linearly with prompt size: roughly 10-20 ms per call on a typical agent payload, measured at ~31 ms for a ~100K-token prompt and ~61 ms for ~190K tokens (19 tools, gpt-4o tokenizer). | ||
|
|
||
| ## Next Steps | ||
|
|
||
| - **[Metrics Tracking](/sdk/guides/metrics)** - Token usage, costs, and latency metrics for your agents | ||
| - **[Context Condenser](/sdk/guides/context-condenser)** - How OpenHands keeps history within the context window | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
usageis read from the same log file'susage_summary; there's no join (prompt_composition_report.py#L145-L157).