Support adaptive thinking for Anthropic-compatible aliases

closes #4790
This commit is contained in:
Mario Zechner
2026-05-22 18:30:04 +02:00
parent 7002c68f8b
commit d801d88a11
11 changed files with 239 additions and 33 deletions

View File

@@ -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)).

View File

@@ -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);
}

View File

@@ -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: {

View File

@@ -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) {

View File

@@ -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;
}
/**

View 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());
});
});

View 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();
});
});

View File

@@ -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

View File

@@ -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;
};
}
```

View File

@@ -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

View File

@@ -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([