219 lines
7.0 KiB
TypeScript
Executable File
219 lines
7.0 KiB
TypeScript
Executable File
// Extended advanced-API routes through the real handler: Files retrieve/
|
|
// delete/content and Batches list/cancel: rawProxy passthrough with
|
|
// capability enforcement. Companion to advanced_apis_test.ts.
|
|
|
|
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 {
|
|
BatchListResponseSchema,
|
|
BatchResponseSchema,
|
|
FileDeleteResponseSchema,
|
|
FileObjectSchema,
|
|
} from "../../packages/contracts/src/mod.ts";
|
|
|
|
const base = "http://gateway.test";
|
|
|
|
function makeContext(mockUrl: 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 },
|
|
},
|
|
{
|
|
id: "cohere",
|
|
type: "cohere",
|
|
apiKey: "co-adv",
|
|
baseUrl: mockUrl,
|
|
enabled: true,
|
|
models: ["command-r"],
|
|
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,
|
|
};
|
|
}
|
|
|
|
// Distinct paths per operation because MockCall records path (not method).
|
|
const mock = new MockProvider((call) => {
|
|
switch (call.path) {
|
|
case "/files/file-ret":
|
|
return jsonResponse({
|
|
id: "file-ret",
|
|
object: "file",
|
|
bytes: 42,
|
|
created_at: 10,
|
|
filename: "results.jsonl",
|
|
purpose: "batch",
|
|
status: "processed",
|
|
});
|
|
case "/files/file-ret/content":
|
|
return new Response('{"custom_id":"a","response":{}}\n', {
|
|
headers: { "Content-Type": "application/jsonl" },
|
|
});
|
|
case "/files/file-del":
|
|
return jsonResponse({ id: "file-del", object: "file", deleted: true });
|
|
case "/batches":
|
|
return jsonResponse({
|
|
object: "list",
|
|
data: [{
|
|
id: "batch-1",
|
|
object: "batch",
|
|
input_file_id: "file-ret",
|
|
endpoint: "/v1/chat/completions",
|
|
status: "completed",
|
|
request_counts: { total: 2, completed: 2, failed: 0 },
|
|
}],
|
|
has_more: false,
|
|
last_id: "batch-1",
|
|
});
|
|
case "/batches/batch-1/cancel":
|
|
return jsonResponse({
|
|
id: "batch-1",
|
|
object: "batch",
|
|
input_file_id: "file-ret",
|
|
endpoint: "/v1/chat/completions",
|
|
status: "cancelling",
|
|
});
|
|
case "/batches/batch-1/results":
|
|
return new Response(
|
|
'{"custom_id":"a","response":{"status_code":200}}\n',
|
|
{
|
|
headers: { "Content-Type": "application/jsonl" },
|
|
},
|
|
);
|
|
default:
|
|
return jsonResponse({ error: `unexpected path ${call.path}` }, 500);
|
|
}
|
|
});
|
|
|
|
Deno.test("extended advanced APIs: files retrieve/delete/content, batches list/cancel", async (t) => {
|
|
const handler = createHandler(makeContext(mock.url));
|
|
|
|
try {
|
|
await t.step("GET /v1/files/:id retrieves a file object", async () => {
|
|
const res = await handler(
|
|
new Request(`${base}/v1/files/file-ret?provider=openai`),
|
|
);
|
|
assertEquals(res.status, 200);
|
|
const parsed = FileObjectSchema.safeParse(await res.json());
|
|
assert(parsed.success);
|
|
assertEquals(mock.calls.at(-1)!.path, "/files/file-ret");
|
|
});
|
|
|
|
await t.step("GET /v1/files/:id/content returns raw bytes", async () => {
|
|
const res = await handler(
|
|
new Request(`${base}/v1/files/file-ret/content?provider=openai`),
|
|
);
|
|
assertEquals(res.status, 200);
|
|
assertEquals(res.headers.get("Content-Type"), "application/jsonl");
|
|
assert((await res.text()).includes("custom_id"));
|
|
assertEquals(mock.calls.at(-1)!.path, "/files/file-ret/content");
|
|
});
|
|
|
|
await t.step("DELETE /v1/files/:id deletes a file", async () => {
|
|
const res = await handler(
|
|
new Request(`${base}/v1/files/file-del?provider=openai`, {
|
|
method: "DELETE",
|
|
}),
|
|
);
|
|
assertEquals(res.status, 200);
|
|
const parsed = FileDeleteResponseSchema.safeParse(await res.json());
|
|
assert(parsed.success);
|
|
if (parsed.success) {
|
|
assertEquals(parsed.data.deleted, true);
|
|
}
|
|
assertEquals(mock.calls.at(-1)!.path, "/files/file-del");
|
|
});
|
|
|
|
await t.step("GET /v1/batches lists batches", async () => {
|
|
const res = await handler(
|
|
new Request(`${base}/v1/batches?provider=openai`),
|
|
);
|
|
assertEquals(res.status, 200);
|
|
const parsed = BatchListResponseSchema.safeParse(await res.json());
|
|
assert(parsed.success);
|
|
if (parsed.success) {
|
|
assertEquals(parsed.data.data[0].request_counts?.completed, 2);
|
|
}
|
|
assertEquals(mock.calls.at(-1)!.path, "/batches");
|
|
});
|
|
|
|
await t.step("POST /v1/batches/:id/cancel cancels a batch", async () => {
|
|
const res = await handler(
|
|
new Request(`${base}/v1/batches/batch-1/cancel?provider=openai`, {
|
|
method: "POST",
|
|
}),
|
|
);
|
|
assertEquals(res.status, 200);
|
|
const parsed = BatchResponseSchema.safeParse(await res.json());
|
|
assert(parsed.success);
|
|
if (parsed.success) {
|
|
assertEquals(parsed.data.status, "cancelling");
|
|
}
|
|
assertEquals(mock.calls.at(-1)!.path, "/batches/batch-1/cancel");
|
|
});
|
|
|
|
await t.step(
|
|
"GET /v1/batches/:id/results proxies batch output bytes",
|
|
async () => {
|
|
const res = await handler(
|
|
new Request(`${base}/v1/batches/batch-1/results?provider=openai`),
|
|
);
|
|
assertEquals(res.status, 200);
|
|
assertEquals(res.headers.get("Content-Type"), "application/jsonl");
|
|
assert((await res.text()).includes("custom_id"));
|
|
assertEquals(mock.calls.at(-1)!.path, "/batches/batch-1/results");
|
|
},
|
|
);
|
|
|
|
await t.step(
|
|
"capability enforced: files rejected for cohere",
|
|
async () => {
|
|
const res = await handler(
|
|
new Request(`${base}/v1/files/file-ret?provider=cohere`),
|
|
);
|
|
assertEquals(res.status, 400);
|
|
assert((await res.json()).error.message.includes("does not support"));
|
|
},
|
|
);
|
|
} finally {
|
|
await mock.close();
|
|
}
|
|
});
|