nexaai 1.0.12rc1__cp310-cp310-win_amd64.whl → 1.0.13rc2__cp310-cp310-win_amd64.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.

Potentially problematic release.


This version of nexaai might be problematic. Click here for more details.

Binary file
nexaai/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is generated by CMake from _version.py.in
2
2
  # Do not modify this file manually - it will be overwritten
3
3
 
4
- __version__ = "1.0.12-rc1"
4
+ __version__ = "1.0.13-rc2"
nexaai/binds/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
1
  from .common_bind import *
2
2
  from .llm_bind import *
3
3
  from .embedder_bind import *
4
+ from .vlm_bind import *
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -11,7 +11,7 @@ This module provides utilities to:
11
11
  import os
12
12
  import json
13
13
  from datetime import datetime
14
- from typing import Dict, Any, List, Optional
14
+ from typing import Dict, Any, List, Optional, Union
15
15
 
16
16
  from .quantization_utils import (
17
17
  extract_quantization_from_filename,
@@ -101,7 +101,7 @@ def save_download_metadata(directory_path: str, metadata: Dict[str, Any]) -> Non
101
101
  pass
102
102
 
103
103
 
104
- def create_gguf_manifest(repo_id: str, files: List[str], directory_path: str, old_metadata: Dict[str, Any]) -> Dict[str, Any]:
104
+ def create_gguf_manifest(repo_id: str, files: List[str], directory_path: str, old_metadata: Dict[str, Any], is_mmproj: bool = False, file_name: Optional[Union[str, List[str]]] = None) -> Dict[str, Any]:
105
105
  """Create GGUF format manifest."""
106
106
 
107
107
  # Load existing manifest to merge GGUF files if it exists
@@ -111,14 +111,27 @@ def create_gguf_manifest(repo_id: str, files: List[str], directory_path: str, ol
111
111
  if existing_manifest and "ModelFile" in existing_manifest:
112
112
  model_files = existing_manifest["ModelFile"].copy()
113
113
 
114
+ # Initialize MMProjFile from existing manifest or empty
115
+ mmproj_file = {
116
+ "Name": "",
117
+ "Downloaded": False,
118
+ "Size": 0
119
+ }
120
+ if existing_manifest and "MMProjFile" in existing_manifest:
121
+ mmproj_file = existing_manifest["MMProjFile"].copy()
122
+
114
123
  # Process GGUF files
115
- for file_name in files:
116
- if file_name.endswith('.gguf'):
117
- # Use the new enum-based quantization extraction
118
- quantization_type = extract_quantization_from_filename(file_name)
119
- quant_level = quantization_type.value if quantization_type else "UNKNOWN"
124
+ for current_file_name in files:
125
+ if current_file_name.endswith('.gguf'):
126
+ # Check if this file is an mmproj file
127
+ is_current_mmproj = 'mmproj' in current_file_name.lower()
128
+
129
+ # If we're downloading specific files and this is marked as mmproj, respect that
130
+ if is_mmproj and file_name is not None:
131
+ filenames_to_check = file_name if isinstance(file_name, list) else [file_name]
132
+ is_current_mmproj = current_file_name in filenames_to_check
120
133
 
121
- file_path = os.path.join(directory_path, file_name)
134
+ file_path = os.path.join(directory_path, current_file_name)
122
135
  file_size = 0
123
136
  if os.path.exists(file_path):
124
137
  try:
@@ -126,22 +139,31 @@ def create_gguf_manifest(repo_id: str, files: List[str], directory_path: str, ol
126
139
  except (OSError, IOError):
127
140
  pass
128
141
 
129
- model_files[quant_level] = {
130
- "Name": file_name,
131
- "Downloaded": True,
132
- "Size": file_size
133
- }
142
+ if is_current_mmproj:
143
+ # This is an mmproj file, put it in MMProjFile
144
+ mmproj_file = {
145
+ "Name": current_file_name,
146
+ "Downloaded": True,
147
+ "Size": file_size
148
+ }
149
+ else:
150
+ # Regular model file, put in ModelFile
151
+ # Use the new enum-based quantization extraction
152
+ quantization_type = extract_quantization_from_filename(current_file_name)
153
+ quant_level = quantization_type.value if quantization_type else "UNKNOWN"
154
+
155
+ model_files[quant_level] = {
156
+ "Name": current_file_name,
157
+ "Downloaded": True,
158
+ "Size": file_size
159
+ }
134
160
 
135
161
  manifest = {
136
162
  "Name": repo_id,
137
163
  "ModelType": PIPELINE_TO_MODEL_TYPE.get(old_metadata.get('pipeline_tag'), "other"),
138
164
  "PluginId": "llama_cpp",
139
165
  "ModelFile": model_files,
140
- "MMProjFile": {
141
- "Name": "",
142
- "Downloaded": False,
143
- "Size": 0
144
- },
166
+ "MMProjFile": mmproj_file,
145
167
  "TokenizerFile": {
146
168
  "Name": "",
147
169
  "Downloaded": False,
@@ -157,20 +179,27 @@ def create_gguf_manifest(repo_id: str, files: List[str], directory_path: str, ol
157
179
  return manifest
158
180
 
159
181
 
160
- def create_mlx_manifest(repo_id: str, files: List[str], directory_path: str, old_metadata: Dict[str, Any]) -> Dict[str, Any]:
182
+ def create_mlx_manifest(repo_id: str, files: List[str], directory_path: str, old_metadata: Dict[str, Any], is_mmproj: bool = False, file_name: Optional[Union[str, List[str]]] = None) -> Dict[str, Any]:
161
183
  """Create MLX format manifest."""
162
184
 
163
185
  model_files = {}
164
186
  extra_files = []
165
187
 
188
+ # Initialize MMProjFile
189
+ mmproj_file = {
190
+ "Name": "",
191
+ "Downloaded": False,
192
+ "Size": 0
193
+ }
194
+
166
195
  # Try different methods to extract quantization for MLX models
167
196
  quantization_type = detect_quantization_for_mlx(repo_id, directory_path)
168
197
 
169
198
  # Use the detected quantization or default to "DEFAULT"
170
199
  quant_level = quantization_type.value if quantization_type else "DEFAULT"
171
200
 
172
- for file_name in files:
173
- file_path = os.path.join(directory_path, file_name)
201
+ for current_file_name in files:
202
+ file_path = os.path.join(directory_path, current_file_name)
174
203
  file_size = 0
175
204
  if os.path.exists(file_path):
176
205
  try:
@@ -178,17 +207,32 @@ def create_mlx_manifest(repo_id: str, files: List[str], directory_path: str, old
178
207
  except (OSError, IOError):
179
208
  pass
180
209
 
210
+ # Check if this file is an mmproj file
211
+ is_current_mmproj = 'mmproj' in current_file_name.lower()
212
+
213
+ # If we're downloading specific files and this is marked as mmproj, respect that
214
+ if is_mmproj and file_name is not None:
215
+ filenames_to_check = file_name if isinstance(file_name, list) else [file_name]
216
+ is_current_mmproj = current_file_name in filenames_to_check
217
+
218
+ if is_current_mmproj:
219
+ # This is an mmproj file, put it in MMProjFile
220
+ mmproj_file = {
221
+ "Name": current_file_name,
222
+ "Downloaded": True,
223
+ "Size": file_size
224
+ }
181
225
  # Check if this is a main model file (safetensors but not index files)
182
- if (file_name.endswith('.safetensors') and not file_name.endswith('.index.json')):
226
+ elif (current_file_name.endswith('.safetensors') and not current_file_name.endswith('.index.json')):
183
227
  model_files[quant_level] = {
184
- "Name": file_name,
228
+ "Name": current_file_name,
185
229
  "Downloaded": True,
186
230
  "Size": file_size
187
231
  }
188
232
  else:
189
233
  # Add to extra files
190
234
  extra_files.append({
191
- "Name": file_name,
235
+ "Name": current_file_name,
192
236
  "Downloaded": True,
193
237
  "Size": file_size
194
238
  })
@@ -198,11 +242,7 @@ def create_mlx_manifest(repo_id: str, files: List[str], directory_path: str, old
198
242
  "ModelType": PIPELINE_TO_MODEL_TYPE.get(old_metadata.get('pipeline_tag'), "other"),
199
243
  "PluginId": "mlx",
200
244
  "ModelFile": model_files,
201
- "MMProjFile": {
202
- "Name": "",
203
- "Downloaded": False,
204
- "Size": 0
205
- },
245
+ "MMProjFile": mmproj_file,
206
246
  "TokenizerFile": {
207
247
  "Name": "",
208
248
  "Downloaded": False,
@@ -232,7 +272,7 @@ def detect_model_type(files: List[str]) -> str:
232
272
  return "mlx"
233
273
 
234
274
 
235
- def create_manifest_from_files(repo_id: str, files: List[str], directory_path: str, old_metadata: Dict[str, Any]) -> Dict[str, Any]:
275
+ def create_manifest_from_files(repo_id: str, files: List[str], directory_path: str, old_metadata: Dict[str, Any], is_mmproj: bool = False, file_name: Optional[Union[str, List[str]]] = None) -> Dict[str, Any]:
236
276
  """
237
277
  Create appropriate manifest format based on detected model type.
238
278
 
@@ -241,6 +281,8 @@ def create_manifest_from_files(repo_id: str, files: List[str], directory_path: s
241
281
  files: List of files in the model directory
242
282
  directory_path: Path to the model directory
243
283
  old_metadata: Existing metadata (pipeline_tag, download_time, avatar_url)
284
+ is_mmproj: Whether the downloaded file is an mmproj file
285
+ file_name: The specific file(s) that were downloaded (None if entire repo was downloaded)
244
286
 
245
287
  Returns:
246
288
  Dict containing the appropriate manifest format
@@ -248,12 +290,12 @@ def create_manifest_from_files(repo_id: str, files: List[str], directory_path: s
248
290
  model_type = detect_model_type(files)
249
291
 
250
292
  if model_type == "gguf":
251
- return create_gguf_manifest(repo_id, files, directory_path, old_metadata)
293
+ return create_gguf_manifest(repo_id, files, directory_path, old_metadata, is_mmproj, file_name)
252
294
  else: # mlx or other
253
- return create_mlx_manifest(repo_id, files, directory_path, old_metadata)
295
+ return create_mlx_manifest(repo_id, files, directory_path, old_metadata, is_mmproj, file_name)
254
296
 
255
297
 
256
- def save_manifest_with_files_metadata(repo_id: str, local_dir: str, old_metadata: Dict[str, Any]) -> None:
298
+ def save_manifest_with_files_metadata(repo_id: str, local_dir: str, old_metadata: Dict[str, Any], is_mmproj: bool = False, file_name: Optional[Union[str, List[str]]] = None) -> None:
257
299
  """
258
300
  Create and save manifest based on files found in the directory.
259
301
 
@@ -261,6 +303,8 @@ def save_manifest_with_files_metadata(repo_id: str, local_dir: str, old_metadata
261
303
  repo_id: Repository ID
262
304
  local_dir: Local directory containing the model files
263
305
  old_metadata: Existing metadata to preserve
306
+ is_mmproj: Whether the downloaded file is an mmproj file
307
+ file_name: The specific file(s) that were downloaded (None if entire repo was downloaded)
264
308
  """
265
309
  # Get list of files in the directory
266
310
  files = []
@@ -274,7 +318,7 @@ def save_manifest_with_files_metadata(repo_id: str, local_dir: str, old_metadata
274
318
  pass
275
319
 
276
320
  # Create appropriate manifest
277
- manifest = create_manifest_from_files(repo_id, files, local_dir, old_metadata)
321
+ manifest = create_manifest_from_files(repo_id, files, local_dir, old_metadata, is_mmproj, file_name)
278
322
 
279
323
  # Save manifest
280
324
  save_download_metadata(local_dir, manifest)
@@ -21,6 +21,13 @@ from .manifest_utils import (
21
21
  # Default path for model storage
22
22
  DEFAULT_MODEL_SAVING_PATH = "~/.cache/nexa.ai/nexa_sdk/models/"
23
23
 
24
+
25
+ @dataclass
26
+ class MMProjInfo:
27
+ """Data class for mmproj file information."""
28
+ mmproj_path: Optional[str] = None
29
+ size: int = 0
30
+
24
31
  @dataclass
25
32
  class DownloadedModel:
26
33
  """Data class representing a downloaded model with all its metadata."""
@@ -34,6 +41,7 @@ class DownloadedModel:
34
41
  pipeline_tag: Optional[str] = None # Pipeline tag from HuggingFace model info
35
42
  download_time: Optional[str] = None # ISO format timestamp of download
36
43
  avatar_url: Optional[str] = None # Avatar URL for the model author
44
+ mmproj_info: Optional[MMProjInfo] = None # mmproj file information
37
45
 
38
46
  def to_dict(self) -> Dict[str, Any]:
39
47
  """Convert to dictionary format for backward compatibility."""
@@ -47,7 +55,11 @@ class DownloadedModel:
47
55
  'full_repo_download_complete': self.full_repo_download_complete,
48
56
  'pipeline_tag': self.pipeline_tag,
49
57
  'download_time': self.download_time,
50
- 'avatar_url': self.avatar_url
58
+ 'avatar_url': self.avatar_url,
59
+ 'mmproj_info': {
60
+ 'mmproj_path': self.mmproj_info.mmproj_path,
61
+ 'size': self.mmproj_info.size
62
+ } if self.mmproj_info else None
51
63
  }
52
64
  return result
53
65
 
@@ -125,6 +137,46 @@ def _has_valid_metadata(directory_path: str) -> bool:
125
137
  return os.path.exists(manifest_path) or os.path.exists(old_metadata_path)
126
138
 
127
139
 
140
+ def _extract_mmproj_info(manifest: Dict[str, Any], local_path: str) -> Optional[MMProjInfo]:
141
+ """
142
+ Extract mmproj information from manifest data.
143
+
144
+ Args:
145
+ manifest: Dictionary containing manifest data
146
+ local_path: Local path to the model directory
147
+
148
+ Returns:
149
+ MMProjInfo object if mmproj file exists, None otherwise
150
+ """
151
+ # Check if manifest has MMProjFile information
152
+ mmproj_file_info = manifest.get('MMProjFile')
153
+ if not mmproj_file_info or not mmproj_file_info.get('Downloaded') or not mmproj_file_info.get('Name'):
154
+ return None
155
+
156
+ mmproj_filename = mmproj_file_info.get('Name', '')
157
+ if not mmproj_filename:
158
+ return None
159
+
160
+ # Construct full path to mmproj file
161
+ mmproj_path = os.path.join(local_path, mmproj_filename)
162
+
163
+ # Get size from manifest, but verify file exists
164
+ mmproj_size = mmproj_file_info.get('Size', 0)
165
+ if os.path.exists(mmproj_path):
166
+ try:
167
+ # Verify size matches actual file size
168
+ actual_size = os.path.getsize(mmproj_path)
169
+ mmproj_size = actual_size # Use actual size if different
170
+ except (OSError, IOError):
171
+ # If we can't get actual size, use size from manifest
172
+ pass
173
+ else:
174
+ # File doesn't exist, don't include mmproj info
175
+ return None
176
+
177
+ return MMProjInfo(mmproj_path=mmproj_path, size=mmproj_size)
178
+
179
+
128
180
  def _scan_for_repo_folders(base_path: str) -> List[DownloadedModel]:
129
181
  """Scan a directory for repository folders and return model information."""
130
182
  models = []
@@ -159,6 +211,10 @@ def _scan_for_repo_folders(base_path: str) -> List[DownloadedModel]:
159
211
  # Load metadata if it exists
160
212
  repo_id = f"{item}/{subitem}"
161
213
  metadata = load_download_metadata(subitem_path, repo_id)
214
+
215
+ # Extract mmproj information
216
+ mmproj_info = _extract_mmproj_info(metadata, subitem_path)
217
+
162
218
  models.append(DownloadedModel(
163
219
  repo_id=repo_id,
164
220
  files=files,
@@ -169,7 +225,8 @@ def _scan_for_repo_folders(base_path: str) -> List[DownloadedModel]:
169
225
  full_repo_download_complete=download_complete,
170
226
  pipeline_tag=metadata.get('pipeline_tag'),
171
227
  download_time=metadata.get('download_time'),
172
- avatar_url=metadata.get('avatar_url')
228
+ avatar_url=metadata.get('avatar_url'),
229
+ mmproj_info=mmproj_info
173
230
  ))
174
231
  else:
175
232
  direct_files.append(subitem)
@@ -188,6 +245,10 @@ def _scan_for_repo_folders(base_path: str) -> List[DownloadedModel]:
188
245
  # Load metadata if it exists
189
246
  repo_id = item
190
247
  metadata = load_download_metadata(item_path, repo_id)
248
+
249
+ # Extract mmproj information
250
+ mmproj_info = _extract_mmproj_info(metadata, item_path)
251
+
191
252
  models.append(DownloadedModel(
192
253
  repo_id=repo_id,
193
254
  files=files,
@@ -198,7 +259,8 @@ def _scan_for_repo_folders(base_path: str) -> List[DownloadedModel]:
198
259
  full_repo_download_complete=download_complete,
199
260
  pipeline_tag=metadata.get('pipeline_tag'),
200
261
  download_time=metadata.get('download_time'),
201
- avatar_url=metadata.get('avatar_url')
262
+ avatar_url=metadata.get('avatar_url'),
263
+ mmproj_info=mmproj_info
202
264
  ))
203
265
 
204
266
  except (OSError, IOError):
@@ -236,6 +298,7 @@ def list_downloaded_models(local_dir: Optional[str] = None) -> List[DownloadedMo
236
298
  - pipeline_tag: Optional[str] - Pipeline tag from HuggingFace model info
237
299
  - download_time: Optional[str] - ISO format timestamp when the model was downloaded
238
300
  - avatar_url: Optional[str] - Avatar URL for the model author
301
+ - mmproj_info: Optional[MMProjInfo] - mmproj file information with mmproj_path and size
239
302
  """
240
303
 
241
304
  # Set up local directory
@@ -727,7 +790,7 @@ class HuggingFaceDownloader:
727
790
  # If no expected size, just check that file is not empty
728
791
  return os.path.getsize(file_path) > 0
729
792
 
730
- def _fetch_and_save_metadata(self, repo_id: str, local_dir: str) -> None:
793
+ def _fetch_and_save_metadata(self, repo_id: str, local_dir: str, is_mmproj: bool = False, file_name: Optional[Union[str, List[str]]] = None) -> None:
731
794
  """Fetch model info and save metadata after successful download."""
732
795
  # Initialize metadata with defaults to ensure manifest is always created
733
796
  old_metadata = {
@@ -757,7 +820,7 @@ class HuggingFaceDownloader:
757
820
 
758
821
  # CRITICAL: Always create the manifest file, regardless of metadata fetch failures
759
822
  try:
760
- save_manifest_with_files_metadata(repo_id, local_dir, old_metadata)
823
+ save_manifest_with_files_metadata(repo_id, local_dir, old_metadata, is_mmproj, file_name)
761
824
  print(f"[OK] Successfully created nexa.manifest for {repo_id}")
762
825
  except Exception as e:
763
826
  # This is critical - if manifest creation fails, we should know about it
@@ -819,7 +882,7 @@ class HuggingFaceDownloader:
819
882
  progress_tracker.stop_tracking()
820
883
 
821
884
  # Save metadata after successful download
822
- self._fetch_and_save_metadata(repo_id, file_local_dir)
885
+ self._fetch_and_save_metadata(repo_id, file_local_dir, self._current_is_mmproj, self._current_file_name)
823
886
 
824
887
  return downloaded_path
825
888
 
@@ -864,7 +927,7 @@ class HuggingFaceDownloader:
864
927
  progress_tracker.stop_tracking()
865
928
 
866
929
  # Save metadata after successful download
867
- self._fetch_and_save_metadata(repo_id, repo_local_dir)
930
+ self._fetch_and_save_metadata(repo_id, repo_local_dir, self._current_is_mmproj, self._current_file_name)
868
931
 
869
932
  return downloaded_path
870
933
 
@@ -926,7 +989,7 @@ class HuggingFaceDownloader:
926
989
  progress_tracker.stop_tracking()
927
990
 
928
991
  # Save metadata after successful download
929
- self._fetch_and_save_metadata(repo_id, repo_local_dir)
992
+ self._fetch_and_save_metadata(repo_id, repo_local_dir, self._current_is_mmproj, self._current_file_name)
930
993
 
931
994
  return repo_local_dir
932
995
 
@@ -951,7 +1014,8 @@ class HuggingFaceDownloader:
951
1014
  local_dir: Optional[str] = None,
952
1015
  progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
953
1016
  show_progress: bool = True,
954
- force_download: bool = False
1017
+ force_download: bool = False,
1018
+ is_mmproj: bool = False
955
1019
  ) -> str:
956
1020
  """
957
1021
  Main download method that handles all download scenarios.
@@ -970,6 +1034,10 @@ class HuggingFaceDownloader:
970
1034
  # Validate and normalize parameters
971
1035
  repo_id, file_name = self._validate_and_setup_params(repo_id, file_name)
972
1036
 
1037
+ # Store parameters as instance variables for use in _fetch_and_save_metadata
1038
+ self._current_is_mmproj = is_mmproj
1039
+ self._current_file_name = file_name
1040
+
973
1041
  # Set up local directory
974
1042
  local_dir = self._created_dir_if_not_exists(local_dir)
975
1043
 
@@ -1038,7 +1106,8 @@ def download_from_huggingface(
1038
1106
  show_progress: bool = True,
1039
1107
  token: Union[bool, str, None] = None,
1040
1108
  custom_endpoint: Optional[str] = None,
1041
- force_download: bool = False
1109
+ force_download: bool = False,
1110
+ is_mmproj: Optional[bool] = None
1042
1111
  ) -> str:
1043
1112
  """
1044
1113
  Download models or files from HuggingFace Hub or custom mirror endpoints.
@@ -1065,6 +1134,9 @@ def download_from_huggingface(
1065
1134
  The endpoint will be used to initialize HfApi for all downloads.
1066
1135
  force_download (bool, optional): If True, download files even if they already exist locally.
1067
1136
  Default False (skip existing files).
1137
+ is_mmproj (bool, optional): Whether the file being downloaded is an mmproj file. Only used when
1138
+ file_name is not None. If None, defaults to True if 'mmproj' is in
1139
+ the filename, False otherwise.
1068
1140
 
1069
1141
  Returns:
1070
1142
  str: Path to the downloaded file or directory
@@ -1101,6 +1173,15 @@ def download_from_huggingface(
1101
1173
  }
1102
1174
  }
1103
1175
  """
1176
+ # Set default value for is_mmproj based on filename if not explicitly provided
1177
+ if is_mmproj is None and file_name is not None:
1178
+ # Check if any filename contains 'mmproj'
1179
+ filenames_to_check = file_name if isinstance(file_name, list) else [file_name]
1180
+ is_mmproj = any('mmproj' in filename.lower() for filename in filenames_to_check)
1181
+ elif is_mmproj is None:
1182
+ # Default to False if no file_name is provided
1183
+ is_mmproj = False
1184
+
1104
1185
  # Create downloader instance with custom endpoint if provided
1105
1186
  downloader = HuggingFaceDownloader(
1106
1187
  endpoint=custom_endpoint,
@@ -1115,7 +1196,8 @@ def download_from_huggingface(
1115
1196
  local_dir=local_dir,
1116
1197
  progress_callback=progress_callback,
1117
1198
  show_progress=show_progress,
1118
- force_download=force_download
1199
+ force_download=force_download,
1200
+ is_mmproj=is_mmproj
1119
1201
  )
1120
1202
 
1121
1203
 
@@ -1124,15 +1206,66 @@ def download_from_huggingface(
1124
1206
  ##########################################################################
1125
1207
 
1126
1208
 
1209
+ def _download_model_if_needed(
1210
+ model_path: str,
1211
+ param_name: str,
1212
+ progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
1213
+ token: Union[bool, str, None] = None,
1214
+ is_mmproj: bool = False
1215
+ ) -> str:
1216
+ """
1217
+ Helper function to download a model from HuggingFace if it doesn't exist locally.
1218
+
1219
+ Args:
1220
+ model_path: The model path that may be local or remote
1221
+ param_name: Name of the parameter (for error messages)
1222
+ progress_callback: Callback function for download progress updates
1223
+ token: HuggingFace authentication token for private repositories
1224
+
1225
+ Returns:
1226
+ str: Local path to the model (either existing or downloaded)
1227
+
1228
+ Raises:
1229
+ RuntimeError: If download fails
1230
+ """
1231
+ # Check if model_path exists locally (file or directory)
1232
+ if os.path.exists(model_path):
1233
+ # Local path exists, return as-is
1234
+ return model_path
1235
+
1236
+ # Model path doesn't exist locally, try to download from HuggingFace
1237
+ try:
1238
+ # Parse model_path to extract repo_id and filename
1239
+ repo_id, file_name = _parse_model_path(model_path)
1240
+
1241
+ # Download the model
1242
+ downloaded_path = download_from_huggingface(
1243
+ repo_id=repo_id,
1244
+ file_name=file_name,
1245
+ local_dir=None, # Use default cache directory
1246
+ enable_transfer=True,
1247
+ progress_callback=progress_callback,
1248
+ show_progress=True,
1249
+ token=token,
1250
+ is_mmproj=is_mmproj
1251
+ )
1252
+
1253
+ return downloaded_path
1254
+
1255
+ except Exception as e:
1256
+ # Only handle download-related errors
1257
+ raise RuntimeError(f"Could not load model from '{param_name}={model_path}': {e}")
1258
+
1259
+
1127
1260
  def auto_download_model(func: Callable) -> Callable:
1128
1261
  """
1129
1262
  Decorator that automatically downloads models from HuggingFace if they don't exist locally.
1130
1263
 
1131
- This decorator should be applied to __init__ methods that take a name_or_path parameter.
1132
- If name_or_path doesn't exist as a local file/directory, it will attempt to download
1133
- it from HuggingFace Hub using the download_from_huggingface function.
1264
+ This decorator should be applied to __init__ methods that take a name_or_path parameter
1265
+ and optionally an mmproj_path parameter. If these paths don't exist as local files/directories,
1266
+ it will attempt to download them from HuggingFace Hub using the download_from_huggingface function.
1134
1267
 
1135
- The name_or_path can be in formats like:
1268
+ The name_or_path and mmproj_path can be in formats like:
1136
1269
  - "microsoft/DialoGPT-small" (downloads entire repo)
1137
1270
  - "microsoft/DialoGPT-small/pytorch_model.bin" (downloads specific file)
1138
1271
  - "Qwen/Qwen3-4B-GGUF/Qwen3-4B-Q4_K_M.gguf" (downloads specific file)
@@ -1149,21 +1282,6 @@ def auto_download_model(func: Callable) -> Callable:
1149
1282
  """
1150
1283
  @functools.wraps(func)
1151
1284
  def wrapper(*args, **kwargs):
1152
- # Find name_or_path in arguments
1153
- # Assuming name_or_path is the first argument after self
1154
- if len(args) >= 2:
1155
- name_or_path = args[1]
1156
- args_list = list(args)
1157
- path_index = 1
1158
- is_positional = True
1159
- elif 'name_or_path' in kwargs:
1160
- name_or_path = kwargs['name_or_path']
1161
- path_index = None
1162
- is_positional = False
1163
- else:
1164
- # No name_or_path found, call original function
1165
- return func(*args, **kwargs)
1166
-
1167
1285
  # Extract progress_callback and token from arguments
1168
1286
  progress_callback = None
1169
1287
  if 'progress_callback' in kwargs:
@@ -1173,39 +1291,63 @@ def auto_download_model(func: Callable) -> Callable:
1173
1291
  if 'token' in kwargs:
1174
1292
  token = kwargs.pop('token') # Remove from kwargs to avoid passing to original func
1175
1293
 
1176
- # Check if name_or_path exists locally (file or directory)
1177
- if os.path.exists(name_or_path):
1178
- # Local path exists, use as-is without downloading
1294
+ # Handle name_or_path parameter
1295
+ name_or_path = None
1296
+ name_path_index = None
1297
+ is_name_positional = False
1298
+
1299
+ # Find name_or_path in arguments
1300
+ # Assuming name_or_path is the first argument after self
1301
+ if len(args) >= 2:
1302
+ name_or_path = args[1]
1303
+ args_list = list(args)
1304
+ name_path_index = 1
1305
+ is_name_positional = True
1306
+ elif 'name_or_path' in kwargs:
1307
+ name_or_path = kwargs['name_or_path']
1308
+ is_name_positional = False
1309
+
1310
+ # Handle mmproj_path parameter
1311
+ mmproj_path = None
1312
+ if 'mmproj_path' in kwargs:
1313
+ mmproj_path = kwargs['mmproj_path']
1314
+
1315
+ # If neither parameter is found, call original function
1316
+ if name_or_path is None and mmproj_path is None:
1179
1317
  return func(*args, **kwargs)
1180
1318
 
1181
- # Model path doesn't exist locally, try to download from HuggingFace
1182
- try:
1183
- # Parse name_or_path to extract repo_id and filename
1184
- repo_id, file_name = _parse_model_path(name_or_path)
1185
-
1186
- # Download the model
1187
- downloaded_path = download_from_huggingface(
1188
- repo_id=repo_id,
1189
- file_name=file_name,
1190
- local_dir=None, # Use default cache directory
1191
- enable_transfer=True,
1192
- progress_callback=progress_callback, # Use the extracted callback
1193
- show_progress=True,
1194
- token=token # Use the extracted token
1195
- )
1196
-
1197
- # Replace name_or_path with downloaded path
1198
- if is_positional:
1199
- args_list[path_index] = downloaded_path
1200
- args = tuple(args_list)
1201
- else:
1202
- kwargs['name_or_path'] = downloaded_path
1203
-
1204
- except Exception as e:
1205
- # Only handle download-related errors
1206
- raise RuntimeError(f"Could not load model from '{name_or_path}': {e}")
1319
+ # Download name_or_path if needed
1320
+ if name_or_path is not None:
1321
+ try:
1322
+ downloaded_name_path = _download_model_if_needed(
1323
+ name_or_path, 'name_or_path', progress_callback, token
1324
+ )
1325
+
1326
+ # Replace name_or_path with downloaded path
1327
+ if is_name_positional:
1328
+ if name_path_index is not None:
1329
+ args_list[name_path_index] = downloaded_name_path
1330
+ args = tuple(args_list)
1331
+ else:
1332
+ kwargs['name_or_path'] = downloaded_name_path
1333
+
1334
+ except Exception as e:
1335
+ raise e # Re-raise the error from _download_model_if_needed
1336
+
1337
+ # Download mmproj_path if needed
1338
+ if mmproj_path is not None:
1339
+ try:
1340
+ downloaded_mmproj_path = _download_model_if_needed(
1341
+ mmproj_path, 'mmproj_path', progress_callback, token, is_mmproj=True
1342
+ )
1343
+
1344
+ # Replace mmproj_path with downloaded path
1345
+ kwargs['mmproj_path'] = downloaded_mmproj_path
1346
+
1347
+ except Exception as e:
1348
+ raise e # Re-raise the error from _download_model_if_needed
1207
1349
 
1208
- # Call original function with updated path (outside try-catch to let model creation errors bubble up)
1350
+ # Call original function with updated paths (outside try-catch to let model creation errors bubble up)
1209
1351
  return func(*args, **kwargs)
1210
1352
 
1211
1353
  return wrapper
@@ -1,7 +1,6 @@
1
1
  from typing import Generator, Optional, List, Dict, Any, Union
2
2
  import queue
3
3
  import threading
4
- import base64
5
4
  from pathlib import Path
6
5
 
7
6
  from nexaai.common import ModelConfig, GenerationConfig, MultiModalMessage, PluginID
@@ -102,26 +101,16 @@ class PyBindVLMImpl(VLM):
102
101
  t = c["type"]
103
102
  if t == "text":
104
103
  blocks.append({"type": "text", "text": c.get("text","") or ""})
104
+ elif t == "image":
105
+ # Pass through the original structure - let vlm-bind.cpp handle field extraction
106
+ blocks.append(c)
105
107
  else:
106
- # image/audio/video
107
- src = c.get("url") or c.get("path")
108
- if not src:
109
- raise ValueError(f"No url/path for {t}")
110
- # read local file or strip data URI
111
- if Path(src).exists():
112
- raw = Path(src).read_bytes()
113
- b64 = base64.b64encode(raw).decode("ascii")
114
- blocks.append({"type": t, "text": b64})
115
- elif src.startswith("data:"):
116
- b64 = src.split(",",1)[1]
117
- blocks.append({"type": t, "text": b64})
118
- else:
119
- # remote URL
120
- blocks.append({"type": t, "text": src})
108
+ raise ValueError(f"Unsupported content type: {t}. Use 'text' or 'image' to match the golden reference in vlm.cpp")
121
109
 
122
110
  payload.append({"role": role, "content": blocks})
123
111
 
124
- return vlm_bind.ml_vlm_apply_chat_template(self._handle, payload, tools)
112
+ result = vlm_bind.ml_vlm_apply_chat_template(self._handle, payload, tools)
113
+ return result
125
114
 
126
115
  def generate_stream(self, prompt: str, g_cfg: GenerationConfig = GenerationConfig()) -> Generator[str, None, None]:
127
116
  """Generate text with streaming."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nexaai
3
- Version: 1.0.12rc1
3
+ Version: 1.0.13rc2
4
4
  Summary: Python bindings for NexaSDK C-lib backend
5
5
  Author-email: "Nexa AI, Inc." <dev@nexa.ai>
6
6
  Project-URL: Homepage, https://github.com/NexaAI/nexasdk-bridge
@@ -1,6 +1,6 @@
1
1
  nexaai/__init__.py,sha256=Lt8NU57eTMtWrDYzpFeYR9XtGAPXqizynP83TPU0UW0,2105
2
- nexaai/_stub.cp310-win_amd64.pyd,sha256=l_sOvn0sGZaAzxUURKum8xng89EtFH_aE1lcWv_ZpKA,10752
3
- nexaai/_version.py,sha256=56eZbYiAoFM_PEDgdfRtf4Lkj9WNLYjfs6jyjl979Rs,147
2
+ nexaai/_stub.cp310-win_amd64.pyd,sha256=yh_bkjT4c0dWKl1ClfZ5hTHcaNN9FHKZd2PUPimpN7g,10752
3
+ nexaai/_version.py,sha256=9VhLiWBFeo9dM94_u0HoZj6ukbol57nkGewSXnnRmpE,147
4
4
  nexaai/asr.py,sha256=_fsGaxpiU137bUtO5ujtFSYCI1RLsyeEm3Gf4GhHVRk,2118
5
5
  nexaai/base.py,sha256=qQBCiQVNzgpkQjZX9aiFDEdbAAe56TROKC3WnWra2Zg,1021
6
6
  nexaai/common.py,sha256=6keIpdX5XS5us4z79EMoa6RSkVze9SbbXax13IJ9yvs,3525
@@ -15,21 +15,22 @@ nexaai/vlm.py,sha256=STjXCw67ABrHrEll8A2NGiwmfo7MotfYgBh1k1aNxkk,4775
15
15
  nexaai/asr_impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  nexaai/asr_impl/mlx_asr_impl.py,sha256=XwMX3LYMeulp8cDS0TCCYcjvttFHAyDWQ_oMvABwQmI,3349
17
17
  nexaai/asr_impl/pybind_asr_impl.py,sha256=20o5SOPzhF9x41ra8L_qIM7YxCkYeLb5csSrNde-dds,1560
18
- nexaai/binds/__init__.py,sha256=tYvy0pFhoY29GstDT5r-oRiPRarPLECvJAkcamJItOg,83
19
- nexaai/binds/common_bind.cp310-win_amd64.pyd,sha256=rWuxRVTVPuakItIeyiXeouD25e4Ekl-niQix_GOriIs,201216
20
- nexaai/binds/embedder_bind.cp310-win_amd64.pyd,sha256=Bz5tKsR7W_ohrsshGq314-ZkfG39H5fs6dH9kJh584A,182784
21
- nexaai/binds/libcrypto-3-x64.dll,sha256=-Lau6pL5DpDXzpg9MED63gCeL8oRrSLI_e2LeaxIHqk,7314432
22
- nexaai/binds/libssl-3-x64.dll,sha256=Tzzyu5jRpUugFxr_65hbFlAtFpjxIDpOYMU1E0ijkJw,1313792
23
- nexaai/binds/llm_bind.cp310-win_amd64.pyd,sha256=Wpm1mEohC6DWlwYuK8BU_PJQoW5tjsarcjXhLFhdSgY,162816
24
- nexaai/binds/nexa_bridge.dll,sha256=UbbvGrUkwFk_tMJeALO5HM7O9rMfJ7xt8YXpB5LheEQ,168448
25
- nexaai/binds/nexa_llama_cpp/ggml-base.dll,sha256=qpJM5qmcne4UqEbsYEBeoDHOe0KyWjqPk-DrKd8TD_k,532480
26
- nexaai/binds/nexa_llama_cpp/ggml-cpu.dll,sha256=lbNh62Q7bW0hB_Dbps8HqUkLMNwh_VOQhT3gOTrpOt4,672768
27
- nexaai/binds/nexa_llama_cpp/ggml-cuda.dll,sha256=Hs_WgTlv9d5MkS2S-hENjwMVX953bhUZtM5959Q7jFs,313528832
28
- nexaai/binds/nexa_llama_cpp/ggml-vulkan.dll,sha256=siZ1qWTIg4LWcOBlwVsdf7mryOIUrhhiMfdITMdH-TM,36627456
29
- nexaai/binds/nexa_llama_cpp/ggml.dll,sha256=sASNHyXx2Kor4O5Ao5X2AhmLZwMg3hdJe2jMSxdJmi0,66560
30
- nexaai/binds/nexa_llama_cpp/llama.dll,sha256=S_SaHxVswdt8LlLazOsLhA72pOoD4d2iba9JKjWGTlg,1611776
31
- nexaai/binds/nexa_llama_cpp/mtmd.dll,sha256=5bAv5F32eaZdlJ_lbwO53Ph3b3c62t14tmZiflkVL8U,561152
32
- nexaai/binds/nexa_llama_cpp/nexa_plugin.dll,sha256=1AruxKWsmmuvod95SwAQaffwbrtm38EZSJT9M2-ngM0,1405440
18
+ nexaai/binds/__init__.py,sha256=ENl-uoIF9-3XGIXitVgZ2QmJ6p7Yet4h1-X7nUDZ0Hk,108
19
+ nexaai/binds/common_bind.cp310-win_amd64.pyd,sha256=67kvH9ytidgKpTSPSBQEIWXng4iyIKsGvLgm5qZAtYM,201216
20
+ nexaai/binds/embedder_bind.cp310-win_amd64.pyd,sha256=4m1HZAS3eqXeHph6NRdg0mCZLzRCfVaFe0yV5kCll7g,182784
21
+ nexaai/binds/libcrypto-3-x64.dll,sha256=PYwJJtM4xpo2IQfxm-of6VR6rtVcHCyodwcvBHmfzig,7315968
22
+ nexaai/binds/libssl-3-x64.dll,sha256=mp_RoRjYcCeAuqMy3QDYvxQ-cjAMUNCuwA320oXVVpg,1313792
23
+ nexaai/binds/llm_bind.cp310-win_amd64.pyd,sha256=6tLXslo9CQl77mozzHq0mJhcpQU2bG8jFn--J5ATYoo,162816
24
+ nexaai/binds/nexa_bridge.dll,sha256=3CeitLeaXfzk_kwz8znPc2fQdJCzvey2a5EHcn41bjs,168448
25
+ nexaai/binds/vlm_bind.cp310-win_amd64.pyd,sha256=_up-xlK3LCeKvMMFe4IbQ4gfkuYy2I3tX6GdXJV0mF8,168960
26
+ nexaai/binds/nexa_llama_cpp/ggml-base.dll,sha256=4eKg9swCCKJ4NB5HFH1m4_UurkzIlbUm3gmQcC4ygpQ,532480
27
+ nexaai/binds/nexa_llama_cpp/ggml-cpu.dll,sha256=P_e6-nkKKNsyhlIKBHKZJZ7158ZCj0uFlBrCQywd7ag,672768
28
+ nexaai/binds/nexa_llama_cpp/ggml-cuda.dll,sha256=HnlhW9PufnU_x5Mr4hka5hz5pVk6VLXD9KYxxsy9ksc,313528832
29
+ nexaai/binds/nexa_llama_cpp/ggml-vulkan.dll,sha256=47PEdfb2RzZyydi7vPReIpQKB4osrwmFkTYvE-oLap4,36627456
30
+ nexaai/binds/nexa_llama_cpp/ggml.dll,sha256=yr5qn5GRhfw-1iZcf35hzO2unBC_qPYi5ei6Ea6NGQM,66560
31
+ nexaai/binds/nexa_llama_cpp/llama.dll,sha256=mP71izs2Zeqw1z-p_gHRxYMWIbXI4JDXHsImeMKmmHo,1611776
32
+ nexaai/binds/nexa_llama_cpp/mtmd.dll,sha256=Ya3J-d-b3ENtSPCrJkl9VdA3Onep77rSngM9Sycq0Ww,561152
33
+ nexaai/binds/nexa_llama_cpp/nexa_plugin.dll,sha256=Ne7ObJB5aC2A_7LYzOsy2sNBGV1UgcieopoAw13tEW8,1405440
33
34
  nexaai/cv_impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
35
  nexaai/cv_impl/mlx_cv_impl.py,sha256=QLd_8w90gtxH8kmssaDYatCTRvQNIJuUGKZNnYrmx6E,3317
35
36
  nexaai/cv_impl/pybind_cv_impl.py,sha256=aSOCAxmHrwJbEkSN6VX3Cykqlj_9RIpVrZXILul04GA,1096
@@ -50,15 +51,15 @@ nexaai/tts_impl/mlx_tts_impl.py,sha256=LcH9bVdIl3Q6lOzSUB_X2s-_nWFmlCl1yL7XSUK0f
50
51
  nexaai/tts_impl/pybind_tts_impl.py,sha256=n3z4zmPQayQJgAwcvETw0IBUCp8IYROuYFSg0tAy_8Y,1487
51
52
  nexaai/utils/avatar_fetcher.py,sha256=D01f8je-37Nd68zGw8MYK2m7y3fvGlC6h0KR-aN9kdU,3925
52
53
  nexaai/utils/decode.py,sha256=0Z9jDH4ICzw4YXj8nD4L-sMouDaev-TISGRQ4KzidWE,421
53
- nexaai/utils/manifest_utils.py,sha256=zMgQpf5dAgF2RjGhk73zBggxRDGMRKDGxh2a8m8kmYg,10045
54
- nexaai/utils/model_manager.py,sha256=TmaT1fFculHgfAdutpNXP4d07HIhEMPVTfPvFKE-zR0,51480
54
+ nexaai/utils/manifest_utils.py,sha256=nPqK24srLX41x3WU4R-OGQR9u0XA7ZTHY1MQXBKXdrM,12652
55
+ nexaai/utils/model_manager.py,sha256=3rDODGClgnM24LFWIoM-TDpXkpV1vyJSdlv7Qk4ZaPE,57434
55
56
  nexaai/utils/model_types.py,sha256=arIyb9q-1uG0nyUGdWZaxxDJAxv0cfnJEpjCzyELL5Q,1416
56
57
  nexaai/utils/progress_tracker.py,sha256=BztrFqtjwNUmeREwZ5m7H6ZcrVzQEbpZfsxndWh4z0A,15778
57
58
  nexaai/utils/quantization_utils.py,sha256=jjQaz7K4qH6TdP8Tnv5Ktb2viz8BaVBSOrb_jm3ns28,7889
58
59
  nexaai/vlm_impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
60
  nexaai/vlm_impl/mlx_vlm_impl.py,sha256=oY_qb9z_iF0zArBuY5CCYIvZcA3R0i_NKXrr_r-QSgg,10989
60
- nexaai/vlm_impl/pybind_vlm_impl.py,sha256=Hu8g8OXyPn8OzLQOpRSE5lfGmhjChiKj7fMRB8mC_cI,9147
61
- nexaai-1.0.12rc1.dist-info/METADATA,sha256=6sAd6d9nS11AE5aIHCxvqT5MeuRxoYeezwFsQGjsGkg,1233
62
- nexaai-1.0.12rc1.dist-info/WHEEL,sha256=KUuBC6lxAbHCKilKua8R9W_TM71_-9Sg5uEP3uDWcoU,101
63
- nexaai-1.0.12rc1.dist-info/top_level.txt,sha256=LRE2YERlrZk2vfuygnSzsEeqSknnZbz3Z1MHyNmBU4w,7
64
- nexaai-1.0.12rc1.dist-info/RECORD,,
61
+ nexaai/vlm_impl/pybind_vlm_impl.py,sha256=7Bo0kpSrmOdr--bWSpQBvcaexkPPRt3x1yt9e_jIyDs,8686
62
+ nexaai-1.0.13rc2.dist-info/METADATA,sha256=PP51i9S_euhcIZSaDxFexcZU-3g3qvNOyY3biwZVtgs,1233
63
+ nexaai-1.0.13rc2.dist-info/WHEEL,sha256=KUuBC6lxAbHCKilKua8R9W_TM71_-9Sg5uEP3uDWcoU,101
64
+ nexaai-1.0.13rc2.dist-info/top_level.txt,sha256=LRE2YERlrZk2vfuygnSzsEeqSknnZbz3Z1MHyNmBU4w,7
65
+ nexaai-1.0.13rc2.dist-info/RECORD,,