@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 +6 -0
- package/dist/types/types.d.ts +16 -1
- package/package.json +3 -3
- package/src/compat/openai.ts +72 -0
- package/src/types.ts +16 -0
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
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
42
|
+
"@oh-my-pi/pi-ai": "16.1.22",
|
|
43
43
|
"@types/bun": "^1.3.14"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
package/src/compat/openai.ts
CHANGED
|
@@ -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"
|