# Explicit model reference pairs, and a batch of review fixes

The provider group is no longer resolved automatically anywhere — a model is referenced by the complete `(provider, model_id)` pair or not at all — and a code review of the release branch turned up a further set of correctness, accessibility and documentation defects, all fixed here.

- **The provider is never inferred, guessed, or defaulted (core, CLI, server, web).** Two mechanisms used to pick a group on the user's behalf, and both are gone. Catalog inference (`inferProviderForUpstream`) took the first bare-id match, and gateways resell vendor models under their upstream ids — `glm-5.2`, `qwen3.7-max`, `qwen3.7-plus` and `deepseek-v4-pro` each belong to two groups now — so `penguin config model add --model-id glm-5.2 --api-key <key>` wrote a Z.AI credential onto the Qwen Token Plan entry and sent it to Alibaba's endpoint (on the previous release the same command resolved to `zhipu`). Config resolution (`resolveModelRef`) separately matched a bare `model_id` against the Project's configured models whenever it happened to be unique. Both are deleted, along with `inferProviderForUpstream` and `providersForUpstream`. The rule everywhere is now **both halves or neither**: `--provider` is required on `config model add`; on `run` / `chat` the pair as a whole stays optional (omit both for the Project default) but supplying one half is an error; `addModel` and `Agent.createSession` require the pair at the type level; `POST /sessions` and the schedule routes return 400 for a half reference, and a schedule file persisted with only a `modelId` is reported invalid instead of silently binding to a guessed group. `run_subagent` gains a `provider` argument beside `model_id` and refuses half a reference, so a delegating model cannot land a subagent on the wrong vendor either. The trace viewer stops attributing cost to a first-match guess for pre-tag traces, showing no cost instead. CLI help, the CLI/models/server-api/quickstart/configuration/interfaces docs (en+zh), the READMEs and every skill that scripts the CLI were updated to pass the pair.
- **Escape no longer wipes the composer (Web App).** The slash menu's Escape handler cleared the whole input — correct back when the menu only opened on a draft starting with `/`, but the menu became positional this release, so dismissing it from a `/token` mid-draft destroyed everything typed so far, unrecoverably (the textarea is controlled, so undo could not bring it back). Escape now only dismisses the menu, mirroring `@` mentions via a `slashDismissed` offset. Separately, `removeSlashToken` collapsed *every* run of two or more spaces in the surviving draft rather than the join seam, silently reflowing pasted code, YAML, aligned tables and indentation; it now touches the seam alone.
- **The penguin.ooo installer forwards its arguments.** `https://penguin.ooo/install.sh` piped the real installer straight into `sh`, so `--universal` and `--version` were silently discarded — leaving unsupported-architecture users with advice ("re-run with `--universal`") that could not work through the documented URL, and turning version pinning into "latest". The forwarder now downloads to a temp file, runs it with `"$@"`, and propagates the exit code; this also removes the partial-execution hazard of piping a truncated download into a script that deletes the old install before moving the new one in.
- **Speed test reports throughput or nothing.** Speed mode raised the output cap to 64 tokens but reused the connectivity prompt, which demands a single word — so the rate was computed over a 1-3 token sample dominated by the closing usage chunk's round trip, and the same model graded green or yellow on 30ms of jitter. Speed mode now has its own prompt that keeps generating into the cap, and the new pure `probeTps` drops the rate below meaningful sample floors (16 tokens, 100ms), leaving TTFT alone rather than a fabricated number.
- **Announcement bar (landing).** Rotation pauses on keyboard focus as well as hover, so the focused link is no longer left inside an `aria-hidden` slide translated out of view (an ARIA violation and a WCAG 2.2.2 failure); under `prefers-reduced-motion` the carousel no longer strands itself on the clone slide.
- **Streaming fidelity rules are pinned by tests.** The two rules added with the AgentHub 0.4.0 payload — a `fidelity.signature` closes the open text segment, and fidelity keys merge rather than replace across deltas — had no coverage; both mutations passed the whole suite. Tests now fail on either, guarding against a replay handing the provider a signature covering text it never signed, and against GPT-5 phase segmentation being lost on resume.
- **New benchmark results, and a comparison that changed shape.** Both suites are refreshed, and the design of the comparison changed with them: instead of running all three harnesses on one shared DeepSeek V4 Pro, each now runs the model it is normally paired with — PenguinHarness on DeepSeek V4 Pro, Claude Code on Claude Opus 4.8, OpenAI Codex on GPT-5.5 — so the model column is part of the result rather than a constant, and Tokens/cost are suite totals rather than per-run means. Data analysis (15 tasks, single run): 66.67% for PenguinHarness against 53.33% for both rivals, at $0.55 versus $38.48 and $19.41. Coding (40 tasks × 2 runs, accuracy over all 80 outcomes): 71.25%, level with Codex and behind Claude Code's 86.25%, at $3.81 versus $220.08 and $146.97. The copy moved with the data — the section no longer claims "same model" or "highest accuracy of the three", both of which the new results contradict, and leads on cost-per-quality instead; the 35×/70×/58×/39× multiples in that copy are now asserted against the data by unit test so they cannot drift on the next refresh. The README and blog charts are generated from the same source by `render-benchmark-svg.mjs`. Bars stay linear and zero-based, but carry a minimum length: at true scale the cost panels put PenguinHarness at ~2px, which reads as an empty slot rather than as the winning series, so the floor keeps it visible while still plainly the shortest bar — with the exact figure printed beside every one. The README's chart caption is now a single-line conclusion rather than a paragraph of run settings, matching the RAG cost hook.
- **The demo videos reach the landing page.** The self-improvement section's "demo video coming soon" pill is replaced by the recording itself, and the RAG case tab plays the finished app instead of only picturing it — both following the READMEs, both locale-matched (`evo_zh`/`evo_en`, `rag_zh`/`rag_en`). The files stay out of this repo: at ~9 MB each against a ~17 MB history, committing four would triple what every contributor clones for assets only the marketing site shows, so they are served from the sibling `penguin-harness-community` repo via `raw.githubusercontent`, which answers with `accept-ranges: bytes` (seeking works) and `access-control-allow-origin: *`. Its `application/octet-stream` type does not block playback — `nosniff` is enforced for scripts and styles, not media — and this was verified end to end in Chromium rather than assumed. Every embed pairs a poster with `preload="none"`, so a page view downloads one ~37 KB still and no video byte until someone presses play (asserted by watching the network on load: zero mp4 requests). The standalone embed deliberately drops the BrowserFrame chrome the screenshots use, because the self-improvement recording is a narrated deck rather than the product's own UI; the RAG case keeps the frame, because there it really is the app.
- **The draft page is genuinely centred, and the upward menus stop clipping.** The brand block sat inside the upper flex spacer, so the gap above it was shorter than the gap below the example cards by exactly the brand's own height — the whole page rode high, and the slash menu, which opens upward from the composer, ran into the top of the scroll area. The brand now lives in the same centred block as the composer, pills and example tasks, making the two gaps identical (measured: 134/134 at 1000px tall, 84/84 at 900, 34/34 at 800). The slash and `@` menus additionally cap their height to the room actually measured between the composer and the nearest clipping ancestor instead of a fixed `40vh`, which could not know that distance: previously the menu was cut by ~50px at 900px tall and still 5px at 800px, and now it fits at every height down to 620px, shrinking and scrolling internally instead of being clipped.
- **Web polish.** Speed-test badges reset when the active Project changes (results are keyed by `(provider, model_id)`, so they used to leak across Projects); the work-group header ticks and settles on the same span, so its duration never jumps backwards; the markdown renderer's component map has a stable identity, so streaming no longer remounts closed code blocks and drops text selection or Copy state.
- **Sources are diffable again.** Four files embedded a literal NUL byte as a composite-key separator instead of the `\0` escape, which made git treat them as binary — `catalog-sync.ts`, the model-catalog and models tests, and the scheduler produced no reviewable diff, no `git grep` output, and no textual merge. The runtime value is unchanged.
- **Docs and metadata.** The workspace `files/content` preview mode and its response headers are documented in the server API reference (en/zh); the skills README lists the groups that actually exist; the skill-library page's docstrings describe the per-skill palette and two-column grid it really renders; `penguin-cli`'s `updated` date matches its version bump; the penguin-sdk RAG recipe stops installing the entire skill library into the app it generates; the README model table no longer lists Kimi K3 under Moonshot, which does not serve it; and a Chinese comment in the catalog's pricing block is translated.
- **A test that did not test.** The early-title case asserting that sub-session output does not count toward the 1000-character trigger stayed green with the origin guard removed; it now fails without it.
