import { afterEach, beforeEach, describe, expect, test } from "vitest";import { ChatAnthropic } from "@langchain/anthropic";import { ChatGoogle } from "@langchain/google/node";import { ChatOpenAI } from "@langchain/openai";import { createModel } from "../src/agent/index.ts"; // Constructing a LangChain chat model makes no network calls (auth/clients// resolve lazily on first request), so these assert the gemini-enterprise// surface dispatch and the project-id guard without needing GCP credentials. const PROJECT_KEY = "GOOGLE_CLOUD_PROJECT";const LOCATION_KEY = "GOOGLE_CLOUD_LOCATION";const GEMINI_KEY = "GEMINI_API_KEY"; function modelName(model: unknown): string | undefined { return (model as { model?: string }).model;} describe("createModel gemini-enterprise surface dispatch", () => { let savedProject: string | undefined; let savedLocation: string | undefined; beforeEach(() => { savedProject = process.env[PROJECT_KEY]; savedLocation = process.env[LOCATION_KEY]; process.env[PROJECT_KEY] = "test-project"; process.env[LOCATION_KEY] = "us-central1"; }); afterEach(() => { restoreEnv(PROJECT_KEY, savedProject); restoreEnv(LOCATION_KEY, savedLocation); }); test("routes Claude IDs to ChatAnthropic and strips the publisher path", () => { const model = createModel( "gemini-enterprise", "publishers/anthropic/models/claude-sonnet-4-5@20250929", 0, ); expect(model).toBeInstanceOf(ChatAnthropic); expect(modelName(model)).toBe("claude-sonnet-4-5@20250929"); }); test("routes MaaS IDs to ChatOpenAI and normalizes to publisher/model", () => { const model = createModel( "gemini-enterprise", "publishers/meta/models/llama-3.3-70b-instruct-maas", 0, ); expect(model).toBeInstanceOf(ChatOpenAI); expect(model).not.toBeInstanceOf(ChatGoogle); expect(modelName(model)).toBe("meta/llama-3.3-70b-instruct-maas"); }); test("routes Gemini IDs to ChatGoogle", () => { const model = createModel("gemini-enterprise", "gemini-3.1-pro", 0); expect(model).toBeInstanceOf(ChatGoogle); expect(modelName(model)).toBe("gemini-3.1-pro"); }); test("routes Gemma IDs to ChatGoogle (default surface)", () => { const model = createModel("gemini-enterprise", "gemma-3-27b-it", 0); expect(model).toBeInstanceOf(ChatGoogle); expect(modelName(model)).toBe("gemma-3-27b-it"); }); test("strips the publisher path on the Gemini surface", () => { const model = createModel( "gemini-enterprise", "publishers/google/models/gemini-3-pro", 0, ); expect(model).toBeInstanceOf(ChatGoogle); expect(modelName(model)).toBe("gemini-3-pro"); }); test("resolves the global endpoint for the MaaS surface when location is unset", () => { delete process.env[LOCATION_KEY]; const model = createModel( "gemini-enterprise", "publishers/meta/models/llama-3.3-70b-instruct-maas", 0, ); // The global endpoint uses the unprefixed host (not global-aiplatform…). const baseURL = (model as { clientConfig?: { baseURL?: string } }) .clientConfig?.baseURL; expect(baseURL).toBe( "https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/endpoints/openapi", ); }); test("throws a clear error when the project ID is missing", () => { delete process.env[PROJECT_KEY]; expect(() => createModel("gemini-enterprise", "gemini-3.1-pro", 0)).toThrow( /GOOGLE_CLOUD_PROJECT is required/u, ); });}); describe("createModel gemini (AI Studio)", () => { let savedGeminiKey: string | undefined; beforeEach(() => { savedGeminiKey = process.env[GEMINI_KEY]; process.env[GEMINI_KEY] = "test-gemini-key"; }); afterEach(() => { restoreEnv(GEMINI_KEY, savedGeminiKey); }); test("builds a ChatGoogle AI Studio client with v0 output pinned", () => { const model = createModel("gemini", "gemini-3.1-pro", 0); expect(model).toBeInstanceOf(ChatGoogle); expect(modelName(model)).toBe("gemini-3.1-pro"); // Thought-signature workaround: streaming disabled + v0 output, on the AI // Studio ("gai") platform. (The API key is stored privately by ChatGoogle // and is asserted via the constructor mock in gemini-retry.test.ts.) const config = model as { _platform?: string; disableStreaming?: boolean; outputVersion?: string; }; expect(config.disableStreaming).toBe(true); expect(config.outputVersion).toBe("v0"); expect(config._platform).toBe("gai"); });}); describe("createModel OpenAI-compatible transport selection", () => { test("routes Copilot GPT-5 models through the Responses API", () => { const model = createModel("copilot", "gpt-5.5", 0) as { useResponsesApi?: boolean; }; expect(model.useResponsesApi).toBe(true); }); test("keeps non-GPT-5 Copilot models on chat completions", () => { const model = createModel("copilot", "claude-sonnet-5", 0) as { useResponsesApi?: boolean; }; expect(model.useResponsesApi).toBe(false); });}); function restoreEnv(key: string, value: string | undefined): void { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; }}
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatGoogle } from "@langchain/google/node";
import { ChatOpenAI } from "@langchain/openai";
import { createModel } from "../src/agent/index.ts";
// Constructing a LangChain chat model makes no network calls (auth/clients
// resolve lazily on first request), so these assert the gemini-enterprise
// surface dispatch and the project-id guard without needing GCP credentials.
const PROJECT_KEY = "GOOGLE_CLOUD_PROJECT";
const LOCATION_KEY = "GOOGLE_CLOUD_LOCATION";
const GEMINI_KEY = "GEMINI_API_KEY";
function modelName(model: unknown): string | undefined {
return (model as { model?: string }).model;
}
describe("createModel gemini-enterprise surface dispatch", () => {
let savedProject: string | undefined;
let savedLocation: string | undefined;
beforeEach(() => {
savedProject = process.env[PROJECT_KEY];
savedLocation = process.env[LOCATION_KEY];
process.env[PROJECT_KEY] = "test-project";
process.env[LOCATION_KEY] = "us-central1";
});
afterEach(() => {
restoreEnv(PROJECT_KEY, savedProject);
restoreEnv(LOCATION_KEY, savedLocation);
});
test("routes Claude IDs to ChatAnthropic and strips the publisher path", () => {
const model = createModel(
"gemini-enterprise",
"publishers/anthropic/models/claude-sonnet-4-5@20250929",
0,
);
expect(model).toBeInstanceOf(ChatAnthropic);
expect(modelName(model)).toBe("claude-sonnet-4-5@20250929");
});
test("routes MaaS IDs to ChatOpenAI and normalizes to publisher/model", () => {
const model = createModel(
"gemini-enterprise",
"publishers/meta/models/llama-3.3-70b-instruct-maas",
0,
);
expect(model).toBeInstanceOf(ChatOpenAI);
expect(model).not.toBeInstanceOf(ChatGoogle);
expect(modelName(model)).toBe("meta/llama-3.3-70b-instruct-maas");
});
test("routes Gemini IDs to ChatGoogle", () => {
const model = createModel("gemini-enterprise", "gemini-3.1-pro", 0);
expect(model).toBeInstanceOf(ChatGoogle);
expect(modelName(model)).toBe("gemini-3.1-pro");
});
test("routes Gemma IDs to ChatGoogle (default surface)", () => {
const model = createModel("gemini-enterprise", "gemma-3-27b-it", 0);
expect(model).toBeInstanceOf(ChatGoogle);
expect(modelName(model)).toBe("gemma-3-27b-it");
});
test("strips the publisher path on the Gemini surface", () => {
const model = createModel(
"gemini-enterprise",
"publishers/google/models/gemini-3-pro",
0,
);
expect(model).toBeInstanceOf(ChatGoogle);
expect(modelName(model)).toBe("gemini-3-pro");
});
test("resolves the global endpoint for the MaaS surface when location is unset", () => {
delete process.env[LOCATION_KEY];
const model = createModel(
"gemini-enterprise",
"publishers/meta/models/llama-3.3-70b-instruct-maas",
0,
);
// The global endpoint uses the unprefixed host (not global-aiplatform…).
const baseURL = (model as { clientConfig?: { baseURL?: string } })
.clientConfig?.baseURL;
expect(baseURL).toBe(
"https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/endpoints/openapi",
);
});
test("throws a clear error when the project ID is missing", () => {
delete process.env[PROJECT_KEY];
expect(() => createModel("gemini-enterprise", "gemini-3.1-pro", 0)).toThrow(
/GOOGLE_CLOUD_PROJECT is required/u,
);
});
});
describe("createModel gemini (AI Studio)", () => {
let savedGeminiKey: string | undefined;
beforeEach(() => {
savedGeminiKey = process.env[GEMINI_KEY];
process.env[GEMINI_KEY] = "test-gemini-key";
});
afterEach(() => {
restoreEnv(GEMINI_KEY, savedGeminiKey);
});
test("builds a ChatGoogle AI Studio client with v0 output pinned", () => {
const model = createModel("gemini", "gemini-3.1-pro", 0);
expect(model).toBeInstanceOf(ChatGoogle);
expect(modelName(model)).toBe("gemini-3.1-pro");
// Thought-signature workaround: streaming disabled + v0 output, on the AI
// Studio ("gai") platform. (The API key is stored privately by ChatGoogle
// and is asserted via the constructor mock in gemini-retry.test.ts.)
const config = model as {
_platform?: string;
disableStreaming?: boolean;
outputVersion?: string;
};
expect(config.disableStreaming).toBe(true);
expect(config.outputVersion).toBe("v0");
expect(config._platform).toBe("gai");
});
});
describe("createModel OpenAI-compatible transport selection", () => {
test("routes Copilot GPT-5 models through the Responses API", () => {
const model = createModel("copilot", "gpt-5.5", 0) as {
useResponsesApi?: boolean;
};
expect(model.useResponsesApi).toBe(true);
});
test("keeps non-GPT-5 Copilot models on chat completions", () => {
const model = createModel("copilot", "claude-sonnet-5", 0) as {
useResponsesApi?: boolean;
};
expect(model.useResponsesApi).toBe(false);
});
});
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}