licos-dev-sdk 0.2.9__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.9 → licos_dev_sdk-0.2.11}/PKG-INFO +1 -1
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/pyproject.toml +1 -1
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/model.py +303 -42
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/tests/test_model.py +134 -16
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/.gitignore +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/__init__.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/_utils.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/archive.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/chart.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/data.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/diagram.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/document.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/image.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/observability.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/presentation.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/spreadsheet.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/src/licos_dev_sdk/web.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/tests/test_document_spreadsheet.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/tests/test_observability.py +0 -0
- {licos_dev_sdk-0.2.9 → licos_dev_sdk-0.2.11}/tests/test_output_paths.py +0 -0
|
@@ -23,9 +23,11 @@ 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
|
-
_CATALOG_CACHE: dict[tuple[str, str, str, str, str], tuple[float, list[dict[str, Any]]]] = {}
|
|
30
|
+
_CATALOG_CACHE: dict[tuple[str, str, str, str, str, str], tuple[float, list[dict[str, Any]]]] = {}
|
|
29
31
|
_DETAIL_CACHE: dict[tuple[str, str, str, str], tuple[float, dict[str, Any] | None]] = {}
|
|
30
32
|
|
|
31
33
|
|
|
@@ -35,6 +37,13 @@ class ModelRuntime:
|
|
|
35
37
|
token: str
|
|
36
38
|
user_id: str | None = None
|
|
37
39
|
workspace_id: str | None = None
|
|
40
|
+
owner_project_id: str | None = None
|
|
41
|
+
owner_project_type: str | None = None
|
|
42
|
+
owner_application_type: str | None = None
|
|
43
|
+
owner_org_id: str | None = None
|
|
44
|
+
runtime_env: str | None = None
|
|
45
|
+
current_user_id: str | None = None
|
|
46
|
+
current_org_id: str | None = None
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
@dataclass(frozen=True)
|
|
@@ -44,6 +53,7 @@ class ModelEndpoint:
|
|
|
44
53
|
base_url: str
|
|
45
54
|
model: str
|
|
46
55
|
required_headers: dict[str, str] = field(default_factory=dict)
|
|
56
|
+
request_headers: dict[str, str] = field(default_factory=dict)
|
|
47
57
|
endpoint: dict[str, Any] = field(default_factory=dict)
|
|
48
58
|
response_url: str | None = None
|
|
49
59
|
cache_context: bool = False
|
|
@@ -173,7 +183,7 @@ def create_chat_openai(
|
|
|
173
183
|
|
|
174
184
|
runtime = _model_runtime(base_url=base_url, user_token=user_token, user_id=user_id, workspace_id=workspace_id)
|
|
175
185
|
endpoint = _resolve_chat_endpoint(runtime, model_group=model_group, requested_model=model)
|
|
176
|
-
headers = {**endpoint
|
|
186
|
+
headers = {**_request_headers(endpoint), **(default_headers or {})}
|
|
177
187
|
output_tokens = max_completion_tokens if max_completion_tokens is not None else max_tokens
|
|
178
188
|
chat_kwargs: dict[str, Any] = {
|
|
179
189
|
"model": endpoint.model,
|
|
@@ -408,21 +418,21 @@ class ImageGenerationClient:
|
|
|
408
418
|
) -> ModelResult:
|
|
409
419
|
endpoint = _resolve_endpoint_with_detail(self.runtime, "imageGeneration", requested_model=model)
|
|
410
420
|
selected_model = endpoint.model
|
|
411
|
-
if raw_request:
|
|
412
|
-
body = dict(raw_request)
|
|
413
|
-
body.setdefault("model", selected_model)
|
|
414
|
-
else:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
return _submit_model_task(
|
|
424
|
-
endpoint,
|
|
425
|
-
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,
|
|
426
436
|
body,
|
|
427
437
|
wait=wait,
|
|
428
438
|
timeout=timeout,
|
|
@@ -577,17 +587,105 @@ def _model_runtime(
|
|
|
577
587
|
owner_user_id = user_id or env("LICOS_USER_ID") or env("AGENT_USER_ID")
|
|
578
588
|
resolved_workspace_id = _workspace_id(workspace_id)
|
|
579
589
|
token = (user_token or "").strip() or resolve_user_token(resolved_base_url, owner_user_id)
|
|
590
|
+
current_user_id = env("LICOS_CURRENT_USER_ID") or env("AGENT_CURRENT_USER_ID")
|
|
591
|
+
current_org_id = (
|
|
592
|
+
env("LICOS_CURRENT_ORG_ID")
|
|
593
|
+
or env("AGENT_CURRENT_ORG_ID")
|
|
594
|
+
or env("LICOS_CURRENT_TENANT_ID")
|
|
595
|
+
or env("AGENT_CURRENT_TENANT_ID")
|
|
596
|
+
)
|
|
597
|
+
if not current_user_id or not current_org_id:
|
|
598
|
+
current_user_id, current_org_id = _fetch_current_user_context(resolved_base_url, token)
|
|
599
|
+
owner_project_id = env("LICOS_PROJECT_ID") or env("AGENT_PROJECT_ID")
|
|
600
|
+
owner_project_type = (
|
|
601
|
+
env("LICOS_OWNER_PROJECT_TYPE")
|
|
602
|
+
or env("AGENT_OWNER_PROJECT_TYPE")
|
|
603
|
+
or env("LICOS_PROJECT_TYPE")
|
|
604
|
+
or env("AGENT_PROJECT_TYPE")
|
|
605
|
+
)
|
|
606
|
+
owner_application_type = (
|
|
607
|
+
env("LICOS_OWNER_APPLICATION_TYPE")
|
|
608
|
+
or env("AGENT_OWNER_APPLICATION_TYPE")
|
|
609
|
+
or env("LICOS_APPLICATION_TYPE")
|
|
610
|
+
or env("AGENT_APPLICATION_TYPE")
|
|
611
|
+
or env("LICOS_OWNER_APPLIACTION_TYPE")
|
|
612
|
+
or env("AGENT_OWNER_APPLIACTION_TYPE")
|
|
613
|
+
)
|
|
614
|
+
owner_org_id = (
|
|
615
|
+
env("LICOS_OWNER_ORG_ID")
|
|
616
|
+
or env("AGENT_OWNER_ORG_ID")
|
|
617
|
+
or env("LICOS_TENANT_ID")
|
|
618
|
+
or env("AGENT_TENANT_ID")
|
|
619
|
+
or env("LICOS_ORG_ID")
|
|
620
|
+
or env("AGENT_ORG_ID")
|
|
621
|
+
)
|
|
622
|
+
runtime_env = env("LICOS_RUNTIME_ENV") or env("AGENT_RUNTIME_ENV")
|
|
623
|
+
if owner_project_id:
|
|
624
|
+
if not resolved_workspace_id:
|
|
625
|
+
raise ConfigurationError("project owner workspace ID is not configured")
|
|
626
|
+
if not owner_user_id:
|
|
627
|
+
raise ConfigurationError("project owner user ID is not configured")
|
|
628
|
+
if not runtime_env:
|
|
629
|
+
raise ConfigurationError("project runtime environment is not configured")
|
|
630
|
+
if not owner_org_id:
|
|
631
|
+
owner_org_id = _fetch_workspace_owner_org_id(resolved_base_url, token, resolved_workspace_id)
|
|
580
632
|
return ModelRuntime(
|
|
581
633
|
base_url=resolved_base_url,
|
|
582
634
|
token=token,
|
|
583
635
|
user_id=owner_user_id,
|
|
584
636
|
workspace_id=resolved_workspace_id,
|
|
637
|
+
owner_project_id=owner_project_id,
|
|
638
|
+
owner_project_type=owner_project_type,
|
|
639
|
+
owner_application_type=owner_application_type,
|
|
640
|
+
owner_org_id=owner_org_id,
|
|
641
|
+
runtime_env=runtime_env,
|
|
642
|
+
current_user_id=current_user_id,
|
|
643
|
+
current_org_id=current_org_id,
|
|
585
644
|
)
|
|
586
645
|
|
|
587
646
|
|
|
588
647
|
def _refresh_model_runtime(runtime: ModelRuntime) -> ModelRuntime:
|
|
589
648
|
token = resolve_user_token(runtime.base_url, runtime.user_id, force_refresh=True)
|
|
590
|
-
|
|
649
|
+
current_user_id = runtime.current_user_id
|
|
650
|
+
current_org_id = runtime.current_org_id
|
|
651
|
+
if not current_user_id or not current_org_id:
|
|
652
|
+
current_user_id, current_org_id = _fetch_current_user_context(runtime.base_url, token)
|
|
653
|
+
return replace(runtime, token=token, current_user_id=current_user_id, current_org_id=current_org_id)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _fetch_current_user_context(base_url: str, token: str) -> tuple[str, str]:
|
|
657
|
+
payload = _request_json(
|
|
658
|
+
"GET",
|
|
659
|
+
f"{base_url}/api/v1/admin/auth/me",
|
|
660
|
+
token=token,
|
|
661
|
+
timeout=30,
|
|
662
|
+
)
|
|
663
|
+
data = payload.get("data") if isinstance(payload, dict) else None
|
|
664
|
+
if not isinstance(data, dict):
|
|
665
|
+
data = payload if isinstance(payload, dict) else {}
|
|
666
|
+
user_id = _first_non_empty_field(data, ["id", "userId", "user_id", "sub"])
|
|
667
|
+
org_id = _first_non_empty_field(data, ["tenantId", "tenant_id"])
|
|
668
|
+
if not user_id:
|
|
669
|
+
raise ApiError("current user response missing id", details=payload)
|
|
670
|
+
if not org_id:
|
|
671
|
+
raise ApiError("current user response missing tenantId", details=payload)
|
|
672
|
+
return user_id, org_id
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _fetch_workspace_owner_org_id(base_url: str, token: str, workspace_id: str) -> str:
|
|
676
|
+
payload = _request_json(
|
|
677
|
+
"GET",
|
|
678
|
+
f"{base_url}/api/v1/admin/workspaces/{parse.quote(workspace_id, safe='')}",
|
|
679
|
+
token=token,
|
|
680
|
+
timeout=30,
|
|
681
|
+
)
|
|
682
|
+
data = payload.get("data") if isinstance(payload, dict) else None
|
|
683
|
+
if not isinstance(data, dict):
|
|
684
|
+
data = payload if isinstance(payload, dict) else {}
|
|
685
|
+
owner_org_id = _first_non_empty_field(data, ["tenantId", "tenant_id"])
|
|
686
|
+
if not owner_org_id:
|
|
687
|
+
raise ApiError("owner workspace response missing tenantId", details=payload)
|
|
688
|
+
return owner_org_id
|
|
591
689
|
|
|
592
690
|
|
|
593
691
|
def _fetch_model_catalogs(
|
|
@@ -609,6 +707,7 @@ def _fetch_model_catalogs(
|
|
|
609
707
|
resolved_workspace_id,
|
|
610
708
|
category_code,
|
|
611
709
|
input_capabilities or "",
|
|
710
|
+
_routing_cache_fingerprint(runtime),
|
|
612
711
|
)
|
|
613
712
|
ttl = _int_env("LICOS_MODEL_CATALOG_CACHE_TTL_SECS", DEFAULT_CATALOG_CACHE_TTL_SECS)
|
|
614
713
|
cached = _CATALOG_CACHE.get(cache_key)
|
|
@@ -621,13 +720,33 @@ def _fetch_model_catalogs(
|
|
|
621
720
|
query = {"categoryCode": category_code}
|
|
622
721
|
if input_capabilities:
|
|
623
722
|
query["inputCapabilities"] = input_capabilities
|
|
723
|
+
if runtime.owner_project_id:
|
|
724
|
+
query["projectId"] = runtime.owner_project_id
|
|
725
|
+
if runtime.owner_project_type:
|
|
726
|
+
query["ownerProjectType"] = runtime.owner_project_type
|
|
727
|
+
if runtime.owner_application_type:
|
|
728
|
+
query["ownerApplicationType"] = runtime.owner_application_type
|
|
729
|
+
if runtime.workspace_id:
|
|
730
|
+
query["ownerWorkspaceId"] = runtime.workspace_id
|
|
731
|
+
if runtime.owner_org_id:
|
|
732
|
+
query["tenantId"] = runtime.owner_org_id
|
|
733
|
+
query["ownerOrgId"] = runtime.owner_org_id
|
|
734
|
+
if runtime.user_id:
|
|
735
|
+
query["ownerUserId"] = runtime.user_id
|
|
736
|
+
if runtime.runtime_env:
|
|
737
|
+
query["runtimeEnv"] = runtime.runtime_env
|
|
738
|
+
if runtime.current_user_id:
|
|
739
|
+
query["currentUserId"] = runtime.current_user_id
|
|
740
|
+
if runtime.current_org_id:
|
|
741
|
+
query["currentOrgId"] = runtime.current_org_id
|
|
742
|
+
query["currentTenantId"] = runtime.current_org_id
|
|
624
743
|
url = f"{runtime.base_url}{path}?{parse.urlencode(query)}"
|
|
625
744
|
try:
|
|
626
745
|
payload = _request_json(
|
|
627
746
|
"GET",
|
|
628
747
|
url,
|
|
629
748
|
token=runtime.token,
|
|
630
|
-
headers={"X-Workspace-Id": resolved_workspace_id},
|
|
749
|
+
headers={**_routing_headers(runtime), "X-Workspace-Id": resolved_workspace_id},
|
|
631
750
|
timeout=30,
|
|
632
751
|
)
|
|
633
752
|
except ApiError as exc:
|
|
@@ -655,6 +774,85 @@ def _workspace_id(workspace_id: str | None = None) -> str | None:
|
|
|
655
774
|
return value or None
|
|
656
775
|
|
|
657
776
|
|
|
777
|
+
def _routing_cache_fingerprint(runtime: ModelRuntime) -> str:
|
|
778
|
+
pairs = [
|
|
779
|
+
("op", runtime.owner_project_id),
|
|
780
|
+
("opt", runtime.owner_project_type),
|
|
781
|
+
("oat", runtime.owner_application_type),
|
|
782
|
+
("ow", runtime.workspace_id),
|
|
783
|
+
("oo", runtime.owner_org_id),
|
|
784
|
+
("ou", runtime.user_id),
|
|
785
|
+
("re", runtime.runtime_env),
|
|
786
|
+
("cu", runtime.current_user_id),
|
|
787
|
+
("co", runtime.current_org_id),
|
|
788
|
+
]
|
|
789
|
+
return "|".join(f"{key}={value}" for key, value in pairs if value)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _routing_headers(runtime: ModelRuntime) -> dict[str, str]:
|
|
793
|
+
headers: dict[str, str] = {}
|
|
794
|
+
|
|
795
|
+
def add(name: str, value: str | None) -> None:
|
|
796
|
+
if value and str(value).strip():
|
|
797
|
+
headers[name] = str(value).strip()
|
|
798
|
+
|
|
799
|
+
add("X-Licos-Current-User-Id", runtime.current_user_id)
|
|
800
|
+
add("X-Licos-Current-Org-Id", runtime.current_org_id)
|
|
801
|
+
add("X-Licos-Owner-Project-Id", runtime.owner_project_id)
|
|
802
|
+
add("X-Licos-Owner-Project-Type", runtime.owner_project_type)
|
|
803
|
+
add("X-Licos-Owner-Application-Type", runtime.owner_application_type)
|
|
804
|
+
add("X-Licos-Owner-Workspace-Id", runtime.workspace_id)
|
|
805
|
+
add("X-Licos-Owner-Org-Id", runtime.owner_org_id)
|
|
806
|
+
add("X-Licos-Owner-User-Id", runtime.user_id)
|
|
807
|
+
add("X-Licos-Runtime-Env", runtime.runtime_env)
|
|
808
|
+
add("X-Project-Id", runtime.owner_project_id)
|
|
809
|
+
add("X-Project-Type", runtime.owner_project_type)
|
|
810
|
+
add("X-Application-Type", runtime.owner_application_type)
|
|
811
|
+
add("X-Workspace-Id", runtime.workspace_id)
|
|
812
|
+
add("X-Tenant-Id", runtime.owner_org_id)
|
|
813
|
+
add("X-User-Id", runtime.user_id)
|
|
814
|
+
add("X-Runtime-Env", runtime.runtime_env)
|
|
815
|
+
return headers
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _request_headers(endpoint: ModelEndpoint) -> dict[str, str]:
|
|
819
|
+
if endpoint.request_headers:
|
|
820
|
+
return endpoint.request_headers
|
|
821
|
+
return endpoint.required_headers
|
|
822
|
+
|
|
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
|
+
|
|
658
856
|
def _normalize_input_capabilities(value: str | None = None) -> str | None:
|
|
659
857
|
if value is None:
|
|
660
858
|
return None
|
|
@@ -685,7 +883,53 @@ def _video_input_capabilities(
|
|
|
685
883
|
value = input_payload.get(key)
|
|
686
884
|
if value is not None and str(value).strip():
|
|
687
885
|
return "image"
|
|
688
|
-
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
|
|
689
933
|
|
|
690
934
|
|
|
691
935
|
def _fetch_model_detail(
|
|
@@ -698,20 +942,43 @@ def _fetch_model_detail(
|
|
|
698
942
|
model_code = str(model_code or "").strip()
|
|
699
943
|
if not model_code:
|
|
700
944
|
return None
|
|
701
|
-
|
|
702
|
-
cache_key = (runtime.base_url, runtime.token, model_code)
|
|
945
|
+
resolved_workspace_id = _workspace_id(workspace_id or runtime.workspace_id)
|
|
946
|
+
cache_key = (runtime.base_url, runtime.token, model_code, _routing_cache_fingerprint(runtime))
|
|
703
947
|
ttl = _int_env("LICOS_MODEL_CATALOG_CACHE_TTL_SECS", DEFAULT_CATALOG_CACHE_TTL_SECS)
|
|
704
948
|
cached = _DETAIL_CACHE.get(cache_key)
|
|
705
949
|
if cached and not refresh and time.time() - cached[0] <= ttl:
|
|
706
950
|
return cached[1]
|
|
707
951
|
|
|
708
952
|
query = {"code": model_code}
|
|
953
|
+
if resolved_workspace_id:
|
|
954
|
+
query["workspaceId"] = resolved_workspace_id
|
|
955
|
+
if runtime.owner_project_id:
|
|
956
|
+
query["projectId"] = runtime.owner_project_id
|
|
957
|
+
if runtime.owner_project_type:
|
|
958
|
+
query["ownerProjectType"] = runtime.owner_project_type
|
|
959
|
+
if runtime.owner_application_type:
|
|
960
|
+
query["ownerApplicationType"] = runtime.owner_application_type
|
|
961
|
+
if runtime.workspace_id:
|
|
962
|
+
query["ownerWorkspaceId"] = runtime.workspace_id
|
|
963
|
+
if runtime.owner_org_id:
|
|
964
|
+
query["tenantId"] = runtime.owner_org_id
|
|
965
|
+
query["ownerOrgId"] = runtime.owner_org_id
|
|
966
|
+
if runtime.user_id:
|
|
967
|
+
query["ownerUserId"] = runtime.user_id
|
|
968
|
+
if runtime.runtime_env:
|
|
969
|
+
query["runtimeEnv"] = runtime.runtime_env
|
|
970
|
+
if runtime.current_user_id:
|
|
971
|
+
query["currentUserId"] = runtime.current_user_id
|
|
972
|
+
if runtime.current_org_id:
|
|
973
|
+
query["currentOrgId"] = runtime.current_org_id
|
|
974
|
+
query["currentTenantId"] = runtime.current_org_id
|
|
709
975
|
url = f"{runtime.base_url}{MODEL_DETAIL_PATH}?{parse.urlencode(query)}"
|
|
710
976
|
try:
|
|
711
977
|
payload = _request_json(
|
|
712
978
|
"GET",
|
|
713
979
|
url,
|
|
714
980
|
token=runtime.token,
|
|
981
|
+
headers=_routing_headers(runtime),
|
|
715
982
|
timeout=30,
|
|
716
983
|
)
|
|
717
984
|
except ApiError as exc:
|
|
@@ -821,6 +1088,7 @@ def _resolve_endpoint(
|
|
|
821
1088
|
default_endpoint: ModelEndpoint | None = None
|
|
822
1089
|
for item in catalogs:
|
|
823
1090
|
for endpoint in _endpoint_candidates(item, capability_key, model_group):
|
|
1091
|
+
endpoint = _with_request_headers(endpoint, runtime)
|
|
824
1092
|
default_endpoint = default_endpoint or endpoint
|
|
825
1093
|
if requested_model and _selected_model(requested_model, "") == endpoint.model:
|
|
826
1094
|
return endpoint
|
|
@@ -829,6 +1097,11 @@ def _resolve_endpoint(
|
|
|
829
1097
|
raise ApiError(f"capability `{capability_key}` is not available in model catalog", details=catalogs)
|
|
830
1098
|
|
|
831
1099
|
|
|
1100
|
+
def _with_request_headers(endpoint: ModelEndpoint, runtime: ModelRuntime) -> ModelEndpoint:
|
|
1101
|
+
headers = {**_routing_headers(runtime), **endpoint.required_headers}
|
|
1102
|
+
return replace(endpoint, request_headers=headers)
|
|
1103
|
+
|
|
1104
|
+
|
|
832
1105
|
def _endpoint_candidates(
|
|
833
1106
|
item: dict[str, Any],
|
|
834
1107
|
capability_key: str,
|
|
@@ -1143,7 +1416,7 @@ def _post_model_json(
|
|
|
1143
1416
|
endpoint.base_url,
|
|
1144
1417
|
token=runtime.token,
|
|
1145
1418
|
body=body,
|
|
1146
|
-
headers=endpoint
|
|
1419
|
+
headers=_task_submit_headers(endpoint),
|
|
1147
1420
|
timeout=request_timeout,
|
|
1148
1421
|
)
|
|
1149
1422
|
except ApiError as exc:
|
|
@@ -1154,7 +1427,7 @@ def _post_model_json(
|
|
|
1154
1427
|
endpoint.base_url,
|
|
1155
1428
|
token=refreshed.token,
|
|
1156
1429
|
body=body,
|
|
1157
|
-
headers=endpoint
|
|
1430
|
+
headers=_task_submit_headers(endpoint),
|
|
1158
1431
|
timeout=request_timeout,
|
|
1159
1432
|
)
|
|
1160
1433
|
raise
|
|
@@ -1171,7 +1444,7 @@ def _submit_model_task(
|
|
|
1171
1444
|
) -> ModelResult:
|
|
1172
1445
|
submit_response = _post_model_json(endpoint, runtime, body, timeout=timeout)
|
|
1173
1446
|
response = submit_response
|
|
1174
|
-
if endpoint
|
|
1447
|
+
if _is_async_model_task(endpoint) and wait:
|
|
1175
1448
|
response = _await_async_model_result(
|
|
1176
1449
|
endpoint,
|
|
1177
1450
|
runtime,
|
|
@@ -1186,7 +1459,7 @@ def _submit_model_task(
|
|
|
1186
1459
|
response=response,
|
|
1187
1460
|
urls=_collect_urls(response),
|
|
1188
1461
|
texts=_collect_texts(response),
|
|
1189
|
-
submit_response=submit_response if endpoint
|
|
1462
|
+
submit_response=submit_response if _is_async_model_task(endpoint) else None,
|
|
1190
1463
|
)
|
|
1191
1464
|
|
|
1192
1465
|
|
|
@@ -1204,7 +1477,7 @@ def _stream_model_json(
|
|
|
1204
1477
|
endpoint.base_url,
|
|
1205
1478
|
token=active_runtime.token,
|
|
1206
1479
|
body=body,
|
|
1207
|
-
headers=endpoint
|
|
1480
|
+
headers=_request_headers(endpoint),
|
|
1208
1481
|
)
|
|
1209
1482
|
try:
|
|
1210
1483
|
with request.urlopen(req, timeout=timeout or _request_timeout()) as response:
|
|
@@ -1309,7 +1582,7 @@ def _await_async_model_result(
|
|
|
1309
1582
|
"GET",
|
|
1310
1583
|
query_url,
|
|
1311
1584
|
token=runtime.token,
|
|
1312
|
-
headers=endpoint
|
|
1585
|
+
headers=_request_headers(endpoint),
|
|
1313
1586
|
timeout=timeout or _request_timeout(),
|
|
1314
1587
|
)
|
|
1315
1588
|
except ApiError as exc:
|
|
@@ -1319,7 +1592,7 @@ def _await_async_model_result(
|
|
|
1319
1592
|
"GET",
|
|
1320
1593
|
query_url,
|
|
1321
1594
|
token=refreshed.token,
|
|
1322
|
-
headers=endpoint
|
|
1595
|
+
headers=_request_headers(endpoint),
|
|
1323
1596
|
timeout=timeout or _request_timeout(),
|
|
1324
1597
|
)
|
|
1325
1598
|
else:
|
|
@@ -1484,19 +1757,7 @@ def _selected_model(model: str | None, default: str) -> str:
|
|
|
1484
1757
|
return selected
|
|
1485
1758
|
|
|
1486
1759
|
|
|
1487
|
-
def
|
|
1488
|
-
values = [count, parameters.get("n"), parameters.get("count")]
|
|
1489
|
-
for value in values:
|
|
1490
|
-
try:
|
|
1491
|
-
parsed = int(value)
|
|
1492
|
-
except (TypeError, ValueError):
|
|
1493
|
-
continue
|
|
1494
|
-
if parsed > 0:
|
|
1495
|
-
return max(1, min(4, parsed))
|
|
1496
|
-
return 1
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
def _non_empty_list(values: Sequence[str | None]) -> list[str]:
|
|
1760
|
+
def _non_empty_list(values: Sequence[str | None]) -> list[str]:
|
|
1500
1761
|
return [value.strip() for value in values if isinstance(value, str) and value.strip()]
|
|
1501
1762
|
|
|
1502
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,
|
|
@@ -152,6 +165,8 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
152
165
|
"LICOS_PLATFORM_API_BASE_URL": "http://platform.example/api/v1",
|
|
153
166
|
"AGENT_USER_ID": "user-1",
|
|
154
167
|
"AGENT_WORKSPACE_ID": "workspace-1",
|
|
168
|
+
"LICOS_CURRENT_USER_ID": "current-user-1",
|
|
169
|
+
"LICOS_CURRENT_ORG_ID": "current-org-1",
|
|
155
170
|
"LICOS_AI_AGENT_TOKEN": "ai-agent-token",
|
|
156
171
|
},
|
|
157
172
|
clear=True,
|
|
@@ -170,6 +185,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
170
185
|
captured["exchange_body"] = json.loads(req.data.decode("utf-8"))
|
|
171
186
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
172
187
|
if _is_available_models_request(req):
|
|
188
|
+
captured["catalog_url"] = req.full_url
|
|
173
189
|
captured["catalog_headers"] = dict(req.header_items())
|
|
174
190
|
return _available_models_response(req)
|
|
175
191
|
if req.full_url.startswith("http://platform.example/api/v1/admin/workspaces/models/detail?"):
|
|
@@ -193,6 +209,40 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
193
209
|
self.assertEqual(captured["chat_body"]["model"], "chat-text")
|
|
194
210
|
self.assertEqual(captured["chat_body"]["max_completion_tokens"], 64000)
|
|
195
211
|
|
|
212
|
+
def test_model_requests_fetch_current_user_context_when_env_missing(self) -> None:
|
|
213
|
+
os.environ.pop("LICOS_CURRENT_USER_ID", None)
|
|
214
|
+
os.environ.pop("LICOS_CURRENT_ORG_ID", None)
|
|
215
|
+
captured: dict[str, Any] = {}
|
|
216
|
+
|
|
217
|
+
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
218
|
+
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
219
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
220
|
+
if req.full_url == "http://platform.example/api/v1/admin/auth/me":
|
|
221
|
+
captured["me_headers"] = dict(req.header_items())
|
|
222
|
+
return _FakeResponse(
|
|
223
|
+
{"code": 0, "success": True, "data": {"id": "current-user-token", "tenantId": "current-org-token"}}
|
|
224
|
+
)
|
|
225
|
+
if _is_available_models_request(req):
|
|
226
|
+
captured["catalog_url"] = req.full_url
|
|
227
|
+
captured["catalog_headers"] = dict(req.header_items())
|
|
228
|
+
return _available_models_response(req)
|
|
229
|
+
if req.full_url.startswith("http://platform.example/api/v1/admin/workspaces/models/detail?"):
|
|
230
|
+
return _FakeResponse(_model_detail_payload("chat-text"))
|
|
231
|
+
raise AssertionError(req.full_url)
|
|
232
|
+
|
|
233
|
+
with mock.patch.object(model.request, "urlopen", fake_urlopen):
|
|
234
|
+
endpoint = model.resolve_llm_endpoint()
|
|
235
|
+
|
|
236
|
+
query = parse.parse_qs(parse.urlparse(captured["catalog_url"]).query)
|
|
237
|
+
self.assertEqual(endpoint.request_headers["X-Licos-Current-User-Id"], "current-user-token")
|
|
238
|
+
self.assertEqual(endpoint.request_headers["X-Licos-Current-Org-Id"], "current-org-token")
|
|
239
|
+
self.assertEqual(captured["me_headers"]["Authorization"], "Bearer user-token")
|
|
240
|
+
catalog_headers = {key.lower(): value for key, value in captured["catalog_headers"].items()}
|
|
241
|
+
self.assertEqual(catalog_headers["x-licos-current-user-id"], "current-user-token")
|
|
242
|
+
self.assertEqual(catalog_headers["x-licos-current-org-id"], "current-org-token")
|
|
243
|
+
self.assertEqual(query["currentUserId"], ["current-user-token"])
|
|
244
|
+
self.assertEqual(query["currentOrgId"], ["current-org-token"])
|
|
245
|
+
|
|
196
246
|
def test_llm_explicit_model_overrides_catalog_default(self) -> None:
|
|
197
247
|
captured: dict[str, Any] = {}
|
|
198
248
|
|
|
@@ -214,6 +264,38 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
214
264
|
self.assertEqual(result.text, "hello")
|
|
215
265
|
self.assertEqual(captured["chat_body"]["model"], "custom-chat-model")
|
|
216
266
|
|
|
267
|
+
def test_model_catalog_request_includes_owner_project_types_when_configured(self) -> None:
|
|
268
|
+
os.environ["LICOS_PROJECT_ID"] = "project-1"
|
|
269
|
+
os.environ["LICOS_PROJECT_TYPE"] = "AGENT"
|
|
270
|
+
os.environ["LICOS_OWNER_APPLICATION_TYPE"] = "AICoding"
|
|
271
|
+
os.environ["LICOS_OWNER_ORG_ID"] = "owner-org-1"
|
|
272
|
+
os.environ["LICOS_RUNTIME_ENV"] = "dev"
|
|
273
|
+
captured: dict[str, Any] = {}
|
|
274
|
+
|
|
275
|
+
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
276
|
+
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
277
|
+
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
278
|
+
if _is_available_models_request(req):
|
|
279
|
+
captured["catalog_url"] = req.full_url
|
|
280
|
+
captured["catalog_headers"] = dict(req.header_items())
|
|
281
|
+
return _available_models_response(req)
|
|
282
|
+
if req.full_url.startswith("http://platform.example/api/v1/admin/workspaces/models/detail?"):
|
|
283
|
+
captured["detail_url"] = req.full_url
|
|
284
|
+
return _FakeResponse(_model_detail_payload("chat-text"))
|
|
285
|
+
raise AssertionError(req.full_url)
|
|
286
|
+
|
|
287
|
+
with mock.patch.object(model.request, "urlopen", fake_urlopen):
|
|
288
|
+
endpoint = model.resolve_llm_endpoint()
|
|
289
|
+
|
|
290
|
+
catalog_query = parse.parse_qs(parse.urlparse(captured["catalog_url"]).query)
|
|
291
|
+
detail_query = parse.parse_qs(parse.urlparse(captured["detail_url"]).query)
|
|
292
|
+
self.assertEqual(endpoint.request_headers["X-Licos-Owner-Project-Type"], "AGENT")
|
|
293
|
+
self.assertEqual(endpoint.request_headers["X-Licos-Owner-Application-Type"], "AICoding")
|
|
294
|
+
self.assertEqual(catalog_query["ownerProjectType"], ["AGENT"])
|
|
295
|
+
self.assertEqual(catalog_query["ownerApplicationType"], ["AICoding"])
|
|
296
|
+
self.assertEqual(detail_query["ownerProjectType"], ["AGENT"])
|
|
297
|
+
self.assertEqual(detail_query["ownerApplicationType"], ["AICoding"])
|
|
298
|
+
|
|
217
299
|
def test_llm_invoke_refreshes_user_token_once_after_unauthorized(self) -> None:
|
|
218
300
|
tokens = iter(["old-token", "new-token"])
|
|
219
301
|
catalog_tokens: list[str] = []
|
|
@@ -329,7 +411,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
329
411
|
self.assertEqual(kwargs["timeout"], 30)
|
|
330
412
|
self.assertTrue(kwargs["streaming"])
|
|
331
413
|
|
|
332
|
-
def
|
|
414
|
+
def test_image_generation_uses_sync_multimodal_body(self) -> None:
|
|
333
415
|
captured: dict[str, Any] = {}
|
|
334
416
|
|
|
335
417
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
@@ -340,6 +422,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
340
422
|
if req.full_url.startswith("http://platform.example/api/v1/admin/workspaces/models/detail?"):
|
|
341
423
|
return _FakeResponse(_model_detail_payload("image-model"))
|
|
342
424
|
if req.full_url == "http://gateway.example/image":
|
|
425
|
+
captured["headers"] = dict(req.header_items())
|
|
343
426
|
captured["body"] = json.loads(req.data.decode("utf-8"))
|
|
344
427
|
return _FakeResponse({"output": {"url": "https://cdn.example/image.png"}})
|
|
345
428
|
raise AssertionError(req.full_url)
|
|
@@ -347,9 +430,13 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
347
430
|
with mock.patch.object(model.request, "urlopen", fake_urlopen):
|
|
348
431
|
result = model.ImageGenerationClient().generate("blue sky")
|
|
349
432
|
|
|
350
|
-
self.assertEqual(captured["body"]["model"], "image-model")
|
|
351
|
-
self.assertEqual(captured["body"]["
|
|
352
|
-
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"])
|
|
353
440
|
|
|
354
441
|
def test_video_generation_uses_text_input_capability_without_image(self) -> None:
|
|
355
442
|
captured: dict[str, Any] = {}
|
|
@@ -363,6 +450,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
363
450
|
if req.full_url.startswith("http://platform.example/api/v1/admin/workspaces/models/detail?"):
|
|
364
451
|
return _FakeResponse(_model_detail_payload("video-text-model"))
|
|
365
452
|
if req.full_url == "http://gateway.example/video-text":
|
|
453
|
+
captured["headers"] = dict(req.header_items())
|
|
366
454
|
captured["body"] = json.loads(req.data.decode("utf-8"))
|
|
367
455
|
return _FakeResponse({"output": {"video_url": "https://cdn.example/text.mp4"}})
|
|
368
456
|
raise AssertionError(req.full_url)
|
|
@@ -372,8 +460,38 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
372
460
|
|
|
373
461
|
self.assertEqual(captured["catalog_query"]["inputCapabilities"], ["text"])
|
|
374
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")
|
|
375
465
|
self.assertEqual(result.urls, ["https://cdn.example/text.mp4"])
|
|
376
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
|
+
|
|
377
495
|
def test_video_generation_uses_image_input_capability_with_image(self) -> None:
|
|
378
496
|
captured: dict[str, Any] = {}
|
|
379
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
|