janito 2.1.1__py3-none-any.whl → 2.3.0__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.
- janito/__init__.py +6 -6
- janito/agent/setup_agent.py +14 -5
- janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +3 -1
- janito/cli/chat_mode/bindings.py +6 -0
- janito/cli/chat_mode/session.py +16 -0
- janito/cli/chat_mode/shell/autocomplete.py +21 -21
- janito/cli/chat_mode/shell/commands/__init__.py +3 -2
- janito/cli/chat_mode/shell/commands/clear.py +12 -12
- janito/cli/chat_mode/shell/commands/exec.py +27 -0
- janito/cli/chat_mode/shell/commands/multi.py +51 -51
- janito/cli/chat_mode/shell/commands/tools.py +17 -6
- janito/cli/chat_mode/shell/input_history.py +62 -62
- janito/cli/chat_mode/shell/session/manager.py +1 -0
- janito/cli/chat_mode/toolbar.py +3 -1
- janito/cli/cli_commands/list_models.py +35 -35
- janito/cli/cli_commands/list_providers.py +9 -9
- janito/cli/cli_commands/list_tools.py +53 -53
- janito/cli/cli_commands/model_selection.py +50 -50
- janito/cli/cli_commands/model_utils.py +13 -2
- janito/cli/cli_commands/set_api_key.py +19 -19
- janito/cli/cli_commands/show_config.py +51 -51
- janito/cli/cli_commands/show_system_prompt.py +62 -62
- janito/cli/config.py +2 -1
- janito/cli/core/__init__.py +4 -4
- janito/cli/core/event_logger.py +59 -59
- janito/cli/core/getters.py +3 -1
- janito/cli/core/runner.py +27 -6
- janito/cli/core/setters.py +5 -1
- janito/cli/core/unsetters.py +54 -54
- janito/cli/main_cli.py +12 -1
- janito/cli/prompt_core.py +5 -2
- janito/cli/rich_terminal_reporter.py +22 -3
- janito/cli/single_shot_mode/__init__.py +6 -6
- janito/cli/single_shot_mode/handler.py +11 -1
- janito/cli/verbose_output.py +1 -1
- janito/config.py +5 -5
- janito/config_manager.py +2 -0
- janito/driver_events.py +14 -0
- janito/drivers/anthropic/driver.py +113 -113
- janito/drivers/azure_openai/driver.py +38 -3
- janito/drivers/driver_registry.py +0 -2
- janito/drivers/openai/driver.py +196 -36
- janito/formatting_token.py +54 -54
- janito/i18n/__init__.py +35 -35
- janito/i18n/messages.py +23 -23
- janito/i18n/pt.py +47 -47
- janito/llm/__init__.py +5 -5
- janito/llm/agent.py +443 -443
- janito/llm/auth.py +1 -0
- janito/llm/driver.py +7 -1
- janito/llm/driver_config.py +1 -0
- janito/llm/driver_config_builder.py +34 -34
- janito/llm/driver_input.py +12 -12
- janito/llm/message_parts.py +60 -60
- janito/llm/model.py +38 -38
- janito/llm/provider.py +196 -196
- janito/provider_config.py +7 -3
- janito/provider_registry.py +29 -5
- janito/providers/__init__.py +1 -0
- janito/providers/anthropic/model_info.py +22 -22
- janito/providers/anthropic/provider.py +2 -2
- janito/providers/azure_openai/model_info.py +7 -6
- janito/providers/azure_openai/provider.py +44 -2
- janito/providers/deepseek/__init__.py +1 -1
- janito/providers/deepseek/model_info.py +16 -16
- janito/providers/deepseek/provider.py +91 -91
- janito/providers/google/model_info.py +21 -29
- janito/providers/google/provider.py +49 -38
- janito/providers/mistralai/provider.py +2 -2
- janito/providers/openai/model_info.py +0 -11
- janito/providers/openai/provider.py +1 -1
- janito/providers/provider_static_info.py +2 -3
- janito/providers/registry.py +26 -26
- janito/tools/adapters/__init__.py +1 -1
- janito/tools/adapters/local/__init__.py +62 -62
- janito/tools/adapters/local/adapter.py +33 -11
- janito/tools/adapters/local/ask_user.py +102 -102
- janito/tools/adapters/local/copy_file.py +84 -84
- janito/tools/adapters/local/create_directory.py +69 -69
- janito/tools/adapters/local/create_file.py +82 -82
- janito/tools/adapters/local/delete_text_in_file.py +4 -7
- janito/tools/adapters/local/fetch_url.py +97 -97
- janito/tools/adapters/local/find_files.py +138 -140
- janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
- janito/tools/adapters/local/get_file_outline/core.py +117 -151
- janito/tools/adapters/local/get_file_outline/java_outline.py +40 -0
- janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
- janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
- janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
- janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
- janito/tools/adapters/local/move_file.py +3 -13
- janito/tools/adapters/local/open_html_in_browser.py +24 -29
- janito/tools/adapters/local/open_url.py +3 -2
- janito/tools/adapters/local/python_code_run.py +166 -166
- janito/tools/adapters/local/python_command_run.py +164 -164
- janito/tools/adapters/local/python_file_run.py +163 -163
- janito/tools/adapters/local/remove_directory.py +6 -17
- janito/tools/adapters/local/remove_file.py +9 -15
- janito/tools/adapters/local/replace_text_in_file.py +6 -9
- janito/tools/adapters/local/run_bash_command.py +176 -176
- janito/tools/adapters/local/run_powershell_command.py +219 -219
- janito/tools/adapters/local/search_text/__init__.py +1 -1
- janito/tools/adapters/local/search_text/core.py +201 -201
- janito/tools/adapters/local/search_text/match_lines.py +1 -1
- janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
- janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
- janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
- janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
- janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
- janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
- janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
- janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
- janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
- janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
- janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
- janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
- janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
- janito/tools/adapters/local/view_file.py +167 -167
- janito/tools/inspect_registry.py +17 -17
- janito/tools/tool_base.py +105 -105
- janito/tools/tool_events.py +58 -58
- janito/tools/tool_run_exception.py +12 -12
- janito/tools/tool_use_tracker.py +81 -81
- janito/tools/tool_utils.py +45 -45
- janito/tools/tools_adapter.py +78 -6
- janito/tools/tools_schema.py +104 -104
- janito/version.py +4 -4
- {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/METADATA +388 -232
- janito-2.3.0.dist-info/RECORD +181 -0
- janito-2.3.0.dist-info/licenses/LICENSE +21 -0
- janito/cli/chat_mode/shell/commands/last.py +0 -137
- janito/drivers/google_genai/driver.py +0 -54
- janito/drivers/google_genai/schema_generator.py +0 -67
- janito-2.1.1.dist-info/RECORD +0 -181
- {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/WHEEL +0 -0
- {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/entry_points.txt +0 -0
- {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/top_level.txt +0 -0
@@ -1,113 +1,113 @@
|
|
1
|
-
from janito.llm.driver import LLMDriver
|
2
|
-
from janito.llm.driver_config import LLMDriverConfig
|
3
|
-
from janito.driver_events import (
|
4
|
-
GenerationStarted,
|
5
|
-
GenerationFinished,
|
6
|
-
RequestStarted,
|
7
|
-
RequestFinished,
|
8
|
-
ResponseReceived,
|
9
|
-
)
|
10
|
-
from janito.llm.message_parts import TextMessagePart
|
11
|
-
import uuid
|
12
|
-
import traceback
|
13
|
-
import time
|
14
|
-
|
15
|
-
# Safe import of anthropic SDK
|
16
|
-
try:
|
17
|
-
import anthropic
|
18
|
-
|
19
|
-
DRIVER_AVAILABLE = True
|
20
|
-
DRIVER_UNAVAILABLE_REASON = None
|
21
|
-
except ImportError:
|
22
|
-
DRIVER_AVAILABLE = False
|
23
|
-
DRIVER_UNAVAILABLE_REASON = "Missing dependency: anthropic (pip install anthropic)"
|
24
|
-
|
25
|
-
|
26
|
-
class AnthropicModelDriver(LLMDriver):
|
27
|
-
available = False
|
28
|
-
unavailable_reason = "AnthropicModelDriver is not implemented yet."
|
29
|
-
|
30
|
-
@classmethod
|
31
|
-
def is_available(cls):
|
32
|
-
return cls.available
|
33
|
-
|
34
|
-
"""
|
35
|
-
LLMDriver for Anthropic's Claude API (v3), using the anthropic SDK.
|
36
|
-
"""
|
37
|
-
required_config = ["api_key", "model"]
|
38
|
-
|
39
|
-
def __init__(self, tools_adapter=None):
|
40
|
-
raise ImportError(self.unavailable_reason)
|
41
|
-
|
42
|
-
def _create_client(self):
|
43
|
-
try:
|
44
|
-
import anthropic
|
45
|
-
except ImportError:
|
46
|
-
raise Exception(
|
47
|
-
"The 'anthropic' Python SDK is required. Please install via `pip install anthropic`."
|
48
|
-
)
|
49
|
-
return anthropic.Anthropic(api_key=self.api_key)
|
50
|
-
|
51
|
-
def _run_generation(
|
52
|
-
self, messages_or_prompt, system_prompt=None, tools=None, **kwargs
|
53
|
-
):
|
54
|
-
request_id = str(uuid.uuid4())
|
55
|
-
client = self._create_client()
|
56
|
-
try:
|
57
|
-
prompt = ""
|
58
|
-
if isinstance(messages_or_prompt, str):
|
59
|
-
prompt = messages_or_prompt
|
60
|
-
elif isinstance(messages_or_prompt, list):
|
61
|
-
chat = []
|
62
|
-
for msg in messages_or_prompt:
|
63
|
-
if msg.get("role") == "user":
|
64
|
-
chat.append("Human: " + msg.get("content", ""))
|
65
|
-
elif msg.get("role") == "assistant":
|
66
|
-
chat.append("Assistant: " + msg.get("content", ""))
|
67
|
-
prompt = "\n".join(chat)
|
68
|
-
if system_prompt:
|
69
|
-
prompt = f"System: {system_prompt}\n{prompt}"
|
70
|
-
|
71
|
-
self.publish(
|
72
|
-
GenerationStarted,
|
73
|
-
request_id,
|
74
|
-
conversation_history=list(getattr(self, "_history", [])),
|
75
|
-
)
|
76
|
-
self.publish(RequestStarted, request_id, payload={})
|
77
|
-
start_time = time.time()
|
78
|
-
response = client.completions.create(
|
79
|
-
model=self.model_name,
|
80
|
-
max_tokens_to_sample=int(getattr(self.config, "max_response", 1024)),
|
81
|
-
prompt=prompt,
|
82
|
-
temperature=float(getattr(self.config, "default_temp", 0.7)),
|
83
|
-
)
|
84
|
-
duration = time.time() - start_time
|
85
|
-
content = response.completion if hasattr(response, "completion") else None
|
86
|
-
self.publish(
|
87
|
-
RequestFinished,
|
88
|
-
request_id,
|
89
|
-
response=content,
|
90
|
-
status=RequestStatus.SUCCESS,
|
91
|
-
usage={},
|
92
|
-
)
|
93
|
-
parts = []
|
94
|
-
if content:
|
95
|
-
parts.append(TextMessagePart(content=content))
|
96
|
-
self.publish(
|
97
|
-
ResponseReceived,
|
98
|
-
request_id=request_id,
|
99
|
-
parts=parts,
|
100
|
-
tool_results=[],
|
101
|
-
timestamp=time.time(),
|
102
|
-
metadata={"raw_response": response},
|
103
|
-
)
|
104
|
-
self.publish(GenerationFinished, request_id, total_turns=1)
|
105
|
-
except Exception as e:
|
106
|
-
self.publish(
|
107
|
-
RequestFinished,
|
108
|
-
request_id,
|
109
|
-
status=RequestStatus.ERROR,
|
110
|
-
error=str(e),
|
111
|
-
exception=e,
|
112
|
-
traceback=traceback.format_exc(),
|
113
|
-
)
|
1
|
+
from janito.llm.driver import LLMDriver
|
2
|
+
from janito.llm.driver_config import LLMDriverConfig
|
3
|
+
from janito.driver_events import (
|
4
|
+
GenerationStarted,
|
5
|
+
GenerationFinished,
|
6
|
+
RequestStarted,
|
7
|
+
RequestFinished,
|
8
|
+
ResponseReceived,
|
9
|
+
)
|
10
|
+
from janito.llm.message_parts import TextMessagePart
|
11
|
+
import uuid
|
12
|
+
import traceback
|
13
|
+
import time
|
14
|
+
|
15
|
+
# Safe import of anthropic SDK
|
16
|
+
try:
|
17
|
+
import anthropic
|
18
|
+
|
19
|
+
DRIVER_AVAILABLE = True
|
20
|
+
DRIVER_UNAVAILABLE_REASON = None
|
21
|
+
except ImportError:
|
22
|
+
DRIVER_AVAILABLE = False
|
23
|
+
DRIVER_UNAVAILABLE_REASON = "Missing dependency: anthropic (pip install anthropic)"
|
24
|
+
|
25
|
+
|
26
|
+
class AnthropicModelDriver(LLMDriver):
|
27
|
+
available = False
|
28
|
+
unavailable_reason = "AnthropicModelDriver is not implemented yet."
|
29
|
+
|
30
|
+
@classmethod
|
31
|
+
def is_available(cls):
|
32
|
+
return cls.available
|
33
|
+
|
34
|
+
"""
|
35
|
+
LLMDriver for Anthropic's Claude API (v3), using the anthropic SDK.
|
36
|
+
"""
|
37
|
+
required_config = ["api_key", "model"]
|
38
|
+
|
39
|
+
def __init__(self, tools_adapter=None):
|
40
|
+
raise ImportError(self.unavailable_reason)
|
41
|
+
|
42
|
+
def _create_client(self):
|
43
|
+
try:
|
44
|
+
import anthropic
|
45
|
+
except ImportError:
|
46
|
+
raise Exception(
|
47
|
+
"The 'anthropic' Python SDK is required. Please install via `pip install anthropic`."
|
48
|
+
)
|
49
|
+
return anthropic.Anthropic(api_key=self.api_key)
|
50
|
+
|
51
|
+
def _run_generation(
|
52
|
+
self, messages_or_prompt, system_prompt=None, tools=None, **kwargs
|
53
|
+
):
|
54
|
+
request_id = str(uuid.uuid4())
|
55
|
+
client = self._create_client()
|
56
|
+
try:
|
57
|
+
prompt = ""
|
58
|
+
if isinstance(messages_or_prompt, str):
|
59
|
+
prompt = messages_or_prompt
|
60
|
+
elif isinstance(messages_or_prompt, list):
|
61
|
+
chat = []
|
62
|
+
for msg in messages_or_prompt:
|
63
|
+
if msg.get("role") == "user":
|
64
|
+
chat.append("Human: " + msg.get("content", ""))
|
65
|
+
elif msg.get("role") == "assistant":
|
66
|
+
chat.append("Assistant: " + msg.get("content", ""))
|
67
|
+
prompt = "\n".join(chat)
|
68
|
+
if system_prompt:
|
69
|
+
prompt = f"System: {system_prompt}\n{prompt}"
|
70
|
+
|
71
|
+
self.publish(
|
72
|
+
GenerationStarted,
|
73
|
+
request_id,
|
74
|
+
conversation_history=list(getattr(self, "_history", [])),
|
75
|
+
)
|
76
|
+
self.publish(RequestStarted, request_id, payload={})
|
77
|
+
start_time = time.time()
|
78
|
+
response = client.completions.create(
|
79
|
+
model=self.model_name,
|
80
|
+
max_tokens_to_sample=int(getattr(self.config, "max_response", 1024)),
|
81
|
+
prompt=prompt,
|
82
|
+
temperature=float(getattr(self.config, "default_temp", 0.7)),
|
83
|
+
)
|
84
|
+
duration = time.time() - start_time
|
85
|
+
content = response.completion if hasattr(response, "completion") else None
|
86
|
+
self.publish(
|
87
|
+
RequestFinished,
|
88
|
+
request_id,
|
89
|
+
response=content,
|
90
|
+
status=RequestStatus.SUCCESS,
|
91
|
+
usage={},
|
92
|
+
)
|
93
|
+
parts = []
|
94
|
+
if content:
|
95
|
+
parts.append(TextMessagePart(content=content))
|
96
|
+
self.publish(
|
97
|
+
ResponseReceived,
|
98
|
+
request_id=request_id,
|
99
|
+
parts=parts,
|
100
|
+
tool_results=[],
|
101
|
+
timestamp=time.time(),
|
102
|
+
metadata={"raw_response": response},
|
103
|
+
)
|
104
|
+
self.publish(GenerationFinished, request_id, total_turns=1)
|
105
|
+
except Exception as e:
|
106
|
+
self.publish(
|
107
|
+
RequestFinished,
|
108
|
+
request_id,
|
109
|
+
status=RequestStatus.ERROR,
|
110
|
+
error=str(e),
|
111
|
+
exception=e,
|
112
|
+
traceback=traceback.format_exc(),
|
113
|
+
)
|
@@ -14,6 +14,18 @@ from janito.llm.driver_config import LLMDriverConfig
|
|
14
14
|
|
15
15
|
|
16
16
|
class AzureOpenAIModelDriver(OpenAIModelDriver):
|
17
|
+
def start(self, *args, **kwargs):
|
18
|
+
# Ensure azure_deployment_name is set before starting
|
19
|
+
config = getattr(self, 'config', None)
|
20
|
+
deployment_name = None
|
21
|
+
if config and hasattr(config, 'extra'):
|
22
|
+
deployment_name = config.extra.get('azure_deployment_name')
|
23
|
+
if not deployment_name:
|
24
|
+
raise RuntimeError("AzureOpenAIModelDriver requires 'azure_deployment_name' to be set in config.extra['azure_deployment_name'] before starting.")
|
25
|
+
# Call parent start if exists
|
26
|
+
if hasattr(super(), 'start'):
|
27
|
+
return super().start(*args, **kwargs)
|
28
|
+
|
17
29
|
available = DRIVER_AVAILABLE
|
18
30
|
unavailable_reason = DRIVER_UNAVAILABLE_REASON
|
19
31
|
|
@@ -23,17 +35,39 @@ class AzureOpenAIModelDriver(OpenAIModelDriver):
|
|
23
35
|
|
24
36
|
required_config = {"base_url"} # Update key as used in your config logic
|
25
37
|
|
26
|
-
def __init__(self, tools_adapter=None):
|
38
|
+
def __init__(self, tools_adapter=None, provider_name=None):
|
27
39
|
if not self.available:
|
28
40
|
raise ImportError(
|
29
41
|
f"AzureOpenAIModelDriver unavailable: {self.unavailable_reason}"
|
30
42
|
)
|
31
|
-
#
|
32
|
-
|
43
|
+
# Ensure proper parent initialization
|
44
|
+
super().__init__(tools_adapter=tools_adapter, provider_name=provider_name)
|
33
45
|
self.azure_endpoint = None
|
34
46
|
self.api_version = None
|
35
47
|
self.api_key = None
|
36
48
|
|
49
|
+
def _prepare_api_kwargs(self, config, conversation):
|
50
|
+
"""
|
51
|
+
Prepares API kwargs for Azure OpenAI, using the deployment name as the model parameter.
|
52
|
+
Also ensures tool schemas are included if tools_adapter is present.
|
53
|
+
"""
|
54
|
+
api_kwargs = super()._prepare_api_kwargs(config, conversation)
|
55
|
+
deployment_name = config.extra.get("azure_deployment_name") if hasattr(config, "extra") else None
|
56
|
+
if deployment_name:
|
57
|
+
api_kwargs["model"] = deployment_name
|
58
|
+
# Patch: Ensure tools are included for Azure as for OpenAI
|
59
|
+
if self.tools_adapter:
|
60
|
+
try:
|
61
|
+
from janito.providers.openai.schema_generator import generate_tool_schemas
|
62
|
+
tool_classes = self.tools_adapter.get_tool_classes()
|
63
|
+
tool_schemas = generate_tool_schemas(tool_classes)
|
64
|
+
api_kwargs["tools"] = tool_schemas
|
65
|
+
except Exception as e:
|
66
|
+
api_kwargs["tools"] = []
|
67
|
+
if hasattr(config, "verbose_api") and config.verbose_api:
|
68
|
+
print(f"[AzureOpenAIModelDriver] Tool schema generation failed: {e}")
|
69
|
+
return api_kwargs
|
70
|
+
|
37
71
|
def _instantiate_openai_client(self, config):
|
38
72
|
try:
|
39
73
|
from openai import AzureOpenAI
|
@@ -45,6 +79,7 @@ class AzureOpenAIModelDriver(OpenAIModelDriver):
|
|
45
79
|
"azure_endpoint": getattr(config, "base_url", None),
|
46
80
|
"api_version": config.extra.get("api_version", "2023-05-15"),
|
47
81
|
}
|
82
|
+
# Do NOT pass azure_deployment; deployment name is used as the 'model' param in API calls
|
48
83
|
client = AzureOpenAI(**client_kwargs)
|
49
84
|
return client
|
50
85
|
except Exception as e:
|
@@ -8,14 +8,12 @@ from typing import Dict, Type
|
|
8
8
|
# --- Import driver classes ---
|
9
9
|
from janito.drivers.anthropic.driver import AnthropicModelDriver
|
10
10
|
from janito.drivers.azure_openai.driver import AzureOpenAIModelDriver
|
11
|
-
from janito.drivers.google_genai.driver import GoogleGenaiModelDriver
|
12
11
|
from janito.drivers.mistralai.driver import MistralAIModelDriver
|
13
12
|
from janito.drivers.openai.driver import OpenAIModelDriver
|
14
13
|
|
15
14
|
_DRIVER_REGISTRY: Dict[str, Type] = {
|
16
15
|
"AnthropicModelDriver": AnthropicModelDriver,
|
17
16
|
"AzureOpenAIModelDriver": AzureOpenAIModelDriver,
|
18
|
-
"GoogleGenaiModelDriver": GoogleGenaiModelDriver,
|
19
17
|
"MistralAIModelDriver": MistralAIModelDriver,
|
20
18
|
"OpenAIModelDriver": OpenAIModelDriver,
|
21
19
|
}
|
janito/drivers/openai/driver.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
import uuid
|
2
2
|
import traceback
|
3
3
|
from rich import pretty
|
4
|
+
import os
|
4
5
|
from janito.llm.driver import LLMDriver
|
5
6
|
from janito.llm.driver_input import DriverInput
|
6
|
-
from janito.driver_events import RequestFinished, RequestStatus
|
7
|
+
from janito.driver_events import RequestFinished, RequestStatus, RateLimitRetry
|
7
8
|
|
8
9
|
# Safe import of openai SDK
|
9
10
|
try:
|
@@ -70,6 +71,7 @@ class OpenAIModelDriver(LLMDriver):
|
|
70
71
|
"presence_penalty",
|
71
72
|
"frequency_penalty",
|
72
73
|
"stop",
|
74
|
+
"reasoning_effort",
|
73
75
|
):
|
74
76
|
v = getattr(config, p, None)
|
75
77
|
if v is not None:
|
@@ -79,6 +81,21 @@ class OpenAIModelDriver(LLMDriver):
|
|
79
81
|
return api_kwargs
|
80
82
|
|
81
83
|
def _call_api(self, driver_input: DriverInput):
|
84
|
+
"""Call the OpenAI-compatible chat completion endpoint.
|
85
|
+
|
86
|
+
Implements automatic retry logic when the provider returns a *retriable*
|
87
|
+
HTTP 429 or ``RESOURCE_EXHAUSTED`` error **that is not caused by quota
|
88
|
+
exhaustion**. A ``RateLimitRetry`` driver event is emitted each time a
|
89
|
+
retry is scheduled so that user-interfaces can inform the user about
|
90
|
+
the wait.
|
91
|
+
|
92
|
+
OpenAI uses the 429 status code both for temporary rate-limit errors *and*
|
93
|
+
for permanent quota-exceeded errors (``insufficient_quota``). Retrying
|
94
|
+
the latter is pointless, so we inspect the error payload for
|
95
|
+
``insufficient_quota`` or common quota-exceeded wording and treat those
|
96
|
+
as fatal, bubbling them up as a regular RequestFinished/ERROR instead of
|
97
|
+
emitting a RateLimitRetry.
|
98
|
+
"""
|
82
99
|
cancel_event = getattr(driver_input, "cancel_event", None)
|
83
100
|
config = driver_input.config
|
84
101
|
conversation = self.convert_history_to_api_messages(
|
@@ -86,45 +103,172 @@ class OpenAIModelDriver(LLMDriver):
|
|
86
103
|
)
|
87
104
|
request_id = getattr(config, "request_id", None)
|
88
105
|
if config.verbose_api:
|
106
|
+
tool_adapter_name = type(self.tools_adapter).__name__ if self.tools_adapter else None
|
107
|
+
tool_names = []
|
108
|
+
if self.tools_adapter and hasattr(self.tools_adapter, "list_tools"):
|
109
|
+
try:
|
110
|
+
tool_names = self.tools_adapter.list_tools()
|
111
|
+
except Exception:
|
112
|
+
tool_names = ["<error retrieving tools>"]
|
89
113
|
print(
|
90
|
-
f"[verbose-api] OpenAI API call about to be sent. Model: {config.model}, max_tokens: {config.max_tokens}, tools_adapter: {
|
114
|
+
f"[verbose-api] OpenAI API call about to be sent. Model: {config.model}, max_tokens: {config.max_tokens}, tools_adapter: {tool_adapter_name}, tool_names: {tool_names}",
|
91
115
|
flush=True,
|
92
116
|
)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
117
|
+
import time, re, json
|
118
|
+
|
119
|
+
client = self._instantiate_openai_client(config)
|
120
|
+
api_kwargs = self._prepare_api_kwargs(config, conversation)
|
121
|
+
max_retries = getattr(config, "max_retries", 3)
|
122
|
+
attempt = 1
|
123
|
+
while True:
|
124
|
+
try:
|
125
|
+
if config.verbose_api:
|
126
|
+
print(
|
127
|
+
f"[OpenAI] API CALL (attempt {attempt}/{max_retries}): chat.completions.create(**{api_kwargs})",
|
128
|
+
flush=True,
|
129
|
+
)
|
130
|
+
if self._check_cancel(cancel_event, request_id, before_call=True):
|
131
|
+
return None
|
132
|
+
result = client.chat.completions.create(**api_kwargs)
|
133
|
+
if self._check_cancel(cancel_event, request_id, before_call=False):
|
134
|
+
return None
|
135
|
+
# Success path
|
136
|
+
self._print_verbose_result(config, result)
|
137
|
+
usage_dict = self._extract_usage(result)
|
138
|
+
if config.verbose_api:
|
139
|
+
print(
|
140
|
+
f"[OpenAI][DEBUG] Attaching usage info to RequestFinished: {usage_dict}",
|
141
|
+
flush=True,
|
142
|
+
)
|
143
|
+
self.output_queue.put(
|
144
|
+
RequestFinished(
|
145
|
+
driver_name=self.__class__.__name__,
|
146
|
+
request_id=request_id,
|
147
|
+
response=result,
|
148
|
+
status=RequestStatus.SUCCESS,
|
149
|
+
usage=usage_dict,
|
150
|
+
)
|
100
151
|
)
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
return
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
152
|
+
if config.verbose_api:
|
153
|
+
pretty.install()
|
154
|
+
print("[OpenAI] API RESPONSE:", flush=True)
|
155
|
+
pretty.pprint(result)
|
156
|
+
return result
|
157
|
+
except Exception as e:
|
158
|
+
# Check for rate-limit errors (HTTP 429 or RESOURCE_EXHAUSTED)
|
159
|
+
status_code = getattr(e, "status_code", None)
|
160
|
+
err_str = str(e)
|
161
|
+
# Determine if this is a retriable rate-limit error (HTTP 429) or a non-retriable
|
162
|
+
# quota exhaustion error. OpenAI returns the same 429 status code for both, so we
|
163
|
+
# additionally check for the ``insufficient_quota`` code or typical quota-related
|
164
|
+
# strings in the error message. If the error is quota-related we treat it as fatal
|
165
|
+
# so that the caller can surface a proper error message instead of silently
|
166
|
+
# retrying forever.
|
167
|
+
lower_err = err_str.lower()
|
168
|
+
is_insufficient_quota = (
|
169
|
+
"insufficient_quota" in lower_err
|
170
|
+
or "exceeded your current quota" in lower_err
|
112
171
|
)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
request_id=request_id,
|
117
|
-
response=result,
|
118
|
-
status=RequestStatus.SUCCESS,
|
119
|
-
usage=usage_dict,
|
172
|
+
is_rate_limit = (
|
173
|
+
(status_code == 429 or "error code: 429" in lower_err or "resource_exhausted" in lower_err)
|
174
|
+
and not is_insufficient_quota
|
120
175
|
)
|
176
|
+
if not is_rate_limit or attempt > max_retries:
|
177
|
+
# If it's not a rate-limit error or we've exhausted retries, handle as fatal
|
178
|
+
self._handle_fatal_exception(e, config, api_kwargs)
|
179
|
+
# Parse retry delay from error message (default 1s)
|
180
|
+
retry_delay = self._extract_retry_delay_seconds(e)
|
181
|
+
if retry_delay is None:
|
182
|
+
# simple exponential backoff if not provided
|
183
|
+
retry_delay = min(2 ** (attempt - 1), 30)
|
184
|
+
# Emit RateLimitRetry event so UIs can show a spinner / message
|
185
|
+
self.output_queue.put(
|
186
|
+
RateLimitRetry(
|
187
|
+
driver_name=self.__class__.__name__,
|
188
|
+
request_id=request_id,
|
189
|
+
attempt=attempt,
|
190
|
+
retry_delay=retry_delay,
|
191
|
+
error=err_str,
|
192
|
+
details={},
|
193
|
+
)
|
194
|
+
)
|
195
|
+
if config.verbose_api:
|
196
|
+
print(
|
197
|
+
f"[OpenAI][RateLimit] Attempt {attempt}/{max_retries} failed with rate-limit. Waiting {retry_delay}s before retry.",
|
198
|
+
flush=True,
|
199
|
+
)
|
200
|
+
# Wait while still allowing cancellation
|
201
|
+
start_wait = time.time()
|
202
|
+
while time.time() - start_wait < retry_delay:
|
203
|
+
if self._check_cancel(cancel_event, request_id, before_call=False):
|
204
|
+
return None
|
205
|
+
time.sleep(0.1)
|
206
|
+
attempt += 1
|
207
|
+
continue
|
208
|
+
# console with large JSON payloads when the service returns HTTP 429.
|
209
|
+
# We still surface the exception to the caller so that standard error
|
210
|
+
# handling (e.g. retries in higher-level code) continues to work.
|
211
|
+
status_code = getattr(e, "status_code", None)
|
212
|
+
err_str = str(e)
|
213
|
+
is_rate_limit = (
|
214
|
+
status_code == 429
|
215
|
+
or "Error code: 429" in err_str
|
216
|
+
or "RESOURCE_EXHAUSTED" in err_str
|
121
217
|
)
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
218
|
+
is_verbose = getattr(config, "verbose_api", False)
|
219
|
+
|
220
|
+
# Only print the full diagnostics if the user explicitly requested
|
221
|
+
# verbose output or if the problem is not a rate-limit situation.
|
222
|
+
if is_verbose or not is_rate_limit:
|
223
|
+
print(f"[ERROR] Exception during OpenAI API call: {e}", flush=True)
|
224
|
+
print(f"[ERROR] config: {config}", flush=True)
|
225
|
+
print(
|
226
|
+
f"[ERROR] api_kwargs: {api_kwargs if 'api_kwargs' in locals() else 'N/A'}",
|
227
|
+
flush=True,
|
228
|
+
)
|
229
|
+
import traceback
|
230
|
+
|
231
|
+
print("[ERROR] Full stack trace:", flush=True)
|
232
|
+
print(traceback.format_exc(), flush=True)
|
233
|
+
# Re-raise so that the calling logic can convert this into a
|
234
|
+
# RequestFinished event with status=ERROR.
|
235
|
+
raise
|
236
|
+
|
237
|
+
def _extract_retry_delay_seconds(self, exception) -> float | None:
|
238
|
+
"""Extract the retry delay in seconds from the provider error response.
|
239
|
+
|
240
|
+
Handles both the Google Gemini style ``RetryInfo`` protobuf (where it's a
|
241
|
+
``retryDelay: '41s'`` string in JSON) and any number found after the word
|
242
|
+
``retryDelay``. Returns ``None`` if no delay could be parsed.
|
243
|
+
"""
|
244
|
+
import re, json, math
|
245
|
+
|
246
|
+
try:
|
247
|
+
# Some SDKs expose the raw response JSON on e.args[0]
|
248
|
+
if hasattr(exception, "response") and hasattr(exception.response, "text"):
|
249
|
+
payload = exception.response.text
|
250
|
+
else:
|
251
|
+
payload = str(exception)
|
252
|
+
# Look for 'retryDelay': '41s' or similar
|
253
|
+
m = re.search(r"retryDelay['\"]?\s*[:=]\s*['\"]?(\d+(?:\.\d+)?)(s)?", payload)
|
254
|
+
if m:
|
255
|
+
return float(m.group(1))
|
256
|
+
# Fallback: generic number of seconds in the message
|
257
|
+
m2 = re.search(r"(\d+(?:\.\d+)?)\s*s(?:econds)?", payload)
|
258
|
+
if m2:
|
259
|
+
return float(m2.group(1))
|
260
|
+
except Exception:
|
261
|
+
pass
|
262
|
+
return None
|
263
|
+
|
264
|
+
def _handle_fatal_exception(self, e, config, api_kwargs):
|
265
|
+
"""Common path for unrecoverable exceptions.
|
266
|
+
|
267
|
+
Prints diagnostics (respecting ``verbose_api``) then re-raises the
|
268
|
+
exception so standard error handling in ``LLMDriver`` continues.
|
269
|
+
"""
|
270
|
+
is_verbose = getattr(config, "verbose_api", False)
|
271
|
+
if is_verbose:
|
128
272
|
print(f"[ERROR] Exception during OpenAI API call: {e}", flush=True)
|
129
273
|
print(f"[ERROR] config: {config}", flush=True)
|
130
274
|
print(
|
@@ -132,10 +276,9 @@ class OpenAIModelDriver(LLMDriver):
|
|
132
276
|
flush=True,
|
133
277
|
)
|
134
278
|
import traceback
|
135
|
-
|
136
279
|
print("[ERROR] Full stack trace:", flush=True)
|
137
280
|
print(traceback.format_exc(), flush=True)
|
138
|
-
|
281
|
+
raise
|
139
282
|
|
140
283
|
def _instantiate_openai_client(self, config):
|
141
284
|
try:
|
@@ -145,6 +288,19 @@ class OpenAIModelDriver(LLMDriver):
|
|
145
288
|
client_kwargs = {"api_key": config.api_key}
|
146
289
|
if getattr(config, "base_url", None):
|
147
290
|
client_kwargs["base_url"] = config.base_url
|
291
|
+
|
292
|
+
# HTTP debug wrapper
|
293
|
+
if os.environ.get("OPENAI_DEBUG_HTTP", "0") == "1":
|
294
|
+
from http.client import HTTPConnection
|
295
|
+
HTTPConnection.debuglevel = 1
|
296
|
+
import logging
|
297
|
+
logging.basicConfig()
|
298
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
299
|
+
requests_log = logging.getLogger("http.client")
|
300
|
+
requests_log.setLevel(logging.DEBUG)
|
301
|
+
requests_log.propagate = True
|
302
|
+
print("[OpenAIModelDriver] HTTP debug enabled via OPENAI_DEBUG_HTTP=1", flush=True)
|
303
|
+
|
148
304
|
client = openai.OpenAI(**client_kwargs)
|
149
305
|
return client
|
150
306
|
except Exception as e:
|
@@ -289,7 +445,7 @@ class OpenAIModelDriver(LLMDriver):
|
|
289
445
|
except Exception:
|
290
446
|
tool_calls = []
|
291
447
|
api_messages.append(
|
292
|
-
{"role": "assistant", "content":
|
448
|
+
{"role": "assistant", "content": "", "tool_calls": tool_calls}
|
293
449
|
)
|
294
450
|
else:
|
295
451
|
# Special handling for 'function' role: extract 'name' from metadata if present
|
@@ -307,6 +463,10 @@ class OpenAIModelDriver(LLMDriver):
|
|
307
463
|
)
|
308
464
|
else:
|
309
465
|
api_messages.append(msg)
|
466
|
+
# Post-processing: Google Gemini API (OpenAI-compatible) rejects null content. Replace None with empty string.
|
467
|
+
for m in api_messages:
|
468
|
+
if m.get("content", None) is None:
|
469
|
+
m["content"] = ""
|
310
470
|
return api_messages
|
311
471
|
|
312
472
|
def _convert_completion_message_to_parts(self, message):
|