simplyllm 0.1.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.
myllm/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """myllm - Free AI LLM client with multi-provider fallback.
2
+
3
+ Provides a unified API for multiple AI providers (Cerebras, Groq,
4
+ OpenRouter, NVIDIA NIM) with automatic fallback, rate limiting, and
5
+ cooldown tracking.
6
+
7
+ Example:
8
+ >>> from myllm import MyLLM
9
+ >>> client = MyLLM(server_url="http://localhost:10000")
10
+ >>> result = client.chat([{"role": "user", "content": "Hello"}])
11
+ >>> print(result.content, result.provider, result.latency)
12
+ """
13
+
14
+ from .client import MyLLM, FallbackResult
15
+ from .config import Provider, PROVIDERS, PROVIDER_CHAIN, PROVIDER_MAP
16
+ from .server import app
17
+ from .tracker import Tracker, tracker
18
+
19
+ __version__ = "0.1.0"
20
+ __all__ = [
21
+ "MyLLM",
22
+ "FallbackResult",
23
+ "Provider",
24
+ "PROVIDERS",
25
+ "PROVIDER_CHAIN",
26
+ "PROVIDER_MAP",
27
+ "Tracker",
28
+ "tracker",
29
+ "app",
30
+ ]
myllm/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ main()
myllm/client.py ADDED
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.request
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class FallbackResult:
11
+ """Result from a chat completion with provider metadata.
12
+
13
+ Attributes:
14
+ content: The generated text response.
15
+ provider: Name of the provider that fulfilled the request.
16
+ model: Model identifier used.
17
+ latency: Time taken in seconds.
18
+ attempts: List of failed attempts before success.
19
+ """
20
+
21
+ content: str
22
+ provider: str
23
+ model: str
24
+ latency: float
25
+ attempts: list[dict[str, Any]] = field(default_factory=list)
26
+
27
+
28
+ def _call_server(server_url: str, params: dict[str, Any]) -> FallbackResult:
29
+ """Send a chat request to the myllm server.
30
+
31
+ Args:
32
+ server_url: Base URL of the myllm server.
33
+ params: Request parameters (messages, model, temperature, etc.).
34
+
35
+ Returns:
36
+ FallbackResult with generated content and metadata.
37
+
38
+ Raises:
39
+ RuntimeError: If the server returns an error or is unreachable.
40
+ """
41
+ url = f"{server_url.rstrip('/')}/chat/completions"
42
+ body = json.dumps(params).encode()
43
+ req = urllib.request.Request(
44
+ url,
45
+ data=body,
46
+ headers={"Content-Type": "application/json"},
47
+ method="POST",
48
+ )
49
+ try:
50
+ with urllib.request.urlopen(req, timeout=300) as resp:
51
+ data = json.loads(resp.read())
52
+ except Exception as exc:
53
+ raise RuntimeError(f"Server request failed: {exc}") from exc
54
+
55
+ if "error" in data:
56
+ raise RuntimeError(data["error"])
57
+
58
+ return FallbackResult(
59
+ content=data.get("content", ""),
60
+ provider=data.get("provider", ""),
61
+ model=data.get("model", ""),
62
+ latency=data.get("latency", 0),
63
+ attempts=data.get("attempts", []),
64
+ )
65
+
66
+
67
+ class MyLLM:
68
+ """Client for the myllm multi-provider LLM server.
69
+
70
+ Connects to a running myllm server and provides a simple interface
71
+ for chat completions with automatic provider fallback.
72
+
73
+ Args:
74
+ timeout: Request timeout in seconds. Defaults to 30.0.
75
+ server_url: myllm server URL. Defaults to "http://localhost:10000".
76
+ max_wait: Maximum wait time for provider availability in seconds.
77
+ Defaults to 80.0.
78
+
79
+ Example:
80
+ >>> client = MyLLM(server_url="http://localhost:10000")
81
+ >>> result = client.chat([{"role": "user", "content": "Hello"}])
82
+ >>> print(result.content)
83
+ >>> print(result.provider, result.latency)
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ *,
89
+ timeout: float = 30.0,
90
+ server_url: str = "http://localhost:10000",
91
+ max_wait: float = 80.0,
92
+ ) -> None:
93
+ self.timeout = timeout
94
+ self.server_url = server_url
95
+ self.max_wait = max_wait
96
+
97
+ def chat(
98
+ self,
99
+ messages: list[dict[str, Any]],
100
+ *,
101
+ model: str | None = None,
102
+ temperature: float | None = None,
103
+ max_tokens: int | None = None,
104
+ debug_provider: str | None = None,
105
+ max_wait: float | None = None,
106
+ **kwargs: Any,
107
+ ) -> FallbackResult:
108
+ """Send a chat completion request.
109
+
110
+ Args:
111
+ messages: List of message dicts with 'role' and 'content' keys.
112
+ model: Model identifier override. Uses provider default if None.
113
+ temperature: Sampling temperature (0.0-2.0).
114
+ max_tokens: Maximum tokens to generate.
115
+ debug_provider: Force a specific provider name (e.g. "groq").
116
+ max_wait: Override max wait time for this request.
117
+ **kwargs: Additional parameters passed to the server.
118
+
119
+ Returns:
120
+ FallbackResult with generated content and metadata.
121
+
122
+ Example:
123
+ >>> result = client.chat(
124
+ ... [{"role": "user", "content": "Explain quantum computing"}],
125
+ ... temperature=0.7,
126
+ ... max_tokens=500,
127
+ ... )
128
+ """
129
+ params: dict[str, Any] = {"messages": messages, **kwargs}
130
+ if model is not None:
131
+ params["model"] = model
132
+ if temperature is not None:
133
+ params["temperature"] = temperature
134
+ if max_tokens is not None:
135
+ params["max_tokens"] = max_tokens
136
+ if debug_provider is not None:
137
+ params["debug_provider"] = debug_provider
138
+ params["max_wait"] = max_wait if max_wait is not None else self.max_wait
139
+ params.setdefault("timeout", self.timeout)
140
+ return _call_server(self.server_url, params)
141
+
142
+ def complete(self, prompt: str, **kwargs: Any) -> FallbackResult:
143
+ """Convenience method to send a single user prompt.
144
+
145
+ Args:
146
+ prompt: The user message text.
147
+ **kwargs: Additional arguments passed to chat().
148
+
149
+ Returns:
150
+ FallbackResult with generated content and metadata.
151
+
152
+ Example:
153
+ >>> result = client.complete("What is 2+2?")
154
+ >>> print(result.content)
155
+ """
156
+ return self.chat([{"role": "user", "content": prompt}], **kwargs)
myllm/config.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Provider:
8
+ """AI provider configuration.
9
+
10
+ Attributes:
11
+ name: Provider identifier (e.g. "groq", "cerebras").
12
+ base_url: API base URL.
13
+ api_key_env: Environment variable name for the API key.
14
+ model: Default model identifier.
15
+ priority: Lower number = higher priority.
16
+ rpm: Requests per minute limit (0 = unlimited).
17
+ rpd: Requests per day limit (0 = unlimited).
18
+ """
19
+
20
+ name: str
21
+ base_url: str
22
+ api_key_env: str
23
+ model: str
24
+ priority: int = 0
25
+ rpm: int = 0
26
+ rpd: int = 0
27
+
28
+
29
+ PROVIDERS: list[Provider] = [
30
+ Provider(
31
+ name="cerebras",
32
+ base_url="https://api.cerebras.ai/v1",
33
+ api_key_env="CEREBRAS_API_KEY",
34
+ model="zai-glm-4.7",
35
+ priority=1,
36
+ rpm=5,
37
+ ),
38
+ Provider(
39
+ name="groq",
40
+ base_url="https://api.groq.com/openai/v1",
41
+ api_key_env="GROQ_API_KEY",
42
+ model="openai/gpt-oss-120b",
43
+ priority=2,
44
+ rpm=30,
45
+ rpd=1000,
46
+ ),
47
+ Provider(
48
+ name="openrouter",
49
+ base_url="https://openrouter.ai/api/v1",
50
+ api_key_env="OPENROUTER_API_KEY",
51
+ model="google/gemma-4-31b-it:free",
52
+ priority=3,
53
+ rpm=20,
54
+ rpd=1000,
55
+ ),
56
+ Provider(
57
+ name="nvidia_nim",
58
+ base_url="https://integrate.api.nvidia.com/v1",
59
+ api_key_env="NVIDIA_API_KEY",
60
+ model="openai/gpt-oss-120b",
61
+ priority=4,
62
+ rpm=40,
63
+ ),
64
+ ]
65
+
66
+ PROVIDER_CHAIN: list[Provider] = sorted(PROVIDERS, key=lambda p: p.priority)
67
+ PROVIDER_MAP: dict[str, Provider] = {p.name: p for p in PROVIDERS}
myllm/diagnose.py ADDED
@@ -0,0 +1,30 @@
1
+ """Diagnose: test each provider individually (1 request each)."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ import time
6
+
7
+ sys.stdout.reconfigure(line_buffering=True)
8
+
9
+ from myllm.server import _route
10
+ from myllm.config import PROVIDERS
11
+ from myllm.tracker import tracker
12
+
13
+ PROMPT = "Say hello in one sentence."
14
+
15
+ for p in PROVIDERS:
16
+ print(f"\n--- {p.name} ({p.model}) ---")
17
+ t0 = time.monotonic()
18
+ result = _route(
19
+ messages=[{"role": "user", "content": PROMPT}],
20
+ debug_provider=p.name,
21
+ max_tokens=256,
22
+ )
23
+ lat = time.monotonic() - t0
24
+ if "error" in result:
25
+ print(f" FAIL ({lat:.2f}s): {result['error'][:120]}")
26
+ else:
27
+ print(f" OK ({lat:.2f}s): {result['content'][:80]}")
28
+ print(f" tracker: {tracker.snapshot()}")
29
+
30
+ print("\nDone.")
myllm/lib.py ADDED
@@ -0,0 +1,10 @@
1
+ """Utility functions for myllm."""
2
+
3
+
4
+ def main() -> None:
5
+ """Entry point for myllm CLI."""
6
+ print("Hello from myllm!")
7
+
8
+
9
+ if __name__ == "__main__":
10
+ main()
myllm/plain_execute.py ADDED
@@ -0,0 +1,275 @@
1
+ """Benchmark: 60 parallel requests, 120s hard timeout."""
2
+ from __future__ import annotations
3
+
4
+ import statistics
5
+ import sys
6
+ import threading
7
+ import time
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ sys.stdout.reconfigure(line_buffering=True)
13
+
14
+ from myllm.server import _route
15
+
16
+ TOTAL = 60
17
+ TIMEOUT = 120.0
18
+
19
+ PROMPTS = [
20
+ "대학원 레벨로 3차원 토러스의 가우스 곡률을 섹셔널 커버쳐 공식으로 유도하고, 오일러 특성 수를 계산해",
21
+ "리만 기하학에서 슈바르츠실드 계량의 크리스토펠 기호를 계산하고, 빛의 편향각을 뉴턴 근사로 유도해",
22
+ "양자역학에서 수소원자의 n=3, l=2, m=1 상태의 파동함수를 구하고, 각도분포함수를 그래프로 설명해",
23
+ "콕크로프트-월턴 가속기의 리플 전압을 Fourier 해석하고, 출력 에너지 스펙트럼의 반치폭을 유도해",
24
+ "열역학에서 반데르발스 기체의 보일 온도를 유도하고, 임계점에서의 압력-부피-온도 관계를 전개해",
25
+ "전자기학에서 전자기파의 스넬 법칙을 프레넬 방정식으로 유도하고, 브루스터 각에서의 반사율을 계산해",
26
+ "유체역학에서 나비에-스토크스 방정식을 무차원화하고, 레이놀즈 수에 따른 층류-난류 천이를 설명해",
27
+ "고체물리학에서 브릴루앙 영역의 페르미 표면을 자유전자 모델로 구하고, 밴드 구조를 설명해",
28
+ "미분기하학에서 리치 흐름 방정식을 2차원 구면에 대해 풀고, 곡률이 시간에 따라 변하는 과정을 전개해",
29
+ "응집물리학에서 BCS 이론으로 초전도체의 에너지 갭을 유도하고, 임계온도와의 관계를 설명해",
30
+ "양자장론에서 자유 스칼라장의 파동방정식을 그린 함수로 풀고, 전파자를 유도해",
31
+ "상대론적 역학에서 쌍생성 에너지를 계산하고, 전자-양전자 쌍의 운동량 보존을 전개해",
32
+ "천체물리학에서 토만-오펜하이머-볼코프 한계를 유도하고, 중성자별의 최대 질량을 추정해",
33
+ "통계역학에서 이상 보즈-아인슈타인 응축의 임계온도를 유도하고, 응축 비율을 온도 함수로 전개해",
34
+ "광학에서 파브리-페로 간섭계의 자유 스펙트럼 범위와 분해능을 유도하고, 피네스와의 관계를 설명해",
35
+ "입자물리학에서 쿼크 모형으로 중성자의 자기 모멘트를 계산하고, 양성자와의 비율을 유도해",
36
+ "편미분방정식에서 열방정식의 푸리에 해법을 유도하고, 그린 함수 방법으로 초기값 문제를 풀어",
37
+ "대수학에서 갈루아 이론으로 5차 방정식의 비가해성을 증명하고, 갈루아 군의 구조를 분석해",
38
+ "해석학에서 리만 제타 함수의 함수 방정식을 유도하고, 영점 분포와 소수 정리와의 관계를 설명해",
39
+ "위상수학에서 베티 수를 CW 복합체로 계산하고, 오일러 지표와의 관계를 전개해",
40
+ "확률론에서 중심극한정리를 특성함수 방법으로 증명하고, 수렴 속도를 베리-에센 정리로 추정해",
41
+ "수치해석에서 룽게-쿠타 현상의 원인을 분석하고, 체비셰프 점을 사용한 다항식 보간의 안정성을 증명해",
42
+ "제어이론에서 PID 제어기의 안정 영역을 루트 궤적법으로 구하고, 위상 여유와 이득 여유를 계산해",
43
+ "신호처리에서 이산 푸리에 변환의 원형 컨볼루션 정리를 증명하고, 빠른 푸리에 변환의 계산 복잡도를 분석해",
44
+ "정보이론에서 샤논 채널 용량定理을 증명하고, AWGN 채널에서의 최적 전달률을 유도해",
45
+ "기계학습에서 서포트 벡터 머신의 라그랑지 쌍대 문제를 유도하고, 커널 트릭의 수학적 근거를 설명해",
46
+ "최적화 이론에서 카루시-쿤타 조건을 유도하고, 등식 제약이 있는 라그랑지 승수법의 수렴성을 증명해",
47
+ "그래프 이론에서 맥스플로-민컷 정리를 증명하고, 에드몬드-카프 알고리즘의 시간복잡도를 분석해",
48
+ "암호학에서 RSA의 안전성을 정수 분해 문제로 환원하고, 밀러-라빈 소수 판별법의 오류 확률을 계산해",
49
+ "컴파일러 이론에서 LR 파서의 충돌 검출 알고리즘을 설계하고, LALR 테이블 생성 과정을 전개해",
50
+ "분산시스템에서 CAP 정리를 증명하고, Paxos 합의 알고리즘의 안전성을 형식적으로 검증해",
51
+ "운영체제에서 페이지 교체 알고리즘의 경쟁 비율을 분석하고, LRU의 최악 경우를 예시로 보여줘",
52
+ "데이터베이스에서 B+ 트리의 삽입/삭제 알고리즘을 분석하고, 높이와 디스크 접근의 관계를 유도해",
53
+ "네트워크에서 TCP의 혼잡 제어 알고리즘을 수학적으로 모델링하고, 공평성을 분석해",
54
+ "양자컴퓨팅에서 쇼어 알고리즘의 양자 푸리에 변환을 전개하고, 주기 찾기의 정확도를 분석해",
55
+ "블록체인에서 비잔틴 장군 문제의 합의 조건을 형식화하고, PBFT의 안전성을 증명해",
56
+ "로봇공학에서 역기구학의 해를 뉴턴-랩슨 방법으로 구하고, 특이점 분석을 전개해",
57
+ "영상처리에서 허프 변환의 수학적 원리를 유도하고, 원 검출의 정확도를 분석해",
58
+ "음성신호처리에서 LPC 분석을 유도하고, 음성 합성 필터의 안정성을 증명해",
59
+ "계산복잡도에서 P=NP 문제의 NP-완전성 증명 예시로 SAT 문제를 사용하고, 쿡 정리를 전개해",
60
+ "형식언어 이론에서 푸싱다운 오토마타와 CFG의 동치성을 증명하고, 결정화 가능 조건을 분석해",
61
+ "추상대수학에서 유한체 GF(2^8)의 곱셈 구조를 분석하고, AES에서 사용하는 기약 다항식을 유도해",
62
+ "대수기하학에서 베주 정리를 증명하고, 힐베르트 영점定理의 기하학적 의미를 설명해",
63
+ "콕세터 군의 반사 표현을 유도하고, 와일 군의 분류를 전개해",
64
+ "호모토피 이론에서 기본군을 CW 복합체로 계산하고, 판-트리콥토의 기본군을 구해",
65
+ "K-이론에서 벡터 다발의 휘트니 합과 텐서곱을 정의하고, 복소 투영 공간의 K-군을 계산해",
66
+ "호모로지 대수학에서 Ext와 Tor 함자를 유도하고, 유니버설 계수 정리를 증명해",
67
+ "모형론에서 괘델 완전성定理을 증명하고, 로벤하임-스콜렘定理의 철학적 함의를 설명해",
68
+ "집합론에서 선택 공리와 초른 보조정리의 동치성을 증명하고, 바나흐-타르스키 역설을 전개해",
69
+ "수리논리에서 Church-Turing thesis를 형식화하고, 정지 문제의 비결정성을 증명해",
70
+ ]
71
+
72
+ PROMPT = "대학원 레벨로 3차원 토러스 섹셔널 커버쳐 사용해서 오일러 넘버 산출 전개 과정 전개해"
73
+
74
+
75
+ @dataclass
76
+ class Req:
77
+ seq: int
78
+ ok: bool
79
+ latency: float
80
+ provider: str = ""
81
+ model: str = ""
82
+ error: str = ""
83
+ chars: int = 0
84
+
85
+
86
+ @dataclass
87
+ class Stats:
88
+ results: list[Req] = field(default_factory=list)
89
+ lock: threading.Lock = field(default_factory=threading.Lock)
90
+ done: int = 0
91
+
92
+ def add(self, r: Req) -> None:
93
+ with self.lock:
94
+ self.results.append(r)
95
+ self.done += 1
96
+
97
+ @property
98
+ def ok(self) -> list[Req]:
99
+ with self.lock:
100
+ return [r for r in self.results if r.ok]
101
+
102
+ @property
103
+ def fail(self) -> list[Req]:
104
+ with self.lock:
105
+ return [r for r in self.results if not r.ok]
106
+
107
+
108
+ _stop = threading.Event()
109
+
110
+
111
+ def _send_one(seq: int, stats: Stats) -> None:
112
+ if _stop.is_set():
113
+ return
114
+ prompt = PROMPTS[seq % len(PROMPTS)]
115
+ t0 = time.monotonic()
116
+ try:
117
+ result = _route(
118
+ messages=[{"role": "user", "content": prompt}],
119
+ max_tokens=60000,
120
+ max_wait=80.0,
121
+ )
122
+ lat = time.monotonic() - t0
123
+ if "error" in result:
124
+ req = Req(seq=seq, ok=False, latency=lat,
125
+ provider=result.get("provider", ""),
126
+ model=result.get("model", ""),
127
+ error=result["error"][:120])
128
+ else:
129
+ req = Req(seq=seq, ok=True, latency=lat,
130
+ provider=result["provider"],
131
+ model=result["model"],
132
+ chars=len(result.get("content", "")))
133
+ except Exception as exc:
134
+ lat = time.monotonic() - t0
135
+ req = Req(seq=seq, ok=False, latency=lat, error=str(exc)[:120])
136
+
137
+ stats.add(req)
138
+ mark = "OK" if req.ok else "FAIL"
139
+ print(f"[{req.seq:02d}] {mark} {req.latency:5.2f}s "
140
+ f"provider={req.provider}/{req.model} "
141
+ f"ok={len(stats.ok)}/{stats.done}", flush=True)
142
+
143
+
144
+ def run() -> tuple[Stats, float]:
145
+ stats = Stats()
146
+ wall_start = time.monotonic()
147
+
148
+ threads: list[threading.Thread] = []
149
+ for i in range(TOTAL):
150
+ t = threading.Thread(target=_send_one, args=(i, stats), daemon=True)
151
+ t.start()
152
+ threads.append(t)
153
+
154
+ deadline = wall_start + TIMEOUT
155
+ while time.monotonic() < deadline:
156
+ if stats.done >= TOTAL:
157
+ break
158
+ time.sleep(0.2)
159
+
160
+ _stop.set()
161
+ wall_time = time.monotonic() - wall_start
162
+ print(f"\n--- wall {wall_time:.1f}s, completed {stats.done}/{TOTAL} ---", flush=True)
163
+ return stats, wall_time
164
+
165
+
166
+ def report(stats: Stats, wall_time: float) -> None:
167
+ ok = stats.ok
168
+ fail = stats.fail
169
+ lats = [r.latency for r in ok]
170
+ rpm = len(ok) / (wall_time / 60) if wall_time > 0 else 0
171
+
172
+ print("\n" + "=" * 60)
173
+ print("BENCHMARK REPORT")
174
+ print("=" * 60)
175
+ print(f"Sent: {stats.done}")
176
+ print(f"OK: {len(ok)}")
177
+ print(f"Fail: {len(fail)}")
178
+ print(f"Wall time: {wall_time:.1f}s")
179
+ print(f"Throughput: {rpm:.1f} ok/min")
180
+
181
+ if lats:
182
+ sl = sorted(lats)
183
+ p50 = sl[len(sl) // 2]
184
+ p95 = sl[min(int(len(sl) * 0.95), len(sl) - 1)]
185
+ print(f"Latency min: {min(lats):.2f}s")
186
+ print(f"Latency max: {max(lats):.2f}s")
187
+ print(f"Latency avg: {statistics.mean(lats):.2f}s")
188
+ print(f"Latency p50: {p50:.2f}s")
189
+ print(f"Latency p95: {p95:.2f}s")
190
+ if len(lats) >= 2:
191
+ print(f"Latency std: {statistics.stdev(lats):.2f}s")
192
+
193
+ by_prov: dict[str, dict[str, int]] = {}
194
+ for r in stats.results:
195
+ k = f"{r.provider}/{r.model}" if r.provider else "unknown"
196
+ by_prov.setdefault(k, {"ok": 0, "fail": 0})
197
+ if r.ok:
198
+ by_prov[k]["ok"] += 1
199
+ else:
200
+ by_prov[k]["fail"] += 1
201
+
202
+ if by_prov:
203
+ print("\nPer-provider:")
204
+ for k, v in sorted(by_prov.items()):
205
+ print(f" {k}: ok={v['ok']} fail={v['fail']}")
206
+
207
+ if fail:
208
+ print("\nErrors (sample):")
209
+ seen: set[str] = set()
210
+ for r in fail:
211
+ if r.error not in seen:
212
+ seen.add(r.error)
213
+ print(f" - {r.error[:100]}")
214
+
215
+ md = [
216
+ "# Benchmark Report",
217
+ "",
218
+ f"**Date**: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')} ",
219
+ f"**Wall time**: {wall_time:.1f}s ",
220
+ f"**Timeout**: {TIMEOUT}s ",
221
+ f"**Mode**: parallel (threads) ",
222
+ "",
223
+ "## Summary",
224
+ "",
225
+ "| Metric | Value |",
226
+ "|--------|-------|",
227
+ f"| Sent | {stats.done} |",
228
+ f"| OK | {len(ok)} |",
229
+ f"| Fail | {len(fail)} |",
230
+ f"| Throughput (ok/min) | **{rpm:.1f}** |",
231
+ "",
232
+ ]
233
+ if lats:
234
+ md += [
235
+ "## Latency",
236
+ "",
237
+ "| Metric | Value |",
238
+ "|--------|-------|",
239
+ f"| Min | {min(lats):.2f}s |",
240
+ f"| Max | {max(lats):.2f}s |",
241
+ f"| Mean | {statistics.mean(lats):.2f}s |",
242
+ f"| p50 | {p50:.2f}s |",
243
+ f"| p95 | {p95:.2f}s |",
244
+ "",
245
+ ]
246
+ if by_prov:
247
+ md += [
248
+ "## Per-provider",
249
+ "",
250
+ "| Provider/Model | OK | Fail |",
251
+ "|----------------|----|------|",
252
+ ]
253
+ for k, v in sorted(by_prov.items()):
254
+ md.append(f"| {k} | {v['ok']} | {v['fail']} |")
255
+ md.append("")
256
+
257
+ if fail:
258
+ md += ["## Errors", ""]
259
+ seen: set[str] = set()
260
+ for r in fail:
261
+ if r.error not in seen:
262
+ seen.add(r.error)
263
+ md.append(f"- `{r.error[:100]}`")
264
+ md.append("")
265
+
266
+ out = Path(__file__).parent.parent / "tests" / "benchmark_report.md"
267
+ out.parent.mkdir(parents=True, exist_ok=True)
268
+ out.write_text("\n".join(md), encoding="utf-8")
269
+ print(f"\nReport: {out}")
270
+
271
+
272
+ if __name__ == "__main__":
273
+ stats, wall_time = run()
274
+ report(stats, wall_time)
275
+ sys.exit(0 if stats.ok else 1)
myllm/py.typed ADDED
File without changes
myllm/server.py ADDED
@@ -0,0 +1,253 @@
1
+ """FastAPI server — provider routing, fallback, RPM/RPD tracking.
2
+
3
+ Provides a /chat/completions endpoint that routes requests through
4
+ multiple AI providers with automatic fallback, rate limiting, and
5
+ cooldown tracking.
6
+
7
+ Endpoints:
8
+ POST /chat/completions: Chat completion with provider fallback.
9
+ GET /health: Health check.
10
+ GET /tracker: Provider rate limit and cooldown status.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import threading
16
+ import time
17
+ from typing import Any
18
+
19
+ import uvicorn
20
+ from fastapi import FastAPI
21
+ from pydantic import BaseModel
22
+ from dotenv import load_dotenv
23
+ from pathlib import Path
24
+
25
+ load_dotenv(Path(__file__).parent.parent / ".env")
26
+
27
+ from openai import OpenAI, APIStatusError, APITimeoutError, APIConnectionError
28
+
29
+ from .config import PROVIDER_CHAIN, PROVIDER_MAP, Provider
30
+ from .tracker import tracker
31
+
32
+ app = FastAPI(title="myllm server")
33
+ _claim_lock = threading.Lock()
34
+
35
+
36
+ def _make_client(provider: Provider) -> OpenAI | None:
37
+ """Create an OpenAI client for the given provider.
38
+
39
+ Args:
40
+ provider: Provider configuration with base_url and api_key_env.
41
+
42
+ Returns:
43
+ OpenAI client instance, or None if API key is not set.
44
+ """
45
+ api_key = os.environ.get(provider.api_key_env, "")
46
+ if not api_key:
47
+ return None
48
+ return OpenAI(base_url=provider.base_url, api_key=api_key, timeout=30.0)
49
+
50
+
51
+ RETRY_INTERVAL = 20.0
52
+ DEFAULT_MAX_WAIT = 80.0
53
+
54
+
55
+ def _claim_provider(model: str | None = None) -> Provider | None:
56
+ """Claim an available provider from the rate-limited chain.
57
+
58
+ Args:
59
+ model: Unused, reserved for future model-based routing.
60
+
61
+ Returns:
62
+ Available Provider, or None if all are at capacity.
63
+ """
64
+ with _claim_lock:
65
+ for p in PROVIDER_CHAIN:
66
+ if tracker.is_available(p.name, p.rpm, p.rpd):
67
+ tracker.record_claim(p.name)
68
+ return p
69
+ return None
70
+
71
+
72
+ def _try_api_call(
73
+ provider: Provider,
74
+ messages: list[dict[str, Any]],
75
+ *,
76
+ model: str | None = None,
77
+ temperature: float | None = None,
78
+ max_tokens: int | None = None,
79
+ timeout: float | None = None,
80
+ **kwargs: Any,
81
+ ) -> dict[str, Any]:
82
+ client = _make_client(provider)
83
+ if client is None:
84
+ raise RuntimeError(f"No API key for {provider.name}")
85
+
86
+ m = model or provider.model
87
+ params: dict[str, Any] = {
88
+ "model": m,
89
+ "messages": messages,
90
+ **kwargs,
91
+ }
92
+ if temperature is not None:
93
+ params["temperature"] = temperature
94
+ if max_tokens is not None:
95
+ params["max_tokens"] = max_tokens
96
+ params.setdefault("timeout", timeout or 30.0)
97
+
98
+ t0 = time.monotonic()
99
+ resp = client.chat.completions.create(**params)
100
+ elapsed = time.monotonic() - t0
101
+
102
+ tracker.record_success(provider.name)
103
+ msg = resp.choices[0].message
104
+ content = msg.content or ""
105
+ if not content:
106
+ reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
107
+ if reasoning:
108
+ content = reasoning
109
+
110
+ return {
111
+ "content": content,
112
+ "provider": provider.name,
113
+ "model": m,
114
+ "latency": elapsed,
115
+ }
116
+
117
+
118
+ def _route(
119
+ messages: list[dict[str, Any]],
120
+ *,
121
+ model: str | None = None,
122
+ temperature: float | None = None,
123
+ max_tokens: int | None = None,
124
+ timeout: float | None = None,
125
+ debug_provider: str | None = None,
126
+ max_wait: float = DEFAULT_MAX_WAIT,
127
+ **kwargs: Any,
128
+ ) -> dict[str, Any]:
129
+ errors: list[dict[str, Any]] = []
130
+
131
+ if debug_provider:
132
+ provider = PROVIDER_MAP.get(debug_provider)
133
+ if provider is None:
134
+ return {"error": f"Unknown provider: {debug_provider}"}
135
+ try:
136
+ return _try_api_call(
137
+ provider, messages, model=model, temperature=temperature,
138
+ max_tokens=max_tokens, timeout=timeout, **kwargs,
139
+ )
140
+ except Exception as exc:
141
+ return {"error": str(exc)[:200], "attempts": []}
142
+
143
+ deadline = time.monotonic() + max_wait
144
+ while True:
145
+ remaining = deadline - time.monotonic()
146
+ if remaining <= 0:
147
+ break
148
+
149
+ provider = _claim_provider()
150
+ if provider is None:
151
+ wait = min(RETRY_INTERVAL, remaining)
152
+ time.sleep(wait)
153
+ continue
154
+
155
+ try:
156
+ result = _try_api_call(
157
+ provider, messages, model=model, temperature=temperature,
158
+ max_tokens=max_tokens, timeout=timeout, **kwargs,
159
+ )
160
+ result["attempts"] = errors
161
+ return result
162
+ except (APIStatusError, APITimeoutError, APIConnectionError, Exception) as exc:
163
+ tracker.record_failure(provider.name)
164
+ errors.append({
165
+ "provider": provider.name,
166
+ "model": model or provider.model,
167
+ "error": str(exc)[:200],
168
+ })
169
+ continue
170
+
171
+ return {"error": "All providers failed", "attempts": errors}
172
+
173
+
174
+ class ChatRequest(BaseModel):
175
+ """Request schema for /chat/completions endpoint.
176
+
177
+ Attributes:
178
+ messages: List of chat messages.
179
+ model: Model identifier override.
180
+ temperature: Sampling temperature (0.0-2.0).
181
+ max_tokens: Maximum tokens to generate.
182
+ timeout: Per-request timeout in seconds.
183
+ debug_provider: Force a specific provider name.
184
+ max_wait: Max wait time for provider availability.
185
+ """
186
+
187
+ messages: list[dict[str, Any]]
188
+ model: str | None = None
189
+ temperature: float | None = None
190
+ max_tokens: int | None = None
191
+ timeout: float | None = None
192
+ debug_provider: str | None = None
193
+ max_wait: float = DEFAULT_MAX_WAIT
194
+
195
+
196
+ @app.post("/chat/completions")
197
+ def chat_completions(req: ChatRequest) -> dict[str, Any]:
198
+ """Chat completion endpoint with multi-provider fallback.
199
+
200
+ Routes the request through available providers in priority order.
201
+ Automatically falls back to the next provider on failure.
202
+
203
+ Args:
204
+ req: ChatRequest with messages and optional parameters.
205
+
206
+ Returns:
207
+ Dict with content, provider, model, latency, and attempts.
208
+ """
209
+ return _route(
210
+ messages=req.messages,
211
+ model=req.model,
212
+ temperature=req.temperature,
213
+ max_tokens=req.max_tokens,
214
+ timeout=req.timeout,
215
+ debug_provider=req.debug_provider,
216
+ max_wait=req.max_wait,
217
+ )
218
+
219
+
220
+ @app.get("/health")
221
+ def health() -> dict[str, Any]:
222
+ """Health check endpoint.
223
+
224
+ Returns:
225
+ Dict with status "ok".
226
+ """
227
+ return {"status": "ok"}
228
+
229
+
230
+ @app.get("/tracker")
231
+ def tracker_status() -> dict[str, Any]:
232
+ """Provider rate limit and cooldown status.
233
+
234
+ Returns:
235
+ Dict mapping provider names to their current RPM, RPD,
236
+ fail count, and cooldown status.
237
+ """
238
+ return tracker.snapshot()
239
+
240
+
241
+ def main() -> None:
242
+ import argparse
243
+
244
+ parser = argparse.ArgumentParser(description="myllm API server")
245
+ parser.add_argument("--host", default="0.0.0.0")
246
+ parser.add_argument("--port", type=int, default=10000)
247
+ args = parser.parse_args()
248
+
249
+ uvicorn.run(app, host=args.host, port=args.port)
250
+
251
+
252
+ if __name__ == "__main__":
253
+ main()
myllm/test.py ADDED
@@ -0,0 +1,227 @@
1
+ """Benchmark: 60 requests in parallel, 1-minute wall-clock timeout.
2
+
3
+ Usage:
4
+ uv run python -m myllm.test # via server (RPM-controlled)
5
+ uv run python -m myllm.test --direct # direct provider calls
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import os
11
+ import statistics
12
+ import sys
13
+ import threading
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ from dotenv import load_dotenv
20
+
21
+ load_dotenv(Path(__file__).parent.parent / ".env")
22
+
23
+ sys.stdout.reconfigure(line_buffering=True)
24
+
25
+ from myllm import MyLLM
26
+
27
+ TOTAL = 60
28
+ TIMEOUT = 60.0
29
+ PER_REQ_TIMEOUT = 30.0
30
+ DEFAULT_SERVER = "http://localhost:10000"
31
+
32
+ PROMPT = (
33
+ "What is the Gaussian curvature of a torus with major radius R=3 and minor radius r=1 "
34
+ "at the point where theta=pi/4? Show the formula and numerical result in 3 lines."
35
+ )
36
+
37
+
38
+ @dataclass
39
+ class Req:
40
+ seq: int
41
+ ok: bool
42
+ latency: float
43
+ provider: str = ""
44
+ model: str = ""
45
+ error: str = ""
46
+ chars: int = 0
47
+
48
+
49
+ @dataclass
50
+ class Stats:
51
+ results: list[Req] = field(default_factory=list)
52
+ lock: threading.Lock = field(default_factory=threading.Lock)
53
+ done: int = 0
54
+
55
+ def add(self, r: Req) -> None:
56
+ with self.lock:
57
+ self.results.append(r)
58
+ self.done += 1
59
+
60
+ @property
61
+ def ok(self) -> list[Req]:
62
+ with self.lock:
63
+ return [r for r in self.results if r.ok]
64
+
65
+ @property
66
+ def fail(self) -> list[Req]:
67
+ with self.lock:
68
+ return [r for r in self.results if not r.ok]
69
+
70
+
71
+ _stop = threading.Event()
72
+
73
+
74
+ def _send_one(llm: MyLLM, seq: int, stats: Stats) -> None:
75
+ if _stop.is_set():
76
+ return
77
+ t0 = time.monotonic()
78
+ try:
79
+ result = llm.complete(PROMPT)
80
+ lat = time.monotonic() - t0
81
+ req = Req(seq=seq, ok=True, latency=lat,
82
+ provider=result.provider, model=result.model,
83
+ chars=len(result.content))
84
+ except Exception as exc:
85
+ lat = time.monotonic() - t0
86
+ req = Req(seq=seq, ok=False, latency=lat, error=str(exc)[:120])
87
+
88
+ stats.add(req)
89
+ mark = "OK" if req.ok else "FAIL"
90
+ print(f"[{req.seq:02d}] {mark} {req.latency:5.2f}s "
91
+ f"provider={req.provider}/{req.model} "
92
+ f"ok={len(stats.ok)}/{stats.done}", flush=True)
93
+
94
+
95
+ def run(server_url: str | None = None) -> tuple[Stats, float]:
96
+ llm = MyLLM(timeout=PER_REQ_TIMEOUT, server_url=server_url)
97
+ stats = Stats()
98
+ wall_start = time.monotonic()
99
+
100
+ threads: list[threading.Thread] = []
101
+ for i in range(TOTAL):
102
+ t = threading.Thread(target=_send_one, args=(llm, i, stats), daemon=True)
103
+ t.start()
104
+ threads.append(t)
105
+
106
+ deadline = wall_start + TIMEOUT
107
+ while time.monotonic() < deadline:
108
+ if stats.done >= TOTAL:
109
+ break
110
+ time.sleep(0.2)
111
+
112
+ _stop.set()
113
+ wall_time = time.monotonic() - wall_start
114
+ print(f"\n--- wall {wall_time:.1f}s, completed {stats.done}/{TOTAL} ---", flush=True)
115
+ return stats, wall_time
116
+
117
+
118
+ def report(stats: Stats, wall_time: float) -> None:
119
+ ok = stats.ok
120
+ fail = stats.fail
121
+ lats = [r.latency for r in ok]
122
+ rpm = len(ok) / (wall_time / 60) if wall_time > 0 else 0
123
+
124
+ print("\n" + "=" * 60)
125
+ print("BENCHMARK REPORT")
126
+ print("=" * 60)
127
+ print(f"Sent: {stats.done}")
128
+ print(f"OK: {len(ok)}")
129
+ print(f"Fail: {len(fail)}")
130
+ print(f"Wall time: {wall_time:.1f}s")
131
+ print(f"Throughput: {rpm:.1f} ok/min")
132
+
133
+ if lats:
134
+ sl = sorted(lats)
135
+ p50 = sl[len(sl) // 2]
136
+ p95 = sl[min(int(len(sl) * 0.95), len(sl) - 1)]
137
+ print(f"Latency min: {min(lats):.2f}s")
138
+ print(f"Latency max: {max(lats):.2f}s")
139
+ print(f"Latency avg: {statistics.mean(lats):.2f}s")
140
+ print(f"Latency p50: {p50:.2f}s")
141
+ print(f"Latency p95: {p95:.2f}s")
142
+ if len(lats) >= 2:
143
+ print(f"Latency std: {statistics.stdev(lats):.2f}s")
144
+
145
+ by_prov: dict[str, dict[str, int]] = {}
146
+ for r in stats.results:
147
+ k = f"{r.provider}/{r.model}" if r.provider else "unknown"
148
+ by_prov.setdefault(k, {"ok": 0, "fail": 0})
149
+ if r.ok:
150
+ by_prov[k]["ok"] += 1
151
+ else:
152
+ by_prov[k]["fail"] += 1
153
+
154
+ if by_prov:
155
+ print("\nPer-provider:")
156
+ for k, v in sorted(by_prov.items()):
157
+ print(f" {k}: ok={v['ok']} fail={v['fail']}")
158
+
159
+ if fail:
160
+ print("\nErrors (sample):")
161
+ seen: set[str] = set()
162
+ for r in fail:
163
+ if r.error not in seen:
164
+ seen.add(r.error)
165
+ print(f" - {r.error[:100]}")
166
+
167
+ md = [
168
+ "# Benchmark Report",
169
+ "",
170
+ f"**Date**: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')} ",
171
+ f"**Wall time**: {wall_time:.1f}s ",
172
+ f"**Per-request timeout**: {PER_REQ_TIMEOUT}s ",
173
+ f"**Mode**: parallel (threads) ",
174
+ "",
175
+ "## Summary",
176
+ "",
177
+ "| Metric | Value |",
178
+ "|--------|-------|",
179
+ f"| Sent | {stats.done} |",
180
+ f"| OK | {len(ok)} |",
181
+ f"| Fail | {len(fail)} |",
182
+ f"| Throughput (ok/min) | **{rpm:.1f}** |",
183
+ "",
184
+ ]
185
+ if lats:
186
+ md += [
187
+ "## Latency",
188
+ "",
189
+ "| Metric | Value |",
190
+ "|--------|-------|",
191
+ f"| Min | {min(lats):.2f}s |",
192
+ f"| Max | {max(lats):.2f}s |",
193
+ f"| Mean | {statistics.mean(lats):.2f}s |",
194
+ f"| p50 | {p50:.2f}s |",
195
+ f"| p95 | {p95:.2f}s |",
196
+ "",
197
+ ]
198
+ if by_prov:
199
+ md += [
200
+ "## Per-provider",
201
+ "",
202
+ "| Provider/Model | OK | Fail |",
203
+ "|----------------|----|------|",
204
+ ]
205
+ for k, v in sorted(by_prov.items()):
206
+ md.append(f"| {k} | {v['ok']} | {v['fail']} |")
207
+ md.append("")
208
+
209
+ out = Path(__file__).parent.parent / "tests" / "benchmark_report.md"
210
+ out.parent.mkdir(parents=True, exist_ok=True)
211
+ out.write_text("\n".join(md), encoding="utf-8")
212
+ print(f"\nReport: {out}")
213
+
214
+
215
+ if __name__ == "__main__":
216
+ parser = argparse.ArgumentParser()
217
+ parser.add_argument("--direct", action="store_true", help="Call providers directly (no server)")
218
+ parser.add_argument("--server", default=DEFAULT_SERVER, help="Server URL")
219
+ args = parser.parse_args()
220
+
221
+ server_url = None if args.direct else args.server
222
+ mode = "direct" if args.direct else f"server ({server_url})"
223
+ print(f"Mode: {mode}", flush=True)
224
+
225
+ stats, wall_time = run(server_url)
226
+ report(stats, wall_time)
227
+ os._exit(0 if stats.ok else 1)
myllm/tracker.py ADDED
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections import defaultdict, deque
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class _ProviderState:
10
+ """Internal state for tracking a single provider's rate limits."""
11
+
12
+ rpm_window: deque[float] = field(default_factory=lambda: deque())
13
+ rpd_window: deque[float] = field(default_factory=lambda: deque())
14
+ fail_count: int = 0
15
+ cooldown_until: float = 0.0
16
+
17
+
18
+ class Tracker:
19
+ """Rate limit and cooldown tracker for AI providers.
20
+
21
+ Tracks requests per minute (RPM) and requests per day (RPD) for
22
+ each provider. Implements exponential backoff on failures.
23
+
24
+ Example:
25
+ >>> tracker = Tracker()
26
+ >>> tracker.is_available("groq", rpm=30, rpd=1000)
27
+ True
28
+ >>> tracker.record_claim("groq")
29
+ >>> tracker.record_success("groq")
30
+ >>> tracker.snapshot()
31
+ {'groq': {'rpm': 1, 'rpd': 1, 'fail_count': 0, 'cooldown_left': 0}}
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ self._states: dict[str, _ProviderState] = defaultdict(_ProviderState)
36
+
37
+ def _prune(self, state: _ProviderState, now: float) -> None:
38
+ while state.rpm_window and now - state.rpm_window[0] > 60:
39
+ state.rpm_window.popleft()
40
+ while state.rpd_window and now - state.rpd_window[0] > 86400:
41
+ state.rpd_window.popleft()
42
+
43
+ def _is_cooling_down(self, state: _ProviderState, now: float) -> bool:
44
+ return now < state.cooldown_until
45
+
46
+ def _all_others_cooling(self, provider: str, now: float) -> bool:
47
+ for name, state in self._states.items():
48
+ if name == provider:
49
+ continue
50
+ if not self._is_cooling_down(state, now):
51
+ return False
52
+ return bool(self._states)
53
+
54
+ def is_available(self, provider: str, rpm: int = 0, rpd: int = 0) -> bool:
55
+ """Check if a provider is available for a new request.
56
+
57
+ Args:
58
+ provider: Provider name.
59
+ rpm: Requests per minute limit.
60
+ rpd: Requests per day limit.
61
+
62
+ Returns:
63
+ True if the provider can accept a new request.
64
+ """
65
+ state = self._states[provider]
66
+ now = time.monotonic()
67
+
68
+ # nvidia_nim: always available when all others are cooling down
69
+ if provider == "nvidia_nim" and self._all_others_cooling(provider, now):
70
+ return True
71
+
72
+ if self._is_cooling_down(state, now):
73
+ return False
74
+
75
+ if rpm <= 0 and rpd <= 0:
76
+ return True
77
+
78
+ self._prune(state, now)
79
+ if rpm > 0 and len(state.rpm_window) >= rpm:
80
+ return False
81
+ if rpd > 0 and len(state.rpd_window) >= rpd:
82
+ return False
83
+ return True
84
+
85
+ def record_claim(self, provider: str) -> None:
86
+ """Record a request claim for rate limiting.
87
+
88
+ Args:
89
+ provider: Provider name.
90
+ """
91
+ now = time.monotonic()
92
+ state = self._states[provider]
93
+ state.rpm_window.append(now)
94
+ state.rpd_window.append(now)
95
+
96
+ def record_success(self, provider: str) -> None:
97
+ """Record a successful request, resetting failure count.
98
+
99
+ Args:
100
+ provider: Provider name.
101
+ """
102
+ state = self._states[provider]
103
+ state.fail_count = 0
104
+ state.cooldown_until = 0.0
105
+
106
+ def record_failure(self, provider: str) -> None:
107
+ """Record a failed request with exponential backoff.
108
+
109
+ Args:
110
+ provider: Provider name.
111
+ """
112
+ state = self._states[provider]
113
+ state.fail_count += 1
114
+ delay = min(120, 2 ** (state.fail_count - 1))
115
+ state.cooldown_until = time.monotonic() + delay
116
+
117
+ def snapshot(self) -> dict[str, dict]:
118
+ """Get current status of all tracked providers.
119
+
120
+ Returns:
121
+ Dict mapping provider names to their status including
122
+ RPM, RPD, fail count, and cooldown remaining.
123
+ """
124
+ now = time.monotonic()
125
+ out: dict[str, dict] = {}
126
+ for name, state in self._states.items():
127
+ self._prune(state, now)
128
+ cooling = self._is_cooling_down(state, now)
129
+ out[name] = {
130
+ "rpm": len(state.rpm_window),
131
+ "rpd": len(state.rpd_window),
132
+ "fail_count": state.fail_count,
133
+ "cooldown_left": max(0, state.cooldown_until - now) if cooling else 0,
134
+ }
135
+ return out
136
+
137
+
138
+ tracker = Tracker()
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: simplyllm
3
+ Version: 0.1.0
4
+ Summary: Free AI LLM client with multi-provider fallback and rate limiting
5
+ Project-URL: Repository, https://github.com/freeai/myllm
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: fastapi>=0.100.0
9
+ Requires-Dist: openai>=1.0.0
10
+ Requires-Dist: python-dotenv>=1.0.0
11
+ Requires-Dist: uvicorn>=0.20.0
@@ -0,0 +1,15 @@
1
+ myllm/__init__.py,sha256=p1RgiyY6bm5wO0KVaKaMUylYF5xhbOAJAbxweMfayfY,815
2
+ myllm/__main__.py,sha256=3dYKHfmWsrdExFlTFlcR5a_icR9fAkn06Yh14TQkEd8,33
3
+ myllm/client.py,sha256=Vx7k71qdbzVVhb2J2d65-tdC0nVqdARC1Jnr09VEaEU,5077
4
+ myllm/config.py,sha256=BsK2_H21xK3i-YjozVdjTT0XDn9L-cWzgm3IJ7WL8pc,1701
5
+ myllm/diagnose.py,sha256=haVha9EP91lzJrkQoZI0b2p8N8TR_ut_LDb63vb-4Sg,800
6
+ myllm/lib.py,sha256=67pEgWqYpuFov_XT4rmziugn8sK4e-EsAwPe8Fdlxcs,165
7
+ myllm/plain_execute.py,sha256=hqWQdieLkJfxrCK0R4-tCznq4w8Lc6wCTwSuf4_NcW4,12966
8
+ myllm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ myllm/server.py,sha256=22Uzaq8eOkawiSMlVbNl54ac5K0MRVHjaiu-faXQW2A,7152
10
+ myllm/test.py,sha256=uL5AsF0rDbY-1KW8VzvqeohcaGk94NVgzQfJqIgJJPk,6637
11
+ myllm/tracker.py,sha256=wyn-rMClHRC0KgJPLOa-RxLtt6_4LkFA4OJRHgehXZU,4445
12
+ simplyllm-0.1.0.dist-info/METADATA,sha256=BG66rB4QH-wI16bGMssBA0VUtWxe5SJa9q1g5L3VrDY,360
13
+ simplyllm-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
14
+ simplyllm-0.1.0.dist-info/entry_points.txt,sha256=qbDfb0GPF43wm5ioGV-wTRsUyYzG5iqYt13e2uKjprQ,44
15
+ simplyllm-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ myllm = myllm.server:main