| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- import { beforeEach, describe, expect, test, vi } from "vitest";
- import type { Provider } from "@/types/provider";
- const circuitBreakerMocks = vi.hoisted(() => ({
- isCircuitOpen: vi.fn(async () => false),
- getCircuitState: vi.fn(() => "closed"),
- }));
- vi.mock("@/lib/circuit-breaker", () => circuitBreakerMocks);
- describe("ProxyProviderResolver.pickRandomProvider - format/providerType compatibility", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
- function createSessionStub(originalFormat: string, providers: Provider[], originalModel: string) {
- return {
- originalFormat,
- authState: null,
- getProvidersSnapshot: async () => providers,
- getOriginalModel: () => originalModel,
- getCurrentModel: () => originalModel,
- clientRequestsContext1m: () => false,
- } as any;
- }
- function createProvider(
- id: number,
- providerType: string,
- overrides: Partial<Provider> = {}
- ): Provider {
- return {
- id,
- name: `provider-${id}`,
- isEnabled: true,
- providerType,
- groupTag: null,
- weight: 1,
- priority: 0,
- costMultiplier: 1,
- allowedModels: null,
- ...overrides,
- } as unknown as Provider;
- }
- async function setupResolverMocks() {
- const { ProxyProviderResolver } = await import("@/app/v1/_lib/proxy/provider-selector");
- vi.spyOn(ProxyProviderResolver as any, "filterByLimits").mockImplementation(
- async (...args: unknown[]) => args[0] as Provider[]
- );
- vi.spyOn(ProxyProviderResolver as any, "selectTopPriority").mockImplementation(
- (...args: unknown[]) => args[0] as Provider[]
- );
- vi.spyOn(ProxyProviderResolver as any, "selectOptimal").mockImplementation(
- (...args: unknown[]) => (args[0] as Provider[])[0] ?? null
- );
- return ProxyProviderResolver;
- }
- test("openai format rejects claude provider, selects openai-compatible", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const incompatible = createProvider(1, "claude");
- const compatible = createProvider(2, "openai-compatible");
- const session = createSessionStub("openai", [incompatible, compatible], "gpt-4o");
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(2);
- expect(provider?.providerType).toBe("openai-compatible");
- const mismatch = context.filteredProviders.find(
- (fp: any) => fp.id === 1 && fp.reason === "format_type_mismatch"
- );
- expect(mismatch).toBeDefined();
- expect(mismatch.details).toContain("openai");
- expect(mismatch.details).toContain("claude");
- });
- test("openai format rejects codex provider, selects openai-compatible", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const incompatible = createProvider(1, "codex");
- const compatible = createProvider(2, "openai-compatible");
- const session = createSessionStub("openai", [incompatible, compatible], "gpt-4o");
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(2);
- expect(provider?.providerType).toBe("openai-compatible");
- const mismatch = context.filteredProviders.find(
- (fp: any) => fp.id === 1 && fp.reason === "format_type_mismatch"
- );
- expect(mismatch).toBeDefined();
- });
- test("response format rejects openai-compatible provider, selects codex", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const incompatible = createProvider(1, "openai-compatible");
- const compatible = createProvider(2, "codex");
- const session = createSessionStub("response", [incompatible, compatible], "codex-mini-latest");
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(2);
- expect(provider?.providerType).toBe("codex");
- const mismatch = context.filteredProviders.find(
- (fp: any) => fp.id === 1 && fp.reason === "format_type_mismatch"
- );
- expect(mismatch).toBeDefined();
- expect(mismatch.details).toContain("response");
- expect(mismatch.details).toContain("openai-compatible");
- });
- test("response format rejects claude provider, selects codex", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const incompatible = createProvider(1, "claude");
- const compatible = createProvider(2, "codex");
- const session = createSessionStub("response", [incompatible, compatible], "codex-mini-latest");
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(2);
- expect(provider?.providerType).toBe("codex");
- const mismatch = context.filteredProviders.find(
- (fp: any) => fp.id === 1 && fp.reason === "format_type_mismatch"
- );
- expect(mismatch).toBeDefined();
- });
- test("claude format rejects openai-compatible provider, selects claude", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const incompatible = createProvider(1, "openai-compatible");
- const compatible = createProvider(2, "claude");
- const session = createSessionStub(
- "claude",
- [incompatible, compatible],
- "claude-sonnet-4-20250514"
- );
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(2);
- expect(provider?.providerType).toBe("claude");
- const mismatch = context.filteredProviders.find(
- (fp: any) => fp.id === 1 && fp.reason === "format_type_mismatch"
- );
- expect(mismatch).toBeDefined();
- expect(mismatch.details).toContain("claude");
- expect(mismatch.details).toContain("openai-compatible");
- });
- test("claude format accepts claude-auth provider", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const incompatible = createProvider(1, "codex");
- const compatible = createProvider(2, "claude-auth");
- const session = createSessionStub(
- "claude",
- [incompatible, compatible],
- "claude-sonnet-4-20250514"
- );
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(2);
- expect(provider?.providerType).toBe("claude-auth");
- const mismatch = context.filteredProviders.find(
- (fp: any) => fp.id === 1 && fp.reason === "format_type_mismatch"
- );
- expect(mismatch).toBeDefined();
- });
- test("gemini format rejects claude provider, selects gemini", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const incompatible = createProvider(1, "claude");
- const compatible = createProvider(2, "gemini");
- const session = createSessionStub("gemini", [incompatible, compatible], "gemini-2.0-flash");
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(2);
- expect(provider?.providerType).toBe("gemini");
- const mismatch = context.filteredProviders.find(
- (fp: any) => fp.id === 1 && fp.reason === "format_type_mismatch"
- );
- expect(mismatch).toBeDefined();
- expect(mismatch.details).toContain("gemini");
- });
- test("gemini-cli format rejects gemini provider, selects gemini-cli", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const incompatible = createProvider(1, "gemini");
- const compatible = createProvider(2, "gemini-cli");
- const session = createSessionStub("gemini-cli", [incompatible, compatible], "gemini-2.0-flash");
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(2);
- expect(provider?.providerType).toBe("gemini-cli");
- const mismatch = context.filteredProviders.find(
- (fp: any) => fp.id === 1 && fp.reason === "format_type_mismatch"
- );
- expect(mismatch).toBeDefined();
- expect(mismatch.details).toContain("gemini-cli");
- expect(mismatch.details).toContain("gemini");
- });
- test("returns null when no compatible providers exist for response format", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const p1 = createProvider(1, "claude");
- const p2 = createProvider(2, "openai-compatible");
- const session = createSessionStub("response", [p1, p2], "codex-mini-latest");
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider).toBeNull();
- const mismatches = context.filteredProviders.filter(
- (fp: any) => fp.reason === "format_type_mismatch"
- );
- expect(mismatches.length).toBe(2);
- });
- test("multiple incompatible providers are all recorded in filteredProviders", async () => {
- const ProxyProviderResolver = await setupResolverMocks();
- const p1 = createProvider(1, "claude");
- const p2 = createProvider(2, "codex");
- const p3 = createProvider(3, "gemini");
- const compatible = createProvider(4, "openai-compatible");
- const session = createSessionStub("openai", [p1, p2, p3, compatible], "gpt-4o");
- const { provider, context } = await (ProxyProviderResolver as any).pickRandomProvider(
- session,
- []
- );
- expect(provider?.id).toBe(4);
- const mismatches = context.filteredProviders.filter(
- (fp: any) => fp.reason === "format_type_mismatch"
- );
- expect(mismatches.length).toBe(3);
- expect(mismatches.map((m: any) => m.id).sort()).toEqual([1, 2, 3]);
- });
- });
|