Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Refresh the static capability snapshot to `command-code@1.79.2`: add `claude-haiku-5-5`, `mistral/mistral-large-4` (image input, 262K output limit), and the free `stealth/glyph-cluster:free`; retire `stealth/pixel-canary` and `stealth/space-bunny-alpha`.
- Add reviewed display pricing for the refreshed catalog — `claude-haiku-5-5` with its 100K long-context tier, `mistral/mistral-large-4`, and the free `stealth/glyph-cluster:free` — drop the retired `stealth/space-bunny-alpha`, and correct the `claude-sonnet-5-5` cache-read rate to $0.10 per million tokens, verified against the official pricing page on 2026-10-09. The snapshot now covers all 87 advertised models.
- Fix `400 input[N].call_id` failures on the `/alpha/generate` fallback by shortening compound Responses tool-call ids (`<call_id>|<item_id>`, 73 characters in the reported session) to the 64-character wire limit. The mapping is deterministic per request and shared by tool calls and their results, so pairing is preserved and distinct ids never collide; short ids pass through unchanged and the Provider API path is untouched ([#141](https://github.com/patlux/pi-commandcode-provider/issues/141)).
- Replay prior assistant reasoning on the generate transport so the model can see its own earlier thinking on follow-up turns, matching the Command Code CLI's `/alpha/generate` wire (`toWireMessages` in `command-code@1.79.2`), which replays every thinking block as a `reasoning` part with no model check and no signature. Prior reasoning was dropped in 0.5.0, which made multi-turn tasks whose state lives only in thinking — plans, games, consistency checks — silently re-decide on the fallback transport.

## 0.7.6 - 2026-10-06

Expand Down
5 changes: 5 additions & 0 deletions src/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,11 @@ export function messagesToCC(
for (const content of recordArray(message.content)) {
if (content.type === "text") {
parts.push({ type: "text", text: stringValue(content.text) ?? "" })
} else if (content.type === "thinking") {
// The official Command Code CLI replays prior assistant reasoning as
// `reasoning` parts (toWireMessages), and dropping it leaves the model
// unable to see its own earlier thinking on follow-up turns.
parts.push({ type: "reasoning", text: stringValue(content.thinking) ?? "" })
} else if (content.type === "toolCall") {
const rawToolCallId = stringValue(content.id) ?? ""
const toolName = stringValue(content.name) ?? ""
Expand Down
60 changes: 60 additions & 0 deletions tests/fixtures/issue-146-generate-turn3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"note": "params.messages captured from the live turn-3 POST /alpha/generate request for the history in issue-146-session.json. Both prior assistant turns carry a reasoning part.",
"model": "deepseek/deepseek-v4.1-flash",
"reasoning_effort": "max",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Let's play a guessing game. Think of a number between 1 and 100 and I'll guess. After each guess, tell me if it's higher or lower."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "reasoning",
"text": "The user wants to play a guessing game. They want me to think of a number between 1 and 100, and they'll guess. After each guess I tell them higher or lower.\n\nThis is a roleplay task. I need to pick a number and keep it consistent. Let me pick something like 42... no, too obvious. Let me pick 73. Actually, any number works. I'll pick 37 or 73. Let me go with 73.\n\nI should just respond playfully, tell them I've thought of a number, and invite their first guess. I need to remember the number throughout the conversation. Let me pick 73.\n\nKeep response concise."
},
{
"type": "text",
"text": "I've got one. It's between 1 and 100 — go ahead and make your first guess!"
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "66"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "reasoning",
"text": "The number is 73. 66 is lower than 73, so I say higher."
},
{
"type": "text",
"text": "Higher!"
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "50"
}
]
}
]
}
59 changes: 59 additions & 0 deletions tests/fixtures/issue-146-session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"note": "Sanitized history of a live 3-turn guessing-game session on the generate fallback (deepseek/deepseek-v4.1-flash, thinking max, tools=[read]). Each assistant turn carries hidden thinking plus visible text and no tool calls; the final assistant reply is omitted because it is not part of the turn-3 request.",
"model": "deepseek/deepseek-v4.1-flash",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Let's play a guessing game. Think of a number between 1 and 100 and I'll guess. After each guess, tell me if it's higher or lower."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "The user wants to play a guessing game. They want me to think of a number between 1 and 100, and they'll guess. After each guess I tell them higher or lower.\n\nThis is a roleplay task. I need to pick a number and keep it consistent. Let me pick something like 42... no, too obvious. Let me pick 73. Actually, any number works. I'll pick 37 or 73. Let me go with 73.\n\nI should just respond playfully, tell them I've thought of a number, and invite their first guess. I need to remember the number throughout the conversation. Let me pick 73.\n\nKeep response concise."
},
{
"type": "text",
"text": "I've got one. It's between 1 and 100 — go ahead and make your first guess!"
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "66"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "The number is 73. 66 is lower than 73, so I say higher."
},
{
"type": "text",
"text": "Higher!"
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "50"
}
]
}
]
}
37 changes: 30 additions & 7 deletions tests/test-pure-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import assert from "node:assert/strict"
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { describe, it } from "node:test"
Expand Down Expand Up @@ -713,9 +713,11 @@ describe("messagesToCC()", () => {

assert.equal(objectAt(result, ["0", "role"]), "user")
assert.equal(objectAt(result, ["1", "role"]), "assistant")
assert.equal(objectAt(result, ["1", "content", "0", "type"]), "text")
assert.equal(objectAt(result, ["1", "content", "1", "type"]), "tool-call")
assert.equal(objectAt(result, ["1", "content", "2"]), undefined)
assert.equal(objectAt(result, ["1", "content", "0", "type"]), "reasoning")
assert.equal(objectAt(result, ["1", "content", "0", "text"]), "I will read")
assert.equal(objectAt(result, ["1", "content", "1", "type"]), "text")
assert.equal(objectAt(result, ["1", "content", "2", "type"]), "tool-call")
assert.equal(objectAt(result, ["1", "content", "3"]), undefined)
assert.equal(objectAt(result, ["2", "role"]), "tool")
assert.equal(objectAt(result, ["2", "content", "0", "output", "value"]), "hello\nworld")
})
Expand Down Expand Up @@ -942,7 +944,7 @@ describe("messagesToCC()", () => {
assert.equal(objectAt(result, ["2", "content", "0", "output", "value"]), "plain result")
})

it("drops previous assistant reasoning while preserving text and tool calls", () => {
it("replays previous assistant reasoning while preserving text and tool calls", () => {
const result = messagesToCC([
{ role: "user", content: "first question" },
{
Expand All @@ -957,12 +959,18 @@ describe("messagesToCC()", () => {

assert.deepEqual(result, [
{ role: "user", content: "first question" },
{ role: "assistant", content: [{ type: "text", text: "first answer" }] },
{
role: "assistant",
content: [
{ type: "reasoning", text: "private reasoning from turn one" },
{ type: "text", text: "first answer" },
],
},
{ role: "user", content: "follow-up question" },
])
})

it("omits assistant turns that contain only previous reasoning", () => {
it("keeps assistant turns that contain only previous reasoning", () => {
const result = messagesToCC([
{ role: "user", content: "first question" },
{
Expand All @@ -974,10 +982,25 @@ describe("messagesToCC()", () => {

assert.deepEqual(result, [
{ role: "user", content: "first question" },
{
role: "assistant",
content: [{ type: "reasoning", text: "private reasoning" }],
},
{ role: "user", content: "follow-up question" },
])
})

it("replays cumulative reasoning for text-only assistant turns in a tool-enabled session (#146 fixtures)", () => {
const session = JSON.parse(
readFileSync(new URL("./fixtures/issue-146-session.json", import.meta.url), "utf8"),
) as { messages: Parameters<typeof messagesToCC>[0] }
const expected = JSON.parse(
readFileSync(new URL("./fixtures/issue-146-generate-turn3.json", import.meta.url), "utf8"),
) as { messages: ReturnType<typeof messagesToCC> }

assert.deepEqual(messagesToCC(session.messages), expected.messages)
})

it("synthesizes missing results for orphaned tool calls", () => {
const result = messagesToCC([
{ role: "user", content: "edit a file" },
Expand Down
36 changes: 36 additions & 0 deletions tests/test-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,42 @@ describe("streamCommandCode — request serialization", () => {
assert.equal(headers["x-session-id"], undefined)
})

it("replays previous assistant reasoning in the generate request history", async () => {
server.mockResponse({
type: "success",
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
})
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })

await collectEvents(
streamCommandCode(
makeModel(),
makeContext({
messages: [
{ role: "user", content: "think of a number" },
{
role: "assistant",
content: [
{ type: "thinking", thinking: "my number is 57" },
{ type: "text", text: "ready" },
],
},
{ role: "user", content: "is it bigger than 50?" },
],
}),
{ apiKey: "mock-key" },
),
)

assert.deepEqual(objectAt(server.lastRequestBody(), ["params", "messages", "1"]), {
role: "assistant",
content: [
{ type: "reasoning", text: "my number is 57" },
{ type: "text", text: "ready" },
],
})
})

it("normalizes nullable tool parameters only for Gemini on the generate wire (#99)", async () => {
server.mockResponse({
type: "success",
Expand Down
Loading