fastsdk 0.4.2__tar.gz → 0.4.3__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 (55) hide show
  1. {fastsdk-0.4.2 → fastsdk-0.4.3}/PKG-INFO +1 -1
  2. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/api_client.py +2 -1
  3. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/api_client_runpod.py +29 -1
  4. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/response_parser.py +28 -0
  5. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/PKG-INFO +1 -1
  6. {fastsdk-0.4.2 → fastsdk-0.4.3}/pyproject.toml +4 -4
  7. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_apipod_debug_test_services.py +105 -0
  8. {fastsdk-0.4.2 → fastsdk-0.4.3}/LICENSE +0 -0
  9. {fastsdk-0.4.2 → fastsdk-0.4.3}/README.md +0 -0
  10. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/__init__.py +0 -0
  11. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/api.py +0 -0
  12. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/cli.py +0 -0
  13. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/fastClient.py +0 -0
  14. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/fastSDK.py +0 -0
  15. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/fastStub.py +0 -0
  16. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/requires.py +0 -0
  17. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/sdk_factory/__init__.py +0 -0
  18. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/sdk_factory/sdk_factory.py +0 -0
  19. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/sdk_factory/sdk_template.j2 +0 -0
  20. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_access.py +0 -0
  21. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/__init__.py +0 -0
  22. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/api_job_manager.py +0 -0
  23. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/api_seex.py +0 -0
  24. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/async_bridge.py +0 -0
  25. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/job_runtime.py +0 -0
  26. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/job_tasks.py +0 -0
  27. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/pipeline_planner.py +0 -0
  28. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/provider_factory.py +0 -0
  29. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/provider_stack_registry.py +0 -0
  30. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/__init__.py +0 -0
  31. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/api_client_replicate.py +0 -0
  32. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/api_client_socaity.py +0 -0
  33. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/file_handler.py +0 -0
  34. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/api_job_status.py +0 -0
  35. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/response_schemas.py +0 -0
  36. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/sse_assembly.py +0 -0
  37. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/stream_session.py +0 -0
  38. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_specification_loader/replicate_loader.py +0 -0
  39. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_specification_loader/runpod_open_api_loader.py +0 -0
  40. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_specification_loader/spec_loader.py +0 -0
  41. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/SOURCES.txt +0 -0
  42. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/dependency_links.txt +0 -0
  43. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/entry_points.txt +0 -0
  44. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/requires.txt +0 -0
  45. {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/top_level.txt +0 -0
  46. {fastsdk-0.4.2 → fastsdk-0.4.3}/setup.cfg +0 -0
  47. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_apipod_registry_service_management_example.py +0 -0
  48. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_client_factory.py +0 -0
  49. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_client_factory_manual.py +0 -0
  50. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_client_from_fastapi.py +0 -0
  51. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_client_from_runpod_serverless.py +0 -0
  52. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_httpx_api.py +0 -0
  53. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_localhost_services.py +0 -0
  54. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_manual_sdk.py +0 -0
  55. {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_replicate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastsdk
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Turn any AI/web service (OpenAPI, Replicate, RunPod, APIPod) into a native-feeling Python client.
5
5
  Author: SocAIty
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -225,7 +225,6 @@ class APIClient:
225
225
  return True
226
226
  return bool(schema.get("properties"))
227
227
 
228
-
229
228
  def format_request_params(self, endpoint: Endpoint, data: dict) -> RequestData:
230
229
  """Prepare all request parameters for the endpoint."""
231
230
  body_content_type = getattr(endpoint, "request_body_content_type", None)
@@ -242,6 +241,8 @@ class APIClient:
242
241
  rq = RequestData(body_content_type=body_content_type)
243
242
  embed_files_in_json_body = self._uses_json_request_body(endpoint)
244
243
  body_params = [p for p in endpoint.parameters if p.location == "body"]
244
+ # Sole JSON object body (e.g. ChatCompletionRequest as ``request``):
245
+ # spread model fields onto the wire root. Callers still pass request={...}.
245
246
  single_json_object_body = (
246
247
  embed_files_in_json_body
247
248
  and len(body_params) == 1
@@ -35,8 +35,16 @@ class APIClientRunpod(APIClient):
35
35
  return getattr(response, "output", None)
36
36
 
37
37
  def format_request_params(self, endpoint: Endpoint, *args, **kwargs) -> RequestData:
38
- """Prepare request parameters for Runpod API."""
38
+ """Prepare request parameters for Runpod API.
39
+
40
+ Shared ``APIClient`` spreads a sole JSON object body onto the wire root
41
+ for HTTP. RunPod ``input`` is function kwargs, so re-nest that object
42
+ under the schema param name (usually ``request``) and keep ``path``.
43
+ """
39
44
  request_data = super().format_request_params(endpoint, *args, **kwargs)
45
+ request_data.body_params = self._renest_sole_json_object_body(
46
+ endpoint, request_data.body_params
47
+ )
40
48
 
41
49
  # adding path to the body for runpod apipod services
42
50
  if endpoint.path:
@@ -44,6 +52,26 @@ class APIClientRunpod(APIClient):
44
52
 
45
53
  return request_data
46
54
 
55
+ @classmethod
56
+ def _renest_sole_json_object_body(cls, endpoint: Endpoint, body_params: dict) -> dict:
57
+ body_defs = [p for p in (endpoint.parameters or []) if p.location == "body"]
58
+ if (
59
+ len(body_defs) != 1
60
+ or not cls._is_json_object_request_body(body_defs[0])
61
+ ):
62
+ return body_params
63
+
64
+ name = body_defs[0].name
65
+ if name in body_params:
66
+ return body_params
67
+
68
+ reserved = {"path"}
69
+ nested = {k: v for k, v in body_params.items() if k not in reserved}
70
+ keep = {k: v for k, v in body_params.items() if k in reserved}
71
+ if not nested:
72
+ return body_params
73
+ return {name: nested, **keep}
74
+
47
75
  async def send_request(self, request_data: RequestData, timeout_s: float = 60) -> httpx.Response:
48
76
  """
49
77
  Send the prepared request to the API.
@@ -12,6 +12,7 @@ Design:
12
12
  from __future__ import annotations
13
13
 
14
14
  import json
15
+ import os
15
16
  from typing import Any, Callable, Optional, Union
16
17
 
17
18
  import httpx
@@ -29,10 +30,31 @@ from socaity_schemas import (
29
30
  # ---------------------------------------------------------------------------
30
31
 
31
32
 
33
+ def _materialize_media() -> bool:
34
+ """When false (MCP / agents), keep FileModel URLs instead of downloading bytes."""
35
+ return os.environ.get("FASTSDK_MATERIALIZE_MEDIA", "1").strip().lower() not in {
36
+ "0",
37
+ "false",
38
+ "no",
39
+ "off",
40
+ }
41
+
42
+
32
43
  def _looks_like_file_model_dict(value: dict) -> bool:
33
44
  return "content" in value and ("content_type" in value or "file_name" in value)
34
45
 
35
46
 
47
+ def _file_ref_from_model(value: dict) -> dict:
48
+ content = value.get("content")
49
+ return {
50
+ "url": content if isinstance(content, str) else None,
51
+ "content": content if isinstance(content, str) else None,
52
+ "content_type": value.get("content_type"),
53
+ "file_name": value.get("file_name"),
54
+ "file_size": value.get("file_size"),
55
+ }
56
+
57
+
36
58
  def _parse_media_socaity(result: Any) -> Any:
37
59
  if result is None:
38
60
  return result
@@ -42,12 +64,16 @@ def _parse_media_socaity(result: Any) -> Any:
42
64
  if callable(model_dump):
43
65
  dumped = model_dump()
44
66
  if isinstance(dumped, dict) and _looks_like_file_model_dict(dumped):
67
+ if not _materialize_media():
68
+ return _file_ref_from_model(dumped)
45
69
  try:
46
70
  return media_from_any(dumped, allow_reads_from_disk=False)
47
71
  except Exception:
48
72
  pass
49
73
  if isinstance(result, dict):
50
74
  if _looks_like_file_model_dict(result):
75
+ if not _materialize_media():
76
+ return _file_ref_from_model(result)
51
77
  try:
52
78
  return media_from_any(result, allow_reads_from_disk=False)
53
79
  except Exception:
@@ -58,6 +84,8 @@ def _parse_media_socaity(result: Any) -> Any:
58
84
 
59
85
  def _parse_media_replicate(result: Any) -> Any:
60
86
  if isinstance(result, str) and "https://replicate.delivery" in result:
87
+ if not _materialize_media():
88
+ return {"url": result, "content": result}
61
89
  try:
62
90
  return media_from_any(result, allow_reads_from_disk=False)
63
91
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastsdk
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Turn any AI/web service (OpenAPI, Replicate, RunPod, APIPod) into a native-feeling Python client.
5
5
  Author: SocAIty
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastsdk"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "Turn any AI/web service (OpenAPI, Replicate, RunPod, APIPod) into a native-feeling Python client."
9
9
  requires-python = ">=3.8"
10
10
  authors = [
@@ -40,10 +40,10 @@ fastsdk = "fastsdk.cli:main"
40
40
  Repository = "https://github.com/SocAIty/fastsdk"
41
41
  Homepage = "https://www.socaity.ai"
42
42
 
43
- [tool.setuptools.package-data]
44
- "fastsdk.sdk_factory" = ["*.j2"]
45
-
46
43
  [tool.setuptools.packages.find]
47
44
  where = ["."]
48
45
  include = ["fastsdk*"]
49
46
  exclude = ["output*", "test*"]
47
+
48
+ [tool.setuptools.package-data]
49
+ "fastsdk.sdk_factory" = ["*.j2"]
@@ -17,6 +17,7 @@ Job polling follows the parsed contract's ``has_job_queue`` for queue-backed lau
17
17
  """
18
18
  from __future__ import annotations
19
19
 
20
+ import json
20
21
  import os
21
22
  from pathlib import Path
22
23
  from typing import Any, Iterable, Optional
@@ -191,6 +192,15 @@ def _choice_message(result: dict) -> str:
191
192
  return result["choices"][0]["message"]["content"]
192
193
 
193
194
 
195
+ def _collect_chunks(job) -> list[dict]:
196
+ """Collect the decoded ChatCompletionChunk dicts of one SSE stream."""
197
+ session = job.stream()
198
+ try:
199
+ return [chunk for chunk in session.iter_chunks() if isinstance(chunk, dict)]
200
+ finally:
201
+ session.close()
202
+
203
+
194
204
  def _collect_stream_text(job) -> str:
195
205
  session = job.stream()
196
206
  try:
@@ -398,6 +408,98 @@ def test_media_results_are_media_toolkit_files():
398
408
  pytest.skip("No media-returning endpoints are exposed by the current service launch")
399
409
 
400
410
 
411
+ WEATHER_TOOL = {
412
+ "type": "function",
413
+ "function": {
414
+ "name": "get_weather",
415
+ "description": "Current weather for a city.",
416
+ "parameters": {
417
+ "type": "object",
418
+ "properties": {"location": {"type": "string"}},
419
+ "required": ["location"],
420
+ },
421
+ },
422
+ }
423
+
424
+
425
+ def test_chat_tool_call():
426
+ """Tool round trip: tool_calls result, streamed tool delta, tool-result turn."""
427
+ client = _client()
428
+ endpoint = _require_stream_endpoint(_service_def(), "/chat")
429
+ messages = [{"role": "user", "content": "What is the weather in Boston?"}]
430
+
431
+ # Non-stream: parsed function call and finish_reason=tool_calls.
432
+ result = client.submit_job(
433
+ endpoint.path, messages=messages, tools=[WEATHER_TOOL], stream=False,
434
+ ).wait_for_result(timeout_s=JOB_TIMEOUT_S)
435
+ choice = result["choices"][0]
436
+ assert choice["finish_reason"] == "tool_calls"
437
+ call = choice["message"]["tool_calls"][0]
438
+ assert call["function"]["name"] == "get_weather"
439
+ assert "location" in json.loads(call["function"]["arguments"])
440
+
441
+ # Stream: a chunk carries the tool_calls delta; the stream closes with tool_calls.
442
+ stream_job = client.submit_job(endpoint.path, messages=messages, tools=[WEATHER_TOOL], stream=True)
443
+ chunks = _collect_chunks(stream_job)
444
+ tool_deltas = [c for c in chunks if c["choices"][0].get("delta", {}).get("tool_calls")]
445
+ assert tool_deltas, "no tool_calls delta in stream"
446
+ assert tool_deltas[0]["choices"][0]["delta"]["tool_calls"][0]["function"]["name"] == "get_weather"
447
+ finish_reasons = [c["choices"][0].get("finish_reason") for c in chunks if c["choices"][0].get("finish_reason")]
448
+ assert finish_reasons[-1] == "tool_calls"
449
+
450
+ # Tool-result turn: the assistant answers from the tool output.
451
+ follow_up = messages + [
452
+ {"role": "assistant", "content": None, "tool_calls": [call]},
453
+ {"role": "tool", "tool_call_id": call["id"], "content": "sunny, 21 degrees"},
454
+ ]
455
+ result = client.submit_job(
456
+ endpoint.path, messages=follow_up, tools=[WEATHER_TOOL], stream=False,
457
+ ).wait_for_result(timeout_s=JOB_TIMEOUT_S)
458
+ assert _choice_message(result)
459
+
460
+
461
+ def test_chat_logprobs():
462
+ """logprobs=True returns per-token entries with top_logprobs."""
463
+ client = _client()
464
+ endpoint = _require_stream_endpoint(_service_def(), "/chat")
465
+
466
+ result = client.submit_job(
467
+ endpoint.path,
468
+ messages=[{"role": "user", "content": "hi"}],
469
+ logprobs=True,
470
+ top_logprobs=2,
471
+ stream=False,
472
+ ).wait_for_result(timeout_s=JOB_TIMEOUT_S)
473
+
474
+ entries = result["choices"][0]["logprobs"]["content"]
475
+ assert entries, "empty logprobs content"
476
+ first = entries[0]
477
+ assert first["token"]
478
+ assert isinstance(first["logprob"], float) and first["logprob"] <= 0.0
479
+ assert first.get("top_logprobs"), "top_logprobs missing"
480
+
481
+
482
+ def test_chat_reasoning():
483
+ """Reasoning (<think>) output lands in reasoning_content, not in content."""
484
+ client = _client()
485
+ endpoint = _require_stream_endpoint(_service_def(), "/chat")
486
+ messages = [{"role": "user", "content": "hi, please think first"}]
487
+
488
+ result = client.submit_job(
489
+ endpoint.path, messages=messages, stream=False,
490
+ ).wait_for_result(timeout_s=JOB_TIMEOUT_S)
491
+ message = result["choices"][0]["message"]
492
+ assert message["reasoning_content"]
493
+ assert message["content"] and "<think>" not in message["content"]
494
+
495
+ stream_job = client.submit_job(endpoint.path, messages=messages, stream=True)
496
+ chunks = _collect_chunks(stream_job)
497
+ reasoning = "".join(c["choices"][0].get("delta", {}).get("reasoning_content") or "" for c in chunks)
498
+ content = "".join(c["choices"][0].get("delta", {}).get("content") or "" for c in chunks)
499
+ assert reasoning
500
+ assert content and "<think>" not in content
501
+
502
+
401
503
  def test_connect_chat_extended():
402
504
  client = _client()
403
505
  endpoint = _endpoint_by_suffix(_service_def(), "/chat-extended")
@@ -417,6 +519,9 @@ def _main_tests() -> Iterable[str]:
417
519
  "test_streaming_text",
418
520
  "test_streaming_video",
419
521
  "test_streaming_chat_schema",
522
+ "test_chat_tool_call",
523
+ "test_chat_logprobs",
524
+ "test_chat_reasoning",
420
525
  "test_media_results_are_media_toolkit_files",
421
526
  "test_connect_chat_extended",
422
527
  )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes