@@ -5,6 +5,7 @@
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions ([#4505](https://github.com/earendil-works/pi/issues/4505)).
|
- Fixed Xiaomi MiMo model metadata to use the OpenAI-compatible endpoints and `openai-completions` API, restoring multi-turn thinking/tool-call sessions ([#4505](https://github.com/earendil-works/pi/issues/4505)).
|
||||||
|
- Fixed OpenCode Go Kimi reasoning replay by normalizing streamed `reasoning` fields back to `reasoning_content` for OpenCode Go only ([#4251](https://github.com/earendil-works/pi/issues/4251)).
|
||||||
|
|
||||||
## [0.75.0] - 2026-05-17
|
## [0.75.0] - 2026-05-17
|
||||||
|
|
||||||
|
|||||||
@@ -326,7 +326,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
|||||||
if (foundReasoningField) {
|
if (foundReasoningField) {
|
||||||
const delta = deltaFields[foundReasoningField];
|
const delta = deltaFields[foundReasoningField];
|
||||||
if (typeof delta === "string" && delta.length > 0) {
|
if (typeof delta === "string" && delta.length > 0) {
|
||||||
const block = ensureThinkingBlock(foundReasoningField);
|
const thinkingSignature =
|
||||||
|
model.provider === "opencode-go" && foundReasoningField === "reasoning"
|
||||||
|
? "reasoning_content"
|
||||||
|
: foundReasoningField;
|
||||||
|
const block = ensureThinkingBlock(thinkingSignature);
|
||||||
block.thinking += delta;
|
block.thinking += delta;
|
||||||
stream.push({
|
stream.push({
|
||||||
type: "thinking_delta",
|
type: "thinking_delta",
|
||||||
@@ -844,7 +848,10 @@ export function convertMessages(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
|
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
|
||||||
const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
|
let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
|
||||||
|
if (model.provider === "opencode-go" && signature === "reasoning") {
|
||||||
|
signature = "reasoning_content";
|
||||||
|
}
|
||||||
if (signature && signature.length > 0) {
|
if (signature && signature.length > 0) {
|
||||||
(assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
|
(assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Type } from "typebox";
|
import { Type } from "typebox";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { getModel } from "../src/models.js";
|
import { getModel } from "../src/models.js";
|
||||||
|
import { convertMessages } from "../src/providers/openai-completions.js";
|
||||||
import { streamSimple } from "../src/stream.js";
|
import { streamSimple } from "../src/stream.js";
|
||||||
import type { Tool } from "../src/types.js";
|
import type { Model, Tool } from "../src/types.js";
|
||||||
|
|
||||||
const mockState = vi.hoisted(() => ({
|
const mockState = vi.hoisted(() => ({
|
||||||
lastParams: undefined as unknown,
|
lastParams: undefined as unknown,
|
||||||
@@ -816,6 +817,114 @@ describe("openai-completions tool_choice", () => {
|
|||||||
expect(writeCall).not.toHaveProperty("partialArgs");
|
expect(writeCall).not.toHaveProperty("partialArgs");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes OpenCode Go reasoning deltas to reasoning_content for replay", async () => {
|
||||||
|
mockState.chunks = [
|
||||||
|
{
|
||||||
|
id: "chatcmpl-opencode-go-reasoning",
|
||||||
|
choices: [{ delta: { reasoning: "think" }, finish_reason: "stop" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { compat: _compat, ...baseModel } = getModel("opencode-go", "kimi-k2.6")!;
|
||||||
|
const model = { ...baseModel, api: "openai-completions" } as const;
|
||||||
|
const response = await streamSimple(
|
||||||
|
model,
|
||||||
|
{
|
||||||
|
messages: [{ role: "user", content: "Use reasoning.", timestamp: Date.now() }],
|
||||||
|
},
|
||||||
|
{ apiKey: "test" },
|
||||||
|
).result();
|
||||||
|
|
||||||
|
expect(response.content).toEqual([
|
||||||
|
{
|
||||||
|
type: "thinking",
|
||||||
|
thinking: "think",
|
||||||
|
thinkingSignature: "reasoning_content",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps non-OpenCode Go reasoning deltas on the original reasoning field", async () => {
|
||||||
|
mockState.chunks = [
|
||||||
|
{
|
||||||
|
id: "chatcmpl-reasoning",
|
||||||
|
choices: [{ delta: { reasoning: "think" }, finish_reason: "stop" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!;
|
||||||
|
const model = { ...baseModel, api: "openai-completions" } as const;
|
||||||
|
const response = await streamSimple(
|
||||||
|
model,
|
||||||
|
{
|
||||||
|
messages: [{ role: "user", content: "Use reasoning.", timestamp: Date.now() }],
|
||||||
|
},
|
||||||
|
{ apiKey: "test" },
|
||||||
|
).result();
|
||||||
|
|
||||||
|
expect(response.content).toEqual([
|
||||||
|
{
|
||||||
|
type: "thinking",
|
||||||
|
thinking: "think",
|
||||||
|
thinkingSignature: "reasoning",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replays OpenCode Go reasoning thinking blocks as reasoning_content", () => {
|
||||||
|
const { compat: _compat, ...baseModel } = getModel("opencode-go", "kimi-k2.6")!;
|
||||||
|
const model = { ...baseModel, api: "openai-completions" } as Model<"openai-completions">;
|
||||||
|
const messages = convertMessages(
|
||||||
|
model,
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "opencode-go",
|
||||||
|
model: "kimi-k2.6",
|
||||||
|
content: [
|
||||||
|
{ type: "thinking", thinking: "think", thinkingSignature: "reasoning" },
|
||||||
|
{ type: "toolCall", id: "call_1", name: "read", arguments: { path: "README.md" } },
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
stopReason: "stop",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...model.compat,
|
||||||
|
supportsStore: false,
|
||||||
|
supportsDeveloperRole: false,
|
||||||
|
supportsReasoningEffort: true,
|
||||||
|
supportsUsageInStreaming: true,
|
||||||
|
maxTokensField: "max_completion_tokens",
|
||||||
|
requiresToolResultName: false,
|
||||||
|
requiresAssistantAfterToolResult: false,
|
||||||
|
requiresThinkingAsText: false,
|
||||||
|
requiresReasoningContentOnAssistantMessages: false,
|
||||||
|
thinkingFormat: "openai",
|
||||||
|
openRouterRouting: {},
|
||||||
|
vercelGatewayRouting: {},
|
||||||
|
zaiToolStream: false,
|
||||||
|
supportsStrictMode: true,
|
||||||
|
sendSessionAffinityHeaders: false,
|
||||||
|
supportsLongCacheRetention: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(messages[0]).toMatchObject({ role: "assistant", reasoning_content: "think" });
|
||||||
|
expect(messages[0]).not.toHaveProperty("reasoning");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not double-count reasoning tokens in completion usage", async () => {
|
it("does not double-count reasoning tokens in completion usage", async () => {
|
||||||
mockState.chunks = [
|
mockState.chunks = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user