Skip to content

Release 0.5.2 - #252

Merged
hiyouga merged 5 commits into
mainfrom
dev
Oct 9, 2026
Merged

hiyouga merged 5 commits into
mainfrom
dev

Conversation

@hiyouga

@hiyouga hiyouga commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

hiyouga and others added 5 commits October 4, 2026 19:18
… table, and the mmsp client that calls it (#247)

## Summary

This adds an MMSP server, which serves the models of an explicit table
over HTTP as MMSP streams, and the `mmsp` client type, which calls it.
Python and TypeScript behave the same, and each client works against
either server.

- **Config:** one JSON file, passed as `--config` or
`MMSP_SERVER_CONFIG`:
- `models` is the table. Each row has `model_id`, `api_key` and
`server_model_id` (the id clients name), which are required, and
`client_type` and `base_url`, which are optional. An empty `client_type`
means the official client the model id's family names; an empty
`base_url` means that client's default endpoint. Adding a model means
adding a row.
- `api_keys` holds the bearer keys clients may send. Several are
allowed; an empty list makes an open server, and the server says so at
start.
- A cell written as `$VAR` or `${VAR}` in `base_url`, `api_key`,
`client_type` or `api_keys` is read from the environment when the file
loads. An unset variable stops the start.
- **Start:** `python -m mmsp.integration.server --config FILE [--metrics
FILE]` or `npm run server -- --config FILE [--metrics FILE]`, defaulting
to `127.0.0.1:25752`. It prints these lines, through `announce_server` /
`announceServer`, the same as when the playground starts it:
  - `Starting MMSP server at http://127.0.0.1:25752/v1`
  - `Serving models: ...`
  - the open-server line, when there are no keys
- **Code API:** `create_server_app(models, api_keys, metrics_path=None)`
/ `createServerApp({ models, apiKeys, metricsPath })`, `start_server` /
`startServer` (which returns the `http.Server`; both take the metrics
path), and `load_server_config` / `loadServerConfig`.
- **Routes:** a client's base URL ends in `/v1`, as with OpenAI and
vLLM.
  - `GET /v1/models` lists the table in OpenAI's list shape.
- `POST /v1/stream` takes `{model, messages, config}` and answers with
the public stream as SSE, then `data: [DONE]`. It sends a `: keep-alive`
comment every 15 s while the model is silent. Bytes travel as base64.
- **Metrics:** `ServerMetrics` keeps, since start, per model and in
total:
- requests, successes (ended with the stop event), failures (an error
event) and client disconnects (counted apart)
  - refusals (401, malformed body, unknown model)
- p50/p90 of first-event and total latency over the last 1000 successes
- token totals, `tokens_out` (thinking + response), `generation_ms`
(first event to end, summed) and `tps`
- each model's last outcome and error, and the latest 100 failures
(`errors`)

Beside those, a history of the last 60 days: 10 s buckets for 2 h, 1 min
buckets for 2 days, 1 h buckets for 60 days; a bucket that ages out of
its tier is added into the next one's, its latency samples thinned to 64
evenly spaced order statistics. The snapshot gains `since`, the moment
the history begins. `GET /v1/metrics` serves it under the key rule.
`?window=N` (10 to 5184000) or `?from=F&to=T` (unix seconds, at most 60
days apart), with `?columns=C` (1 to 1440, 360 by default), adds
`window`: columns of a span the server picks from 10 s … 12 h
(`bucket_s`), arrays covering the whole range, sums and per-column
values in total and per model, and `previous`, the range before. A bad
query is a 400. `--metrics FILE` / `metrics_path` / `metricsPath` keep
the history (buckets, errors, `since`) in one line of JSON, read at
start and written atomically every 60 s when something changed and at
stop. The playground's server page draws it, running or stopped (below);
the server's root serves no page.
- **Errors** have the shape `{"error": {type, message, ...}}`:
  - 401 when no valid key is sent
  - 400 for a malformed body
- 404 `NotFoundError` for a model not in the table, and for any other
route (including `/` and `/models` without the prefix)
  - 413 for a body over 50 MB
- Inside a stream, one error event, then `[DONE]`. A disconnect aborts
the upstream request.
- **`mmsp` client** (`mmsp/`, `MmspClient`): a forwarding client. It
yields the server's public stream as sent, bytes decoded, so what `curl
-N` shows is what it yields; it saves no trace (the server does). Reads
`MMSP_API_KEY` / `MMSP_BASE_URL` under the credential rule, default
`http://127.0.0.1:25752/v1`. `list_models` returns the table's ids.
- Every error the server reports, a refusal or an error event, is raised
as the new `UpstreamError`: `client`, `status` (the refusal's HTTP
status, else the upstream's own), `error_type` (the server-side class),
`message`, and `error`, the server's error object with that error's
fields.
- **Playground:**
- `mmsp` is in the client-type picker. Because the listing uses OpenAI's
shape, the OpenAI client types can list the server too.
- A **server page** at `/server/` opens from the top bar ("Open Server",
after "Open Tracer"). On it you fill in a models table, the server keys,
host and port.
- The page has a header (state, base URL with a copy button, Apply /
Save / Start-Stop) and three tabs: **Overview**, **Models**,
**Settings**.
- **Models:** each model row is one summary line (served id, upstream,
state) that opens to its five fields, one row at a time. Typing a model
id fills only Served as. Client type shows Auto and base URL shows
Default. **Settings:** the server keys, host and port, and the **File**
card: the saved file pretty-printed with token colours, plain-text keys
shown as `••••••••` until the eye reveals them (`$VAR` as written), the
path and a copy button.
- Every model row, key and the listen pair reads **Live**, **Saved** or
**Unsaved**: a word next to its dot, at every width.
- **Save**, or **Ctrl/Cmd+S** anywhere on the page, writes the table to
`MMSP_SERVER_CONFIG`, else `cache/server.json`, in the CLI's format;
`$VAR` cells are written as typed.
- **Start** runs the saved file and is enabled once one is saved.
**Apply** shows while the file differs from what runs and replaces the
running server with it; it builds the new app first, so a refused table
keeps the old server running. **Stop** stops it.
- **Overview, running:** a range of 15 min, 1 h, 6 h, 24 h, 7 d or 30 d,
or a custom From/To of at most 60 days (a calendar segment opening a
popover); six tiles (Requests, Success, Latency p50, Latency p90, Tokens
out, TPS), each with its delta against the previous window and a
sparkline; a card per model (state, requests, success, p50, p90, TPS,
last outcome, trend) that opens its row on Models; the Requests chart
(stacked ok / failed / dropped); the Latency chart (p50 and p90 lines);
the Errors list. The page polls `GET
/server/api/metrics?window=N&columns=C` (or `?from=F&to=T&columns=C`)
every 3 s while visible, asking 30–90 columns from the chart's width;
ticks fall on round local minutes, hours or days. **Stopped:** a
checklist (add a model, save, start), with the dashboard of the history
below it once there is one.
- The playground keeps the history in `<config>-metrics.json` beside the
config (`cache/server-metrics.json` by default), saved before Apply so
the new server continues it, and closed with the server.
- A refusal naming a model row shows under that row on Models and rings
the cell it names; any other shows under the header.
- The API: `GET`/`PUT /server/api/config` (with `text`, the file as
written), `GET /server/api/status` (which also returns the running
`config`), `GET /server/api/metrics` (the running server's
`ServerMetrics`, with `?window=N`, `?from=F&to=T` and `?columns=C` as on
`/v1/metrics`; stopped, `{"running": false}`, plus the history's
`since`, `errors` and the window asked for when the file can be read),
and `POST /server/api/start` / `restart` / `stop`.
- **Chat page:** entries in the model picker are told apart by model id,
client type, API key and base URL. An id listed from the server stays
pickable next to the built-in with the same id, described as `mmsp ·
127.0.0.1:25752`, and picking an entry fills all four fields.
- **Docs:** a README section, a site Server page (en/zh), the
models/errors pages, the skills references and the changelog entry.

## Design notes

- **The table is explicit (owner's decision).** There is no scanning:
the server serves exactly its rows. An empty `client_type` is
`AutoLLMClient`'s choice (`CLIENT_TYPE`, else the official client the
model id names); gateway ids such as `qwen/qwen3.8-27b` need one. A row
with `client_type: "mmsp"` pointing at another server's `/v1` relays it.
- **Upstream clients are built once per row, at start.** The whole table
is validated first: an empty table, a missing or empty column, a
duplicate `server_model_id`, or a client that refuses to build all stop
the start, with the row named. Sharing a client across requests is safe:
- Every client sets its fields only in its constructor, and
`streaming_response` keeps its state in locals.
- Python builds the SDK clients on the main thread and uses them on the
one background loop, as the playground already does with its session
clients.
- A wrong key or an unreachable endpoint still shows only on the row's
first request, as an in-stream `UpstreamError`.
- **A forwarding client, one error class.** `POST /v1/stream` → SSE →
`[DONE]` is already what `curl -N` does, and the server's upstream
client has already closed the items, stamped the events and saved the
trace. So `MmspClient` overrides `streaming_response` to yield each
event as parsed, with only base64 decoded back to bytes; it no longer
rebuilds items or saves a trace. Abort is one `run_with_abort` per event
(TypeScript: the fetch's `signal`). `streaming_response_stateful` is
unchanged: it reads the done items and the stop event the forwarded
stream carries. Errors: re-raising the five MMSP errors as themselves
meant rebuilding each from its wire fields (and patching the parse
error's message); one `UpstreamError` carrying the server's object keeps
every field without that, so `from_wire_error` / `fromWireError` are
gone. The server's `to_wire_error` keeps the typed fields, and a
`StreamProtocolError` message now travels whole.
- **Tiers.** 10 s buckets for 2 h, 1 min for 2 days, 1 h for 60 days. A
tier's floor is aligned to the next tier's bucket, so a whole minute
(hour) moves at once and every stretch of time is stored in exactly one
tier: at most 5106 buckets per series, each 9 counters and 2 × 64
samples. That is the memory bound, and the file holds the same buckets.
Roll-up adds the counts and thins the latency samples to 64 evenly
spaced order statistics; a column's percentiles are over the thinned
samples of the buckets inside it, so coarse columns are as honest as
fine ones, and a 15 min window at 10 s gives exactly the numbers of the
last round. Compaction runs at most once a minute. A request's outcome,
latency and tokens go into the bucket it began in, wherever that bucket
has rolled to (none past 60 days; it still counts since start), so a
bucket's requests equal its outcomes plus what is still streaming; a
refusal goes into the bucket of its moment. `previous` is `null` unless
the whole previous range lies inside the history (from `since`), so a
delta never compares against time nobody measured.
- **Column spans.** The server picks the span per request: the smallest
of `COLUMN_SPANS` (10 s … 12 h) that fits `?columns=` and is no finer
than the tier storing the range's start; the page asks 30–90 from its
width. 12 h is the widest because day columns would be UTC days on a
local axis: columns align to UTC multiples of the span, labels are local
(lead's decision, accepted for now). `start` is no longer clamped to the
server's first bucket; the arrays always cover the range, so the page
dropped its padding.
- **Persistence.** The CLI writes a history only with `--metrics FILE`;
the playground always does, beside its config, keyed by the config path
so two playgrounds on two configs never share one (lead's decision).
Writes go through a temporary file and a rename, every 60 s when
something changed and at close; a failed write prints once and is
retried. Apply saves first, so the new store loads everything up to that
moment; the old one's close may then write a few hundred milliseconds of
outcomes the new one overwrites, the one place a request can go missing.
The since-start counters stay this run's; `since` says where the history
begins. A series is its `server_model_id`: a renamed model starts fresh,
a removed one's history stays in the file and returns with its id. An
unreadable file starts fresh and says so once; the stopped page reads
the file silently through `ServerMetrics.from_history` / `fromHistory`.
- **File card.** `GET /server/api/config` returns `text`, the file as
written; the card prints the parsed JSON with 2 spaces and colours keys,
strings, numbers and punctuation from the existing tokens. Plain-text
keys are masked until the eye reveals them (lead's decision); Copy
copies what is shown.
- **TPS.** `tokens_out = thoughts + response`; `generation_ms =
max(total_ms − first_event_ms, 1)` per success; `tps = Σ tokens_out / Σ
generation_ms × 1000`, one decimal half up in both languages, `null`
before a success. It is a ratio of sums, never a mean of per-request
rates, so long generations weigh by their length; the 1 ms floor only
touches a stream whose first event is its stop event.
- **Dashboard colours.** Validated with the dataviz palette check
against both surfaces: ok = accent, failed = red, dropped = grey. Green
against red fails the colour-vision check in both themes, so success is
never green in a chart; green stays the Live dot and the "improved"
delta. p90 is the accent line, p50 muted; no band, no `p50–p90` text.
- **Case-sensitive routing.** Express routes case-insensitively by
default, so `POST /V1/stream` bypassed the `/v1/` key check. The
TypeScript server now routes case-sensitively, as Flask does, and a test
covers it in both languages.
- **The playground's server page.**
- One file per language: the page is one HTML document,
`SERVER_TEMPLATE`, at the end of `server.py` / `server.ts` between two
marker lines, identical in both, with the chat page's tokens, combobox
and client-type lists and the tracer's top bar. The embed script
rewrites only that region; the backends edit only above it.
- The config's shape check and `$VAR` resolution moved into
`resolve_server_config` / `resolveServerConfig`, which the file loader
and the page share.
- On a failed bind, werkzeug's `make_server` prints the error and calls
`sys.exit(1)` instead of raising, and it treats a `unix://` host as a
socket file to replace. So the playground binds the socket itself, as
werkzeug would, and hands over the fd; a port in use comes back as a
400.
  - `server_base_url` / `serverBaseUrl` put an IPv6 host in brackets.
- `GET /server/api/metrics` reads the running server's `ServerMetrics`
in-process, so the page needs no server key.
- **Keep-alive.** Without it, Node's fetch (undici's 300 s limit between
body chunks) and common reverse proxies would cut a long thought.
- **Shared e2e files (owner-approved).** These add one `ROUTING_CASES`
row and an env-gated `AVAILABLE_MODELS` entry (`MMSP_API_KEY` +
`MMSP_BASE_URL` + `MMSP_MODEL`). CI sets none of them.

## Tests

On vps4:
- **Unit:** Python 632 passed / 15 skipped; TypeScript 18 suites, 620
passed. Each was run twice. ruff, eslint, prettier, tsc and `npm run
build` are clean, and the site builds.
- **Live checks, both real playgrounds:** each history file was seeded
with a deterministic 60-day synthetic history (`history.mjs`, same
format as the server writes), with real `deepseek-flash` traffic on top.
Python passed 89 checks and TypeScript 88, none failed.
- The 15 min tiles moved within about 2.4 s and matched the traffic log.
- `?window=86400&columns=72` returns `bucket_s` 1200 with 72 entries on
`/v1/metrics` and `/server/api/metrics`.
- 24 h, 7 d and 30 d draw the synthetic shape: day labels at midnights,
weekly ticks on Mondays, and more than 10,000 requests over 30 d.
  - A custom range of yesterday 09:00–18:00 shows a busy day.
- Apply, Stop and Start again never lowered the count over a fixed
range, and Stop shows the past under the checklist.
- The Settings File card masks `sk-bad` until the eye reveals it, and
Copy copies what is shown.
- **Stand-in layout checks:** 337 passed, none failed. They cover every
preset and a custom range at 1280 and 390 px, light and dark: tick
labels never overlap, the popover works from the keyboard, there are no
console errors and no horizontal scroll.

- **Real-model e2e through the server** (earlier rounds, `e2e_mmsp.sh`):
five models through both servers, plus the cross-language pairs.

- **Offline suites** (`test_mmsp_server.py`, `mmsp-server.test.ts`,
`test_playground_server.py`, `playground-server.test.ts`) put a scripted
upstream behind an in-process server and require the mmsp client's
events to equal the upstream's public stream and the server's raw SSE.
They also cover the `UpstreamError` round trips, auth, the 404s, the
listing, startup validation, config loading, the start lines, abort,
keep-alive, the metrics and their windows, the playground's server API,
and the page's ids and functions.

`site/src/docs/server.zh.md` has not been through
`site/scripts/polish-zh.mjs`: there is no TokenDance key on the dev
machine or on vps4.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…google-official (#248)

## Summary

The official Gemini client is renamed `google-official`, after its
vendor, like `openai-official` and `anthropic-official`.

- `gemini_official/GeminiOfficialClient` →
`google_official/GoogleOfficialClient` (Python and TypeScript, via `git
mv`).
- `OFFICIAL_CLIENT_TYPES` lists `google-official`, and the `gemini-`
model family routes to it.
- `gemini-official` still works as an unlisted alias, because 0.5.x
shipped that name. This follows the precedent of `google-genai` keeping
`gemini-generate-content`. A new `ROUTING_CASES` row in each language
checks the alias.
- Also updated:
  - The registry's Gemini entries.
- The playground chat page and server page: the default base URL key,
and the client-type descriptions in `server.html`, re-embedded into
`server.py` / `server.ts`.
- Tests, READMEs, site docs, skills, the dev skill and the copilot
instructions.
- `GEMINI_API_KEY` / `GEMINI_BASE_URL` are unchanged.
- Changelog entry: `changelog/unreleased/2026-10-09-google-official.md`
(+ `.zh.md`).

### Compatibility

Not marked breaking:
- `AutoLLMClient(client_type="gemini-official")` keeps working.
- The client classes are not exported from the package root. Only a deep
Python import of `mmsp.gemini_official` breaks, and the TS exports map
already blocks deep imports.

## Test plan (vps4)

- [x] Python unit: 633 passed, 15 skipped. `ruff check` and `ruff format
--check` are clean.
- [x] TS unit: 621 passed. eslint, `tsc --noEmit`, prettier and build
are clean.
- [x] Gemini e2e: 14/17 passed in Python and 14/15 in TS. The failures
are connection errors (`APIConnectionError` / `fetch failed`) in the
image tool-result and TTS tests. The same tests failed the same way on
an unmodified dev tree on vps4, so they look like a vps4 network
problem; CI runs them too.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

Support Claude Haiku 5.5 (`claude-haiku-5-5`, released 2026-10-07) and
refresh the Claude 5.5 docs snapshot. Opus 5.5 and Sonnet 5.5 were
already supported. Their official pages changed only in the lineup table
and in Sonnet 5.5's cache-read price.

- **`anthropic-official`** serves `claude-haiku-5-5`, matched by the
explicit `haiku-5-5` version substring. That also covers Bedrock's
`anthropic.claude-haiku-5-5`.
- `thinking_level="none"` → adaptive thinking at `low` effort, as on
Opus 5.5 / Fable 5.1. The other levels send the effort of the same name.
- `tool_choice="required"` or a tool-name list →
`UnsupportedParameterError`, as on Opus 5.5 / Sonnet 5.5.
  - `fast_mode` → `UnsupportedParameterError`.
- The `_ALWAYS_THINKING` tuple is renamed `_LOW_EFFORT_FOR_NONE` (TS
`LOW_EFFORT_FOR_NONE`), because Haiku 5.5 is not always-thinking.
- **Registry**
- New `claude-haiku-5-5` row: $0.10 / $0.50, cache reads $0.01, 1M
context. This is the ≤100K-prompt tier; a comment notes that every rate
is 5× above 100K, the same convention as MiniMax-M3 and gpt-6.1-sol.
- New OpenRouter `anthropic/claude-haiku-5.5` row, with prices from the
live `/models` API.
- Sonnet 5.5 cache reads drop $0.20 → $0.10 in both the official and
OpenRouter rows, per the official pricing page and OpenRouter's API.
- **Tests:** a `claude-haiku-5-5` entry in `AVAILABLE_MODELS`, and rows
in the existing thinking-level and summary mapping tables.
- **Docs**
- `llmsdk_docs/claude5_5/` adds the Haiku 5.5 overview, migration guide
and what's-new pages, plus the pricing page. The models overview, the
Opus and Sonnet 5.5 pages and their migration guides are refreshed.
  - The site and skills list the new model.
- **Changelog:** `changelog/unreleased/2026-10-09-claude-haiku-5-5.md`
(+ `.zh.md`).

## Live probes (claude-haiku-5-5, raw HTTP, 2026-10-09)

| Request | Result |
| --- | --- |
| no thinking config | 200; adaptive thinking runs by default (it thinks
at `max` even when no display is asked for) |
| `thinking: disabled`, effort default / low / high | 200, no thinking |
| `thinking: disabled`, effort xhigh / max | 400 "effort 'xhigh' is not
supported when thinking is disabled on this model" |
| `thinking: disabled` + `display` | 400 "Extra inputs are not
permitted" |
| `between_tools` | 400 "not supported for this model" |
| `enabled` + `budget_tokens` | 400 |
| adaptive, effort low … max | 200 |
| `speed: "fast"` (+ beta) | 400 "does not support the `speed`
parameter" |
| `temperature: 1` / `0.5` | 200 / 400 "`temperature` is deprecated for
this model" (the client already rejects ≠ 1 for all Claude) |
| `tool_choice` any / named tool | 200, but the response has no thinking
block |
| top-level `cache_control` 5m / 1h | 200 |

**Replay of a thinking-bearing assistant turn,** both before tool
results and in a later follow-up:
- Missing `signature` → 400 "Field required".
- Tampered signature → 400 "Invalid `signature`".
- Empty or edited thinking text → 200, with the input token count
unchanged, so the signature carries the thinking.
- Dropping the thinking block → 200, but the thinking is lost.
- Dropping the tool_use `caller` field → 200.

The existing replay (thinking text + signature) is therefore kept
unchanged. Usage now also carries
`output_tokens_details.thinking_tokens`; the client keeps reading
`output_tokens` as before.

## Decisions (confirmed with the owner)

- **NONE:** Haiku 5.5 does accept `disabled`, but NONE maps to adaptive
+ `low`, consistent with Opus 5.5 / Fable 5.1. The alternatives were
`disabled` and omitting the config (which leaves adaptive thinking at
the default effort).
- **Forced tool_choice:** the API accepts it but silently skips
thinking. It is rejected for consistency with the rest of the 5.5
family.
- **Tiered pricing:** the registry has a single price per bucket, so it
stores the ≤100K tier and notes the 5× factor in a comment.

## Test plan (vps4)

- [x] Python unit: 635 passed, 15 skipped. `ruff check` and `ruff format
--check` are clean.
- [x] TS unit: 623 passed. eslint, `tsc --noEmit`, prettier and build
are clean.
- [x] Python e2e `-k claude-haiku-5-5`: 12 passed, 3 skipped (image
generation / TTS / embedding are off by capability flags).
- [x] TS e2e `-t claude-haiku-5-5`: 15 passed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…tion; the server page tests rows, warns while open and confirms a start (#250)

## Summary

Four playground fixes the owner asked for before 0.5.2.

1. **Chat: sending to another model starts a new conversation.**
- **Before:** the backend silently started an empty history whenever the
client options changed, while the page kept showing the earlier messages
as one continuous chat.
- **Now:** sending to another entry (a different model id, client type
or base URL) calls `/api/clear`, starts a new session, and draws a
divider: "New conversation with `<model>` · the messages above are not
sent".
- **Composer hint:** it says this before Enter is pressed. Picking
another model and back before sending changes nothing.
- **Key or extra-header edits:** the conversation continues. `/api/chat`
now carries a session's history over when it rebuilds the client, so no
history resets silently.
2. **Server page: Test / Test all for model rows**, sent straight to the
upstream through `POST /server/api/test`.
- **Input:** `{"model": row}`, the row as typed. `$VAR` cells are read
from the playground's environment.
- **Output:** "Passed" with first token, total, tokens out (thinking +
response) and TPS, or "Failed" with the upstream's message.
   - **Limits:** three rows at a time, 60 s each.
- **First token:** the first event that carries an item. Anthropic
reports usage in a stop event at `message_start`, before any token, so
the first event alone would undercount the wait.
3. **Open-server banner:** while the running server has no keys, the top
of the page says anyone who can reach its URL can use the models, with
an **Add a key** button.
4. **Start / Apply confirmation:**
- **Dialog contents:** a native `<dialog>` showing the listen URL, the
model count and the keys the saved file holds, plus the open-server
warning when there are none.
- **Keyboard:** Enter confirms, Escape cancels, and Ctrl/Cmd+S does
nothing while it is open.
- **Why Apply confirms too:** it replaces the running server, so it can
change the URL or drop the keys.
   - **Stop:** doesn't ask.

The site docs (`server`, `tracing`) and an unreleased changelog entry
are included.

## Decisions

- **Owner:** "new conversation", not carrying history across models or
keeping a thread per model.
- **Owner:** the test goes straight to the upstream, not through the
running server.
- **Earlier messages stay visible above the divider:** comparing models
on one prompt is what the playground is for.
- **Key or extra-header edits continue the conversation:** fixing a
wrong key mid-chat shouldn't wipe it.

## Test plan (vps4)

- [x] Python: 649 passed, 15 skipped. `ruff check` and `ruff format
--check` clean.
- [x] TS: 637 passed. eslint, `tsc --noEmit`, prettier and build clean.
- [x] Server page against the stub in headless Chrome: 420 checks, 0
FAIL.
- Covers the dialog (contents, Escape, focus trap, Enter confirms once),
the banner (shows while open, hides after a key is applied) and Test /
Test all (disabled until model id + key, Passed / Failed text, a result
is dropped when the row is edited).
- The palette checker's chroma-floor lines on the unchanged neutral
greys are not page checks.
- [x] Live, both playgrounds (Python 25761, TS 25762), with real
DeepSeek:
  - Server checks: 104 / 103 ok, 0 FAIL.
- `/server/api/test`: `ok: true` with ~300 TPS. An unset `$NOPE` gives
the unset-variable message, `sk-bad` gives the vendor's 401, and
`client_type: nope` gives the unknown-client message.
  - The banner appears exactly when the console prints `Open server:`.
- [x] Chat switch check on both playgrounds:
- Switching the client type posts `/api/clear` with the old session,
draws one divider, and the reply no longer knows the earlier word.
- A key-only edit keeps the session, and the model still recalls the
word from after the switch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Folds `changelog/unreleased/` into `changelog/0.5.2/` (#247 MMSP server
and `mmsp` client, #248 `google-official`, #249 Claude Haiku 5.5, #250
playground fixes), adds the 0.5.2 line to `CHANGELOG.md` /
`CHANGELOG.zh.md`, and versions `mmsp` and `@prismshadow/mmsp` 0.5.2.

Verified on vps4: Python 649 passed, 15 skipped; TS 637 passed; build
clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 9, 2026 21:45

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
hiyouga merged commit 790b270 into main Oct 9, 2026
5 of 6 checks passed
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