xinference-client 3.2.0__tar.gz → 3.4.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.
Files changed (27) hide show
  1. {xinference_client-3.2.0 → xinference_client-3.4.0}/PKG-INFO +2 -1
  2. {xinference_client-3.2.0 → xinference_client-3.4.0}/setup.cfg +1 -0
  3. xinference_client-3.4.0/tests/test_async_client.py +37 -0
  4. xinference_client-3.4.0/tests/test_async_client_cleanup.py +126 -0
  5. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/_version.py +3 -3
  6. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/client/common.py +42 -14
  7. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/client/restful/async_restful_client.py +138 -24
  8. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/client/restful/restful_client.py +98 -16
  9. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/types.py +22 -2
  10. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client.egg-info/PKG-INFO +2 -1
  11. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client.egg-info/SOURCES.txt +2 -0
  12. {xinference_client-3.2.0 → xinference_client-3.4.0}/LICENSE +0 -0
  13. {xinference_client-3.2.0 → xinference_client-3.4.0}/MANIFEST.in +0 -0
  14. {xinference_client-3.2.0 → xinference_client-3.4.0}/README.md +0 -0
  15. {xinference_client-3.2.0 → xinference_client-3.4.0}/pyproject.toml +0 -0
  16. {xinference_client-3.2.0 → xinference_client-3.4.0}/setup.py +0 -0
  17. {xinference_client-3.2.0 → xinference_client-3.4.0}/third_party/inference/xinference/thirdparty/matcha/utils/monotonic_align/core.pyx +0 -0
  18. {xinference_client-3.2.0 → xinference_client-3.4.0}/versioneer.py +0 -0
  19. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/__init__.py +0 -0
  20. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/_compat.py +0 -0
  21. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/client/__init__.py +0 -0
  22. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/client/restful/__init__.py +0 -0
  23. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client/fields.py +0 -0
  24. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client.egg-info/dependency_links.txt +0 -0
  25. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client.egg-info/not-zip-safe +0 -0
  26. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client.egg-info/requires.txt +0 -0
  27. {xinference_client-3.2.0 → xinference_client-3.4.0}/xinference_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xinference-client
3
- Version: 3.2.0
3
+ Version: 3.4.0
4
4
  Summary: Client for Xinference
5
5
  Home-page: https://github.com/xorbitsai/inference-client
6
6
  Author: Qin Xuye
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
19
20
  Classifier: Programming Language :: Python :: Implementation :: CPython
20
21
  Classifier: Topic :: Software Development :: Libraries
21
22
  Description-Content-Type: text/markdown
@@ -17,6 +17,7 @@ classifier =
17
17
  Programming Language :: Python :: 3.11
18
18
  Programming Language :: Python :: 3.12
19
19
  Programming Language :: Python :: 3.13
20
+ Programming Language :: Python :: 3.14
20
21
  Programming Language :: Python :: Implementation :: CPython
21
22
  Topic :: Software Development :: Libraries
22
23
 
@@ -0,0 +1,37 @@
1
+ import pytest
2
+ from aiohttp import web
3
+
4
+ from xinference_client import AsyncRESTfulClient
5
+
6
+
7
+ @pytest.mark.asyncio
8
+ async def test_list_models_and_explicit_close(monkeypatch):
9
+ # Authentication discovery is synchronous; isolate it from the async server.
10
+ monkeypatch.setattr(
11
+ AsyncRESTfulClient, "_check_cluster_authenticated", lambda self: None
12
+ )
13
+ model = {"id": "test-model", "model_type": "LLM"}
14
+
15
+ async def list_models(request):
16
+ return web.json_response({"data": [model]})
17
+
18
+ app = web.Application()
19
+ app.router.add_get("/v1/models", list_models)
20
+ runner = web.AppRunner(app)
21
+ await runner.setup()
22
+ client = None
23
+ try:
24
+ site = web.TCPSite(runner, "127.0.0.1", 0)
25
+ await site.start()
26
+ port = runner.addresses[0][1]
27
+ client = AsyncRESTfulClient(f"http://127.0.0.1:{port}")
28
+ session = client.session
29
+ assert await client.list_models() == {"test-model": model}
30
+ await client.close()
31
+ assert session.closed
32
+ assert client.session is None
33
+ await client.close()
34
+ finally:
35
+ if client is not None:
36
+ await client.close()
37
+ await runner.cleanup()
@@ -0,0 +1,126 @@
1
+ # Copyright 2022-2026 Xinference Holdings Pte. Ltd
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import asyncio
16
+ import gc
17
+ import threading
18
+
19
+ import pytest
20
+
21
+ from xinference_client.client.restful import async_restful_client as client_module
22
+
23
+
24
+ @pytest.mark.parametrize(
25
+ "client_class",
26
+ [client_module.AsyncRESTfulModelHandle, client_module.AsyncClient],
27
+ )
28
+ def test_destructor_without_initialized_owner_loop(client_class, monkeypatch):
29
+ def fail_get_event_loop():
30
+ raise AssertionError("destructor must not request an implicit event loop")
31
+
32
+ monkeypatch.setattr(client_module.asyncio, "get_event_loop", fail_get_event_loop)
33
+ client = client_class.__new__(client_class)
34
+ client.session = object()
35
+
36
+ client.__del__()
37
+
38
+ client.session = None
39
+
40
+
41
+ @pytest.mark.parametrize(
42
+ "client_class",
43
+ [client_module.AsyncRESTfulModelHandle, client_module.AsyncClient],
44
+ )
45
+ def test_destructor_after_owner_loop_closed(client_class):
46
+ client = client_class.__new__(client_class)
47
+ client.session = object()
48
+ client._session_loop = asyncio.new_event_loop()
49
+ client._session_loop.close()
50
+ client.__del__()
51
+ client.session = None
52
+
53
+
54
+ @pytest.mark.asyncio
55
+ @pytest.mark.parametrize(
56
+ "client_class",
57
+ [client_module.AsyncRESTfulModelHandle, client_module.AsyncClient],
58
+ )
59
+ async def test_destructor_schedules_close_on_running_loop(client_class):
60
+ client = client_class.__new__(client_class)
61
+ client._session_loop = asyncio.get_running_loop()
62
+ client.session = object()
63
+ closed = asyncio.Event()
64
+
65
+ async def close():
66
+ client.session = None
67
+ closed.set()
68
+
69
+ client.close = close
70
+
71
+ client.__del__()
72
+
73
+ await asyncio.wait_for(closed.wait(), timeout=1)
74
+ assert client.session is None
75
+
76
+
77
+ @pytest.mark.parametrize(
78
+ "client_class",
79
+ [client_module.AsyncRESTfulModelHandle, client_module.AsyncClient],
80
+ )
81
+ @pytest.mark.parametrize("other_thread", [False, True])
82
+ def test_real_session_cleanup_on_paused_owner_loop(
83
+ client_class, other_thread, monkeypatch
84
+ ):
85
+ monkeypatch.setattr(
86
+ client_module.AsyncClient, "_check_cluster_authenticated", lambda self: None
87
+ )
88
+ loop = asyncio.new_event_loop()
89
+ loop.set_debug(True)
90
+
91
+ async def create():
92
+ if client_class is client_module.AsyncClient:
93
+ return client_class("http://localhost:9997")
94
+ return client_class("model", "http://localhost:9997", {})
95
+
96
+ clients = [loop.run_until_complete(create())]
97
+ session = clients[0].session
98
+ closed_on = []
99
+ original_close = session.close
100
+
101
+ async def close():
102
+ closed_on.append(asyncio.get_running_loop())
103
+ await original_close()
104
+
105
+ monkeypatch.setattr(session, "close", close)
106
+
107
+ async def drop():
108
+ clients.clear()
109
+ gc.collect()
110
+ await asyncio.sleep(0)
111
+
112
+ try:
113
+ if other_thread:
114
+ thread = threading.Thread(target=lambda: asyncio.run(drop()))
115
+ thread.start()
116
+ thread.join()
117
+ else:
118
+ clients.clear()
119
+ gc.collect()
120
+ assert not session.closed
121
+ loop.run_until_complete(asyncio.sleep(0))
122
+ assert session.closed
123
+ assert closed_on == [loop]
124
+ finally:
125
+ loop.run_until_complete(original_close())
126
+ loop.close()
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-08-15T14:17:59+0800",
11
+ "date": "2026-09-11T20:38:48+0800",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "3ed6a1c10c0b3793ebe05c29c6cc6ba331af318b",
15
- "version": "3.2.0"
14
+ "full-revisionid": "9ed05c51a3b9121ab6c7dac6f161f50fd4e28755",
15
+ "version": "3.4.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -12,10 +12,26 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import base64
15
16
  import json
17
+ import os
16
18
  from typing import Any, AsyncIterator, Iterator, Union
17
19
 
18
20
 
21
+ def encode_world_reference(reference: Union[str, bytes], media_type: str) -> str:
22
+ if isinstance(reference, str) and not os.path.isfile(reference):
23
+ return reference
24
+ if isinstance(reference, str):
25
+ with open(reference, "rb") as reference_file:
26
+ data = reference_file.read()
27
+ elif isinstance(reference, bytes):
28
+ data = reference
29
+ else:
30
+ raise TypeError("World generation references must be strings or bytes")
31
+ encoded = base64.b64encode(data).decode("ascii")
32
+ return f"data:{media_type};base64,{encoded}"
33
+
34
+
19
35
  def convert_float_to_int_or_str(model_size: float) -> Union[int, str]:
20
36
  """convert float to int or string
21
37
 
@@ -64,7 +80,7 @@ def streaming_response_iterator(
64
80
 
65
81
 
66
82
  async def async_streaming_response_iterator(
67
- response_lines: AsyncIterator[bytes],
83
+ response_or_lines: Any,
68
84
  ) -> AsyncIterator[Any]:
69
85
  """
70
86
  Create an AsyncIterator to handle the streaming type of generation.
@@ -76,8 +92,8 @@ async def async_streaming_response_iterator(
76
92
 
77
93
  Parameters
78
94
  ----------
79
- response_lines: AsyncIterator[bytes]
80
- Generated lines by the Model Generator.
95
+ response_or_lines: aiohttp.ClientResponse or AsyncIterator[bytes]
96
+ The owning HTTP response, or generated lines for compatibility.
81
97
 
82
98
  Returns
83
99
  -------
@@ -86,14 +102,26 @@ async def async_streaming_response_iterator(
86
102
 
87
103
  """
88
104
 
89
- async for line in response_lines:
90
- line = line.strip()
91
- if line.startswith(b"data:"):
92
- json_str = line[len(b"data:") :].strip()
93
- if json_str == b"[DONE]":
94
- continue
95
- data = json.loads(json_str.decode("utf-8"))
96
- error = data.get("error", None)
97
- if error is not None:
98
- raise Exception(str(error))
99
- yield data
105
+ response = (
106
+ response_or_lines
107
+ if hasattr(response_or_lines, "content")
108
+ and hasattr(response_or_lines, "release")
109
+ else None
110
+ )
111
+ response_lines = response.content if response is not None else response_or_lines
112
+ try:
113
+ async for line in response_lines:
114
+ line = line.strip()
115
+ if line.startswith(b"data:"):
116
+ json_str = line[len(b"data:") :].strip()
117
+ if json_str == b"[DONE]":
118
+ continue
119
+ data = json.loads(json_str.decode("utf-8"))
120
+ error = data.get("error", None)
121
+ if error is not None:
122
+ raise Exception(str(error))
123
+ yield data
124
+ finally:
125
+ if response is not None:
126
+ response.release()
127
+ await response.wait_for_close()
@@ -18,7 +18,11 @@ from urllib.parse import quote
18
18
 
19
19
  import aiohttp
20
20
 
21
- from ..common import async_streaming_response_iterator, convert_float_to_int_or_str
21
+ from ..common import (
22
+ async_streaming_response_iterator,
23
+ convert_float_to_int_or_str,
24
+ encode_world_reference,
25
+ )
22
26
 
23
27
  if TYPE_CHECKING:
24
28
  from ...types import (
@@ -66,6 +70,25 @@ async def _release_response(response: aiohttp.ClientResponse):
66
70
  await response.wait_for_close()
67
71
 
68
72
 
73
+ def _schedule_session_close(client: Any) -> None:
74
+ if not getattr(client, "session", None):
75
+ return
76
+ loop = getattr(client, "_session_loop", None)
77
+ if loop is None or loop.is_closed():
78
+ return
79
+
80
+ def close_on_owner_loop():
81
+ loop.create_task(client.close())
82
+
83
+ try:
84
+ # Also queues cleanup while a manually driven loop is paused, and when
85
+ # the last reference is released by another thread.
86
+ loop.call_soon_threadsafe(close_on_owner_loop)
87
+ except RuntimeError:
88
+ # The owning loop may have closed since the check above.
89
+ pass
90
+
91
+
69
92
  class AsyncRESTfulModelHandle:
70
93
  """
71
94
  A sync model interface (for RESTful client) which provides type hints that makes it much easier to use xinference
@@ -77,6 +100,7 @@ class AsyncRESTfulModelHandle:
77
100
  self._base_url = base_url
78
101
  self.auth_headers = auth_headers
79
102
  self.timeout = aiohttp.ClientTimeout(total=1800)
103
+ self._session_loop = asyncio.get_running_loop()
80
104
  self.session = aiohttp.ClientSession(
81
105
  connector=aiohttp.TCPConnector(force_close=True)
82
106
  )
@@ -88,23 +112,32 @@ class AsyncRESTfulModelHandle:
88
112
  self.session = None
89
113
 
90
114
  def __del__(self):
91
- if self.session:
92
- loop = asyncio.get_event_loop()
93
- loop.create_task(self.close())
115
+ _schedule_session_close(self)
94
116
 
95
117
 
96
118
  class AsyncRESTfulEmbeddingModelHandle(AsyncRESTfulModelHandle):
97
119
  async def create_embedding(
98
- self, input: Union[str, List[str]], **kwargs
120
+ self,
121
+ input: Union[
122
+ str,
123
+ List[str],
124
+ List[int],
125
+ List[List[int]],
126
+ Dict[str, Any],
127
+ List[Dict[str, Any]],
128
+ List[Union[str, Dict[str, Any]]],
129
+ ],
130
+ **kwargs,
99
131
  ) -> "Embedding":
100
132
  """
101
133
  Create an Embedding from user input via RESTful APIs.
102
134
 
103
135
  Parameters
104
136
  ----------
105
- input: Union[str, List[str]]
106
- Input text to embed, encoded as a string or array of tokens.
107
- To embed multiple inputs in a single request, pass an array of strings or array of token arrays.
137
+ input: Union[str, List[str], List[int], List[List[int]], Dict[str, Any],
138
+ List[Dict[str, Any]], List[Union[str, Dict[str, Any]]]]
139
+ Text, token IDs, or multimodal input. Multimodal dictionaries may
140
+ contain text, image, video, or interleaved role/content messages.
108
141
 
109
142
  Returns
110
143
  -------
@@ -179,8 +212,8 @@ class AsyncRESTfulEmbeddingModelHandle(AsyncRESTfulModelHandle):
179
212
  class AsyncRESTfulRerankModelHandle(AsyncRESTfulModelHandle):
180
213
  async def rerank(
181
214
  self,
182
- documents: List[str],
183
- query: str,
215
+ documents: List[Union[str, Dict[str, Any]]],
216
+ query: Union[str, Dict[str, Any]],
184
217
  top_n: Optional[int] = None,
185
218
  max_chunks_per_doc: Optional[int] = None,
186
219
  return_documents: Optional[bool] = None,
@@ -192,10 +225,10 @@ class AsyncRESTfulRerankModelHandle(AsyncRESTfulModelHandle):
192
225
 
193
226
  Parameters
194
227
  ----------
195
- query: str
196
- The search query
197
- documents: List[str]
198
- The documents to rerank
228
+ query: Union[str, Dict[str, Any]]
229
+ Text or multimodal query.
230
+ documents: List[Union[str, Dict[str, Any]]]
231
+ Text or multimodal documents to rerank.
199
232
  top_n: int
200
233
  The number of results to return, defaults to returning all results
201
234
  max_chunks_per_doc: int
@@ -795,6 +828,52 @@ class AsyncRESTfulVideoModelHandle(AsyncRESTfulModelHandle):
795
828
  return response_data
796
829
 
797
830
 
831
+ class AsyncRESTfulWorldModelHandle(AsyncRESTfulModelHandle):
832
+ async def generate(
833
+ self,
834
+ prompt: str,
835
+ image: Optional[Union[str, bytes]] = None,
836
+ video: Optional[Union[str, bytes]] = None,
837
+ generation_config: Optional[Dict[str, Any]] = None,
838
+ **model_kwargs,
839
+ ) -> "VideoList":
840
+ """Generate a world video from text and an optional image or video."""
841
+ if image is not None and video is not None:
842
+ raise ValueError("Only one of image and video may be provided")
843
+ encoded_image = (
844
+ await asyncio.to_thread(encode_world_reference, image, "image/png")
845
+ if image is not None
846
+ else None
847
+ )
848
+ encoded_video = (
849
+ await asyncio.to_thread(encode_world_reference, video, "video/mp4")
850
+ if video is not None
851
+ else None
852
+ )
853
+ request_body = {
854
+ "model": self._model_uid,
855
+ "prompt": prompt,
856
+ "image": encoded_image,
857
+ "video": encoded_video,
858
+ "generation_config": generation_config or {},
859
+ "extra_body": model_kwargs,
860
+ }
861
+ response = await self.session.post(
862
+ f"{self._base_url}/v1/worlds/generations",
863
+ json=request_body,
864
+ headers=self.auth_headers,
865
+ timeout=self.timeout,
866
+ )
867
+ if response.status != 200:
868
+ raise RuntimeError(
869
+ "Failed to generate the world, detail: "
870
+ f"{await _get_error_string(response)}"
871
+ )
872
+ response_data = await response.json()
873
+ await _release_response(response)
874
+ return response_data
875
+
876
+
798
877
  class AsyncRESTfulGenerateModelHandle(AsyncRESTfulModelHandle):
799
878
  async def generate(
800
879
  self,
@@ -844,7 +923,7 @@ class AsyncRESTfulGenerateModelHandle(AsyncRESTfulModelHandle):
844
923
  )
845
924
 
846
925
  if stream:
847
- return async_streaming_response_iterator(response.content)
926
+ return async_streaming_response_iterator(response)
848
927
  response_data = await response.json()
849
928
  await _release_response(response)
850
929
  return response_data
@@ -925,7 +1004,7 @@ class AsyncRESTfulChatModelHandle(AsyncRESTfulGenerateModelHandle):
925
1004
  )
926
1005
 
927
1006
  if stream:
928
- return async_streaming_response_iterator(response.content)
1007
+ return async_streaming_response_iterator(response)
929
1008
 
930
1009
  response_data = await response.json()
931
1010
  await _release_response(response)
@@ -1216,6 +1295,7 @@ class AsyncClient:
1216
1295
  self._headers: Dict[str, str] = {}
1217
1296
  self._cluster_authed = False
1218
1297
  self.timeout = aiohttp.ClientTimeout(total=1800)
1298
+ self._session_loop = asyncio.get_running_loop()
1219
1299
  self.session = aiohttp.ClientSession(
1220
1300
  connector=aiohttp.TCPConnector(force_close=True), timeout=self.timeout
1221
1301
  )
@@ -1230,9 +1310,7 @@ class AsyncClient:
1230
1310
  self.session = None
1231
1311
 
1232
1312
  def __del__(self):
1233
- if self.session:
1234
- loop = asyncio.get_event_loop()
1235
- loop.create_task(self.close())
1313
+ _schedule_session_close(self)
1236
1314
 
1237
1315
  def _set_token(self, token: Optional[str]):
1238
1316
  if not self._cluster_authed or token is None:
@@ -1345,6 +1423,10 @@ class AsyncClient:
1345
1423
  replica_config: Optional[List[Dict]] = None,
1346
1424
  model_path: Optional[str] = None,
1347
1425
  enable_thinking: Optional[bool] = None,
1426
+ enable_virtual_env: Optional[bool] = None,
1427
+ virtual_env_packages: Optional[List[str]] = None,
1428
+ envs: Optional[Dict[str, str]] = None,
1429
+ virtual_env_find_links: Optional[List[str]] = None,
1348
1430
  **kwargs,
1349
1431
  ) -> str:
1350
1432
  """
@@ -1357,7 +1439,7 @@ class AsyncClient:
1357
1439
  model_type: str
1358
1440
  type of model.
1359
1441
  model_engine: Optional[str]
1360
- Specify the inference engine of the model when launching LLM.
1442
+ Specify the inference engine to use when launching the model.
1361
1443
  model_uid: str
1362
1444
  UID of model, auto generate a UUID if is None.
1363
1445
  model_size_in_billions: Optional[Union[int, str, float]]
@@ -1396,6 +1478,14 @@ class AsyncClient:
1396
1478
  enable_thinking: Optional[bool]
1397
1479
  Enable or disable thinking mode for hybrid reasoning LLMs (e.g., Qwen3). None uses
1398
1480
  the model default.
1481
+ enable_virtual_env: Optional[bool]
1482
+ If enable virtual env.
1483
+ virtual_env_packages: Optional[List[str]]
1484
+ Packages to specify in virtual env, can be used to override builtin packages in virtual env.
1485
+ virtual_env_find_links: Optional[List[str]]
1486
+ Worker-local wheel directories to use when installing virtual env packages.
1487
+ envs: Optional[Dict[str, str]]
1488
+ Environment variables to pass when launching model.
1399
1489
  **kwargs:
1400
1490
  Any other parameters been specified. e.g. multimodal_projector for multimodal inference with the llama.cpp backend.
1401
1491
 
@@ -1430,6 +1520,10 @@ class AsyncClient:
1430
1520
  "replica_config": replica_config,
1431
1521
  "model_path": model_path,
1432
1522
  "enable_thinking": enable_thinking,
1523
+ "enable_virtual_env": enable_virtual_env,
1524
+ "virtual_env_packages": virtual_env_packages,
1525
+ "virtual_env_find_links": virtual_env_find_links,
1526
+ "envs": envs,
1433
1527
  }
1434
1528
 
1435
1529
  wait_ready = kwargs.pop("wait_ready", True)
@@ -1524,15 +1618,18 @@ class AsyncClient:
1524
1618
  async def add_model_replica(
1525
1619
  self,
1526
1620
  model_uid: str,
1527
- replica_config: Optional[dict] = None,
1621
+ replica_config: Optional[Union[dict, List[dict]]] = None,
1622
+ replica: int = 1,
1623
+ model_engine: Optional[str] = None,
1624
+ n_gpu: Optional[Union[int, str]] = None,
1528
1625
  ) -> dict:
1529
- """Add a new replica to a running model (scale-up).
1626
+ """Add one or more replicas to a running model (scale-up).
1530
1627
 
1531
1628
  Parameters
1532
1629
  ----------
1533
1630
  model_uid : str
1534
1631
  The UID of the running model to extend.
1535
- replica_config : Optional[dict]
1632
+ replica_config : Optional[Union[dict, List[dict]]]
1536
1633
  Optional single-device placement config, e.g.::
1537
1634
 
1538
1635
  {
@@ -1543,16 +1640,29 @@ class AsyncClient:
1543
1640
  }
1544
1641
 
1545
1642
  Omit to let the supervisor auto-select a worker and GPU.
1643
+ replica : int
1644
+ Number of replicas to add, default is 1.
1645
+ model_engine : Optional[str]
1646
+ Override the model engine for the new replicas only.
1647
+ n_gpu : Optional[Union[int, str]]
1648
+ Override GPU usage for the new replicas. Use 0 for CPU or ``"auto"``.
1546
1649
 
1547
1650
  Returns
1548
1651
  -------
1549
1652
  dict
1550
- ``{"replica_id": int, "replica_model_uid": str, "worker_address": str}``
1653
+ A single-replica result, or ``{"replica": int, "replicas": list}``
1654
+ when more than one replica is requested.
1551
1655
  """
1552
1656
  url = f"{self.base_url}/v1/models/{model_uid}/replicas"
1553
1657
  payload: Dict[str, Any] = {}
1554
1658
  if replica_config is not None:
1555
1659
  payload["replica_config"] = replica_config
1660
+ if replica != 1:
1661
+ payload["replica"] = replica
1662
+ if model_engine is not None:
1663
+ payload["model_engine"] = model_engine
1664
+ if n_gpu is not None:
1665
+ payload["n_gpu"] = n_gpu
1556
1666
  response = await self.session.post(url, json=payload, headers=self._headers)
1557
1667
  if response.status != 200:
1558
1668
  raise RuntimeError(
@@ -1715,6 +1825,10 @@ class AsyncClient:
1715
1825
  return AsyncRESTfulVideoModelHandle(
1716
1826
  model_uid, self.base_url, auth_headers=self._headers
1717
1827
  )
1828
+ elif desc["model_type"] == "world":
1829
+ return AsyncRESTfulWorldModelHandle(
1830
+ model_uid, self.base_url, auth_headers=self._headers
1831
+ )
1718
1832
  elif desc["model_type"] == "flexible":
1719
1833
  return AsyncRESTfulFlexibleModelHandle(
1720
1834
  model_uid, self.base_url, auth_headers=self._headers
@@ -17,7 +17,11 @@ from urllib.parse import quote
17
17
 
18
18
  import requests
19
19
 
20
- from ..common import convert_float_to_int_or_str, streaming_response_iterator
20
+ from ..common import (
21
+ convert_float_to_int_or_str,
22
+ encode_world_reference,
23
+ streaming_response_iterator,
24
+ )
21
25
 
22
26
  if TYPE_CHECKING:
23
27
  from ...types import (
@@ -72,15 +76,28 @@ class RESTfulModelHandle:
72
76
 
73
77
 
74
78
  class RESTfulEmbeddingModelHandle(RESTfulModelHandle):
75
- def create_embedding(self, input: Union[str, List[str]], **kwargs) -> "Embedding":
79
+ def create_embedding(
80
+ self,
81
+ input: Union[
82
+ str,
83
+ List[str],
84
+ List[int],
85
+ List[List[int]],
86
+ Dict[str, Any],
87
+ List[Dict[str, Any]],
88
+ List[Union[str, Dict[str, Any]]],
89
+ ],
90
+ **kwargs,
91
+ ) -> "Embedding":
76
92
  """
77
93
  Create an Embedding from user input via RESTful APIs.
78
94
 
79
95
  Parameters
80
96
  ----------
81
- input: Union[str, List[str]]
82
- Input text to embed, encoded as a string or array of tokens.
83
- To embed multiple inputs in a single request, pass an array of strings or array of token arrays.
97
+ input: Union[str, List[str], List[int], List[List[int]], Dict[str, Any],
98
+ List[Dict[str, Any]], List[Union[str, Dict[str, Any]]]]
99
+ Text, token IDs, or multimodal input. Multimodal dictionaries may
100
+ contain text, image, video, or interleaved role/content messages.
84
101
 
85
102
  Returns
86
103
  -------
@@ -149,8 +166,8 @@ class RESTfulEmbeddingModelHandle(RESTfulModelHandle):
149
166
  class RESTfulRerankModelHandle(RESTfulModelHandle):
150
167
  def rerank(
151
168
  self,
152
- documents: List[str],
153
- query: str,
169
+ documents: List[Union[str, Dict[str, Any]]],
170
+ query: Union[str, Dict[str, Any]],
154
171
  top_n: Optional[int] = None,
155
172
  max_chunks_per_doc: Optional[int] = None,
156
173
  return_documents: Optional[bool] = None,
@@ -162,10 +179,10 @@ class RESTfulRerankModelHandle(RESTfulModelHandle):
162
179
 
163
180
  Parameters
164
181
  ----------
165
- query: str
166
- The search query
167
- documents: List[str]
168
- The documents to rerank
182
+ query: Union[str, Dict[str, Any]]
183
+ Text or multimodal query.
184
+ documents: List[Union[str, Dict[str, Any]]]
185
+ Text or multimodal documents to rerank.
169
186
  top_n: int
170
187
  The number of results to return, defaults to returning all results
171
188
  max_chunks_per_doc: int
@@ -710,6 +727,47 @@ class RESTfulVideoModelHandle(RESTfulModelHandle):
710
727
  return response_data
711
728
 
712
729
 
730
+ class RESTfulWorldModelHandle(RESTfulModelHandle):
731
+ def generate(
732
+ self,
733
+ prompt: str,
734
+ image: Optional[Union[str, bytes]] = None,
735
+ video: Optional[Union[str, bytes]] = None,
736
+ generation_config: Optional[Dict[str, Any]] = None,
737
+ **model_kwargs,
738
+ ) -> "VideoList":
739
+ """Generate a world video from text and an optional image or video."""
740
+ if image is not None and video is not None:
741
+ raise ValueError("Only one of image and video may be provided")
742
+ request_body = {
743
+ "model": self._model_uid,
744
+ "prompt": prompt,
745
+ "image": (
746
+ encode_world_reference(image, "image/png")
747
+ if image is not None
748
+ else None
749
+ ),
750
+ "video": (
751
+ encode_world_reference(video, "video/mp4")
752
+ if video is not None
753
+ else None
754
+ ),
755
+ "generation_config": generation_config or {},
756
+ "extra_body": model_kwargs,
757
+ }
758
+ response = self.session.post(
759
+ f"{self._base_url}/v1/worlds/generations",
760
+ json=request_body,
761
+ headers=self.auth_headers,
762
+ )
763
+ if response.status_code != 200:
764
+ raise RuntimeError(
765
+ "Failed to generate the world, detail: "
766
+ f"{_get_error_string(response)}"
767
+ )
768
+ return response.json()
769
+
770
+
713
771
  class RESTfulGenerateModelHandle(RESTfulModelHandle):
714
772
  def generate(
715
773
  self,
@@ -1232,6 +1290,7 @@ class Client:
1232
1290
  enable_virtual_env: Optional[bool] = None,
1233
1291
  virtual_env_packages: Optional[List[str]] = None,
1234
1292
  envs: Optional[Dict[str, str]] = None,
1293
+ virtual_env_find_links: Optional[List[str]] = None,
1235
1294
  **kwargs,
1236
1295
  ) -> str:
1237
1296
  """
@@ -1244,7 +1303,7 @@ class Client:
1244
1303
  model_type: str
1245
1304
  type of model.
1246
1305
  model_engine: Optional[str]
1247
- Specify the inference engine of the model when launching LLM.
1306
+ Specify the inference engine to use when launching the model.
1248
1307
  model_uid: str
1249
1308
  UID of model, auto generate a UUID if is None.
1250
1309
  model_size_in_billions: Optional[Union[int, str, float]]
@@ -1287,6 +1346,8 @@ class Client:
1287
1346
  If enable virtual env.
1288
1347
  virtual_env_packages: Optional[List[str]]
1289
1348
  Packages to specify in virtual env, can be used to override builtin packages in virtual env.
1349
+ virtual_env_find_links: Optional[List[str]]
1350
+ Worker-local wheel directories to use when installing virtual env packages.
1290
1351
  envs: Optional[Dict[str, str]]
1291
1352
  Environment variables to pass when launching model.
1292
1353
 
@@ -1326,6 +1387,7 @@ class Client:
1326
1387
  "enable_thinking": enable_thinking,
1327
1388
  "enable_virtual_env": enable_virtual_env,
1328
1389
  "virtual_env_packages": virtual_env_packages,
1390
+ "virtual_env_find_links": virtual_env_find_links,
1329
1391
  "envs": envs,
1330
1392
  }
1331
1393
 
@@ -1411,15 +1473,18 @@ class Client:
1411
1473
  def add_model_replica(
1412
1474
  self,
1413
1475
  model_uid: str,
1414
- replica_config: Optional[dict] = None,
1476
+ replica_config: Optional[Union[dict, List[dict]]] = None,
1477
+ replica: int = 1,
1478
+ model_engine: Optional[str] = None,
1479
+ n_gpu: Optional[Union[int, str]] = None,
1415
1480
  ) -> dict:
1416
- """Add a new replica to a running model (scale-up).
1481
+ """Add one or more replicas to a running model (scale-up).
1417
1482
 
1418
1483
  Parameters
1419
1484
  ----------
1420
1485
  model_uid : str
1421
1486
  The UID of the running model to extend.
1422
- replica_config : Optional[dict]
1487
+ replica_config : Optional[Union[dict, List[dict]]]
1423
1488
  Optional single-device placement config, e.g.::
1424
1489
 
1425
1490
  {
@@ -1430,16 +1495,29 @@ class Client:
1430
1495
  }
1431
1496
 
1432
1497
  Omit to let the supervisor auto-select a worker and GPU.
1498
+ replica : int
1499
+ Number of replicas to add, default is 1.
1500
+ model_engine : Optional[str]
1501
+ Override the model engine for the new replicas only.
1502
+ n_gpu : Optional[Union[int, str]]
1503
+ Override GPU usage for the new replicas. Use 0 for CPU or ``"auto"``.
1433
1504
 
1434
1505
  Returns
1435
1506
  -------
1436
1507
  dict
1437
- ``{"replica_id": int, "replica_model_uid": str, "worker_address": str}``
1508
+ A single-replica result, or ``{"replica": int, "replicas": list}``
1509
+ when more than one replica is requested.
1438
1510
  """
1439
1511
  url = f"{self.base_url}/v1/models/{model_uid}/replicas"
1440
1512
  payload: Dict[str, Any] = {}
1441
1513
  if replica_config is not None:
1442
1514
  payload["replica_config"] = replica_config
1515
+ if replica != 1:
1516
+ payload["replica"] = replica
1517
+ if model_engine is not None:
1518
+ payload["model_engine"] = model_engine
1519
+ if n_gpu is not None:
1520
+ payload["n_gpu"] = n_gpu
1443
1521
  response = self.session.post(url, json=payload, headers=self._headers)
1444
1522
  if response.status_code != 200:
1445
1523
  raise RuntimeError(
@@ -1594,6 +1672,10 @@ class Client:
1594
1672
  return RESTfulVideoModelHandle(
1595
1673
  model_uid, self.base_url, auth_headers=self._headers
1596
1674
  )
1675
+ elif desc["model_type"] == "world":
1676
+ return RESTfulWorldModelHandle(
1677
+ model_uid, self.base_url, auth_headers=self._headers
1678
+ )
1597
1679
  elif desc["model_type"] == "flexible":
1598
1680
  return RESTfulFlexibleModelHandle(
1599
1681
  model_uid, self.base_url, auth_headers=self._headers
@@ -201,6 +201,18 @@ class ToolCalls(TypedDict):
201
201
  function: ToolCallFunction
202
202
 
203
203
 
204
+ class ToolCallDeltaFunction(TypedDict, total=False):
205
+ name: str
206
+ arguments: str
207
+
208
+
209
+ class ToolCallDelta(TypedDict):
210
+ index: int
211
+ id: NotRequired[str]
212
+ type: NotRequired[Literal["function"]]
213
+ function: NotRequired[ToolCallDeltaFunction]
214
+
215
+
204
216
  class CompletionChoice(TypedDict):
205
217
  text: NotRequired[str]
206
218
  index: int
@@ -209,10 +221,15 @@ class CompletionChoice(TypedDict):
209
221
  tool_calls: NotRequired[List[ToolCalls]]
210
222
 
211
223
 
224
+ class PromptTokensDetails(TypedDict):
225
+ cached_tokens: int
226
+
227
+
212
228
  class CompletionUsage(TypedDict):
213
229
  prompt_tokens: int
214
230
  completion_tokens: int
215
231
  total_tokens: int
232
+ prompt_tokens_details: NotRequired[PromptTokensDetails]
216
233
 
217
234
 
218
235
  class CompletionChunk(TypedDict):
@@ -269,7 +286,7 @@ class ChatCompletionChunkDelta(TypedDict):
269
286
  role: NotRequired[str]
270
287
  reasoning_content: NotRequired[Union[str, None]]
271
288
  content: NotRequired[Union[str, None]]
272
- tool_calls: NotRequired[List[ToolCalls]]
289
+ tool_calls: NotRequired[List[ToolCallDelta]]
273
290
 
274
291
 
275
292
  class ChatCompletionChunkChoice(TypedDict):
@@ -472,12 +489,14 @@ class CreateChatCompletion( # type: ignore
472
489
 
473
490
 
474
491
  class LoRA:
475
- def __init__(self, lora_name: str, local_path: str):
492
+ def __init__(self, lora_name: str, local_path: str, lora_scale: float = 1.0):
493
+ self.lora_scale = lora_scale
476
494
  self.lora_name = lora_name
477
495
  self.local_path = local_path
478
496
 
479
497
  def to_dict(self):
480
498
  return {
499
+ **({"lora_scale": self.lora_scale} if self.lora_scale != 1.0 else {}),
481
500
  "lora_name": self.lora_name,
482
501
  "local_path": self.local_path,
483
502
  }
@@ -485,6 +504,7 @@ class LoRA:
485
504
  @classmethod
486
505
  def from_dict(cls, data: Dict):
487
506
  return cls(
507
+ lora_scale=data.get("lora_scale", 1.0),
488
508
  lora_name=data["lora_name"],
489
509
  local_path=data["local_path"],
490
510
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xinference-client
3
- Version: 3.2.0
3
+ Version: 3.4.0
4
4
  Summary: Client for Xinference
5
5
  Home-page: https://github.com/xorbitsai/inference-client
6
6
  Author: Qin Xuye
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
19
20
  Classifier: Programming Language :: Python :: Implementation :: CPython
20
21
  Classifier: Topic :: Software Development :: Libraries
21
22
  Description-Content-Type: text/markdown
@@ -5,6 +5,8 @@ pyproject.toml
5
5
  setup.cfg
6
6
  setup.py
7
7
  versioneer.py
8
+ tests/test_async_client.py
9
+ tests/test_async_client_cleanup.py
8
10
  third_party/inference/xinference/thirdparty/matcha/utils/monotonic_align/core.pyx
9
11
  xinference_client/__init__.py
10
12
  xinference_client/_compat.py