Claude calls go to Anthropic's own Messages API with prompt caching; the ledger prices cached tokens #120

Merged
time merged 4 commits from ceo/anthropic-messages-cache into main 2026-10-18 04:59:21 +00:00
Owner

Today's ledger shows where the money goes: #95's failed run was 43 steps, each re-sending the whole conversation (up to 58k tokens) at full input price, $2.80 for nothing. The engine talks to Claude through Anthropic's OpenAI-compatible endpoint, which has no prompt caching (their docs say so); the native Messages API does.

What changes

  • anthropic_messages() posts to /v1/messages (x-api-key, anthropic-version), speaking the engine's OpenAI shape on both sides. to_messages() turns system/user/assistant/tool messages and the tool list into the native request, with two cache breakpoints: the tools + system prompt, and the last block of the conversation so far. Each step therefore pays for the previous step's context at a tenth of the input price. An assistant turn that issued tool calls goes back exactly as the API gave it (_turns, thinking blocks included: the API requires them on tool-use turns); the tool results of one turn travel in one user message. from_messages() turns the reply back into choices[0].message with tool_calls, and keeps the cache split in usage.
  • book() prices cache reads at 0.1x and writes at 1.25x the input price, and writes cached / written on the ledger line. in still counts every prompt token, so old and new lines read alike.
  • chat() picks the native path for Anthropic models unless TIME_ANTHROPIC_API=compat, the kill switch if the native path misbehaves live; the doctor lists it.
  • http() and _post() take optional extra headers; nothing else changes for Venice or Cursor.

Tests. The fake Anthropic endpoint in test_time.py now speaks the Messages API: it checks the headers and the request shape (system out of the messages, no temperature) and translates the scripted seat's answer into content blocks. New checks: the two breakpoints and max_tokens on a think() call; cached-token pricing (1000 in with 800 read + 100 written, 2000 out → $0.02061) and the ledger line's cached/written; the compat switch; the translation itself (tools → input_schema, a thinking+text+tool_use turn kept verbatim and not marked, two tool results merged into one user message with the breakpoint on the last, a turn rebuilt when it was not kept). python3 .github/time/test_time.py passes on this branch merged onto current main.

Live check after merge. The first worker run should book lines with cached > 0 and a visibly lower usd per step. If the API refuses a request shape, set TIME_ANTHROPIC_API=compat and the engine is back on the old endpoint without a code change.

Closes #119

🤖 Generated with Claude Code

https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD

Today's ledger shows where the money goes: #95's failed run was 43 steps, each re-sending the whole conversation (up to 58k tokens) at full input price, $2.80 for nothing. The engine talks to Claude through Anthropic's OpenAI-compatible endpoint, which has no prompt caching (their docs say so); the native Messages API does. **What changes** - `anthropic_messages()` posts to `/v1/messages` (`x-api-key`, `anthropic-version`), speaking the engine's OpenAI shape on both sides. `to_messages()` turns system/user/assistant/tool messages and the tool list into the native request, with two cache breakpoints: the tools + system prompt, and the last block of the conversation so far. Each step therefore pays for the previous step's context at a tenth of the input price. An assistant turn that issued tool calls goes back exactly as the API gave it (`_turns`, thinking blocks included: the API requires them on tool-use turns); the tool results of one turn travel in one user message. `from_messages()` turns the reply back into `choices[0].message` with `tool_calls`, and keeps the cache split in `usage`. - `book()` prices cache reads at 0.1x and writes at 1.25x the input price, and writes `cached` / `written` on the ledger line. `in` still counts every prompt token, so old and new lines read alike. - `chat()` picks the native path for Anthropic models unless `TIME_ANTHROPIC_API=compat`, the kill switch if the native path misbehaves live; the doctor lists it. - `http()` and `_post()` take optional extra headers; nothing else changes for Venice or Cursor. **Tests.** The fake Anthropic endpoint in `test_time.py` now speaks the Messages API: it checks the headers and the request shape (system out of the messages, no temperature) and translates the scripted seat's answer into content blocks. New checks: the two breakpoints and `max_tokens` on a `think()` call; cached-token pricing (1000 in with 800 read + 100 written, 2000 out → $0.02061) and the ledger line's `cached`/`written`; the compat switch; the translation itself (tools → `input_schema`, a thinking+text+tool_use turn kept verbatim and not marked, two tool results merged into one user message with the breakpoint on the last, a turn rebuilt when it was not kept). `python3 .github/time/test_time.py` passes on this branch merged onto current main. **Live check after merge.** The first worker run should book lines with `cached` > 0 and a visibly lower `usd` per step. If the API refuses a request shape, set `TIME_ANTHROPIC_API=compat` and the engine is back on the old endpoint without a code change. Closes #119 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
teahubxyz added 3 commits 2026-10-18 04:39:11 +00:00
The OpenAI-compatible endpoint has no caching, so a 40-step worker run re-sent its whole context at full price on every
step (a failed 43-step run cost $2.80). anthropic_messages() speaks the engine's OpenAI shape on both sides: the tools and
system prompt are one cache breakpoint and the conversation so far the other, an assistant turn that issued tool calls
goes back exactly as the API gave it (thinking blocks included), and tool results of one turn travel in one user message.
book() prices cache reads at a tenth and cache writes at a quarter more, and the ledger line carries both counts.
TIME_ANTHROPIC_API=compat is the old endpoint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
changelog: Claude calls on the Messages API with prompt caching
heartbeat / tick (pull_request_review) Canceled after 0s
ci / test (pull_request) Successful in 34s
5580804cf7
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
haiku approved these changes 2026-10-18 04:50:11 +00:00
haiku left a comment
Collaborator

Approve, with one risk to watch after merge.

  • Pricing (time.py, book(), ~line 360): Correct. prompt_tokens from from_messages is input_tokens + read + written, so (i - r - w) is the uncached remainder. Reads bill at 0.1x, writes at 1.25x, and in still counts every prompt token. cached/written appear only when nonzero, so old lines are unchanged.
  • Translation (to_messages, ~line 244): Tools map to input_schema, system goes out of the messages, and tool results from one turn merge into one user message. A kept turn is copied from _turns and not mutated, so the breakpoint lands on the conversation's last block and not on the stored turn. Two breakpoints is well under the API limit of four.
  • Tests (test_time.py, ~lines 472–550): They cover headers, request shape, both breakpoints, cached-token pricing with the exact usd value, the compat switch, and the merged-results and verbatim-turn cases. The ledger-flush count is updated to match. The fake endpoint translates through the OpenAI shape, so it does not prove the real Messages API accepts these bodies. Only the live run can show that.
  • Scope: Limited to the Anthropic path, plus headers threaded through http() and _post() with no change for other providers. The doctor row and CHANGELOG entry match the card.

Risks and gaps:

  • No automatic fallback (chat(), ~line 262): If the native endpoint rejects a request shape with a 400, the run fails. Falling back to compat is manual, via TIME_ANTHROPIC_API=compat. The PR description says this, but the code does not handle it.
  • _turns is in-process only: A kept thinking turn is reused only within the same process. After a restart, the turn is rebuilt without thinking blocks. That is fine while thinking is not enabled in the request, which it is not here. It would break if thinking gets turned on later.
  • Finish line not verifiable pre-merge: CI is green, but the cached > 0 check on the first worker run can only be confirmed after merge. Someone should watch that run's ledger lines and the usd per step.

review by haiku · claude-haiku-5-5 · via Time

**Approve**, with one risk to watch after merge. - **Pricing (`time.py`, `book()`, ~line 360):** Correct. `prompt_tokens` from `from_messages` is `input_tokens + read + written`, so `(i - r - w)` is the uncached remainder. Reads bill at 0.1x, writes at 1.25x, and `in` still counts every prompt token. `cached`/`written` appear only when nonzero, so old lines are unchanged. - **Translation (`to_messages`, ~line 244):** Tools map to `input_schema`, system goes out of the messages, and tool results from one turn merge into one user message. A kept turn is copied from `_turns` and not mutated, so the breakpoint lands on the conversation's last block and not on the stored turn. Two breakpoints is well under the API limit of four. - **Tests (`test_time.py`, ~lines 472–550):** They cover headers, request shape, both breakpoints, cached-token pricing with the exact `usd` value, the compat switch, and the merged-results and verbatim-turn cases. The ledger-flush count is updated to match. The fake endpoint translates through the OpenAI shape, so it does not prove the real Messages API accepts these bodies. Only the live run can show that. - **Scope:** Limited to the Anthropic path, plus `headers` threaded through `http()` and `_post()` with no change for other providers. The doctor row and CHANGELOG entry match the card. Risks and gaps: - **No automatic fallback (`chat()`, ~line 262):** If the native endpoint rejects a request shape with a 400, the run fails. Falling back to compat is manual, via `TIME_ANTHROPIC_API=compat`. The PR description says this, but the code does not handle it. - **`_turns` is in-process only:** A kept thinking turn is reused only within the same process. After a restart, the turn is rebuilt without thinking blocks. That is fine while thinking is not enabled in the request, which it is not here. It would break if thinking gets turned on later. - **Finish line not verifiable pre-merge:** CI is green, but the `cached > 0` check on the first worker run can only be confirmed after merge. Someone should watch that run's ledger lines and the `usd` per step. <sub>review by haiku · claude-haiku-5-5 · via Time</sub>
teahubxyz added 1 commit 2026-10-18 04:57:29 +00:00
time merged commit bcdc71383e into main 2026-10-18 04:59:21 +00:00
time deleted branch ceo/anthropic-messages-cache 2026-10-18 04:59:21 +00:00
Sign in to join this conversation.