licos-dev-sdk 0.6.4__tar.gz → 0.6.6__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 (56) hide show
  1. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/PKG-INFO +2 -2
  2. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/pyproject.toml +2 -2
  3. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/model.py +4 -1
  4. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/observability.py +3 -2
  5. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_model.py +1 -1
  6. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_observability.py +150 -150
  7. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_request_identity.py +2 -2
  8. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/.gitignore +0 -0
  9. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/DOCX.md +0 -0
  10. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/README.md +0 -0
  11. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/__init__.py +0 -0
  12. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/_usage_identity.py +0 -0
  13. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/_utils.py +0 -0
  14. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/archive.py +0 -0
  15. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/chart.py +0 -0
  16. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/data.py +0 -0
  17. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/diagram.py +0 -0
  18. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document.py +0 -0
  19. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_common.py +0 -0
  20. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx.py +0 -0
  21. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_edit.py +0 -0
  22. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_expectations.py +0 -0
  23. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_inspect.py +0 -0
  24. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_layout.py +0 -0
  25. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_package.py +0 -0
  26. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_preservation.py +0 -0
  27. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_structure.py +0 -0
  28. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_table.py +0 -0
  29. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_docx_table_actions.py +0 -0
  30. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_read.py +0 -0
  31. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/document_table_export.py +0 -0
  32. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/image.py +0 -0
  33. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/langchain_reasoning.py +0 -0
  34. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/pdf_inspect.py +0 -0
  35. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
  36. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/presentation.py +0 -0
  37. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/presentation_canvas.py +0 -0
  38. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/presentation_inspect.py +0 -0
  39. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/spreadsheet.py +0 -0
  40. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
  41. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
  42. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
  43. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/src/licos_dev_sdk/web.py +0 -0
  44. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_document_read.py +0 -0
  45. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_document_spreadsheet.py +0 -0
  46. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_document_table_export.py +0 -0
  47. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_docx_inspect.py +0 -0
  48. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_docx_structure.py +0 -0
  49. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_docx_table_edit.py +0 -0
  50. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_html_docx_features.py +0 -0
  51. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_langchain_protocol.py +0 -0
  52. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_output_paths.py +0 -0
  53. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_ppt_master_runtime.py +0 -0
  54. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_presentation.py +0 -0
  55. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_spreadsheet_html_edit.py +0 -0
  56. {licos_dev_sdk-0.6.4 → licos_dev_sdk-0.6.6}/tests/test_usage_identity.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: licos-dev-sdk
3
- Version: 0.6.4
3
+ Version: 0.6.6
4
4
  Summary: LICOS Dev SDK - file generation and model capability clients
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: beautifulsoup4>=4.12
7
7
  Requires-Dist: docxtpl>=0.16
8
8
  Requires-Dist: graphviz>=0.20
9
9
  Requires-Dist: jinja2>=3.1
10
- Requires-Dist: licos-platform-sdk>=0.7.4
10
+ Requires-Dist: licos-platform-sdk>=0.7.6
11
11
  Requires-Dist: matplotlib>=3.9
12
12
  Requires-Dist: mistune>=3.0
13
13
  Requires-Dist: openpyxl>=3.1
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-dev-sdk"
7
- version = "0.6.4"
7
+ version = "0.6.6"
8
8
  description = "LICOS Dev SDK - file generation and model capability clients"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
- "licos-platform-sdk>=0.7.4",
12
+ "licos-platform-sdk>=0.7.6",
13
13
  "weasyprint>=62.0",
14
14
  "python-docx>=1.1",
15
15
  "docxtpl>=0.16",
@@ -1229,6 +1229,7 @@ def _model_runtime(
1229
1229
  token = runtime_user_token or resolve_user_token(
1230
1230
  resolved_base_url,
1231
1231
  owner_user_id,
1232
+ workspace_id=resolved_workspace_id,
1232
1233
  )
1233
1234
  owner_project_id = env("LICOS_PROJECT_ID") or env("AGENT_PROJECT_ID")
1234
1235
  owner_project_name = env("LICOS_OWNER_PROJECT_NAME") or env("AGENT_OWNER_PROJECT_NAME")
@@ -1274,7 +1275,7 @@ def _model_runtime(
1274
1275
  except ApiError as error:
1275
1276
  if explicit_user_token or not should_refresh_user_token(error):
1276
1277
  raise
1277
- token = resolve_user_token(resolved_base_url, owner_user_id, force_refresh=True)
1278
+ token = resolve_user_token(resolved_base_url, owner_user_id, force_refresh=True, workspace_id=resolved_workspace_id)
1278
1279
  current_user_id, current_user_name, current_org_id, current_org_name = _fetch_current_user_context(
1279
1280
  resolved_base_url, token,
1280
1281
  )
@@ -1332,6 +1333,7 @@ def _refresh_model_runtime(runtime: ModelRuntime) -> ModelRuntime:
1332
1333
  runtime.base_url,
1333
1334
  runtime.user_id,
1334
1335
  force_refresh=True,
1336
+ workspace_id=runtime.workspace_id,
1335
1337
  )
1336
1338
  current_user_id, current_user_name, current_org_id, current_org_name = _fetch_current_user_context(
1337
1339
  runtime.base_url,
@@ -1422,6 +1424,7 @@ def _credit_subject(
1422
1424
  runtime.base_url,
1423
1425
  owner_user_id,
1424
1426
  force_refresh=force_refresh,
1427
+ workspace_id=runtime.workspace_id,
1425
1428
  )
1426
1429
  return subject_user_id, subject_org_id, token
1427
1430
 
@@ -31,6 +31,7 @@ class ObservabilityRuntime:
31
31
  base_url: str
32
32
  project_id: str
33
33
  token: str
34
+ explicit_user_token: bool = False
34
35
  workspace_id: str | None = None
35
36
  user_id: str | None = None
36
37
  project_type: str | None = None
@@ -289,7 +290,6 @@ def _observability_runtime(
289
290
  ) -> ObservabilityRuntime:
290
291
  resolved_base_url = normalize_base_url(base_url) if base_url else platform_base_url()
291
292
  owner_user_id = user_id or env("LICOS_USER_ID") or env("AGENT_USER_ID")
292
- token = (user_token or "").strip() or resolve_user_token(resolved_base_url, owner_user_id)
293
293
  resolved_project_id = project_id or env("LICOS_PROJECT_ID") or env("AGENT_PROJECT_ID")
294
294
  if not resolved_project_id:
295
295
  raise ConfigurationError("LICOS_PROJECT_ID or AGENT_PROJECT_ID is not configured")
@@ -300,6 +300,7 @@ def _observability_runtime(
300
300
  or env("LICOS_OWNER_WORKSPACE_ID")
301
301
  or env("AGENT_WORKSPACE_ID")
302
302
  )
303
+ token = (user_token or "").strip() or resolve_user_token(resolved_base_url, owner_user_id, workspace_id=resolved_workspace_id)
303
304
  resolved_project_type = (
304
305
  project_type or env("LICOS_PROJECT_TYPE") or env("AGENT_PROJECT_TYPE")
305
306
  )
@@ -325,7 +326,7 @@ def _observability_runtime(
325
326
 
326
327
 
327
328
  def _refresh_runtime(runtime: ObservabilityRuntime) -> ObservabilityRuntime:
328
- token = resolve_user_token(runtime.base_url, runtime.user_id, force_refresh=True)
329
+ token = resolve_user_token(runtime.base_url, runtime.user_id, force_refresh=True, workspace_id=runtime.workspace_id)
329
330
  return replace(runtime, token=token)
330
331
 
331
332
 
@@ -521,7 +521,7 @@ class ModelSdkTests(unittest.TestCase):
521
521
 
522
522
  self.assertEqual(result.text, "hello")
523
523
  self.assertEqual(captured["exchange_headers"]["Authorization"], "Bearer ai-agent-token")
524
- self.assertEqual(captured["exchange_body"], {"userId": "user-1"})
524
+ self.assertEqual(captured["exchange_body"], {"userId": "user-1", "workspaceId": "workspace-1"})
525
525
  self.assertEqual(captured["catalog_headers"]["Authorization"], "Bearer user-token")
526
526
  self.assertEqual(captured["detail_headers"]["Authorization"], "Bearer user-token")
527
527
  self.assertEqual(captured["chat_headers"]["Authorization"], "Bearer user-token")
@@ -1,150 +1,150 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import os
5
- import sys
6
- import unittest
7
- from pathlib import Path
8
- from typing import Any
9
- from unittest import mock
10
- from urllib import error as urlerror
11
-
12
- sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
13
- sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "licos-platform-sdk" / "src"))
14
-
15
- from licos_dev_sdk import observability
16
- from licos_platform_sdk import _runtime
17
-
18
-
19
- class _FakeResponse:
20
- status = 200
21
-
22
- def __init__(self, payload: dict[str, Any]) -> None:
23
- self._payload = payload
24
-
25
- def __enter__(self) -> "_FakeResponse":
26
- return self
27
-
28
- def __exit__(self, *_args: Any) -> None:
29
- return None
30
-
31
- def read(self) -> bytes:
32
- return json.dumps(self._payload).encode("utf-8")
33
-
34
-
35
- class _FakeErrorBody:
36
- def __init__(self, payload: dict[str, Any]) -> None:
37
- self._payload = payload
38
-
39
- def read(self) -> bytes:
40
- return json.dumps(self._payload).encode("utf-8")
41
-
42
-
43
- class ObservabilitySdkTests(unittest.TestCase):
44
- def setUp(self) -> None:
45
- self.env = mock.patch.dict(
46
- os.environ,
47
- {
48
- "LICOS_PLATFORM_API_BASE_URL": "http://platform.example/api/v1",
49
- "AGENT_USER_ID": "user-1",
50
- "AGENT_WORKSPACE_ID": "workspace-1",
51
- "AGENT_PROJECT_ID": "project-1",
52
- "AGENT_PROJECT_TYPE": "AGENT",
53
- "LICOS_AI_AGENT_TOKEN": "ai-agent-token",
54
- "LICOS_DEPLOYMENT_VERSION": "v1",
55
- "LICOS_DEPLOYMENT_HASH": "hash-1",
56
- },
57
- clear=True,
58
- )
59
- self.env.start()
60
- self.addCleanup(self.env.stop)
61
- _runtime._clear_token_cache_for_tests()
62
-
63
- def test_log_uses_runtime_context_and_owner_token(self) -> None:
64
- captured: dict[str, Any] = {}
65
-
66
- def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
67
- if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
68
- captured["exchange_headers"] = dict(req.header_items())
69
- captured["exchange_body"] = json.loads(req.data.decode("utf-8"))
70
- return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
71
- if req.full_url == "http://platform.example/api/v1/studio/observability/logs?projectId=project-1":
72
- captured["log_headers"] = dict(req.header_items())
73
- captured["log_body"] = json.loads(req.data.decode("utf-8"))
74
- return _FakeResponse({"code": 0, "success": True, "data": {"id": 1}})
75
- raise AssertionError(req.full_url)
76
-
77
- with mock.patch.object(observability.request, "urlopen", fake_urlopen):
78
- result = observability.log_info("started", log_time="2026-05-22T10:00:00+08:00")
79
-
80
- self.assertEqual(result, {"id": 1})
81
- self.assertEqual(captured["exchange_headers"]["Authorization"], "Bearer ai-agent-token")
82
- self.assertEqual(captured["exchange_body"], {"userId": "user-1"})
83
- self.assertEqual(captured["log_headers"]["Authorization"], "Bearer user-token")
84
- log_headers = {key.lower(): value for key, value in captured["log_headers"].items()}
85
- self.assertEqual(log_headers["x-workspace-id"], "workspace-1")
86
- self.assertEqual(captured["log_body"]["deploymentVersion"], "v1")
87
- self.assertEqual(captured["log_body"]["level"], "info")
88
- self.assertEqual(captured["log_body"]["logContent"], "started")
89
-
90
- def test_trace_metric_and_error_payloads(self) -> None:
91
- calls: list[tuple[str, dict[str, Any]]] = []
92
-
93
- def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
94
- if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
95
- return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
96
- calls.append((req.full_url, json.loads(req.data.decode("utf-8"))))
97
- return _FakeResponse({"code": 0, "success": True, "data": {"ok": True}})
98
-
99
- with mock.patch.object(observability.request, "urlopen", fake_urlopen):
100
- client = observability.ObservabilityClient()
101
- client.record_trace(trace_id="trace-1", input={"q": "hi"}, output="ok", input_tokens=3, output_tokens=4, latency_seconds=1.2)
102
- client.record_metric(route="/stream_run", method="post", latency_ms=640, status_code=500)
103
- client.record_error(route="/stream_run", method="post", error_code="MODEL_TIMEOUT", error_message="timeout", trace_id="trace-1")
104
-
105
- self.assertEqual(calls[0][0], "http://platform.example/api/v1/studio/observability/traces?projectId=project-1")
106
- self.assertEqual(calls[0][1]["traceId"], "trace-1")
107
- self.assertEqual(calls[0][1]["hashVersion"], "hash-1")
108
- self.assertEqual(calls[0][1]["input"], '{"q": "hi"}')
109
- self.assertEqual(calls[0][1]["inputTokens"], 3)
110
-
111
- self.assertEqual(calls[1][0], "http://platform.example/api/v1/studio/observability/analysis/metrics?projectId=project-1")
112
- self.assertEqual(calls[1][1]["workspaceId"], "workspace-1")
113
- self.assertEqual(calls[1][1]["projectType"], "AGENT")
114
- self.assertEqual(calls[1][1]["method"], "POST")
115
- self.assertEqual(calls[1][1]["errorCount"], 1)
116
- self.assertEqual(calls[1][1]["status5xxCount"], 1)
117
- self.assertEqual(calls[1][1]["latencyBuckets"], {"500+": 1})
118
-
119
- self.assertEqual(calls[2][0], "http://platform.example/api/v1/studio/observability/analysis/errors?projectId=project-1")
120
- self.assertEqual(calls[2][1]["errorCode"], "MODEL_TIMEOUT")
121
- self.assertEqual(calls[2][1]["traceId"], "trace-1")
122
-
123
- def test_refreshes_owner_token_once_after_unauthorized(self) -> None:
124
- tokens = iter(["old-token", "new-token"])
125
- log_tokens: list[str] = []
126
-
127
- def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
128
- if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
129
- return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": next(tokens)}})
130
- if req.full_url == "http://platform.example/api/v1/studio/observability/logs?projectId=project-1":
131
- log_tokens.append(dict(req.header_items())["Authorization"])
132
- if len(log_tokens) == 1:
133
- raise urlerror.HTTPError(
134
- req.full_url,
135
- 401,
136
- "Unauthorized",
137
- hdrs=None,
138
- fp=_FakeErrorBody({"code": 10002, "message": "token invalid or expired", "success": False}),
139
- )
140
- return _FakeResponse({"code": 0, "success": True, "data": {"id": 1}})
141
- raise AssertionError(req.full_url)
142
-
143
- with mock.patch.object(observability.request, "urlopen", fake_urlopen):
144
- observability.log_error("failed")
145
-
146
- self.assertEqual(log_tokens, ["Bearer old-token", "Bearer new-token"])
147
-
148
-
149
- if __name__ == "__main__":
150
- unittest.main()
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import unittest
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from unittest import mock
10
+ from urllib import error as urlerror
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
13
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "licos-platform-sdk" / "src"))
14
+
15
+ from licos_dev_sdk import observability
16
+ from licos_platform_sdk import _runtime
17
+
18
+
19
+ class _FakeResponse:
20
+ status = 200
21
+
22
+ def __init__(self, payload: dict[str, Any]) -> None:
23
+ self._payload = payload
24
+
25
+ def __enter__(self) -> "_FakeResponse":
26
+ return self
27
+
28
+ def __exit__(self, *_args: Any) -> None:
29
+ return None
30
+
31
+ def read(self) -> bytes:
32
+ return json.dumps(self._payload).encode("utf-8")
33
+
34
+
35
+ class _FakeErrorBody:
36
+ def __init__(self, payload: dict[str, Any]) -> None:
37
+ self._payload = payload
38
+
39
+ def read(self) -> bytes:
40
+ return json.dumps(self._payload).encode("utf-8")
41
+
42
+
43
+ class ObservabilitySdkTests(unittest.TestCase):
44
+ def setUp(self) -> None:
45
+ self.env = mock.patch.dict(
46
+ os.environ,
47
+ {
48
+ "LICOS_PLATFORM_API_BASE_URL": "http://platform.example/api/v1",
49
+ "AGENT_USER_ID": "user-1",
50
+ "AGENT_WORKSPACE_ID": "workspace-1",
51
+ "AGENT_PROJECT_ID": "project-1",
52
+ "AGENT_PROJECT_TYPE": "AGENT",
53
+ "LICOS_AI_AGENT_TOKEN": "ai-agent-token",
54
+ "LICOS_DEPLOYMENT_VERSION": "v1",
55
+ "LICOS_DEPLOYMENT_HASH": "hash-1",
56
+ },
57
+ clear=True,
58
+ )
59
+ self.env.start()
60
+ self.addCleanup(self.env.stop)
61
+ _runtime._clear_token_cache_for_tests()
62
+
63
+ def test_log_uses_runtime_context_and_owner_token(self) -> None:
64
+ captured: dict[str, Any] = {}
65
+
66
+ def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
67
+ if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
68
+ captured["exchange_headers"] = dict(req.header_items())
69
+ captured["exchange_body"] = json.loads(req.data.decode("utf-8"))
70
+ return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
71
+ if req.full_url == "http://platform.example/api/v1/studio/observability/logs?projectId=project-1":
72
+ captured["log_headers"] = dict(req.header_items())
73
+ captured["log_body"] = json.loads(req.data.decode("utf-8"))
74
+ return _FakeResponse({"code": 0, "success": True, "data": {"id": 1}})
75
+ raise AssertionError(req.full_url)
76
+
77
+ with mock.patch.object(observability.request, "urlopen", fake_urlopen):
78
+ result = observability.log_info("started", log_time="2026-05-22T10:00:00+08:00")
79
+
80
+ self.assertEqual(result, {"id": 1})
81
+ self.assertEqual(captured["exchange_headers"]["Authorization"], "Bearer ai-agent-token")
82
+ self.assertEqual(captured["exchange_body"], {"userId": "user-1", "workspaceId": "workspace-1"})
83
+ self.assertEqual(captured["log_headers"]["Authorization"], "Bearer user-token")
84
+ log_headers = {key.lower(): value for key, value in captured["log_headers"].items()}
85
+ self.assertEqual(log_headers["x-workspace-id"], "workspace-1")
86
+ self.assertEqual(captured["log_body"]["deploymentVersion"], "v1")
87
+ self.assertEqual(captured["log_body"]["level"], "info")
88
+ self.assertEqual(captured["log_body"]["logContent"], "started")
89
+
90
+ def test_trace_metric_and_error_payloads(self) -> None:
91
+ calls: list[tuple[str, dict[str, Any]]] = []
92
+
93
+ def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
94
+ if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
95
+ return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": "user-token"}})
96
+ calls.append((req.full_url, json.loads(req.data.decode("utf-8"))))
97
+ return _FakeResponse({"code": 0, "success": True, "data": {"ok": True}})
98
+
99
+ with mock.patch.object(observability.request, "urlopen", fake_urlopen):
100
+ client = observability.ObservabilityClient()
101
+ client.record_trace(trace_id="trace-1", input={"q": "hi"}, output="ok", input_tokens=3, output_tokens=4, latency_seconds=1.2)
102
+ client.record_metric(route="/stream_run", method="post", latency_ms=640, status_code=500)
103
+ client.record_error(route="/stream_run", method="post", error_code="MODEL_TIMEOUT", error_message="timeout", trace_id="trace-1")
104
+
105
+ self.assertEqual(calls[0][0], "http://platform.example/api/v1/studio/observability/traces?projectId=project-1")
106
+ self.assertEqual(calls[0][1]["traceId"], "trace-1")
107
+ self.assertEqual(calls[0][1]["hashVersion"], "hash-1")
108
+ self.assertEqual(calls[0][1]["input"], '{"q": "hi"}')
109
+ self.assertEqual(calls[0][1]["inputTokens"], 3)
110
+
111
+ self.assertEqual(calls[1][0], "http://platform.example/api/v1/studio/observability/analysis/metrics?projectId=project-1")
112
+ self.assertEqual(calls[1][1]["workspaceId"], "workspace-1")
113
+ self.assertEqual(calls[1][1]["projectType"], "AGENT")
114
+ self.assertEqual(calls[1][1]["method"], "POST")
115
+ self.assertEqual(calls[1][1]["errorCount"], 1)
116
+ self.assertEqual(calls[1][1]["status5xxCount"], 1)
117
+ self.assertEqual(calls[1][1]["latencyBuckets"], {"500+": 1})
118
+
119
+ self.assertEqual(calls[2][0], "http://platform.example/api/v1/studio/observability/analysis/errors?projectId=project-1")
120
+ self.assertEqual(calls[2][1]["errorCode"], "MODEL_TIMEOUT")
121
+ self.assertEqual(calls[2][1]["traceId"], "trace-1")
122
+
123
+ def test_refreshes_owner_token_once_after_unauthorized(self) -> None:
124
+ tokens = iter(["old-token", "new-token"])
125
+ log_tokens: list[str] = []
126
+
127
+ def fake_urlopen(req: Any, timeout: int = 0) -> _FakeResponse:
128
+ if req.full_url == "http://platform.example/api/v1/internal/auth/ai-user-token":
129
+ return _FakeResponse({"code": 0, "success": True, "data": {"accessToken": next(tokens)}})
130
+ if req.full_url == "http://platform.example/api/v1/studio/observability/logs?projectId=project-1":
131
+ log_tokens.append(dict(req.header_items())["Authorization"])
132
+ if len(log_tokens) == 1:
133
+ raise urlerror.HTTPError(
134
+ req.full_url,
135
+ 401,
136
+ "Unauthorized",
137
+ hdrs=None,
138
+ fp=_FakeErrorBody({"code": 10002, "message": "token invalid or expired", "success": False}),
139
+ )
140
+ return _FakeResponse({"code": 0, "success": True, "data": {"id": 1}})
141
+ raise AssertionError(req.full_url)
142
+
143
+ with mock.patch.object(observability.request, "urlopen", fake_urlopen):
144
+ observability.log_error("failed")
145
+
146
+ self.assertEqual(log_tokens, ["Bearer old-token", "Bearer new-token"])
147
+
148
+
149
+ if __name__ == "__main__":
150
+ unittest.main()
@@ -17,8 +17,8 @@ class ModelIdentityTests(TestCase):
17
17
  self.assertEqual(runtime.token, "renewed")
18
18
  self.assertEqual(runtime.current_user_id, "owner")
19
19
  self.assertEqual(exchange.call_args_list, [
20
- mock.call("https://platform.example", "owner"),
21
- mock.call("https://platform.example", "owner", force_refresh=True),
20
+ mock.call("https://platform.example", "owner", workspace_id=None),
21
+ mock.call("https://platform.example", "owner", force_refresh=True, workspace_id=None),
22
22
  ])
23
23
 
24
24
  def test_business_authentication_error_is_not_a_user_profile(self):
File without changes
File without changes
File without changes