Skip to content

feat(llm): native Anthropic Messages API wire protocol "anthropic" - #47

Open
DamieMoon wants to merge 16 commits into
GottZ:rootfrom
DamieMoon:feat/anthropic-protocol
Open

DamieMoon wants to merge 16 commits into
GottZ:rootfrom
DamieMoon:feat/anthropic-protocol

Conversation

@DamieMoon

@DamieMoon DamieMoon commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #46 (fix/embed-backfill-budget): the first three commits here are that PR's; the rest are this one. A deploy of this branch carries both.

Summary

ctx learns a third wire protocol, anthropic: the native Anthropic Messages API (POST {base_url}/v1/messages, x-api-key + anthropic-version). Until now a hosted-Claude install had to reach Anthropic through its OpenAI-compatible layer with the openai protocol, which rejects every sampling parameter on the Claude 5 models (ctx always sends temperature), and — as observed on my install — drops response_format silently and offers neither prompt caching nor effort control, so a stripping sidecar sat in front of it. With this PR the chat-class roles (synthesis, translate, dream, digest, chat) talk to api.anthropic.com directly; the embed/rerank roles stay on a local backend, since the Messages API has no such endpoints.

What the wire does:

  • No sampling parameters, no thinking field, no tool_choice — the request carries model, max_tokens (from the pipeline's num_predict), the system prompt as a cached top-level block (cache_control: ephemeral), the messages, and output_config.effort.
  • Effort resolves model_map params.effort > row extra_body.effort > medium (the Claude Opus 5.5 default), validated to low|medium|high|xhigh|max. The chat harness carries params.effort too (it builds its options by hand and initially dropped it — caught in review).
  • Prompt cache that actually hits: the pipelines end their system prompt with a per-call promptguard nonce rule, so that rule goes into a second, uncached block and the stable prefix is the breakpoint; the web-chat stream path adds the top-level automatic cache_control for the growing history. cache_read_input_tokens / cache_creation_input_tokens land in context_llm_log.metadata; prompt_tokens is the total prompt (Anthropic's input_tokens is the uncached remainder only).
  • Streaming with tools for the web-chat harness (ChatStreamBackend, the new protocol dispatch of the stream path): the Messages API SSE events map onto the existing StreamEvent/StreamResult contract; ToolDef → input_schema, assistant tool calls → tool_use blocks, tool rows → tool_result blocks folded into one user turn. Three repairs keep a session alive: the answer's thinking blocks (when the model emits any) ride along in memory to the next iteration of the same turn and are replayed verbatim (the reference asks to keep them while a tool_use awaits its result), an unanswered tool_use from an interrupted turn gets a synthetic is_error result, and id-less tool calls from an openai-wire row earlier in the session get synthetic ids.
  • Stop reasons are mapped onto the vocabulary the consumers read (end_turn→stop, max_tokens→length, tool_use→tool_calls, refusal→content_filter); errors classify like every other wire (429 → rate-limit cooldown with Retry-After, 529 → gateway failover, 401 → auth, 400 → bad request), the same for the error types inside a mid-stream error event.
  • JSON mode is a documented no-op on this wire: output_config.format needs a JSON schema ctx does not carry, so ChatJSON sends the same body as Chat and the pipelines' parsers stay the validator (run Dream with CTX_DREAM_JSON_MODE=off, as the OpenAI-compatible layer already forced).

Row shape (422 on create/update): api_key_ref required, locality: external, num_ctx > 0 (no window discovery on this wire), provider_class: generic, no embed/dream-embed/rerank roles, /v1-root guard. Migration 155 widens the CHECK on context_backends.protocol (the baseline's unnamed constraint is resolved from pg_constraint); no rows change. backend-test probes GET /v1/models with the key; ctx backends seed accepts anthropic on the chat leg (two-host spec), ctx init asks the extra questions; the UI enum and docs (api.md, operations.md, architecture.md, README) carry the protocol.

Also in this PR: ci(lint) bumps the golangci-lint pin to v2.14.0 — the runner's GO_VERSION: "1.27" resolves to go1.27.2 since 2026-10-10 and v2.13.2 cannot read its export data (Lint was red on the root nightlies of 10 and 11 October with everything else green).

How it was verified

Unit (fake upstream, httptest, no key): request shape and headers (non-stream and stream, forbidden sampling keys absent), effort precedence, extra_body merge, JSON no-op, max_tokens fallback, empty-system omission, response decode incl. cache counters and llmlog telemetry, the stop-reason table, error → failover class (429 with Retry-After, 529, 401, 400, 500), SSE fixtures (text with thinking + ping, two tool calls with fragmented partial_json, mid-stream overloaded_error), lenient/hard stream endings, callback abort, thinking/redacted blocks carried and replayed first, orphan and empty-id repair, per-field usage merge, nameless tool_use dropped, the promptguard split (byte-identical prefix across nonces), the stream/non-stream cache_control placement, protocol dispatch across openai/ollama/anthropic rows, validation rules one by one, seed/wizard flows, the handler probe, the chat harness's effort pass-through, and the MW6 dispatch invariant (eighth wire site, Σ acquires == Σ wire contacts). -race clean on the touched packages.

Integration (testcontainers, prod-parity pgvector-timescaledb:pg18 built from db-image/): the full migration chain through 155, then TestMigration155ProtocolCheckAdmitsAnthropic pins the named constraint, that it admits anthropic, is the only CHECK naming the column, keeps the column's DEFAULT 'openai', inserts an anthropic row and rejects an unknown protocol with SQLSTATE 23514. The schema-manifest generator (-tags=genmanifest) ran on the same image and reproduced manifest.json byte for byte (the header had been bumped by hand first). The whole integration suite (-tags=integration -p 1 ./...) ran locally as well: 60 packages green, 10 tests in 6 packages red on this macOS/arm64 seat (armsweep ×3, rrf ×2, handler ×2, llmlogexport, dream, events) — each of them either fails identically on the base commit 218d5f84 (float last-digit parity, /tmp symlink semantics, a byte-identical response anchor) or passes in isolation at HEAD (two timing-sensitive tests under the serial run); none touches this branch's diff, and CI's six Linux shards are green.

Web: bun install --frozen-lockfile, bun run test (100 files, 995 tests) and bun run check (svelte-check, tsc, lint:css — 0 errors).

Live (claude-opus-5-5, 2026-10-11, env-gated TestAnthropicLive*, skipped in CI): the request shape is accepted; the prompt cache hits on the second call (cache_creation 3033 → cache_read 3033 with a fresh promptguard nonce per call); JSON-mode no-op still yields JSON; effort low and high accepted; the 1-token backend-test probe answers 200 with stop_reason: max_tokens and empty text (so the probe reports ok); a streamed tool round trip completes (tool_use → tool_result → final answer); a bad key → 401 → auth. Not exercised live: the thinking-block replay — Opus 5.5 emitted no thinking block on the short tool turn, even with thinking.display: summarized and effort high, and the block-less round trip was accepted; the replay path is covered by fixtures and follows the reference's rule.

Three independent adversarial review passes (wire vs the Claude API reference, tests vs code, docs vs code; and a pre-push pass on every later commit) produced the follow-up commits on the branch; the remaining review notes are recorded in the code comments.

Operator notes

  • Existing installs: Migration 155 runs at boot and changes no rows. To move a row, set protocol: "anthropic", base_url: "https://api.anthropic.com" (no /v1), keep api_key_ref and num_ctx; the sidecar/shim can go.
  • Thinking tokens count against max_tokens; if a role's rows show metadata.finish_reason = length, raise that role's model_map params.max_tokens.
  • Downgrading below 155 with an anthropic row in the pool dispatches it onto the wrong wire — disable or delete such rows first (operations.md, Migration 155).

🤖 Generated with Claude Code

DamieMoon and others added 11 commits October 9, 2026 04:34
… query path

Two gaps showed up on a CPU embed backend (Qwen3-Embedding-8B, the documented
~23 tok/s) with a ~10k-token block (issue damienmoon/ctx#1):

1. backfillOneEmbedding was called only at the top of runDreamLoop, so with
   dream.enabled=false (the default) or Dream switched off at runtime nothing
   embedded pending blocks in the background. runEmbedBackfillLoop is now its
   own goroutine, started regardless of Dream: a successful pick loops at once,
   an empty pick waits embed_backfill.interval (default 15 s), 0 = arm off,
   polled for a hot re-enable. The dream loop keeps its inline call; the
   tx-wrapped FOR UPDATE SKIP LOCKED pick makes two pickers safe (Welle-49).

2. backfillPending had a count cap (sync_cap) but no time budget: one slow
   block consumed the caller's whole deadline (120 s on the CLI client), the
   question's own embed was never admitted and the query answered 500, and the
   failure was memoized as class wire although the backend was only slow for
   this caller. embed_backfill.sync_budget (seconds, default 30, 0 = unbounded)
   bounds the whole pre-search loop per request; each wire call runs under
   what is left. A block that overruns it, or dies with the request context,
   is memoized with the new class caller_timeout: next_attempt_at = now(), the
   query path never picks it again (EmbedFailureQueryPathExcludedPredicate),
   the background arm picks it at once, and a later wire failure there
   reclassifies it as wire with the ordinary backoff.

Also: the "german detected" log line is now "non-english heuristic matched"
(the heuristic fires on Swedish, Danish and Finnish too; the translation was
right, the label was not).

Tests (Rot/Grün through the production functions):
- handler: TestBackfillPending_SyncBudget_Integration — SyncBudget=0 holds the
  request for the backend's full delay (red); SyncBudget=300ms returns inside
  the budget with the parent context intact, the backend sees the cancel, the
  memo is caller_timeout and due now, a second backfillPending makes no wire
  call, the background predicate still sees the block; a request cancel
  mid-embed classifies caller_timeout, not wire.
- events: TestEmbedBackfillArm_RunsWithoutDream_Integration — the arm embeds a
  pending block with DreamEnabled=false, picks a caller_timeout block at once,
  and Interval=0 turns it off.
- compose and description gates: TestComposeDeclaresEveryRegistryKey and
  TestKeyDescriptionsComplete pass with the two new keys.

Run on 2026-10-09 in golang:1.27-alpine with CTX_TEST_PG_IMAGE=localhost/
ctx-db:38fdb74f: go build ./..., go vet -tags=integration, go test -short for
config/store/handler/events (one pre-existing failure, TestRebuildViaWorker_
TimeoutKillsHangingChild, fails identically on the unpatched checkout in this
container), and the backfill integration suites of handler, events and store
all green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The comments and the operations paragraph said "issue damienmoon/ctx#1",
which on GitHub reads as a reference into a repository that does not exist
there. The issue lives on the author's Forgejo, a read-only mirror of the
GitHub fork: https://force-push.me/damienmoon/ctx/issues/1. Comments only;
no code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
embed_backfill.sync_budget and embed_backfill.interval are two new
typDuration keys. TestValidateRejectsNegativeOnEveryDurationKey pins the
count as a drift guard (45 before this branch) and failed in CI. Both keys
are covered by the generic V17 sign walk, which the test exercises per key;
the pin moves to 47 in the commit that explains why, as the comment above
it asks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ation 155

A fourth wire protocol, "anthropic", for the native Anthropic Messages API
(POST /v1/messages). This commit adds the constant and the row shape the
wire needs; the wire itself follows in go/internal/llm.

Validation (422 on create and update, protocol-bound so openai/ollama rows
keep their freedoms): api_key_ref required (the key travels as x-api-key),
locality external, num_ctx > 0 (no window discovery on this wire —
synthesis would fail closed on promptguard.ErrUndeclaredWindow),
provider_class generic, no embed/dream-embed/rerank roles (the Messages
API has no such endpoints; embed.go would silently dispatch them onto the
Ollama path), effort in extra_body or model_map params limited to
low|medium|high|xhigh|max, and the /v1 root guard extended (the wire
appends /v1/messages itself).

Migration 155 widens the CHECK on context_backends.protocol. The baseline
declared it inline and unnamed, so the DO block resolves the name from
pg_constraint by table and definition (the pattern 113 and 114 already
use) before re-adding it as context_backends_protocol_check.

Manifest: the schema contract records column types, indexes, functions,
triggers, rules and hypertables — no CHECK clauses — so a regeneration
after 155 changes only generated_against.migration_max (155) and
migration_count (43). Those two fields are bumped by hand here because the
generator needs Docker (testcontainers), which the authoring seat does not
have; the integration job's contract check verifies the manifest against
the live chain.

Tests: TestValidateAnthropicHappyPath, TestValidateAnthropicRules (one
mutation per rule, plus the valid effort set and the protocol-boundedness
check). docs/architecture.md names the protocol.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
chatWithFormat gains a third leg: ProtocolAnthropic dispatches to
chatAnthropic (anthropic.go), POST {base_url}/v1/messages with x-api-key
and anthropic-version headers (core headers set last, after
extra_headers, as setChatHeaders does). The request carries the system
prompt as a top-level text block with cache_control ephemeral, the user
prompt as the single message, max_tokens from NumPredict (16000 when the
caller sets none — the field is required on this wire), and
output_config.effort resolved from the role's model_map params
(Options.Extra) over the row's extra_body over the explicit default
medium. No sampling parameter is ever marshalled: the Claude 5 models
answer 400 to temperature/top_p/top_k, and the OpenAI-compatible layer ctx
used until now needed a stripping sidecar for exactly that. The think
toggle is ignored (thinking cannot be disabled on Opus 5.5; effort is the
depth control), logprobs are never requested (the decide primitive falls
back through its existing ErrNoLogprobs memo), and ChatJSON is a
documented no-op (output_config.format needs a JSON schema ctx does not
have; the pipelines' parsers stay the validator, the CTX_DREAM_JSON_MODE=off
contract). Other extra_body keys merge top-level, row over params.

Response: text blocks joined, thinking blocks skipped; PromptTokens is the
TOTAL prompt (input_tokens + cache_read + cache_creation — Anthropic's
input_tokens is the uncached remainder) so llmlog's prompt_tokens stays
comparable across wires; the two cache counters travel in new
ChatResponse fields and reach llmlog metadata through
ApplyProviderTelemetry (cache_read_input_tokens /
cache_creation_input_tokens, written only when non-zero so other wires'
rows stay byte-identical); the message id lands in provider_request_id;
cost stays NULL. stop_reason is mapped onto the vocabulary the consumers
read (end_turn→stop, max_tokens→length, tool_use→tool_calls,
refusal→content_filter, unknown raw, absent ""), because dream's cap
detection compares against "length" and the chat harness against
"tool_calls" — a raw pass-through would hide both. Non-2xx answers wrap
httpx.StatusError as on every wire (429 → RateLimit with Retry-After,
529 overloaded → Gateway failover, 401 → Auth, 400 → BadRequest, 500 →
ServerFault) with the provider's error type folded into the message.

Tests (httptest fakes, no live key): request shape and headers, effort
precedence, extra_body merge, JSON no-op, max_tokens fallback, empty
system omitted, response decode incl. cache counters and telemetry,
stop-reason table, error classification; anthropic rows in the wire-path
and finish-reason tables; the MW6 dispatch invariant gains the seventh
wire site (llm/anthropic.go pinned, Σ acquires == Σ wire contacts).

docs/api.md: the Anthropic paragraph under Backend pool.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ChatStreamBackend is the new protocol dispatch of the streaming path: it
takes the Backend (ChatStream took host/key/model loosely and never read
b.Protocol) and routes ProtocolAnthropic to chatStreamAnthropic
(anthropic_stream.go), everything else to ChatStream as before — ollama
rows included, which always streamed over /v1/chat/completions. The chat
harness (chat/engine.go runStream) calls it; ChatStream stays exported as
the openai implementation its tests drive.

The anthropic leg feeds the same StreamEvent/StreamResult contract: a
dedicated SSE parser keys on the data frame's own "type" (message_start →
prompt + cache counters; content_block_start tool_use → pending call and
the ToolCallName event; text_delta → ContentDelta; input_json_delta →
arguments fragments, assembled into the object form ToolCall carries;
message_delta → mapped stop reason and output_tokens; message_stop →
done; error → *StreamError with the provider's error type; ping,
thinking_delta and signature_delta ignored). Lenient ending as on the
openai parser: EOF after message_delta is a complete turn, EOF before it
is a hard error.

Message mapping: system rows lift into the cached top-level system block,
an assistant row with tool calls becomes [text?, tool_use…] (unparseable
arguments replay as {} — the executor already answered them ok:false), a
tool row becomes a user turn with a tool_result block, and consecutive
same-role turns fold into one message (the API requires alternation; a
parallel-call turn yields several tool rows). ToolDef becomes
{name, description, input_schema}; no tool_choice (a 400 on Claude Opus
5.5; auto is what the openai wire sends too).

StreamResult gains the cache counters and the message id; recordLLM
writes them into the web-chat llmlog row (metadata.cache_read_input_tokens
/ cache_creation_input_tokens / provider_request_id) through the shared
ApplyCacheTelemetry.

Tests: three hand-written fixtures from the Messages API event reference
(content with thinking + ping, two tool calls with fragmented
partial_json and an empty-input call, mid-stream overloaded_error);
lenient and hard endings; 529 → ClassGateway via errors.As; callback
abort; the full tool round-trip request shape (headers, cached system,
folded tool results, input_schema, effort, forbidden keys absent); the
dispatch across openai/ollama/anthropic rows; message edge cases. The
MW6 dispatch invariant drives the eighth wire site (llm/anthropic_stream.go
pinned, Σ acquires == Σ wire contacts holds at 8/8 and 10/10).

docs/api.md: the web-chat harness paragraph names the per-protocol stream.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The operator surfaces of the new wire protocol.

backend-test: probeBackendURL branches on the protocol before the
provider class — an anthropic row probes GET {base_url}/v1/models with
x-api-key + anthropic-version (no Bearer header); the generic branch
would have hit the bare root with a Bearer header, which the API answers
404 (reported as unreachable) or 401. The 1-token chat probe runs through
llm.Chat as before and so takes the anthropic wire automatically.

ctx backends seed: the chat leg accepts protocol anthropic and is
validated on the spec, before any secret is sealed — the embed leg
rejects it (the Messages API serves no embeddings), a keyless leg and a
leg without num_ctx (no window discovery on this wire) fail with the
reason. The same rules the server enforces as 422s, one layer earlier.

ctx init: the protocol prompt names anthropic; choosing it turns the
single-host wizard into a two-host one — it asks the chat model's
context window (default 100000), a separate embedding host and its
protocol (ollama default), and goes straight to the no-echo key prompt
instead of the "does the host need a key?" detour.

Web UI: 'anthropic' in the PROTOCOLS enum of the backend dialog.

Tests: seed error table (anthropic on embed / keyless / no num_ctx) and
the mixed-protocol happy path; the wizard flow with the scripted ask
(window default, separate embed host, key sealed once, not echoed, not
attached to the embed row); the handler probe against an httptest fake
(path, headers, base_url/auth ok).

Docs: operations.md (Backends — the two-host SeedSpec example and the
spec-level rules; Models & hardware; the CTX_DREAM_JSON_MODE row's
anthropic clause; the Migration 155 section with the downgrade note),
README's engine-agnostic sentence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
chatOptions builds the stream call's Options by hand (the harness owns
NumPredict and the think toggle, so it never runs applyModelParams) and
copied only temperature and top_p from the chat role's params. The
anthropic wire resolves output_config.effort from Options.Extra["effort"]
first — the slot applyModelParams fills on the non-stream path — so a row's
model_map.chat.params.effort was honoured by synthesis and dream but
silently ignored by web chat, which sent extra_body.effort or the default
medium instead. docs/api.md promised the params-over-extra_body precedence
for both. Found by the adversarial review; the existing tests injected
Options.Extra by hand and never went through chatOptions.

chatOptions now lifts params.effort into Options.Extra. Test:
TestChatOptionsCarriesModelMapEffort (and the nil slot without the param).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the tail

Follow-ups from the three-way review of the anthropic wire against the
claude-api reference, the tests and the docs. Each one is a request the
Messages API would have answered 400 to, or a cache write paid for
nothing.

Thinking blocks ride along. The Claude 5 models think on every answer and
require the FINAL assistant turn of a tool loop to start with its thinking
block (empty text under the default display, the signature carries it).
The parser now re-assembles thinking (thinking_delta + signature_delta,
sealed on content_block_stop) and redacted_thinking blocks into
StreamResult.ProviderBlocks; the harness puts them on the assistant
ChatMsg it appends for the next iteration (ChatMsg.ProviderBlocks,
json:"-" so the OpenAI wire never sees it, not persisted — later turns
replay without them, which the API accepts for non-final turns), and
anthropicMessages emits them first in the assistant turn. Without this
the second iteration of every tool-using turn was a 400.

Orphaned tool_use is repaired. A turn interrupted between the model's
tool call and the tool's result leaves an assistant tool_use without a
tool_result in the persisted history, and the API rejects that on every
later turn of the session. anthropicMessages now answers every unanswered
tool_use with a synthetic is_error tool_result before the next user text
(and at the end of the history). Tool calls without ids — an openai-wire
row earlier in the session; llama.cpp streams calls without one — get
synthetic ids, matched in order to the tool rows that follow.

Prompt cache actually hits. Every pipeline ends its system prompt with
promptguard.Rule(nonce), fresh per call; one cached block carrying it
would be written on every call and read never (cache_creation > 0,
cache_read == 0, a pure 1.25x surcharge). anthropicSystem splits the rule
into a second, unmarked block, so the stable prefix is the breakpoint.
The stream path additionally sends the top-level automatic cache_control,
so the growing history is read from cache across iterations and turns.

Smaller: message_delta usage copies each input counter only when it is
itself present, so a delta carrying input_tokens alone cannot zero the
cache counters; a nameless tool_use block (proxy artefact) is dropped
instead of dispatched as an empty call; StreamError exposes its provider
error type and backends.Classify maps the anthropic types inside a
mid-stream error event onto the classes the equivalent status codes get
(overloaded_error → gateway failover, rate_limit_error → rate limit,
authentication_error → auth, invalid_request_error → bad request,
api_error → stop) — OpenRouter's numeric codes keep the stop default;
handleFrame split into per-event methods (cyclop); the package doc names
the model floor (effort is always sent) and why raw net/http over the Go
SDK.

Tests: thinking/redacted blocks carried and placed first (fixture now
opens with the thinking block Opus 5.5 always sends), orphan and empty-id
repair, per-field usage, nameless tool_use, top-level cache_control on
the stream path and its absence on the non-stream path, the promptguard
split (byte-identical prefix across nonces), StreamError classification.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mbed key

ctx init, anthropic branch: a non-numeric context window ("100k") no
longer falls back to 100000 silently — the step aborts with the reason
before any secret is sealed or any row written (a typo'd window would
have seeded a tenth of the prompt budget the operator meant). The
separately asked embedding host gets the same "does it need an API key?"
question the single-host flow has, so a keyed openai embedding service
can be seeded next to the Anthropic chat row.

probeBackendURL: the comment claimed the chat probe never runs after a
401 on /v1/models; it does (the row counts as reachable) and 401s the
same way, billing nothing — the comment now says so.

Tests: TestInitBackendsStepAnthropicRejectsBadWindow; the wizard flow
test answers the new embed-key question.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…re and UI help

docs/api.md: the complete stop-reason mapping (stop_sequence, pause_turn,
model_context_window_exceeded were applied but not listed), the 16000
max_tokens fallback, the model floor of the always-sent effort field, the
mid-stream error classification, the refusal-after-partial-text
behaviour, the three session repairs and the two cache breakpoints of the
stream path, and the OpenAI-compatible-layer claims marked as what they
are — observed on the ku-heimdall install, not reference facts.

docs/operations.md: the first-run step names the anthropic wizard
questions; the Migration 155 downgrade note describes both old
fallthroughs (Ollama path on the pipelines, OpenAI SSE on web chat) and
that the pool loads the row unvalidated. docs/architecture.md: the
transport-retry enumeration names the anthropic wire.

go/internal/config/validate.go: the V4 comment lists the four protocols.
Web: ModelMapEditor's help text names the Anthropic wire (and the effort
key's destination); settings.ts' dormant protocol mirror and its test
carry 'anthropic' so the declared mirror is true again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@DamieMoon
DamieMoon marked this pull request as draft October 11, 2026 10:32
DamieMoon and others added 5 commits October 11, 2026 12:37
…hain

The Lint job fails on every run since 2026-10-10 (nightly on root
38019738113 and 38107728729, this PR's 38132148410), all other jobs
green: GO_VERSION "1.27" now resolves to go1.27.2 on the runner, and the
pinned v2.13.2 cannot read its export data — "export data version 5 is
greater than maximum supported version 4", reported as a typecheck error
on the first `import "fmt"` (internal/clustersql/clustersql.go:28).
v2.14.0 (2026-09-24) reads it; locally against go1.27.2 with the same
.golangci.yml it reports 0 issues over ./..., and the deadcode gate is
green. Deliberate pin bump, as the comment in the workflow asks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hropic.com

Skipped under -short and without CTX_ANTHROPIC_SMOKE_KEY, so CI never
spends tokens. By hand it proves what the httptest fakes cannot: the
request shape is accepted by the real endpoint, the prompt cache hits on
the second call (stable prefix above 512 tokens, fresh promptguard nonce
per call), the thinking block replays through a streamed tool round trip
(and what the API says about the same round trip without it), effort low
and high are accepted, the 1-token backend-test probe's behaviour on a
thinking model, and a bad key classifying as auth. Every probe logs the
usage it saw (tokens, cache counters, message id).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Integration-tagged: the full migration chain on the prod-parity image,
then the constraint context_backends_protocol_check exists by name,
admits 'anthropic' next to openai/ollama/rerank, is the only CHECK on
the table that mentions the column (the DO block's catalog lookup stays
unambiguous), an anthropic row inserts, and an unknown protocol fails
with SQLSTATE 23514 on that named constraint. Ran locally against
testcontainers (colima) — PASS in 2.2 s; the schema-manifest generator
run on the same image reproduced the hand-bumped manifest.json byte for
byte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n observed 400

The adversarial review of the last three commits caught an overstatement:
docs/api.md, the anthropic_stream.go comments and a test comment said the
API "requires" the final assistant turn of a tool loop to start with its
thinking block. The claude-api reference asks that a turn whose tool_use
still awaits its result KEEP its thinking block and rejects an altered
one; live on 2026-10-11 (claude-opus-5-5) a short tool turn came back
with no thinking block at all — even with thinking.display summarized
and effort high — and the round trip was accepted. The replay mechanism
stays (it honours the reference's rule whenever a block is produced, and
the fixtures cover that path); the wording now says exactly that.

The live smoke test reports when the replay path was not exercised
instead of logging an "accepted without the block" line that compared
two identical requests, asks for visible reasoning on the tool turn, and
its header says that the integration job (which runs without -short)
relies on the absent key alone. The Migration 155 integration test also
pins that the column's DEFAULT 'openai' survives the constraint swap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ay commit

"accepted either way" said two variants were sent when the live run sent
one (no block was produced); the stale header line said thinking deltas
are ignored while the parser accumulates them into the replay block; the
live test's stripped-block log no longer calls a rejection "expected"
(the reference only asks to keep the block). In the live test the second
and the stripped third round-trip call now send the same effort ("high")
as the first — an effort change invalidates the messages cache by itself
and would mask the prefix question — and the second call's NumPredict
rises 1024 → 2048 to match the first, headroom for the high-effort turn.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@DamieMoon
DamieMoon marked this pull request as ready for review October 11, 2026 11:14

This branch has not been deployed

No deployments
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.

1 participant