feat(ai): complete models runtime migration
This commit is contained in:
@@ -9,7 +9,7 @@ const model: Model<"openai-responses"> = {
|
||||
id: "test-model",
|
||||
name: "Test Model",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
provider: "custom-openai",
|
||||
baseUrl: "https://example.test/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
@@ -38,12 +38,12 @@ function message(): AssistantMessage {
|
||||
};
|
||||
}
|
||||
|
||||
describe("compat env API key injection", () => {
|
||||
describe("compat legacy API fallback", () => {
|
||||
afterEach(() => {
|
||||
resetApiProviders();
|
||||
});
|
||||
|
||||
it("uses request-scoped env when injecting provider API keys", async () => {
|
||||
it("dispatches unknown providers through the legacy API registry", async () => {
|
||||
let capturedApiKey: string | undefined;
|
||||
registerApiProvider({
|
||||
api: "openai-responses",
|
||||
@@ -67,8 +67,8 @@ describe("compat env API key injection", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await complete(model, context, { env: { OPENAI_API_KEY: "scoped-key" } });
|
||||
await complete(model, context, { apiKey: "request-key" });
|
||||
|
||||
expect(capturedApiKey).toBe("scoped-key");
|
||||
expect(capturedApiKey).toBe("request-key");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,6 +127,7 @@ describe("openai-completions empty tools handling", () => {
|
||||
});
|
||||
|
||||
it("uses conservative OpenAI-compatible fields for Cloudflare AI Gateway /compat models", async () => {
|
||||
process.env.CLOUDFLARE_API_KEY = "cf-token";
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID = "account-id";
|
||||
process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id";
|
||||
const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!;
|
||||
@@ -137,7 +138,7 @@ describe("openai-completions empty tools handling", () => {
|
||||
systemPrompt: "You are helpful.",
|
||||
messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
|
||||
},
|
||||
{ apiKey: "test", maxTokens: 1234, reasoning: "high" },
|
||||
{ maxTokens: 1234, reasoning: "high" },
|
||||
).result();
|
||||
|
||||
const params = mockState.lastParams as {
|
||||
@@ -159,35 +160,25 @@ describe("openai-completions empty tools handling", () => {
|
||||
};
|
||||
expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/compat");
|
||||
expect(clientOptions.defaultHeaders?.Authorization).toBeNull();
|
||||
expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer test");
|
||||
expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer cf-token");
|
||||
});
|
||||
|
||||
it("uses provider env before process.env for Cloudflare AI Gateway base URL", async () => {
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID = "process-account";
|
||||
process.env.CLOUDFLARE_GATEWAY_ID = "process-gateway";
|
||||
it("resolves Cloudflare AI Gateway base URL through provider auth", async () => {
|
||||
process.env.CLOUDFLARE_API_KEY = "cf-token";
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID = "account-id";
|
||||
process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id";
|
||||
const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!;
|
||||
|
||||
await streamSimple(
|
||||
model,
|
||||
{
|
||||
messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
|
||||
},
|
||||
{
|
||||
apiKey: "test",
|
||||
env: {
|
||||
CLOUDFLARE_ACCOUNT_ID: "provider-account",
|
||||
CLOUDFLARE_GATEWAY_ID: "provider-gateway",
|
||||
},
|
||||
},
|
||||
).result();
|
||||
await streamSimple(model, {
|
||||
messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
|
||||
}).result();
|
||||
|
||||
const clientOptions = mockState.lastClientOptions as { baseURL?: string };
|
||||
expect(clientOptions.baseURL).toBe(
|
||||
"https://gateway.ai.cloudflare.com/v1/provider-account/provider-gateway/compat",
|
||||
);
|
||||
expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/compat");
|
||||
});
|
||||
|
||||
it("preserves inline upstream Authorization for Cloudflare AI Gateway BYOK requests", async () => {
|
||||
process.env.CLOUDFLARE_API_KEY = "cf-token";
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID = "account-id";
|
||||
process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id";
|
||||
const model = getModel("cloudflare-ai-gateway", "gpt-5.1")!;
|
||||
@@ -197,7 +188,7 @@ describe("openai-completions empty tools handling", () => {
|
||||
{
|
||||
messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
|
||||
},
|
||||
{ apiKey: "cf-token", headers: { Authorization: "Bearer upstream-token" } },
|
||||
{ headers: { Authorization: "Bearer upstream-token" } },
|
||||
).result();
|
||||
|
||||
const clientOptions = mockState.lastClientOptions as { defaultHeaders?: Record<string, unknown> };
|
||||
@@ -206,6 +197,7 @@ describe("openai-completions empty tools handling", () => {
|
||||
});
|
||||
|
||||
it("sends session affinity headers for Workers AI through Cloudflare AI Gateway", async () => {
|
||||
process.env.CLOUDFLARE_API_KEY = "cf-token";
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID = "account-id";
|
||||
process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id";
|
||||
const workersModel = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!;
|
||||
@@ -215,7 +207,7 @@ describe("openai-completions empty tools handling", () => {
|
||||
{
|
||||
messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
|
||||
},
|
||||
{ apiKey: "test", sessionId: "session-1" },
|
||||
{ sessionId: "session-1" },
|
||||
).result();
|
||||
|
||||
const clientOptions = mockState.lastClientOptions as { defaultHeaders?: Record<string, string> };
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createModels, createProvider } from "../src/models.ts";
|
||||
import { builtinModels, builtinProviders } from "../src/providers/all.ts";
|
||||
import { amazonBedrockProvider } from "../src/providers/amazon-bedrock.ts";
|
||||
import { anthropicProvider } from "../src/providers/anthropic.ts";
|
||||
import { cloudflareAIGatewayProvider } from "../src/providers/cloudflare-ai-gateway.ts";
|
||||
import { cloudflareWorkersAIProvider } from "../src/providers/cloudflare-workers-ai.ts";
|
||||
import { fauxAssistantMessage, fauxProvider } from "../src/providers/faux.ts";
|
||||
import { googleVertexProvider } from "../src/providers/google-vertex.ts";
|
||||
import type { Api, Context, Model, ProviderStreams } from "../src/types.ts";
|
||||
@@ -66,6 +68,55 @@ describe("builtin providers", () => {
|
||||
expect(await unconfigured.getAuth(model)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("requires Cloudflare Workers AI account config and returns scoped env", async () => {
|
||||
const missingAccount = createModels({ authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key" }) });
|
||||
missingAccount.setProvider(cloudflareWorkersAIProvider());
|
||||
const model = missingAccount.getModels("cloudflare-workers-ai")[0];
|
||||
expect(await missingAccount.getAuth(model)).toBeUndefined();
|
||||
|
||||
const configured = createModels({
|
||||
authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key", CLOUDFLARE_ACCOUNT_ID: "account-id" }),
|
||||
});
|
||||
configured.setProvider(cloudflareWorkersAIProvider());
|
||||
const result = await configured.getAuth(model);
|
||||
expect(result?.auth).toEqual({
|
||||
apiKey: "cf-key",
|
||||
baseUrl: "https://api.cloudflare.com/client/v4/accounts/account-id/ai/v1",
|
||||
});
|
||||
expect(result?.env).toEqual({ CLOUDFLARE_ACCOUNT_ID: "account-id" });
|
||||
});
|
||||
|
||||
it("requires Cloudflare AI Gateway account and gateway config and returns scoped env headers", async () => {
|
||||
const missingGateway = createModels({
|
||||
authContext: fakeAuthContext({ CLOUDFLARE_API_KEY: "cf-key", CLOUDFLARE_ACCOUNT_ID: "account-id" }),
|
||||
});
|
||||
missingGateway.setProvider(cloudflareAIGatewayProvider());
|
||||
const model = missingGateway.getModels("cloudflare-ai-gateway")[0];
|
||||
expect(await missingGateway.getAuth(model)).toBeUndefined();
|
||||
|
||||
const configured = createModels({
|
||||
authContext: fakeAuthContext({
|
||||
CLOUDFLARE_API_KEY: "cf-key",
|
||||
CLOUDFLARE_ACCOUNT_ID: "account-id",
|
||||
CLOUDFLARE_GATEWAY_ID: "gateway-id",
|
||||
}),
|
||||
});
|
||||
configured.setProvider(cloudflareAIGatewayProvider());
|
||||
const result = await configured.getAuth(model);
|
||||
expect(result?.auth).toEqual({
|
||||
headers: {
|
||||
"cf-aig-authorization": "Bearer cf-key",
|
||||
Authorization: null,
|
||||
"x-api-key": null,
|
||||
},
|
||||
baseUrl: "https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/anthropic",
|
||||
});
|
||||
expect(result?.env).toEqual({
|
||||
CLOUDFLARE_ACCOUNT_ID: "account-id",
|
||||
CLOUDFLARE_GATEWAY_ID: "gateway-id",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves vertex via ADC file plus project and location", async () => {
|
||||
const adc = "~/.config/gcloud/application_default_credentials.json";
|
||||
const configured = createModels({
|
||||
|
||||
Reference in New Issue
Block a user