licos-dev-sdk 0.2.10__tar.gz → 0.2.11__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.
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/PKG-INFO +1 -1
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/pyproject.toml +1 -1
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/model.py +101 -33
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/tests/test_model.py +65 -16
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/.gitignore +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/__init__.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/_utils.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/archive.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/chart.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/data.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/diagram.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/document.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/image.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/observability.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/presentation.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/spreadsheet.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/web.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/tests/test_document_spreadsheet.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/tests/test_observability.py +0 -0
- {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/tests/test_output_paths.py +0 -0
|
@@ -23,6 +23,8 @@ MODEL_DETAIL_PATH = "/api/v1/admin/workspaces/models/detail"
|
|
|
23
23
|
DEFAULT_REQUEST_TIMEOUT_SECS = 120
|
|
24
24
|
DEFAULT_ASYNC_TIMEOUT_SECS = 600
|
|
25
25
|
DEFAULT_ASYNC_POLL_INTERVAL_SECS = 2.0
|
|
26
|
+
ASYNC_MODEL_TASK_HEADER_NAME = "X-DashScope-Async"
|
|
27
|
+
ASYNC_MODEL_TASK_HEADER_VALUE = "enable"
|
|
26
28
|
DEFAULT_CATALOG_CACHE_TTL_SECS = 300
|
|
27
29
|
|
|
28
30
|
_CATALOG_CACHE: dict[tuple[str, str, str, str, str, str], tuple[float, list[dict[str, Any]]]] = {}
|
|
@@ -416,21 +418,21 @@ class ImageGenerationClient:
|
|
|
416
418
|
) -> ModelResult:
|
|
417
419
|
endpoint = _resolve_endpoint_with_detail(self.runtime, "imageGeneration", requested_model=model)
|
|
418
420
|
selected_model = endpoint.model
|
|
419
|
-
if raw_request:
|
|
420
|
-
body = dict(raw_request)
|
|
421
|
-
body.setdefault("model", selected_model)
|
|
422
|
-
else:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return _submit_model_task(
|
|
432
|
-
endpoint,
|
|
433
|
-
self.runtime,
|
|
421
|
+
if raw_request:
|
|
422
|
+
body = dict(raw_request)
|
|
423
|
+
body.setdefault("model", selected_model)
|
|
424
|
+
else:
|
|
425
|
+
body = _image_generation_body(
|
|
426
|
+
prompt,
|
|
427
|
+
selected_model,
|
|
428
|
+
count=count,
|
|
429
|
+
negative_prompt=negative_prompt,
|
|
430
|
+
size=size,
|
|
431
|
+
parameters=parameters,
|
|
432
|
+
)
|
|
433
|
+
return _submit_model_task(
|
|
434
|
+
endpoint,
|
|
435
|
+
self.runtime,
|
|
434
436
|
body,
|
|
435
437
|
wait=wait,
|
|
436
438
|
timeout=timeout,
|
|
@@ -819,6 +821,38 @@ def _request_headers(endpoint: ModelEndpoint) -> dict[str, str]:
|
|
|
819
821
|
return endpoint.required_headers
|
|
820
822
|
|
|
821
823
|
|
|
824
|
+
def _task_submit_headers(endpoint: ModelEndpoint) -> dict[str, str]:
|
|
825
|
+
headers = dict(_request_headers(endpoint))
|
|
826
|
+
if _is_async_model_task(endpoint) and not any(
|
|
827
|
+
key.lower() == ASYNC_MODEL_TASK_HEADER_NAME.lower() for key in headers
|
|
828
|
+
):
|
|
829
|
+
headers[ASYNC_MODEL_TASK_HEADER_NAME] = ASYNC_MODEL_TASK_HEADER_VALUE
|
|
830
|
+
return headers
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _is_async_model_task(endpoint: ModelEndpoint) -> bool:
|
|
834
|
+
if endpoint.capability.strip().lower() in {"imagegeneration", "image_generation"}:
|
|
835
|
+
return False
|
|
836
|
+
return (
|
|
837
|
+
endpoint.async_task
|
|
838
|
+
or bool(endpoint.task_query_url)
|
|
839
|
+
or any(key.lower() == ASYNC_MODEL_TASK_HEADER_NAME.lower() for key in endpoint.request_headers)
|
|
840
|
+
or _is_async_model_task_capability(endpoint.capability)
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _is_async_model_task_capability(capability: str) -> bool:
|
|
845
|
+
return capability.strip().lower() in {
|
|
846
|
+
"imagegeneration",
|
|
847
|
+
"image_generation",
|
|
848
|
+
"videogeneration",
|
|
849
|
+
"video_generation",
|
|
850
|
+
"speechrecognition",
|
|
851
|
+
"speech_recognition",
|
|
852
|
+
"speech",
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
|
|
822
856
|
def _normalize_input_capabilities(value: str | None = None) -> str | None:
|
|
823
857
|
if value is None:
|
|
824
858
|
return None
|
|
@@ -849,7 +883,53 @@ def _video_input_capabilities(
|
|
|
849
883
|
value = input_payload.get(key)
|
|
850
884
|
if value is not None and str(value).strip():
|
|
851
885
|
return "image"
|
|
852
|
-
return "text"
|
|
886
|
+
return "text"
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _image_generation_body(
|
|
890
|
+
prompt: str,
|
|
891
|
+
model: str,
|
|
892
|
+
*,
|
|
893
|
+
count: int | None = None,
|
|
894
|
+
negative_prompt: str | None = None,
|
|
895
|
+
size: str | None = None,
|
|
896
|
+
parameters: dict[str, Any] | None = None,
|
|
897
|
+
) -> dict[str, Any]:
|
|
898
|
+
params = dict(parameters or {})
|
|
899
|
+
image_count = _image_count(count, params)
|
|
900
|
+
params.pop("count", None)
|
|
901
|
+
if image_count is not None:
|
|
902
|
+
params["n"] = image_count
|
|
903
|
+
else:
|
|
904
|
+
params.pop("n", None)
|
|
905
|
+
if negative_prompt:
|
|
906
|
+
params.setdefault("negative_prompt", negative_prompt)
|
|
907
|
+
if size:
|
|
908
|
+
params.setdefault("size", size)
|
|
909
|
+
return {
|
|
910
|
+
"model": model,
|
|
911
|
+
"input": {
|
|
912
|
+
"messages": [
|
|
913
|
+
{
|
|
914
|
+
"role": "user",
|
|
915
|
+
"content": [{"text": prompt}],
|
|
916
|
+
}
|
|
917
|
+
]
|
|
918
|
+
},
|
|
919
|
+
"parameters": params,
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _image_count(count: int | None, parameters: dict[str, Any]) -> int | None:
|
|
924
|
+
values = [count, parameters.get("n"), parameters.get("count")]
|
|
925
|
+
for value in values:
|
|
926
|
+
try:
|
|
927
|
+
parsed = int(value)
|
|
928
|
+
except (TypeError, ValueError):
|
|
929
|
+
continue
|
|
930
|
+
if parsed > 0:
|
|
931
|
+
return max(1, min(4, parsed))
|
|
932
|
+
return None
|
|
853
933
|
|
|
854
934
|
|
|
855
935
|
def _fetch_model_detail(
|
|
@@ -1336,7 +1416,7 @@ def _post_model_json(
|
|
|
1336
1416
|
endpoint.base_url,
|
|
1337
1417
|
token=runtime.token,
|
|
1338
1418
|
body=body,
|
|
1339
|
-
headers=
|
|
1419
|
+
headers=_task_submit_headers(endpoint),
|
|
1340
1420
|
timeout=request_timeout,
|
|
1341
1421
|
)
|
|
1342
1422
|
except ApiError as exc:
|
|
@@ -1347,7 +1427,7 @@ def _post_model_json(
|
|
|
1347
1427
|
endpoint.base_url,
|
|
1348
1428
|
token=refreshed.token,
|
|
1349
1429
|
body=body,
|
|
1350
|
-
headers=
|
|
1430
|
+
headers=_task_submit_headers(endpoint),
|
|
1351
1431
|
timeout=request_timeout,
|
|
1352
1432
|
)
|
|
1353
1433
|
raise
|
|
@@ -1364,7 +1444,7 @@ def _submit_model_task(
|
|
|
1364
1444
|
) -> ModelResult:
|
|
1365
1445
|
submit_response = _post_model_json(endpoint, runtime, body, timeout=timeout)
|
|
1366
1446
|
response = submit_response
|
|
1367
|
-
if endpoint
|
|
1447
|
+
if _is_async_model_task(endpoint) and wait:
|
|
1368
1448
|
response = _await_async_model_result(
|
|
1369
1449
|
endpoint,
|
|
1370
1450
|
runtime,
|
|
@@ -1379,7 +1459,7 @@ def _submit_model_task(
|
|
|
1379
1459
|
response=response,
|
|
1380
1460
|
urls=_collect_urls(response),
|
|
1381
1461
|
texts=_collect_texts(response),
|
|
1382
|
-
submit_response=submit_response if endpoint
|
|
1462
|
+
submit_response=submit_response if _is_async_model_task(endpoint) else None,
|
|
1383
1463
|
)
|
|
1384
1464
|
|
|
1385
1465
|
|
|
@@ -1677,19 +1757,7 @@ def _selected_model(model: str | None, default: str) -> str:
|
|
|
1677
1757
|
return selected
|
|
1678
1758
|
|
|
1679
1759
|
|
|
1680
|
-
def
|
|
1681
|
-
values = [count, parameters.get("n"), parameters.get("count")]
|
|
1682
|
-
for value in values:
|
|
1683
|
-
try:
|
|
1684
|
-
parsed = int(value)
|
|
1685
|
-
except (TypeError, ValueError):
|
|
1686
|
-
continue
|
|
1687
|
-
if parsed > 0:
|
|
1688
|
-
return max(1, min(4, parsed))
|
|
1689
|
-
return 1
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
def _non_empty_list(values: Sequence[str | None]) -> list[str]:
|
|
1760
|
+
def _non_empty_list(values: Sequence[str | None]) -> list[str]:
|
|
1693
1761
|
return [value.strip() for value in values if isinstance(value, str) and value.strip()]
|
|
1694
1762
|
|
|
1695
1763
|
|
|
@@ -59,14 +59,14 @@ def _available_models_payload(category_code: str = "llm", input_capabilities: st
|
|
|
59
59
|
"baseUrl": "http://gateway.example/v1/chat/completions",
|
|
60
60
|
}
|
|
61
61
|
],
|
|
62
|
-
"image_generation": [
|
|
63
|
-
{
|
|
64
|
-
"providerCode": "test-provider",
|
|
65
|
-
"categoryCode": "image_generation",
|
|
66
|
-
"modelCode": "image-model",
|
|
67
|
-
"baseUrl": "http://gateway.example/image",
|
|
68
|
-
}
|
|
69
|
-
],
|
|
62
|
+
"image_generation": [
|
|
63
|
+
{
|
|
64
|
+
"providerCode": "test-provider",
|
|
65
|
+
"categoryCode": "image_generation",
|
|
66
|
+
"modelCode": "image-model",
|
|
67
|
+
"baseUrl": "http://gateway.example/image",
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
70
|
"video_generation": [
|
|
71
71
|
{
|
|
72
72
|
"providerCode": "test-provider",
|
|
@@ -75,6 +75,8 @@ def _available_models_payload(category_code: str = "llm", input_capabilities: st
|
|
|
75
75
|
"baseUrl": "http://gateway.example/video-image"
|
|
76
76
|
if input_capabilities == "image"
|
|
77
77
|
else "http://gateway.example/video-text",
|
|
78
|
+
"async": True,
|
|
79
|
+
"taskQueryUrl": "http://gateway.example/tasks/{taskId}",
|
|
78
80
|
}
|
|
79
81
|
],
|
|
80
82
|
"speech": [
|
|
@@ -83,6 +85,7 @@ def _available_models_payload(category_code: str = "llm", input_capabilities: st
|
|
|
83
85
|
"categoryCode": "speech",
|
|
84
86
|
"modelCode": "asr-model",
|
|
85
87
|
"baseUrl": "http://gateway.example/asr",
|
|
88
|
+
"taskQueryUrl": "http://gateway.example/tasks/{taskId}",
|
|
86
89
|
}
|
|
87
90
|
],
|
|
88
91
|
}.get(category_code, [])
|
|
@@ -105,10 +108,14 @@ def _model_detail_payload(model_code: str = "chat-text") -> dict[str, Any]:
|
|
|
105
108
|
}
|
|
106
109
|
if model_code == "image-model":
|
|
107
110
|
return {
|
|
108
|
-
"code": 0,
|
|
109
|
-
"success": True,
|
|
110
|
-
"data": {
|
|
111
|
-
|
|
111
|
+
"code": 0,
|
|
112
|
+
"success": True,
|
|
113
|
+
"data": {
|
|
114
|
+
"code": model_code,
|
|
115
|
+
"baseUrl": "http://gateway.example/image",
|
|
116
|
+
"requiredHeaders": [],
|
|
117
|
+
},
|
|
118
|
+
}
|
|
112
119
|
if model_code in ("video-text-model", "video-image-model"):
|
|
113
120
|
suffix = "image" if model_code == "video-image-model" else "text"
|
|
114
121
|
return {
|
|
@@ -116,6 +123,12 @@ def _model_detail_payload(model_code: str = "chat-text") -> dict[str, Any]:
|
|
|
116
123
|
"success": True,
|
|
117
124
|
"data": {"code": model_code, "baseUrl": f"http://gateway.example/video-{suffix}"},
|
|
118
125
|
}
|
|
126
|
+
if model_code == "asr-model":
|
|
127
|
+
return {
|
|
128
|
+
"code": 0,
|
|
129
|
+
"success": True,
|
|
130
|
+
"data": {"code": model_code, "baseUrl": "http://gateway.example/asr"},
|
|
131
|
+
}
|
|
119
132
|
return {
|
|
120
133
|
"code": 0,
|
|
121
134
|
"success": True,
|
|
@@ -398,7 +411,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
398
411
|
self.assertEqual(kwargs["timeout"], 30)
|
|
399
412
|
self.assertTrue(kwargs["streaming"])
|
|
400
413
|
|
|
401
|
-
def
|
|
414
|
+
def test_image_generation_uses_sync_multimodal_body(self) -> None:
|
|
402
415
|
captured: dict[str, Any] = {}
|
|
403
416
|
|
|
404
417
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
@@ -409,6 +422,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
409
422
|
if req.full_url.startswith("http://platform.example/api/v1/admin/workspaces/models/detail?"):
|
|
410
423
|
return _FakeResponse(_model_detail_payload("image-model"))
|
|
411
424
|
if req.full_url == "http://gateway.example/image":
|
|
425
|
+
captured["headers"] = dict(req.header_items())
|
|
412
426
|
captured["body"] = json.loads(req.data.decode("utf-8"))
|
|
413
427
|
return _FakeResponse({"output": {"url": "https://cdn.example/image.png"}})
|
|
414
428
|
raise AssertionError(req.full_url)
|
|
@@ -416,9 +430,13 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
416
430
|
with mock.patch.object(model.request, "urlopen", fake_urlopen):
|
|
417
431
|
result = model.ImageGenerationClient().generate("blue sky")
|
|
418
432
|
|
|
419
|
-
self.assertEqual(captured["body"]["model"], "image-model")
|
|
420
|
-
self.assertEqual(captured["body"]["
|
|
421
|
-
self.assertEqual(
|
|
433
|
+
self.assertEqual(captured["body"]["model"], "image-model")
|
|
434
|
+
self.assertEqual(captured["body"]["input"]["messages"][0]["role"], "user")
|
|
435
|
+
self.assertEqual(captured["body"]["input"]["messages"][0]["content"][0]["text"], "blue sky")
|
|
436
|
+
self.assertNotIn("n", captured["body"]["parameters"])
|
|
437
|
+
headers = {key.lower(): value for key, value in captured["headers"].items()}
|
|
438
|
+
self.assertNotIn("x-dashscope-async", headers)
|
|
439
|
+
self.assertEqual(result.urls, ["https://cdn.example/image.png"])
|
|
422
440
|
|
|
423
441
|
def test_video_generation_uses_text_input_capability_without_image(self) -> None:
|
|
424
442
|
captured: dict[str, Any] = {}
|
|
@@ -432,6 +450,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
432
450
|
if req.full_url.startswith("http://platform.example/api/v1/admin/workspaces/models/detail?"):
|
|
433
451
|
return _FakeResponse(_model_detail_payload("video-text-model"))
|
|
434
452
|
if req.full_url == "http://gateway.example/video-text":
|
|
453
|
+
captured["headers"] = dict(req.header_items())
|
|
435
454
|
captured["body"] = json.loads(req.data.decode("utf-8"))
|
|
436
455
|
return _FakeResponse({"output": {"video_url": "https://cdn.example/text.mp4"}})
|
|
437
456
|
raise AssertionError(req.full_url)
|
|
@@ -441,8 +460,38 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
441
460
|
|
|
442
461
|
self.assertEqual(captured["catalog_query"]["inputCapabilities"], ["text"])
|
|
443
462
|
self.assertEqual(captured["body"]["model"], "video-text-model")
|
|
463
|
+
headers = {key.lower(): value for key, value in captured["headers"].items()}
|
|
464
|
+
self.assertEqual(headers["x-dashscope-async"], "enable")
|
|
444
465
|
self.assertEqual(result.urls, ["https://cdn.example/text.mp4"])
|
|
445
466
|
|
|
467
|
+
def test_speech_recognition_submits_with_async_header(self) -> None:
|
|
468
|
+
captured: dict[str, Any] = {}
|
|
469
|
+
|
|
470
|
+
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
471
|
+
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
472
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
473
|
+
if _is_available_models_request(req):
|
|
474
|
+
return _available_models_response(req)
|
|
475
|
+
if req.full_url.startswith("http://platform.example/api/v1/admin/workspaces/models/detail?"):
|
|
476
|
+
return _FakeResponse(_model_detail_payload("asr-model"))
|
|
477
|
+
if req.full_url == "http://gateway.example/asr":
|
|
478
|
+
captured["headers"] = dict(req.header_items())
|
|
479
|
+
captured["body"] = json.loads(req.data.decode("utf-8"))
|
|
480
|
+
return _FakeResponse({"output": {"task_id": "asr-task-1", "task_status": "PENDING"}})
|
|
481
|
+
raise AssertionError(req.full_url)
|
|
482
|
+
|
|
483
|
+
with mock.patch.object(model.request, "urlopen", fake_urlopen):
|
|
484
|
+
result = model.SpeechRecognitionClient().recognize(
|
|
485
|
+
audio_url="https://cdn.example/a.wav",
|
|
486
|
+
wait=False,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
headers = {key.lower(): value for key, value in captured["headers"].items()}
|
|
490
|
+
self.assertEqual(headers["x-dashscope-async"], "enable")
|
|
491
|
+
self.assertEqual(captured["body"]["model"], "asr-model")
|
|
492
|
+
self.assertEqual(captured["body"]["input"]["file_urls"], ["https://cdn.example/a.wav"])
|
|
493
|
+
self.assertEqual(result.submit_response["output"]["task_id"], "asr-task-1")
|
|
494
|
+
|
|
446
495
|
def test_video_generation_uses_image_input_capability_with_image(self) -> None:
|
|
447
496
|
captured: dict[str, Any] = {}
|
|
448
497
|
|
|
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
|