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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gptcmd
3
- Version: 2.3.4
3
+ Version: 2.3.5
4
4
  Summary: Command line GPT conversation and experimentation environment
5
5
  Author-email: Bill Dengler <codeofdusk@gmail.com>
6
6
  License-Expression: MPL-2.0
@@ -8,6 +8,6 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
8
8
 
9
9
  __all__ = ["__version__", "Gptcmd"]
10
10
 
11
- __version__ = "2.3.4"
11
+ __version__ = "2.3.5"
12
12
 
13
13
  from .cli import Gptcmd # noqa: E402
@@ -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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gptcmd
3
- Version: 2.3.4
3
+ Version: 2.3.5
4
4
  Summary: Command line GPT conversation and experimentation environment
5
5
  Author-email: Bill Dengler <codeofdusk@gmail.com>
6
6
  License-Expression: MPL-2.0
@@ -15,5 +15,7 @@ src/gptcmd.egg-info/requires.txt
15
15
  src/gptcmd.egg-info/top_level.txt
16
16
  src/gptcmd/llm/__init__.py
17
17
  src/gptcmd/llm/openai.py
18
+ tests/test_cli.py
18
19
  tests/test_llm.py
19
- tests/test_message.py
20
+ tests/test_message.py
21
+ tests/test_openai.py
@@ -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