diff --git a/lib/ai/canvas-agent-runtime/review.ts b/lib/ai/canvas-agent-runtime/review.ts index d16f8fe13..77569ff99 100644 --- a/lib/ai/canvas-agent-runtime/review.ts +++ b/lib/ai/canvas-agent-runtime/review.ts @@ -87,6 +87,7 @@ export async function analyzeRuntimeImages(params: { maxRetries: 0, abortSignal: signal ? AbortSignal.any([signal, AbortSignal.timeout(60_000)]) : AbortSignal.timeout(60_000), }); + if (answer.finishReason === "content-filter") throw new Error("content policy rejection"); const raw = answer.text.trim().replace(/^```(?:json)?\s*|\s*```$/g, ""); let review: z.infer; try { review = reviewSchema.parse(JSON.parse(raw)); } catch { throw new AgentModelProtocolError(); } diff --git a/lib/ai/runtime/agent-model-failover.ts b/lib/ai/runtime/agent-model-failover.ts index ac91dbea8..6b16e4c85 100644 --- a/lib/ai/runtime/agent-model-failover.ts +++ b/lib/ai/runtime/agent-model-failover.ts @@ -52,7 +52,8 @@ export function agentModelFault(error: unknown, signal?: AbortSignal): AgentMode if (status === 404 || /model.not.found|invalid.model/.test(message)) return { reason: "model_unavailable", status }; if (status === 408) return { reason: "connection_or_timeout", status }; if (status && status >= 500) return { reason: "upstream_unavailable", status }; - if (e?.name === "AgentModelProtocolError") return { reason: "invalid_response", status }; + if (["AgentModelProtocolError", "AI_NoOutputGeneratedError", "AI_InvalidResponseDataError", "AI_JSONParseError"].includes(String(e?.name)) || + /未返回可用的回答|未返回所要求的工具|empty.response|no.output.generated/.test(message)) return { reason: "invalid_response", status }; if (/timeout|timed out|fetch failed|econnreset|econnrefused|network|socket|terminated/.test(message) || e?.name === "TimeoutError") return { reason: "connection_or_timeout", status }; return e?.cause ? agentModelFault(e.cause, signal) : null; } diff --git a/lib/ai/zcode-agent/model-gateway-protocol.ts b/lib/ai/zcode-agent/model-gateway-protocol.ts index ca531248c..4373485c3 100644 --- a/lib/ai/zcode-agent/model-gateway-protocol.ts +++ b/lib/ai/zcode-agent/model-gateway-protocol.ts @@ -24,6 +24,8 @@ export function toResponsesRequest(body: RecordValue): RecordValue { } export function responsesCompletion(payload: RecordValue): RecordValue { + if (/content.?policy|content.?filter|safety|moderation/.test(JSON.stringify(payload.error || {}).toLowerCase()) || + payload.output?.some((item: RecordValue) => item.content?.some((part: RecordValue) => part.type === "refusal"))) throw new Error("content policy rejection"); if (payload.error || payload.status !== "completed" || !Array.isArray(payload.output)) throw new AgentModelProtocolError(); const calls = payload.output.filter((item: RecordValue) => item.type === "function_call") .map((item: RecordValue) => ({ id: item.call_id, type: "function", function: { name: item.name, arguments: item.arguments } })); @@ -37,6 +39,7 @@ export function responsesCompletion(payload: RecordValue): RecordValue { export function completionResponse(payload: RecordValue, request: RecordValue) { const choice = payload?.choices?.[0]; const message = choice?.message; + if (choice?.finish_reason === "content_filter" || message?.refusal) throw new Error("content policy rejection"); if (payload?.error || !message || !["stop", "tool_calls", "function_call"].includes(choice.finish_reason)) throw new AgentModelProtocolError(); const calls = message.tool_calls || []; if (!Array.isArray(calls) || (!message.content && !calls.length)) throw new AgentModelProtocolError(); diff --git a/tests/integration/agent-multimodal-failover.test.ts b/tests/integration/agent-multimodal-failover.test.ts index 1927ceb95..0628ddac8 100644 --- a/tests/integration/agent-multimodal-failover.test.ts +++ b/tests/integration/agent-multimodal-failover.test.ts @@ -94,3 +94,12 @@ test("complete valid replies become native SSE; truncated/invalid tool replies a completion.choices[0].message.tool_calls[0].function.arguments = '{"broken"'; assert.throws(() => completionResponse(completion, request), AgentModelProtocolError); }); + + +test("successful HTTP policy refusals are not retried as provider faults", () => { + let rejection: unknown; + try { completionResponse({ choices: [{ finish_reason: "content_filter", message: { content: "" } }] }, {}); } catch (error) { rejection = error; } + assert(rejection); assert.equal(agentModelFault(rejection), null); + assert.throws(() => responsesCompletion({ status: "completed", output: [{ type: "message", content: [{ type: "refusal" }] }] }), /content policy/); + assert.equal(agentModelFault(Object.assign(new Error("No output generated"), { name: "AI_NoOutputGeneratedError" }))?.reason, "invalid_response"); +});