slice-gateway 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jishnu Janardanan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # The slice-gateway distribution is the CLI only. Keep the sdist to the CLI module,
2
+ # its PyPI readme, the license, and build metadata. Nothing from the server repo.
3
+ prune tests
4
+ prune app
5
+ prune infra
6
+ prune k8s
7
+ prune dashboard
8
+ prune desktop
9
+ prune website
10
+ prune migrations
11
+ prune demo
12
+ prune scripts
13
+ prune guardrails
14
+ prune docs
15
+ prune mcp_server
16
+ prune gateway
17
+ prune pipeline
18
+ prune rag_store
19
+ exclude README.md
20
+ exclude requirements.txt
21
+ exclude docker-compose.yml
22
+ exclude Dockerfile
23
+ exclude pytest.ini
24
+ global-exclude *.pyc
25
+ global-exclude .DS_Store
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: slice-gateway
3
+ Version: 0.1.0
4
+ Summary: Command-line client for slice, the self-hosted LLM gateway.
5
+ Author: Jishnu Janardanan
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://sliceapp.dev
8
+ Project-URL: Repository, https://github.com/jjk30/slice
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: typer
17
+ Requires-Dist: httpx
18
+ Dynamic: license-file
19
+
20
+ # slice-gateway
21
+
22
+ Command-line client for slice, a self-hosted LLM gateway that routes, caches, and caps your AI spend.
23
+
24
+ This package installs the `slice` command. It logs you in through GitHub, then prints the environment lines that point your existing tools at a slice gateway. It has no server dependencies. It needs only typer and httpx, plus the standard library.
25
+
26
+ ## Quickstart
27
+
28
+ 1. Install the CLI:
29
+
30
+ pip install slice-gateway
31
+
32
+ 2. Log in through GitHub:
33
+
34
+ slice login
35
+
36
+ 3. Point a tool at slice. For Claude Code, set three variables:
37
+
38
+ export ANTHROPIC_BASE_URL=https://api.sliceapp.dev
39
+ export ANTHROPIC_API_KEY=sk-ant-api... # your own Anthropic key
40
+ export ANTHROPIC_AUTH_TOKEN=slk_live_... # your slice key, from slice login
41
+
42
+ `ANTHROPIC_AUTH_TOKEN` goes out as `Authorization: Bearer`, which is where slice reads its key. `ANTHROPIC_API_KEY` stays your own Anthropic key in `x-api-key`, and slice forwards it upstream.
43
+
44
+ Run `slice use anthropic`, `slice use openai`, `slice use curl`, or `slice use claude-code` to print the exact lines for your tool.
45
+
46
+ ## The gateway
47
+
48
+ This is only the client. The slice gateway itself, the server that does the routing, caching, budgets, and dashboards, lives on GitHub at https://github.com/jjk30/slice.
49
+
50
+ Homepage: https://sliceapp.dev
@@ -0,0 +1,31 @@
1
+ # slice-gateway
2
+
3
+ Command-line client for slice, a self-hosted LLM gateway that routes, caches, and caps your AI spend.
4
+
5
+ This package installs the `slice` command. It logs you in through GitHub, then prints the environment lines that point your existing tools at a slice gateway. It has no server dependencies. It needs only typer and httpx, plus the standard library.
6
+
7
+ ## Quickstart
8
+
9
+ 1. Install the CLI:
10
+
11
+ pip install slice-gateway
12
+
13
+ 2. Log in through GitHub:
14
+
15
+ slice login
16
+
17
+ 3. Point a tool at slice. For Claude Code, set three variables:
18
+
19
+ export ANTHROPIC_BASE_URL=https://api.sliceapp.dev
20
+ export ANTHROPIC_API_KEY=sk-ant-api... # your own Anthropic key
21
+ export ANTHROPIC_AUTH_TOKEN=slk_live_... # your slice key, from slice login
22
+
23
+ `ANTHROPIC_AUTH_TOKEN` goes out as `Authorization: Bearer`, which is where slice reads its key. `ANTHROPIC_API_KEY` stays your own Anthropic key in `x-api-key`, and slice forwards it upstream.
24
+
25
+ Run `slice use anthropic`, `slice use openai`, `slice use curl`, or `slice use claude-code` to print the exact lines for your tool.
26
+
27
+ ## The gateway
28
+
29
+ This is only the client. The slice gateway itself, the server that does the routing, caching, budgets, and dashboards, lives on GitHub at https://github.com/jjk30/slice.
30
+
31
+ Homepage: https://sliceapp.dev
@@ -0,0 +1,39 @@
1
+ # Builds the `slice-gateway` distribution: the slice CLI only (slice login / init / use).
2
+ # The wheel ships a single top-level module, slice_cli.py, and depends on typer and httpx
3
+ # only. None of the server stack (fastapi, langgraph, redis, torch, ...) is imported or
4
+ # required. Inside the repo, `python -m app.cli` still works via the app/cli.py shim.
5
+ [build-system]
6
+ requires = ["setuptools>=77"]
7
+ build-backend = "setuptools.build_meta"
8
+
9
+ [project]
10
+ name = "slice-gateway"
11
+ version = "0.1.0"
12
+ description = "Command-line client for slice, the self-hosted LLM gateway."
13
+ readme = "PYPI.md"
14
+ requires-python = ">=3.11"
15
+ license = "MIT"
16
+ authors = [{ name = "Jishnu Janardanan" }]
17
+ dependencies = [
18
+ "typer",
19
+ "httpx",
20
+ ]
21
+ classifiers = [
22
+ "Environment :: Console",
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3",
25
+ "Topic :: Utilities",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://sliceapp.dev"
30
+ Repository = "https://github.com/jjk30/slice"
31
+
32
+ [project.scripts]
33
+ # `slice login`, `slice init`, `slice use <tool>`. See slice_cli.py.
34
+ slice = "slice_cli:main"
35
+
36
+ [tool.setuptools]
37
+ # Ship only the CLI module. Setting py-modules disables package auto-discovery, so the
38
+ # app/ server package is never pulled into the wheel.
39
+ py-modules = ["slice_cli"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,242 @@
1
+ """The ``slice`` CLI (phase 12): log in from the terminal, then point tools at slice.
2
+
3
+ Installed as a console entry point via pyproject.toml, so ``pip install -e .`` gives the
4
+ ``slice`` command. Three subcommands:
5
+
6
+ - ``slice login``, runs the GitHub device flow against the gateway's ``/auth/device/*``
7
+ endpoints (the gateway holds the OAuth client id and talks to GitHub; the CLI never
8
+ does). It shows the user code and URL, opens the browser, polls on the interval, and on
9
+ success saves the slice key and JWT to ``~/.slice/config.json`` (chmod 600).
10
+ - ``slice init``, confirms the saved config, records the default base URL, and prints
11
+ the current account status (a ``/auth/me`` call with the saved key).
12
+ - ``slice use <tool>``, prints the env lines that point a tool at slice. For tools where
13
+ the caller controls headers (SDK, curl) that is the whole story; for ``claude-code`` it
14
+ prints the three variables (base URL, your Anthropic key in ANTHROPIC_API_KEY, your slice
15
+ key in ANTHROPIC_AUTH_TOKEN) that run it end to end through slice.
16
+
17
+ The CLI talks only to the gateway over HTTP; it holds no secrets of its own. The saved
18
+ slice key is a bearer credential, so the config file is written 0600.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import stat
26
+ import time
27
+ import webbrowser
28
+ from pathlib import Path
29
+
30
+ import httpx
31
+ import typer
32
+
33
+ app = typer.Typer(add_completion=False, help="slice. Log in and point your tools at the gateway.")
34
+
35
+ CONFIG_DIR = Path.home() / ".slice"
36
+ CONFIG_PATH = CONFIG_DIR / "config.json"
37
+ DEFAULT_BASE_URL = "http://localhost:8080"
38
+
39
+ # How the device flow gives up if the user never authorizes: the gateway returns the
40
+ # expiry, but cap the CLI's own patience too so a wedged terminal doesn't spin forever.
41
+ POLL_CEILING_SECONDS = 900
42
+
43
+
44
+ # --- config file ------------------------------------------------------------
45
+
46
+
47
+ def base_url() -> str:
48
+ """The gateway base URL: the saved one, else ``SLICE_BASE_URL``, else the local default."""
49
+ saved = load_config().get("base_url")
50
+ return (saved or os.getenv("SLICE_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
51
+
52
+
53
+ def load_config() -> dict:
54
+ try:
55
+ return json.loads(CONFIG_PATH.read_text())
56
+ except (FileNotFoundError, ValueError):
57
+ return {}
58
+
59
+
60
+ def save_config(data: dict) -> None:
61
+ """Write ``~/.slice/config.json`` as 0600 (it holds a bearer credential)."""
62
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
63
+ CONFIG_PATH.write_text(json.dumps(data, indent=2) + "\n")
64
+ os.chmod(CONFIG_PATH, stat.S_IRUSR | stat.S_IWUSR) # 0600
65
+
66
+
67
+ # --- commands ---------------------------------------------------------------
68
+
69
+
70
+ @app.command()
71
+ def login(
72
+ base: str = typer.Option(None, "--base-url", help="Gateway base URL (default: saved / SLICE_BASE_URL / localhost)."),
73
+ open_browser: bool = typer.Option(True, "--open/--no-open", help="Open the verification URL in a browser."),
74
+ ) -> None:
75
+ """Log in to slice via GitHub (device flow) and save the slice key."""
76
+ target = (base or base_url()).rstrip("/")
77
+ existing = load_config()
78
+
79
+ with httpx.Client(timeout=30.0) as http:
80
+ try:
81
+ started = http.post(f"{target}/auth/device/start")
82
+ except httpx.HTTPError as exc:
83
+ _fail(f"Could not reach the gateway at {target}: {exc}")
84
+ if started.status_code != 200:
85
+ _fail(f"Login could not start: {_message(started)}")
86
+ data = started.json()
87
+ session_id = data["session_id"]
88
+ user_code = data["user_code"]
89
+ verification_uri = data["verification_uri"]
90
+ interval = int(data.get("interval") or 5)
91
+
92
+ typer.echo("")
93
+ typer.secho(" To finish signing in, open:", bold=True)
94
+ typer.secho(f" {verification_uri}", fg=typer.colors.CYAN)
95
+ typer.secho(" and enter this code:", bold=True)
96
+ typer.secho(f" {user_code}", fg=typer.colors.GREEN, bold=True)
97
+ typer.echo("")
98
+ if open_browser:
99
+ try:
100
+ webbrowser.open(verification_uri)
101
+ except Exception: # noqa: BLE001, a headless box just shows the URL above.
102
+ pass
103
+
104
+ typer.echo("Waiting for you to authorize in the browser…")
105
+ deadline = time.monotonic() + POLL_CEILING_SECONDS
106
+ while time.monotonic() < deadline:
107
+ time.sleep(max(1, interval))
108
+ try:
109
+ polled = http.post(f"{target}/auth/device/poll", json={"session_id": session_id})
110
+ except httpx.HTTPError as exc:
111
+ _fail(f"Lost contact with the gateway: {exc}")
112
+ if polled.status_code != 200:
113
+ _fail(f"Login failed: {_message(polled)}")
114
+ body = polled.json()
115
+ status = body.get("status")
116
+ if status == "authorized":
117
+ _save_login(target, existing, body)
118
+ account = body.get("account") or {}
119
+ typer.secho(f"Logged in as {account.get('login')}", fg=typer.colors.GREEN, bold=True)
120
+ return
121
+ if status == "slow_down":
122
+ interval = int(body.get("interval") or interval + 5)
123
+ continue
124
+ if status == "pending":
125
+ interval = int(body.get("interval") or interval)
126
+ continue
127
+ if status == "expired":
128
+ _fail("The login request expired before you authorized it. Run `slice login` again.")
129
+ if status == "denied":
130
+ _fail("Login was denied on the GitHub page.")
131
+ _fail(f"Unexpected login status: {status!r}")
132
+ _fail("Timed out waiting for authorization. Run `slice login` again.")
133
+
134
+
135
+ def _save_login(target: str, existing: dict, body: dict) -> None:
136
+ account = body.get("account") or {}
137
+ save_config(
138
+ {
139
+ **existing,
140
+ "base_url": target,
141
+ "slice_key": body.get("slice_key"),
142
+ "jwt": body.get("jwt"),
143
+ "login": account.get("login"),
144
+ "account_id": account.get("id"),
145
+ }
146
+ )
147
+
148
+
149
+ @app.command()
150
+ def init(
151
+ base: str = typer.Option(None, "--base-url", help="Set the default gateway base URL."),
152
+ ) -> None:
153
+ """Confirm the saved login, set the default base URL, and show account status."""
154
+ config = load_config()
155
+ if base:
156
+ config["base_url"] = base.rstrip("/")
157
+ save_config(config)
158
+ target = (config.get("base_url") or base_url()).rstrip("/")
159
+ key = config.get("slice_key")
160
+
161
+ typer.echo(f"Gateway: {target}")
162
+ typer.echo(f"Config: {CONFIG_PATH}")
163
+ if not key:
164
+ typer.secho("Not logged in. Run `slice login` first.", fg=typer.colors.YELLOW)
165
+ raise typer.Exit(code=1)
166
+
167
+ with httpx.Client(timeout=15.0) as http:
168
+ try:
169
+ me = http.get(f"{target}/auth/me", headers={"Authorization": f"Bearer {key}"})
170
+ except httpx.HTTPError as exc:
171
+ _fail(f"Could not reach the gateway at {target}: {exc}")
172
+ if me.status_code != 200:
173
+ typer.secho(f"Saved key was rejected: {_message(me)}", fg=typer.colors.RED)
174
+ typer.echo("Run `slice login` to get a fresh key.")
175
+ raise typer.Exit(code=1)
176
+ account = me.json().get("account") or {}
177
+ typer.secho(f"Logged in as {account.get('login')} (account {account.get('id')}).", fg=typer.colors.GREEN)
178
+
179
+
180
+ @app.command()
181
+ def use(tool: str = typer.Argument(..., help="Which tool: anthropic | openai | curl | claude-code")) -> None:
182
+ """Print the environment lines that point a tool at slice."""
183
+ config = load_config()
184
+ target = base_url()
185
+ key = config.get("slice_key")
186
+ if not key:
187
+ typer.secho("Not logged in. Run `slice login` first.", fg=typer.colors.YELLOW)
188
+ raise typer.Exit(code=1)
189
+
190
+ name = tool.strip().lower()
191
+ if name in {"anthropic", "sdk", "python"}:
192
+ typer.echo(f"export ANTHROPIC_BASE_URL={target}")
193
+ typer.echo(f"export SLICE_KEY={key}")
194
+ typer.echo("# Send the slice key as the Authorization header, your provider key as x-api-key:")
195
+ typer.echo('# Authorization: Bearer $SLICE_KEY')
196
+ typer.echo('# x-api-key: <your Anthropic key>')
197
+ elif name in {"openai", "codex"}:
198
+ typer.echo(f"export OPENAI_BASE_URL={target}/v1")
199
+ typer.echo(f"export SLICE_KEY={key}")
200
+ typer.echo("# Authorization: Bearer $SLICE_KEY, and your provider key in x-api-key.")
201
+ elif name in {"curl", "http"}:
202
+ typer.echo(f'curl {target}/v1/messages \\')
203
+ typer.echo(f' -H "Authorization: Bearer {key}" \\')
204
+ typer.echo(' -H "x-api-key: $ANTHROPIC_API_KEY" \\')
205
+ typer.echo(' -H "content-type: application/json" \\')
206
+ typer.echo(' -d \'{"model":"claude-sonnet-5","max_tokens":64,"messages":[{"role":"user","content":"hi"}]}\'')
207
+ elif name in {"claude-code", "claudecode", "cc"}:
208
+ typer.echo(f"export ANTHROPIC_BASE_URL={target}")
209
+ typer.echo("export ANTHROPIC_API_KEY=sk-ant-api... # your own Anthropic key")
210
+ typer.echo(f"export ANTHROPIC_AUTH_TOKEN={key} # your slice key, from slice login")
211
+ typer.echo("# ANTHROPIC_AUTH_TOKEN goes out as Authorization: Bearer, where slice reads its key.")
212
+ typer.echo("# ANTHROPIC_API_KEY stays your own Anthropic key in x-api-key; slice forwards it.")
213
+ typer.echo("# Claude Code notes that env auth takes precedence over your claude.ai login while")
214
+ typer.echo("# these are set, which is expected; unset the three variables to go back to normal.")
215
+ else:
216
+ _fail(f"Unknown tool {tool!r}. Try: anthropic | openai | curl | claude-code")
217
+
218
+
219
+ # --- helpers ----------------------------------------------------------------
220
+
221
+
222
+ def _message(response: httpx.Response) -> str:
223
+ try:
224
+ body = response.json()
225
+ except ValueError:
226
+ return f"HTTP {response.status_code}"
227
+ if isinstance(body, dict) and isinstance(body.get("error"), dict):
228
+ return body["error"].get("message") or f"HTTP {response.status_code}"
229
+ return f"HTTP {response.status_code}"
230
+
231
+
232
+ def _fail(message: str) -> None:
233
+ typer.secho(message, fg=typer.colors.RED, err=True)
234
+ raise typer.Exit(code=1)
235
+
236
+
237
+ def main() -> None:
238
+ app()
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: slice-gateway
3
+ Version: 0.1.0
4
+ Summary: Command-line client for slice, the self-hosted LLM gateway.
5
+ Author: Jishnu Janardanan
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://sliceapp.dev
8
+ Project-URL: Repository, https://github.com/jjk30/slice
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: typer
17
+ Requires-Dist: httpx
18
+ Dynamic: license-file
19
+
20
+ # slice-gateway
21
+
22
+ Command-line client for slice, a self-hosted LLM gateway that routes, caches, and caps your AI spend.
23
+
24
+ This package installs the `slice` command. It logs you in through GitHub, then prints the environment lines that point your existing tools at a slice gateway. It has no server dependencies. It needs only typer and httpx, plus the standard library.
25
+
26
+ ## Quickstart
27
+
28
+ 1. Install the CLI:
29
+
30
+ pip install slice-gateway
31
+
32
+ 2. Log in through GitHub:
33
+
34
+ slice login
35
+
36
+ 3. Point a tool at slice. For Claude Code, set three variables:
37
+
38
+ export ANTHROPIC_BASE_URL=https://api.sliceapp.dev
39
+ export ANTHROPIC_API_KEY=sk-ant-api... # your own Anthropic key
40
+ export ANTHROPIC_AUTH_TOKEN=slk_live_... # your slice key, from slice login
41
+
42
+ `ANTHROPIC_AUTH_TOKEN` goes out as `Authorization: Bearer`, which is where slice reads its key. `ANTHROPIC_API_KEY` stays your own Anthropic key in `x-api-key`, and slice forwards it upstream.
43
+
44
+ Run `slice use anthropic`, `slice use openai`, `slice use curl`, or `slice use claude-code` to print the exact lines for your tool.
45
+
46
+ ## The gateway
47
+
48
+ This is only the client. The slice gateway itself, the server that does the routing, caching, budgets, and dashboards, lives on GitHub at https://github.com/jjk30/slice.
49
+
50
+ Homepage: https://sliceapp.dev
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ PYPI.md
4
+ pyproject.toml
5
+ slice_cli.py
6
+ slice_gateway.egg-info/PKG-INFO
7
+ slice_gateway.egg-info/SOURCES.txt
8
+ slice_gateway.egg-info/dependency_links.txt
9
+ slice_gateway.egg-info/entry_points.txt
10
+ slice_gateway.egg-info/requires.txt
11
+ slice_gateway.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ slice = slice_cli:main
@@ -0,0 +1,2 @@
1
+ typer
2
+ httpx
@@ -0,0 +1 @@
1
+ slice_cli