Skip to content

Review view: chat(protocol=) - #461

Open
rejojer wants to merge 8 commits into
review/base-91b6c36from
feat/chat-protocol
Open

Review view: chat(protocol=)#461
rejojer wants to merge 8 commits into
review/base-91b6c36from
feat/chat-protocol

Conversation

@rejojer

@rejojer rejojer commented Sep 1, 2026

Copy link
Copy Markdown
Member

Review-only view of #460 (chat(protocol=): the protocol doors move behind the front door) plus every follow-up on feat/chat-protocol, diffed against review/base-91b6c36, main frozen at the commit #460 was merged onto.

Never merge. The base must never advance; merging would only pollute it. Follow-up review rounds run on this PR; fixes found here push to feat/chat-protocol (this diff updates) and reach main through their own small PRs.

Same pattern as #457 / #458 (v0.2.13) and #427 (v0.2.11).

https://claude.ai/code/session_01M9GdjnuDHHwDKqMzvPWCcj

responses() and messages() become _responses()/_messages(): the same
engines, reachable as chat(protocol="responses"|"messages") with the
protocol's own input and output shapes (transcript items / content
blocks in, the envelope or native stream out). The old names are the
vendor SDKs' own, and an agent-written client.messages(...) now fails
fast with the way in — a runtime-only __getattr__, invisible to the
type checker so attribute typos on the client still get flagged.

chat() gains the knobs that lost their public home: instructions
(appended after the managed prompt — a string on every lane, Messages
system blocks with protocol="messages"), max_turns, backend,
extra_headers, extra_body. reasoning_effort lands natively on each
lane: LiteLLM's kwarg, Responses reasoning.effort, Anthropic
output_config.effort. show_process stays the answer lane's view.

Six overloads keep the return types narrow for py.typed consumers; the
answer lane's contract is unchanged. The managed cloud chat rejects the
own-model knobs as before.

Claude-Session: https://claude.ai/code/session_017uvMD9eatgdaupLGFUjuKM
…remedies

- Messages lane: through the public door the thinking budget rides
  extra_body, so the default max_tokens lift reads it there (extra_body
  wins on the wire, so it wins in the lift); the documented
  extra_body={"thinking": ...} no longer 400s on max_tokens < budget.
- Responses lane: temperature/top_p/reasoning/max_output_tokens reach the
  wire via extra_body only; the envelope now reports what was sent instead
  of the never-set locals.
- One _require_own_chat refusal for chat(protocol=...), the doors behind
  it, and instructions. The doors' own copies had already drifted from
  chat()'s text, and every copy pointed a local client with chat_model
  blank at the managed chat it does not have; a door reached directly on
  such a client fell through to a raw AttributeError. The check is the
  one chat_completions already makes.
- Messages lane treats model="" as unset, like every other model check.
- The protocol refusal for show_process runs before the stream=True hint,
  so the first remedy offered is the right one.
- instructions="" configures nothing (no empty system row, cache key
  unchanged), matching the protocol lanes.
- Responses validation names messages, the public parameter, not input.
- Stale docstring pointer to messages() fixed.
- A protocol=None, stream: bool overload restores str | ChatStream for a
  runtime-variable stream; the catch-all had widened it to a 4-way union.
- Tests: door X refuses like chat(protocol=X) on managed and blank-local
  clients; the protocol gate and the non-stream show_process order are
  asserted by distinct messages; chat(stream=True) joins the max_turns
  matrix; each fix carries a red-verified assertion.

Claude-Session: https://claude.ai/code/session_01M9GdjnuDHHwDKqMzvPWCcj
@rejojer rejojer changed the title Review view: chat(protocol=) (#460 + follow-ups) on frozen main 91b6c36 Review view: chat(protocol=) Sep 1, 2026
… does

The rule chat() follows, written down: chat() names PageIndex's own
parameters plus a subset of LiteLLM's unified vocabulary; anything
vendor-specific rides extra_body under the lane's wire names. Three
layers meet on the wire — the skeleton (managed prompt, conversation,
tools) is the SDK's, the named knobs are translated per lane, and
extra_body is the caller's, merged last so it wins. A key the SDK writes
into a nested object must go in through extra_body too, so the caller's
other keys in that object survive — the SDK merge is shallow.

- Responses lane: reasoning_effort joins the caller's extra_body
  "reasoning" object (their keys win) instead of riding a separate
  reasoning= that extra_body's object replaced whole on the wire.
  Mirrors the Messages lane's output_config. The envelope's
  given.get("reasoning") now reports the merged object for free.
- reasoning_effort="" is unset on both protocol lanes, like model=""
  and instructions="".
- extra_body docstring: drop the invitation to override the SDK's
  system/input — that is the skeleton, and a fixed input breaks the
  tool loop; name the wire fields per lane instead of one mixed list.
- messages docstring: every system row joins the managed prompt on the
  answer lane, not only a leading one (_split_chat_messages hoists all).
- max_turns docstring: the OpenAI lanes raise at the cap, the Messages
  lane returns the truncated run — the divergence was documented only on
  the private door.
- Migration message: parameters go by keyword; the doors' sampling and
  thinking fields ride extra_body.
- chat_model docstring: responses is no longer a chat surface.
- _split_chat_messages refusals no longer name chat_completions, a
  method the chat() caller never typed.
- Tests: the Responses door equivalence takes the extra_body shape;
  effort/extra_body collision, key survival and "" pinned on both
  protocol lanes; the ModelSettings spy asserts the values reached the
  wire, not only the envelope.

Claude-Session: https://claude.ai/code/session_01Hj6t26s7thUjkcho6sn4zE
metadata is a Responses request field the caller sets through
extra_body; the envelope hard-coded None. Same source as the other
caller-set fields: given.get().

Claude-Session: https://claude.ai/code/session_01Hj6t26s7thUjkcho6sn4zE
…s unset

reasoning_effort="" reached LiteLLM as a literal empty effort on the
answer lane while the protocol lanes already treated it as unset. And
chat_completions' managed-cloud own-model gate, which chat() routes
through, refused model="" / reasoning_effort="" / {} as knobs the caller
never set. Non-numeric knobs now read falsy as unset, as the local lane
always has (model or chat_model, if extra_body, backend or {}); numeric
ones keep `is not None`.

Claude-Session: https://claude.ai/code/session_01BAmVWYKoSnFEydbMjuZjHc
extra_body merges last on every lane, so a caller's system / instructions / input / messages / tools replaced the managed prompt, the conversation or the doc tools wholesale. The run succeeded and answered without tool guidance or document scoping, silently. Those are the SDK's on every lane (the three-layer rule: skeleton, named knobs, caller extras); the door for the prompt is instructions=. Refused at the two seams where extra_body meets the wire, before the Anthropic transport exists on the Messages lane.

Claude-Session: https://claude.ai/code/session_01BAmVWYKoSnFEydbMjuZjHc
…eLLM-routed models

On the answer lane, extra_body for a non-OpenAI destination becomes ModelSettings.extra_args, the LiteLLM kwargs channel (LiteLLM would plant extra_body as a literal request field, which Anthropic rejects). openai-agents passes ModelSettings' own fields to litellm.acompletion by name beside **extra_args, so a caller's temperature / top_p / max_tokens / penalties / tool_choice in extra_args collided with the explicit keyword and died as a raw TypeError inside the framework. Split by the framework's own contract: keys that are ModelSettings fields ride their field (the caller's value winning over ours), the rest stay LiteLLM kwargs. The field list is the public dataclass, not a hand-kept name list; bad values now fail ModelSettings validation and surface as PageIndexAPIError. response_format is not a ModelSettings field and still has no door on this lane; documented.

Claude-Session: https://claude.ai/code/session_01BAmVWYKoSnFEydbMjuZjHc
chat_completions shares the seam that now refuses the skeleton keys; its docstring still promised an unconditional merged-last win. Same sentence as chat().

Claude-Session: https://claude.ai/code/session_01BAmVWYKoSnFEydbMjuZjHc
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