OpenAI Protocol 01, A Quick Understanding of the OpenAI Low-Level Protocol
Updated: 2026-10-05
Applicable scenario: understanding the OpenAI Chat Completions wire protocol thatagentforge-model-openaiconnects to
Maintainer: changlu
One-sentence conclusion: AgentForge's OpenAI Provider connects to Chat Completions (POST {baseUrl}/chat/completions). This article only covers "what the protocol itself looks like"; for how AgentForge maps it to the unified ChatRequest / ChatResponse, see "OpenAI Protocol 02, AgentForge OpenAI Integration Core Practices".
1. Background and Problem Introduction
1.1 Scenario-Driven: Why Chat Completions Was Chosen for the First Version
When unifying access to multiple large models for AgentForge, we ran into a real problem: OpenAI officially recommends the new Responses API, while at the same time there are a large number of OpenAI-compatible services (self-built gateways, domestic models) that only support
/chat/completions.
Therefore AgentForge release_1.x chose Chat Completions as the first version of the unified integration protocol: it is still an API that clearly exists, and it has the broadest compatibility.
1.2 Problem Guidance: What Exactly Is Sent in a Single Conversation Call?
Question: When an Agent calls an OpenAI model once, what do the request body, response body, and streaming SSE look like respectively? How are tool calls (function calling) expressed and filled back on the wire?
This article explains the protocol in the order of "request → response → streaming → tool calls".
1.3 Protocol Positioning
POST {baseUrl}/chat/completions
By default:
baseUrl = https://api.openai.com/v1
→ actual address https://api.openai.com/v1/chat/completions
Official references:
- https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create
- https://developers.openai.com/api/docs/guides/text
2. Request Protocol
2.1 Headers
Content-Type: application/json
Accept: application/json
Authorization: Bearer ${apiKey}
Key point: Authentication uses the standard
Authorization: Bearer; enterprise gateways can add custom Headers (trace / tenant / routing).
2.2 Request Body: messages (Core)
| role | Key field | Meaning |
|---|---|---|
system | content | System prompt |
user | content or content[] | User input (an array when multimodal) |
assistant | content / tool_calls[] | Model reply, may carry tool calls |
tool | tool_call_id + content | Tool execution result fill-back |
Plain text example:
{
"model": "your-model",
"messages": [
{ "role": "system", "content": "You are a concise Java assistant." },
{ "role": "user", "content": "What is CAS?" }
]
}
2.3 Request Body: Sampling and Tool Parameters
| Field | Description |
|---|---|
temperature | Sampling temperature |
max_tokens | Maximum generated tokens |
top_p | Nucleus sampling |
stop | Stop sequences |
tools[] | Tool declarations: {"type":"function","function":{name,description,parameters,strict}} |
tool_choice | "auto" / "none" / "required" / {"type":"function","function":{"name":X}} |
2.4 Complete Non-Streaming Request Example
{
"model": "your-model",
"messages": [
{ "role": "system", "content": "You are a concise Java assistant." },
{ "role": "user", "content": "What is CAS?" }
],
"temperature": 0.2,
"max_tokens": 1024,
"top_p": 0.9
}
3. Response Protocol
3.1 Non-Streaming chat.completion
{
"id": "chatcmpl_xxx",
"object": "chat.completion",
"created": 1780000000,
"model": "your-model",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "CAS means Compare-And-Swap." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 20, "completion_tokens": 10, "total_tokens": 30 }
}
Note:
choicesis an array; consumers typically only takechoices[0].
3.2 finish_reason
| Value | Meaning |
|---|---|
stop | Normal completion |
length | Reached max_tokens |
tool_calls | A tool needs to be executed |
function_call | Legacy field (deprecated) |
content_filter | Content was filtered |
4. Streaming Protocol (SSE)
Add to the request:
{ "stream": true, "stream_options": { "include_usage": true } }
The response is SSE:
data: {"choices":[{"index":0,"delta":{"content":"Hel"},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"content":"lo"}}]}
data: {"choices":[],"usage":{"prompt_tokens":20,"completion_tokens":2,"total_tokens":22}}
data: [DONE]
Key point: When
stream_options.include_usage=true, an extra chunk withchoices=[]carrying the overallusageappears before[DONE]; during parsing you should read usage first, then determine whether choices is empty.
4.1 Streaming Increments of Tool Calls (Aggregate by index)
delta.tool_calls[] is delivered in fragments and must be aggregated by index:
chunk: {"index":0,"id":"call_1","function":{"name":"getWeather","arguments":""}}
chunk: {"index":0,"function":{"arguments":"{\"city\":"}}
chunk: {"index":0,"function":{"arguments":"\"hangzhou\"}"}}
│
▼ merge by index
ToolRequest(id=call_1, name=getWeather, arguments={"city":"hangzhou"})
| delta field | Aggregation method |
|---|---|
index | Bucket key |
id | Overwrite into the current bucket (the first chunk carries the full id) |
function.name | Append |
function.arguments | Append the raw text, preserving the JSON text |
Note: Some compatible gateways do not deliver
index; in that case use "a new non-emptyidappears" as the start of a new call (a fallback counter buckets it), to avoid incorrectly splitting the argument increments of the same call into multiple calls.
5. Tool Calling (Function Calling) Wire Details
5.1 The assistant Initiates a Call
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": { "name": "getWeather", "arguments": "{\"city\":\"hangzhou\"}" }
}
]
}
Key point:
argumentsis a string (JSON text), not an object—this is the key to the subsequent AgentForge normalization and re-parsing.
5.2 tool Result Fill-Back
{ "role": "tool", "tool_call_id": "call_1", "content": "{\"temperature\":22}" }
tool_call_id must correspond one-to-one with the id at initiation time.
5.3 Multiple Tool Calls
A single response can return multiple tool_calls, which need to be executed one by one and filled back separately with the tool role, with id matching one-to-one.
6. Complete Call Walkthrough: user prompt + function calls (curl)
Scenario: The user asks "What's the weather like in Hangzhou today?" and declares a get_weather tool. Below, from curl construction to response, both non-streaming and streaming are demonstrated (including the second round of tool result fill-back).
6.1 Declare tools
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string", "description": "城市名" } },
"required": ["city"]
}
}
}
]
6.2 Non-Streaming · Round One: The Model Decides to Call a Tool
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [
{ "role": "user", "content": "杭州今天天气怎么样?" }
],
"tools": [
{ "type": "function", "function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": { "type": "object",
"properties": { "city": { "type": "string", "description": "城市名" } },
"required": ["city"] } } }
],
"tool_choice": "auto"
}'
Return (note finish_reason=tool_calls, content=null):
{
"id": "chatcmpl_abc",
"object": "chat.completion",
"created": 1780000000,
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"杭州\"}" }
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": { "prompt_tokens": 62, "completion_tokens": 15, "total_tokens": 77 }
}
Key point:
argumentsis the string"{\"city\":\"杭州\"}"; after local parsing, executeget_weather("杭州").
6.3 Non-Streaming · Round Two: Fill Back the Tool Result to Get the Final Answer
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [
{ "role": "user", "content": "杭州今天天气怎么样?" },
{ "role": "assistant", "content": null,
"tool_calls": [
{ "id": "call_1", "type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"杭州\"}" } } ] },
{ "role": "tool", "tool_call_id": "call_1",
"content": "{\"temperature\":26,\"text\":\"晴\"}" }
]
}'
Return (finish_reason=stop):
{
"id": "chatcmpl_def",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "杭州今天 26℃,天气晴。" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 85, "completion_tokens": 12, "total_tokens": 97 }
}
6.4 Streaming · Round One: tool_calls Increments (stream:true)
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"stream": true,
"stream_options": { "include_usage": true },
"messages": [ { "role": "user", "content": "杭州今天天气怎么样?" } ],
"tools": [
{ "type": "function", "function": {
"name": "get_weather", "description": "查询指定城市的当前天气",
"parameters": { "type": "object",
"properties": { "city": { "type": "string" } }, "required": ["city"] } } }
],
"tool_choice": "auto"
}'
SSE (in arrival order):
data: {"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"杭州\"}"}}]},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}
data: {"choices":[],"usage":{"prompt_tokens":62,"completion_tokens":15,"total_tokens":77}}
data: [DONE]
Aggregation (bucket by tool_calls[].index; id overwrites, name/arguments append):
index=0 → id=call_1, name=get_weather, arguments={"city":"杭州"}
6.5 Streaming · Round Two: Generate the Final Text After Fill-Back
The request body is the same as 6.3, with "stream": true, "stream_options": {"include_usage": true} appended.
data: {"choices":[{"index":0,"delta":{"role":"assistant","content":"杭州"},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"content":"今天 26℃"},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"content":",天气晴。"},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"choices":[],"usage":{"prompt_tokens":85,"completion_tokens":12,"total_tokens":97}}
data: [DONE]
Aggregation: concatenate delta.content in order → "杭州今天 26℃,天气晴。".
6.6 Walkthrough Key Points Summary
| Observation point | Non-streaming | Streaming |
|---|---|---|
| Tool arguments | message.tool_calls[].function.arguments (string) | delta.tool_calls[].function.arguments fragmented, aggregated by index |
| Whether a tool is sent | finish_reason == "tool_calls" | finish_reason == "tool_calls" of the last chunk |
| Text | message.content | delta.content returned segment by segment |
| usage | Response body usage | The usage chunk with choices=[] at the end |
| End | — | data: [DONE] |
6.7 Real Test Verification
You can run the script in the repository directly (an API Key needs to be set):
export OPENAI_API_KEY=sk-...
bash docs/dev/backend/AgentForge核心模块设计原理/01、model模型协议层/chatmodel/openai/verify-openai-chat.sh
The script executes in order: ① a non-streaming tool-calling request; ② printing tool_calls with jq; ③ a streaming request printing SSE. Optional environment variables OPENAI_BASE_URL / OPENAI_MODEL override the defaults.
Note: Real calls incur charges; do not write the Key into the script or commit it to the repository.
7. Summary
- Endpoint:
POST {baseUrl}/chat/completions, authenticated withAuthorization: Bearer. - Request:
messages(system / user / assistant / tool) + sampling parameters +tools/tool_choice. - Response:
choices[0].message(contentortool_calls) +usage+finish_reason. - Streaming: SSE
data:chunk;delta.contenttext,delta.tool_callsaggregated byindex; ends with[DONE]. - Tools:
assistant.tool_calls(argumentsis a string) →toolrole filled back bytool_call_id.
For how AgentForge maps these fields to unified types, see "OpenAI Protocol 02, AgentForge OpenAI Integration Core Practices".
References
[1]. OpenAI Chat Completions API (official reference)
[2]. OpenAI Text Generation Guide
[4]. Related internal document: [AgentForge OpenAI Integration Core Practices](/blog/01、model模型协议层/chatmodel/openai/OpenAI协议02、AgentForge 适配OpenAI接入核心实践)
Compiler: changlu Created: 2026.10.5 Updated: 2026.10.5