can1357/oh-my-pi · error · ToolError
completion() did not return a structured response matching t
Error message
completion() did not return a structured response matching the schema.
What it means
With a `schema` supplied, the bridge expects either a forced `respond` tool call or JSON text it can parse with `parseJsonPayload`. When the model returns non-empty plain text that is not parseable JSON, this ToolError is thrown. It means the model answered but not in the structured shape requested.
Source
Thrown at packages/coding-agent/src/eval/completion-bridge.ts:192
throw new ToolError(response.errorMessage ?? "completion() request failed.");
}
if (response.stopReason === "aborted") {
throw new ToolError("completion() request aborted.");
}
let resultText: string;
if (schema) {
const call = extractToolCall(response, STRUCTURED_TOOL_NAME);
let value: unknown;
if (call) {
value = call.arguments;
} else {
const text = extractTextContent(response);
if (!text) throw new ToolError("completion() returned no structured response.");
try {
value = parseJsonPayload(text);
} catch {
throw new ToolError("completion() did not return a structured response matching the schema.");
}
}
resultText = JSON.stringify(value);
} else {
resultText = extractTextContent(response);
if (!resultText) throw new ToolError("completion() returned no text output.");
}
options.emitStatus?.({
op: "completion",
model: formatModelString(model),
tier: finalTier,
chars: resultText.length,
});
return {
text: resultText,
details: { model: formatModelString(model), tier: finalTier, structured: Boolean(schema) },View on GitHub (pinned to 9690622007)
Solutions
- Retry, or move to a stronger tier (`model: "slow"`) that reliably makes the forced tool call.
- Reword the prompt to demand strict JSON with no prose ("Respond only with JSON matching ...").
- Simplify the schema — fewer/nested-fewer fields improves tool-call compliance.
- In the caller, catch this error and fall back to a second attempt or a heuristic parse of the raw text.
Example fix
// before
const v = JSON.parse((await completion(q, { schema })).text); // may throw
// after: tolerate prose replies
try {
var val = JSON.parse((await completion(q, { schema })).text);
} catch {
var val = await completion(q + " Respond ONLY with JSON.", { model: "slow", schema });
} Defensive patterns
Strategy: retry
Try / catch
try {
const r = await completion(prompt, { schema });
return JSON.parse(r.text);
} catch (e) {
if (String(e).includes("structured response")) {
const r2 = await completion(prompt + "\nRespond ONLY with JSON.", { model: "slow", schema });
return JSON.parse(r2.text);
}
throw e;
} Prevention
- Prefer tiers with strong tool-call compliance for schema work.
- Phrase prompts to demand raw JSON with no commentary.
- Validate/sanitize model text output before parsing (strip fences).
- Retry with an upgraded tier on structured-output failures.
When it happens
Trigger: `completion(prompt, { schema })` where the model declines the tool call and replies with prose like "Here is the data: ..." or a fenced code block with trailing commentary that `parseJsonPayload` cannot extract.
Common situations: Weaker models ignoring toolChoice on providers that don't enforce it; models wrapping JSON in markdown fences with explanation text; prompts that ask a question conversationally so the model answers in prose; schemas too complex for a smol model.
Related errors
- completion() returned no structured response.
- Gemini Files API ${context} response is not a JSON object
- ${context} returned an invalid JSON object
- invalid JSON: ${error instanceof Error ? error.message : Str
- metadata root is not an object
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/94c8eb65e9eb870c.
Report an issue: GitHub.