meshapi-code 0.1.0__tar.gz → 0.2.1__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.1
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.1"
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.1"
@@ -0,0 +1,146 @@
1
+ """meshapi — terminal chat REPL for Mesh API."""
2
+ import argparse
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import httpx
7
+ from prompt_toolkit import PromptSession
8
+ from prompt_toolkit.history import FileHistory
9
+ from prompt_toolkit.styles import Style
10
+ from rich.text import Text
11
+
12
+ from . import __version__
13
+ from .client import stream_chat
14
+ from .commands import handle_command
15
+ from .config import CONFIG_FILE, HISTORY_FILE, load_config
16
+ from .render import BRAND, BRAND_BG, BRAND_BG_FG, BRAND_DIM, console, fmt_usd, pretty_cwd, render_stream
17
+
18
+ # ANSI Shadow figlet font
19
+ MESH_LOGO_LINES = [
20
+ "███╗ ███╗███████╗███████╗██╗ ██╗",
21
+ "████╗ ████║██╔════╝██╔════╝██║ ██║",
22
+ "██╔████╔██║█████╗ ███████╗███████║",
23
+ "██║╚██╔╝██║██╔══╝ ╚════██║██╔══██║",
24
+ "██║ ╚═╝ ██║███████╗███████║██║ ██║",
25
+ "╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝",
26
+ ]
27
+ LOGO_WIDTH = 35 # chars per line
28
+ LOGO_GUTTER = 3 # spaces between logo and info column
29
+
30
+
31
+ def parse_args(argv=None) -> argparse.Namespace:
32
+ p = argparse.ArgumentParser(prog="meshapi", description="Terminal chat for Mesh API")
33
+ p.add_argument("--version", action="version", version=f"meshapi {__version__}")
34
+ p.add_argument("--model", help="Override model for this session (e.g. openai/gpt-4o-mini)")
35
+ p.add_argument("--route", choices=["cheapest", "fastest", "balanced"], help="Routing mode")
36
+ return p.parse_args(argv)
37
+
38
+
39
+ def main() -> None:
40
+ args = parse_args()
41
+ cfg = load_config()
42
+ if args.model:
43
+ cfg["model"] = args.model
44
+ if args.route:
45
+ cfg["route"] = args.route
46
+
47
+ if not cfg["api_key"]:
48
+ console.print(
49
+ "[red]No API key found. Set MESHAPI_API_KEY env var or edit "
50
+ f"{CONFIG_FILE}[/red]"
51
+ )
52
+ sys.exit(1)
53
+
54
+ state = {
55
+ "cfg": cfg,
56
+ "messages": [{"role": "system", "content": cfg["system"]}],
57
+ "session_cost": 0.0,
58
+ }
59
+
60
+ session = PromptSession(history=FileHistory(str(HISTORY_FILE)))
61
+
62
+ info_per_line: list = [
63
+ None,
64
+ None,
65
+ Text.from_markup(f"[bold {BRAND}]✦ meshapi {__version__}[/bold {BRAND}]"),
66
+ Text.from_markup(f"cwd: [{BRAND}]{pretty_cwd()}[/{BRAND}]"),
67
+ Text.from_markup(f"model: [bold {BRAND}]{cfg['model']}[/bold {BRAND}]"),
68
+ Text.from_markup(f"route: [{BRAND}]{cfg.get('route') or 'default'}[/{BRAND}]"),
69
+ ]
70
+
71
+ console.print() # top gap so banner doesn't crowd the shell prompt
72
+ for i, logo_line in enumerate(MESH_LOGO_LINES):
73
+ line = Text()
74
+ line.append(logo_line, style=BRAND)
75
+ info = info_per_line[i] if i < len(info_per_line) else None
76
+ if info is not None:
77
+ pad = max(0, LOGO_WIDTH - len(logo_line))
78
+ line.append(" " * (pad + LOGO_GUTTER))
79
+ line.append(info)
80
+ console.print(line)
81
+ console.print()
82
+ console.print("type /help for commands, /exit to quit", style=BRAND_DIM)
83
+ console.print() # bottom gap before the first prompt rule
84
+
85
+ while True:
86
+ try:
87
+ console.rule(
88
+ title=f"[{BRAND_DIM}]{Path.cwd().name}[/{BRAND_DIM}]",
89
+ align="right",
90
+ style=BRAND_DIM,
91
+ characters="─",
92
+ )
93
+ user_input = session.prompt(
94
+ "› ",
95
+ style=Style.from_dict({
96
+ "prompt": f"bold fg:{BRAND} bg:{BRAND_BG}",
97
+ "": f"fg:{BRAND_BG_FG} bg:{BRAND_BG}",
98
+ }),
99
+ )
100
+ console.rule(style=BRAND_DIM, characters="─")
101
+ except (KeyboardInterrupt, EOFError):
102
+ console.print("\n[dim]bye[/dim]")
103
+ break
104
+
105
+ if not user_input.strip():
106
+ continue
107
+ if user_input.startswith("/"):
108
+ if not handle_command(user_input, state):
109
+ break
110
+ continue
111
+
112
+ state["messages"].append({"role": "user", "content": user_input})
113
+ console.print()
114
+ try:
115
+ reply, meta = render_stream(stream_chat(state["messages"], state["cfg"]))
116
+ state["messages"].append({"role": "assistant", "content": reply})
117
+
118
+ cost = meta.get("cost")
119
+ if cost is not None:
120
+ try:
121
+ state["session_cost"] += float(cost)
122
+ except (TypeError, ValueError):
123
+ pass
124
+ usage = meta.get("usage") or {}
125
+ model = meta.get("model") or state["cfg"]["model"]
126
+ elapsed = meta.get("elapsed", 0.0)
127
+ prompt_t = usage.get("prompt_tokens", "?")
128
+ completion_t = usage.get("completion_tokens", "?")
129
+ cost_str = fmt_usd(cost) if cost is not None else "—"
130
+ console.rule(style=BRAND_DIM, characters="─")
131
+ console.print(
132
+ f"[dim]{model} • {prompt_t}→{completion_t} tok • {cost_str} • "
133
+ f"session {fmt_usd(state['session_cost'])} • {elapsed:.1f}s[/dim]"
134
+ )
135
+ except httpx.HTTPStatusError as e:
136
+ console.rule(style="dim red", characters="─")
137
+ console.print(f"[red]API error {e.response.status_code}: {e.response.text}[/red]")
138
+ state["messages"].pop()
139
+ except Exception as e:
140
+ console.rule(style="dim red", characters="─")
141
+ console.print(f"[red]Error: {e}[/red]")
142
+ state["messages"].pop()
143
+
144
+
145
+ if __name__ == "__main__":
146
+ 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,121 @@
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
+ if _detect_theme() == "dark":
33
+ BRAND = "#8b78f7" # bumped lighter on dark — official #6f5af5 reads dim on dark wine/black
34
+ BRAND_DIM = "#aea3f0" # lighter dim — clearly visible on dark backgrounds
35
+ BRAND_BG = "#372d73" # mid-dark purple — clearly visible without being loud
36
+ BRAND_BG_FG = "#f5f0ff" # near-white with slight purple tint for input text
37
+ else:
38
+ BRAND = "#6f5af5" # official brand color — strong contrast on white
39
+ BRAND_DIM = "#5a4ec4" # darker dim — visible on light bg
40
+ BRAND_BG = "#ebe4fc" # pale lavender highlight against white
41
+ BRAND_BG_FG = "#2c2540" # near-black with purple tint for input text on light theme
42
+
43
+
44
+ def fmt_usd(value) -> str:
45
+ """USD formatter matching dashboard `fmtUsd` (routersvc-client/src/lib/utils.ts).
46
+
47
+ Always 6 decimals; K/M abbreviations for large values. Never use raw f-string
48
+ rounding for money — `999.999833` would render `$1000.00`.
49
+ """
50
+ try:
51
+ n = float(value)
52
+ except (TypeError, ValueError):
53
+ return "$0.000000"
54
+ if abs(n) >= 1_000_000:
55
+ return f"${n / 1_000_000:.2f}M"
56
+ if abs(n) >= 1_000:
57
+ return f"${n / 1_000:.2f}K"
58
+ return f"${n:.6f}"
59
+
60
+
61
+ def pretty_cwd() -> str:
62
+ """Show cwd as `~/relative` if under $HOME, else absolute."""
63
+ cwd = Path.cwd()
64
+ home = Path.home()
65
+ try:
66
+ return f"~/{cwd.relative_to(home)}"
67
+ except ValueError:
68
+ return str(cwd)
69
+
70
+
71
+ class _StreamView:
72
+ """Renderable: meshing-around spinner + elapsed timer above streamed markdown."""
73
+
74
+ def __init__(self) -> None:
75
+ self.start = time.monotonic()
76
+ self.buf = ""
77
+ self.first_token_at: Optional[float] = None
78
+ self.done = False
79
+ self._spinner = Spinner("dots", style=BRAND)
80
+
81
+ def elapsed(self) -> float:
82
+ return time.monotonic() - self.start
83
+
84
+ def __rich_console__(self, console, options):
85
+ if self.done:
86
+ if self.buf:
87
+ yield Markdown(self.buf)
88
+ return
89
+ label = "meshing around" if not self.buf else "still meshing"
90
+ self._spinner.text = Text(f"{label}... {self.elapsed():.1f}s", style=BRAND_DIM)
91
+ if self.buf:
92
+ yield Markdown(self.buf)
93
+ yield self._spinner
94
+ else:
95
+ yield self._spinner
96
+
97
+
98
+ def render_stream(events: Iterable) -> tuple[str, dict]:
99
+ """Live-render streamed content with a meshing-around spinner + timer.
100
+
101
+ Returns (full_text, metadata). Generator yields strings (content deltas)
102
+ and an optional final dict (usage + cost + model from the SSE tail).
103
+ `meta['elapsed']` and `meta['ttft']` (time-to-first-token) are added on
104
+ the way out.
105
+ """
106
+ view = _StreamView()
107
+ meta: dict = {}
108
+ with Live(view, console=console, refresh_per_second=12, auto_refresh=True) as live:
109
+ for event in events:
110
+ if isinstance(event, str):
111
+ if view.first_token_at is None:
112
+ view.first_token_at = view.elapsed()
113
+ view.buf += event
114
+ elif isinstance(event, dict):
115
+ meta.update(event)
116
+ view.done = True
117
+ live.refresh()
118
+ meta["elapsed"] = view.elapsed()
119
+ if view.first_token_at is not None:
120
+ meta["ttft"] = view.first_token_at
121
+ return view.buf, meta
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
@@ -1,102 +0,0 @@
1
- """meshapi — terminal chat REPL for Mesh API."""
2
- import argparse
3
- import sys
4
-
5
- import httpx
6
- from prompt_toolkit import PromptSession
7
- from prompt_toolkit.history import FileHistory
8
- from prompt_toolkit.styles import Style
9
- from rich.panel import Panel
10
-
11
- from . import __version__
12
- from .client import stream_chat
13
- from .commands import handle_command
14
- from .config import CONFIG_FILE, HISTORY_FILE, load_config
15
- from .render import console, fmt_usd, render_stream
16
-
17
-
18
- def parse_args(argv=None) -> argparse.Namespace:
19
- p = argparse.ArgumentParser(prog="meshapi", description="Terminal chat for Mesh API")
20
- p.add_argument("--version", action="version", version=f"meshapi {__version__}")
21
- p.add_argument("--model", help="Override model for this session (e.g. openai/gpt-4o-mini)")
22
- p.add_argument("--route", choices=["cheapest", "fastest", "balanced"], help="Routing mode")
23
- return p.parse_args(argv)
24
-
25
-
26
- def main() -> None:
27
- args = parse_args()
28
- cfg = load_config()
29
- if args.model:
30
- cfg["model"] = args.model
31
- if args.route:
32
- cfg["route"] = args.route
33
-
34
- if not cfg["api_key"]:
35
- console.print(
36
- "[red]No API key found. Set MESHAPI_API_KEY env var or edit "
37
- f"{CONFIG_FILE}[/red]"
38
- )
39
- sys.exit(1)
40
-
41
- state = {
42
- "cfg": cfg,
43
- "messages": [{"role": "system", "content": cfg["system"]}],
44
- "session_cost": 0.0,
45
- }
46
-
47
- session = PromptSession(history=FileHistory(str(HISTORY_FILE)))
48
- console.print(Panel.fit(
49
- 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"
52
- "type /help for commands, /exit to quit",
53
- border_style="cyan",
54
- ))
55
-
56
- while True:
57
- try:
58
- user_input = session.prompt(
59
- "you > ",
60
- style=Style.from_dict({"prompt": "ansicyan bold"}),
61
- )
62
- except (KeyboardInterrupt, EOFError):
63
- console.print("\n[dim]bye[/dim]")
64
- break
65
-
66
- if not user_input.strip():
67
- continue
68
- if user_input.startswith("/"):
69
- if not handle_command(user_input, state):
70
- break
71
- continue
72
-
73
- state["messages"].append({"role": "user", "content": user_input})
74
- console.print()
75
- try:
76
- reply, meta = render_stream(stream_chat(state["messages"], state["cfg"]))
77
- state["messages"].append({"role": "assistant", "content": reply})
78
-
79
- cost = meta.get("cost")
80
- if cost is not None:
81
- try:
82
- state["session_cost"] += float(cost)
83
- except (TypeError, ValueError):
84
- pass
85
- usage = meta.get("usage") or {}
86
- tokens = (
87
- f"{usage.get('prompt_tokens', '?')} → {usage.get('completion_tokens', '?')} tok"
88
- if usage else ""
89
- )
90
- cost_line = f"[dim]{tokens} • {fmt_usd(cost)} • session {fmt_usd(state['session_cost'])}[/dim]"
91
- console.print(cost_line)
92
- except httpx.HTTPStatusError as e:
93
- console.print(f"[red]API error {e.response.status_code}: {e.response.text}[/red]")
94
- state["messages"].pop()
95
- except Exception as e:
96
- console.print(f"[red]Error: {e}[/red]")
97
- state["messages"].pop()
98
- console.print()
99
-
100
-
101
- if __name__ == "__main__":
102
- main()
@@ -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