@oh-my-pi/pi-catalog 16.1.21 → 16.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.1.22] - 2026-06-26
6
+
7
+ ### Added
8
+
9
+ - Added `OpenAICompat.replayReasoningContent` — auto-enabled for the built-in local OpenAI-compatible providers (`llama.cpp`, `lm-studio`, `vllm`, `ollama` on `openai-completions`) and for any provider pointed at a loopback / RFC1918 / `*.local` baseUrl. NOT gated on `spec.reasoning`: the runtime discovery paths for `llama.cpp` / `lm-studio` / `openai-models-list` hardcode `reasoning: false` because the upstream `/models` endpoints don't advertise the capability, while the stream parser still records incoming `reasoning_content` deltas as thinking blocks — gating on the spec flag would leave every discovered local Qwen / DeepSeek model re-triggering #3528. The encoder only writes `reasoning_content` when a thinking block actually exists on the turn, so the flag is a no-op on pure-text histories. Built-in proxy providers (currently `litellm`) are excluded from both checks because they forward to an unrelated upstream that gains no KV-cache benefit and may 400 on the extra field; users running a custom proxy in front of a llama.cpp-style backend can opt in via the sparse `compat.replayReasoningContent: true` override. Signals to the `openai-completions` encoder that preserved `thinking` blocks must be re-emitted as `reasoning_content` on every assistant turn so chat templates that reconstruct `<think>…</think>` from the field (Qwen3, DeepSeek-R1, GLM-5.x) keep the prior turn's tokens byte-stable and llama.cpp's prefix KV cache survives. ([#3528](https://github.com/can1357/oh-my-pi/issues/3528))
10
+
5
11
  ## [16.1.20] - 2026-06-25
6
12
 
7
13
  ### Fixed
@@ -185,6 +185,20 @@ export interface OpenAICompat {
185
185
  requiresReasoningContentForAllAssistantTurns?: boolean;
186
186
  /** Whether the provider accepts a synthetic placeholder (e.g. ".") for missing reasoning_content on tool-call turns. Default: true. Set to false for providers like DeepSeek that validate the exact reasoning_content value. */
187
187
  allowsSyntheticReasoningContentForToolCalls?: boolean;
188
+ /**
189
+ * Replay preserved thinking blocks as `reasoning_content` (or the configured
190
+ * `reasoningContentField`) on EVERY assistant turn that carried reasoning,
191
+ * regardless of whether the upstream provider validates the field. Local
192
+ * llama.cpp-style servers (llama.cpp, LM Studio, vLLM, sglang, Ollama in
193
+ * openai-completions mode) re-tokenize the full chat-template prompt every
194
+ * request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct the `<think>`
195
+ * block from `reasoning_content`. Dropping the field re-renders the
196
+ * assistant turn without `<think>`, diverging from the slot's KV cache state
197
+ * and forcing full prompt re-processing (#3528). Default: auto-detected
198
+ * (loopback/private baseUrl or local provider id with thinking-enabled
199
+ * models).
200
+ */
201
+ replayReasoningContent?: boolean;
188
202
  /** Whether assistant tool-call messages must include non-empty content. Default: false. */
189
203
  requiresAssistantContentForToolCalls?: boolean;
190
204
  /** Whether the provider supports the `tool_choice` parameter. Default: true. */
@@ -391,6 +405,7 @@ export interface ResolvedOpenAISharedCompat {
391
405
  requiresReasoningContentForToolCalls: boolean;
392
406
  requiresReasoningContentForAllAssistantTurns: boolean;
393
407
  allowsSyntheticReasoningContentForToolCalls: boolean;
408
+ replayReasoningContent: boolean;
394
409
  requiresThinkingAsText: boolean;
395
410
  requiresMistralToolIds: boolean;
396
411
  requiresToolResultName: boolean;
@@ -418,7 +433,7 @@ export interface ResolvedOpenAISharedCompat {
418
433
  * `buildModel`; request handlers read fields and never detect, resolve, or
419
434
  * allocate.
420
435
  */
421
- export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "requiresJuiceZeroHack" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
436
+ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "requiresJuiceZeroHack" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
422
437
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
423
438
  extraBody?: OpenAICompat["extraBody"];
424
439
  cacheControlFormat?: OpenAICompat["cacheControlFormat"];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "16.1.21",
4
+ "version": "16.1.22",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -34,12 +34,12 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@bufbuild/protobuf": "^2.12.0",
37
- "@oh-my-pi/pi-utils": "16.1.21",
37
+ "@oh-my-pi/pi-utils": "16.1.22",
38
38
  "arktype": "^2.2.0",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "@oh-my-pi/pi-ai": "16.1.21",
42
+ "@oh-my-pi/pi-ai": "16.1.22",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -166,6 +166,53 @@ function detectStrictModeSupport(provider: string, baseUrl: string): boolean {
166
166
  );
167
167
  }
168
168
 
169
+ /**
170
+ * Local OpenAI-compatible inference servers whose chat templates re-tokenize
171
+ * the entire prompt every request — llama.cpp prefix-KV-cache reuse only
172
+ * survives when the rendered tokens stay byte-identical across turns. The
173
+ * runtime auto-enables {@link OpenAICompat.replayReasoningContent} for these
174
+ * providers (and for any provider pointed at a loopback / RFC1918 baseUrl) so
175
+ * Qwen3 / DeepSeek-R1 / GLM templates can reconstruct the prior assistant
176
+ * turn's `<think>` block from `reasoning_content` (#3528).
177
+ */
178
+ const LOCAL_OPENAI_COMPAT_PROVIDERS = new Set(["llama.cpp", "lm-studio", "vllm", "ollama"]);
179
+
180
+ /**
181
+ * Local proxy providers that share the loopback-default baseUrl but forward
182
+ * to an unrelated upstream (OpenAI, Anthropic, …) rather than running a
183
+ * chat-template renderer themselves — `replayReasoningContent` would push
184
+ * `reasoning_content` to the upstream, which gains no KV-cache benefit and
185
+ * may 400 on the extra field. Excluded from BOTH the provider check above
186
+ * and the loopback heuristic below; users who want the replay on a custom
187
+ * proxy setup can opt in via the sparse `compat.replayReasoningContent`
188
+ * override.
189
+ */
190
+ const PROXY_OPENAI_COMPAT_PROVIDERS = new Set(["litellm"]);
191
+
192
+ function hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {
193
+ if (!baseUrl) return false;
194
+ let hostname: string;
195
+ try {
196
+ hostname = new URL(baseUrl).hostname.toLowerCase();
197
+ } catch {
198
+ return false;
199
+ }
200
+ if (
201
+ hostname === "localhost" ||
202
+ hostname === "127.0.0.1" ||
203
+ hostname === "0.0.0.0" ||
204
+ hostname === "::1" ||
205
+ hostname === "[::1]"
206
+ ) {
207
+ return true;
208
+ }
209
+ if (/^10\./.test(hostname)) return true;
210
+ if (/^192\.168\./.test(hostname)) return true;
211
+ if (/^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname)) return true;
212
+ if (hostname.endsWith(".local")) return true;
213
+ return false;
214
+ }
215
+
169
216
  /**
170
217
  * Build the resolved chat-completions compat record for a model spec.
171
218
  * Provider takes precedence over URL-based detection since it's explicitly configured.
@@ -400,6 +447,27 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
400
447
  // DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (".") on tool-call turns.
401
448
  // Kimi and OpenRouter accept them when actual reasoning is unavailable.
402
449
  allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,
450
+ // Local llama.cpp-style servers re-tokenize the entire chat-template
451
+ // prompt each request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct
452
+ // the prior assistant turn's `<think>` block from `reasoning_content`,
453
+ // so dropping the field re-renders the assistant turn without thinking
454
+ // content and forces full prompt re-processing (#3528). The
455
+ // `requires*ReasoningContent*` flags above stay off for these hosts —
456
+ // they accept but don't validate the field — so the encoder needs a
457
+ // distinct opt-in to replay on every reasoning turn. NOT gated on
458
+ // `spec.reasoning`: the runtime discovery paths for `llama.cpp` /
459
+ // `lm-studio` / `openai-models-list` hardcode `reasoning: false`
460
+ // because the upstream `/models` endpoints don't advertise the
461
+ // capability, but the OpenAI stream parser still records incoming
462
+ // `reasoning_content` deltas as thinking blocks. Gating on the spec
463
+ // flag would leave every discovered local Qwen / DeepSeek model
464
+ // re-triggering #3528. The encoder only writes `reasoning_content`
465
+ // when a thinking block actually exists on the turn
466
+ // (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on
467
+ // pure-text histories.
468
+ replayReasoningContent:
469
+ !PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
470
+ (LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl)),
403
471
  requiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,
404
472
  cacheControlFormat: isOpenRouter && spec.id.startsWith("anthropic/") ? "anthropic" : undefined,
405
473
  openRouterRouting: undefined,
@@ -517,6 +585,10 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
517
585
  reasoningCapable,
518
586
  requiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,
519
587
  allowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,
588
+ // The Responses API replays reasoning through encrypted `summary` items,
589
+ // not via a top-level `reasoning_content` field — this flag is
590
+ // chat-completions-only.
591
+ replayReasoningContent: false,
520
592
  requiresThinkingAsText: false,
521
593
  requiresMistralToolIds: false,
522
594
  requiresToolResultName: false,
package/src/types.ts CHANGED
@@ -222,6 +222,20 @@ export interface OpenAICompat {
222
222
  requiresReasoningContentForAllAssistantTurns?: boolean;
223
223
  /** Whether the provider accepts a synthetic placeholder (e.g. ".") for missing reasoning_content on tool-call turns. Default: true. Set to false for providers like DeepSeek that validate the exact reasoning_content value. */
224
224
  allowsSyntheticReasoningContentForToolCalls?: boolean;
225
+ /**
226
+ * Replay preserved thinking blocks as `reasoning_content` (or the configured
227
+ * `reasoningContentField`) on EVERY assistant turn that carried reasoning,
228
+ * regardless of whether the upstream provider validates the field. Local
229
+ * llama.cpp-style servers (llama.cpp, LM Studio, vLLM, sglang, Ollama in
230
+ * openai-completions mode) re-tokenize the full chat-template prompt every
231
+ * request; Qwen3 / DeepSeek-R1 / GLM templates reconstruct the `<think>`
232
+ * block from `reasoning_content`. Dropping the field re-renders the
233
+ * assistant turn without `<think>`, diverging from the slot's KV cache state
234
+ * and forcing full prompt re-processing (#3528). Default: auto-detected
235
+ * (loopback/private baseUrl or local provider id with thinking-enabled
236
+ * models).
237
+ */
238
+ replayReasoningContent?: boolean;
225
239
  /** Whether assistant tool-call messages must include non-empty content. Default: false. */
226
240
  requiresAssistantContentForToolCalls?: boolean;
227
241
  /** Whether the provider supports the `tool_choice` parameter. Default: true. */
@@ -433,6 +447,7 @@ export interface ResolvedOpenAISharedCompat {
433
447
  requiresReasoningContentForToolCalls: boolean;
434
448
  requiresReasoningContentForAllAssistantTurns: boolean;
435
449
  allowsSyntheticReasoningContentForToolCalls: boolean;
450
+ replayReasoningContent: boolean;
436
451
  requiresThinkingAsText: boolean;
437
452
  requiresMistralToolIds: boolean;
438
453
  requiresToolResultName: boolean;
@@ -482,6 +497,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
482
497
  | "requiresReasoningContentForToolCalls"
483
498
  | "requiresReasoningContentForAllAssistantTurns"
484
499
  | "allowsSyntheticReasoningContentForToolCalls"
500
+ | "replayReasoningContent"
485
501
  | "requiresThinkingAsText"
486
502
  | "requiresMistralToolIds"
487
503
  | "requiresToolResultName"