feat(endpoint): 端点默认 body_rules 机制与 Codex 规则回填

- EndpointDefinition 新增 default_body_rules 字段,openai:cli/compact 配置 Codex 默认规则
- 创建端点时若未指定 body_rules 则自动填充对应格式的默认值
- 新增 GET /defaults/{api_format}/body-rules 接口查询默认规则
- 前端 EndpointFormDialog 增加"重置请求体"按钮,支持一键恢复默认
- Alembic 迁移回填已有 Codex 端点的默认 body_rules
- 新增 metadata 和 endpoint 创建默认值的单元测试
This commit is contained in:
fawney19
2026-03-02 22:08:39 +08:00
parent 3384c6d666
commit 0bff15f964
8 changed files with 392 additions and 1 deletions

View File

@@ -0,0 +1,69 @@
"""backfill_codex_default_body_rules
Backfill default body_rules for openai:cli and openai:compact endpoints
that currently have body_rules IS NULL.
Rules:
- drop max_output_tokens
- drop temperature
- drop top_p
- set store = false
- set instructions = "You are GPT-5." (when instructions not exists)
Revision ID: dd0278c0a28c
Revises: 1d2e3f4a5b6c
Create Date: 2026-03-02 15:00:00.000000+00:00
"""
from __future__ import annotations
import json
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "dd0278c0a28c"
down_revision = "1d2e3f4a5b6c"
branch_labels = None
depends_on = None
_TARGET_FORMATS = ("openai:cli", "openai:compact")
_DEFAULT_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"},
},
]
def upgrade() -> None:
conn = op.get_bind()
# 幂等性: 仅回填 body_rules 为空SQL NULL 或 JSON null的记录
rules_json = json.dumps(_DEFAULT_BODY_RULES, ensure_ascii=False)
result = conn.execute(
sa.text("""
UPDATE provider_endpoints
SET body_rules = CAST(:rules AS json),
updated_at = CURRENT_TIMESTAMP
WHERE api_format IN (:fmt1, :fmt2)
AND (body_rules IS NULL OR body_rules::text = 'null')
"""),
{"rules": rules_json, "fmt1": _TARGET_FORMATS[0], "fmt2": _TARGET_FORMATS[1]},
)
if result.rowcount:
print(f" backfilled body_rules for {result.rowcount} endpoint(s)")
def downgrade() -> None:
# Data backfill: no-op to avoid removing user-customized rules.
return

View File

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

View File

@@ -204,6 +204,18 @@
<Plus class="w-3 h-3 mr-1" /> <Plus class="w-3 h-3 mr-1" />
请求体 请求体
</Button> </Button>
<Button
v-if="isFixedProvider"
variant="ghost"
size="sm"
class="h-7 text-xs px-2"
title="重置请求体"
:disabled="resettingDefaultRulesEndpointId === endpoint.id"
@click="handleResetBodyRulesToDefault(endpoint)"
>
<RotateCcw class="w-3 h-3 mr-1" />
重置请求体
</Button>
</div> </div>
</div> </div>
<CollapsibleContent class="pt-3"> <CollapsibleContent class="pt-3">
@@ -829,6 +841,7 @@ import { log } from '@/utils/logger'
import AlertDialog from '@/components/common/AlertDialog.vue' import AlertDialog from '@/components/common/AlertDialog.vue'
import { import {
createEndpoint, createEndpoint,
getDefaultBodyRules,
updateEndpoint, updateEndpoint,
deleteEndpoint, deleteEndpoint,
type ProviderEndpoint, type ProviderEndpoint,
@@ -1084,6 +1097,7 @@ function handleBodyRuleDragEnd(endpointId: string) {
// 状态 // 状态
const addingEndpoint = ref(false) const addingEndpoint = ref(false)
const savingEndpointId = ref<string | null>(null) const savingEndpointId = ref<string | null>(null)
const resettingDefaultRulesEndpointId = ref<string | null>(null)
const deletingEndpointId = ref<string | null>(null) const deletingEndpointId = ref<string | null>(null)
const togglingEndpointId = ref<string | null>(null) const togglingEndpointId = ref<string | null>(null)
const togglingFormatEndpointId = ref<string | null>(null) const togglingFormatEndpointId = ref<string | null>(null)
@@ -1114,6 +1128,9 @@ function setBodyRuleHelpOpen(endpointId: string, open: boolean) {
// 每个端点的编辑状态(内联编辑) // 每个端点的编辑状态(内联编辑)
const endpointEditStates = ref<Record<string, EndpointEditState>>({}) const endpointEditStates = ref<Record<string, EndpointEditState>>({})
const defaultBodyRulesByFormat = ref<Record<string, BodyRule[]>>({})
const defaultBodyRulesLoaded = ref<Record<string, boolean>>({})
const loadingDefaultBodyRulesByFormat = ref<Record<string, boolean>>({})
// 系统保留的 header 名称(不允许用户设置) // 系统保留的 header 名称(不允许用户设置)
const RESERVED_HEADERS = new Set([ const RESERVED_HEADERS = new Set([
@@ -1261,6 +1278,40 @@ const deleteConfirmDescription = computed(() => {
return `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。` return `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。`
}) })
async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> {
if (!apiFormat) return []
if (!force && defaultBodyRulesLoaded.value[apiFormat]) {
return defaultBodyRulesByFormat.value[apiFormat] || []
}
if (loadingDefaultBodyRulesByFormat.value[apiFormat]) {
return defaultBodyRulesByFormat.value[apiFormat] || []
}
loadingDefaultBodyRulesByFormat.value[apiFormat] = true
try {
const response = await getDefaultBodyRules(apiFormat)
const normalized = response.api_format || apiFormat
const rules = response.body_rules || []
defaultBodyRulesByFormat.value[normalized] = rules
defaultBodyRulesByFormat.value[apiFormat] = rules
defaultBodyRulesLoaded.value[normalized] = true
defaultBodyRulesLoaded.value[apiFormat] = true
return rules
} catch (error: unknown) {
defaultBodyRulesByFormat.value[apiFormat] = []
defaultBodyRulesLoaded.value[apiFormat] = true
log.warn('加载默认请求体规则失败', apiFormat, error)
return []
} finally {
loadingDefaultBodyRulesByFormat.value[apiFormat] = false
}
}
async function preloadDefaultBodyRules(endpoints: ProviderEndpoint[]): Promise<void> {
const formats = Array.from(new Set(endpoints.map(e => e.api_format).filter(Boolean)))
await Promise.all(formats.map(fmt => loadDefaultBodyRulesForFormat(fmt)))
}
// 获取指定 API 格式的默认路径 // 获取指定 API 格式的默认路径
function getDefaultPath(apiFormat: string, baseUrl?: string): string { 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)
@@ -2052,6 +2103,37 @@ function resetEndpointChanges(endpoint: ProviderEndpoint) {
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint) endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
} }
async function handleResetBodyRulesToDefault(endpoint: ProviderEndpoint) {
resettingDefaultRulesEndpointId.value = endpoint.id
try {
const defaultRules = await loadDefaultBodyRulesForFormat(endpoint.api_format, true)
if (!defaultRules.length) {
showError('该端点没有默认请求体规则')
return
}
if (!endpointEditStates.value[endpoint.id]) {
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
}
const state = endpointEditStates.value[endpoint.id]
if (!state) return
const resetState = initEndpointEditState({
...endpoint,
body_rules: defaultRules,
})
state.bodyRules = resetState.bodyRules
endpointRulesExpanded.value[endpoint.id] = (state.rules.length + state.bodyRules.length) > 0
clearBodyRuleDragState(endpoint.id)
clearBodyRuleSelectOpen(endpoint.id)
success('已重置请求体为默认规则请点击保存生效')
} catch (error: unknown) {
showError(parseApiError(error, '重置失败'), '错误')
} finally {
resettingDefaultRulesEndpointId.value = null
}
}
// 将可编辑规则数组转换为 API 需要的 HeaderRule[] // 将可编辑规则数组转换为 API 需要的 HeaderRule[]
function rulesToHeaderRules(rules: EditableRule[]): HeaderRule[] | null { function rulesToHeaderRules(rules: EditableRule[]): HeaderRule[] | null {
const result: HeaderRule[] = [] const result: HeaderRule[] = []
@@ -2126,6 +2208,7 @@ watch(() => props.modelValue, (open) => {
const hasRules = (endpoint.header_rules?.length || 0) + (endpoint.body_rules?.length || 0) > 0 const hasRules = (endpoint.header_rules?.length || 0) + (endpoint.body_rules?.length || 0) > 0
endpointRulesExpanded.value[endpoint.id] = hasRules endpointRulesExpanded.value[endpoint.id] = hasRules
} }
void preloadDefaultBodyRules(localEndpoints.value)
} else { } else {
// 关闭对话框时完全清空新端点表单 // 关闭对话框时完全清空新端点表单
newEndpoint.value = { api_format: '', base_url: '', custom_path: '' } newEndpoint.value = { api_format: '', base_url: '', custom_path: '' }
@@ -2141,6 +2224,12 @@ watch(() => props.endpoints, (endpoints) => {
endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint) endpointEditStates.value[endpoint.id] = initEndpointEditState(endpoint)
} }
} }
const newFormats = localEndpoints.value
.filter(e => e.api_format && !defaultBodyRulesLoaded.value[e.api_format])
.map(e => ({ api_format: e.api_format }) as ProviderEndpoint)
if (newFormats.length) {
void preloadDefaultBodyRules(newFormats)
}
} }
}, { deep: true }) }, { deep: true })

View File

@@ -18,6 +18,7 @@ from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext from src.api.base.context import ApiRequestContext
from src.api.base.models_service import invalidate_models_list_cache from src.api.base.models_service import invalidate_models_list_cache
from src.api.base.pipeline import ApiRequestPipeline from src.api.base.pipeline import ApiRequestPipeline
from src.core.api_format.metadata import get_default_body_rules_for_endpoint
from src.core.api_format.signature import parse_signature_key from src.core.api_format.signature import parse_signature_key
from src.core.exceptions import InvalidRequestException, NotFoundException from src.core.exceptions import InvalidRequestException, NotFoundException
from src.core.logger import logger from src.core.logger import logger
@@ -136,6 +137,17 @@ async def create_provider_endpoint(
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)
@router.get("/defaults/{api_format}/body-rules")
async def get_default_endpoint_body_rules(
api_format: str,
request: Request,
db: Session = Depends(get_db),
) -> dict[str, Any]:
"""获取指定 endpoint signature 的默认 body_rules。"""
adapter = AdminGetDefaultBodyRulesAdapter(api_format=api_format)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{endpoint_id}", response_model=ProviderEndpointResponse) @router.get("/{endpoint_id}", response_model=ProviderEndpointResponse)
async def get_endpoint( async def get_endpoint(
endpoint_id: str, endpoint_id: str,
@@ -324,6 +336,9 @@ class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
endpoint_kind = sig.endpoint_kind.value endpoint_kind = sig.endpoint_kind.value
# 使用归一化后的 signature key确保格式一致性 # 使用归一化后的 signature key确保格式一致性
normalized_api_format = sig.key 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
new_endpoint = ProviderEndpoint( new_endpoint = ProviderEndpoint(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@@ -334,7 +349,7 @@ class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
base_url=self.endpoint_data.base_url, base_url=self.endpoint_data.base_url,
custom_path=self.endpoint_data.custom_path, custom_path=self.endpoint_data.custom_path,
header_rules=self.endpoint_data.header_rules, header_rules=self.endpoint_data.header_rules,
body_rules=self.endpoint_data.body_rules, body_rules=body_rules,
max_retries=self.endpoint_data.max_retries, max_retries=self.endpoint_data.max_retries,
is_active=True, is_active=True,
config=self.endpoint_data.config, config=self.endpoint_data.config,
@@ -593,3 +608,19 @@ class AdminDeleteProviderEndpointAdapter(AdminApiAdapter):
"message": f"Endpoint {self.endpoint_id} 已删除", "message": f"Endpoint {self.endpoint_id} 已删除",
"affected_keys_count": affected_keys_count, "affected_keys_count": affected_keys_count,
} }
@dataclass
class AdminGetDefaultBodyRulesAdapter(AdminApiAdapter):
api_format: str
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
try:
normalized_api_format = parse_signature_key(self.api_format).key
except Exception as exc:
raise InvalidRequestException(f"无效的 api_format: {self.api_format}") from exc
return {
"api_format": normalized_api_format,
"body_rules": get_default_body_rules_for_endpoint(normalized_api_format),
}

View File

@@ -51,6 +51,7 @@ from src.core.api_format.metadata import (
can_passthrough_endpoint, can_passthrough_endpoint,
get_auth_config_for_endpoint, get_auth_config_for_endpoint,
get_data_format_id_for_endpoint, get_data_format_id_for_endpoint,
get_default_body_rules_for_endpoint,
get_default_path_for_endpoint, get_default_path_for_endpoint,
get_endpoint_definition, get_endpoint_definition,
get_extra_headers_for_endpoint, get_extra_headers_for_endpoint,
@@ -98,6 +99,7 @@ __all__ = [
"get_extra_headers_for_endpoint", "get_extra_headers_for_endpoint",
"get_protected_keys_for_endpoint", "get_protected_keys_for_endpoint",
"get_data_format_id_for_endpoint", "get_data_format_id_for_endpoint",
"get_default_body_rules_for_endpoint",
"can_passthrough_endpoint", "can_passthrough_endpoint",
# Utils # Utils
"is_cli_format", "is_cli_format",

View File

@@ -9,8 +9,10 @@ API endpoint metadata (new mode).
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable, Mapping, Sequence from collections.abc import Iterable, Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass, field from dataclasses import dataclass, field
from types import MappingProxyType from types import MappingProxyType
from typing import Any
from src.core.api_format.enums import ApiFamily, AuthMethod, EndpointKind from src.core.api_format.enums import ApiFamily, AuthMethod, EndpointKind
from src.core.api_format.signature import EndpointSignature, make_signature_key, parse_signature_key from src.core.api_format.signature import EndpointSignature, make_signature_key, parse_signature_key
@@ -47,6 +49,7 @@ class EndpointDefinition:
stream_in_body: bool = True stream_in_body: bool = True
data_format_id: str = "" data_format_id: str = ""
default_body_rules: Sequence[dict[str, Any]] = field(default_factory=tuple)
@property @property
def signature(self) -> EndpointSignature: def signature(self) -> EndpointSignature:
@@ -65,6 +68,19 @@ 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(
@@ -122,6 +138,7 @@ _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,
@@ -135,6 +152,7 @@ _ENDPOINT_DEFINITIONS: dict[tuple[ApiFamily, EndpointKind], EndpointDefinition]
# compact endpoint is non-streaming by design. # compact endpoint is non-streaming by design.
stream_in_body=False, stream_in_body=False,
data_format_id="openai_responses", data_format_id="openai_responses",
default_body_rules=_CODEX_DEFAULT_BODY_RULES,
), ),
(ApiFamily.OPENAI, EndpointKind.VIDEO): EndpointDefinition( (ApiFamily.OPENAI, EndpointKind.VIDEO): EndpointDefinition(
api_family=ApiFamily.OPENAI, api_family=ApiFamily.OPENAI,
@@ -292,6 +310,15 @@ def get_data_format_id_for_endpoint(
return "" return ""
def get_default_body_rules_for_endpoint(
value: str | EndpointSignature | tuple[ApiFamily, EndpointKind],
) -> list[dict[str, Any]]:
definition = resolve_endpoint_definition(value)
if not definition or not definition.default_body_rules:
return []
return deepcopy(list(definition.default_body_rules))
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],
@@ -336,6 +363,7 @@ __all__ = [
"get_extra_headers_for_endpoint", "get_extra_headers_for_endpoint",
"get_protected_keys_for_endpoint", "get_protected_keys_for_endpoint",
"get_data_format_id_for_endpoint", "get_data_format_id_for_endpoint",
"get_default_body_rules_for_endpoint",
"can_passthrough_endpoint", "can_passthrough_endpoint",
"make_endpoint_signature", "make_endpoint_signature",
] ]

View File

@@ -0,0 +1,103 @@
from types import SimpleNamespace
import pytest
from src.api.admin.endpoints import routes
from src.api.admin.endpoints.routes import AdminCreateProviderEndpointAdapter
from src.models.database import Provider, ProviderEndpoint
from src.models.endpoint_models import ProviderEndpointCreate
class _FakeQuery:
def __init__(self, result: object | None) -> None:
self._result = result
def filter(self, *_args: object, **_kwargs: object) -> "_FakeQuery":
return self
def first(self) -> object | None:
return self._result
class _FakeDB:
def __init__(self, provider: object) -> None:
self.provider = provider
self.added: ProviderEndpoint | None = None
def query(self, model: object) -> _FakeQuery:
if model is Provider:
return _FakeQuery(self.provider)
if model is ProviderEndpoint:
return _FakeQuery(None)
raise AssertionError(f"unexpected model: {model}")
def add(self, obj: ProviderEndpoint) -> None:
self.added = obj
def commit(self) -> None:
return None
def refresh(self, _obj: ProviderEndpoint) -> None:
return None
async def _noop_invalidate_cache() -> None:
return None
@pytest.mark.asyncio
async def test_create_endpoint_injects_default_body_rules_when_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(routes, "invalidate_models_list_cache", _noop_invalidate_cache)
monkeypatch.setattr(
routes,
"get_default_body_rules_for_endpoint",
lambda _fmt: [{"action": "drop", "path": "max_output_tokens"}],
)
db = _FakeDB(
provider=SimpleNamespace(id="p1", name="P1", provider_type="custom"),
)
adapter = AdminCreateProviderEndpointAdapter(
provider_id="p1",
endpoint_data=ProviderEndpointCreate(
provider_id="p1",
api_format="openai:cli",
base_url="https://api.example.com",
),
)
await adapter.handle(SimpleNamespace(db=db)) # type: ignore[arg-type]
assert db.added is not None
assert db.added.body_rules == [{"action": "drop", "path": "max_output_tokens"}]
@pytest.mark.asyncio
async def test_create_endpoint_keeps_user_body_rules_when_provided(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(routes, "invalidate_models_list_cache", _noop_invalidate_cache)
monkeypatch.setattr(
routes,
"get_default_body_rules_for_endpoint",
lambda _fmt: [{"action": "drop", "path": "max_output_tokens"}],
)
user_rules = [{"action": "set", "path": "metadata.source", "value": "user"}]
db = _FakeDB(
provider=SimpleNamespace(id="p1", name="P1", provider_type="custom"),
)
adapter = AdminCreateProviderEndpointAdapter(
provider_id="p1",
endpoint_data=ProviderEndpointCreate(
provider_id="p1",
api_format="openai:cli",
base_url="https://api.example.com",
body_rules=user_rules,
),
)
await adapter.handle(SimpleNamespace(db=db)) # type: ignore[arg-type]
assert db.added is not None
assert db.added.body_rules == user_rules

View File

@@ -0,0 +1,61 @@
import pytest
import src.core.api_format.metadata as metadata
from src.core.api_format.enums import ApiFamily, EndpointKind
from src.core.api_format.metadata import EndpointDefinition, get_default_body_rules_for_endpoint
def test_get_default_body_rules_for_endpoint_returns_empty_for_invalid() -> None:
assert get_default_body_rules_for_endpoint("not-a-valid-signature") == []
def test_get_default_body_rules_for_endpoint_returns_empty_for_no_rules() -> None:
"""claude:chat 没有配置 default_body_rules应返回空列表。"""
assert get_default_body_rules_for_endpoint("claude:chat") == []
def test_get_default_body_rules_for_endpoint_returns_codex_rules() -> None:
"""openai:cli 和 openai:compact 应返回 Codex 默认规则。"""
cli_rules = get_default_body_rules_for_endpoint("openai:cli")
assert len(cli_rules) == 5
actions = [r["action"] for r in cli_rules]
assert actions == ["drop", "drop", "drop", "set", "set"]
assert cli_rules[0]["path"] == "max_output_tokens"
assert cli_rules[1]["path"] == "temperature"
assert cli_rules[2]["path"] == "top_p"
assert cli_rules[3] == {"action": "set", "path": "store", "value": False}
assert cli_rules[4]["path"] == "instructions"
assert cli_rules[4]["condition"]["op"] == "not_exists"
compact_rules = get_default_body_rules_for_endpoint("openai:compact")
assert compact_rules == cli_rules
def test_get_default_body_rules_for_endpoint_returns_deep_copy(
monkeypatch: pytest.MonkeyPatch,
) -> None:
definition = EndpointDefinition(
api_family=ApiFamily.OPENAI,
endpoint_kind=EndpointKind.CLI,
default_body_rules=(
{
"action": "set",
"path": "metadata",
"value": {"source": "default"},
},
),
)
monkeypatch.setattr(metadata, "resolve_endpoint_definition", lambda _value: definition)
rules = get_default_body_rules_for_endpoint("openai:cli")
assert rules == [
{
"action": "set",
"path": "metadata",
"value": {"source": "default"},
}
]
rules[0]["value"]["source"] = "changed"
assert definition.default_body_rules[0]["value"]["source"] == "default"