@@ -2,6 +2,14 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Replaced `compat.reasoningEffortMap` in `models.json` and `pi.registerProvider()` model definitions with model-level `thinkingLevelMap` ([#3208](https://github.com/badlogic/pi-mono/issues/3208)). Migration: move old mappings from `compat.reasoningEffortMap` to `thinkingLevelMap`. Use string values for provider-specific thinking values and `null` for unsupported pi levels that should be hidden and skipped by cycling. See `docs/models.md#thinking-level-map` and `docs/custom-provider.md`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added model-level `thinkingLevelMap` support in `models.json` and `pi.registerProvider()`, allowing models to expose only the thinking levels they actually support ([#3208](https://github.com/badlogic/pi-mono/issues/3208)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `pi.registerProvider()` to honor per-model `baseUrl` overrides ([#4063](https://github.com/badlogic/pi-mono/issues/4063)).
|
||||
|
||||
@@ -201,28 +201,29 @@ The `api` field determines which streaming implementation is used:
|
||||
| `google-vertex` | Google Vertex AI API |
|
||||
| `bedrock-converse-stream` | Amazon Bedrock Converse API |
|
||||
|
||||
Most OpenAI-compatible providers work with `openai-completions`. Use `compat` for quirks:
|
||||
Most OpenAI-compatible providers work with `openai-completions`. Use model-level `thinkingLevelMap` for model-specific thinking levels, and `compat` for provider quirks:
|
||||
|
||||
```typescript
|
||||
models: [{
|
||||
id: "custom-model",
|
||||
// ...
|
||||
reasoning: true,
|
||||
thinkingLevelMap: { // map pi levels to provider values; null hides unsupported levels
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: "default",
|
||||
xhigh: "max"
|
||||
},
|
||||
compat: {
|
||||
supportsDeveloperRole: false, // use "system" instead of "developer"
|
||||
supportsDeveloperRole: false, // use "system" instead of "developer"
|
||||
supportsReasoningEffort: true,
|
||||
reasoningEffortMap: { // map pi-ai levels to provider values
|
||||
minimal: "default",
|
||||
low: "default",
|
||||
medium: "default",
|
||||
high: "default",
|
||||
xhigh: "default"
|
||||
},
|
||||
maxTokensField: "max_tokens", // instead of "max_completion_tokens"
|
||||
requiresToolResultName: true, // tool results need name field
|
||||
thinkingFormat: "qwen", // top-level enable_thinking: true
|
||||
cacheControlFormat: "anthropic" // Anthropic-style cache_control markers
|
||||
}
|
||||
}]
|
||||
maxTokensField: "max_tokens", // instead of "max_completion_tokens"
|
||||
requiresToolResultName: true, // tool results need name field
|
||||
thinkingFormat: "qwen", // top-level enable_thinking: true
|
||||
cacheControlFormat: "anthropic" // Anthropic-style cache_control markers
|
||||
}
|
||||
}]
|
||||
```
|
||||
|
||||
Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
||||
@@ -601,6 +602,9 @@ interface ProviderModelConfig {
|
||||
/** Whether the model supports extended thinking. */
|
||||
reasoning: boolean;
|
||||
|
||||
/** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */
|
||||
thinkingLevelMap?: Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh", string | null>>;
|
||||
|
||||
/** Supported input types. */
|
||||
input: ("text" | "image")[];
|
||||
|
||||
@@ -626,7 +630,6 @@ interface ProviderModelConfig {
|
||||
supportsStore?: boolean;
|
||||
supportsDeveloperRole?: boolean;
|
||||
supportsReasoningEffort?: boolean;
|
||||
reasoningEffortMap?: Partial<Record<"minimal" | "low" | "medium" | "high" | "xhigh", string>>;
|
||||
supportsUsageInStreaming?: boolean;
|
||||
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
||||
requiresToolResultName?: boolean;
|
||||
|
||||
@@ -192,6 +192,7 @@ If your command is slow, expensive, rate-limited, or should keep using a previou
|
||||
| `name` | No | `id` | Human-readable model label. Used for matching (`--model` patterns) and shown in model details/status text. |
|
||||
| `api` | No | provider's `api` | Override provider's API for this model |
|
||||
| `reasoning` | No | `false` | Supports extended thinking |
|
||||
| `thinkingLevelMap` | No | omitted | Maps pi thinking levels to provider values and marks unsupported levels (see below) |
|
||||
| `input` | No | `["text"]` | Input types: `["text"]` or `["text", "image"]` |
|
||||
| `contextWindow` | No | `128000` | Context window size in tokens |
|
||||
| `maxTokens` | No | `16384` | Maximum output tokens |
|
||||
@@ -202,6 +203,48 @@ Current behavior:
|
||||
- `/model` and `--list-models` list entries by model `id`.
|
||||
- The configured `name` is used for model matching and detail/status text.
|
||||
|
||||
### Thinking Level Map
|
||||
|
||||
Use `thinkingLevelMap` on a model to describe model-specific thinking controls. Keys are pi thinking levels: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`.
|
||||
|
||||
Values are tristate:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| omitted | Level is supported and uses the provider's default mapping |
|
||||
| string | Level is supported and this value is sent to the provider |
|
||||
| `null` | Level is unsupported and hidden/skipped/clamped away |
|
||||
|
||||
Example for a model that only supports off, high, and max reasoning:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "deepseek-v4-pro",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": {
|
||||
"minimal": null,
|
||||
"low": null,
|
||||
"medium": null,
|
||||
"high": "high",
|
||||
"xhigh": "max"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example for a model where thinking cannot be disabled:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "always-thinking-model",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": {
|
||||
"off": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Migration: older configs that used `compat.reasoningEffortMap` should move that mapping to model-level `thinkingLevelMap`. Use `null` for levels that should not appear in the UI.
|
||||
|
||||
## Overriding Built-in Providers
|
||||
|
||||
Route a built-in provider through a proxy without redefining models:
|
||||
@@ -332,7 +375,6 @@ For providers with partial OpenAI compatibility, use the `compat` field.
|
||||
| `supportsStore` | Provider supports `store` field |
|
||||
| `supportsDeveloperRole` | Use `developer` vs `system` role |
|
||||
| `supportsReasoningEffort` | Support for `reasoning_effort` parameter |
|
||||
| `reasoningEffortMap` | Map pi thinking levels to provider-specific `reasoning_effort` values |
|
||||
| `supportsUsageInStreaming` | Supports `stream_options: { include_usage: true }` (default: `true`) |
|
||||
| `maxTokensField` | Use `max_completion_tokens` or `max_tokens` |
|
||||
| `requiresToolResultName` | Include `name` on tool result messages |
|
||||
|
||||
@@ -24,7 +24,13 @@ import type {
|
||||
ThinkingLevel,
|
||||
} from "@mariozechner/pi-agent-core";
|
||||
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai";
|
||||
import { isContextOverflow, modelsAreEqual, resetApiProviders, supportsXhigh } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
clampThinkingLevel,
|
||||
getSupportedThinkingLevels,
|
||||
isContextOverflow,
|
||||
modelsAreEqual,
|
||||
resetApiProviders,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import { theme } from "../modes/interactive/theme/theme.js";
|
||||
import { stripFrontmatter } from "../utils/frontmatter.js";
|
||||
import { sleep } from "../utils/sleep.js";
|
||||
@@ -230,9 +236,6 @@ interface ToolDefinitionEntry {
|
||||
/** Standard thinking levels */
|
||||
const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"];
|
||||
|
||||
/** Thinking levels including xhigh (for supported models) */
|
||||
const THINKING_LEVELS_WITH_XHIGH: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
||||
|
||||
// ============================================================================
|
||||
// AgentSession Class
|
||||
// ============================================================================
|
||||
@@ -1546,15 +1549,8 @@ export class AgentSession {
|
||||
* The provider will clamp to what the specific model supports internally.
|
||||
*/
|
||||
getAvailableThinkingLevels(): ThinkingLevel[] {
|
||||
if (!this.supportsThinking()) return ["off"];
|
||||
return this.supportsXhighThinking() ? THINKING_LEVELS_WITH_XHIGH : THINKING_LEVELS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current model supports xhigh thinking level.
|
||||
*/
|
||||
supportsXhighThinking(): boolean {
|
||||
return this.model ? supportsXhigh(this.model) : false;
|
||||
if (!this.model) return THINKING_LEVELS;
|
||||
return getSupportedThinkingLevels(this.model) as ThinkingLevel[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1574,22 +1570,8 @@ export class AgentSession {
|
||||
return this.thinkingLevel;
|
||||
}
|
||||
|
||||
private _clampThinkingLevel(level: ThinkingLevel, availableLevels: ThinkingLevel[]): ThinkingLevel {
|
||||
const ordered = THINKING_LEVELS_WITH_XHIGH;
|
||||
const available = new Set(availableLevels);
|
||||
const requestedIndex = ordered.indexOf(level);
|
||||
if (requestedIndex === -1) {
|
||||
return availableLevels[0] ?? "off";
|
||||
}
|
||||
for (let i = requestedIndex; i < ordered.length; i++) {
|
||||
const candidate = ordered[i];
|
||||
if (available.has(candidate)) return candidate;
|
||||
}
|
||||
for (let i = requestedIndex - 1; i >= 0; i--) {
|
||||
const candidate = ordered[i];
|
||||
if (available.has(candidate)) return candidate;
|
||||
}
|
||||
return availableLevels[0] ?? "off";
|
||||
private _clampThinkingLevel(level: ThinkingLevel, _availableLevels: ThinkingLevel[]): ThinkingLevel {
|
||||
return this.model ? (clampThinkingLevel(this.model, level) as ThinkingLevel) : "off";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -1359,6 +1359,8 @@ export interface ProviderModelConfig {
|
||||
baseUrl?: string;
|
||||
/** Whether the model supports extended thinking. */
|
||||
reasoning: boolean;
|
||||
/** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */
|
||||
thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
|
||||
/** Supported input types. */
|
||||
input: ("text" | "image")[];
|
||||
/** Cost per token (for tracking, can be 0). */
|
||||
|
||||
@@ -80,20 +80,21 @@ const VercelGatewayRoutingSchema = Type.Object({
|
||||
order: Type.Optional(Type.Array(Type.String())),
|
||||
});
|
||||
|
||||
// Schema for OpenAI compatibility settings
|
||||
const ReasoningEffortMapSchema = Type.Object({
|
||||
minimal: Type.Optional(Type.String()),
|
||||
low: Type.Optional(Type.String()),
|
||||
medium: Type.Optional(Type.String()),
|
||||
high: Type.Optional(Type.String()),
|
||||
xhigh: Type.Optional(Type.String()),
|
||||
// Schema for thinking level support and provider-specific values
|
||||
const ThinkingLevelMapValueSchema = Type.Union([Type.String(), Type.Null()]);
|
||||
const ThinkingLevelMapSchema = Type.Object({
|
||||
off: Type.Optional(ThinkingLevelMapValueSchema),
|
||||
minimal: Type.Optional(ThinkingLevelMapValueSchema),
|
||||
low: Type.Optional(ThinkingLevelMapValueSchema),
|
||||
medium: Type.Optional(ThinkingLevelMapValueSchema),
|
||||
high: Type.Optional(ThinkingLevelMapValueSchema),
|
||||
xhigh: Type.Optional(ThinkingLevelMapValueSchema),
|
||||
});
|
||||
|
||||
const OpenAICompletionsCompatSchema = Type.Object({
|
||||
supportsStore: Type.Optional(Type.Boolean()),
|
||||
supportsDeveloperRole: Type.Optional(Type.Boolean()),
|
||||
supportsReasoningEffort: Type.Optional(Type.Boolean()),
|
||||
reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),
|
||||
supportsUsageInStreaming: Type.Optional(Type.Boolean()),
|
||||
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
|
||||
requiresToolResultName: Type.Optional(Type.Boolean()),
|
||||
@@ -141,6 +142,7 @@ const ModelDefinitionSchema = Type.Object({
|
||||
api: Type.Optional(Type.String({ minLength: 1 })),
|
||||
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
||||
reasoning: Type.Optional(Type.Boolean()),
|
||||
thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema),
|
||||
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
||||
cost: Type.Optional(
|
||||
Type.Object({
|
||||
@@ -160,6 +162,7 @@ const ModelDefinitionSchema = Type.Object({
|
||||
const ModelOverrideSchema = Type.Object({
|
||||
name: Type.Optional(Type.String({ minLength: 1 })),
|
||||
reasoning: Type.Optional(Type.Boolean()),
|
||||
thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema),
|
||||
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
||||
cost: Type.Optional(
|
||||
Type.Object({
|
||||
@@ -288,6 +291,9 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
||||
// Simple field overrides
|
||||
if (override.name !== undefined) result.name = override.name;
|
||||
if (override.reasoning !== undefined) result.reasoning = override.reasoning;
|
||||
if (override.thinkingLevelMap !== undefined) {
|
||||
result.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };
|
||||
}
|
||||
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
||||
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
|
||||
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
||||
@@ -581,6 +587,7 @@ export class ModelRegistry {
|
||||
provider: providerName,
|
||||
baseUrl,
|
||||
reasoning: modelDef.reasoning ?? false,
|
||||
thinkingLevelMap: modelDef.thinkingLevelMap,
|
||||
input: (modelDef.input ?? ["text"]) as ("text" | "image")[],
|
||||
cost: modelDef.cost ?? defaultCost,
|
||||
contextWindow: modelDef.contextWindow ?? 128000,
|
||||
@@ -878,6 +885,7 @@ export class ModelRegistry {
|
||||
provider: providerName,
|
||||
baseUrl: modelDef.baseUrl ?? config.baseUrl!,
|
||||
reasoning: modelDef.reasoning,
|
||||
thinkingLevelMap: modelDef.thinkingLevelMap,
|
||||
input: modelDef.input as ("text" | "image")[],
|
||||
cost: modelDef.cost,
|
||||
contextWindow: modelDef.contextWindow,
|
||||
@@ -926,6 +934,7 @@ export interface ProviderConfigInput {
|
||||
api?: Api;
|
||||
baseUrl?: string;
|
||||
reasoning: boolean;
|
||||
thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
|
||||
input: ("text" | "image")[];
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from "node:path";
|
||||
import { Agent, type AgentMessage, type ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import { type Message, type Model, streamSimple } from "@mariozechner/pi-ai";
|
||||
import { clampThinkingLevel, type Message, type Model, streamSimple } from "@mariozechner/pi-ai";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import { AgentSession } from "./agent-session.js";
|
||||
import { formatNoModelsAvailableMessage } from "./auth-guidance.js";
|
||||
@@ -262,8 +262,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
}
|
||||
|
||||
// Clamp to model capabilities
|
||||
if (!model || !model.reasoning) {
|
||||
if (!model) {
|
||||
thinkingLevel = "off";
|
||||
} else {
|
||||
thinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;
|
||||
}
|
||||
|
||||
const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"];
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { type ImageContent, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
||||
import { type ImageContent, modelsAreEqual } from "@mariozechner/pi-ai";
|
||||
import { ProcessTerminal, setKeybindings, TUI } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.js";
|
||||
@@ -595,15 +595,7 @@ export async function main(args: string[], options?: MainOptions) {
|
||||
});
|
||||
const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;
|
||||
if (created.session.model && cliThinkingOverride) {
|
||||
let effectiveThinking = created.session.thinkingLevel;
|
||||
if (!created.session.model.reasoning) {
|
||||
effectiveThinking = "off";
|
||||
} else if (effectiveThinking === "xhigh" && !supportsXhigh(created.session.model)) {
|
||||
effectiveThinking = "high";
|
||||
}
|
||||
if (effectiveThinking !== created.session.thinkingLevel) {
|
||||
created.session.setThinkingLevel(effectiveThinking);
|
||||
}
|
||||
created.session.setThinkingLevel(created.session.thinkingLevel);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -396,7 +396,7 @@ describe("ModelRegistry", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("compat schema accepts reasoningEffortMap, supportsStrictMode, and cacheControlFormat", () => {
|
||||
test("model schema accepts thinkingLevelMap and compat schema accepts supportsStrictMode and cacheControlFormat", () => {
|
||||
writeRawModelsJson({
|
||||
demo: {
|
||||
baseUrl: "https://example.com/v1",
|
||||
@@ -410,11 +410,11 @@ describe("ModelRegistry", () => {
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1000,
|
||||
maxTokens: 100,
|
||||
thinkingLevelMap: {
|
||||
minimal: null,
|
||||
high: "max",
|
||||
},
|
||||
compat: {
|
||||
reasoningEffortMap: {
|
||||
minimal: "default",
|
||||
high: "max",
|
||||
},
|
||||
supportsStrictMode: false,
|
||||
cacheControlFormat: "anthropic",
|
||||
},
|
||||
@@ -424,10 +424,11 @@ describe("ModelRegistry", () => {
|
||||
});
|
||||
|
||||
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
|
||||
const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined;
|
||||
const model = registry.find("demo", "demo-model");
|
||||
const compat = model?.compat as OpenAICompletionsCompat | undefined;
|
||||
|
||||
expect(registry.getError()).toBeUndefined();
|
||||
expect(compat?.reasoningEffortMap).toEqual({ minimal: "default", high: "max" });
|
||||
expect(model?.thinkingLevelMap).toEqual({ minimal: null, high: "max" });
|
||||
expect(compat?.supportsStrictMode).toBe(false);
|
||||
expect(compat?.cacheControlFormat).toBe("anthropic");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user