Il s’agit d’une erreur d’ordre des messages, pas d’une erreur d’outil. Claude exige que chaque bloc tool_use d’un tour assistant reçoive une réponse sous forme de bloc tool_result dans le tour user immédiatement suivant — mêmes identifiants, rien entre les deux. Votre boucle en a omis un, généralement parce que l’outil a levé une exception.
L’erreur
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "messages.1: tool_use ids were found without tool_result blocks immediately after: toolu_01A... Each tool_use block must have a corresponding tool_result block in the next message."
}
}Causes et solutions en bref
| Cause | Solution |
|---|---|
| Votre outil a levé une exception, donc rien n’a été ajouté | Envoyez quand même un tool_result, avec is_error: true et le texte de l’erreur. |
| Le tool_result est arrivé dans un message ultérieur | Il doit se trouver dans le message immédiatement suivant — aucun tour assistant ou user ne doit s’intercaler. |
| tool_use_id ne correspond pas | Renvoyez l’identifiant exact du bloc tool_use ; n’en générez pas un nouveau. |
| Historique tronqué au milieu d’un aller-retour | Tronquez uniquement entre des allers-retours complets, jamais entre les deux moitiés d’un même aller-retour. |
L’invariant, énoncé une fois pour toutes
Chaque bloc tool_use d’un tour assistant nécessite exactement un bloc tool_result dans le message user immédiatement suivant, avec le même tool_use_id. Si un tour contient plusieurs blocs tool_use, ce message suivant doit contenir plusieurs blocs tool_result. Rien ne doit s’intercaler entre les deux tours.
Répondez toujours, même lorsque l’outil a échoué
Le modèle gère parfaitement un outil en échec ; il ne gère pas un outil manquant. Renvoyer l’erreur sous forme de tool_result maintient la conversation valide et permet généralement une récupération cohérente au lieu d’une erreur 400.
results = []
for block in (b for b in resp.content if b.type == "tool_use"):
try:
out = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(out),
})
except Exception as e:
# A failed tool still owes the model an answer.
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Tool failed: {e}",
"is_error": True,
})
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": results})Validez les deux derniers tours avant l’envoi
Une douzaine de lignes d’assertions détectent le problème au point d’appel plutôt qu’après une erreur 400 du réseau : parcourez les tool_use ids du tour assistant et vérifiez que le tour user suivant répond à chacun d’eux.
def check_pairs(messages):
for i, m in enumerate(messages):
if m["role"] != "assistant" or not isinstance(m.get("content"), list):
continue
ids = {b.get("id") for b in m["content"]
if isinstance(b, dict) and b.get("type") == "tool_use"}
if not ids:
continue
nxt = messages[i + 1] if i + 1 < len(messages) else None
answered = {b.get("tool_use_id") for b in (nxt or {}).get("content", [])
if isinstance(b, dict) and b.get("type") == "tool_result"}
missing = ids - answered
assert not missing, f"message {i}: unanswered tool_use {missing}"Tronquez l’historique aux frontières des allers-retours
Une troncature de la fenêtre de contexte fondée sur le nombre de messages finira par couper entre un tool_use et son tool_result. Traitez la paire comme une unité indivisible lorsque vous décidez quoi supprimer.
Si vous appelez via Kunavo
Le problème vient de votre contenu, et Kunavo ne le masque pas : 400 figure parmi les erreurs pour lesquelles il ne faut pas réessayer ; une boucle d’outil mal formée échoue donc une seule fois au lieu de consommer un second aller-retour amont pour atteindre la même erreur, et la requête rejetée est enregistrée avec un coût nul. Sur /v1/messages, vous utilisez directement le protocole Messages ; les blocs tools, tool_use et tool_result sont donc transmis sans traduction. Le 400 revient alors typé invalid_request_error, comme dans l’API d’Anthropic, avec le message de l’amont suivi de son propre identifiant de requête et sans champ request_id ; jusqu’au 24 septembre 2026, le type était api_error, alors utilisez le statut HTTP et le texte du message si vos journaux sont antérieurs.
Questions fréquentes
Puis-je simplement supprimer le tour tool_use au lieu d’y répondre ?
Oui, si vous supprimez le tour assistant entier. Ce qui est invalide, c’est de conserver le tool_use et d’omettre son tool_result.
Le point de terminaison compatible OpenAI applique-t-il la même règle ?
Le même appariement est requis, avec une autre syntaxe : tool_calls dans le message assistant, puis un message role: "tool" par appel, contenant tool_call_id.
Une requête rejetée est-elle facturée ?
Sur Kunavo, non. Les requêtes échouées sont enregistrées avec un coût nul et n’atteignent jamais un appel amont facturé.
Guides associés
- model_not_found / 404 — noms de modèles entre Claude, Gemini et les passerelles
- Claude API 429 rate_limit_error — causes et solution durable
La sémantique détaillée des erreurs est disponible dans référence des erreurs ; obtenir une clé prend une minute via inscription et la guide d’authentification.