Skip to content

fix(posit): switch API flavor when chat.model crosses model families - #430

Open
cpsievert wants to merge 7 commits into
mainfrom
fix/posit-set-model-family-switch
Open

fix(posit): switch API flavor when chat.model crosses model families#430
cpsievert wants to merge 7 commits into
mainfrom
fix/posit-set-model-family-switch

Conversation

@cpsievert

Copy link
Copy Markdown
Collaborator

Mirrors tidyverse/ellmer#1139.

Summary

ChatPosit() picks its provider class from the model name: Claude models get PositAnthropicProvider (pointing at {base_url}/anthropic), everything else gets PositOpenAIProvider (pointing at {base_url}/openai/v1). But the Chat.model setter only updated the model name, so switching to a model from the other family sent requests in the wrong wire format to the wrong gateway endpoint.

This adds a Provider.set_model() hook that the Chat.model setter consults (self.provider = self.provider.set_model(value)). The default implementation returns the provider unchanged, so no other provider is affected. PositAnthropicProvider and PositOpenAIProvider override it to swap in their sibling class when the new model belongs to the other family, carrying over credentials and the gateway base URL. The Claude-only cache setting survives family switches via an inert cache attribute on PositOpenAIProvider, and ChatPosit() now passes its cache argument to both flavors so switching to Claude honors it.

A second, related fix: Anthropic-backed providers now drop thinking blocks that lack a signature when replaying conversation history. Reasoning emitted by a non-Claude model (e.g., GLM via the OpenAI flavor) has no signature, and the Anthropic API rejects it with Invalid signature in thinking block — family switching makes this reachable mid-conversation. Claude's own (signed) thinking blocks are unaffected, so interleaved-thinking replay still works. Note: ellmer appears to have the same latent issue.

Verification

from chatlas import ChatPosit

chat = ChatPosit(model="claude-haiku-4-5")
chat.chat("Hi")  # works via /anthropic

chat.model = "zai-org/GLM-5.3-Flash"
chat.chat("Hi")  # now works: request goes to /openai/v1

chat.model = "claude-haiku-4-5"
chat.chat("What did I say first?")  # works, even with GLM thinking in history

All three steps verified live against the Posit gateway. New tests in tests/test_provider_posit.py cover family swaps in both directions, no-op within a family, credential carryover, and cache preservation; tests/test_provider_anthropic.py covers dropping unsigned thinking blocks. Full suite: 1278 passed.

cpsievert and others added 4 commits September 8, 2026 11:10
ChatPosit() picks its provider class from the model name (Claude models
get the Anthropic flavor, everything else the OpenAI-compatible one),
but the Chat.model setter only updated the model name, so switching to
a model from the other family sent requests in the wrong wire format
to the wrong gateway endpoint.

Adds a Provider.set_model() hook that the Chat.model setter consults;
the default returns the provider unchanged. The two Posit providers
override it to swap in their sibling class on a family switch,
carrying over credentials, the gateway base URL, and the cache
setting (stored inertly on PositOpenAIProvider so it survives a round
trip). Mirrors tidyverse/ellmer#1139.

Also drops unsigned thinking blocks when replaying history to
Anthropic-backed providers: reasoning emitted by a non-Claude model
has no signature, and the API rejects it with "Invalid signature in
thinking block". Family switching makes this reachable; Claude's own
(signed) thinking is unaffected.

Verified end-to-end against the Posit gateway with family switches in
both directions, including a conversation containing GLM thinking
content replayed to Claude.
CI's pyright resolves pillow_heif's stubs and flags register_heif_opener
as a private re-export; this failure is present on main as well.
@cpsievert
cpsievert marked this pull request as ready for review September 8, 2026 16:29
@cpsievert
cpsievert requested a lite review from Copilot September 8, 2026 16:29

This comment was marked as resolved.

Addresses Copilot review feedback on #430:
- Chat.model setter now closes the previous provider's HTTP clients
  when set_model() swaps in a new provider instance (async resources
  are closed via close_async() when an event loop is running).
- Posit set_model() now carries over a custom provider name across
  Anthropic/OpenAI flavor switches.
Async clients can't be closed from sync code (their transport requires
a running event loop), so deterministically closing them from the
Chat.model setter would require fire-and-forget task scheduling with
its own edge cases. The leak scenario (repeated provider swaps after
async usage in a long-lived process) is narrow enough that relying on
GC for async resources is an acceptable, documented tradeoff.
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.

2 participants