rosetta-llm 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.
rosetta/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """LLM Proxy — Multi-format bidirectional translation service."""
2
+
3
+ __version__ = "0.1.0"
rosetta/__main__.py ADDED
@@ -0,0 +1,78 @@
1
+ """Entrypoint for `python -m rosetta` and the `rosetta-llm` CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ from pathlib import Path
8
+
9
+ import uvicorn
10
+
11
+ from rosetta.app import create_app
12
+ from rosetta.config import load_config
13
+ from rosetta.observability import setup_logging
14
+
15
+ DEFAULT_CONFIG = str(Path.home() / ".rosetta-llm" / "config.json")
16
+
17
+
18
+ def main() -> None:
19
+ parser = argparse.ArgumentParser(
20
+ prog="rosetta-llm",
21
+ description="Multi-format bidirectional LLM proxy",
22
+ )
23
+ parser.add_argument(
24
+ "--config",
25
+ "-c",
26
+ default=os.environ.get("ROSETTA_CONFIG", DEFAULT_CONFIG),
27
+ help=f"Path to config.json (default: {DEFAULT_CONFIG})",
28
+ )
29
+ parser.add_argument(
30
+ "--host",
31
+ default=None,
32
+ help="Bind address (overrides config)",
33
+ )
34
+ parser.add_argument(
35
+ "--port",
36
+ "-p",
37
+ type=int,
38
+ default=None,
39
+ help="Bind port (overrides config)",
40
+ )
41
+ parser.add_argument(
42
+ "--proxy-headers",
43
+ action=argparse.BooleanOptionalAction,
44
+ default=True,
45
+ help="Trust X-Forwarded-* headers from proxies (default: on)",
46
+ )
47
+ parser.add_argument(
48
+ "--forwarded-allow-ips",
49
+ default="*",
50
+ help="Allowed forwarded IPs (default: *)",
51
+ )
52
+
53
+ args = parser.parse_args()
54
+
55
+ config = load_config(args.config)
56
+ setup_logging(config.log_level)
57
+
58
+ if args.host is not None:
59
+ config.host = args.host
60
+ if args.port is not None:
61
+ config.port = args.port
62
+
63
+ app = create_app(config)
64
+
65
+ uvicorn.run(
66
+ app,
67
+ host=config.host,
68
+ port=config.port,
69
+ log_level=config.log_level,
70
+ access_log=False,
71
+ log_config=None,
72
+ proxy_headers=args.proxy_headers,
73
+ forwarded_allow_ips=args.forwarded_allow_ips,
74
+ )
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
rosetta/app.py ADDED
@@ -0,0 +1,74 @@
1
+ """FastAPI app factory with lifespan management."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ from collections.abc import AsyncIterator
6
+ from contextlib import asynccontextmanager
7
+
8
+ from fastapi import FastAPI
9
+
10
+ from rosetta.auth import AuthMiddleware
11
+ from rosetta.config import Config
12
+ from rosetta.observability import RequestContextMiddleware
13
+ from rosetta.routes.chat_completions import router as chat_router
14
+ from rosetta.routes.health import router as health_router
15
+ from rosetta.routes.messages import router as messages_router
16
+ from rosetta.routes.models import refresh_models
17
+ from rosetta.routes.models import router as models_router
18
+ from rosetta.routes.responses import router as responses_router
19
+ from rosetta.upstream import UpstreamClient
20
+
21
+ _REFRESH_INTERVAL = 300 # seconds
22
+
23
+
24
+ async def _models_refresh_loop(app: FastAPI, interval: float) -> None:
25
+ while True:
26
+ try:
27
+ await asyncio.sleep(interval)
28
+ await refresh_models(app.state.config, app.state.upstream, app.state.model_snapshot)
29
+ except asyncio.CancelledError:
30
+ break
31
+ except Exception:
32
+ from rosetta.observability import get_logger
33
+
34
+ get_logger().exception("models_refresh_loop_error")
35
+
36
+
37
+ @asynccontextmanager
38
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
39
+ app.state.upstream = UpstreamClient(app.state.config.providers)
40
+ await app.state.upstream.start()
41
+
42
+ refresh_task = asyncio.create_task(
43
+ refresh_models(app.state.config, app.state.upstream, app.state.model_snapshot)
44
+ )
45
+ loop_task = asyncio.create_task(_models_refresh_loop(app, _REFRESH_INTERVAL))
46
+
47
+ try:
48
+ yield
49
+ finally:
50
+ loop_task.cancel()
51
+ refresh_task.cancel()
52
+ with contextlib.suppress(asyncio.CancelledError):
53
+ await loop_task
54
+ await refresh_task
55
+ await app.state.upstream.close()
56
+
57
+
58
+ def create_app(config: Config) -> FastAPI:
59
+ app = FastAPI(lifespan=lifespan)
60
+ app.state.config = config
61
+ app.state.provider_status = {}
62
+ app.state.model_snapshot = {}
63
+ app.state.model_refresh_lock = asyncio.Lock()
64
+
65
+ app.add_middleware(AuthMiddleware)
66
+ app.add_middleware(RequestContextMiddleware)
67
+
68
+ app.include_router(health_router)
69
+ app.include_router(messages_router)
70
+ app.include_router(chat_router)
71
+ app.include_router(responses_router)
72
+ app.include_router(models_router)
73
+
74
+ return app
rosetta/asgi.py ADDED
@@ -0,0 +1,18 @@
1
+ """ASGI entrypoint — creates the app at module level so uvicorn can import it.
2
+
3
+ uvicorn rosetta.asgi:app --host 0.0.0.0 --port 7860
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from pathlib import Path
10
+
11
+ from rosetta.app import create_app
12
+ from rosetta.config import load_config
13
+ from rosetta.observability import setup_logging
14
+
15
+ config_path = os.environ.get("ROSETTA_CONFIG", str(Path.home() / ".rosetta-llm" / "config.json"))
16
+ config = load_config(config_path)
17
+ setup_logging(config.log_level)
18
+ app = create_app(config)
rosetta/auth.py ADDED
@@ -0,0 +1,88 @@
1
+ """Bearer-token authentication as a pure ASGI middleware.
2
+
3
+ BaseHTTPMiddleware buffers streaming responses, which would break SSE.
4
+ Implementing as raw ASGI keeps streams flowing chunk-by-chunk.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Awaitable, Callable
10
+ from typing import Any
11
+
12
+ import orjson
13
+
14
+ ASGIApp = Callable[
15
+ [
16
+ dict[str, Any],
17
+ Callable[[], Awaitable[dict[str, Any]]],
18
+ Callable[[dict[str, Any]], Awaitable[None]],
19
+ ],
20
+ Awaitable[None],
21
+ ]
22
+ Receive = Callable[[], Awaitable[dict[str, Any]]]
23
+ Send = Callable[[dict[str, Any]], Awaitable[None]]
24
+
25
+ _PUBLIC_PATHS = frozenset({"/health", "/providers"})
26
+
27
+
28
+ class AuthMiddleware:
29
+ """Strict ASGI middleware that rejects requests lacking a valid bearer token."""
30
+
31
+ def __init__(self, app: ASGIApp) -> None:
32
+ self.app = app
33
+
34
+ async def __call__(self, scope: dict[str, Any], receive: Receive, send: Send) -> None:
35
+ if scope["type"] != "http":
36
+ await self.app(scope, receive, send)
37
+ return
38
+
39
+ config = scope["app"].state.config
40
+ keys = set(config.proxy.api_keys)
41
+ if not keys or scope["path"] in _PUBLIC_PATHS:
42
+ await self.app(scope, receive, send)
43
+ return
44
+
45
+ headers = {k.decode().lower(): v.decode() for k, v in scope.get("headers", [])}
46
+ token = ""
47
+ auth_header = headers.get("authorization", "")
48
+ if auth_header[:7].lower() == "bearer ":
49
+ token = auth_header[7:].strip()
50
+ if not token:
51
+ token = headers.get("x-api-key", "").strip()
52
+
53
+ if token in keys:
54
+ await self.app(scope, receive, send)
55
+ return
56
+
57
+ body = _error_body(scope["path"])
58
+ await send(
59
+ {
60
+ "type": "http.response.start",
61
+ "status": 401,
62
+ "headers": [
63
+ (b"content-type", b"application/json"),
64
+ (b"content-length", str(len(body)).encode()),
65
+ ],
66
+ }
67
+ )
68
+ await send({"type": "http.response.body", "body": body})
69
+
70
+
71
+ def _error_body(path: str) -> bytes:
72
+ if path.startswith("/v1/messages"):
73
+ return orjson.dumps(
74
+ {
75
+ "type": "error",
76
+ "error": {"type": "authentication_error", "message": "Invalid or missing API key"},
77
+ }
78
+ )
79
+ return orjson.dumps(
80
+ {
81
+ "error": {
82
+ "message": "Invalid or missing API key",
83
+ "type": "authentication_error",
84
+ "code": None,
85
+ "param": None,
86
+ },
87
+ }
88
+ )
@@ -0,0 +1 @@
1
+ """Non-streaming parse/render codecs for each API format."""