Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 111 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
6 changes: 3 additions & 3 deletions site/.well-known/mcp/server-card.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
"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",
"source": "github"
},
"serverInfo": {
"name": "synthpanel",
"version": "1.0.4"
"version": "1.0.5"
},
"capabilities": {
"tools": { "listChanged": false },
Expand All @@ -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]" },
Expand Down
6 changes: 3 additions & 3 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"applicationCategory": "DeveloperApplication",
"applicationSubCategory": "Research Tool",
"operatingSystem": "Cross-platform",
"softwareVersion": "1.0.4",
"softwareVersion": "1.0.5",
"dateModified": "2026-05-10",
"license": "https://opensource.org/licenses/MIT",
"codeRepository": "https://github.com/DataViking-Tech/SynthPanel",
Expand Down Expand Up @@ -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"
>
<span class="h-1.5 w-1.5 rounded-full bg-emerald-400"></span>
v1.0.4 — public beta
v1.0.5 — public beta
</p>
<h1
class="bg-gradient-to-br from-white to-slate-400 bg-clip-text font-mono text-5xl font-bold tracking-tight text-transparent sm:text-6xl"
Expand Down Expand Up @@ -705,7 +705,7 @@ <h2 class="mb-2 text-xs font-semibold uppercase tracking-wider text-slate-500">
class="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-slate-800 py-6 text-xs text-slate-500"
>
<span>
&copy; 2026 DataViking · MIT-licensed · v1.0.4 ·
&copy; 2026 DataViking · MIT-licensed · v1.0.5 ·
<a
href="https://github.com/DataViking-Tech/SynthPanel"
rel="noopener"
Expand Down
2 changes: 1 addition & 1 deletion site/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# synthpanel — Run synthetic focus groups with any LLM

v1.0.4 — public beta
v1.0.5 — public beta

# synthpanel

Expand Down
2 changes: 1 addition & 1 deletion src/synth_panel/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.4"
__version__ = "1.0.5"
2 changes: 1 addition & 1 deletion src/synth_panel/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 2 additions & 5 deletions tests/test_site_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ def test_global_default_cache_control_is_html_bucket() -> 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}"
)


Expand All @@ -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:
Expand Down
Loading