my-freecode
§ · Nested detail page

协议转换与流式输出 深度解析

拆解 Anthropic blocks、OpenAI function calls、Responses 包装和 SSE delta 的转换边界。

§ 00 · IMPORT CHAIN

导入链与契约

▸入口契约

协议转换的入口是 sources/my-free-code/my_free_code/core/anthropic.py:11。它把 Anthropic 的 system、messages 和 tools 改成 OpenAI Chat payload;sources/my-free-code/my_free_code/core/anthropic.py:24 到 sources/my-free-code/my_free_code/core/anthropic.py:58 逐块处理文本、图片、tool_use 与 tool_result,避免把工具调用当普通文本丢掉。

sources/my-free-code/my_free_code/core/anthropic.py:11 - sources/my-free-code/my_free_code/core/anthropic.py:83Python
11def anthropic_to_openai(body: dict, remote_model: str) -> dict:
12    messages = []
13    system = body.get("system")
14    if system:
15        messages.append({"role": "system", "content": content_text(system)})
16
17    for msg in body.get("messages", []):
18        role = msg.get("role", "user")
19        content = msg.get("content", "")
20        if isinstance(content, str):
21            messages.append({"role": role, "content": content})
22            continue
23
24        parts = []
25        tool_calls = []
26        for block in content:
27            kind = block.get("type")
28            if kind == "text":
29                parts.append({"type": "text", "text": block.get("text", "")})
30            elif kind == "image":
31                src = block.get("source", {})
32                if src.get("type") == "base64":
33                    parts.append({
34                        "type": "image_url",
35                        "image_url": {
36                            "url": f"data:{src.get('media_type','image/png')};base64,{src.get('data','')}"
37                        }
38                    })
39            elif kind == "tool_use":
40                tool_calls.append({
41                    "id": block.get("id"),
42                    "type": "function",
43                    "function": {
44                        "name": block.get("name"),
45                        "arguments": json.dumps(block.get("input", {}))
46                    }
47                })
48            elif kind == "tool_result":
49                messages.append({
50                    "role": "tool",
51                    "tool_call_id": block.get("tool_use_id"),
52                    "content": content_text(block.get("content", ""))
53                })
54
55        item = {"role": role, "content": parts}
56        if tool_calls:
57            item["tool_calls"] = tool_calls
58        messages.append(item)
59
60    out = {
61        "model": remote_model,
62        "messages": messages,
63        "stream": bool(body.get("stream")),
64        "max_tokens": body.get("max_tokens", 4096),
65    }
66    for key in ("temperature", "top_p", "stop"):
67        if key in body:
68            out[key] = body[key]
69
70    if body.get("tools"):
71        out["tools"] = [{
72            "type": "function",
73            "function": {
74                "name": t["name"],
75                "description": t.get("description", ""),
76                "parameters": t.get("input_schema", {}),
77            }
78        } for t in body["tools"]]
79
80    if body.get("tool_choice"):
81        out["tool_choice"] = body["tool_choice"]
82
83    return out
§ 01 · CORE IMPL

核心实现,逐段拆

▸OpenAI Adapter 的 HTTP 边界

OpenAIChatAdapter 是大多数 provider 的默认执行器。sources/my-free-code/my_free_code/providers/adapters.py:24 到 sources/my-free-code/my_free_code/providers/adapters.py:36 处理非流式请求,sources/my-free-code/my_free_code/providers/adapters.py:38 到 sources/my-free-code/my_free_code/providers/adapters.py:64 处理 SSE 行流;两者都在 sources/my-free-code/my_free_code/providers/adapters.py:34 或 sources/my-free-code/my_free_code/providers/adapters.py:51 对上游错误做硬失败。

sources/my-free-code/my_free_code/providers/adapters.py:23 - sources/my-free-code/my_free_code/providers/adapters.py:64Python
23class OpenAIChatAdapter(ProviderAdapter):
24    async def complete(self, ctx, payload):
25        headers = {"Content-Type": "application/json"}
26        if ctx.api_key:
27            headers["Authorization"] = f"Bearer {ctx.api_key}"
28        async with httpx.AsyncClient(timeout=ctx.timeout) as client:
29            response = await client.post(
30                ctx.base_url.rstrip("/") + "/chat/completions",
31                headers=headers,
32                json=payload,
33            )
34        if response.status_code >= 400:
35            raise RuntimeError(f"upstream HTTP {response.status_code}: {response.text[:1000]}")
36        return response.json()
37
38    async def stream(self, ctx, payload):
39        headers = {"Content-Type": "application/json"}
40        if ctx.api_key:
41            headers["Authorization"] = f"Bearer {ctx.api_key}"
42        payload = dict(payload)
43        payload["stream"] = True
44        async with httpx.AsyncClient(timeout=ctx.timeout) as client:
45            async with client.stream(
46                "POST",
47                ctx.base_url.rstrip("/") + "/chat/completions",
48                headers=headers,
49                json=payload,
50            ) as response:
51                if response.status_code >= 400:
52                    text = (await response.aread()).decode(errors="replace")
53                    raise RuntimeError(f"upstream HTTP {response.status_code}: {text[:1000]}")
54                async for line in response.aiter_lines():
55                    if not line.startswith("data:"):
56                        continue
57                    value = line[5:].strip()
58                    if value == "[DONE]":
59                        return
60                    try:
61                        import json
62                        yield json.loads(value)
63                    except ValueError:
64                        continue

▸流式 delta 的 Anthropic 化

streaming 模块只有一个小转换器,但它决定前端看到的事件类型。sources/my-free-code/my_free_code/core/streaming.py:3 到 sources/my-free-code/my_free_code/core/streaming.py:4 负责 SSE 字符串,sources/my-free-code/my_free_code/core/streaming.py:6 到 sources/my-free-code/my_free_code/core/streaming.py:13 把 content delta 改成 text_delta,sources/my-free-code/my_free_code/core/streaming.py:14 到 sources/my-free-code/my_free_code/core/streaming.py:21 保留 thinking_delta 的可能性。

sources/my-free-code/my_free_code/core/streaming.py:1 - sources/my-free-code/my_free_code/core/streaming.py:22Python
1import json
2
3def sse(event: str, data: dict) -> str:
4    return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
5
6def openai_delta_to_anthropic(delta: dict, index: int = 0):
7    events = []
8    if delta.get("content"):
9        events.append(sse("content_block_delta", {
10            "type": "content_block_delta",
11            "index": index,
12            "delta": {"type": "text_delta", "text": delta["content"]},
13        }))
14    # Preserve provider reasoning when it is exposed in a common field.
15    reasoning = delta.get("reasoning_content") or delta.get("reasoning")
16    if reasoning:
17        events.append(sse("content_block_delta", {
18            "type": "content_block_delta",
19            "index": index,
20            "delta": {"type": "thinking_delta", "thinking": reasoning},
21        }))
22    return events
§ 02 · KEY FLOW

关键流程

Anthropic content blocks -> anthropic_to_openai -> OpenAIChatAdapter -> provider JSON/SSE -> openai_to_anthropic or text_delta events

Responses API 在 sources/my-free-code/my_free_code/api/routes.py:117 到 sources/my-free-code/my_free_code/api/routes.py:124 复用 Messages 路径,所以新增协议入口没有分裂 provider 层。sources/my-free-code/my_free_code/core/responses.py:1 到 sources/my-free-code/my_free_code/core/responses.py:22 做输入规范化,sources/my-free-code/my_free_code/core/responses.py:24 到 sources/my-free-code/my_free_code/core/responses.py:48 做输出包装。

§ 03 · EDGE CASES

边界 / 失败模式

  • sources/my-free-code/my_free_code/core/anthropic.py:96 到 sources/my-free-code/my_free_code/core/anthropic.py:99 会把无法解析的 tool arguments 包成 raw,避免整个响应崩掉。
  • sources/my-free-code/my_free_code/providers/adapters.py:54 到 sources/my-free-code/my_free_code/providers/adapters.py:64 忽略非 data 行和坏 JSON,容错强但可能吞掉 provider 诊断信息。
  • sources/my-free-code/my_free_code/api/routes.py:90 到 sources/my-free-code/my_free_code/api/routes.py:100 当前只读取 text content,没有调用 openai_delta_to_anthropic,因此 reasoning delta 未真正穿透主流式路径。
Sub-page · 2026-08-31 Source · hkqr/my-free-code ↑ my-free-code 主页面