1111import re
1212import time
1313from collections .abc import AsyncIterator
14- from typing import Any
14+ from typing import TYPE_CHECKING , Any
1515
1616import httpx
1717
18+ if TYPE_CHECKING :
19+ from ..config .session_policy import TitleVendorBinding
20+
1821from ..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 )
0 commit comments