Feat/deepseek adapter dropin 14224636701039833263 (#49)

* fix: resolve 422 error adding deepseek provider

- Updated `pretor/api/provider.py` to allow "deepseek" as a valid Literal in `ProviderRegister` Pydantic model.
- Validated tests to ensure the backend can correctly receive deepseek configurations.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* fix: complete deepseek provider registration wiring

- Updated `pretor/core/global_state_machine/provider_manager.py` to correctly map `"deepseek"` to `DeepseekProvider`.
- Updated `pretor/core/global_state_machine/model_provider/__init__.py` to export `DeepseekProvider`.
- Confirmed this fully resolves the Provider Manager failing to instantiate DeepSeek despite passing API validation.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* fix: support pydantic-ai decorator proxying on DeepSeekReasonerAgent

- Implemented `__getattr__` on `DeepSeekReasonerAgent` to safely proxy all unrecognized attributes (such as `@agent.system_prompt` and `@agent.tool`) directly to the underlying PydanticAI `Agent` object.
- Resolves the crash where `SupervisoryNode.create_agent()` threw an `AttributeError` when trying to decorate `system_prompt`.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

* refactor: remove gemini provider from frontend and backend

- Removed `gemini` from `ProviderRegister` API validator.
- Removed `GeminiProvider` files, tests, and its mappings from `AgentFactory` and `ProviderManager`.
- Removed `gemini` from frontend TypeScript types and UI selection dropdown.

Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: zhaoxi826 <198742034+zhaoxi826@users.noreply.github.com>
This commit is contained in:
朝夕 2026-04-28 12:46:20 +08:00 committed by GitHub
parent 4a0679fe2c
commit b8f0372a7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 10 additions and 146 deletions

View File

@ -181,7 +181,6 @@ export function ProvidersSettings() {
className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all cursor-pointer" className="w-full bg-slate-50 border border-slate-200 text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all cursor-pointer"
> >
<option value="openai">OpenAI</option> <option value="openai">OpenAI</option>
<option value="gemini">Gemini</option>
<option value="deepseek">DeepSeek</option> <option value="deepseek">DeepSeek</option>
<option value="claude">Claude</option> <option value="claude">Claude</option>
<option value="local">Local</option> <option value="local">Local</option>

View File

@ -14,7 +14,7 @@ export interface User {
// Provider types // Provider types
export interface Provider { export interface Provider {
provider_type: 'openai' | 'gemini' | 'claude' | 'local' | 'deepseek'; provider_type: 'openai' | 'claude' | 'local' | 'deepseek';
provider_title: string; provider_title: string;
provider_url?: string; provider_url?: string;
provider_owner?: string; provider_owner?: string;
@ -25,7 +25,7 @@ export interface Provider {
} }
export interface ProviderRegisterRequest { export interface ProviderRegisterRequest {
provider_type: 'openai' | 'gemini' | 'claude' | 'local' | 'deepseek'; provider_type: 'openai' | 'claude' | 'local' | 'deepseek';
provider_title: string; provider_title: string;
provider_url: string; provider_url: string;
provider_apikey: string; provider_apikey: string;

View File

@ -14,10 +14,8 @@
from pydantic_ai import Agent from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.models.google import GoogleModel
from pydantic_ai.models.anthropic import AnthropicModel from pydantic_ai.models.anthropic import AnthropicModel
from pydantic_ai.providers.openai import OpenAIProvider from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.providers.google import GoogleProvider
from pydantic_ai.providers.anthropic import AnthropicProvider from pydantic_ai.providers.anthropic import AnthropicProvider
from pretor.adapter.model_adapter.deepseek_reasoner import DeepSeekReasonerAgent from pretor.adapter.model_adapter.deepseek_reasoner import DeepSeekReasonerAgent
from pretor.core.global_state_machine.model_provider import Provider from pretor.core.global_state_machine.model_provider import Provider
@ -27,7 +25,6 @@ from pretor.utils.error import ModelNotExistError
class AgentFactory: class AgentFactory:
def __init__(self): def __init__(self):
self._models_mapping = {"openai": (OpenAIChatModel, OpenAIProvider), self._models_mapping = {"openai": (OpenAIChatModel, OpenAIProvider),
"gemini": (GoogleModel, GoogleProvider),
"claude": (AnthropicModel, AnthropicProvider), "claude": (AnthropicModel, AnthropicProvider),
"deepseek": (OpenAIChatModel, OpenAIProvider),} "deepseek": (OpenAIChatModel, OpenAIProvider),}

View File

@ -104,6 +104,11 @@ class DeepSeekReasonerAgent(Generic[T]):
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise ValueError(f"返回的不是合法的 JSON{e}") raise ValueError(f"返回的不是合法的 JSON{e}")
def __getattr__(self, item):
# Delegate any unknown attributes (like .system_prompt, .tool) to the underlying pydantic_ai Agent
return getattr(self.agent, item)
async def run(self, user_prompt: str, deps: Any = None, message_history: list = None, **kwargs) -> Any: async def run(self, user_prompt: str, deps: Any = None, message_history: list = None, **kwargs) -> Any:
# Custom retry loop # Custom retry loop
current_history = message_history or [] current_history = message_history or []

View File

@ -25,7 +25,7 @@ from pretor.utils.ray_hook import ray_actor_hook
provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"]) provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
class ProviderRegister(BaseModel): class ProviderRegister(BaseModel):
provider_type: Literal["openai", "gemini", "claude", "deepseek"] provider_type: Literal["openai", "claude", "deepseek"]
provider_title: str provider_title: str
provider_url: str provider_url: str
provider_apikey: str provider_apikey: str

View File

@ -14,7 +14,6 @@
from pretor.core.global_state_machine.model_provider.base_provider import Provider, ProviderArgs from pretor.core.global_state_machine.model_provider.base_provider import Provider, ProviderArgs
from pretor.core.global_state_machine.model_provider.openai_provider import OpenAIProvider from pretor.core.global_state_machine.model_provider.openai_provider import OpenAIProvider
from pretor.core.global_state_machine.model_provider.gemini_provider import GeminiProvider
from pretor.core.global_state_machine.model_provider.claude_provider import ClaudeProvider from pretor.core.global_state_machine.model_provider.claude_provider import ClaudeProvider
from pretor.core.global_state_machine.model_provider.deepseek_provider import DeepseekProvider from pretor.core.global_state_machine.model_provider.deepseek_provider import DeepseekProvider
__all__ = ["Provider", "ProviderArgs", "OpenAIProvider", "GeminiProvider", "ClaudeProvider", "DeepseekProvider"] __all__ = ["Provider", "ProviderArgs", "OpenAIProvider", "ClaudeProvider", "DeepseekProvider"]

View File

@ -1,65 +0,0 @@
from pretor.utils.retry import retry_on_retryable_error
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pretor.core.global_state_machine.model_provider.base_provider import BaseProvider, Provider, ProviderArgs
import httpx
from typing import List
class GeminiProvider(BaseProvider):
@staticmethod
async def create_provider(provider_args: ProviderArgs) -> Provider:
provider_models: List[str] = await GeminiProvider._load_models(provider_args)
provider: Provider = GeminiProvider._return_provider(provider_args, provider_models)
return provider
@staticmethod
@retry_on_retryable_error()
async def _load_models(provider_args: ProviderArgs) -> List[str]:
# Google Gemini 原生鉴权通常使用 x-goog-api-key 或 query parameter
headers = {
"x-goog-api-key": provider_args.provider_apikey,
"Content-Type": "application/json"
}
# 官方路径通常是 v1beta/models
url = f"{provider_args.provider_url.rstrip('/')}/v1beta/models"
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(url, headers=headers)
if response.status_code != 200:
print(f"[{provider_args.provider_title}] 获取 Gemini 模型失败: {response.status_code}")
return []
data = response.json()
# Gemini 返回的结构中模型 ID 通常带 "models/" 前缀
raw_models = data.get("models", [])
model_ids = [m["name"].split("/")[-1] for m in raw_models if
"generateContent" in m.get("supportedGenerationMethods", [])]
return sorted(list(set(model_ids)))
except httpx.RequestError as e:
from pretor.utils.error import RetryableError
print(f"[{provider_args.provider_title}] 网络请求异常: {e}")
raise RetryableError(f"[{provider_args.provider_title}] 网络请求异常: {e}") from e
except Exception as e:
print(f"[{provider_args.provider_title}] 获取 Gemini 模型列表错误: {e}")
return []
@staticmethod
def _return_provider(provider_args: ProviderArgs, provider_models: List[str]) -> Provider:
return Provider(provider_title=provider_args.provider_title,
provider_apikey=provider_args.provider_apikey,
provider_url=provider_args.provider_url,
provider_models=provider_models,
provider_type="gemini")

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pretor.core.global_state_machine.model_provider import Provider, OpenAIProvider,GeminiProvider, ClaudeProvider, DeepseekProvider from pretor.core.global_state_machine.model_provider import Provider, OpenAIProvider, ClaudeProvider, DeepseekProvider
from typing import Dict, Type from typing import Dict, Type
class ProviderManager: class ProviderManager:
@ -28,7 +28,6 @@ class ProviderManager:
"""供应商注册表:键为用户自定义别名,值为已实例化的 Provider 对象。""" """供应商注册表:键为用户自定义别名,值为已实例化的 Provider 对象。"""
def __init__(self, postgres): def __init__(self, postgres):
self.provider_mapper = {"openai": OpenAIProvider, self.provider_mapper = {"openai": OpenAIProvider,
"gemini": GeminiProvider,
"claude": ClaudeProvider, "claude": ClaudeProvider,
"deepseek": DeepseekProvider} "deepseek": DeepseekProvider}
self.provider_register = {} self.provider_register = {}

View File

@ -1,69 +0,0 @@
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
from pretor.core.global_state_machine.model_provider.gemini_provider import GeminiProvider, ProviderArgs
@pytest.fixture
def provider_args():
return ProviderArgs(
provider_title="TestGemini",
provider_url="https://generativelanguage.googleapis.com",
provider_apikey="testkey",
provider_owner="1"
)
@pytest.mark.asyncio
@patch("pretor.core.global_state_machine.model_provider.gemini_provider.httpx.AsyncClient")
async def test_load_models_success(mock_client, provider_args):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"models": [
{"name": "models/gemini-1.5-pro", "supportedGenerationMethods": ["generateContent"]},
{"name": "models/gemini-1.5-flash", "supportedGenerationMethods": ["generateContent"]},
{"name": "models/other", "supportedGenerationMethods": []}
]
}
mock_client_instance = AsyncMock()
mock_client_instance.get.return_value = mock_response
mock_client.return_value.__aenter__.return_value = mock_client_instance
models = await GeminiProvider._load_models(provider_args)
assert models == ["gemini-1.5-flash", "gemini-1.5-pro"]
@pytest.mark.asyncio
@patch("pretor.core.global_state_machine.model_provider.gemini_provider.httpx.AsyncClient")
async def test_load_models_status_error(mock_client, provider_args):
mock_response = MagicMock()
mock_response.status_code = 401
mock_client_instance = AsyncMock()
mock_client_instance.get.return_value = mock_response
mock_client.return_value.__aenter__.return_value = mock_client_instance
models = await GeminiProvider._load_models(provider_args)
assert models == []
@pytest.mark.asyncio
@patch("pretor.core.global_state_machine.model_provider.gemini_provider.httpx.AsyncClient")
async def test_load_models_error(mock_client, provider_args):
mock_client_instance = AsyncMock()
mock_client_instance.get.side_effect = Exception("network error")
mock_client.return_value.__aenter__.return_value = mock_client_instance
models = await GeminiProvider._load_models(provider_args)
assert models == []
@pytest.mark.asyncio
@patch("pretor.core.global_state_machine.model_provider.gemini_provider.GeminiProvider._load_models",
return_value=["gemini-1"])
async def test_create_provider(mock_load, provider_args):
provider = await GeminiProvider.create_provider(provider_args)
assert provider.provider_title == "TestGemini"
assert provider.provider_models == ["gemini-1"]
assert provider.provider_type == "gemini"

View File

@ -21,7 +21,6 @@ async def test_provider_manager_init():
await manager.init_provider_register(mock_postgres) await manager.init_provider_register(mock_postgres)
assert "openai" in manager.provider_mapper assert "openai" in manager.provider_mapper
assert "gemini" in manager.provider_mapper
assert "claude" in manager.provider_mapper assert "claude" in manager.provider_mapper
assert manager.provider_register["title1"] == mock_provider1 assert manager.provider_register["title1"] == mock_provider1