llmshim 0.1.2__py3-none-win_amd64.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.
- llmshim/__init__.py +17 -0
- llmshim/_client.py +246 -0
- llmshim/_server.py +172 -0
- llmshim-0.1.2.data/scripts/llmshim.exe +0 -0
- llmshim-0.1.2.dist-info/METADATA +152 -0
- llmshim-0.1.2.dist-info/RECORD +8 -0
- llmshim-0.1.2.dist-info/WHEEL +4 -0
- llmshim-0.1.2.dist-info/sboms/llmshim.cyclonedx.json +5786 -0
llmshim/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
llmshim — Multi-provider LLM gateway for Python.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
import llmshim
|
|
6
|
+
|
|
7
|
+
llmshim.configure(anthropic="sk-ant-...", openai="sk-...")
|
|
8
|
+
resp = llmshim.chat("claude-sonnet-4-6", "Hello!")
|
|
9
|
+
print(resp["message"]["content"])
|
|
10
|
+
|
|
11
|
+
The proxy server starts automatically on first use and stops on exit.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from llmshim._client import chat, stream, models, health, configure
|
|
15
|
+
|
|
16
|
+
__all__ = ["chat", "stream", "models", "health", "configure"]
|
|
17
|
+
__version__ = "0.1.2"
|
llmshim/_client.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
llmshim module-level API.
|
|
3
|
+
|
|
4
|
+
import llmshim
|
|
5
|
+
resp = llmshim.chat("claude-sonnet-4-6", "Hello!")
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Generator, Optional
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from llmshim._server import ensure_server
|
|
18
|
+
|
|
19
|
+
_base_url: Optional[str] = None
|
|
20
|
+
_http: Optional[httpx.Client] = None
|
|
21
|
+
_timeout: float = 120.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_base_url() -> str:
|
|
25
|
+
global _base_url
|
|
26
|
+
if _base_url is None:
|
|
27
|
+
_base_url = ensure_server()
|
|
28
|
+
return _base_url
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_http() -> httpx.Client:
|
|
32
|
+
global _http
|
|
33
|
+
if _http is None:
|
|
34
|
+
_http = httpx.Client(timeout=_timeout)
|
|
35
|
+
return _http
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def configure(
|
|
39
|
+
*,
|
|
40
|
+
openai: Optional[str] = None,
|
|
41
|
+
anthropic: Optional[str] = None,
|
|
42
|
+
gemini: Optional[str] = None,
|
|
43
|
+
xai: Optional[str] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Configure API keys. Writes to ~/.llmshim/config.toml.
|
|
46
|
+
|
|
47
|
+
Keys are persistent — configure once, use everywhere.
|
|
48
|
+
Only provided keys are updated; others are left unchanged.
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
import llmshim
|
|
52
|
+
llmshim.configure(anthropic="sk-ant-...", openai="sk-...")
|
|
53
|
+
"""
|
|
54
|
+
config_dir = Path.home() / ".llmshim"
|
|
55
|
+
config_path = config_dir / "config.toml"
|
|
56
|
+
|
|
57
|
+
# Read existing config
|
|
58
|
+
existing: dict[str, Any] = {}
|
|
59
|
+
if config_path.exists():
|
|
60
|
+
try:
|
|
61
|
+
import tomllib # Python 3.11+
|
|
62
|
+
except ImportError:
|
|
63
|
+
tomllib = None # type: ignore
|
|
64
|
+
if tomllib:
|
|
65
|
+
try:
|
|
66
|
+
with open(config_path, "rb") as f:
|
|
67
|
+
existing = tomllib.load(f)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
keys = existing.get("keys", {})
|
|
72
|
+
if openai is not None:
|
|
73
|
+
keys["openai"] = openai
|
|
74
|
+
if anthropic is not None:
|
|
75
|
+
keys["anthropic"] = anthropic
|
|
76
|
+
if gemini is not None:
|
|
77
|
+
keys["gemini"] = gemini
|
|
78
|
+
if xai is not None:
|
|
79
|
+
keys["xai"] = xai
|
|
80
|
+
|
|
81
|
+
# Write back
|
|
82
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
lines = ["[keys]"]
|
|
84
|
+
for key, val in keys.items():
|
|
85
|
+
if val:
|
|
86
|
+
lines.append(f'{key} = "{val}"')
|
|
87
|
+
if "proxy" in existing:
|
|
88
|
+
lines.append("")
|
|
89
|
+
lines.append("[proxy]")
|
|
90
|
+
proxy = existing["proxy"]
|
|
91
|
+
if "host" in proxy:
|
|
92
|
+
lines.append(f'host = "{proxy["host"]}"')
|
|
93
|
+
if "port" in proxy:
|
|
94
|
+
lines.append(f"port = {proxy['port']}")
|
|
95
|
+
|
|
96
|
+
config_path.write_text("\n".join(lines) + "\n")
|
|
97
|
+
|
|
98
|
+
# Also set as env vars for the current process (so server picks them up)
|
|
99
|
+
if openai:
|
|
100
|
+
os.environ["OPENAI_API_KEY"] = openai
|
|
101
|
+
if anthropic:
|
|
102
|
+
os.environ["ANTHROPIC_API_KEY"] = anthropic
|
|
103
|
+
if gemini:
|
|
104
|
+
os.environ["GEMINI_API_KEY"] = gemini
|
|
105
|
+
if xai:
|
|
106
|
+
os.environ["XAI_API_KEY"] = xai
|
|
107
|
+
|
|
108
|
+
# If server is already running, it won't pick up new keys until restart.
|
|
109
|
+
# Force restart on next call.
|
|
110
|
+
global _base_url
|
|
111
|
+
from llmshim._server import _stop_server
|
|
112
|
+
|
|
113
|
+
_stop_server()
|
|
114
|
+
_base_url = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def chat(
|
|
118
|
+
model: str,
|
|
119
|
+
messages: str | list[dict[str, Any]],
|
|
120
|
+
*,
|
|
121
|
+
max_tokens: Optional[int] = None,
|
|
122
|
+
temperature: Optional[float] = None,
|
|
123
|
+
reasoning_effort: Optional[str] = None,
|
|
124
|
+
provider_config: Optional[dict[str, Any]] = None,
|
|
125
|
+
fallback: Optional[list[str]] = None,
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
"""Send a chat completion request.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
model: Model ID (e.g., "anthropic/claude-sonnet-4-6" or "claude-sonnet-4-6")
|
|
131
|
+
messages: A string (single user message) or list of message dicts
|
|
132
|
+
max_tokens: Maximum output tokens
|
|
133
|
+
temperature: Sampling temperature
|
|
134
|
+
reasoning_effort: "low", "medium", or "high"
|
|
135
|
+
provider_config: Raw provider-specific JSON
|
|
136
|
+
fallback: Ordered list of fallback model IDs
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Response dict with keys: id, model, provider, message, usage, latency_ms
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
resp = llmshim.chat("claude-sonnet-4-6", "What is Rust?")
|
|
143
|
+
print(resp["message"]["content"])
|
|
144
|
+
"""
|
|
145
|
+
if isinstance(messages, str):
|
|
146
|
+
msgs = [{"role": "user", "content": messages}]
|
|
147
|
+
else:
|
|
148
|
+
msgs = messages
|
|
149
|
+
|
|
150
|
+
body: dict[str, Any] = {"model": model, "messages": msgs}
|
|
151
|
+
|
|
152
|
+
config: dict[str, Any] = {}
|
|
153
|
+
if max_tokens is not None:
|
|
154
|
+
config["max_tokens"] = max_tokens
|
|
155
|
+
if temperature is not None:
|
|
156
|
+
config["temperature"] = temperature
|
|
157
|
+
if reasoning_effort is not None:
|
|
158
|
+
config["reasoning_effort"] = reasoning_effort
|
|
159
|
+
if config:
|
|
160
|
+
body["config"] = config
|
|
161
|
+
|
|
162
|
+
if provider_config is not None:
|
|
163
|
+
body["provider_config"] = provider_config
|
|
164
|
+
if fallback is not None:
|
|
165
|
+
body["fallback"] = fallback
|
|
166
|
+
|
|
167
|
+
resp = _get_http().post(f"{_get_base_url()}/v1/chat", json=body)
|
|
168
|
+
resp.raise_for_status()
|
|
169
|
+
return resp.json()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def stream(
|
|
173
|
+
model: str,
|
|
174
|
+
messages: str | list[dict[str, Any]],
|
|
175
|
+
*,
|
|
176
|
+
max_tokens: Optional[int] = None,
|
|
177
|
+
temperature: Optional[float] = None,
|
|
178
|
+
reasoning_effort: Optional[str] = None,
|
|
179
|
+
provider_config: Optional[dict[str, Any]] = None,
|
|
180
|
+
) -> Generator[dict[str, Any], None, None]:
|
|
181
|
+
"""Stream a chat completion. Yields typed event dicts.
|
|
182
|
+
|
|
183
|
+
Event types: content, reasoning, tool_call, usage, done, error
|
|
184
|
+
|
|
185
|
+
Usage:
|
|
186
|
+
for event in llmshim.stream("claude-sonnet-4-6", "Write a poem"):
|
|
187
|
+
if event["type"] == "content":
|
|
188
|
+
print(event["text"], end="")
|
|
189
|
+
"""
|
|
190
|
+
if isinstance(messages, str):
|
|
191
|
+
msgs = [{"role": "user", "content": messages}]
|
|
192
|
+
else:
|
|
193
|
+
msgs = messages
|
|
194
|
+
|
|
195
|
+
body: dict[str, Any] = {"model": model, "messages": msgs}
|
|
196
|
+
|
|
197
|
+
config: dict[str, Any] = {}
|
|
198
|
+
if max_tokens is not None:
|
|
199
|
+
config["max_tokens"] = max_tokens
|
|
200
|
+
if temperature is not None:
|
|
201
|
+
config["temperature"] = temperature
|
|
202
|
+
if reasoning_effort is not None:
|
|
203
|
+
config["reasoning_effort"] = reasoning_effort
|
|
204
|
+
if config:
|
|
205
|
+
body["config"] = config
|
|
206
|
+
|
|
207
|
+
if provider_config is not None:
|
|
208
|
+
body["provider_config"] = provider_config
|
|
209
|
+
|
|
210
|
+
with httpx.stream(
|
|
211
|
+
"POST",
|
|
212
|
+
f"{_get_base_url()}/v1/chat/stream",
|
|
213
|
+
json=body,
|
|
214
|
+
timeout=_timeout,
|
|
215
|
+
) as resp:
|
|
216
|
+
resp.raise_for_status()
|
|
217
|
+
current_event = ""
|
|
218
|
+
for line in resp.iter_lines():
|
|
219
|
+
if line.startswith("event: "):
|
|
220
|
+
current_event = line[7:]
|
|
221
|
+
elif line.startswith("data: "):
|
|
222
|
+
data = json.loads(line[6:])
|
|
223
|
+
data["type"] = current_event or data.get("type", "")
|
|
224
|
+
yield data
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def models() -> list[dict[str, str]]:
|
|
228
|
+
"""List available models.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of dicts with keys: id, provider, name
|
|
232
|
+
"""
|
|
233
|
+
resp = _get_http().get(f"{_get_base_url()}/v1/models")
|
|
234
|
+
resp.raise_for_status()
|
|
235
|
+
return resp.json()["models"]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def health() -> dict[str, Any]:
|
|
239
|
+
"""Health check.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Dict with keys: status, providers
|
|
243
|
+
"""
|
|
244
|
+
resp = _get_http().get(f"{_get_base_url()}/health")
|
|
245
|
+
resp.raise_for_status()
|
|
246
|
+
return resp.json()
|
llmshim/_server.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-managed llmshim proxy server.
|
|
3
|
+
|
|
4
|
+
Starts the bundled Rust binary on a random port, waits for it to be ready,
|
|
5
|
+
and stops it on process exit. The server is shared across all LlmShim
|
|
6
|
+
instances in the same process.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import atexit
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import signal
|
|
13
|
+
import socket
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_server_process = None
|
|
20
|
+
_server_port = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _find_binary() -> str:
|
|
24
|
+
"""Find the llmshim binary. Checks in order:
|
|
25
|
+
1. Bundled in the package (clients/python/llmshim/bin/)
|
|
26
|
+
2. On PATH (e.g., installed via cargo install)
|
|
27
|
+
3. In the repo's target/release/ directory
|
|
28
|
+
"""
|
|
29
|
+
bin_name = "llmshim.exe" if platform.system() == "Windows" else "llmshim"
|
|
30
|
+
pkg_dir = Path(__file__).parent
|
|
31
|
+
|
|
32
|
+
# 1. Python scripts directory (where maturin/pip installs binaries)
|
|
33
|
+
import sysconfig
|
|
34
|
+
scripts_dir = sysconfig.get_path("scripts")
|
|
35
|
+
if scripts_dir:
|
|
36
|
+
candidate = Path(scripts_dir) / bin_name
|
|
37
|
+
if candidate.exists() and os.access(str(candidate), os.X_OK):
|
|
38
|
+
return str(candidate)
|
|
39
|
+
|
|
40
|
+
# 2. Bundled in package (dev installs, manual copy)
|
|
41
|
+
bundled = pkg_dir / "bin" / bin_name
|
|
42
|
+
if bundled.exists() and os.access(str(bundled), os.X_OK):
|
|
43
|
+
return str(bundled)
|
|
44
|
+
|
|
45
|
+
# 3. On PATH (cargo install)
|
|
46
|
+
import shutil
|
|
47
|
+
on_path = shutil.which("llmshim")
|
|
48
|
+
if on_path:
|
|
49
|
+
return on_path
|
|
50
|
+
|
|
51
|
+
# 4. Repo target directory (development)
|
|
52
|
+
for root in [pkg_dir.parent.parent.parent, pkg_dir.parent.parent]:
|
|
53
|
+
for build_dir in ["target/release", "target/debug"]:
|
|
54
|
+
candidate = root / build_dir / bin_name
|
|
55
|
+
if candidate.exists() and os.access(str(candidate), os.X_OK):
|
|
56
|
+
return str(candidate)
|
|
57
|
+
|
|
58
|
+
raise FileNotFoundError(
|
|
59
|
+
"llmshim binary not found. Install with:\n"
|
|
60
|
+
" pip install llmshim (includes the binary)\n"
|
|
61
|
+
" cargo install llmshim (from crates.io)\n"
|
|
62
|
+
" cargo build --release --features proxy (from source)"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _find_free_port() -> int:
|
|
67
|
+
"""Find a free port on localhost."""
|
|
68
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
69
|
+
s.bind(("127.0.0.1", 0))
|
|
70
|
+
return s.getsockname()[1]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _wait_for_server(port: int, timeout: float = 10.0) -> bool:
|
|
74
|
+
"""Wait until the server is accepting connections."""
|
|
75
|
+
start = time.time()
|
|
76
|
+
while time.time() - start < timeout:
|
|
77
|
+
try:
|
|
78
|
+
with socket.create_connection(("127.0.0.1", port), timeout=0.5):
|
|
79
|
+
return True
|
|
80
|
+
except (ConnectionRefusedError, OSError):
|
|
81
|
+
time.sleep(0.1)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _stop_server():
|
|
86
|
+
"""Stop the server process."""
|
|
87
|
+
global _server_process, _server_port
|
|
88
|
+
if _server_process is not None:
|
|
89
|
+
try:
|
|
90
|
+
_server_process.terminate()
|
|
91
|
+
_server_process.wait(timeout=5)
|
|
92
|
+
except Exception:
|
|
93
|
+
try:
|
|
94
|
+
_server_process.kill()
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
_server_process = None
|
|
98
|
+
_server_port = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def ensure_server() -> str:
|
|
102
|
+
"""Ensure the proxy server is running. Returns the base URL.
|
|
103
|
+
|
|
104
|
+
Starts the server automatically if not already running.
|
|
105
|
+
The server is stopped automatically on process exit.
|
|
106
|
+
"""
|
|
107
|
+
global _server_process, _server_port
|
|
108
|
+
|
|
109
|
+
# Already running?
|
|
110
|
+
if _server_process is not None and _server_process.poll() is None:
|
|
111
|
+
return f"http://127.0.0.1:{_server_port}"
|
|
112
|
+
|
|
113
|
+
# Find binary
|
|
114
|
+
binary = _find_binary()
|
|
115
|
+
|
|
116
|
+
# Pick a free port
|
|
117
|
+
port = _find_free_port()
|
|
118
|
+
|
|
119
|
+
# Start the server
|
|
120
|
+
env = os.environ.copy()
|
|
121
|
+
env["LLMSHIM_HOST"] = "127.0.0.1"
|
|
122
|
+
env["LLMSHIM_PORT"] = str(port)
|
|
123
|
+
|
|
124
|
+
_server_process = subprocess.Popen(
|
|
125
|
+
[binary, "proxy"],
|
|
126
|
+
env=env,
|
|
127
|
+
stdout=subprocess.DEVNULL,
|
|
128
|
+
stderr=subprocess.PIPE,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Register cleanup
|
|
132
|
+
atexit.register(_stop_server)
|
|
133
|
+
|
|
134
|
+
# Also handle SIGTERM
|
|
135
|
+
try:
|
|
136
|
+
original_handler = signal.getsignal(signal.SIGTERM)
|
|
137
|
+
def _handle_sigterm(signum, frame):
|
|
138
|
+
_stop_server()
|
|
139
|
+
if callable(original_handler) and original_handler not in (signal.SIG_DFL, signal.SIG_IGN):
|
|
140
|
+
original_handler(signum, frame)
|
|
141
|
+
sys.exit(0)
|
|
142
|
+
signal.signal(signal.SIGTERM, _handle_sigterm)
|
|
143
|
+
except (ValueError, OSError):
|
|
144
|
+
pass # Can't set signal handler in some contexts (e.g., threads)
|
|
145
|
+
|
|
146
|
+
# Wait for server to be ready
|
|
147
|
+
if not _wait_for_server(port):
|
|
148
|
+
stderr_output = ""
|
|
149
|
+
try:
|
|
150
|
+
stderr_output = _server_process.stderr.read().decode() if _server_process.stderr else ""
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
_stop_server()
|
|
154
|
+
|
|
155
|
+
# Detect common errors and give helpful messages
|
|
156
|
+
if "No API keys found" in stderr_output:
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
"No API keys configured. Set them with:\n\n"
|
|
159
|
+
" import llmshim\n"
|
|
160
|
+
" llmshim.configure(anthropic='sk-ant-...', openai='sk-...')\n\n"
|
|
161
|
+
"Or from the command line:\n"
|
|
162
|
+
" llmshim configure"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"llmshim proxy failed to start on port {port}.\n"
|
|
167
|
+
f"Binary: {binary}\n"
|
|
168
|
+
f"stderr: {stderr_output}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
_server_port = port
|
|
172
|
+
return f"http://127.0.0.1:{port}"
|
|
Binary file
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llmshim
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Rust
|
|
9
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
10
|
+
Requires-Dist: httpx>=0.24
|
|
11
|
+
Summary: Multi-provider LLM gateway — one interface, every provider
|
|
12
|
+
Home-Page: https://github.com/sanjay920/llmshim
|
|
13
|
+
License: MIT
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Project-URL: Homepage, https://github.com/sanjay920/llmshim
|
|
17
|
+
Project-URL: Repository, https://github.com/sanjay920/llmshim
|
|
18
|
+
|
|
19
|
+
# llmshim
|
|
20
|
+
|
|
21
|
+
One interface, every LLM provider. The proxy server starts automatically — no setup needed.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install llmshim
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configure
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import llmshim
|
|
33
|
+
|
|
34
|
+
# Set API keys (writes to ~/.llmshim/config.toml — only needed once)
|
|
35
|
+
llmshim.configure(
|
|
36
|
+
anthropic="sk-ant-...",
|
|
37
|
+
openai="sk-...",
|
|
38
|
+
gemini="AIza...",
|
|
39
|
+
xai="xai-...",
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or from the CLI: `llmshim configure`
|
|
44
|
+
|
|
45
|
+
## Chat
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import llmshim
|
|
49
|
+
|
|
50
|
+
resp = llmshim.chat("claude-sonnet-4-6", "What is Rust?")
|
|
51
|
+
print(resp["message"]["content"])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With options:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
resp = llmshim.chat(
|
|
58
|
+
"openai/gpt-5.4",
|
|
59
|
+
"Explain quicksort",
|
|
60
|
+
max_tokens=500,
|
|
61
|
+
temperature=0.7,
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With message history:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
resp = llmshim.chat("claude-sonnet-4-6", [
|
|
69
|
+
{"role": "system", "content": "You are a pirate."},
|
|
70
|
+
{"role": "user", "content": "Hello!"},
|
|
71
|
+
], max_tokens=500)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Streaming
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
for event in llmshim.stream("claude-sonnet-4-6", "Write a poem"):
|
|
78
|
+
if event["type"] == "content":
|
|
79
|
+
print(event["text"], end="", flush=True)
|
|
80
|
+
elif event["type"] == "reasoning":
|
|
81
|
+
pass # thinking tokens
|
|
82
|
+
elif event["type"] == "usage":
|
|
83
|
+
print(f"\n[↑{event['input_tokens']} ↓{event['output_tokens']}]")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Multi-Model Conversations
|
|
87
|
+
|
|
88
|
+
Switch models mid-conversation. History carries over.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
messages = [{"role": "user", "content": "What is a closure?"}]
|
|
92
|
+
|
|
93
|
+
r1 = llmshim.chat("claude-sonnet-4-6", messages, max_tokens=500)
|
|
94
|
+
print(f"Claude: {r1['message']['content']}")
|
|
95
|
+
|
|
96
|
+
messages.append({"role": "assistant", "content": r1["message"]["content"]})
|
|
97
|
+
messages.append({"role": "user", "content": "Now explain differently."})
|
|
98
|
+
|
|
99
|
+
r2 = llmshim.chat("gpt-5.4", messages, max_tokens=500)
|
|
100
|
+
print(f"GPT: {r2['message']['content']}")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Reasoning / Thinking
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
resp = llmshim.chat(
|
|
107
|
+
"claude-sonnet-4-6",
|
|
108
|
+
"Solve: x^2 - 5x + 6 = 0",
|
|
109
|
+
max_tokens=4000,
|
|
110
|
+
reasoning_effort="high",
|
|
111
|
+
)
|
|
112
|
+
print(resp["reasoning"]) # thinking content
|
|
113
|
+
print(resp["message"]["content"]) # answer
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Fallback Chains
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
resp = llmshim.chat(
|
|
120
|
+
"anthropic/claude-sonnet-4-6",
|
|
121
|
+
"Hello",
|
|
122
|
+
max_tokens=100,
|
|
123
|
+
fallback=["openai/gpt-5.4", "gemini/gemini-3-flash-preview"],
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Other
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
llmshim.models() # list available models
|
|
131
|
+
llmshim.health() # {"status": "ok", "providers": [...]}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## How It Works
|
|
135
|
+
|
|
136
|
+
On first call, the package:
|
|
137
|
+
1. Finds the `llmshim` binary (bundled, on PATH, or in repo)
|
|
138
|
+
2. Starts the proxy on a random localhost port
|
|
139
|
+
3. Routes your request through it
|
|
140
|
+
4. Server stops automatically when Python exits
|
|
141
|
+
|
|
142
|
+
No Docker, no background services, no manual server management.
|
|
143
|
+
|
|
144
|
+
## Supported Models
|
|
145
|
+
|
|
146
|
+
| Provider | Models |
|
|
147
|
+
|----------|--------|
|
|
148
|
+
| OpenAI | `gpt-5.4` |
|
|
149
|
+
| Anthropic | `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001` |
|
|
150
|
+
| Gemini | `gemini-3.1-pro-preview`, `gemini-3-flash-preview`, `gemini-3.1-flash-lite-preview` |
|
|
151
|
+
| xAI | `grok-4-1-fast-reasoning`, `grok-4-1-fast-non-reasoning` |
|
|
152
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
llmshim/__init__.py,sha256=IkkeWimsM5SKVQHTjYt7vzTEe_NKjGAuV4tmopq6BQA,485
|
|
2
|
+
llmshim/_client.py,sha256=2PMEBY-iIBaFwB4hp2Gi-Rb6uTdPjbYm8I9CgGV_1bc,7162
|
|
3
|
+
llmshim/_server.py,sha256=RNbopsB83BhYst9Yfbiu_b2m9iJOHWF6nxZkhUX7AeY,5500
|
|
4
|
+
llmshim-0.1.2.data/scripts/llmshim.exe,sha256=_bL6XAePCHXVPISFeKgoZg5yQijZ-wdJBGgRV86AHfg,5669376
|
|
5
|
+
llmshim-0.1.2.dist-info/METADATA,sha256=UNcWHAVX_SuWuXuLyFSbXLguJrNZlPBM-e-Q5BkUSaw,3802
|
|
6
|
+
llmshim-0.1.2.dist-info/WHEEL,sha256=uJOc2U-Q1x95AlblQcqMRb3iR4QnPtdI7X2ycPN99rM,94
|
|
7
|
+
llmshim-0.1.2.dist-info/sboms/llmshim.cyclonedx.json,sha256=jd2OJlouFEf0x-NhDGHqC3jDc8lhQxKRBoQYsjAe_Pc,186159
|
|
8
|
+
llmshim-0.1.2.dist-info/RECORD,,
|