Research: connector contract v3 prior art and probes (reasoning state, spend, wire)

Serves eureka-body cut connector-contract-v3 and questions connector-v3-wire,
connector-v2-coexistence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-28 07:27:59 +01:00
commit 725f5a5a75

View file

@ -0,0 +1,222 @@
# Connector contract v3: prior art and probes
Serves: cut `eureka-body:cut_spec:cut-connector-contract-v3.r1` (map line B3,
GameCult/ModelConnector), questions `eureka-body:question:connector-v3-wire` and
`eureka-body:question:connector-v2-coexistence`, and the map line `connector-spend`
(B3b), which is mapped from section 2 when it is next. Also answers follow-up
`eureka-body:follow_up:connector-reasoning-state-cheap-providers` (section 1d) and
`v3-ciphertext-as-bytes` (section 4).
Sections 1-3 are an Eyes facts pass (2026-10-28, session `self-2026-10-28-eureka`), fetched
from current primary docs; UNVERIFIED marks what it could not confirm. Section 4 is
Imagination's probes on Yggdrasil against ModelConnector `ec0519d` and CultLib `a4eae90`.
Section 5 is what Imagination drew from them for the cut. Recommendations live in the
questions; this page is their evidence.
Facts only, each with a pointer. Fetched 2026-10-28 from current docs. UNVERIFIED marks what I could not confirm.
Provider docs have moved hosts: docs.claude.com -> platform.claude.com/docs; platform.openai.com -> developers.openai.com/api/docs.
Model names in current Anthropic docs: Fable 5.1, Mythos 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5 (and older).
## 1. Opaque / reasoning state a caller must replay
### 1a. Anthropic Messages API
Pointers: T = https://platform.claude.com/docs/en/build-with-claude/thinking ; TS = .../thinking-troubleshooting ; P = .../preserved-thinking (linked from T, NOT fetched)
- Thinking arrives as `thinking` content blocks (fields `thinking`, `signature`) and `redacted_thinking` blocks (field `data`, opaque, encrypted). [T "Thinking encryption", "Redacted thinking blocks"]
- `signature` holds the full encrypted thinking; API uses it to verify blocks were generated by Claude; opaque, "don't interpret or parse". Streaming: arrives as `signature_delta` in a `content_block_delta` just before `content_block_stop`. [T "Thinking encryption"]
- `display: "omitted"` (default on newer models) returns thinking blocks with empty `thinking` string and populated `signature`; such a block is complete and must be replayed; text put into the empty field is ignored not rejected. [TS "The thinking field is empty"; T "Preserving thinking blocks" Note]
- Replay rules [T "Thinking with tool use", "Preserving thinking blocks"]:
- Required: within a tool-use turn, pass thinking blocks back, complete and unmodified, with the `tool_use` block they accompanied.
- Recommended: across turns pass everything back. Allowed: outside tool use, omit prior turns' thinking.
- A tool-use loop is one assistant turn. Consecutive thinking blocks in the latest assistant message must match what the model generated: no reorder, edit, partial drop; includes `redacted_thinking`.
- Filtering by `block.type == "thinking"` silently drops `redacted_thinking` and breaks the protocol. [T "Redacted thinking blocks" Tip]
- Errors [TS]:
- Modified/dropped in latest assistant message: 400 `invalid_request_error`, message contains "`thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified".
- Signature present but fails verify (truncated/altered/empty): 400 "Invalid `signature` in `thinking` block" (ends there; no prefix clause).
- Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5: replayed block valid only while `system`, `tools` and preceding messages are unchanged ("prefix check"): 400 "...The block is bound to a different conversation. Remove the block, or set `thinking.block_binding.prefix_mismatch_behavior` to "drop_block"." Enforced by default for accounts created on/after 2026-08-31; older accounts only when the request sets that field. Option needs beta header `thinking-binding-controls-2026-08-01`; response then carries an `input_transformations` array listing dropped blocks. [T "Preserved thinking"; TS]
- Mid-turn toggle of thinking: no error; API silently disables thinking for that request and may strip blocks. [T "Thinking with tool use"]
- Manual mode (`type:"enabled"`): final assistant turn must start with a thinking block; `type:"enabled"` is 400 on 4.7+ models. [extended-thinking page "Turn structure in manual mode"; TS table]
- Earlier-turn thinking stripped or kept: per model. Keep all prior turns: Opus 4.5+, Sonnet 4.6+, Haiku 5.5, Fable/Mythos 5/5.1. Keep last turn only: earlier Opus/Sonnet and Haiku through 4.5; API strips automatically on pass-back. Kept blocks bill as input. Override: `clear_thinking_20251015` context-editing strategy. [T "Thinking block preservation by model", "Preserving thinking blocks"]
- Portability:
- Across platforms: `signature` values are compatible across Claude API, Bedrock, Google Cloud (Google managed); exception: Sonnet 5.5 and Haiku 5.5 blocks work only in the producing account or a linked account. [T "Thinking encryption"; P#account-bound-thinking not fetched]
- Across models: "readable only by the model that produced it and certain other models"; API drops unreadable blocks without error and without billing. Fable 5.1 reads Opus 5 and (Claude API) Opus 5.5 blocks; Opus 5/5.5 do NOT read Fable 5.1 blocks; Fable 5.1 and Mythos 5.1 read each other only. Switching to them keeps reasoning; away from them drops it. [T "Thinking block preservation by model: Switching models", "Preserved thinking"]
- Prompt caching [T "Thinking and prompt caching"]:
- Thinking config and resolved effort are rendered into the prompt; changing mode, `budget_tokens` or effort invalidates message-level breakpoints (and may invalidate tool/system ones).
- Thinking blocks in a tool loop are cached automatically with tool results and count as input when read from cache.
- Last-turn-only models: after a non-tool-result user message, earlier thinking is stripped from context/cache.
- 1-hour cache suggested for long thinking sessions.
- Billing: thinking tokens billed as output; `usage.output_tokens_details.thinking_tokens` reports them (final `message_delta` when streaming). [extended-thinking page "Budget rules and tuning"]
Stop reasons (https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons, first 100k chars read):
- `end_turn`, `max_tokens`, `stop_sequence`, `tool_use`, `pause_turn` (server-tool loop hit iteration limit, default 10; resend assistant content to continue), `refusal` (HTTP 200, `stop_details` names policy category, null for other reasons), `model_context_window_exceeded` (treat as truncated).
- Streaming: `stop_reason` is null in `message_start`, appears in `message_delta`. `max_tokens` can end in an incomplete `tool_use` block.
Usage object (https://platform.claude.com/docs/en/build-with-claude/prompt-caching "Usage fields"):
- `input_tokens` = tokens after last cache breakpoint (uncached); `cache_creation_input_tokens` = 5m + 1h writes; `cache_read_input_tokens`; `cache_creation{ephemeral_5m_input_tokens, ephemeral_1h_input_tokens}`; `output_tokens`. total_input = cache_read + cache_creation + input_tokens.
- `output_tokens_details.thinking_tokens` (above). `server_tool_use` and `service_tier` fields: only seen via fetch-tool general-knowledge answer, API reference page was truncated before the schema: UNVERIFIED against docs. Token counts depend on tokenizer: Claude 4.7+ models (incl. Fable/Mythos 5/5.1) count ~30% more tokens for the same text. [token-counting page "Supported models", "Token counts on Claude Fable and Claude Mythos models"]
- Min cacheable prompt: 512 (Fable/Mythos/Opus 5.5/Opus 5/Sonnet 5.5/Haiku 5.5), 1024 (Opus 4.8, Sonnet 5/4.6/4.5), 2048 (Opus 4.7, Mythos Preview), 4096 (Opus 4.6/4.5, Haiku 4.5). Lookback 20 block positions. [prompt-caching page]
### 1b. OpenAI Responses API
Pointer: https://developers.openai.com/api/docs/guides/reasoning (via fetch summary)
- Reasoning items appear in `output`; opaque, no reasoning text returned (optional `summary`). Example item id prefix `rs_`; page gives no other id rules.
- Stateless mode (`store:false` or ZDR org): reasoning items include `encrypted_content` by default; `include: ["reasoning.encrypted_content"]` still accepted as legacy/no-op.
- Replay: stored -> `previous_response_id`. Manual -> pass prior output items. Function calling: pass back reasoning items returned with the last function call plus outputs; with several calls in a row, pass back all reasoning, call and output items since last user message. `all_turns` with `store:false`: keep every output item (encrypted reasoning, assistant `phase`) and replay full history. `reasoning.context` in the response reports effective mode (`current_turn`/`all_turns`).
- Other models: reusable only within the same model family; switching families, API omits incompatible reasoning even with `all_turns` (GPT-5.6 Sol/Terra/Luna interchange, not GPT-5.5).
- Usage: `usage.output_tokens_details.reasoning_tokens`, `usage.input_tokens_details.cached_tokens`; reasoning billed as output.
### 1c. Google Gemini
Official pages fetched were thin or truncated:
- https://ai.google.dev/gemini-api/docs/thought-signatures redirects to thinking guide; page that did render describes the Interactions API: signatures are "encrypted representations of the model's internal reasoning"; limited to `thought` steps and built-in tool steps; stateless mode "MUST always resend all `thought` blocks exactly as they were received"; stateful (`store:true`, `previous_interaction_id`) server handles it; on `generateContent` signatures "can be attached to any part, such as living inside `functionCall` parts or the final part of a response"; streaming `thought_signature` delta arrives in last delta before `step.stop`; a thought block may carry only a signature.
- https://ai.google.dev/gemini-api/docs/gemini-3 : stateless callers must include thought blocks with signatures (summary only).
- `generateContent` function-calling rules (first functionCall part in parallel calls, each step in sequential, 400 on missing, `skip_thought_signature_validator` dummy): UNVERIFIED in official docs (Vertex page docs.cloud.google.com/vertex-ai/generative-ai/docs/thought-signatures body was truncated in fetch).
- Observed third-party evidence (not official): error text "Unable to submit request because function call `X` in the N. content block is missing a `thought_signature`" and reports that only the first 1-2 parallel calls get signatures, https://discuss.ai.google.dev/t/gemini-3-flash-preview-inconsistent-thought-signature-generation-in-parallel-function-calls-causes-400-errors-and-potential-silent-data-loss/118936 ; thread quotes Google doc line "If you use the official SDKs and standard chat history, thought signatures are handled automatically". Text turns without a signature reportedly don't 400 (apiyi FAQ, https://docs.apiyi.com/en/faq/gemini-thought-signature-missing, third party). "Signature present but fails validation" is a distinct 400 reported when route/model/key changes mid-conversation (same FAQ, third party).
### 1d. OpenAI-compatible providers (cheap-providers follow-up)
| Provider | Field | Form | Must replay in tool loop | Across turns | Pointer |
|---|---|---|---|---|---|
| DeepSeek (deepseek-flash, deepseek-v4-pro on page) | `reasoning_content` (sibling of content) | plaintext | Yes. With `tools`: omitting it -> documented 400. Each sub-request in a turn's loop must carry that turn's reasoning even if no tool call | With tools: pass back all previous turns' reasoning (concatenated into context). Without tools: ignored if sent, not carried | https://api-docs.deepseek.com/guides/thinking_mode |
| Moonshot Kimi | `reasoning_content` at level of `content` | plaintext | Within a task keep all and send each request; append complete assistant message unchanged. Page names no error for omission | Per model: kimi-k3.1-pro/k3 require it multi-turn; k2.7-code always keeps; k2.6 drops by default unless `thinking.keep:"all"` | https://platform.kimi.ai/docs/guide/use-kimi-k2-thinking-model (page covers k2.6/k2.7/k3, not "K2 thinking" naming) |
| Zhipu GLM (z.ai) | `reasoning_content`; request `thinking:{type, clear_thinking}` | plaintext; page silent on signing | Return with the assistant message that made tool calls; interleaved thinking on by default | "Preserved thinking": `clear_thinking:false` keeps earlier turns (default on for Coding Plan endpoint, off standard); when on, must return complete unmodified; reorder/edit "may degrade" and lowers cache hits. GLM-5.3 cannot disable thinking | https://docs.z.ai/guides/capabilities/thinking-mode |
| Qwen (Alibaba Model Studio) | `reasoning_content`; `enable_thinking`, `thinking_budget`, `preserve_thinking` | plaintext | Page doesn't say; links to function-calling docs | `preserve_thinking:true` (qwen3.8-max, 3.7-max/plus, 3.6-plus, some Kimi) makes model read historical reasoning, billed as input; omission not an error | https://www.alibabacloud.com/help/en/model-studio/deep-thinking |
| MiniMax (page = M3.1-Flash; M2 not on that page) | `reasoning_content` (OpenAI) / `thinking` blocks (Anthropic SDK) | plaintext | Append entire assistant message incl. `reasoning_content`; "essential" for best performance; no benchmark or error stated. Thinking is optional per turn | n/a | https://platform.minimax.io/docs/guides/text-m2-function-call . M2-specific `<think>` tags / `reasoning_details` / `reasoning_split`: UNVERIFIED (not on fetched page) |
- None of these pages describe signatures or encryption; the DeepSeek 400 is the only documented hard failure.
### 1e. Multi-provider libraries
- LiteLLM (https://docs.litellm.ai/docs/reasoning_content): `reasoning_content` string on message "returned across all providers"; `thinking_blocks` list (Anthropic only; `type`, `thinking`, `signature`) must be included in the assistant message with `tool_calls` when returning tool results; `provider_specific_fields` unspecified (Bedrock example `reasoningContentBlocks[].reasoningText{signature,text}`). Known failure: Anthropic 400 when `thinking_blocks` missing ("assistant message doesn't start with a thinking block"). OpenAI-compatible clients (LibreChat, Open WebUI, Vercel AI SDK) ignore `thinking_blocks` and lose them. Anthropic->DeepSeek switch needs `drop_params=True`. `litellm.modify_params=True` disables `thinking` for a turn when blocks are missing.
- OpenRouter (https://openrouter.ai/docs/guides/best-practices/reasoning-tokens): `reasoning_details` array at `choices[].message` / `choices[].delta`; common fields `id` (string|null), `format`, `index`; types `reasoning.summary` (`summary`), `reasoning.text` (`text`, optional `signature`), `reasoning.encrypted` (`data`; may stream as `[REDACTED]`). `format` values: unknown, openai-responses-v1, azure-openai-responses-v1, bedrock-openai-responses-v1, bedrock-xai-responses-v1, xai-responses-v1, meta-responses-v1, anthropic-claude-v1 (default), google-gemini-v1. Must preserve full consecutive sequence, no rearrange/modify. `reasoning` string field for plain-string models; `reasoning_content` alias. Doc claims switching OpenAI/Anthropic works without code change.
- Vercel AI SDK: `sendReasoning` (default true) controls whether reasoning is included in requests; Anthropic provider surfaces `providerMetadata.anthropic.inputTransformations`; `blockBinding.prefixMismatchBehavior` option; `providerOptions` escape hatch; between-tools thinking blocks "sent back unchanged on the next step" (https://ai-sdk.dev/providers/ai-sdk-providers/anthropic, https://ai-sdk.dev/docs/ai-sdk-core/reasoning). Thinking `signature`/`redactedData` parts: UNVERIFIED (not on fetched pages).
- Cross-provider replay failure evidence: LiteLLM page above (Anthropic<->DeepSeek, UI clients dropping fields); Gemini third-party reports of dropped `thoughtSignature` by OpenAI-compatible clients (https://discuss.ai.google.dev/t/openai-api-compatibility-broken-due-to-thought-signature-on-gemini-3-pro-preview/109823 , title only, body not fetched).
## 2. Spend accounting and budget enforcement
### 2a. Usage and money
- Anthropic usage: tokens only (fields in 1a); no money field seen in docs fetched. Cost must be computed from pricing page. UNVERIFIED that the full Message schema has no cost field (schema page truncated).
- OpenRouter: `usage.cost` = total charged in credits; `usage.cost_details.upstream_inference_cost` (BYOK only via generation endpoint); `usage.prompt_tokens_details.cached_tokens` and `.cache_write_tokens`; `usage.completion_tokens_details.reasoning_tokens`; `usage:{include:true}` deprecated/no effect, full usage always present; streaming usage in last SSE message. https://openrouter.ai/docs/guides/guides/usage-accounting
- OpenAI: tokens (1b).
### 2b. LiteLLM
- Per-request cost in header `x-litellm-response-cost`; spend log field is `spend` (USD). The cost-tracking page does not name a `response_cost` field or `model_prices_and_context_window.json`; says it uses the "model cost map" and can sync from GitHub. https://docs.litellm.ai/docs/proxy/cost_tracking (file name `model_prices_and_context_window.json`: UNVERIFIED on page). Zero-priced usage on non-zero-rate model logs spend 0 with WARNING and counter `litellm_zero_cost_requests_total`.
- Budgets: `max_budget` on keys (`/key/generate`; `budget_limits` multiple windows), users, teams, team members, customers; global `litellm_settings.max_budget`; `budget_duration` ("30s/30m/30h/30d"; unset = never resets); reset check default every 10 min. https://docs.litellm.ai/docs/proxy/users
- Timing: with budget reservation (default on) LiteLLM estimates max cost from the request and reserves before calling; rejects pre-provider if reservation exceeds budget; after response, reservation is replaced with actual cost. Batch: reservation released, final cost recorded at completion. Zero-cost models allowed over budget.
- Errors vary by scope: `ExceededTokenBudget` (key), `budget_exceeded` code 400 or 429 (per-model), `ExceededBudget` with 401 (customer), 429 (agent sessions); `fail_closed_budget_enforcement: true` -> 503 when budget cannot be verified.
- No DB: "Budgets require a database". Global `max_budget` fails open (warning, requests continue); key/team/user budgets unavailable ("No connected db.").
### 2c. OpenRouter limits
https://openrouter.ai/docs/api-reference/limits
- Per-key `limit`, `limit_reset`, `limit_remaining`. HTTP 402 with `error.metadata.limit_source`: `openrouter_key_limit`, `openrouter_credits`, `openrouter_in_flight_budget` (wait `Retry-After`).
- In-flight budget (certain prepaid accounts): request whose estimated cost doesn't fit is rejected pre-provider even with positive balance; estimate = input tokens + completion tokens allowed by `max_tokens` (fixed per-request cap if unset), excludes per-request fees/plugins/images; charged on finish.
- Negative balance causes errors incl. free models.
### 2d. Pre-call reservation / bound patterns
- LiteLLM reservation (2b) and OpenRouter in-flight estimate (2c) both = estimated input + max_tokens-bounded output, hold, settle on actual.
- Anthropic token counting: `POST /v1/messages/count_tokens`, same inputs as Messages (system, tools, images, PDFs, thinking blocks); returns `{input_tokens}`; free; separate rate limit (5,000/10,000/20,000 RPM by tier Start/Build/Scale); result is an "estimate" and may include system-added tokens not billed; no caching logic; rejects server tools, MCP connector, url/file image/document sources; uses tokenizer of passed model. https://platform.claude.com/docs/en/build-with-claude/token-counting
- Portkey / Helicone / cloud billing ledgers: NOT searched (budget). UNVERIFIED.
### 2e. Anthropic pricing (https://platform.claude.com/docs/en/about-claude/pricing, $/MTok: input / 5m write / 1h write / cache read / output)
- Fable 5.1 and Mythos 5.1: 10 / 12.50 / 20 / 0.25 / 50 (read = 0.025x)
- Opus 5.5: 4 / 5 / 8 / 0.20 / 20 (read = 0.05x)
- Opus 5, 4.8, 4.7, 4.6, 4.5: 5 / 6.25 / 10 / 0.50 / 25
- Sonnet 5.5: 2 / 2.50 / 4 / 0.10 / 10 (read = 0.05x)
- Sonnet 5: 2 / 2.50 / 4 / 0.20 / 10 ; Sonnet 4.6, 4.5: 3 / 3.75 / 6 / 0.30 / 15
- Haiku 5.5: prompts <=100k tokens 0.10 / 0.125 / 0.20 / 0.01 / 0.50 ; prompts >100k 0.50 / 0.625 / 1 / 0.05 / 2.50 (long-context tier keyed on prompt size)
- Haiku 4.5: 1 / 1.25 / 2 / 0.10 / 5
- Multipliers: 5m write 1.25x, 1h write 2x, read 0.1x base unless noted above. `inference_geo:"us"` = 1.1x on all categories (4.6+; 400 on earlier models). Batch API discount: table exists on page (lines show batch input/output at about half), exact multiplier not extracted. Multipliers stack with batch and residency.
- Opus 5.5 / Sonnet 5.5 / Haiku 5.5 / Fable 5.1 listed as current at fetch time.
## 3. Wire choice for a typed local RPC
### 3a. MCP (spec revision 2026-07-28 is "latest" at fetch)
- JSON-RPC 2.0, UTF-8. Standard transports: stdio (newline-delimited) and Streamable HTTP (POST per message; JSON or request-scoped SSE reply). https://modelcontextprotocol.io/specification/latest/basic/transports
- Custom transports allowed; those over reliable byte streams (Unix sockets, TCP) "SHOULD reuse the stdio framing" (same page).
- Versioning: latest revision has NO initialize handshake; every request carries protocol version in `_meta.io.modelcontextprotocol/*` (HTTP also `MCP-Protocol-Version` header); unsupported -> error -32022 with `data{supported[], requested}`; client retries; `server/discover` MUST be implemented. Legacy (<= 2025-11-25) used `initialize` negotiation. Dual-era servers MAY serve both on one endpoint, selecting by how the client opens (modern `_meta` vs `initialize`). https://modelcontextprotocol.io/specification/latest/basic/lifecycle (rendered as "Versioning and Compatibility")
- Binary: base64 strings paired with `mimeType` (`ImageContent.data`, `AudioContent.data`, `BlobResourceContents.blob`).
- Error codes: -32700, -32600, -32601, -32602, -32603 (JSON-RPC); -32020 HEADER_MISMATCH, -32021 MISSING_REQUIRED_CLIENT_CAPABILITY, -32022 UNSUPPORTED_PROTOCOL_VERSION. https://modelcontextprotocol.io/specification/2026-07-28/schema
### 3b. gRPC / protobuf (https://protobuf.dev/programming-guides/proto3/)
- Unknown fields preserved on parse and re-serialized (lost when going through JSON or field-by-field copy). Never reuse field numbers; reserve removed numbers/names. Adding fields, removing (no reuse), adding enum values are wire-safe; changing a field number or moving a field into an existing oneof is not. Unknown enum values preserved (open enums store int; closed enums, e.g. Java, special-case).
- `bytes`: arbitrary byte sequence (length limit garbled in fetch).
- `package` prevents name clashes; page does NOT frame packages as a versioning mechanism ("versioning by package names" claim: UNVERIFIED on this page).
### 3c. MessagePack / rmp-serde
- msgpack spec: bin 8/16/32 = 0xc4/0xc5/0xc6 with 8/16/32-bit BE length; str family separate (0xd9-0xdb, fixstr 0xa0-0xbf); ext 0xc7-0xc9 and fixext 0xd4-0xd8, app ext types 0-127, -1 = Timestamp; array 0x90-0x9f/0xdc/0xdd; map 0x80-0x8f/0xde/0xdf. Spec recommends deserializers accept bin and str8 in minor releases, serializers distinguish in major. https://github.com/msgpack/msgpack/blob/master/spec.md
- rmp-serde: serde derive treats `Vec<u8>` as an array of values (about 50% overhead per docs); wrap with `serde_bytes` or use `Serializer::with_bytes` to emit bin. Re-exports `to_vec` and `to_vec_named` (the page does not explain the struct encoding difference; array-vs-map: UNVERIFIED here). Unknown-field handling not covered. https://docs.rs/rmp-serde/latest/rmp_serde/
- Cap'n Proto: not searched. UNVERIFIED.
### 3d. Keeping two wire versions alive
- MCP dual-era server (3a): one endpoint serves both generations; discriminator is how the first message opens (per-request `_meta` vs `initialize`); client probes `server/discover` (stdio) or attempts modern request and inspects 400 body (HTTP) and caches the result per server process/origin; a modern-only server SHOULD name its supported versions in the error it returns to `initialize`; compatibility matrix in the versioning page.
- Protobuf field-number discipline (3b).
- Version byte / frame tag, side-by-side schema ids, generic dual-stack guidance: no source fetched. UNVERIFIED.
## Not done (budget)
Portkey, Helicone, Cap'n Proto, OpenAI pricing, Gemini usage fields (`usageMetadata.thoughtsTokenCount`, `cachedContentTokenCount`), Anthropic Messages API reference schema (page too large), P preserved-thinking page, Bedrock/Vertex-specific usage differences.
## 4. Probes (Imagination, Yggdrasil ygg-verify rust image, 2026-10-28)
Scratch clone of ModelConnector at `ec0519d` plus a probe-only commit (never pushed to the
forge). Baseline: `cargo test --workspace` and `--features model-connector/daemon` green, 77
tests passed in all.
- P2 Decoding the real v2 frame `tests/fixtures/wire_v2/envelope_invocation.mp` (the bytes
CodexConnector `fd7cec4` produced): 735-byte frame for 429 bytes of ciphertext (209 of them
>= 128), ratio 1.713. First byte `0x96`: a positional six-element array. It decrypts under
`parity-distinct-test-key` to caller `parity-caller`, request `request-1`, model `gpt-5.4`,
4 input items, 1 tool, schema `gamecult.codex.transport_invocation.v2`.
- P2 The same envelope with the ciphertext as MessagePack `bin` (`serde_bytes`): 526 bytes,
ratio 1.226 (the rest is ids and nonce). A 1 MiB all-high ciphertext: v2 frame 2,097,251
bytes, bin frame 1,048,675; the kit's `frame_bytes_for_payload(1 MiB)` = 2,097,750.
- P2 The v2 `TransportEnvelope` type decodes a bin-encoded ciphertext and compares equal
(rmp-serde reads bin into `Vec<u8>`). Encoding is the only change bin needs.
- P3 `ProviderRequest` positional 267 bytes (`0xdc` array16), named (`to_vec_named`) 590
bytes (`0xde` map16). The v2 type decodes the named encoding and compares equal.
- P4a A struct with one added field: positional encoding fails to decode in the old type
("array had incorrect length, expected 3"); named encoding decodes with the field ignored.
So positional MessagePack cannot add a field without breaking every reader, and named
encoding drops unknown fields silently unless the type denies them.
- P4b A v2 `InputItem` decoding a `ReasoningState` variant fails: "unknown variant". A v2
peer cannot read a v3 transcript at all; there is no partial compatibility to preserve.
- P5 A v3 envelope schema id under v2 `decrypt_invocation`: `Err(Envelope)`, before any
decryption. A v2 daemon given a v3 envelope drops the connection; it never answers.
- PC CultNet carriage. `CultNetMessage::OperationRequest` (cultnet-rs at CultLib `a4eae90`)
carrying the sealed envelope (named, bin ciphertext, ids at 256 bytes) as its
`messagepack-base64` payload, encoded with `encode_cultnet_message_to_vec(..,
CultNetSchemaV0)` and decoded back, round-trips equal. Frame over ciphertext: 4.45 at 429
bytes (fixed overhead), 1.354 at 64 KiB, 1.335 at 1 MiB, 1.334 at 8 MiB; encode 6.5 ms and
decode 1.0 ms at 1 MiB (release). The worst case is deterministic (base64 is exactly
4*ceil(n/3)), where v2's integer array is up to 2.0x and depends on the bytes.
Source reads behind the wire question (not probes):
- CultNet's `TcpFramed` transport protocol (`contracts.rs:112-119`) uses the same four-byte
big-endian length prefix as the kit (`framing.rs:6-15`; kit `envelope.rs:152-184`).
- `validate_message` requires `payload_encoding == "messagepack-base64"` on operation
messages (`contracts.rs:780`): CultNet has no binary operation payload today.
- CultNet's security options hold one connection key per server (`security.rs:51-75`); the
kit binds one key and epoch per caller and seals ids into the AES-GCM AAD
(`service.rs:19-65`, `envelope.rs:245-253`).
- Huginn already speaks this shape: `huginn-mind/src/envelope.rs` carries named MessagePack
as base64 in `cultnet.operation_request.v0`, checks `service_id`, `payload_schema` and the
operation against the decoded payload, and publishes its JSON Schema with `schemars`.
## 5. What the cut takes from this
- Reasoning item granularity follows the providers: Anthropic requires the latest assistant
turn's thinking blocks complete, unmodified and in place beside their `tool_use`; OpenRouter
carries `reasoning_details` per block with an index. So one `ReasoningState` item per
provider block, in transcript order, holding the connector's encoding of the whole block
(Anthropic: `thinking` text and `signature`, or `redacted_thinking` data, since a summarized
block must come back with both).
- Foreign state: every provider that tags reasoning by model drops what it cannot read across
completed turns (Anthropic drops unreadable blocks silently; OpenAI omits other families).
Only the in-flight tool exchange fails hard (Anthropic 400, DeepSeek 400). So the kit refuses
foreign state only after the last user text, and drops it before the backend sees it
elsewhere, counting the drop on the receipt (as Anthropic's `input_transformations` does).
- Anthropic's prefix check binds a block to the unchanged system, tools and earlier messages:
the caller's verbatim replay (ruling connector-opaque-state-item) is necessary, and the Claude
connector's lowering must be deterministic (B4).
- The cheap providers (1d) all keep plaintext `reasoning_content` and want it replayed inside a
tool loop; the item stays opaque to callers either way.
- Spend (B3b): LiteLLM and OpenRouter both bound a request before the call as estimated input
plus `max_tokens` times the output price, hold it, and settle to actual; LiteLLM without a
database fails open, which ruling connector-owner refuses. Anthropic returns tokens only, so
cost is the connector's arithmetic over a price table it owns. v3 therefore makes
`max_output_tokens` required, so the bound exists.