urouter-sdk 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.
- urouter/__init__.py +51 -0
- urouter/_client.py +83 -0
- urouter/_constants.py +21 -0
- urouter/_exceptions.py +42 -0
- urouter/_http.py +159 -0
- urouter/_types.py +128 -0
- urouter/resources/__init__.py +6 -0
- urouter/resources/chat.py +494 -0
- urouter/resources/models.py +117 -0
- urouter_sdk-0.1.0.dist-info/METADATA +93 -0
- urouter_sdk-0.1.0.dist-info/RECORD +12 -0
- urouter_sdk-0.1.0.dist-info/WHEEL +4 -0
urouter/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from urouter._client import Client, OpenAI, URouter
|
|
4
|
+
from urouter._exceptions import (
|
|
5
|
+
APIConnectionError,
|
|
6
|
+
APIError,
|
|
7
|
+
APITimeoutError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
BadRequestError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
URouterError,
|
|
13
|
+
)
|
|
14
|
+
from urouter._types import (
|
|
15
|
+
ChatCompletion,
|
|
16
|
+
ChatCompletionChunk,
|
|
17
|
+
ChatCompletionMessage,
|
|
18
|
+
Choice,
|
|
19
|
+
ChoiceDelta,
|
|
20
|
+
ChunkChoice,
|
|
21
|
+
CompletionUsage,
|
|
22
|
+
Model,
|
|
23
|
+
ModelList,
|
|
24
|
+
Stream,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = (
|
|
28
|
+
"URouter",
|
|
29
|
+
"OpenAI",
|
|
30
|
+
"Client",
|
|
31
|
+
"URouterError",
|
|
32
|
+
"APIError",
|
|
33
|
+
"AuthenticationError",
|
|
34
|
+
"RateLimitError",
|
|
35
|
+
"BadRequestError",
|
|
36
|
+
"NotFoundError",
|
|
37
|
+
"APIConnectionError",
|
|
38
|
+
"APITimeoutError",
|
|
39
|
+
"ChatCompletion",
|
|
40
|
+
"ChatCompletionChunk",
|
|
41
|
+
"ChatCompletionMessage",
|
|
42
|
+
"Choice",
|
|
43
|
+
"ChoiceDelta",
|
|
44
|
+
"ChunkChoice",
|
|
45
|
+
"CompletionUsage",
|
|
46
|
+
"Model",
|
|
47
|
+
"ModelList",
|
|
48
|
+
"Stream",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|
urouter/_client.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from urouter._constants import API_BASE, CHAT_BASE, DEFAULT_HEADERS, DEFAULT_TIMEOUT
|
|
6
|
+
from urouter._http import HTTP
|
|
7
|
+
from urouter.resources.chat import Chat
|
|
8
|
+
from urouter.resources.models import Models
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class URouter:
|
|
12
|
+
__slots__ = (
|
|
13
|
+
"api_key",
|
|
14
|
+
"base_url",
|
|
15
|
+
"timeout",
|
|
16
|
+
"_http",
|
|
17
|
+
"chat",
|
|
18
|
+
"models",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
api_key: str | None = None,
|
|
24
|
+
*,
|
|
25
|
+
base_url: str | None = None,
|
|
26
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
27
|
+
proxy: str | None = None,
|
|
28
|
+
http2: bool = True,
|
|
29
|
+
default_headers: dict[str, str] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.api_key = api_key
|
|
32
|
+
|
|
33
|
+
self.base_url = base_url or (API_BASE if api_key else CHAT_BASE)
|
|
34
|
+
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
|
|
37
|
+
headers = dict(DEFAULT_HEADERS)
|
|
38
|
+
|
|
39
|
+
if default_headers:
|
|
40
|
+
headers.update(default_headers)
|
|
41
|
+
|
|
42
|
+
self._http = HTTP(
|
|
43
|
+
timeout=timeout,
|
|
44
|
+
headers=headers,
|
|
45
|
+
proxy=proxy,
|
|
46
|
+
http2=http2,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self.chat = Chat(self)
|
|
50
|
+
|
|
51
|
+
self.models = Models(self)
|
|
52
|
+
|
|
53
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
54
|
+
headers: dict[str, str] = {}
|
|
55
|
+
|
|
56
|
+
if self.api_key:
|
|
57
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
58
|
+
|
|
59
|
+
return headers
|
|
60
|
+
|
|
61
|
+
def _chat_headers(self) -> dict[str, str]:
|
|
62
|
+
return {
|
|
63
|
+
"Origin": CHAT_BASE,
|
|
64
|
+
"Referer": f"{CHAT_BASE}/",
|
|
65
|
+
"Accept": "application/json",
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
self._http.close()
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> URouter:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, *args: object) -> None:
|
|
76
|
+
self.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OpenAI(URouter):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
Client = URouter
|
urouter/_constants.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
CHAT_BASE: str = "https://chat.urouter.net"
|
|
4
|
+
|
|
5
|
+
API_BASE: str = "https://api.urouter.net/v1"
|
|
6
|
+
|
|
7
|
+
DEFAULT_TIMEOUT: float = 120.0
|
|
8
|
+
|
|
9
|
+
FREE_PRESETS: dict[str, tuple[str, str]] = {
|
|
10
|
+
"normal": ("deepseek-v4.1-flash", "normal"),
|
|
11
|
+
"censored": ("deepseek-v4.1-flash", "normal"),
|
|
12
|
+
"free": ("deepseek-v4.1-flash", "normal"),
|
|
13
|
+
"uncensored": ("gemma-4-uncensored", "uncensored"),
|
|
14
|
+
"deepseek-v4.1-flash": ("deepseek-v4.1-flash", "normal"),
|
|
15
|
+
"gemma-4-uncensored": ("gemma-4-uncensored", "uncensored"),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
DEFAULT_HEADERS: dict[str, str] = {
|
|
19
|
+
"Accept": "application/json",
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
}
|
urouter/_exceptions.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class URouterError(Exception):
|
|
7
|
+
def __init__(self, message: str, *, status_code: int | None = None, body: Any = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
|
|
10
|
+
self.message = message
|
|
11
|
+
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
|
|
14
|
+
self.body = body
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class APIError(URouterError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthenticationError(APIError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RateLimitError(APIError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BadRequestError(APIError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NotFoundError(APIError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class APIConnectionError(URouterError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class APITimeoutError(URouterError):
|
|
42
|
+
pass
|
urouter/_http.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Iterator
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import orjson
|
|
7
|
+
|
|
8
|
+
from urouter._exceptions import (
|
|
9
|
+
APIConnectionError,
|
|
10
|
+
APIError,
|
|
11
|
+
APITimeoutError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
BadRequestError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTTP:
|
|
20
|
+
__slots__ = ("_client",)
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
timeout: float,
|
|
26
|
+
headers: dict[str, str],
|
|
27
|
+
proxy: str | None = None,
|
|
28
|
+
http2: bool = True,
|
|
29
|
+
) -> None:
|
|
30
|
+
kwargs: dict[str, Any] = {
|
|
31
|
+
"timeout": httpx.Timeout(timeout, connect=10.0),
|
|
32
|
+
"headers": headers,
|
|
33
|
+
"http2": http2,
|
|
34
|
+
"follow_redirects": True,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if proxy:
|
|
38
|
+
kwargs["proxy"] = proxy
|
|
39
|
+
|
|
40
|
+
self._client = httpx.Client(**kwargs)
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
self._client.close()
|
|
44
|
+
|
|
45
|
+
def request(
|
|
46
|
+
self,
|
|
47
|
+
method: str,
|
|
48
|
+
url: str,
|
|
49
|
+
*,
|
|
50
|
+
json_body: Any = None,
|
|
51
|
+
params: dict[str, Any] | None = None,
|
|
52
|
+
headers: dict[str, str] | None = None,
|
|
53
|
+
) -> httpx.Response:
|
|
54
|
+
content = None
|
|
55
|
+
|
|
56
|
+
req_headers = dict(headers or {})
|
|
57
|
+
|
|
58
|
+
if json_body is not None:
|
|
59
|
+
content = orjson.dumps(json_body)
|
|
60
|
+
|
|
61
|
+
req_headers.setdefault("Content-Type", "application/json")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
resp = self._client.request(
|
|
65
|
+
method,
|
|
66
|
+
url,
|
|
67
|
+
content=content,
|
|
68
|
+
params=params,
|
|
69
|
+
headers=req_headers,
|
|
70
|
+
)
|
|
71
|
+
except httpx.TimeoutException as e:
|
|
72
|
+
raise APITimeoutError(str(e)) from e
|
|
73
|
+
except httpx.HTTPError as e:
|
|
74
|
+
raise APIConnectionError(str(e)) from e
|
|
75
|
+
|
|
76
|
+
return resp
|
|
77
|
+
|
|
78
|
+
def stream(
|
|
79
|
+
self,
|
|
80
|
+
method: str,
|
|
81
|
+
url: str,
|
|
82
|
+
*,
|
|
83
|
+
json_body: Any = None,
|
|
84
|
+
headers: dict[str, str] | None = None,
|
|
85
|
+
) -> Iterator[bytes]:
|
|
86
|
+
content = None
|
|
87
|
+
|
|
88
|
+
req_headers = dict(headers or {})
|
|
89
|
+
|
|
90
|
+
if json_body is not None:
|
|
91
|
+
content = orjson.dumps(json_body)
|
|
92
|
+
|
|
93
|
+
req_headers.setdefault("Content-Type", "application/json")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with self._client.stream(
|
|
97
|
+
method,
|
|
98
|
+
url,
|
|
99
|
+
content=content,
|
|
100
|
+
headers=req_headers,
|
|
101
|
+
) as resp:
|
|
102
|
+
if resp.status_code >= 400:
|
|
103
|
+
raw = resp.read()
|
|
104
|
+
|
|
105
|
+
self.raise_for_status(resp.status_code, raw)
|
|
106
|
+
|
|
107
|
+
yield from resp.iter_bytes()
|
|
108
|
+
except httpx.TimeoutException as e:
|
|
109
|
+
raise APITimeoutError(str(e)) from e
|
|
110
|
+
except httpx.HTTPError as e:
|
|
111
|
+
raise APIConnectionError(str(e)) from e
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def parse_json(resp: httpx.Response) -> Any:
|
|
115
|
+
if not resp.content:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
return orjson.loads(resp.content)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def raise_for_status(status: int, body: bytes | Any) -> None:
|
|
122
|
+
data: Any
|
|
123
|
+
|
|
124
|
+
if isinstance(body, (bytes, bytearray)):
|
|
125
|
+
try:
|
|
126
|
+
data = orjson.loads(body) if body else None
|
|
127
|
+
except Exception:
|
|
128
|
+
data = body.decode("utf-8", errors="replace") if body else None
|
|
129
|
+
else:
|
|
130
|
+
data = body
|
|
131
|
+
|
|
132
|
+
message = None
|
|
133
|
+
|
|
134
|
+
if isinstance(data, dict):
|
|
135
|
+
err = data.get("error")
|
|
136
|
+
|
|
137
|
+
if isinstance(err, dict):
|
|
138
|
+
message = err.get("message") or err.get("code")
|
|
139
|
+
elif isinstance(err, str):
|
|
140
|
+
message = err
|
|
141
|
+
else:
|
|
142
|
+
message = data.get("message")
|
|
143
|
+
|
|
144
|
+
if not message:
|
|
145
|
+
message = f"HTTP {status}"
|
|
146
|
+
|
|
147
|
+
if status == 401:
|
|
148
|
+
raise AuthenticationError(str(message), status_code=status, body=data)
|
|
149
|
+
|
|
150
|
+
if status == 404:
|
|
151
|
+
raise NotFoundError(str(message), status_code=status, body=data)
|
|
152
|
+
|
|
153
|
+
if status == 429:
|
|
154
|
+
raise RateLimitError(str(message), status_code=status, body=data)
|
|
155
|
+
|
|
156
|
+
if 400 <= status < 500:
|
|
157
|
+
raise BadRequestError(str(message), status_code=status, body=data)
|
|
158
|
+
|
|
159
|
+
raise APIError(str(message), status_code=status, body=data)
|
urouter/_types.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Iterator, Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Role = Literal["system", "user", "assistant", "tool", "function", "developer"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class ChatCompletionMessage:
|
|
12
|
+
role: str
|
|
13
|
+
|
|
14
|
+
content: str | None = None
|
|
15
|
+
|
|
16
|
+
reasoning: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class Choice:
|
|
21
|
+
index: int
|
|
22
|
+
|
|
23
|
+
message: ChatCompletionMessage
|
|
24
|
+
|
|
25
|
+
finish_reason: str | None = "stop"
|
|
26
|
+
|
|
27
|
+
logprobs: Any = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class CompletionUsage:
|
|
32
|
+
prompt_tokens: int = 0
|
|
33
|
+
|
|
34
|
+
completion_tokens: int = 0
|
|
35
|
+
|
|
36
|
+
total_tokens: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class ChatCompletion:
|
|
41
|
+
id: str
|
|
42
|
+
|
|
43
|
+
choices: list[Choice]
|
|
44
|
+
|
|
45
|
+
created: int
|
|
46
|
+
|
|
47
|
+
model: str
|
|
48
|
+
|
|
49
|
+
object: str = "chat.completion"
|
|
50
|
+
|
|
51
|
+
usage: CompletionUsage | None = None
|
|
52
|
+
|
|
53
|
+
system_fingerprint: str | None = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def content(self) -> str:
|
|
57
|
+
if not self.choices:
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
return self.choices[0].message.content or ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(slots=True)
|
|
64
|
+
class ChoiceDelta:
|
|
65
|
+
role: str | None = None
|
|
66
|
+
|
|
67
|
+
content: str | None = None
|
|
68
|
+
|
|
69
|
+
reasoning: str | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(slots=True)
|
|
73
|
+
class ChunkChoice:
|
|
74
|
+
index: int
|
|
75
|
+
|
|
76
|
+
delta: ChoiceDelta
|
|
77
|
+
|
|
78
|
+
finish_reason: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(slots=True)
|
|
82
|
+
class ChatCompletionChunk:
|
|
83
|
+
id: str
|
|
84
|
+
|
|
85
|
+
choices: list[ChunkChoice]
|
|
86
|
+
|
|
87
|
+
created: int
|
|
88
|
+
|
|
89
|
+
model: str
|
|
90
|
+
|
|
91
|
+
object: str = "chat.completion.chunk"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(slots=True)
|
|
95
|
+
class Model:
|
|
96
|
+
id: str
|
|
97
|
+
|
|
98
|
+
object: str = "model"
|
|
99
|
+
|
|
100
|
+
created: int = 0
|
|
101
|
+
|
|
102
|
+
owned_by: str = "urouter"
|
|
103
|
+
|
|
104
|
+
name: str | None = None
|
|
105
|
+
|
|
106
|
+
brand: str | None = None
|
|
107
|
+
|
|
108
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(slots=True)
|
|
112
|
+
class ModelList:
|
|
113
|
+
data: list[Model]
|
|
114
|
+
|
|
115
|
+
object: str = "list"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Stream[T]:
|
|
119
|
+
__slots__ = ("_iterator",)
|
|
120
|
+
|
|
121
|
+
def __init__(self, iterator: Iterator[T]) -> None:
|
|
122
|
+
self._iterator = iterator
|
|
123
|
+
|
|
124
|
+
def __iter__(self) -> Iterator[T]:
|
|
125
|
+
return self._iterator
|
|
126
|
+
|
|
127
|
+
def __next__(self) -> T:
|
|
128
|
+
return next(self._iterator)
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Iterator, Literal, overload
|
|
6
|
+
|
|
7
|
+
import orjson
|
|
8
|
+
|
|
9
|
+
from urouter._constants import CHAT_BASE, FREE_PRESETS
|
|
10
|
+
from urouter._types import (
|
|
11
|
+
ChatCompletion,
|
|
12
|
+
ChatCompletionChunk,
|
|
13
|
+
ChatCompletionMessage,
|
|
14
|
+
Choice,
|
|
15
|
+
ChoiceDelta,
|
|
16
|
+
ChunkChoice,
|
|
17
|
+
CompletionUsage,
|
|
18
|
+
Stream,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from urouter._client import URouter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Completions:
|
|
26
|
+
__slots__ = ("_client",)
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: URouter) -> None:
|
|
29
|
+
self._client = client
|
|
30
|
+
|
|
31
|
+
@overload
|
|
32
|
+
def create(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
messages: list[dict[str, Any]],
|
|
36
|
+
model: str = ...,
|
|
37
|
+
stream: Literal[False] = False,
|
|
38
|
+
max_tokens: int | None = ...,
|
|
39
|
+
temperature: float | None = ...,
|
|
40
|
+
top_p: float | None = ...,
|
|
41
|
+
stop: str | list[str] | None = ...,
|
|
42
|
+
presence_penalty: float | None = ...,
|
|
43
|
+
frequency_penalty: float | None = ...,
|
|
44
|
+
user: str | None = ...,
|
|
45
|
+
extra_body: dict[str, Any] | None = ...,
|
|
46
|
+
) -> ChatCompletion: ...
|
|
47
|
+
|
|
48
|
+
@overload
|
|
49
|
+
def create(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
messages: list[dict[str, Any]],
|
|
53
|
+
model: str = ...,
|
|
54
|
+
stream: Literal[True],
|
|
55
|
+
max_tokens: int | None = ...,
|
|
56
|
+
temperature: float | None = ...,
|
|
57
|
+
top_p: float | None = ...,
|
|
58
|
+
stop: str | list[str] | None = ...,
|
|
59
|
+
presence_penalty: float | None = ...,
|
|
60
|
+
frequency_penalty: float | None = ...,
|
|
61
|
+
user: str | None = ...,
|
|
62
|
+
extra_body: dict[str, Any] | None = ...,
|
|
63
|
+
) -> Stream[ChatCompletionChunk]: ...
|
|
64
|
+
|
|
65
|
+
def create(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
messages: list[dict[str, Any]],
|
|
69
|
+
model: str = "normal",
|
|
70
|
+
stream: bool = False,
|
|
71
|
+
max_tokens: int | None = None,
|
|
72
|
+
temperature: float | None = None,
|
|
73
|
+
top_p: float | None = None,
|
|
74
|
+
stop: str | list[str] | None = None,
|
|
75
|
+
presence_penalty: float | None = None,
|
|
76
|
+
frequency_penalty: float | None = None,
|
|
77
|
+
user: str | None = None,
|
|
78
|
+
extra_body: dict[str, Any] | None = None,
|
|
79
|
+
) -> ChatCompletion | Stream[ChatCompletionChunk]:
|
|
80
|
+
if self._client.api_key:
|
|
81
|
+
return self._paid_create(
|
|
82
|
+
messages=messages,
|
|
83
|
+
model=model,
|
|
84
|
+
stream=stream,
|
|
85
|
+
max_tokens=max_tokens,
|
|
86
|
+
temperature=temperature,
|
|
87
|
+
top_p=top_p,
|
|
88
|
+
stop=stop,
|
|
89
|
+
presence_penalty=presence_penalty,
|
|
90
|
+
frequency_penalty=frequency_penalty,
|
|
91
|
+
user=user,
|
|
92
|
+
extra_body=extra_body,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return self._free_create(
|
|
96
|
+
messages=messages,
|
|
97
|
+
model=model,
|
|
98
|
+
stream=stream,
|
|
99
|
+
max_tokens=max_tokens,
|
|
100
|
+
extra_body=extra_body,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _resolve_free(self, model: str) -> tuple[str, str]:
|
|
104
|
+
key = model.strip().lower()
|
|
105
|
+
|
|
106
|
+
if key in FREE_PRESETS:
|
|
107
|
+
return FREE_PRESETS[key]
|
|
108
|
+
|
|
109
|
+
return model, "normal"
|
|
110
|
+
|
|
111
|
+
def _free_create(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
messages: list[dict[str, Any]],
|
|
115
|
+
model: str,
|
|
116
|
+
stream: bool,
|
|
117
|
+
max_tokens: int | None,
|
|
118
|
+
extra_body: dict[str, Any] | None,
|
|
119
|
+
) -> ChatCompletion | Stream[ChatCompletionChunk]:
|
|
120
|
+
model_id, preset = self._resolve_free(model)
|
|
121
|
+
|
|
122
|
+
content = ""
|
|
123
|
+
|
|
124
|
+
history: list[dict[str, Any]] = []
|
|
125
|
+
|
|
126
|
+
for msg in messages:
|
|
127
|
+
role = str(msg.get("role") or "user")
|
|
128
|
+
|
|
129
|
+
text = msg.get("content")
|
|
130
|
+
|
|
131
|
+
if text is None:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
if not isinstance(text, str):
|
|
135
|
+
text = str(text)
|
|
136
|
+
|
|
137
|
+
if role == "system":
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if role == "user":
|
|
141
|
+
content = text
|
|
142
|
+
|
|
143
|
+
history.append(
|
|
144
|
+
{
|
|
145
|
+
"id": str(uuid.uuid4()),
|
|
146
|
+
"role": role,
|
|
147
|
+
"content": text,
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if history and history[-1]["role"] == "user":
|
|
152
|
+
history = history[:-1]
|
|
153
|
+
|
|
154
|
+
if not content and messages:
|
|
155
|
+
last = messages[-1].get("content")
|
|
156
|
+
|
|
157
|
+
content = last if isinstance(last, str) else str(last or "")
|
|
158
|
+
|
|
159
|
+
turn_id = str(uuid.uuid4())
|
|
160
|
+
|
|
161
|
+
body: dict[str, Any] = {
|
|
162
|
+
"turnId": turn_id,
|
|
163
|
+
"model": model_id,
|
|
164
|
+
"mode": "free",
|
|
165
|
+
"preset": preset,
|
|
166
|
+
"message": {
|
|
167
|
+
"id": str(uuid.uuid4()),
|
|
168
|
+
"role": "user",
|
|
169
|
+
"content": content,
|
|
170
|
+
},
|
|
171
|
+
"history": history,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if max_tokens is not None:
|
|
175
|
+
body["maxTokens"] = max_tokens
|
|
176
|
+
|
|
177
|
+
if extra_body:
|
|
178
|
+
body.update(extra_body)
|
|
179
|
+
|
|
180
|
+
url = f"{CHAT_BASE}/api/chat/send"
|
|
181
|
+
|
|
182
|
+
headers = self._client._chat_headers()
|
|
183
|
+
|
|
184
|
+
headers["Accept"] = "text/event-stream"
|
|
185
|
+
|
|
186
|
+
if stream:
|
|
187
|
+
return Stream(self._iter_free_stream(url, body, headers, model_id, turn_id))
|
|
188
|
+
|
|
189
|
+
text_parts: list[str] = []
|
|
190
|
+
|
|
191
|
+
reasoning_parts: list[str] = []
|
|
192
|
+
|
|
193
|
+
request_id = turn_id
|
|
194
|
+
|
|
195
|
+
for event in self._iter_sse(url, body, headers):
|
|
196
|
+
etype = event.get("type")
|
|
197
|
+
|
|
198
|
+
if etype == "start":
|
|
199
|
+
request_id = str(event.get("requestId") or request_id)
|
|
200
|
+
|
|
201
|
+
elif etype == "delta":
|
|
202
|
+
chunk = event.get("text")
|
|
203
|
+
|
|
204
|
+
if chunk:
|
|
205
|
+
text_parts.append(str(chunk))
|
|
206
|
+
|
|
207
|
+
reason = event.get("reasoning")
|
|
208
|
+
|
|
209
|
+
if reason:
|
|
210
|
+
reasoning_parts.append(str(reason))
|
|
211
|
+
|
|
212
|
+
elif etype == "done":
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
created = int(time.time())
|
|
216
|
+
|
|
217
|
+
message = ChatCompletionMessage(
|
|
218
|
+
role="assistant",
|
|
219
|
+
content="".join(text_parts),
|
|
220
|
+
reasoning="".join(reasoning_parts) or None,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return ChatCompletion(
|
|
224
|
+
id=str(request_id),
|
|
225
|
+
choices=[Choice(index=0, message=message, finish_reason="stop")],
|
|
226
|
+
created=created,
|
|
227
|
+
model=model_id,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _iter_free_stream(
|
|
231
|
+
self,
|
|
232
|
+
url: str,
|
|
233
|
+
body: dict[str, Any],
|
|
234
|
+
headers: dict[str, str],
|
|
235
|
+
model_id: str,
|
|
236
|
+
turn_id: str,
|
|
237
|
+
) -> Iterator[ChatCompletionChunk]:
|
|
238
|
+
created = int(time.time())
|
|
239
|
+
|
|
240
|
+
request_id = turn_id
|
|
241
|
+
|
|
242
|
+
for event in self._iter_sse(url, body, headers):
|
|
243
|
+
etype = event.get("type")
|
|
244
|
+
|
|
245
|
+
if etype == "start":
|
|
246
|
+
request_id = str(event.get("requestId") or request_id)
|
|
247
|
+
|
|
248
|
+
yield ChatCompletionChunk(
|
|
249
|
+
id=str(request_id),
|
|
250
|
+
choices=[
|
|
251
|
+
ChunkChoice(
|
|
252
|
+
index=0,
|
|
253
|
+
delta=ChoiceDelta(role="assistant", content=""),
|
|
254
|
+
)
|
|
255
|
+
],
|
|
256
|
+
created=created,
|
|
257
|
+
model=model_id,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
elif etype == "delta":
|
|
261
|
+
text = event.get("text")
|
|
262
|
+
|
|
263
|
+
reason = event.get("reasoning")
|
|
264
|
+
|
|
265
|
+
if not text and not reason:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
yield ChatCompletionChunk(
|
|
269
|
+
id=str(request_id),
|
|
270
|
+
choices=[
|
|
271
|
+
ChunkChoice(
|
|
272
|
+
index=0,
|
|
273
|
+
delta=ChoiceDelta(
|
|
274
|
+
content=str(text) if text else None,
|
|
275
|
+
reasoning=str(reason) if reason else None,
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
],
|
|
279
|
+
created=created,
|
|
280
|
+
model=model_id,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
elif etype == "done":
|
|
284
|
+
yield ChatCompletionChunk(
|
|
285
|
+
id=str(request_id),
|
|
286
|
+
choices=[
|
|
287
|
+
ChunkChoice(
|
|
288
|
+
index=0,
|
|
289
|
+
delta=ChoiceDelta(),
|
|
290
|
+
finish_reason="stop",
|
|
291
|
+
)
|
|
292
|
+
],
|
|
293
|
+
created=created,
|
|
294
|
+
model=model_id,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
def _iter_sse(
|
|
300
|
+
self,
|
|
301
|
+
url: str,
|
|
302
|
+
body: dict[str, Any],
|
|
303
|
+
headers: dict[str, str],
|
|
304
|
+
) -> Iterator[dict[str, Any]]:
|
|
305
|
+
buf = b""
|
|
306
|
+
|
|
307
|
+
for chunk in self._client._http.stream("POST", url, json_body=body, headers=headers):
|
|
308
|
+
buf += chunk
|
|
309
|
+
|
|
310
|
+
while b"\n" in buf:
|
|
311
|
+
line, buf = buf.split(b"\n", 1)
|
|
312
|
+
|
|
313
|
+
line = line.strip()
|
|
314
|
+
|
|
315
|
+
if not line or line.startswith(b":"):
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
if line.startswith(b"data:"):
|
|
319
|
+
payload = line[5:].strip()
|
|
320
|
+
|
|
321
|
+
if payload == b"[DONE]":
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
yield orjson.loads(payload)
|
|
326
|
+
except Exception:
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
def _paid_create(
|
|
330
|
+
self,
|
|
331
|
+
*,
|
|
332
|
+
messages: list[dict[str, Any]],
|
|
333
|
+
model: str,
|
|
334
|
+
stream: bool,
|
|
335
|
+
max_tokens: int | None,
|
|
336
|
+
temperature: float | None,
|
|
337
|
+
top_p: float | None,
|
|
338
|
+
stop: str | list[str] | None,
|
|
339
|
+
presence_penalty: float | None,
|
|
340
|
+
frequency_penalty: float | None,
|
|
341
|
+
user: str | None,
|
|
342
|
+
extra_body: dict[str, Any] | None,
|
|
343
|
+
) -> ChatCompletion | Stream[ChatCompletionChunk]:
|
|
344
|
+
model_id, _ = self._resolve_free(model)
|
|
345
|
+
|
|
346
|
+
body: dict[str, Any] = {
|
|
347
|
+
"model": model_id,
|
|
348
|
+
"messages": messages,
|
|
349
|
+
"stream": stream,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if max_tokens is not None:
|
|
353
|
+
body["max_tokens"] = max_tokens
|
|
354
|
+
|
|
355
|
+
if temperature is not None:
|
|
356
|
+
body["temperature"] = temperature
|
|
357
|
+
|
|
358
|
+
if top_p is not None:
|
|
359
|
+
body["top_p"] = top_p
|
|
360
|
+
|
|
361
|
+
if stop is not None:
|
|
362
|
+
body["stop"] = stop
|
|
363
|
+
|
|
364
|
+
if presence_penalty is not None:
|
|
365
|
+
body["presence_penalty"] = presence_penalty
|
|
366
|
+
|
|
367
|
+
if frequency_penalty is not None:
|
|
368
|
+
body["frequency_penalty"] = frequency_penalty
|
|
369
|
+
|
|
370
|
+
if user is not None:
|
|
371
|
+
body["user"] = user
|
|
372
|
+
|
|
373
|
+
if extra_body:
|
|
374
|
+
body.update(extra_body)
|
|
375
|
+
|
|
376
|
+
url = f"{self._client.base_url.rstrip('/')}/chat/completions"
|
|
377
|
+
|
|
378
|
+
headers = self._client._auth_headers()
|
|
379
|
+
|
|
380
|
+
if stream:
|
|
381
|
+
return Stream(self._iter_paid_stream(url, body, headers, model_id))
|
|
382
|
+
|
|
383
|
+
resp = self._client._http.request(
|
|
384
|
+
"POST",
|
|
385
|
+
url,
|
|
386
|
+
json_body=body,
|
|
387
|
+
headers=headers,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if resp.status_code >= 400:
|
|
391
|
+
self._client._http.raise_for_status(resp.status_code, resp.content)
|
|
392
|
+
|
|
393
|
+
data = self._client._http.parse_json(resp) or {}
|
|
394
|
+
|
|
395
|
+
return self._parse_completion(data, fallback_model=model_id)
|
|
396
|
+
|
|
397
|
+
def _iter_paid_stream(
|
|
398
|
+
self,
|
|
399
|
+
url: str,
|
|
400
|
+
body: dict[str, Any],
|
|
401
|
+
headers: dict[str, str],
|
|
402
|
+
model_id: str,
|
|
403
|
+
) -> Iterator[ChatCompletionChunk]:
|
|
404
|
+
headers = {**headers, "Accept": "text/event-stream"}
|
|
405
|
+
|
|
406
|
+
created = int(time.time())
|
|
407
|
+
|
|
408
|
+
for event in self._iter_sse(url, body, headers):
|
|
409
|
+
if event.get("object") == "chat.completion.chunk" or "choices" in event:
|
|
410
|
+
choices_in = event.get("choices") or []
|
|
411
|
+
|
|
412
|
+
choices: list[ChunkChoice] = []
|
|
413
|
+
|
|
414
|
+
for i, ch in enumerate(choices_in):
|
|
415
|
+
delta = ch.get("delta") or {}
|
|
416
|
+
|
|
417
|
+
choices.append(
|
|
418
|
+
ChunkChoice(
|
|
419
|
+
index=int(ch.get("index") or i),
|
|
420
|
+
delta=ChoiceDelta(
|
|
421
|
+
role=delta.get("role"),
|
|
422
|
+
content=delta.get("content"),
|
|
423
|
+
),
|
|
424
|
+
finish_reason=ch.get("finish_reason"),
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
yield ChatCompletionChunk(
|
|
429
|
+
id=str(event.get("id") or ""),
|
|
430
|
+
choices=choices,
|
|
431
|
+
created=int(event.get("created") or created),
|
|
432
|
+
model=str(event.get("model") or model_id),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
etype = event.get("type")
|
|
438
|
+
|
|
439
|
+
if etype == "delta" and event.get("text"):
|
|
440
|
+
yield ChatCompletionChunk(
|
|
441
|
+
id=str(event.get("requestId") or ""),
|
|
442
|
+
choices=[
|
|
443
|
+
ChunkChoice(
|
|
444
|
+
index=0,
|
|
445
|
+
delta=ChoiceDelta(content=str(event.get("text"))),
|
|
446
|
+
)
|
|
447
|
+
],
|
|
448
|
+
created=created,
|
|
449
|
+
model=model_id,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def _parse_completion(self, data: dict[str, Any], *, fallback_model: str) -> ChatCompletion:
|
|
453
|
+
choices: list[Choice] = []
|
|
454
|
+
|
|
455
|
+
for i, ch in enumerate(data.get("choices") or []):
|
|
456
|
+
msg = ch.get("message") or {}
|
|
457
|
+
|
|
458
|
+
choices.append(
|
|
459
|
+
Choice(
|
|
460
|
+
index=int(ch.get("index") or i),
|
|
461
|
+
message=ChatCompletionMessage(
|
|
462
|
+
role=str(msg.get("role") or "assistant"),
|
|
463
|
+
content=msg.get("content"),
|
|
464
|
+
),
|
|
465
|
+
finish_reason=ch.get("finish_reason"),
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
usage = None
|
|
470
|
+
|
|
471
|
+
raw_usage = data.get("usage")
|
|
472
|
+
|
|
473
|
+
if isinstance(raw_usage, dict):
|
|
474
|
+
usage = CompletionUsage(
|
|
475
|
+
prompt_tokens=int(raw_usage.get("prompt_tokens") or 0),
|
|
476
|
+
completion_tokens=int(raw_usage.get("completion_tokens") or 0),
|
|
477
|
+
total_tokens=int(raw_usage.get("total_tokens") or 0),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return ChatCompletion(
|
|
481
|
+
id=str(data.get("id") or ""),
|
|
482
|
+
choices=choices,
|
|
483
|
+
created=int(data.get("created") or time.time()),
|
|
484
|
+
model=str(data.get("model") or fallback_model),
|
|
485
|
+
usage=usage,
|
|
486
|
+
system_fingerprint=data.get("system_fingerprint"),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class Chat:
|
|
491
|
+
__slots__ = ("completions",)
|
|
492
|
+
|
|
493
|
+
def __init__(self, client: URouter) -> None:
|
|
494
|
+
self.completions = Completions(client)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from urouter._constants import API_BASE, CHAT_BASE
|
|
6
|
+
from urouter._types import Model, ModelList
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from urouter._client import URouter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Models:
|
|
13
|
+
__slots__ = ("_client",)
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: URouter) -> None:
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
def list(self) -> ModelList:
|
|
19
|
+
if self._client.api_key:
|
|
20
|
+
url = f"{self._client.base_url.rstrip('/')}/models"
|
|
21
|
+
|
|
22
|
+
resp = self._client._http.request(
|
|
23
|
+
"GET",
|
|
24
|
+
url,
|
|
25
|
+
headers=self._client._auth_headers(),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if resp.status_code >= 400:
|
|
29
|
+
self._client._http.raise_for_status(resp.status_code, resp.content)
|
|
30
|
+
|
|
31
|
+
payload = self._client._http.parse_json(resp) or {}
|
|
32
|
+
|
|
33
|
+
items = []
|
|
34
|
+
|
|
35
|
+
for row in payload.get("data") or []:
|
|
36
|
+
items.append(
|
|
37
|
+
Model(
|
|
38
|
+
id=str(row.get("id") or ""),
|
|
39
|
+
name=row.get("name"),
|
|
40
|
+
brand=row.get("brand"),
|
|
41
|
+
owned_by=str(row.get("brand") or row.get("owned_by") or "urouter"),
|
|
42
|
+
raw=row if isinstance(row, dict) else {},
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return ModelList(data=items)
|
|
47
|
+
|
|
48
|
+
resp = self._client._http.request(
|
|
49
|
+
"GET",
|
|
50
|
+
f"{CHAT_BASE}/api/chat/state",
|
|
51
|
+
headers=self._client._chat_headers(),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if resp.status_code >= 400:
|
|
55
|
+
self._client._http.raise_for_status(resp.status_code, resp.content)
|
|
56
|
+
|
|
57
|
+
payload = self._client._http.parse_json(resp) or {}
|
|
58
|
+
|
|
59
|
+
items = []
|
|
60
|
+
|
|
61
|
+
for row in payload.get("models") or []:
|
|
62
|
+
mid = str(row.get("id") or "")
|
|
63
|
+
|
|
64
|
+
items.append(
|
|
65
|
+
Model(
|
|
66
|
+
id=mid,
|
|
67
|
+
name=mid,
|
|
68
|
+
owned_by="urouter",
|
|
69
|
+
raw=row if isinstance(row, dict) else {},
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for row in payload.get("presets") or []:
|
|
74
|
+
mid = str(row.get("modelId") or row.get("id") or "")
|
|
75
|
+
|
|
76
|
+
if mid and not any(m.id == mid for m in items):
|
|
77
|
+
items.append(
|
|
78
|
+
Model(
|
|
79
|
+
id=mid,
|
|
80
|
+
name=str(row.get("label") or mid),
|
|
81
|
+
owned_by="urouter-free",
|
|
82
|
+
raw=row if isinstance(row, dict) else {},
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return ModelList(data=items)
|
|
87
|
+
|
|
88
|
+
def retrieve(self, model: str) -> Model:
|
|
89
|
+
if self._client.api_key:
|
|
90
|
+
url = f"{self._client.base_url.rstrip('/')}/models/{model}"
|
|
91
|
+
|
|
92
|
+
resp = self._client._http.request(
|
|
93
|
+
"GET",
|
|
94
|
+
url,
|
|
95
|
+
headers=self._client._auth_headers(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if resp.status_code >= 400:
|
|
99
|
+
self._client._http.raise_for_status(resp.status_code, resp.content)
|
|
100
|
+
|
|
101
|
+
row: dict[str, Any] = self._client._http.parse_json(resp) or {}
|
|
102
|
+
|
|
103
|
+
return Model(
|
|
104
|
+
id=str(row.get("id") or model),
|
|
105
|
+
name=row.get("name"),
|
|
106
|
+
brand=row.get("brand"),
|
|
107
|
+
owned_by=str(row.get("brand") or "urouter"),
|
|
108
|
+
raw=row,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
listed = self.list()
|
|
112
|
+
|
|
113
|
+
for item in listed.data:
|
|
114
|
+
if item.id == model:
|
|
115
|
+
return item
|
|
116
|
+
|
|
117
|
+
return Model(id=model, owned_by="urouter")
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: urouter-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenAI-compatible Python client for uRouter (free + paid)
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: httpx[http2]>=0.27.0
|
|
8
|
+
Requires-Dist: orjson>=3.10.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# urouter
|
|
15
|
+
|
|
16
|
+
OpenAI-compatible Python client for [uRouter](https://urouter.net) / [chat.urouter.net](https://chat.urouter.net).
|
|
17
|
+
|
|
18
|
+
Free presets (no API key): **normal** (censored) and **uncensored**.
|
|
19
|
+
Paid path uses `https://api.urouter.net/v1` with a Bearer key.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install urouter
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Free chat
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from urouter import URouter
|
|
31
|
+
|
|
32
|
+
client = URouter()
|
|
33
|
+
|
|
34
|
+
# censored free
|
|
35
|
+
r = client.chat.completions.create(
|
|
36
|
+
model="normal",
|
|
37
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
38
|
+
)
|
|
39
|
+
print(r.choices[0].message.content)
|
|
40
|
+
|
|
41
|
+
# uncensored free
|
|
42
|
+
r = client.chat.completions.create(
|
|
43
|
+
model="uncensored",
|
|
44
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
45
|
+
)
|
|
46
|
+
print(r.content)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Aliases: `normal` / `censored` / `free` → `deepseek-v4.1-flash`
|
|
50
|
+
`uncensored` → `gemma-4-uncensored`
|
|
51
|
+
|
|
52
|
+
## Streaming
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
stream = client.chat.completions.create(
|
|
56
|
+
model="normal",
|
|
57
|
+
messages=[{"role": "user", "content": "Hi"}],
|
|
58
|
+
stream=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for chunk in stream:
|
|
62
|
+
delta = chunk.choices[0].delta.content
|
|
63
|
+
if delta:
|
|
64
|
+
print(delta, end="", flush=True)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Paid (OpenAI-compatible)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from urouter import OpenAI
|
|
71
|
+
|
|
72
|
+
client = OpenAI(api_key="ur-...")
|
|
73
|
+
|
|
74
|
+
r = client.chat.completions.create(
|
|
75
|
+
model="deepseek-v4-flash",
|
|
76
|
+
messages=[{"role": "user", "content": "Hi"}],
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Models
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
for m in client.models.list().data:
|
|
84
|
+
print(m.id)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Drop-in shape
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
client.chat.completions.create(...)
|
|
91
|
+
client.models.list()
|
|
92
|
+
client.models.retrieve(id)
|
|
93
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
urouter/__init__.py,sha256=VKHtSnIkSltLdoJri0ZSuLRdNmmeBphCqpwmyuq2BQc,950
|
|
2
|
+
urouter/_client.py,sha256=T9srNkHVWJeGp4NU2hNyV8fHYffKuzaeo9ZtQx0YYiY,1839
|
|
3
|
+
urouter/_constants.py,sha256=UmDTFeeS-P_H5sIwheWjaiU0mtQJk-ZItpjK4l01wWM,648
|
|
4
|
+
urouter/_exceptions.py,sha256=MhRkXzgjDB5ENujCvP-aL2Pgr-uRudlQ3UW5zyk_ybs,640
|
|
5
|
+
urouter/_http.py,sha256=4xdqmtasYCC2mn25pUjtJ_zJXtGVC_WDG0mLiBiza4I,4140
|
|
6
|
+
urouter/_types.py,sha256=90xq3zIvkKf3UrIGBWQ187xljwgAC439JFHq0AiBm40,2058
|
|
7
|
+
urouter/resources/__init__.py,sha256=IJI6YESPjyHO4piMrr3G-Oynb9nlb70ASXJQY6Ef7fw,178
|
|
8
|
+
urouter/resources/chat.py,sha256=DjEFXIKE3BDrwyb-wWCqXaI0DzlDmtm7xu9AGa9Kq4s,14254
|
|
9
|
+
urouter/resources/models.py,sha256=KjAhQY3irLKesh1oRdGupnzDiPkSpyhpfY2tDyJA_no,3512
|
|
10
|
+
urouter_sdk-0.1.0.dist-info/METADATA,sha256=D1GnTrXxgLaduyNiW--SW1XQmsONCPWtbYccKepYtAI,1875
|
|
11
|
+
urouter_sdk-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
12
|
+
urouter_sdk-0.1.0.dist-info/RECORD,,
|