grid-cortex-client 0.5.1__tar.gz → 0.6.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 (64) hide show
  1. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/PKG-INFO +1 -1
  2. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/model_type.py +43 -21
  3. grid_cortex_client-0.6.0/src/grid_cortex_client/models/c_radio.py +129 -0
  4. grid_cortex_client-0.6.0/tests/test_c_radio.py +124 -0
  5. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/.gitignore +0 -0
  6. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/CLAUDE.md +0 -0
  7. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/README.md +0 -0
  8. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/pyproject.toml +0 -0
  9. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/__init__.py +0 -0
  10. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/client.py +0 -0
  11. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/cortex_client.py +0 -0
  12. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/cortex_hub_client.py +0 -0
  13. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/__init__.py +0 -0
  14. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/base_model.py +0 -0
  15. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/contact_graspnet.py +0 -0
  16. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/da3metric.py +0 -0
  17. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/fast_foundation_stereo.py +0 -0
  18. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/finetune_pi05.py +0 -0
  19. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/foundation_stereo.py +0 -0
  20. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/graspgen.py +0 -0
  21. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/graspgenx.py +0 -0
  22. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/gsam2.py +0 -0
  23. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/locate_anything.py +0 -0
  24. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/metric3d.py +0 -0
  25. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/moondream.py +0 -0
  26. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/oneformer.py +0 -0
  27. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/owlv2.py +0 -0
  28. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/pi05.py +0 -0
  29. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/qwen_vl.py +0 -0
  30. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/sam2.py +0 -0
  31. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/sam3.py +0 -0
  32. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/ur5e_pyroki_collision_batch.py +0 -0
  33. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/ur5e_pyroki_fk.py +0 -0
  34. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/models/zoedepth.py +0 -0
  35. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/preprocessing.py +0 -0
  36. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/tools/__init__.py +0 -0
  37. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/tools/generate_enum.py +0 -0
  38. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/tools/registry.py +0 -0
  39. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/utils.py +0 -0
  40. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/src/grid_cortex_client/ws.py +0 -0
  41. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/README.md +0 -0
  42. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/__init__.py +0 -0
  43. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/conftest.py +0 -0
  44. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_contact_graspnet.py +0 -0
  45. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_cortex_hub_client.py +0 -0
  46. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_da3metric.py +0 -0
  47. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_fast_foundation_stereo.py +0 -0
  48. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_finetune_pi05.py +0 -0
  49. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_foundation_stereo.py +0 -0
  50. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_graspgen.py +0 -0
  51. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_graspgenx.py +0 -0
  52. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_gsam2.py +0 -0
  53. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_locate_anything.py +0 -0
  54. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_metric3d.py +0 -0
  55. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_moondream.py +0 -0
  56. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_oneformer.py +0 -0
  57. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_owlv2.py +0 -0
  58. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_pi05.py +0 -0
  59. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_qwen_vl.py +0 -0
  60. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_sam2.py +0 -0
  61. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_sam3.py +0 -0
  62. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_ur5e_pyroki_collision_batch.py +0 -0
  63. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_ur5e_pyroki_fk.py +0 -0
  64. {grid_cortex_client-0.5.1 → grid_cortex_client-0.6.0}/tests/test_zoedepth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grid-cortex-client
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Python client for Grid Cortex
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.8
@@ -13,36 +13,54 @@ from enum import Enum
13
13
 
14
14
 
15
15
  class ModelType(Enum):
16
- CONTACT_GRASPNET = "contact-graspnet"
16
+ C_RADIO = "c-radio"
17
17
  """
18
- Generate 6-DoF grasp poses from a depth image + camera intrinsics
19
- (+ optional segmentation mask) using Contact-GraspNet (PyTorch port).
18
+ Compute an L2-normalized embedding for a single RGB image.
20
19
 
21
20
  Args:
22
- depth_image (Union[str, Image.Image, np.ndarray]): Depth image (path/URL/PIL/ndarray; float32 H×W).
23
- camera_intrinsics (Union[str, np.ndarray]): 3×3 intrinsics matrix.
24
- seg_image (Union[str, Image.Image, np.ndarray, None]): Optional segmentation mask (uint8 H×W).
25
- When supplied + aux_args["filter_grasps"] = True, grasps are restricted to the masked region.
26
- aux_args (Dict[str, Any]): Optional dict. Keys:
27
- - "top_k" (int, default 64): cap on returned grasps after score-sort.
28
- - "z_range" ([float, float], default [0.2, 1.8]): z-axis crop in metres.
29
- - "local_regions" (bool, default False): crop 3D regions around segments.
30
- - "filter_grasps" (bool, default False): restrict grasps to segmap; requires seg_image.
31
- - "forward_passes" (int, default 1): how many forward passes to ensemble.
32
- timeout (float | None): Optional HTTP timeout.
21
+ image_input (Union[str, Image.Image, np.ndarray]): RGB image as file path, URL, PIL Image, or numpy array.
22
+ timeout (float | None): Optional timeout in seconds for the HTTP request.
33
23
 
34
24
  Returns:
35
- Dict with:
36
- - grasps: Array of 4×4 grasp poses in camera frame, shape (N, 4, 4), N <= top_k.
37
- - confidence: Array of confidence scores in [0, 1], shape (N,).
38
- - latency_ms: Optional server-reported latency in milliseconds.
25
+ np.ndarray: Embedding as numpy array of shape ``(D,)``, dtype ``float32``,
26
+ L2-normalized so dot products give cosine similarity directly.
39
27
 
40
28
  Raises:
41
- ValueError: If depth or intrinsics cannot be loaded.
29
+ ValueError: If image cannot be loaded.
42
30
  RuntimeError: If no HTTP transport is configured.
43
31
  Exception: If the HTTP request fails.
44
32
 
45
33
  Example:
34
+ >>> from grid_cortex_client import CortexClient, ModelType
35
+ >>> import numpy as np
36
+ >>> from PIL import Image
37
+ >>> client = CortexClient()
38
+ >>> image = np.array(Image.open("cat.jpg"))
39
+ >>> emb = client.run(ModelType.C_RADIO, image_input=image)
40
+ >>> print(emb.shape, float(np.linalg.norm(emb))) # (D,) 1.0
41
+ """
42
+ CONTACT_GRASPNET = "contact-graspnet"
43
+ """
44
+ Generate grasps using ContactGraspNet.
45
+
46
+ Args:
47
+ depth_image (Union[str, Image.Image, np.ndarray, None]): Depth image (required).
48
+ camera_intrinsics (Union[str, np.ndarray, None]): 3×3 intrinsics matrix (required).
49
+ seg_image (Union[str, Image.Image, np.ndarray, None]): Optional segmentation mask (uint8 H×W).
50
+ aux_args (Dict[str, Any] | None): Optional dict. Keys: ``top_k`` (int, default 64),
51
+ ``z_range`` ([float, float], default [0.2, 1.8]),
52
+ ``local_regions`` (bool, default False),
53
+ ``filter_grasps`` (bool, default False; requires ``seg_image``),
54
+ ``forward_passes`` (int, default 1).
55
+ timeout (float | None): Optional HTTP timeout.
56
+
57
+ Returns:
58
+ Dict with:
59
+ - ``grasps``: (N, 4, 4) array of grasp poses in camera frame
60
+ - ``confidence``: (N,) array of confidence scores in [0, 1]
61
+ - ``latency_ms``: server-reported latency
62
+
63
+ Examples:
46
64
  >>> from grid_cortex_client import CortexClient, ModelType
47
65
  >>> import numpy as np
48
66
  >>> client = CortexClient()
@@ -302,14 +320,18 @@ class ModelType(Enum):
302
320
  """
303
321
  METRIC3D = "metric3d"
304
322
  """
305
- Estimate metric depth and surface normals from a single RGB image.
323
+ Estimate metric depth and/or surface normals from a single RGB image.
306
324
 
307
325
  Args:
308
326
  image_input (Union[str, Image.Image, np.ndarray]): RGB image as file path, URL, PIL Image, or numpy array.
309
327
  timeout (float | None): Optional timeout in seconds for the HTTP request.
328
+ outputs (Optional[Union[str, Sequence[str]]]): Keyword-only. Optional selection of maps to return —
329
+ ``"depth"``, ``"normals"``, or a non-empty subset of both.
330
+ ``None`` (default) returns both. Requesting only what you need
331
+ shrinks the response payload (normals are 3x the size of depth).
310
332
 
311
333
  Returns:
312
- Dict with keys:
334
+ Dict with the requested subset of:
313
335
  - ``"depth"``: (H, W) float32 array, metric depth in metres.
314
336
  - ``"normals"``: (H, W, 3) float32 array, unit surface normals.
315
337
 
@@ -0,0 +1,129 @@
1
+ """C-RADIO wrapper.
2
+
3
+ Client-side helper for the dense visual embedding endpoint powered by
4
+ NVIDIA C-RADIOv4-SO400M.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from io import BytesIO
10
+ from typing import Any, Dict, Union
11
+
12
+ import numpy as np
13
+ from PIL import Image
14
+
15
+ from ..preprocessing import load_image, image_to_bytes
16
+ from .base_model import BaseModel
17
+
18
+
19
+ class CRadio(BaseModel):
20
+ """Dense visual embedding (C-RADIOv4-SO400M).
21
+
22
+ Preferred usage (through :class:`CortexClient`)
23
+ ---------------------------------------------
24
+ ```pycon
25
+ >>> emb = CortexClient().run(ModelType.C_RADIO, image_input=img)
26
+ >>> emb.shape
27
+ (2304,)
28
+ >>> float(np.linalg.norm(emb))
29
+ 1.0
30
+ ```
31
+
32
+ Args (for :py:meth:`run`)
33
+ ------------------------
34
+ image_input: RGB image (path, URL, ``PIL.Image`` or ``np.ndarray``).
35
+
36
+ Returns
37
+ -------
38
+ np.ndarray
39
+ L2-normalized embedding vector (``float32``, 1-D). Cosine similarity
40
+ between two outputs is a plain ``a @ b``.
41
+ """
42
+
43
+ name: str = "c-radio"
44
+ model_id: str = "c-radio"
45
+
46
+ def __init__(self, *, transport=None) -> None:
47
+ """Create a C-RADIO wrapper.
48
+
49
+ Args:
50
+ transport: Optional shared HTTP transport injected by
51
+ :class:`~grid_cortex_client.CortexClient`.
52
+ """
53
+ super().__init__(transport=transport)
54
+
55
+ # ------------------------------------------------------------------
56
+ # BaseModel interface
57
+ # ------------------------------------------------------------------
58
+
59
+ def preprocess(
60
+ self,
61
+ *,
62
+ image_input: Union[str, Image.Image, np.ndarray],
63
+ ) -> Dict[str, Any]:
64
+ """Return msgpack-ready payload with raw JPEG image bytes."""
65
+ pil_img = load_image(image_input)
66
+ return {"image_input": image_to_bytes(pil_img, encoding_format="JPEG")}
67
+
68
+ def postprocess(
69
+ self,
70
+ response_data: Dict[str, Any],
71
+ **_: Any,
72
+ ) -> np.ndarray:
73
+ """Decode backend response into an L2-normalized embedding array.
74
+
75
+ Args:
76
+ response_data: Backend dict (or list-wrapped dict) containing an
77
+ ``"output"`` key holding ``.npy`` bytes for a 1-D float vector.
78
+
79
+ Returns:
80
+ np.ndarray of shape ``(D,)``, dtype ``float32``, L2-normalized.
81
+
82
+ Raises:
83
+ ValueError: If the payload is missing or cannot be decoded.
84
+ """
85
+ if isinstance(response_data, list):
86
+ response_dict = response_data[0]
87
+ else:
88
+ response_dict = response_data
89
+
90
+ emb_bytes = response_dict.get("output")
91
+ if not isinstance(emb_bytes, (bytes, bytearray, memoryview)):
92
+ raise ValueError("'output' field missing or not bytes")
93
+
94
+ arr = np.load(BytesIO(emb_bytes), allow_pickle=False)
95
+ return arr.astype(np.float32)
96
+
97
+ def run(
98
+ self,
99
+ image_input: Union[str, Image.Image, np.ndarray],
100
+ timeout: float | None = None,
101
+ ) -> np.ndarray:
102
+ """Compute an L2-normalized embedding for a single RGB image.
103
+
104
+ Args:
105
+ image_input: RGB image as file path, URL, PIL Image, or numpy array.
106
+ timeout: Optional timeout in seconds for the HTTP request.
107
+
108
+ Returns:
109
+ Embedding as numpy array of shape ``(D,)``, dtype ``float32``,
110
+ L2-normalized so dot products give cosine similarity directly.
111
+
112
+ Raises:
113
+ ValueError: If image cannot be loaded.
114
+ RuntimeError: If no HTTP transport is configured.
115
+ Exception: If the HTTP request fails.
116
+
117
+ Example:
118
+ >>> from grid_cortex_client import CortexClient, ModelType
119
+ >>> import numpy as np
120
+ >>> from PIL import Image
121
+ >>> client = CortexClient()
122
+ >>> image = np.array(Image.open("cat.jpg"))
123
+ >>> emb = client.run(ModelType.C_RADIO, image_input=image)
124
+ >>> print(emb.shape, float(np.linalg.norm(emb))) # (D,) 1.0
125
+ """
126
+ return super().run(
127
+ image_input=image_input,
128
+ timeout=timeout,
129
+ )
@@ -0,0 +1,124 @@
1
+ """Test C-RADIO model."""
2
+
3
+ import os
4
+ import time
5
+
6
+ import numpy as np
7
+ import pytest
8
+
9
+ from grid_cortex_client import AsyncCortexClient, CortexClient, CortexHubClient, ModelType
10
+ from grid_cortex_client.client import CortexAPIError
11
+
12
+ from .conftest import BaseModelTestHTTP, run_concurrent_requests, is_gpu_mem_close
13
+
14
+
15
+ class TestCRadioHTTP(BaseModelTestHTTP):
16
+ """HTTP-based tests for C-RADIO dense visual embedding model."""
17
+
18
+ @property
19
+ def batch_size(self) -> int:
20
+ """C-RADIO model batch size (matches `max_ongoing_requests` in config.yaml)."""
21
+ return 8
22
+
23
+ @property
24
+ def gpu_mem(self) -> float:
25
+ """C-RADIO model GPU memory budget (MB, matches config.yaml `gpu_mem`)."""
26
+ return 2000.0
27
+
28
+ @property
29
+ def max_input_size(self) -> tuple:
30
+ """C-RADIO model maximum input size (width, height)."""
31
+ return (720, 1280)
32
+
33
+ def test_basic_inference(self, client: CortexClient, rgb_image_np: np.ndarray):
34
+ """Test basic embedding extraction with valid input."""
35
+ emb = client.run(ModelType.C_RADIO, image_input=rgb_image_np)
36
+
37
+ assert emb is not None
38
+ assert isinstance(emb, np.ndarray)
39
+ assert emb.ndim == 1, f"emb.ndim {emb.ndim} != 1"
40
+ assert emb.shape[0] == 2304, f"emb dim {emb.shape[0]} != 2304 (C-RADIOv4-SO400M summary)"
41
+ assert emb.dtype == np.float32, f"emb.dtype {emb.dtype} != float32"
42
+ assert np.isfinite(emb).all(), "embedding contains NaN/Inf"
43
+ # L2-normalized → magnitude should be 1.0 within float32 tolerance.
44
+ norm = float(np.linalg.norm(emb))
45
+ assert abs(norm - 1.0) < 1e-3, f"embedding not L2-normalized (||emb|| = {norm})"
46
+
47
+ def test_image_input_list_rejected(self, client: CortexClient, rgb_image_np: np.ndarray):
48
+ """List image_input is rejected — at the client (TypeError) or server (422)."""
49
+ with pytest.raises((TypeError, CortexAPIError)):
50
+ client.run(ModelType.C_RADIO, image_input=[rgb_image_np, rgb_image_np])
51
+
52
+ def test_maximum_payload_size(self, client: CortexClient):
53
+ """Test that inference works with maximum allowed payload size."""
54
+ large_image = np.random.randint(0, 255, (*self.max_input_size, 3), dtype=np.uint8)
55
+ emb = client.run(ModelType.C_RADIO, image_input=large_image)
56
+
57
+ assert emb is not None
58
+ assert isinstance(emb, np.ndarray)
59
+ assert emb.ndim == 1
60
+
61
+ oversized_image = np.random.randint(0, 255, (4096, 4096, 3), dtype=np.uint8)
62
+ with pytest.raises(CortexAPIError) as exc_info:
63
+ client.run(ModelType.C_RADIO, image_input=oversized_image)
64
+ assert exc_info.value.status_code == 413
65
+
66
+ large_image = np.random.randint(0, 255, (*self.max_input_size, 3), dtype=np.uint8)
67
+ emb = client.run(ModelType.C_RADIO, image_input=large_image)
68
+
69
+ assert emb is not None
70
+ assert isinstance(emb, np.ndarray)
71
+ assert emb.ndim == 1
72
+
73
+ def test_inference_latency(self, client: CortexClient, rgb_image_np: np.ndarray):
74
+ """Test that inference latency is within acceptable range."""
75
+ start_time = time.time()
76
+ emb = client.run(ModelType.C_RADIO, image_input=rgb_image_np)
77
+ elapsed_time = time.time() - start_time
78
+
79
+ assert emb is not None
80
+ assert elapsed_time < 10.0, f"Inference took {elapsed_time:.2f}s, expected < 10s"
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_concurrent_requests(self, async_client: AsyncCortexClient, rgb_image_np: np.ndarray):
84
+ """Test that multiple concurrent requests are handled correctly."""
85
+
86
+ async def make_request():
87
+ return await async_client.run(ModelType.C_RADIO, image_input=rgb_image_np)
88
+
89
+ results = await run_concurrent_requests(make_request, num_requests=self.batch_size)
90
+
91
+ assert len(results) == self.batch_size
92
+ assert all(not isinstance(r, Exception) for r in results)
93
+ assert all(isinstance(r, np.ndarray) for r in results)
94
+ assert all(r.ndim == 1 for r in results)
95
+
96
+ @pytest.mark.asyncio
97
+ async def test_gpu_memory_usage(self, async_client: AsyncCortexClient, rgb_image_np: np.ndarray):
98
+ """Test that GPU memory usage is within expected limits during inference."""
99
+ large_image = np.random.randint(0, 255, (*self.max_input_size, 3), dtype=np.uint8)
100
+
101
+ async def make_request():
102
+ return await async_client.run(ModelType.C_RADIO, image_input=large_image, raw_response=True)
103
+
104
+ results = await run_concurrent_requests(make_request, num_requests=self.batch_size)
105
+
106
+ assert len(results) == self.batch_size
107
+ assert all(not isinstance(r, Exception) for r in results)
108
+ assert all(is_gpu_mem_close(self.gpu_mem, result["gpu_stats"]["mem_usage_mb"]) for result in results), (
109
+ f"GPU memory peak too high: {[result['gpu_stats']['mem_usage_mb'] for result in results]} MB"
110
+ )
111
+
112
+ @pytest.mark.asyncio
113
+ async def test_cortex_hub(self, rgb_image_np: np.ndarray):
114
+ """Test C-RADIO inference via CortexHubClient (WebSocket)."""
115
+ api_key = os.getenv("GRID_CORTEX_API_KEY") or os.getenv("CORTEX_API_KEY")
116
+ if not api_key:
117
+ pytest.skip("GRID_CORTEX_API_KEY or CORTEX_API_KEY not set")
118
+
119
+ async with CortexHubClient(api_key=api_key) as hub:
120
+ emb = await hub.run(ModelType.C_RADIO, image_input=rgb_image_np)
121
+
122
+ assert emb is not None
123
+ assert isinstance(emb, np.ndarray)
124
+ assert emb.ndim == 1