Skip to content

fix(catalog): expose real thinking efforts to omp's effort picker - #42

Merged
simonsmh merged 1 commit into
simonsmh:mainfrom
fredtanxo:feat/omp-thinking-config
Oct 22, 2026
Merged

simonsmh merged 1 commit into
simonsmh:mainfrom
fredtanxo:feat/omp-thinking-config

Conversation

@fredtanxo

Copy link
Copy Markdown
Contributor

Problem

omp's effort picker reads model.thinking (its own ThinkingConfig), not the thinkingLevelMap this plugin provides. When thinking is absent, omp falls back to a generic minimal..high ladder, so a model like DeepSeek-V4-Pro (which only supports high/max) shows low/medium it cannot honor and hides max.

Fix

Add buildThinkingConfig(entry) in catalog.ts, mapping Qoder's thinking_config into omp's ThinkingConfig:

  • mode: "effort" - Qoder encodes effort as a reasoning_effort string.
  • efforts - the thinking_config.enabled.efforts keys, in canonical order (minimal..max).
  • defaultLevel - the effort marked is_default.
  • requiresEffort: !tc.disabled - mirrors the catalog's disabled branch.

Attach it as model.thinking on each registered model. earendil pi ignores the field (it reads thinkingLevelMap), so this only affects omp's picker.

Tests

Four buildThinkingConfig unit cases: effort-based with/without a disabled branch, effort ordering, and undefined for toggle-only or non-reasoning entries.

Notes

  • QoderThinkingConfig is defined inline (not imported from omp) because the plugin compiles against @earendil-works/pi-ai, which has no such type. Same pattern as the existing compat as Record<string, unknown> OMP shim in index.ts.
  • requiresEffort only mirrors the catalog's disabled branch; it is not currently consumed on omp's qoder path (omp reads it for anthropic/bedrock/budget models), so it has no request-side effect for Qoder today.
  • Toggle-only models (no efforts) still return undefined, keeping omp's generic behavior for those.
  • omp always shows off/auto/inherit regardless of metadata; this change only corrects the concrete effort list, not the always-on selectors.

omp populates its effort picker from `model.thinking` (not the
`thinkingLevelMap` that earendil pi-ai exposes). Without it, omp falls
back to a generic `minimal`..`high` ladder, so `DeepSeek-V4-Pro` offers
`low`/`medium` it does not supported and hides `max`.

Map `thinkint_config` into omp's `ThinkingConfig` shape (mode "effort",
ordered efforts, `defaultLevel`, `requiresEffort = !disabled`) and
attach it to each registered model. earendil pi keeps reading
`thinkingLevelMap` and ignores the new field.
@simonsmh
simonsmh merged commit 3c29b36 into simonsmh:main Oct 22, 2026
1 check passed
@simonsmh

Copy link
Copy Markdown
Owner

Merged — thank you! Squash-merged as 3c29b36 and shipped in v0.6.4: https://github.com/simonsmh/pi-provider-qoder/releases/tag/v0.6.4

Before merging I installed OMP 18.12.6 and verified this end-to-end against the live catalog, A/B against v0.6.3 (which has no thinking field):

  • DeepSeek-V4-Pro: v0.6.3 lists low, high, max — your high, max with this PR. ✓
  • Ultimate / Sonus / Qwen3.8-Max: v0.6.3 falls back to minimal, low, medium, high, hiding xhigh/max; with this PR they show the real catalog ladder. ✓
  • GLM-5.3-Flash similarly loses the bogus low; toggle-only models (Qwen3.7-Plus) and non-reasoning models are unchanged. ✓
  • An end-to-end omp -p --model qoder/DeepSeek-V4-Pro --thinking max request succeeded with the PR build.

One tiny note: on this OMP version the fallback for DeepSeek-V4-Pro included max already (the inaccuracy was the extra low) — the hidden-max symptom shows up on the Ultimate/Sonus class instead. Either way, the fix lands exactly as intended. Thanks again!

jischeng added a commit to jischeng/pi-provider-qoder that referenced this pull request Oct 23, 2026
Sync upstream v0.6.2..v0.6.5 while keeping this fork's features, then fix the
bugs that surfaced during the review. The upstream changes and the fixes touch
the same functions, so they land together rather than as artificially split
commits that would not pass on their own.

Upstream taken: tool-call aggregation by ID instead of stream index (simonsmh#39),
combined thinking-disable signals (simonsmh#40) with the effort-less catalog exception,
bounded model-busy recovery (simonsmh#41), omp effort metadata (simonsmh#42), service-enabled
Auto retention (simonsmh#43), COSY version tracking, and wire-format tool-call IDs.
Fork behaviour preserved: multi-account providers, per-account entitlement
pre-flight, price factors, keep-alive SSE, premature-disconnect surfacing,
XML <tool_call> recovery, and always-on-thinking effort fallback.

Fixes:

1. Cross-region credential leak. The global provider prefix ("qoder") is a
   strict prefix of the CN one ("qoder-cn"), so `id.startsWith(prefix + "-")`
   matched CN slots when listing global accounts. A global catalogue refresh
   then called /model/list with CN credentials against the GLOBAL endpoint and
   wrote that account into the global cache. Added isProviderOfMode() in
   region.ts and routed both account discovery and re-registration through it.

2. System-message folding was computed and discarded. The request body still
   prepended systemText to the unfiltered list, so it carried the top-level
   prompt plus every system turn instead of one merged message.

3. Orphan `tool` messages. A result whose id no preceding assistant turn
   declared was passed through (the "no assistant to pair with" case returned
   null, which read as permission rather than a veto). Upstreams reject that
   with "tool must follow a message with tool_calls", failing every later
   request in the session. Affected: resumed mid-turn, a user turn between call
   and result, and calls the lookahead trimmed.

4. Startup could hang forever. The default export awaits automatic login, and
   the PAT exchange / token refresh had no deadline, so an upstream that
   accepted the connection and never answered blocked extension load with no
   models, no UI, and no error. Bounded via authFetchSignal() (15s, merged with
   the caller's signal); the usage fetch defaults to a 10s timeout too.

5. Multi-pane torn reads of the COSY version cache. Plain writeFileSync let a
   second process parse a half-written body, throwing and leaving that pane on
   the bundled fallback version for the rest of the day. Now write-then-rename,
   matching the model catalogue.

6. Non-object tool arguments. `<tool_call>` XML whose arguments were an array,
   scalar, null, or a non-JSON string produced a value violating
   `ToolCall.arguments: JsonObject`, so argument validation failed instead of
   the tool running with no arguments.

7. Content order shuffled around tool calls. When a stream produced text, a
   tool call, then more text, the trailing text was appended to the block
   BEFORE the call: content read ["BEFOREAFTER", toolCall] and events addressed
   indexes that no longer matched. Root cause was a cached text-block index in
   ThinkingTagParser.emitText (and the equivalent branch in stream.ts) that was
   never re-checked once tool calls appended to `content`. Present upstream in
   v0.6.5 as well; its test asserted the shuffled order, so that assertion is
   corrected and a contentIndex-consistency check added.

Also fixes provider re-registration across session replacement (issue #1): the
registered-provider set was module-level, and ESM modules stay cached, so
/resume, /new and /fork built a ModelRuntime with zero Qoder providers and the
picker fell back to the default model. Now a per-instance WeakMap cleared on
session_shutdown.

Deviating points from upstream are marked `// fork:` in the source so the next
sync can enumerate them mechanically.
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