230 lines
8.9 KiB
TypeScript
230 lines
8.9 KiB
TypeScript
import { describe, expect } from "bun:test"
|
|
import { Effect } from "effect"
|
|
import { HttpClientRequest } from "effect/unstable/http"
|
|
import { LLM } from "../../src"
|
|
import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers"
|
|
import { LLMClient } from "../../src/route"
|
|
import { compileRequest } from "../../src/route/client"
|
|
import { it } from "../lib/effect"
|
|
import { dynamicResponse } from "../lib/http"
|
|
import { deltaChunk, finishChunk } from "../lib/openai-chunks"
|
|
import { sseEvents } from "../lib/sse"
|
|
|
|
describe("Google Vertex providers", () => {
|
|
it.effect("sends Gemini requests to the global Vertex endpoint", () =>
|
|
Effect.gen(function* () {
|
|
const response = yield* LLMClient.generate(
|
|
LLM.request({
|
|
model: GoogleVertex.configure({
|
|
accessToken: "vertex-token",
|
|
location: "global",
|
|
project: "vertex-project",
|
|
}).model("gemini-3.5-flash"),
|
|
prompt: "Say hello.",
|
|
}),
|
|
).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
expect(request.url).toBe(
|
|
"https://aiplatform.googleapis.com/v1beta1/projects/vertex-project/locations/global/publishers/google/models/gemini-3.5-flash:streamGenerateContent?alt=sse",
|
|
)
|
|
expect(request.headers.get("authorization")).toBe("Bearer vertex-token")
|
|
expect(yield* Effect.promise(() => request.json())).toMatchObject({
|
|
contents: [{ role: "user", parts: [{ text: "Say hello." }] }],
|
|
})
|
|
return input.respond(
|
|
sseEvents({
|
|
candidates: [
|
|
{
|
|
content: { role: "model", parts: [{ text: "Hello." }] },
|
|
finishReason: "STOP",
|
|
},
|
|
],
|
|
}),
|
|
{ headers: { "content-type": "text/event-stream" } },
|
|
)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
expect(response.text).toBe("Hello.")
|
|
}),
|
|
)
|
|
|
|
it.effect("projects Anthropic Messages onto the Vertex raw-predict API", () =>
|
|
Effect.gen(function* () {
|
|
const response = yield* LLMClient.generate(
|
|
LLM.request({
|
|
model: GoogleVertexMessages.configure({
|
|
accessToken: "vertex-token",
|
|
location: "eu",
|
|
project: "vertex-project",
|
|
}).model("claude-sonnet-4-6"),
|
|
prompt: "Say hello.",
|
|
}),
|
|
).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
expect(request.url).toBe(
|
|
"https://aiplatform.eu.rep.googleapis.com/v1/projects/vertex-project/locations/eu/publishers/anthropic/models/claude-sonnet-4-6:streamRawPredict",
|
|
)
|
|
expect(request.headers.get("authorization")).toBe("Bearer vertex-token")
|
|
expect(request.headers.get("anthropic-version")).toBeNull()
|
|
const body = yield* Effect.promise(() => request.json())
|
|
expect(body).toMatchObject({
|
|
anthropic_version: "vertex-2023-10-16",
|
|
messages: [{ role: "user", content: [{ type: "text", text: "Say hello." }] }],
|
|
stream: true,
|
|
})
|
|
expect(body).not.toHaveProperty("model")
|
|
return input.respond(
|
|
sseEvents(
|
|
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
|
|
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hello." } },
|
|
{ type: "content_block_stop", index: 0 },
|
|
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } },
|
|
{ type: "message_stop" },
|
|
),
|
|
{ headers: { "content-type": "text/event-stream" } },
|
|
)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
expect(response.text).toBe("Hello.")
|
|
}),
|
|
)
|
|
|
|
it.effect("sends MaaS requests through Vertex Chat Completions", () =>
|
|
Effect.gen(function* () {
|
|
const response = yield* LLMClient.generate(
|
|
LLM.request({
|
|
model: GoogleVertexChat.configure({
|
|
accessToken: "vertex-token",
|
|
location: "global",
|
|
project: "vertex-project",
|
|
}).model("deepseek-ai/deepseek-v3.2-maas"),
|
|
prompt: "Say hello.",
|
|
}),
|
|
).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
expect(request.url).toBe(
|
|
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi/chat/completions",
|
|
)
|
|
expect(request.headers.get("authorization")).toBe("Bearer vertex-token")
|
|
expect(yield* Effect.promise(() => request.json())).toMatchObject({
|
|
model: "deepseek-ai/deepseek-v3.2-maas",
|
|
messages: [{ role: "user", content: "Say hello." }],
|
|
stream: true,
|
|
stream_options: { include_usage: true },
|
|
})
|
|
return input.respond(sseEvents(deltaChunk({ content: "Hello." }), finishChunk("stop")), {
|
|
headers: { "content-type": "text/event-stream" },
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
expect(response.text).toBe("Hello.")
|
|
}),
|
|
)
|
|
|
|
it.effect("sends Grok requests through Vertex Responses", () =>
|
|
Effect.gen(function* () {
|
|
const response = yield* LLMClient.generate(
|
|
LLM.request({
|
|
model: GoogleVertexResponses.configure({
|
|
accessToken: "vertex-token",
|
|
location: "global",
|
|
project: "vertex-project",
|
|
}).model("xai/grok-4.20-reasoning"),
|
|
prompt: "Say hello.",
|
|
}),
|
|
).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
expect(request.url).toBe(
|
|
"https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi/responses",
|
|
)
|
|
expect(request.headers.get("authorization")).toBe("Bearer vertex-token")
|
|
expect(yield* Effect.promise(() => request.json())).toMatchObject({
|
|
model: "xai/grok-4.20-reasoning",
|
|
input: [{ role: "user", content: [{ type: "input_text", text: "Say hello." }] }],
|
|
store: false,
|
|
stream: true,
|
|
})
|
|
return input.respond(
|
|
sseEvents(
|
|
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Hello." },
|
|
{ type: "response.completed", response: { id: "resp_1" } },
|
|
),
|
|
{ headers: { "content-type": "text/event-stream" } },
|
|
)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
expect(response.text).toBe("Hello.")
|
|
}),
|
|
)
|
|
|
|
it.effect("routes tuned Gemini models through their deployed endpoint", () =>
|
|
Effect.gen(function* () {
|
|
const response = yield* LLMClient.generate(
|
|
LLM.request({
|
|
model: GoogleVertex.configure({
|
|
accessToken: "vertex-token",
|
|
location: "us-central1",
|
|
project: "vertex-project",
|
|
}).model("endpoints/1234567890"),
|
|
prompt: "Say hello.",
|
|
}),
|
|
).pipe(
|
|
Effect.provide(
|
|
dynamicResponse((input) =>
|
|
Effect.gen(function* () {
|
|
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
|
expect(request.url).toBe(
|
|
"https://us-central1-aiplatform.googleapis.com/v1beta1/projects/vertex-project/locations/us-central1/endpoints/1234567890:streamGenerateContent?alt=sse",
|
|
)
|
|
return input.respond(
|
|
sseEvents({
|
|
candidates: [
|
|
{
|
|
content: { role: "model", parts: [{ text: "Hello." }] },
|
|
finishReason: "STOP",
|
|
},
|
|
],
|
|
}),
|
|
{ headers: { "content-type": "text/event-stream" } },
|
|
)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
expect(response.text).toBe("Hello.")
|
|
}),
|
|
)
|
|
|
|
it.effect("rejects tuned Gemini models in express mode", () =>
|
|
Effect.sync(() => {
|
|
expect(() => GoogleVertex.configure({ apiKey: "fixture" }).model("endpoints/1234567890")).toThrow(
|
|
"Google Vertex tuned models do not support Express Mode API keys",
|
|
)
|
|
}),
|
|
)
|
|
})
|