diff --git a/docs/research/connector-contract-v3-prior-art.md b/docs/research/connector-contract-v3-prior-art.md new file mode 100644 index 0000000..4056cd0 --- /dev/null +++ b/docs/research/connector-contract-v3-prior-art.md @@ -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 `` 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` 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`). 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.