Anthropic和OpenAi格式互转(openai系模型驱动claude code)

文章目录

简介

理论上说,两者是不兼容的,就是说如果你希望用openai的模型,就不能用claude code,因为两者发送消息的格式不兼容。但是有一种解决方案,就是用网关做中转,即claude code发出去的消息先过网关,转化成openai模型能接受的消息,回来时同理
OpenAI模型 网关 Claude Code OpenAI模型 网关 Claude Code 接收并中转处理 处理请求 发送消息 转发消息 返回响应 转发响应

Claude code实现

Claude Code / Agent SDK → OpenRouter 这个网关 → 再由 OpenRouter 路由到别的模型

OpenRouter 的集成文档明确写了:把 ANTHROPIC_BASE_URL 设成它的 API 地址后,Claude Code 会直接用自己的原生协议去跟 OpenRouter 说话,不需要你本地再起一个代理;OpenRouter 这层负责兼容 Anthropic 风格接口、做模型映射,并保留像 tool use、thinking 这类高级能力。

先看 Claude Code 的最小配置例子。

复制代码
# 1) 你的 OpenRouter key
export OPENROUTER_API_KEY="你的_openrouter_key"

# 2) 告诉 Claude Code:不要直连 Anthropic,改走 gateway
export ANTHROPIC_BASE_URL="https://openrouter.ai/api"

# 3) 把认证 token 交给 gateway
export ANTHROPIC_AUTH_TOKEN="$OPENROUTER_API_KEY"

# 4) 很重要:显式清空 Anthropic API key,避免冲突
export ANTHROPIC_API_KEY=""

OpenRouter 的文档就是这样配的,而且特别强调 ANTHROPIC_API_KEY 要显式置空;如果你之前已经登录过 Claude Code,最好先在 Claude Code 里执行 /logout,不然缓存的登录态可能继续生效。官方环境变量文档也说明了:Claude Code 会读取这些环境变量,而且 ANTHROPIC_API_KEY 一旦存在,会优先于你的订阅登录态。

如果你还想指定"经过 gateway 后,到底用哪个模型",可以再加这些环境变量:

复制代码
export ANTHROPIC_DEFAULT_OPUS_MODEL="anthropic/claude-opus-4.6"
export ANTHROPIC_DEFAULT_SONNET_MODEL="anthropic/claude-sonnet-4.6"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="anthropic/claude-haiku-4.5"
export CLAUDE_CODE_SUBAGENT_MODEL="anthropic/claude-opus-4.6"

Agent SDK实现

不用在代码里额外写"OpenRouter 客户端",因为 Agent SDK 本身就是 Claude Code runtime。Anthropic 的 Agent SDK 文档说明它继承了 Claude Code 的工具、agent loop 和上下文管理;OpenRouter 也明确写了,Agent SDK 用同样的环境变量就能接上去。也就是说,先在 shell 里 export 上面那几个变量,然后代码还是正常写

SDK->openRouter->deepseek

先注册一个OpenRouter的账号,然后拿到OpenRouter api key

接下来配置BYOK模式(中转到特定的模型用你自己的api key,走自己的厂商额度/结算,而不是用OpenRouter去消费)

接下来只需要改三个东西即可

复制代码
os.environ["ANTHROPIC_BASE_URL"] = "https://openrouter.ai/api"
os.environ["ANTHROPIC_AUTH_TOKEN"] = OPENROUTER_API_KEY #你的OpenRouter api key,用于认证身份
os.environ["ANTHROPIC_API_KEY"] = ""   # 必须显式为空
os.environ["ANTHROPIC_MODEL"] = OPENROUTER_MODEL #模型名称

其他代码正常写,可以从OP看到调用记录就说明设置成功了

OpenRouter兼顾不到的情况

目前OpenRouter只能转发主流的66个平台,但是我们总能从一些小的二手站点拿到key做测试,而OP是不能使用这些站点的key的,所以就只能在本地做一个中间转发

以下是proxy代码

它只是一个纯协议转换库

它只负责做两件事:

  1. Anthropic / Claude Messages 格式 转成 OpenAI Chat Completions 格式
  2. OpenAI Chat Completions 响应 转回 Anthropic / Claude Messages 响应格式

所以它本身:

  • 不监听端口
  • 不持有 key
  • 不绑定 iFlow / OpenRouter / 阿里云 / 任何平台
  • 不发 HTTP 请求

你可以把它理解成一个"中间格式翻译器"。

复制代码
import json
import uuid
from typing import Any, Dict, List, Optional


def _text_blocks_to_text(content: Any) -> str:
    if isinstance(content, str):
        return content

    if isinstance(content, list):
        texts: List[str] = []
        for block in content:
            if isinstance(block, dict) and block.get("type") == "text":
                texts.append(block.get("text", ""))
        return "\n".join(x for x in texts if x)

    return str(content)


def anthropic_tools_to_openai(
    tools: Optional[List[Dict[str, Any]]],
) -> Optional[List[Dict[str, Any]]]:
    """
    Anthropic tools -> OpenAI tools
    """
    if not tools:
        return None

    out: List[Dict[str, Any]] = []
    for tool in tools:
        out.append(
            {
                "type": "function",
                "function": {
                    "name": tool["name"],
                    "description": tool.get("description", ""),
                    "parameters": tool.get(
                        "input_schema",
                        {"type": "object", "properties": {}},
                    ),
                },
            }
        )
    return out


def anthropic_tool_choice_to_openai(tool_choice: Any) -> Any:
    """
    Anthropic:
      {"type":"auto"}
      {"type":"any"}
      {"type":"none"}
      {"type":"tool","name":"xxx"}

    OpenAI:
      "auto"
      "required"
      "none"
      {"type":"function","function":{"name":"xxx"}}
    """
    if not tool_choice:
        return "auto"

    if isinstance(tool_choice, str):
        if tool_choice in {"auto", "required", "none"}:
            return tool_choice
        return "auto"

    if isinstance(tool_choice, dict):
        t = tool_choice.get("type")
        if t == "auto":
            return "auto"
        if t == "any":
            return "required"
        if t == "none":
            return "none"
        if t == "tool" and tool_choice.get("name"):
            return {
                "type": "function",
                "function": {"name": tool_choice["name"]},
            }

    return "auto"


def anthropic_messages_to_openai_messages(
    system: Any,
    messages: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """
    Anthropic Messages API -> OpenAI chat.completions messages

    支持:
    - system
    - user/assistant text
    - assistant.tool_use -> assistant.tool_calls
    - user.tool_result -> tool
    """
    out: List[Dict[str, Any]] = []

    if system:
        if isinstance(system, str):
            out.append({"role": "system", "content": system})
        elif isinstance(system, list):
            system_texts: List[str] = []
            for block in system:
                if isinstance(block, dict) and block.get("type") == "text":
                    system_texts.append(block.get("text", ""))
            if system_texts:
                out.append({"role": "system", "content": "\n".join(system_texts)})

    for msg in messages:
        role = msg.get("role")
        content = msg.get("content")

        if isinstance(content, str):
            out.append({"role": role, "content": content})
            continue

        if not isinstance(content, list):
            out.append({"role": role, "content": str(content)})
            continue

        if role == "assistant":
            text_parts: List[str] = []
            tool_calls: List[Dict[str, Any]] = []

            for block in content:
                if not isinstance(block, dict):
                    continue

                block_type = block.get("type")
                if block_type == "text":
                    text_parts.append(block.get("text", ""))
                elif block_type == "tool_use":
                    tool_calls.append(
                        {
                            "id": block["id"],
                            "type": "function",
                            "function": {
                                "name": block["name"],
                                "arguments": json.dumps(
                                    block.get("input", {}),
                                    ensure_ascii=False,
                                ),
                            },
                        }
                    )

            assistant_msg: Dict[str, Any] = {
                "role": "assistant",
                "content": "\n".join(x for x in text_parts if x) or None,
            }
            if tool_calls:
                assistant_msg["tool_calls"] = tool_calls

            out.append(assistant_msg)
            continue

        if role == "user":
            pending_user_text: List[str] = []

            def flush_user_text() -> None:
                if pending_user_text:
                    out.append(
                        {
                            "role": "user",
                            "content": "\n".join(pending_user_text),
                        }
                    )
                    pending_user_text.clear()

            for block in content:
                if not isinstance(block, dict):
                    pending_user_text.append(str(block))
                    continue

                block_type = block.get("type")
                if block_type == "text":
                    pending_user_text.append(block.get("text", ""))
                elif block_type == "tool_result":
                    flush_user_text()

                    tool_content = block.get("content", "")
                    if isinstance(tool_content, list):
                        tool_content = _text_blocks_to_text(tool_content)
                    elif not isinstance(tool_content, str):
                        tool_content = json.dumps(tool_content, ensure_ascii=False)

                    out.append(
                        {
                            "role": "tool",
                            "tool_call_id": block["tool_use_id"],
                            "content": tool_content,
                        }
                    )

            flush_user_text()
            continue

        out.append({"role": role, "content": _text_blocks_to_text(content)})

    return out


def anthropic_request_to_openai_chat_completions(
    anthropic_request: Dict[str, Any],
    model: str,
) -> Dict[str, Any]:
    """
    完整 Anthropic 请求 -> OpenAI chat.completions 请求体
    """
    system = anthropic_request.get("system")
    messages = anthropic_request.get("messages", [])
    tools = anthropic_request.get("tools")
    tool_choice = anthropic_request.get("tool_choice")
    temperature = anthropic_request.get("temperature")
    max_tokens = anthropic_request.get("max_tokens", 1024)

    payload: Dict[str, Any] = {
        "model": model,
        "messages": anthropic_messages_to_openai_messages(system, messages),
        "max_tokens": max_tokens,
        "stream": False,
    }

    if temperature is not None:
        payload["temperature"] = temperature

    openai_tools = anthropic_tools_to_openai(tools)
    if openai_tools:
        payload["tools"] = openai_tools
        payload["tool_choice"] = anthropic_tool_choice_to_openai(tool_choice)

    return payload


def openai_choice_to_anthropic_message(
    choice: Dict[str, Any],
    model: str,
) -> Dict[str, Any]:
    """
    OpenAI chat.completions 的单个 choice -> Anthropic Messages response
    """
    message = choice["message"]
    finish_reason = choice.get("finish_reason")

    content_blocks: List[Dict[str, Any]] = []

    raw_content = message.get("content")
    if isinstance(raw_content, str) and raw_content.strip():
        content_blocks.append({"type": "text", "text": raw_content})
    elif isinstance(raw_content, list):
        texts: List[str] = []
        for item in raw_content:
            if isinstance(item, dict) and item.get("type") == "text":
                texts.append(item.get("text", ""))
        if texts:
            content_blocks.append({"type": "text", "text": "\n".join(texts)})

    tool_calls = message.get("tool_calls") or []
    for tool_call in tool_calls:
        fn = tool_call.get("function", {})
        raw_args = fn.get("arguments", "{}")

        try:
            parsed_args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
        except Exception:
            parsed_args = {"raw_arguments": raw_args}

        content_blocks.append(
            {
                "type": "tool_use",
                "id": tool_call["id"],
                "name": fn["name"],
                "input": parsed_args,
            }
        )

    if tool_calls:
        stop_reason = "tool_use"
    elif finish_reason == "length":
        stop_reason = "max_tokens"
    else:
        stop_reason = "end_turn"

    return {
        "id": f"msg_{uuid.uuid4().hex}",
        "type": "message",
        "role": "assistant",
        "model": model,
        "content": content_blocks,
        "stop_reason": stop_reason,
        "stop_sequence": None,
    }


def openai_response_to_anthropic_response(
    openai_response: Dict[str, Any],
    model: str,
    fallback_input_tokens: int = 0,
) -> Dict[str, Any]:
    """
    OpenAI 完整响应 -> Anthropic 完整响应
    """
    choice = openai_response["choices"][0]
    usage = openai_response.get("usage", {})

    result = openai_choice_to_anthropic_message(choice, model=model)
    result["usage"] = {
        "input_tokens": usage.get("prompt_tokens", fallback_input_tokens),
        "output_tokens": usage.get("completion_tokens", 0),
    }
    return result


def estimate_anthropic_input_tokens(anthropic_request: Dict[str, Any]) -> int:
    """
    只是占位估算,方便本地桥接先跑通。
    不是精确 tokenizer。
    """
    raw = json.dumps(anthropic_request, ensure_ascii=False)
    return max(1, len(raw) // 4)

runtime 代理文件常用方式

复制代码
from proxy_core import (
    anthropic_request_to_openai_chat_completions,
    openai_response_to_anthropic_response,
    estimate_anthropic_input_tokens,
)

# 1. 收到 Anthropic 风格请求
anthropic_request = await request.json()

# 2. 转成 OpenAI 风格请求
openai_payload = anthropic_request_to_openai_chat_completions(
    anthropic_request=anthropic_request,
    model=UPSTREAM_MODEL,
)

# 3. 发给上游
resp = await client.post(
    CHAT_COMPLETIONS_URL,
    headers=headers,
    json=openai_payload,
)

openai_response = resp.json()

# 4. 再转回 Anthropic 风格响应
anthropic_response = openai_response_to_anthropic_response(
    openai_response=openai_response,
    model=UPSTREAM_MODEL,
    fallback_input_tokens=estimate_anthropic_input_tokens(anthropic_request),
)

# 5. 返回给 Claude / Agent SDK
return anthropic_response
相关推荐
ws2019072 小时前
技术交流与商贸融合,2026广州汽车测试测量展释放产业协同新动能
大数据·人工智能·科技·汽车
MyBFuture3 小时前
Halcon 金字塔与边缘检测技术解析
人工智能·计算机视觉·halcon
树獭非懒3 小时前
AI大模型小白手册 | RAG进阶:从胡说八道到引经据典
人工智能
攻城狮7号3 小时前
SaaS的末日重构:AI Agent浪潮下的危机与新生
人工智能·ai agent·saas末日·saas升级重构
2601_949925183 小时前
空运舱位突发爆舱?解析 AI Agent 如何在 2 小时内重构物流应急响应底层逻辑
人工智能·重构·物流rpa
F1FJJ3 小时前
Shield CLI Postgres v0.3.10:当 142 张表挤在一张 ER 图里,我们做了什么
网络·vscode·网络协议·postgresql·开源软件
FluxMelodySun3 小时前
机器学习(二十八) 特征选择与常见的特征选择方法
人工智能·机器学习
小陈工3 小时前
2026年3月31日技术资讯洞察:AI智能体安全、异步编程突破与Python运行时演进
开发语言·jvm·数据库·人工智能·python·安全·oracle
香港科大商学院内地办事处3 小时前
港科资讯|郑光廷教授出席国际科技组织发展与全球科技治理论坛 分享协作实践
人工智能·科技
Westward-sun.3 小时前
基于 OpenCV DNN 模块实现图像风格迁移
人工智能·神经网络·opencv·计算机视觉·dnn