Skip unrecognized stream output, add list_models and default headers, and prepare 0.4.5 - #181
Merged
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
…sforms Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
…ME order The GLM-5.3 branch added its own getSelectedClientType for presets that declare a protocol; this keeps one helper serving both that and a listing's own tagging, and shows the field only where the protocol is not already declared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h
This was referenced Aug 20, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four changes, one release folder (
changelog/0.4.5/).1. Unknown stream output is skipped unless
AGENTHUB_DEBUGis set (fix)A relay in front of a Responses-compatible API injected
{"type":"ping","cost":"@"}into the SSE stream.pingis the Anthropic Messages heartbeat spelling, so the Responses clients' unknown-event guard raisedUnknown output: {"type":"ping","cost":"@"}and the stream died; the harness in front classified the throw as a retryable request failure and every retry hit the next heartbeat. 0.4.3 (#174, for Prism-Shadow/penguin-harness#286) had taught each family only its own spelling —keepalivefor Responses,pingfor Messages — so either spelling crossing into the other protocol still raised.The clients no longer end a stream over output they do not recognize:
claude5,ant_messages,openai_responses,gpt5_6andminimax_m3mark an unknown eventunused, andgemini3_7skips an unknown content part.is_debug_enabled()/isDebugEnabled()inutilsreadsAGENTHUB_DEBUGper event; set it to anything other than0/false/no/offand every unrecognized event or part raises again with the same message.Two notes on scope:
gemini3_7's part guard got the same treatment. It reports an unknown content part, not an event, so it sits outside the letter of the change — but it is the same failure mode (one unrecognized thing kills the stream), and leaving it would have left the policy with a hole. Easy to revert if content parts should stay loud.The heartbeat tests were rewritten around the switch and the files renamed to
test_unknown_events.py/unknown-events.test.ts: heartbeats, cross-protocol spellings, in-protocol unknown events, gateway error frames and unknown Gemini parts all pass through silently by default, and each raises onceAGENTHUB_DEBUGis set.2.
AutoLLMClient.list_models()(feature)Returns the model ids the configured endpoint actually serves, as
list[str], in the endpoint's own order. Every client implements it against its own SDK (models.list()for the OpenAI-compatible and Anthropic families, the Gemini model list forgemini3_7); paging belongs to the SDKs, so a result spans every page.The playground exposes it through a List models control on the Base URL label row, backed by
POST /api/models; the result renders in a panel that overlays the field, so the control costs no vertical space while idle. A rejected listing returns400with the provider's message — verified unmocked against an unreachable base URL (Connection error.) and abedrock://URL (Bedrock does not support listing models.).Two judgment calls:
models/gemini-3.7-flash,publishers/google/models/…), so the client returns the last path segment: an id fromlist_models()is directly usable asAutoLLMClient'smodel.claude5raises the newUnsupportedOperationError. A dedicated class rather thanUnsupportedParameterErrorbecause penguin-core reads.parameterto classify errors (err.parameter === "fast_mode"), and a method name has no business in a field consumers branch on.LLMClientdeclares the method abstract. Not a public break: neither package exportsLLMClient.3. Clients accept default headers (feature)
AutoLLMClient(..., default_headers=…)/{ defaultHeaders }, sent with every request to that endpoint — for gateways that reject a client they do not recognize (unauthorized_client_error, a requiredHTTP-Referer/X-Title/Anthropic-Beta). A connection property, not a request one: passed once at construction, never enteringUniConfig. OpenAI and Anthropic takedefault_headersdirectly; Gemini carries them insidehttp_options.headers.Verified over the wire, not by poking SDK internals: both suites start a local HTTP server, list models through the OpenAI, Anthropic and Gemini clients, and assert the declared headers arrived — plus that a client declaring none adds none. (The OpenAI SDK lowercases custom header names while the Anthropic one sends them as given, so the comparison is case-insensitive.)
The playground gained an Extra Headers field; text that is not a JSON object marks the field instead of being sent.
4. The Responses input transforms match the GPT client (refactor)
openai_responsesandminimax_m3dropped the early-continueguards for oneif/elifchain, and now flush buffered text once at the end of a message, exactly asgpt5_6does.That second half changes replay order, deliberately. Extracting the pre-refactor transform from
HEADand running it beside the new one over 7 message shapes: 4 differ, all in the same way — text that preceded a top-level item now follows it, and two text spans separated by such an item arrive merged.[text, tool_call]replays as[function_call, message], which is whatgpt5_6has always sent. Messages carrying only text and images are untouched.Verification
src_py:make lintclean (ruff check + format);uv run pytest tests --ignore=tests/test_client.py→ 199 passed.src_ts:npm run lint,npm run buildclean; the offline suites → 184 passed.git merge-treeagainstglm-5.3-ga-and-model-list, the other open branch touching both playground files, reports no conflict.🤖 Generated with Claude Code
https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h