Support adaptive thinking for Anthropic-compatible aliases
closes #4790
This commit is contained in:
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user