mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(embeddings): reject empty vectors from compatible providers (#128480)
* fix(embeddings): reject empty provider vectors * chore(embeddings): tighten assertion safety baseline
This commit is contained in:
committed by
GitHub
parent
a6a765cd44
commit
8635794d4d
@@ -3624,7 +3624,7 @@ src/plugins/native-module-require.ts 3
|
||||
src/plugins/official-external-plugin-catalog-snapshot-store.ts 4
|
||||
src/plugins/official-external-plugin-targets.ts 1
|
||||
src/plugins/official-external-provider-endpoints.ts 1
|
||||
src/plugins/openai-compatible-embedding-provider.ts 3
|
||||
src/plugins/openai-compatible-embedding-provider.ts 2
|
||||
src/plugins/plugin-cache-primitives.ts 2
|
||||
src/plugins/plugin-command-dispatch-contract.ts 1
|
||||
src/plugins/plugin-command-execution.ts 3
|
||||
|
||||
@@ -77,7 +77,7 @@ async function readJsonBody(req: IncomingMessage): Promise<Record<string, unknow
|
||||
|
||||
async function startEmbeddingServer(params?: {
|
||||
token?: string;
|
||||
respond?: (request: CapturedRequest) => FixtureResponse | Record<string, unknown>;
|
||||
respond?: (request: CapturedRequest) => FixtureResponse | Record<string, unknown> | null;
|
||||
status?: number;
|
||||
}): Promise<{ baseUrl: string; requests: CapturedRequest[] }> {
|
||||
const requests: CapturedRequest[] = [];
|
||||
@@ -102,11 +102,13 @@ async function startEmbeddingServer(params?: {
|
||||
res.writeHead(params?.status ?? 200, { "content-type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify(
|
||||
params?.respond?.(captured) ?? {
|
||||
object: "list",
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2, 0.3], index: 0 }],
|
||||
model: body.model,
|
||||
},
|
||||
params?.respond
|
||||
? params.respond(captured)
|
||||
: {
|
||||
object: "list",
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2, 0.3], index: 0 }],
|
||||
model: body.model,
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -908,16 +910,26 @@ describe("openai-compatible generic embedding provider", () => {
|
||||
).rejects.toThrow("missing model");
|
||||
});
|
||||
|
||||
it("keeps remote parser failures behind the provider-specific error prefix", async () => {
|
||||
const server = await startEmbeddingServer({ respond: () => ({ data: [] }) });
|
||||
it.each([
|
||||
{ name: "missing vectors", input: "hello", response: { data: [] } },
|
||||
{ name: "empty direct vector", input: "hello", response: { data: [{ embedding: [] }] } },
|
||||
{
|
||||
name: "empty batch vector",
|
||||
input: ["hello", "world"],
|
||||
response: { data: [{ embedding: [1] }, { embedding: [] }] },
|
||||
},
|
||||
{ name: "null response root", input: "hello", response: null },
|
||||
])("rejects malformed $name with the provider-specific error", async ({ input, response }) => {
|
||||
const server = await startEmbeddingServer({ respond: () => response });
|
||||
const { provider } = await createOpenAICompatibleEmbeddingProvider(
|
||||
createOptions({
|
||||
model: "text-embedding-bge-m3",
|
||||
remote: { baseUrl: server.baseUrl },
|
||||
}),
|
||||
);
|
||||
const request = typeof input === "string" ? provider.embed(input) : provider.embedBatch(input);
|
||||
|
||||
await expect(provider.embed("hello")).rejects.toThrow(
|
||||
await expect(request).rejects.toThrow(
|
||||
"openai-compatible embeddings failed: malformed JSON response",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { asOptionalRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
|
||||
import { readProviderJsonArrayFieldResponse } from "../agents/provider-http-errors.js";
|
||||
import type {
|
||||
AcquireConfiguredProviderLocalService,
|
||||
ConfiguredProviderLocalServiceTarget,
|
||||
@@ -44,10 +44,6 @@ type OpenAICompatibleEmbeddingClient = {
|
||||
acquireLocalService?: AcquireConfiguredProviderLocalService;
|
||||
};
|
||||
|
||||
type OpenAICompatibleEmbeddingResponse = {
|
||||
data?: unknown;
|
||||
};
|
||||
|
||||
type ConfiguredEmbeddingProvider = {
|
||||
api?: string;
|
||||
baseUrl?: string;
|
||||
@@ -252,7 +248,7 @@ function malformedEmbeddingResponse(): Error {
|
||||
}
|
||||
|
||||
function readEmbeddingVector(value: unknown): number[] {
|
||||
if (!Array.isArray(value)) {
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
throw malformedEmbeddingResponse();
|
||||
}
|
||||
for (const entry of value) {
|
||||
@@ -263,14 +259,11 @@ function readEmbeddingVector(value: unknown): number[] {
|
||||
return value;
|
||||
}
|
||||
|
||||
function readEmbeddingVectors(
|
||||
payload: OpenAICompatibleEmbeddingResponse,
|
||||
expectedCount: number,
|
||||
): number[][] {
|
||||
if (!Array.isArray(payload.data) || payload.data.length !== expectedCount) {
|
||||
function readEmbeddingVectors(data: unknown[], expectedCount: number): number[][] {
|
||||
if (data.length !== expectedCount) {
|
||||
throw malformedEmbeddingResponse();
|
||||
}
|
||||
return payload.data.map((entry) => {
|
||||
return data.map((entry) => {
|
||||
const record = asRecord(entry);
|
||||
if (!record) {
|
||||
throw malformedEmbeddingResponse();
|
||||
@@ -279,10 +272,6 @@ function readEmbeddingVectors(
|
||||
});
|
||||
}
|
||||
|
||||
async function readJsonResponse(response: Response): Promise<unknown> {
|
||||
return await readProviderJsonResponse(response, "openai-compatible embeddings failed");
|
||||
}
|
||||
|
||||
async function readEmbeddingErrorBodySnippet(response: Response): Promise<string | undefined> {
|
||||
if (!response.body || response.bodyUsed) {
|
||||
return undefined;
|
||||
@@ -342,7 +331,11 @@ async function postEmbeddingRequest(params: {
|
||||
throw await createEmbeddingHttpError(response);
|
||||
}
|
||||
return readEmbeddingVectors(
|
||||
(await readJsonResponse(response)) as OpenAICompatibleEmbeddingResponse,
|
||||
await readProviderJsonArrayFieldResponse(
|
||||
response,
|
||||
"openai-compatible embeddings failed",
|
||||
"data",
|
||||
),
|
||||
input.length,
|
||||
);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user