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.
Files changed (57) hide show
  1. licos_dev_sdk-0.6.0/PKG-INFO +42 -0
  2. licos_dev_sdk-0.6.0/README.md +14 -0
  3. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/pyproject.toml +3 -2
  4. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/model.py +40 -64
  5. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_model.py +293 -16
  6. licos_dev_sdk-0.6.0/tests/test_request_identity.py +86 -0
  7. licos_dev_sdk-0.5.0/PKG-INFO +0 -26
  8. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/.gitignore +0 -0
  9. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/DOCX.md +0 -0
  10. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/__init__.py +0 -0
  11. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/_usage_identity.py +0 -0
  12. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/_utils.py +0 -0
  13. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/archive.py +0 -0
  14. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/chart.py +0 -0
  15. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/data.py +0 -0
  16. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/diagram.py +0 -0
  17. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document.py +0 -0
  18. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_common.py +0 -0
  19. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx.py +0 -0
  20. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_edit.py +0 -0
  21. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_expectations.py +0 -0
  22. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_inspect.py +0 -0
  23. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_layout.py +0 -0
  24. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_package.py +0 -0
  25. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_preservation.py +0 -0
  26. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_structure.py +0 -0
  27. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_table.py +0 -0
  28. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_docx_table_actions.py +0 -0
  29. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_read.py +0 -0
  30. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/document_table_export.py +0 -0
  31. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/image.py +0 -0
  32. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/langchain_reasoning.py +0 -0
  33. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/observability.py +0 -0
  34. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/pdf_inspect.py +0 -0
  35. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
  36. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/presentation.py +0 -0
  37. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/presentation_canvas.py +0 -0
  38. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/presentation_inspect.py +0 -0
  39. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/spreadsheet.py +0 -0
  40. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
  41. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
  42. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
  43. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/src/licos_dev_sdk/web.py +0 -0
  44. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_document_read.py +0 -0
  45. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_document_spreadsheet.py +0 -0
  46. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_document_table_export.py +0 -0
  47. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_docx_inspect.py +0 -0
  48. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_docx_structure.py +0 -0
  49. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_docx_table_edit.py +0 -0
  50. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_html_docx_features.py +0 -0
  51. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_langchain_protocol.py +0 -0
  52. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_observability.py +0 -0
  53. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_output_paths.py +0 -0
  54. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_ppt_master_runtime.py +0 -0
  55. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_presentation.py +0 -0
  56. {licos_dev_sdk-0.5.0 → licos_dev_sdk-0.6.0}/tests/test_spreadsheet_html_edit.py +0 -0
  57. {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.5.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.6.1",
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 or env("LICOS_USER_TOKEN") or "").strip()
1228
- explicit_user_token = bool(runtime_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
- current_user_id or owner_user_id,
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
- current_user_name = env("LICOS_CURRENT_USER_NAME") or env("AGENT_CURRENT_USER_NAME")
1271
- current_org_id = (
1272
- env("LICOS_CURRENT_ORG_ID")
1273
- or env("LICOS_CURRENT_USER_ORG_ID")
1274
- or env("AGENT_CURRENT_ORG_ID")
1275
- or env("LICOS_CURRENT_TENANT_ID")
1276
- or env("AGENT_CURRENT_TENANT_ID")
1277
- )
1278
- current_org_name = env("LICOS_CURRENT_ORG_NAME") or env("AGENT_CURRENT_ORG_NAME")
1279
- if not current_user_id or not current_user_name:
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.current_user_id or runtime.user_id,
1333
+ runtime.user_id,
1346
1334
  force_refresh=True,
1347
1335
  )
1348
- current_user_id = runtime.current_user_id
1349
- current_user_name = runtime.current_user_name
1350
- current_org_id = runtime.current_org_id
1351
- current_org_name = runtime.current_org_name
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 subject_user_id == owner_user_id and not force_refresh:
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
- subject_user_id,
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
- try:
1500
- payload = _request_json(
1501
- "GET",
1502
- f"{base_url}/api/v1/admin/auth/me",
1503
- token=token,
1504
- timeout=30,
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 test_credit_availability_uses_current_user_token_and_workspace(self) -> None:
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 current-user-token")
390
- self.assertEqual(captured["headers"]["X-user-id"], "current-user-1")
391
- self.assertEqual(captured["headers"]["X-tenant-id"], "current-org-1")
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 test_credit_availability_exchanges_current_user_token_even_when_owner_token_is_explicit(self) -> None:
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.assertEqual(captured["exchange_body"], {"userId": "current-user-1"})
455
- self.assertEqual(captured["headers"]["Authorization"], "Bearer current-user-token")
456
- self.assertEqual(captured["headers"]["X-user-id"], "current-user-1")
457
- self.assertEqual(captured["headers"]["X-tenant-id"], "current-org-1")
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": "current-user-1"})
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"], "current-user-1")
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
- raise AssertionError("explicit current user must not be replaced with owner token context")
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-current-user")
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-current-user")
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"], "current-user-1")
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()
@@ -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