feat: 完善跨格式转换支持,添加 Gemini 双向转换器

- 实现 Claude/OpenAI -> Gemini 的请求和流式响应转换
- 实现 Gemini -> Claude/OpenAI 的请求转换
- 新增 ClaudeStreamConversionState 和 OpenAIStreamConversionState 状态类
- 添加 model_in_body 和 stream_in_body 元数据字段区分格式特性
- 格式转换后自动设置目标格式所需的 model/stream 字段
- 修复流式转换中 delta/choices 等字段的空值防护
This commit is contained in:
fawney19
2026-01-23 18:50:38 +08:00
parent f45fd06fb0
commit 2256ad1233
11 changed files with 877 additions and 24 deletions

View File

@@ -13,6 +13,7 @@ API 格式核心模块
"""
from src.core.api_format.conversion import (
ClaudeStreamConversionState,
ClaudeToGeminiConverter,
ClaudeToOpenAIConverter,
FormatConversionError,
@@ -20,6 +21,7 @@ from src.core.api_format.conversion import (
GeminiStreamConversionState,
GeminiToClaudeConverter,
GeminiToOpenAIConverter,
OpenAIStreamConversionState,
OpenAIToClaudeConverter,
OpenAIToGeminiConverter,
RequestConverter,
@@ -137,6 +139,8 @@ __all__ = [
# State
"StreamConversionState",
"GeminiStreamConversionState",
"ClaudeStreamConversionState",
"OpenAIStreamConversionState",
# Exceptions
"FormatConversionError",
# Compatibility

View File

@@ -33,7 +33,9 @@ from src.core.api_format.conversion.registry import (
converter_registry,
)
from src.core.api_format.conversion.state import (
ClaudeStreamConversionState,
GeminiStreamConversionState,
OpenAIStreamConversionState,
StreamConversionState,
)
from src.core.logger import logger
@@ -72,6 +74,8 @@ __all__ = [
# State
"StreamConversionState",
"GeminiStreamConversionState",
"ClaudeStreamConversionState",
"OpenAIStreamConversionState",
# Exceptions
"FormatConversionError",
# Compatibility

View File

@@ -370,7 +370,7 @@ class ClaudeToOpenAIConverter:
return None
if event_type == "content_block_delta":
delta_payload = event.get("delta", {})
delta_payload = event.get("delta") or {}
delta_type = delta_payload.get("type")
if delta_type == "text_delta":
@@ -390,7 +390,7 @@ class ClaudeToOpenAIConverter:
return None
if event_type == "message_delta":
delta = event.get("delta", {})
delta = event.get("delta") or {}
stop_reason = delta.get("stop_reason")
finish_reason = self.STOP_REASON_MAP.get(stop_reason, stop_reason)
return self._base_chunk(chunk_id, model, {}, finish_reason=finish_reason)

View File

@@ -11,7 +11,11 @@ import time
from typing import TYPE_CHECKING, Any, Dict, List, Optional
if TYPE_CHECKING:
from src.core.api_format.conversion.state import GeminiStreamConversionState
from src.core.api_format.conversion.state import (
ClaudeStreamConversionState,
GeminiStreamConversionState,
OpenAIStreamConversionState,
)
class ClaudeToGeminiConverter:
@@ -169,14 +173,335 @@ class ClaudeToGeminiConverter:
return [{"function_declarations": function_declarations}]
def convert_response(self, claude_response: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Claude 响应转换为 Gemini 响应
Args:
claude_response: Claude 格式的响应字典
Returns:
Gemini 格式的响应字典
"""
content_blocks = claude_response.get("content", [])
parts = self._convert_response_content_to_parts(content_blocks)
# 转换停止原因
stop_reason = claude_response.get("stop_reason")
finish_reason = self._convert_stop_reason_to_gemini(stop_reason)
# 转换使用量
usage = claude_response.get("usage", {})
return {
"candidates": [
{
"content": {
"parts": parts,
"role": "model",
},
"finishReason": finish_reason,
"index": 0,
}
],
"usageMetadata": {
"promptTokenCount": usage.get("input_tokens", 0),
"candidatesTokenCount": usage.get("output_tokens", 0),
"totalTokenCount": usage.get("input_tokens", 0) + usage.get("output_tokens", 0),
},
"modelVersion": claude_response.get("model", "claude"),
}
def _convert_response_content_to_parts(
self, content_blocks: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""将 Claude content blocks 转换为 Gemini parts"""
parts = []
for block in content_blocks:
block_type = block.get("type")
if block_type == "text":
parts.append({"text": block.get("text", "")})
elif block_type == "tool_use":
parts.append(
{
"functionCall": {
"name": block.get("name", ""),
"args": block.get("input", {}),
}
}
)
return parts if parts else [{"text": ""}]
def _convert_stop_reason_to_gemini(self, stop_reason: Optional[str]) -> str:
"""转换停止原因为 Gemini 格式"""
mapping = {
"end_turn": "STOP",
"max_tokens": "MAX_TOKENS",
"stop_sequence": "STOP",
"tool_use": "STOP",
}
return mapping.get(stop_reason or "", "STOP")
# ==================== 流式转换 ====================
def convert_stream_chunk(
self,
chunk: Dict[str, Any],
state: Optional["ClaudeStreamConversionState"] = None,
) -> List[Dict[str, Any]]:
"""
将 Claude 流式响应转换为 Gemini 流式响应
Args:
chunk: Claude SSE 事件
state: 流式转换状态
Returns:
Gemini 流式响应列表
"""
from src.core.api_format.conversion.state import ClaudeStreamConversionState
if state is None:
state = ClaudeStreamConversionState()
events: List[Dict[str, Any]] = []
event_type = chunk.get("type")
if event_type == "message_start":
# 初始化状态
message = chunk.get("message", {})
state.model = message.get("model", "claude")
state.message_id = message.get("id", "msg_claude")
elif event_type == "content_block_start":
# 记录内容块开始
content_block = chunk.get("content_block", {})
state.current_block_type = content_block.get("type", "text")
state.current_block_index = chunk.get("index", 0)
if state.current_block_type == "tool_use":
state.current_tool_name = content_block.get("name", "")
state.current_tool_id = content_block.get("id", "")
state.accumulated_tool_input = ""
elif event_type == "content_block_delta":
delta = chunk.get("delta") or {}
delta_type = delta.get("type")
if delta_type == "text_delta":
text = delta.get("text", "")
if text:
# 发送 Gemini 流式响应
events.append(
{
"candidates": [
{
"content": {
"parts": [{"text": text}],
"role": "model",
},
"index": 0,
}
],
"modelVersion": state.model,
}
)
elif delta_type == "input_json_delta":
# 累积工具输入
state.accumulated_tool_input += delta.get("partial_json", "")
elif event_type == "content_block_stop":
# 如果是工具调用块结束,发送工具调用
if state.current_block_type == "tool_use" and state.current_tool_name:
try:
args = json.loads(state.accumulated_tool_input) if state.accumulated_tool_input else {}
except json.JSONDecodeError:
args = {}
events.append(
{
"candidates": [
{
"content": {
"parts": [
{
"functionCall": {
"name": state.current_tool_name,
"args": args,
}
}
],
"role": "model",
},
"index": 0,
}
],
"modelVersion": state.model,
}
)
state.current_tool_name = ""
state.accumulated_tool_input = ""
elif event_type == "message_delta":
# 消息结束
delta = chunk.get("delta") or {}
stop_reason = delta.get("stop_reason")
if stop_reason:
finish_reason = self._convert_stop_reason_to_gemini(stop_reason)
events.append(
{
"candidates": [
{
"content": {"parts": [], "role": "model"},
"finishReason": finish_reason,
"index": 0,
}
],
"usageMetadata": chunk.get("usage", {}),
"modelVersion": state.model,
}
)
return events
class GeminiToClaudeConverter:
"""
Gemini -> Claude 响应转换器
Gemini -> Claude 转换器
将 Gemini generateContent 响应转换为 Claude Messages API 格式
- 请求转换:将 Gemini generateContent 请求转换为 Claude Messages API 格式
- 响应转换:将 Gemini generateContent 响应转换为 Claude Messages API 格式
"""
def convert_request(self, gemini_request: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Gemini 请求转换为 Claude 请求
Args:
gemini_request: Gemini 格式的请求字典
Returns:
Claude 格式的请求字典
"""
claude_request: Dict[str, Any] = {
"messages": self._convert_contents_to_messages(gemini_request.get("contents", [])),
}
# 转换 system instruction
system_instruction = gemini_request.get("system_instruction")
if system_instruction:
parts = system_instruction.get("parts", [])
system_text = "".join(p.get("text", "") for p in parts if "text" in p)
if system_text:
claude_request["system"] = system_text
# 转换生成配置
generation_config = gemini_request.get("generation_config", {})
if "max_output_tokens" in generation_config:
claude_request["max_tokens"] = generation_config["max_output_tokens"]
else:
claude_request["max_tokens"] = 4096 # Claude 需要 max_tokens
if "temperature" in generation_config:
claude_request["temperature"] = generation_config["temperature"]
if "top_p" in generation_config:
claude_request["top_p"] = generation_config["top_p"]
if "top_k" in generation_config:
claude_request["top_k"] = generation_config["top_k"]
if "stop_sequences" in generation_config:
claude_request["stop_sequences"] = generation_config["stop_sequences"]
# 转换工具
tools = gemini_request.get("tools", [])
if tools:
claude_request["tools"] = self._convert_tools_to_claude(tools)
return claude_request
def _convert_contents_to_messages(
self, contents: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""转换 Gemini contents 为 Claude messages"""
messages = []
for content in contents:
role = content.get("role", "user")
# Gemini 使用 "model"Claude 使用 "assistant"
claude_role = "assistant" if role == "model" else "user"
parts = content.get("parts", [])
claude_content = self._convert_parts_to_claude_content(parts)
messages.append(
{
"role": claude_role,
"content": claude_content,
}
)
return messages
def _convert_parts_to_claude_content(
self, parts: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""将 Gemini parts 转换为 Claude content blocks"""
content = []
for part in parts:
if "text" in part:
content.append(
{
"type": "text",
"text": part["text"],
}
)
elif "inline_data" in part:
# 转换图片
inline_data = part["inline_data"]
content.append(
{
"type": "image",
"source": {
"type": "base64",
"media_type": inline_data.get("mime_type", "image/png"),
"data": inline_data.get("data", ""),
},
}
)
elif "function_call" in part:
# 转换工具调用
func_call = part["function_call"]
content.append(
{
"type": "tool_use",
"id": f"toolu_{func_call.get('name', '')}",
"name": func_call.get("name", ""),
"input": func_call.get("args", {}),
}
)
elif "function_response" in part:
# 转换工具结果
func_response = part["function_response"]
result = func_response.get("response", {}).get("result", "")
content.append(
{
"type": "tool_result",
"tool_use_id": func_response.get("name", ""),
"content": result if isinstance(result, str) else json.dumps(result),
}
)
return content if content else [{"type": "text", "text": ""}]
def _convert_tools_to_claude(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""转换 Gemini 工具为 Claude 格式"""
claude_tools = []
for tool in tools:
function_declarations = tool.get("function_declarations", [])
for func_decl in function_declarations:
claude_tool = {
"name": func_decl.get("name", ""),
}
if "description" in func_decl:
claude_tool["description"] = func_decl["description"]
if "parameters" in func_decl:
claude_tool["input_schema"] = func_decl["parameters"]
claude_tools.append(claude_tool)
return claude_tools
def convert_response(self, gemini_response: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Gemini 响应转换为 Claude 响应
@@ -303,13 +628,13 @@ class GeminiToClaudeConverter:
state = GeminiStreamConversionState()
events: List[Dict[str, Any]] = []
candidates = chunk.get("candidates", [])
candidates = chunk.get("candidates") or []
if not candidates:
return events
candidate = candidates[0]
content = candidate.get("content", {})
parts = content.get("parts", [])
content = candidate.get("content") or {}
parts = content.get("parts") or []
# 发送 message_start首次
if not state.message_started:
@@ -582,14 +907,348 @@ class OpenAIToGeminiConverter:
return [{"function_declarations": function_declarations}]
def convert_response(self, openai_response: Dict[str, Any]) -> Dict[str, Any]:
"""
将 OpenAI 响应转换为 Gemini 响应
Args:
openai_response: OpenAI 格式的响应字典
Returns:
Gemini 格式的响应字典
"""
choices = openai_response.get("choices", [])
candidates = []
for i, choice in enumerate(choices):
message = choice.get("message", {})
parts = []
# 转换文本内容
content = message.get("content")
if content:
parts.append({"text": content})
# 转换工具调用
tool_calls = message.get("tool_calls", [])
for tc in tool_calls:
if tc.get("type") == "function":
func = tc.get("function", {})
try:
args = json.loads(func.get("arguments", "{}"))
except json.JSONDecodeError:
args = {}
parts.append(
{
"functionCall": {
"name": func.get("name", ""),
"args": args,
}
}
)
# 转换停止原因
finish_reason = self._convert_finish_reason_to_gemini(choice.get("finish_reason"))
candidates.append(
{
"content": {
"parts": parts if parts else [{"text": ""}],
"role": "model",
},
"finishReason": finish_reason,
"index": i,
}
)
# 转换使用量
usage = openai_response.get("usage", {})
return {
"candidates": candidates,
"usageMetadata": {
"promptTokenCount": usage.get("prompt_tokens", 0),
"candidatesTokenCount": usage.get("completion_tokens", 0),
"totalTokenCount": usage.get("total_tokens", 0),
},
"modelVersion": openai_response.get("model", "gpt"),
}
def _convert_finish_reason_to_gemini(self, finish_reason: Optional[str]) -> str:
"""转换停止原因为 Gemini 格式"""
mapping = {
"stop": "STOP",
"length": "MAX_TOKENS",
"content_filter": "SAFETY",
"tool_calls": "STOP",
"function_call": "STOP",
}
return mapping.get(finish_reason or "", "STOP")
# ==================== 流式转换 ====================
def convert_stream_chunk(
self,
chunk: Dict[str, Any],
state: Optional["OpenAIStreamConversionState"] = None,
) -> List[Dict[str, Any]]:
"""
将 OpenAI 流式响应转换为 Gemini 流式响应
Args:
chunk: OpenAI chat.completion.chunk
state: 流式转换状态
Returns:
Gemini 流式响应列表
"""
from src.core.api_format.conversion.state import OpenAIStreamConversionState
if state is None:
state = OpenAIStreamConversionState()
events: List[Dict[str, Any]] = []
choices = chunk.get("choices") or []
if not choices:
return events
choice = choices[0]
delta = choice.get("delta") or {}
finish_reason = choice.get("finish_reason")
# 记录模型
if chunk.get("model"):
state.model = chunk["model"]
# 处理文本增量
content = delta.get("content")
if content:
events.append(
{
"candidates": [
{
"content": {
"parts": [{"text": content}],
"role": "model",
},
"index": 0,
}
],
"modelVersion": state.model,
}
)
# 处理工具调用
tool_calls = delta.get("tool_calls") or []
for tc in tool_calls:
if tc.get("function"):
func = tc["function"]
# 工具名称
if func.get("name"):
state.current_tool_name = func["name"]
state.accumulated_tool_args = ""
# 工具参数
if func.get("arguments"):
state.accumulated_tool_args += func["arguments"]
# 处理结束
if finish_reason:
# 如果有累积的工具调用,先发送
if state.current_tool_name and state.accumulated_tool_args:
try:
args = json.loads(state.accumulated_tool_args)
except json.JSONDecodeError:
args = {}
events.append(
{
"candidates": [
{
"content": {
"parts": [
{
"functionCall": {
"name": state.current_tool_name,
"args": args,
}
}
],
"role": "model",
},
"index": 0,
}
],
"modelVersion": state.model,
}
)
state.current_tool_name = ""
state.accumulated_tool_args = ""
# 发送结束标记
gemini_finish_reason = self._convert_finish_reason_to_gemini(finish_reason)
events.append(
{
"candidates": [
{
"content": {"parts": [], "role": "model"},
"finishReason": gemini_finish_reason,
"index": 0,
}
],
"modelVersion": state.model,
}
)
return events
class GeminiToOpenAIConverter:
"""
Gemini -> OpenAI 响应转换器
Gemini -> OpenAI 转换器
将 Gemini generateContent 响应转换为 OpenAI Chat Completions API 格式
- 请求转换:将 Gemini generateContent 请求转换为 OpenAI Chat Completions API 格式
- 响应转换:将 Gemini generateContent 响应转换为 OpenAI Chat Completions API 格式
"""
def convert_request(self, gemini_request: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Gemini 请求转换为 OpenAI 请求
Args:
gemini_request: Gemini 格式的请求字典
Returns:
OpenAI 格式的请求字典
"""
openai_request: Dict[str, Any] = {
"messages": self._convert_contents_to_messages(gemini_request),
}
# 注意stream 参数由调用方根据请求类型设置
# Gemini 通过 URL 端点区分流式/非流式streamGenerateContent vs generateContent
# OpenAI 通过请求体中的 stream 字段区分
# 调用方chat_handler_base会在格式转换后设置 stream 参数
# 转换生成配置
generation_config = gemini_request.get("generation_config", {})
if "max_output_tokens" in generation_config:
openai_request["max_tokens"] = generation_config["max_output_tokens"]
if "temperature" in generation_config:
openai_request["temperature"] = generation_config["temperature"]
if "top_p" in generation_config:
openai_request["top_p"] = generation_config["top_p"]
if "stop_sequences" in generation_config:
openai_request["stop"] = generation_config["stop_sequences"]
if "candidate_count" in generation_config:
openai_request["n"] = generation_config["candidate_count"]
# 转换工具
tools = gemini_request.get("tools", [])
if tools:
openai_request["tools"] = self._convert_tools_to_openai(tools)
return openai_request
def _convert_contents_to_messages(
self, gemini_request: Dict[str, Any]
) -> List[Dict[str, Any]]:
"""转换 Gemini contents 为 OpenAI messages"""
messages = []
# 转换 system instruction
system_instruction = gemini_request.get("system_instruction")
if system_instruction:
parts = system_instruction.get("parts", [])
system_text = "".join(p.get("text", "") for p in parts if "text" in p)
if system_text:
messages.append({"role": "system", "content": system_text})
# 转换 contents
for content in gemini_request.get("contents", []):
role = content.get("role", "user")
# Gemini 使用 "model"OpenAI 使用 "assistant"
openai_role = "assistant" if role == "model" else "user"
parts = content.get("parts", [])
openai_content, tool_calls = self._convert_parts_to_openai_content(parts)
message: Dict[str, Any] = {
"role": openai_role,
}
if openai_content:
message["content"] = openai_content
if tool_calls:
message["tool_calls"] = tool_calls
messages.append(message)
return messages
def _convert_parts_to_openai_content(
self, parts: List[Dict[str, Any]]
) -> tuple[Any, List[Dict[str, Any]]]:
"""将 Gemini parts 转换为 OpenAI content 和 tool_calls"""
content_parts: List[Any] = []
tool_calls: List[Dict[str, Any]] = []
for part in parts:
if "text" in part:
content_parts.append({"type": "text", "text": part["text"]})
elif "inline_data" in part:
# 转换图片
inline_data = part["inline_data"]
mime_type = inline_data.get("mime_type", "image/png")
data = inline_data.get("data", "")
content_parts.append(
{
"type": "image_url",
"image_url": {"url": f"data:{mime_type};base64,{data}"},
}
)
elif "function_call" in part:
# 转换工具调用
func_call = part["function_call"]
tool_calls.append(
{
"id": f"call_{func_call.get('name', '')}",
"type": "function",
"function": {
"name": func_call.get("name", ""),
"arguments": json.dumps(func_call.get("args", {})),
},
}
)
# 简化内容格式
if len(content_parts) == 1 and content_parts[0].get("type") == "text":
content = content_parts[0]["text"]
elif content_parts:
content = content_parts
else:
content = None
return content, tool_calls
def _convert_tools_to_openai(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""转换 Gemini 工具为 OpenAI 格式"""
openai_tools = []
for tool in tools:
function_declarations = tool.get("function_declarations", [])
for func_decl in function_declarations:
openai_tool: Dict[str, Any] = {
"type": "function",
"function": {
"name": func_decl.get("name", ""),
},
}
if "description" in func_decl:
openai_tool["function"]["description"] = func_decl["description"]
if "parameters" in func_decl:
openai_tool["function"]["parameters"] = func_decl["parameters"]
openai_tools.append(openai_tool)
return openai_tools
def convert_response(self, gemini_response: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Gemini 响应转换为 OpenAI 响应
@@ -707,13 +1366,13 @@ class GeminiToOpenAIConverter:
state = GeminiStreamConversionState()
events: List[Dict[str, Any]] = []
candidates = chunk.get("candidates", [])
candidates = chunk.get("candidates") or []
if not candidates:
return events
candidate = candidates[0]
content = candidate.get("content", {})
parts = content.get("parts", [])
content = candidate.get("content") or {}
parts = content.get("parts") or []
finish_reason = candidate.get("finishReason")
chunk_id = f"chatcmpl-{state.message_id or 'gemini'}"

View File

@@ -389,12 +389,12 @@ class OpenAIToClaudeConverter:
events: List[Dict[str, Any]] = []
choices = chunk.get("choices", [])
choices = chunk.get("choices") or []
if not choices:
return events
choice = choices[0]
delta = choice.get("delta", {})
delta = choice.get("delta") or {}
finish_reason = choice.get("finish_reason")
# 处理角色(第一个 chunk
@@ -429,7 +429,7 @@ class OpenAIToClaudeConverter:
)
# 处理工具调用
tool_calls = delta.get("tool_calls", [])
tool_calls = delta.get("tool_calls") or []
for tool_call in tool_calls:
index = tool_call.get("index", 0)

View File

@@ -23,7 +23,12 @@ from src.core.metrics import format_conversion_duration_seconds, format_conversi
from .exceptions import FormatConversionError
if TYPE_CHECKING:
from .state import GeminiStreamConversionState, StreamConversionState
from .state import (
ClaudeStreamConversionState,
GeminiStreamConversionState,
OpenAIStreamConversionState,
StreamConversionState,
)
@contextmanager
@@ -390,7 +395,14 @@ class FormatConverterRegistry:
chunk: Dict[str, Any],
source_format: str,
target_format: str,
state: Optional[Union["StreamConversionState", "GeminiStreamConversionState"]] = None,
state: Optional[
Union[
"StreamConversionState",
"GeminiStreamConversionState",
"ClaudeStreamConversionState",
"OpenAIStreamConversionState",
]
] = None,
) -> list[Dict[str, Any]]:
"""
严格模式流式块转换 - 失败时抛出异常

View File

@@ -63,7 +63,52 @@ class GeminiStreamConversionState:
self.has_sent_usage = False
@dataclass
class ClaudeStreamConversionState:
"""
Claude -> Gemini 流式转换状态
用于将 Claude SSE 事件流转换为 Gemini JSON 流式响应
"""
message_id: str = ""
model: str = ""
current_block_type: str = "" # 当前内容块类型text/tool_use
current_block_index: int = 0
current_tool_name: str = ""
current_tool_id: str = ""
accumulated_tool_input: str = "" # 累积的工具输入 JSON
def reset(self) -> None:
"""重置状态(重试时调用)"""
self.current_block_type = ""
self.current_block_index = 0
self.current_tool_name = ""
self.current_tool_id = ""
self.accumulated_tool_input = ""
@dataclass
class OpenAIStreamConversionState:
"""
OpenAI -> Gemini 流式转换状态
用于将 OpenAI SSE 事件流转换为 Gemini JSON 流式响应
"""
model: str = ""
current_tool_name: str = ""
accumulated_tool_args: str = "" # 累积的工具参数 JSON
def reset(self) -> None:
"""重置状态(重试时调用)"""
self.current_tool_name = ""
self.accumulated_tool_args = ""
__all__ = [
"StreamConversionState",
"GeminiStreamConversionState",
"ClaudeStreamConversionState",
"OpenAIStreamConversionState",
]

View File

@@ -36,6 +36,8 @@ class ApiFormatDefinition:
- auth_type: 认证类型 ("header" 直接放值, "bearer" 加 Bearer 前缀)
- extra_headers: 该格式必须携带的额外头部(如 anthropic-version
- protected_keys: 不应被 extra_headers 覆盖的头部(小写)
- model_in_body: 是否需要在请求体中包含 model 字段Gemini 等格式通过 URL 传递模型名)
- stream_in_body: 是否需要在请求体中包含 stream 字段Gemini 等格式通过 URL 端点区分流式)
"""
api_format: APIFormat
@@ -46,6 +48,8 @@ class ApiFormatDefinition:
auth_type: str = "bearer" # "bearer" or "header"
extra_headers: Mapping[str, str] = field(default_factory=dict) # 格式必须的额外头部
protected_keys: frozenset[str] = field(default_factory=frozenset) # 受保护的头部 key小写
model_in_body: bool = True # 是否需要在请求体中包含 model 字段
stream_in_body: bool = True # 是否需要在请求体中包含 stream 字段
def iter_aliases(self) -> Iterable[str]:
"""返回大小写统一后的别名集合,包含枚举名本身。"""
@@ -112,6 +116,8 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
auth_header="x-goog-api-key",
auth_type="header",
protected_keys=frozenset({"x-goog-api-key", "content-type"}),
model_in_body=False, # Gemini 通过 URL 路径传递模型名
stream_in_body=False, # Gemini 通过 URL 端点区分流式streamGenerateContent vs generateContent
),
APIFormat.GEMINI_CLI: ApiFormatDefinition(
api_format=APIFormat.GEMINI_CLI,
@@ -121,6 +127,8 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
auth_header="x-goog-api-key",
auth_type="header",
protected_keys=frozenset({"x-goog-api-key", "content-type"}),
model_in_body=False, # Gemini 通过 URL 路径传递模型名
stream_in_body=False, # Gemini 通过 URL 端点区分流式
),
}