Einladen & verdienen

So funktionieren Einladungsboni

Teile deinen Einladungslink. Registriert sich ein Freund darüber und lädt Guthaben auf, erhältst du die angezeigte Prämie für seine weiteren Aufladungen.

Anthropic Messages, Chat Completions und Responses: Auswahl, Konvertierung und Grenzen der Kompatibilität

HTTP 200 bedeutet nicht, dass ein Agent kompatibel ist. Drei vollständige Tool-Zyklen zeigen die tatsächlichen Unterschiede zwischen Messages, Chat Completions und Responses sowie Migration, Tests und Fehlersuche.

Inhalt

Ein Agent kann nach dem Austausch von Endpoint und Feldnamen weiterhin 200 erhalten und trotzdem keine Tools mehr ausführen, JSON außerhalb des Schemas liefern oder plötzlich den Kontext über mehrere Runden verlieren. Meist ist das Modell nicht „schlechter geworden“. Die Anwendung hat vielmehr drei unterschiedliche Protokolle wie eine einzige Schnittstelle behandelt.

Eine erfolgreiche Migration muss mindestens auf drei Ebenen geprüft werden:

  1. Das Format wird akzeptiert: Der Server kann den Request verarbeiten und liefert einen Erfolgsstatus.
  2. Das Verhalten ist gleichwertig: Tools werden aufgerufen, Ergebnisse korrekt zurückgegeben, Streams vollständig beendet und der Mehrturn-Kontext bleibt konsistent.
  3. Die Fähigkeiten bleiben erhalten: Strikte Schemas, nativer Reasoning-Zustand, gehostete Tools, strukturierte Ausgaben und ähnliche Funktionen werden nicht stillschweigend ignoriert oder herabgestuft.

HTTP 200 belegt nur die erste Ebene. Für einen Request, der lediglich einen Textblock erzeugt, reicht eine einfache Konvertierung oft aus. Sobald ein Agent Tools, gestreamte Argumente, Mehrturn-Zustand oder Reasoning-Modelle verwendet, muss die gesamte Interaktionskette validiert werden.

Praktische Entscheidung: Das Protokoll richtet sich nach Client und benötigten Fähigkeiten, nicht nach dem Modellnamen

SzenarioGeeigneter AusgangspunktWarum
Eine bestehende Anwendung nutzt OpenAI SDK und messages bereits stabilChat CompletionsGeringster Änderungsaufwand; die vorhandene Nachrichten- und Tool-Schleife kann bestehen bleiben
Ein neuer OpenAI-Agent benötigt gehostete Tools, typisierte Items oder serverseitige ZustandsfortsetzungResponsesOpenAI empfiehlt diese Schnittstelle derzeit für neue Projekte, und sie bietet mehr Agent-Funktionen
Claude Code, eine native Claude-Anwendung oder ein Ablauf mit Claude-spezifischen FähigkeitenAnthropic MessagesInhaltsblöcke, Tool-Ergebnisse, thinking und weiteres Verhalten folgen dem nativen Vertrag von Anthropic
Eigenes Gateway oder Multi-Modell-RouterFür jedes Upstream-Protokoll einen eigenen Adapter behaltenEin einzelnes „Universal-JSON“ kann nicht alle nativen Fähigkeiten verlustfrei darstellen

OpenAI unterstützt Chat Completions weiterhin. Eine stabile Anwendung muss daher nicht sofort neu geschrieben werden, nur weil es eine neuere Schnittstelle gibt. Eine Migration ist für neue Projekte oder beim Bedarf an Responses-nativen Funktionen sinnvoller. Anthropic Messages ist ebenfalls keine OpenAI-Schnittstelle, bei der nur ein Feld in messages umbenannt wurde: Inhaltsblöcke, Tool-Übergabe, Stream-Ereignisse und Zustandsregeln bilden einen eigenen Vertrag.

Zentrale Unterschiede der drei APIs

Mit Completions ist in diesem Artikel Chat Completions gemeint, nicht der alte Endpoint /v1/completions.

DimensionOpenAI Chat CompletionsOpenAI ResponsesAnthropic Messages
Endpoint/v1/chat/completions/v1/responses/v1/messages
HaupteingabemessagesItems in input; einfache Nachrichteneingaben sind ebenfalls möglichmessages, üblicherweise mit separatem system auf oberster Ebene
Hauptausgabechoices[].messageTypisierte Items in output[]Inhaltsblöcke in content[]
Tool-Definitiontools[].functionname und parameters direkt in tools[]input_schema in tools[]
Tool-Argumentefunction.arguments, JSON-Stringarguments, JSON-Stringtool_use.input, JSON-Objekt
Korrelations-IDtool_calls[].idcall_idtool_use.id
Tool-Ergebnis zurückgebenrole: "tool" + tool_call_idfunction_call_output + call_idtool_result + tool_use_id in einer user-Nachricht
Mehrturn-ZustandAnwendung sendet den Nachrichtenverlauf erneutItems erneut senden, previous_response_id oder ConversationsAnwendung sendet Nachrichten und Inhaltsblöcke erneut
Finale strukturierte Ausgaberesponse_formattext.formatoutput_config.format
Streamingchoices[].deltaTypisierte Responses-Ereignissemessage/content-block-Ereignisse

Die Tabelle kann den Eindruck erwecken, es gehe nur um andere Feldnamen. Die eigentlichen Fehler treten meist beim zweiten Request auf: Wie führt die Anwendung den Tool-Aufruf aus, welche ID muss sie behalten und mit welcher Rolle und Reihenfolge gibt sie das Ergebnis zurück? Im Folgenden wird dieselbe nebenwirkungsfreie Aufgabe in allen drei Protokollen vollständig durchlaufen.

Gemeinsames Beispiel: einen Testtarif abrufen

Die Nutzerfrage lautet:

Rufe den Tarif team ab und sage mir, ob Mehrverbrauch nutzungsbasiert abgerechnet werden kann.

Das Tool heißt get_plan_info. Es liest nur feste lokale Daten und erzeugt keine externen Nebenwirkungen, wodurch es sich für Protokoll-Migrationstests eignet.

Die folgenden Tarifdaten sind synthetische Lehrdaten. Sie beschreiben keine realen Tarife, Preise oder Leistungsrechte von OpenAI, Anthropic oder BetterToken. Die drei Request-/Response-Sequenzen zeigen die Protokollstruktur und sind keine Aufzeichnungen echter API-Ausführungen.

Das anwendungsseitige Tool lässt sich als protokollunabhängige Funktion schreiben:

from __future__ import annotations

import json
from typing import Any


PLAN_FIXTURES: dict[str, dict[str, Any]] = {
    "team": {
        "plan_code": "team",
        "display_name": "Team",
        "billing_mode": "usage_based",
        "included_requests": 10_000,
        "overage_allowed": True,
        "source_version": "fixture-2026-09-01",
    }
}


def execute_tool(name: str, raw_arguments: str | dict[str, Any]) -> str:
    """Führt das schreibgeschützte Lehrbeispiel-Tool aus und gibt einen JSON-String zurück, der direkt an das Modell gesendet werden kann."""
    if isinstance(raw_arguments, str):
        arguments = json.loads(raw_arguments)
    elif isinstance(raw_arguments, dict):
        arguments = raw_arguments
    else:
        raise TypeError("Tool-Argumente müssen ein JSON-String oder ein Objekt sein")

    if name != "get_plan_info":
        raise ValueError(f"unbekanntes Tool: {name}")

    if set(arguments) != {"plan_code"}:
        raise ValueError("get_plan_info akzeptiert nur plan_code")

    plan_code = arguments["plan_code"]
    if not isinstance(plan_code, str):
        raise TypeError("plan_code muss ein String sein")

    plan = PLAN_FIXTURES.get(plan_code)
    if plan is None:
        return json.dumps(
            {"ok": False, "error": "plan_not_found", "plan_code": plan_code},
            ensure_ascii=False,
        )

    return json.dumps({"ok": True, "data": plan}, ensure_ascii=False)

Auch bei einem strikten Schema im Request sollte die Anwendung ihre eigene Eingabevalidierung beibehalten. Der Strict-Modus begrenzt die vom Modell erzeugten Tool-Argumente, ersetzt aber weder Berechtigungsprüfung noch Enum-Validierung, Idempotenz oder Sicherheitsprüfungen der Geschäftslogik.

Chat Completions: vollständiger Tool-Zyklus

Erster Request: Das Modell soll einen Tool-Aufruf erzeugen

Die folgenden Beispiele verwenden den offiziellen OpenAI-Endpoint zur Darstellung des Protokolls. Bei einem kompatiblen Dienst müssen Base URL, Authentifizierung und Model ID gemäß der Dokumentation des Anbieters ersetzt werden.

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "messages": [
      {
        "role": "system",
        "content": "Du bist ein Tarifassistent. Antworte ausschließlich anhand der vom Tool zurückgegebenen Daten und rate nicht."
      },
      {
        "role": "user",
        "content": "Rufe den Tarif team ab und sage mir, ob Mehrverbrauch nutzungsbasiert abgerechnet werden kann."
      }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_plan_info",
          "description": "Feste Testdaten anhand des Tarifcodes abrufen",
          "strict": true,
          "parameters": {
            "type": "object",
            "properties": {
              "plan_code": {
                "type": "string",
                "enum": ["team"]
              }
            },
            "required": ["plan_code"],
            "additionalProperties": false
          }
        }
      }
    ],
    "tool_choice": "required",
    "parallel_tool_calls": false
  }'

Die Anwendung muss tool_calls aus der assistant-Nachricht lesen. Die folgende Antwort enthält nur die für den weiteren Ablauf benötigten Felder:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_plan_001",
            "type": "function",
            "function": {
              "name": "get_plan_info",
              "arguments": "{\"plan_code\":\"team\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

Zwei Werte dürfen nicht verloren gehen:

  • tool_calls[0].id: Im zweiten Request unverändert als tool_call_id zurücksenden.
  • function.arguments: Das ist ein JSON-String. Zuerst parsen, anschließend mit eigenem Schema und eigenen Geschäftsregeln validieren.

Tool ausführen:

tool_result = execute_tool(
    "get_plan_info",
    "{\"plan_code\":\"team\"}",
)

Zweiter Request: Tool-Ergebnis an das Modell zurückgeben

Bei Chat Completions muss die assistant-Nachricht mit dem ursprünglichen Tool-Aufruf im Verlauf erhalten bleiben. Danach folgt eine Ergebnisnachricht mit role: "tool".

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "messages": [
      {
        "role": "system",
        "content": "Du bist ein Tarifassistent. Antworte ausschließlich anhand der vom Tool zurückgegebenen Daten und rate nicht."
      },
      {
        "role": "user",
        "content": "Rufe den Tarif team ab und sage mir, ob Mehrverbrauch nutzungsbasiert abgerechnet werden kann."
      },
      {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_plan_001",
            "type": "function",
            "function": {
              "name": "get_plan_info",
              "arguments": "{\"plan_code\":\"team\"}"
            }
          }
        ]
      },
      {
        "role": "tool",
        "tool_call_id": "call_plan_001",
        "content": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
      }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_plan_info",
          "description": "Feste Testdaten anhand des Tarifcodes abrufen",
          "strict": true,
          "parameters": {
            "type": "object",
            "properties": {
              "plan_code": {
                "type": "string",
                "enum": ["team"]
              }
            },
            "required": ["plan_code"],
            "additionalProperties": false
          }
        }
      }
    ]
  }'

Eine repräsentative finale Nachricht lautet:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "Der Tarif Team unterstützt eine nutzungsbasierte Abrechnung von Mehrverbrauch. Die Testdaten enthalten 10.000 Anfragen, und overage_allowed ist true."
      },
      "finish_reason": "stop"
    }
  ]
}

Wenn ein Adapter nur den ersten Nutzerturn konvertiert, die tool_calls der assistant-Nachricht aber nicht speichert oder eine falsche ID in tool_call_id einträgt, setzt der zweite Request nicht denselben Tool-Aufruf fort.

Responses: vollständiger Tool-Zyklus

Responses modelliert Nachrichten, Reasoning, Tool-Aufrufe und Tool-Ergebnisse als unterschiedliche Item-Typen. output[0] darf nicht pauschal als finaler Text behandelt werden; die Verarbeitung muss nach dem type jedes Items verzweigen.

Erster Request: Das Modell soll ein function_call-Item zurückgeben

curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "instructions": "Du bist ein Tarifassistent. Antworte ausschließlich anhand der vom Tool zurückgegebenen Daten und rate nicht.",
    "input": "Rufe den Tarif team ab und sage mir, ob Mehrverbrauch nutzungsbasiert abgerechnet werden kann.",
    "tools": [
      {
        "type": "function",
        "name": "get_plan_info",
        "description": "Feste Testdaten anhand des Tarifcodes abrufen",
        "strict": true,
        "parameters": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "tool_choice": "required",
    "parallel_tool_calls": false,
    "store": false
  }'

Ein repräsentatives Tool-Aufruf-Item:

{
  "id": "resp_plan_001",
  "object": "response",
  "output": [
    {
      "type": "function_call",
      "id": "fc_plan_001",
      "call_id": "call_plan_001",
      "name": "get_plan_info",
      "arguments": "{\"plan_code\":\"team\"}",
      "status": "completed"
    }
  ]
}

Das Tool-Ergebnis wird über call_id zugeordnet. id: "fc_plan_001" ist die ID des Items selbst und darf call_id nicht ersetzen.

Tool ausführen:

tool_result = execute_tool(
    "get_plan_info",
    "{\"plan_code\":\"team\"}",
)

Zweiter Request: ein function_call_output zurückgeben

Das folgende Beispiel verwendet manuelles zustandsloses Replay. Deshalb werden instructions, ursprüngliche Nutzereingabe, Tool-Aufruf und Tool-Ergebnis erneut übermittelt.

curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "instructions": "Du bist ein Tarifassistent. Antworte ausschließlich anhand der vom Tool zurückgegebenen Daten und rate nicht.",
    "input": [
      {
        "role": "user",
        "content": "Rufe den Tarif team ab und sage mir, ob Mehrverbrauch nutzungsbasiert abgerechnet werden kann."
      },
      {
        "type": "function_call",
        "call_id": "call_plan_001",
        "name": "get_plan_info",
        "arguments": "{\"plan_code\":\"team\"}"
      },
      {
        "type": "function_call_output",
        "call_id": "call_plan_001",
        "output": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
      }
    ],
    "tools": [
      {
        "type": "function",
        "name": "get_plan_info",
        "description": "Feste Testdaten anhand des Tarifcodes abrufen",
        "strict": true,
        "parameters": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "store": false
  }'

Ein repräsentatives finales Ausgabe-Item:

{
  "id": "resp_plan_002",
  "object": "response",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "Der Tarif Team unterstützt eine nutzungsbasierte Abrechnung von Mehrverbrauch. Die Testdaten enthalten 10.000 Anfragen, und overage_allowed ist true."
        }
      ]
    }
  ]
}

Bei serverseitiger Zustandsfortsetzung kann die erste Antwort gespeichert und im zweiten Request folgende Form verwendet werden:

{
  "model": "YOUR_OPENAI_MODEL",
  "previous_response_id": "resp_plan_001",
  "input": [
    {
      "type": "function_call_output",
      "call_id": "call_plan_001",
      "output": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"overage_allowed\":true}}"
    }
  ]
}

Eine previous_response_id gehört zum Upstream-Dienst, der die Antwort erzeugt hat. Sie kann nicht an einen anderen Anbieter zur Fortsetzung übergeben werden. Frühere Eingaben werden dadurch auch nicht kostenlos: Laut aktueller OpenAI-Dokumentation werden vorherige Input-token in der Kette weiterhin als input abgerechnet.

Enthält die Antwort ein Reasoning-Item, muss bei zustandslosem Replay auch dieses Item gemäß Dokumentation erhalten bleiben. Es darf nicht zugunsten eines „einheitlichen Formats“ verworfen werden, wenn anschließend weiterhin ein gleichwertiger Reasoning-Kontext behauptet wird.

Anthropic Messages: vollständiger Tool-Zyklus

Messages stellt einen Tool-Aufruf als tool_use-Block im assistant-Inhalt dar und gibt das Ergebnis als tool_result-Block in der nächsten user-Nachricht zurück. Die Tool-Argumente liegen bereits als Objekt vor und nicht als noch zu parsenden JSON-String.

Erster Request: Claude soll tool_use zurückgeben

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "YOUR_CLAUDE_MODEL",
    "max_tokens": 512,
    "system": "Du bist ein Tarifassistent. Antworte ausschließlich anhand der vom Tool zurückgegebenen Daten und rate nicht.",
    "messages": [
      {
        "role": "user",
        "content": "Rufe den Tarif team ab und sage mir, ob Mehrverbrauch nutzungsbasiert abgerechnet werden kann."
      }
    ],
    "tools": [
      {
        "name": "get_plan_info",
        "description": "Feste Testdaten anhand des Tarifcodes abrufen",
        "strict": true,
        "input_schema": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "tool_choice": {
      "type": "tool",
      "name": "get_plan_info"
    }
  }'

Ob ein bestimmtes Tool erzwungen werden kann, hängt von der gewählten Modell- und Konfigurationsunterstützung ab. Unterstützt das Zielmodell dies nicht, verwenden Sie auto und prüfen in der Anwendung, ob tatsächlich ein Tool-Aufruf zurückgegeben wurde.

Eine repräsentative Antwort:

{
  "id": "msg_plan_001",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_plan_001",
      "name": "get_plan_info",
      "input": {
        "plan_code": "team"
      }
    }
  ],
  "stop_reason": "tool_use"
}

Das input-Objekt kann direkt an den Tool-Executor übergeben werden:

tool_result = execute_tool(
    "get_plan_info",
    {"plan_code": "team"},
)

Zweiter Request: tool_result in die unmittelbar folgende user-Nachricht setzen

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "YOUR_CLAUDE_MODEL",
    "max_tokens": 512,
    "system": "Du bist ein Tarifassistent. Antworte ausschließlich anhand der vom Tool zurückgegebenen Daten und rate nicht.",
    "messages": [
      {
        "role": "user",
        "content": "Rufe den Tarif team ab und sage mir, ob Mehrverbrauch nutzungsbasiert abgerechnet werden kann."
      },
      {
        "role": "assistant",
        "content": [
          {
            "type": "tool_use",
            "id": "toolu_plan_001",
            "name": "get_plan_info",
            "input": {
              "plan_code": "team"
            }
          }
        ]
      },
      {
        "role": "user",
        "content": [
          {
            "type": "tool_result",
            "tool_use_id": "toolu_plan_001",
            "content": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
          }
        ]
      }
    ],
    "tools": [
      {
        "name": "get_plan_info",
        "description": "Feste Testdaten anhand des Tarifcodes abrufen",
        "strict": true,
        "input_schema": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ]
  }'

Eine repräsentative finale Antwort:

{
  "id": "msg_plan_002",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Der Tarif Team unterstützt eine nutzungsbasierte Abrechnung von Mehrverbrauch. Die Testdaten enthalten 10.000 Anfragen, und overage_allowed ist true."
    }
  ],
  "stop_reason": "end_turn"
}

Messages stellt klare Anforderungen an die Reihenfolge: tool_result muss direkt auf die assistant-Nachricht mit dem passenden tool_use folgen. Erzeugt ein assistant-Turn mehrere clientseitige Tool-Aufrufe, müssen alle entsprechenden Ergebnisblöcke in der nächsten user-Nachricht zurückgegeben und jeweils über tool_use_id zugeordnet werden. Enthält dieselbe user-Nachricht auch normalen Text, stehen die Tool-Ergebnisblöcke davor.

Was direkt abbildbar ist und welche Konvertierungen zwangsläufig verlustbehaftet sind

FähigkeitBewertung der KonvertierungRichtige Behandlung
Normaler NutzertextMeist direkt abbildbarText, Reihenfolge und multimodale Typen erhalten, nicht nur sichtbare Strings kopieren
Einfaches Funktions-SchemaUmformbarZwischen function.parameters, Responses parameters und Messages input_schema konvertieren und das unterstützte JSON-Schema-Subset erneut validieren
Tool-ArgumenteTyp muss konvertiert werdenBeide OpenAI-Schnittstellen liefern meist JSON-Strings, Messages ein Objekt. Vor der Geschäftslogik normalisieren, parsen und validieren
Tool-Aufruf-IDBedeutung erhalten, Namespace nicht wiederverwendenEine interne canonical call ID zusammen mit der ursprünglichen Upstream-ID speichern und über das protokollspezifische Feld zurückgeben
Parallele Tool-AufrufeUnterstützbar, aber nie nach Array-Position zuordnenJedes Ergebnis über tool_call_id, call_id oder tool_use_id verbinden
system/developer-AnweisungenMöglicherweise verlustbehaftetGlobale, phasenbezogene und einzelne Turn-Geltungsbereiche unterscheiden; bei fehlender Zielunterstützung explizit degradieren oder ablehnen
Finale strukturierte AusgabeFelder nicht mechanisch austauschbarChat nutzt response_format, Responses text.format, Messages output_config.format
Gestreamte Tool-ArgumenteProtokollspezifischer Parser erforderlichFragmente nach Ereignis und Aufruf-ID sammeln und JSON erst nach dem Abschlussereignis parsen
Serverseitiger Mehrturn-ZustandKein universelles ÄquivalentZustands-IDs wie previous_response_id sind an den ursprünglichen Upstream gebunden; beim Wechsel sichtbaren Kontext wiederholen oder sticky routing einsetzen
thinking/reasoning-ZustandMeist nicht verlustfrei konvertierbarOpaque Items, thinking blocks, Signaturen oder verschlüsselte Inhalte exakt gemäß nativem Protokoll bewahren; nie selbst erzeugen
Gehostete ToolsHäufig ohne direktes ÄquivalentUnterstützung und Alternativen für web search, file search, computer use, server tools und ähnliche Funktionen einzeln deklarieren
Mehrere KandidatenMöglicherweise ohne ÄquivalentNicht annehmen, dass Chat-Completions-n direkt auf Responses abbildbar ist; mehrere Requests auf Anwendungsebene ausführen oder Produktverhalten ändern

Die zuverlässigste interne Gateway-Abstraktion ist daher kein riesiges Objekt mit allen denkbaren Feldern. Nachrichten, Anweisungsbereich, Tool-Definitionen, Tool-Aufrufe, Ergebnisse, Zustands-Handles, Stream-Ereignisse und opaker nativer Zustand sollten getrennt modelliert werden. Kann eine Fähigkeit nicht ausgedrückt werden, sollte ein eindeutiger Status wie „nicht unterstützt“ oder „verlustbehaftete Konvertierung“ zurückgegeben werden, statt das Feld stillschweigend zu löschen.

strict, response_format und text.format lösen unterschiedliche Probleme

Ein häufiger Migrationsfehler ist, „gültige Tool-Argumente“ und „eine finale Antwort in einer vorgegebenen JSON-Form“ als dieselbe Funktion zu behandeln.

ZielChat CompletionsResponsesAnthropic Messages
Tool-Aufrufargumente einschränkentools[].function.stricttools[].stricttools[].strict
Finale Modellausgabe einschränkenresponse_formattext.formatoutput_config.format

Das strict eines Tools bestimmt, wie das Modell die Funktion aufruft. Die finale strukturierte Ausgabe bestimmt den Inhalt für den Nutzer. Ein Agent kann beides gleichzeitig benötigen: zuerst ein Tool mit strikten Argumenten aufrufen, danach das Endergebnis unter einem festen JSON-Schema zurückgeben.

Die aktuelle OpenAI-Dokumentation enthält außerdem einen leicht zu übersehenden Standardunterschied:

  • Funktionsaufrufe in Chat Completions sind standardmäßig nicht strikt.
  • Wird strict in Responses weggelassen, versucht der Dienst, das Schema in einen strikten Modus zu normalisieren. Bei Inkompatibilität kann er auf nicht strikt zurückfallen und in der geparsten Tool-Definition strict: false anzeigen.

Um die Absicht klarzumachen und nicht von schnittstellenspezifischen Defaults abzuhängen, sollten Produktions-Requests strict: true oder strict: false ausdrücklich setzen. Ein striktes Schema muss auch die jeweiligen Anforderungen erfüllen, etwa zusätzliche Objekteigenschaften verbieten und alle Pflichtfelder aufführen.

Noch wichtiger: Eine Kompatibilitätsschicht kann ein Feld akzeptieren, ohne die Einschränkung umzusetzen. Anthropic führt in der offiziellen Dokumentation zur OpenAI-SDK-Kompatibilität auf, dass in genau dieser Schicht unter anderem function strict, response_format und reasoning_effort ignoriert werden und die meisten nicht unterstützten Felder keinen Fehler auslösen. Ein Request kann somit 200 liefern, obwohl Schema oder Reasoning-Einstellung nicht wirksam waren.

Das bedeutet nicht, dass natives Anthropic Messages keine entsprechenden Fähigkeiten besitzt. Natives Messages unterstützt strikte Tool-Eingaben und verwendet output_config.format für finales JSON. Die Fehlersuche beginnt daher mit der Frage: Wird natives Messages aufgerufen oder eine OpenAI-kompatible Schicht?

system, developer und der Geltungsbereich von Anweisungen lassen sich nicht durch bloßes Verketten bewahren

OpenAI-artige Schnittstellen erlauben unterschiedliche Rollen im Nachrichtenverlauf; Responses bietet zusätzlich instructions. Anthropic Messages nutzt traditionell ein system-Feld auf oberster Ebene. Stand September 2026 unterstützen einige aktuelle Modelle auch role: "system" mitten in einer Unterhaltung, aber nicht alle, und es gelten Positions- sowie Tool-Reihenfolgebeschränkungen.

Gleichzeitig sammelt Anthropics OpenAI-SDK-Kompatibilitätsschicht system/developer-Nachrichten, verbindet sie mit Zeilenumbrüchen und verschiebt sie als einen system-Prompt an den Anfang. Der Request wird dadurch nutzbar, aber Zeitpunkt und Geltungsbereich verändern sich. Eine developer-Anweisung, die erst ab Turn acht gelten soll, kann nach der Verschiebung bereits die Semantik der ersten sieben Turns beeinflussen.

Ein sichererer Adapter unterscheidet intern zunächst drei Ebenen:

  • Globale Anweisungen: gelten für die gesamte Unterhaltung.
  • Phasenbezogene Anweisungen: gelten ab einem bestimmten Turn dauerhaft.
  • Einzelturn-Anweisungen: steuern nur die aktuelle Aufgabe.

Eine Anweisung sollte nur dann abgebildet werden, wenn das Zielprotokoll denselben Geltungsbereich ausdrücken kann. Andernfalls ist eine klare Strategie nötig: auf einem Modell mit entsprechender Unterstützung bleiben, die Anweisung degradieren und die Abweichung protokollieren oder die Migration ablehnen. Stilles Verketten spart Code, führt aber oft zu „Request erfolgreich, Verhalten verändert“.

Streaming erfordert eine Zustandsmaschine statt einfacher Text-token-Verkettung

Alle drei Schnittstellen unterstützen Streaming, ihre Ereignisse sind jedoch nicht gleichwertig:

  • Chat Completions sammelt Text und tool_calls-Fragmente üblicherweise aus choices[].delta.
  • Responses sendet typisierte Ereignisse wie response.output_text.delta, response.function_call_arguments.delta, response.function_call_arguments.done, response.completed und error.
  • Messages verwendet message_start, content_block_start, content_block_delta, content_block_stop, message_delta und message_stop; Tool-Argumente treffen fragmentiert über input_json_delta.partial_json ein.

Tool-Argumente können so aufgeteilt sein:

{"plan_
code":"te
am"}

Keines dieser Fragmente ist für sich gültiges JSON. Sie müssen nach Aufruf-ID oder Inhaltsblockindex gesammelt und erst nach dem Abschlussereignis für die Argumente geparst werden:

from __future__ import annotations

import json
from collections import defaultdict
from typing import Any


class ToolArgumentAssembler:
    def __init__(self) -> None:
        self._buffers: dict[str, list[str]] = defaultdict(list)

    def add_delta(self, call_id: str, fragment: str) -> None:
        self._buffers[call_id].append(fragment)

    def finish(self, call_id: str) -> dict[str, Any]:
        if call_id not in self._buffers:
            raise KeyError(f"unbekannte call_id: {call_id}")

        raw = "".join(self._buffers.pop(call_id))
        value = json.loads(raw)
        if not isinstance(value, dict):
            raise TypeError("Tool-Argumente müssen zu einem Objekt dekodiert werden")
        return value

    def discard(self, call_id: str) -> None:
        self._buffers.pop(call_id, None)

Der Adapter sollte außerdem einen eindeutigen Endzustand erfassen:

created -> receiving -> completed
                   \-> failed
                   \-> disconnected

disconnected ist nicht completed. Anthropic Messages kann nach bereits erfolgreicher HTTP-Verbindung ein event: error im Stream melden; Responses hat ebenfalls eigene Fehlerereignisse. Wer nur den anfänglichen HTTP-Status betrachtet oder eine geschlossene Verbindung als normale Fertigstellung wertet, kann Tool-Argumente oder die finale Antwort abschneiden.

Der Ereignisparser sollte auch unbekannte Typen tolerieren: protokollieren und überspringen, sofern sie die aktuell unterstützte Fähigkeit nicht betreffen, statt beim Hinzufügen eines Serverereignisses den gesamten Client abstürzen zu lassen.

Mehrturn-Zustand und Reasoning-Zustand können nicht erfunden werden

Chat Completions und klassische Messages-Abläufe basieren meist darauf, dass die Anwendung den Verlauf wiederholt. Responses kann serverseitigen Zustand zusätzlich über previous_response_id oder Conversations halten. Ihr Verständnis vom „vorherigen Turn“ ist nicht austauschbar.

Erhält ein Gateway eine Zustands-ID, gibt es nur drei gültige Strategien:

  1. Sticky routing: Spätere Requests gehen an denselben Upstream, der den Zustand erzeugt hat.
  2. Vollständiges Replay: Alle Nachrichten, Tool-Aufrufe, Ergebnisse und nativen Zustandselemente, die legal wiederholt werden dürfen, werden erneut gesendet.
  3. Explizite Ablehnung: Kann der Ziel-Upstream nicht fortsetzen, wird ein diagnostizierbarer Fehler zurückgegeben und der Client beginnt die Unterhaltung neu.

Eine OpenAI-previous_response_id darf nicht an Anthropic übergeben werden. Ebenso darf eine interne Gateway-Konversations-ID nicht als Zustands-Handle ausgegeben werden, das ein anderer Anbieter versteht.

Auch Reasoning-Zustand lässt sich nicht durch Umbenennen von Feldern lösen:

  • In zustandslosen oder bestimmten Datenaufbewahrungs-Szenarien kann Responses verschlüsselte Reasoning-Items zurückgeben, die in einem späteren Request erneut übermittelt werden müssen.
  • Anthropic-thinking-Abläufe können thinking blocks, Signaturen oder anderen opaken Zustand enthalten. Bei Tool-Nutzung und Mehrturn-Unterhaltungen muss dieser gemäß nativer Dokumentation erhalten bleiben.
  • Stand September 2026 ist Anthropics manuelles thinking.type: "enabled" mit budget_tokens bei Modellen der Generation 4.6 veraltet und wird ab Generation 4.7 abgelehnt; neuere Modelle verwenden adaptive thinking und die zugehörige effort-Steuerung.

Daher kann keine dauerhafte Regel OpenAI reasoning_effort mit Anthropic budget_tokens gleichsetzen. Eine korrekte Fähigkeitsbeschreibung nennt Zielmodell, aktuellen thinking-Modus und das Verhalten bei fehlender Unterstützung.

Parallele Tool-Aufrufe: nach ID zuordnen, nie nach Array-Position

Ein Modell kann in einem Turn mehrere Tools anfordern. Unterschiedliche Ausführungszeiten bedeuten, dass Ergebnisse in anderer Reihenfolge eintreffen können. Der Adapter benötigt eine Beziehung wie diese:

canonical_call_id
  -> provider
  -> provider_call_id
  -> tool_name
  -> validated_arguments
  -> execution_status
  -> result

Bei der Rückgabe der Ergebnisse:

  • Chat Completions erzeugt pro Ergebnis eine Nachricht mit role: "tool" und passender tool_call_id.
  • Responses erzeugt pro Ergebnis ein function_call_output-Item und setzt die passende call_id.
  • Messages legt passende tool_result-Blöcke in den unmittelbar folgenden user-Turn und setzt jeweils die zugehörige tool_use_id.

Während Migrationstests sollten Sie mit parallel_tool_calls: false beginnen, den Einzel-Tool-Pfad vollständig zum Laufen bringen und erst dann Parallelität aktivieren. In Produktion benötigen Tools mit Nebenwirkungen — E-Mail-Versand, Abbuchungen oder Ressourcenerstellung — zusätzlich Idempotenzschlüssel. Netzwerk-Retries, Stream-Abbrüche oder Upstream-Replay können denselben semantischen Aufruf erneut zustellen; am modellgenerierten Text lässt sich nicht zuverlässig erkennen, ob er bereits ausgeführt wurde.

Warum HTTP 200 zur Kompatibilitätsprüfung nicht ausreicht

Ein aussagekräftiger Migrationstest sollte mindestens diese Pfade abdecken:

TestErfolgskriterium
Normaler TextInhalt ist lesbar und system/developer-Geltungsbereich verhält sich wie erwartet
Einzelner Tool-AufrufTool-Name, Argumente, Aufruf-ID, Ergebnis und finale Antwort bilden einen vollständigen Zyklus
Parallele Tool-AufrufeJedes Ergebnis ist per ID zugeordnet, ohne Vertauschung oder Verlust
Gestreamte ArgumenteFragmente werden vollständig zusammengesetzt, danach lässt sich das JSON parsen
Striktes Tool-SchemaUngültige Felder und Typen werden wie erwartet abgelehnt oder ausdrücklich degradiert
Finale strukturierte AusgabeDie Antwort erfüllt das angegebene Schema und „sieht“ nicht nur wie JSON aus
Tool-AusführungsfehlerDas Modell erhält einen strukturierten Fehler, läuft nicht endlos weiter und erfindet keinen Erfolg
Mehrturn-FortsetzungDer zweite Turn kann Fakten aus dem ersten verwenden und Zustandswechselregeln sind klar
reasoning/thinkingDer angegebene Modus funktioniert und nativer Zustand wird weder gelöscht noch erfunden
Stream-Fehler und AbbrücheDer Client unterscheidet Fertigstellung, Fehler und Verbindungsunterbrechung
Kontrollierter API-FehlerFehlertyp, request ID und Retry-Strategie bleiben diagnostizierbar

Verwenden Sie feste Eingaben und feste Tool-Fixtures und erfassen Sie je Protokoll separat:

  • ob das finale Geschäftsergebnis gleichwertig ist;
  • ob Tool-Aufruf und Ergebnisrückgabe vollständig sind;
  • P50- und P95-Latenz;
  • input-, output- und cache-bezogenes usage;
  • Fehlertyp, request ID und Endzustand;
  • welche Fähigkeiten ausdrücklich degradiert wurden.

API Keys, vollständige sensible Prompts und private Nutzerausgaben dürfen nicht geloggt werden. Fehlerlogs sollten mindestens HTTP status, upstream error type/code, eine kurze Meldung, request ID, endpoint, Protokoll, Model ID und Stream-Endzustand enthalten. Andernfalls können model_not_found, fehlende Berechtigungen und inkompatible Pfade in einem nicht diagnostizierbaren 400 zusammenfallen.

Fehlersuche nach Symptom: Wo ist der Agent kaputtgegangen?

SymptomHäufige UrsacheDiagnose und Behebung
Request liefert 200, aber das Modell ruft nie ein Tool aufTool-Definition wurde nicht gesendet, tool_choice ignoriert, Modell ohne Tool-Unterstützung oder Prompt unzureichendFinalen ausgehenden Request ausgeben; Zielmodell und Kompatibilitätsschicht prüfen; im Test nur ein schreibgeschütztes Tool anbieten und dessen Nutzung erzwingen oder ausdrücklich verlangen
Modell liefert einen Tool-Aufruf, Anwendung führt ihn nicht ausAnwendung liest noch ein altes Feld, etwa nur message.contentJe nach Protokoll tool_calls, function_call-Item oder tool_use-Block lesen
JSON der Tool-Argumente lässt sich nicht parsenStream-Fragment wurde als vollständiges JSON behandelt oder ein Objekt erneut als String geparstAbschlussereignis der Argumente abwarten; zuerst feststellen, ob Wert String oder Objekt ist
Trotz strict erscheinen zusätzliche FelderKompatibilitätsschicht ignoriert es, Schema erfüllt Strict-Anforderungen nicht oder Request geht nicht an nativen EndpointFinalen Endpoint und Dokumentation prüfen; strict explizit setzen; Regressionstest mit absichtlichem Schema-Verstoß hinzufügen
Zweiter Request meldet fehlendes Tool-ErgebnisAufruf-ID stimmt nicht oder erstes assistant/tool-Item wurde nicht bewahrtUpstream-Aufruf und ID unverändert speichern; Ergebnis direkt an der vom Protokoll geforderten Stelle zurückgeben
Messages meldet tool_use ids ... without tool_resulttool_result folgt nicht direkt oder normaler Text steht davorAlle passenden tool_result-Blöcke in die nächste user-Nachricht setzen und vor optionalem Text platzieren
Streaming bleibt hängen oder liefert nur halbe ArgumenteClient wartet nur auf Textende und verarbeitet Tool-Argument- oder Fehler-Endzustände nichtSeparate Ereigniszustandsmaschine pro Protokoll implementieren und completed, failed, error sowie disconnected unterscheiden
Zweiter Turn vergisst den erstenVerlauf, Tool-Aufrufe oder result Items fehlen; oder previous_response_id gehört zu anderem UpstreamVollständigen sichtbaren Kontext wiederholen oder sticky routing beibehalten; Zustands-IDs nie zwischen Anbietern übertragen
system-Anweisung wirkt nach Schnittstellenwechsel zu frühKompatibilitätsschicht hat eine spätere system/developer-Anweisung an den Anfang verschobenGeltungsbereich modellieren; bei nicht verlustfreier Abbildung sichtbar degradieren oder natives Protokoll beibehalten
Tool wird doppelt ausgeführtRequest-Retry, Replay nach Stream-Abbruch oder fehlende IdempotenzIn Tests schreibgeschützte Tools einsetzen; Idempotenzschlüssel für produktive Nebenwirkungs-Tools aus canonical call ID ableiten
Finale Ausgabe ist JSON, aber Felder fehlen sporadischPrompt fordert nur „JSON zurückgeben“, strukturierte Ausgabe ist nicht aktiviertresponse_format, text.format oder output_config.format der jeweiligen Schnittstelle verwenden und anschließend in der Anwendung erneut validieren

Sicherere Migrationsreihenfolge

  1. Ermitteln Sie das tatsächlich vom Client gesendete Protokoll. Nicht vom Modellnamen ableiten. Vollständigen Endpoint, SDK-Methode, oberste Request-Felder und Stream-Ereignistypen festhalten.
  2. Listen Sie das zu erhaltende Verhalten auf. Mindestens Tools, parallele Aufrufe, striktes Schema, finale strukturierte Ausgabe, Mehrturn-Zustand, Streaming und thinking/reasoning.
  3. Bevorzugen Sie das native Protokoll. Kann eine Funktion direkt über natives Messages oder Responses umgesetzt werden, vermeiden Sie eine zusätzliche Kompatibilitätsschicht.
  4. Erstellen Sie eine Konvertierungs-Fähigkeitsmatrix. Jede Funktion als vollständig unterstützt, verlustbehaftet unterstützt oder nicht unterstützt markieren und das Ergebnis dem caller zugänglich machen.
  5. Führen Sie mit einem nebenwirkungsfreien Fixture einen vollständigen Zwei-Request-Zyklus aus. Nicht bei „ein Request liefert Text“ aufhören; Tool ausführen und Ergebnis zurückgeben.
  6. Testen Sie danach Parallelität, Streaming und Fehlerpfade. Nebenwirkungs-Tools und realen Geschäftstraffic erst nach erfolgreichem Normalpfad aktivieren.
  7. Steigern Sie den Traffic schrittweise und vergleichen Sie Kennzahlen. Korrektheit, Latenz, usage, Fehler und doppelte Tool-Ausführungen beobachten, nicht nur HTTP-Erfolgsrate.

Passenden BetterToken-Einstiegspunkt wählen

BetterToken bietet unterschiedliche Verbindungswege für verschiedene Clients. Das Protokoll wird weiterhin durch den tatsächlich verwendeten wire contract bestimmt:

  • Chat Completions: Die vollständige URL lautet https://www.bettertoken.ai/v1/chat/completions. Bei SDKs oder Tools, die den Pfad selbst anhängen, ist die Base URL üblicherweise https://www.bettertoken.ai/v1. Siehe Chat Completions API reference.
  • Codex / Responses: Die aktuelle Codex-Dokumentation verwendet base_url = "https://www.bettertoken.ai/v1" und wire_api = "responses"; Codex hängt /responses selbst an. Siehe Codex-Anleitung.
  • Claude Code / Messages: Die aktuelle Dokumentation verwendet ANTHROPIC_BASE_URL=https://bettertoken.ai ohne /v1 am Ende der Base URL; der Client hängt /v1/messages an. Siehe Claude-Code-Anleitung.

Dasselbe Dashboard, derselbe API Key oder Modellname verwandelt drei Protokolle nicht in ein Format. Bei einem vorhandenen Tool wählen Sie das erwartete Protokoll. Für einen eigenen Agenten gleichen Sie die benötigten Fähigkeiten mit den vollständigen Zyklen und der Abnahmematrix dieses Artikels ab.

Häufige Fragen

Bedeutet OpenAI-compatible eine vollständige Kopie der OpenAI API?

Nein. Es bedeutet meist nur, dass bestimmte Endpoints und Datenstrukturen von OpenAI-artigen Clients aufgerufen werden können. Modelle, Parameter, Stream-Ereignisse, Tools, strukturierte Ausgabe, gehostete Tools und Fehlersemantik müssen einzeln geprüft werden.

Reicht es, Base URL und API Key zu ersetzen?

Manchmal bei einfachen Text-Requests, die bereits denselben wire contract verwenden. Ein Tool-Agent erfordert weiterhin Prüfung von Tool-Definitionen, Ergebnisrückgabe im zweiten Request, Stream-Ereignissen, striktem Schema, Zustand und Fehlern. Erwartet ein Client Responses, reicht /chat/completions allein nicht; erwartet er Messages, passt sich ein OpenAI-artiger Endpoint nicht automatisch an.

Kann ein universeller Adapter alle drei Protokolle konvertieren?

Er kann normalen Text und einen Teil der Funktions-Tool-Schleife abdecken, darf aber keine vollständige verlustfreie Unterstützung behaupten. Provider-managed state, gehostete Tools, opaker thinking/reasoning-Zustand, manche system-Geltungsbereiche und modellspezifische Funktionen haben oft kein universelles Äquivalent. Der Adapter sollte eine Fähigkeitsmatrix und Degradierungsinformationen offenlegen.

Warum bestehen Unit-Tests, während der echte Agent scheitert?

Viele Tests simulieren nur die erste Modellantwort. Sie prüfen weder die Ergebnisrückgabe im zweiten Request noch parallele Aufrufe, Stream-Fragmente oder Zustandsfortsetzung. Erweitern Sie den Test auf „Nutzerrequest → Tool-Aufruf des Modells → Ausführung in der Anwendung → Rückgabe des Ergebnisses → finale Antwort“, um echte Protokollfehler zu finden.

Sollte zuerst Chat Completions oder Responses migriert werden?

Eine stabile Chat-Completions-Anwendung kann weiterlaufen und je nach Geschäftswert Funktion für Funktion migrieren. Ein neuer OpenAI-Agent oder einer, der typisierte Items, gehostete Tools oder Responses-Zustandsfunktionen ausdrücklich benötigt, sollte direkt auf Responses starten. Entscheidend sind Fähigkeiten und Migrationsaufwand, nicht ob der Schnittstellenname neuer klingt.

Bereit, Ihren LLM-Workflow zu optimieren?

Verbinden Sie Modelle über eine API, verwalten Sie Schlüssel und behalten Sie KI-Kosten im Blick.

Kostenlos starten