feat(provider): 增加 Claude Code 适配器、高级配置能力与 OAuth 账号类型统一解析

- 新增 Claude Code provider adapter (context, envelope, plugin, constants)
- 扩展 provider admin 路由,支持 Claude Code 高级配置 (CRUD)
- 统一 OAuth 账号类型解析逻辑,前后端对齐
- 重构 BatchAssignModelsDialog / ModelMappingDialog,简化组件逻辑
- handler 基类增强: request_builder 支持 Claude Code 信封格式
- CLI stream/sync mixin 适配 Claude Code 流式与同步模式
- 扩展 candidate builder / failover / scheduler 对 Claude Code 的支持
- 前端增加请求时间线可视化 (HorizontalRequestTimeline)
- 补充 Claude Code envelope / runtime controls / distributed sessions 等测试

Closes #183
Closes #185

Co-Authored-By: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
fawney19
2026-02-27 13:54:46 +08:00
parent f2f2a2dbc4
commit 579b5e4623
55 changed files with 3106 additions and 652 deletions

View File

@@ -0,0 +1,84 @@
from __future__ import annotations
import json
from collections.abc import AsyncIterator
import pytest
from src.api.handlers.base.upstream_stream_bridge import (
aggregate_upstream_stream_to_internal_response,
)
from src.core.api_format.conversion import register_default_normalizers
from src.core.api_format.conversion.internal import TextBlock
async def _iter_stream_lines(lines: list[str]) -> AsyncIterator[bytes]:
for line in lines:
yield line.encode("utf-8")
@pytest.mark.asyncio
async def test_aggregate_claude_stream_uses_message_start_usage_when_message_delta_absent() -> None:
register_default_normalizers()
lines = [
"data: "
+ json.dumps(
{
"type": "message_start",
"message": {
"id": "msg_bridge_usage",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-5",
"content": [],
"usage": {
"input_tokens": 120,
"output_tokens": 0,
"cache_read_input_tokens": 11,
},
},
},
ensure_ascii=False,
)
+ "\n",
"data: "
+ json.dumps(
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "text", "text": ""},
},
ensure_ascii=False,
)
+ "\n",
"data: "
+ json.dumps(
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "hello"},
},
ensure_ascii=False,
)
+ "\n",
"data: "
+ json.dumps({"type": "content_block_stop", "index": 0}, ensure_ascii=False)
+ "\n",
]
internal = await aggregate_upstream_stream_to_internal_response(
_iter_stream_lines(lines),
provider_api_format="claude:cli",
provider_name="claude_code",
model="claude-sonnet-4-5",
request_id="req_bridge_usage",
)
assert internal.usage is not None
assert internal.usage.input_tokens == 120
assert internal.usage.output_tokens == 0
assert internal.usage.cache_read_tokens == 11
assert len(internal.content) == 1
assert isinstance(internal.content[0], TextBlock)
assert internal.content[0].text == "hello"