diff --git a/CHANGELOG.md b/CHANGELOG.md index 879148f..ad3e0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- Enable thinking for `deepseek/deepseek-v4.1-flash` and future catalog-gap models. `src/commandcode-catalog.ts` is pinned to a Command Code CLI release, so a model added upstream afterwards has no reasoning flag until the next catalog sync, and Pi forwarded no `reasoning_effort` for it. A new `MODEL_REASONING_OVERRIDES` hook next to the existing effort overrides carries the flag, and Pi and Oh My Pi now expose `low`, `high`, and `max` for that model (#90). + +### Contributors + +- @Fu3rte — reported the catalog-gap reasoning failure and proposed the override hook (#90). + ## 0.6.4 - 2026-09-03 - Refresh the generated Command Code capability catalog from `command-code@1.40.1` to `command-code@1.44.0`, adding current image-input, reasoning, effort, and output-limit metadata for newly published models. diff --git a/src/commandcode-catalog-overrides.ts b/src/commandcode-catalog-overrides.ts index cb4accd..7e72219 100644 --- a/src/commandcode-catalog-overrides.ts +++ b/src/commandcode-catalog-overrides.ts @@ -1,5 +1,26 @@ import type { CommandCodeReasoningEffort } from "./commandcode-catalog.ts" +/** + * Manual reasoning-flag policy for models the official CLI marks as + * reasoning-capable but that are missing from the pinned catalog. + * + * `src/commandcode-catalog.ts` is generated from the CLI package and must stay + * byte-identical to upstream so the daily drift check works, so a model added + * upstream after the last sync has no reasoning flag until the catalog is + * regenerated. `MODEL_REASONING` gates everything downstream + * (`src/core.ts` drops `reasoning_effort` without it and `index.ts` derives + * `compat.supportsReasoningEffort` from the effort list), so an efforts-only + * override is not enough. + * + * Add a model only when the upstream CLI bundle marks it `reasoning:!0`; + * remove it once the generated catalog carries the flag. + */ +export const MODEL_REASONING_OVERRIDES: Readonly> = { + // Command Code CLI 1.53.0: DEEPSEEK_V4_1_FLASH {reasoning:!0, + // reasoningEfforts:["low","high","max"]}; absent from the pinned catalog. + "deepseek/deepseek-v4.1-flash": true, +} + /** * Manual reasoning-effort policy for models the official CLI marks as * reasoning-capable without publishing selectable efforts. @@ -22,4 +43,8 @@ export const MODEL_EFFORT_OVERRIDES: Readonly< "meta/muse-spark-1.2-contributor": ["minimal", "low", "medium", "high", "xhigh"], "meta/muse-spark-1.3": ["minimal", "low", "medium", "high", "xhigh"], "meta/muse-spark-1.3-contributor": ["minimal", "low", "medium", "high", "xhigh"], + + // Command Code CLI 1.53.0 publishes these efforts upstream; the pinned + // catalog predates the model and therefore carries neither flag nor efforts. + "deepseek/deepseek-v4.1-flash": ["low", "high", "max"], } diff --git a/src/models.ts b/src/models.ts index 0d8ffe9..2fd929b 100644 --- a/src/models.ts +++ b/src/models.ts @@ -1,12 +1,15 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises" import { dirname } from "node:path" -import { MODEL_EFFORT_OVERRIDES } from "./commandcode-catalog-overrides.ts" +import { + MODEL_EFFORT_OVERRIDES, + MODEL_REASONING_OVERRIDES, +} from "./commandcode-catalog-overrides.ts" import { MODEL_EFFORTS as CATALOG_MODEL_EFFORTS, MODEL_INPUT_MODALITIES, MODEL_MAX_OUTPUT_TOKENS, - MODEL_REASONING, + MODEL_REASONING as CATALOG_MODEL_REASONING, type CommandCodeInputType, type CommandCodeReasoningEffort, } from "./commandcode-catalog.ts" @@ -17,7 +20,13 @@ export const MODEL_EFFORTS: Readonly> = { + ...CATALOG_MODEL_REASONING, + ...MODEL_REASONING_OVERRIDES, +} + +export { MODEL_INPUT_MODALITIES, MODEL_MAX_OUTPUT_TOKENS } export type { CommandCodeInputType } export const DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1" diff --git a/tests/test-models.ts b/tests/test-models.ts index 821a2e6..14a8edf 100644 --- a/tests/test-models.ts +++ b/tests/test-models.ts @@ -4,10 +4,14 @@ import { tmpdir } from "node:os" import { join } from "node:path" import { describe, it } from "node:test" -import { MODEL_EFFORT_OVERRIDES } from "../src/commandcode-catalog-overrides.ts" +import { + MODEL_EFFORT_OVERRIDES, + MODEL_REASONING_OVERRIDES, +} from "../src/commandcode-catalog-overrides.ts" import { COMMAND_CODE_CLI_VERSION, MODEL_EFFORTS as CATALOG_MODEL_EFFORTS, + MODEL_REASONING as CATALOG_MODEL_REASONING, } from "../src/commandcode-catalog.ts" import { apiForModelId, @@ -216,6 +220,22 @@ describe("commandCodeModelsFromApiResponse()", () => { } }) + it("merges manual reasoning overrides over the generated catalog", () => { + assert.ok(Object.keys(MODEL_REASONING_OVERRIDES).length > 0) + for (const modelId of Object.keys(MODEL_REASONING_OVERRIDES)) { + assert.equal( + CATALOG_MODEL_REASONING[modelId], + undefined, + `${modelId} now has an upstream reasoning flag; drop the manual override`, + ) + assert.equal(MODEL_REASONING[modelId], true) + assert.ok( + MODEL_EFFORTS[modelId]?.length, + `${modelId} needs selectable efforts; the endpoint rejects a bare reasoning flag`, + ) + } + }) + it("builds separate canonical pi and OMP metadata", () => { for (const [modelId, efforts] of Object.entries(MODEL_EFFORTS)) { const metadata = thinkingMetadataForModel(modelId) diff --git a/tests/test-stream.ts b/tests/test-stream.ts index 61f4b13..026ff73 100644 --- a/tests/test-stream.ts +++ b/tests/test-stream.ts @@ -8,7 +8,7 @@ import { after, before, beforeEach, describe, it } from "node:test" import { COMMAND_CODE_CLI_VERSION } from "../src/commandcode-catalog.ts" import type { AssistantMessageEvent } from "../src/core.ts" -import { MODEL_EFFORTS, thinkingLevelMapForEfforts } from "../src/models.ts" +import { MODEL_EFFORTS, MODEL_REASONING, thinkingLevelMapForEfforts } from "../src/models.ts" import { collectEvents, createTestDeps, @@ -852,6 +852,28 @@ describe("streamCommandCode — request serialization", () => { assert.equal(objectAt(server.lastRequestBody(), ["params", "reasoning_effort"]), "max") }) + it("forwards a supported Pi reasoning level for a catalog-gap model", async () => { + server.mockResponse({ + type: "success", + events: [JSON.stringify({ type: "finish", finishReason: "stop" })], + }) + const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() }) + // Model added upstream after the pinned catalog: its metadata comes only + // from the manual overrides, so a missing flag or effort list drops the field. + const modelId = "deepseek/deepseek-v4.1-flash" + const model = makeModel({ + id: modelId, + reasoning: MODEL_REASONING[modelId] === true, + thinkingLevelMap: thinkingLevelMapForEfforts(MODEL_EFFORTS[modelId]), + }) + + await collectEvents( + streamCommandCode(model, makeContext(), { apiKey: "mock-key", reasoning: "max" }), + ) + + assert.equal(objectAt(server.lastRequestBody(), ["params", "reasoning_effort"]), "max") + }) + it("omits reasoning_effort for off, unsupported, and unknown reasoning levels", async () => { const model = makeModel({ id: "deepseek/deepseek-v4-flash",