import { describe, it, expect, beforeEach } from "vitest";
import {
estimateCostUsd,
truncateToolResult,
getMaxToolResultChars,
sanitizeError,
LoopDetector,
isRetryableError,
getContextManagement,
getMaxOutputTokens,
MODEL_PRICING,
addPromptCaching,
getThinkingConfig,
} from "./agent-core.js";
// ============================================================================
// estimateCostUsd
// ============================================================================
describe("estimateCostUsd", () => {
it("calculates cost for claude-opus-4-6", () => {
// 1M input tokens at $5/M + 1M output tokens at $25/M = $30
const cost = estimateCostUsd(1_000_000, 1_000_000, "claude-opus-4-6");
expect(cost).toBeCloseTo(30.0);
});
it("calculates cost for claude-sonnet-4-20250514", () => {
// 1M input at $3/M + 1M output at $15/M = $18
const cost = estimateCostUsd(1_000_000, 1_000_000, "claude-sonnet-4-20250514");
expect(cost).toBeCloseTo(18.0);
});
it("calculates cost for claude-haiku-4-5-20251001", () => {
// 1M input at $1/M + 1M output at $5/M = $6
const cost = estimateCostUsd(1_000_000, 1_000_000, "claude-haiku-4-5-20251001");
expect(cost).toBeCloseTo(6.0);
});
it("includes thinking tokens in cost", () => {
// Opus: 0 input + 0 output + 1M thinking at $25/M = $25
const cost = estimateCostUsd(0, 0, "claude-opus-4-6", 1_000_000);
expect(cost).toBeCloseTo(25.0);
});
it("handles zero tokens", () => {
expect(estimateCostUsd(0, 0, "claude-opus-4-6")).toBe(0);
});
it("falls back to sonnet pricing for unknown models", () => {
// Unknown model falls back to sonnet pricing: 1M input at $3/M + 1M output at $15/M
const cost = estimateCostUsd(1_000_000, 1_000_000, "claude-unknown-model");
expect(cost).toBeCloseTo(18.0);
});
it("handles partial model name matching", () => {
// "claude-opus-4-6" is contained in "claude-opus-4-6-20260101"
const cost = estimateCostUsd(1_000_000, 0, "claude-opus-4-6-20260101");
// model.includes(k) checks if the passed model includes the pricing key: $5/M input
expect(cost).toBeCloseTo(5.0);
});
it("calculates fractional token costs correctly", () => {
// 500 input tokens for opus: 500/1M * $5 = $0.0025
const cost = estimateCostUsd(500, 0, "claude-opus-4-6");
expect(cost).toBeCloseTo(0.0025);
});
// ---------- Cache-aware pricing ----------
it("applies Anthropic 90% cache read discount", () => {
// Opus: 1M input at $5/M = $5, but 500K are cache reads → save 500K/1M * $5 * 0.9 = $2.25
const cost = estimateCostUsd(1_000_000, 0, "claude-opus-4-6", 0, 500_000, 0);
expect(cost).toBeCloseTo(5.0 - 2.25); // $2.75
});
it("applies Anthropic 25% cache creation surcharge", () => {
// Opus: 1M input at $5/M = $5 + 200K creation → 200K/1M * $5 * 0.25 = $0.25
const cost = estimateCostUsd(1_000_000, 0, "claude-opus-4-6", 0, 0, 200_000);
expect(cost).toBeCloseTo(5.25);
});
it("applies both Anthropic cache read and creation adjustments", () => {
// Opus: 1M input at $5/M = $5
// 800K cache reads → save 800K/1M * $5 * 0.9 = $3.60
// 100K cache creation → add 100K/1M * $5 * 0.25 = $0.125
// Total: $5 - $3.60 + $0.125 = $1.525
const cost = estimateCostUsd(1_000_000, 0, "claude-opus-4-6", 0, 800_000, 100_000);
expect(cost).toBeCloseTo(1.525);
});
it("applies OpenAI 50% cache read discount", () => {
// GPT-5: 1M input at some rate, 500K cache reads → 50% discount on those
const baseCost = estimateCostUsd(1_000_000, 0, "gpt-5");
const cachedCost = estimateCostUsd(1_000_000, 0, "gpt-5", 0, 500_000, 0);
// cachedCost should be less than baseCost
expect(cachedCost).toBeLessThan(baseCost);
// The savings should be 50% of 500K tokens' input rate
const savings = baseCost - cachedCost;
const expectedSavings = (500_000 / 1_000_000) * MODEL_PRICING["gpt-5"].inputPer1M * 0.5;
expect(savings).toBeCloseTo(expectedSavings);
});
it("applies Gemini 75% cache read discount", () => {
const baseCost = estimateCostUsd(1_000_000, 0, "gemini-3-pro-preview");
const cachedCost = estimateCostUsd(1_000_000, 0, "gemini-3-pro-preview", 0, 500_000, 0);
expect(cachedCost).toBeLessThan(baseCost);
const savings = baseCost - cachedCost;
const expectedSavings = (500_000 / 1_000_000) * MODEL_PRICING["gemini-3-pro-preview"].inputPer1M * 0.75;
expect(savings).toBeCloseTo(expectedSavings);
});
it("does not apply cache discount when cache tokens are zero", () => {
const noCacheCost = estimateCostUsd(1_000_000, 0, "claude-opus-4-6");
const zeroCacheCost = estimateCostUsd(1_000_000, 0, "claude-opus-4-6", 0, 0, 0);
expect(zeroCacheCost).toBe(noCacheCost);
});
it("applies Bedrock cache pricing same as Anthropic", () => {
// Bedrock uses same 90% cache read discount
const baseCost = estimateCostUsd(1_000_000, 0, "us.anthropic.claude-sonnet-4-20250514-v1:0");
const cachedCost = estimateCostUsd(1_000_000, 0, "us.anthropic.claude-sonnet-4-20250514-v1:0", 0, 500_000, 0);
expect(cachedCost).toBeLessThan(baseCost);
});
});
// ============================================================================
// truncateToolResult
// ============================================================================
describe("truncateToolResult", () => {
it("returns content unchanged when under limit", () => {
const content = "short content";
expect(truncateToolResult(content, 100)).toBe(content);
});
it("returns content unchanged when exactly at limit", () => {
const content = "x".repeat(100);
expect(truncateToolResult(content, 100)).toBe(content);
});
it("truncates content exceeding the limit", () => {
const content = "x".repeat(200);
const result = truncateToolResult(content, 100);
expect(result.length).toBeLessThan(content.length);
expect(result).toContain("truncated");
expect(result).toContain("200");
});
it("truncates at low effort level (10K chars)", () => {
const content = "a".repeat(15_000);
const result = truncateToolResult(content, 10_000);
expect(result).toContain("truncated");
// First 10K chars should be preserved
expect(result.startsWith("a".repeat(10_000))).toBe(true);
});
it("truncates at medium effort level (20K chars)", () => {
const content = "b".repeat(25_000);
const result = truncateToolResult(content, 20_000);
expect(result).toContain("truncated");
expect(result.startsWith("b".repeat(20_000))).toBe(true);
});
it("truncates at high effort level (30K chars)", () => {
const content = "c".repeat(35_000);
const result = truncateToolResult(content, 30_000);
expect(result).toContain("truncated");
expect(result.startsWith("c".repeat(30_000))).toBe(true);
});
it("includes total character count in truncation message", () => {
const content = "x".repeat(50_000);
const result = truncateToolResult(content, 10_000);
expect(result).toContain("50,000");
});
});
// ============================================================================
// getMaxToolResultChars
// ============================================================================
describe("getMaxToolResultChars", () => {
it("returns default 20K when no config provided", () => {
expect(getMaxToolResultChars()).toBe(20_000);
expect(getMaxToolResultChars(null)).toBe(20_000);
expect(getMaxToolResultChars(undefined)).toBe(20_000);
});
it("returns configured value when provided", () => {
expect(getMaxToolResultChars({ max_tool_result_chars: 10_000 })).toBe(10_000);
expect(getMaxToolResultChars({ max_tool_result_chars: 30_000 })).toBe(30_000);
});
it("returns default when config has no max_tool_result_chars", () => {
expect(getMaxToolResultChars({})).toBe(20_000);
});
});
// ============================================================================
// sanitizeError
// ============================================================================
describe("sanitizeError", () => {
it("strips API keys starting with sk-", () => {
const err = "Error: invalid key sk-abcdef1234567890abcdef";
const result = sanitizeError(err);
expect(result).not.toContain("abcdef1234567890");
expect(result).toContain("sk-***");
});
it("strips key=value patterns", () => {
const err = "Error: key=abcdefghijklmnopqrstuvwxyz";
const result = sanitizeError(err);
expect(result).toContain("key=***");
expect(result).not.toContain("abcdefghijklmnopqrstuvwxyz");
});
it("strips password patterns", () => {
const err = "Error: password=mysecretpassword123";
const result = sanitizeError(err);
expect(result).toContain("password=***");
expect(result).not.toContain("mysecretpassword123");
});
it("strips stack traces (lines starting with at)", () => {
const err = "Error: something failed\n at Object.<anonymous> (/foo/bar.js:10:15)\n at Module._compile (internal/modules/cjs/loader.js:999:30)";
const result = sanitizeError(err);
expect(result).not.toContain("at Object.<anonymous>");
expect(result).not.toContain("at Module._compile");
expect(result).toContain("Error: something failed");
});
it("truncates to 500 characters", () => {
const err = "x".repeat(1000);
const result = sanitizeError(err);
expect(result.length).toBeLessThanOrEqual(500);
});
it("handles non-string errors", () => {
const result = sanitizeError({ code: 500, message: "server error" });
expect(result).toContain("object");
});
it("handles null/undefined", () => {
expect(sanitizeError(null)).toBe("null");
expect(sanitizeError(undefined)).toBe("undefined");
});
it("strips key: value patterns with colon separator", () => {
const err = "Error: key: abcdefghijklmnopqrstuvwxyz";
const result = sanitizeError(err);
expect(result).toContain("key=***");
});
it("strips password: value patterns with colon separator", () => {
const err = 'Config: password: "mysecret123"';
const result = sanitizeError(err);
expect(result).toContain("password=***");
});
});
// ============================================================================
// LoopDetector
// ============================================================================
describe("LoopDetector", () => {
let detector: LoopDetector;
beforeEach(() => {
detector = new LoopDetector();
});
describe("recordCall — identical call detection", () => {
it("allows first call", () => {
const result = detector.recordCall("read_file", { path: "/foo" });
expect(result.blocked).toBe(false);
});
it("allows calls below the identical limit", () => {
for (let i = 0; i < LoopDetector.IDENTICAL_CALL_LIMIT - 1; i++) {
const result = detector.recordCall("read_file", { path: "/foo" });
expect(result.blocked).toBe(false);
}
});
it("blocks after IDENTICAL_CALL_LIMIT identical calls", () => {
for (let i = 0; i < LoopDetector.IDENTICAL_CALL_LIMIT; i++) {
detector.recordCall("read_file", { path: "/foo" });
}
const result = detector.recordCall("read_file", { path: "/foo" });
expect(result.blocked).toBe(true);
expect(result.reason).toContain("identical call");
});
it("does not block calls with different inputs", () => {
for (let i = 0; i < 10; i++) {
const result = detector.recordCall("read_file", { path: `/file-${i}` });
expect(result.blocked).toBe(false);
}
});
});
describe("recordResult — consecutive error tracking", () => {
it("blocks after CONSECUTIVE_ERROR_LIMIT consecutive errors for same tool", () => {
for (let i = 0; i < LoopDetector.CONSECUTIVE_ERROR_LIMIT; i++) {
detector.recordCall("bash", { command: "fail" });
detector.recordResult("bash", false);
}
const result = detector.recordCall("bash", { command: "different" });
expect(result.blocked).toBe(true);
expect(result.reason).toContain("failed");
expect(result.reason).toContain("consecutively");
});
it("resets consecutive errors on success", () => {
// Fail twice
detector.recordCall("bash", { command: "fail" });
detector.recordResult("bash", false);
detector.recordCall("bash", { command: "fail2" });
detector.recordResult("bash", false);
// Succeed once — resets counter
detector.recordCall("bash", { command: "ok" });
detector.recordResult("bash", true);
// Should be able to fail again without being blocked
detector.recordCall("bash", { command: "fail3" });
detector.recordResult("bash", false);
const result = detector.recordCall("bash", { command: "new" });
expect(result.blocked).toBe(false);
});
});
describe("recordResult — turn error limit", () => {
it("blocks after TURN_ERROR_LIMIT errors in a single turn", () => {
for (let i = 0; i < LoopDetector.TURN_ERROR_LIMIT; i++) {
detector.recordCall(`tool${i}`, { arg: i });
detector.recordResult(`tool${i}`, false);
}
const result = detector.recordCall("another_tool", { arg: "x" });
expect(result.blocked).toBe(true);
expect(result.reason).toContain("errors this turn");
});
});
describe("recordResult — failed strategy tracking", () => {
it("blocks exact same call that previously failed", () => {
detector.recordCall("edit_file", { path: "/x", content: "y" });
detector.recordResult("edit_file", false, { path: "/x", content: "y" });
// Reset turn to simulate new turn
detector.resetTurn();
const result = detector.recordCall("edit_file", { path: "/x", content: "y" });
expect(result.blocked).toBe(true);
expect(result.reason).toContain("failed in a previous turn");
});
});
describe("session error tracking", () => {
it("blocks tool after SESSION_TOOL_ERROR_LIMIT failures", () => {
for (let i = 0; i < LoopDetector.SESSION_TOOL_ERROR_LIMIT; i++) {
detector.recordCall("bad_tool", { attempt: i });
detector.recordResult("bad_tool", false);
detector.resetTurn(); // reset turn errors to avoid turn limit
}
const result = detector.recordCall("bad_tool", { attempt: "final" });
expect(result.blocked).toBe(true);
expect(result.reason).toContain("failed");
expect(result.reason).toContain("times this session");
});
});
describe("endTurn — bail detection", () => {
it("does not bail after successful turns", () => {
for (let i = 0; i < 5; i++) {
detector.recordCall("tool", { i });
detector.recordResult("tool", true);
const bail = detector.endTurn();
expect(bail.shouldBail).toBe(false);
detector.resetTurn();
}
});
it("bails after CONSECUTIVE_FAILED_TURN_LIMIT failed turns", () => {
for (let i = 0; i < LoopDetector.CONSECUTIVE_FAILED_TURN_LIMIT; i++) {
detector.recordCall("tool", { i });
detector.recordResult("tool", false);
const bail = detector.endTurn();
if (i < LoopDetector.CONSECUTIVE_FAILED_TURN_LIMIT - 1) {
expect(bail.shouldBail).toBe(false);
} else {
expect(bail.shouldBail).toBe(true);
expect(bail.message).toContain("consecutive turns");
}
detector.resetTurn();
}
});
it("resets consecutive failed turns on a successful turn", () => {
// 2 failed turns
for (let i = 0; i < 2; i++) {
detector.recordCall("tool", { i });
detector.recordResult("tool", false);
detector.endTurn();
detector.resetTurn();
}
// 1 successful turn
detector.recordCall("tool", { ok: true });
detector.recordResult("tool", true);
detector.endTurn();
detector.resetTurn();
// More failed turns should not trigger bail yet
detector.recordCall("tool", { fail: true });
detector.recordResult("tool", false);
const bail = detector.endTurn();
expect(bail.shouldBail).toBe(false);
});
});
describe("reset", () => {
it("clears all state", () => {
// Build up state
for (let i = 0; i < 5; i++) {
detector.recordCall("tool", { i });
detector.recordResult("tool", false, { i });
}
detector.reset();
const stats = detector.getSessionStats();
expect(stats.totalErrors).toBe(0);
expect(stats.failedStrategies).toBe(0);
expect(stats.consecutiveFailedTurns).toBe(0);
});
});
describe("getSessionStats", () => {
it("tracks error counts", () => {
detector.recordCall("a", { x: 1 });
detector.recordResult("a", false, { x: 1 });
detector.recordCall("b", { y: 2 });
detector.recordResult("b", false, { y: 2 });
const stats = detector.getSessionStats();
expect(stats.totalErrors).toBe(2);
expect(stats.failedStrategies).toBe(2);
});
});
});
// ============================================================================
// isRetryableError
// ============================================================================
describe("isRetryableError", () => {
it("returns true for status 429 (rate limit)", () => {
expect(isRetryableError({ status: 429 })).toBe(true);
});
it("returns true for status 500 (server error)", () => {
expect(isRetryableError({ status: 500 })).toBe(true);
});
it("returns true for status 529 (overloaded)", () => {
expect(isRetryableError({ status: 529 })).toBe(true);
});
it("returns false for status 400 (bad request)", () => {
expect(isRetryableError({ status: 400 })).toBe(false);
});
it("returns false for status 401 (unauthorized)", () => {
expect(isRetryableError({ status: 401 })).toBe(false);
});
it("returns false for status 404 (not found)", () => {
expect(isRetryableError({ status: 404 })).toBe(false);
});
it("returns true for overloaded message", () => {
expect(isRetryableError({ message: "API is overloaded" })).toBe(true);
});
it("returns true for rate limit message", () => {
expect(isRetryableError({ message: "Rate limit exceeded" })).toBe(true);
});
it("returns true for timeout message", () => {
expect(isRetryableError({ message: "Request timeout" })).toBe(true);
});
it("returns false for generic error", () => {
expect(isRetryableError({ message: "something went wrong" })).toBe(false);
});
it("handles statusCode property", () => {
expect(isRetryableError({ statusCode: 429 })).toBe(true);
expect(isRetryableError({ statusCode: 400 })).toBe(false);
});
it("handles null/undefined", () => {
expect(isRetryableError(null)).toBe(false);
expect(isRetryableError(undefined)).toBe(false);
});
});
// ============================================================================
// getContextManagement
// ============================================================================
describe("getContextManagement", () => {
it("returns compact + clear betas for opus-4-6", () => {
const result = getContextManagement("claude-opus-4-6");
expect(result.betas).toContain("context-management-2025-06-27");
expect(result.betas).toContain("compact-2026-01-12");
expect(result.betas).toHaveLength(2);
});
it("includes compact edit at 120K for opus-4-6", () => {
const result = getContextManagement("claude-opus-4-6");
const compactEdit = result.config.edits.find((e) => e.type === "compact_20260112");
expect(compactEdit).toBeDefined();
expect(compactEdit!.trigger).toEqual({ type: "input_tokens", value: 120_000 });
});
it("includes clear_tool_uses edit at 80K for opus-4-6", () => {
const result = getContextManagement("claude-opus-4-6");
const clearEdit = result.config.edits.find((e) => e.type === "clear_tool_uses_20250919");
expect(clearEdit).toBeDefined();
expect(clearEdit!.trigger).toEqual({ type: "input_tokens", value: 80_000 });
expect(clearEdit!.keep).toEqual({ type: "tool_uses", value: 3 });
});
it("returns only clear beta for sonnet", () => {
const result = getContextManagement("claude-sonnet-4-20250514");
expect(result.betas).toEqual(["context-management-2025-06-27"]);
expect(result.betas).toHaveLength(1);
});
it("does not include compact edit for non-opus models", () => {
const result = getContextManagement("claude-sonnet-4-20250514");
const compactEdit = result.config.edits.find((e) => e.type === "compact_20260112");
expect(compactEdit).toBeUndefined();
});
it("includes clear_tool_uses for non-opus models", () => {
const result = getContextManagement("claude-haiku-4-5-20251001");
expect(result.config.edits).toHaveLength(1);
expect(result.config.edits[0].type).toBe("clear_tool_uses_20250919");
});
it("opus-4-6 has 2 edits (compact + clear)", () => {
const result = getContextManagement("claude-opus-4-6");
expect(result.config.edits).toHaveLength(2);
});
});
// ============================================================================
// getMaxOutputTokens
// ============================================================================
describe("getMaxOutputTokens", () => {
it("returns 16384 as default for any model", () => {
expect(getMaxOutputTokens("claude-opus-4-6")).toBe(16384);
expect(getMaxOutputTokens("claude-sonnet-4-20250514")).toBe(16384);
expect(getMaxOutputTokens("claude-haiku-4-5-20251001")).toBe(16384);
});
it("respects agentMax when lower than model max", () => {
expect(getMaxOutputTokens("claude-opus-4-6", 8192)).toBe(8192);
});
it("caps agentMax at model max", () => {
expect(getMaxOutputTokens("claude-opus-4-6", 32768)).toBe(16384);
});
it("returns model max when agentMax is 0 (falsy)", () => {
expect(getMaxOutputTokens("claude-opus-4-6", 0)).toBe(16384);
});
});
// ============================================================================
// MODEL_PRICING
// ============================================================================
describe("MODEL_PRICING", () => {
it("has pricing for all three expected models", () => {
expect(MODEL_PRICING).toHaveProperty("claude-sonnet-4-20250514");
expect(MODEL_PRICING).toHaveProperty("claude-opus-4-6");
expect(MODEL_PRICING).toHaveProperty("claude-haiku-4-5-20251001");
});
it("has entries for Anthropic, Bedrock, and Gemini models", () => {
expect(Object.keys(MODEL_PRICING).length).toBeGreaterThanOrEqual(3);
});
it("opus is more expensive than sonnet", () => {
expect(MODEL_PRICING["claude-opus-4-6"].inputPer1M).toBeGreaterThan(
MODEL_PRICING["claude-sonnet-4-20250514"].inputPer1M
);
expect(MODEL_PRICING["claude-opus-4-6"].outputPer1M).toBeGreaterThan(
MODEL_PRICING["claude-sonnet-4-20250514"].outputPer1M
);
});
it("sonnet is more expensive than haiku", () => {
expect(MODEL_PRICING["claude-sonnet-4-20250514"].inputPer1M).toBeGreaterThan(
MODEL_PRICING["claude-haiku-4-5-20251001"].inputPer1M
);
});
it("Anthropic models have thinking pricing", () => {
for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
if (key.startsWith("claude-") || key.startsWith("us.anthropic")) {
expect(pricing.thinkingPer1M).toBeDefined();
expect(pricing.thinkingPer1M!).toBeGreaterThan(0);
}
}
});
it("thinking rate matches output rate for Anthropic models", () => {
for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
if (key.startsWith("claude-") || key.startsWith("us.anthropic")) {
expect(pricing.thinkingPer1M).toBe(pricing.outputPer1M);
}
}
});
});
// ============================================================================
// addPromptCaching
// ============================================================================
describe("addPromptCaching", () => {
it("adds cache_control to last tool", () => {
const tools = [{ name: "tool1" }, { name: "tool2" }];
const messages = [{ role: "user", content: "hello" }];
const result = addPromptCaching(tools, messages);
expect(result.tools[0]).not.toHaveProperty("cache_control");
expect(result.tools[1]).toHaveProperty("cache_control", { type: "ephemeral" });
});
it("handles empty tools array", () => {
const result = addPromptCaching([], [{ role: "user", content: "hi" }]);
expect(result.tools).toHaveLength(0);
});
it("adds cache_control to second-to-last message (string content)", () => {
const messages = [
{ role: "user", content: "first" },
{ role: "assistant", content: "response" },
{ role: "user", content: "second" },
];
const result = addPromptCaching([], messages);
// Second-to-last message (index 1) should be converted to array with cache_control
const cached = result.messages[1] as any;
expect(Array.isArray(cached.content)).toBe(true);
expect(cached.content[0]).toEqual({
type: "text",
text: "response",
cache_control: { type: "ephemeral" },
});
});
it("adds cache_control to last block of second-to-last message (array content)", () => {
const messages = [
{ role: "user", content: [{ type: "text", text: "a" }, { type: "text", text: "b" }] },
{ role: "assistant", content: "response" },
];
const result = addPromptCaching([], messages);
const cached = result.messages[0] as any;
expect(cached.content[0]).not.toHaveProperty("cache_control");
expect(cached.content[1]).toHaveProperty("cache_control", { type: "ephemeral" });
});
it("does not modify messages with fewer than 2 entries", () => {
const messages = [{ role: "user", content: "only one" }];
const result = addPromptCaching([], messages);
expect(result.messages[0]).toEqual({ role: "user", content: "only one" });
});
it("does not mutate original arrays", () => {
const tools = [{ name: "t1" }];
const messages = [{ role: "user", content: "hi" }];
addPromptCaching(tools, messages);
expect(tools[0]).not.toHaveProperty("cache_control");
expect(messages[0]).toEqual({ role: "user", content: "hi" });
});
});
// ============================================================================
// getThinkingConfig
// ============================================================================
describe("getThinkingConfig", () => {
it("returns disabled when not enabled", () => {
const result = getThinkingConfig("claude-opus-4-6", false);
expect(result.thinking.type).toBe("disabled");
expect(result.beta).toBe("");
});
it("returns adaptive thinking for opus-4-6", () => {
const result = getThinkingConfig("claude-opus-4-6", true);
expect(result.thinking.type).toBe("adaptive");
expect(result.thinking.budget_tokens).toBeUndefined();
expect(result.beta).toBe("adaptive-thinking-2026-01-28");
});
it("returns fixed budget thinking for sonnet", () => {
const result = getThinkingConfig("claude-sonnet-4-20250514", true);
expect(result.thinking.type).toBe("enabled");
expect(result.thinking.budget_tokens).toBe(10_000);
expect(result.beta).toBe("interleaved-thinking-2025-05-14");
});
it("returns fixed budget thinking for haiku", () => {
const result = getThinkingConfig("claude-haiku-4-5-20251001", true);
expect(result.thinking.type).toBe("enabled");
expect(result.thinking.budget_tokens).toBe(10_000);
});
});