amd-gaia 0.15.2__py3-none-any.whl → 0.15.3__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.
@@ -386,13 +386,46 @@ class LemonadeInstaller:
386
386
  if silent:
387
387
  cmd.extend(["/qn", "/norestart"])
388
388
 
389
+ log_dir = Path.home() / ".cache" / "gaia" / "installer"
390
+ log_dir.mkdir(parents=True, exist_ok=True)
391
+ msi_log = log_dir / "msi_install.log"
392
+ cmd.extend(["/l*v", str(msi_log)]) # Verbose logging to file
393
+
389
394
  log.debug(f"Running: {' '.join(cmd)}")
390
395
 
396
+ # Check for other msiexec processes before starting (helps diagnose hangs)
397
+ try:
398
+ check_result = subprocess.run(
399
+ ["tasklist", "/FI", "IMAGENAME eq msiexec.exe", "/NH"],
400
+ capture_output=True,
401
+ text=True,
402
+ timeout=5,
403
+ check=False,
404
+ )
405
+ if (
406
+ check_result.returncode == 0
407
+ and "msiexec.exe" in check_result.stdout
408
+ ):
409
+ msi_count = check_result.stdout.count("msiexec.exe")
410
+ log.warning(
411
+ f"Found {msi_count} existing msiexec process(es) - installation may be blocked"
412
+ )
413
+ except Exception:
414
+ pass # Skip check if tasklist fails
415
+
416
+ if silent:
417
+ self._print_status(
418
+ "Running silent MSI installer (should complete in ~10 seconds)..."
419
+ )
420
+ else:
421
+ self._print_status("Running MSI installer...")
422
+
423
+ # MSI should install in 10-15 seconds, timeout after 60 seconds (indicates stuck process)
391
424
  result = subprocess.run(
392
425
  cmd,
393
426
  capture_output=True,
394
427
  text=True,
395
- timeout=300, # 5 minute timeout
428
+ timeout=60, # 60 second timeout (should complete in ~10s)
396
429
  check=False,
397
430
  )
398
431
 
@@ -418,7 +451,32 @@ class LemonadeInstaller:
418
451
  )
419
452
 
420
453
  except subprocess.TimeoutExpired:
421
- return InstallResult(success=False, error="Installation timed out")
454
+ # Print MSI log to help diagnose the hang
455
+ error_msg = "Installation timed out (expected ~10s, hung for 60s)"
456
+ try:
457
+ if msi_log.exists():
458
+ self._print_status(f"MSI log file: {msi_log}")
459
+ log_content = msi_log.read_text(encoding="utf-16", errors="ignore")
460
+ # Print last 100 lines of log
461
+ log_lines = log_content.split("\n")
462
+ relevant_lines = log_lines[-100:]
463
+ log.error("=== MSI Install Log (last 100 lines) ===")
464
+ for line in relevant_lines:
465
+ log.error(line)
466
+ log.error("=== End MSI Install Log ===")
467
+
468
+ # Also print to console
469
+ if self.console:
470
+ self.console.print(
471
+ "\n [red]MSI Install Log (last 50 lines):[/red]"
472
+ )
473
+ for line in relevant_lines[-50:]:
474
+ if line.strip():
475
+ self.console.print(f" [dim]{line}[/dim]")
476
+ except Exception as e:
477
+ log.debug(f"Could not read MSI log: {e}")
478
+
479
+ return InstallResult(success=False, error=error_msg)
422
480
  except FileNotFoundError:
423
481
  return InstallResult(success=False, error="msiexec not found")
424
482
  except Exception as e:
@@ -533,9 +591,10 @@ class LemonadeInstaller:
533
591
  installed_version = info.version.lstrip("v")
534
592
 
535
593
  # Create installer for the installed version (not target version)
594
+ # Use minimal=True (lemonade-server-minimal.msi exists for all versions)
536
595
  # Pass console to child installer for consistent output
537
596
  uninstall_installer = LemonadeInstaller(
538
- target_version=installed_version, console=self.console
597
+ target_version=installed_version, minimal=True, console=self.console
539
598
  )
540
599
 
541
600
  # Download the MSI matching the installed version
@@ -1601,6 +1601,139 @@ class LemonadeClient:
1601
1601
  self.log.error(f"Error generating embeddings: {str(e)}")
1602
1602
  raise LemonadeClientError(f"Error generating embeddings: {str(e)}")
1603
1603
 
1604
+ # =========================================================================
1605
+ # Image Generation (Stable Diffusion)
1606
+ # =========================================================================
1607
+
1608
+ # Supported SD configurations
1609
+ SD_MODELS = ["SD-1.5", "SD-Turbo", "SDXL-Base-1.0", "SDXL-Turbo"]
1610
+ SD_SIZES = ["512x512", "768x768", "1024x1024"]
1611
+
1612
+ # Model-specific defaults
1613
+ SD_MODEL_DEFAULTS = {
1614
+ "SD-1.5": {"steps": 20, "cfg_scale": 7.5, "size": "512x512"},
1615
+ "SD-Turbo": {"steps": 4, "cfg_scale": 1.0, "size": "512x512"},
1616
+ "SDXL-Base-1.0": {"steps": 20, "cfg_scale": 7.5, "size": "1024x1024"},
1617
+ "SDXL-Turbo": {"steps": 4, "cfg_scale": 1.0, "size": "512x512"},
1618
+ }
1619
+
1620
+ def generate_image(
1621
+ self,
1622
+ prompt: str,
1623
+ model: str = "SDXL-Turbo",
1624
+ size: Optional[str] = None,
1625
+ steps: Optional[int] = None,
1626
+ cfg_scale: Optional[float] = None,
1627
+ seed: Optional[int] = None,
1628
+ timeout: int = 300,
1629
+ ) -> Dict[str, Any]:
1630
+ """
1631
+ Generate an image from a text prompt using Stable Diffusion.
1632
+
1633
+ Args:
1634
+ prompt: Text description of the image to generate
1635
+ model: SD model - SD-1.5, SD-Turbo, SDXL-Base-1.0 (photorealistic), SDXL-Turbo
1636
+ size: Image dimensions (auto-selected if None, or 512x512, 768x768, 1024x1024)
1637
+ steps: Inference steps (auto-selected if None: Turbo=4, Base=20)
1638
+ cfg_scale: CFG scale (auto-selected if None: Turbo=1.0, Base=7.5)
1639
+ seed: Random seed for reproducibility (optional)
1640
+ timeout: Request timeout in seconds (default: 300 for slower Base models)
1641
+
1642
+ Returns:
1643
+ Dict with 'data' containing list of generated images in b64_json format
1644
+
1645
+ Raises:
1646
+ LemonadeClientError: If generation fails or invalid parameters
1647
+
1648
+ Example:
1649
+ # Photorealistic with SDXL-Base-1.0 (auto-settings)
1650
+ result = client.generate_image(
1651
+ prompt="a sunset over mountains, golden hour, photorealistic",
1652
+ model="SDXL-Base-1.0"
1653
+ )
1654
+
1655
+ # Fast stylized with SDXL-Turbo
1656
+ result = client.generate_image(
1657
+ prompt="cyberpunk city",
1658
+ model="SDXL-Turbo"
1659
+ )
1660
+ """
1661
+ # Validate model
1662
+ if model not in self.SD_MODELS:
1663
+ raise LemonadeClientError(
1664
+ f"Invalid model '{model}'. Choose from: {self.SD_MODELS}"
1665
+ )
1666
+
1667
+ # Apply model-specific defaults
1668
+ defaults = self.SD_MODEL_DEFAULTS.get(model, {})
1669
+ size = size or defaults.get("size", "512x512")
1670
+ steps = steps if steps is not None else defaults.get("steps", 20)
1671
+ cfg_scale = (
1672
+ cfg_scale if cfg_scale is not None else defaults.get("cfg_scale", 7.5)
1673
+ )
1674
+
1675
+ # Validate size
1676
+ if size not in self.SD_SIZES:
1677
+ raise LemonadeClientError(
1678
+ f"Invalid size '{size}'. Choose from: {self.SD_SIZES}"
1679
+ )
1680
+
1681
+ try:
1682
+ # Generate random seed if not provided for varied results
1683
+ import random
1684
+
1685
+ if seed is None:
1686
+ seed = random.randint(0, 2**32 - 1)
1687
+
1688
+ payload = {
1689
+ "prompt": prompt,
1690
+ "model": model,
1691
+ "size": size,
1692
+ "n": 1,
1693
+ "response_format": "b64_json",
1694
+ "cfg_scale": cfg_scale,
1695
+ "steps": steps,
1696
+ "seed": seed,
1697
+ }
1698
+
1699
+ self.log.info(
1700
+ f"Generating image: model={model}, size={size}, steps={steps}, cfg={cfg_scale}"
1701
+ )
1702
+ url = f"{self.base_url}/images/generations"
1703
+ response = self._send_request("POST", url, data=payload, timeout=timeout)
1704
+
1705
+ return response
1706
+
1707
+ except LemonadeClientError:
1708
+ raise
1709
+ except Exception as e:
1710
+ self.log.error(f"Error generating image: {str(e)}")
1711
+ raise LemonadeClientError(f"Error generating image: {str(e)}")
1712
+
1713
+ def list_sd_models(self) -> List[Dict[str, Any]]:
1714
+ """
1715
+ List available Stable Diffusion models from the server.
1716
+
1717
+ Returns:
1718
+ List of SD model info dicts with id, labels, and image_defaults
1719
+
1720
+ Example:
1721
+ sd_models = client.list_sd_models()
1722
+ for m in sd_models:
1723
+ print(f"{m['id']}: {m.get('image_defaults', {})}")
1724
+ """
1725
+ try:
1726
+ models = self.list_models()
1727
+ sd_models = [
1728
+ m
1729
+ for m in models.get("data", [])
1730
+ if m.get("id") in self.SD_MODELS or "image" in m.get("labels", [])
1731
+ ]
1732
+ return sd_models
1733
+ except Exception as e:
1734
+ self.log.error(f"Error listing SD models: {str(e)}")
1735
+ raise LemonadeClientError(f"Error listing SD models: {str(e)}")
1736
+
1604
1737
  def list_models(self, show_all: bool = False) -> Dict[str, Any]:
1605
1738
  """
1606
1739
  List available models from the server.
@@ -2184,6 +2317,8 @@ class LemonadeClient:
2184
2317
  auto_download: bool = False,
2185
2318
  _download_timeout: int = 7200, # Reserved for future use
2186
2319
  llamacpp_args: Optional[str] = None,
2320
+ ctx_size: Optional[int] = None,
2321
+ save_options: bool = False,
2187
2322
  prompt: bool = True,
2188
2323
  ) -> Dict[str, Any]:
2189
2324
  """
@@ -2203,6 +2338,10 @@ class LemonadeClient:
2203
2338
  Large models can be 100GB+ and take hours to download
2204
2339
  llamacpp_args: Optional llama.cpp arguments (e.g., "--ubatch-size 2048").
2205
2340
  Used to configure model loading parameters like batch sizes.
2341
+ ctx_size: Context size for the model in tokens (e.g., 8192, 32768).
2342
+ Overrides the default value for this model.
2343
+ save_options: If True, persists ctx_size and llamacpp_args to config file.
2344
+ Model will use these settings on future loads.
2206
2345
  prompt: If True, prompt user before downloading (default: True).
2207
2346
  Set to False to download automatically without user confirmation.
2208
2347
 
@@ -2219,6 +2358,10 @@ class LemonadeClient:
2219
2358
  request_data = {"model_name": model_name}
2220
2359
  if llamacpp_args:
2221
2360
  request_data["llamacpp_args"] = llamacpp_args
2361
+ if ctx_size is not None:
2362
+ request_data["ctx_size"] = ctx_size
2363
+ if save_options:
2364
+ request_data["save_options"] = save_options
2222
2365
  url = f"{self.base_url}/load"
2223
2366
 
2224
2367
  try:
@@ -203,16 +203,45 @@ class LemonadeManager:
203
203
  )
204
204
  return True
205
205
  else:
206
- # Context size insufficient - warn and continue
207
- cls._log.warning(
208
- f"Lemonade running with {cls._context_size} tokens, "
209
- f"but {min_context_size} requested. "
210
- f"Restart with: lemonade-server serve --ctx-size {min_context_size}"
211
- )
212
- if not quiet:
213
- cls.print_context_message(
214
- cls._context_size, min_context_size, MessageType.WARNING
206
+ # Context size may be cached from before models were loaded
207
+ # Re-check current status to see if models are loaded now
208
+ try:
209
+ client = LemonadeClient(
210
+ host=host,
211
+ port=port,
212
+ keep_alive=True,
213
+ verbose=not quiet,
215
214
  )
215
+ status = client.get_status()
216
+ # Update cached context size
217
+ cls._context_size = status.context_size or 0
218
+
219
+ # Only warn if LLM models are loaded AND context is insufficient
220
+ # SD models don't have context size, only LLM models do
221
+ llm_models_loaded = any(
222
+ "image" not in model.get("labels", [])
223
+ for model in status.loaded_models
224
+ )
225
+
226
+ # Only warn if context_size is non-zero (0 means no model loaded or still loading)
227
+ if (
228
+ cls._context_size > 0
229
+ and cls._context_size < min_context_size
230
+ and llm_models_loaded
231
+ ):
232
+ cls._log.warning(
233
+ f"Lemonade running with {cls._context_size} tokens, "
234
+ f"but {min_context_size} requested. "
235
+ f"Restart with: lemonade-server serve --ctx-size {min_context_size}"
236
+ )
237
+ if not quiet:
238
+ cls.print_context_message(
239
+ cls._context_size,
240
+ min_context_size,
241
+ MessageType.WARNING,
242
+ )
243
+ except Exception as e:
244
+ cls._log.debug(f"Failed to re-check status: {e}")
216
245
  return True
217
246
 
218
247
  cls._log.debug(f"Initializing Lemonade (min context: {min_context_size})")
@@ -246,8 +275,23 @@ class LemonadeManager:
246
275
  f"(context: {cls._context_size} tokens)"
247
276
  )
248
277
 
249
- # Verify context size - warn if insufficient
250
- if cls._context_size < min_context_size:
278
+ # Verify context size - only warn if insufficient AND LLM models are loaded
279
+ # SD models don't have context size, only LLM models do
280
+ # Check if any loaded models are LLMs (not SD models with "image" label)
281
+ llm_models_loaded = any(
282
+ "image" not in model.get("labels", [])
283
+ for model in status.loaded_models
284
+ )
285
+
286
+ # Only warn if:
287
+ # 1. Context size is non-zero (0 means no model loaded or model still loading)
288
+ # 2. Context size is less than required
289
+ # 3. LLM models are loaded (SD models don't have context size)
290
+ if (
291
+ cls._context_size > 0
292
+ and cls._context_size < min_context_size
293
+ and llm_models_loaded
294
+ ):
251
295
  cls._log.warning(
252
296
  f"Context size {cls._context_size} is less than "
253
297
  f"requested {min_context_size}. Some features may not work correctly."
@@ -79,6 +79,15 @@ class LemonadeProvider(LLMClient):
79
79
  )
80
80
  if stream:
81
81
  return self._handle_stream(response)
82
+
83
+ # Handle error responses gracefully
84
+ if not isinstance(response, dict) or "choices" not in response:
85
+ error_msg = f"Unexpected response format from Lemonade Server: {response}"
86
+ raise ValueError(error_msg)
87
+
88
+ if not response["choices"] or len(response["choices"]) == 0:
89
+ raise ValueError("Empty choices in response from Lemonade Server")
90
+
82
91
  return response["choices"][0]["message"]["content"]
83
92
 
84
93
  def embed(self, texts: list[str], **kwargs) -> list[list[float]]:
gaia/version.py CHANGED
@@ -6,10 +6,10 @@ import os
6
6
  import subprocess
7
7
  from importlib.metadata import version as get_package_version_metadata
8
8
 
9
- __version__ = "0.15.2"
9
+ __version__ = "0.15.3"
10
10
 
11
11
  # Lemonade version used across CI and installer
12
- LEMONADE_VERSION = "9.1.4"
12
+ LEMONADE_VERSION = "9.2.0"
13
13
 
14
14
 
15
15
  def get_package_version() -> str: