@@ -5,6 +5,7 @@
|
||||
### 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 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
|
||||
|
||||
|
||||
@@ -326,7 +326,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
||||
if (foundReasoningField) {
|
||||
const delta = deltaFields[foundReasoningField];
|
||||
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;
|
||||
stream.push({
|
||||
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)
|
||||
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) {
|
||||
(assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Type } from "typebox";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getModel } from "../src/models.js";
|
||||
import { convertMessages } from "../src/providers/openai-completions.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(() => ({
|
||||
lastParams: undefined as unknown,
|
||||
@@ -816,6 +817,114 @@ describe("openai-completions tool_choice", () => {
|
||||
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 () => {
|
||||
mockState.chunks = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user