licos-dev-sdk 0.5.0__tar.gz → 0.6.0__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.6.0/PKG-INFO +42 -0
- licos_dev_sdk-0.6.0/README.md +14 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/pyproject.toml +3 -2
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/model.py +40 -64
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_model.py +293 -16
- licos_dev_sdk-0.6.0/tests/test_request_identity.py +86 -0
- licos_dev_sdk-0.5.0/PKG-INFO +0 -26
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/.gitignore +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/DOCX.md +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/__init__.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/_usage_identity.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/_utils.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/archive.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/chart.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/data.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/diagram.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_common.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_edit.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_expectations.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_inspect.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_layout.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_package.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_preservation.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_structure.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_table.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_table_actions.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_read.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_table_export.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/image.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/langchain_reasoning.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/observability.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/pdf_inspect.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/presentation.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/presentation_canvas.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/presentation_inspect.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/spreadsheet.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/web.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_document_read.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_document_spreadsheet.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_document_table_export.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_docx_inspect.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_docx_structure.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_docx_table_edit.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_html_docx_features.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_langchain_protocol.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_observability.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_output_paths.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_presentation.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_spreadsheet_html_edit.py +0 -0
- {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_usage_identity.py +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: licos-dev-sdk
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: LICOS Dev SDK - file generation and model capability clients
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
7
|
+
Requires-Dist: docxtpl>=0.16
|
|
8
|
+
Requires-Dist: graphviz>=0.20
|
|
9
|
+
Requires-Dist: jinja2>=3.1
|
|
10
|
+
Requires-Dist: licos-platform-sdk>=0.7.0
|
|
11
|
+
Requires-Dist: matplotlib>=3.9
|
|
12
|
+
Requires-Dist: mistune>=3.0
|
|
13
|
+
Requires-Dist: openpyxl>=3.1
|
|
14
|
+
Requires-Dist: pillow>=10.0
|
|
15
|
+
Requires-Dist: pypdf>=5.0
|
|
16
|
+
Requires-Dist: python-barcode>=0.15
|
|
17
|
+
Requires-Dist: python-docx>=1.1
|
|
18
|
+
Requires-Dist: python-pptx>=1.0
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Requires-Dist: qrcode[pil]>=8.0
|
|
21
|
+
Requires-Dist: skia-pathops>=0.9.2
|
|
22
|
+
Requires-Dist: uharfbuzz>=0.50.0
|
|
23
|
+
Requires-Dist: weasyprint>=62.0
|
|
24
|
+
Requires-Dist: xlsxwriter>=3.2
|
|
25
|
+
Provides-Extra: ppt-master
|
|
26
|
+
Requires-Dist: licos-ppt-master-assets==0.0.1; extra == 'ppt-master'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# licos-dev-sdk
|
|
30
|
+
|
|
31
|
+
服务端模型和文件工具 SDK。模型目录、模型调用及积分检查默认使用项目所有者身份。需要以网页登录用户执行时,在本次请求创建客户端并显式传 `user_token`:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from licos_dev_sdk import LLMClient, check_credit_availability
|
|
35
|
+
|
|
36
|
+
availability = check_credit_availability(user_token=token, resource_code='LLM_INVOKE')
|
|
37
|
+
result = LLMClient(user_token=token).invoke('你好')
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Token 不含 `Bearer ` 前缀。用户信息来自平台认证结果;不从 `LICOS_CURRENT_USER_ID` 或 `LICOS_USER_TOKEN` 推断网页登录身份。Token 失效直接报错,由调用方更新会话。项目资源归属和平台签发的计费上下文保持独立。
|
|
41
|
+
|
|
42
|
+
可观测性继续使用项目身份;文件生成无需用户 Token。完整约定见仓库的 `tools/IDENTITY.md`。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# licos-dev-sdk
|
|
2
|
+
|
|
3
|
+
服务端模型和文件工具 SDK。模型目录、模型调用及积分检查默认使用项目所有者身份。需要以网页登录用户执行时,在本次请求创建客户端并显式传 `user_token`:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from licos_dev_sdk import LLMClient, check_credit_availability
|
|
7
|
+
|
|
8
|
+
availability = check_credit_availability(user_token=token, resource_code='LLM_INVOKE')
|
|
9
|
+
result = LLMClient(user_token=token).invoke('你好')
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Token 不含 `Bearer ` 前缀。用户信息来自平台认证结果;不从 `LICOS_CURRENT_USER_ID` 或 `LICOS_USER_TOKEN` 推断网页登录身份。Token 失效直接报错,由调用方更新会话。项目资源归属和平台签发的计费上下文保持独立。
|
|
13
|
+
|
|
14
|
+
可观测性继续使用项目身份;文件生成无需用户 Token。完整约定见仓库的 `tools/IDENTITY.md`。
|
|
@@ -4,11 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "licos-dev-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "LICOS Dev SDK - file generation and model capability clients"
|
|
9
|
+
readme = "README.md"
|
|
9
10
|
requires-python = ">=3.10"
|
|
10
11
|
dependencies = [
|
|
11
|
-
"licos-platform-sdk>=0.
|
|
12
|
+
"licos-platform-sdk>=0.7.0",
|
|
12
13
|
"weasyprint>=62.0",
|
|
13
14
|
"python-docx>=1.1",
|
|
14
15
|
"docxtpl>=0.16",
|
|
@@ -27,6 +27,7 @@ from licos_platform_sdk._runtime import (
|
|
|
27
27
|
platform_base_url,
|
|
28
28
|
resolve_user_token,
|
|
29
29
|
should_refresh_user_token,
|
|
30
|
+
require_user_token,
|
|
30
31
|
)
|
|
31
32
|
from ._usage_identity import UsageIdentityLease, has_signature
|
|
32
33
|
|
|
@@ -1222,13 +1223,12 @@ def _model_runtime(
|
|
|
1222
1223
|
) -> ModelRuntime:
|
|
1223
1224
|
resolved_base_url = normalize_base_url(base_url) if base_url else platform_base_url()
|
|
1224
1225
|
owner_user_id = user_id or env("LICOS_USER_ID") or env("AGENT_USER_ID")
|
|
1225
|
-
current_user_id = env("LICOS_CURRENT_USER_ID") or env("AGENT_CURRENT_USER_ID")
|
|
1226
1226
|
resolved_workspace_id = _workspace_id(workspace_id)
|
|
1227
|
-
runtime_user_token = (user_token
|
|
1228
|
-
explicit_user_token =
|
|
1227
|
+
runtime_user_token = require_user_token(user_token) if user_token is not None else None
|
|
1228
|
+
explicit_user_token = user_token is not None
|
|
1229
1229
|
token = runtime_user_token or resolve_user_token(
|
|
1230
1230
|
resolved_base_url,
|
|
1231
|
-
|
|
1231
|
+
owner_user_id,
|
|
1232
1232
|
)
|
|
1233
1233
|
owner_project_id = env("LICOS_PROJECT_ID") or env("AGENT_PROJECT_ID")
|
|
1234
1234
|
owner_project_name = env("LICOS_OWNER_PROJECT_NAME") or env("AGENT_OWNER_PROJECT_NAME")
|
|
@@ -1267,29 +1267,17 @@ def _model_runtime(
|
|
|
1267
1267
|
or env("AGENT_USAGE_SCENE")
|
|
1268
1268
|
or ("plugin" if owner_plugin_id else "ai_coding" if owner_project_id else "sdk")
|
|
1269
1269
|
)
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
or
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
(
|
|
1281
|
-
fetched_current_user_id,
|
|
1282
|
-
fetched_current_user_name,
|
|
1283
|
-
fetched_current_org_id,
|
|
1284
|
-
fetched_current_org_name,
|
|
1285
|
-
) = _fetch_current_user_context(
|
|
1286
|
-
resolved_base_url,
|
|
1287
|
-
token,
|
|
1270
|
+
try:
|
|
1271
|
+
current_user_id, current_user_name, current_org_id, current_org_name = _fetch_current_user_context(
|
|
1272
|
+
resolved_base_url, token,
|
|
1273
|
+
)
|
|
1274
|
+
except ApiError as error:
|
|
1275
|
+
if explicit_user_token or not should_refresh_user_token(error):
|
|
1276
|
+
raise
|
|
1277
|
+
token = resolve_user_token(resolved_base_url, owner_user_id, force_refresh=True)
|
|
1278
|
+
current_user_id, current_user_name, current_org_id, current_org_name = _fetch_current_user_context(
|
|
1279
|
+
resolved_base_url, token,
|
|
1288
1280
|
)
|
|
1289
|
-
current_user_id = current_user_id or fetched_current_user_id
|
|
1290
|
-
current_user_name = current_user_name or fetched_current_user_name
|
|
1291
|
-
current_org_id = current_org_id or fetched_current_org_id
|
|
1292
|
-
current_org_name = current_org_name or fetched_current_org_name
|
|
1293
1281
|
if owner_project_id:
|
|
1294
1282
|
if not resolved_workspace_id:
|
|
1295
1283
|
raise ConfigurationError("project owner workspace ID is not configured")
|
|
@@ -1342,27 +1330,13 @@ def _refresh_model_runtime(runtime: ModelRuntime) -> ModelRuntime:
|
|
|
1342
1330
|
return runtime
|
|
1343
1331
|
token = resolve_user_token(
|
|
1344
1332
|
runtime.base_url,
|
|
1345
|
-
runtime.
|
|
1333
|
+
runtime.user_id,
|
|
1346
1334
|
force_refresh=True,
|
|
1347
1335
|
)
|
|
1348
|
-
current_user_id =
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if not current_user_id or not current_user_name:
|
|
1353
|
-
(
|
|
1354
|
-
fetched_current_user_id,
|
|
1355
|
-
fetched_current_user_name,
|
|
1356
|
-
fetched_current_org_id,
|
|
1357
|
-
fetched_current_org_name,
|
|
1358
|
-
) = _fetch_current_user_context(
|
|
1359
|
-
runtime.base_url,
|
|
1360
|
-
token,
|
|
1361
|
-
)
|
|
1362
|
-
current_user_id = current_user_id or fetched_current_user_id
|
|
1363
|
-
current_user_name = current_user_name or fetched_current_user_name
|
|
1364
|
-
current_org_id = current_org_id or fetched_current_org_id
|
|
1365
|
-
current_org_name = current_org_name or fetched_current_org_name
|
|
1336
|
+
current_user_id, current_user_name, current_org_id, current_org_name = _fetch_current_user_context(
|
|
1337
|
+
runtime.base_url,
|
|
1338
|
+
token,
|
|
1339
|
+
)
|
|
1366
1340
|
return replace(
|
|
1367
1341
|
runtime,
|
|
1368
1342
|
token=token,
|
|
@@ -1404,10 +1378,7 @@ def _check_credit_availability(
|
|
|
1404
1378
|
)
|
|
1405
1379
|
break
|
|
1406
1380
|
except ApiError as exc:
|
|
1407
|
-
if (
|
|
1408
|
-
attempt == 0
|
|
1409
|
-
and should_refresh_user_token(exc)
|
|
1410
|
-
):
|
|
1381
|
+
if attempt == 0 and not runtime.explicit_user_token and should_refresh_user_token(exc):
|
|
1411
1382
|
_, _, subject_token = _credit_subject(runtime, force_refresh=True)
|
|
1412
1383
|
continue
|
|
1413
1384
|
raise
|
|
@@ -1444,12 +1415,12 @@ def _credit_subject(
|
|
|
1444
1415
|
if current_user_id
|
|
1445
1416
|
else (runtime.owner_org_id or "").strip() or None
|
|
1446
1417
|
)
|
|
1447
|
-
if
|
|
1418
|
+
if runtime.explicit_user_token or not force_refresh:
|
|
1448
1419
|
token = runtime.token
|
|
1449
1420
|
else:
|
|
1450
1421
|
token = resolve_user_token(
|
|
1451
1422
|
runtime.base_url,
|
|
1452
|
-
|
|
1423
|
+
owner_user_id,
|
|
1453
1424
|
force_refresh=force_refresh,
|
|
1454
1425
|
)
|
|
1455
1426
|
return subject_user_id, subject_org_id, token
|
|
@@ -1496,19 +1467,24 @@ def _fetch_current_user_context(
|
|
|
1496
1467
|
base_url: str,
|
|
1497
1468
|
token: str,
|
|
1498
1469
|
) -> tuple[str | None, str | None, str | None, str | None]:
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1470
|
+
payload = _request_json(
|
|
1471
|
+
"GET",
|
|
1472
|
+
f"{base_url}/api/v1/admin/auth/me",
|
|
1473
|
+
token=token,
|
|
1474
|
+
timeout=30,
|
|
1475
|
+
)
|
|
1476
|
+
if payload.get("code") not in (None, 0) or payload.get("success") is False:
|
|
1477
|
+
raise ApiError(
|
|
1478
|
+
str(payload.get("message") or "User authentication failed"),
|
|
1479
|
+
code=payload.get("code"),
|
|
1480
|
+
details=payload,
|
|
1505
1481
|
)
|
|
1506
|
-
except ApiError:
|
|
1507
|
-
return None, None, None, None
|
|
1508
1482
|
data = payload.get("data") if isinstance(payload, dict) else None
|
|
1509
1483
|
if not isinstance(data, dict):
|
|
1510
1484
|
data = payload if isinstance(payload, dict) else {}
|
|
1511
1485
|
user_id = _first_non_empty_field(data, ["id", "userId", "user_id", "sub"])
|
|
1486
|
+
if not user_id:
|
|
1487
|
+
raise ApiError("Authenticated user response missing id")
|
|
1512
1488
|
user_name = _first_non_empty_field(data, ["username", "userName", "name", "displayName"])
|
|
1513
1489
|
org_id = _first_non_empty_field(data, ["tenantId", "tenant_id"])
|
|
1514
1490
|
org_name = _first_non_empty_field(data, ["tenantName", "tenant_name", "orgName", "org_name"])
|
|
@@ -1580,7 +1556,7 @@ def _fetch_model_catalogs(
|
|
|
1580
1556
|
timeout=30,
|
|
1581
1557
|
)
|
|
1582
1558
|
except ApiError as exc:
|
|
1583
|
-
if not refresh and should_refresh_user_token(exc):
|
|
1559
|
+
if not refresh and not runtime.explicit_user_token and should_refresh_user_token(exc):
|
|
1584
1560
|
return _fetch_model_catalogs(
|
|
1585
1561
|
_refresh_model_runtime(runtime),
|
|
1586
1562
|
refresh=True,
|
|
@@ -2115,7 +2091,7 @@ def _fetch_model_detail(
|
|
|
2115
2091
|
timeout=30,
|
|
2116
2092
|
)
|
|
2117
2093
|
except ApiError as exc:
|
|
2118
|
-
if not refresh and should_refresh_user_token(exc):
|
|
2094
|
+
if not refresh and not runtime.explicit_user_token and should_refresh_user_token(exc):
|
|
2119
2095
|
return _fetch_model_detail(
|
|
2120
2096
|
_refresh_model_runtime(runtime),
|
|
2121
2097
|
model_code,
|
|
@@ -2563,7 +2539,7 @@ def _post_model_json(
|
|
|
2563
2539
|
timeout=request_timeout,
|
|
2564
2540
|
)
|
|
2565
2541
|
except ApiError as exc:
|
|
2566
|
-
if attempt == 0 and should_refresh_user_token(exc):
|
|
2542
|
+
if attempt == 0 and not active_runtime.explicit_user_token and should_refresh_user_token(exc):
|
|
2567
2543
|
active_runtime = _refresh_model_runtime(active_runtime)
|
|
2568
2544
|
continue
|
|
2569
2545
|
raise
|
|
@@ -2680,7 +2656,7 @@ def _stream_model_json(
|
|
|
2680
2656
|
return
|
|
2681
2657
|
except urlerror.HTTPError as exc:
|
|
2682
2658
|
error = _api_error_from_http(exc)
|
|
2683
|
-
if attempt == 0 and should_refresh_user_token(error):
|
|
2659
|
+
if attempt == 0 and not active_runtime.explicit_user_token and should_refresh_user_token(error):
|
|
2684
2660
|
active_runtime = _refresh_model_runtime(active_runtime)
|
|
2685
2661
|
continue
|
|
2686
2662
|
raise error from exc
|
|
@@ -2775,7 +2751,7 @@ def _await_async_model_result(
|
|
|
2775
2751
|
timeout=timeout or _request_timeout(),
|
|
2776
2752
|
)
|
|
2777
2753
|
except ApiError as exc:
|
|
2778
|
-
if should_refresh_user_token(exc):
|
|
2754
|
+
if not active_runtime.explicit_user_token and should_refresh_user_token(exc):
|
|
2779
2755
|
active_runtime = _refresh_model_runtime(active_runtime)
|
|
2780
2756
|
request_headers = {
|
|
2781
2757
|
**_request_headers(endpoint, active_runtime),
|
|
@@ -354,10 +354,22 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
354
354
|
self.assertEqual(len(sent), 1)
|
|
355
355
|
self.assertEqual(sent[0]["requestId"], "request-flush")
|
|
356
356
|
|
|
357
|
-
def
|
|
357
|
+
def test_credit_availability_defaults_to_project_identity(self) -> None:
|
|
358
358
|
captured: dict[str, Any] = {}
|
|
359
359
|
|
|
360
360
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
361
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
362
|
+
return _FakeResponse(
|
|
363
|
+
{
|
|
364
|
+
"code": 0,
|
|
365
|
+
"data": {
|
|
366
|
+
"id": "user-1",
|
|
367
|
+
"username": "Owner User",
|
|
368
|
+
"tenantId": "owner-org-1",
|
|
369
|
+
"tenantName": "Owner Org",
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
)
|
|
361
373
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
362
374
|
exchange_body = json.loads(req.data.decode("utf-8"))
|
|
363
375
|
token = (
|
|
@@ -386,9 +398,9 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
386
398
|
)
|
|
387
399
|
|
|
388
400
|
self.assertTrue(result["allowed"])
|
|
389
|
-
self.assertEqual(captured["headers"]["Authorization"], "Bearer
|
|
390
|
-
self.assertEqual(captured["headers"]["X-user-id"], "
|
|
391
|
-
self.assertEqual(captured["headers"]["X-tenant-id"], "
|
|
401
|
+
self.assertEqual(captured["headers"]["Authorization"], "Bearer owner-user-token")
|
|
402
|
+
self.assertEqual(captured["headers"]["X-user-id"], "user-1")
|
|
403
|
+
self.assertEqual(captured["headers"]["X-tenant-id"], "owner-org-1")
|
|
392
404
|
self.assertEqual(captured["headers"]["X-workspace-id"], "workspace-1")
|
|
393
405
|
self.assertEqual(
|
|
394
406
|
captured["body"],
|
|
@@ -427,10 +439,22 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
427
439
|
self.assertEqual(captured["headers"]["X-User-Id"], "owner-user")
|
|
428
440
|
self.assertEqual(captured["headers"]["X-Tenant-Id"], "owner-org")
|
|
429
441
|
|
|
430
|
-
def
|
|
442
|
+
def test_credit_availability_keeps_explicit_token(self) -> None:
|
|
431
443
|
captured: dict[str, Any] = {}
|
|
432
444
|
|
|
433
445
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
446
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
447
|
+
return _FakeResponse(
|
|
448
|
+
{
|
|
449
|
+
"code": 0,
|
|
450
|
+
"data": {
|
|
451
|
+
"id": "user-1",
|
|
452
|
+
"username": "Owner User",
|
|
453
|
+
"tenantId": "owner-org-1",
|
|
454
|
+
"tenantName": "Owner Org",
|
|
455
|
+
},
|
|
456
|
+
}
|
|
457
|
+
)
|
|
434
458
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
435
459
|
captured["exchange_body"] = json.loads(req.data.decode("utf-8"))
|
|
436
460
|
return _FakeResponse(
|
|
@@ -451,15 +475,27 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
451
475
|
)
|
|
452
476
|
|
|
453
477
|
self.assertTrue(result["allowed"])
|
|
454
|
-
self.
|
|
455
|
-
self.assertEqual(captured["headers"]["Authorization"], "Bearer
|
|
456
|
-
self.assertEqual(captured["headers"]["X-user-id"], "
|
|
457
|
-
self.assertEqual(captured["headers"]["X-tenant-id"], "
|
|
478
|
+
self.assertNotIn("exchange_body", captured)
|
|
479
|
+
self.assertEqual(captured["headers"]["Authorization"], "Bearer owner-token")
|
|
480
|
+
self.assertEqual(captured["headers"]["X-user-id"], "user-1")
|
|
481
|
+
self.assertEqual(captured["headers"]["X-tenant-id"], "owner-org-1")
|
|
458
482
|
|
|
459
483
|
def test_llm_invoke_resolves_catalog_and_user_token(self) -> None:
|
|
460
484
|
captured: dict[str, Any] = {}
|
|
461
485
|
|
|
462
486
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
487
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
488
|
+
return _FakeResponse(
|
|
489
|
+
{
|
|
490
|
+
"code": 0,
|
|
491
|
+
"data": {
|
|
492
|
+
"id": "user-1",
|
|
493
|
+
"username": "Owner User",
|
|
494
|
+
"tenantId": "owner-org-1",
|
|
495
|
+
"tenantName": "Owner Org",
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
)
|
|
463
499
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
464
500
|
captured["exchange_headers"] = dict(req.header_items())
|
|
465
501
|
captured["exchange_body"] = json.loads(req.data.decode("utf-8"))
|
|
@@ -485,7 +521,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
485
521
|
|
|
486
522
|
self.assertEqual(result.text, "hello")
|
|
487
523
|
self.assertEqual(captured["exchange_headers"]["Authorization"], "Bearer ai-agent-token")
|
|
488
|
-
self.assertEqual(captured["exchange_body"], {"userId": "
|
|
524
|
+
self.assertEqual(captured["exchange_body"], {"userId": "user-1"})
|
|
489
525
|
self.assertEqual(captured["catalog_headers"]["Authorization"], "Bearer user-token")
|
|
490
526
|
self.assertEqual(captured["detail_headers"]["Authorization"], "Bearer user-token")
|
|
491
527
|
self.assertEqual(captured["chat_headers"]["Authorization"], "Bearer user-token")
|
|
@@ -496,7 +532,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
496
532
|
self.assertNotIn("x-request-id", normalized_chat_headers)
|
|
497
533
|
self.assertNotIn("x-aios-request-id", normalized_chat_headers)
|
|
498
534
|
self.assertNotIn("x-client-request-id", normalized_chat_headers)
|
|
499
|
-
self.assertEqual(normalized_chat_headers["x-licos-current-user-id"], "
|
|
535
|
+
self.assertEqual(normalized_chat_headers["x-licos-current-user-id"], "user-1")
|
|
500
536
|
self.assertEqual(normalized_chat_headers["x-provider-route"], "provider-a")
|
|
501
537
|
self.assertEqual(
|
|
502
538
|
[name for name in normalized_chat_headers if name == "x-licos-current-user-id"],
|
|
@@ -513,6 +549,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
513
549
|
|
|
514
550
|
def test_llm_stream_does_not_report_incomplete_transport_stream(self) -> None:
|
|
515
551
|
def fake_urlopen(req: Any, timeout: int = 0) -> Any:
|
|
552
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
553
|
+
return _FakeResponse(
|
|
554
|
+
{
|
|
555
|
+
"code": 0,
|
|
556
|
+
"data": {
|
|
557
|
+
"id": "user-1",
|
|
558
|
+
"username": "Owner User",
|
|
559
|
+
"tenantId": "owner-org-1",
|
|
560
|
+
"tenantName": "Owner Org",
|
|
561
|
+
},
|
|
562
|
+
}
|
|
563
|
+
)
|
|
516
564
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
517
565
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
518
566
|
if _is_available_models_request(req):
|
|
@@ -538,6 +586,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
538
586
|
captured: dict[str, Any] = {}
|
|
539
587
|
|
|
540
588
|
def fake_urlopen(req: Any, timeout: int = 0) -> Any:
|
|
589
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
590
|
+
return _FakeResponse(
|
|
591
|
+
{
|
|
592
|
+
"code": 0,
|
|
593
|
+
"data": {
|
|
594
|
+
"id": "user-1",
|
|
595
|
+
"username": "Owner User",
|
|
596
|
+
"tenantId": "owner-org-1",
|
|
597
|
+
"tenantName": "Owner Org",
|
|
598
|
+
},
|
|
599
|
+
}
|
|
600
|
+
)
|
|
541
601
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
542
602
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
543
603
|
if _is_available_models_request(req):
|
|
@@ -638,7 +698,8 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
638
698
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
639
699
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
640
700
|
if req.full_url == "http://platform.example/api/v1/admin/auth/me":
|
|
641
|
-
|
|
701
|
+
self.assertEqual(req.get_header("Authorization"), "Bearer request-token")
|
|
702
|
+
return _FakeResponse({"code": 0, "data": {"id": "personal-caller"}})
|
|
642
703
|
if _is_available_models_request(req):
|
|
643
704
|
captured["catalog_url"] = req.full_url
|
|
644
705
|
captured["catalog_headers"] = dict(req.header_items())
|
|
@@ -648,13 +709,13 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
648
709
|
raise AssertionError(req.full_url)
|
|
649
710
|
|
|
650
711
|
with mock.patch.object(model.request, "urlopen", fake_urlopen):
|
|
651
|
-
endpoint = model.resolve_llm_endpoint()
|
|
712
|
+
endpoint = model.resolve_llm_endpoint(user_token="request-token")
|
|
652
713
|
|
|
653
714
|
query = parse.parse_qs(parse.urlparse(captured["catalog_url"]).query)
|
|
654
715
|
catalog_headers = {key.lower(): value for key, value in captured["catalog_headers"].items()}
|
|
655
|
-
self.assertEqual(endpoint.request_headers["X-Licos-Current-User-Id"], "personal-
|
|
716
|
+
self.assertEqual(endpoint.request_headers["X-Licos-Current-User-Id"], "personal-caller")
|
|
656
717
|
self.assertNotIn("X-Licos-Current-Org-Id", endpoint.request_headers)
|
|
657
|
-
self.assertEqual(catalog_headers["x-licos-current-user-id"], "personal-
|
|
718
|
+
self.assertEqual(catalog_headers["x-licos-current-user-id"], "personal-caller")
|
|
658
719
|
self.assertNotIn("x-licos-current-org-id", catalog_headers)
|
|
659
720
|
self.assertNotIn("currentUserId", query)
|
|
660
721
|
self.assertNotIn("currentOrgId", query)
|
|
@@ -699,6 +760,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
699
760
|
captured: dict[str, Any] = {}
|
|
700
761
|
|
|
701
762
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
763
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
764
|
+
return _FakeResponse(
|
|
765
|
+
{
|
|
766
|
+
"code": 0,
|
|
767
|
+
"data": {
|
|
768
|
+
"id": "user-1",
|
|
769
|
+
"username": "Owner User",
|
|
770
|
+
"tenantId": "owner-org-1",
|
|
771
|
+
"tenantName": "Owner Org",
|
|
772
|
+
},
|
|
773
|
+
}
|
|
774
|
+
)
|
|
702
775
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
703
776
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
704
777
|
if _is_available_models_request(req):
|
|
@@ -734,6 +807,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
734
807
|
captured: dict[str, Any] = {}
|
|
735
808
|
|
|
736
809
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
810
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
811
|
+
return _FakeResponse(
|
|
812
|
+
{
|
|
813
|
+
"code": 0,
|
|
814
|
+
"data": {
|
|
815
|
+
"id": "user-1",
|
|
816
|
+
"username": "Owner User",
|
|
817
|
+
"tenantId": "owner-org-1",
|
|
818
|
+
"tenantName": "Owner Org",
|
|
819
|
+
},
|
|
820
|
+
}
|
|
821
|
+
)
|
|
737
822
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
738
823
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
739
824
|
if _is_available_models_request(req):
|
|
@@ -774,6 +859,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
774
859
|
chat_tokens: list[str] = []
|
|
775
860
|
|
|
776
861
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
862
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
863
|
+
return _FakeResponse(
|
|
864
|
+
{
|
|
865
|
+
"code": 0,
|
|
866
|
+
"data": {
|
|
867
|
+
"id": "user-1",
|
|
868
|
+
"username": "Owner User",
|
|
869
|
+
"tenantId": "owner-org-1",
|
|
870
|
+
"tenantName": "Owner Org",
|
|
871
|
+
},
|
|
872
|
+
}
|
|
873
|
+
)
|
|
777
874
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
778
875
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": next(tokens)}})
|
|
779
876
|
if _is_available_models_request(req):
|
|
@@ -803,6 +900,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
803
900
|
|
|
804
901
|
def test_resolve_llm_endpoint_applies_model_detail_limits(self) -> None:
|
|
805
902
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
903
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
904
|
+
return _FakeResponse(
|
|
905
|
+
{
|
|
906
|
+
"code": 0,
|
|
907
|
+
"data": {
|
|
908
|
+
"id": "user-1",
|
|
909
|
+
"username": "Owner User",
|
|
910
|
+
"tenantId": "owner-org-1",
|
|
911
|
+
"tenantName": "Owner Org",
|
|
912
|
+
},
|
|
913
|
+
}
|
|
914
|
+
)
|
|
806
915
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
807
916
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
808
917
|
if _is_available_models_request(req):
|
|
@@ -823,6 +932,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
823
932
|
|
|
824
933
|
def test_resolve_llm_endpoint_parses_catalog_limit_suffixes(self) -> None:
|
|
825
934
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
935
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
936
|
+
return _FakeResponse(
|
|
937
|
+
{
|
|
938
|
+
"code": 0,
|
|
939
|
+
"data": {
|
|
940
|
+
"id": "user-1",
|
|
941
|
+
"username": "Owner User",
|
|
942
|
+
"tenantId": "owner-org-1",
|
|
943
|
+
"tenantName": "Owner Org",
|
|
944
|
+
},
|
|
945
|
+
}
|
|
946
|
+
)
|
|
826
947
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
827
948
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
828
949
|
if _is_available_models_request(req):
|
|
@@ -842,6 +963,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
842
963
|
|
|
843
964
|
def test_resolve_llm_endpoint_accepts_explicit_model(self) -> None:
|
|
844
965
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
966
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
967
|
+
return _FakeResponse(
|
|
968
|
+
{
|
|
969
|
+
"code": 0,
|
|
970
|
+
"data": {
|
|
971
|
+
"id": "user-1",
|
|
972
|
+
"username": "Owner User",
|
|
973
|
+
"tenantId": "owner-org-1",
|
|
974
|
+
"tenantName": "Owner Org",
|
|
975
|
+
},
|
|
976
|
+
}
|
|
977
|
+
)
|
|
845
978
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
846
979
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
847
980
|
if _is_available_models_request(req):
|
|
@@ -867,6 +1000,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
867
1000
|
fake_langchain_openai.ChatOpenAI = FakeChatOpenAI
|
|
868
1001
|
|
|
869
1002
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1003
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1004
|
+
return _FakeResponse(
|
|
1005
|
+
{
|
|
1006
|
+
"code": 0,
|
|
1007
|
+
"data": {
|
|
1008
|
+
"id": "user-1",
|
|
1009
|
+
"username": "Owner User",
|
|
1010
|
+
"tenantId": "owner-org-1",
|
|
1011
|
+
"tenantName": "Owner Org",
|
|
1012
|
+
},
|
|
1013
|
+
}
|
|
1014
|
+
)
|
|
870
1015
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
871
1016
|
captured["exchange_headers"] = dict(req.header_items())
|
|
872
1017
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
@@ -913,7 +1058,7 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
913
1058
|
normalized_default_headers = {
|
|
914
1059
|
key.lower(): value for key, value in kwargs["default_headers"].items()
|
|
915
1060
|
}
|
|
916
|
-
self.assertEqual(normalized_default_headers["x-licos-current-user-id"], "
|
|
1061
|
+
self.assertEqual(normalized_default_headers["x-licos-current-user-id"], "user-1")
|
|
917
1062
|
self.assertEqual(normalized_default_headers["x-caller"], "caller")
|
|
918
1063
|
self.assertEqual(normalized_default_headers["x-provider-route"], "provider-a")
|
|
919
1064
|
|
|
@@ -921,6 +1066,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
921
1066
|
captured: dict[str, Any] = {}
|
|
922
1067
|
|
|
923
1068
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1069
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1070
|
+
return _FakeResponse(
|
|
1071
|
+
{
|
|
1072
|
+
"code": 0,
|
|
1073
|
+
"data": {
|
|
1074
|
+
"id": "user-1",
|
|
1075
|
+
"username": "Owner User",
|
|
1076
|
+
"tenantId": "owner-org-1",
|
|
1077
|
+
"tenantName": "Owner Org",
|
|
1078
|
+
},
|
|
1079
|
+
}
|
|
1080
|
+
)
|
|
924
1081
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
925
1082
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
926
1083
|
if _is_available_models_request(req):
|
|
@@ -948,6 +1105,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
948
1105
|
captured: dict[str, Any] = {}
|
|
949
1106
|
|
|
950
1107
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1108
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1109
|
+
return _FakeResponse(
|
|
1110
|
+
{
|
|
1111
|
+
"code": 0,
|
|
1112
|
+
"data": {
|
|
1113
|
+
"id": "user-1",
|
|
1114
|
+
"username": "Owner User",
|
|
1115
|
+
"tenantId": "owner-org-1",
|
|
1116
|
+
"tenantName": "Owner Org",
|
|
1117
|
+
},
|
|
1118
|
+
}
|
|
1119
|
+
)
|
|
951
1120
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
952
1121
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
953
1122
|
if _is_available_models_request(req):
|
|
@@ -984,6 +1153,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
984
1153
|
captured: dict[str, Any] = {}
|
|
985
1154
|
|
|
986
1155
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1156
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1157
|
+
return _FakeResponse(
|
|
1158
|
+
{
|
|
1159
|
+
"code": 0,
|
|
1160
|
+
"data": {
|
|
1161
|
+
"id": "user-1",
|
|
1162
|
+
"username": "Owner User",
|
|
1163
|
+
"tenantId": "owner-org-1",
|
|
1164
|
+
"tenantName": "Owner Org",
|
|
1165
|
+
},
|
|
1166
|
+
}
|
|
1167
|
+
)
|
|
987
1168
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
988
1169
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
989
1170
|
if _is_available_models_request(req):
|
|
@@ -1010,6 +1191,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
1010
1191
|
captured: dict[str, Any] = {}
|
|
1011
1192
|
|
|
1012
1193
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1194
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1195
|
+
return _FakeResponse(
|
|
1196
|
+
{
|
|
1197
|
+
"code": 0,
|
|
1198
|
+
"data": {
|
|
1199
|
+
"id": "user-1",
|
|
1200
|
+
"username": "Owner User",
|
|
1201
|
+
"tenantId": "owner-org-1",
|
|
1202
|
+
"tenantName": "Owner Org",
|
|
1203
|
+
},
|
|
1204
|
+
}
|
|
1205
|
+
)
|
|
1013
1206
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
1014
1207
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
1015
1208
|
if _is_available_models_request(req):
|
|
@@ -1038,6 +1231,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
1038
1231
|
captured: dict[str, Any] = {}
|
|
1039
1232
|
|
|
1040
1233
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1234
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1235
|
+
return _FakeResponse(
|
|
1236
|
+
{
|
|
1237
|
+
"code": 0,
|
|
1238
|
+
"data": {
|
|
1239
|
+
"id": "user-1",
|
|
1240
|
+
"username": "Owner User",
|
|
1241
|
+
"tenantId": "owner-org-1",
|
|
1242
|
+
"tenantName": "Owner Org",
|
|
1243
|
+
},
|
|
1244
|
+
}
|
|
1245
|
+
)
|
|
1041
1246
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
1042
1247
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
1043
1248
|
if _is_available_models_request(req):
|
|
@@ -1075,6 +1280,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
1075
1280
|
captured: dict[str, Any] = {}
|
|
1076
1281
|
|
|
1077
1282
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1283
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1284
|
+
return _FakeResponse(
|
|
1285
|
+
{
|
|
1286
|
+
"code": 0,
|
|
1287
|
+
"data": {
|
|
1288
|
+
"id": "user-1",
|
|
1289
|
+
"username": "Owner User",
|
|
1290
|
+
"tenantId": "owner-org-1",
|
|
1291
|
+
"tenantName": "Owner Org",
|
|
1292
|
+
},
|
|
1293
|
+
}
|
|
1294
|
+
)
|
|
1078
1295
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
1079
1296
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
1080
1297
|
if _is_available_models_request(req):
|
|
@@ -1104,6 +1321,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
1104
1321
|
captured: dict[str, Any] = {}
|
|
1105
1322
|
|
|
1106
1323
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1324
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1325
|
+
return _FakeResponse(
|
|
1326
|
+
{
|
|
1327
|
+
"code": 0,
|
|
1328
|
+
"data": {
|
|
1329
|
+
"id": "user-1",
|
|
1330
|
+
"username": "Owner User",
|
|
1331
|
+
"tenantId": "owner-org-1",
|
|
1332
|
+
"tenantName": "Owner Org",
|
|
1333
|
+
},
|
|
1334
|
+
}
|
|
1335
|
+
)
|
|
1107
1336
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
1108
1337
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
1109
1338
|
if _is_available_models_request(req):
|
|
@@ -1131,6 +1360,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
1131
1360
|
captured: dict[str, Any] = {}
|
|
1132
1361
|
|
|
1133
1362
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1363
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1364
|
+
return _FakeResponse(
|
|
1365
|
+
{
|
|
1366
|
+
"code": 0,
|
|
1367
|
+
"data": {
|
|
1368
|
+
"id": "user-1",
|
|
1369
|
+
"username": "Owner User",
|
|
1370
|
+
"tenantId": "owner-org-1",
|
|
1371
|
+
"tenantName": "Owner Org",
|
|
1372
|
+
},
|
|
1373
|
+
}
|
|
1374
|
+
)
|
|
1134
1375
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
1135
1376
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
1136
1377
|
if _is_available_models_request(req):
|
|
@@ -1160,6 +1401,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
1160
1401
|
captured: dict[str, Any] = {}
|
|
1161
1402
|
|
|
1162
1403
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1404
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1405
|
+
return _FakeResponse(
|
|
1406
|
+
{
|
|
1407
|
+
"code": 0,
|
|
1408
|
+
"data": {
|
|
1409
|
+
"id": "user-1",
|
|
1410
|
+
"username": "Owner User",
|
|
1411
|
+
"tenantId": "owner-org-1",
|
|
1412
|
+
"tenantName": "Owner Org",
|
|
1413
|
+
},
|
|
1414
|
+
}
|
|
1415
|
+
)
|
|
1163
1416
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
1164
1417
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
1165
1418
|
if _is_available_models_request(req):
|
|
@@ -1185,6 +1438,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
1185
1438
|
captured: dict[str, Any] = {}
|
|
1186
1439
|
|
|
1187
1440
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1441
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1442
|
+
return _FakeResponse(
|
|
1443
|
+
{
|
|
1444
|
+
"code": 0,
|
|
1445
|
+
"data": {
|
|
1446
|
+
"id": "user-1",
|
|
1447
|
+
"username": "Owner User",
|
|
1448
|
+
"tenantId": "owner-org-1",
|
|
1449
|
+
"tenantName": "Owner Org",
|
|
1450
|
+
},
|
|
1451
|
+
}
|
|
1452
|
+
)
|
|
1188
1453
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
1189
1454
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
1190
1455
|
if _is_available_models_request(req):
|
|
@@ -1214,6 +1479,18 @@ class ModelSdkTests(unittest.TestCase):
|
|
|
1214
1479
|
captured: dict[str, Any] = {}
|
|
1215
1480
|
|
|
1216
1481
|
def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
|
|
1482
|
+
if req.full_url.endswith("/admin/auth/me"):
|
|
1483
|
+
return _FakeResponse(
|
|
1484
|
+
{
|
|
1485
|
+
"code": 0,
|
|
1486
|
+
"data": {
|
|
1487
|
+
"id": "user-1",
|
|
1488
|
+
"username": "Owner User",
|
|
1489
|
+
"tenantId": "owner-org-1",
|
|
1490
|
+
"tenantName": "Owner Org",
|
|
1491
|
+
},
|
|
1492
|
+
}
|
|
1493
|
+
)
|
|
1217
1494
|
if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
|
|
1218
1495
|
return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
|
|
1219
1496
|
if _is_available_models_request(req):
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from unittest import TestCase, mock
|
|
4
|
+
|
|
5
|
+
from licos_dev_sdk import model
|
|
6
|
+
from licos_platform_sdk import ConfigurationError, ApiError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelIdentityTests(TestCase):
|
|
10
|
+
def test_expired_project_credentials_refresh_the_same_owner(self):
|
|
11
|
+
with mock.patch.dict(os.environ, {"LICOS_CURRENT_USER_ID": "initializer"}, clear=True), \
|
|
12
|
+
mock.patch.object(model, "resolve_user_token", side_effect=["expired", "renewed"]) as exchange, \
|
|
13
|
+
mock.patch.object(model, "_request_json", side_effect=[
|
|
14
|
+
ApiError("expired", status=401), {"code": 0, "data": {"id": "owner"}}
|
|
15
|
+
]):
|
|
16
|
+
runtime = model._model_runtime(base_url="https://platform.example", user_id="owner")
|
|
17
|
+
self.assertEqual(runtime.token, "renewed")
|
|
18
|
+
self.assertEqual(runtime.current_user_id, "owner")
|
|
19
|
+
self.assertEqual(exchange.call_args_list, [
|
|
20
|
+
mock.call("https://platform.example", "owner"),
|
|
21
|
+
mock.call("https://platform.example", "owner", force_refresh=True),
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
def test_business_authentication_error_is_not_a_user_profile(self):
|
|
25
|
+
with mock.patch.object(model, "_request_json", return_value={
|
|
26
|
+
"code": 10002, "message": "expired", "data": {"id": "stale-user"}
|
|
27
|
+
}), mock.patch.object(model, "resolve_user_token") as exchange:
|
|
28
|
+
with self.assertRaisesRegex(ApiError, "expired"):
|
|
29
|
+
model._model_runtime(base_url="https://platform.example", user_token="expired")
|
|
30
|
+
exchange.assert_not_called()
|
|
31
|
+
|
|
32
|
+
def test_concurrent_request_identity_ignores_initialization_environment(self):
|
|
33
|
+
def respond(method, url, *, token, **kwargs):
|
|
34
|
+
self.assertTrue(url.endswith("/auth/me"))
|
|
35
|
+
return {"code": 0, "data": {"id": token, "username": token, "tenantId": token + "-org"}}
|
|
36
|
+
|
|
37
|
+
with mock.patch.dict(
|
|
38
|
+
os.environ,
|
|
39
|
+
{
|
|
40
|
+
"LICOS_USER_ID": "owner",
|
|
41
|
+
"LICOS_USER_TOKEN": "ambient",
|
|
42
|
+
"LICOS_CURRENT_USER_ID": "initializer",
|
|
43
|
+
"LICOS_CURRENT_ORG_ID": "initializer-org",
|
|
44
|
+
},
|
|
45
|
+
clear=True,
|
|
46
|
+
), mock.patch.object(model, "_request_json", side_effect=respond), ThreadPoolExecutor(2) as pool:
|
|
47
|
+
values = list(
|
|
48
|
+
pool.map(
|
|
49
|
+
lambda token: model._model_runtime(base_url="https://platform.example", user_token=token),
|
|
50
|
+
["alice", "bob"],
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
self.assertEqual(
|
|
54
|
+
[(value.current_user_id, value.current_org_id, value.user_id) for value in values],
|
|
55
|
+
[("alice", "alice-org", "owner"), ("bob", "bob-org", "owner")],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def test_invalid_explicit_tokens_never_exchange_identity(self):
|
|
59
|
+
with mock.patch.object(model, "resolve_user_token") as exchange, mock.patch.object(
|
|
60
|
+
model, "_request_json", side_effect=ApiError("expired", status=401)
|
|
61
|
+
) as http:
|
|
62
|
+
with self.assertRaises(ConfigurationError):
|
|
63
|
+
model._model_runtime(base_url="https://platform.example", user_token=" ")
|
|
64
|
+
http.assert_not_called()
|
|
65
|
+
with self.assertRaises(ApiError):
|
|
66
|
+
model._model_runtime(base_url="https://platform.example", user_token="expired")
|
|
67
|
+
http.assert_called_once()
|
|
68
|
+
exchange.assert_not_called()
|
|
69
|
+
|
|
70
|
+
def test_credit_authentication_failure_does_not_retry_with_runtime_identity(self):
|
|
71
|
+
runtime = model.ModelRuntime(
|
|
72
|
+
base_url="https://platform.example",
|
|
73
|
+
token="caller",
|
|
74
|
+
user_id="owner",
|
|
75
|
+
current_user_id="caller",
|
|
76
|
+
explicit_user_token=True,
|
|
77
|
+
)
|
|
78
|
+
with mock.patch.object(
|
|
79
|
+
model, "_request_json", side_effect=ApiError("expired", status=401)
|
|
80
|
+
) as http, mock.patch.object(model, "resolve_user_token") as exchange:
|
|
81
|
+
with self.assertRaises(ApiError):
|
|
82
|
+
model._check_credit_availability(
|
|
83
|
+
runtime, estimated_points=1, resource_type="MODEL_TOOL", resource_code="LLM_INVOKE"
|
|
84
|
+
)
|
|
85
|
+
http.assert_called_once()
|
|
86
|
+
exchange.assert_not_called()
|
licos_dev_sdk-0.5.0/PKG-INFO
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: licos-dev-sdk
|
|
3
|
-
Version: 0.5.0
|
|
4
|
-
Summary: LICOS Dev SDK - file generation and model capability clients
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: beautifulsoup4>=4.12
|
|
7
|
-
Requires-Dist: docxtpl>=0.16
|
|
8
|
-
Requires-Dist: graphviz>=0.20
|
|
9
|
-
Requires-Dist: jinja2>=3.1
|
|
10
|
-
Requires-Dist: licos-platform-sdk>=0.6.1
|
|
11
|
-
Requires-Dist: matplotlib>=3.9
|
|
12
|
-
Requires-Dist: mistune>=3.0
|
|
13
|
-
Requires-Dist: openpyxl>=3.1
|
|
14
|
-
Requires-Dist: pillow>=10.0
|
|
15
|
-
Requires-Dist: pypdf>=5.0
|
|
16
|
-
Requires-Dist: python-barcode>=0.15
|
|
17
|
-
Requires-Dist: python-docx>=1.1
|
|
18
|
-
Requires-Dist: python-pptx>=1.0
|
|
19
|
-
Requires-Dist: pyyaml>=6.0
|
|
20
|
-
Requires-Dist: qrcode[pil]>=8.0
|
|
21
|
-
Requires-Dist: skia-pathops>=0.9.2
|
|
22
|
-
Requires-Dist: uharfbuzz>=0.50.0
|
|
23
|
-
Requires-Dist: weasyprint>=62.0
|
|
24
|
-
Requires-Dist: xlsxwriter>=3.2
|
|
25
|
-
Provides-Extra: ppt-master
|
|
26
|
-
Requires-Dist: licos-ppt-master-assets==0.0.1; extra == 'ppt-master'
|
|
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
|
{licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_table_actions.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
|
|
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
|