llmstack-cli 0.1.0__py3-none-any.whl → 0.2.0__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.
- llmstack/__init__.py +1 -1
- llmstack/cli/app.py +18 -0
- llmstack/cli/commands/chat.py +104 -0
- llmstack/cli/commands/export.py +190 -0
- llmstack/core/stack.py +12 -1
- llmstack/docker/manager.py +23 -7
- llmstack/gateway/Dockerfile +3 -1
- llmstack/services/gateway/service.py +16 -1
- llmstack/services/observe/prometheus.py +76 -23
- {llmstack_cli-0.1.0.dist-info → llmstack_cli-0.2.0.dist-info}/METADATA +102 -44
- {llmstack_cli-0.1.0.dist-info → llmstack_cli-0.2.0.dist-info}/RECORD +14 -12
- {llmstack_cli-0.1.0.dist-info → llmstack_cli-0.2.0.dist-info}/WHEEL +0 -0
- {llmstack_cli-0.1.0.dist-info → llmstack_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {llmstack_cli-0.1.0.dist-info → llmstack_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
llmstack/__init__.py
CHANGED
llmstack/cli/app.py
CHANGED
|
@@ -80,6 +80,24 @@ def logs(
|
|
|
80
80
|
_logs(service=service, follow=follow, tail=tail)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
@app.command()
|
|
84
|
+
def chat(
|
|
85
|
+
model: str = typer.Option(None, "--model", "-m", help="Model name to chat with"),
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Interactive chat with your local LLM."""
|
|
88
|
+
from llmstack.cli.commands.chat import chat as _chat
|
|
89
|
+
_chat(model=model)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.command()
|
|
93
|
+
def export(
|
|
94
|
+
output: str = typer.Option("docker-compose.yml", "--output", "-o", help="Output file path"),
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Export llmstack.yaml as a standalone docker-compose.yml."""
|
|
97
|
+
from llmstack.cli.commands.export import export as _export
|
|
98
|
+
_export(output=output)
|
|
99
|
+
|
|
100
|
+
|
|
83
101
|
@app.command()
|
|
84
102
|
def doctor() -> None:
|
|
85
103
|
"""Check system requirements and diagnose issues."""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""llmstack chat — interactive terminal chat with your local LLM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from llmstack.cli.console import console
|
|
11
|
+
from llmstack.config.loader import load_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def chat(model: str | None = None) -> None:
|
|
15
|
+
"""Start an interactive chat session with the running LLM."""
|
|
16
|
+
config = load_config()
|
|
17
|
+
base_url = f"http://localhost:{config.gateway.port}"
|
|
18
|
+
model_name = model or config.models.chat.name
|
|
19
|
+
|
|
20
|
+
# Get API key
|
|
21
|
+
api_key = config.gateway.api_keys[0] if config.gateway.api_keys else ""
|
|
22
|
+
headers = {"Content-Type": "application/json"}
|
|
23
|
+
if api_key:
|
|
24
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
25
|
+
|
|
26
|
+
# Verify gateway is reachable
|
|
27
|
+
try:
|
|
28
|
+
resp = httpx.get(f"{base_url}/healthz", headers=headers, timeout=5)
|
|
29
|
+
if resp.status_code != 200:
|
|
30
|
+
console.print("[error]Gateway is not healthy. Run 'llmstack up' first.[/]")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
except httpx.ConnectError:
|
|
33
|
+
console.print("[error]Cannot connect to gateway. Run 'llmstack up' first.[/]")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
console.print(f"\n[bold]LLMStack Chat[/] — model: [cyan]{model_name}[/]")
|
|
37
|
+
console.print("[dim]Type 'exit' or Ctrl+C to quit. '/clear' to reset conversation.[/]\n")
|
|
38
|
+
|
|
39
|
+
messages: list[dict[str, str]] = []
|
|
40
|
+
|
|
41
|
+
while True:
|
|
42
|
+
try:
|
|
43
|
+
user_input = console.input("[bold green]You:[/] ")
|
|
44
|
+
except (KeyboardInterrupt, EOFError):
|
|
45
|
+
console.print("\n[dim]Goodbye![/]")
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
text = user_input.strip()
|
|
49
|
+
if not text:
|
|
50
|
+
continue
|
|
51
|
+
if text.lower() in ("exit", "quit"):
|
|
52
|
+
console.print("[dim]Goodbye![/]")
|
|
53
|
+
break
|
|
54
|
+
if text == "/clear":
|
|
55
|
+
messages.clear()
|
|
56
|
+
console.print("[dim]Conversation cleared.[/]\n")
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
messages.append({"role": "user", "content": text})
|
|
60
|
+
|
|
61
|
+
# Stream response
|
|
62
|
+
console.print("[bold cyan]Assistant:[/] ", end="")
|
|
63
|
+
assistant_text = ""
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with httpx.stream(
|
|
67
|
+
"POST",
|
|
68
|
+
f"{base_url}/v1/chat/completions",
|
|
69
|
+
headers=headers,
|
|
70
|
+
json={"model": model_name, "messages": messages, "stream": True},
|
|
71
|
+
timeout=httpx.Timeout(300, connect=10),
|
|
72
|
+
) as resp:
|
|
73
|
+
if resp.status_code != 200:
|
|
74
|
+
console.print(f"[error]Error {resp.status_code}[/]")
|
|
75
|
+
messages.pop()
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
for line in resp.iter_lines():
|
|
79
|
+
if not line.startswith("data: "):
|
|
80
|
+
continue
|
|
81
|
+
data = line[6:]
|
|
82
|
+
if data.strip() == "[DONE]":
|
|
83
|
+
break
|
|
84
|
+
try:
|
|
85
|
+
chunk = json.loads(data)
|
|
86
|
+
delta = chunk["choices"][0].get("delta", {})
|
|
87
|
+
token = delta.get("content", "")
|
|
88
|
+
if token:
|
|
89
|
+
print(token, end="", flush=True)
|
|
90
|
+
assistant_text += token
|
|
91
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
except httpx.ConnectError:
|
|
95
|
+
console.print("[error]Connection lost.[/]")
|
|
96
|
+
messages.pop()
|
|
97
|
+
continue
|
|
98
|
+
except KeyboardInterrupt:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
print() # newline after streamed response
|
|
102
|
+
if assistant_text:
|
|
103
|
+
messages.append({"role": "assistant", "content": assistant_text})
|
|
104
|
+
print()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""llmstack export — generate a standalone docker-compose.yml from llmstack.yaml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from llmstack.cli.console import console
|
|
8
|
+
from llmstack.config.loader import load_config
|
|
9
|
+
from llmstack.config.schema import StackConfig
|
|
10
|
+
from llmstack.core.hardware import detect_hardware
|
|
11
|
+
from llmstack.core.resolver import resolve_inference_backend, resolve_embedding_backend
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _build_compose(config: StackConfig) -> dict:
|
|
15
|
+
"""Convert StackConfig into a docker-compose.yml dict."""
|
|
16
|
+
hw = detect_hardware()
|
|
17
|
+
inference_backend = resolve_inference_backend(config.models.chat, hw)
|
|
18
|
+
embed_backend = resolve_embedding_backend(config.models.embeddings, hw)
|
|
19
|
+
|
|
20
|
+
services: dict = {}
|
|
21
|
+
volumes: dict = {}
|
|
22
|
+
network_name = config.docker.network
|
|
23
|
+
|
|
24
|
+
# 1. Qdrant
|
|
25
|
+
services["qdrant"] = {
|
|
26
|
+
"image": "qdrant/qdrant:latest",
|
|
27
|
+
"container_name": "llmstack-qdrant",
|
|
28
|
+
"ports": [f"{config.services.vectors.port}:6333", f"{config.services.vectors.port + 1}:6334"],
|
|
29
|
+
"volumes": ["qdrant_data:/qdrant/storage"],
|
|
30
|
+
"restart": "unless-stopped",
|
|
31
|
+
"networks": [network_name],
|
|
32
|
+
}
|
|
33
|
+
volumes["qdrant_data"] = None
|
|
34
|
+
|
|
35
|
+
# 2. Redis
|
|
36
|
+
services["redis"] = {
|
|
37
|
+
"image": "redis:7-alpine",
|
|
38
|
+
"container_name": "llmstack-redis",
|
|
39
|
+
"ports": [f"{config.services.cache.port}:6379"],
|
|
40
|
+
"command": f"redis-server --maxmemory {config.services.cache.max_memory} --maxmemory-policy allkeys-lru",
|
|
41
|
+
"restart": "unless-stopped",
|
|
42
|
+
"networks": [network_name],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# 3. Inference
|
|
46
|
+
if inference_backend == "vllm":
|
|
47
|
+
model = config.models.chat
|
|
48
|
+
cmd = f"--model {model.name} --host 0.0.0.0 --port 8000 --max-model-len {model.context_length}"
|
|
49
|
+
if model.quantization:
|
|
50
|
+
cmd += f" --quantization {model.quantization}"
|
|
51
|
+
services["vllm"] = {
|
|
52
|
+
"image": "vllm/vllm-openai:latest",
|
|
53
|
+
"container_name": "llmstack-vllm",
|
|
54
|
+
"ports": ["8001:8000"],
|
|
55
|
+
"command": cmd,
|
|
56
|
+
"volumes": ["vllm_cache:/root/.cache/huggingface"],
|
|
57
|
+
"shm_size": "4g",
|
|
58
|
+
"deploy": {"resources": {"reservations": {"devices": [{"driver": "nvidia", "count": "all", "capabilities": ["gpu"]}]}}},
|
|
59
|
+
"restart": "unless-stopped",
|
|
60
|
+
"networks": [network_name],
|
|
61
|
+
}
|
|
62
|
+
volumes["vllm_cache"] = None
|
|
63
|
+
inference_url = "http://vllm:8000/v1"
|
|
64
|
+
else:
|
|
65
|
+
svc: dict = {
|
|
66
|
+
"image": "ollama/ollama:latest",
|
|
67
|
+
"container_name": "llmstack-ollama",
|
|
68
|
+
"ports": ["11434:11434"],
|
|
69
|
+
"volumes": ["ollama_data:/root/.ollama"],
|
|
70
|
+
"restart": "unless-stopped",
|
|
71
|
+
"networks": [network_name],
|
|
72
|
+
}
|
|
73
|
+
if hw.gpu_vendor == "nvidia":
|
|
74
|
+
svc["deploy"] = {"resources": {"reservations": {"devices": [{"driver": "nvidia", "count": "all", "capabilities": ["gpu"]}]}}}
|
|
75
|
+
services["ollama"] = svc
|
|
76
|
+
volumes["ollama_data"] = None
|
|
77
|
+
inference_url = "http://ollama:11434/v1"
|
|
78
|
+
|
|
79
|
+
# 4. Embeddings
|
|
80
|
+
embeddings_url = inference_url # fallback: use Ollama for embeddings
|
|
81
|
+
if embed_backend == "tei":
|
|
82
|
+
tei_image = "ghcr.io/huggingface/text-embeddings-inference:cpu-latest"
|
|
83
|
+
tei_svc: dict = {
|
|
84
|
+
"image": tei_image,
|
|
85
|
+
"container_name": "llmstack-tei",
|
|
86
|
+
"ports": ["8002:80"],
|
|
87
|
+
"command": f"--model-id {config.models.embeddings.name} --port 80",
|
|
88
|
+
"volumes": ["tei_cache:/data"],
|
|
89
|
+
"restart": "unless-stopped",
|
|
90
|
+
"networks": [network_name],
|
|
91
|
+
}
|
|
92
|
+
if hw.gpu_vendor == "nvidia":
|
|
93
|
+
tei_svc["image"] = "ghcr.io/huggingface/text-embeddings-inference:latest"
|
|
94
|
+
tei_svc["deploy"] = {"resources": {"reservations": {"devices": [{"driver": "nvidia", "count": "all", "capabilities": ["gpu"]}]}}}
|
|
95
|
+
services["tei"] = tei_svc
|
|
96
|
+
volumes["tei_cache"] = None
|
|
97
|
+
embeddings_url = "http://tei:80/v1"
|
|
98
|
+
|
|
99
|
+
# 5. Gateway
|
|
100
|
+
qdrant_url = f"http://qdrant:{config.services.vectors.port}"
|
|
101
|
+
redis_url = f"redis://redis:{config.services.cache.port}"
|
|
102
|
+
api_keys = ",".join(config.gateway.api_keys) if config.gateway.api_keys else ""
|
|
103
|
+
|
|
104
|
+
services["gateway"] = {
|
|
105
|
+
"build": {
|
|
106
|
+
"context": ".",
|
|
107
|
+
"dockerfile": "src/llmstack/gateway/Dockerfile",
|
|
108
|
+
},
|
|
109
|
+
"container_name": "llmstack-gateway",
|
|
110
|
+
"ports": [f"{config.gateway.port}:8000"],
|
|
111
|
+
"environment": {
|
|
112
|
+
"LLMSTACK_INFERENCE_URL": inference_url,
|
|
113
|
+
"LLMSTACK_EMBEDDINGS_URL": embeddings_url,
|
|
114
|
+
"LLMSTACK_QDRANT_URL": qdrant_url,
|
|
115
|
+
"LLMSTACK_REDIS_URL": redis_url,
|
|
116
|
+
"LLMSTACK_API_KEYS": api_keys,
|
|
117
|
+
"LLMSTACK_CORS_ORIGINS": ",".join(config.gateway.cors),
|
|
118
|
+
"LLMSTACK_REQUEST_TIMEOUT": str(config.gateway.request_timeout),
|
|
119
|
+
"LLMSTACK_RATE_LIMIT": config.gateway.rate_limit,
|
|
120
|
+
},
|
|
121
|
+
"depends_on": ["qdrant", "redis"],
|
|
122
|
+
"restart": "unless-stopped",
|
|
123
|
+
"networks": [network_name],
|
|
124
|
+
}
|
|
125
|
+
# Add inference dependency
|
|
126
|
+
if inference_backend == "vllm":
|
|
127
|
+
services["gateway"]["depends_on"].append("vllm")
|
|
128
|
+
else:
|
|
129
|
+
services["gateway"]["depends_on"].append("ollama")
|
|
130
|
+
if embed_backend == "tei":
|
|
131
|
+
services["gateway"]["depends_on"].append("tei")
|
|
132
|
+
|
|
133
|
+
# 6. Observability
|
|
134
|
+
if config.observe.metrics:
|
|
135
|
+
services["prometheus"] = {
|
|
136
|
+
"image": "prom/prometheus:latest",
|
|
137
|
+
"container_name": "llmstack-prometheus",
|
|
138
|
+
"ports": ["9090:9090"],
|
|
139
|
+
"command": [
|
|
140
|
+
"--config.file=/etc/prometheus/prometheus.yml",
|
|
141
|
+
f"--storage.tsdb.retention.time={config.observe.retention}",
|
|
142
|
+
"--web.enable-lifecycle",
|
|
143
|
+
],
|
|
144
|
+
"volumes": [
|
|
145
|
+
"./prometheus.yml:/etc/prometheus/prometheus.yml:ro",
|
|
146
|
+
"prometheus_data:/prometheus",
|
|
147
|
+
],
|
|
148
|
+
"restart": "unless-stopped",
|
|
149
|
+
"networks": [network_name],
|
|
150
|
+
}
|
|
151
|
+
volumes["prometheus_data"] = None
|
|
152
|
+
|
|
153
|
+
services["grafana"] = {
|
|
154
|
+
"image": "grafana/grafana:latest",
|
|
155
|
+
"container_name": "llmstack-grafana",
|
|
156
|
+
"ports": [f"{config.observe.dashboard_port}:3000"],
|
|
157
|
+
"environment": {
|
|
158
|
+
"GF_SECURITY_ADMIN_USER": "admin",
|
|
159
|
+
"GF_SECURITY_ADMIN_PASSWORD": "llmstack",
|
|
160
|
+
"GF_AUTH_ANONYMOUS_ENABLED": "true",
|
|
161
|
+
"GF_AUTH_ANONYMOUS_ORG_ROLE": "Viewer",
|
|
162
|
+
},
|
|
163
|
+
"restart": "unless-stopped",
|
|
164
|
+
"networks": [network_name],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
compose = {
|
|
168
|
+
"version": "3.8",
|
|
169
|
+
"services": services,
|
|
170
|
+
"volumes": volumes,
|
|
171
|
+
"networks": {network_name: {"driver": "bridge"}},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return compose
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def export(output: str = "docker-compose.yml") -> None:
|
|
178
|
+
"""Generate a docker-compose.yml from the current llmstack.yaml."""
|
|
179
|
+
config = load_config()
|
|
180
|
+
compose = _build_compose(config)
|
|
181
|
+
|
|
182
|
+
# Clean up None volume values for yaml output
|
|
183
|
+
compose["volumes"] = {k: {} for k in compose["volumes"]}
|
|
184
|
+
|
|
185
|
+
with open(output, "w") as f:
|
|
186
|
+
yaml.dump(compose, f, default_flow_style=False, sort_keys=False)
|
|
187
|
+
|
|
188
|
+
service_count = len(compose["services"])
|
|
189
|
+
console.print(f"\n[success]Exported {service_count} services to {output}[/]")
|
|
190
|
+
console.print(f"Run with: [bold]docker compose -f {output} up -d[/]\n")
|
llmstack/core/stack.py
CHANGED
|
@@ -7,6 +7,7 @@ import secrets
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
from rich.table import Table
|
|
9
9
|
|
|
10
|
+
from llmstack.config.loader import save_config
|
|
10
11
|
from llmstack.config.schema import StackConfig
|
|
11
12
|
from llmstack.core.hardware import detect_hardware
|
|
12
13
|
from llmstack.core.health import wait_healthy
|
|
@@ -100,11 +101,21 @@ class Stack:
|
|
|
100
101
|
self._services = self._build_services()
|
|
101
102
|
self.docker.ensure_network()
|
|
102
103
|
|
|
104
|
+
# Build local images if needed
|
|
105
|
+
for svc in self._services:
|
|
106
|
+
if hasattr(svc, "build_info") and svc.build_info():
|
|
107
|
+
info = svc.build_info()
|
|
108
|
+
console.print(f" [cyan]Building {svc.name} image...[/]", end="")
|
|
109
|
+
self.docker.build_image(**info)
|
|
110
|
+
console.print(" [green]done[/]")
|
|
111
|
+
|
|
103
112
|
# Generate API key if needed
|
|
104
113
|
if self.config.gateway.auth == "api_key" and not self.config.gateway.api_keys:
|
|
105
114
|
key = f"sk-llmstack-{secrets.token_urlsafe(24)}"
|
|
106
115
|
self.config.gateway.api_keys = [key]
|
|
107
|
-
|
|
116
|
+
save_config(self.config)
|
|
117
|
+
console.print(f"\n[bold green]Generated API key:[/] {key}")
|
|
118
|
+
console.print("[dim]Saved to llmstack.yaml[/]\n")
|
|
108
119
|
|
|
109
120
|
for svc in self._services:
|
|
110
121
|
console.print(f" [cyan]Starting {svc.name}...[/]", end="")
|
llmstack/docker/manager.py
CHANGED
|
@@ -35,6 +35,11 @@ class DockerManager:
|
|
|
35
35
|
except NotFound:
|
|
36
36
|
self.client.networks.create(self.network_name, driver="bridge")
|
|
37
37
|
|
|
38
|
+
def build_image(self, path: str, dockerfile: str, tag: str) -> str:
|
|
39
|
+
"""Build a Docker image from a local Dockerfile. Returns the image tag."""
|
|
40
|
+
self.client.images.build(path=path, dockerfile=dockerfile, tag=tag, rm=True)
|
|
41
|
+
return tag
|
|
42
|
+
|
|
38
43
|
def run_service(self, service: ServiceBase) -> Container:
|
|
39
44
|
"""Start a container for a service. Removes any existing container with the same name."""
|
|
40
45
|
spec = service.container_spec()
|
|
@@ -53,13 +58,24 @@ class DockerManager:
|
|
|
53
58
|
self.LABEL_SERVICE: service.name,
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
try:
|
|
62
|
+
container = self.client.containers.run(
|
|
63
|
+
detach=True,
|
|
64
|
+
name=name,
|
|
65
|
+
network=self.network_name,
|
|
66
|
+
labels=labels,
|
|
67
|
+
**spec,
|
|
68
|
+
)
|
|
69
|
+
except APIError as exc:
|
|
70
|
+
msg = str(exc)
|
|
71
|
+
if "port is already allocated" in msg or "address already in use" in msg:
|
|
72
|
+
ports = spec.get("ports", {})
|
|
73
|
+
port = next(iter(ports.values()), "unknown") if ports else "unknown"
|
|
74
|
+
raise SystemExit(
|
|
75
|
+
f"Port {port} is already in use. "
|
|
76
|
+
f"Stop the conflicting service or change the port in llmstack.yaml."
|
|
77
|
+
) from exc
|
|
78
|
+
raise
|
|
63
79
|
return container
|
|
64
80
|
|
|
65
81
|
def stop_service(self, service_name: str) -> None:
|
llmstack/gateway/Dockerfile
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from llmstack.config.schema import GatewayConfig
|
|
8
9
|
from llmstack.services.base import ServiceBase
|
|
9
10
|
|
|
11
|
+
IMAGE_TAG = "llmstack-gateway:local"
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
class GatewayService(ServiceBase):
|
|
12
15
|
name = "gateway"
|
|
@@ -26,9 +29,21 @@ class GatewayService(ServiceBase):
|
|
|
26
29
|
self.qdrant_url = qdrant_url
|
|
27
30
|
self.redis_url = redis_url
|
|
28
31
|
|
|
32
|
+
def build_info(self) -> dict[str, str] | None:
|
|
33
|
+
"""Return build context for the gateway Docker image."""
|
|
34
|
+
import llmstack
|
|
35
|
+
pkg_dir = Path(llmstack.__file__).resolve().parent
|
|
36
|
+
project_root = pkg_dir.parent.parent # src/llmstack -> src -> project root
|
|
37
|
+
dockerfile = str(pkg_dir / "gateway" / "Dockerfile")
|
|
38
|
+
return {
|
|
39
|
+
"path": str(project_root),
|
|
40
|
+
"dockerfile": dockerfile,
|
|
41
|
+
"tag": IMAGE_TAG,
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
def container_spec(self) -> dict[str, Any]:
|
|
30
45
|
return {
|
|
31
|
-
"image":
|
|
46
|
+
"image": IMAGE_TAG,
|
|
32
47
|
"name": "llmstack-gateway",
|
|
33
48
|
"ports": {"8000/tcp": self.config.port},
|
|
34
49
|
"environment": {
|
|
@@ -3,31 +3,38 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
6
8
|
from typing import Any
|
|
7
9
|
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
8
12
|
from llmstack.config.schema import ObserveConfig
|
|
9
13
|
from llmstack.services.base import ServiceBase
|
|
10
14
|
|
|
15
|
+
CONFIG_DIR = Path.home() / ".llmstack" / "config"
|
|
11
16
|
|
|
12
17
|
# Prometheus config that scrapes the gateway /metrics endpoint
|
|
13
|
-
PROMETHEUS_CONFIG =
|
|
14
|
-
global:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
scrape_configs:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
PROMETHEUS_CONFIG = {
|
|
19
|
+
"global": {
|
|
20
|
+
"scrape_interval": "15s",
|
|
21
|
+
"evaluation_interval": "15s",
|
|
22
|
+
},
|
|
23
|
+
"scrape_configs": [
|
|
24
|
+
{
|
|
25
|
+
"job_name": "llmstack-gateway",
|
|
26
|
+
"metrics_path": "/metrics",
|
|
27
|
+
"static_configs": [{"targets": ["llmstack-gateway:8000"]}],
|
|
28
|
+
"scrape_interval": "5s",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"job_name": "qdrant",
|
|
32
|
+
"metrics_path": "/metrics",
|
|
33
|
+
"static_configs": [{"targets": ["llmstack-qdrant:6333"]}],
|
|
34
|
+
"scrape_interval": "15s",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}
|
|
31
38
|
|
|
32
39
|
# Grafana datasource provisioning
|
|
33
40
|
GRAFANA_DATASOURCE = {
|
|
@@ -47,7 +54,7 @@ GRAFANA_DASHBOARD_PROVIDER = {
|
|
|
47
54
|
"providers": [{
|
|
48
55
|
"name": "LLMStack",
|
|
49
56
|
"type": "file",
|
|
50
|
-
"options": {"path": "/
|
|
57
|
+
"options": {"path": "/opt/grafana/dashboards"},
|
|
51
58
|
}],
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -97,6 +104,12 @@ GRAFANA_DASHBOARD = {
|
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
|
|
107
|
+
def _write_file(path: Path, content: str) -> None:
|
|
108
|
+
"""Write content to file, creating parent dirs as needed."""
|
|
109
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
path.write_text(content)
|
|
111
|
+
|
|
112
|
+
|
|
100
113
|
class PrometheusService(ServiceBase):
|
|
101
114
|
name = "prometheus"
|
|
102
115
|
category = "observe"
|
|
@@ -105,7 +118,14 @@ class PrometheusService(ServiceBase):
|
|
|
105
118
|
self.config = config
|
|
106
119
|
self.host_port = 9090
|
|
107
120
|
|
|
121
|
+
def _prepare_config(self) -> str:
|
|
122
|
+
"""Write prometheus.yml to ~/.llmstack/config/prometheus/ and return the dir path."""
|
|
123
|
+
config_dir = CONFIG_DIR / "prometheus"
|
|
124
|
+
_write_file(config_dir / "prometheus.yml", yaml.dump(PROMETHEUS_CONFIG, default_flow_style=False))
|
|
125
|
+
return str(config_dir)
|
|
126
|
+
|
|
108
127
|
def container_spec(self) -> dict[str, Any]:
|
|
128
|
+
config_dir = self._prepare_config()
|
|
109
129
|
return {
|
|
110
130
|
"image": "prom/prometheus:latest",
|
|
111
131
|
"name": "llmstack-prometheus",
|
|
@@ -116,7 +136,7 @@ class PrometheusService(ServiceBase):
|
|
|
116
136
|
"--web.enable-lifecycle",
|
|
117
137
|
],
|
|
118
138
|
"volumes": {
|
|
119
|
-
|
|
139
|
+
config_dir: {"bind": "/etc/prometheus", "mode": "ro"},
|
|
120
140
|
"llmstack_prometheus_data": {"bind": "/prometheus", "mode": "rw"},
|
|
121
141
|
},
|
|
122
142
|
"environment": {},
|
|
@@ -127,7 +147,7 @@ class PrometheusService(ServiceBase):
|
|
|
127
147
|
|
|
128
148
|
def get_config_yaml(self) -> str:
|
|
129
149
|
"""Return the prometheus.yml content."""
|
|
130
|
-
return PROMETHEUS_CONFIG
|
|
150
|
+
return yaml.dump(PROMETHEUS_CONFIG, default_flow_style=False)
|
|
131
151
|
|
|
132
152
|
|
|
133
153
|
class GrafanaService(ServiceBase):
|
|
@@ -138,7 +158,32 @@ class GrafanaService(ServiceBase):
|
|
|
138
158
|
self.config = config
|
|
139
159
|
self.host_port = config.dashboard_port
|
|
140
160
|
|
|
161
|
+
def _prepare_provisioning(self) -> str:
|
|
162
|
+
"""Write Grafana provisioning files to ~/.llmstack/config/grafana/ and return the dir."""
|
|
163
|
+
base = CONFIG_DIR / "grafana"
|
|
164
|
+
|
|
165
|
+
# Datasource provisioning
|
|
166
|
+
_write_file(
|
|
167
|
+
base / "provisioning" / "datasources" / "datasource.yml",
|
|
168
|
+
yaml.dump(GRAFANA_DATASOURCE, default_flow_style=False),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Dashboard provider provisioning
|
|
172
|
+
_write_file(
|
|
173
|
+
base / "provisioning" / "dashboards" / "provider.yml",
|
|
174
|
+
yaml.dump(GRAFANA_DASHBOARD_PROVIDER, default_flow_style=False),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Dashboard JSON
|
|
178
|
+
_write_file(
|
|
179
|
+
base / "dashboards" / "llmstack.json",
|
|
180
|
+
json.dumps(GRAFANA_DASHBOARD, indent=2),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return str(base)
|
|
184
|
+
|
|
141
185
|
def container_spec(self) -> dict[str, Any]:
|
|
186
|
+
base = self._prepare_provisioning()
|
|
142
187
|
return {
|
|
143
188
|
"image": "grafana/grafana:latest",
|
|
144
189
|
"name": "llmstack-grafana",
|
|
@@ -148,10 +193,18 @@ class GrafanaService(ServiceBase):
|
|
|
148
193
|
"GF_SECURITY_ADMIN_PASSWORD": "llmstack",
|
|
149
194
|
"GF_AUTH_ANONYMOUS_ENABLED": "true",
|
|
150
195
|
"GF_AUTH_ANONYMOUS_ORG_ROLE": "Viewer",
|
|
151
|
-
"GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH": "/
|
|
196
|
+
"GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH": "/opt/grafana/dashboards/llmstack.json",
|
|
152
197
|
},
|
|
153
198
|
"volumes": {
|
|
154
|
-
|
|
199
|
+
os.path.join(base, "provisioning", "datasources"): {
|
|
200
|
+
"bind": "/etc/grafana/provisioning/datasources", "mode": "ro",
|
|
201
|
+
},
|
|
202
|
+
os.path.join(base, "provisioning", "dashboards"): {
|
|
203
|
+
"bind": "/etc/grafana/provisioning/dashboards", "mode": "ro",
|
|
204
|
+
},
|
|
205
|
+
os.path.join(base, "dashboards"): {
|
|
206
|
+
"bind": "/opt/grafana/dashboards", "mode": "ro",
|
|
207
|
+
},
|
|
155
208
|
},
|
|
156
209
|
}
|
|
157
210
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: llmstack-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: One command. Full LLM stack. Zero config.
|
|
5
5
|
Author: mara-werils
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -44,21 +44,43 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
<a href="https://github.com/mara-werils/llmstack/actions/workflows/ci.yml"><img src="https://github.com/mara-werils/llmstack/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
45
45
|
<a href="https://github.com/mara-werils/llmstack/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License"></a>
|
|
46
46
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.11+-blue" alt="Python"></a>
|
|
47
|
+
<a href="https://github.com/mara-werils/llmstack/stargazers"><img src="https://img.shields.io/github/stars/mara-werils/llmstack?style=social" alt="Stars"></a>
|
|
47
48
|
</p>
|
|
48
49
|
|
|
49
50
|
---
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
<p align="center">
|
|
53
|
+
<img src="demo.gif" alt="llmstack demo" width="800">
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
52
57
|
|
|
53
58
|
```bash
|
|
54
59
|
pip install llmstack-cli
|
|
55
|
-
llmstack init
|
|
60
|
+
llmstack init --preset rag
|
|
56
61
|
llmstack up
|
|
57
62
|
```
|
|
58
63
|
|
|
59
|
-
That's it. You now have
|
|
64
|
+
That's it. You now have **7 services** running: inference, embeddings, vector DB, cache, API gateway, Prometheus, and Grafana.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Test it immediately
|
|
68
|
+
curl http://localhost:8000/v1/chat/completions \
|
|
69
|
+
-H "Authorization: Bearer YOUR_KEY" \
|
|
70
|
+
-H "Content-Type: application/json" \
|
|
71
|
+
-d '{"model":"llama3.2","messages":[{"role":"user","content":"Hello!"}]}'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Works with **any OpenAI-compatible client**: LangChain, LlamaIndex, Vercel AI SDK, openai-python.
|
|
75
|
+
|
|
76
|
+
## Who is this for?
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
- **AI app developers** who want local inference without Docker boilerplate
|
|
79
|
+
- **Teams** who need an OpenAI-compatible API backed by local models
|
|
80
|
+
- **Hobbyists** running LLMs locally who want vector search, caching, and monitoring out of the box
|
|
81
|
+
- **Anyone** tired of writing 200+ lines of docker-compose.yml every time
|
|
82
|
+
|
|
83
|
+
## What you get
|
|
62
84
|
|
|
63
85
|
```
|
|
64
86
|
llmstack up
|
|
@@ -84,8 +106,6 @@ That's it. You now have a full LLM API running locally.
|
|
|
84
106
|
:8080
|
|
85
107
|
```
|
|
86
108
|
|
|
87
|
-
## What you get
|
|
88
|
-
|
|
89
109
|
| Layer | Service | Default | Port |
|
|
90
110
|
|-------|---------|---------|------|
|
|
91
111
|
| Inference | Ollama / vLLM (auto) | llama3.2 | 11434 |
|
|
@@ -97,7 +117,7 @@ That's it. You now have a full LLM API running locally.
|
|
|
97
117
|
|
|
98
118
|
## How it works
|
|
99
119
|
|
|
100
|
-
```
|
|
120
|
+
```bash
|
|
101
121
|
llmstack init # Detects hardware, generates llmstack.yaml
|
|
102
122
|
# Picks optimal backend: vLLM for NVIDIA 16GB+, Ollama otherwise
|
|
103
123
|
|
|
@@ -109,27 +129,6 @@ llmstack logs ollama # Stream inference logs
|
|
|
109
129
|
llmstack down # Stops everything
|
|
110
130
|
```
|
|
111
131
|
|
|
112
|
-
### Use the API
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
curl http://localhost:8000/v1/chat/completions \
|
|
116
|
-
-H "Authorization: Bearer YOUR_KEY" \
|
|
117
|
-
-H "Content-Type: application/json" \
|
|
118
|
-
-d '{"model":"llama3.2","messages":[{"role":"user","content":"Hello!"}]}'
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Works with **any OpenAI-compatible client**: LangChain, LlamaIndex, Vercel AI SDK, openai-python.
|
|
122
|
-
|
|
123
|
-
```python
|
|
124
|
-
from openai import OpenAI
|
|
125
|
-
|
|
126
|
-
client = OpenAI(base_url="http://localhost:8000/v1", api_key="YOUR_KEY")
|
|
127
|
-
response = client.chat.completions.create(
|
|
128
|
-
model="llama3.2",
|
|
129
|
-
messages=[{"role": "user", "content": "Explain quantum computing"}]
|
|
130
|
-
)
|
|
131
|
-
```
|
|
132
|
-
|
|
133
132
|
## Auto hardware detection
|
|
134
133
|
|
|
135
134
|
| Your hardware | Backend | Why |
|
|
@@ -181,6 +180,50 @@ observe:
|
|
|
181
180
|
dashboard_port: 8080
|
|
182
181
|
```
|
|
183
182
|
|
|
183
|
+
## Interactive Chat
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
llmstack chat
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
LLMStack Chat — model: llama3.2
|
|
191
|
+
Type 'exit' or Ctrl+C to quit. '/clear' to reset conversation.
|
|
192
|
+
|
|
193
|
+
You: What is quantum computing?
|
|
194
|
+
Assistant: Quantum computing uses quantum mechanical phenomena like
|
|
195
|
+
superposition and entanglement to process information...
|
|
196
|
+
|
|
197
|
+
You: /clear
|
|
198
|
+
Conversation cleared.
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Streaming responses, conversation history, works with any model in your stack.
|
|
202
|
+
|
|
203
|
+
## Export to Docker Compose
|
|
204
|
+
|
|
205
|
+
Don't want to install llmstack? Generate a standalone `docker-compose.yml`:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
llmstack export
|
|
209
|
+
# Exported 7 services to docker-compose.yml
|
|
210
|
+
# Run with: docker compose up -d
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Share the generated file with your team — no llmstack dependency required.
|
|
214
|
+
|
|
215
|
+
## Use the API
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from openai import OpenAI
|
|
219
|
+
|
|
220
|
+
client = OpenAI(base_url="http://localhost:8000/v1", api_key="YOUR_KEY")
|
|
221
|
+
response = client.chat.completions.create(
|
|
222
|
+
model="llama3.2",
|
|
223
|
+
messages=[{"role": "user", "content": "Explain quantum computing"}]
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
184
227
|
## CLI
|
|
185
228
|
|
|
186
229
|
| Command | Description |
|
|
@@ -189,6 +232,8 @@ observe:
|
|
|
189
232
|
| `llmstack up [--attach]` | Start all services |
|
|
190
233
|
| `llmstack down [--volumes]` | Stop and clean up |
|
|
191
234
|
| `llmstack status` | Health check all services |
|
|
235
|
+
| `llmstack chat [--model]` | Interactive terminal chat |
|
|
236
|
+
| `llmstack export [--output]` | Generate docker-compose.yml |
|
|
192
237
|
| `llmstack logs <service>` | Stream service logs |
|
|
193
238
|
| `llmstack doctor` | Diagnose system issues |
|
|
194
239
|
|
|
@@ -204,6 +249,34 @@ When `observe.metrics: true`, llmstack boots Prometheus + Grafana with a pre-bui
|
|
|
204
249
|
|
|
205
250
|
Access at `http://localhost:8080` (login: admin / llmstack)
|
|
206
251
|
|
|
252
|
+
## Why not just Docker Compose?
|
|
253
|
+
|
|
254
|
+
Here's what llmstack replaces:
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
# Without llmstack: ~200 lines of docker-compose.yml
|
|
258
|
+
# You have to configure each service, write health checks,
|
|
259
|
+
# set up networking, manage GPU passthrough, create Prometheus
|
|
260
|
+
# scrape configs, provision Grafana dashboards...
|
|
261
|
+
|
|
262
|
+
# With llmstack:
|
|
263
|
+
llmstack init && llmstack up
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Comparison
|
|
267
|
+
|
|
268
|
+
| | llmstack | Ollama | LocalAI | AnythingLLM | LiteLLM |
|
|
269
|
+
|---|---|---|---|---|---|
|
|
270
|
+
| One-command full stack | **Yes** | No (inference only) | No | Partial | No (proxy only) |
|
|
271
|
+
| Auto hardware detection | **Yes** | No | No | No | No |
|
|
272
|
+
| OpenAI-compatible API | **Yes** | Yes | Yes | No | Yes |
|
|
273
|
+
| Built-in vector DB | **Yes** | No | No | Bundled | No |
|
|
274
|
+
| Built-in embeddings | **Yes** | No | No | Bundled | No |
|
|
275
|
+
| Caching (Redis) | **Yes** | No | No | No | No |
|
|
276
|
+
| Auth + rate limiting | **Yes** | No | No | Yes | Yes |
|
|
277
|
+
| Observability dashboard | **Yes** | No | Partial | No | Partial |
|
|
278
|
+
| Plugin ecosystem | **Yes** | No | No | No | No |
|
|
279
|
+
|
|
207
280
|
## Plugins
|
|
208
281
|
|
|
209
282
|
Extend llmstack with new backends via pip:
|
|
@@ -215,21 +288,6 @@ pip install llmstack-cli-plugin-chromadb
|
|
|
215
288
|
|
|
216
289
|
Create your own: implement `ServiceBase`, register via entry_points. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
217
290
|
|
|
218
|
-
## Why llmstack?
|
|
219
|
-
|
|
220
|
-
| | llmstack | Ollama | Harbor | AnythingLLM | LiteLLM |
|
|
221
|
-
|---|---|---|---|---|---|
|
|
222
|
-
| One-command full stack | Yes | No (inference only) | Partial | Partial | No (proxy only) |
|
|
223
|
-
| Auto hardware detection | Yes | No | No | No | No |
|
|
224
|
-
| OpenAI-compatible API | Yes | Yes | Varies | No | Yes |
|
|
225
|
-
| Built-in vector DB | Yes | No | Config needed | Bundled | No |
|
|
226
|
-
| Built-in embeddings | Yes | No | No | Bundled | No |
|
|
227
|
-
| Caching (Redis) | Yes | No | No | No | No |
|
|
228
|
-
| Auth + rate limiting | Yes | No | No | Yes | Yes |
|
|
229
|
-
| Observability dashboard | Yes | No | Partial | No | Partial |
|
|
230
|
-
| Plugin ecosystem | Yes | No | No | No | No |
|
|
231
|
-
| SSE streaming | Yes | Yes | Yes | Yes | Yes |
|
|
232
|
-
|
|
233
291
|
## Tech stack
|
|
234
292
|
|
|
235
293
|
- **CLI**: [Typer](https://typer.tiangolo.com/) + [Rich](https://rich.readthedocs.io/)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
llmstack/__init__.py,sha256=
|
|
1
|
+
llmstack/__init__.py,sha256=ixXALf2pEpG4MqBgKPZQGn27zrgKhzcpe22AoV7XXdY,84
|
|
2
2
|
llmstack/__main__.py,sha256=XhknKG6tlsgyslTC57MpF3IGUu_cIo6G5rSIz4kxuyc,86
|
|
3
3
|
llmstack/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
llmstack/cli/app.py,sha256=
|
|
4
|
+
llmstack/cli/app.py,sha256=eYm9U7BNTFsFNYjfE3COHBw7FX0znRNtce4A0Lbb4ic,2924
|
|
5
5
|
llmstack/cli/console.py,sha256=qnwJ2g39BwXhA_WhGtJW5E1ZdbTau4npG9j9mBo2zX0,259
|
|
6
6
|
llmstack/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
llmstack/cli/commands/chat.py,sha256=pdhVHqHj2hXMcxhcywYUgAkFLHN98x8jQyCu4I-zgYU,3586
|
|
7
8
|
llmstack/cli/commands/doctor.py,sha256=71PM5uG-a8A4nNf4ywMXmVIL8w6W5MwLgLco9DxbnG8,2440
|
|
8
9
|
llmstack/cli/commands/down.py,sha256=51XiP8n_Nq3oX4gbeLd_vCvXB1OP7dZEi3392eYgSJM,752
|
|
10
|
+
llmstack/cli/commands/export.py,sha256=Xv6r-htPfHCyxqOFQlC0jlRNAD7nP0ocM3NU_aXscWU,7425
|
|
9
11
|
llmstack/cli/commands/init.py,sha256=1olPvaPLapdSkuSBVIum4kzXh2BMtkRw-HNZKs1slKc,2311
|
|
10
12
|
llmstack/cli/commands/logs.py,sha256=CGZO9kFiqc224LtpYH22uf0k5E26sCmAucrozjtb7BQ,840
|
|
11
13
|
llmstack/cli/commands/status.py,sha256=ahnhKsw0C6-x2umOVUNsCnTU5XQS-nGIjKOFxEZVjdA,1382
|
|
@@ -21,10 +23,9 @@ llmstack/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
21
23
|
llmstack/core/hardware.py,sha256=dO2VRD2OZ4BnDETkHBRw1YeTERz6XhFByQpy8z7-jWc,3844
|
|
22
24
|
llmstack/core/health.py,sha256=BxH-Y1DsoDFc0zNxYVTdYEmKTC1dIWiBxhCLDLbUyr0,706
|
|
23
25
|
llmstack/core/resolver.py,sha256=MPUNLAoe0U_htLtMy789RbBir6xljWnw9p0AGOHYrAc,1633
|
|
24
|
-
llmstack/core/stack.py,sha256=
|
|
26
|
+
llmstack/core/stack.py,sha256=p0nPreFr8B7g1kKDJ1gONxxrjt9uLV488qW5NXb2tpk,8812
|
|
25
27
|
llmstack/docker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
-
llmstack/docker/manager.py,sha256=
|
|
27
|
-
llmstack/gateway/Dockerfile,sha256=Mtiy9fcfK1DikZ6v-I4zBI_Op1pABVm1nYZ-LT8J9fg,308
|
|
28
|
+
llmstack/docker/manager.py,sha256=dpKFNgkpKYOvrIPHeMGHwmVGrHLXu9rIArlquZToQqI,5528
|
|
28
29
|
llmstack/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
30
|
llmstack/gateway/main.py,sha256=PYT48w4MuBNCqJ3YTq27OF_yC0dRRpnC61bpos1aveY,1483
|
|
30
31
|
llmstack/gateway/proxy.py,sha256=WLErhSUICVK1XWu6Qt9v5gBWYtwalP3LKJE_D5Dj6C4,2077
|
|
@@ -47,16 +48,17 @@ llmstack/services/cache/redis.py,sha256=_6_JlFxFEvVutkmFwo-qzola6heODG_fxrfGVt0x
|
|
|
47
48
|
llmstack/services/embeddings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
49
|
llmstack/services/embeddings/tei.py,sha256=GNJL6PxQVUu7FQEzR0jj1N2jDCkhZoO1ezKLcEdo5EI,1502
|
|
49
50
|
llmstack/services/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
|
-
llmstack/services/gateway/service.py,sha256=
|
|
51
|
+
llmstack/services/gateway/service.py,sha256=lDJ1f7JMkSUMsnfUH-PjvZQLG37vs-zEQZajpV48d0E,2086
|
|
51
52
|
llmstack/services/inference/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
53
|
llmstack/services/inference/ollama.py,sha256=0A-4V3Ks3fiyQwnJkBhLtj9oIvKAYPK-QgsbcAsuqFI,1814
|
|
53
54
|
llmstack/services/inference/vllm.py,sha256=fhi7xa2RitNG0FNnhypUYfjEemvylnnxOIksRfBzJcE,1607
|
|
54
55
|
llmstack/services/observe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
-
llmstack/services/observe/prometheus.py,sha256=
|
|
56
|
+
llmstack/services/observe/prometheus.py,sha256=9gyoU7uhrNpQCvnmIKcr5xLpIx8e9z05LB31ri2UryI,7418
|
|
56
57
|
llmstack/services/vectordb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
58
|
llmstack/services/vectordb/qdrant.py,sha256=zIOqf5km-4pItXOW8Y-qdxHLiU9lFfhDz33nBrTgPhM,900
|
|
58
|
-
|
|
59
|
-
llmstack_cli-0.
|
|
60
|
-
llmstack_cli-0.
|
|
61
|
-
llmstack_cli-0.
|
|
62
|
-
llmstack_cli-0.
|
|
59
|
+
llmstack/gateway/Dockerfile,sha256=ZoId9IGitvOViaF4m6cXo-GGVwPYxPWZZHKu89Tnjak,385
|
|
60
|
+
llmstack_cli-0.2.0.dist-info/METADATA,sha256=nfDKQ6CRTHENvR7ohwk9FgSZxAnWd1rrCgimY6S2KKs,9791
|
|
61
|
+
llmstack_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
62
|
+
llmstack_cli-0.2.0.dist-info/entry_points.txt,sha256=i2BIacwqAqUaN1yAe-MaJZ22unHqAAUkTopk9M_iZPo,50
|
|
63
|
+
llmstack_cli-0.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
64
|
+
llmstack_cli-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|