Skip to content

Commit c342880

Browse files
committed
Fix UTF-16-safe media runner output truncation
1 parent 632efa2 commit c342880

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

src/media-understanding/runner.entries.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
normalizeNullableString,
88
} from "@openclaw/normalization-core/string-coerce";
99
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
10+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
1011
import { MediaUnderstandingSkipError } from "../../packages/media-understanding-common/src/errors.js";
1112
import { extractGeminiResponse } from "../../packages/media-understanding-common/src/output-extract.js";
1213
import {
@@ -100,7 +101,7 @@ function trimOutput(text: string, maxChars?: number): string {
100101
if (!maxChars || trimmed.length <= maxChars) {
101102
return trimmed;
102103
}
103-
return trimmed.slice(0, maxChars).trim();
104+
return truncateUtf16Safe(trimmed, maxChars).trim();
104105
}
105106

106107
function extractSherpaOnnxText(raw: string): { matched: boolean; text: string } {

src/media-understanding/runner.video.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,53 @@ function requireCapabilityOutput(result: CapabilityResult, index: number) {
4141
}
4242

4343
describe("runCapability video provider wiring", () => {
44+
it("truncates provider output without splitting a boundary emoji", async () => {
45+
await withVideoFixture("openclaw-video-utf16-output", async ({ ctx, media, cache }) => {
46+
const prefix = "v".repeat(79);
47+
const result = await runCapability({
48+
capability: "video",
49+
cfg: {
50+
models: {
51+
providers: {
52+
moonshot: {
53+
apiKey: "test-key",
54+
models: [],
55+
},
56+
},
57+
},
58+
tools: {
59+
media: {
60+
video: {
61+
enabled: true,
62+
models: [{ provider: "moonshot", model: "kimi-k2.5", maxChars: 80 }],
63+
},
64+
},
65+
},
66+
} as unknown as OpenClawConfig,
67+
ctx,
68+
attachments: cache,
69+
media,
70+
providerRegistry: new Map<string, MediaUnderstandingProvider>([
71+
[
72+
"moonshot",
73+
{
74+
id: "moonshot",
75+
capabilities: ["video"],
76+
describeVideo: async (req) => ({
77+
text: `${prefix}${String.fromCodePoint(0x1f600)}tail`,
78+
model: req.model,
79+
}),
80+
},
81+
],
82+
]),
83+
});
84+
85+
const output = requireCapabilityOutput(result, 0);
86+
expect(output.text).toBe(prefix);
87+
expect(output.text).not.toContain(String.fromCharCode(0xd83d));
88+
});
89+
});
90+
4491
it("merges video baseUrl and headers with entry precedence", async () => {
4592
let seenBaseUrl: string | undefined;
4693
let seenHeaders: Record<string, string> | undefined;

0 commit comments

Comments
 (0)