Skip to content

Commit a6fe951

Browse files
committed
fix(parallel): keep normalized search fields UTF-16 safe
1 parent 4e29b2f commit a6fe951

2 files changed

Lines changed: 20 additions & 3 deletions

File tree

extensions/parallel/src/parallel-search-normalize.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
wrapWebContent,
99
} from "openclaw/plugin-sdk/provider-web-search";
1010
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
11+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
1112

1213
// Internal-only bounds (the model-facing tool schema declares its own copies).
1314
const PARALLEL_MAX_SEARCH_COUNT = 40;
@@ -58,7 +59,7 @@ export function normalizeParallelObjective(value: string | undefined): string |
5859
}
5960
return trimmed.length <= PARALLEL_MAX_OBJECTIVE_CHARS
6061
? trimmed
61-
: trimmed.slice(0, PARALLEL_MAX_OBJECTIVE_CHARS);
62+
: truncateUtf16Safe(trimmed, PARALLEL_MAX_OBJECTIVE_CHARS);
6263
}
6364

6465
export function normalizeParallelClientModel(value: string | undefined): string | undefined {
@@ -68,7 +69,7 @@ export function normalizeParallelClientModel(value: string | undefined): string
6869
}
6970
return trimmed.length <= PARALLEL_CLIENT_MODEL_MAX_LENGTH
7071
? trimmed
71-
: trimmed.slice(0, PARALLEL_CLIENT_MODEL_MAX_LENGTH);
72+
: truncateUtf16Safe(trimmed, PARALLEL_CLIENT_MODEL_MAX_LENGTH);
7273
}
7374

7475
// Parallel's API caps each entry at 200 chars and accepts up to 5 queries. We
@@ -90,7 +91,7 @@ export function normalizeParallelSearchQueries(value: unknown): string[] {
9091
const capped =
9192
trimmed.length <= PARALLEL_MAX_SEARCH_QUERY_CHARS
9293
? trimmed
93-
: trimmed.slice(0, PARALLEL_MAX_SEARCH_QUERY_CHARS);
94+
: truncateUtf16Safe(trimmed, PARALLEL_MAX_SEARCH_QUERY_CHARS);
9495
if (seen.has(capped)) {
9596
continue;
9697
}

extensions/parallel/src/parallel-web-search-provider.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ import { testing } from "../test-api.js";
6464
import { createParallelWebSearchProvider as createContractParallelWebSearchProvider } from "../web-search-contract-api.js";
6565
import { createParallelWebSearchProvider } from "./parallel-web-search-provider.js";
6666

67+
const loneHighSurrogate = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/;
68+
6769
describe("parallel web search provider", () => {
6870
beforeEach(() => {
6971
endpointMockState.calls = [];
@@ -254,6 +256,9 @@ describe("parallel web search provider", () => {
254256
expect(testing.normalizeParallelObjective(undefined)).toBeUndefined();
255257
expect(testing.normalizeParallelObjective("")).toBeUndefined();
256258
expect((testing.normalizeParallelObjective("x".repeat(6000)) ?? "").length).toBe(5000);
259+
const boundary = testing.normalizeParallelObjective(`${"x".repeat(4999)}\u{1f63e}tail`);
260+
expect(boundary).toBe("x".repeat(4999));
261+
expect(loneHighSurrogate.test(boundary ?? "")).toBe(false);
257262
});
258263

259264
it("normalizes search_queries: trim, drop blanks, dedupe, cap length, cap count", () => {
@@ -270,6 +275,14 @@ describe("parallel web search provider", () => {
270275
expect(testing.normalizeParallelSearchQueries(undefined)).toEqual([]);
271276
expect(testing.normalizeParallelSearchQueries("openclaw github")).toEqual([]);
272277
expect(testing.normalizeParallelSearchQueries(["x".repeat(250)])).toEqual(["x".repeat(200)]);
278+
expect(testing.normalizeParallelSearchQueries([`${"x".repeat(199)}\u{1f63e}tail`])).toEqual([
279+
"x".repeat(199),
280+
]);
281+
expect(
282+
loneHighSurrogate.test(
283+
testing.normalizeParallelSearchQueries([`${"x".repeat(199)}\u{1f63e}tail`])[0] ?? "",
284+
),
285+
).toBe(false);
273286
const six = ["a", "b", "c", "d", "e", "f"];
274287
expect(testing.normalizeParallelSearchQueries(six)).toEqual(["a", "b", "c", "d", "e"]);
275288
});
@@ -289,6 +302,9 @@ describe("parallel web search provider", () => {
289302
expect(testing.normalizeParallelClientModel(" gpt-5.5 ")).toBe("gpt-5.5");
290303
expect(testing.normalizeParallelClientModel(undefined)).toBeUndefined();
291304
expect((testing.normalizeParallelClientModel("a".repeat(200)) ?? "").length).toBe(100);
305+
const boundary = testing.normalizeParallelClientModel(`${"m".repeat(99)}\u{1f63e}tail`);
306+
expect(boundary).toBe("m".repeat(99));
307+
expect(loneHighSurrogate.test(boundary ?? "")).toBe(false);
292308
});
293309

294310
it("normalizes the Parallel /v1/search response shape", () => {

0 commit comments

Comments
 (0)