@seanyao/roll 2026.529.5 → 2026.601.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +57 -25
  2. package/README.md +10 -7
  3. package/bin/roll +3952 -317
  4. package/conventions/config.yaml +7 -0
  5. package/lib/__pycache__/github_sync.cpython-314.pyc +0 -0
  6. package/lib/__pycache__/loop_result_eval.cpython-314.pyc +0 -0
  7. package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
  8. package/lib/__pycache__/roll-home.cpython-314.pyc +0 -0
  9. package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
  10. package/lib/__pycache__/roll_git.cpython-314.pyc +0 -0
  11. package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
  12. package/lib/agent_usage/__init__.py +4 -0
  13. package/lib/agent_usage/__pycache__/__init__.cpython-314.pyc +0 -0
  14. package/lib/agent_usage/__pycache__/gemini.cpython-314.pyc +0 -0
  15. package/lib/agent_usage/__pycache__/kimi.cpython-314.pyc +0 -0
  16. package/lib/agent_usage/__pycache__/openai.cpython-314.pyc +0 -0
  17. package/lib/agent_usage/__pycache__/qwen.cpython-314.pyc +0 -0
  18. package/lib/agent_usage/gemini.py +127 -0
  19. package/lib/agent_usage/kimi.py +127 -0
  20. package/lib/agent_usage/openai.py +126 -0
  21. package/lib/agent_usage/qwen.py +128 -0
  22. package/lib/context_feed_budget.sh +194 -0
  23. package/lib/github_sync.py +876 -0
  24. package/lib/i18n/agent.sh +54 -0
  25. package/lib/i18n/init.sh +22 -0
  26. package/lib/i18n/peer.sh +7 -0
  27. package/lib/i18n/peer_help.sh +4 -0
  28. package/lib/i18n/skills_catalog.sh +30 -0
  29. package/lib/loop-exit-summary.py +393 -0
  30. package/lib/loop-fmt.py +93 -75
  31. package/lib/loop_pick_agent.py +241 -170
  32. package/lib/loop_result_eval.py +469 -0
  33. package/lib/model_prices.py +0 -10
  34. package/lib/roll-home.py +1 -28
  35. package/lib/roll-loop-status.py +330 -40
  36. package/lib/roll-onboard-render.py +378 -0
  37. package/lib/roll-peer.py +1 -1
  38. package/lib/roll-plan-validate.py +165 -0
  39. package/lib/roll_git.py +41 -0
  40. package/lib/slides/components/README.md +8 -2
  41. package/lib/slides/templates/introduction-v3.html +1 -6
  42. package/lib/slides-render.py +305 -15
  43. package/lib/slides-validate.py +195 -7
  44. package/package.json +1 -1
  45. package/skills/roll-.changelog/SKILL.md +67 -56
  46. package/skills/roll-brief/SKILL.md +1 -1
  47. package/skills/roll-build/SKILL.md +14 -12
  48. package/skills/roll-deck/SKILL.md +152 -0
  49. package/skills/roll-design/SKILL.md +13 -6
  50. package/skills/roll-doc/SKILL.md +269 -6
  51. package/skills/roll-fix/SKILL.md +15 -9
  52. package/skills/roll-loop/SKILL.md +9 -7
  53. package/skills/roll-notes/SKILL.md +1 -1
  54. package/skills/roll-onboard/SKILL.md +85 -0
  55. package/skills/roll-peer/SKILL.md +6 -5
  56. package/lib/agent_routes_lint.py +0 -203
  57. package/skills/roll-research/SKILL.md +0 -316
  58. package/skills/roll-research/references/schema.json +0 -166
  59. package/skills/roll-research/scripts/md_to_pdf.py +0 -289
@@ -22,3 +22,10 @@ editor: ${EDITOR:-vim}
22
22
  # file paths, and other potentially sensitive information.
23
23
  #
24
24
  # roll_records_remote: "git@github.com:you/roll-loop-records.git"
25
+
26
+ # Remote monitoring (optional) — US-OBS-014.
27
+ # Local checkout of your roll-meta repo. When set, each loop cycle end
28
+ # auto-pushes a status snapshot via ops/push-loop-status.sh so the
29
+ # remote-watch prompt always sees fresh data (no manual cron needed).
30
+ # Path supports ~ expansion; a missing path logs one WARNING and is skipped.
31
+ # roll_meta_dir: ~/projects/roll-meta
@@ -66,6 +66,10 @@ _PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
66
66
  _PLUGINS = {
67
67
  # agent name → python module name (relative to this package)
68
68
  "pi": ".pi",
69
+ "openai": ".openai",
70
+ "gemini": ".gemini",
71
+ "kimi": ".kimi",
72
+ "qwen": ".qwen",
69
73
  }
70
74
 
71
75
  for _agent, _mod_suffix in _PLUGINS.items():
@@ -0,0 +1,127 @@
1
+ """
2
+ gemini (Google Gemini CLI) agent usage extractor.
3
+
4
+ Like openai (and unlike pi, which persists usage to session files), the
5
+ Gemini CLI prints a token-usage summary to stdout at the end of a session.
6
+ So this plugin implements the standard ``extract()`` registry contract:
7
+ scrape the passthrough stdout lines for the usage / model lines.
8
+
9
+ Recognised lines (case-insensitive, robust to thousands separators)::
10
+
11
+ Model: gemini-2.5-pro
12
+ Tokens: input=15300 output=3120
13
+
14
+ The Gemini CLI's "stats" / session-summary block is also accepted::
15
+
16
+ Input tokens: 15,300
17
+ Output tokens: 3,120
18
+ Total tokens: 18,420
19
+ model: gemini-2.5-flash
20
+
21
+ When an explicit USD cost line isn't present, cost is computed from
22
+ ``lib/model_prices.py`` (list price) so the dashboard never shows ``—``
23
+ for a recognised gemini cycle. Returns None if no usage line is found,
24
+ so the caller falls back to the null payload (US-LOOP-010 compatible).
25
+ """
26
+
27
+ import os
28
+ import re
29
+ import sys
30
+ from typing import Optional
31
+
32
+ # model_prices lives one level up (lib/), alongside this package.
33
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
34
+ try:
35
+ import model_prices
36
+ except Exception: # pragma: no cover - import guard
37
+ model_prices = None
38
+
39
+ # Default model when the output omits an explicit model line.
40
+ _DEFAULT_MODEL = "gemini-2.5-pro"
41
+
42
+ _MODEL_RE = re.compile(r"^\s*model\s*[:=]\s*([A-Za-z0-9][\w.\-]*)", re.IGNORECASE)
43
+ _INPUT_RE = re.compile(r"input(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
44
+ _OUTPUT_RE = re.compile(r"output(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
45
+ _TOTAL_RE = re.compile(r"total(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
46
+ _COST_RE = re.compile(r"cost\s*[:=]?\s*\$?\s*([\d.]+)\s*(?:usd)?", re.IGNORECASE)
47
+
48
+
49
+ def _to_int(s: str) -> int:
50
+ """Parse a token count string, tolerating thousands separators."""
51
+ return int(s.replace(",", ""))
52
+
53
+
54
+ def extract(stdin_lines: list[str]) -> Optional[dict]:
55
+ """Parse Gemini CLI stdout and return a usage dict, or None.
56
+
57
+ Scans every line (the usage summary is at the tail but may be wrapped
58
+ by surrounding text) and accumulates the last seen model / token / cost
59
+ values. Requires at least one of input/output/total tokens to be found;
60
+ otherwise returns None (caller falls back to null payload).
61
+ """
62
+ if not stdin_lines:
63
+ return None
64
+
65
+ model = None
66
+ tin = tout = ttotal = None
67
+ cost = None
68
+
69
+ for raw in stdin_lines:
70
+ line = raw.rstrip("\n")
71
+
72
+ m = _MODEL_RE.match(line)
73
+ if m:
74
+ model = m.group(1)
75
+
76
+ m = _INPUT_RE.search(line)
77
+ if m:
78
+ tin = _to_int(m.group(1))
79
+
80
+ m = _OUTPUT_RE.search(line)
81
+ if m:
82
+ tout = _to_int(m.group(1))
83
+
84
+ m = _TOTAL_RE.search(line)
85
+ if m:
86
+ ttotal = _to_int(m.group(1))
87
+
88
+ m = _COST_RE.search(line)
89
+ if m:
90
+ try:
91
+ cost = float(m.group(1))
92
+ except ValueError:
93
+ pass
94
+
95
+ # Require at least one token figure; otherwise this isn't a gemini cycle.
96
+ if tin is None and tout is None and ttotal is None:
97
+ return None
98
+ if tin is None and tout is None and ttotal is not None:
99
+ # No split available — attribute the whole total to input so the
100
+ # cycle is non-zero; output stays 0.
101
+ tin = ttotal
102
+ tout = 0
103
+ else:
104
+ tin = tin or 0
105
+ tout = tout or 0
106
+ if ttotal is not None and tin == 0 and tout == 0:
107
+ tin = ttotal
108
+
109
+ model = model or _DEFAULT_MODEL
110
+
111
+ if cost is None:
112
+ if model_prices is not None:
113
+ cost = model_prices.compute_list_cost(
114
+ model,
115
+ input_tokens=tin,
116
+ output_tokens=tout,
117
+ )
118
+ else: # pragma: no cover - only when model_prices unimportable
119
+ cost = 0.0
120
+
121
+ return {
122
+ "model": model,
123
+ "input_tokens": tin,
124
+ "output_tokens": tout,
125
+ "cost_list_usd": cost,
126
+ "duration_ms": None,
127
+ }
@@ -0,0 +1,127 @@
1
+ """
2
+ kimi (Moonshot Kimi CLI) agent usage extractor.
3
+
4
+ Like openai and gemini (and unlike pi, which persists usage to session
5
+ files), the Kimi CLI prints a token-usage summary to stdout at the end of a
6
+ session. So this plugin implements the standard ``extract()`` registry
7
+ contract: scrape the passthrough stdout lines for the usage / model lines.
8
+
9
+ Recognised lines (case-insensitive, robust to thousands separators)::
10
+
11
+ Model: kimi-k2
12
+ Tokens: input=15300 output=3120
13
+
14
+ The Kimi CLI's "usage" / session-summary block is also accepted::
15
+
16
+ Input tokens: 15,300
17
+ Output tokens: 3,120
18
+ Total tokens: 18,420
19
+ model: kimi-k2
20
+
21
+ When an explicit USD cost line isn't present, cost is computed from
22
+ ``lib/model_prices.py`` (list price) so the dashboard never shows ``—``
23
+ for a recognised kimi cycle. Returns None if no usage line is found,
24
+ so the caller falls back to the null payload (US-LOOP-010 compatible).
25
+ """
26
+
27
+ import os
28
+ import re
29
+ import sys
30
+ from typing import Optional
31
+
32
+ # model_prices lives one level up (lib/), alongside this package.
33
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
34
+ try:
35
+ import model_prices
36
+ except Exception: # pragma: no cover - import guard
37
+ model_prices = None
38
+
39
+ # Default model when the output omits an explicit model line.
40
+ _DEFAULT_MODEL = "kimi-k2"
41
+
42
+ _MODEL_RE = re.compile(r"^\s*model\s*[:=]\s*([A-Za-z0-9][\w.\-]*)", re.IGNORECASE)
43
+ _INPUT_RE = re.compile(r"input(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
44
+ _OUTPUT_RE = re.compile(r"output(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
45
+ _TOTAL_RE = re.compile(r"total(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
46
+ _COST_RE = re.compile(r"cost\s*[:=]?\s*\$?\s*([\d.]+)\s*(?:usd)?", re.IGNORECASE)
47
+
48
+
49
+ def _to_int(s: str) -> int:
50
+ """Parse a token count string, tolerating thousands separators."""
51
+ return int(s.replace(",", ""))
52
+
53
+
54
+ def extract(stdin_lines: list[str]) -> Optional[dict]:
55
+ """Parse Kimi CLI stdout and return a usage dict, or None.
56
+
57
+ Scans every line (the usage summary is at the tail but may be wrapped
58
+ by surrounding text) and accumulates the last seen model / token / cost
59
+ values. Requires at least one of input/output/total tokens to be found;
60
+ otherwise returns None (caller falls back to null payload).
61
+ """
62
+ if not stdin_lines:
63
+ return None
64
+
65
+ model = None
66
+ tin = tout = ttotal = None
67
+ cost = None
68
+
69
+ for raw in stdin_lines:
70
+ line = raw.rstrip("\n")
71
+
72
+ m = _MODEL_RE.match(line)
73
+ if m:
74
+ model = m.group(1)
75
+
76
+ m = _INPUT_RE.search(line)
77
+ if m:
78
+ tin = _to_int(m.group(1))
79
+
80
+ m = _OUTPUT_RE.search(line)
81
+ if m:
82
+ tout = _to_int(m.group(1))
83
+
84
+ m = _TOTAL_RE.search(line)
85
+ if m:
86
+ ttotal = _to_int(m.group(1))
87
+
88
+ m = _COST_RE.search(line)
89
+ if m:
90
+ try:
91
+ cost = float(m.group(1))
92
+ except ValueError:
93
+ pass
94
+
95
+ # Require at least one token figure; otherwise this isn't a kimi cycle.
96
+ if tin is None and tout is None and ttotal is None:
97
+ return None
98
+ if tin is None and tout is None and ttotal is not None:
99
+ # No split available — attribute the whole total to input so the
100
+ # cycle is non-zero; output stays 0.
101
+ tin = ttotal
102
+ tout = 0
103
+ else:
104
+ tin = tin or 0
105
+ tout = tout or 0
106
+ if ttotal is not None and tin == 0 and tout == 0:
107
+ tin = ttotal
108
+
109
+ model = model or _DEFAULT_MODEL
110
+
111
+ if cost is None:
112
+ if model_prices is not None:
113
+ cost = model_prices.compute_list_cost(
114
+ model,
115
+ input_tokens=tin,
116
+ output_tokens=tout,
117
+ )
118
+ else: # pragma: no cover - only when model_prices unimportable
119
+ cost = 0.0
120
+
121
+ return {
122
+ "model": model,
123
+ "input_tokens": tin,
124
+ "output_tokens": tout,
125
+ "cost_list_usd": cost,
126
+ "duration_ms": None,
127
+ }
@@ -0,0 +1,126 @@
1
+ """
2
+ openai (codex / o-series) agent usage extractor.
3
+
4
+ Unlike pi (which runs in text mode and persists usage to session files),
5
+ the OpenAI Codex CLI prints a token-usage summary to stdout at the end of
6
+ a session. So this plugin implements the standard ``extract()`` registry
7
+ contract: scrape the passthrough stdout lines for the usage / model lines.
8
+
9
+ Recognised lines (case-insensitive, robust to thousands separators)::
10
+
11
+ Model: gpt-4o
12
+ Token usage: total=18420 input=15300 output=3120
13
+
14
+ Older / alternate Codex CLI formats are also accepted::
15
+
16
+ tokens used: 12,345 (treated as total only)
17
+ input tokens: 15300 output tokens: 3120
18
+ model: o3-mini
19
+
20
+ When an explicit USD cost line isn't present, cost is computed from
21
+ ``lib/model_prices.py`` (list price) so the dashboard never shows ``—``
22
+ for a recognised openai cycle. Returns None if no usage line is found,
23
+ so the caller falls back to the null payload (US-LOOP-010 compatible).
24
+ """
25
+
26
+ import os
27
+ import re
28
+ import sys
29
+ from typing import Optional
30
+
31
+ # model_prices lives one level up (lib/), alongside this package.
32
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
+ try:
34
+ import model_prices
35
+ except Exception: # pragma: no cover - import guard
36
+ model_prices = None
37
+
38
+ # Default model when the output omits an explicit model line.
39
+ _DEFAULT_MODEL = "gpt-4o"
40
+
41
+ _MODEL_RE = re.compile(r"^\s*model\s*[:=]\s*([A-Za-z0-9][\w.\-]*)", re.IGNORECASE)
42
+ _INPUT_RE = re.compile(r"input(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
43
+ _OUTPUT_RE = re.compile(r"output(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
44
+ _TOTAL_RE = re.compile(r"(?:tokens\s+used|total)\s*[:=]\s*([\d,]+)", re.IGNORECASE)
45
+ _COST_RE = re.compile(r"cost\s*[:=]?\s*\$?\s*([\d.]+)\s*(?:usd)?", re.IGNORECASE)
46
+
47
+
48
+ def _to_int(s: str) -> int:
49
+ """Parse a token count string, tolerating thousands separators."""
50
+ return int(s.replace(",", ""))
51
+
52
+
53
+ def extract(stdin_lines: list[str]) -> Optional[dict]:
54
+ """Parse Codex CLI stdout and return a usage dict, or None.
55
+
56
+ Scans every line (the usage summary is at the tail but may be wrapped
57
+ by surrounding text) and accumulates the last seen model / token / cost
58
+ values. Requires at least one of input/output/total tokens to be found;
59
+ otherwise returns None (caller falls back to null payload).
60
+ """
61
+ if not stdin_lines:
62
+ return None
63
+
64
+ model = None
65
+ tin = tout = ttotal = None
66
+ cost = None
67
+
68
+ for raw in stdin_lines:
69
+ line = raw.rstrip("\n")
70
+
71
+ m = _MODEL_RE.match(line)
72
+ if m:
73
+ model = m.group(1)
74
+
75
+ m = _INPUT_RE.search(line)
76
+ if m:
77
+ tin = _to_int(m.group(1))
78
+
79
+ m = _OUTPUT_RE.search(line)
80
+ if m:
81
+ tout = _to_int(m.group(1))
82
+
83
+ m = _TOTAL_RE.search(line)
84
+ if m:
85
+ ttotal = _to_int(m.group(1))
86
+
87
+ m = _COST_RE.search(line)
88
+ if m:
89
+ try:
90
+ cost = float(m.group(1))
91
+ except ValueError:
92
+ pass
93
+
94
+ # Derive input/output from total when only a total was reported.
95
+ if tin is None and tout is None and ttotal is None:
96
+ return None
97
+ if tin is None and tout is None and ttotal is not None:
98
+ # No split available — attribute the whole total to input so the
99
+ # cycle is non-zero; output stays 0.
100
+ tin = ttotal
101
+ tout = 0
102
+ else:
103
+ tin = tin or 0
104
+ tout = tout or 0
105
+ if ttotal is not None and tin == 0 and tout == 0:
106
+ tin = ttotal
107
+
108
+ model = model or _DEFAULT_MODEL
109
+
110
+ if cost is None:
111
+ if model_prices is not None:
112
+ cost = model_prices.compute_list_cost(
113
+ model,
114
+ input_tokens=tin,
115
+ output_tokens=tout,
116
+ )
117
+ else: # pragma: no cover - only when model_prices unimportable
118
+ cost = 0.0
119
+
120
+ return {
121
+ "model": model,
122
+ "input_tokens": tin,
123
+ "output_tokens": tout,
124
+ "cost_list_usd": cost,
125
+ "duration_ms": None,
126
+ }
@@ -0,0 +1,128 @@
1
+ """
2
+ qwen (Alibaba Qwen / dashscope CLI) agent usage extractor.
3
+
4
+ Like openai, gemini and kimi (and unlike pi, which persists usage to session
5
+ files), the Qwen / qwen-coder / dashscope CLI prints a token-usage summary to
6
+ stdout at the end of a session. So this plugin implements the standard
7
+ ``extract()`` registry contract: scrape the passthrough stdout lines for the
8
+ usage / model lines.
9
+
10
+ Recognised lines (case-insensitive, robust to thousands separators)::
11
+
12
+ Model: qwen-coder-plus
13
+ Tokens: input=15300 output=3120
14
+
15
+ The dashscope "usage" / session-summary block is also accepted::
16
+
17
+ Input tokens: 15,300
18
+ Output tokens: 3,120
19
+ Total tokens: 18,420
20
+ model: qwen-max
21
+
22
+ When an explicit USD cost line isn't present, cost is computed from
23
+ ``lib/model_prices.py`` (list price) so the dashboard never shows ``—``
24
+ for a recognised qwen cycle. Returns None if no usage line is found,
25
+ so the caller falls back to the null payload (US-LOOP-010 compatible).
26
+ """
27
+
28
+ import os
29
+ import re
30
+ import sys
31
+ from typing import Optional
32
+
33
+ # model_prices lives one level up (lib/), alongside this package.
34
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
35
+ try:
36
+ import model_prices
37
+ except Exception: # pragma: no cover - import guard
38
+ model_prices = None
39
+
40
+ # Default model when the output omits an explicit model line.
41
+ _DEFAULT_MODEL = "qwen-coder-plus"
42
+
43
+ _MODEL_RE = re.compile(r"^\s*model\s*[:=]\s*([A-Za-z0-9][\w.\-]*)", re.IGNORECASE)
44
+ _INPUT_RE = re.compile(r"input(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
45
+ _OUTPUT_RE = re.compile(r"output(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
46
+ _TOTAL_RE = re.compile(r"total(?:\s+tokens)?\s*[:=]\s*([\d,]+)", re.IGNORECASE)
47
+ _COST_RE = re.compile(r"cost\s*[:=]?\s*\$?\s*([\d.]+)\s*(?:usd)?", re.IGNORECASE)
48
+
49
+
50
+ def _to_int(s: str) -> int:
51
+ """Parse a token count string, tolerating thousands separators."""
52
+ return int(s.replace(",", ""))
53
+
54
+
55
+ def extract(stdin_lines: list[str]) -> Optional[dict]:
56
+ """Parse Qwen CLI stdout and return a usage dict, or None.
57
+
58
+ Scans every line (the usage summary is at the tail but may be wrapped
59
+ by surrounding text) and accumulates the last seen model / token / cost
60
+ values. Requires at least one of input/output/total tokens to be found;
61
+ otherwise returns None (caller falls back to null payload).
62
+ """
63
+ if not stdin_lines:
64
+ return None
65
+
66
+ model = None
67
+ tin = tout = ttotal = None
68
+ cost = None
69
+
70
+ for raw in stdin_lines:
71
+ line = raw.rstrip("\n")
72
+
73
+ m = _MODEL_RE.match(line)
74
+ if m:
75
+ model = m.group(1)
76
+
77
+ m = _INPUT_RE.search(line)
78
+ if m:
79
+ tin = _to_int(m.group(1))
80
+
81
+ m = _OUTPUT_RE.search(line)
82
+ if m:
83
+ tout = _to_int(m.group(1))
84
+
85
+ m = _TOTAL_RE.search(line)
86
+ if m:
87
+ ttotal = _to_int(m.group(1))
88
+
89
+ m = _COST_RE.search(line)
90
+ if m:
91
+ try:
92
+ cost = float(m.group(1))
93
+ except ValueError:
94
+ pass
95
+
96
+ # Require at least one token figure; otherwise this isn't a qwen cycle.
97
+ if tin is None and tout is None and ttotal is None:
98
+ return None
99
+ if tin is None and tout is None and ttotal is not None:
100
+ # No split available — attribute the whole total to input so the
101
+ # cycle is non-zero; output stays 0.
102
+ tin = ttotal
103
+ tout = 0
104
+ else:
105
+ tin = tin or 0
106
+ tout = tout or 0
107
+ if ttotal is not None and tin == 0 and tout == 0:
108
+ tin = ttotal
109
+
110
+ model = model or _DEFAULT_MODEL
111
+
112
+ if cost is None:
113
+ if model_prices is not None:
114
+ cost = model_prices.compute_list_cost(
115
+ model,
116
+ input_tokens=tin,
117
+ output_tokens=tout,
118
+ )
119
+ else: # pragma: no cover - only when model_prices unimportable
120
+ cost = 0.0
121
+
122
+ return {
123
+ "model": model,
124
+ "input_tokens": tin,
125
+ "output_tokens": tout,
126
+ "cost_list_usd": cost,
127
+ "duration_ms": None,
128
+ }