mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10: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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 级别的 keys、models 和映射预览
|
// 并行加载端点列表、Provider 级别的 keys 和 models
|
||||||
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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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("/")
|
||||||
|
|||||||
@@ -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("/")
|
||||||
|
|||||||
@@ -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("/")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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("/")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user