Skip to content

Commit 95c08d7

Browse files
committed
feat(session-routing): 新增基于 Session 标题前缀的可配置 Vendor 自动绑定;
当 Session 标题以配置前缀(默认「# 目标」)开头时,自动将该 Session 绑定到指定 vendor(默认 zhipu),不走默认 vendor 路由。前缀与目标 vendor 均通过 YAML 配置(session_policies.title_vendor_bindings), 支持多条规则、按顺序匹配首次命中生效、置空列表即禁用。 实现复用现有 SessionPolicyResolver.upsert() 运行时绑定机制,在标题 提取后、tier 解析前注入策略,仅对新 Session 首请求生效以避免覆盖手动 绑定。新增启动期 vendor 存在性校验(未知 vendor 告警而非硬失败)与 prefix/vendor 非空约束(杜绝空前缀全量误绑定)。 - config/session_policy.py: 新增 TitleVendorBinding 模型 + 配置字段 - config/config.default.yaml: 默认「# 目标」→ zhipu 绑定 - routing/executor.py: _apply_title_based_policy 方法、启动校验、双路径注入 - routing/router.py + server/app.py: 配置透传 - tests: 新增 17 个用例(前缀匹配/边缘/校验/流式与非流式端到端) 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
1 parent a778271 commit 95c08d7

7 files changed

Lines changed: 378 additions & 3 deletions

File tree

src/coding/proxy/config/config.default.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,4 +685,11 @@ native_api:
685685
# tiers: ["copilot", "anthropic", "zhipu"]
686686
#
687687
# 未配置时(默认),所有 Session 使用全局 tiers 顺序。
688-
session_policies: []
688+
session_policies:
689+
policies: []
690+
# 标题前缀 → 供应商自动绑定。
691+
# 当 Session 标题以指定前缀开头时,自动将该 Session 绑定到对应供应商。
692+
# 匹配规则按列表顺序求值,首次匹配生效。
693+
title_vendor_bindings:
694+
- prefix: "# 目标"
695+
vendor: "zhipu"

src/coding/proxy/config/schema.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
# ── 子模块 re-export ────────────────────────────────────────────
4646
from .server import DatabaseConfig, LoggingConfig, ServerConfig # noqa: F401
47-
from .session_policy import SessionPoliciesConfig # noqa: F401
47+
from .session_policy import SessionPoliciesConfig, TitleVendorBinding # noqa: F401
4848
from .vendors import ( # noqa: F401
4949
AlibabaConfig,
5050
AnthropicConfig,
@@ -350,4 +350,5 @@ def compat_state_path(self) -> Path:
350350
"NativeApiConfig",
351351
# session policy
352352
"SessionPoliciesConfig",
353+
"TitleVendorBinding",
353354
]

src/coding/proxy/config/session_policy.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,34 @@ class SessionPolicy(BaseModel):
5050
)
5151

5252

53+
class TitleVendorBinding(BaseModel):
54+
"""标题前缀 → 供应商自动绑定规则."""
55+
56+
prefix: str = Field(
57+
min_length=1,
58+
description=(
59+
"标题前缀匹配模式(大小写敏感的 startswith 匹配)。"
60+
"禁止空字符串——空前缀会匹配所有标题,导致全量误绑定。"
61+
),
62+
)
63+
vendor: str = Field(
64+
min_length=1,
65+
description="匹配后绑定的目标供应商名称",
66+
)
67+
68+
5369
class SessionPoliciesConfig(BaseModel):
5470
"""顶层 Session 策略配置容器."""
5571

5672
policies: list[SessionPolicy] = Field(
5773
default_factory=list,
5874
description="Session 路由策略列表,按定义顺序求值,首次匹配生效",
5975
)
76+
title_vendor_bindings: list[TitleVendorBinding] = Field(
77+
default_factory=list,
78+
description=(
79+
"标题前缀 → 供应商自动绑定规则。"
80+
"当 Session 标题以指定前缀开头时,自动绑定到对应供应商。"
81+
"匹配规则按列表顺序求值,首次匹配生效。"
82+
),
83+
)

src/coding/proxy/routing/executor.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
import re
1212
import time
1313
from collections.abc import AsyncIterator
14-
from typing import Any
14+
from typing import TYPE_CHECKING, Any
1515

1616
import httpx
1717

18+
if TYPE_CHECKING:
19+
from ..config.session_policy import TitleVendorBinding
20+
1821
from ..vendors.base import (
1922
NoCompatibleVendorError,
2023
RequestCapabilities,
@@ -610,20 +613,43 @@ def __init__(
610613
session_manager: RouteSessionManager,
611614
reauth_coordinator: Any | None = None,
612615
session_policy_resolver: SessionPolicyResolver | None = None,
616+
title_vendor_bindings: list[TitleVendorBinding] | None = None,
613617
) -> None:
614618
self._router = router
615619
self._tiers = tiers
616620
self._recorder = usage_recorder
617621
self._session_mgr = session_manager
618622
self._reauth_coordinator = reauth_coordinator
619623
self._policy_resolver = session_policy_resolver or SessionPolicyResolver()
624+
self._title_vendor_bindings = title_vendor_bindings or []
625+
self._validate_title_vendor_bindings()
620626

621627
# Tier 名称 → OAuth provider 名称的映射
622628
self._tier_provider_map: dict[str, str] = {
623629
"copilot": "github",
624630
"antigravity": "google",
625631
}
626632

633+
def _validate_title_vendor_bindings(self) -> None:
634+
"""启动期校验标题绑定引用的 vendor 均存在,缺失则告警.
635+
636+
与手动绑定 API(拒绝未知 vendor)的语义对齐:此处不硬失败,
637+
仅记录警告——避免单条误配置阻断整个代理启动;运行时
638+
`_resolve_effective_tiers` 会静默跳过未知 vendor 回退默认顺序。
639+
"""
640+
if not self._title_vendor_bindings:
641+
return
642+
valid = {t.name for t in self._tiers}
643+
for binding in self._title_vendor_bindings:
644+
if binding.vendor not in valid:
645+
logger.warning(
646+
"title_vendor_bindings 引用了未知 vendor %r(前缀 %r);"
647+
"可用 vendor: %s。该绑定将在运行时被静默跳过。",
648+
binding.vendor,
649+
binding.prefix,
650+
sorted(valid),
651+
)
652+
627653
# ── 公开执行入口 ──────────────────────────────────────
628654

629655
def _resolve_effective_tiers(self, session_key: str) -> list[VendorTier]:
@@ -650,6 +676,27 @@ def _resolve_effective_tiers(self, session_key: str) -> list[VendorTier]:
650676
seen.add(tier.name)
651677
return ordered
652678

679+
def _apply_title_based_policy(self, session_key: str, title: str) -> None:
680+
"""根据 Session 标题前缀自动绑定供应商.
681+
682+
当标题以预配置的前缀开头时,通过 SessionPolicyResolver.upsert()
683+
将该 Session 绑定到指定供应商,后续请求无需再走默认路由。
684+
685+
仅在新 Session 首次提取标题时调用,避免覆盖手动绑定的策略。
686+
"""
687+
if not title or not self._title_vendor_bindings:
688+
return
689+
for binding in self._title_vendor_bindings:
690+
if title.startswith(binding.prefix):
691+
self._policy_resolver.upsert(session_key, [binding.vendor])
692+
logger.info(
693+
"Session title prefix %r matched → auto-bind to %s (session=%s)",
694+
binding.prefix,
695+
binding.vendor,
696+
session_key[:12],
697+
)
698+
return
699+
653700
def _prepare_body_for_tier(
654701
self,
655702
body: dict[str, Any],
@@ -748,6 +795,7 @@ async def execute_stream(
748795
await self._recorder.set_session_title(
749796
canonical_request.session_key, title
750797
)
798+
self._apply_title_based_policy(canonical_request.session_key, title)
751799
else:
752800
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
753801
title = _extract_session_title(canonical_request)
@@ -934,6 +982,7 @@ async def execute_message(
934982
await self._recorder.set_session_title(
935983
canonical_request.session_key, title
936984
)
985+
self._apply_title_based_policy(canonical_request.session_key, title)
937986
else:
938987
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
939988
title = _extract_session_title(canonical_request)

src/coding/proxy/routing/router.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import TYPE_CHECKING, Any
1515

1616
if TYPE_CHECKING:
17+
from ..config.session_policy import TitleVendorBinding
1718
from ..pricing import PricingTable
1819

1920
from .executor import _RouteExecutor
@@ -38,6 +39,7 @@ def __init__(
3839
reauth_coordinator: Any | None = None,
3940
compat_session_store: CompatSessionStore | None = None,
4041
session_policy_resolver: SessionPolicyResolver | None = None,
42+
title_vendor_bindings: list[TitleVendorBinding] | None = None,
4143
) -> None:
4244
if not tiers:
4345
raise ValueError("至少需要一个供应商层级")
@@ -56,6 +58,7 @@ def __init__(
5658
session_manager=self._session_mgr,
5759
reauth_coordinator=reauth_coordinator,
5860
session_policy_resolver=session_policy_resolver,
61+
title_vendor_bindings=title_vendor_bindings,
5962
)
6063

6164
def set_pricing_table(self, table: PricingTable) -> None:

src/coding/proxy/server/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
161161
reauth_coordinator,
162162
compat_session_store,
163163
session_policy_resolver=SessionPolicyResolver(config.session_policies.policies),
164+
title_vendor_bindings=config.session_policies.title_vendor_bindings,
164165
)
165166

166167
app = FastAPI(title="coding-proxy", version=__version__, lifespan=lifespan)

0 commit comments

Comments
 (0)