mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat(conversion): 支持 Claude output_config.effort 跨格式转换,新增 xhigh 档位
- 新增 Claude output_config.effort 与标准化 reasoning_effort 的双向映射 - 新增 xhigh 档位(budget_tokens=8192),对应 Claude effort=max - reasoning_effort 独立于 thinking 存入 extra,支持无 thinking 场景的跨格式传递 - OpenAI/Responses API 输出时 xhigh 自动降级为 high
This commit is contained in:
@@ -130,15 +130,34 @@ REASONING_EFFORT_TO_THINKING_BUDGET: dict[str, int] = {
|
|||||||
"low": 1280,
|
"low": 1280,
|
||||||
"medium": 2048,
|
"medium": 2048,
|
||||||
"high": 4096,
|
"high": 4096,
|
||||||
|
"xhigh": 8192,
|
||||||
}
|
}
|
||||||
|
|
||||||
# thinking budget_tokens -> OpenAI reasoning_effort(反向映射,取最近区间)
|
# thinking budget_tokens -> OpenAI reasoning_effort(反向映射,取最近区间)
|
||||||
THINKING_BUDGET_TO_REASONING_EFFORT: list[tuple[int, str]] = [
|
THINKING_BUDGET_TO_REASONING_EFFORT: list[tuple[int, str]] = [
|
||||||
(1664, "low"), # <= 1664 -> low (midpoint of 1280..2048)
|
(1664, "low"), # <= 1664 -> low (midpoint of 1280..2048)
|
||||||
(3072, "medium"), # <= 3072 -> medium (midpoint of 2048..4096)
|
(3072, "medium"), # <= 3072 -> medium (midpoint of 2048..4096)
|
||||||
(2**31, "high"), # > 3072 -> high
|
(6144, "high"), # <= 6144 -> high (midpoint of 4096..8192)
|
||||||
|
(2**31, "xhigh"), # > 6144 -> xhigh
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Claude output_config.effort -> 标准化 reasoning_effort
|
||||||
|
CLAUDE_EFFORT_TO_REASONING_EFFORT: dict[str, str] = {
|
||||||
|
"low": "low",
|
||||||
|
"medium": "medium",
|
||||||
|
"high": "high",
|
||||||
|
"max": "xhigh",
|
||||||
|
# "auto" 不映射,让目标格式使用默认行为
|
||||||
|
}
|
||||||
|
|
||||||
|
# 标准化 reasoning_effort -> Claude output_config.effort
|
||||||
|
REASONING_EFFORT_TO_CLAUDE_EFFORT: dict[str, str] = {
|
||||||
|
"low": "low",
|
||||||
|
"medium": "medium",
|
||||||
|
"high": "high",
|
||||||
|
"xhigh": "max",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# OpenAI web_search_options.search_context_size -> Claude web_search max_uses
|
# OpenAI web_search_options.search_context_size -> Claude web_search max_uses
|
||||||
WEB_SEARCH_CONTEXT_SIZE_TO_MAX_USES: dict[str, int] = {
|
WEB_SEARCH_CONTEXT_SIZE_TO_MAX_USES: dict[str, int] = {
|
||||||
@@ -190,6 +209,8 @@ __all__ = [
|
|||||||
"RETRYABLE_ERROR_TYPES",
|
"RETRYABLE_ERROR_TYPES",
|
||||||
"REASONING_EFFORT_TO_THINKING_BUDGET",
|
"REASONING_EFFORT_TO_THINKING_BUDGET",
|
||||||
"THINKING_BUDGET_TO_REASONING_EFFORT",
|
"THINKING_BUDGET_TO_REASONING_EFFORT",
|
||||||
|
"CLAUDE_EFFORT_TO_REASONING_EFFORT",
|
||||||
|
"REASONING_EFFORT_TO_CLAUDE_EFFORT",
|
||||||
"WEB_SEARCH_CONTEXT_SIZE_TO_MAX_USES",
|
"WEB_SEARCH_CONTEXT_SIZE_TO_MAX_USES",
|
||||||
"CLAUDE_DEFAULT_MAX_TOKENS",
|
"CLAUDE_DEFAULT_MAX_TOKENS",
|
||||||
"THINKING_BUDGET_TOKENS_PERCENTAGE",
|
"THINKING_BUDGET_TOKENS_PERCENTAGE",
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import json
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from src.core.api_format.conversion.field_mappings import (
|
from src.core.api_format.conversion.field_mappings import (
|
||||||
|
CLAUDE_EFFORT_TO_REASONING_EFFORT,
|
||||||
ERROR_TYPE_MAPPINGS,
|
ERROR_TYPE_MAPPINGS,
|
||||||
|
REASONING_EFFORT_TO_CLAUDE_EFFORT,
|
||||||
RETRYABLE_ERROR_TYPES,
|
RETRYABLE_ERROR_TYPES,
|
||||||
STOP_REASON_MAPPINGS,
|
STOP_REASON_MAPPINGS,
|
||||||
THINKING_BUDGET_TOKENS_MIN,
|
THINKING_BUDGET_TOKENS_MIN,
|
||||||
@@ -187,11 +189,19 @@ class ClaudeNormalizer(FormatNormalizer):
|
|||||||
"tools",
|
"tools",
|
||||||
"tool_choice",
|
"tool_choice",
|
||||||
"thinking",
|
"thinking",
|
||||||
|
"output_config",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# output_config.effort -> reasoning_effort (独立于 thinking)
|
||||||
|
output_config = request.get("output_config")
|
||||||
|
if isinstance(output_config, dict):
|
||||||
|
effort = output_config.get("effort")
|
||||||
|
if isinstance(effort, str) and effort in CLAUDE_EFFORT_TO_REASONING_EFFORT:
|
||||||
|
internal.extra["reasoning_effort"] = CLAUDE_EFFORT_TO_REASONING_EFFORT[effort]
|
||||||
|
|
||||||
if dropped:
|
if dropped:
|
||||||
internal.extra.setdefault("raw", {})["dropped_blocks"] = dropped
|
internal.extra.setdefault("raw", {})["dropped_blocks"] = dropped
|
||||||
|
|
||||||
@@ -314,6 +324,11 @@ class ClaudeNormalizer(FormatNormalizer):
|
|||||||
result["max_tokens"] = bt + 1
|
result["max_tokens"] = bt + 1
|
||||||
result["thinking"] = thinking_out
|
result["thinking"] = thinking_out
|
||||||
|
|
||||||
|
# reasoning_effort -> output_config.effort (独立于 thinking)
|
||||||
|
effort = internal.extra.get("reasoning_effort") if internal.extra else None
|
||||||
|
if isinstance(effort, str) and effort in REASONING_EFFORT_TO_CLAUDE_EFFORT:
|
||||||
|
result["output_config"] = {"effort": REASONING_EFFORT_TO_CLAUDE_EFFORT[effort]}
|
||||||
|
|
||||||
# web_search_options -> Claude web_search tool
|
# web_search_options -> Claude web_search tool
|
||||||
web_search_opts = internal.extra.get("web_search_options") if internal.extra else None
|
web_search_opts = internal.extra.get("web_search_options") if internal.extra else None
|
||||||
if isinstance(web_search_opts, dict):
|
if isinstance(web_search_opts, dict):
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
budget_tokens=REASONING_EFFORT_TO_THINKING_BUDGET[reasoning_effort],
|
budget_tokens=REASONING_EFFORT_TO_THINKING_BUDGET[reasoning_effort],
|
||||||
extra={"reasoning_effort": reasoning_effort},
|
extra={"reasoning_effort": reasoning_effort},
|
||||||
)
|
)
|
||||||
|
extra["reasoning_effort"] = reasoning_effort
|
||||||
|
|
||||||
# web_search_options 存入 extra 供目标 normalizer 使用
|
# web_search_options 存入 extra 供目标 normalizer 使用
|
||||||
web_search_options = request.get("web_search_options")
|
web_search_options = request.get("web_search_options")
|
||||||
@@ -344,6 +345,7 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
result["tool_choice"] = self._tool_choice_to_openai(internal.tool_choice)
|
result["tool_choice"] = self._tool_choice_to_openai(internal.tool_choice)
|
||||||
|
|
||||||
# thinking -> reasoning_effort
|
# thinking -> reasoning_effort
|
||||||
|
effort: str | None = None
|
||||||
if internal.thinking and internal.thinking.enabled:
|
if internal.thinking and internal.thinking.enabled:
|
||||||
effort = internal.thinking.extra.get("reasoning_effort")
|
effort = internal.thinking.extra.get("reasoning_effort")
|
||||||
if not effort and internal.thinking.budget_tokens is not None:
|
if not effort and internal.thinking.budget_tokens is not None:
|
||||||
@@ -351,8 +353,14 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
if internal.thinking.budget_tokens <= threshold:
|
if internal.thinking.budget_tokens <= threshold:
|
||||||
effort = level
|
effort = level
|
||||||
break
|
break
|
||||||
if effort:
|
# 兜底: 从 internal.extra 读取 (支持 output_config.effort 独立于 thinking 的场景)
|
||||||
result["reasoning_effort"] = effort
|
if not effort and internal.extra:
|
||||||
|
effort = internal.extra.get("reasoning_effort")
|
||||||
|
if effort:
|
||||||
|
# OpenAI Chat Completions 仅支持 low/medium/high,xhigh 降级为 high
|
||||||
|
if effort == "xhigh":
|
||||||
|
effort = "high"
|
||||||
|
result["reasoning_effort"] = effort
|
||||||
|
|
||||||
# parallel_tool_calls
|
# parallel_tool_calls
|
||||||
if internal.parallel_tool_calls is not None:
|
if internal.parallel_tool_calls is not None:
|
||||||
|
|||||||
@@ -196,6 +196,10 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# reasoning_effort 同步存入 extra (支持独立于 thinking 的跨格式转换)
|
||||||
|
if thinking and thinking.extra.get("reasoning_effort"):
|
||||||
|
internal.extra["reasoning_effort"] = thinking.extra["reasoning_effort"]
|
||||||
|
|
||||||
return internal
|
return internal
|
||||||
|
|
||||||
def request_from_internal(
|
def request_from_internal(
|
||||||
@@ -259,11 +263,13 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
result["tool_choice"] = self._tool_choice_to_openai(internal.tool_choice)
|
result["tool_choice"] = self._tool_choice_to_openai(internal.tool_choice)
|
||||||
|
|
||||||
# thinking -> reasoning (Responses API)
|
# thinking -> reasoning (Responses API)
|
||||||
|
effort: str | None = None
|
||||||
if internal.thinking and internal.thinking.enabled:
|
if internal.thinking and internal.thinking.enabled:
|
||||||
# 优先还原原始 reasoning 对象
|
# 优先还原原始 reasoning 对象
|
||||||
original_reasoning = internal.thinking.extra.get("reasoning")
|
original_reasoning = internal.thinking.extra.get("reasoning")
|
||||||
if isinstance(original_reasoning, dict):
|
if isinstance(original_reasoning, dict):
|
||||||
result["reasoning"] = original_reasoning
|
result["reasoning"] = original_reasoning
|
||||||
|
effort = None # 已还原,不需要再构造
|
||||||
else:
|
else:
|
||||||
effort = internal.thinking.extra.get("reasoning_effort")
|
effort = internal.thinking.extra.get("reasoning_effort")
|
||||||
if not effort and internal.thinking.budget_tokens is not None:
|
if not effort and internal.thinking.budget_tokens is not None:
|
||||||
@@ -271,8 +277,14 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
if internal.thinking.budget_tokens <= threshold:
|
if internal.thinking.budget_tokens <= threshold:
|
||||||
effort = level
|
effort = level
|
||||||
break
|
break
|
||||||
if effort:
|
# 兜底: 从 internal.extra 读取
|
||||||
result["reasoning"] = {"effort": effort}
|
if not effort and internal.extra and "reasoning" not in result:
|
||||||
|
effort = internal.extra.get("reasoning_effort")
|
||||||
|
if effort:
|
||||||
|
# xhigh 降级为 high (Responses API 也仅支持 low/medium/high)
|
||||||
|
if effort == "xhigh":
|
||||||
|
effort = "high"
|
||||||
|
result["reasoning"] = {"effort": effort}
|
||||||
|
|
||||||
# parallel_tool_calls
|
# parallel_tool_calls
|
||||||
if internal.parallel_tool_calls is not None:
|
if internal.parallel_tool_calls is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user