code-yeongyu / code-yeongyu/senpi

OpenRouter catalog ignores reasoning metadata; mandatory-reasoning models (z-ai/glm-5.3) 400 on no-effort calls

Open
#1,239 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.