SysDeck/klanker-gate/tests/contract/advanced_apis_test.ts

476 lines
16 KiB
TypeScript
Executable File

// Advanced API families through the real handler: embeddings, images,
// speech, transcription, files, batches — with capability enforcement.
import { assert, assertEquals } from "@std/assert";
import { createHandler } from "../../apps/gateway/main.ts";
import {
type AppContext,
NullToolExecutor,
VERSION,
} from "../../apps/gateway/context.ts";
import { ProviderManager } from "../../packages/providers/src/mod.ts";
import { Metrics } from "../../packages/telemetry/src/metrics.ts";
import { LogBus } from "../../packages/telemetry/src/logbus.ts";
import { VirtualKeyManager } from "../../packages/governance/src/virtual_keys.ts";
import { MCPRegistry } from "../../packages/mcp/src/registry.ts";
import { PluginManager } from "../../packages/plugins/src/lifecycle.ts";
import { jsonResponse, MockProvider } from "../../packages/testing/src/mod.ts";
import {
ImageGenerationRequestSchema,
MAX_TTS_INPUT_CHARS,
} from "../../packages/contracts/src/mod.ts";
const base = "http://gateway.test";
// A real RSA private key so VertexAdapter.getToken() can sign the SA JWT
// offline; token_uri points at the in-process mock's /token route.
async function serviceAccountJson(tokenUri: string): Promise<string> {
const kp = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["sign", "verify"],
);
const pkcs8 = new Uint8Array(
await crypto.subtle.exportKey("pkcs8", kp.privateKey),
);
let binary = "";
for (const b of pkcs8) {
binary += String.fromCharCode(b);
}
const b64 = btoa(binary).match(/.{1,64}/g)!.join("\n");
const pem =
`-----BEGIN PRIVATE KEY-----\n${b64}\n-----END PRIVATE KEY-----\n`;
return JSON.stringify({
client_email: "svc@test.iam.gserviceaccount.com",
private_key: pem,
token_uri: tokenUri,
});
}
function makeContext(mockUrl: string, vertexSaJson: string): AppContext {
return {
providers: new ProviderManager([
{
id: "openai",
type: "openai",
apiKey: "sk-adv",
baseUrl: mockUrl,
enabled: true,
models: ["gpt-4o"],
priority: 0,
retry: { maxRetries: 0 },
},
{
id: "anthropic",
type: "anthropic",
apiKey: "sk-ant",
baseUrl: mockUrl,
enabled: true,
models: ["claude-x"],
priority: 0,
retry: { maxRetries: 0 },
},
{
// Gemini Imagen via the native generativelanguage :predict surface.
id: "gemini",
type: "gemini",
apiKey: "g-key",
baseUrl: `${mockUrl}/v1beta/openai`,
enabled: true,
models: ["imagen-3.0-generate-002"],
priority: 0,
retry: { maxRetries: 0 },
},
{
// Vertex Imagen via the publishers/google :predict surface (OAuth).
id: "vertex",
type: "vertex",
baseUrl: mockUrl,
projectId: "proj",
location: "us-central1",
serviceAccountJson: vertexSaJson,
enabled: true,
models: ["imagen-3.0-generate-002"],
priority: 0,
retry: { maxRetries: 0 },
},
], "openai"),
metrics: new Metrics(),
logBus: new LogBus(),
virtualKeys: new VirtualKeyManager(),
mcp: new MCPRegistry(),
plugins: new PluginManager(),
toolExecutor: new NullToolExecutor(),
version: VERSION,
};
}
Deno.test("advanced APIs: embeddings, images, speech, files, batches", async (t) => {
const mock = new MockProvider((call) => {
// Vertex OAuth token exchange (SA JWT -> access token).
if (call.path === "/token") {
return jsonResponse({ access_token: "vertex-token", expires_in: 3600 });
}
// Google Imagen :predict, shared by the gemini (native generativelanguage)
// and vertex (publishers/google) surfaces; both return base64 image bytes.
if (call.path.endsWith(":predict")) {
return jsonResponse({
predictions: [
{ bytesBase64Encoded: "aW1hZ2VieXRlcw==", mimeType: "image/png" },
],
});
}
switch (call.path) {
case "/embeddings":
return jsonResponse({
object: "list",
data: [{ object: "embedding", index: 0, embedding: [0.1, 0.2] }],
model: "text-embedding-3-small",
usage: { prompt_tokens: 2, total_tokens: 2 },
});
case "/images/generations":
return jsonResponse({
created: 1,
data: [{ url: "https://img.example/1.png" }],
});
case "/audio/speech":
return new Response(new Uint8Array([1, 2, 3]), {
headers: { "Content-Type": "audio/mpeg" },
});
case "/audio/transcriptions":
return jsonResponse({ text: "transcribed words" });
case "/files":
return jsonResponse({
id: "file-1",
object: "file",
bytes: 10,
created_at: 1,
filename: "data.jsonl",
purpose: "batch",
});
case "/batches":
return jsonResponse({
id: "batch-1",
object: "batch",
input_file_id: "file-1",
endpoint: "/v1/chat/completions",
status: "validating",
});
default:
return jsonResponse({ error: `unexpected path ${call.path}` }, 500);
}
});
const handler = createHandler(
makeContext(mock.url, await serviceAccountJson(`${mock.url}/token`)),
);
try {
await t.step("embeddings dispatch with model prefix stripped", async () => {
const res = await handler(
new Request(`${base}/v1/embeddings`, {
method: "POST",
body: JSON.stringify({
model: "openai/text-embedding-3-small",
input: "hello",
}),
}),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.data[0].embedding, [0.1, 0.2]);
const sent = mock.calls.at(-1)!;
assertEquals(sent.path, "/embeddings");
assertEquals(
(sent.body as { model: string }).model,
"text-embedding-3-small",
);
assertEquals(sent.headers.get("Authorization"), "Bearer sk-adv");
});
await t.step(
"embeddings rejected for providers without support",
async () => {
const res = await handler(
new Request(`${base}/v1/embeddings`, {
method: "POST",
body: JSON.stringify({ model: "anthropic/claude-x", input: "x" }),
}),
);
assertEquals(res.status, 400);
assert(
(await res.json()).error.message.includes("does not support"),
);
},
);
await t.step("image generation (OpenAI rawProxy passthrough)", async () => {
const res = await handler(
new Request(`${base}/v1/images/generations`, {
method: "POST",
body: JSON.stringify({
model: "openai/gpt-image-1",
prompt: "a frosty fjord",
// The five vendor fields the request schema stripped before it
// became .passthrough(). Asserted at the WIRE below, not at the
// schema: a parse-level assertion passes while the widening is
// still being dropped somewhere between parse and egress.
quality: "high",
style: "vivid",
background: "transparent",
output_format: "webp",
moderation: "low",
}),
}),
);
assertEquals(res.status, 200);
assertEquals(
(await res.json()).data[0].url,
"https://img.example/1.png",
);
// Byte-identical passthrough: forwarded to the OpenAI /images surface.
const sentImage = mock.calls.at(-1)!;
assertEquals(sentImage.path, "/images/generations");
assertEquals(sentImage.body, {
model: "gpt-image-1",
prompt: "a frosty fjord",
quality: "high",
style: "vivid",
background: "transparent",
output_format: "webp",
moderation: "low",
});
});
// D1-T28, envelope half: the bound has to surface as the canonical 400
// naming the offending field, and it has to refuse BEFORE the provider is
// reached - an over-n request that still dispatches has bought the memory
// the bound exists to deny.
await t.step("D1-T28: over-bound n and sampleCount are 400", async () => {
for (const field of ["n", "sampleCount"]) {
const before = mock.calls.length;
const res = await handler(
new Request(`${base}/v1/images/generations`, {
method: "POST",
body: JSON.stringify({
model: "openai/gpt-image-1",
prompt: "a frosty fjord",
[field]: 50,
}),
}),
);
assertEquals(res.status, 400);
const body = await res.json();
assertEquals(body.error.type, "invalid_request_error");
assertEquals(body.error.param, field);
assert(body.error.message.includes(field));
assertEquals(mock.calls.length, before);
}
});
await t.step(
"D1-T28: n = 10 is admitted, n = 0 / -1 / 2.5 are not",
async () => {
const ok = await handler(
new Request(`${base}/v1/images/generations`, {
method: "POST",
body: JSON.stringify({
model: "openai/gpt-image-1",
prompt: "a frosty fjord",
n: 10,
}),
}),
);
assertEquals(ok.status, 200);
await ok.body?.cancel();
for (const value of [0, -1, 2.5]) {
const res = await handler(
new Request(`${base}/v1/images/generations`, {
method: "POST",
body: JSON.stringify({
model: "openai/gpt-image-1",
prompt: "a frosty fjord",
n: value,
}),
}),
);
assertEquals(res.status, 400, `n = ${value} must be refused`);
assertEquals((await res.json()).error.param, "n");
}
},
);
// D1-T28's companion on the TTS surface: the same envelope, from the same
// class of gateway-set quantity ceiling.
await t.step("speech input over MAX_TTS_INPUT_CHARS is 400", async () => {
const before = mock.calls.length;
const res = await handler(
new Request(`${base}/v1/audio/speech`, {
method: "POST",
body: JSON.stringify({
model: "openai/tts-1",
input: "a".repeat(MAX_TTS_INPUT_CHARS + 1),
voice: "alloy",
}),
}),
);
assertEquals(res.status, 400);
const body = await res.json();
assertEquals(body.error.type, "invalid_request_error");
assertEquals(body.error.param, "input");
assertEquals(mock.calls.length, before);
});
await t.step("image generation (Gemini Imagen :predict)", async () => {
const res = await handler(
new Request(`${base}/v1/images/generations`, {
method: "POST",
body: JSON.stringify({
model: "gemini/imagen-3.0-generate-002",
prompt: "a frosty fjord",
}),
}),
);
assertEquals(res.status, 200);
// Imagen returns base64 bytes -> mapped to b64_json (no hosted URL).
assertEquals((await res.json()).data[0].b64_json, "aW1hZ2VieXRlcw==");
assertEquals(
mock.calls.at(-1)!.path,
"/v1beta/models/imagen-3.0-generate-002:predict",
);
});
await t.step("image generation (Vertex Imagen :predict)", async () => {
const res = await handler(
new Request(`${base}/v1/images/generations`, {
method: "POST",
body: JSON.stringify({
model: "vertex/imagen-3.0-generate-002",
prompt: "a frosty fjord",
}),
}),
);
assertEquals(res.status, 200);
assertEquals((await res.json()).data[0].b64_json, "aW1hZ2VieXRlcw==");
assertEquals(
mock.calls.at(-1)!.path,
"/v1/projects/proj/locations/us-central1" +
"/publishers/google/models/imagen-3.0-generate-002:predict",
);
});
await t.step("speech returns binary audio", async () => {
const res = await handler(
new Request(`${base}/v1/audio/speech`, {
method: "POST",
body: JSON.stringify({
model: "openai/tts-1",
input: "hello",
voice: "alloy",
}),
}),
);
assertEquals(res.status, 200);
assertEquals(res.headers.get("Content-Type"), "audio/mpeg");
assertEquals((await res.arrayBuffer()).byteLength, 3);
});
await t.step("transcription multipart passthrough", async () => {
const form = new FormData();
form.set("model", "whisper-1");
form.set("file", new Blob([new Uint8Array(4)]), "audio.mp3");
const res = await handler(
new Request(`${base}/v1/audio/transcriptions?provider=openai`, {
method: "POST",
body: form,
}),
);
assertEquals(res.status, 200);
assertEquals((await res.json()).text, "transcribed words");
const sent = mock.calls.at(-1)!;
assert(sent.headers.get("Content-Type")?.includes("multipart/form-data"));
});
await t.step("file upload and batch creation", async () => {
const form = new FormData();
form.set("purpose", "batch");
form.set("file", new Blob([new Uint8Array(4)]), "data.jsonl");
const upload = await handler(
new Request(`${base}/v1/files?provider=openai`, {
method: "POST",
body: form,
}),
);
assertEquals(upload.status, 200);
assertEquals((await upload.json()).id, "file-1");
const batch = await handler(
new Request(`${base}/v1/batches?provider=openai`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
input_file_id: "file-1",
endpoint: "/v1/chat/completions",
completion_window: "24h",
}),
}),
);
assertEquals(batch.status, 200);
assertEquals((await batch.json()).status, "validating");
});
await t.step("files rejected for providers without support", async () => {
const form = new FormData();
form.set("purpose", "batch");
const res = await handler(
new Request(`${base}/v1/files?provider=anthropic`, {
method: "POST",
body: form,
}),
);
assertEquals(res.status, 400);
await res.body?.cancel();
});
} finally {
await mock.close();
}
});
Deno.test("ImageGenerationRequest: legacy shape parses; Imagen params optional", () => {
// Backward compatibility: the original {model?, prompt, n?, size?} shape
// still parses with none of the new fields present.
const legacy = ImageGenerationRequestSchema.safeParse({
model: "openai/gpt-image-1",
prompt: "a frosty fjord",
n: 2,
size: "1024x1024",
});
assert(legacy.success);
if (legacy.success) {
assertEquals(legacy.data.aspectRatio, undefined);
assertEquals(legacy.data.sampleCount, undefined);
assertEquals(legacy.data.negativePrompt, undefined);
assertEquals(legacy.data.seed, undefined);
}
// The additive Imagen parameters are accepted when supplied.
const imagen = ImageGenerationRequestSchema.safeParse({
prompt: "a frosty fjord",
aspectRatio: "16:9",
sampleCount: 3,
negativePrompt: "blurry",
seed: 42,
});
assert(imagen.success);
if (imagen.success) {
assertEquals(imagen.data.aspectRatio, "16:9");
assertEquals(imagen.data.sampleCount, 3);
assertEquals(imagen.data.seed, 42);
}
});