fix(agent): reject malformed model envelopes before tool execution
This commit is contained in:
parent
c0ff503c85
commit
bd27401b6c
@ -52,7 +52,7 @@ 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 (["AgentModelProtocolError", "AI_NoOutputGeneratedError", "AI_InvalidResponseDataError", "AI_JSONParseError"].includes(String(e?.name)) ||
|
||||
if (["AgentModelProtocolError", "AI_NoOutputGeneratedError", "AI_InvalidResponseDataError", "AI_TypeValidationError", "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;
|
||||
|
||||
@ -24,8 +24,9 @@ export function toResponsesRequest(body: RecordValue): RecordValue {
|
||||
}
|
||||
|
||||
export function responsesCompletion(payload: RecordValue): RecordValue {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new AgentModelProtocolError();
|
||||
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");
|
||||
Array.isArray(payload.output) && payload.output.some((item: RecordValue) => Array.isArray(item?.content) && 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 } }));
|
||||
@ -42,10 +43,12 @@ export function completionResponse(payload: RecordValue, request: RecordValue) {
|
||||
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();
|
||||
if ((message.content != null && typeof message.content !== "string") || !Array.isArray(calls) || (!message.content && !calls.length)) throw new AgentModelProtocolError();
|
||||
const allowed = new Set((request.tools || []).map((tool: RecordValue) => tool.function?.name));
|
||||
const seenIds = new Set<string>();
|
||||
for (const call of calls) {
|
||||
if (!call.id || !allowed.has(call.function?.name) || typeof call.function?.arguments !== "string") throw new AgentModelProtocolError();
|
||||
if (!call || typeof call.id !== "string" || !call.id || seenIds.has(call.id) || call.type !== "function" || !allowed.has(call.function?.name) || typeof call.function?.arguments !== "string") throw new AgentModelProtocolError();
|
||||
seenIds.add(call.id);
|
||||
try { const args = JSON.parse(call.function.arguments); if (!args || typeof args !== "object" || Array.isArray(args)) throw new Error(); }
|
||||
catch { throw new AgentModelProtocolError(); }
|
||||
}
|
||||
|
||||
@ -103,3 +103,13 @@ test("successful HTTP policy refusals are not retried as provider faults", () =>
|
||||
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");
|
||||
});
|
||||
|
||||
|
||||
test("malformed protocol envelopes and duplicate tool IDs are classified before native execution", () => {
|
||||
for (const value of [null, [], { status: "completed", output: {} }]) {
|
||||
assert.throws(() => responsesCompletion(value as any), AgentModelProtocolError);
|
||||
}
|
||||
assert.throws(() => completionResponse({ choices: [{ finish_reason: "stop", message: { content: {} } }] }, {}), AgentModelProtocolError);
|
||||
const call = { id: "duplicate", type: "function", function: { name: "readCanvas", arguments: "{}" } };
|
||||
assert.throws(() => completionResponse({ choices: [{ finish_reason: "tool_calls", message: { tool_calls: [call, call] } }] }, { tools: [{ function: { name: "readCanvas" } }] }), AgentModelProtocolError);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user