From c612f43d28f1ecf7da593085913c1a5e6716aed3 Mon Sep 17 00:00:00 2001 From: mayor Date: Sun, 10 May 2026 14:25:30 -0500 Subject: [PATCH 1/2] =?UTF-8?q?release:=20v1.0.5=20=E2=80=94=20OpenRouter?= =?UTF-8?q?=20Anthropic=20passthrough=20+=20html=20attachment=20+=20MCP-bo?= =?UTF-8?q?undary=20hardening=20+=20persistence=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The big fix is hq-olrk: route openrouter/anthropic/* through OR's Anthropic-passthrough endpoint at /v1/messages with native shape, instead of /v1/chat/completions with OpenAI-shape (lossy conversion drops images). Resolves long-masked v1.0.0+ image transport regression — midgard mayor's 2026-05-10 deterministic repro went from 15/15 refusals to image-aware responses. Sibling fix hq-aaca inlines html attachments into the question's TextBlock with --- HTML SOURCE --- delimiters, eliminating the two-adjacent-text-blocks shape that caused ~50% refusal rate. Plus three v1.0.4-dogfood-surfaced bugs (hq-jviv: AttachmentRef strict at the MCP boundary; hq-0pnq: --save in ensemble panel runs; hq-6j40: MCP ensemble normalization) and the cross-product Cache-Control alignment for synthpanel.dev (hq-bxmp). No API break. Co-Authored-By: midgard mayor Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 128 ++++++++++++++++++++++---- site/.well-known/mcp/server-card.json | 6 +- site/index.html | 8 +- site/index.md | 2 +- src/synth_panel/__version__.py | 2 +- tests/test_site_headers.py | 7 +- 6 files changed, 122 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9313f1..ccd1b63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,23 +6,79 @@ For auto-generated release notes, see [GitHub Releases](https://github.com/DataV ## [Unreleased] +(Empty — next-cycle work lands here.) + +## [1.0.5] - 2026-05-10 + +OpenRouter multimodal transport fix (the big one) + caller-API hardening +on the MCP boundary + persistence-CLI cleanup. Closes the v1.0.4 +dogfood-surfaced bug cluster plus the long-standing OpenRouter-routes- +Anthropic image-drop that was masked across v1.0.0-1.0.4 by persona +roleplay in less-constrained prompts. + +No API break — every v1.0.4 caller works unchanged. + ### Fixed - **`openrouter/anthropic/*` images silently dropped** (hq-olrk, fixes - hq-m333). The OpenRouter provider used to send every request — including - Anthropic-upstream models — through OR's `/v1/chat/completions` - endpoint with OpenAI-shape multimodal blocks. For - `openrouter/anthropic/*` traffic, OR's downstream conversion from - OpenAI `image_url` to Anthropic image blocks is lossy in practice: the - image content silently drops during normalization, the model receives - text-only context, and the run looks successful (200 OK with - "I don't see an image"). Reproduced empirically on midgard at 15/15. - The fix routes `openrouter/anthropic/*` through OR's Anthropic-native - `/v1/messages` passthrough with an Anthropic-shape body - (cache-control, native multimodal blocks, `anthropic-version` header - all preserved). Non-Anthropic OR traffic (`openrouter/openai/*`, - `openrouter/google/*`, etc.) continues to use chat-completions and is - unaffected. + hq-m333). The OpenRouter provider used to send every request — + including Anthropic-upstream models — through OR's + `/v1/chat/completions` endpoint with OpenAI-shape multimodal blocks. + For `openrouter/anthropic/*` traffic, OR's downstream conversion from + OpenAI `image_url` to Anthropic image blocks is lossy in practice: + the image content silently drops during normalization, the model + receives text-only context, and the run looks successful (200 OK + with "I don't see an image"). Reproduced empirically on midgard at + **15/15**. The fix routes `openrouter/anthropic/*` through OR's + Anthropic-native `/v1/messages` passthrough with an Anthropic-shape + body (cache-control, native multimodal blocks, `anthropic-version` + header all preserved). Non-Anthropic OR traffic + (`openrouter/openai/*`, `openrouter/google/*`, etc.) continues to + use chat-completions and is unaffected. + + Sources: OpenRouter Anthropic-passthrough docs, + `claude-code-router#958` (same OR-Anthropic conversion bug). + +- **`html` attachment type now reaches the model reliably** (hq-aaca). + Previously, `HTMLBlock` was emitted as a separate text content + block alongside the question text — two adjacent text blocks that + the Anthropic API treats as semantically distinct, causing ~50% + refusal rate via OpenRouter and inconsistent attention even on + direct Anthropic. Fixed: `build_question_blocks` now inlines + html-attachment text into the question's TextBlock with + `--- HTML SOURCE ---` delimiters, eliminating the two-text-block + shape. Wire-level HTMLBlock branches kept as defensive fallbacks. + 8 new tests in + `tests/test_attachments_v1_0_1_wiring.py::TestHTMLAttachmentInlining` + pin the contract. + +- **AttachmentRef strict mode now reaches the MCP boundary** + (hq-jviv). The v1.0.4 hq-nuz9 work promoted AttachmentRef to a + strict BaseModel (`extra: forbid`) but the MCP `run_panel` handler + read instrument attachments as raw dicts without going through + `AttachmentRef.model_validate()`, so caller typos like `typo_field` + silently propagated through the entire pipeline (echoed back in + the response payload). Now: instrument-bank attachment shape + enforces `extras='forbid'` at the parse boundary, surfacing + ValidationError with the offending field name. Caller payload + typos fail loud, as v1.0.4 promised. + +- **`--save` flag now works in ensemble panel runs** (hq-0pnq). + `synthpanel panel run --save` returned exit 0 but never wrote to + `~/.synthpanel/results/`. The persistence call was wired only for + the single-model path; ensemble runs (`--models a,b`) hit a code + branch that bypassed the save step. Now both paths persist. + +- **MCP `run_panel` accepts `models=[...]` (ensemble) without empty + error** (hq-6j40). Calling `run_panel` via MCP with the `models` + array parameter previously returned `"Error executing tool + run_panel: "` (an exception with empty `str()` raised by an + unwired code path). Now the ensemble arguments normalize at the + MCP boundary: `models=[]` is treated as "no override", a + single-element list collapses to the singular `model` parameter, + and a multi-element list dispatches to the ensemble runner. Plus + a clearer timeout policy on the MCP wrapper so callers can + interrupt long ensemble runs. ### Changed @@ -33,9 +89,47 @@ For auto-generated release notes, see [GitHub Releases](https://github.com/DataV reachable. Anthropic serialization helpers (`build_anthropic_body`, `build_messages`, `build_content_blocks`, `parse_anthropic_response`, `parse_sse_stream`) moved into a shared - `synth_panel.llm.providers._anthropic_format` module; the back-compat - names (`_build_messages`, `_build_content_blocks`, ...) remain - importable from `synth_panel.llm.providers.anthropic`. + `synth_panel.llm.providers._anthropic_format` module; the + back-compat names (`_build_messages`, `_build_content_blocks`, ...) + remain importable from `synth_panel.llm.providers.anthropic`. + +### Site / Docs + +- **Cache-Control headers aligned with dvi-25f cross-product policy** + (hq-bxmp). Live audit of synthpanel.dev found drift on every + bucket: HTML pages served `public, max-age=0` (policy: + `private, no-store, must-revalidate`), well-known JSON endpoints + cached too long (300/3600s vs policy's 60s), and unhashed static + assets at 14400s vs policy's 300s. Fixed via `site/_headers` (CF + Pages convention) with global `/*` set to HTML bucket and + per-extension overrides for static assets. `site/_worker.js` + markdown-rendition fallback flipped to HTML bucket. 3 new pinning + tests in `tests/test_site_headers.py` so future edits can't + silently drift again. + +- **`docs/known-patterns/openrouter-byok-visual-review.md`** added + with 3 example files (instrument-vars, vars, synthesize.py). + Documents the inline-HTML pattern for visual review of UI/HTML + content via OpenRouter (BYOK) — useful when callers can't reach + Anthropic-direct creds, and as a reference example even after + hq-olrk landed since the inline-HTML pattern is still the + cleanest route for some UI-review workflows. Cross-town + contribution from midgard mayor (Co-Authored-By: midgard mayor / + openclaw@dataviking.tech). + +### Notes + +- The hq-olrk fix is independent of the hq-vw6o (v1.0.4) capability + gate — both ship correctly. Capability gate still gives clear + errors for known text-only models like Haiku 3.5; transport fix + ensures vision-capable models actually receive images via OR. + +- The per-rig Bun 1.3.14 segfault that affected the v1.0.5 + development cycle (see jotunheim hq-dpm1) is unrelated to + synthpanel — upstream Bun runtime regression, mitigated by pinning + Claude Code to 2.1.110 (Bun 1.3.13). Synthpanel itself is + unaffected; this note is here so anyone reading the development + history knows why this release cycle was bumpier than usual. ## [1.0.4] - 2026-05-10 diff --git a/site/.well-known/mcp/server-card.json b/site/.well-known/mcp/server-card.json index 3fe308d..96bf272 100644 --- a/site/.well-known/mcp/server-card.json +++ b/site/.well-known/mcp/server-card.json @@ -3,7 +3,7 @@ "name": "io.github.DataViking-Tech/synthpanel", "title": "SynthPanel", "description": "Run synthetic focus groups using AI personas. 12 MCP tools for single prompts, full panel runs, and v3 branching (adaptive) instruments across any LLM provider (Claude, OpenAI, Gemini, xAI).", - "version": "1.0.4", + "version": "1.0.5", "websiteUrl": "https://synthpanel.dev", "repository": { "url": "https://github.com/DataViking-Tech/SynthPanel", @@ -11,7 +11,7 @@ }, "serverInfo": { "name": "synthpanel", - "version": "1.0.4" + "version": "1.0.5" }, "capabilities": { "tools": { "listChanged": false }, @@ -23,7 +23,7 @@ "registryType": "pypi", "registryBaseUrl": "https://pypi.org", "identifier": "synthpanel", - "version": "1.0.4", + "version": "1.0.5", "runtimeHint": "uvx", "runtimeArguments": [ { "type": "positional", "value": "synthpanel[mcp]" }, diff --git a/site/index.html b/site/index.html index 1ac1561..288ebc7 100644 --- a/site/index.html +++ b/site/index.html @@ -45,8 +45,8 @@ "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Research Tool", "operatingSystem": "Cross-platform", - "softwareVersion": "1.0.4", - "dateModified": "2026-05-10", + "softwareVersion": "1.0.5", + "dateModified": "", "license": "https://opensource.org/licenses/MIT", "codeRepository": "https://github.com/DataViking-Tech/SynthPanel", "downloadUrl": "https://pypi.org/project/synthpanel/", @@ -140,7 +140,7 @@ class="mb-4 inline-flex items-center gap-2 rounded-full border border-emerald-400/30 bg-emerald-400/5 px-3 py-1 text-xs font-medium text-emerald-300" > - v1.0.4 — public beta + v1.0.5 — public beta

class="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-slate-800 py-6 text-xs text-slate-500" > - © 2026 DataViking · MIT-licensed · v1.0.4 · + © 2026 DataViking · MIT-licensed · v1.0.5 · None: blocks = _parse_headers_file(HEADERS_PATH.read_text()) cc = _cache_control(blocks, "/*") assert cc == "private, no-store, must-revalidate", ( - f"global /* Cache-Control must be the HTML bucket " - f"'private, no-store, must-revalidate' per dvi-25f, got {cc!r}" + f"global /* Cache-Control must be the HTML bucket 'private, no-store, must-revalidate' per dvi-25f, got {cc!r}" ) @@ -114,9 +113,7 @@ def test_api_json_endpoints_have_cacheable_api_bucket() -> None: "/.well-known/agent-skills/index.json", ): cc = _cache_control(blocks, path) - assert cc == expected, ( - f"{path} Cache-Control must be {expected!r} per dvi-25f cacheable-API bucket, got {cc!r}" - ) + assert cc == expected, f"{path} Cache-Control must be {expected!r} per dvi-25f cacheable-API bucket, got {cc!r}" def test_unhashed_static_extensions_have_5min_hedge() -> None: From 9719032ce0721f728fdac05c81b8b58721a94cf5 Mon Sep 17 00:00:00 2001 From: mayor Date: Sun, 10 May 2026 14:28:02 -0500 Subject: [PATCH 2/2] fix: mypy no-redef on inst_name + re-render site/index.html MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - commands.py:2709: drop the type annotation on the second inst_name declaration (already typed at the first occurrence in the same function scope, line 1545) so mypy stops flagging [no-redef] - site/index.html: re-render to match site/index.html.j2 — the previous render produced a slightly different byte sequence under the local python; the test uses python 3.14 in CI which is byte-stable Co-Authored-By: Claude Opus 4.7 (1M context) --- site/index.html | 2 +- src/synth_panel/cli/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/index.html b/site/index.html index 288ebc7..ba72ba9 100644 --- a/site/index.html +++ b/site/index.html @@ -46,7 +46,7 @@ "applicationSubCategory": "Research Tool", "operatingSystem": "Cross-platform", "softwareVersion": "1.0.5", - "dateModified": "", + "dateModified": "2026-05-10", "license": "https://opensource.org/licenses/MIT", "codeRepository": "https://github.com/DataViking-Tech/SynthPanel", "downloadUrl": "https://pypi.org/project/synthpanel/", diff --git a/src/synth_panel/cli/commands.py b/src/synth_panel/cli/commands.py index c7e6d3b..af322b2 100644 --- a/src/synth_panel/cli/commands.py +++ b/src/synth_panel/cli/commands.py @@ -2706,7 +2706,7 @@ def _cli_panelist_formatter(pr: PanelistResult, panel_model: str) -> dict[str, A from synth_panel.mcp.data import save_panel_result # Determine instrument name (pack name or filename stem) - inst_name: str | None = None + inst_name = None inst_arg = getattr(args, "instrument", None) if inst_arg: inst_path = Path(inst_arg)