mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix(anthropic): recover orphan text deltas
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Plugins: reuse gateway-bindable plugin loader cache entries for later default-mode loads without serving default-built registries to gateway-bound requests, reducing repeated plugin registration during dispatch. Refs #61756. Thanks @DmitryPogodaev.
|
||||
- Gateway/secrets: include the caught error message in `secrets.reload` and `secrets.resolve` warning logs while keeping RPC errors generic, so operators can diagnose reload and permission failures. Thanks @davidangularme.
|
||||
- Anthropic-compatible streams: recover text deltas that arrive before their matching content block, so Kimi Code and similar providers do not finish as empty `incomplete_result` replies. Fixes #76007. Thanks @vliuyt.
|
||||
- fix(infra): block workspace state-directory env override [AI]. (#75940) Thanks @pgondhi987.
|
||||
- MCP/OpenAI: normalize parameter-free tool schemas whose top-level object `properties` is missing, null, or invalid before sending tools to OpenAI, so MCP tools without params stay usable. Fixes #75362. Thanks @tolkonepiu and @SymbolStar.
|
||||
- TTS: honor explicit short `[[tts:text]]...[[/tts:text]]` blocks while keeping untagged short auto-TTS suppressed, so tagged voice replies are synthesized instead of being dropped as empty voice-only payloads. Fixes #73758. Thanks @yfge.
|
||||
|
||||
@@ -533,6 +533,65 @@ describe("anthropic transport stream", () => {
|
||||
expect(result.usage.output).toBe(9);
|
||||
});
|
||||
|
||||
it("recovers orphan text deltas when an Anthropic-compatible provider omits block start", async () => {
|
||||
guardedFetchMock.mockResolvedValueOnce(
|
||||
createSseResponse([
|
||||
{
|
||||
type: "message_start",
|
||||
message: { id: "msg_1", usage: { input_tokens: 6, output_tokens: 0 } },
|
||||
},
|
||||
{
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: { type: "text_delta", text: "你好" },
|
||||
},
|
||||
{
|
||||
type: "content_block_stop",
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
type: "message_delta",
|
||||
delta: { stop_reason: "end_turn" },
|
||||
usage: { input_tokens: 6, output_tokens: 1 },
|
||||
},
|
||||
]),
|
||||
);
|
||||
const streamFn = createAnthropicMessagesTransportStreamFn();
|
||||
const stream = await Promise.resolve(
|
||||
streamFn(
|
||||
makeAnthropicTransportModel({
|
||||
provider: "kimi-coding",
|
||||
baseUrl: "https://api.kimi.com/coding/",
|
||||
}),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
} as Parameters<typeof streamFn>[1],
|
||||
{
|
||||
apiKey: "kimi-key",
|
||||
} as Parameters<typeof streamFn>[2],
|
||||
),
|
||||
);
|
||||
const events: Array<{ type?: string; delta?: string; content?: string }> = [];
|
||||
for await (const event of stream as AsyncIterable<{
|
||||
type?: string;
|
||||
delta?: string;
|
||||
content?: string;
|
||||
}>) {
|
||||
events.push(event);
|
||||
}
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.content).toEqual([{ type: "text", text: "你好" }]);
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(events).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: "text_start" }),
|
||||
expect.objectContaining({ type: "text_delta", delta: "你好" }),
|
||||
expect.objectContaining({ type: "text_end", content: "你好" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips malformed tools when building Anthropic payloads", async () => {
|
||||
await runTransportStream(
|
||||
makeAnthropicTransportModel(),
|
||||
|
||||
@@ -1018,9 +1018,20 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
|
||||
continue;
|
||||
}
|
||||
if (event.type === "content_block_delta") {
|
||||
const index = blocks.findIndex((block) => block.index === event.index);
|
||||
const block = blocks[index];
|
||||
const delta = event.delta as Record<string, unknown> | undefined;
|
||||
let index = blocks.findIndex((block) => block.index === event.index);
|
||||
let block = blocks[index];
|
||||
if (!block && delta?.type === "text_delta" && typeof delta.text === "string") {
|
||||
const recoveredIndex = typeof event.index === "number" ? event.index : blocks.length;
|
||||
block = { type: "text", text: "", index: recoveredIndex };
|
||||
output.content.push(block);
|
||||
index = output.content.length - 1;
|
||||
stream.push({
|
||||
type: "text_start",
|
||||
contentIndex: index,
|
||||
partial: output as never,
|
||||
});
|
||||
}
|
||||
if (
|
||||
block?.type === "text" &&
|
||||
delta?.type === "text_delta" &&
|
||||
|
||||
Reference in New Issue
Block a user