Support adaptive thinking for Anthropic-compatible aliases
closes #4790
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed custom Anthropic-compatible model aliases for adaptive-thinking Claude models by adding `compat.forceAdaptiveThinking` model metadata and moving built-in adaptive-thinking selection out of provider id substring checks ([#4797](https://github.com/earendil-works/pi-mono/pull/4797) by [@mbazso](https://github.com/mbazso)).
|
||||
- Fixed GitHub Copilot OAuth login to rely on the required device-code callback without a runtime callback availability guard.
|
||||
- Fixed Amazon Bedrock provider loading under strict package managers by declaring its direct `@smithy/node-http-handler` dependency ([#4842](https://github.com/earendil-works/pi/issues/4842)).
|
||||
- Fixed Amazon Bedrock Claude requests to send the model output token cap by default, matching Anthropic requests and avoiding Bedrock's 4096-token default truncation ([#4848](https://github.com/earendil-works/pi/issues/4848)).
|
||||
|
||||
@@ -178,6 +178,21 @@ function isGoogleThinkingApi(model: Model<any>): boolean {
|
||||
return model.api === "google-generative-ai" || model.api === "google-vertex";
|
||||
}
|
||||
|
||||
function isAnthropicAdaptiveThinkingModel(modelId: string): boolean {
|
||||
return (
|
||||
modelId.includes("opus-4-6") ||
|
||||
modelId.includes("opus-4.6") ||
|
||||
modelId.includes("opus-4-7") ||
|
||||
modelId.includes("opus-4.7") ||
|
||||
modelId.includes("sonnet-4-6") ||
|
||||
modelId.includes("sonnet-4.6")
|
||||
);
|
||||
}
|
||||
|
||||
function mergeAnthropicMessagesCompat(model: Model<Api>, compat: AnthropicMessagesCompat): void {
|
||||
model.compat = { ...(model.compat as AnthropicMessagesCompat | undefined), ...compat };
|
||||
}
|
||||
|
||||
function isGemini3ProModel(modelId: string): boolean {
|
||||
return /gemini-3(?:\.\d+)?-pro/.test(modelId.toLowerCase());
|
||||
}
|
||||
@@ -216,6 +231,9 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
|
||||
if (model.id.includes("opus-4-7") || model.id.includes("opus-4.7")) {
|
||||
mergeThinkingLevelMap(model, { xhigh: "xhigh" });
|
||||
}
|
||||
if (model.api === "anthropic-messages" && isAnthropicAdaptiveThinkingModel(model.id)) {
|
||||
mergeAnthropicMessagesCompat(model, { forceAdaptiveThinking: true });
|
||||
}
|
||||
if (model.api === "openai-completions" && model.id.includes("deepseek-v4")) {
|
||||
mergeThinkingLevelMap(model, DEEPSEEK_V4_THINKING_LEVEL_MAP);
|
||||
}
|
||||
|
||||
@@ -1723,6 +1723,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"max"},
|
||||
input: ["text", "image"],
|
||||
@@ -1741,6 +1742,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text", "image"],
|
||||
@@ -1827,6 +1829,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
@@ -2826,6 +2829,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"max"},
|
||||
input: ["text", "image"],
|
||||
@@ -2844,6 +2848,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text", "image"],
|
||||
@@ -2896,6 +2901,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "cloudflare-ai-gateway",
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
@@ -3924,6 +3930,7 @@ export const MODELS = {
|
||||
provider: "github-copilot",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"max"},
|
||||
input: ["text", "image"],
|
||||
@@ -3943,6 +3950,7 @@ export const MODELS = {
|
||||
provider: "github-copilot",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text", "image"],
|
||||
@@ -3981,6 +3989,7 @@ export const MODELS = {
|
||||
provider: "github-copilot",
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"},
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
@@ -7226,6 +7235,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"max"},
|
||||
input: ["text", "image"],
|
||||
@@ -7244,6 +7254,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text", "image"],
|
||||
@@ -7296,6 +7307,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
@@ -13369,6 +13381,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"max"},
|
||||
input: ["text", "image"],
|
||||
@@ -13387,6 +13400,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {"xhigh":"xhigh"},
|
||||
input: ["text", "image"],
|
||||
@@ -13439,6 +13453,7 @@ export const MODELS = {
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
compat: {"forceAdaptiveThinking":true},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
|
||||
@@ -164,7 +164,9 @@ export type AnthropicThinkingDisplay = "summarized" | "omitted";
|
||||
const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
|
||||
const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
|
||||
|
||||
function getAnthropicCompat(model: Model<"anthropic-messages">): Required<AnthropicMessagesCompat> {
|
||||
function getAnthropicCompat(
|
||||
model: Model<"anthropic-messages">,
|
||||
): Required<Omit<AnthropicMessagesCompat, "forceAdaptiveThinking">> {
|
||||
// Auto-detect session affinity and cache control support from provider
|
||||
const isFireworks = model.provider === "fireworks";
|
||||
const isCloudflareAiGatewayAnthropic =
|
||||
@@ -181,29 +183,34 @@ function getAnthropicCompat(model: Model<"anthropic-messages">): Required<Anthro
|
||||
export interface AnthropicOptions extends StreamOptions {
|
||||
/**
|
||||
* Enable extended thinking.
|
||||
* For Opus 4.6 and Sonnet 4.6: uses adaptive thinking (model decides when/how much to think).
|
||||
* For adaptive thinking models: the model decides when/how much to think.
|
||||
* For older models: uses budget-based thinking with thinkingBudgetTokens.
|
||||
* Default: undefined (thinking is omitted unless `streamSimpleAnthropic()` maps
|
||||
* a simple reasoning level to this option, or callers set it explicitly).
|
||||
*/
|
||||
thinkingEnabled?: boolean;
|
||||
/**
|
||||
* Token budget for extended thinking (older models only).
|
||||
* Ignored for Opus 4.6 and Sonnet 4.6, which use adaptive thinking.
|
||||
* Ignored for adaptive thinking models.
|
||||
* Default: 1024 when `thinkingEnabled` is true and no budget is provided.
|
||||
*/
|
||||
thinkingBudgetTokens?: number;
|
||||
/**
|
||||
* Effort level for adaptive thinking (Opus 4.6+ and Sonnet 4.6).
|
||||
* Effort level for adaptive thinking models.
|
||||
* Controls how much thinking Claude allocates:
|
||||
* - "max": Always thinks with no constraints (Opus 4.6 only)
|
||||
* - "xhigh": Highest reasoning level (Opus 4.7)
|
||||
* - "high": Always thinks, deep reasoning (default)
|
||||
* - "high": Always thinks, deep reasoning
|
||||
* - "medium": Moderate thinking, may skip for simple queries
|
||||
* - "low": Minimal thinking, skips for simple tasks
|
||||
* Ignored for older models.
|
||||
* Default: omitted unless `streamSimpleAnthropic()` maps a simple reasoning
|
||||
* level to this option.
|
||||
*/
|
||||
effort?: AnthropicEffort;
|
||||
/**
|
||||
* Controls how thinking content is returned in API responses.
|
||||
* - "summarized": Thinking blocks contain summarized thinking text (default here).
|
||||
* - "summarized": Thinking blocks contain summarized thinking text.
|
||||
* - "omitted": Thinking blocks return an empty thinking field; the encrypted
|
||||
* signature still travels back for multi-turn continuity. Use for faster
|
||||
* time-to-first-text-token when your UI does not surface thinking.
|
||||
@@ -211,9 +218,21 @@ export interface AnthropicOptions extends StreamOptions {
|
||||
* Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview
|
||||
* is "omitted". We default to "summarized" here to keep behavior consistent
|
||||
* with older Claude 4 models. Set this explicitly to "omitted" to opt in.
|
||||
* Default: "summarized" when thinking is enabled.
|
||||
*/
|
||||
thinkingDisplay?: AnthropicThinkingDisplay;
|
||||
/**
|
||||
* Whether to request the interleaved thinking beta header for non-adaptive
|
||||
* thinking models. Adaptive thinking models have interleaved thinking built in,
|
||||
* so the header is skipped for them regardless of this setting.
|
||||
* Default: true.
|
||||
*/
|
||||
interleavedThinking?: boolean;
|
||||
/**
|
||||
* Anthropic tool choice behavior. String values map to Anthropic's built-in
|
||||
* choices; `{ type: "tool", name }` forces a specific tool.
|
||||
* Default: omitted (Anthropic default behavior, currently equivalent to auto).
|
||||
*/
|
||||
toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
|
||||
/**
|
||||
* Pre-built Anthropic client instance. When provided, skips internal client
|
||||
@@ -686,21 +705,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
|
||||
return stream;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a model supports adaptive thinking (Opus 4.6+, Sonnet 4.6)
|
||||
*/
|
||||
function supportsAdaptiveThinking(modelId: string): boolean {
|
||||
// Adaptive-thinking model IDs (with or without date suffix)
|
||||
return (
|
||||
modelId.includes("opus-4-6") ||
|
||||
modelId.includes("opus-4.6") ||
|
||||
modelId.includes("opus-4-7") ||
|
||||
modelId.includes("opus-4.7") ||
|
||||
modelId.includes("sonnet-4-6") ||
|
||||
modelId.includes("sonnet-4.6")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map ThinkingLevel to Anthropic effort levels for adaptive thinking.
|
||||
* Note: effort "max" is only valid on Opus 4.6, while Opus 4.7 supports "xhigh".
|
||||
@@ -740,9 +744,9 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS
|
||||
return streamAnthropic(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions);
|
||||
}
|
||||
|
||||
// For Opus 4.6 and Sonnet 4.6: use adaptive thinking with effort level
|
||||
// For older models: use budget-based thinking
|
||||
if (supportsAdaptiveThinking(model.id)) {
|
||||
// For models with adaptive thinking: use an effort level.
|
||||
// For older models: use budget-based thinking.
|
||||
if (model.compat?.forceAdaptiveThinking === true) {
|
||||
const effort = mapThinkingLevelToEffort(model, options.reasoning);
|
||||
return streamAnthropic(model, context, {
|
||||
...base,
|
||||
@@ -781,9 +785,8 @@ function createClient(
|
||||
dynamicHeaders?: Record<string, string>,
|
||||
sessionId?: string,
|
||||
): { client: Anthropic; isOAuthToken: boolean } {
|
||||
// Adaptive thinking models (Opus 4.6, Sonnet 4.6) have interleaved thinking built-in.
|
||||
// The beta header is deprecated on Opus 4.6 and redundant on Sonnet 4.6, so skip it.
|
||||
const needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinking(model.id);
|
||||
// Adaptive thinking models have interleaved thinking built in, so skip the beta header.
|
||||
const needsInterleavedBeta = interleavedThinking && model.compat?.forceAdaptiveThinking !== true;
|
||||
const betaFeatures: string[] = [];
|
||||
if (useFineGrainedToolStreamingBeta) {
|
||||
betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA);
|
||||
@@ -939,14 +942,13 @@ function buildParams(
|
||||
);
|
||||
}
|
||||
|
||||
// Configure thinking mode: adaptive (Opus 4.6+ and Sonnet 4.6),
|
||||
// budget-based (older models), or explicitly disabled.
|
||||
// Configure thinking mode: adaptive, budget-based, or explicitly disabled.
|
||||
if (model.reasoning) {
|
||||
if (options?.thinkingEnabled) {
|
||||
// Default to "summarized" so Opus 4.7 and Mythos Preview behave like
|
||||
// older Claude 4 models (whose API default is also "summarized").
|
||||
const display: AnthropicThinkingDisplay = options.thinkingDisplay ?? "summarized";
|
||||
if (supportsAdaptiveThinking(model.id)) {
|
||||
if (model.compat?.forceAdaptiveThinking === true) {
|
||||
// Adaptive thinking: Claude decides when and how much to think.
|
||||
params.thinking = { type: "adaptive", display };
|
||||
if (options.effort) {
|
||||
|
||||
@@ -435,6 +435,16 @@ export interface AnthropicMessagesCompat {
|
||||
* Default: true.
|
||||
*/
|
||||
supportsCacheControlOnTools?: boolean;
|
||||
/**
|
||||
* Whether to force adaptive thinking (`thinking.type: "adaptive"` plus
|
||||
* `output_config.effort`) regardless of the model id. Built-in models that
|
||||
* require adaptive thinking set this in generated metadata. Custom
|
||||
* Anthropic-compatible providers can set this to `true` for any model whose
|
||||
* upstream requires the adaptive format. Set to `false` to
|
||||
* opt out on overridden built-in models.
|
||||
* Default: false.
|
||||
*/
|
||||
forceAdaptiveThinking?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
37
packages/ai/test/anthropic-adaptive-thinking-models.test.ts
Normal file
37
packages/ai/test/anthropic-adaptive-thinking-models.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getModels, getProviders } from "../src/models.ts";
|
||||
import type { Api, Model } from "../src/types.ts";
|
||||
|
||||
const EXPECTED_ADAPTIVE_THINKING_MODELS = [
|
||||
"anthropic/claude-opus-4-6",
|
||||
"anthropic/claude-opus-4-7",
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
"cloudflare-ai-gateway/claude-opus-4-6",
|
||||
"cloudflare-ai-gateway/claude-opus-4-7",
|
||||
"cloudflare-ai-gateway/claude-sonnet-4-6",
|
||||
"github-copilot/claude-opus-4.6",
|
||||
"github-copilot/claude-opus-4.7",
|
||||
"github-copilot/claude-sonnet-4.6",
|
||||
"opencode/claude-opus-4-6",
|
||||
"opencode/claude-opus-4-7",
|
||||
"opencode/claude-sonnet-4-6",
|
||||
"vercel-ai-gateway/anthropic/claude-opus-4.6",
|
||||
"vercel-ai-gateway/anthropic/claude-opus-4.7",
|
||||
"vercel-ai-gateway/anthropic/claude-sonnet-4.6",
|
||||
];
|
||||
|
||||
function getAllModels(): Model<Api>[] {
|
||||
return getProviders().flatMap((provider) => getModels(provider) as Model<Api>[]);
|
||||
}
|
||||
|
||||
describe("Anthropic adaptive thinking model metadata", () => {
|
||||
it("marks exactly the built-in Anthropic Messages models that use adaptive thinking", () => {
|
||||
const flaggedModels = getAllModels()
|
||||
.filter((model): model is Model<"anthropic-messages"> => model.api === "anthropic-messages")
|
||||
.filter((model) => model.compat?.forceAdaptiveThinking === true)
|
||||
.map((model) => `${model.provider}/${model.id}`)
|
||||
.sort();
|
||||
|
||||
expect(flaggedModels).toEqual([...EXPECTED_ADAPTIVE_THINKING_MODELS].sort());
|
||||
});
|
||||
});
|
||||
103
packages/ai/test/anthropic-force-adaptive-thinking.test.ts
Normal file
103
packages/ai/test/anthropic-force-adaptive-thinking.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getModel } from "../src/models.ts";
|
||||
import { streamSimple } from "../src/stream.ts";
|
||||
import type { Context, Model, SimpleStreamOptions } from "../src/types.ts";
|
||||
|
||||
interface AnthropicThinkingPayload {
|
||||
thinking?: { type: string; budget_tokens?: number; display?: string };
|
||||
output_config?: { effort?: string };
|
||||
}
|
||||
|
||||
class PayloadCaptured extends Error {
|
||||
constructor() {
|
||||
super("payload captured");
|
||||
this.name = "PayloadCaptured";
|
||||
}
|
||||
}
|
||||
|
||||
function makeContext(): Context {
|
||||
return {
|
||||
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }],
|
||||
};
|
||||
}
|
||||
|
||||
function makeCustomModel(compat?: Model<"anthropic-messages">["compat"]): Model<"anthropic-messages"> {
|
||||
return {
|
||||
// Id intentionally does not match any built-in adaptive substring. This
|
||||
// mirrors corporate proxy schemes such as `anthropic--claude-opus-latest`.
|
||||
id: "vendor--claude-opus-latest",
|
||||
name: "Vendor Proxy Opus Latest",
|
||||
api: "anthropic-messages",
|
||||
provider: "vendor-proxy",
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 32000,
|
||||
compat,
|
||||
};
|
||||
}
|
||||
|
||||
async function capturePayload(
|
||||
model: Model<"anthropic-messages">,
|
||||
options?: SimpleStreamOptions,
|
||||
): Promise<AnthropicThinkingPayload> {
|
||||
let capturedPayload: AnthropicThinkingPayload | undefined;
|
||||
|
||||
const payloadCaptureModel: Model<"anthropic-messages"> = {
|
||||
...model,
|
||||
baseUrl: "http://127.0.0.1:9",
|
||||
};
|
||||
|
||||
const s = streamSimple(payloadCaptureModel, makeContext(), {
|
||||
...options,
|
||||
apiKey: "fake-key",
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as AnthropicThinkingPayload;
|
||||
throw new PayloadCaptured();
|
||||
},
|
||||
});
|
||||
|
||||
await s.result();
|
||||
|
||||
if (!capturedPayload) {
|
||||
throw new Error("Expected payload to be captured before request failure");
|
||||
}
|
||||
|
||||
return capturedPayload;
|
||||
}
|
||||
|
||||
describe("Anthropic forceAdaptiveThinking compat override", () => {
|
||||
it("sends legacy thinking payload for custom model ids by default", async () => {
|
||||
const payload = await capturePayload(makeCustomModel(), { reasoning: "medium" });
|
||||
|
||||
expect(payload.thinking?.type).toBe("enabled");
|
||||
expect(payload.output_config).toBeUndefined();
|
||||
});
|
||||
|
||||
it("sends adaptive thinking payload when compat.forceAdaptiveThinking is true", async () => {
|
||||
const payload = await capturePayload(makeCustomModel({ forceAdaptiveThinking: true }), { reasoning: "medium" });
|
||||
|
||||
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.output_config).toEqual({ effort: "medium" });
|
||||
});
|
||||
|
||||
it("allows built-in adaptive models to opt out with compat.forceAdaptiveThinking false", async () => {
|
||||
const model: Model<"anthropic-messages"> = {
|
||||
...getModel("anthropic", "claude-opus-4-7"),
|
||||
compat: { forceAdaptiveThinking: false },
|
||||
};
|
||||
const payload = await capturePayload(model, { reasoning: "medium" });
|
||||
|
||||
expect(payload.thinking?.type).toBe("enabled");
|
||||
expect(payload.output_config).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves thinking.type=disabled when reasoning is off regardless of override", async () => {
|
||||
const payload = await capturePayload(makeCustomModel({ forceAdaptiveThinking: true }));
|
||||
|
||||
expect(payload.thinking).toEqual({ type: "disabled" });
|
||||
expect(payload.output_config).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Added `compat.forceAdaptiveThinking` support to custom Anthropic-compatible model configuration docs and validation ([#4797](https://github.com/earendil-works/pi-mono/pull/4797) by [@mbazso](https://github.com/mbazso)).
|
||||
- Added a standard unified patch to edit tool result details for SDK consumers ([#4821](https://github.com/earendil-works/pi/issues/4821)).
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -230,6 +230,8 @@ models: [{
|
||||
Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
||||
Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user/assistant text content.
|
||||
|
||||
For Anthropic-compatible providers using `api: "anthropic-messages"`, set `compat.forceAdaptiveThinking: true` on models or providers whose upstream model requires adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`). Built-in adaptive Claude models set this automatically.
|
||||
|
||||
> Migration note: Mistral moved from `openai-completions` to `mistral-conversations`.
|
||||
> Use `mistral-conversations` for native Mistral models.
|
||||
> If you intentionally route Mistral-compatible/custom endpoints through `openai-completions`, set `compat` flags explicitly as needed.
|
||||
@@ -702,8 +704,9 @@ interface ProviderModelConfig {
|
||||
/** Custom headers for this specific model. */
|
||||
headers?: Record<string, string>;
|
||||
|
||||
/** OpenAI compatibility settings for openai-completions API. */
|
||||
/** Compatibility settings for the selected API. */
|
||||
compat?: {
|
||||
// openai-completions
|
||||
supportsStore?: boolean;
|
||||
supportsDeveloperRole?: boolean;
|
||||
supportsReasoningEffort?: boolean;
|
||||
@@ -715,6 +718,13 @@ interface ProviderModelConfig {
|
||||
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template";
|
||||
cacheControlFormat?: "anthropic";
|
||||
|
||||
// anthropic-messages
|
||||
supportsEagerToolInputStreaming?: boolean;
|
||||
supportsLongCacheRetention?: boolean;
|
||||
sendSessionAffinityHeaders?: boolean;
|
||||
supportsCacheControlOnTools?: boolean;
|
||||
forceAdaptiveThinking?: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -315,10 +315,12 @@ Behavior notes:
|
||||
|
||||
## Anthropic Messages Compatibility
|
||||
|
||||
For providers or proxies using `api: "anthropic-messages"`, use `compat.supportsEagerToolInputStreaming` to control Anthropic fine-grained tool streaming compatibility.
|
||||
For providers or proxies using `api: "anthropic-messages"`, use `compat` to control Anthropic-specific request compatibility.
|
||||
|
||||
By default pi sends per-tool `eager_input_streaming: true`. If a proxy or Anthropic-compatible backend rejects that field, set `supportsEagerToolInputStreaming` to `false`. Pi will omit `tools[].eager_input_streaming` and send the legacy `fine-grained-tool-streaming-2025-05-14` beta header for tool-enabled requests instead.
|
||||
|
||||
Some Anthropic models require adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`) instead of the legacy budget-based thinking payload. Built-in models set this automatically. For custom providers or aliases that route to those models, set `forceAdaptiveThinking` to `true`.
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
@@ -328,7 +330,8 @@ By default pi sends per-tool `eager_input_streaming: true`. If a proxy or Anthro
|
||||
"apiKey": "ANTHROPIC_PROXY_KEY",
|
||||
"compat": {
|
||||
"supportsEagerToolInputStreaming": false,
|
||||
"supportsLongCacheRetention": true
|
||||
"supportsLongCacheRetention": true,
|
||||
"forceAdaptiveThinking": true
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
@@ -346,6 +349,9 @@ By default pi sends per-tool `eager_input_streaming: true`. If a proxy or Anthro
|
||||
|-------|-------------|
|
||||
| `supportsEagerToolInputStreaming` | Whether the provider accepts per-tool `eager_input_streaming`. Default: `true`. Set to `false` to omit that field and use the legacy fine-grained tool streaming beta header on tool-enabled requests. |
|
||||
| `supportsLongCacheRetention` | Whether the provider accepts Anthropic long cache retention (`cache_control.ttl: "1h"`) when cache retention is `long`. Default: `true`. |
|
||||
| `sendSessionAffinityHeaders` | Whether to send `x-session-affinity` from the session id when caching is enabled. Default: auto-detected for known providers. |
|
||||
| `supportsCacheControlOnTools` | Whether the provider accepts Anthropic-style `cache_control` markers on tool definitions. Default: `true`. |
|
||||
| `forceAdaptiveThinking` | Whether to send adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`) for this model. Built-in adaptive models set this automatically. Default: `false`. |
|
||||
|
||||
## OpenAI Compatibility
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ const OpenAIResponsesCompatSchema = Type.Object({
|
||||
const AnthropicMessagesCompatSchema = Type.Object({
|
||||
supportsEagerToolInputStreaming: Type.Optional(Type.Boolean()),
|
||||
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
|
||||
sendSessionAffinityHeaders: Type.Optional(Type.Boolean()),
|
||||
supportsCacheControlOnTools: Type.Optional(Type.Boolean()),
|
||||
forceAdaptiveThinking: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
const ProviderCompatSchema = Type.Union([
|
||||
|
||||
Reference in New Issue
Block a user