fix(ai): support Anthropic eager tool streaming compat

closes #3575
This commit is contained in:
Mario Zechner
2026-04-23 23:12:45 +02:00
parent 6af10c9c7f
commit ffa0f31239
9 changed files with 412 additions and 16 deletions

View File

@@ -9,6 +9,7 @@ import type {
import { getEnvApiKey } from "../env-api-keys.js";
import { calculateCost } from "../models.js";
import type {
AnthropicMessagesCompat,
Api,
AssistantMessage,
CacheRetention,
@@ -159,6 +160,15 @@ export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
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> {
return {
supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
};
}
export interface AnthropicOptions extends StreamOptions {
/**
* Enable extended thinking.
@@ -433,6 +443,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
model,
apiKey,
options?.interleavedThinking ?? true,
shouldUseFineGrainedToolStreamingBeta(model, context),
options?.headers,
copilotDynamicHeaders,
);
@@ -728,6 +739,7 @@ function createClient(
model: Model<"anthropic-messages">,
apiKey: string,
interleavedThinking: boolean,
useFineGrainedToolStreamingBeta: boolean,
optionsHeaders?: Record<string, string>,
dynamicHeaders?: Record<string, string>,
): { client: Anthropic; isOAuthToken: boolean } {
@@ -735,11 +747,14 @@ function createClient(
// The beta header is deprecated on Opus 4.6 and redundant on Sonnet 4.6, so skip it.
const needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinking(model.id);
// Copilot: Bearer auth, selective betas (no fine-grained-tool-streaming)
// Copilot: Bearer auth, selective betas.
if (model.provider === "github-copilot") {
const betaFeatures: string[] = [];
if (useFineGrainedToolStreamingBeta) {
betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA);
}
if (needsInterleavedBeta) {
betaFeatures.push("interleaved-thinking-2025-05-14");
betaFeatures.push(INTERLEAVED_THINKING_BETA);
}
const client = new Anthropic({
@@ -763,8 +778,11 @@ function createClient(
}
const betaFeatures: string[] = [];
if (useFineGrainedToolStreamingBeta) {
betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA);
}
if (needsInterleavedBeta) {
betaFeatures.push("interleaved-thinking-2025-05-14");
betaFeatures.push(INTERLEAVED_THINKING_BETA);
}
// OAuth: Bearer auth, Claude Code identity headers
@@ -856,7 +874,12 @@ function buildParams(
}
if (context.tools) {
params.tools = convertTools(context.tools, isOAuthToken, cacheControl);
params.tools = convertTools(
context.tools,
isOAuthToken,
getAnthropicCompat(model).supportsEagerToolInputStreaming,
cacheControl,
);
}
// Configure thinking mode: adaptive (Opus 4.6+ and Sonnet 4.6),
@@ -1078,9 +1101,14 @@ function convertMessages(
return params;
}
function shouldUseFineGrainedToolStreamingBeta(model: Model<"anthropic-messages">, context: Context): boolean {
return !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming;
}
function convertTools(
tools: Tool[],
isOAuthToken: boolean,
supportsEagerToolInputStreaming: boolean,
cacheControl?: CacheControlEphemeral,
): Anthropic.Messages.Tool[] {
if (!tools) return [];
@@ -1091,7 +1119,7 @@ function convertTools(
return {
name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,
description: tool.description,
eager_input_streaming: true,
...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
input_schema: {
type: "object",
properties: schema.properties ?? {},