Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Expose per-model thinking tiers and forward the selected level as `reasoning_effort`. Each model now registers a `thinkingLevelMap` built from its supported efforts (extracted from the official command-code CLI catalog into `MODEL_EFFORTS`), so pi/OMP expose exactly the tiers each model supports and send the selected level to `/alpha/generate` as `params.reasoning_effort`. Previously all Command Code models registered `reasoning: true` with no `thinkingLevelMap`, so pi hid the `xhigh` and `max` tiers and silently clamped them to `high`. The `off` level and unsupported tiers are omitted from the request. Models absent from `MODEL_EFFORTS` fall back to pi's default `off` → `high` tiers.
- Resolve `COMMANDCODE_MODELS_URL` and `COMMANDCODE_MODELS_CACHE` inside the entry function so overrides are honored at call time (were fixed at module load).
- Allow pi to start when model discovery is unavailable. The provider now caches the last successfully fetched model catalog so previously discovered Command Code models remain selectable offline; a first offline start without a cache keeps Command Code unavailable until `/reload` succeeds.

### Contributors
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ You can list the current Command Code models with:
pi -e index.ts --list-models
```

## Thinking levels

Each model registers a `thinkingLevelMap` built from its supported reasoning efforts, so pi/OMP expose exactly the tiers the model supports — and forward the selected level as `reasoning_effort` in the `/alpha/generate` request body.

**Per-model efforts.** The supported levels vary by model (e.g. DeepSeek V4 Flash supports only `high` and `max`; Claude Sonnet 5 supports `low` through `max`). These are extracted from the official command-code CLI catalog (`MODEL_EFFORTS` in `src/models.ts`), because the Provider API exposes no effort metadata. Selecting an unsupported tier is hidden from the UI. Models absent from the table get no map, preserving pi's default `off` → `high` tiers.

**Real passthrough.** The selected thinking level is sent to Command Code as `params.reasoning_effort` (e.g. `"max"`), matching the official CLI. The `off` level has no upstream equivalent and is omitted, so the model's default reasoning applies.

## Install

```sh
Expand Down
54 changes: 39 additions & 15 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ import { join } from "node:path"

import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
import { calculateCommandCodeCost } from "./src/cost.ts"
import { DEFAULT_MODELS_URL, loadCommandCodeModels } from "./src/models.ts"
import {
DEFAULT_MODELS_URL,
loadCommandCodeModels,
thinkingMetadataForModel,
} from "./src/models.ts"
import { getApiKey, login, refreshToken } from "./src/oauth.ts"

const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
const MODELS_CACHE_PATH =
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
// NOTE: COMMANDCODE_MODELS_URL / COMMANDCODE_MODELS_CACHE are resolved inside the
// entry function so tests (and callers) can override them per-invocation via the
// process environment.

type CommandCodeModelCost = {
input: number
Expand Down Expand Up @@ -71,6 +75,17 @@ const MODEL_COSTS: Record<string, CommandCodeModelCost> = {
"xiaomi/mimo-v2.5": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
}

// ---------------------------------------------------------------------------
// Thinking level metadata
// ---------------------------------------------------------------------------
//
// Command Code's /alpha/generate accepts a reasoning_effort parameter whose
// supported values vary per model (extracted from the official command-code CLI
// catalog into MODEL_EFFORTS). We build a per-model reasoningEffortMap from that
// list so pi exposes exactly the tiers the upstream supports — and forwards the
// selected level as reasoning_effort in the request body (see core.ts).
// Models absent from MODEL_EFFORTS get no map, preserving pi's default tiers.

const streamCommandCode = createStreamCommandCode({
createStream: () => new AssistantMessageEventStream(),
calculateCost: calculateCommandCodeCost,
Expand All @@ -82,9 +97,12 @@ const streamCommandCode = createStreamCommandCode({
// ---------------------------------------------------------------------------

export default async function (pi: ExtensionAPI) {
const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
const modelsCachePath =
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
const { models, warning } = await loadCommandCodeModels({
url: MODELS_URL,
cachePath: MODELS_CACHE_PATH,
url: modelsUrl,
cachePath: modelsCachePath,
})

if (warning) console.warn(`[commandcode] ${warning}`)
Expand All @@ -106,14 +124,20 @@ export default async function (pi: ExtensionAPI) {
refreshToken,
getApiKey,
},
models: models.map((model) => ({
id: model.id,
name: model.name,
reasoning: model.reasoning,
input: ["text"] as const,
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
})),
models: models.map((model) => {
const thinking = thinkingMetadataForModel(model.id)
return {
id: model.id,
name: model.name,
reasoning: model.reasoning,
// OMP 17.2.x reads model.thinking.effortMap; pi-ai <=0.75.5 reads
// model.thinkingLevelMap. Emit both so the plugin works across versions.
...(thinking ?? {}),
input: ["text"] as const,
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
}
}),
})
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@
"LICENSE"
],
"scripts": {
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && tsx tests/test-retry.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-thinking-levels.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
"typecheck": "tsc --noEmit",
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
"test:unit": "tsx tests/test-pure-functions.ts",
"test:models": "tsx tests/test-models.ts",
"test:thinking": "tsx tests/test-thinking-levels.ts",
"test:pricing": "tsx tests/test-pricing.ts",
"test:oauth": "tsx tests/test-oauth.ts",
"test:abort": "tsx tests/test-abort.ts",
Expand Down
11 changes: 11 additions & 0 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,16 @@ export function createStreamCommandCode(deps: CoreDependencies) {
const workingDir = cwd()
const threadId = uuid()

// pi passes the resolved thinking level as options.reasoning ("off".."max").
// Resolve the effort map from whichever field the host version uses:
// pi-ai <=0.75.5 attaches `thinkingLevelMap`; OMP 17.2.x attaches
// `thinking.effortMap`. Then forward as reasoning_effort. Omit when the
// level is "off" or maps to null (unsupported) so the upstream default applies.
const piLevel = options?.reasoning
const effortMap = model.thinking?.effortMap ?? model.thinkingLevelMap
const entry = piLevel && piLevel !== "off" ? effortMap?.[piLevel] : undefined
const reasoningEffort = entry && entry !== "off" ? entry : undefined

let body: unknown = {
config: {
workingDir,
Expand All @@ -444,6 +454,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
max_tokens: generateMaxTokens(model, options),
temperature: 0.3,
stream: true,
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
},
threadId,
}
Expand Down
87 changes: 87 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,93 @@ export interface CommandCodeModel {
maxTokens: number
}

/**
* Per-model reasoning effort levels supported by Command Code's /alpha/generate.
*
* Extracted from the official command-code CLI (1.7.0) catalog. The Provider API
* (/provider/v1/models) exposes no effort metadata, so this static table is the
* only source. Unknown model ids fall back to no thinkingLevelMap, preserving
* pi's default off..high tiers.
*/
export const MODEL_EFFORTS: Readonly<Record<string, readonly string[]>> = {
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
"claude-haiku-4-5-20251001": ["low", "medium", "high", "xhigh", "max"],
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
"claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
"deepseek/deepseek-v4-flash": ["high", "max"],
"deepseek/deepseek-v4-pro": ["high", "max"],
"gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
"gpt-5.4": ["low", "medium", "high", "xhigh"],
"gpt-5.4-mini": ["low", "medium", "high"],
"gpt-5.5": ["low", "medium", "high", "xhigh"],
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
"google/gemini-3.1-flash-lite": ["low", "medium", "high"],
"google/gemini-3.5-flash": ["low", "medium", "high"],
"google/gemini-3.5-flash-lite": ["low", "medium", "high"],
"google/gemini-3.6-flash": ["low", "medium", "high"],
"meta/muse-spark-1.1": ["low", "medium", "high"],
"moonshotai/Kimi-K2.5": ["high", "max"],
"moonshotai/Kimi-K2.6": ["high", "max"],
"sakana/fugu-ultra": ["high", "xhigh"],
"tencent/hy3-paid": ["low", "medium", "high"],

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checked-in table does not match the authoritative command-code@1.7.0 catalog. These five entries have no adjustable effort surface in the published CLI: claude-haiku-4-5-20251001, moonshotai/Kimi-K2.5, moonshotai/Kimi-K2.6, meta/muse-spark-1.1, and tencent/hy3-paid. The CLI's generated reference/models.md marks their Efforts column as ("models without an effort column entry decide their own reasoning depth"). Please remove these entries; otherwise the provider exposes and sends effort values that the official CLI does not support. The remaining 22 entries match the 1.7.0 effort map.

"xai/grok-4.5": ["low", "medium", "high"],
"zai-org/GLM-5.2": ["high", "max"],
}

/** pi thinking levels in increasing order (mirrors pi-ai ThinkingLevel). */
const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const

/**
* Build a pi ThinkingLevelMap from a Command Code effort list. Supported levels
* map to their upstream effort string; unsupported levels are explicitly `null`
* so pi-ai's getSupportedThinkingLevels hides them and clampThinkingLevel snaps
* to the nearest supported level. `off` always maps to itself.
*/
export function thinkingLevelMapForEfforts(
efforts: readonly string[],
): Record<string, string | null> {
const map: Record<string, string | null> = { off: "off" }
for (const level of PI_THINKING_LEVELS) {
if (level === "off") continue
map[level] = efforts.includes(level) ? level : null
}
return map
}

export type ThinkingMetadata = {
thinkingLevelMap: Record<string, string | null>
thinking: {
effortMap: Record<string, string | null>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OMP 17.2.x expects canonical ThinkingConfig: { mode: "effort", efforts: [...], defaultLevel?, effortMap? }. mode is required, while this effortMap also contains legacy pi-only off/null values that do not belong in OMP's string-valued map. Please keep the legacy thinkingLevelMap for pi, but emit OMP metadata separately, e.g. thinking: { mode: "effort", efforts: [...efforts] }. An OMP identity map is unnecessary because omitted mappings already pass the supported effort through unchanged.

efforts: string[]
defaultLevel: string
}
}

/**
* Resolve per-model thinking metadata for provider registration. Models absent
* from MODEL_EFFORTS get no map, preserving pi's default off..high tiers.
* Emits both `thinkingLevelMap` (pi-ai <=0.75.5) and `thinking.effortMap`
* (OMP 17.2.x) so the plugin works across host versions.
*/
export function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined {
const efforts = MODEL_EFFORTS[modelId]
if (!efforts) return undefined
const effortMap = thinkingLevelMapForEfforts(efforts)
return {
thinkingLevelMap: effortMap,
thinking: {
effortMap,
efforts: [...efforts],
defaultLevel: efforts[efforts.length - 2] ?? efforts[0],

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not infer defaultLevel from array position. OMP actively applies this value on initial model selection and every model switch, so this makes Claude/GPT-5.6 default to xhigh, Gemini/Grok to medium, etc. The Command Code catalog publishes supported efforts but no per-model default, so this changes runtime behavior and potentially cost without an upstream source. Omit defaultLevel unless an authoritative default becomes available.

},
}
}

interface FetchCommandCodeModelsOptions {
url?: string
fetchImpl?: typeof fetch
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export interface ModelLike {
provider: string
maxTokens: number
cost: ModelCost
reasoning?: boolean
thinkingLevelMap?: Record<string, string | null>
thinking?: {
effortMap?: Record<string, string | null>
efforts?: readonly string[]
defaultLevel?: string
}
}

export interface MessageLike {
Expand Down Expand Up @@ -95,6 +102,8 @@ export interface StreamOptions {
signal?: AbortSignal
headers?: Record<string, string>
maxTokens?: number
/** Resolved pi thinking level ("off".."max"); mapped to reasoning_effort upstream. */
reasoning?: string
onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
onResponse?: (response: ProviderResponseInfo, model: ModelLike) => void | Promise<void>
/**
Expand Down
Loading
Loading