gpt4free(g4f)推理字段标准化:统一 reasoning 输出格式与双向输入兼容设计
在 gpt4free(下称 g4f)的多供应商聚合架构中,DeepSeek 使用 reasoning_content 字段而 OpenAI 使用 reasoning 字段来携带思维链(reasoning)内容,这一命名分歧曾让下游消费方不知该读哪个字段。本文基于仓库中的决策文档 reasoning-standardization.md 展开,讲清 g4f 如何以 OpenAI 的 reasoning 字段作为统一输出标准、同时保留双格式输入兼容,并结合 g4f/providers/response.py、g4f/client/stubs.py、g4f/Provider/template/OpenaiTemplate.py 等源码,完整梳理该标准化从「供应商响应解析」到「客户端模型序列化」再到「内部 JSON 流」的全链路实现与测试验证方式。
问题背景:两种字段命名的冲突
g4f 的核心价值在于把大量不同后端(OpenAI 兼容 API、DeepSeek、本地模型、各类逆向/免费接口)的流式响应收敛成统一的内部响应类型。但在「推理型模型」(DeepSeek-R1、o 系列、Kimi 等)的场景下,各家供应商对思维链内容在 chat completion 流式响应中的字段命名并不一致:
- OpenAI 格式:
choices[0].delta.reasoning - DeepSeek 格式:
choices[0].delta.reasoning_content
决策文档明确指出,这一不一致曾造成「g4f Interference API 到底该输出哪个字段名」的困惑。最终决策为:API 输出标准化为 OpenAI 的 reasoning 字段格式,同时保持输入侧对两种格式均兼容。
标准化决策与理由
文档给出了四点标准化依据,理解它们有助于把握 g4f 的整体 API 设计取向:
- OpenAI 兼容性:OpenAI 是 chat completion API 的事实标准;
- 生态兼容性:绝大多数工具库与 SDK 默认按 OpenAI 格式解析响应;
- 一致性:无论底层 provider 是谁,对外输出格式保持统一;
- 向后兼容:输入解析继续同时接受
reasoning_content与reasoning两种格式。
这个「输出收敛、输入宽进」的策略在 g4f 中是普遍适用的模式——它避免了让每个下游客户端去感知底层 provider 的差异。
输入侧兼容:OpenaiTemplate 的双字段解析
决策文档中引用的核心实现位于 g4f/Provider/template/OpenaiTemplate.py:
reasoning_content = choice.get("delta", {}).get("reasoning_content", choice.get("delta", {}).get("reasoning"))
这行代码利用 dict.get 的默认值机制实现了「先查 DeepSeek 字段,再回退到 OpenAI 字段」的兼容读取。在源码中可以确认,这一模式出现在 read_response 函数(g4f/Provider/template/OpenaiTemplate.py)的两个分支中:
流式(SSE)分支(L435-L440):
reasoning_content = choice.get("delta", {}).get(
"reasoning_content", choice.get("delta", {}).get("reasoning")
)
if reasoning_content:
reasoning = True
yield Reasoning(reasoning_content)
这里除了提取字段外,还维护了一个 reasoning 布尔状态:当思维链 token 流之后正式回复的 content 开始时(见 L422-L431),会先 yield Reasoning(status="") 发送一个「推理结束」标记,把思维链段落与正文段落清晰切分。从源码结构看,这个状态机使得 g4f 的 SSE 消费方能可靠地区分「模型在想」和「模型在说」。
非流式(JSON)分支(L385-L390)对 choice 做同样的双字段探测,命中后以空 status 的 Reasoning 对象发出。
值得注意的是,OpenaiTemplate.create_async_generator 的 extra_parameters 白名单(L203-L218)中同时包含 reasoning_effort、include_reasoning 等参数,说明输入侧标准化不止于响应解析——请求侧对各家「开启/控制推理」的参数也做了透传归一。
内部表示:Reasoning 响应类型
供应商响应被解析后,统一封装为 g4f/providers/response.py 中的 Reasoning 类型:
class Reasoning(ResponseType):
def __init__(
self,
token: Optional[str] = None,
label: Optional[str] = None,
status: Optional[str] = None,
is_thinking: Optional[str] = None,
) -> None:
...
Reasoning 承担三种语义角色:
- 携带思维链 token:
token字段保存增量文本,get_dict()返回{"token": ..., "status": ...}; - 状态标记:
status用于「开始思考/停止思考」的边界信号(例如上面流式分支中Reasoning(status="")表示推理段结束); - 字符串化规则:
__str__按is_thinking→token→status/label的优先级回退,保证无论哪种形态都能被str()安全消费。
正因为内部类型与字段命名彻底解耦(g4f 内部只认 Reasoning 对象,不关心供应商用哪个 key 命名),输出侧才能在序列化时统一改写为 reasoning。
输出侧标准化:client stubs 统一使用 reasoning 字段
文档「Files Changed」一节提到 g4f/client/stubs.py 已由 reasoning_content 改为 reasoning 字段。当前源码中可以直接验证这一决策的落地:
流式 Delta(ChatCompletionDelta,g4f/client/stubs.py):
class ChatCompletionDelta(BaseModel):
role: str
content: Optional[str]
reasoning: Optional[str] = None
tool_calls: list[ToolCallModel] = None
@classmethod
def model_construct(cls, content: Optional[str]):
if isinstance(content, Reasoning):
return super().model_construct(
role="assistant", content=None, reasoning=str(content)
)
...
关键点:当 g4f 内部流产出一个 Reasoning 对象时,ChatCompletionDelta.model_construct 会把它放入 reasoning 字段并置 content=None;field_serializer("content") 还会兜底处理历史数据中 content 里残留 Reasoning/ToolCalls 对象的情况,确保序列化时思维链内容只会出现在 reasoning 键上。由此产出的流式响应与文档给出的 OpenAI 兼容格式一致:
{
"id": "chatcmpl-example",
"object": "chat.completion.chunk",
"choices": [{
"index": 0,
"delta": {
"role": "assistant",
"reasoning": "I need to think about this step by step..."
},
"finish_reason": null
}]
}
非流式 Message(ChatCompletionMessage,g4f/client/stubs.py):
class ChatCompletionMessage(BaseModel):
role: str
content: str
reasoning: Optional[str] = None
...
@classmethod
def model_construct(
cls, content: str, reasoning: list[Reasoning] = None, tool_calls: list = None
):
if reasoning is not None and isinstance(reasoning, list):
reasoning = "".join([str(content) for content in reasoning])
return super().model_construct(
role="assistant",
content=content,
**filter_none(tool_calls=tool_calls, reasoning=reasoning),
)
非流式路径接收的是一段流中收集到的 Reasoning 列表,通过 "".join([str(...)]) 拼接为完整思维链文本后写入 message.reasoning;filter_none 保证无推理内容时该键不会出现在 JSON 中(不产生空字段噪音)。ChatCompletion.model_construct(L266-L293)则把 reasoning 参数一路透传到 message 层。这与文档给出的非流式格式对应:
{
"choices": [{
"message": {
"role": "assistant",
"content": "Here's my answer",
"reasoning": "My reasoning process was..."
}
}]
}
此外,用量统计中同样沿用了 reasoning 前缀的 OpenAI 命名:CompletionTokenDetails.reasoning_tokens(g4f/client/stubs.py),使计费/用量维度也与输出字段命名保持同源。
另一条输出路径:内部 JSON 流中的 reasoning 事件
g4f 除 OpenAI 兼容端点外,还有 GUI/前端消费的 JSON 事件流。在 g4f/gui/server/api.py 中:
elif isinstance(chunk, Reasoning):
yield self._format_json("reasoning", **chunk.get_dict())
即内部流中 Reasoning 对象被格式化为 {"type": "reasoning", "token": ..., "status": ...} 事件。测试用例 test_reasoning_standardization.py 中的 test_current_api_format_consistency 正是模拟了这段 _format_json 逻辑,验证该事件的字段结构稳定;而 test_openai_compatible_streaming_format 与 test_deepseek_compatible_format 两个用例则分别构造了两种上游命名下的 delta 结构,作为「输入兼容、输出统一」这一决策的可执行证据:
test_streaming_delta_with_reasoning:验证Reasoning对象经ChatCompletionDelta.model_construct后落在delta.reasoning上,且role="assistant"、content=None;test_reasoning_object_structure:验证Reasoning.get_dict()的{"token": ..., "status": ...}结构与str()行为;test_proposed_standardization:显式断言标准化后的 g4f 流式输出应使用reasoning键而非reasoning_content。
边界说明:何时仍会出现 reasoning_content
需要区分两个层面,避免误读「标准化」的范围:
-
g4f 对外输出(client stubs 的 OpenAI 兼容端点):只输出
reasoning,不含reasoning_content; -
g4f 对上游供应商的转发:内部仍可能使用
reasoning_content。例如 g4f/tools/run_tools.py 中的注释明确写道,思考模式下reasoning_content必须回传给 DeepSeek 类 API:# The `reasoning_content` in the thinking mode must be passed back to the API. for message in messages: if isinstance(message, dict) and message.get("role") == "assistant" and message.get("tool_calls"): message["reasoning_content"] = message.get("reasoning_content", "")也就是说,工具调用循环(tool loop)中携带历史 assistant 消息回传上游时,DeepSeek 侧的字段名要求被原样保留——这是「输入/上游兼容」原则在请求侧的体现,与「输出标准化」并不矛盾。
小结与实践指引
- 下游集成 g4f 的 OpenAI 兼容端点时,统一读取
choices[0].delta.reasoning(流式)与choices[0].message.reasoning(非流式),不要依赖reasoning_content; - 对接 g4f 作为上游时(例如把 g4f 输出写入其他供应商的上下文),若目标供应商要求
reasoning_content(如 DeepSeek 思考模式),需要在转发层自行映射字段名; - 排查字段落点时,按「供应商 SSE →
OpenaiTemplate.read_response双字段解析 →Reasoning对象 →ChatCompletionDelta/ChatCompletionMessage序列化」这条链路定位即可,各环节均可在 g4f/Provider/template/OpenaiTemplate.py、g4f/providers/response.py、g4f/client/stubs.py 中直接查证,回归验证可参考 etc/unittest/test_reasoning_standardization.py。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00