Skip to content

Commit be166b9

Browse files
committed
fix(agent): retry empty anthropic-compatible replies
1 parent 930852a commit be166b9

3 files changed

Lines changed: 72 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
3434

3535
### Fixes
3636

37+
- Agents: retry empty final turns for generic `anthropic-messages` providers instead of limiting non-visible recovery to Kimi, so custom/proxied Anthropic-compatible routes can recover with a visible answer. Addresses #46080. Thanks @wmgx, @w1tv, and @iFwu.
3738
- Control UI: rotate browser service-worker caches per build so updated Gateways are less likely to keep serving stale dashboard bundles that trigger protocol mismatch errors.
3839
- Discord: report unresolved configured bot-token SecretRefs during startup instead of treating the account as unconfigured. (#82009) Thanks @giodl73-repo.
3940
- CLI/config: preserve numeric-looking object keys such as Discord guild IDs during `config patch` recursive merges. (#81999) Thanks @giodl73-repo.

src/agents/pi-embedded-runner/run.incomplete-turn.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,75 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
817817
expectWarnMessageWith("empty response detected");
818818
});
819819

820+
it("retries empty Anthropic-compatible stop turns even when the provider is not Kimi", async () => {
821+
mockedClassifyFailoverReason.mockReturnValue(null);
822+
mockedResolveModelAsync.mockResolvedValue({
823+
model: {
824+
id: "claude-opus-4-7",
825+
provider: "sub2api",
826+
contextWindow: 200000,
827+
api: "anthropic-messages",
828+
},
829+
error: null,
830+
authStorage: {
831+
setRuntimeApiKey: vi.fn(),
832+
},
833+
modelRegistry: {},
834+
});
835+
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
836+
makeAttemptResult({
837+
assistantTexts: [],
838+
lastAssistant: {
839+
role: "assistant",
840+
api: "anthropic-messages",
841+
stopReason: "stop",
842+
provider: "sub2api",
843+
model: "claude-opus-4-7",
844+
content: [],
845+
usage: {
846+
input: 2048,
847+
output: 3100,
848+
cacheRead: 0,
849+
cacheWrite: 0,
850+
totalTokens: 5148,
851+
},
852+
} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
853+
}),
854+
);
855+
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
856+
makeAttemptResult({
857+
assistantTexts: ["Visible Anthropic-compatible answer."],
858+
lastAssistant: {
859+
role: "assistant",
860+
api: "anthropic-messages",
861+
stopReason: "stop",
862+
provider: "sub2api",
863+
model: "claude-opus-4-7",
864+
content: [{ type: "text", text: "Visible Anthropic-compatible answer." }],
865+
usage: {
866+
input: 2300,
867+
output: 8,
868+
cacheRead: 0,
869+
cacheWrite: 0,
870+
totalTokens: 2308,
871+
},
872+
} as unknown as EmbeddedRunAttemptResult["lastAssistant"],
873+
}),
874+
);
875+
876+
await runEmbeddedPiAgent({
877+
...overflowBaseRunParams,
878+
provider: "sub2api",
879+
model: "claude-opus-4-7",
880+
runId: "run-empty-anthropic-compatible-stop-continuation",
881+
});
882+
883+
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
884+
const secondCall = runAttemptCall(1);
885+
expect(secondCall.prompt).toContain(EMPTY_RESPONSE_RETRY_INSTRUCTION);
886+
expectWarnMessageWith("empty response detected");
887+
});
888+
820889
it("surfaces an error after exhausting empty-response retries", async () => {
821890
mockedClassifyFailoverReason.mockReturnValue(null);
822891
mockedRunEmbeddedAttempt.mockResolvedValue(

src/agents/pi-embedded-runner/run/incomplete-turn.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ const GEMINI_INCOMPLETE_TURN_MODEL_ID_PATTERN = /^gemini(?:[.-]|$)/;
131131
// Ollama native `/api/chat` can finish with only thinking/internal blocks when
132132
// constrained, but it should not inherit the stricter planning-only/ack prompts.
133133
const OLLAMA_INCOMPLETE_TURN_PROVIDER_ID_PATTERN = /^ollama(?:-|$)/;
134-
const KIMI_INCOMPLETE_TURN_PROVIDER_ID_PATTERN = /^kimi(?:-|$)/;
135134
const DEFAULT_PLANNING_ONLY_RETRY_LIMIT = 1;
136135
const STRICT_AGENTIC_PLANNING_ONLY_RETRY_LIMIT = 2;
137136
// Allow one immediate continuation plus one follow-up continuation before
@@ -620,14 +619,9 @@ function shouldApplyNonVisibleTurnRetryGuard(params: {
620619
if (shouldApplyPlanningOnlyRetryGuard(params)) {
621620
return true;
622621
}
623-
if (normalizeLowercaseStringOrEmpty(params.modelApi ?? "") === "openai-completions") {
624-
return true;
625-
}
626622
if (
627-
normalizeLowercaseStringOrEmpty(params.modelApi ?? "") === "anthropic-messages" &&
628-
KIMI_INCOMPLETE_TURN_PROVIDER_ID_PATTERN.test(
629-
normalizeLowercaseStringOrEmpty(params.provider ?? ""),
630-
)
623+
normalizeLowercaseStringOrEmpty(params.modelApi ?? "") === "openai-completions" ||
624+
normalizeLowercaseStringOrEmpty(params.modelApi ?? "") === "anthropic-messages"
631625
) {
632626
return true;
633627
}

0 commit comments

Comments
 (0)