gpt-5.6-luna 调用报 400 bad_request 怎么办?同样的 messages 在 gpt-5.5 没问题------两处校验差异和修复写法
上周三把一个 RAG pipeline 从 gpt-5.5 切到 gpt-5.6-luna,代码一行没改,直接炸了一片 400。结论先放这儿:gpt-5.6-luna 对 messages 数组的校验比 gpt-5.5 严了两处------一是 assistant 消息在非 tool_calls 场景下 content 不能为 null,二是连续同 role 消息会被拒绝(gpt-5.5 的处理行为与 5.6 不同)。修起来不难,但不知道规则变了的话能排查一整天。
下面把完整排查路径和修复代码贴出来,五分钟搞定。
声明 :
gpt-5.5、gpt-5.6-luna、gpt-5.6-sol、gpt-5.6-terra均为真实存在的模型 ID,但本文描述的两处校验差异系基于实际踩坑经验总结,OpenAI 官方未就版本间校验变更发布专项说明,具体行为请以你实际调用的返回结果为准。
为什么会出现这个问题
gpt-5.6 系列(包括 gpt-5.6-sol、gpt-5.6-luna、gpt-5.6-terra)上线后,OpenAI 收紧了 messages 结构校验。gpt-5.5 时代那些"虽然不规范但能跑"的写法,到 5.6 直接 400。
你的参数没写错,是规则变了。
排查第一步:先确认模型能访问
别急着调试 messages,先排除最蠢的可能------模型名打错了或者你的 Key 没权限。
python
import openai
client = openai.OpenAI()
models = [m.id for m in client.models.list()]
print("gpt-5.6-luna" in models)
注意 :
/v1/models接口不保证列出所有可用模型,部分模型需要特定权限才会出现在列表中。如果返回False,不一定说明模型不存在或无权限,也可能是该模型未出现在列表里------建议结合实际报错的error.code字段综合判断,而不是单独依赖这个检查。
如果模型名无效或无权限,你看到的报错长这样:
openai.BadRequestError: Error code: 400 - {'error': {'message': 'The model `gpt-5.6-luna` does not exist or you do not have access to it.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_found'}}
注意这个 model_not_found 是以 400 而不是 404 返回的,挺反直觉。有用户反映过这个坑------一堆人以为是请求体的问题,其实是模型访问权限。
如果你用的是 OpenRouter、ofox.io 这类聚合 API 网关,要确认平台侧已经上架了 gpt-5.6-luna 这个 model ID,否则同样会在这一步拿到 model_not_found 的 400,而不是后续的 messages 格式问题。
确认模型能访问之后,继续往下。
排查第二步:定位具体是哪种 400
同一个 400 状态码背后可能是三种完全不同的问题。关键是读 error.code 字段:
python
try:
pass # 你的 API 调用放这里
except openai.BadRequestError as e:
# e.error 是 openai-python v1.x 推荐的访问方式
# e.body 的类型在不同版本下不保证为 dict,直接用 e.body['error'] 可能抛 TypeError
err = e.error
if err:
print(f"code: {err.code}, message: {err.message}")
else:
# 兜底:e.body 可能是 dict,也可能是其他类型,先做类型检查
body = e.body
if isinstance(body, dict) and "error" in body:
err_dict = body["error"]
print(f"code: {err_dict.get('code')}, param: {err_dict.get('param')}")
else:
print(f"raw body: {body}")
| error.code | 含义 | 排查方向 |
|---|---|---|
model_not_found |
模型名无效或无权限 | 检查拼写、检查 Key 权限 |
invalid_value |
messages 某个字段值不合法 | 看 param 字段定位到第几条消息 |
context_length_exceeded |
token 超了 | 截断历史消息 |
我那次踩的坑是 invalid_value,param 指向 messages[3].content。
新增校验规则一:非 tool_calls 场景下 content 不能为 null
重要说明 :OpenAI 官方 API 文档明确指出,当 assistant 消息包含 tool_calls 时,content 字段可以为 null ,这是合法写法。以下讨论的是非 tool_calls 场景下的普通 assistant 消息。
gpt-5.6-luna 的行为:普通 assistant 消息 content 为 None 时直接拒绝,报错如下:
openai.BadRequestError: Error code: 400 - {'error': {'message': "Invalid 'messages[1].content': expected a string, but got null instead.", 'type': 'invalid_request_error', 'param': 'messages[1].content', 'code': 'invalid_value'}}
修复写法:仅对非 tool_calls 消息把 null 替换成空字符串。
python
for msg in messages:
if msg.get("content") is None and not msg.get("tool_calls"):
msg["content"] = ""
注意:如果消息带有 tool_calls,不要把 content 从 null 改为空字符串,保留 null 才是符合官方规范的写法。
如果你通过聚合 API 网关调用 gpt-5.6-luna,这个修复同样需要在客户端侧做------网关层通常透传 messages 结构,不会自动补全 content 字段,invalid_value 的 400 会原样返回给你。
新增校验规则二:连续同 role 消息被拒绝
gpt-5.6-luna 的行为:返回 400,error.code 是 invalid_value,message 大意是 "messages must alternate between user and assistant roles"(具体措辞以实际返回为准)。
这在什么时候出现?多轮对话拼接时,如果你的逻辑是"用户连续发了两条消息就都塞进去",5.6 会直接拒绝。
修复写法:合并连续同 role 消息。
python
def merge_consecutive(messages):
if not messages:
return messages
merged = [messages[0]]
for msg in messages[1:]:
if msg["role"] == merged[-1]["role"]:
# 注意:直接用换行符拼接对结构化内容(如 JSON、代码块)可能破坏语义,
# 如果你的消息内容有特殊格式,建议根据业务逻辑选择合适的分隔方式
prev_content = merged[-1]["content"] if merged[-1]["content"] is not None else ""
curr_content = msg["content"] if msg["content"] is not None else ""
merged[-1]["content"] = prev_content + "\n" + curr_content
else:
merged.append(msg)
return merged
跑一遍 messages = merge_consecutive(messages) 再发请求就行。注意:带 tool_calls 的 assistant 消息通常不应被合并,实际使用时建议根据业务逻辑单独处理这类消息。
完整修复流程图
常见问题 FAQ
Q: gpt-5.6-luna 和 gpt-5.6-sol、gpt-5.6-terra 有什么区别?
三个是 GPT-5.6 的不同变体。各变体的具体定位和能力差异请参考 OpenAI 官方文档,本文不做推断性描述。messages 校验规则三个实测下来都一样严格。
Q: 我用 gpt-5.5 同样的请求体没问题,切到 luna 就 400,是 bug 吗?
不是 bug,是 5.6 系列收紧了校验。修复方法见上面两处规则。
Q: content 为空字符串会报错吗?
不会。在非 tool_calls 场景下,只有 null/None 才触发 400,空字符串 "" 是合法值。带 tool_calls 的 assistant 消息 content 为 null 是官方允许的合法写法,不需要修改。
Q: 用第三方 API 代理时报 400 怎么确认是代理的问题还是 OpenAI 的问题?
把 base_url 切回 https://api.openai.com/v1 用官方直连跑一遍同样的请求。如果官方也 400,就是你的 messages 格式问题;如果官方正常代理 400,找代理的文档确认模型名白名单。
Q: 怎么一次性检查 messages 数组有没有这两个问题?
python
def validate_messages(msgs):
for i, m in enumerate(msgs):
if m.get("content") is None and not m.get("tool_calls"):
print(f"⚠️ messages[{i}].content is null(且无 tool_calls,需修复)")
if i > 0 and m["role"] == msgs[i-1]["role"]:
print(f"⚠️ messages[{i}] 连续同 role")
跑一遍就知道哪里要改。
小结
gpt-5.6-luna 的 400 不是你代码写错了,是新版校验比 gpt-5.5 严了两处:非 tool_calls 场景下 null content 不再容忍、连续同 role 不再自动处理。需要特别注意的是,带 tool_calls 的 assistant 消息 content 保持 null 才是规范写法,不要错误地将其改为空字符串。修起来各加两三行代码的事,但不知道规则变了确实能排查到怀疑人生。如果你通过 OpenRouter 或 ofox.io 等 API 网关路由请求,上述两处 messages 格式修复同样适用,网关层不会替你做结构订正,客户端侧的 validate_messages + merge_consecutive 该跑还是要跑。