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,178 @@
from __future__ import annotations
from types import SimpleNamespace
import pytest
from src.api.handlers.base.request_builder import PassthroughRequestBuilder
from src.config.settings import config
from src.services.provider.adapters.claude_code.constants import (
BETA_CLAUDE_CODE,
BETA_CONTEXT_1M,
BETA_INTERLEAVED_THINKING,
BETA_OAUTH,
CLAUDE_CODE_REQUIRED_BETA_TOKENS,
DEFAULT_ACCEPT,
DEFAULT_ANTHROPIC_VERSION,
STREAM_HELPER_METHOD,
)
from src.services.provider.adapters.claude_code.context import set_claude_code_request_context
from src.services.provider.adapters.claude_code.envelope import (
claude_code_envelope,
merge_anthropic_beta_tokens,
)
@pytest.fixture(autouse=True)
def _reset_claude_code_context():
set_claude_code_request_context(None)
yield
set_claude_code_request_context(None)
def test_merge_anthropic_beta_tokens_adds_required_and_deduplicates() -> None:
merged = merge_anthropic_beta_tokens(
"context-1m-2025-08-07,oauth-2025-04-20,custom-beta,claude-code-20250219"
)
assert merged.split(",") == [
BETA_CLAUDE_CODE,
BETA_OAUTH,
BETA_INTERLEAVED_THINKING,
"context-1m-2025-08-07",
"custom-beta",
]
def test_claude_code_envelope_extra_headers_include_required_defaults(
monkeypatch,
) -> None:
monkeypatch.setattr(config, "internal_user_agent_claude_cli", "claude-code/test")
headers = claude_code_envelope.extra_headers() or {}
assert headers.get("anthropic-version") == DEFAULT_ANTHROPIC_VERSION
assert headers.get("anthropic-beta") == ",".join(CLAUDE_CODE_REQUIRED_BETA_TOKENS)
assert headers.get("Accept") == DEFAULT_ACCEPT
assert headers.get("X-App") == "cli"
assert headers.get("X-Stainless-Lang") == "js"
assert headers.get("Anthropic-Dangerous-Direct-Browser-Access") == "true"
assert headers.get("User-Agent") == "claude-code/test"
assert "x-stainless-helper-method" not in headers
def test_claude_code_envelope_adds_stream_helper_header_for_stream_request() -> None:
_, _ = claude_code_envelope.wrap_request(
{"stream": True},
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)
headers = claude_code_envelope.extra_headers() or {}
assert headers.get("Accept") == DEFAULT_ACCEPT
assert headers.get("x-stainless-helper-method") == STREAM_HELPER_METHOD
def test_passthrough_request_builder_drops_context_1m_for_claude_code_oauth() -> None:
builder = PassthroughRequestBuilder()
endpoint = SimpleNamespace(header_rules=None)
key = SimpleNamespace(api_key="unused")
headers = builder.build_headers(
original_headers={"anthropic-beta": BETA_CONTEXT_1M},
endpoint=endpoint,
key=key,
extra_headers={"anthropic-beta": ",".join(CLAUDE_CODE_REQUIRED_BETA_TOKENS)},
pre_computed_auth=("Authorization", "Bearer test-token"),
envelope=claude_code_envelope,
)
assert headers.get("anthropic-beta") == ",".join(CLAUDE_CODE_REQUIRED_BETA_TOKENS)
def test_passthrough_request_builder_keeps_context_1m_for_non_claude_code_provider() -> None:
builder = PassthroughRequestBuilder()
endpoint = SimpleNamespace(header_rules=None)
key = SimpleNamespace(api_key="unused")
headers = builder.build_headers(
original_headers={"anthropic-beta": BETA_CONTEXT_1M},
endpoint=endpoint,
key=key,
extra_headers={"anthropic-beta": ",".join(CLAUDE_CODE_REQUIRED_BETA_TOKENS)},
pre_computed_auth=("Authorization", "Bearer test-token"),
envelope=None,
)
assert BETA_CONTEXT_1M in str(headers.get("anthropic-beta") or "")
def test_claude_code_envelope_filters_invalid_thinking_blocks_when_enabled() -> None:
body = {
"thinking": {"type": "enabled"},
"messages": [
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "keep", "signature": "sig_valid"},
{"type": "thinking", "thinking": "drop-empty-signature", "signature": ""},
{
"type": "thinking",
"thinking": "drop-dummy-signature",
"signature": "skip_thought_signature_validator",
},
{"type": "redacted_thinking", "data": "keep", "signature": "sig_redacted"},
{"type": "redacted_thinking", "data": "drop-no-signature"},
{"thinking": "drop-no-type"},
{"type": "text", "text": "ok"},
],
}
],
}
wrapped, _ = claude_code_envelope.wrap_request(
body,
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)
content = wrapped["messages"][0]["content"]
assert content == [
{"type": "thinking", "thinking": "keep", "signature": "sig_valid"},
{"type": "redacted_thinking", "data": "keep", "signature": "sig_redacted"},
{"type": "text", "text": "ok"},
]
def test_claude_code_envelope_drops_all_thinking_blocks_when_disabled() -> None:
body = {
"thinking": {"type": "disabled"},
"messages": [
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "remove", "signature": "sig_valid"},
{"type": "redacted_thinking", "data": "remove", "signature": "sig_redacted"},
{"type": "text", "text": "keep"},
],
},
{
"role": "user",
"content": [
{"type": "thinking", "thinking": "remove-user", "signature": "sig_user"},
{"type": "text", "text": "keep-user"},
],
},
],
}
wrapped, _ = claude_code_envelope.wrap_request(
body,
model="claude-sonnet-4-5-20250929",
url_model=None,
decrypted_auth_config=None,
)
assert wrapped["messages"][0]["content"] == [{"type": "text", "text": "keep"}]
assert wrapped["messages"][1]["content"] == [{"type": "text", "text": "keep-user"}]