simplyllm 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simplyllm-0.1.0/.env.example +4 -0
- simplyllm-0.1.0/.gitignore +9 -0
- simplyllm-0.1.0/.python-version +1 -0
- simplyllm-0.1.0/Makefile +66 -0
- simplyllm-0.1.0/PKG-INFO +11 -0
- simplyllm-0.1.0/README.md +0 -0
- simplyllm-0.1.0/logs/server.log +4 -0
- simplyllm-0.1.0/myllm/__init__.py +30 -0
- simplyllm-0.1.0/myllm/__main__.py +3 -0
- simplyllm-0.1.0/myllm/client.py +156 -0
- simplyllm-0.1.0/myllm/config.py +67 -0
- simplyllm-0.1.0/myllm/diagnose.py +30 -0
- simplyllm-0.1.0/myllm/lib.py +10 -0
- simplyllm-0.1.0/myllm/plain_execute.py +275 -0
- simplyllm-0.1.0/myllm/py.typed +0 -0
- simplyllm-0.1.0/myllm/server.py +253 -0
- simplyllm-0.1.0/myllm/test.py +227 -0
- simplyllm-0.1.0/myllm/tracker.py +138 -0
- simplyllm-0.1.0/pyproject.toml +31 -0
- simplyllm-0.1.0/tests/test_providers.py +48 -0
- simplyllm-0.1.0/uv.lock +804 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
simplyllm-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
SESSION = myllm
|
|
2
|
+
PORT ?= 10000
|
|
3
|
+
LOG = logs/server.log
|
|
4
|
+
|
|
5
|
+
.DEFAULT_GOAL := help
|
|
6
|
+
|
|
7
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
.PHONY: up down restart logs attach status install test build publish help
|
|
10
|
+
|
|
11
|
+
up: ## Start API server in a background tmux session
|
|
12
|
+
@mkdir -p logs
|
|
13
|
+
@if tmux has-session -t $(SESSION) 2>/dev/null; then \
|
|
14
|
+
echo "Already running. Use 'make restart' to restart, 'make attach' to view."; \
|
|
15
|
+
else \
|
|
16
|
+
tmux new-session -d -s $(SESSION) \
|
|
17
|
+
"uv run python -m myllm --host 0.0.0.0 --port $(PORT) 2>&1 | tee $(LOG)"; \
|
|
18
|
+
echo "Server started on :$(PORT)"; \
|
|
19
|
+
echo " log: make logs"; \
|
|
20
|
+
echo " attach: make attach"; \
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
down: ## Stop API server
|
|
24
|
+
@tmux kill-session -t $(SESSION) 2>/dev/null \
|
|
25
|
+
&& echo "Server stopped." \
|
|
26
|
+
|| echo "Server was not running."
|
|
27
|
+
|
|
28
|
+
restart: ## Restart API server
|
|
29
|
+
@$(MAKE) --no-print-directory down
|
|
30
|
+
@sleep 1
|
|
31
|
+
@$(MAKE) --no-print-directory up
|
|
32
|
+
|
|
33
|
+
attach: ## Attach to server tmux session (Ctrl-B D to detach)
|
|
34
|
+
tmux attach -t $(SESSION)
|
|
35
|
+
|
|
36
|
+
logs: ## Tail server log file
|
|
37
|
+
@test -f $(LOG) || (echo "No log file yet. Start server with 'make up'."; exit 1)
|
|
38
|
+
tail -f $(LOG)
|
|
39
|
+
|
|
40
|
+
status: ## Show server status + health endpoint
|
|
41
|
+
@echo "── tmux sessions ──────────────────────────"
|
|
42
|
+
@tmux has-session -t $(SESSION) 2>/dev/null \
|
|
43
|
+
&& echo " server : running ($(SESSION))" \
|
|
44
|
+
|| echo " server : stopped"
|
|
45
|
+
@echo "── health check ───────────────────────────"
|
|
46
|
+
@curl -sf http://localhost:$(PORT)/health | python3 -m json.tool 2>/dev/null \
|
|
47
|
+
|| echo " (server not responding on :$(PORT))"
|
|
48
|
+
|
|
49
|
+
install: ## Install / update project in editable mode
|
|
50
|
+
uv tool install --editable .
|
|
51
|
+
|
|
52
|
+
build: ## Build package for PyPI
|
|
53
|
+
uv build
|
|
54
|
+
|
|
55
|
+
publish: build ## Build and publish to PyPI
|
|
56
|
+
@uv publish --username __token__ --password pypi-AgEIcHlwaS5vcmcCJGEwMDcyYTBkLWIwZjUtNDdhZC1hOGFlLTE3YzEyOGU0MmE5NgACKlszLCI4OWVjNTkyMy1hOWNhLTQ4NDQtYWExNi00MzBiZmNjNGYyMWYiXQAABiCDf68p7OzntEbSAxLcbRmTkFgEVRIVrnashAONbPJEMw
|
|
57
|
+
|
|
58
|
+
test: ## Run benchmark (60 parallel requests via server, 1 min)
|
|
59
|
+
PYTHONUNBUFFERED=1 uv run python -m myllm.test
|
|
60
|
+
|
|
61
|
+
test-direct: ## Run benchmark (direct provider calls, no server)
|
|
62
|
+
PYTHONUNBUFFERED=1 uv run python -m myllm.test --direct
|
|
63
|
+
|
|
64
|
+
help: ## Show this help
|
|
65
|
+
@grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) \
|
|
66
|
+
| awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
|
simplyllm-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
|
File without changes
|
|
@@ -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
|
+
]
|
|
@@ -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)
|
|
@@ -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}
|
|
@@ -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.")
|
|
@@ -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)
|