Nếu bạn vẫn gọi /v1/chat/completions trong code mới, bạn đang xây trên con đường legacy. OpenAI đã dồn trọng tâm sang Responses API: endpoint /v1/responses, trạng thái phiên do server giữ, hosted tools, reasoning được mã hóa. Assistants API thì không còn để so sánh — trang tài liệu của nó trả 404.
Bài này hướng dẫn Responses API và Structured Outputs bằng HTTP thuần: mọi ví dụ là request thật, dán được vào curl hoặc client HTTP của bất kỳ ngôn ngữ nào.
Mục lục
Cập nhật 2026: những gì đã đổi
- Responses API là khuyến nghị cho mọi dự án mới. Chat Completions vẫn được phục vụ nhưng không còn là mặc định.
- Từ GPT-5.4, Chat Completions không gọi được tool trừ khi
reasoning_effortbằngnone. Riêng GPT-6 Astra và GPT-6.1 Sol bắt buộc dùng Responses cho tool calling. - Hosted tools,
previous_response_id,store, Conversations và encrypted reasoning chỉ tồn tại trên Responses. - Assistants API đã bị gỡ: trang tài liệu trả 404, ngày gỡ chính thức chưa được xác nhận. Code Assistants phải viết lại, không có endpoint để gọi.
- Structured Outputs nhận JSON Schema qua
text.formatvớistrict. Lần đầu dùng một schema tốn thêm latency; các request sau dùng lại schema đó thì không.
Mental model: một endpoint, hai chiều dữ liệu có kiểu
Chat Completions nhận một mảng messages phẳng và trả về choices. Responses nhận input — chuỗi hoặc danh sách item có kiểu — và trả về mảng output. Mỗi item trong output có type riêng: message cho văn bản, function_call cho lời gọi tool, reasoning cho phần suy luận được mã hóa. Với agent, đây là khác biệt về chất: vòng lặp không phải suy đoán xem một đoạn text là câu trả lời hay là lời gọi tool.
Điều thứ hai: Responses có thể giữ trạng thái. Bạn gửi store và nhận về một id; lần sau gửi previous_response_id để server nối tiếp. Token không biến mất — ngữ cảnh đã lưu vẫn được tính vào usage; trạng thái chỉ giảm code client và lỗi thứ tự message.
Request tối thiểu trên /v1/responses
Ba field thay đổi so với đường cũ: instructions tách system prompt khỏi lịch sử, input thay cho messages, max_output_tokens thay cho max_tokens. max_output_tokens bao gồm cả reasoning token.
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-6.1-terra",
"instructions": "Bạn là trợ lý kỹ thuật. Trả lời tiếng Việt, tối đa 5 câu.",
"input": "Giải thích KV cache trong một đoạn.",
"max_output_tokens": 600
}'
Response không có choices. Văn bản nằm trong item type: "message", và bên trong item đó là các content part type: "output_text". Viết parser theo cấu trúc này ngay từ request đầu tiên:
# File: app/responses_client.py
import json
import urllib.request
API_KEY = "..." # đọc từ secret store, không hard-code
def ask(prompt: str) -> str:
body = json.dumps({
"model": "gpt-6.1-terra",
"input": prompt,
"max_output_tokens": 600,
}).encode("utf-8")
req = urllib.request.Request(
"https://api.openai.com/v1/responses",
data=body,
headers={"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=60) as r:
payload = json.load(r)
return "".join(
part["text"]
for item in payload["output"] if item["type"] == "message"
for part in item["content"] if part["type"] == "output_text"
)
Đừng parse bằng cách nối mọi text trong output. Reasoning item có thể chứa nội dung riêng, và function call không có text. Lọc theo type là hợp đồng, không phải tối ưu.
Hội thoại nhiều lượt: store, previous_response_id và Conversations
Có ba cách giữ mạch hội thoại, chọn theo mức kiểm soát bạn cần:
- Gửi lại lịch sử trong
input. Bạn kiểm soát hoàn toàn nội dung, dễ redact, dễ tái lập. Đổi lại request dài dần và chi phí input tăng theo số lượt. previous_response_id+store. Server nối lượt trước; client chỉ gửi phần mới. Ít code, nhưng bạn phải quản lý vòng đời response id và không được dùng id của phiên này cho phiên khác.- Conversations. Đơn vị lưu trữ dài hạn hơn, phù hợp khi nhiều phiên hoặc nhiều kênh cùng tham gia một cuộc trò chuyện.
Cả ba đều tốn token như nhau ở tầng tính giá; previous_response_id không phải bộ nén ngữ cảnh. Muốn giảm chi phí, phải cắt hoặc tóm tắt trước khi gửi, và đo usage.input_tokens của từng request.
Trước khi bật store, kiểm tra chính sách lưu dữ liệu của tài khoản và ranh giới tenant. Response id là opaque nhưng không phải capability token; đừng dùng lại nó qua ranh giới người dùng.
Tool calling: hình dạng phẳng, kết quả là item riêng
Trên Responses, function tool được làm phẳng: name, description, parameters và strict nằm ở cấp tool, không lồng trong object function như Chat Completions. Kết quả tool không phải message role tool; nó là item function_call_output với call_id khớp lời gọi.
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-6.1-sol",
"input": "Tra cứu trạng thái đơn hàng 12345.",
"tools": [{
"type": "function",
"name": "get_order",
"description": "Lấy trạng thái đơn hàng theo id.",
"strict": true,
"parameters": {
"type": "object",
"properties": {"id": {"type": "string"}},
"required": ["id"],
"additionalProperties": false
}
}]
}'
Vòng lặp đúng gồm bốn bước: gửi request, đọc function_call, chạy tool thật, gửi function_call_output kèm cùng previous_response_id. Không bước nào được bỏ qua, kể cả khi tool trả lỗi — model cần thấy lỗi để đổi cách làm.
Giới hạn của Chat Completions là lý do nhiều người phải chuyển: từ GPT-5.4, tool calling yêu cầu reasoning_effort: "none". Với Astra và Sol, kể cả mẹo đó cũng không cứu được — hai model này cần Responses. Đừng hạ chất lượng suy luận để giữ một API đang bị đẩy về legacy.
Hosted tools và encrypted reasoning
Hai tính năng chỉ có trên Responses:
- Hosted tools: web search, file search và các tool do OpenAI vận hành. Bạn không phải tự viết vòng lặp HTTP cho từng tool, nhưng vẫn phải đọc kết quả trả về như dữ liệu không tin cậy.
- Encrypted reasoning: phần suy luận được mã hóa thay vì trả về nội dung thô. Hữu ích khi bạn cần nối reasoning qua nhiều lượt mà không muốn lưu chain-of-thought ở client.
Cả hai đều không miễn phí: chúng là input/output token như mọi item khác. Đo usage trước và sau khi bật, đừng ước lượng bằng số tool call.
Structured Outputs: JSON Schema và strict mode
Structured Outputs biến model thành bộ sinh dữ liệu theo schema. Trên Responses, bật bằng text.format với type: "json_schema" và strict: true. Khi strict bật, mọi property phải nằm trong required, additionalProperties phải là false, và field “tuỳ chọn” phải diễn đạt bằng union với null.
{
"model": "gpt-6.1-terra",
"instructions": "Phân loại ticket hỗ trợ. Không bịa thông tin ngoài nội dung ticket.",
"input": "Khách báo bị trừ tiền hai lần, muốn hoàn ngay.",
"max_output_tokens": 400,
"text": {
"format": {
"type": "json_schema",
"name": "support_ticket",
"strict": true,
"schema": {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["refund", "bug", "other"]},
"summary": {"type": "string"},
"needs_human": {"type": "boolean"}
},
"required": ["category", "summary", "needs_human"],
"additionalProperties": false
}
}
}
}
Ba điều schema không bảo vệ được:
- Truncation. Chạm
max_output_tokenscó thể trả về JSON không đóng. Đặt giới hạn đủ rộng và kiểm tra tính toàn vẹn trước khi parse. - Refusal. Model từ chối trả về
refusal, không phải object đúng schema. Nhánh này phải có trong code, không được giả định mọi response đều là JSON hợp lệ. - Semantic sai. Schema đúng nhưng giá trị vô nghĩa vẫn qua. Ràng buộc enum giúp ít; business rule phải kiểm tra riêng.
Chiến lược retry nên hẹp: parse lỗi hoặc thiếu field thì gửi lại một lần kèm mô tả lỗi; lỗi 400 do schema không hợp lệ thì không retry, vì payload sẽ hỏng y hệt. Tối đa hai lần thử, sau đó trả lỗi cho caller. Đừng để vòng retry tự do biến một lỗi schema thành hàng trăm request tính tiền.
# File: app/structured.py
import json
def parse_or_retry(call, prompt: str, schema: dict, attempts: int = 2) -> dict:
last_error = None
for _ in range(attempts):
suffix = "" if last_error is None else (
f"\n\nLần trước sai: {last_error}. Trả lại đúng schema, không thêm chữ."
)
raw = call(prompt + suffix)
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
last_error = str(exc)
continue
missing = [k for k in schema["required"] if k not in data]
if missing:
last_error = f"thiếu field {missing}"
continue
return data
raise ValueError(f"không có JSON hợp lệ sau {attempts} lần: {last_error}")
Bảng migration từ Chat Completions và Assistants
| Đường cũ | Responses | Ghi chú |
|---|---|---|
messages role system |
instructions |
Tách khỏi lịch sử, ổn định cho cache |
messages user/assistant |
input items |
Giữ role, thêm type khi cần |
max_tokens / max_completion_tokens |
max_output_tokens |
Bao gồm reasoning token |
tools + tool_calls |
tools + function_call |
Function tool phẳng, không lồng function |
| message role tool | function_call_output |
Item riêng, khớp call_id |
| Không có | store + previous_response_id |
Trạng thái server, không giảm token |
| Assistant + thread + run | Responses + Conversations | Assistants đã bị gỡ, không còn để chạy song song |
Thứ tự migrate an toàn: đổi endpoint và parser trước, giữ nguyên logic nghiệp vụ; sau đó chuyển tool result sang item; cuối cùng mới bật state và hosted tools. Đổi cả ba cùng lúc sẽ khó biết lỗi nằm ở tầng nào.
Chi phí và latency
Giá niêm yết tháng 11/2026, USD trên 1M token, chưa gồm batch hay fast:
| Model | Input / Output | Dùng cho |
|---|---|---|
gpt-6-astra |
$10 / $50 | Bài toán khó nhất, tài liệu dài |
gpt-6.1-sol |
$2 / $10 | Agent coding, tool calling |
gpt-6.1-terra |
$1 / $5 | Trích xuất, phân loại, chat |
gpt-6-luna |
$0.10 / $0.50 | Khối lượng lớn, việc đơn giản |
Ví dụ request 100K input và 2K output trên Sol: 100.000 × $2/1M = $0.20, 2.000 × $10/1M = $0.02, tổng $0.22. Nếu prompt lặp lại và trúng cache, input đọc từ cache giá $0.10/1M, nhưng ghi cache cũng tính tiền.
Ba đòn bẩy lớn nhất, theo thứ tự:
- Đường dài trên 272K token. Toàn bộ request vượt ngưỡng này bị tính 2x cho input/cache và 1.5x cho output — con số không hiện trong bảng giá cơ bản. Cắt trước, đừng mua thêm context.
- Reasoning token. Chúng tính như output, và output của dòng GPT-6 có giá gấp 5 lần input. Với tác vụ không cần suy luận sâu, đặt
reasoning.effortthấp; Sol mặc địnhmedium. - Batch/Flex và Fast. Batch/Flex rẻ 50% nhưng bất đồng bộ; Fast đắt gấp đôi. Đừng dùng Fast cho eval hoặc backfill.
Streaming cải thiện cảm nhận, không giảm tổng thời gian. Với tool loop, stream cả tiến trình (đang gọi tool nào) chứ không chỉ chữ cuối. p95 thường bị chi phối bởi request dài và schema mới — đo theo từng loại tác vụ.
Cạm bẫy thường gặp
- Giữ parser
choices. Lỗi xuất hiện ngay request đầu nhưng chỉ ở nhánh thành công, nên test lỗi không bắt được. - Tưởng
previous_response_idnén ngữ cảnh. Code ngắn hơn, hóa đơn không ngắn hơn. Đọcusagemỗi request. - Gửi system prompt trong
input. Nó phá prefix ổn định và làm cache vô hiệu; dùnginstructions. - Đặt
max_output_tokensquá thấp. JSON bị cắt, parse fail, retry tốn thêm; với schema lớn, giới hạn phải tính cả reasoning. - Retry 400/401/403. Chỉ retry lỗi tạm thời (429, 5xx) với backoff có jitter; lỗi schema và auth là lỗi code.
- Bật hosted tools mà không coi output là untrusted. Nội dung web trả về có thể chứa chỉ dẫn độc hại; đây là indirect injection, không phải giả thuyết.
Khi nào vẫn giữ Chat Completions
- Client hoặc gateway bên thứ ba chỉ hiểu
/v1/chat/completions, và bạn đang phát hành SDK nội bộ. - Tác vụ một lượt, không tool, không state, không cần hosted tool — bề mặt code nhỏ hơn có giá trị thật.
- Đường fallback đa nhà cung cấp: Chat Completions là hình dạng chung dễ adapter nhất.
Ngược lại, đừng giữ Chat Completions cho agent, cho tính năng cần hosted tools, hay cho Astra/Sol với tool calling. Ở ba trường hợp đó, đường cũ không rẻ hơn — nó không hoạt động.
Nhìn tới 2027
Responses sẽ tiếp tục hút tính năng mới; Chat Completions nhiều khả năng chỉ còn là surface tương thích. Khi thiết kế lớp trừu tượng nội bộ, giả định “trạng thái phiên do server giữ” và “tool call là item có kiểu” là chuẩn, rồi map provider khác về hai khái niệm đó.
Checklist chuyển đổi
- Pin phiên bản SDK hoặc client HTTP và đọc tài liệu của chính phiên bản đó trước khi đổi endpoint.
- Đổi parser sang duyệt
outputvà content partoutput_text; viết test chofunction_callvàrefusal. - Chuyển system prompt sang
instructions; kiểm tra prefix ổn định trước khi bật cache. - Map tool result sang
function_call_outputvớicall_id; test cả nhánh tool trả lỗi. - Bật Structured Outputs cho tác vụ cần dữ liệu máy đọc; đặt retry tối đa hai lần và log lỗi schema.
- Xoá toàn bộ code Assistants; grep
assistant_id,thread,runtrước khi lên kế hoạch. - Đo
usage, p95 và tỉ lệ parse fail trong một tuần trước khi tuyên bố migration thành công.
Để hiểu vì sao độ dài ngữ cảnh ảnh hưởng trực tiếp đến hóa đơn, đọc Nắm Vững Khả Năng và Độ Dài Ngữ Cảnh Của Các Mô Hình OpenAI. Bài Bắt Đầu Với OpenAI Chat Completions API giải thích đường legacy mà bạn đang migrate từ đó. Và Tokens, Cửa Sổ Ngữ Cảnh và Chi Phí API là phần kế toán chi phí chi tiết hơn.
