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.
- {fastsdk-0.4.2 → fastsdk-0.4.3}/PKG-INFO +1 -1
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/api_client.py +2 -1
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/api_client_runpod.py +29 -1
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/response_parser.py +28 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/PKG-INFO +1 -1
- {fastsdk-0.4.2 → fastsdk-0.4.3}/pyproject.toml +4 -4
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_apipod_debug_test_services.py +105 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/LICENSE +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/README.md +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/__init__.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/api.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/cli.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/fastClient.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/fastSDK.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/fastStub.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/requires.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/sdk_factory/__init__.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/sdk_factory/sdk_factory.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/sdk_factory/sdk_template.j2 +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_access.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/__init__.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/api_job_manager.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/api_seex.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/async_bridge.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/job_runtime.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/job_tasks.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/pipeline_planner.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/provider_factory.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/provider_stack_registry.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/__init__.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/api_client_replicate.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/api_client_socaity.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/request/file_handler.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/api_job_status.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/response_schemas.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/sse_assembly.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_interaction/response/stream_session.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_specification_loader/replicate_loader.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_specification_loader/runpod_open_api_loader.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_specification_loader/spec_loader.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/SOURCES.txt +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/dependency_links.txt +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/entry_points.txt +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/requires.txt +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk.egg-info/top_level.txt +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/setup.cfg +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_apipod_registry_service_management_example.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_client_factory.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_client_factory_manual.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_client_from_fastapi.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_client_from_runpod_serverless.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_httpx_api.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_localhost_services.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_manual_sdk.py +0 -0
- {fastsdk-0.4.2 → fastsdk-0.4.3}/test/test_replicate.py +0 -0
|
@@ -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:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastsdk"
|
|
7
|
-
version = "0.4.
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastsdk-0.4.2 → fastsdk-0.4.3}/fastsdk/service_specification_loader/runpod_open_api_loader.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|