- Enable default overwrite installation policy for overlapping skills - Support deep recursive GitHub trees discovery mechanism to resolve #58 - Refactor internal architecture to fully decouple stateless helper logic - READMEs and docs synced (v0.3.0)
296 lines
9.3 KiB
Markdown
296 lines
9.3 KiB
Markdown
# Client传入和管理分析
|
||
|
||
## 当前的Client管理架构
|
||
|
||
```
|
||
┌────────────────────────────────────────┐
|
||
│ Pipe Instance (github_copilot_sdk.py) │
|
||
│ │
|
||
│ _shared_clients = { │
|
||
│ "token_hash_1": CopilotClient(...), │ ← 基于GitHub Token缓存
|
||
│ "token_hash_2": CopilotClient(...), │
|
||
│ } │
|
||
└────────────────────────────────────────┘
|
||
│
|
||
│ await _get_client(token)
|
||
│
|
||
▼
|
||
┌────────────────────────────────────────┐
|
||
│ CopilotClient Instance │
|
||
│ │
|
||
│ [仅需GitHub Token配置] │
|
||
│ │
|
||
│ config { │
|
||
│ github_token: "ghp_...", │
|
||
│ cli_path: "...", │
|
||
│ config_dir: "...", │
|
||
│ env: {...}, │
|
||
│ cwd: "..." │
|
||
│ } │
|
||
└────────────────────────────────────────┘
|
||
│
|
||
│ create_session(session_config)
|
||
│
|
||
▼
|
||
┌────────────────────────────────────────┐
|
||
│ Session (per-session configuration) │
|
||
│ │
|
||
│ session_config { │
|
||
│ model: "real_model_id", │
|
||
│ provider: { │ ← ⭐ BYOK配置在这里
|
||
│ type: "openai", │
|
||
│ base_url: "https://api.openai...",
|
||
│ api_key: "sk-...", │
|
||
│ ... │
|
||
│ }, │
|
||
│ infinite_sessions: {...}, │
|
||
│ system_message: {...}, │
|
||
│ ... │
|
||
│ } │
|
||
└────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 目前的流程(代码实际位置)
|
||
|
||
### 步骤1:获取或创建Client(line 6208)
|
||
```python
|
||
# _pipe_impl中
|
||
client = await self._get_client(token)
|
||
```
|
||
|
||
### 步骤2:_get_client函数(line 5523-5561)
|
||
```python
|
||
async def _get_client(self, token: str) -> Any:
|
||
"""Get or create the persistent CopilotClient from the pool based on token."""
|
||
if not token:
|
||
raise ValueError("GitHub Token is required to initialize CopilotClient")
|
||
|
||
token_hash = hashlib.md5(token.encode()).hexdigest()
|
||
|
||
# 查看是否已有缓存的client
|
||
client = self.__class__._shared_clients.get(token_hash)
|
||
if client and client状态正常:
|
||
return client # ← 复用已有的client
|
||
|
||
# 否则创建新client
|
||
client_config = self._build_client_config(user_id=None, chat_id=None)
|
||
client_config["github_token"] = token
|
||
new_client = CopilotClient(client_config)
|
||
await new_client.start()
|
||
self.__class__._shared_clients[token_hash] = new_client
|
||
return new_client
|
||
```
|
||
|
||
### 步骤3:创建会话时传入provider(line 6253-6270)
|
||
```python
|
||
# _pipe_impl中,BYOK部分
|
||
if is_byok_model:
|
||
provider_config = {
|
||
"type": byok_type, # "openai" or "anthropic"
|
||
"wire_api": byok_wire_api,
|
||
"base_url": byok_base_url,
|
||
"api_key": byok_api_key or None,
|
||
"bearer_token": byok_bearer_token or None,
|
||
}
|
||
|
||
# 然后传入session config
|
||
session = await client.create_session(config={
|
||
"model": real_model_id,
|
||
"provider": provider_config, # ← provider在这里传给session
|
||
...
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 关键问题:架构的2个层级
|
||
|
||
| 层级 | 用途 | 配置内容 | 缓存方式 |
|
||
|------|------|---------|---------|
|
||
| **CopilotClient** | CLI和运行时底层逻辑 | GitHub Token, CLI path, 环境变量 | 基于token_hash全局缓存 |
|
||
| **Session** | 具体的对话会话 | Model, Provider(BYOK), Tools, System Prompt | 不缓存(每次新建) |
|
||
|
||
---
|
||
|
||
## 当前的问题
|
||
|
||
### 问题1:Client是全局缓存的,但Provider是会话级别的
|
||
```python
|
||
# ❓ 如果用户想为不同的BYOK模型使用不同的Client呢?
|
||
# 当前无法做到,因为Client基于token缓存是全局的
|
||
|
||
# 例子:
|
||
# Client A: OpenAI API key (token_hash_1)
|
||
# Client B: Anthropic API key (token_hash_2)
|
||
|
||
# 但在Pipe中,只有一个GH_TOKEN,导致只能有一个Client
|
||
```
|
||
|
||
### 问题2:Provider和Client是不同的东西
|
||
```python
|
||
# CopilotClient = GitHub Copilot SDK客户端
|
||
# ProviderConfig = OpenAI/Anthropic等的API配置
|
||
|
||
# 用户可能混淆:
|
||
# "怎么传入BYOK的client和provider"
|
||
# → 实际上只能传provider到session,client是全局的
|
||
```
|
||
|
||
### 问题3:BYOK模型混用的情况处理不清楚
|
||
```python
|
||
# 如果用户想在同一个Pipe中:
|
||
# - Model A 用 OpenAI API
|
||
# - Model B 用 Anthropic API
|
||
# - Model C 用自己的本地LLM
|
||
|
||
# 当前代码是基于全局BYOK配置的,无法为各模型单独设置
|
||
```
|
||
|
||
---
|
||
|
||
## 改进方案
|
||
|
||
### 方案A:保持当前架构,只改Provider映射
|
||
|
||
**思路**:Client保持全局(基于GH_TOKEN),但Provider配置基于模型动态选择
|
||
|
||
```python
|
||
# 在Valves中添加
|
||
class Valves(BaseModel):
|
||
# ... 现有配置 ...
|
||
|
||
# 新增:模型到Provider的映射 (JSON)
|
||
MODEL_PROVIDER_MAP: str = Field(
|
||
default="{}",
|
||
description='Map model IDs to BYOK providers (JSON). Example: '
|
||
'{"gpt-4": {"type": "openai", "base_url": "...", "api_key": "..."}, '
|
||
'"claude-3": {"type": "anthropic", "base_url": "...", "api_key": "..."}}'
|
||
)
|
||
|
||
# 在_pipe_impl中
|
||
def _get_provider_config(self, model_id: str, byok_active: bool) -> Optional[dict]:
|
||
"""Get provider config for a specific model"""
|
||
if not byok_active:
|
||
return None
|
||
|
||
try:
|
||
model_map = json.loads(self.valves.MODEL_PROVIDER_MAP or "{}")
|
||
return model_map.get(model_id)
|
||
except:
|
||
return None
|
||
|
||
# 使用时
|
||
provider_config = self._get_provider_config(real_model_id, byok_active) or {
|
||
"type": byok_type,
|
||
"base_url": byok_base_url,
|
||
"api_key": byok_api_key,
|
||
...
|
||
}
|
||
```
|
||
|
||
**优点**:最小改动,复用现有Client架构
|
||
**缺点**:多个BYOK模型仍共享一个Client(只要GH_TOKEN相同)
|
||
|
||
---
|
||
|
||
### 方案B:为不同BYOK提供商创建不同的Client
|
||
|
||
**思路**:扩展_get_client,支持基于provider_type的多client缓存
|
||
|
||
```python
|
||
async def _get_or_create_client(
|
||
self,
|
||
token: str,
|
||
provider_type: str = "github" # "github", "openai", "anthropic"
|
||
) -> Any:
|
||
"""Get or create client based on token and provider type"""
|
||
|
||
if provider_type == "github" or not provider_type:
|
||
# 现有逻辑
|
||
token_hash = hashlib.md5(token.encode()).hexdigest()
|
||
else:
|
||
# 为BYOK提供商创建不同的client
|
||
composite_key = f"{token}:{provider_type}"
|
||
token_hash = hashlib.md5(composite_key.encode()).hexdigest()
|
||
|
||
# 从缓存获取或创建
|
||
...
|
||
```
|
||
|
||
**优点**:隔离不同BYOK提供商的Client
|
||
**缺点**:更复杂,需要更多改动
|
||
|
||
---
|
||
|
||
## 建议的改进路线
|
||
|
||
**优先级1(高):方案A - 模型到Provider的映射**
|
||
|
||
添加Valves配置:
|
||
```python
|
||
MODEL_PROVIDER_MAP: str = Field(
|
||
default="{}",
|
||
description='Map specific models to their BYOK providers (JSON format)'
|
||
)
|
||
```
|
||
|
||
使用方式:
|
||
```
|
||
{
|
||
"gpt-4": {
|
||
"type": "openai",
|
||
"base_url": "https://api.openai.com/v1",
|
||
"api_key": "sk-..."
|
||
},
|
||
"claude-3": {
|
||
"type": "anthropic",
|
||
"base_url": "https://api.anthropic.com/v1",
|
||
"api_key": "ant-..."
|
||
},
|
||
"llama-2": {
|
||
"type": "openai", # 开源模型通常使用openai兼容API
|
||
"base_url": "http://localhost:8000/v1",
|
||
"api_key": "sk-local"
|
||
}
|
||
}
|
||
```
|
||
|
||
**优先级2(中):在_build_session_config中考虑provider_config**
|
||
|
||
修改infinite_session初始化,基于provider_config判断:
|
||
```python
|
||
def _build_session_config(..., provider_config=None):
|
||
# 如果使用了BYOK provider,需要特殊处理infinite_session
|
||
infinite_session_config = None
|
||
if self.valves.INFINITE_SESSION and provider_config is None:
|
||
# 仅官方Copilot模型启用compression
|
||
infinite_session_config = InfiniteSessionConfig(...)
|
||
```
|
||
|
||
**优先级3(低):方案B - 多client缓存(长期改进)**
|
||
|
||
如果需要完全隔离不同BYOK提供商的Client。
|
||
|
||
---
|
||
|
||
## 总结:如果你要传入BYOK client
|
||
|
||
**现状**:
|
||
- CopilotClient是基于GH_TOKEN全局缓存的
|
||
- Provider配置是在SessionConfig级别动态设置的
|
||
- 一个Client可以创建多个Session,每个Session用不同的Provider
|
||
|
||
**改进后**:
|
||
- 添加MODEL_PROVIDER_MAP配置
|
||
- 对每个模型的请求,动态选择对应的Provider配置
|
||
- 同一个Client可以为不同Provider服务不同的models
|
||
|
||
**你需要做的**:
|
||
1. 在Valves中配置MODEL_PROVIDER_MAP
|
||
2. 在模型选择时读取这个映射
|
||
3. 创建session时用对应的provider_config
|
||
|
||
无需修改Client的创建逻辑!
|