Preserve or explicitly reject provider reasoning output semantics #158
Labels
No labels
accessibility
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
P1
P2
P3
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Creatidy/scarcity-router#158
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Defect (reproduced on canonical
develop@6fec46ca82901a5fb8761909778c16d8322bbb97, 2026-09-28)The OpenAI-compatible translation core accepts reasoning controls on requests (D-055) and maps them deliberately per preset, but provider reasoning OUTPUT is not represented anywhere in the normalized execution seam. A backend can successfully return reasoning and Scarcity Router silently discards it while returning a successful response. Probe output on the exact base:
This violates the project's central translation rule: unsupported or unrepresentable semantics must never be silently discarded.
Affected source paths
scarcity_router/providers/openai_http_core.py—parse_chat_completion_response()reads onlycontent/tool_calls;interpret_stream_frame()reads onlydelta.content/delta.tool_calls;StreamFrameViewhas no reasoning member;TranslationPolicyhas no response-side reasoning fact.scarcity_router/gateway_adapters.py—AdapterMessagehas no reasoning member;CHUNK_KINDShas no reasoning-delta kind.scarcity_router/gateway_openai.py—_message_payload/chunk_payloadrender no reasoning representation.scarcity_router/providers/openai_http_adapter.py(server-direct) andscarcity_router/worker_local_translation.py(worker-loopback) — both consume the shared core, so both transports lose reasoning identically; the server-direct stream loop also assembles the final message without reasoning.scarcity_router/worker_protocol.py—message_to_dict/message_from_dictcannot carry reasoning across the worker↔server boundary.scarcity_router/providers/openai_http_presets.py— no preset carries a response-reasoning fact.Provider evidence (official docs, fetched 2026-09-28)
api-docs.deepseek.com/api/create-chat-completion):message.reasoning_contentanddelta.reasoning_content, "string nullable, for thinking mode only … the reasoning contents of the assistant message, before the final answer". Inputreasoning_contentexists only as a BETA Chat-Prefix-Completion feature on the beta base URL withprefix=true— not part of the evidenced production surface (response-only for us).openrouter.ai/docs/use-cases/reasoning-tokens):message.reasoning(string) withreasoning_contentdocumented as an identical alias; streaming plaintextdelta.reasoning; structuredmessage/delta.reasoning_detailsarrays (summary/encrypted/text objects) — a structured shape this task deliberately does NOT represent.docs.z.ai/api-reference/llm/chat-completion):message.reasoning_content(string), "Reasoning content, supported by GLM-4.5 series"; delta-level schema not separately documented on the retrieved reference (coding-endpoint parity remains PARTIAL, as the preset already records for reasoning controls).platform.openai.comchat completions exposes no reasoning text;docs.ollama.com/api/openai-compatibilitydocuments reasoning effort controls only, "Reasoning summaries" only under the Responses API).Semantic invariant (after this task)
Any provider reasoning output that an evidenced preset can produce must either be preserved through the normalized execution path and rendered to the client with documented semantics, or the response must fail explicitly with a typed translation/protocol-drift error. Silent loss is forbidden, in non-streaming AND streaming execution.
Design (frozen before implementation)
AdapterMessage.reasoning: str | None(bounded like content; opaque provider output — never parsed, never routed on, never telemetry) and a dedicatedreasoning_deltastream-chunk kind carrying its own bounded text; a reasoning delta is never atext_delta.reasoning_output_policy∈ {none,reasoning_content,reasoning}. The preset decides which response shape is evidenced. Known reasoning-output field names (reasoning_content,reasoning,reasoning_details) in a response are: mapped when evidenced (strict string-or-null typing; OpenRouter's documented identical alias reconciled, conflicting values fail closed), typed drift when not evidenced — never flattened, never stringified, never silently discarded. No auto-detection from payload shape, no inference from hostname/model.choices[0].message.reasoning_content(non-streaming) andchoices[0].delta.reasoning_content(streaming), present only when reasoning was preserved (absence remains absence — no fabricated nulls/empties).reasoning_contentis the ecosystem-standard name: defined natively by DeepSeek and Z.ai, and documented by OpenRouter as an identical alias ofreasoning; LiteLLM's normalized representation is exactlyreasoning_contentwithdelta.reasoningmapped into it.reasoning_contentback receives the existing typedunknown_parameter400 (never an unexplained drop). Matches DeepSeek's documented input restrictions; no evidenced preset supports reasoning re-injection.TranslationError(never stringified); two conflicting equivalent fields → fail closed; structuredreasoning_details→ typed drift; reasoning preserved independently alongside content and tool calls; reasoning-only terminal responses become representable if evidenced.reasoningmember on the assistant result message; worker protocol version bump per the documented v2 "one optional member" precedent, with the server accepting older peers; a negotiated-old session can never silently drop reasoning (new workers negotiate their own version or fail at hello, fail-closed).Privacy/security
Reasoning content is potentially sensitive model output. It must not newly enter logs, audit notes, error messages, provider diagnostic notes, persisted configuration, SQLite audit metadata, routing state, metrics or telemetry (the audit record already structurally cannot carry content). Errors stay structural (parameter names, never values). Reasoning strings carry deterministic bounds consistent with existing assistant-content limits.
Reuse (focused; #150's broad audit not repeated)
1ceeefbf(MIT),litellm/types/utils.py: normalizedreasoning_contenton message and delta;delta.reasoningcanonicalized into it. ADOPT the normalized semantics (not the code — pydantic framework idioms do not fit the stdlib translation core).4a2c8186(MIT),internal/thinking/summary.goand translators: per-provider reasoning-output field knowledge confined to translation edges. Corroborates keeping provider-specific knowledge at the policy edge; no code reuse.Scope
Translation core + policy + presets + normalized seam + gateway rendering + worker protocol member + tests + docs/decision record (next decision number from current
develop; D-060/D-061 are reserved by open #156/#157). Both transports (server-direct, worker-loopback) preserve reasoning through the ONE shared core; no second implementation.Non-goals (explicitly out of scope)
Z.ai
CREDIT_LIMIT; worker chunk-buffer hardening; HTTP transport changes; SSE parser replacement; invalid-UTF-8 typing cleanup; Responses API; Anthropic Messages; new providers; Codex schema automation; client-owned tool execution; generic content-block abstractions; the Codex (M06) adapter'sitem/reasoning/*summary handling (separate protocol + evidence path, recorded here as adjacent observed behavior, unchanged); structuredreasoning_detailsrepresentation; the #150 deferred items (HTTP early-response/write-error refinement, connect-phase error classification, worker chunk bounds).Acceptance criteria
uv sync --only-dev,make check,uv run basedpyright,git diff --check).docs/decisions.md; execution-surface/providers/worker-protocol docs updated in place.develop, never merged by agents.