gptcmd 2.3.4__tar.gz → 2.3.5__tar.gz
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.
- {gptcmd-2.3.4 → gptcmd-2.3.5}/PKG-INFO +1 -1
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd/__init__.py +1 -1
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd/llm/openai.py +6 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd.egg-info/PKG-INFO +1 -1
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd.egg-info/SOURCES.txt +3 -1
- gptcmd-2.3.5/tests/test_cli.py +81 -0
- gptcmd-2.3.5/tests/test_openai.py +155 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/COPYING.txt +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/README.md +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/pyproject.toml +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/setup.cfg +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd/cli.py +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd/config.py +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd/config_sample.toml +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd/llm/__init__.py +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd/macros.py +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd/message.py +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd.egg-info/dependency_links.txt +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd.egg-info/entry_points.txt +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd.egg-info/requires.txt +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/src/gptcmd.egg-info/top_level.txt +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/tests/test_llm.py +0 -0
- {gptcmd-2.3.4 → gptcmd-2.3.5}/tests/test_message.py +0 -0
|
@@ -30,6 +30,11 @@ ModelCostInfo = namedtuple(
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
OPENAI_COSTS: Dict[str, ModelCostInfo] = {
|
|
33
|
+
"gpt-5.4-2026-03-05": ModelCostInfo(
|
|
34
|
+
Decimal("2.5") / Decimal("1000000"),
|
|
35
|
+
Decimal("15") / Decimal("1000000"),
|
|
36
|
+
Decimal("0.1"),
|
|
37
|
+
),
|
|
33
38
|
"gpt-5.2-2025-12-11": ModelCostInfo(
|
|
34
39
|
Decimal("1.75") / Decimal("1000000"),
|
|
35
40
|
Decimal("14") / Decimal("1000000"),
|
|
@@ -371,6 +376,7 @@ class OpenAI(LLMProvider):
|
|
|
371
376
|
|
|
372
377
|
def get_best_model(self):
|
|
373
378
|
BEST_MODELS = (
|
|
379
|
+
"gpt-5.4",
|
|
374
380
|
"gpt-5.2",
|
|
375
381
|
"gpt-5.1",
|
|
376
382
|
"gpt-5",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from typing import Dict, Sequence
|
|
5
|
+
|
|
6
|
+
from gptcmd.cli import Gptcmd
|
|
7
|
+
from gptcmd.llm import LLMProvider, LLMProviderFeature, LLMResponse
|
|
8
|
+
from gptcmd.message import Message, MessageRole
|
|
9
|
+
from gptcmd.tools import ToolDefinition, ToolKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ToolCompletionProvider(LLMProvider):
|
|
13
|
+
SUPPORTED_FEATURES = LLMProviderFeature.TOOL_CALLING
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self._tools_available = (
|
|
17
|
+
ToolDefinition(
|
|
18
|
+
name="web_search",
|
|
19
|
+
description="Search tool",
|
|
20
|
+
kind=ToolKind.PROVIDER_HOSTED,
|
|
21
|
+
input_schema={
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {},
|
|
24
|
+
"additionalProperties": True,
|
|
25
|
+
},
|
|
26
|
+
provider_payload={"type": "web_search_preview"},
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
super().__init__(model="dummy-1")
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_config(cls, conf: Dict):
|
|
33
|
+
return cls()
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def available_tools(self):
|
|
37
|
+
return self._tools_available
|
|
38
|
+
|
|
39
|
+
def complete(self, messages: Sequence[Message]) -> LLMResponse:
|
|
40
|
+
return LLMResponse(
|
|
41
|
+
Message(content="ok", role=MessageRole.ASSISTANT)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def validate_api_params(self, params):
|
|
45
|
+
return params
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def valid_models(self):
|
|
49
|
+
return ("dummy-1",)
|
|
50
|
+
|
|
51
|
+
def get_best_model(self):
|
|
52
|
+
return "dummy-1"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestToolCompletion(unittest.TestCase):
|
|
56
|
+
def setUp(self):
|
|
57
|
+
self.cmd = Gptcmd.__new__(Gptcmd)
|
|
58
|
+
self.cmd._account = SimpleNamespace(provider=ToolCompletionProvider())
|
|
59
|
+
|
|
60
|
+
def test_complete_tool_name_when_partial_first_argument(self):
|
|
61
|
+
result = self.cmd.complete_tool(
|
|
62
|
+
text="we",
|
|
63
|
+
line="tool we",
|
|
64
|
+
begidx=5,
|
|
65
|
+
endidx=7,
|
|
66
|
+
)
|
|
67
|
+
self.assertEqual(result, ["web_search"])
|
|
68
|
+
|
|
69
|
+
def test_complete_policy_on_second_argument(self):
|
|
70
|
+
result = self.cmd.complete_tool(
|
|
71
|
+
text="a",
|
|
72
|
+
line="tool web_search a",
|
|
73
|
+
begidx=16,
|
|
74
|
+
endidx=17,
|
|
75
|
+
)
|
|
76
|
+
self.assertIn("always", result)
|
|
77
|
+
self.assertIn("ask", result)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
unittest.main()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from typing import Dict, Sequence
|
|
5
|
+
|
|
6
|
+
from gptcmd.llm.openai import OpenAI
|
|
7
|
+
from gptcmd.message import Message, MessageRole
|
|
8
|
+
from gptcmd.tools import (
|
|
9
|
+
ToolContext,
|
|
10
|
+
ToolExecutionPolicy,
|
|
11
|
+
ToolSettings,
|
|
12
|
+
callable_to_tool_definition,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _sum_numbers(a: int, b: int, ctx: ToolContext) -> Dict[str, int]:
|
|
17
|
+
return {"sum": a + b}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _usage(
|
|
21
|
+
prompt_tokens: int,
|
|
22
|
+
completion_tokens: int,
|
|
23
|
+
cached_tokens: int,
|
|
24
|
+
) -> SimpleNamespace:
|
|
25
|
+
return SimpleNamespace(
|
|
26
|
+
prompt_tokens=prompt_tokens,
|
|
27
|
+
prompt_tokens_details=SimpleNamespace(cached_tokens=cached_tokens),
|
|
28
|
+
completion_tokens=completion_tokens,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _choice(
|
|
33
|
+
*,
|
|
34
|
+
role: str,
|
|
35
|
+
content: str,
|
|
36
|
+
tool_calls: Sequence[SimpleNamespace],
|
|
37
|
+
) -> SimpleNamespace:
|
|
38
|
+
return SimpleNamespace(
|
|
39
|
+
message=SimpleNamespace(
|
|
40
|
+
role=role,
|
|
41
|
+
content=content,
|
|
42
|
+
tool_calls=list(tool_calls),
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _response(
|
|
48
|
+
*,
|
|
49
|
+
model: str,
|
|
50
|
+
prompt_tokens: int,
|
|
51
|
+
cached_tokens: int,
|
|
52
|
+
completion_tokens: int,
|
|
53
|
+
choices: Sequence[SimpleNamespace],
|
|
54
|
+
) -> SimpleNamespace:
|
|
55
|
+
return SimpleNamespace(
|
|
56
|
+
model=model,
|
|
57
|
+
usage=_usage(
|
|
58
|
+
prompt_tokens=prompt_tokens,
|
|
59
|
+
completion_tokens=completion_tokens,
|
|
60
|
+
cached_tokens=cached_tokens,
|
|
61
|
+
),
|
|
62
|
+
choices=list(choices),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _FakeCompletions:
|
|
67
|
+
def __init__(self, responses):
|
|
68
|
+
self.responses = list(responses)
|
|
69
|
+
self.calls = []
|
|
70
|
+
|
|
71
|
+
def create(self, **kwargs):
|
|
72
|
+
self.calls.append(kwargs)
|
|
73
|
+
if not self.responses:
|
|
74
|
+
raise AssertionError("No fake responses remain")
|
|
75
|
+
return self.responses.pop(0)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _FakeClient:
|
|
79
|
+
def __init__(self, responses):
|
|
80
|
+
self.models = SimpleNamespace(
|
|
81
|
+
list=lambda: SimpleNamespace(
|
|
82
|
+
data=[SimpleNamespace(id="gpt-4o-2024-11-20")]
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
self.chat = SimpleNamespace(
|
|
86
|
+
completions=_FakeCompletions(responses)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestOpenAIToolUsage(unittest.TestCase):
|
|
91
|
+
def test_usage_is_accumulated_across_tool_iterations(self):
|
|
92
|
+
model = "gpt-4o-2024-11-20"
|
|
93
|
+
tool_call = SimpleNamespace(
|
|
94
|
+
type="function",
|
|
95
|
+
id="call-1",
|
|
96
|
+
function=SimpleNamespace(
|
|
97
|
+
name="sum_numbers",
|
|
98
|
+
arguments='{"a": 2, "b": 3}',
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
responses = [
|
|
102
|
+
_response(
|
|
103
|
+
model=model,
|
|
104
|
+
prompt_tokens=10,
|
|
105
|
+
cached_tokens=1,
|
|
106
|
+
completion_tokens=2,
|
|
107
|
+
choices=[
|
|
108
|
+
_choice(
|
|
109
|
+
role="assistant",
|
|
110
|
+
content="",
|
|
111
|
+
tool_calls=[tool_call],
|
|
112
|
+
)
|
|
113
|
+
],
|
|
114
|
+
),
|
|
115
|
+
_response(
|
|
116
|
+
model=model,
|
|
117
|
+
prompt_tokens=6,
|
|
118
|
+
cached_tokens=2,
|
|
119
|
+
completion_tokens=4,
|
|
120
|
+
choices=[
|
|
121
|
+
_choice(
|
|
122
|
+
role="assistant",
|
|
123
|
+
content="done",
|
|
124
|
+
tool_calls=[],
|
|
125
|
+
)
|
|
126
|
+
],
|
|
127
|
+
),
|
|
128
|
+
]
|
|
129
|
+
provider = OpenAI(client=_FakeClient(responses), model=model)
|
|
130
|
+
tool = callable_to_tool_definition("sum_numbers", _sum_numbers)
|
|
131
|
+
|
|
132
|
+
resp = provider._complete_with_tools(
|
|
133
|
+
messages=[Message(content="Please sum", role=MessageRole.USER)],
|
|
134
|
+
tool_map={"sum_numbers": tool},
|
|
135
|
+
active_tools={
|
|
136
|
+
"sum_numbers": ToolSettings(
|
|
137
|
+
policy=ToolExecutionPolicy.ALWAYS
|
|
138
|
+
)
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
self.assertEqual(resp.message.content, "done")
|
|
143
|
+
self.assertEqual(resp.prompt_tokens, 16)
|
|
144
|
+
self.assertEqual(resp.sampled_tokens, 6)
|
|
145
|
+
expected_cost = provider.__class__._estimate_cost_in_cents(
|
|
146
|
+
model=model,
|
|
147
|
+
prompt_tokens=16,
|
|
148
|
+
cached_prompt_tokens=3,
|
|
149
|
+
sampled_tokens=6,
|
|
150
|
+
)
|
|
151
|
+
self.assertEqual(resp.cost_in_cents, expected_cost)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|