mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-24 10:58:37 +00:00
fix(openai-embedding): preserve openai/ prefix for non-native base URLs (#92135)
* fix(openai-embedding): preserve openai/ prefix for non-native base URLs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(openai-embedding): normalize model before maxInputTokens lookup so qualified models retain token cap * fix(openai-embedding): use semantic hostname check for native OpenAI URL detection --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,15 @@
|
||||
import type { MemoryEmbeddingProviderCreateOptions } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const DEFAULT_MOCK_CLIENT = {
|
||||
baseUrl: "https://embeddings.example/v1",
|
||||
headers: { Authorization: "Bearer test" },
|
||||
model: "text-embedding-3-small",
|
||||
};
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
fetchRemoteEmbeddingVectors: vi.fn(async () => [[1, 0]]),
|
||||
resolveRemoteEmbeddingClient: vi.fn(async () => ({
|
||||
baseUrl: "https://embeddings.example/v1",
|
||||
headers: { Authorization: "Bearer test" },
|
||||
model: "text-embedding-3-small",
|
||||
})),
|
||||
resolveRemoteEmbeddingClient: vi.fn(async () => ({ ...DEFAULT_MOCK_CLIENT })),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/memory-core-host-engine-embeddings", () => ({
|
||||
@@ -120,4 +122,93 @@ describe("OpenAI embedding provider", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// --- openai/ prefix preservation ---
|
||||
|
||||
it("strips openai/ prefix when using native OpenAI API base URL", async () => {
|
||||
mocks.resolveRemoteEmbeddingClient.mockResolvedValueOnce({
|
||||
...DEFAULT_MOCK_CLIENT,
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
const { provider } = await createOpenAiEmbeddingProvider(
|
||||
createOptions({ model: "openai/text-embedding-3-small" }),
|
||||
);
|
||||
|
||||
expect(provider.model).toBe("text-embedding-3-small");
|
||||
});
|
||||
|
||||
it("strips openai/ prefix for semantically native URLs (uppercase hostname)", async () => {
|
||||
mocks.resolveRemoteEmbeddingClient.mockResolvedValueOnce({
|
||||
...DEFAULT_MOCK_CLIENT,
|
||||
baseUrl: "https://API.OPENAI.COM/v1",
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
const { provider } = await createOpenAiEmbeddingProvider(
|
||||
createOptions({ model: "openai/text-embedding-3-small" }),
|
||||
);
|
||||
|
||||
expect(provider.model).toBe("text-embedding-3-small");
|
||||
});
|
||||
|
||||
it("preserves openai/ prefix for non-native OpenAI base URLs", async () => {
|
||||
mocks.resolveRemoteEmbeddingClient.mockResolvedValueOnce({
|
||||
...DEFAULT_MOCK_CLIENT,
|
||||
baseUrl: "https://router.requesty.ai/v1",
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
const { provider } = await createOpenAiEmbeddingProvider(
|
||||
createOptions({ model: "openai/text-embedding-3-small" }),
|
||||
);
|
||||
|
||||
expect(provider.model).toBe("openai/text-embedding-3-small");
|
||||
});
|
||||
|
||||
it("provides maxInputTokens for qualified model with non-native base URL", async () => {
|
||||
mocks.resolveRemoteEmbeddingClient.mockResolvedValueOnce({
|
||||
...DEFAULT_MOCK_CLIENT,
|
||||
baseUrl: "https://router.requesty.ai/v1",
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
const { provider } = await createOpenAiEmbeddingProvider(
|
||||
createOptions({ model: "openai/text-embedding-3-small" }),
|
||||
);
|
||||
|
||||
expect(provider.maxInputTokens).toBe(8192);
|
||||
});
|
||||
|
||||
it("preserves openai/ prefix in embedding request body for non-native base URLs", async () => {
|
||||
mocks.resolveRemoteEmbeddingClient.mockResolvedValueOnce({
|
||||
...DEFAULT_MOCK_CLIENT,
|
||||
baseUrl: "https://router.requesty.ai/v1",
|
||||
model: "text-embedding-3-small",
|
||||
});
|
||||
|
||||
const { provider } = await createOpenAiEmbeddingProvider(
|
||||
createOptions({
|
||||
model: "openai/text-embedding-3-small",
|
||||
inputType: "query",
|
||||
}),
|
||||
);
|
||||
|
||||
await provider.embedQuery("test");
|
||||
|
||||
expect(mocks.fetchRemoteEmbeddingVectors).toHaveBeenCalledWith({
|
||||
url: "https://router.requesty.ai/v1/embeddings",
|
||||
headers: { Authorization: "Bearer test" },
|
||||
ssrfPolicy: undefined,
|
||||
fetchImpl: undefined,
|
||||
signal: undefined,
|
||||
body: {
|
||||
model: "openai/text-embedding-3-small",
|
||||
input: ["test"],
|
||||
input_type: "query",
|
||||
},
|
||||
errorPrefix: "openai embeddings failed",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,15 @@ function normalizeOpenAiModel(model: string): string {
|
||||
return trimmed.startsWith("openai/") ? trimmed.slice("openai/".length) : trimmed;
|
||||
}
|
||||
|
||||
/** Whether the embedding base URL points to the native OpenAI API endpoint. */
|
||||
function isNativeOpenAiBaseUrl(baseUrl: string): boolean {
|
||||
try {
|
||||
return new URL(baseUrl).hostname.toLowerCase().replace(/\.+$/, "") === "api.openai.com";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createOpenAiEmbeddingProvider(
|
||||
options: MemoryEmbeddingProviderCreateOptions,
|
||||
): Promise<{ provider: MemoryEmbeddingProvider; client: OpenAiEmbeddingClient }> {
|
||||
@@ -79,8 +88,8 @@ export async function createOpenAiEmbeddingProvider(
|
||||
provider: {
|
||||
id: "openai",
|
||||
model: client.model,
|
||||
...(typeof OPENAI_MAX_INPUT_TOKENS[client.model] === "number"
|
||||
? { maxInputTokens: OPENAI_MAX_INPUT_TOKENS[client.model] }
|
||||
...(typeof OPENAI_MAX_INPUT_TOKENS[normalizeOpenAiModel(client.model)] === "number"
|
||||
? { maxInputTokens: OPENAI_MAX_INPUT_TOKENS[normalizeOpenAiModel(client.model)] }
|
||||
: {}),
|
||||
embedQuery: async (text, optionsValue) => {
|
||||
const [vec] = await embed([text], "query", optionsValue?.signal);
|
||||
@@ -96,12 +105,19 @@ export async function createOpenAiEmbeddingProvider(
|
||||
async function resolveOpenAiEmbeddingClient(
|
||||
options: MemoryEmbeddingProviderCreateOptions,
|
||||
): Promise<OpenAiEmbeddingClient> {
|
||||
const originalModel = options.model;
|
||||
const client = await resolveRemoteEmbeddingClient({
|
||||
provider: options.provider ?? "openai",
|
||||
options,
|
||||
defaultBaseUrl: DEFAULT_OPENAI_BASE_URL,
|
||||
normalizeModel: normalizeOpenAiModel,
|
||||
});
|
||||
// Non-native OpenAI routers (e.g. Requesty) expect the provider-qualified
|
||||
// model name ("openai/text-embedding-3-small") in embedding requests.
|
||||
// Strip the prefix only when talking to the native OpenAI API.
|
||||
if (!isNativeOpenAiBaseUrl(client.baseUrl) && originalModel.startsWith("openai/")) {
|
||||
client.model = `openai/${normalizeOpenAiModel(originalModel)}`;
|
||||
}
|
||||
return {
|
||||
...client,
|
||||
inputType: options.inputType,
|
||||
|
||||
Reference in New Issue
Block a user