Files
wehub-resource-sync 979fb22d7c
CI / test (20, macos-latest) (push) Waiting to run
CI / test (20, ubuntu-latest) (push) Waiting to run
CI / test (22, macos-latest) (push) Waiting to run
CI / test (22, ubuntu-latest) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:01:18 +08:00

305 lines
11 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
buildAuthHeaders,
buildChatUrl,
buildEmbeddingUrl,
detectAzure,
normalizeBaseUrl,
} from "../src/providers/_openai-shared.js";
import { OpenAIEmbeddingProvider } from "../src/providers/embedding/openai.js";
describe("_openai-shared — detectAzure", () => {
it("detects standard Azure resource hostname", () => {
expect(
detectAzure(
"https://myresource.openai.azure.com/openai/deployments/mydeploy",
),
).toBe(true);
});
it("does not flag api.openai.com", () => {
expect(detectAzure("https://api.openai.com")).toBe(false);
});
it("does not flag DeepSeek / SiliconFlow / Ollama / vLLM", () => {
expect(detectAzure("https://api.deepseek.com/v1")).toBe(false);
expect(detectAzure("https://api.siliconflow.cn")).toBe(false);
expect(detectAzure("http://localhost:11434/v1")).toBe(false);
expect(detectAzure("http://localhost:8000/v1")).toBe(false);
});
it("returns false for malformed URLs", () => {
expect(detectAzure("not-a-url")).toBe(false);
expect(detectAzure("")).toBe(false);
});
});
describe("_openai-shared — buildChatUrl", () => {
it("appends /v1/chat/completions for standard OpenAI", () => {
expect(buildChatUrl("https://api.openai.com", false, "2024-08-01-preview")).toBe(
"https://api.openai.com/v1/chat/completions",
);
});
it("appends /chat/completions + api-version for Azure", () => {
const url = buildChatUrl(
"https://myresource.openai.azure.com/openai/deployments/mydeploy",
true,
"2024-08-01-preview",
);
expect(url).toBe(
"https://myresource.openai.azure.com/openai/deployments/mydeploy/chat/completions?api-version=2024-08-01-preview",
);
});
it("URL-encodes the api-version", () => {
const url = buildChatUrl(
"https://r.openai.azure.com/openai/deployments/d",
true,
"preview/with/slashes",
);
expect(url).toContain("api-version=preview%2Fwith%2Fslashes");
});
it("preserves pre-existing query params on the base URL (CodeRabbit catch)", () => {
// A corporate proxy or diagnostics endpoint might already carry
// query parameters on the base URL. String-concat would have
// interpolated the route path into the query string; URL-API
// composition keeps the query intact and adds api-version
// alongside.
const url = buildChatUrl(
"https://proxy.example.com/openai/deployments/d?tenant=acme",
true,
"2024-08-01-preview",
);
const parsed = new URL(url);
expect(parsed.pathname).toBe("/openai/deployments/d/chat/completions");
expect(parsed.searchParams.get("tenant")).toBe("acme");
expect(parsed.searchParams.get("api-version")).toBe("2024-08-01-preview");
});
it("strips trailing slashes from base path before joining route", () => {
const url = buildChatUrl(
"https://r.openai.azure.com/openai/deployments/d/",
true,
"2024-08-01-preview",
);
expect(new URL(url).pathname).toBe("/openai/deployments/d/chat/completions");
});
it("routes through /openai/v1 when the base URL has no /deployments/ segment (Azure v1 GA)", () => {
// Azure shipped a v1 URL pattern that mirrors the OpenAI shape:
// /openai/v1/chat/completions, deployment passed in the body as
// `model`. No api-version query param.
const url = buildChatUrl(
"https://r.openai.azure.com",
true,
"2024-08-01-preview", // ignored on v1
);
const parsed = new URL(url);
expect(parsed.pathname).toBe("/openai/v1/chat/completions");
expect(parsed.searchParams.get("api-version")).toBeNull();
});
it("strips a trailing /openai or /openai/v1 prefix when composing v1 URLs", () => {
// Users may pre-configure OPENAI_BASE_URL with the /openai/v1
// suffix already present. We should not double it.
const fromOpenai = buildChatUrl(
"https://r.openai.azure.com/openai",
true,
"ignored",
);
expect(new URL(fromOpenai).pathname).toBe("/openai/v1/chat/completions");
const fromV1 = buildChatUrl(
"https://r.openai.azure.com/openai/v1",
true,
"ignored",
);
expect(new URL(fromV1).pathname).toBe("/openai/v1/chat/completions");
});
});
describe("_openai-shared — buildEmbeddingUrl", () => {
it("appends /v1/embeddings for standard OpenAI", () => {
expect(
buildEmbeddingUrl("https://api.openai.com", false, "2024-08-01-preview"),
).toBe("https://api.openai.com/v1/embeddings");
});
it("appends /embeddings + api-version for Azure legacy (no /v1/ prefix)", () => {
const url = buildEmbeddingUrl(
"https://r.openai.azure.com/openai/deployments/embed-deploy",
true,
"2024-08-01-preview",
);
expect(url).toBe(
"https://r.openai.azure.com/openai/deployments/embed-deploy/embeddings?api-version=2024-08-01-preview",
);
});
it("routes through /openai/v1/embeddings on Azure v1 (no api-version)", () => {
const url = buildEmbeddingUrl(
"https://r.openai.azure.com",
true,
"2024-08-01-preview", // ignored on v1
);
const parsed = new URL(url);
expect(parsed.pathname).toBe("/openai/v1/embeddings");
expect(parsed.searchParams.get("api-version")).toBeNull();
});
});
describe("_openai-shared — non-OpenAI base URLs (#628, #646)", () => {
it("does not double /v1 when base URL already ends with /v1 (DeepSeek shape, #628)", () => {
expect(
buildChatUrl("https://api.deepseek.com/v1", false, "2024-08-01-preview"),
).toBe("https://api.deepseek.com/v1/chat/completions");
expect(
buildEmbeddingUrl("https://api.deepseek.com/v1", false, "2024-08-01-preview"),
).toBe("https://api.deepseek.com/v1/embeddings");
});
it("does not inject /v1 when provider uses non-OpenAI version segment (Zhipu /api/paas/v4, #646)", () => {
expect(
buildChatUrl(
"https://open.bigmodel.cn/api/paas/v4",
false,
"2024-08-01-preview",
),
).toBe("https://open.bigmodel.cn/api/paas/v4/chat/completions");
expect(
buildEmbeddingUrl(
"https://open.bigmodel.cn/api/paas/v4",
false,
"2024-08-01-preview",
),
).toBe("https://open.bigmodel.cn/api/paas/v4/embeddings");
});
it("tolerates trailing slash on already-versioned base", () => {
expect(
buildChatUrl("https://api.deepseek.com/v1/", false, "2024-08-01-preview"),
).toBe("https://api.deepseek.com/v1/chat/completions");
});
it("handles localhost OpenAI-compatible servers with explicit /v1", () => {
expect(
buildChatUrl("http://localhost:11434/v1", false, "2024-08-01-preview"),
).toBe("http://localhost:11434/v1/chat/completions");
expect(
buildChatUrl("http://localhost:8000/v1", false, "2024-08-01-preview"),
).toBe("http://localhost:8000/v1/chat/completions");
});
});
describe("_openai-shared — buildAuthHeaders", () => {
it("emits Authorization: Bearer for standard OpenAI", () => {
expect(buildAuthHeaders("sk-test", false)).toEqual({
"Content-Type": "application/json",
Authorization: "Bearer sk-test",
});
});
it("emits api-key header for Azure", () => {
expect(buildAuthHeaders("azure-key", true)).toEqual({
"Content-Type": "application/json",
"api-key": "azure-key",
});
});
});
describe("_openai-shared — normalizeBaseUrl", () => {
it("returns default when no value passed", () => {
expect(normalizeBaseUrl(undefined)).toBe("https://api.openai.com");
expect(normalizeBaseUrl("")).toBe("https://api.openai.com");
});
it("strips trailing slashes", () => {
expect(normalizeBaseUrl("https://api.deepseek.com/v1///")).toBe(
"https://api.deepseek.com/v1",
);
});
it("returns explicit values unchanged otherwise", () => {
expect(normalizeBaseUrl("https://api.deepseek.com/v1")).toBe(
"https://api.deepseek.com/v1",
);
});
});
// ─────────────────────────────────────────────────────────────
// OpenAIEmbeddingProvider — Azure transport (#371)
// Verifies the embedding path now uses the shared Azure helpers:
// hits /embeddings (not /v1/embeddings), includes api-version, uses
// api-key header instead of Authorization: Bearer.
// ─────────────────────────────────────────────────────────────
describe("OpenAIEmbeddingProvider — Azure auto-detection (#371)", () => {
const ORIGINAL_BASE = process.env["OPENAI_BASE_URL"];
const ORIGINAL_VERSION = process.env["OPENAI_API_VERSION"];
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
if (ORIGINAL_BASE === undefined) delete process.env["OPENAI_BASE_URL"];
else process.env["OPENAI_BASE_URL"] = ORIGINAL_BASE;
if (ORIGINAL_VERSION === undefined) delete process.env["OPENAI_API_VERSION"];
else process.env["OPENAI_API_VERSION"] = ORIGINAL_VERSION;
vi.restoreAllMocks();
});
it("uses Azure shape when OPENAI_BASE_URL points at *.openai.azure.com", async () => {
process.env["OPENAI_BASE_URL"] =
"https://myres.openai.azure.com/openai/deployments/embed-d";
process.env["OPENAI_API_VERSION"] = "2024-08-01-preview";
let capturedUrl = "";
let capturedHeaders = new Headers();
vi.spyOn(globalThis, "fetch").mockImplementation(
async (url: string | URL | Request, init?: RequestInit) => {
capturedUrl = String(url);
capturedHeaders = new Headers(init?.headers);
return new Response(
JSON.stringify({ data: [{ embedding: [0.1, 0.2, 0.3] }] }),
{ status: 200 },
);
},
);
const provider = new OpenAIEmbeddingProvider("azure-key");
await provider.embedBatch(["hello"]);
expect(capturedUrl).toBe(
"https://myres.openai.azure.com/openai/deployments/embed-d/embeddings?api-version=2024-08-01-preview",
);
expect(capturedHeaders.get("api-key")).toBe("azure-key");
expect(capturedHeaders.get("Authorization")).toBeNull();
});
it("uses standard shape when OPENAI_BASE_URL points at api.openai.com", async () => {
process.env["OPENAI_BASE_URL"] = "https://api.openai.com";
let capturedUrl = "";
let capturedHeaders = new Headers();
vi.spyOn(globalThis, "fetch").mockImplementation(
async (url: string | URL | Request, init?: RequestInit) => {
capturedUrl = String(url);
capturedHeaders = new Headers(init?.headers);
return new Response(
JSON.stringify({ data: [{ embedding: [0.4, 0.5, 0.6] }] }),
{ status: 200 },
);
},
);
const provider = new OpenAIEmbeddingProvider("sk-test");
await provider.embedBatch(["hello"]);
expect(capturedUrl).toBe("https://api.openai.com/v1/embeddings");
expect(capturedHeaders.get("Authorization")).toBe("Bearer sk-test");
expect(capturedHeaders.get("api-key")).toBeNull();
});
});