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:
fawney19
2026-03-05 19:04:21 +08:00
parent 694167f78f
commit da915208a8
15 changed files with 214 additions and 54 deletions

View File

@@ -72,7 +72,9 @@ export async function deleteEndpoint(endpointId: string): Promise<{ message: str
/** /**
* 获取指定 API 格式的默认请求体规则 * 获取指定 API 格式的默认请求体规则
*/ */
export async function getDefaultBodyRules(apiFormat: string): Promise<{ api_format: string; body_rules: BodyRule[] }> { export async function getDefaultBodyRules(apiFormat: string, providerType?: string): Promise<{ api_format: string; body_rules: BodyRule[] }> {
const response = await client.get(`/api/admin/endpoints/defaults/${encodeURIComponent(apiFormat)}/body-rules`) const params: Record<string, string> = {}
if (providerType) params.provider_type = providerType
const response = await client.get(`/api/admin/endpoints/defaults/${encodeURIComponent(apiFormat)}/body-rules`, { params })
return response.data return response.data
} }

View File

@@ -1280,30 +1280,30 @@ const deleteConfirmDescription = computed(() => {
async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> { async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> {
if (!apiFormat) return [] if (!apiFormat) return []
if (!force && defaultBodyRulesLoaded.value[apiFormat]) { const providerType = (props.provider?.provider_type || '').toLowerCase()
return defaultBodyRulesByFormat.value[apiFormat] || [] // 缓存 key 需要包含 provider_type不同类型的 provider 有不同的默认规则
const cacheKey = providerType ? `${apiFormat}:${providerType}` : apiFormat
if (!force && defaultBodyRulesLoaded.value[cacheKey]) {
return defaultBodyRulesByFormat.value[cacheKey] || []
} }
if (loadingDefaultBodyRulesByFormat.value[apiFormat]) { if (loadingDefaultBodyRulesByFormat.value[cacheKey]) {
return defaultBodyRulesByFormat.value[apiFormat] || [] return defaultBodyRulesByFormat.value[cacheKey] || []
} }
loadingDefaultBodyRulesByFormat.value[apiFormat] = true loadingDefaultBodyRulesByFormat.value[cacheKey] = true
try { try {
const response = await getDefaultBodyRules(apiFormat) const response = await getDefaultBodyRules(apiFormat, providerType || undefined)
const normalized = response.api_format || apiFormat
const rules = response.body_rules || [] const rules = response.body_rules || []
defaultBodyRulesByFormat.value[normalized] = rules defaultBodyRulesByFormat.value[cacheKey] = rules
defaultBodyRulesByFormat.value[apiFormat] = rules defaultBodyRulesLoaded.value[cacheKey] = true
defaultBodyRulesLoaded.value[normalized] = true
defaultBodyRulesLoaded.value[apiFormat] = true
return rules return rules
} catch (error: unknown) { } catch (error: unknown) {
defaultBodyRulesByFormat.value[apiFormat] = [] defaultBodyRulesByFormat.value[cacheKey] = []
defaultBodyRulesLoaded.value[apiFormat] = true defaultBodyRulesLoaded.value[cacheKey] = true
log.warn('加载默认请求体规则失败', apiFormat, error) log.warn('加载默认请求体规则失败', apiFormat, error)
return [] return []
} finally { } finally {
loadingDefaultBodyRulesByFormat.value[apiFormat] = false loadingDefaultBodyRulesByFormat.value[cacheKey] = false
} }
} }
@@ -1317,7 +1317,11 @@ function getDefaultPath(apiFormat: string, baseUrl?: string): string {
const format = apiFormats.value.find(f => f.value === apiFormat) const format = apiFormats.value.find(f => f.value === apiFormat)
const defaultPath = format?.default_path || '' const defaultPath = format?.default_path || ''
// Codex 端点使用 /responses 而非 /v1/responses // Codex 端点使用 /responses 而非 /v1/responses
if (apiFormat === 'openai:cli' && baseUrl && isCodexUrl(baseUrl)) { const providerType = (props.provider?.provider_type || '').toLowerCase()
const isCodex = providerType
? providerType === 'codex'
: (!!baseUrl && isCodexUrl(baseUrl))
if (apiFormat === 'openai:cli' && isCodex) {
return '/responses' return '/responses'
} }
return defaultPath return defaultPath

View File

@@ -1274,6 +1274,8 @@ watch(
[() => props.providerId, () => props.open], [() => props.providerId, () => props.open],
async ([newId, newOpen], [_oldId, oldOpen]) => { async ([newId, newOpen], [_oldId, oldOpen]) => {
if (newOpen && newId) { if (newOpen && newId) {
// mapping-preview 较慢,不阻塞首屏渲染
void loadMappingPreview()
await Promise.all([ await Promise.all([
loadProvider(), loadProvider(),
loadEndpoints(), loadEndpoints(),
@@ -1958,6 +1960,7 @@ async function handleBatchAssignChanged() {
// 处理模型映射变更 // 处理模型映射变更
async function handleModelMappingChanged() { async function handleModelMappingChanged() {
void loadMappingPreview()
await loadEndpoints() await loadEndpoints()
emit('refresh') emit('refresh')
} }
@@ -2621,18 +2624,17 @@ async function loadEndpoints() {
const requestId = ++endpointsLoadRequestId const requestId = ++endpointsLoadRequestId
try { try {
// 并行加载端点列表、Provider 级别的 keysmodels 和映射预览 // 并行加载端点列表、Provider 级别的 keysmodels
const [endpointsList, providerKeysResult, modelsResult, mappingPreviewResult] = await Promise.all([ // mapping-preview 较慢,拆到 loadMappingPreview 独立加载,不阻塞首屏
const [endpointsList, providerKeysResult, modelsResult] = await Promise.all([
getProviderEndpoints(props.providerId), getProviderEndpoints(props.providerId),
getProviderKeys(props.providerId).catch(() => []), getProviderKeys(props.providerId).catch(() => []),
getProviderModels(props.providerId).catch(() => []), getProviderModels(props.providerId).catch(() => []),
getProviderMappingPreview(props.providerId).catch(() => null),
]) ])
if (requestId !== endpointsLoadRequestId) return if (requestId !== endpointsLoadRequestId) return
providerKeys.value = providerKeysResult providerKeys.value = providerKeysResult
providerModels.value = modelsResult providerModels.value = modelsResult
providerMappingPreview.value = mappingPreviewResult
// 按 API 格式排序 // 按 API 格式排序
endpoints.value = endpointsList.sort((a, b) => { endpoints.value = endpointsList.sort((a, b) => {
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format) const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
@@ -2648,6 +2650,16 @@ async function loadEndpoints() {
} }
} }
// 加载映射预览(独立于 loadEndpoints不阻塞首屏渲染
async function loadMappingPreview() {
if (!props.providerId) return
try {
providerMappingPreview.value = await getProviderMappingPreview(props.providerId)
} catch {
providerMappingPreview.value = null
}
}
// 添加 ESC 键监听 // 添加 ESC 键监听
useEscapeKey(() => { useEscapeKey(() => {
if (props.open) { if (props.open) {

View File

@@ -1083,9 +1083,11 @@
<KeyFormDialog <KeyFormDialog
v-if="selectedProviderId" v-if="selectedProviderId"
:open="keyFormDialogOpen" :open="keyFormDialogOpen"
:endpoint="null"
:provider-type="selectedProviderData?.provider_type || selectedProviderType" :provider-type="selectedProviderData?.provider_type || selectedProviderType"
:editing-key="editingKey" :editing-key="editingKey"
:provider-id="selectedProviderId" :provider-id="selectedProviderId"
:available-api-formats="selectedProviderData?.api_formats || []"
@close="closeKeyFormDialog" @close="closeKeyFormDialog"
@saved="handleDialogSaved" @saved="handleDialogSaved"
/> />

View File

@@ -141,10 +141,13 @@ async def create_provider_endpoint(
async def get_default_endpoint_body_rules( async def get_default_endpoint_body_rules(
api_format: str, api_format: str,
request: Request, request: Request,
provider_type: str | None = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> dict[str, Any]: ) -> dict[str, Any]:
"""获取指定 endpoint signature 的默认 body_rules。""" """获取指定 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) 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} 不存在") raise NotFoundException(f"Provider {self.provider_id} 不存在")
# 固定类型 Provider禁止通过该接口新增 Endpoints端点由模板自动创建并锁定 # 固定类型 Provider禁止通过该接口新增 Endpoints端点由模板自动创建并锁定
provider_type = getattr(provider, "provider_type", "custom") provider_type = getattr(provider, "provider_type", None) or "custom"
if _is_fixed_provider(provider_type): if _is_fixed_provider(provider_type):
raise InvalidRequestException("固定类型 Provider 不允许手动新增 Endpoint") raise InvalidRequestException("固定类型 Provider 不允许手动新增 Endpoint")
@@ -338,7 +341,12 @@ class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
normalized_api_format = sig.key normalized_api_format = sig.key
body_rules = self.endpoint_data.body_rules body_rules = self.endpoint_data.body_rules
if body_rules is None: 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( new_endpoint = ProviderEndpoint(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@@ -613,6 +621,7 @@ class AdminDeleteProviderEndpointAdapter(AdminApiAdapter):
@dataclass @dataclass
class AdminGetDefaultBodyRulesAdapter(AdminApiAdapter): class AdminGetDefaultBodyRulesAdapter(AdminApiAdapter):
api_format: str api_format: str
provider_type: str | None = None
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override] async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
try: try:
@@ -622,5 +631,7 @@ class AdminGetDefaultBodyRulesAdapter(AdminApiAdapter):
return { return {
"api_format": normalized_api_format, "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
),
} }

View File

@@ -489,11 +489,20 @@ class AdminCreateProviderAdapter(AdminApiAdapter):
# 固定类型 Provider自动创建并锁定预置 Endpoints同一事务 # 固定类型 Provider自动创建并锁定预置 Endpoints同一事务
template = _get_fixed_provider_template(provider.provider_type) template = _get_fixed_provider_template(provider.provider_type)
if template: if template:
from src.core.api_format.metadata import get_default_body_rules_for_endpoint
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
for sig in template.endpoint_signatures: for sig in template.endpoint_signatures:
endpoint_config: dict[str, str] | None = None endpoint_config: dict[str, str] | None = None
if provider.provider_type == ProviderType.CODEX.value and sig == "openai:cli": if provider.provider_type == ProviderType.CODEX.value and sig == "openai:cli":
endpoint_config = {"upstream_stream_policy": "force_stream"} 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( endpoint = ProviderEndpoint(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
provider_id=provider.id, provider_id=provider.id,
@@ -503,6 +512,7 @@ class AdminCreateProviderAdapter(AdminApiAdapter):
base_url=template.api_base_url, base_url=template.api_base_url,
custom_path=None, custom_path=None,
header_rules=None, header_rules=None,
body_rules=default_body_rules,
max_retries=provider.max_retries or 2, max_retries=provider.max_retries or 2,
is_active=True, is_active=True,
config=endpoint_config, config=endpoint_config,

View File

@@ -309,11 +309,12 @@ class HandlerAdapterBase(ApiAdapter):
request_data: dict[str, Any] | None = None, request_data: dict[str, Any] | None = None,
*, *,
base_url: str | None = None, base_url: str | None = None,
provider_type: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""构建测试请求体,使用转换器注册表自动处理格式转换""" """构建测试请求体,使用转换器注册表自动处理格式转换"""
from src.api.handlers.base.request_builder import build_test_request_body 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) return build_test_request_body(cls.FORMAT_ID, request_data)
@classmethod @classmethod
@@ -408,10 +409,12 @@ class HandlerAdapterBase(ApiAdapter):
decrypted_auth_config=effective_auth_config, decrypted_auth_config=effective_auth_config,
) )
else: 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 ---- # ---- 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 = dict(extra_headers) if extra_headers else {}
merged_extra.update(cli_extra) merged_extra.update(cli_extra)
@@ -458,7 +461,7 @@ class HandlerAdapterBase(ApiAdapter):
headers["Authorization"] = f"Bearer {api_key}" headers["Authorization"] = f"Bearer {api_key}"
# ---- Body ---- # ---- 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: if body_rules:
body = apply_body_rules(body, body_rules) body = apply_body_rules(body, body_rules)
@@ -536,6 +539,8 @@ class HandlerAdapterBase(ApiAdapter):
base_url: str, base_url: str,
request_data: dict[str, Any] | None = None, request_data: dict[str, Any] | None = None,
model_name: str | None = None, model_name: str | None = None,
*,
provider_type: str | None = None,
) -> str: ) -> str:
"""构建 API 端点 URL - 子类应覆盖""" """构建 API 端点 URL - 子类应覆盖"""
return base_url return base_url
@@ -546,7 +551,9 @@ class HandlerAdapterBase(ApiAdapter):
return None return None
@classmethod @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] = {} headers: dict[str, str] = {}
cli_user_agent = cls.get_cli_user_agent() cli_user_agent = cls.get_cli_user_agent()

View File

@@ -304,6 +304,8 @@ class ClaudeChatAdapter(ChatAdapterBase):
base_url: str, base_url: str,
request_data: dict[str, Any] | None = None, request_data: dict[str, Any] | None = None,
model_name: str | None = None, model_name: str | None = None,
*,
provider_type: str | None = None,
) -> str: ) -> str:
"""构建Claude API端点URL""" """构建Claude API端点URL"""
base_url = base_url.rstrip("/") base_url = base_url.rstrip("/")

View File

@@ -122,7 +122,12 @@ class ClaudeCliAdapter(CliAdapterBase):
@classmethod @classmethod
def build_endpoint_url( 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: ) -> str:
"""构建Claude CLI API端点URL""" """构建Claude CLI API端点URL"""
base_url = base_url.rstrip("/") base_url = base_url.rstrip("/")

View File

@@ -264,6 +264,8 @@ class GeminiChatAdapter(ChatAdapterBase):
base_url: str, base_url: str,
request_data: dict[str, Any] | None = None, request_data: dict[str, Any] | None = None,
model_name: str | None = None, model_name: str | None = None,
*,
provider_type: str | None = None,
) -> str: ) -> str:
"""构建Gemini API端点URL""" """构建Gemini API端点URL"""
base_url = base_url.rstrip("/") base_url = base_url.rstrip("/")

View File

@@ -144,7 +144,12 @@ class GeminiCliAdapter(CliAdapterBase):
@classmethod @classmethod
def build_endpoint_url( 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: ) -> str:
"""构建Gemini CLI API端点URL""" """构建Gemini CLI API端点URL"""
effective_model_name = model_name or request_data.get("model", "") effective_model_name = model_name or request_data.get("model", "")
@@ -166,9 +171,11 @@ class GeminiCliAdapter(CliAdapterBase):
return config.internal_user_agent_gemini_cli return config.internal_user_agent_gemini_cli
@classmethod @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 标识""" """获取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 headers["x-app"] = "cli" # 标识 CLI 模式,让上游使用正确的 adapter
return headers return headers

View File

@@ -153,6 +153,8 @@ class OpenAIChatAdapter(ChatAdapterBase):
base_url: str, base_url: str,
request_data: dict[str, Any] | None = None, request_data: dict[str, Any] | None = None,
model_name: str | None = None, model_name: str | None = None,
*,
provider_type: str | None = None,
) -> str: ) -> str:
"""构建OpenAI API端点URL""" """构建OpenAI API端点URL"""
base_url = base_url.rstrip("/") base_url = base_url.rstrip("/")

View File

@@ -16,6 +16,7 @@ from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
from src.api.handlers.openai.adapter import OpenAIChatAdapter from src.api.handlers.openai.adapter import OpenAIChatAdapter
from src.config.settings import config from src.config.settings import config
from src.core.api_format import ApiFamily, EndpointKind from src.core.api_format import ApiFamily, EndpointKind
from src.core.provider_types import ProviderType
from src.utils.url_utils import is_codex_url from src.utils.url_utils import is_codex_url
@@ -97,17 +98,26 @@ class OpenAICliAdapter(CliAdapterBase):
model_name: str | None = None, model_name: str | None = None,
*, *,
compact: bool = False, compact: bool = False,
provider_type: str | None = None,
) -> str: ) -> str:
"""构建OpenAI CLI API端点URL使用 Responses API """构建OpenAI CLI API端点URL使用 Responses API
对于 Codex OAuth 端点(如 chatgpt.com/backend-api/codex直接追加 /responses 对于 Codex OAuth 端点(如 chatgpt.com/backend-api/codex直接追加 /responses
对于标准 OpenAI API使用 /v1/responses。 对于标准 OpenAI API使用 /v1/responses。
compact=True 时追加 /compact 后缀。 compact=True 时追加 /compact 后缀。
provider_type 优先:仅当 provider_type 为 codex 时才使用 Codex 路由规则;
未传入 provider_type 时回退到 URL 模式匹配(兼容旧调用方)。
""" """
suffix = "/responses/compact" if compact else "/responses" suffix = "/responses/compact" if compact else "/responses"
base_url = base_url.rstrip("/") base_url = base_url.rstrip("/")
# Codex OAuth 端点chatgpt.com/backend-api/codex -> /responses[/compact] # 判断是否按 Codex 规则构建 URL
if is_codex_url(base_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}" return f"{base_url}{suffix}"
# 标准 OpenAI API # 标准 OpenAI API
if base_url.endswith("/v1"): if base_url.endswith("/v1"):
@@ -124,11 +134,21 @@ class OpenAICliAdapter(CliAdapterBase):
request_data: dict[str, Any] | None = None, request_data: dict[str, Any] | None = None,
*, *,
base_url: str | None = None, base_url: str | None = None,
provider_type: str | None = None,
) -> dict[str, Any]: ) -> 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 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( return build_test_request_body(
cls.FORMAT_ID, cls.FORMAT_ID,
request_data, request_data,
@@ -141,12 +161,17 @@ class OpenAICliAdapter(CliAdapterBase):
return config.internal_user_agent_openai_cli return config.internal_user_agent_openai_cli
@classmethod @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 拦截)。 对于 Codex OAuth 端点,添加特定头部(缺少可能导致 Cloudflare 拦截)。
对于标准 OpenAI API 端点,仅添加 User-Agent。 对于标准 OpenAI API 端点,仅添加 User-Agent。
provider_type 优先:仅当 provider_type 为 codex 时才添加 Codex 头部;
未传入 provider_type 时回退到 URL 模式匹配(兼容旧调用方)。
""" """
headers: dict[str, str] = {} headers: dict[str, str] = {}
@@ -156,7 +181,12 @@ class OpenAICliAdapter(CliAdapterBase):
headers["User-Agent"] = cli_user_agent headers["User-Agent"] = cli_user_agent
# 仅 Codex 端点添加特定头部 # 仅 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。 # 与运行时路径保持一致:使用 Codex envelope 的 best-effort headers。
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope from src.services.provider.adapters.codex.envelope import codex_oauth_envelope

View File

@@ -68,19 +68,6 @@ class EndpointDefinition:
yield value yield value
_CODEX_DEFAULT_BODY_RULES: tuple[dict[str, Any], ...] = (
{"action": "drop", "path": "max_output_tokens"},
{"action": "drop", "path": "temperature"},
{"action": "drop", "path": "top_p"},
{"action": "set", "path": "store", "value": False},
{
"action": "set",
"path": "instructions",
"value": "You are GPT-5.",
"condition": {"path": "instructions", "op": "not_exists"},
},
)
_ENDPOINT_DEFINITIONS: dict[tuple[ApiFamily, EndpointKind], EndpointDefinition] = { _ENDPOINT_DEFINITIONS: dict[tuple[ApiFamily, EndpointKind], EndpointDefinition] = {
# Claude # Claude
(ApiFamily.CLAUDE, EndpointKind.CHAT): EndpointDefinition( (ApiFamily.CLAUDE, EndpointKind.CHAT): EndpointDefinition(
@@ -138,7 +125,6 @@ _ENDPOINT_DEFINITIONS: dict[tuple[ApiFamily, EndpointKind], EndpointDefinition]
auth_type="bearer", auth_type="bearer",
protected_keys=frozenset({"authorization", "content-type"}), protected_keys=frozenset({"authorization", "content-type"}),
data_format_id="openai_responses", data_format_id="openai_responses",
default_body_rules=_CODEX_DEFAULT_BODY_RULES,
), ),
(ApiFamily.OPENAI, EndpointKind.COMPACT): EndpointDefinition( (ApiFamily.OPENAI, EndpointKind.COMPACT): EndpointDefinition(
api_family=ApiFamily.OPENAI, api_family=ApiFamily.OPENAI,
@@ -311,13 +297,73 @@ def get_data_format_id_for_endpoint(
def get_default_body_rules_for_endpoint( def get_default_body_rules_for_endpoint(
value: str | EndpointSignature | tuple[ApiFamily, EndpointKind], value: str | EndpointSignature | tuple[ApiFamily, EndpointKind],
*,
provider_type: str | None = None,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""获取端点的默认 body_rules。
优先查找 provider_type 维度的注册规则(如 Codex 对 openai:cli 的定制规则),
找不到时回退到 EndpointDefinition 上的通用默认规则。
"""
# 确保 provider plugins 已注册(填充 _provider_default_body_rules
# ensure_providers_bootstrapped 是幂等的,重复调用无副作用
if provider_type:
try:
from src.services.provider.envelope import ensure_providers_bootstrapped
ensure_providers_bootstrapped()
except Exception:
pass
# 1) provider_type 维度的注册规则优先
if provider_type:
pt = provider_type.strip().lower()
sig = _normalize_sig_key(value)
provider_rules = _provider_default_body_rules.get((pt, sig))
if provider_rules is not None:
return deepcopy(list(provider_rules))
# 2) 回退到 EndpointDefinition 上的通用默认规则
definition = resolve_endpoint_definition(value) definition = resolve_endpoint_definition(value)
if not definition or not definition.default_body_rules: if not definition or not definition.default_body_rules:
return [] return []
return deepcopy(list(definition.default_body_rules)) return deepcopy(list(definition.default_body_rules))
# ---------------------------------------------------------------------------
# Provider-scoped default body rules registry
# ---------------------------------------------------------------------------
# key: (provider_type, endpoint_sig_key) e.g. ("codex", "openai:cli")
_provider_default_body_rules: dict[tuple[str, str], Sequence[dict[str, Any]]] = {}
def register_provider_default_body_rules(
provider_type: str,
endpoint_sig: str,
rules: Sequence[dict[str, Any]],
) -> None:
"""注册特定 provider_type + endpoint_sig 的默认 body_rules。"""
pt = provider_type.strip().lower()
sig = _normalize_sig_key(endpoint_sig)
_provider_default_body_rules[(pt, sig)] = tuple(rules)
def _normalize_sig_key(
value: str | EndpointSignature | tuple[ApiFamily, EndpointKind],
) -> str:
"""将各种端点标识形式归一化为 signature key 字符串。"""
if isinstance(value, str):
try:
return parse_signature_key(value).key
except Exception:
return value.strip().lower()
if isinstance(value, EndpointSignature):
return value.key
if isinstance(value, tuple) and len(value) == 2:
return make_signature_key(value[0], value[1])
return str(value).strip().lower()
def can_passthrough_endpoint( def can_passthrough_endpoint(
client: str | EndpointSignature | tuple[ApiFamily, EndpointKind], client: str | EndpointSignature | tuple[ApiFamily, EndpointKind],
provider: str | EndpointSignature | tuple[ApiFamily, EndpointKind], provider: str | EndpointSignature | tuple[ApiFamily, EndpointKind],

View File

@@ -176,6 +176,24 @@ def register_all() -> None:
# Behavior # Behavior
register_behavior_variant("codex", same_format=True, cross_format=True) register_behavior_variant("codex", same_format=True, cross_format=True)
# Default Body Rules (Codex-specific, not format-wide)
from src.core.api_format.metadata import register_provider_default_body_rules
_codex_body_rules = (
{"action": "drop", "path": "max_output_tokens"},
{"action": "drop", "path": "temperature"},
{"action": "drop", "path": "top_p"},
{"action": "set", "path": "store", "value": False},
{
"action": "set",
"path": "instructions",
"value": "You are GPT-5.",
"condition": {"path": "instructions", "op": "not_exists"},
},
)
register_provider_default_body_rules("codex", "openai:cli", _codex_body_rules)
register_provider_default_body_rules("codex", "openai:compact", _codex_body_rules)
# Export: Codex uses the default export builder (strip null + temp fields) # Export: Codex uses the default export builder (strip null + temp fields)
# No need to register a custom one — the default in export.py suffices. # No need to register a custom one — the default in export.py suffices.