Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions patches/@mariozechner+pi-ai+0.60.0.patch
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
diff --git a/node_modules/@mariozechner/pi-ai/dist/providers/openai-completions.js b/node_modules/@mariozechner/pi-ai/dist/providers/openai-completions.js
index 29ae8cf..6d9ea61 100644
index 29ae8cf..7204589 100644
--- a/node_modules/@mariozechner/pi-ai/dist/providers/openai-completions.js
+++ b/node_modules/@mariozechner/pi-ai/dist/providers/openai-completions.js
@@ -474,13 +474,32 @@ export function convertMessages(model, context, compat) {
@@ -446,9 +446,15 @@ export function convertMessages(model, context, compat) {
: content;
if (filteredContent.length === 0)
continue;
+ // Flatten text-only content to a plain string for maximum endpoint
+ // compatibility. Ollama and some OpenAI-compatible servers reject the
+ // content-block array form for plain text with "invalid message format".
+ const isTextOnly = filteredContent.every((c) => c.type === "text");
params.push({
role: "user",
- content: filteredContent,
+ content: isTextOnly
+ ? filteredContent.map((c) => c.text).join("")
+ : filteredContent,
});
}
}
@@ -474,13 +480,32 @@ export function convertMessages(model, context, compat) {
// Filter out empty thinking blocks to avoid API validation errors
const nonEmptyThinkingBlocks = thinkingBlocks.filter((b) => b.thinking && b.thinking.trim().length > 0);
if (nonEmptyThinkingBlocks.length > 0) {
Expand Down Expand Up @@ -37,23 +54,23 @@ index 29ae8cf..6d9ea61 100644
else {
assistantMsg.content = [{ type: "text", text: thinkingText }];
}
@@ -658,6 +677,7 @@ function mapStopReason(reason) {
@@ -658,6 +683,7 @@ function mapStopReason(reason) {
function detectCompat(model) {
const provider = model.provider;
const baseUrl = model.baseUrl;
+ const isDeepSeekV4 = /(?:^|[/_-])deepseek[-_.]?v4(?:$|[-:_.])/i.test(model.id);
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
const isNonStandard = provider === "cerebras" ||
baseUrl.includes("cerebras.ai") ||
@@ -690,6 +710,7 @@ function detectCompat(model) {
@@ -690,6 +716,7 @@ function detectCompat(model) {
requiresToolResultName: false,
requiresAssistantAfterToolResult: false,
requiresThinkingAsText: false,
+ requiresThinkingInContent: isDeepSeekV4,
thinkingFormat: isZai ? "zai" : "openai",
openRouterRouting: {},
vercelGatewayRouting: {},
@@ -714,6 +735,7 @@ function getCompat(model) {
@@ -714,6 +741,7 @@ function getCompat(model) {
requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,
requiresAssistantAfterToolResult: model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
Expand Down
11 changes: 8 additions & 3 deletions src/main/claude/pi-model-resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,15 +328,20 @@ export function applyPiModelRuntimeOverrides(
} as typeof nextModel;
}

// DeepSeek V4 models on custom/relay endpoints need thinking blocks in content[] array.
// DeepSeek V4 models on some custom/relay endpoints (and local Ollama) need thinking
// blocks in the content[] array. Ollama CLOUD (ollama.com), however, rejects that
// non-standard {type:"thinking"} content block with "invalid message format". Local
// Ollama (loopback :11434) is a different service and keeps the existing behavior.
if (nextModel.api === 'openai-completions' && DEEPSEEK_V4_MODEL_PATTERN.test(nextModel.id)) {
const currentCompat = (nextModel.compat || {}) as Record<string, unknown>;
if (!currentCompat.requiresThinkingInContent) {
const endpoint = `${options.customBaseUrl || ''} ${nextModel.baseUrl || ''}`.toLowerCase();
const isOllamaCloud = endpoint.includes('ollama.com');
if (currentCompat.requiresThinkingInContent === undefined) {
nextModel = {
...nextModel,
compat: {
...currentCompat,
requiresThinkingInContent: true,
requiresThinkingInContent: !isOllamaCloud,
},
} as typeof nextModel;
}
Expand Down
43 changes: 43 additions & 0 deletions src/tests/claude/pi-model-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ const openAIResponsesModel = {
maxTokens: 16384,
} as Model<Api>;

const deepseekV4CompletionsModel = {
id: 'deepseek-v4-pro',
name: 'deepseek-v4-pro',
api: 'openai-completions',
provider: 'ollama',
baseUrl: 'https://ollama.com/v1',
reasoning: true,
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384,
} as Model<Api>;

describe('pi model runtime overrides', () => {
it('keeps OpenAI Responses for custom OpenAI configs that target official OpenAI', () => {
const model = resolvePiRegistryModel('openai/gpt-5.4', {
Expand Down Expand Up @@ -46,4 +59,34 @@ describe('pi model runtime overrides', () => {
supportsStore: false,
});
});

it('disables thinking-in-content for DeepSeek V4 on Ollama Cloud (ollama.com)', () => {
// Ollama Cloud rejects the non-standard {type:"thinking"} content block with
// "invalid message format", so thinking must not be serialized into content[].
const model = applyPiModelRuntimeOverrides(deepseekV4CompletionsModel, {
configProvider: 'ollama',
rawProvider: 'ollama',
customBaseUrl: 'https://ollama.com/v1',
});

expect(model.compat).toMatchObject({ requiresThinkingInContent: false });
});

it('keeps thinking-in-content for DeepSeek V4 on other custom/relay endpoints', () => {
const model = applyPiModelRuntimeOverrides(
{
...deepseekV4CompletionsModel,
provider: 'custom',
baseUrl: 'https://relay.example.test/v1',
} as Model<Api>,
{
configProvider: 'custom',
rawProvider: 'custom',
customProtocol: 'openai',
customBaseUrl: 'https://relay.example.test/v1',
}
);

expect(model.compat).toMatchObject({ requiresThinkingInContent: true });
});
});
Loading