meshapi-code 0.1.0__tar.gz → 0.2.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: meshapi-code
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Terminal chat for Mesh API — OpenAI-compatible LLM gateway
5
5
  Project-URL: Homepage, https://meshapi.ai
6
6
  Project-URL: Documentation, https://docs.meshapi.ai
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "meshapi-code"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Terminal chat for Mesh API — OpenAI-compatible LLM gateway"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -1,6 +1,7 @@
1
1
  """meshapi — terminal chat REPL for Mesh API."""
2
2
  import argparse
3
3
  import sys
4
+ from pathlib import Path
4
5
 
5
6
  import httpx
6
7
  from prompt_toolkit import PromptSession
@@ -12,7 +13,7 @@ from . import __version__
12
13
  from .client import stream_chat
13
14
  from .commands import handle_command
14
15
  from .config import CONFIG_FILE, HISTORY_FILE, load_config
15
- from .render import console, fmt_usd, render_stream
16
+ from .render import BRAND, BRAND_BG, BRAND_DIM, console, fmt_usd, pretty_cwd, render_stream
16
17
 
17
18
 
18
19
  def parse_args(argv=None) -> argparse.Namespace:
@@ -47,18 +48,29 @@ def main() -> None:
47
48
  session = PromptSession(history=FileHistory(str(HISTORY_FILE)))
48
49
  console.print(Panel.fit(
49
50
  f"meshapi {__version__}\n"
50
- f"model: [bold cyan]{cfg['model']}[/bold cyan]\n"
51
- f"route: [cyan]{cfg.get('route') or 'default'}[/cyan]\n"
51
+ f"cwd: [{BRAND}]{pretty_cwd()}[/{BRAND}]\n"
52
+ f"model: [bold {BRAND}]{cfg['model']}[/bold {BRAND}]\n"
53
+ f"route: [{BRAND}]{cfg.get('route') or 'default'}[/{BRAND}]\n"
52
54
  "type /help for commands, /exit to quit",
53
- border_style="cyan",
55
+ border_style=BRAND,
54
56
  ))
55
57
 
56
58
  while True:
57
59
  try:
60
+ console.rule(
61
+ title=f"[{BRAND_DIM}]{Path.cwd().name}[/{BRAND_DIM}]",
62
+ align="right",
63
+ style=BRAND_DIM,
64
+ characters="─",
65
+ )
58
66
  user_input = session.prompt(
59
- "you > ",
60
- style=Style.from_dict({"prompt": "ansicyan bold"}),
67
+ " ",
68
+ style=Style.from_dict({
69
+ "prompt": f"bold fg:{BRAND} bg:{BRAND_BG}",
70
+ "": f"bg:{BRAND_BG}",
71
+ }),
61
72
  )
73
+ console.rule(style=BRAND_DIM, characters="─")
62
74
  except (KeyboardInterrupt, EOFError):
63
75
  console.print("\n[dim]bye[/dim]")
64
76
  break
@@ -83,19 +95,24 @@ def main() -> None:
83
95
  except (TypeError, ValueError):
84
96
  pass
85
97
  usage = meta.get("usage") or {}
86
- tokens = (
87
- f"{usage.get('prompt_tokens', '?')} → {usage.get('completion_tokens', '?')} tok"
88
- if usage else ""
98
+ model = meta.get("model") or state["cfg"]["model"]
99
+ elapsed = meta.get("elapsed", 0.0)
100
+ prompt_t = usage.get("prompt_tokens", "?")
101
+ completion_t = usage.get("completion_tokens", "?")
102
+ cost_str = fmt_usd(cost) if cost is not None else "—"
103
+ console.rule(style=BRAND_DIM, characters="─")
104
+ console.print(
105
+ f"[dim]{model} • {prompt_t}→{completion_t} tok • {cost_str} • "
106
+ f"session {fmt_usd(state['session_cost'])} • {elapsed:.1f}s[/dim]"
89
107
  )
90
- cost_line = f"[dim]{tokens} • {fmt_usd(cost)} • session {fmt_usd(state['session_cost'])}[/dim]"
91
- console.print(cost_line)
92
108
  except httpx.HTTPStatusError as e:
109
+ console.rule(style="dim red", characters="─")
93
110
  console.print(f"[red]API error {e.response.status_code}: {e.response.text}[/red]")
94
111
  state["messages"].pop()
95
112
  except Exception as e:
113
+ console.rule(style="dim red", characters="─")
96
114
  console.print(f"[red]Error: {e}[/red]")
97
115
  state["messages"].pop()
98
- console.print()
99
116
 
100
117
 
101
118
  if __name__ == "__main__":
@@ -24,6 +24,7 @@ def stream_chat(messages: list, cfg: dict) -> Iterable:
24
24
  payload["route"] = cfg["route"]
25
25
 
26
26
  last_meta: dict = {}
27
+ last_model: str = ""
27
28
  with httpx.stream("POST", url, json=payload, headers=headers, timeout=120) as r:
28
29
  r.raise_for_status()
29
30
  for line in r.iter_lines():
@@ -37,6 +38,9 @@ def stream_chat(messages: list, cfg: dict) -> Iterable:
37
38
  except json.JSONDecodeError:
38
39
  continue
39
40
 
41
+ if obj.get("model"):
42
+ last_model = obj["model"]
43
+
40
44
  choices = obj.get("choices") or []
41
45
  if choices:
42
46
  delta = choices[0].get("delta", {}).get("content")
@@ -48,5 +52,7 @@ def stream_chat(messages: list, cfg: dict) -> Iterable:
48
52
  if usage or cost:
49
53
  last_meta = {"usage": usage, "cost": cost}
50
54
 
55
+ if last_model:
56
+ last_meta["model"] = last_model
51
57
  if last_meta:
52
58
  yield last_meta
@@ -0,0 +1,118 @@
1
+ """Rich-based markdown live rendering and shared formatters."""
2
+ import os
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Iterable, Optional
6
+
7
+ from rich.console import Console
8
+ from rich.live import Live
9
+ from rich.markdown import Markdown
10
+ from rich.spinner import Spinner
11
+ from rich.text import Text
12
+
13
+ console = Console()
14
+
15
+
16
+ def _detect_theme() -> str:
17
+ """Returns 'dark' or 'light'. Override with MESHAPI_THEME=light|dark."""
18
+ forced = os.environ.get("MESHAPI_THEME", "").strip().lower()
19
+ if forced in ("light", "dark"):
20
+ return forced
21
+ # COLORFGBG is set by Konsole, urxvt, some VS Code configs. Format: "fg;bg".
22
+ parts = os.environ.get("COLORFGBG", "").strip().split(";")
23
+ if len(parts) >= 2:
24
+ try:
25
+ return "dark" if int(parts[-1]) < 8 else "light"
26
+ except ValueError:
27
+ pass
28
+ return "dark" # most devs use dark; safe default
29
+
30
+
31
+ # Brand palette — Mesh API purple, theme-adaptive
32
+ BRAND = "#6f5af5" # foreground brand, same on both themes
33
+ if _detect_theme() == "dark":
34
+ BRAND_DIM = "#9d92e8" # lighter dim — visible on dark bg
35
+ BRAND_BG = "#2d2454" # darker, brand-tinted highlight against ~#000-#1e1e1e
36
+ else:
37
+ BRAND_DIM = "#5a4ec4" # darker dim — visible on light bg
38
+ BRAND_BG = "#ebe4fc" # pale lavender highlight against white
39
+
40
+
41
+ def fmt_usd(value) -> str:
42
+ """USD formatter matching dashboard `fmtUsd` (routersvc-client/src/lib/utils.ts).
43
+
44
+ Always 6 decimals; K/M abbreviations for large values. Never use raw f-string
45
+ rounding for money — `999.999833` would render `$1000.00`.
46
+ """
47
+ try:
48
+ n = float(value)
49
+ except (TypeError, ValueError):
50
+ return "$0.000000"
51
+ if abs(n) >= 1_000_000:
52
+ return f"${n / 1_000_000:.2f}M"
53
+ if abs(n) >= 1_000:
54
+ return f"${n / 1_000:.2f}K"
55
+ return f"${n:.6f}"
56
+
57
+
58
+ def pretty_cwd() -> str:
59
+ """Show cwd as `~/relative` if under $HOME, else absolute."""
60
+ cwd = Path.cwd()
61
+ home = Path.home()
62
+ try:
63
+ return f"~/{cwd.relative_to(home)}"
64
+ except ValueError:
65
+ return str(cwd)
66
+
67
+
68
+ class _StreamView:
69
+ """Renderable: meshing-around spinner + elapsed timer above streamed markdown."""
70
+
71
+ def __init__(self) -> None:
72
+ self.start = time.monotonic()
73
+ self.buf = ""
74
+ self.first_token_at: Optional[float] = None
75
+ self.done = False
76
+ self._spinner = Spinner("dots", style=BRAND)
77
+
78
+ def elapsed(self) -> float:
79
+ return time.monotonic() - self.start
80
+
81
+ def __rich_console__(self, console, options):
82
+ if self.done:
83
+ if self.buf:
84
+ yield Markdown(self.buf)
85
+ return
86
+ label = "meshing around" if not self.buf else "still meshing"
87
+ self._spinner.text = Text(f"{label}... {self.elapsed():.1f}s", style=BRAND_DIM)
88
+ if self.buf:
89
+ yield Markdown(self.buf)
90
+ yield self._spinner
91
+ else:
92
+ yield self._spinner
93
+
94
+
95
+ def render_stream(events: Iterable) -> tuple[str, dict]:
96
+ """Live-render streamed content with a meshing-around spinner + timer.
97
+
98
+ Returns (full_text, metadata). Generator yields strings (content deltas)
99
+ and an optional final dict (usage + cost + model from the SSE tail).
100
+ `meta['elapsed']` and `meta['ttft']` (time-to-first-token) are added on
101
+ the way out.
102
+ """
103
+ view = _StreamView()
104
+ meta: dict = {}
105
+ with Live(view, console=console, refresh_per_second=12, auto_refresh=True) as live:
106
+ for event in events:
107
+ if isinstance(event, str):
108
+ if view.first_token_at is None:
109
+ view.first_token_at = view.elapsed()
110
+ view.buf += event
111
+ elif isinstance(event, dict):
112
+ meta.update(event)
113
+ view.done = True
114
+ live.refresh()
115
+ meta["elapsed"] = view.elapsed()
116
+ if view.first_token_at is not None:
117
+ meta["ttft"] = view.first_token_at
118
+ return view.buf, meta
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
@@ -1,43 +0,0 @@
1
- """Rich-based markdown live rendering and shared formatters."""
2
- from typing import Iterable
3
-
4
- from rich.console import Console
5
- from rich.live import Live
6
- from rich.markdown import Markdown
7
-
8
- console = Console()
9
-
10
-
11
- def fmt_usd(value) -> str:
12
- """USD formatter matching dashboard `fmtUsd` (routersvc-client/src/lib/utils.ts).
13
-
14
- Always 6 decimals; K/M abbreviations for large values. Never use raw f-string
15
- rounding for money — `999.999833` would render `$1000.00`.
16
- """
17
- try:
18
- n = float(value)
19
- except (TypeError, ValueError):
20
- return "$0.000000"
21
- if abs(n) >= 1_000_000:
22
- return f"${n / 1_000_000:.2f}M"
23
- if abs(n) >= 1_000:
24
- return f"${n / 1_000:.2f}K"
25
- return f"${n:.6f}"
26
-
27
-
28
- def render_stream(events: Iterable) -> tuple[str, dict]:
29
- """Live-render markdown content; return (full_text, metadata).
30
-
31
- Generator yields strings (content deltas) and an optional final dict
32
- (usage + cost from Mesh API's last SSE chunk).
33
- """
34
- buf = ""
35
- meta: dict = {}
36
- with Live(console=console, refresh_per_second=20) as live:
37
- for event in events:
38
- if isinstance(event, str):
39
- buf += event
40
- live.update(Markdown(buf))
41
- elif isinstance(event, dict):
42
- meta.update(event)
43
- return buf, meta
File without changes
File without changes
File without changes
File without changes