chore: 升级到 Python 3.14 并现代化代码

- 升级 Docker 基础镜像从 Python 3.12 到 3.14
- 更新 pyproject.toml 支持 Python 3.13/3.14
- 移除 Python 3.8/3.9/3.10/3.11 分类器
- 更新 black 和 mypy 配置目标版本
- 将 get_event_loop() 替换为 get_running_loop() 加上 RuntimeError 处理
- 简化 compute_cost_sync 中的 asyncio.run 使用
- Dict/List/Tuple/Set → dict/list/tuple/set (PEP 585)
- Optional[T] → T | None (PEP 604)
- Union[A, B] → A | B (PEP 604)
- 移除废弃的 typing 导入
- 移除不必要的字符串引号注解
This commit is contained in:
AAEE86
2026-01-30 03:10:21 +08:00
parent 3e75bc8964
commit 24d24f6829
255 changed files with 4062 additions and 4173 deletions

View File

@@ -11,7 +11,7 @@ ClaudeNormalizer 单元测试
from __future__ import annotations
from typing import Any, Dict, List, cast
from typing import Any, cast
from src.core.api_format.conversion.internal import (
ErrorType,
@@ -62,7 +62,7 @@ def test_claude_request_system_roundtrip() -> None:
assert out["model"] == "claude-3-opus"
assert out["system"] == "sys"
out_messages: List[Dict[str, Any]] = out["messages"]
out_messages: list[dict[str, Any]] = out["messages"]
assert [m["role"] for m in out_messages] == ["user", "assistant"]
assert out_messages[0]["content"] == "hi"
assert out_messages[1]["content"] == "ok"
@@ -128,19 +128,19 @@ def test_claude_request_tool_blocks_roundtrip() -> None:
assert tool_result.is_error is False
out = n.request_from_internal(internal)
out_messages: List[Dict[str, Any]] = out["messages"]
out_messages: list[dict[str, Any]] = out["messages"]
assert [m["role"] for m in out_messages] == ["user", "assistant", "user"]
assistant_out = out_messages[1]
assert isinstance(assistant_out["content"], list)
a_blocks = cast(List[Dict[str, Any]], assistant_out["content"])
a_blocks = cast(list[dict[str, Any]], assistant_out["content"])
assert a_blocks[0]["type"] == "tool_use"
assert a_blocks[0]["id"] == "toolu_1"
assert a_blocks[0]["name"] == "get_weather"
user_out = out_messages[2]
assert isinstance(user_out["content"], list)
u_blocks = cast(List[Dict[str, Any]], user_out["content"])
u_blocks = cast(list[dict[str, Any]], user_out["content"])
assert u_blocks[0]["type"] == "tool_result"
assert u_blocks[0]["tool_use_id"] == "toolu_1"
assert u_blocks[0]["content"] == {"temp_c": 20}
@@ -173,7 +173,7 @@ def test_claude_unknown_block_drop_on_output() -> None:
out = n.request_from_internal(internal)
# Claude 要求以 user 开头,且会做最小修复:插入空 user
out_messages: List[Dict[str, Any]] = out["messages"]
out_messages: list[dict[str, Any]] = out["messages"]
assert out_messages[0]["role"] == "user"
assert out_messages[1]["role"] == "assistant"
assert out_messages[1]["content"] == "ok"
@@ -255,7 +255,7 @@ def test_claude_stream_chunk_and_event_roundtrip_basic() -> None:
{"type": "message_stop"},
]
events: List[Any] = []
events: list[Any] = []
for ch in chunks:
events.extend(n.stream_chunk_to_internal(ch, state))
@@ -266,7 +266,7 @@ def test_claude_stream_chunk_and_event_roundtrip_basic() -> None:
# internal events -> Claude events
state2 = StreamState()
out_events: List[Dict[str, Any]] = []
out_events: list[dict[str, Any]] = []
for e in events:
out_events.extend(n.stream_event_from_internal(e, state2))

View File

@@ -8,7 +8,7 @@ CLI 格式参与转换的单元测试
from __future__ import annotations
from typing import Any, Dict, List, cast
from typing import Any, cast
from src.core.api_format.conversion.normalizers.claude import ClaudeNormalizer
from src.core.api_format.conversion.normalizers.claude_cli import ClaudeCliNormalizer
@@ -72,10 +72,10 @@ def test_claude_response_to_openai_cli() -> None:
openai_cli_resp = reg.convert_response(claude_resp, "CLAUDE", "OPENAI_CLI")
assert openai_cli_resp["object"] == "response"
assert isinstance(openai_cli_resp.get("output"), list)
msg = cast(Dict[str, Any], openai_cli_resp["output"][0])
msg = cast(dict[str, Any], openai_cli_resp["output"][0])
assert msg["type"] == "message"
assert msg["role"] == "assistant"
content = cast(List[Dict[str, Any]], msg.get("content") or [])
content = cast(list[dict[str, Any]], msg.get("content") or [])
assert content and content[0]["type"] == "output_text"
assert content[0]["text"] == "hello"
@@ -117,8 +117,8 @@ def test_stream_openai_cli_to_openai_delta() -> None:
# 第二个 chunk 才是文本增量
choices = out_events[1].get("choices") or []
assert isinstance(choices, list) and choices
delta = cast(Dict[str, Any], choices[0]).get("delta") or {}
assert cast(Dict[str, Any], delta).get("content") == "hi"
delta = cast(dict[str, Any], choices[0]).get("delta") or {}
assert cast(dict[str, Any], delta).get("content") == "hi"
def test_openai_cli_function_call_to_claude() -> None:
@@ -484,7 +484,7 @@ def test_real_claude_cli_stream_response_conversion() -> None:
]
# 收集所有转换后的 OpenAI 格式事件
all_openai_events: List[Dict[str, Any]] = []
all_openai_events: list[dict[str, Any]] = []
for chunk in chunks:
events = reg.convert_stream_chunk(chunk, "CLAUDE_CLI", "OPENAI", state=state)
all_openai_events.extend(events)
@@ -553,7 +553,7 @@ def test_real_claude_cli_stream_to_openai_cli() -> None:
{"type": "message_stop"},
]
all_events: List[Dict[str, Any]] = []
all_events: list[dict[str, Any]] = []
for chunk in chunks:
events = reg.convert_stream_chunk(chunk, "CLAUDE_CLI", "OPENAI_CLI", state=state)
all_events.extend(events)

View File

@@ -8,7 +8,7 @@
from __future__ import annotations
from typing import Any, Dict, cast
from typing import Any, cast
from src.core.api_format.conversion.internal import ErrorType, InternalError
from src.core.api_format.conversion.normalizers.claude import ClaudeNormalizer
@@ -65,7 +65,7 @@ def test_error_event_stream_openai_to_claude_via_registry() -> None:
chunk = {"error": {"message": "bad", "type": "invalid_request_error"}}
out = reg.convert_stream_chunk(chunk, "OPENAI", "CLAUDE", state=StreamState())
assert isinstance(out, list) and out
evt0 = cast(Dict[str, Any], out[0])
evt0 = cast(dict[str, Any], out[0])
assert evt0.get("type") == "error"
assert isinstance(evt0.get("error"), dict)
assert evt0["error"]["message"] == "bad"

View File

@@ -12,7 +12,7 @@ GeminiNormalizer 单元测试
from __future__ import annotations
import json
from typing import Any, Dict, List, cast
from typing import Any, cast
from src.core.api_format.conversion.internal import (
ErrorType,
@@ -138,18 +138,18 @@ def test_gemini_request_parts_image_tool_and_unknown_drop() -> None:
assert tool_result.output == {"temp_c": 20}
out = n.request_from_internal(internal)
out_contents: List[Dict[str, Any]] = out["contents"]
out_contents: list[dict[str, Any]] = out["contents"]
# unknown 被丢弃
user_parts = cast(List[Dict[str, Any]], out_contents[0]["parts"])
user_parts = cast(list[dict[str, Any]], out_contents[0]["parts"])
assert any(p.get("text") == "look" for p in user_parts)
assert any(p.get("inline_data", {}).get("mime_type") == "image/png" for p in user_parts)
assert all("foo" not in p for p in user_parts)
model_parts = cast(List[Dict[str, Any]], out_contents[1]["parts"])
model_parts = cast(list[dict[str, Any]], out_contents[1]["parts"])
assert model_parts[0]["function_call"]["name"] == "get_weather"
tool_parts = cast(List[Dict[str, Any]], out_contents[2]["parts"])
tool_parts = cast(list[dict[str, Any]], out_contents[2]["parts"])
assert tool_parts[0]["function_response"]["name"] == "call_1"
assert tool_parts[0]["function_response"]["response"]["result"] == {"temp_c": 20}
@@ -231,7 +231,7 @@ def test_gemini_stream_chunk_and_event_roundtrip_basic() -> None:
},
]
events: List[Any] = []
events: list[Any] = []
for ch in chunks:
events.extend(n.stream_chunk_to_internal(ch, state))
@@ -241,7 +241,7 @@ def test_gemini_stream_chunk_and_event_roundtrip_basic() -> None:
assert any(isinstance(e, MessageStopEvent) and e.stop_reason == StopReason.END_TURN for e in events)
state2 = StreamState()
out_chunks: List[Dict[str, Any]] = []
out_chunks: list[dict[str, Any]] = []
for e in events:
out_chunks.extend(n.stream_event_from_internal(e, state2))

View File

@@ -10,7 +10,7 @@ from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict, List
from typing import Any
from src.core.api_format.conversion.normalizers.claude import ClaudeNormalizer
from src.core.api_format.conversion.normalizers.gemini import GeminiNormalizer
@@ -28,7 +28,7 @@ def _scrub(obj: Any) -> Any:
if isinstance(obj, list):
return [_scrub(x) for x in obj]
if isinstance(obj, dict):
out: Dict[str, Any] = {}
out: dict[str, Any] = {}
for k, v in obj.items():
if k in {"created"}:
continue
@@ -93,7 +93,7 @@ def test_golden_streams() -> None:
reg = _make_registry()
formats = ["OPENAI", "CLAUDE", "GEMINI"]
inputs: Dict[str, List[Dict[str, Any]]] = {
inputs: dict[str, list[dict[str, Any]]] = {
"OPENAI": _load_json(INPUT_DIR / "stream_openai.json"),
"CLAUDE": _load_json(INPUT_DIR / "stream_claude.json"),
"GEMINI": _load_json(INPUT_DIR / "stream_gemini.json"),
@@ -109,7 +109,7 @@ def test_golden_streams() -> None:
if source == "GEMINI":
state.message_id = "gemini_1"
out: List[Dict[str, Any]] = []
out: list[dict[str, Any]] = []
for chunk in inputs[source]:
out.extend(reg.convert_stream_chunk(chunk, source, target, state=state))

View File

@@ -12,7 +12,7 @@ OpenAINormalizer 单元测试
from __future__ import annotations
import json
from typing import Any, Dict, List, cast
from typing import Any, cast
from src.core.api_format.conversion.internal import (
ContentType,
@@ -35,14 +35,14 @@ from src.core.api_format.conversion.stream_events import (
from src.core.api_format.conversion.stream_state import StreamState
def _first_choice_message(response: Dict[str, Any]) -> Dict[str, Any]:
def _first_choice_message(response: dict[str, Any]) -> dict[str, Any]:
choices = response.get("choices") or []
assert isinstance(choices, list) and choices
c0 = choices[0]
assert isinstance(c0, dict)
msg = c0.get("message")
assert isinstance(msg, dict)
return cast(Dict[str, Any], msg)
return cast(dict[str, Any], msg)
def test_openai_request_instructions_roundtrip() -> None:
@@ -169,7 +169,7 @@ def test_openai_request_tool_calls_and_tool_role_roundtrip() -> None:
assert tool_result.output == {"temp_c": 20, "unit": "C"}
out = n.request_from_internal(internal)
out_messages: List[Dict[str, Any]] = out["messages"]
out_messages: list[dict[str, Any]] = out["messages"]
roles = [m.get("role") for m in out_messages]
assert roles == ["user", "assistant", "tool", "assistant"]
@@ -217,7 +217,7 @@ def test_openai_request_content_image_and_unknown_drop() -> None:
out_msg = out["messages"][0]
assert out_msg["role"] == "user"
assert isinstance(out_msg["content"], list)
parts = cast(List[Dict[str, Any]], out_msg["content"])
parts = cast(list[dict[str, Any]], out_msg["content"])
assert any(p.get("type") == "image_url" for p in parts)
assert all(p.get("type") != "foo" for p in parts)
@@ -289,7 +289,7 @@ def test_openai_stream_chunk_and_event_roundtrip_basic() -> None:
{"choices": [{"index": 0, "delta": {}, "finish_reason": "tool_calls"}]},
]
events: List[Any] = []
events: list[Any] = []
for ch in chunks:
events.extend(n.stream_chunk_to_internal(ch, state))
@@ -301,7 +301,7 @@ def test_openai_stream_chunk_and_event_roundtrip_basic() -> None:
# internal events -> OpenAI chunks验证关键字段与 tool_calls index 稳定)
state2 = StreamState()
out_chunks: List[Dict[str, Any]] = []
out_chunks: list[dict[str, Any]] = []
for e in events:
out_chunks.extend(n.stream_event_from_internal(e, state2))

View File

@@ -8,7 +8,7 @@ Canonical Registry 单元测试
from __future__ import annotations
from typing import Any, Dict, List, cast
from typing import Any, cast
from src.core.api_format.conversion.normalizers.claude import ClaudeNormalizer
from src.core.api_format.conversion.normalizers.gemini import GeminiNormalizer
@@ -25,14 +25,14 @@ def _make_registry() -> FormatConversionRegistry:
return reg
def _first_openai_choice_message(resp: Dict[str, Any]) -> Dict[str, Any]:
def _first_openai_choice_message(resp: dict[str, Any]) -> dict[str, Any]:
choices = resp.get("choices") or []
assert isinstance(choices, list) and choices
c0 = choices[0]
assert isinstance(c0, dict)
msg = c0.get("message")
assert isinstance(msg, dict)
return cast(Dict[str, Any], msg)
return cast(dict[str, Any], msg)
def test_registry_canonical_can_convert_full_stream() -> None:
@@ -101,5 +101,5 @@ def test_registry_canonical_stream_openai_to_claude() -> None:
out_events = reg.convert_stream_chunk(chunk, "OPENAI", "CLAUDE", state=state)
assert isinstance(out_events, list) and out_events
types = [cast(Dict[str, Any], e).get("type") for e in cast(List[Dict[str, Any]], out_events)]
types = [cast(dict[str, Any], e).get("type") for e in cast(list[dict[str, Any]], out_events)]
assert types[:3] == ["message_start", "content_block_start", "content_block_delta"]