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.
Files changed (20) hide show
  1. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/PKG-INFO +1 -1
  2. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/pyproject.toml +1 -1
  3. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/model.py +101 -33
  4. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/tests/test_model.py +65 -16
  5. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/.gitignore +0 -0
  6. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/__init__.py +0 -0
  7. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/_utils.py +0 -0
  8. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/archive.py +0 -0
  9. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/chart.py +0 -0
  10. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/data.py +0 -0
  11. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/diagram.py +0 -0
  12. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/document.py +0 -0
  13. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/image.py +0 -0
  14. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/observability.py +0 -0
  15. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/presentation.py +0 -0
  16. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/spreadsheet.py +0 -0
  17. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/web.py +0 -0
  18. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/tests/test_document_spreadsheet.py +0 -0
  19. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/tests/test_observability.py +0 -0
  20. {licos_dev_sdk-0.2.10 → licos_dev_sdk-0.2.11}/tests/test_output_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: licos-dev-sdk
3
- Version: 0.2.10
3
+ Version: 0.2.11
4
4
  Summary: LICOS Dev SDK - file generation and model capability clients
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: docxtpl>=0.16
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-dev-sdk"
7
- version = "0.2.10"
7
+ version = "0.2.11"
8
8
  description = "LICOS Dev SDK - file generation and model capability clients"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -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
- params = dict(parameters or {})
424
- params["n"] = _image_count(count, params)
425
- if size:
426
- params["size"] = size
427
- input_payload = {"prompt": prompt}
428
- if negative_prompt:
429
- input_payload["negative_prompt"] = negative_prompt
430
- body = {"model": selected_model, "input": input_payload, "parameters": params}
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=_request_headers(endpoint),
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=_request_headers(endpoint),
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.async_task and wait:
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.async_task else None,
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 _image_count(count: int | None, parameters: dict[str, Any]) -> int:
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": {"code": model_code, "baseUrl": "http://gateway.example/image"},
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 test_image_generation_defaults_to_one_image(self) -> None:
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"]["parameters"]["n"], 1)
421
- self.assertEqual(result.urls, ["https://cdn.example/image.png"])
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