gptcmd 2.3.4__tar.gz → 2.4.0__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.4.0
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
@@ -221,7 +221,7 @@ The first ten digits of pi are 3.141592653.
221
221
  With no arguments, the `user`, `assistant`, `system`, and `say` commands open an external text editor (based on your system or Gptcmd configuration) for message composition.
222
222
 
223
223
  ### Working with attachments
224
- OpenAI's latest models, such as `gppt-4o`, support images alongside text content. Images can be attached to messages with the `image` command, which accepts two arguments: the location of the image, either a URL or path to a local file; and the index of the message to which the image should be attached (if unspecified, it defaults to the last). We'll ask GPT to describe an image by creating a user message and attaching an image from Wikimedia Commons:
224
+ OpenAI's latest models, such as `gpt-4o`, support images alongside text content. Images can be attached to messages with the `image` command, which accepts two arguments: the location of the image, either a URL or path to a local file; and the index of the message to which the image should be attached (if unspecified, it defaults to the last). We'll ask GPT to describe an image by creating a user message and attaching an image from Wikimedia Commons:
225
225
 
226
226
  ```
227
227
  (gpt-4o) user What's in this image?
@@ -451,7 +451,7 @@ Unset all parameters
451
451
  ```
452
452
 
453
453
  ### Names
454
- GPT allows mesages to be annotated with the name of their author. The `name` command sets the name to be sent with all future messages of the specified role. Its first argument is the role to which this new name should be applied, and its second is the name to use:
454
+ GPT allows messages to be annotated with the name of their author. The `name` command sets the name to be sent with all future messages of the specified role. Its first argument is the role to which this new name should be applied, and its second is the name to use:
455
455
 
456
456
  ```
457
457
  (gpt-4o) name user Michael
@@ -201,7 +201,7 @@ The first ten digits of pi are 3.141592653.
201
201
  With no arguments, the `user`, `assistant`, `system`, and `say` commands open an external text editor (based on your system or Gptcmd configuration) for message composition.
202
202
 
203
203
  ### Working with attachments
204
- OpenAI's latest models, such as `gppt-4o`, support images alongside text content. Images can be attached to messages with the `image` command, which accepts two arguments: the location of the image, either a URL or path to a local file; and the index of the message to which the image should be attached (if unspecified, it defaults to the last). We'll ask GPT to describe an image by creating a user message and attaching an image from Wikimedia Commons:
204
+ OpenAI's latest models, such as `gpt-4o`, support images alongside text content. Images can be attached to messages with the `image` command, which accepts two arguments: the location of the image, either a URL or path to a local file; and the index of the message to which the image should be attached (if unspecified, it defaults to the last). We'll ask GPT to describe an image by creating a user message and attaching an image from Wikimedia Commons:
205
205
 
206
206
  ```
207
207
  (gpt-4o) user What's in this image?
@@ -431,7 +431,7 @@ Unset all parameters
431
431
  ```
432
432
 
433
433
  ### Names
434
- GPT allows mesages to be annotated with the name of their author. The `name` command sets the name to be sent with all future messages of the specified role. Its first argument is the role to which this new name should be applied, and its second is the name to use:
434
+ GPT allows messages to be annotated with the name of their author. The `name` command sets the name to be sent with all future messages of the specified role. Its first argument is the role to which this new name should be applied, and its second is the name to use:
435
435
 
436
436
  ```
437
437
  (gpt-4o) name user Michael
@@ -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.4.0"
12
12
 
13
13
  from .cli import Gptcmd # noqa: E402
@@ -18,6 +18,7 @@ import json
18
18
  import os
19
19
  import re
20
20
  import shlex
21
+ import signal
21
22
  import subprocess
22
23
  import sys
23
24
  import tempfile
@@ -816,7 +817,7 @@ class Gptcmd(cmd.Cmd):
816
817
  """
817
818
  Move the message at the beginning of a range to the end of that range.
818
819
  In other words, move <i> <j> moves the ith message of a thread to
819
- index j.
820
+ index j, using the same inclusive range syntax as other commands.
820
821
  """
821
822
  if not arg:
822
823
  print("Usage: move <from> <to>")
@@ -831,17 +832,22 @@ class Gptcmd(cmd.Cmd):
831
832
  length = len(self._current_thread.messages)
832
833
  if i is None:
833
834
  i = 0
835
+ elif i < 0:
836
+ i += length
837
+
834
838
  if j is None:
835
839
  j = length
836
- if i < 0:
837
- i += length
838
- if j < 0:
840
+ elif j < 0:
839
841
  j += length
840
- elif j > 0:
841
- j -= 1 # Adjust end for 1-based indexing
842
- if not (0 <= j <= length):
842
+ # Range parsing returns an exclusive end; move needs the included end.
843
+ j -= 1
844
+
845
+ if not (0 <= j < length):
843
846
  print("Destination out of bounds")
844
847
  return
848
+ if not (0 <= i < length):
849
+ print("Message doesn't exist")
850
+ return
845
851
  try:
846
852
  msg = self._current_thread.move(i, j)
847
853
  except IndexError:
@@ -1792,7 +1798,7 @@ class Gptcmd(cmd.Cmd):
1792
1798
  return can_exit # Truthy return values cause the cmdloop to stop
1793
1799
 
1794
1800
 
1795
- def _write_crash_dump(shell: Gptcmd, exc: Exception) -> Optional[str]:
1801
+ def _write_crash_dump(shell: Gptcmd, exc: BaseException) -> Optional[str]:
1796
1802
  """
1797
1803
  Serialize the current shell into a JSON file and return its absolute
1798
1804
  path.
@@ -1834,6 +1840,169 @@ def _write_crash_dump(shell: Gptcmd, exc: Exception) -> Optional[str]:
1834
1840
  shell._threads.pop(detached_key, None)
1835
1841
 
1836
1842
 
1843
+ def _shell_has_dirty_state(shell: Gptcmd) -> bool:
1844
+ "Return whether the shell has unsaved conversation state."
1845
+ return bool(
1846
+ (shell._detached and shell._detached.dirty)
1847
+ or any(t and t.dirty for t in shell._threads.values())
1848
+ )
1849
+
1850
+
1851
+ def _signal_name(signum: int) -> str:
1852
+ try:
1853
+ return signal.Signals(signum).name
1854
+ except ValueError:
1855
+ return f"signal {signum}"
1856
+
1857
+
1858
+ class _ShutdownDumpHandler:
1859
+ """
1860
+ Installs best-effort hooks for process shutdown events.
1861
+ """
1862
+
1863
+ _WINDOWS_HANDLED_EVENTS = {
1864
+ 2: "CTRL_CLOSE_EVENT",
1865
+ 5: "CTRL_LOGOFF_EVENT",
1866
+ 6: "CTRL_SHUTDOWN_EVENT",
1867
+ }
1868
+
1869
+ def __init__(self, shell: Gptcmd):
1870
+ self._shell = shell
1871
+ self._previous_signal_handlers: Dict[int, Any] = {}
1872
+ self._windows_handler = None
1873
+ self._windows_kernel32 = None
1874
+ self._dump_lock = threading.RLock()
1875
+ self._dump_attempted = False
1876
+
1877
+ def install(self) -> None:
1878
+ "Install all shutdown hooks supported by the current platform."
1879
+ self._install_signal_handlers()
1880
+ if os.name == "nt":
1881
+ self._install_windows_console_handler()
1882
+
1883
+ def uninstall(self) -> None:
1884
+ "Remove shutdown hooks installed by this object."
1885
+ for signum, previous_handler in self._previous_signal_handlers.items():
1886
+ try:
1887
+ signal.signal(signum, previous_handler)
1888
+ except (OSError, RuntimeError, ValueError) as e:
1889
+ self._warn(
1890
+ "Could not restore the previous handler for "
1891
+ f"{_signal_name(signum)}: {e}"
1892
+ )
1893
+ self._previous_signal_handlers.clear()
1894
+ if self._windows_handler is None:
1895
+ return
1896
+ try:
1897
+ res = self._windows_kernel32.SetConsoleCtrlHandler(
1898
+ self._windows_handler,
1899
+ False,
1900
+ )
1901
+ except OSError as e:
1902
+ self._warn(
1903
+ f"Could not remove the Windows console shutdown handler: {e}"
1904
+ )
1905
+ return
1906
+ if not res:
1907
+ self._warn("Could not remove the Windows console shutdown handler")
1908
+ self._windows_handler = None
1909
+ self._windows_kernel32 = None
1910
+
1911
+ def _install_signal_handlers(self) -> None:
1912
+ signal_names = ["SIGTERM"]
1913
+ if os.name == "nt":
1914
+ signal_names.append("SIGBREAK")
1915
+ else:
1916
+ signal_names.extend(["SIGHUP", "SIGQUIT"])
1917
+
1918
+ for signal_name in signal_names:
1919
+ signum = getattr(signal, signal_name, None)
1920
+ if signum is None:
1921
+ continue
1922
+ if signum in self._previous_signal_handlers:
1923
+ continue
1924
+ try:
1925
+ previous_handler = signal.getsignal(signum)
1926
+ signal.signal(signum, self._handle_signal)
1927
+ except (OSError, RuntimeError, ValueError) as e:
1928
+ self._warn(
1929
+ "Could not install a shutdown handler for "
1930
+ f"{signal_name}: {e}"
1931
+ )
1932
+ continue
1933
+ self._previous_signal_handlers[signum] = previous_handler
1934
+
1935
+ def _install_windows_console_handler(self) -> None:
1936
+ try:
1937
+ import ctypes
1938
+ from ctypes import wintypes
1939
+
1940
+ handler_type = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)
1941
+ kernel32 = ctypes.windll.kernel32
1942
+ except (AttributeError, OSError) as e:
1943
+ self._warn(f"Could not load Windows console shutdown support: {e}")
1944
+ return
1945
+
1946
+ def _handler(ctrl_type: int) -> bool:
1947
+ event = self._WINDOWS_HANDLED_EVENTS.get(ctrl_type)
1948
+ if event is None:
1949
+ return False
1950
+ self._dump_once(event)
1951
+ return True
1952
+
1953
+ console_handler = handler_type(_handler)
1954
+ try:
1955
+ res = kernel32.SetConsoleCtrlHandler(console_handler, True)
1956
+ except OSError as e:
1957
+ self._warn(
1958
+ f"Could not install the Windows console shutdown handler: {e}"
1959
+ )
1960
+ return
1961
+ if not res:
1962
+ self._warn(
1963
+ "Could not install the Windows console shutdown handler"
1964
+ )
1965
+ return
1966
+ self._windows_handler = console_handler
1967
+ self._windows_kernel32 = kernel32
1968
+
1969
+ def _handle_signal(self, signum: int, frame: Any) -> None:
1970
+ del frame
1971
+ self._dump_once(_signal_name(signum))
1972
+ raise SystemExit(128 + signum)
1973
+
1974
+ def _dump_once(self, event: str) -> Optional[str]:
1975
+ with self._dump_lock:
1976
+ if self._dump_attempted:
1977
+ return None
1978
+ self._dump_attempted = True
1979
+
1980
+ if not _shell_has_dirty_state(self._shell):
1981
+ return None
1982
+
1983
+ dump_path = _write_crash_dump(
1984
+ self._shell,
1985
+ RuntimeError(f"Process is shutting down: {event}"),
1986
+ )
1987
+ if dump_path:
1988
+ print(
1989
+ f"Crash dump written to {dump_path}",
1990
+ file=sys.stderr,
1991
+ flush=True,
1992
+ )
1993
+ return dump_path
1994
+
1995
+ @staticmethod
1996
+ def _warn(message: str) -> None:
1997
+ warnings.warn(
1998
+ message
1999
+ + "\nGptcmd may be unable to save a crash dump in the event of "
2000
+ + "unexpected shutdown!",
2001
+ RuntimeWarning,
2002
+ stacklevel=3,
2003
+ )
2004
+
2005
+
1837
2006
  class ExperimentalAPIWarning(Warning):
1838
2007
  pass
1839
2008
 
@@ -1897,17 +2066,15 @@ def _run(shell_cls) -> bool:
1897
2066
  shell.do_account(args.account, _print_on_success=False)
1898
2067
  if args.model:
1899
2068
  shell.do_model(args.model, _print_on_success=False)
2069
+ shutdown_dump_handler = _ShutdownDumpHandler(shell)
2070
+ shutdown_dump_handler.install()
1900
2071
  try:
1901
2072
  shell.cmdloop()
1902
2073
  except SystemExit:
1903
2074
  # Don't write a crash dump
1904
2075
  raise
1905
2076
  except BaseException as e:
1906
- # Does any thread contain messages?
1907
- should_save = (shell._detached and shell._detached.dirty) or any(
1908
- t and t.dirty for t in shell._threads.values()
1909
- )
1910
- if should_save:
2077
+ if _shell_has_dirty_state(shell):
1911
2078
  dump_path = _write_crash_dump(shell, e)
1912
2079
  if dump_path:
1913
2080
  # Hack: Print the "crash dump" notice after the traceback
@@ -1919,6 +2086,8 @@ def _run(shell_cls) -> bool:
1919
2086
  )
1920
2087
  )
1921
2088
  raise
2089
+ finally:
2090
+ shutdown_dump_handler.uninstall()
1922
2091
  return True
1923
2092
 
1924
2093
 
@@ -26,10 +26,35 @@ from ..message import Audio, Image, Message, MessageRole
26
26
  import openai
27
27
 
28
28
  ModelCostInfo = namedtuple(
29
- "ModelCostInfo", ("prompt_scale", "sampled_scale", "cache_discount_factor")
29
+ "ModelCostInfo",
30
+ (
31
+ "prompt_scale",
32
+ "sampled_scale",
33
+ "cache_discount_factor",
34
+ "long_prompt_threshold",
35
+ "long_prompt_factor",
36
+ "long_sampled_factor",
37
+ ),
38
+ defaults=(None, Decimal("1"), Decimal("1")),
30
39
  )
31
40
 
32
41
  OPENAI_COSTS: Dict[str, ModelCostInfo] = {
42
+ "gpt-5.5-2026-04-23": ModelCostInfo(
43
+ Decimal("5") / Decimal("1000000"),
44
+ Decimal("30") / Decimal("1000000"),
45
+ Decimal("0.1"),
46
+ 272000,
47
+ Decimal("2"),
48
+ Decimal("1.5"),
49
+ ),
50
+ "gpt-5.4-2026-03-05": ModelCostInfo(
51
+ Decimal("2.5") / Decimal("1000000"),
52
+ Decimal("15") / Decimal("1000000"),
53
+ Decimal("0.1"),
54
+ 272000,
55
+ Decimal("2"),
56
+ Decimal("1.5"),
57
+ ),
33
58
  "gpt-5.2-2025-12-11": ModelCostInfo(
34
59
  Decimal("1.75") / Decimal("1000000"),
35
60
  Decimal("14") / Decimal("1000000"),
@@ -280,10 +305,21 @@ class OpenAI(LLMProvider):
280
305
  cached_prompt_tokens = min(max(cached_prompt_tokens, 0), prompt_tokens)
281
306
  uncached_prompt_tokens = prompt_tokens - cached_prompt_tokens
282
307
  sampled_tokens = max(0, sampled_tokens)
308
+ long_context = (
309
+ info.long_prompt_threshold is not None
310
+ and prompt_tokens > info.long_prompt_threshold
311
+ )
312
+ prompt_factor = (
313
+ info.long_prompt_factor if long_context else Decimal("1")
314
+ )
315
+ sampled_factor = (
316
+ info.long_sampled_factor if long_context else Decimal("1")
317
+ )
318
+ cached_prompt_scale *= prompt_factor
283
319
  return (
284
- Decimal(uncached_prompt_tokens) * info.prompt_scale
320
+ Decimal(uncached_prompt_tokens) * info.prompt_scale * prompt_factor
285
321
  + Decimal(cached_prompt_tokens) * cached_prompt_scale
286
- + Decimal(sampled_tokens) * info.sampled_scale
322
+ + Decimal(sampled_tokens) * info.sampled_scale * sampled_factor
287
323
  ) * Decimal("100")
288
324
 
289
325
  def complete(self, messages: Sequence[Message]) -> LLMResponse:
@@ -371,6 +407,8 @@ class OpenAI(LLMProvider):
371
407
 
372
408
  def get_best_model(self):
373
409
  BEST_MODELS = (
410
+ "gpt-5.5",
411
+ "gpt-5.4",
374
412
  "gpt-5.2",
375
413
  "gpt-5.1",
376
414
  "gpt-5",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gptcmd
3
- Version: 2.3.4
3
+ Version: 2.4.0
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
@@ -221,7 +221,7 @@ The first ten digits of pi are 3.141592653.
221
221
  With no arguments, the `user`, `assistant`, `system`, and `say` commands open an external text editor (based on your system or Gptcmd configuration) for message composition.
222
222
 
223
223
  ### Working with attachments
224
- OpenAI's latest models, such as `gppt-4o`, support images alongside text content. Images can be attached to messages with the `image` command, which accepts two arguments: the location of the image, either a URL or path to a local file; and the index of the message to which the image should be attached (if unspecified, it defaults to the last). We'll ask GPT to describe an image by creating a user message and attaching an image from Wikimedia Commons:
224
+ OpenAI's latest models, such as `gpt-4o`, support images alongside text content. Images can be attached to messages with the `image` command, which accepts two arguments: the location of the image, either a URL or path to a local file; and the index of the message to which the image should be attached (if unspecified, it defaults to the last). We'll ask GPT to describe an image by creating a user message and attaching an image from Wikimedia Commons:
225
225
 
226
226
  ```
227
227
  (gpt-4o) user What's in this image?
@@ -451,7 +451,7 @@ Unset all parameters
451
451
  ```
452
452
 
453
453
  ### Names
454
- GPT allows mesages to be annotated with the name of their author. The `name` command sets the name to be sent with all future messages of the specified role. Its first argument is the role to which this new name should be applied, and its second is the name to use:
454
+ GPT allows messages to be annotated with the name of their author. The `name` command sets the name to be sent with all future messages of the specified role. Its first argument is the role to which this new name should be applied, and its second is the name to use:
455
455
 
456
456
  ```
457
457
  (gpt-4o) name user Michael
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes