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

该文章已生成可运行项目,

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

简介

理论上说,两者是不兼容的,就是说如果你希望用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去消费)

image-20260331163417503

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

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看到调用记录就说明设置成功了

image-20260331172413286

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
本文章已经生成可运行项目

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值