Skip to content

Skip unrecognized stream output, add list_models and default headers, and prepare 0.4.5 - #181

Merged
hiyouga merged 11 commits into
devfrom
fix/unknown-stream-events
Aug 20, 2026
Merged

Skip unrecognized stream output, add list_models and default headers, and prepare 0.4.5#181
hiyouga merged 11 commits into
devfrom
fix/unknown-stream-events

Conversation

@hiyouga

@hiyouga hiyouga commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Four changes, one release folder (changelog/0.4.5/).

1. Unknown stream output is skipped unless AGENTHUB_DEBUG is set (fix)

A relay in front of a Responses-compatible API injected {"type":"ping","cost":"@"} into the SSE stream. ping is the Anthropic Messages heartbeat spelling, so the Responses clients' unknown-event guard raised Unknown 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 — keepalive for Responses, ping for 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_6 and minimax_m3 mark an unknown event unused, and gemini3_7 skips an unknown content part. is_debug_enabled() / isDebugEnabled() in utils reads AGENTHUB_DEBUG per event; set it to anything other than 0/false/no/off and 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 per-protocol lists of known no-op events are unchanged, so a heartbeat a client already knew is still recognized by name rather than by falling through.

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 once AGENTHUB_DEBUG is 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 for gemini3_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 returns 400 with the provider's message — verified unmocked against an unreachable base URL (Connection error.) and a bedrock:// URL (Bedrock does not support listing models.).

Two judgment calls:

  • Gemini names are path-qualified (models/gemini-3.7-flash, publishers/google/models/…), so the client returns the last path segment: an id from list_models() is directly usable as AutoLLMClient's model.
  • Bedrock has no models resource in either SDK, so claude5 raises the new UnsupportedOperationError. A dedicated class rather than UnsupportedParameterError because penguin-core reads .parameter to classify errors (err.parameter === "fast_mode"), and a method name has no business in a field consumers branch on.

LLMClient declares the method abstract. Not a public break: neither package exports LLMClient.

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 required HTTP-Referer / X-Title / Anthropic-Beta). A connection property, not a request one: passed once at construction, never entering UniConfig. OpenAI and Anthropic take default_headers directly; Gemini carries them inside http_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_responses and minimax_m3 dropped the early-continue guards for one if/elif chain, and now flush buffered text once at the end of a message, exactly as gpt5_6 does.

That second half changes replay order, deliberately. Extracting the pre-refactor transform from HEAD and 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 what gpt5_6 has always sent. Messages carrying only text and images are untouched.

Verification

  • src_py: make lint clean (ruff check + format); uv run pytest tests --ignore=tests/test_client.py → 199 passed.
  • src_ts: npm run lint, npm run build clean; the offline suites → 184 passed.
  • git merge-tree against glm-5.3-ga-and-model-list, the other open branch touching both playground files, reports no conflict.
  • The e2e suites need API keys, so they are CI's.

🤖 Generated with Claude Code

https://claude.ai/code/session_013STCF9fADwqT8jJH9C3g5h

Copilot AI lite review requested due to automatic review settings August 20, 2026 05:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

hiyouga and others added 2 commits August 19, 2026 22:06
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
@hiyouga hiyouga changed the title Skip stream events injected from outside the protocol, and prepare 0.4.5 Skip foreign stream events, add AutoLLMClient.list_models, and prepare 0.4.5 Aug 20, 2026
hiyouga and others added 3 commits August 20, 2026 03:58
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
@hiyouga hiyouga changed the title Skip foreign stream events, add AutoLLMClient.list_models, and prepare 0.4.5 Skip unrecognized stream output, add list_models and default headers, and prepare 0.4.5 Aug 20, 2026
hiyouga and others added 5 commits August 20, 2026 08:12
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
@hiyouga
hiyouga merged commit 3717e08 into dev Aug 20, 2026
2 checks passed
@hiyouga
hiyouga deleted the fix/unknown-stream-events branch August 20, 2026 15:57
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