visionai-sdk-python 2.2.0.dev3__tar.gz → 2.2.0.dev5__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 (63) hide show
  1. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/.gitignore +2 -0
  2. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/PKG-INFO +7 -5
  3. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/README.md +6 -4
  4. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/pyproject.toml +3 -0
  5. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/async_client.py +5 -1
  6. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/auth/credentials.py +2 -1
  7. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/client.py +5 -1
  8. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/relay/__init__.py +4 -0
  9. visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples/__init__.py +28 -0
  10. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/relay/protocol.py +27 -2
  11. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python.egg-info/PKG-INFO +7 -5
  12. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python.egg-info/SOURCES.txt +10 -9
  13. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python.egg-info/scm_file_list.json +9 -8
  14. visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python.egg-info/scm_version.json +8 -0
  15. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/relay/test_protocol.py +37 -8
  16. visionai_sdk_python-2.2.0.dev3/src/visionai_sdk_python.egg-info/scm_version.json +0 -8
  17. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/.github/dependabot.yml +0 -0
  18. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/.github/workflows/ci.yml +0 -0
  19. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/.github/workflows/publish.yml +0 -0
  20. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/setup.cfg +0 -0
  21. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/__init__.py +0 -0
  22. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/_base.py +0 -0
  23. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/_jwt_verifier.py +0 -0
  24. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/_request_attribution.py +0 -0
  25. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/_source_header.py +0 -0
  26. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/auth/__init__.py +0 -0
  27. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/auth/_mixin.py +0 -0
  28. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/auth/async_resource.py +0 -0
  29. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/auth/models.py +0 -0
  30. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/auth/resource.py +0 -0
  31. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/constants.py +0 -0
  32. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/endpoints.py +0 -0
  33. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/exceptions.py +0 -0
  34. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/instrumentation.py +0 -0
  35. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/py.typed +0 -0
  36. {visionai_sdk_python-2.2.0.dev3/tests/relay/fixtures/adr → visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples}/error_connection.json +0 -0
  37. {visionai_sdk_python-2.2.0.dev3/tests/relay/fixtures/adr → visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples}/error_request.json +0 -0
  38. {visionai_sdk_python-2.2.0.dev3/tests/relay/fixtures/adr → visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples}/heartbeat.json +0 -0
  39. {visionai_sdk_python-2.2.0.dev3/tests/relay/fixtures/adr → visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples}/register.json +0 -0
  40. {visionai_sdk_python-2.2.0.dev3/tests/relay/fixtures/adr → visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples}/registered.json +0 -0
  41. {visionai_sdk_python-2.2.0.dev3/tests/relay/fixtures/adr → visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples}/request.json +0 -0
  42. {visionai_sdk_python-2.2.0.dev3/tests/relay/fixtures/adr → visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples}/response.json +0 -0
  43. {visionai_sdk_python-2.2.0.dev3/tests/relay/fixtures/adr → visionai_sdk_python-2.2.0.dev5/src/visionai_sdk_python/relay/examples}/response_text.json +0 -0
  44. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/vlm/__init__.py +0 -0
  45. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/vlm/_mixin.py +0 -0
  46. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/vlm/async_resource.py +0 -0
  47. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/vlm/models.py +0 -0
  48. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/vlm/preprocess.py +0 -0
  49. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python/vlm/resource.py +0 -0
  50. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python.egg-info/dependency_links.txt +0 -0
  51. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python.egg-info/requires.txt +0 -0
  52. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/src/visionai_sdk_python.egg-info/top_level.txt +0 -0
  53. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/__init__.py +0 -0
  54. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/constants.py +0 -0
  55. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/relay/__init__.py +0 -0
  56. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/test__base.py +0 -0
  57. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/test_async_client.py +0 -0
  58. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/test_client.py +0 -0
  59. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/test_credentials.py +0 -0
  60. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/test_instrumentation.py +0 -0
  61. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/test_request.py +0 -0
  62. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/tests/test_vlm_preprocess.py +0 -0
  63. {visionai_sdk_python-2.2.0.dev3 → visionai_sdk_python-2.2.0.dev5}/uv.lock +0 -0
@@ -67,6 +67,8 @@ dmypy.json
67
67
 
68
68
  # Local test usage
69
69
  examples/
70
+ # The relay protocol's ADR examples are package data, not scratch files.
71
+ !src/visionai_sdk_python/relay/examples/
70
72
 
71
73
  # Claude
72
74
  .claude/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visionai-sdk-python
3
- Version: 2.2.0.dev3
3
+ Version: 2.2.0.dev5
4
4
  Summary: VisionAI SDK for Python
5
5
  Author-email: Tony Yang <tonyyang@linkervision.com>
6
6
  Requires-Python: >=3.11
@@ -148,7 +148,8 @@ client = Client(auth_url="...", vlm_url="...", credential=cred) # vlm.chat() u
148
148
 
149
149
  token = cred.get_token().token # for a WebSocket upgrade or another library
150
150
 
151
- # Any other HTTP call with the same token; a 401 gets one fresh token and one retry
151
+ # Any other HTTP call with the same token, sent to the host you name; a 401 gets one
152
+ # fresh token and one retry
152
153
  nodes = client.request("GET", "https://vs.example.com/mgmt/nodes").json()
153
154
  ```
154
155
 
@@ -168,10 +169,11 @@ authentication wins. `vlm_url` is optional for clients that only authenticate.
168
169
 
169
170
  `visionai_sdk_python.relay` holds the wire format between platform-relay and
170
171
  platform-edge-agent: the frame models, the constants and `parse_agent_frame()` /
171
- `parse_relay_frame()` / `dump_frame()`. The spec is platform-service's
172
+ `parse_relay_frame()` / `dump_frame()` (which refuses a frame over `MAX_FRAME_BYTES`). The spec is platform-service's
172
173
  `docs/adr/0001-relay-protocol.md`; both sides import this module instead of keeping a
173
- copy, and the ADR's JSON examples are this package's tests. It is internal to the
174
- platform and not a public API.
174
+ copy. The ADR's JSON examples ship with it (`visionai_sdk_python.relay.examples`:
175
+ `example(name)` / `example_text(name)`), so every implementation tests against the same
176
+ frames. It is internal to the platform and not a public API.
175
177
 
176
178
  ## VLM Inference
177
179
 
@@ -130,7 +130,8 @@ client = Client(auth_url="...", vlm_url="...", credential=cred) # vlm.chat() u
130
130
 
131
131
  token = cred.get_token().token # for a WebSocket upgrade or another library
132
132
 
133
- # Any other HTTP call with the same token; a 401 gets one fresh token and one retry
133
+ # Any other HTTP call with the same token, sent to the host you name; a 401 gets one
134
+ # fresh token and one retry
134
135
  nodes = client.request("GET", "https://vs.example.com/mgmt/nodes").json()
135
136
  ```
136
137
 
@@ -150,10 +151,11 @@ authentication wins. `vlm_url` is optional for clients that only authenticate.
150
151
 
151
152
  `visionai_sdk_python.relay` holds the wire format between platform-relay and
152
153
  platform-edge-agent: the frame models, the constants and `parse_agent_frame()` /
153
- `parse_relay_frame()` / `dump_frame()`. The spec is platform-service's
154
+ `parse_relay_frame()` / `dump_frame()` (which refuses a frame over `MAX_FRAME_BYTES`). The spec is platform-service's
154
155
  `docs/adr/0001-relay-protocol.md`; both sides import this module instead of keeping a
155
- copy, and the ADR's JSON examples are this package's tests. It is internal to the
156
- platform and not a public API.
156
+ copy. The ADR's JSON examples ship with it (`visionai_sdk_python.relay.examples`:
157
+ `example(name)` / `example_text(name)`), so every implementation tests against the same
158
+ frames. It is internal to the platform and not a public API.
157
159
 
158
160
  ## VLM Inference
159
161
 
@@ -32,6 +32,9 @@ build-backend = "setuptools.build_meta"
32
32
 
33
33
  [tool.setuptools_scm]
34
34
 
35
+ [tool.setuptools.package-data]
36
+ "visionai_sdk_python.relay.examples" = ["*.json"]
37
+
35
38
  [tool.uv]
36
39
  # Mitigate supply chain attacks. Kept in sync with
37
40
  # .github/dependabot.yml's cooldown.default-days (3 days, matching the
@@ -58,7 +58,11 @@ class AsyncClient(_BaseClient):
58
58
  async def request(self, method: str, url: str, **kwargs) -> httpx.Response:
59
59
  """Send any request with this client's bearer token.
60
60
 
61
- ``url`` is a full URL (the client has two bases); ``kwargs`` go to httpx.
61
+ ``url`` is a full URL (the client has two bases); ``kwargs`` go to httpx. The
62
+ bearer token goes to whatever host ``url`` names, so ``url`` must come from
63
+ configuration or code, never from a caller's input; redirects are not
64
+ followed unless asked, and httpx drops the header on a cross-origin redirect.
65
+
62
66
  With a ``credential``, a 401 buys one retry with the credential's current
63
67
  token, which it replaces when the rejected one is old enough to have been
64
68
  revoked (``REPLACE_COOLDOWN_S``); a 401 on the retry raises. Without a
@@ -237,7 +237,8 @@ class AsyncClientCredentials(_Credentials):
237
237
 
238
238
  Pass it to ``AsyncClient(credential=...)`` and use ``await get_token()`` for
239
239
  requests the SDK does not make itself, such as a WebSocket upgrade. One
240
- instance serves any number of clients and callers.
240
+ instance serves any number of clients and callers on one event loop; its
241
+ lock and ``httpx.AsyncClient`` bind to that loop.
241
242
  """
242
243
 
243
244
  def __init__(
@@ -58,7 +58,11 @@ class Client(_BaseClient):
58
58
  def request(self, method: str, url: str, **kwargs) -> httpx.Response:
59
59
  """Send any request with this client's bearer token.
60
60
 
61
- ``url`` is a full URL (the client has two bases); ``kwargs`` go to httpx.
61
+ ``url`` is a full URL (the client has two bases); ``kwargs`` go to httpx. The
62
+ bearer token goes to whatever host ``url`` names, so ``url`` must come from
63
+ configuration or code, never from a caller's input; redirects are not
64
+ followed unless asked, and httpx drops the header on a cross-origin redirect.
65
+
62
66
  With a ``credential``, a 401 buys one retry with the credential's current
63
67
  token, which it replaces when the rejected one is old enough to have been
64
68
  revoked (``REPLACE_COOLDOWN_S``); a 401 on the retry raises. Without a
@@ -9,6 +9,7 @@ from .protocol import (
9
9
  HEARTBEAT_INTERVAL_S,
10
10
  MAX_BODY_BYTES,
11
11
  MAX_FRAME_BYTES,
12
+ MAX_MESSAGE_CHARS,
12
13
  REGISTER_DEADLINE_S,
13
14
  RELAY_TIMEOUT_S,
14
15
  STALE_AFTER_S,
@@ -18,6 +19,7 @@ from .protocol import (
18
19
  Error,
19
20
  ErrorCode,
20
21
  Frame,
22
+ FrameTooLarge,
21
23
  Heartbeat,
22
24
  InvalidFrame,
23
25
  ModelEntry,
@@ -40,6 +42,7 @@ __all__ = [
40
42
  "HEARTBEAT_INTERVAL_S",
41
43
  "MAX_BODY_BYTES",
42
44
  "MAX_FRAME_BYTES",
45
+ "MAX_MESSAGE_CHARS",
43
46
  "REGISTER_DEADLINE_S",
44
47
  "RELAY_TIMEOUT_S",
45
48
  "STALE_AFTER_S",
@@ -49,6 +52,7 @@ __all__ = [
49
52
  "Error",
50
53
  "ErrorCode",
51
54
  "Frame",
55
+ "FrameTooLarge",
52
56
  "Heartbeat",
53
57
  "InvalidFrame",
54
58
  "ModelEntry",
@@ -0,0 +1,28 @@
1
+ """The ADR's JSON examples, shipped so every implementation tests against the same frames.
2
+
3
+ ``example("register")`` is the parsed object, ``example_text("register")`` the wire text.
4
+ """
5
+
6
+ import json
7
+ from importlib.resources import files
8
+
9
+ NAMES = (
10
+ "register",
11
+ "registered",
12
+ "heartbeat",
13
+ "request",
14
+ "response",
15
+ "response_text",
16
+ "error_request",
17
+ "error_connection",
18
+ )
19
+
20
+
21
+ def example_text(name: str) -> str:
22
+ """One ADR example as the text that goes over the wire."""
23
+ return (files(__package__) / f"{name}.json").read_text(encoding="utf-8")
24
+
25
+
26
+ def example(name: str) -> dict:
27
+ """One ADR example as a dict, for building or comparing frames in tests."""
28
+ return json.loads(example_text(name))
@@ -28,6 +28,8 @@ MAX_BODY_BYTES = 15 * 1024 * 1024
28
28
  # Timeouts nest: the agent waits for VS, the relay waits a little longer for the agent.
29
29
  AGENT_TIMEOUT_S = 100
30
30
  RELAY_TIMEOUT_S = 110.0
31
+ # An `error` echoes parts of the peer's frame; cut them so the error fits a frame.
32
+ MAX_MESSAGE_CHARS = 200
31
33
 
32
34
  # Platform workspace ids: lowercase hyphenated UUIDs.
33
35
  UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
@@ -58,6 +60,7 @@ class ErrorCode(StrEnum):
58
60
  PATH_NOT_ALLOWED = "path_not_allowed"
59
61
  UPSTREAM_UNREACHABLE = "upstream_unreachable"
60
62
  UPSTREAM_TIMEOUT = "upstream_timeout"
63
+ UPSTREAM_TOO_LARGE = "upstream_too_large"
61
64
 
62
65
 
63
66
  class Frame(BaseModel):
@@ -133,13 +136,30 @@ _relay_frame = TypeAdapter(Annotated[RelayFrame, Field(discriminator="type")])
133
136
 
134
137
 
135
138
  class InvalidFrame(ValueError):
136
- """A frame the receiver cannot read; the text is safe to send back in an ``error``."""
139
+ """A frame the receiver cannot read; the text is safe to send back in an ``error``.
140
+
141
+ Parts of the text come from the peer's frame, so it is cut at ``MAX_MESSAGE_CHARS``
142
+ and a ``request_id`` longer than that is dropped.
143
+ """
137
144
 
138
145
  def __init__(self, message: str, request_id: str | None = None) -> None:
146
+ if len(message) > MAX_MESSAGE_CHARS:
147
+ message = message[: MAX_MESSAGE_CHARS - 1] + "…"
148
+ if request_id is not None and len(request_id) > MAX_MESSAGE_CHARS:
149
+ request_id = None
150
+ message += " (request_id too long, dropped)"
139
151
  super().__init__(message)
140
152
  self.request_id = request_id
141
153
 
142
154
 
155
+ class FrameTooLarge(ValueError):
156
+ """The encoded frame exceeds ``MAX_FRAME_BYTES``; the receiver would close with 1009."""
157
+
158
+ def __init__(self, size: int) -> None:
159
+ super().__init__(f"frame is {size} bytes; the limit is {MAX_FRAME_BYTES}")
160
+ self.size = size
161
+
162
+
143
163
  def parse_agent_frame(text: str) -> AgentFrame:
144
164
  """A frame the relay received from an agent."""
145
165
  return _parse(text, _agent_frame)
@@ -175,4 +195,9 @@ def _parse(text: str, adapter: TypeAdapter):
175
195
 
176
196
 
177
197
  def dump_frame(frame: BaseModel) -> str:
178
- return frame.model_dump_json(exclude_none=True)
198
+ """The frame as JSON text, or ``FrameTooLarge`` when a receiver would refuse it."""
199
+ text = frame.model_dump_json(exclude_none=True)
200
+ size = len(text.encode())
201
+ if size > MAX_FRAME_BYTES:
202
+ raise FrameTooLarge(size)
203
+ return text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visionai-sdk-python
3
- Version: 2.2.0.dev3
3
+ Version: 2.2.0.dev5
4
4
  Summary: VisionAI SDK for Python
5
5
  Author-email: Tony Yang <tonyyang@linkervision.com>
6
6
  Requires-Python: >=3.11
@@ -148,7 +148,8 @@ client = Client(auth_url="...", vlm_url="...", credential=cred) # vlm.chat() u
148
148
 
149
149
  token = cred.get_token().token # for a WebSocket upgrade or another library
150
150
 
151
- # Any other HTTP call with the same token; a 401 gets one fresh token and one retry
151
+ # Any other HTTP call with the same token, sent to the host you name; a 401 gets one
152
+ # fresh token and one retry
152
153
  nodes = client.request("GET", "https://vs.example.com/mgmt/nodes").json()
153
154
  ```
154
155
 
@@ -168,10 +169,11 @@ authentication wins. `vlm_url` is optional for clients that only authenticate.
168
169
 
169
170
  `visionai_sdk_python.relay` holds the wire format between platform-relay and
170
171
  platform-edge-agent: the frame models, the constants and `parse_agent_frame()` /
171
- `parse_relay_frame()` / `dump_frame()`. The spec is platform-service's
172
+ `parse_relay_frame()` / `dump_frame()` (which refuses a frame over `MAX_FRAME_BYTES`). The spec is platform-service's
172
173
  `docs/adr/0001-relay-protocol.md`; both sides import this module instead of keeping a
173
- copy, and the ADR's JSON examples are this package's tests. It is internal to the
174
- platform and not a public API.
174
+ copy. The ADR's JSON examples ship with it (`visionai_sdk_python.relay.examples`:
175
+ `example(name)` / `example_text(name)`), so every implementation tests against the same
176
+ frames. It is internal to the platform and not a public API.
175
177
 
176
178
  ## VLM Inference
177
179
 
@@ -32,6 +32,15 @@ src/visionai_sdk_python/auth/models.py
32
32
  src/visionai_sdk_python/auth/resource.py
33
33
  src/visionai_sdk_python/relay/__init__.py
34
34
  src/visionai_sdk_python/relay/protocol.py
35
+ src/visionai_sdk_python/relay/examples/__init__.py
36
+ src/visionai_sdk_python/relay/examples/error_connection.json
37
+ src/visionai_sdk_python/relay/examples/error_request.json
38
+ src/visionai_sdk_python/relay/examples/heartbeat.json
39
+ src/visionai_sdk_python/relay/examples/register.json
40
+ src/visionai_sdk_python/relay/examples/registered.json
41
+ src/visionai_sdk_python/relay/examples/request.json
42
+ src/visionai_sdk_python/relay/examples/response.json
43
+ src/visionai_sdk_python/relay/examples/response_text.json
35
44
  src/visionai_sdk_python/vlm/__init__.py
36
45
  src/visionai_sdk_python/vlm/_mixin.py
37
46
  src/visionai_sdk_python/vlm/async_resource.py
@@ -48,12 +57,4 @@ tests/test_instrumentation.py
48
57
  tests/test_request.py
49
58
  tests/test_vlm_preprocess.py
50
59
  tests/relay/__init__.py
51
- tests/relay/test_protocol.py
52
- tests/relay/fixtures/adr/error_connection.json
53
- tests/relay/fixtures/adr/error_request.json
54
- tests/relay/fixtures/adr/heartbeat.json
55
- tests/relay/fixtures/adr/register.json
56
- tests/relay/fixtures/adr/registered.json
57
- tests/relay/fixtures/adr/request.json
58
- tests/relay/fixtures/adr/response.json
59
- tests/relay/fixtures/adr/response_text.json
60
+ tests/relay/test_protocol.py
@@ -25,6 +25,15 @@
25
25
  "src/visionai_sdk_python/instrumentation.py",
26
26
  "src/visionai_sdk_python/py.typed",
27
27
  "src/visionai_sdk_python/relay/__init__.py",
28
+ "src/visionai_sdk_python/relay/examples/__init__.py",
29
+ "src/visionai_sdk_python/relay/examples/error_connection.json",
30
+ "src/visionai_sdk_python/relay/examples/error_request.json",
31
+ "src/visionai_sdk_python/relay/examples/heartbeat.json",
32
+ "src/visionai_sdk_python/relay/examples/register.json",
33
+ "src/visionai_sdk_python/relay/examples/registered.json",
34
+ "src/visionai_sdk_python/relay/examples/request.json",
35
+ "src/visionai_sdk_python/relay/examples/response.json",
36
+ "src/visionai_sdk_python/relay/examples/response_text.json",
28
37
  "src/visionai_sdk_python/relay/protocol.py",
29
38
  "src/visionai_sdk_python/vlm/__init__.py",
30
39
  "src/visionai_sdk_python/vlm/_mixin.py",
@@ -35,14 +44,6 @@
35
44
  "tests/__init__.py",
36
45
  "tests/constants.py",
37
46
  "tests/relay/__init__.py",
38
- "tests/relay/fixtures/adr/error_connection.json",
39
- "tests/relay/fixtures/adr/error_request.json",
40
- "tests/relay/fixtures/adr/heartbeat.json",
41
- "tests/relay/fixtures/adr/register.json",
42
- "tests/relay/fixtures/adr/registered.json",
43
- "tests/relay/fixtures/adr/request.json",
44
- "tests/relay/fixtures/adr/response.json",
45
- "tests/relay/fixtures/adr/response_text.json",
46
47
  "tests/relay/test_protocol.py",
47
48
  "tests/test__base.py",
48
49
  "tests/test_async_client.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.2.0.dev5",
3
+ "distance": 0,
4
+ "node": "g300feb4397722a72a04551325a2fc4d0d8c0e3f3",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-22"
8
+ }
@@ -1,12 +1,14 @@
1
1
  """The relay wire format: the ADR's JSON examples parse on both sides, bad frames say why."""
2
2
 
3
3
  import json
4
- from pathlib import Path
5
4
 
6
5
  import pytest
7
6
 
8
7
  from visionai_sdk_python.relay import (
8
+ MAX_FRAME_BYTES,
9
+ MAX_MESSAGE_CHARS,
9
10
  Error,
11
+ FrameTooLarge,
10
12
  Heartbeat,
11
13
  InvalidFrame,
12
14
  Register,
@@ -18,13 +20,8 @@ from visionai_sdk_python.relay import (
18
20
  parse_agent_frame,
19
21
  parse_relay_frame,
20
22
  )
21
-
22
- FIXTURES = Path(__file__).parent / "fixtures" / "adr"
23
-
24
-
25
- def example(name: str) -> str:
26
- """One of the ADR's JSON examples, as the text that goes over the wire."""
27
- return (FIXTURES / f"{name}.json").read_text()
23
+ from visionai_sdk_python.relay.examples import NAMES
24
+ from visionai_sdk_python.relay.examples import example_text as example
28
25
 
29
26
 
30
27
  @pytest.mark.parametrize(
@@ -174,3 +171,35 @@ def test_dump_frame_drops_none_fields():
174
171
  )
175
172
  def test_workspace_id_format(value, ok):
176
173
  assert is_workspace_id(value) is ok
174
+
175
+
176
+ def test_every_shipped_example_is_valid_json_with_a_type():
177
+ for name in NAMES:
178
+ assert isinstance(json.loads(example(name))["type"], str), name
179
+
180
+
181
+ def test_an_error_message_is_cut_so_it_fits_a_frame():
182
+ with pytest.raises(InvalidFrame) as exc:
183
+ parse_agent_frame(json.dumps({"type": "x" * 1_000_000}))
184
+
185
+ assert len(str(exc.value)) == MAX_MESSAGE_CHARS
186
+ assert str(exc.value).endswith("…")
187
+
188
+
189
+ def test_an_oversized_request_id_is_dropped_from_the_error():
190
+ frame = {"type": "request", "request_id": "r" * 1_000_000}
191
+ with pytest.raises(InvalidFrame) as exc:
192
+ parse_relay_frame(json.dumps(frame))
193
+
194
+ assert exc.value.request_id is None
195
+ assert str(exc.value).endswith("(request_id too long, dropped)")
196
+
197
+
198
+ def test_dump_frame_refuses_a_frame_the_receiver_would_close_on():
199
+ frame = Response(request_id="r1", status=200, body="x" * MAX_FRAME_BYTES)
200
+
201
+ with pytest.raises(FrameTooLarge) as exc:
202
+ dump_frame(frame)
203
+
204
+ assert exc.value.size > MAX_FRAME_BYTES
205
+ assert dump_frame(Response(request_id="r1", status=200, body="x"))
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.2.0.dev3",
3
- "distance": 0,
4
- "node": "g33d14e37988767f78ffc344ae07033c41e85d7bf",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-21"
8
- }