mini-code-cli 0.1.1.2__py3-none-any.whl → 0.1.1.3__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.
- mini_code_cli/__init__.py +29 -10
- mini_code_cli/__main__.py +12 -0
- mini_code_cli/agent_loop.py +20 -18
- mini_code_cli/runner.py +14 -6
- mini_code_cli/skills.py +4 -1
- mini_code_cli/ui.py +5 -11
- mini_code_cli/utils.py +63 -24
- mini_code_cli-0.1.1.3.dist-info/METADATA +390 -0
- {mini_code_cli-0.1.1.2.dist-info → mini_code_cli-0.1.1.3.dist-info}/RECORD +13 -12
- mini_code_cli-0.1.1.2.dist-info/METADATA +0 -1028
- {mini_code_cli-0.1.1.2.dist-info → mini_code_cli-0.1.1.3.dist-info}/WHEEL +0 -0
- {mini_code_cli-0.1.1.2.dist-info → mini_code_cli-0.1.1.3.dist-info}/entry_points.txt +0 -0
- {mini_code_cli-0.1.1.2.dist-info → mini_code_cli-0.1.1.3.dist-info}/licenses/LICENSE +0 -0
- {mini_code_cli-0.1.1.2.dist-info → mini_code_cli-0.1.1.3.dist-info}/top_level.txt +0 -0
mini_code_cli/__init__.py
CHANGED
|
@@ -11,22 +11,41 @@ to ``"unknown"`` so importing the package can never fail over a version lookup.
|
|
|
11
11
|
__all__ = ["__version__"]
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _version_from_file(path):
|
|
15
|
+
"""Return the ``[project] version`` in ``path``.
|
|
16
|
+
|
|
17
|
+
``tomllib`` only exists from Python 3.11, and the scan below is not a
|
|
18
|
+
nicety for 3.10: without it this function used to skip the file entirely on
|
|
19
|
+
that interpreter and report the *installed* metadata instead, so a checkout
|
|
20
|
+
with a stale editable install showed the wrong version in the banner (found
|
|
21
|
+
on the 3.10 CI leg). Both paths read the same key, and the section tracking
|
|
22
|
+
matters: a ``version`` key in some other table must not win.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
import tomllib # Python >= 3.11
|
|
26
|
+
except ImportError: # pragma: no cover - exercised on the 3.10 CI leg
|
|
27
|
+
tomllib = None
|
|
28
|
+
if tomllib is not None:
|
|
29
|
+
with open(path, "rb") as f:
|
|
30
|
+
return str(tomllib.load(f)["project"]["version"])
|
|
31
|
+
section = None
|
|
32
|
+
with open(path, encoding="utf-8") as f:
|
|
33
|
+
for line in f:
|
|
34
|
+
stripped = line.strip()
|
|
35
|
+
if stripped.startswith("["):
|
|
36
|
+
section = stripped
|
|
37
|
+
elif section == "[project]" and stripped.startswith("version"):
|
|
38
|
+
return stripped.split("=", 1)[1].strip().strip('"')
|
|
39
|
+
raise KeyError(f"no version in the [project] table of {path}")
|
|
40
|
+
|
|
41
|
+
|
|
14
42
|
def _read_version():
|
|
15
43
|
"""Best-effort package version: pyproject.toml, then installed metadata."""
|
|
16
44
|
try:
|
|
17
45
|
from pathlib import Path
|
|
18
46
|
|
|
19
47
|
pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
|
20
|
-
|
|
21
|
-
import tomllib # Python >= 3.11
|
|
22
|
-
except ImportError: # pragma: no cover - older interpreters
|
|
23
|
-
tomllib = None
|
|
24
|
-
if tomllib is not None:
|
|
25
|
-
try:
|
|
26
|
-
with open(pyproject, "rb") as f:
|
|
27
|
-
return str(tomllib.load(f)["project"]["version"])
|
|
28
|
-
except Exception:
|
|
29
|
-
pass
|
|
48
|
+
return _version_from_file(pyproject)
|
|
30
49
|
except Exception:
|
|
31
50
|
pass
|
|
32
51
|
try:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Allow ``python -m mini_code_cli`` to start the agent.
|
|
2
|
+
|
|
3
|
+
The console script ``mini-code`` (see ``[project.scripts]``) is the normal entry
|
|
4
|
+
point; this module exists so the package also runs straight from a checkout or
|
|
5
|
+
an installed wheel without the script on ``PATH`` -- which is what a reader
|
|
6
|
+
tries first after ``pip install -e .``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .agent_loop import main
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
raise SystemExit(main())
|
mini_code_cli/agent_loop.py
CHANGED
|
@@ -31,7 +31,7 @@ from . import prompts
|
|
|
31
31
|
from . import shell_safety_utils
|
|
32
32
|
from . import tools
|
|
33
33
|
from . import utils
|
|
34
|
-
from .utils import oneline
|
|
34
|
+
from .utils import oneline, plural
|
|
35
35
|
from . import ui
|
|
36
36
|
from . import subagents
|
|
37
37
|
from . import skills
|
|
@@ -118,7 +118,12 @@ def _safe_int(value, default=0):
|
|
|
118
118
|
# ---------------------------------------------------------------------------
|
|
119
119
|
|
|
120
120
|
def parse_args():
|
|
121
|
-
|
|
121
|
+
# ``prog`` is pinned so ``--help`` reads the same whether the agent was
|
|
122
|
+
# started through the ``mini-code`` script, ``python -m mini_code_cli`` or
|
|
123
|
+
# a test harness (without it, the usage line said ``__main__.py``).
|
|
124
|
+
parser = argparse.ArgumentParser(
|
|
125
|
+
prog="mini-code",
|
|
126
|
+
description="Agent loop for interacting with a language model")
|
|
122
127
|
# MODEL / LLM related
|
|
123
128
|
parser.add_argument("--url", type=str, default=f"{DEFAULT_MODEL_URL}", help=f"API host, default={DEFAULT_MODEL_URL}")
|
|
124
129
|
parser.add_argument("--max-tokens", type=int, help="Maximum tokens for a single LLM response (the reply budget), default=None. Also used as the output headroom reserved when deriving the compaction limit from the model's context window.")
|
|
@@ -298,11 +303,6 @@ def _in_flight_tail(messages):
|
|
|
298
303
|
return []
|
|
299
304
|
|
|
300
305
|
|
|
301
|
-
def _plural(count, noun, suffix="s"):
|
|
302
|
-
"""``"1 message"`` / ``"4 messages"`` -- for messages the user reads."""
|
|
303
|
-
return f"{count} {noun}" + ("" if count == 1 else suffix)
|
|
304
|
-
|
|
305
|
-
|
|
306
306
|
def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats,
|
|
307
307
|
history_index=0, token_limit=None, reserved_output_tokens=0,
|
|
308
308
|
current_tokens=None, plan_hook=None, console=None):
|
|
@@ -358,12 +358,12 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
|
|
|
358
358
|
# stake; a compaction note that stays silent about that is exactly what makes
|
|
359
359
|
# the rollover look like it might have eaten the message.
|
|
360
360
|
tail = _in_flight_tail(messages)
|
|
361
|
-
kept_note = (f" The current turn ({
|
|
361
|
+
kept_note = (f" The current turn ({plural(len(tail), 'message')}) is kept "
|
|
362
362
|
f"verbatim." if tail else "")
|
|
363
363
|
|
|
364
364
|
try:
|
|
365
365
|
console.note(
|
|
366
|
-
f"Compacting the conversation: {
|
|
366
|
+
f"Compacting the conversation: {plural(len(messages), 'message')} "
|
|
367
367
|
f"-> a summary ({reason}).{kept_note}", "warn")
|
|
368
368
|
summary_prompt = prompts.compaction_prompt(messages)
|
|
369
369
|
|
|
@@ -404,7 +404,7 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
|
|
|
404
404
|
# may still be too large.
|
|
405
405
|
console.note(
|
|
406
406
|
f"Could not compact history: the summariser returned an empty "
|
|
407
|
-
f"summary twice. Keeping all {
|
|
407
|
+
f"summary twice. Keeping all {plural(len(messages), 'message')} "
|
|
408
408
|
f"and history_{history_index}.json as they are -- nothing was "
|
|
409
409
|
f"lost, but the next request may still be too large "
|
|
410
410
|
f"({reason}).", "warn")
|
|
@@ -449,7 +449,7 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
|
|
|
449
449
|
# and *where it went* (which segment file was closed and which was
|
|
450
450
|
# started).
|
|
451
451
|
console.note(f"Summary preview: {utils.oneline(summary, 120)}", "warn")
|
|
452
|
-
kept = (f"summary + {
|
|
452
|
+
kept = (f"summary + {plural(len(tail), 'kept message')}" if tail
|
|
453
453
|
else "summary only")
|
|
454
454
|
# Phrased as "old segment -> new segment" rather than "N messages -> M
|
|
455
455
|
# messages": the replacement can be *longer* than what it replaced (a
|
|
@@ -457,9 +457,9 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
|
|
|
457
457
|
# counts that sometimes grows reads like a bug.
|
|
458
458
|
console.note(
|
|
459
459
|
f"Compacted history_{history_index}.json "
|
|
460
|
-
f"({
|
|
460
|
+
f"({plural(len(messages), 'message')}) into a summary; "
|
|
461
461
|
f"history_{history_index + 1}.json now starts with "
|
|
462
|
-
f"{
|
|
462
|
+
f"{plural(len(compacted), 'message')} ({kept}).", "warn")
|
|
463
463
|
# A new segment (history_index + 1) is started on compaction: the
|
|
464
464
|
# compacted messages are its snapshot, and every later turn appends
|
|
465
465
|
# to it. The caller records a single high-level 'summary' turn for
|
|
@@ -468,7 +468,7 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
|
|
|
468
468
|
except Exception as e:
|
|
469
469
|
console.note(
|
|
470
470
|
f"Failed to compact history: {e}. Keeping all "
|
|
471
|
-
f"{
|
|
471
|
+
f"{plural(len(messages), 'message')} unchanged.", "error")
|
|
472
472
|
|
|
473
473
|
return messages, False, history_index
|
|
474
474
|
|
|
@@ -1960,7 +1960,9 @@ class AgentSession:
|
|
|
1960
1960
|
# just the turns. In verbose mode everything is printed permanently.
|
|
1961
1961
|
transient = not self.console.verbose
|
|
1962
1962
|
func = self.tools_dict[name]["function"]
|
|
1963
|
-
result = call_function_with_timeout(func, tool_params, name,
|
|
1963
|
+
result = call_function_with_timeout(func, tool_params, name,
|
|
1964
|
+
timeout_seconds=self.args.tool_timeout,
|
|
1965
|
+
console=self.console)
|
|
1964
1966
|
|
|
1965
1967
|
self.console.tool_call_display(name, tool_params, result, transient=transient)
|
|
1966
1968
|
self._append_tool_result(call_id, str(result))
|
|
@@ -2105,8 +2107,8 @@ class AgentSession:
|
|
|
2105
2107
|
("session id", self.unique_id),
|
|
2106
2108
|
("saved under", f"{self.cache_dir}/{self.unique_id}/"),
|
|
2107
2109
|
("history", f"{where} (segment {self.history_index}), "
|
|
2108
|
-
f"{
|
|
2109
|
-
("conversation",
|
|
2110
|
+
f"{plural(len(self.messages), 'message')}"),
|
|
2111
|
+
("conversation", plural(turns, "recorded turn")),
|
|
2110
2112
|
("tokens", f"{count('prompt_tokens'):,} in / "
|
|
2111
2113
|
f"{count('completion_tokens'):,} out "
|
|
2112
2114
|
f"({count('total_tokens'):,} total)"),
|
|
@@ -2176,7 +2178,7 @@ class AgentSession:
|
|
|
2176
2178
|
save_history(self.messages, self.cache_dir, self.unique_id, self.history_index)
|
|
2177
2179
|
self._record_turn(
|
|
2178
2180
|
f"summary: history_{new_index - 1}.json "
|
|
2179
|
-
f"({
|
|
2181
|
+
f"({plural(len(closing_messages), 'message')}) compacted into a "
|
|
2180
2182
|
f"summary; history_{self.history_index}.json started",
|
|
2181
2183
|
self.messages)
|
|
2182
2184
|
|
mini_code_cli/runner.py
CHANGED
|
@@ -18,13 +18,19 @@ class _TimedOut(Exception):
|
|
|
18
18
|
pass
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def call_function_with_timeout(func, tool_params, name, timeout_seconds=DEFAULT_TOOL_TIMEOUT
|
|
21
|
+
def call_function_with_timeout(func, tool_params, name, timeout_seconds=DEFAULT_TOOL_TIMEOUT,
|
|
22
|
+
console=None):
|
|
22
23
|
"""Call ``func(**tool_params)``, aborting after ``timeout_seconds``.
|
|
23
24
|
|
|
24
25
|
On timeout -- or if the tool raises *any* other exception -- a friendly
|
|
25
26
|
``Error:`` string is returned so the failure reaches the model as a tool
|
|
26
27
|
result (letting it recover or retry) instead of propagating and crashing the
|
|
27
28
|
whole agent loop. A timeout is reported distinctly from a generic failure.
|
|
29
|
+
|
|
30
|
+
``console`` is the session's console. It defaults to the shared one so a
|
|
31
|
+
bare call still works, but the agent loop passes its own: a sub-agent's
|
|
32
|
+
console redirects its output, and reporting to the module-level console
|
|
33
|
+
would print a background child's tool errors onto the user's terminal.
|
|
28
34
|
"""
|
|
29
35
|
def _handler(signum, frame):
|
|
30
36
|
raise _TimedOut()
|
|
@@ -35,30 +41,32 @@ def call_function_with_timeout(func, tool_params, name, timeout_seconds=DEFAULT_
|
|
|
35
41
|
# signal only works in the main thread; if we cannot arm it, just run
|
|
36
42
|
# the tool without the wall-clock guard rather than failing outright --
|
|
37
43
|
# but still shield the loop from a tool that raises.
|
|
38
|
-
return _call_tool_safely(func, tool_params, name)
|
|
44
|
+
return _call_tool_safely(func, tool_params, name, console)
|
|
39
45
|
|
|
46
|
+
console = console if console is not None else ui.console
|
|
40
47
|
signal.alarm(int(timeout_seconds) if timeout_seconds else 0)
|
|
41
48
|
try:
|
|
42
49
|
return func(**tool_params)
|
|
43
50
|
except _TimedOut:
|
|
44
|
-
|
|
51
|
+
console.note(f"Function {name} timed out after {timeout_seconds} seconds.", "error")
|
|
45
52
|
return f"Error: Function {name} timed out after {timeout_seconds} seconds."
|
|
46
53
|
except Exception as e: # noqa: BLE001 -- a failing tool must not crash the loop
|
|
47
|
-
|
|
54
|
+
console.note(f"Tool {name} raised an error: {e}", "error")
|
|
48
55
|
return f"Error: tool {name} raised an error: {e}"
|
|
49
56
|
finally:
|
|
50
57
|
signal.alarm(0)
|
|
51
58
|
signal.signal(signal.SIGALRM, previous)
|
|
52
59
|
|
|
53
60
|
|
|
54
|
-
def _call_tool_safely(func, tool_params, name):
|
|
61
|
+
def _call_tool_safely(func, tool_params, name, console=None):
|
|
55
62
|
"""Run a tool with no wall-clock guard (SIGALRM unavailable) but still
|
|
56
63
|
shield the loop from a tool that raises.
|
|
57
64
|
|
|
58
65
|
Used as the fallback when we could not arm SIGALRM (e.g. a non-main thread).
|
|
59
66
|
"""
|
|
67
|
+
console = console if console is not None else ui.console
|
|
60
68
|
try:
|
|
61
69
|
return func(**tool_params)
|
|
62
70
|
except Exception as e: # noqa: BLE001 -- never let a tool exception propagate
|
|
63
|
-
|
|
71
|
+
console.note(f"Tool {name} raised an error: {e}", "error")
|
|
64
72
|
return f"Error: tool {name} raised an error: {e}"
|
mini_code_cli/skills.py
CHANGED
|
@@ -294,7 +294,10 @@ def run_skill(allowed_dir, skill_name, script_name, args=None):
|
|
|
294
294
|
argv = shlex.split(args) if args else []
|
|
295
295
|
cmd = [sys.executable, script_path] + argv
|
|
296
296
|
try:
|
|
297
|
-
|
|
297
|
+
# Run *inside* the skill's own directory: a script that writes or reads
|
|
298
|
+
# a relative path then stays in the skill folder instead of landing in
|
|
299
|
+
# whatever directory the agent process happens to have inherited.
|
|
300
|
+
result = subprocess.run(cmd, capture_output=True, text=True, cwd=base,
|
|
298
301
|
timeout=DEFAULT_TOOL_TIMEOUT, check=False)
|
|
299
302
|
except Exception as e:
|
|
300
303
|
return f"Error: {e}"
|
mini_code_cli/ui.py
CHANGED
|
@@ -29,7 +29,7 @@ import shutil
|
|
|
29
29
|
import sys
|
|
30
30
|
import threading
|
|
31
31
|
|
|
32
|
-
from .utils import oneline
|
|
32
|
+
from .utils import oneline, plural
|
|
33
33
|
|
|
34
34
|
# --- ANSI SGR codes ---------------------------------------------------------
|
|
35
35
|
_RESET = "\033[0m"
|
|
@@ -348,12 +348,6 @@ class Console:
|
|
|
348
348
|
styled_lines.append(text)
|
|
349
349
|
self.print_box(styled_lines, title=title, fg=fg)
|
|
350
350
|
|
|
351
|
-
def truncate(self, text, limit):
|
|
352
|
-
text = str(text)
|
|
353
|
-
if len(text) > limit:
|
|
354
|
-
return text[:limit] + "... (truncated)"
|
|
355
|
-
return text
|
|
356
|
-
|
|
357
351
|
@staticmethod
|
|
358
352
|
def _oneline(text, limit=None):
|
|
359
353
|
"""Collapse ``text`` to one line (delegates to :func:`utils.oneline`)."""
|
|
@@ -570,7 +564,7 @@ class Console:
|
|
|
570
564
|
"""Render one attempted tool call: its name, parameters and result."""
|
|
571
565
|
self.emit(self.style(f"{self.glyph('gear')} {name}", fg="magenta", bold=True))
|
|
572
566
|
for key, value in (params or {}).items():
|
|
573
|
-
snippet = self.
|
|
567
|
+
snippet = self._oneline(str(value), 100)
|
|
574
568
|
key_str = self.style(f" {key}:", fg="gray", dim=True) if self.color_enabled \
|
|
575
569
|
else f" {key}:"
|
|
576
570
|
self.emit(f"{key_str} {snippet}")
|
|
@@ -586,7 +580,7 @@ class Console:
|
|
|
586
580
|
first = text.splitlines()[0] if text.strip() else "(no output)"
|
|
587
581
|
arrow = self.style(self.glyph("arrow"), fg=color) if self.color_enabled \
|
|
588
582
|
else self.glyph("arrow")
|
|
589
|
-
self.emit(f" {arrow} {self.
|
|
583
|
+
self.emit(f" {arrow} {self._oneline(first, 300)}")
|
|
590
584
|
|
|
591
585
|
def tool_call_display(self, name, params=None, result=None, transient=True):
|
|
592
586
|
"""Render one attempted tool call.
|
|
@@ -624,7 +618,7 @@ class Console:
|
|
|
624
618
|
f"total={usage.get('total_tokens', 0)}")
|
|
625
619
|
if llm_calls is not None:
|
|
626
620
|
calls = int(llm_calls)
|
|
627
|
-
text += f" | {calls
|
|
621
|
+
text += f" | {plural(calls, 'LLM call')}"
|
|
628
622
|
label = self.style("tokens", fg="cyan", dim=True) if self.color_enabled else "tokens"
|
|
629
623
|
return f"{label}: {text}"
|
|
630
624
|
|
|
@@ -683,7 +677,7 @@ class Console:
|
|
|
683
677
|
self.emit(self.style(header, bold=True, fg="cyan"))
|
|
684
678
|
for turn, what, where in rows:
|
|
685
679
|
turn_cell = self.style(turn.rjust(turn_w), fg="magenta", bold=True) if self.color_enabled else turn.rjust(turn_w)
|
|
686
|
-
what_cell = self.style(self.
|
|
680
|
+
what_cell = self.style(self._oneline(what, what_w), dim=True) if self.color_enabled else what.ljust(what_w)
|
|
687
681
|
where_cell = self.style(where, fg="gray", dim=True) if self.color_enabled else where
|
|
688
682
|
self.emit(f"{turn_cell} {what_cell} {where_cell}")
|
|
689
683
|
self.emit(self.style("(commands: /history <n> to inspect, /reset <n> to rewind)", dim=True, fg="cyan"))
|
mini_code_cli/utils.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
import json
|
|
3
|
-
import sys
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
def oneline(text, limit=None):
|
|
@@ -23,6 +22,17 @@ def oneline(text, limit=None):
|
|
|
23
22
|
return one
|
|
24
23
|
|
|
25
24
|
|
|
25
|
+
def plural(count, noun, suffix="s"):
|
|
26
|
+
"""``"1 message"`` / ``"4 messages"`` -- for text a human reads.
|
|
27
|
+
|
|
28
|
+
Lives here, next to :func:`oneline`, because both the agent loop (compaction
|
|
29
|
+
notes, the good-bye box) and the console (usage lines) build user-facing
|
|
30
|
+
counts; three inline ``'' if n == 1 else 's'`` expressions had already
|
|
31
|
+
drifted before this was factored out.
|
|
32
|
+
"""
|
|
33
|
+
return f"{count} {noun}" + ("" if count == 1 else suffix)
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
def _coerce_usage_int(value):
|
|
27
37
|
"""Best-effort: coerce a raw usage field to a non-negative int.
|
|
28
38
|
|
|
@@ -131,11 +141,14 @@ def call_openai_server(messages, max_tokens=2048, temperature=0.0, model="defaul
|
|
|
131
141
|
if api_key:
|
|
132
142
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
133
143
|
|
|
144
|
+
# Note: the request timeout is a *transport* concern and is passed to
|
|
145
|
+
# ``requests.post(timeout=...)`` below, not put in the JSON body -- it is
|
|
146
|
+
# not a field of the chat-completions API, and strict gateways reject
|
|
147
|
+
# unexpected keys.
|
|
134
148
|
payload = {
|
|
135
149
|
"model": model,
|
|
136
150
|
"messages": messages,
|
|
137
151
|
"temperature": temperature,
|
|
138
|
-
"timeout": timeout,
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
if max_tokens is not None:
|
|
@@ -159,7 +172,10 @@ def call_openai_server(messages, max_tokens=2048, temperature=0.0, model="defaul
|
|
|
159
172
|
)
|
|
160
173
|
response.raise_for_status()
|
|
161
174
|
|
|
162
|
-
|
|
175
|
+
# Parse the body once: a second ``response.json()`` call re-decodes it
|
|
176
|
+
# and can raise on a server that sends a slightly different body.
|
|
177
|
+
body = response.json()
|
|
178
|
+
result = body.get("choices", [{}])[0].get("message", {})
|
|
163
179
|
content = result.get("content", "")
|
|
164
180
|
tool_calls = result.get("tool_calls", [])
|
|
165
181
|
# Thinking-mode servers return the chain of thought next to ``content``.
|
|
@@ -172,7 +188,7 @@ def call_openai_server(messages, max_tokens=2048, temperature=0.0, model="defaul
|
|
|
172
188
|
reasoning_content = result.get("reasoning_content")
|
|
173
189
|
|
|
174
190
|
# Token usage, when the server reports it (many local servers do not).
|
|
175
|
-
usage =
|
|
191
|
+
usage = body.get("usage", {}) or {}
|
|
176
192
|
prompt_tokens = _coerce_usage_int(usage.get("prompt_tokens", 0))
|
|
177
193
|
completion_tokens = _coerce_usage_int(usage.get("completion_tokens", 0))
|
|
178
194
|
total_tokens = _coerce_usage_int(
|
|
@@ -187,28 +203,51 @@ def call_openai_server(messages, max_tokens=2048, temperature=0.0, model="defaul
|
|
|
187
203
|
}, reasoning_content
|
|
188
204
|
|
|
189
205
|
except requests.exceptions.RequestException as e:
|
|
190
|
-
|
|
191
|
-
# Only probe the response further if we actually got one back. A
|
|
192
|
-
# request-level failure (timeout, connection refused, DNS) leaves
|
|
193
|
-
# ``response`` as None, and there is nothing to inspect then.
|
|
194
|
-
if response is not None:
|
|
195
|
-
try:
|
|
196
|
-
# Print the status code / raw body to see the detailed error.
|
|
197
|
-
print(f"Status Code: {response.status_code}", file=sys.stderr)
|
|
198
|
-
print(f"Response Body: {response.text}", file=sys.stderr)
|
|
199
|
-
# If the response is JSON, parse and pretty-print it.
|
|
200
|
-
try:
|
|
201
|
-
error_details = response.json()
|
|
202
|
-
print("Parsed Error Details:", file=sys.stderr)
|
|
203
|
-
print(json.dumps(error_details, indent=2), file=sys.stderr)
|
|
204
|
-
except json.JSONDecodeError:
|
|
205
|
-
print("Response body is not valid JSON.", file=sys.stderr)
|
|
206
|
-
except Exception as e:
|
|
207
|
-
print(f"Problem with (error) response: {e}", file=sys.stderr)
|
|
206
|
+
_report_request_failure(e, response)
|
|
208
207
|
return None, None, None, None
|
|
209
208
|
except (json.JSONDecodeError, KeyError, IndexError) as e:
|
|
210
|
-
|
|
209
|
+
_report(f"Error processing the model's response: {e}")
|
|
211
210
|
return None, None, None, None
|
|
212
211
|
except Exception as e: # noqa: BLE001 -- a network/parse hiccup must not crash the loop
|
|
213
|
-
|
|
212
|
+
_report(f"Unexpected error calling the model server: {e}")
|
|
214
213
|
return None, None, None, None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _report(message, level="error"):
|
|
217
|
+
"""Report a request failure through the console, never straight to stderr.
|
|
218
|
+
|
|
219
|
+
``ui`` imports :func:`oneline` from this module, so the console is imported
|
|
220
|
+
lazily here to keep the import graph acyclic. A single note -- rather than a
|
|
221
|
+
multi-line ``print`` -- keeps the transient region intact: this can fire
|
|
222
|
+
while the "… thinking" spinner owns the bottom line, and writing multi-line
|
|
223
|
+
text underneath it left the screen mangled.
|
|
224
|
+
"""
|
|
225
|
+
from . import ui # local: ui imports oneline from here
|
|
226
|
+
ui.console.note(oneline(message, 300), level)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _report_request_failure(error, response):
|
|
230
|
+
"""Explain an HTTP failure, folding the server's own words into one line.
|
|
231
|
+
|
|
232
|
+
Only the response is inspected more closely when there *is* one: a
|
|
233
|
+
request-level failure (timeout, refused connection, DNS) leaves ``response``
|
|
234
|
+
as None, and there is nothing to inspect then. The body is the useful part
|
|
235
|
+
(servers put the real reason there), so it is kept -- collapsed and
|
|
236
|
+
truncated -- instead of dumping the whole payload into the terminal.
|
|
237
|
+
"""
|
|
238
|
+
if response is None:
|
|
239
|
+
_report(f"HTTP error talking to the model server: {error}")
|
|
240
|
+
return
|
|
241
|
+
status = getattr(response, "status_code", "?")
|
|
242
|
+
detail = ""
|
|
243
|
+
try:
|
|
244
|
+
body = response.text or ""
|
|
245
|
+
try:
|
|
246
|
+
body = json.dumps(response.json())
|
|
247
|
+
except json.JSONDecodeError:
|
|
248
|
+
pass # not JSON: the raw body is the best we have
|
|
249
|
+
if body.strip():
|
|
250
|
+
detail = f": {oneline(body, 200)}"
|
|
251
|
+
except Exception as e: # a broken response must not break the report
|
|
252
|
+
detail = f" (and the error body was unreadable: {e})"
|
|
253
|
+
_report(f"HTTP {status} from the model server: {error}{detail}")
|