lm15 1.0.0-rc.7

One request and response model for every LLM provider (OpenAI, Anthropic, Gemini, xAI, Groq, Bedrock, Azure, Vertex, local servers), with visible adaptations. Implements the lm15 contract.
Documentation
# The dialects — mapping notes

Reference notes on how each codec maps the canonical types onto its wire,
moved here from the README on 2026-09-08 (the README keeps status,
quick start, the consolidated stated deviations and divergences; the
full pre-consolidation README is `docs/history/README-2026-09-08-pre-consolidation.md`).
Every table named below is copied as data from the reference with a
file:line citation in the source. Deviations and divergences are listed
once, in the README, not repeated here.

Contents: the module 4 skeleton (`wire::emit`), the response side and
stream assembly (module 5), then the four dialects: Anthropic, OpenAI
Responses, OpenAI Chat Completions, Gemini.

## The request skeleton (module 4)

- `AccessPolicy` is every column of `lm15/access.py` as `const` data
  with file:line citations; `tests/support_matrix_contract.rs` checks
  it against `spec/support-matrix.json` in both directions.
- Host settings resolve config → env (only when the caller passes an
  env map; the bare adapter reads none) → default; `region` and
  `resource` have no default; `location` defaults to `global`. A setting
  that lands in a hostname must be a DNS label (letters, digits, `-`);
  `project` and the model are percent-encoded when placed in a path.
- Base URL precedence: explicit `base_url`, then the host template
  rendered over the settings, then the policy's URL, then the compat
  preset's URL, then the dialect default.
- SigV4 signs the finished request: `authorization` and `x-api-key`
  never enter the signature; `host`, `x-amz-date` and
  `x-amz-security-token` are derived from the URL, the injected clock
  and the credential. `x-amz-content-sha256` is not added (S3-only).
- The shim's `build_request` accepts `api_key` or `credential`
  (`credential` wins), `now`, `settings` and `base_url` per PROTOCOL.md;
  it reads no environment.

## Response side and stream assembly (module 5)

- `Dialect::parse_response` and `Dialect::parse_stream_event` on each
  of the four codecs (`src/dialects/*/response.rs`), copied from the
  reference's `parse_response` / `parse_stream_events` with the tables as
  data: the provider-executed item sets (MAP-1), the finish-reason maps,
  the in-band error code tables, the Gemini candidate finish errors.
- The adapter is stateless per frame and may emit one end event per
  provider terminal frame; `stream::Coalescer` merges them into the one
  final end (MAP-3) and synthesizes the one leading start (MAP-4); D9's
  `provider_data` rank rule is `EndProviderData`.
- `stream::StreamAccumulator` is the MAP-9 assembly algorithm verbatim
  (slots, the fixed kind order, the `tool_call_<index>` correlator, the
  refusal with `partial`); `materialize_response` is the one-shot form.
- `Response.provider_data` is the wire body; `_lm15_unmapped` is attached
  when content could not be mapped, and the shim surfaces it as the
  protocol's `unmapped` canary.

## Anthropic

### Layout

- `src/dialects/anthropic/` — W1: `mod.rs` (the `Dialect` impl, headers,
  the `anthropic-beta` join, the refusal constructors), `body.rs` (the
  body in the reference's key order: caching marks, the reasoning plan
  per `thinking_format`, tool choice, `output_config`, extensions, the
  system prefix), `parts.rs` (messages and parts → content blocks,
  thinking replay per MAP-7 rules 8 and 11), `tables.rs` (the data copied
  from the reference with citations: builtin tool types, the adaptive
  model-class markers, the effort → budget table, the visible-token
  default, the API version and beta strings).

### Mapping

- `max_tokens` is required on the wire: `Config.max_tokens` or 1024
  (`_DEFAULT_ANTHROPIC_VISIBLE_TOKENS`, pinned by `anthropic.reasoning_off`).
  On the manual thinking class the wire value is `budget_tokens` +
  that visible share (MAP-7 rule 6; `anthropic.reasoning_budget`: 1000 +
  2048 = 3048); on the adaptive class `Config.max_tokens` is the total.
- Reasoning (MAP-7) by `AnthropicCompat.thinking_format`: `anthropic` —
  the model-class table (`anthropic_adaptive_class`, a substring table
  that rots; `extensions.thinking` overrides): adaptive class →
  `thinking: {type: adaptive}` + `output_config.effort` (`minimal` and
  `thinking_budget` raise), manual class → `thinking: {type: enabled,
  budget_tokens}` from `thinking_budget` or the grading table; `off`
  sends nothing (absence is the native off). `deepseek` — `off` MUST be
  sent as `{type: disabled}`, on is `{type: enabled}` + `output_config.effort`.
  `adaptive` — every model adaptive, `off` sent as `disabled` so the
  server refuses loudly. `effort` — `output_config.effort` alone, `off` as
  `disabled`. `reasoning_efforts` is a client-side allowlist for servers
  that swallow unknown words; `summary` `concise`/`detailed` raise,
  `auto` is satisfied silently.
- Caching (MAP-6): `config.cache` absent → nothing. Present, not `off`,
  `cache_control="anthropic"` → the system block is marked (`auto` and
  `prefix="stable"`), plus the last block of message N for
  `prefix_until_index=N` (clamped) or of the last message for
  `prefix="history"`; `retention="long"` adds `ttl: "1h"`. `mode="off"`
  places nothing (no write switch exists).
- Tool choice (MAP-8): `none`; one name + `required` → `{type: tool,
  name}` (server tools too); an allowlist naming every declared tool →
  `any`/`auto`; a proper subset raises; `parallel=false` →
  `disable_parallel_tool_use: true` (raises under
  `parallel_tool_calls="reject"`, where the server ignores it).
- Structured output (MAP-8): `json_schema` → `output_config.format
  {type: json_schema, schema}`; `name` and `strict` have no slot;
  `json_object` raises; `structured_output="reject"` raises.
- Thinking replay (MAP-7 rules 8, 11): `anthropic:redacted_thinking` →
  `redacted_thinking` with the blob; `anthropic:thinking_signature` with
  a non-empty signature → a signed `thinking` block; otherwise text
  (decision G), or an unsigned `thinking` block under
  `thinking_replay="unsigned"`.
- Headers: `anthropic-version: 2023-06-01`, the policy's static headers,
  one `anthropic-beta` joining the policy's betas with the dialect's
  (`code-execution-2025-05-22` when a `code_execution` builtin is
  offered). `content-type` and the credential are `emit`'s. The
  `claude-code` binding: `system_prefix` first in `system` as a text
  block, then the caller's system (string or blocks, marks kept).
- Refusals, all `UnsupportedFeatureError` unless named: `model_prefixes`
  mismatch (`UnsupportedModelError`), `store`, `logprobs`, `cache.key`
  (marks active), `cache.resource`, `retention="long"` on a "none" server,
  `sampling_params="reject"` with any of temperature/top_p/top_k,
  audio/video/binary parts, non-text `system` parts, an unreadable media
  `path` (`InvalidRequestError`).

## OpenAI Responses

### Mapping

- `src/dialects/openai_responses/`: `mod.rs` (the `Dialect`, the
  refusal constructors, `CODEX_BACKEND`), `payload.rs` (the body in the
  reference's order; `resolve_compat`), `input.rs` (messages → items:
  `input_text`/`input_image`/`input_audio`/`input_file`/`input_video`,
  `output_text`/`refusal`, `function_call`/`function_call_output`,
  reasoning-item replay), `tools.rs` (`_OPENAI_BUILTIN_MAP` as data,
  `tools`, the kind-aware `tool_choice`), `cache.rs` (the `gpt-5.6+`
  detector, breakpoint placement, MAP-6 fields).
- What `emit` does for the dialect: the credential header (`Bearer` on
  `openai`/`meta`/`moonshotai-responses`/`openai-codex`, `api-key` on
  `azure`), the base URL, the Azure host rewrite, `content-type`. The
  dialect sets `Content-Type` first and the policy's static headers after
  (`openai.py:541-549`); `endpoint = "responses"`; `model` for hosts
  that place it in the path.
- Reasoning (MAP-5/7): the word verbatim; `off` → `{"effort": "none"}`;
  `summary` verbatim on `responses_reasoning`, `concise`/`detailed`
  refuse elsewhere; `thinking_budget` refuses. Replay: an
  `openai:reasoning_item` state becomes `{"type": "reasoning", id?,
  encrypted_content?, "summary": [...]}` before the message it preceded
  (`summary` present even when empty); stateless thinking text replays as
  `output_text`; empty stateless thinking sends nothing.
- Caching (MAP-6), under `cache_control="openai"`: `mode="off"` →
  `prompt_cache_options: {mode: explicit}` on `gpt-5.6+` and nothing
  below; `key` → `prompt_cache_key`; `retention="long"` →
  `prompt_cache_retention: "24h"` on every class; `prefix="stable"` moves
  the system prompt into the first developer item with
  `prompt_cache_breakpoint`; `prefix_until_index` marks the last text
  block of that message (clamped) and refuses on an assistant/tool message
  or a message not ending in text; a placed mark on `gpt-5.6+` also sends
  `prompt_cache_options`; `prefix="history"` sends nothing; `resource`
  refuses. `openai_implicit` (Meta, Moonshot): key and retention only.
  `none`/`anthropic`: nothing.
- Tool choice (MAP-8): `none`/`auto`/`required` as strings; a single
  allowed name with `required` is the forced form (`{"type": "function",
  "name"}` or the hosted-tool `{"type": <wire type>}`); every other
  allowlist is `{"type": "allowed_tools", "mode", "tools"}`. `parallel`
  → `parallel_tool_calls`. Structured output: `text.format` with `name`
  defaulting to `"response"` and `strict` verbatim when present.
- Builtin tools: `builtin_tools="openai"` maps `web_search` →
  `web_search_preview`, `code_execution` → `code_interpreter`,
  `file_search`, `computer_use` → `computer_use_preview`; `"verbatim"`
  sends the canonical name; a name outside the table goes out verbatim
  (the server refuses loudly). `config` keys ride verbatim after `type`.
- Promoted knobs: `service_tier`, `user_id` → `safety_identifier`,
  `store` (false included), `logprobs` → `top_logprobs` +
  `include: ["message.output_text.logprobs"]`. INV-049 passthrough:
  every other `extensions` key verbatim (`previous_response_id`,
  `conversation`, `background`, `truncation`, `metadata`, `include`,
  `max_tool_calls`, `stream_options`, `context_management`, `user`).
- The `chatgpt-codex` backend (AUTH-10 branch 1): `instructions`
  defaults to the policy's prefix, `store: false`, `stream: true`, the
  max-tokens field removed; the policy's `OpenAI-Beta` and `originator`
  headers. `client_version` is consumed by `/models` only
  (`openai.py:1610-1613`), module 6. The `chatgpt-account-id` header is
  the account id bound by the router (from the stored file) or the
  token's own claim, added by `emit` at the first build.
- Function tools send `"description": null` when the canonical tool has
  none, as the reference does (`openai.py:849-856`); the schema accepts
  it and the bytes match.
- A tool result whose content renders to no text is sent as the
  reference's type list (`[{"type": "image"}]`, `json.dumps` spacing) —
  the wire's `function_call_output.output` is text only. Stated: media in
  tool results reaches the model as a type name, not as media.
- Compat override from the request: `extensions.openai_responses_compat`
  (or `openai_compat`, or `compat.openai_responses` / `compat.openai`)
  merges over the bound compat (`OpenAIResponsesCompat::merge`,
  `from_json`, `from_extensions`); an unknown knob value is a
  `ConfigurationError` (the reference's dataclass accepts any string).
  The profile layers of `lm15/profiles.py` are not carried.

## OpenAI Chat Completions

### Layout

- `src/dialects/openai_chat/` — the Chat Completions codec: `mod.rs`
  (the `Dialect` impl, headers, the xAI refusal table), `payload.rs` (the
  body in the reference's key order), `messages.rs` (the `messages`
  array), `cache.rs` (MAP-6 and the gpt-5.6+ class detector), `text.rs`
  (lossy text rendering, data URIs, the refusal constructor).

### Mapping

- **Body key order is the fixture's.** Bodies are `serde_json::Map`s
  filled in the reference's insertion order (`lm15/providers/openai_chat.py:377-560`):
  `model, messages, stream, stream_options, <max_tokens_field>,
  temperature, top_p, stop, logprobs, top_logprobs, tools, tool_choice,
  parallel_tool_calls, response_format, reasoning_format, <thinking
  fields>, prompt_cache_key, prompt_cache_retention, prompt_cache_options,
  provider, service_tier, <user_field>, store, extensions…`. This depends
  on `serde_json`'s `preserve_order` feature: the Bedrock doors sign the
  body bytes (SigV4), so the order is part of what those fixtures pin.
- **One compat, consulted at named points.** The binding's
  `OpenAIChatCompat` (the empty partial for a binding without one) is
  resolved per request after `for_model` applies the door's per-model
  overrides (Bedrock: `openai.gpt-oss` refuses forced tool choice and
  `json_schema`; `google.gemma` refuses forced tool choice on
  bedrock-runtime only). Every knob value is exercised by one unit test in
  `src/dialects/openai_chat/tests.rs`.
- **Reasoning (MAP-5, MAP-7).** `effort` goes out in the server's shape
  (`reasoning_effort`; OpenRouter `reasoning: {effort}`; the `deepseek`
  shape `thinking: {type: enabled}` + `reasoning_effort`; Moonshot's
  `kimi` shape sends the word alone and `thinking: {type: disabled}` for
  off; Qwen `enable_thinking`; `qwen_chat_template`). `off` sends the
  native disable. `thinking_budget` and `summary: concise|detailed` refuse
  (`UnsupportedFeatureError`); `summary: auto` becomes Groq's
  `reasoning_format: parsed` and is accepted silently elsewhere (MAP-7.7).
  `reasoning_efforts` allowlists refuse a word the server would swallow
  (Moonshot `medium`). A `thinking_format: none` server (ollama) refuses
  any `config.reasoning`: the wire has no dial, and an omitted dial is
  the silent paid no-op MAP-5 forbids.
- **Tool choice and structured output (MAP-8).** `auto`/`required`/`none`
  verbatim; one allowed name with `required` forces the function; any
  other allowlist is the nested `allowed_tools` form; builtin names in an
  allowlist refuse. `forced_tool_choice: reject` (Z.AI, gpt-oss on
  Bedrock, Gemma on bedrock-runtime) refuses every form but plain `auto`.
  `response_format` is `{type: json_object}` or `{type: json_schema,
  json_schema: {name (default "response"), schema, strict?}}`;
  `json_schema: reject` (Z.AI, gpt-oss on Bedrock) refuses the schema form.
- **xAI's refusal table** lives in the dialect and is keyed on the bound
  policy (`policy.provider == "xai"`), copied from
  `lm15/providers/xai.py:77-125`: reasoning off, `logprobs`, allowlist
  subsets other than one forced name, and a forced tool next to
  `response_format` all refuse before the wire. A second binding of the
  `xai` compat preset (say, a proxy) does not inherit the table: the
  table is a provider fact, the preset is a wire shape.
- **Caching (MAP-6)** follows `cache_control`: `openai` sends
  `prompt_cache_key`, `prompt_cache_retention: 24h`, the
  `prompt_cache_breakpoint` mark on the system block (`prefix: stable`)
  or on the last text block of message `prefix_until_index`, and
  `prompt_cache_options: {mode: explicit}` on the gpt-5.6+ class (with a
  mark, or alone for `mode: off`); `openai_implicit` (Meta, Moonshot)
  forwards only the key and retention; `none` sends nothing;
  `resource` refuses on both OpenAI controls. The gpt-5.6+ detector
  (`gpt-<major>.<minor>` ≥ 5.6) is a stated, rotting table.
- **Messages.** `system` and `developer` rows use the compat's
  `instruction_role`; user content is a bare string for one text part
  and an array of `text`/`image_url` blocks otherwise (URLs verbatim,
  inline data as a data URI, `detail` when set); assistant rows carry
  text, refusal text and — per `thinking_replay` — thinking text in
  `content` (`null` when empty), `reasoning_content` on the native replay
  (always present under `assistant_reasoning_content: include_empty`,
  DeepSeek's tool-loop requirement), and `tool_calls` with compact JSON
  `arguments`; tool rows carry `tool_call_id`, the result's text, and
  `name` under `tool_result_name: include`.

## Gemini

### Layout

- `src/dialects/gemini/mod.rs` — the `Dialect` impl, the model path,
  `tools`, the body assembly, `GEMINI_BUILTIN_TOOLS`, `gemini_level_class`.
- `src/dialects/gemini/config.rs` — `generationConfig` (`thinkingConfig`
  per MAP-7, `EFFORT_THINKING_BUDGETS` from `lm15/providers/common.py:386-393`,
  the MAP-16 schema-field rule), `toolConfig` (MAP-8),
  the MAP-6 cache plan.
- `src/dialects/gemini/contents.rs` — messages and parts, thought-signature
  replay (MAP-7.8), the text-only slots.

### Mapping

- Reasoning (MAP-7): absent → nothing; `off` → `thinkingBudget: 0` on the
  2.5 class, RAISE on the 3.x class; `thinking_budget` → `thinkingBudget`
  on both classes; else `thinkingLevel` verbatim on 3.x (`xhigh`/`max`
  RAISE) or the grading table on 2.5; `summary: auto` →
  `includeThoughts: true`, `concise`/`detailed` RAISE. The class is a
  model-name table (`gemini-3*`); the server 400s when it rots.
- Tool choice (MAP-8): `auto`/`required`/`none` → `AUTO`/`ANY`/`NONE`;
  `allowed` → `allowedFunctionNames` with `ANY` or `VALIDATED` (auto);
  builtin names in `allowed` RAISE; `parallel=false` RAISES.
- Structured output (INV-050): `responseMimeType: application/json`,
  plus the schema in `responseJsonSchema` or `responseSchema` by MAP-16
  (`config::openapi_schema`: the JSON Schema field when a schema node is
  a boolean, has a key outside Gemini's Schema object, a list `type` or a
  non-string `enum` element); `strict` satisfied, `name` dropped (a
  label).
- Function tools (MAP-16): the same rule picks `parameters` or
  `parametersJsonSchema` in every `functionDeclarations` entry
  (generateContent, cached prefixes, Live); the schema is verbatim.
- Caching (MAP-6): `off`, `auto`, `prefix`, `prefix_until_index` alone,
  `retention: short` → nothing; `key` and `retention: long` RAISE;
  `resource` → `cachedContent` and only the messages after
  `prefix_until_index`, with no `systemInstruction`/`tools`/`toolConfig`
  (MAP-6.7); a resource with no suffix message RAISES `InvalidRequestError`.
- `user_id` RAISES; `store` and `service_tier` map verbatim; `logprobs`
  → `responseLogprobs` (+ `logprobs` when `> 0`); `extensions` land at the
  top level verbatim (`safetySettings`, `labels`, …), replacing a built
  key of the same name; `extensions.output` → `responseModalities`.
- Hosts: `gemini` (`x-goog-api-key`), `vertex` (bearer; model in the
  path under `…/publishers/google/models/{model}`), `vertex-express`
  (`?key=`). The dialect sets `WireRequest.model` and
  `endpoint = "generateContent"`; `emit` and the host do the rest.