OpenAI vs Anthropic API 对比:流式返回 + Adapt 适配层完整方案

原文:OpenAI vs Anthropic API 对比:响应体、请求体、消息格式与工具调用 --- 作者 冯叶青

本文在原文基础上,新增 第六节:流式返回场景下的 Adapt 适配层实现


一、响应体格式区别

OpenAI 响应结构

json 复制代码
{
  "id": "chatcmpl-xxx",
  "object": "chat.completion",
  "created": 1234567890,
  "model": "gpt-4",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "回复内容"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": { ... }
}

Anthropic 响应结构

json 复制代码
{
  "id": "msg_xxx",
  "type": "message",
  "role": "assistant",
  "content": [
    { "type": "text", "text": "回复内容" }
  ],
  "model": "claude-3-sonnet-...",
  "stop_reason": "end_turn",
  "usage": { ... }
}
特性 OpenAI Anthropic
消息位置 choices[0].message 顶层 role + content
content 类型 字符串(或数组) 数组(纯文本也需数组形式)
结束原因字段 finish_reason stop_reason
判断依据 存在 "choices" → OpenAI 存在 "type": "message" → Anthropic

二、请求体格式区别

OpenAI

json 复制代码
{
  "model": "gpt-4",
  "messages": [
    { "role": "system", "content": "你是一个助手" },
    { "role": "user", "content": "你好" }
  ],
  "temperature": 0.7,
  "max_tokens": 1000
}

Anthropic

json 复制代码
{
  "model": "claude-3-sonnet-20240229",
  "system": "你是一个助手",
  "messages": [
    { "role": "user", "content": "你好" }
  ],
  "temperature": 0.7,
  "max_tokens": 1000
}
特性 OpenAI Anthropic
系统提示 messagesrole: "system" 顶层 system 字段
消息角色 system, user, assistant, tool user, assistant
max_tokens 可选 必填
content 类型 字符串 必须数组
角色顺序 无强制交替 严格交替,首条必为 user

三、消息角色与格式注意事项

OpenAI 角色

  • system:设定行为
  • user:用户输入
  • assistant:模型回复
  • tool:工具调用结果

Anthropic 角色

userassistant。系统指令在顶层 system 字段。

严格约束

  • ❌ 首条非 user → 报错
  • ❌ 未严格交替 → 报错
  • content 为字符串 → 失败
  • ❌ 最后一条 assistant 消息尾随空白 → 特殊失败

四、工具调用完整示例

4.1 OpenAI --- 第一轮(定义工具 → 返回 tool_calls)

json 复制代码
{
  "model": "gpt-4o",
  "messages": [
    { "role": "user", "content": "北京天气怎么样?" }
  ],
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "获取指定城市的天气",
      "parameters": {
        "type": "object",
        "properties": { "city": { "type": "string" } },
        "required": ["city"]
      }
    }
  }]
}

返回:

json 复制代码
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" }
      }]
    }
  }]
}

4.1 OpenAI --- 第二轮(回传工具结果)

json 复制代码
{
  "messages": [
    { "role": "user", "content": "北京天气怎么样?" },
    { "role": "assistant", "content": null, "tool_calls": [...] },
    { "role": "tool", "tool_call_id": "call_abc123", "content": "{\"temperature\":\"22°C\"}" }
  ]
}

4.2 Anthropic --- 第一轮

json 复制代码
{
  "model": "claude-3-sonnet-20240229",
  "system": "You are a helpful assistant.",
  "messages": [
    { "role": "user", "content": [{ "type": "text", "text": "北京天气怎么样?" }] }
  ],
  "tools": [{
    "name": "get_weather",
    "description": "获取指定城市的天气",
    "input_schema": {
      "type": "object",
      "properties": { "city": { "type": "string" } },
      "required": ["city"]
    }
  }],
  "max_tokens": 1024
}

返回:

json 复制代码
{
  "role": "assistant",
  "content": [
    { "type": "text", "text": "好的,我来查询北京的天气。" },
    { "type": "tool_use", "id": "toolu_01AbC", "name": "get_weather", "input": { "city": "北京" } }
  ],
  "stop_reason": "tool_use"
}

4.2 Anthropic --- 第二轮

json 复制代码
{
  "messages": [
    { "role": "user", "content": [...] },
    { "role": "assistant", "content": [ /* 全文 */ ] },
    { "role": "user", "content": [{
      "type": "tool_result", "tool_use_id": "toolu_01AbC",
      "content": "{\"temperature\":\"22°C\"}"
    }] }
  ]
}

五、总结与建议

对比维度 OpenAI Anthropic
设计哲学 messages 统一时序容器 分离系统指令与对话历史
角色丰富度 4 种角色 2 种核心角色
严格程度 宽容 严格交替、首条必为 user
工具调用 独立 tool 角色 + tool_calls tool_use/tool_result 块嵌入 content
易错点 角色名拼错、缺少字段 内容非数组、未交替、尾随空格

六、流式返回场景下的 Adapt 适配层实现

前面讲的都是**非流式(一次返回完整结果)**的场景。但在实际产品中,几乎都会用流式(Streaming)来提升用户体验------逐字输出,不用等。

问题来了:OpenAI 和 Anthropic 的流式 SSE 事件格式、字段名、语义完全不一样。如果业务代码直接对接两种格式,很快就会变成意大利面条。

6.1 两种 API 的流式格式一览

OpenAI 流式 SSE 事件
json 复制代码
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"你"},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"好"},"finish_reason":null}]}

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

关键字段:

  • 文本增量choices[0].delta.content
  • 工具调用增量choices[0].delta.tool_calls[0].function.name / .arguments
  • 结束choices[0].finish_reason 非 null,或收到 [DONE]
Anthropic 流式 SSE 事件
json 复制代码
event: message_start
data: {"type":"message_start","message":{"id":"msg_xxx","role":"assistant","content":[],"model":"claude-sonnet-4-20250514","usage":{"input_tokens":15}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"你"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"好"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":8}}

event: message_stop
data: {"type":"message_stop"}

关键事件类型:

事件 type 含义
message_start 消息开始,含 input_tokens
content_block_start 内容块开始(text 或 tool_use)
content_block_delta 内容增量(text_deltainput_json_delta
content_block_stop 内容块结束
message_delta 消息级增量(stop_reason, output_tokens)
message_stop 流结束
ping 心跳

工具调用流式示例

json 复制代码
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01X","name":"get_weather","input":{}}}

event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"city\":\"北京\"}"}}

event: content_block_stop
data: {"type":"content_block_stop","index":1}

6.2 核心矛盾

把它们并排放在一起看,差距一目了然:

复制代码
OpenAI 流:一个 flat 的 JSON chunk 流,每个 chunk 带一个 delta 字段
Anthropic 流:一个事件机(状态机),有 start/delta/stop 生命周期

直接在上层业务里 if (openai) ... else if (anthropic) ... 当然可以,但你很快会遇到:

  • 工具调用时 OpenAI 是 tool_calls[0].function.arguments 逐 chunk 拼接 JSON 字符串,而 Anthropic 是 input_json_delta.partial_json
  • 结束语义:OpenAI 靠 finish_reason[DONE],Anthropic 靠 message_delta.stop_reasonmessage_stop
  • 用量信息:OpenAI 在最后的 chunk 或没有,Anthropic 在 message_startmessage_delta 中分两次给

6.3 统一抽象:设计一个 Provider 无关的事件模型

与其让上层分别解析,不如定义一套统一事件(UnifiedEvent),让每个 Provider 的 Adapter 各自翻译。

python 复制代码
# ============================================================
# adapters/types.py --- 统一事件定义(业务层只和这个打交道)
# ============================================================

from dataclasses import dataclass
from typing import Literal, Union

@dataclass
class UsageInfo:
    input_tokens: int
    output_tokens: int

@dataclass
class MessageStart:
    type: Literal["message_start"] = "message_start"
    message_id: str = ""
    model: str = ""

@dataclass
class TextDelta:
    type: Literal["text_delta"] = "text_delta"
    text: str = ""

@dataclass
class ToolCallStart:
    type: Literal["tool_call_start"] = "tool_call_start"
    tool_call_id: str = ""
    name: str = ""

@dataclass
class ToolCallDelta:
    type: Literal["tool_call_delta"] = "tool_call_delta"
    tool_call_id: str = ""
    partial_json: str = ""

@dataclass
class ToolCallEnd:
    type: Literal["tool_call_end"] = "tool_call_end"
    tool_call_id: str = ""

@dataclass
class MessageEnd:
    type: Literal["message_end"] = "message_end"
    stop_reason: str | None = None
    usage: UsageInfo | None = None

@dataclass
class ErrorEvent:
    type: Literal["error"] = "error"
    code: str = ""
    message: str = ""

# 联合类型:任何 yield 出去的事件都是 UnifiedEvent
UnifiedEvent = Union[
    MessageStart,
    TextDelta,
    ToolCallStart,
    ToolCallDelta,
    ToolCallEnd,
    MessageEnd,
    ErrorEvent,
]

@dataclass 而非 TypedDict 的好处:类型检查更友好、可以有默认值、IDE 自动补全更准。

6.4 Adapt 层实现

核心思路:每个 Provider 的 Adapter 是一个 async generatorasync def + yield),吃进原始 SSE 行,吐出统一事件。

6.4.1 OpenAI Streaming Adapter
python 复制代码
# ============================================================
# adapters/openai_adapter.py --- OpenAI SSE → UnifiedEvent
# ============================================================

import json
from typing import AsyncIterator
from .types import (
    UnifiedEvent,
    MessageStart, TextDelta,
    ToolCallStart, ToolCallDelta, ToolCallEnd,
    MessageEnd, UsageInfo,
)


async def adapt_openai_stream(
    sse_lines: AsyncIterator[str],
) -> AsyncIterator[UnifiedEvent]:
    """OpenAI SSE 流 → 统一事件流

    OpenAI 的流式格式很 flat,每个 data: 行就是一个 JSON chunk。
    关键任务:
      1. 从 delta.content 提取文本增量
      2. 从 delta.tool_calls 提取工具调用增量(流式 JSON 片段拼接)
      3. finish_reason 非 None → message_end
      4. [DONE] → 流终止
    """
    message_id = ""
    model = ""
    started = False
    input_tokens = 0
    output_tokens = 0

    # 跟踪工具调用(OpenAI 按 index 区分,id 只在首个 chunk 给出)
    tool_name_map: dict[int, str] = {}   # index → name
    tool_id_map: dict[int, str] = {}     # index → tool_call_id

    async for line in sse_lines:
        # 跳过空行和注释
        line = line.strip()
        if not line or line.startswith(":"):
            continue

        # 解析 data: 前缀
        if not line.startswith("data: "):
            continue
        payload = line[6:]

        # 终止信号
        if payload == "[DONE]":
            break

        try:
            chunk = json.loads(payload)
        except json.JSONDecodeError:
            continue

        # 首次 chunk,提取元信息
        if not started:
            message_id = chunk.get("id", "")
            model = chunk.get("model", "")
            started = True
            yield MessageStart(message_id=message_id, model=model)

        choices = chunk.get("choices", [])
        if not choices:
            continue

        choice = choices[0]
        delta = choice.get("delta", {})
        finish_reason = choice.get("finish_reason")

        # --- 文本增量 ---
        content = delta.get("content")
        if content:
            yield TextDelta(text=content)

        # --- 工具调用增量 ---
        tool_calls = delta.get("tool_calls")
        if tool_calls:
            for tc in tool_calls:
                idx = tc.get("index", 0)

                # 第一次出现 → tool_call_start
                tc_id = tc.get("id")
                if tc_id:
                    name = tc.get("function", {}).get("name", "")
                    tool_id_map[idx] = tc_id
                    tool_name_map[idx] = name
                    yield ToolCallStart(tool_call_id=tc_id, name=name)

                # 参数增量
                fn = tc.get("function", {})
                arguments = fn.get("arguments")
                if arguments:
                    tc_id = tc.get("id") or tool_id_map.get(idx, "")
                    yield ToolCallDelta(
                        tool_call_id=tc_id,
                        partial_json=arguments,
                    )

        # --- 结束判断 ---
        if finish_reason:
            usage = chunk.get("usage")
            if usage:
                input_tokens = usage.get("prompt_tokens", 0)
                output_tokens = usage.get("completion_tokens", 0)
            yield MessageEnd(
                stop_reason=finish_reason,
                usage=UsageInfo(
                    input_tokens=input_tokens,
                    output_tokens=output_tokens,
                ),
            )
6.4.2 Anthropic Streaming Adapter
python 复制代码
# ============================================================
# adapters/anthropic_adapter.py --- Anthropic SSE → UnifiedEvent
# ============================================================

import json
from typing import AsyncIterator
from .types import (
    UnifiedEvent,
    MessageStart, TextDelta,
    ToolCallStart, ToolCallDelta, ToolCallEnd,
    MessageEnd, ErrorEvent, UsageInfo,
)


async def adapt_anthropic_stream(
    sse_lines: AsyncIterator[str],
) -> AsyncIterator[UnifiedEvent]:
    """Anthropic SSE 流 → 统一事件流

    Anthropic 的流是一个事件机,有多种事件类型,每种有不同的生命周期语义。
    关键任务:
      1. message_start → 提取模型名、input_tokens
      2. content_block_start(tool_use) → tool_call_start
      3. content_block_delta(text_delta) → text_delta
      4. content_block_delta(input_json_delta) → tool_call_delta
      5. content_block_stop → tool_call_end(如果当前块是工具块)
      6. message_delta → 提取 stop_reason、output_tokens
      7. message_stop → message_end + 流终止
    """
    message_id = ""
    model = ""
    input_tokens = 0
    output_tokens = 0
    stop_reason: str | None = None

    # 跟踪当前内容块的状态:index → ("text" | "tool_use")
    block_types: dict[int, str] = {}
    # index → tool_use_id
    block_tool_ids: dict[int, str] = {}

    async for line in sse_lines:
        line = line.strip()
        if not line or line.startswith(":"):
            continue

        # Anthropic SSE 可能有 event: 行和 data: 行
        # 简化处理:只解析 data: 行
        if not line.startswith("data: "):
            continue
        payload = line[6:]

        try:
            event = json.loads(payload)
        except json.JSONDecodeError:
            continue

        event_type = event.get("type")

        if event_type == "message_start":
            msg = event.get("message", {})
            message_id = msg.get("id", "")
            model = msg.get("model", "")
            input_tokens = msg.get("usage", {}).get("input_tokens", 0)
            yield MessageStart(message_id=message_id, model=model)

        elif event_type == "content_block_start":
            block = event.get("content_block", {})
            idx = event.get("index", 0)
            if block.get("type") == "tool_use":
                block_types[idx] = "tool_use"
                tool_id = block.get("id", "")
                block_tool_ids[idx] = tool_id
                yield ToolCallStart(
                    tool_call_id=tool_id,
                    name=block.get("name", ""),
                )
            elif block.get("type") == "text":
                block_types[idx] = "text"

        elif event_type == "content_block_delta":
            idx = event.get("index", 0)
            delta = event.get("delta", {})

            if delta.get("type") == "text_delta":
                text = delta.get("text", "")
                if text:
                    yield TextDelta(text=text)

            elif delta.get("type") == "input_json_delta":
                partial = delta.get("partial_json", "")
                if partial:
                    tool_id = block_tool_ids.get(idx, f"tool_{idx}")
                    yield ToolCallDelta(
                        tool_call_id=tool_id,
                        partial_json=partial,
                    )

        elif event_type == "content_block_stop":
            idx = event.get("index", 0)
            if block_types.get(idx) == "tool_use":
                tool_id = block_tool_ids.get(idx, f"tool_{idx}")
                yield ToolCallEnd(tool_call_id=tool_id)
                # 清理
                block_types.pop(idx, None)
                block_tool_ids.pop(idx, None)

        elif event_type == "message_delta":
            stop_reason = event.get("delta", {}).get("stop_reason")
            output_tokens = event.get("usage", {}).get("output_tokens", 0)

        elif event_type == "message_stop":
            yield MessageEnd(
                stop_reason=stop_reason,
                usage=UsageInfo(
                    input_tokens=input_tokens,
                    output_tokens=output_tokens,
                ),
            )

        elif event_type == "ping":
            # 心跳,忽略
            pass

        elif event_type == "error":
            err = event.get("error", {})
            yield ErrorEvent(
                code=err.get("type", "unknown"),
                message=err.get("message", "Unknown error"),
            )

6.5 统一入口:stream_adapter

把两个 Adapter 包装在一个统一入口里,业务代码只需要告诉它用的是哪个 Provider:

python 复制代码
# ============================================================
# adapters/stream_adapter.py --- 统一流式适配器入口
# ============================================================

import json
from typing import AsyncIterator, Literal
import httpx  # 或 aiohttp,这里是 httpx 的用法

from .types import UnifiedEvent
from .openai_adapter import adapt_openai_stream
from .anthropic_adapter import adapt_anthropic_stream

Provider = Literal["openai", "anthropic"]


async def stream_adapter(
    provider: Provider,
    response: httpx.Response,
) -> AsyncIterator[UnifiedEvent]:
    """统一流式适配器入口

    用法:
        async with httpx.AsyncClient() as client:
            async with client.stream("POST", url, json=body, headers=headers) as resp:
                async for event in stream_adapter("openai", resp):
                    # 处理 UnifiedEvent
    """
    line_stream = _read_sse_lines(response)

    if provider == "openai":
        async for event in adapt_openai_stream(line_stream):
            yield event
    elif provider == "anthropic":
        async for event in adapt_anthropic_stream(line_stream):
            yield event
    else:
        raise ValueError(f"Unsupported provider: {provider}")


async def _read_sse_lines(
    response: httpx.Response,
) -> AsyncIterator[str]:
    """工具函数:把 httpx 流式响应按行拆分(SSE 底层)

    等价于 TypeScript 版的 readLines()。
    """
    buffer = ""
    async for chunk in response.aiter_bytes():
        buffer += chunk.decode("utf-8")
        # 按 \n 拆分,最后一段不完整留着下次拼
        while "\n" in buffer:
            line, buffer = buffer.split("\n", 1)
            yield line
    # 处理最后残留
    if buffer:
        yield buffer

依赖说明 :代码用 httpx 做 HTTP 客户端。如果你用 aiohttp,只需要把 response.aiter_bytes() 换成 response.content.iter_chunked() 即可,其余逻辑完全不变。

6.6 业务层使用示例

有了 Adapt 层之后,业务代码干净得像这样:

python 复制代码
# ============================================================
# 业务层 --- 只需 import stream_adapter,零 if-else
# ============================================================

import httpx
from adapters.stream_adapter import stream_adapter, Provider
from adapters.types import (
    MessageStart, TextDelta,
    ToolCallStart, ToolCallDelta, ToolCallEnd,
    MessageEnd, ErrorEvent,
)


async def chat(
    provider: Provider,
    messages: list[dict],
    tools: list[dict] | None = None,
    *,
    on_text: callable = None,
    on_tool_call: callable = None,
) -> str:
    """统一 chat 入口,自动适配 OpenAI / Anthropic 流式返回"""

    # 1. 构造请求(由另一个 Adapter 负责,此处略)
    url, headers, body = build_request(provider, messages, tools, stream=True)

    # 2. 发起流式请求
    full_text = ""
    tool_calls: dict[str, dict] = {}  # tool_call_id → {"name": ..., "args": ...}

    async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as client:
        async with client.stream("POST", url, json=body, headers=headers) as resp:
            resp.raise_for_status()

            # 3. 用统一适配器消费流 --- 关键只有这一行
            async for event in stream_adapter(provider, resp):
                match event:
                    case TextDelta(text=text):
                        full_text += text
                        if on_text:
                            on_text(text)

                    case ToolCallStart(tool_call_id=tc_id, name=name):
                        tool_calls[tc_id] = {"name": name, "args": ""}

                    case ToolCallDelta(tool_call_id=tc_id, partial_json=pj):
                        if tc_id in tool_calls:
                            tool_calls[tc_id]["args"] += pj

                    case ToolCallEnd(tool_call_id=tc_id):
                        tc = tool_calls.get(tc_id)
                        if tc and on_tool_call:
                            on_tool_call(
                                id=tc_id,
                                name=tc["name"],
                                arguments=tc["args"],
                            )

                    case MessageEnd(stop_reason=sr, usage=u):
                        print(f"[Done] stop_reason={sr}, "
                              f"input={u.input_tokens if u else '?'}, "
                              f"output={u.output_tokens if u else '?'}")

                    case ErrorEvent(code=code, message=msg):
                        raise RuntimeError(f"[{code}] {msg}")

    return full_text

Python 3.10+ 的 match/case 配合 @dataclass 简直是绝配------事件分发清晰得像模式匹配语言。

6.7 双协议适配对照表

场景 OpenAI 原始 Anthropic 原始 统一事件
文本增量 choices[0].delta.content content_block_delta.text_delta.text TextDelta
工具调用开始 首个 delta.tool_calls[i]id content_block_start 类型为 tool_use ToolCallStart
工具参数增量 delta.tool_calls[i].function.arguments(字符串片段) content_block_delta.input_json_delta.partial_json ToolCallDelta
工具调用结束 无显式事件(靠下一个 delta 判断) content_block_stop ToolCallEnd
消息结束 finish_reason 非 None message_delta.stop_reason MessageEnd
流终止 data: [DONE] message_stop break(generator 自然退出)
输入 tokens 首 chunk 的 usage.prompt_tokens 或结尾 chunk message_start.usage.input_tokens MessageEnd.usage.input_tokens
输出 tokens 结尾 chunk 的 usage.completion_tokens message_delta.usage.output_tokens MessageEnd.usage.output_tokens
错误 {"error": {...}} 或 HTTP 状态码 {"type":"error", "error":{...}} ErrorEvent

6.8 设计要点总结

  1. Python async generator 是天然的适配器接口async def + yield 惰性求值、内存友好,比回调方案优雅得多。

  2. 工具调用的流式处理是最大的坑 。OpenAI 把 JSON 参数直接当字符串片段给,不需要拼接符号;Anthropic 给的是 partial_json------两者语义相同但字段路径完全不同。Adapt 层统一成 ToolCallDelta.partial_json 后,业务层只需要 += 拼接即可。

  3. 用量信息收集时机不同 。OpenAI 的 usage 可能在首 chunk(stream_options={"include_usage": True})也可能在尾 chunk。Anthropic 的 input_tokens 在 message_start,output_tokens 在 message_delta。Adapt 层内部缓冲,统一在 MessageEnd 一次性吐出。

  4. 错误处理要覆盖两种协议 。OpenAI 可能返回非 200 的 HTTP 响应(body 里是 {"error": ...}),Anthropic 在流内发 {"type": "error"}。Adapt 层统一转成 ErrorEvent

  5. 容易漏掉的细节

    • Anthropic 的 ping 心跳事件要静默忽略
    • OpenAI 的 [DONE] 不是合法 JSON,要特殊处理
    • SSE 行可能以 : 开头(注释行),要跳过
    • Anthropic 的 content_block_startinput: {} 表示工具参数尚未到齐
    • OpenAI 工具调用用 index 关联、id 只在首个 fragment 出现,Adapt 层需要按 index 维护映射表

6.9 扩展:加上 Anthropic 的 event: 行完整解析

上面 adapt_anthropic_stream 做了简化------忽略 event: 行,只用 data: 行。在生产环境中,更稳健的做法是同时处理 event: 行来区分事件类型:

python 复制代码
async def adapt_anthropic_stream_full(
    sse_lines: AsyncIterator[str],
) -> AsyncIterator[UnifiedEvent]:
    """完整版:同时解析 event: 和 data: 行"""
    current_event = ""
    # ... 同上面 adapt_anthropic_stream 的状态变量 ...

    async for line in sse_lines:
        line = line.strip()

        if line.startswith("event: "):
            current_event = line[7:].strip()
            continue

        if line.startswith("data: "):
            payload = line[6:]
            try:
                event = json.loads(payload)
            except json.JSONDecodeError:
                continue

            # current_event 可用于额外校验,例如:
            # if current_event == "ping" and event.get("type") != "ping":
            #     logger.warning(f"event/data mismatch: {current_event=} {event=}")

            # 后续处理同 adapt_anthropic_stream...

        # 空行表示一个事件的结束
        if not line:
            current_event = ""

七、完整的项目结构建议

复制代码
adapters/
  __init__.py            # 对外导出 stream_adapter + 所有类型
  types.py               # UnifiedEvent, UsageInfo 等 @dataclass 定义
  openai_adapter.py      # OpenAI → Unified 的请求/响应/流式适配
  anthropic_adapter.py   # Anthropic → Unified 的请求/响应/流式适配
  stream_adapter.py      # stream_adapter() 统一入口 + _read_sse_lines

这样,当你需要接入第三个 Provider(比如 Google Gemini、国内模型),只需要新增一个 xxx_adapter.py,业务代码零改动。

安装依赖

bash 复制代码
pip install httpx

愿你对接的 API 永远向后兼容,SSE 行从不丢帧。

相关推荐
AI焦点1 小时前
2026年AI大模型中转横评实测:跨越价格陷阱,重构生产级聚合平台的评估基准
人工智能·重构
AI客栈1 小时前
容器启动调优:基于 Go 原生的冷启动时延评估与优化
人工智能
yyuuuzz1 小时前
2026游戏云服务器推荐的技术判断思路
运维·服务器·开发语言·网络·人工智能·游戏·php
-星空下无敌1 小时前
Skills详解(2万字详细教程),Skills是什么,如何安装并使用Skills
人工智能·ai·提示词·codex·mcp·skills·agent skills
Peter(阿斯拉)1 小时前
[Android]_[中级]_[如何创建MVVM架构原型]
android·java·架构·mvvm·viewmodel
文艺倾年1 小时前
【强化学习】数学推导专题,20W字总结(十五)
人工智能·分布式·大模型·强化学习·vibecoding
nanawinona1 小时前
手工策略转量化,回测到底是在验证什么?
人工智能·python
XTIOT6661 小时前
多形态护照 OCR 读取器传输机制、识别算法与行业落地技术对比
大数据·人工智能·嵌入式硬件·物联网·ocr
协享科技1 小时前
AI 视频理解:让 Agent 看视频并总结内容
人工智能·go·音视频·agent·ai编程