code-yeongyu / code-yeongyu/senpi
OpenRouter catalog ignores reasoning metadata; mandatory-reasoning models (z-ai/glm-5.3) 400 on no-effort calls
- Dominant language
- TypeScript
- Stars
- 429
- Forks
- 98
- Avg merge
- 5h 3m
- Merged PRs (30d)
- 526
Description
## Environment
- omo `5.0.0-0.beta.30` → senpi engine `2026.8.30-3` → `@code-yeongyu/senpi-ai@2026.8.30-3` (resolved via the `@earendil-works/pi-ai` alias in senpi's package.json)
- Model: `z-ai/glm-5.3` on provider `openrouter`
## Repro
1. Run any senpi-based agent with `z-ai/glm-5.3` via OpenRouter.
2. Start a new interactive session (auto title generation on).
3. Every session shows:
```
Runtime error (session_title_generation): Reasoning is mandatory for this endpoint and cannot be disabled. (HTTP 400)
```
## Root cause
Three links in the chain:
1. **Catalog entry.** `dist/providers/data/openrouter.json` ships `z-ai/glm-5.3` with `thinkingLevelMap: {"xhigh": "xhigh"}` — no `off` key at all. OpenRouter's live metadata for this model is `reasoning: {mandatory: true, supported_efforts: ["max","high","low"], default_effort: "max"}`, so the correct map is `{"off": null, "low": "low", "high": "high", "max": "max"}` (this is exactly what upstream `@earendil-works/pi-ai@0.84.4` ships for the same model).
2. **Runtime guard treats missing as sendable.** In `api/openai-completions.js`, the OpenRouter thinkingFormat branch sends an explicit disable for no-effort calls whenever `thinkingLevelMap?.off !== null`. A *missing* `off` key is `undefined`, which passes the guard, so `reasoning: {"effort": "none"}` goes on the wire for calls that pass no reasoning level — session title generation is the main caller.
3. **OpenRouter rejects.** For mandatory-reasoning models, `effort: "none"` returns HTTP 400 "Reasoning is mandatory for this endpoint and cannot be disabled". The title generator retries once, fails deterministically, and surfaces the runtime error. Main agent turns are unaffected because they carry a reasoning level.
39 other entries in the shipped catalog also have a `thinkingLevelMap` without an `off` key (e.g. `anthropic/claude-opus-5`, `openai/gpt-5.6-*`, `deepseek/deepseek-v4-*`); any of them that is actually mandatory on OpenRouter 400s the same way.
## Why the fork diverged
Upstream fixed this class on 2026-08-25 in earendil-works/pi#8614 ("fix(ai): derive OpenRouter reasoning controls"):
- `packages/ai/scripts/openrouter-reasoning-options.ts` — converts OpenRouter reasoning metadata into a ThinkingLevelMap, with `mandatory === true` producing `off: null`.
- `packages/ai/scripts/generate-models.ts` — calls `getOpenRouterThinkingLevelMap(model.reasoning)` when constructing each OpenRouter model (upstream line ~1104) and spreads the result into the model.
The fork synced the first file (same commit, mirrored 2026-08-25) but not the second: `packages/ai/scripts/generate-models.ts` in this repo imports `getOpenRouterThinkingLevelMap` (line 8) and never calls it — the import is dead. Catalog regeneration therefore never consults OpenRouter reasoning metadata, and mandatory-reasoning models get heuristic-only maps like `{"xhigh": "xhigh"}`.
## Suggested fix
Port the #8614 wiring: call `getOpenRouterThinkingLevelMap(model.reasoning)` for OpenRouter models in `packages/ai/scripts/generate-models.ts` and regenerate the catalog. The runtime guard in `openai-completions.js` could additionally treat a *missing* `off` as "cannot disable" (`typeof thinkingLevelMap?.off === "string"`), but the data-side fix alone resolves the observed 400s.
## Local workaround for affected users
Add `"off": null` to the `thinkingLevelMap` of affected entries in the installed `dist/providers/data/openrouter.json` (no-effort calls then omit the `reasoning` param and take OpenRouter's model default).
Contributor guide
Research direction
Start in packages/ai/scripts/generate-models.ts and packages/ai/scripts/openrouter-reasoning-options.ts, then inspect api/openai-completions.js and the generated dist/providers/data/openrouter.json. Compare the existing OpenRouter model-generation path with upstream fix #8614 and regenerate the catalog. Done means mandatory-reasoning models receive metadata-derived maps, no-effort title generation no longer sends a disabling effort, and the relevant generation or runtime checks pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend-api-design, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100