Chuyển OpenAI Python SDK sang API tương thích: 5 lỗi thường gặp khi đổi Base URL và model

Một dự án Python đang dùng OpenAI SDK thường không cần viết lại toàn bộ lớp gọi API khi chuyển sang một dịch vụ tương thích OpenAI. Thông thường chỉ cần thay base_url, api_key và model là có thể gửi được yêu cầu đầu tiên.
Nhưng gửi được yêu cầu không có nghĩa là mọi tính năng cũ đều tương thích. Cùng một bộ tham số có thể hoạt động với một model và bị từ chối ở model khác. Các điểm dễ lộ vấn đề nhất là temperature, đầu ra có cấu trúc, tool calling, streaming và thống kê token.
Ngày 13/11/2026, cùng một OpenAI Python SDK được dùng để kiểm tra deepseek-v4-flash, gpt-6.1-terra và claude-sonnet-5-5. Phạm vi dưới đây chỉ bao gồm những gì lần chạy đó chứng minh và các bước có thể lặp lại.
Bắt đầu bằng yêu cầu nhỏ nhất
Trước tiên chỉ cần xác nhận ba giá trị: URL, khóa và model ID có hoạt động cùng nhau hay không.
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["CRAZYROUTER_API_KEY"],
base_url="https://api.crazyrouter.com/v1",
)
model = os.environ.get("OPENAI_MODEL", "deepseek-v4-flash")
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Chỉ trả lời: kết nối thành công"}],
)
print(response.choices[0].message.content)
Ba giá trị có vai trò khác nhau:
base_urlquyết định yêu cầu đi tới đâu;api_keyxác định tài khoản và quyền gọi;modelquyết định tuyến model thực tế.
API key nên đọc từ biến môi trường, không ghi vào mã nguồn hoặc ảnh công khai. Model ID phải được sao chép nguyên văn từ danh mục model. deepseek-v4-flash và deepseek-v4_flash là hai chuỗi khác nhau đối với router.
Nếu yêu cầu trả về HTTP 200 và có nội dung, mới chỉ có thể kết luận lớp kết nối đã thông. Hãy kiểm tra tiếp những tính năng mà ứng dụng thực sự sử dụng.
Lỗi 1: temperature=0 không phải tham số dùng chung cho mọi model
Mã cũ thường đặt temperature=0 trong hàm gọi chung để kết quả ổn định hơn. Trong lần kiểm tra này, deepseek-v4-flash và claude-sonnet-5-5 chấp nhận giá trị đó. Khi chuyển cùng yêu cầu sang gpt-6.1-terra, API trả về HTTP 400; thử 0.3 và 0.7 cũng không thay đổi kết quả.

Phản hồi chứa:
Parameter temperature must be 1 for this model
param=temperature
code=invalid_parameter_value
Điều được chứng minh là route này yêu cầu temperature=1 vào ngày kiểm tra. Không nên biến kết quả đó thành quy tắc cho mọi model suy luận, cũng không thể chỉ nhìn phản hồi để biết giới hạn đến từ model, lớp adapter hay gateway.
Đưa tham số riêng của từng model vào cấu hình sẽ dễ bảo trì hơn:
MODEL_CONFIGS = {
"deepseek-v4-flash": {"temperature": 0},
"gpt-6.1-terra": {"temperature": 1},
"claude-sonnet-5-5": {"temperature": 0},
}
Đây là bảng hành vi đã kiểm tra, không phải tài liệu đặc tả vĩnh viễn. Khi model hoặc route thay đổi, hãy chạy lại.
Lỗi 2: json_object và json_schema không thay thế cho nhau ở mọi route
Bài kiểm tra yêu cầu model trích xuất “Nguyễn Văn A, 30 tuổi, lập trình viên” thành JSON và gửi:
response_format={"type": "json_object"}
Kết quả giữa các model khác nhau.

deepseek-v4-flash trả về 200, nội dung có thể parse bằng json.loads. claude-sonnet-5-5 trả về 400 và yêu cầu đổi sang json_schema với Schema rõ ràng. Sau đó, json_schema hoạt động trên hai route DeepSeek và Claude; gpt-6.1-terra chưa được kiểm tra với định dạng này.
Đừng rải logic fallback vào từng điểm gọi trong ứng dụng. Hãy lưu khả năng đã kiểm tra của từng model, rồi vẫn validate JSON ở phía ứng dụng. Một phản hồi 200 không đảm bảo các field bắt buộc, kiểu dữ liệu và field thừa đã đúng với hợp đồng dữ liệu.
Lỗi 3: tool calling có thể thất bại vì upstream tạm thời không khả dụng
deepseek-v4-flash và claude-sonnet-5-5 trả về tool_calls bình thường. gpt-6.1-terra trả về HTTP 400 hai lần với mã:
provider_temporarily_unavailable
Điểm gây nhiễu là phản hồi chỉ vào param=reasoning_effort, trong khi request body không có trường đó. Vì vậy, param chỉ nên được xem là một manh mối. Khi ghi log chẩn đoán, hãy lưu bản đã loại bỏ bí mật của status code, message, code, gợi ý param, model ID và thời gian gửi.
Với lỗi tạm thời, có thể retry theo backoff với giới hạn rõ ràng rồi chuyển sang model dự phòng đã kiểm tra. Trước khi thêm model dự phòng, hãy đọc tài liệu endpoint tương thích và xem danh mục model hiện tại để xác nhận ID và quyền của tài khoản.
Nếu tool có thể gửi tin nhắn, tạo đơn hoặc ghi dữ liệu, retry phải dừng trước side effect. Phần thực thi tool cần idempotency key hoặc cơ chế chống trùng lặp riêng.
Lỗi 4: streaming có thể chứa content=null và choices=[]
Bài kiểm tra streaming gửi cùng câu “đếm đến 3” cho DeepSeek và Terra rồi so sánh các chunk.

Với deepseek-v4-flash, nhiều chunk trong giai đoạn suy luận có content=null, còn dữ liệu nằm trong reasoning_content. Câu trả lời cuối vẫn đi qua content. Nếu giao diện chỉ hiển thị content, người dùng có thể tưởng rằng request bị treo.
Với gpt-6.1-terra, delta còn có refusal và field lọc nội dung. Chunk cuối dùng để trả usage có choices=[]. Code luôn truy cập chunk.choices[0] sẽ bị lỗi chỉ số dù phần lớn stream đã thành công.
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Đếm đến 3"}],
stream=True,
stream_options={"include_usage": True},
)
answer_parts = []
usage = None
for chunk in stream:
if chunk.usage is not None:
usage = chunk.usage
if not chunk.choices:
continue
delta = chunk.choices[0].delta
if delta.content:
answer_parts.append(delta.content)
reasoning = getattr(delta, "reasoning_content", None)
if reasoning:
# Theo dõi riêng trạng thái suy luận, không trộn vào câu trả lời cuối.
pass
answer = "".join(answer_parts)
Khi lặp stream qua SDK, thường không cần tự parse lớp SSE. Nếu đọc HTTP stream trực tiếp, hãy xử lý cả dòng comment như : PING, data: [DONE], chunk rỗng và event không parse được.
Lỗi 5: không phải field số nào trong usage cũng được cộng trực tiếp
Ba model đều trả về prompt_tokens, completion_tokens và total_tokens, nhưng field mở rộng không giống nhau. Có route trả thêm chi tiết input/output, có route có checkpoint độ trễ, và có route trả alias cùng field cache.
Những số này có thể là chi tiết, alias hoặc metadata tính phí. Cộng toàn bộ field số có thể đếm trùng.
Trong ứng dụng, nên chuẩn hóa ba giá trị chính và giữ raw payload đã loại bỏ dữ liệu nhạy cảm khi thực sự cần:
normalized_usage = {
"input": usage.prompt_tokens,
"output": usage.completion_tokens,
"total": usage.total_tokens,
}
Phần tính chi phí nên tách riêng để xử lý giá model, quy tắc cache và cách đặt tên field của từng nhà cung cấp. Trước khi triển khai rộng, hãy đối chiếu một vài request thật với hóa đơn.
Checklist trước khi đưa vào production
Request không streaming. Xác nhận URL, key, model ID và nội dung trả về.
Tham số riêng của model. Kiểm tra temperature và các field tùy chọn mà ứng dụng gửi.
Structured output. Parse ở phía ứng dụng và kiểm tra field thiếu, sai kiểu, field thừa.
Streaming. Xử lý reasoning, refusal, choices rỗng, câu trả lời cuối và chunk usage.
Tool calling. Kiểm tra tên hàm, tham số, retry và ranh giới trước side effect.
Usage. So sánh field chuẩn hóa với một số bản ghi tính phí.
Lưu base_url, api_key và model cùng nhau trong cấu hình triển khai để rollback khôi phục đúng một route. HTTP 200 đầu tiên chỉ chứng minh kết nối hoạt động; migration hoàn tất khi những khả năng mà ứng dụng thực sự dùng đều vượt qua kiểm tra.
All rights reserved