Skip to content

Commit c815b30

Browse files
Merge pull request #76 from pescheckit/feature_changed-deepseek-to-generic
Generalize DeepSeek provider for any OpenAI-compatible API
2 parents ebdd5b5 + 043a609 commit c815b30

8 files changed

Lines changed: 223 additions & 94 deletions

File tree

python_gpt_po/models/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class ModelProvider(Enum):
99
"""Enum for supported model providers."""
1010
OPENAI = "openai"
1111
ANTHROPIC = "anthropic"
12+
OPENAI_COMPATIBLE = "openai_compatible"
1213
DEEPSEEK = "deepseek"
1314
AZURE_OPENAI = "azure_openai"
1415
OLLAMA = "ollama"

python_gpt_po/models/provider_clients.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ def __init__(self):
1919
self.openai_client = None
2020
self.azure_openai_client = None
2121
self.anthropic_client = None
22-
self.deepseek_api_key = None
23-
self.deepseek_base_url = None
22+
self.openai_compatible_api_key = None
23+
self.openai_compatible_base_url = None
2424
self.ollama_base_url = None
2525
self.ollama_timeout = None
2626

@@ -107,17 +107,35 @@ def initialize_clients(self, args: Namespace) -> Dict[str, str]:
107107
if antropic_key:
108108
self.anthropic_client = Anthropic(api_key=antropic_key)
109109

110-
# DeepSeek
111-
deepseek_key = self._get_setting(
112-
args, 'deepseek_key', 'DEEPSEEK_API_KEY', 'deepseek', 'api_key', ''
110+
# OpenAI-Compatible (accepts both --openai-compatible-* and --deepseek-* args)
111+
openai_compatible_key = self._get_setting(
112+
args, 'openai_compatible_key', 'OPENAI_COMPATIBLE_API_KEY',
113+
'openai_compatible', 'api_key', ''
113114
)
114-
if deepseek_key:
115-
self.deepseek_api_key = deepseek_key
115+
if not openai_compatible_key:
116+
# Backward compatibility: accept deepseek args
117+
openai_compatible_key = self._get_setting(
118+
args, 'deepseek_key', 'DEEPSEEK_API_KEY', 'deepseek', 'api_key', ''
119+
)
120+
if openai_compatible_key:
121+
self.openai_compatible_api_key = openai_compatible_key
122+
123+
# Base URL - default to DeepSeek API if using deepseek provider
124+
provider_name = args.provider if hasattr(args, 'provider') else None
125+
default_base_url = 'https://api.deepseek.com/v1' if provider_name == 'deepseek' else None
116126

117-
self.deepseek_base_url = self._get_setting(
118-
args, 'deepseek_base_url', 'DEEPSEEK_BASE_URL',
119-
'deepseek', 'base_url', 'https://api.deepseek.com/v1'
127+
openai_compatible_base_url = self._get_setting(
128+
args, 'openai_compatible_base_url', 'OPENAI_COMPATIBLE_BASE_URL',
129+
'openai_compatible', 'base_url', None
120130
)
131+
if not openai_compatible_base_url:
132+
# Backward compatibility: accept deepseek args
133+
openai_compatible_base_url = self._get_setting(
134+
args, 'deepseek_base_url', 'DEEPSEEK_BASE_URL',
135+
'deepseek', 'base_url', default_base_url
136+
)
137+
if openai_compatible_base_url:
138+
self.openai_compatible_base_url = openai_compatible_base_url
121139

122140
# Ollama
123141
self.ollama_base_url = self._get_setting(
@@ -132,7 +150,8 @@ def initialize_clients(self, args: Namespace) -> Dict[str, str]:
132150
return {
133151
ModelProvider.OPENAI.value: openai_key,
134152
ModelProvider.ANTHROPIC.value: antropic_key,
135-
ModelProvider.DEEPSEEK.value: deepseek_key,
153+
ModelProvider.OPENAI_COMPATIBLE.value: openai_compatible_key,
154+
ModelProvider.DEEPSEEK.value: openai_compatible_key,
136155
ModelProvider.AZURE_OPENAI.value: azure_openai_key,
137156
ModelProvider.OLLAMA.value: "local", # Ollama doesn't need API key
138157
ModelProvider.CLAUDE_SDK.value: "local",
Lines changed: 7 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,10 @@
11
"""
2-
DeepSeek provider implementation.
2+
DeepSeek provider implementation (legacy alias).
3+
This module is maintained for backward compatibility.
4+
New code should use openai_compatible_provider instead.
35
"""
4-
import logging
5-
from typing import List
6+
# Import the new provider and create an alias
7+
from .openai_compatible_provider import OpenAICompatibleProvider
68

7-
import requests
8-
9-
from ...models.provider_clients import ProviderClients
10-
from .base import ModelProviderInterface
11-
12-
13-
class DeepSeekProvider(ModelProviderInterface):
14-
"""DeepSeek model provider implementation."""
15-
16-
def get_models(self, provider_clients: ProviderClients) -> List[str]:
17-
"""Retrieve available models from DeepSeek."""
18-
models = []
19-
20-
if not self.is_client_initialized(provider_clients):
21-
logging.error("DeepSeek API key not set")
22-
return models
23-
24-
try:
25-
headers = {
26-
"Authorization": f"Bearer {provider_clients.deepseek_api_key}",
27-
"Content-Type": "application/json"
28-
}
29-
response = requests.get(
30-
f"{provider_clients.deepseek_base_url}/models",
31-
headers=headers,
32-
timeout=15
33-
)
34-
response.raise_for_status()
35-
models = [model["id"] for model in response.json().get("data", [])]
36-
except Exception as e:
37-
logging.error("Error fetching DeepSeek models: %s", str(e))
38-
models = self.get_fallback_models()
39-
40-
return models
41-
42-
def get_default_model(self) -> str:
43-
"""Get the default DeepSeek model."""
44-
return "deepseek-chat"
45-
46-
def get_preferred_models(self, task: str = "translation") -> List[str]:
47-
"""Get preferred DeepSeek models for a task."""
48-
return ["deepseek-chat"]
49-
50-
def is_client_initialized(self, provider_clients: ProviderClients) -> bool:
51-
"""Check if DeepSeek client is initialized."""
52-
return provider_clients.deepseek_api_key is not None
53-
54-
def get_fallback_models(self) -> List[str]:
55-
"""Get fallback models for DeepSeek."""
56-
return ["deepseek-chat", "deepseek-coder"]
57-
58-
def translate(self, provider_clients: ProviderClients, model: str, content: str) -> str:
59-
"""Get response from DeepSeek API."""
60-
if not self.is_client_initialized(provider_clients):
61-
raise ValueError("DeepSeek client not initialized")
62-
63-
headers = {
64-
"Authorization": f"Bearer {provider_clients.deepseek_api_key}",
65-
"Content-Type": "application/json"
66-
}
67-
payload = {
68-
"model": model,
69-
"messages": [{"role": "user", "content": content}],
70-
"max_tokens": 4000
71-
}
72-
response = requests.post(
73-
f"{provider_clients.deepseek_base_url}/chat/completions",
74-
headers=headers,
75-
json=payload,
76-
timeout=30
77-
)
78-
response.raise_for_status()
79-
return response.json()["choices"][0]["message"]["content"].strip()
9+
# DeepSeekProvider is now an alias to OpenAICompatibleProvider
10+
DeepSeekProvider = OpenAICompatibleProvider
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
OpenAI-compatible API provider implementation.
3+
Supports any service that implements the OpenAI API format, including:
4+
- DeepSeek
5+
- LM Studio
6+
- z.ai
7+
- Groq
8+
- Together.ai
9+
- Fireworks
10+
- And many others
11+
"""
12+
import logging
13+
from typing import List
14+
15+
import requests
16+
17+
from ...models.provider_clients import ProviderClients
18+
from .base import ModelProviderInterface
19+
20+
21+
class OpenAICompatibleProvider(ModelProviderInterface):
22+
"""OpenAI-compatible API provider implementation."""
23+
24+
def get_models(self, provider_clients: ProviderClients) -> List[str]:
25+
"""Retrieve available models from the API."""
26+
models = []
27+
28+
if not self.is_client_initialized(provider_clients):
29+
logging.error("OpenAI-compatible API key not set")
30+
return models
31+
32+
try:
33+
headers = {
34+
"Authorization": f"Bearer {provider_clients.openai_compatible_api_key}",
35+
"Content-Type": "application/json"
36+
}
37+
response = requests.get(
38+
f"{provider_clients.openai_compatible_base_url}/models",
39+
headers=headers,
40+
timeout=15
41+
)
42+
response.raise_for_status()
43+
models = [model["id"] for model in response.json().get("data", [])]
44+
except Exception as e:
45+
logging.error("Error fetching models: %s", str(e))
46+
models = self.get_fallback_models()
47+
48+
return models
49+
50+
def get_default_model(self) -> str:
51+
"""Get the default model."""
52+
return "gpt-3.5-turbo"
53+
54+
def get_preferred_models(self, task: str = "translation") -> List[str]:
55+
"""Get preferred models for a task."""
56+
return ["gpt-4", "gpt-3.5-turbo"]
57+
58+
def is_client_initialized(self, provider_clients: ProviderClients) -> bool:
59+
"""Check if client is initialized."""
60+
has_key = provider_clients.openai_compatible_api_key is not None
61+
has_url = provider_clients.openai_compatible_base_url is not None
62+
return has_key and has_url
63+
64+
def get_fallback_models(self) -> List[str]:
65+
"""Get fallback models."""
66+
return ["gpt-3.5-turbo", "gpt-4"]
67+
68+
def translate(self, provider_clients: ProviderClients, model: str, content: str) -> str:
69+
"""Get response from OpenAI-compatible API."""
70+
if not self.is_client_initialized(provider_clients):
71+
raise ValueError("OpenAI-compatible client not initialized")
72+
73+
headers = {
74+
"Authorization": f"Bearer {provider_clients.openai_compatible_api_key}",
75+
"Content-Type": "application/json"
76+
}
77+
payload = {
78+
"model": model,
79+
"messages": [{"role": "user", "content": content}],
80+
"max_tokens": 4000
81+
}
82+
response = requests.post(
83+
f"{provider_clients.openai_compatible_base_url}/chat/completions",
84+
headers=headers,
85+
json=payload,
86+
timeout=30
87+
)
88+
response.raise_for_status()
89+
return response.json()["choices"][0]["message"]["content"].strip()

python_gpt_po/services/providers/provider_init.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ def initialize_providers():
1111
from .anthropic_provider import AnthropicProvider
1212
from .azure_openai_provider import AzureOpenAIProvider
1313
from .claude_sdk_provider import ClaudeSdkProvider
14-
from .deepseek_provider import DeepSeekProvider
1514
from .ollama_provider import OllamaProvider
15+
from .openai_compatible_provider import OpenAICompatibleProvider
1616
from .openai_provider import OpenAIProvider
1717
ProviderRegistry.register(ModelProvider.OPENAI, OpenAIProvider)
1818
ProviderRegistry.register(ModelProvider.ANTHROPIC, AnthropicProvider)
19-
ProviderRegistry.register(ModelProvider.DEEPSEEK, DeepSeekProvider)
19+
ProviderRegistry.register(ModelProvider.OPENAI_COMPATIBLE, OpenAICompatibleProvider)
20+
ProviderRegistry.register(ModelProvider.DEEPSEEK, OpenAICompatibleProvider) # Alias
2021
ProviderRegistry.register(ModelProvider.AZURE_OPENAI, AzureOpenAIProvider)
2122
ProviderRegistry.register(ModelProvider.OLLAMA, OllamaProvider)
2223
ProviderRegistry.register(ModelProvider.CLAUDE_SDK, ClaudeSdkProvider)

python_gpt_po/tests/providers/test_deepseek_provider.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from argparse import Namespace
12
from unittest.mock import MagicMock, patch
23

34
import pytest
45

56
from python_gpt_po.models.provider_clients import ProviderClients
67
from python_gpt_po.services.providers.deepseek_provider import DeepSeekProvider
8+
from python_gpt_po.services.providers.openai_compatible_provider import OpenAICompatibleProvider
79

810
DEEPSEEK_TRANSLATION_RESPONSE = {
911
"choices": [
@@ -20,12 +22,12 @@
2022
def mock_provider_clients() -> ProviderClients:
2123
"""Mock provider clients for testing."""
2224
clients = ProviderClients()
23-
clients.deepseek_api_key = "sk-deepseek-mock-key"
24-
clients.deepseek_base_url = "https://api.deepseek.com/v1"
25+
clients.openai_compatible_api_key = "sk-deepseek-mock-key"
26+
clients.openai_compatible_base_url = "https://api.deepseek.com/v1"
2527
return clients
2628

2729

28-
@patch('python_gpt_po.services.providers.deepseek_provider.requests.post')
30+
@patch('python_gpt_po.services.providers.openai_compatible_provider.requests.post')
2931
def test_translate(mock_post: MagicMock, mock_provider_clients: ProviderClients) -> None:
3032
"""Test translation with DeepSeek."""
3133
# Setup mock response
@@ -42,3 +44,65 @@ def test_translate(mock_post: MagicMock, mock_provider_clients: ProviderClients)
4244

4345
print(type(translations))
4446
assert translations == '```json\n["Bonjour", "Monde", "Bienvenue dans notre application", "Au revoir"]\n```'
47+
48+
49+
def test_deepseek_is_alias_to_openai_compatible() -> None:
50+
"""Test that DeepSeekProvider is an alias to OpenAICompatibleProvider."""
51+
assert DeepSeekProvider is OpenAICompatibleProvider
52+
53+
54+
def test_backward_compatibility_deepseek_args() -> None:
55+
"""Test that old --deepseek-* arguments still work."""
56+
args = Namespace(
57+
provider='deepseek',
58+
deepseek_key='sk-test-key',
59+
deepseek_base_url=None,
60+
openai_compatible_key=None,
61+
openai_compatible_base_url=None,
62+
folder=None
63+
)
64+
65+
clients = ProviderClients()
66+
clients.initialize_clients(args)
67+
68+
# Old deepseek args should set openai_compatible fields
69+
assert clients.openai_compatible_api_key == 'sk-test-key'
70+
# Should get DeepSeek default base URL when using deepseek provider
71+
assert clients.openai_compatible_base_url == 'https://api.deepseek.com/v1'
72+
73+
74+
def test_new_openai_compatible_args() -> None:
75+
"""Test that new --openai-compatible-* arguments work."""
76+
args = Namespace(
77+
provider='openai_compatible',
78+
deepseek_key=None,
79+
deepseek_base_url=None,
80+
openai_compatible_key='sk-test-key',
81+
openai_compatible_base_url='http://localhost:1234/v1',
82+
folder=None
83+
)
84+
85+
clients = ProviderClients()
86+
clients.initialize_clients(args)
87+
88+
assert clients.openai_compatible_api_key == 'sk-test-key'
89+
assert clients.openai_compatible_base_url == 'http://localhost:1234/v1'
90+
91+
92+
def test_deepseek_args_priority_over_openai_compatible() -> None:
93+
"""Test that openai_compatible args have priority over deepseek args."""
94+
args = Namespace(
95+
provider='openai_compatible',
96+
deepseek_key='sk-old-key',
97+
deepseek_base_url='https://old.api.com/v1',
98+
openai_compatible_key='sk-new-key',
99+
openai_compatible_base_url='http://new.api.com/v1',
100+
folder=None
101+
)
102+
103+
clients = ProviderClients()
104+
clients.initialize_clients(args)
105+
106+
# New args should take priority
107+
assert clients.openai_compatible_api_key == 'sk-new-key'
108+
assert clients.openai_compatible_base_url == 'http://new.api.com/v1'

python_gpt_po/tests/test_multi_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ def mock_provider_clients() -> ProviderClients:
117117
clients.openai_client = MagicMock()
118118
clients.anthropic_client = MagicMock()
119119
clients.anthropic_client.api_key = "sk-ant-mock-key"
120-
clients.deepseek_api_key = "sk-deepseek-mock-key"
121-
clients.deepseek_base_url = "https://api.deepseek.com/v1"
120+
clients.openai_compatible_api_key = "sk-deepseek-mock-key"
121+
clients.openai_compatible_base_url = "https://api.deepseek.com/v1"
122122
clients.azure_openai_client = MagicMock()
123123
clients.azure_openai_client.api_key = "sk-aoi-mock-key"
124124
return clients

0 commit comments

Comments
 (0)