diff --git a/src/utils/model/__tests__/aliases.test.ts b/src/utils/model/__tests__/aliases.test.ts new file mode 100644 index 0000000..b15a78a --- /dev/null +++ b/src/utils/model/__tests__/aliases.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, test } from "bun:test"; +import { isModelAlias, isModelFamilyAlias } from "../aliases"; + +describe("isModelAlias", () => { + test('returns true for "sonnet"', () => { + expect(isModelAlias("sonnet")).toBe(true); + }); + + test('returns true for "opus"', () => { + expect(isModelAlias("opus")).toBe(true); + }); + + test('returns true for "haiku"', () => { + expect(isModelAlias("haiku")).toBe(true); + }); + + test('returns true for "best"', () => { + expect(isModelAlias("best")).toBe(true); + }); + + test('returns true for "sonnet[1m]"', () => { + expect(isModelAlias("sonnet[1m]")).toBe(true); + }); + + test('returns true for "opus[1m]"', () => { + expect(isModelAlias("opus[1m]")).toBe(true); + }); + + test('returns true for "opusplan"', () => { + expect(isModelAlias("opusplan")).toBe(true); + }); + + test("returns false for full model ID", () => { + expect(isModelAlias("claude-sonnet-4-6-20250514")).toBe(false); + }); + + test("returns false for unknown string", () => { + expect(isModelAlias("gpt-4")).toBe(false); + }); + + test("is case-sensitive", () => { + expect(isModelAlias("Sonnet")).toBe(false); + }); +}); + +describe("isModelFamilyAlias", () => { + test('returns true for "sonnet"', () => { + expect(isModelFamilyAlias("sonnet")).toBe(true); + }); + + test('returns true for "opus"', () => { + expect(isModelFamilyAlias("opus")).toBe(true); + }); + + test('returns true for "haiku"', () => { + expect(isModelFamilyAlias("haiku")).toBe(true); + }); + + test('returns false for "best"', () => { + expect(isModelFamilyAlias("best")).toBe(false); + }); + + test('returns false for "opusplan"', () => { + expect(isModelFamilyAlias("opusplan")).toBe(false); + }); + + test('returns false for "sonnet[1m]"', () => { + expect(isModelFamilyAlias("sonnet[1m]")).toBe(false); + }); +}); diff --git a/src/utils/model/__tests__/model.test.ts b/src/utils/model/__tests__/model.test.ts new file mode 100644 index 0000000..8fd036c --- /dev/null +++ b/src/utils/model/__tests__/model.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, test } from "bun:test"; +import { firstPartyNameToCanonical } from "../model"; + +describe("firstPartyNameToCanonical", () => { + test("maps opus-4-6 full name to canonical", () => { + expect(firstPartyNameToCanonical("claude-opus-4-6-20250514")).toBe( + "claude-opus-4-6" + ); + }); + + test("maps sonnet-4-6 full name", () => { + expect(firstPartyNameToCanonical("claude-sonnet-4-6-20250514")).toBe( + "claude-sonnet-4-6" + ); + }); + + test("maps haiku-4-5", () => { + expect(firstPartyNameToCanonical("claude-haiku-4-5-20251001")).toBe( + "claude-haiku-4-5" + ); + }); + + test("maps 3P provider format", () => { + expect( + firstPartyNameToCanonical("us.anthropic.claude-opus-4-6-v1:0") + ).toBe("claude-opus-4-6"); + }); + + test("maps claude-3-7-sonnet", () => { + expect(firstPartyNameToCanonical("claude-3-7-sonnet-20250219")).toBe( + "claude-3-7-sonnet" + ); + }); + + test("maps claude-3-5-sonnet", () => { + expect(firstPartyNameToCanonical("claude-3-5-sonnet-20241022")).toBe( + "claude-3-5-sonnet" + ); + }); + + test("maps claude-3-5-haiku", () => { + expect(firstPartyNameToCanonical("claude-3-5-haiku-20241022")).toBe( + "claude-3-5-haiku" + ); + }); + + test("maps claude-3-opus", () => { + expect(firstPartyNameToCanonical("claude-3-opus-20240229")).toBe( + "claude-3-opus" + ); + }); + + test("is case insensitive", () => { + expect(firstPartyNameToCanonical("Claude-Opus-4-6-20250514")).toBe( + "claude-opus-4-6" + ); + }); + + test("falls back to input for unknown model", () => { + expect(firstPartyNameToCanonical("unknown-model")).toBe("unknown-model"); + }); + + test("differentiates opus-4 vs opus-4-5 vs opus-4-6", () => { + expect(firstPartyNameToCanonical("claude-opus-4-20240101")).toBe( + "claude-opus-4" + ); + expect(firstPartyNameToCanonical("claude-opus-4-5-20240101")).toBe( + "claude-opus-4-5" + ); + expect(firstPartyNameToCanonical("claude-opus-4-6-20240101")).toBe( + "claude-opus-4-6" + ); + }); + + test("maps opus-4-1", () => { + expect(firstPartyNameToCanonical("claude-opus-4-1-20240101")).toBe( + "claude-opus-4-1" + ); + }); + + test("maps sonnet-4-5", () => { + expect(firstPartyNameToCanonical("claude-sonnet-4-5-20240101")).toBe( + "claude-sonnet-4-5" + ); + }); + + test("maps sonnet-4", () => { + expect(firstPartyNameToCanonical("claude-sonnet-4-20240101")).toBe( + "claude-sonnet-4" + ); + }); +}); diff --git a/src/utils/model/__tests__/providers.test.ts b/src/utils/model/__tests__/providers.test.ts new file mode 100644 index 0000000..ac009dc --- /dev/null +++ b/src/utils/model/__tests__/providers.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, test, beforeEach, afterEach } from "bun:test"; +import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from "../providers"; + +describe("getAPIProvider", () => { + const originalEnv = { ...process.env }; + + afterEach(() => { + delete process.env.CLAUDE_CODE_USE_BEDROCK; + delete process.env.CLAUDE_CODE_USE_VERTEX; + delete process.env.CLAUDE_CODE_USE_FOUNDRY; + }); + + test('returns "firstParty" by default', () => { + delete process.env.CLAUDE_CODE_USE_BEDROCK; + delete process.env.CLAUDE_CODE_USE_VERTEX; + delete process.env.CLAUDE_CODE_USE_FOUNDRY; + expect(getAPIProvider()).toBe("firstParty"); + }); + + test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set', () => { + process.env.CLAUDE_CODE_USE_BEDROCK = "1"; + expect(getAPIProvider()).toBe("bedrock"); + }); + + test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set', () => { + process.env.CLAUDE_CODE_USE_VERTEX = "1"; + expect(getAPIProvider()).toBe("vertex"); + }); + + test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set', () => { + process.env.CLAUDE_CODE_USE_FOUNDRY = "1"; + expect(getAPIProvider()).toBe("foundry"); + }); + + test("bedrock takes precedence over vertex", () => { + process.env.CLAUDE_CODE_USE_BEDROCK = "1"; + process.env.CLAUDE_CODE_USE_VERTEX = "1"; + expect(getAPIProvider()).toBe("bedrock"); + }); +}); + +describe("isFirstPartyAnthropicBaseUrl", () => { + const originalBaseUrl = process.env.ANTHROPIC_BASE_URL; + const originalUserType = process.env.USER_TYPE; + + afterEach(() => { + if (originalBaseUrl !== undefined) { + process.env.ANTHROPIC_BASE_URL = originalBaseUrl; + } else { + delete process.env.ANTHROPIC_BASE_URL; + } + if (originalUserType !== undefined) { + process.env.USER_TYPE = originalUserType; + } else { + delete process.env.USER_TYPE; + } + }); + + test("returns true when ANTHROPIC_BASE_URL is not set", () => { + delete process.env.ANTHROPIC_BASE_URL; + expect(isFirstPartyAnthropicBaseUrl()).toBe(true); + }); + + test("returns true for api.anthropic.com", () => { + process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com"; + expect(isFirstPartyAnthropicBaseUrl()).toBe(true); + }); + + test("returns false for custom URL", () => { + process.env.ANTHROPIC_BASE_URL = "https://my-proxy.com"; + expect(isFirstPartyAnthropicBaseUrl()).toBe(false); + }); + + test("returns false for invalid URL", () => { + process.env.ANTHROPIC_BASE_URL = "not-a-url"; + expect(isFirstPartyAnthropicBaseUrl()).toBe(false); + }); + + test("returns true for staging URL when USER_TYPE is ant", () => { + process.env.ANTHROPIC_BASE_URL = "https://api-staging.anthropic.com"; + process.env.USER_TYPE = "ant"; + expect(isFirstPartyAnthropicBaseUrl()).toBe(true); + }); +});