mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(provider,adapter): Codex 默认 body_rules 按 provider_type 维度注册,adapter 全链路传递 provider_type
- 新增 register_provider_default_body_rules 注册机制,将 Codex 特有的 body_rules 从 EndpointDefinition 全局默认移至 codex plugin 按 (provider_type, endpoint_sig) 维度注册 - handler adapter 的 build_endpoint_url/build_request_body/get_cli_extra_headers 增加 provider_type 参数,Codex 判断优先使用 provider_type 而非 URL 匹配 - 前端 getDefaultBodyRules API 支持 provider_type 参数,缓存 key 区分不同 provider 类型;Codex 路径判断同样优先使用 provider_type - ProviderDetailDrawer 将 mapping-preview 拆为独立加载,不阻塞首屏渲染 - PoolManagement 补全 KeyFormDialog 缺失的 endpoint/available-api-formats props - 固定类型 Provider 创建时自动填充 provider-scoped 默认 body_rules
This commit is contained in:
@@ -141,10 +141,13 @@ async def create_provider_endpoint(
|
||||
async def get_default_endpoint_body_rules(
|
||||
api_format: str,
|
||||
request: Request,
|
||||
provider_type: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
"""获取指定 endpoint signature 的默认 body_rules。"""
|
||||
adapter = AdminGetDefaultBodyRulesAdapter(api_format=api_format)
|
||||
adapter = AdminGetDefaultBodyRulesAdapter(
|
||||
api_format=api_format, provider_type=provider_type or None
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@@ -308,7 +311,7 @@ class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
|
||||
raise NotFoundException(f"Provider {self.provider_id} 不存在")
|
||||
|
||||
# 固定类型 Provider:禁止通过该接口新增 Endpoints(端点由模板自动创建并锁定)
|
||||
provider_type = getattr(provider, "provider_type", "custom")
|
||||
provider_type = getattr(provider, "provider_type", None) or "custom"
|
||||
if _is_fixed_provider(provider_type):
|
||||
raise InvalidRequestException("固定类型 Provider 不允许手动新增 Endpoint")
|
||||
|
||||
@@ -338,7 +341,12 @@ class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
|
||||
normalized_api_format = sig.key
|
||||
body_rules = self.endpoint_data.body_rules
|
||||
if body_rules is None:
|
||||
body_rules = get_default_body_rules_for_endpoint(normalized_api_format) or None
|
||||
body_rules = (
|
||||
get_default_body_rules_for_endpoint(
|
||||
normalized_api_format, provider_type=provider_type
|
||||
)
|
||||
or None
|
||||
)
|
||||
|
||||
new_endpoint = ProviderEndpoint(
|
||||
id=str(uuid.uuid4()),
|
||||
@@ -613,6 +621,7 @@ class AdminDeleteProviderEndpointAdapter(AdminApiAdapter):
|
||||
@dataclass
|
||||
class AdminGetDefaultBodyRulesAdapter(AdminApiAdapter):
|
||||
api_format: str
|
||||
provider_type: str | None = None
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
try:
|
||||
@@ -622,5 +631,7 @@ class AdminGetDefaultBodyRulesAdapter(AdminApiAdapter):
|
||||
|
||||
return {
|
||||
"api_format": normalized_api_format,
|
||||
"body_rules": get_default_body_rules_for_endpoint(normalized_api_format),
|
||||
"body_rules": get_default_body_rules_for_endpoint(
|
||||
normalized_api_format, provider_type=self.provider_type
|
||||
),
|
||||
}
|
||||
|
||||
@@ -489,11 +489,20 @@ class AdminCreateProviderAdapter(AdminApiAdapter):
|
||||
# 固定类型 Provider:自动创建并锁定预置 Endpoints(同一事务)
|
||||
template = _get_fixed_provider_template(provider.provider_type)
|
||||
if template:
|
||||
from src.core.api_format.metadata import get_default_body_rules_for_endpoint
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
for sig in template.endpoint_signatures:
|
||||
endpoint_config: dict[str, str] | None = None
|
||||
if provider.provider_type == ProviderType.CODEX.value and sig == "openai:cli":
|
||||
endpoint_config = {"upstream_stream_policy": "force_stream"}
|
||||
# 获取 provider-scoped 默认 body rules
|
||||
default_body_rules = (
|
||||
get_default_body_rules_for_endpoint(
|
||||
sig, provider_type=provider.provider_type
|
||||
)
|
||||
or None
|
||||
)
|
||||
endpoint = ProviderEndpoint(
|
||||
id=str(uuid.uuid4()),
|
||||
provider_id=provider.id,
|
||||
@@ -503,6 +512,7 @@ class AdminCreateProviderAdapter(AdminApiAdapter):
|
||||
base_url=template.api_base_url,
|
||||
custom_path=None,
|
||||
header_rules=None,
|
||||
body_rules=default_body_rules,
|
||||
max_retries=provider.max_retries or 2,
|
||||
is_active=True,
|
||||
config=endpoint_config,
|
||||
|
||||
@@ -309,11 +309,12 @@ class HandlerAdapterBase(ApiAdapter):
|
||||
request_data: dict[str, Any] | None = None,
|
||||
*,
|
||||
base_url: str | None = None,
|
||||
provider_type: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""构建测试请求体,使用转换器注册表自动处理格式转换"""
|
||||
from src.api.handlers.base.request_builder import build_test_request_body
|
||||
|
||||
_ = base_url
|
||||
_ = base_url, provider_type
|
||||
return build_test_request_body(cls.FORMAT_ID, request_data)
|
||||
|
||||
@classmethod
|
||||
@@ -408,10 +409,12 @@ class HandlerAdapterBase(ApiAdapter):
|
||||
decrypted_auth_config=effective_auth_config,
|
||||
)
|
||||
else:
|
||||
url = cls.build_endpoint_url(base_url, request_data, model_name)
|
||||
url = cls.build_endpoint_url(
|
||||
base_url, request_data, model_name, provider_type=provider_type
|
||||
)
|
||||
|
||||
# ---- Headers ----
|
||||
cli_extra = cls.get_cli_extra_headers(base_url=base_url)
|
||||
cli_extra = cls.get_cli_extra_headers(base_url=base_url, provider_type=provider_type)
|
||||
merged_extra = dict(extra_headers) if extra_headers else {}
|
||||
merged_extra.update(cli_extra)
|
||||
|
||||
@@ -458,7 +461,7 @@ class HandlerAdapterBase(ApiAdapter):
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
# ---- Body ----
|
||||
body = cls.build_request_body(request_data, base_url=base_url)
|
||||
body = cls.build_request_body(request_data, base_url=base_url, provider_type=provider_type)
|
||||
|
||||
if body_rules:
|
||||
body = apply_body_rules(body, body_rules)
|
||||
@@ -536,6 +539,8 @@ class HandlerAdapterBase(ApiAdapter):
|
||||
base_url: str,
|
||||
request_data: dict[str, Any] | None = None,
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建 API 端点 URL - 子类应覆盖"""
|
||||
return base_url
|
||||
@@ -546,7 +551,9 @@ class HandlerAdapterBase(ApiAdapter):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_cli_extra_headers(cls, *, base_url: str | None = None) -> dict[str, str]:
|
||||
def get_cli_extra_headers(
|
||||
cls, *, base_url: str | None = None, provider_type: str | None = None
|
||||
) -> dict[str, str]:
|
||||
"""获取额外请求头 - 子类可覆盖"""
|
||||
headers: dict[str, str] = {}
|
||||
cli_user_agent = cls.get_cli_user_agent()
|
||||
|
||||
@@ -304,6 +304,8 @@ class ClaudeChatAdapter(ChatAdapterBase):
|
||||
base_url: str,
|
||||
request_data: dict[str, Any] | None = None,
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建Claude API端点URL"""
|
||||
base_url = base_url.rstrip("/")
|
||||
|
||||
@@ -122,7 +122,12 @@ class ClaudeCliAdapter(CliAdapterBase):
|
||||
|
||||
@classmethod
|
||||
def build_endpoint_url(
|
||||
cls, base_url: str, request_data: dict[str, Any], model_name: str | None = None
|
||||
cls,
|
||||
base_url: str,
|
||||
request_data: dict[str, Any],
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建Claude CLI API端点URL"""
|
||||
base_url = base_url.rstrip("/")
|
||||
|
||||
@@ -264,6 +264,8 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
base_url: str,
|
||||
request_data: dict[str, Any] | None = None,
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建Gemini API端点URL"""
|
||||
base_url = base_url.rstrip("/")
|
||||
|
||||
@@ -144,7 +144,12 @@ class GeminiCliAdapter(CliAdapterBase):
|
||||
|
||||
@classmethod
|
||||
def build_endpoint_url(
|
||||
cls, base_url: str, request_data: dict[str, Any], model_name: str | None = None
|
||||
cls,
|
||||
base_url: str,
|
||||
request_data: dict[str, Any],
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建Gemini CLI API端点URL"""
|
||||
effective_model_name = model_name or request_data.get("model", "")
|
||||
@@ -166,9 +171,11 @@ class GeminiCliAdapter(CliAdapterBase):
|
||||
return config.internal_user_agent_gemini_cli
|
||||
|
||||
@classmethod
|
||||
def get_cli_extra_headers(cls, *, base_url: str | None = None) -> dict[str, str]:
|
||||
def get_cli_extra_headers(
|
||||
cls, *, base_url: str | None = None, provider_type: str | None = None
|
||||
) -> dict[str, str]:
|
||||
"""获取Gemini CLI额外请求头,包含 x-app: cli 标识"""
|
||||
headers = super().get_cli_extra_headers(base_url=base_url)
|
||||
headers = super().get_cli_extra_headers(base_url=base_url, provider_type=provider_type)
|
||||
headers["x-app"] = "cli" # 标识 CLI 模式,让上游使用正确的 adapter
|
||||
return headers
|
||||
|
||||
|
||||
@@ -153,6 +153,8 @@ class OpenAIChatAdapter(ChatAdapterBase):
|
||||
base_url: str,
|
||||
request_data: dict[str, Any] | None = None,
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建OpenAI API端点URL"""
|
||||
base_url = base_url.rstrip("/")
|
||||
|
||||
@@ -16,6 +16,7 @@ from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
|
||||
from src.api.handlers.openai.adapter import OpenAIChatAdapter
|
||||
from src.config.settings import config
|
||||
from src.core.api_format import ApiFamily, EndpointKind
|
||||
from src.core.provider_types import ProviderType
|
||||
from src.utils.url_utils import is_codex_url
|
||||
|
||||
|
||||
@@ -97,17 +98,26 @@ class OpenAICliAdapter(CliAdapterBase):
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
compact: bool = False,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建OpenAI CLI API端点URL(使用 Responses API)
|
||||
|
||||
对于 Codex OAuth 端点(如 chatgpt.com/backend-api/codex),直接追加 /responses;
|
||||
对于标准 OpenAI API,使用 /v1/responses。
|
||||
compact=True 时追加 /compact 后缀。
|
||||
|
||||
provider_type 优先:仅当 provider_type 为 codex 时才使用 Codex 路由规则;
|
||||
未传入 provider_type 时回退到 URL 模式匹配(兼容旧调用方)。
|
||||
"""
|
||||
suffix = "/responses/compact" if compact else "/responses"
|
||||
base_url = base_url.rstrip("/")
|
||||
# Codex OAuth 端点:chatgpt.com/backend-api/codex -> /responses[/compact]
|
||||
if is_codex_url(base_url):
|
||||
# 判断是否按 Codex 规则构建 URL
|
||||
is_codex = (
|
||||
(provider_type or "").lower() == ProviderType.CODEX
|
||||
if provider_type
|
||||
else is_codex_url(base_url)
|
||||
)
|
||||
if is_codex:
|
||||
return f"{base_url}{suffix}"
|
||||
# 标准 OpenAI API
|
||||
if base_url.endswith("/v1"):
|
||||
@@ -124,11 +134,21 @@ class OpenAICliAdapter(CliAdapterBase):
|
||||
request_data: dict[str, Any] | None = None,
|
||||
*,
|
||||
base_url: str | None = None,
|
||||
provider_type: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""构建测试请求体(Codex 端点需要强制 stream=true 等特性)"""
|
||||
"""构建测试请求体(Codex 端点需要强制 stream=true 等特性)
|
||||
|
||||
provider_type 优先:仅当 provider_type 为 codex 时才应用 Codex 变体;
|
||||
未传入 provider_type 时回退到 URL 模式匹配(兼容旧调用方)。
|
||||
"""
|
||||
from src.api.handlers.base.request_builder import build_test_request_body
|
||||
|
||||
target_variant = "codex" if base_url and is_codex_url(base_url) else None
|
||||
is_codex = (
|
||||
(provider_type or "").lower() == ProviderType.CODEX
|
||||
if provider_type
|
||||
else (bool(base_url) and is_codex_url(base_url))
|
||||
)
|
||||
target_variant = "codex" if is_codex else None
|
||||
return build_test_request_body(
|
||||
cls.FORMAT_ID,
|
||||
request_data,
|
||||
@@ -141,12 +161,17 @@ class OpenAICliAdapter(CliAdapterBase):
|
||||
return config.internal_user_agent_openai_cli
|
||||
|
||||
@classmethod
|
||||
def get_cli_extra_headers(cls, *, base_url: str | None = None) -> dict[str, str]:
|
||||
def get_cli_extra_headers(
|
||||
cls, *, base_url: str | None = None, provider_type: str | None = None
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
获取额外请求头
|
||||
|
||||
对于 Codex OAuth 端点,添加特定头部(缺少可能导致 Cloudflare 拦截)。
|
||||
对于标准 OpenAI API 端点,仅添加 User-Agent。
|
||||
|
||||
provider_type 优先:仅当 provider_type 为 codex 时才添加 Codex 头部;
|
||||
未传入 provider_type 时回退到 URL 模式匹配(兼容旧调用方)。
|
||||
"""
|
||||
headers: dict[str, str] = {}
|
||||
|
||||
@@ -156,7 +181,12 @@ class OpenAICliAdapter(CliAdapterBase):
|
||||
headers["User-Agent"] = cli_user_agent
|
||||
|
||||
# 仅 Codex 端点添加特定头部
|
||||
if base_url and is_codex_url(base_url):
|
||||
is_codex = (
|
||||
(provider_type or "").lower() == ProviderType.CODEX
|
||||
if provider_type
|
||||
else (bool(base_url) and is_codex_url(base_url))
|
||||
)
|
||||
if is_codex:
|
||||
# 与运行时路径保持一致:使用 Codex envelope 的 best-effort headers。
|
||||
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||
|
||||
|
||||
Reference in New Issue
Block a user