{"record":{"id":"bf74448c40a5c772","repo":"hiyouga/LlamaFactory","slug":"invalid-tools","errorCode":null,"errorMessage":"Invalid tools","messagePattern":"Invalid tools","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"src/llamafactory/api/chat.py","lineNumber":174,"sourceCode":"                        check_ssrf_url(audio_url)\n                        audio_stream = requests.get(audio_url, stream=True).raw\n\n                    audios.append(audio_stream)\n                else:\n                    raise HTTPException(\n                        status_code=status.HTTP_400_BAD_REQUEST, detail=f\"Invalid input type {input_item.type}.\"\n                    )\n\n            input_messages.append({\"role\": ROLE_MAPPING[message.role], \"content\": text_content})\n        else:\n            input_messages.append({\"role\": ROLE_MAPPING[message.role], \"content\": message.content})\n\n    tool_list = request.tools\n    if isinstance(tool_list, list) and len(tool_list):\n        try:\n            tools = json.dumps([dictify(tool.function) for tool in tool_list], ensure_ascii=False)\n        except json.JSONDecodeError:\n            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Invalid tools\")\n    else:\n        tools = None\n\n    return input_messages, system, tools, images or None, videos or None, audios or None\n\n\ndef _create_stream_chat_completion_chunk(\n    completion_id: str,\n    model: str,\n    delta: \"ChatCompletionMessage\",\n    index: Optional[int] = 0,\n    finish_reason: Optional[\"Finish\"] = None,\n) -> str:\n    choice_data = ChatCompletionStreamResponseChoice(index=index, delta=delta, finish_reason=finish_reason)\n    chunk = ChatCompletionStreamResponse(id=completion_id, model=model, choices=[choice_data])\n    return jsonify(chunk)\n\n","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/hiyouga/LlamaFactory/blob/f28afaf6355af515454dfb16c97d728307c93897/src/llamafactory/api/chat.py#L156-L192","documentation":"Raised as HTTP 400 when the request includes a non-empty tools array but serializing each tool's .function via dictify() raises json.JSONDecodeError. In practice this means a tool definition is malformed enough that its function object cannot be dictified and dumped — e.g. custom/non-FunctionTool objects or arguments already containing broken JSON.","triggerScenarios":"POST /v1/chat/completions with tools: [...] where a tool entry is not a proper {type:'function', function:{name, description, parameters}} object; or where function.arguments is a string containing invalid JSON that the model/round-trip previously mangled.","commonSituations":"Sending back assistant tool_calls payloads as tools; using a client SDK that emits a different tool schema; hand-written tool JSON with missing 'function' wrapper; proxying another provider's tool format verbatim.","solutions":["Ensure each tool is {type: 'function', function: {name, description?, parameters (JSON schema)}}.","Validate each tool with json.dumps(tool.function) client-side before sending; if it throws, fix the object.","Do not confuse tools (definitions) with messages containing role 'tool' (results) — only send definitions in tools.","Simplify to a single minimal tool and re-add tools one by one to find the malformed entry."],"exampleFix":"// before\ntools: [{ name: 'get_weather', parameters: {type:'object'} }]  // missing function wrapper\n// after\ntools: [{\n  type: 'function',\n  function: { name: 'get_weather', description: 'Get weather', parameters: {type:'object', properties:{city:{type:'string'}}, required:['city']} }\n}]","handlingStrategy":"validation","validationCode":"import json\ndef tools_serializable(tools):\n    if not tools:\n        return True\n    try:\n        for t in tools:\n            json.dumps(t.get(\"function\", t), default=str)\n        return True\n    except (TypeError, ValueError, KeyError):\n        return False","typeGuard":"const isToolDef = (t) => t?.type === 'function' && typeof t.function?.name === 'string';\nconst allToolsValid = (tools) => !tools || tools.every(isToolDef);","tryCatchPattern":"try { ... } catch (e) { if (e.status === 400 && e.detail === 'Invalid tools') { tools = tools.filter(isToolDef); /* resend without the malformed tool */ } }","preventionTips":["Define tools with a schema validator (zod/pydantic) before every request.","Never put assistant tool_call messages into the tools field.","Unit-test your tool builder against json round-tripping."],"tags":["api","tools","function-calling","validation","http-400"],"backgroundTag":null,"analyzedSha":"f28afaf6355af515454dfb16c97d728307c93897","analyzedAt":"2026-08-14T21:57:28.298Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}