devorch 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/commands/ask.py +0 -0
- cli/commands/edit.py +0 -0
- cli/commands/run.py +0 -0
- cli/main.py +1527 -0
- config/permissions.py +285 -0
- config/profiles.py +0 -0
- config/settings.py +185 -0
- core/agent.py +433 -0
- core/context.py +0 -0
- core/executor.py +213 -0
- core/modes.py +141 -0
- core/planner.py +16 -0
- core/sessions.py +271 -0
- core/tasks.py +142 -0
- devorch-0.1.2.dist-info/METADATA +528 -0
- devorch-0.1.2.dist-info/RECORD +50 -0
- devorch-0.1.2.dist-info/WHEEL +4 -0
- devorch-0.1.2.dist-info/entry_points.txt +2 -0
- providers/__init__.py +130 -0
- providers/anthropic.py +127 -0
- providers/base.py +51 -0
- providers/custom.py +186 -0
- providers/deepseek.py +170 -0
- providers/gemini.py +192 -0
- providers/github_copilot.py +196 -0
- providers/groq.py +158 -0
- providers/kimi.py +181 -0
- providers/lmstudio.py +147 -0
- providers/local.py +214 -0
- providers/mistral.py +161 -0
- providers/openai.py +114 -0
- providers/openrouter.py +195 -0
- providers/together.py +159 -0
- schemas/message.py +32 -0
- schemas/task.py +115 -0
- schemas/tool.py +0 -0
- tools/__init__.py +30 -0
- tools/base.py +40 -0
- tools/edit.py +236 -0
- tools/filesystem.py +224 -0
- tools/git.py +0 -0
- tools/grep.py +280 -0
- tools/search.py +150 -0
- tools/shell.py +55 -0
- tools/task.py +91 -0
- tools/terminal.py +123 -0
- tools/terminal_session.py +298 -0
- tools/tests.py +0 -0
- tools/websearch.py +166 -0
- utils/logger.py +52 -0
providers/__init__.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from providers.anthropic import AnthropicProvider
|
|
2
|
+
from providers.base import LLMProvider, ModelInfo
|
|
3
|
+
from providers.custom import CustomProvider
|
|
4
|
+
from providers.deepseek import DeepSeekProvider
|
|
5
|
+
from providers.gemini import GeminiProvider
|
|
6
|
+
from providers.github_copilot import GitHubCopilotProvider
|
|
7
|
+
from providers.groq import GroqProvider
|
|
8
|
+
from providers.kimi import KimiProvider
|
|
9
|
+
from providers.lmstudio import LMStudioProvider
|
|
10
|
+
from providers.local import LocalProvider
|
|
11
|
+
from providers.mistral import MistralProvider
|
|
12
|
+
from providers.openai import OpenAIProvider
|
|
13
|
+
from providers.openrouter import OpenRouterProvider
|
|
14
|
+
from providers.together import TogetherProvider
|
|
15
|
+
|
|
16
|
+
# All available providers
|
|
17
|
+
PROVIDERS: dict[str, type[LLMProvider]] = {
|
|
18
|
+
"openai": OpenAIProvider,
|
|
19
|
+
"anthropic": AnthropicProvider,
|
|
20
|
+
"gemini": GeminiProvider,
|
|
21
|
+
"local": LocalProvider,
|
|
22
|
+
"openrouter": OpenRouterProvider,
|
|
23
|
+
"mistral": MistralProvider,
|
|
24
|
+
"groq": GroqProvider,
|
|
25
|
+
"lmstudio": LMStudioProvider,
|
|
26
|
+
"together": TogetherProvider,
|
|
27
|
+
"github_copilot": GitHubCopilotProvider,
|
|
28
|
+
"deepseek": DeepSeekProvider,
|
|
29
|
+
"kimi": KimiProvider,
|
|
30
|
+
"custom": CustomProvider,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Provider descriptions for help
|
|
34
|
+
PROVIDER_INFO = {
|
|
35
|
+
"openai": "OpenAI - GPT-4o, GPT-4, etc.",
|
|
36
|
+
"anthropic": "Anthropic - Claude 3.5, Claude 3, etc.",
|
|
37
|
+
"gemini": "Google - Gemini Pro, Flash, etc.",
|
|
38
|
+
"local": "Ollama - Local models (llama, mistral, etc.)",
|
|
39
|
+
"openrouter": "OpenRouter - Access 100+ models via one API",
|
|
40
|
+
"mistral": "Mistral AI - Mistral Large, Medium, Codestral",
|
|
41
|
+
"groq": "Groq - Ultra-fast inference (Llama, Mixtral)",
|
|
42
|
+
"lmstudio": "LM Studio - Run local models with UI",
|
|
43
|
+
"together": "Together AI - Open source models at scale",
|
|
44
|
+
"github_copilot": "GitHub Copilot - Multiple models via GitHub subscription",
|
|
45
|
+
"deepseek": "DeepSeek - Powerful reasoning and coding models",
|
|
46
|
+
"kimi": "Kimi (Moonshot) - Long context models up to 128K",
|
|
47
|
+
"custom": "Custom - Any OpenAI-compatible API endpoint",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Environment variable names for API keys
|
|
51
|
+
PROVIDER_ENV_VARS = {
|
|
52
|
+
"openai": "OPENAI_API_KEY",
|
|
53
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
54
|
+
"gemini": "GOOGLE_API_KEY",
|
|
55
|
+
"local": None,
|
|
56
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
57
|
+
"mistral": "MISTRAL_API_KEY",
|
|
58
|
+
"groq": "GROQ_API_KEY",
|
|
59
|
+
"lmstudio": None,
|
|
60
|
+
"together": "TOGETHER_API_KEY",
|
|
61
|
+
"github_copilot": "GITHUB_TOKEN",
|
|
62
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
63
|
+
"kimi": "MOONSHOT_API_KEY",
|
|
64
|
+
"custom": "CUSTOM_API_KEY",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_provider(
|
|
69
|
+
name: str, model: str | None = None, api_key: str | None = None, **kwargs
|
|
70
|
+
) -> LLMProvider:
|
|
71
|
+
"""
|
|
72
|
+
Factory function to get a provider instance.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
name: Provider name (openai, anthropic, gemini, local)
|
|
76
|
+
model: Model name (optional, uses provider default)
|
|
77
|
+
api_key: API key (optional, uses env var)
|
|
78
|
+
**kwargs: Additional provider-specific arguments (e.g., base_url for local)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
An LLMProvider instance
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If provider name is unknown
|
|
85
|
+
"""
|
|
86
|
+
if name not in PROVIDERS:
|
|
87
|
+
available = ", ".join(PROVIDERS.keys())
|
|
88
|
+
raise ValueError(f"Unknown provider: {name}. Available: {available}")
|
|
89
|
+
|
|
90
|
+
provider_class = PROVIDERS[name]
|
|
91
|
+
|
|
92
|
+
init_kwargs = {}
|
|
93
|
+
if model:
|
|
94
|
+
init_kwargs["model"] = model
|
|
95
|
+
if api_key:
|
|
96
|
+
init_kwargs["api_key"] = api_key
|
|
97
|
+
init_kwargs.update(kwargs)
|
|
98
|
+
|
|
99
|
+
return provider_class(**init_kwargs)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_default_models(provider_name: str) -> list[str]:
|
|
103
|
+
"""Get default model list for a provider."""
|
|
104
|
+
if provider_name not in PROVIDERS:
|
|
105
|
+
return []
|
|
106
|
+
return PROVIDERS[provider_name].DEFAULT_MODELS
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
"LLMProvider",
|
|
111
|
+
"ModelInfo",
|
|
112
|
+
"OpenAIProvider",
|
|
113
|
+
"AnthropicProvider",
|
|
114
|
+
"GeminiProvider",
|
|
115
|
+
"LocalProvider",
|
|
116
|
+
"OpenRouterProvider",
|
|
117
|
+
"MistralProvider",
|
|
118
|
+
"GroqProvider",
|
|
119
|
+
"LMStudioProvider",
|
|
120
|
+
"TogetherProvider",
|
|
121
|
+
"GitHubCopilotProvider",
|
|
122
|
+
"DeepSeekProvider",
|
|
123
|
+
"KimiProvider",
|
|
124
|
+
"CustomProvider",
|
|
125
|
+
"get_provider",
|
|
126
|
+
"get_default_models",
|
|
127
|
+
"PROVIDERS",
|
|
128
|
+
"PROVIDER_INFO",
|
|
129
|
+
"PROVIDER_ENV_VARS",
|
|
130
|
+
]
|
providers/anthropic.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from anthropic import Anthropic
|
|
2
|
+
|
|
3
|
+
from providers.base import LLMProvider, ModelInfo
|
|
4
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AnthropicProvider(LLMProvider):
|
|
8
|
+
"""Anthropic/Claude provider with tool use support."""
|
|
9
|
+
|
|
10
|
+
name = "anthropic"
|
|
11
|
+
|
|
12
|
+
DEFAULT_MODELS = [
|
|
13
|
+
"claude-sonnet-4-20250514",
|
|
14
|
+
"claude-opus-4-20250514",
|
|
15
|
+
"claude-3-5-sonnet-20241022",
|
|
16
|
+
"claude-3-5-haiku-20241022",
|
|
17
|
+
"claude-3-opus-20240229",
|
|
18
|
+
"claude-3-sonnet-20240229",
|
|
19
|
+
"claude-3-haiku-20240307",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def __init__(self, model: str = "claude-sonnet-4-20250514", api_key: str | None = None):
|
|
23
|
+
self.client = Anthropic(api_key=api_key)
|
|
24
|
+
self.model = model
|
|
25
|
+
|
|
26
|
+
def list_models(self) -> list[ModelInfo]:
|
|
27
|
+
"""Return available Claude models."""
|
|
28
|
+
# Anthropic doesn't have a models list API, use defaults
|
|
29
|
+
return [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
30
|
+
|
|
31
|
+
def generate(
|
|
32
|
+
self,
|
|
33
|
+
messages: list[Message],
|
|
34
|
+
tools: list | None = None,
|
|
35
|
+
stream: bool = False,
|
|
36
|
+
) -> LLMResponse:
|
|
37
|
+
# Extract system message (Anthropic handles it separately)
|
|
38
|
+
system_content = None
|
|
39
|
+
formatted_messages = []
|
|
40
|
+
|
|
41
|
+
for msg in messages:
|
|
42
|
+
if msg.role == "system":
|
|
43
|
+
system_content = msg.content
|
|
44
|
+
elif msg.role == "tool":
|
|
45
|
+
# Anthropic expects tool results as user messages with tool_result blocks
|
|
46
|
+
formatted_messages.append(
|
|
47
|
+
{
|
|
48
|
+
"role": "user",
|
|
49
|
+
"content": [
|
|
50
|
+
{
|
|
51
|
+
"type": "tool_result",
|
|
52
|
+
"tool_use_id": msg.tool_call_id or msg.name,
|
|
53
|
+
"content": msg.content,
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
elif msg.role == "assistant":
|
|
59
|
+
# Check if this was a tool-calling message (has metadata with tool_use_blocks)
|
|
60
|
+
if msg.metadata and msg.metadata.get("tool_use_blocks"):
|
|
61
|
+
# Reconstruct assistant message with tool_use blocks
|
|
62
|
+
content = msg.metadata["tool_use_blocks"]
|
|
63
|
+
else:
|
|
64
|
+
content = msg.content
|
|
65
|
+
formatted_messages.append({"role": "assistant", "content": content})
|
|
66
|
+
else:
|
|
67
|
+
# User messages
|
|
68
|
+
formatted_messages.append({"role": msg.role, "content": msg.content})
|
|
69
|
+
|
|
70
|
+
# Format tools for Anthropic (uses input_schema instead of parameters)
|
|
71
|
+
formatted_tools = None
|
|
72
|
+
if tools:
|
|
73
|
+
formatted_tools = []
|
|
74
|
+
for tool in tools:
|
|
75
|
+
formatted_tools.append(
|
|
76
|
+
{
|
|
77
|
+
"name": tool["name"],
|
|
78
|
+
"description": tool["description"],
|
|
79
|
+
"input_schema": tool.get(
|
|
80
|
+
"parameters", {"type": "object", "properties": {}}
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Build request kwargs
|
|
86
|
+
kwargs = {
|
|
87
|
+
"model": self.model,
|
|
88
|
+
"max_tokens": 4096,
|
|
89
|
+
"messages": formatted_messages,
|
|
90
|
+
}
|
|
91
|
+
if system_content:
|
|
92
|
+
kwargs["system"] = system_content
|
|
93
|
+
if formatted_tools:
|
|
94
|
+
kwargs["tools"] = formatted_tools
|
|
95
|
+
|
|
96
|
+
response = self.client.messages.create(**kwargs)
|
|
97
|
+
|
|
98
|
+
# Parse response - Anthropic returns content as a list of blocks
|
|
99
|
+
tool_calls = []
|
|
100
|
+
text_content = ""
|
|
101
|
+
tool_use_blocks = [] # Store for reconstructing assistant message later
|
|
102
|
+
|
|
103
|
+
for block in response.content:
|
|
104
|
+
if block.type == "text":
|
|
105
|
+
text_content += block.text
|
|
106
|
+
elif block.type == "tool_use":
|
|
107
|
+
tool_call = ToolCall(
|
|
108
|
+
name=block.name,
|
|
109
|
+
arguments=block.input, # Already a dict, not JSON string
|
|
110
|
+
id=block.id,
|
|
111
|
+
)
|
|
112
|
+
tool_calls.append(tool_call)
|
|
113
|
+
# Store the block for message reconstruction in history
|
|
114
|
+
tool_use_blocks.append(
|
|
115
|
+
{"type": "tool_use", "id": block.id, "name": block.name, "input": block.input}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
content = text_content if text_content else "Calling tool..."
|
|
119
|
+
|
|
120
|
+
# Store tool_use_blocks in metadata for message reconstruction
|
|
121
|
+
metadata = {"tool_use_blocks": tool_use_blocks} if tool_use_blocks else None
|
|
122
|
+
|
|
123
|
+
return LLMResponse(
|
|
124
|
+
message=Message(role="assistant", content=content, metadata=metadata),
|
|
125
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
126
|
+
raw=response,
|
|
127
|
+
)
|
providers/base.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from schemas.message import LLMResponse, Message
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ModelInfo:
|
|
9
|
+
"""Information about an available model."""
|
|
10
|
+
|
|
11
|
+
id: str
|
|
12
|
+
name: str
|
|
13
|
+
context_length: int | None = None
|
|
14
|
+
description: str | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LLMProvider(ABC):
|
|
18
|
+
"""
|
|
19
|
+
Base interface for all LLM providers.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
model: str
|
|
24
|
+
|
|
25
|
+
# Default models for this provider (can be overridden)
|
|
26
|
+
DEFAULT_MODELS: list[str] = []
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def generate(
|
|
30
|
+
self,
|
|
31
|
+
messages: list[Message],
|
|
32
|
+
tools: list | None = None,
|
|
33
|
+
stream: bool = False,
|
|
34
|
+
) -> LLMResponse:
|
|
35
|
+
"""
|
|
36
|
+
Generate a response from the model.
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def list_models(self) -> list[ModelInfo]:
|
|
41
|
+
"""
|
|
42
|
+
List available models for this provider.
|
|
43
|
+
Override in subclasses to fetch from API.
|
|
44
|
+
Returns default models if not overridden.
|
|
45
|
+
"""
|
|
46
|
+
return [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_default_models(cls) -> list[str]:
|
|
50
|
+
"""Get list of default model IDs."""
|
|
51
|
+
return cls.DEFAULT_MODELS
|
providers/custom.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom provider for OpenAI-compatible APIs.
|
|
3
|
+
|
|
4
|
+
Allows users to connect to any OpenAI-compatible endpoint with custom configuration.
|
|
5
|
+
Perfect for self-hosted models, custom endpoints, or providers not officially supported.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from openai import OpenAI
|
|
12
|
+
|
|
13
|
+
from providers.base import LLMProvider, ModelInfo
|
|
14
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CustomProvider(LLMProvider):
|
|
18
|
+
"""
|
|
19
|
+
Generic OpenAI-compatible provider.
|
|
20
|
+
|
|
21
|
+
Can be configured to work with any API that implements the OpenAI chat completions format.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
- Self-hosted vLLM, TGI, or llama.cpp servers
|
|
25
|
+
- Custom model endpoints
|
|
26
|
+
- Provider-specific endpoints not officially supported
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name = "custom"
|
|
30
|
+
|
|
31
|
+
DEFAULT_MODELS = [] # User must specify model
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
model: str,
|
|
36
|
+
base_url: str,
|
|
37
|
+
api_key: str | None = None,
|
|
38
|
+
default_models: list[str] | None = None,
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
Initialize custom provider.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
model: Model name/ID to use
|
|
45
|
+
base_url: Base URL for the API endpoint (e.g., "http://localhost:8000/v1")
|
|
46
|
+
api_key: API key (optional, some self-hosted models don't need it)
|
|
47
|
+
default_models: List of available models (optional)
|
|
48
|
+
"""
|
|
49
|
+
if not base_url:
|
|
50
|
+
raise ValueError("base_url is required for custom provider")
|
|
51
|
+
|
|
52
|
+
if not model:
|
|
53
|
+
raise ValueError("model is required for custom provider")
|
|
54
|
+
|
|
55
|
+
# Use provided API key or "dummy" for self-hosted models that don't need auth
|
|
56
|
+
key = api_key or os.getenv("CUSTOM_API_KEY") or "dummy"
|
|
57
|
+
|
|
58
|
+
self.client = OpenAI(
|
|
59
|
+
api_key=key,
|
|
60
|
+
base_url=base_url,
|
|
61
|
+
)
|
|
62
|
+
self.model = model
|
|
63
|
+
self.base_url = base_url
|
|
64
|
+
|
|
65
|
+
# Update default models if provided
|
|
66
|
+
if default_models:
|
|
67
|
+
self.DEFAULT_MODELS = default_models
|
|
68
|
+
|
|
69
|
+
def list_models(self) -> list[ModelInfo]:
|
|
70
|
+
"""
|
|
71
|
+
List available models.
|
|
72
|
+
|
|
73
|
+
Tries to fetch from API, falls back to configured defaults.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
response = self.client.models.list()
|
|
77
|
+
models = []
|
|
78
|
+
for model in response.data:
|
|
79
|
+
models.append(
|
|
80
|
+
ModelInfo(
|
|
81
|
+
id=model.id,
|
|
82
|
+
name=model.id,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
return models if models else [ModelInfo(id=self.model, name=self.model)]
|
|
86
|
+
except Exception:
|
|
87
|
+
# If API doesn't support model listing, return configured model
|
|
88
|
+
if self.DEFAULT_MODELS:
|
|
89
|
+
return [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
90
|
+
return [ModelInfo(id=self.model, name=self.model)]
|
|
91
|
+
|
|
92
|
+
def generate(
|
|
93
|
+
self,
|
|
94
|
+
messages: list[Message],
|
|
95
|
+
tools: list | None = None,
|
|
96
|
+
stream: bool = False,
|
|
97
|
+
) -> LLMResponse:
|
|
98
|
+
# Format messages for OpenAI-compatible API
|
|
99
|
+
formatted_messages = []
|
|
100
|
+
for msg in messages:
|
|
101
|
+
if msg.role == "tool":
|
|
102
|
+
formatted_msg = {
|
|
103
|
+
"role": "tool",
|
|
104
|
+
"content": msg.content,
|
|
105
|
+
"tool_call_id": msg.tool_call_id or msg.name,
|
|
106
|
+
}
|
|
107
|
+
elif msg.role == "assistant" and msg.metadata and msg.metadata.get("tool_calls"):
|
|
108
|
+
# Preserve tool_calls in assistant messages
|
|
109
|
+
formatted_msg = {
|
|
110
|
+
"role": "assistant",
|
|
111
|
+
"content": msg.content or "",
|
|
112
|
+
"tool_calls": msg.metadata["tool_calls"],
|
|
113
|
+
}
|
|
114
|
+
else:
|
|
115
|
+
formatted_msg = {"role": msg.role, "content": msg.content}
|
|
116
|
+
formatted_messages.append(formatted_msg)
|
|
117
|
+
|
|
118
|
+
# Format tools for OpenAI-compatible API
|
|
119
|
+
formatted_tools = None
|
|
120
|
+
if tools:
|
|
121
|
+
formatted_tools = []
|
|
122
|
+
for tool in tools:
|
|
123
|
+
formatted_tools.append(
|
|
124
|
+
{
|
|
125
|
+
"type": "function",
|
|
126
|
+
"function": {
|
|
127
|
+
"name": tool["name"],
|
|
128
|
+
"description": tool["description"],
|
|
129
|
+
"parameters": tool.get(
|
|
130
|
+
"parameters", {"type": "object", "properties": {}}
|
|
131
|
+
),
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Make the API call
|
|
137
|
+
try:
|
|
138
|
+
response = self.client.chat.completions.create(
|
|
139
|
+
model=self.model,
|
|
140
|
+
messages=formatted_messages,
|
|
141
|
+
tools=formatted_tools if formatted_tools else None,
|
|
142
|
+
temperature=0.0,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
choice = response.choices[0]
|
|
146
|
+
message = choice.message
|
|
147
|
+
|
|
148
|
+
tool_calls = []
|
|
149
|
+
if message.tool_calls:
|
|
150
|
+
for tc in message.tool_calls:
|
|
151
|
+
tool_calls.append(
|
|
152
|
+
ToolCall(
|
|
153
|
+
id=tc.id,
|
|
154
|
+
name=tc.function.name,
|
|
155
|
+
arguments=json.loads(tc.function.arguments),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return LLMResponse(
|
|
160
|
+
content=message.content or "",
|
|
161
|
+
tool_calls=tool_calls,
|
|
162
|
+
metadata={
|
|
163
|
+
"model": response.model,
|
|
164
|
+
"usage": {
|
|
165
|
+
"prompt_tokens": response.usage.prompt_tokens if response.usage else 0,
|
|
166
|
+
"completion_tokens": (
|
|
167
|
+
response.usage.completion_tokens if response.usage else 0
|
|
168
|
+
),
|
|
169
|
+
"total_tokens": response.usage.total_tokens if response.usage else 0,
|
|
170
|
+
},
|
|
171
|
+
"tool_calls": [
|
|
172
|
+
{
|
|
173
|
+
"id": tc.id,
|
|
174
|
+
"type": "function",
|
|
175
|
+
"function": {
|
|
176
|
+
"name": tc.function.name,
|
|
177
|
+
"arguments": tc.function.arguments,
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
for tc in (message.tool_calls or [])
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise RuntimeError(f"Error calling custom API at {self.base_url}: {e}") from e
|
providers/deepseek.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeepSeek AI provider.
|
|
3
|
+
|
|
4
|
+
DeepSeek provides powerful reasoning and coding models with OpenAI-compatible API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from openai import OpenAI
|
|
10
|
+
|
|
11
|
+
from providers.base import LLMProvider, ModelInfo
|
|
12
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DeepSeekProvider(LLMProvider):
|
|
16
|
+
name = "deepseek"
|
|
17
|
+
|
|
18
|
+
DEFAULT_MODELS = [
|
|
19
|
+
"deepseek-chat",
|
|
20
|
+
"deepseek-coder",
|
|
21
|
+
"deepseek-reasoner",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
model: str = "deepseek-chat",
|
|
27
|
+
api_key: str | None = None,
|
|
28
|
+
base_url: str = "https://api.deepseek.com",
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize DeepSeek provider.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
model: Model to use (deepseek-chat, deepseek-coder, deepseek-reasoner)
|
|
35
|
+
api_key: DeepSeek API key (defaults to DEEPSEEK_API_KEY env var)
|
|
36
|
+
base_url: Base URL for the API
|
|
37
|
+
"""
|
|
38
|
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
|
39
|
+
self.model = model
|
|
40
|
+
|
|
41
|
+
def list_models(self) -> list[ModelInfo]:
|
|
42
|
+
"""Fetch available models from DeepSeek API."""
|
|
43
|
+
try:
|
|
44
|
+
response = self.client.models.list()
|
|
45
|
+
models = []
|
|
46
|
+
for model in response.data:
|
|
47
|
+
models.append(
|
|
48
|
+
ModelInfo(
|
|
49
|
+
id=model.id,
|
|
50
|
+
name=model.id,
|
|
51
|
+
description=getattr(model, "description", None),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return models if models else self._get_default_models()
|
|
55
|
+
except Exception:
|
|
56
|
+
# Fallback to default models if API call fails
|
|
57
|
+
return self._get_default_models()
|
|
58
|
+
|
|
59
|
+
def _get_default_models(self) -> list[ModelInfo]:
|
|
60
|
+
"""Get hardcoded default models as fallback."""
|
|
61
|
+
return [
|
|
62
|
+
ModelInfo(
|
|
63
|
+
id="deepseek-chat",
|
|
64
|
+
name="DeepSeek Chat",
|
|
65
|
+
context_length=64000,
|
|
66
|
+
description="General-purpose conversational model",
|
|
67
|
+
),
|
|
68
|
+
ModelInfo(
|
|
69
|
+
id="deepseek-coder",
|
|
70
|
+
name="DeepSeek Coder",
|
|
71
|
+
context_length=64000,
|
|
72
|
+
description="Specialized coding model",
|
|
73
|
+
),
|
|
74
|
+
ModelInfo(
|
|
75
|
+
id="deepseek-reasoner",
|
|
76
|
+
name="DeepSeek Reasoner",
|
|
77
|
+
context_length=64000,
|
|
78
|
+
description="Advanced reasoning model (R1 series)",
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
def generate(
|
|
83
|
+
self,
|
|
84
|
+
messages: list[Message],
|
|
85
|
+
tools: list | None = None,
|
|
86
|
+
stream: bool = False,
|
|
87
|
+
) -> LLMResponse:
|
|
88
|
+
# Format messages for OpenAI-compatible API
|
|
89
|
+
formatted_messages = []
|
|
90
|
+
for msg in messages:
|
|
91
|
+
if msg.role == "tool":
|
|
92
|
+
formatted_msg = {
|
|
93
|
+
"role": "tool",
|
|
94
|
+
"content": msg.content,
|
|
95
|
+
"tool_call_id": msg.tool_call_id or msg.name,
|
|
96
|
+
}
|
|
97
|
+
elif msg.role == "assistant" and msg.metadata and msg.metadata.get("tool_calls"):
|
|
98
|
+
formatted_msg = {
|
|
99
|
+
"role": "assistant",
|
|
100
|
+
"content": msg.content or "",
|
|
101
|
+
"tool_calls": msg.metadata["tool_calls"],
|
|
102
|
+
}
|
|
103
|
+
else:
|
|
104
|
+
formatted_msg = {"role": msg.role, "content": msg.content}
|
|
105
|
+
formatted_messages.append(formatted_msg)
|
|
106
|
+
|
|
107
|
+
# Format tools
|
|
108
|
+
formatted_tools = None
|
|
109
|
+
if tools:
|
|
110
|
+
formatted_tools = []
|
|
111
|
+
for tool in tools:
|
|
112
|
+
formatted_tools.append(
|
|
113
|
+
{
|
|
114
|
+
"type": "function",
|
|
115
|
+
"function": {
|
|
116
|
+
"name": tool["name"],
|
|
117
|
+
"description": tool["description"],
|
|
118
|
+
"parameters": tool.get(
|
|
119
|
+
"parameters", {"type": "object", "properties": {}}
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
response = self.client.chat.completions.create(
|
|
126
|
+
model=self.model,
|
|
127
|
+
messages=formatted_messages,
|
|
128
|
+
tools=formatted_tools,
|
|
129
|
+
temperature=0.0,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
choice = response.choices[0]
|
|
133
|
+
message = choice.message
|
|
134
|
+
|
|
135
|
+
tool_calls = []
|
|
136
|
+
if message.tool_calls:
|
|
137
|
+
for tc in message.tool_calls:
|
|
138
|
+
tool_calls.append(
|
|
139
|
+
ToolCall(
|
|
140
|
+
id=tc.id,
|
|
141
|
+
name=tc.function.name,
|
|
142
|
+
arguments=json.loads(tc.function.arguments),
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return LLMResponse(
|
|
147
|
+
content=message.content or "",
|
|
148
|
+
tool_calls=tool_calls,
|
|
149
|
+
metadata={
|
|
150
|
+
"model": response.model,
|
|
151
|
+
"usage": {
|
|
152
|
+
"prompt_tokens": response.usage.prompt_tokens if response.usage else 0,
|
|
153
|
+
"completion_tokens": (
|
|
154
|
+
response.usage.completion_tokens if response.usage else 0
|
|
155
|
+
),
|
|
156
|
+
"total_tokens": response.usage.total_tokens if response.usage else 0,
|
|
157
|
+
},
|
|
158
|
+
"tool_calls": [
|
|
159
|
+
{
|
|
160
|
+
"id": tc.id,
|
|
161
|
+
"type": "function",
|
|
162
|
+
"function": {
|
|
163
|
+
"name": tc.function.name,
|
|
164
|
+
"arguments": tc.function.arguments,
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
for tc in (message.tool_calls or [])
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
)
|