Merge pull request #7 from skyscribe-yf/main
feat(stream,models): incremental reasoning streaming + live display pricing
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.0 - 2026-05-27
|
||||||
|
|
||||||
|
- Stream `reasoning-delta` events incrementally instead of buffering the full thinking block until `reasoning-end`. Emits `thinking_start`, `thinking_delta`, and `thinking_end` events as they arrive so the UI can show reasoning in real time.
|
||||||
|
- Close open text blocks on `reasoning-start` and `reasoning-delta` so thinking and text never overlap in the output.
|
||||||
|
- Add live display pricing (`MODEL_COSTS`) for known Command Code models. Cost falls back to zero for models not yet in the price table until the Provider API exposes pricing directly.
|
||||||
|
- Fetch models from the Command Code Provider API at startup (inherited from upstream 0.1.1) and overlay the static cost table.
|
||||||
|
|
||||||
## 0.1.1 - 2026-05-26
|
## 0.1.1 - 2026-05-26
|
||||||
|
|
||||||
- Align Command Code generate requests with CLI `0.27.2` headers and payload shape.
|
- Align Command Code generate requests with CLI `0.27.2` headers and payload shape.
|
||||||
|
|||||||
@@ -136,6 +136,15 @@ https://api.commandcode.ai/provider/v1/models
|
|||||||
|
|
||||||
For tests or local mocks, override it with `COMMANDCODE_MODELS_URL`.
|
For tests or local mocks, override it with `COMMANDCODE_MODELS_URL`.
|
||||||
|
|
||||||
|
## Pricing
|
||||||
|
|
||||||
|
Command Code does not yet expose model pricing through its Provider API. The provider ships a static cost table (`MODEL_COSTS` in `index.ts`) for known models so that pi can display per-model pricing.
|
||||||
|
|
||||||
|
- Models present in `MODEL_COSTS` show their real per-million-token rates (including promotional deals like the DeepSeek V4 Pro 4× discount and Qwen 3.7 Max 2× discount).
|
||||||
|
- Models **not** in the table fall back to zero cost. When the Provider API adds a `cost` field, the static table can be removed.
|
||||||
|
|
||||||
|
To add or update a price, edit the `MODEL_COSTS` record in `index.ts` and update the corresponding test in `tests/test-pricing.ts`.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, PR expectations, and commit message rules.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, PR expectations, and commit message rules.
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
* Models are fetched from Command Code's Provider API at startup.
|
* Models are fetched from Command Code's Provider API at startup.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AssistantMessageEventStream, calculateCost } from "@mariozechner/pi-ai"
|
import { AssistantMessageEventStream, calculateCost } from "@earendil-works/pi-ai"
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
||||||
|
|
||||||
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
||||||
import { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
|
import { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
|
||||||
@@ -22,6 +22,51 @@ import { getApiKey, login, refreshToken } from "./src/oauth.ts"
|
|||||||
const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
|
const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
|
||||||
const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
|
const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
|
||||||
|
|
||||||
|
type CommandCodeModelCost = {
|
||||||
|
input: number
|
||||||
|
output: number
|
||||||
|
cacheRead: number
|
||||||
|
cacheWrite: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZERO_MODEL_COST: CommandCodeModelCost = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Provider API supplies the current model list. Keep known display pricing
|
||||||
|
// here until the Provider API exposes prices directly.
|
||||||
|
const MODEL_COSTS: Record<string, CommandCodeModelCost> = {
|
||||||
|
"claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||||
|
"claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||||
|
"claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||||
|
"claude-haiku-4-5-20251001": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
||||||
|
"gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||||
|
"gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
||||||
|
"gpt-5.3-codex": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 },
|
||||||
|
"gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
||||||
|
"google/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
|
||||||
|
"google/gemini-3.1-flash-lite": { input: 0.25, output: 1.5, cacheRead: 0.03, cacheWrite: 0 },
|
||||||
|
// 4× usage deal: 75% off (permanent, no expiry)
|
||||||
|
"deepseek/deepseek-v4-pro": { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 },
|
||||||
|
"deepseek/deepseek-v4-flash": { input: 0.14, output: 0.28, cacheRead: 0.028, cacheWrite: 0 },
|
||||||
|
"moonshotai/Kimi-K2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
|
||||||
|
"moonshotai/Kimi-K2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
||||||
|
"zai-org/GLM-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
||||||
|
"zai-org/GLM-5": { input: 1, output: 3.2, cacheRead: 0.2, cacheWrite: 0 },
|
||||||
|
"MiniMaxAI/MiniMax-M2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
||||||
|
"MiniMaxAI/MiniMax-M2.5": { input: 0.27, output: 0.95, cacheRead: 0.03, cacheWrite: 0 },
|
||||||
|
"Qwen/Qwen3.6-Max-Preview": { input: 1.3, output: 7.8, cacheRead: 0.26, cacheWrite: 1.63 },
|
||||||
|
"Qwen/Qwen3.6-Plus": { input: 0.5, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
||||||
|
// 2× usage deal: 50% off through June 22, 2026
|
||||||
|
"Qwen/Qwen3.7-Max": { input: 1.25, output: 3.75, cacheRead: 0.25, cacheWrite: 1.56 },
|
||||||
|
"stepfun/Step-3.5-Flash": { input: 0.1, output: 0.3, cacheRead: 0.02, cacheWrite: 0 },
|
||||||
|
"xiaomi/mimo-v2.5-pro": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
"xiaomi/mimo-v2.5": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
}
|
||||||
|
|
||||||
const streamCommandCode = createStreamCommandCode({
|
const streamCommandCode = createStreamCommandCode({
|
||||||
createStream: () => new AssistantMessageEventStream(),
|
createStream: () => new AssistantMessageEventStream(),
|
||||||
calculateCost,
|
calculateCost,
|
||||||
@@ -57,7 +102,7 @@ export default async function (pi: ExtensionAPI) {
|
|||||||
name: model.name,
|
name: model.name,
|
||||||
reasoning: model.reasoning,
|
reasoning: model.reasoning,
|
||||||
input: ["text"] as const,
|
input: ["text"] as const,
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
|
||||||
contextWindow: model.contextWindow,
|
contextWindow: model.contextWindow,
|
||||||
maxTokens: model.maxTokens,
|
maxTokens: model.maxTokens,
|
||||||
})),
|
})),
|
||||||
|
|||||||
Generated
+2194
-2666
File diff suppressed because it is too large
Load Diff
+12
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-commandcode-provider",
|
"name": "pi-commandcode-provider",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"description": "pi custom provider for Command Code API (commandcode.ai)",
|
"description": "pi custom provider for Command Code API (commandcode.ai)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -28,12 +28,13 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
|
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
||||||
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
||||||
"test:unit": "tsx tests/test-pure-functions.ts",
|
"test:unit": "tsx tests/test-pure-functions.ts",
|
||||||
"test:models": "tsx tests/test-models.ts",
|
"test:models": "tsx tests/test-models.ts",
|
||||||
|
"test:pricing": "tsx tests/test-pricing.ts",
|
||||||
"test:oauth": "tsx tests/test-oauth.ts",
|
"test:oauth": "tsx tests/test-oauth.ts",
|
||||||
"test:abort": "tsx tests/test-abort.ts",
|
"test:abort": "tsx tests/test-abort.ts",
|
||||||
"test:stream": "tsx tests/test-stream.ts",
|
"test:stream": "tsx tests/test-stream.ts",
|
||||||
@@ -46,13 +47,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mariozechner/pi-coding-agent": "0.72.0",
|
|
||||||
"@types/node": "25.6.0",
|
"@types/node": "25.6.0",
|
||||||
"prettier": "^3.5.0",
|
"prettier": "^3.5.0",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "6.0.3"
|
"typescript": "6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-ai": "0.72.0"
|
"@earendil-works/pi-ai": "0.75.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@earendil-works/pi-coding-agent": "^0.75.5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@earendil-works/pi-coding-agent": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-27
@@ -178,7 +178,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
|
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
|
||||||
let textBlock: TextContent | undefined
|
let textBlock: TextContent | undefined
|
||||||
let currentTextIdx = -1
|
let currentTextIdx = -1
|
||||||
let thinkingBlock: string[] = []
|
let thinkingIdx = -1
|
||||||
let finished = false
|
let finished = false
|
||||||
|
|
||||||
const abortUpstream = () => {
|
const abortUpstream = () => {
|
||||||
@@ -210,29 +210,18 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
currentTextIdx = -1
|
currentTextIdx = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
const flushThinkingBlock = () => {
|
const endThinking = () => {
|
||||||
if (thinkingBlock.length === 0) return
|
if (thinkingIdx < 0) return
|
||||||
const thinkingText = thinkingBlock.join("")
|
const tc = output.content[thinkingIdx]
|
||||||
thinkingBlock = []
|
if (tc && tc.type === "thinking") {
|
||||||
output.content.push({ type: "thinking", thinking: thinkingText })
|
stream.push({
|
||||||
const idx = output.content.length - 1
|
type: "thinking_end",
|
||||||
stream.push({
|
contentIndex: thinkingIdx,
|
||||||
type: "thinking_start",
|
content: (tc as { thinking: string }).thinking,
|
||||||
contentIndex: idx,
|
partial: output,
|
||||||
partial: output,
|
})
|
||||||
})
|
}
|
||||||
stream.push({
|
thinkingIdx = -1
|
||||||
type: "thinking_delta",
|
|
||||||
contentIndex: idx,
|
|
||||||
delta: thinkingText,
|
|
||||||
partial: output,
|
|
||||||
})
|
|
||||||
stream.push({
|
|
||||||
type: "thinking_end",
|
|
||||||
contentIndex: idx,
|
|
||||||
content: thinkingText,
|
|
||||||
partial: output,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEvent = (event: unknown) => {
|
const handleEvent = (event: unknown) => {
|
||||||
@@ -240,6 +229,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "text-delta": {
|
case "text-delta": {
|
||||||
|
endThinking()
|
||||||
if (!textBlock) {
|
if (!textBlock) {
|
||||||
textBlock = { type: "text", text: "" }
|
textBlock = { type: "text", text: "" }
|
||||||
output.content.push(textBlock)
|
output.content.push(textBlock)
|
||||||
@@ -268,12 +258,32 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
|
|
||||||
case "reasoning-delta": {
|
case "reasoning-delta": {
|
||||||
endTextBlock()
|
endTextBlock()
|
||||||
thinkingBlock.push(stringValue(event.text) ?? "")
|
const delta = stringValue(event.text) ?? ""
|
||||||
|
if (thinkingIdx < 0) {
|
||||||
|
output.content.push({ type: "thinking", thinking: delta })
|
||||||
|
thinkingIdx = output.content.length - 1
|
||||||
|
stream.push({
|
||||||
|
type: "thinking_start",
|
||||||
|
contentIndex: thinkingIdx,
|
||||||
|
partial: output,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const tc = output.content[thinkingIdx]
|
||||||
|
if (tc && tc.type === "thinking") {
|
||||||
|
;(tc as { thinking: string }).thinking += delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream.push({
|
||||||
|
type: "thinking_delta",
|
||||||
|
contentIndex: thinkingIdx,
|
||||||
|
delta,
|
||||||
|
partial: output,
|
||||||
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case "reasoning-end": {
|
case "reasoning-end": {
|
||||||
flushThinkingBlock()
|
endThinking()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +293,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
|
|
||||||
case "tool-call": {
|
case "tool-call": {
|
||||||
endTextBlock()
|
endTextBlock()
|
||||||
|
endThinking()
|
||||||
const toolCall: ToolCallContent = {
|
const toolCall: ToolCallContent = {
|
||||||
type: "toolCall",
|
type: "toolCall",
|
||||||
id: stringValue(event.toolCallId) ?? "",
|
id: stringValue(event.toolCallId) ?? "",
|
||||||
@@ -442,7 +453,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
endTextBlock()
|
endTextBlock()
|
||||||
flushThinkingBlock()
|
endThinking()
|
||||||
|
|
||||||
stream.push({
|
stream.push({
|
||||||
type: "done",
|
type: "done",
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { describe, it } from "node:test"
|
||||||
|
|
||||||
|
// MODEL_COSTS is a module-level const in index.ts. We verify the pricing
|
||||||
|
// overlay by importing the map through a dedicated re-export so tests don't
|
||||||
|
// need to spin up the full extension.
|
||||||
|
//
|
||||||
|
// To keep the test self-contained without importing the full extension (which
|
||||||
|
// requires ExtensionAPI), we read the source and extract the constant at
|
||||||
|
// runtime. A cleaner approach would be a dedicated src/pricing.ts module,
|
||||||
|
// but for now we verify the known cost entries directly.
|
||||||
|
|
||||||
|
import { readFileSync } from "node:fs"
|
||||||
|
import { resolve, dirname } from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const indexSource = readFileSync(resolve(__dirname, "..", "index.ts"), "utf-8")
|
||||||
|
|
||||||
|
// Extract MODEL_COSTS object from index.ts source using a simple parse.
|
||||||
|
// The map is written as a Record<string, {input:number,output:number,...}>
|
||||||
|
// so we eval it in a sandboxed context.
|
||||||
|
const match = indexSource.match(
|
||||||
|
/const MODEL_COSTS:\s*Record<string,\s*CommandCodeModelCost>\s*=\s*\{([\s\S]*?)\n\}/,
|
||||||
|
)
|
||||||
|
assert.ok(match, "MODEL_COSTS constant should exist in index.ts")
|
||||||
|
|
||||||
|
// Parse the cost entries from the extracted block.
|
||||||
|
const costBlock = match[1]
|
||||||
|
const entries: Record<string, { input: number; output: number }> = {}
|
||||||
|
for (const line of costBlock.split("\n")) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (!trimmed || trimmed.startsWith("//")) continue
|
||||||
|
const entryMatch = trimmed.match(/^"([^"]+)":\s*\{\s*input:\s*([\d.]+),\s*output:\s*([\d.]+)/)
|
||||||
|
if (entryMatch) {
|
||||||
|
entries[entryMatch[1]] = {
|
||||||
|
input: Number(entryMatch[2]),
|
||||||
|
output: Number(entryMatch[3]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("MODEL_COSTS pricing overlay", () => {
|
||||||
|
it("covers known Command Code models with non-zero pricing", () => {
|
||||||
|
const knownModels = [
|
||||||
|
"deepseek/deepseek-v4-flash",
|
||||||
|
"deepseek/deepseek-v4-pro",
|
||||||
|
"claude-sonnet-4-6",
|
||||||
|
"claude-opus-4-7",
|
||||||
|
"Qwen/Qwen3.7-Max",
|
||||||
|
"gpt-5.5",
|
||||||
|
"stepfun/Step-3.5-Flash",
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const id of knownModels) {
|
||||||
|
const cost = entries[id]
|
||||||
|
assert.ok(cost, `MODEL_COSTS should include "${id}"`)
|
||||||
|
assert.ok(cost.input > 0, `"${id}" input cost should be > 0`)
|
||||||
|
assert.ok(cost.output > 0, `"${id}" output cost should be > 0`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes promotional pricing notes in comments", () => {
|
||||||
|
// The DeepSeek V4 Pro 4× deal and Qwen 3.7 Max 2× deal should be
|
||||||
|
// documented in the source comments.
|
||||||
|
assert.ok(
|
||||||
|
costBlock.includes("4× usage deal") || costBlock.includes("75% off"),
|
||||||
|
"DeepSeek V4 Pro promotional pricing should be documented",
|
||||||
|
)
|
||||||
|
assert.ok(
|
||||||
|
costBlock.includes("2× usage deal") || costBlock.includes("50% off"),
|
||||||
|
"Qwen 3.7 Max promotional pricing should be documented",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("has cache pricing for models that support it", () => {
|
||||||
|
// Claude models should have non-zero cacheRead and cacheWrite costs.
|
||||||
|
const claudeModels = ["claude-sonnet-4-6", "claude-opus-4-7"]
|
||||||
|
for (const id of claudeModels) {
|
||||||
|
const fullEntryMatch = costBlock.match(
|
||||||
|
new RegExp(
|
||||||
|
`"${id.replace(/\//g, "\\\\")}":\\s*\\{[^}]+cacheRead:\\s*([\\d.]+)[^}]+cacheWrite:\\s*([\\d.]+)`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert.ok(fullEntryMatch, `"${id}" should have cacheRead and cacheWrite fields`)
|
||||||
|
assert.ok(Number(fullEntryMatch[1]) > 0, `"${id}" cacheRead should be > 0`)
|
||||||
|
assert.ok(Number(fullEntryMatch[2]) > 0, `"${id}" cacheWrite should be > 0`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -223,6 +223,66 @@ describe("streamCommandCode — successful streams", () => {
|
|||||||
if (done?.type !== "done") throw new Error("expected done")
|
if (done?.type !== "done") throw new Error("expected done")
|
||||||
assert.equal(done.message.content[0]?.type, "thinking")
|
assert.equal(done.message.content[0]?.type, "thinking")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("closes thinking block before text when reasoning-end is missing", async () => {
|
||||||
|
server.mockResponse({
|
||||||
|
type: "success",
|
||||||
|
events: [
|
||||||
|
JSON.stringify({ type: "reasoning-start" }),
|
||||||
|
JSON.stringify({ type: "reasoning-delta", text: "thinking" }),
|
||||||
|
JSON.stringify({ type: "text-delta", text: "answer" }),
|
||||||
|
JSON.stringify({ type: "finish", finishReason: "stop" }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||||
|
|
||||||
|
const events = await collectEvents(
|
||||||
|
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(eventTypes(events), [
|
||||||
|
"start",
|
||||||
|
"thinking_start",
|
||||||
|
"thinking_delta",
|
||||||
|
"thinking_end",
|
||||||
|
"text_start",
|
||||||
|
"text_delta",
|
||||||
|
"text_end",
|
||||||
|
"done",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("closes thinking block before tool-call when reasoning-end is missing", async () => {
|
||||||
|
server.mockResponse({
|
||||||
|
type: "success",
|
||||||
|
events: [
|
||||||
|
JSON.stringify({ type: "reasoning-start" }),
|
||||||
|
JSON.stringify({ type: "reasoning-delta", text: "thinking" }),
|
||||||
|
JSON.stringify({
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "call_1",
|
||||||
|
toolName: "read_file",
|
||||||
|
input: JSON.stringify({ path: "/tmp/x" }),
|
||||||
|
}),
|
||||||
|
JSON.stringify({ type: "finish", finishReason: "tool-calls" }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||||
|
|
||||||
|
const events = await collectEvents(
|
||||||
|
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(eventTypes(events), [
|
||||||
|
"start",
|
||||||
|
"thinking_start",
|
||||||
|
"thinking_delta",
|
||||||
|
"thinking_end",
|
||||||
|
"toolcall_start",
|
||||||
|
"toolcall_end",
|
||||||
|
"done",
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("streamCommandCode — request serialization", () => {
|
describe("streamCommandCode — request serialization", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user