licos-dev-sdk 0.4.5__tar.gz → 0.4.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 (51) hide show
  1. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/PKG-INFO +1 -1
  2. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/pyproject.toml +1 -1
  3. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/model.py +13 -3
  4. licos_dev_sdk-0.4.6/tests/test_langchain_protocol.py +154 -0
  5. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/.gitignore +0 -0
  6. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/DOCX.md +0 -0
  7. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/__init__.py +0 -0
  8. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/_utils.py +0 -0
  9. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/archive.py +0 -0
  10. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/chart.py +0 -0
  11. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/data.py +0 -0
  12. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/diagram.py +0 -0
  13. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document.py +0 -0
  14. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_common.py +0 -0
  15. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx.py +0 -0
  16. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_edit.py +0 -0
  17. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_expectations.py +0 -0
  18. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_inspect.py +0 -0
  19. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_layout.py +0 -0
  20. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_package.py +0 -0
  21. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_preservation.py +0 -0
  22. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_structure.py +0 -0
  23. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_table.py +0 -0
  24. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_docx_table_actions.py +0 -0
  25. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_read.py +0 -0
  26. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/document_table_export.py +0 -0
  27. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/image.py +0 -0
  28. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/observability.py +0 -0
  29. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/pdf_inspect.py +0 -0
  30. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
  31. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation.py +0 -0
  32. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation_canvas.py +0 -0
  33. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/presentation_inspect.py +0 -0
  34. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet.py +0 -0
  35. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
  36. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
  37. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
  38. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/src/licos_dev_sdk/web.py +0 -0
  39. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_document_read.py +0 -0
  40. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_document_spreadsheet.py +0 -0
  41. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_document_table_export.py +0 -0
  42. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_docx_inspect.py +0 -0
  43. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_docx_structure.py +0 -0
  44. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_docx_table_edit.py +0 -0
  45. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_html_docx_features.py +0 -0
  46. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_model.py +0 -0
  47. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_observability.py +0 -0
  48. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_output_paths.py +0 -0
  49. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_ppt_master_runtime.py +0 -0
  50. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_presentation.py +0 -0
  51. {licos_dev_sdk-0.4.5 → licos_dev_sdk-0.4.6}/tests/test_spreadsheet_html_edit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: licos-dev-sdk
3
- Version: 0.4.5
3
+ Version: 0.4.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-dev-sdk"
7
- version = "0.4.5"
7
+ version = "0.4.6"
8
8
  description = "LICOS Dev SDK - file generation and model capability clients"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -482,6 +482,7 @@ def create_chat_openai(
482
482
  max_completion_tokens: int | None = None,
483
483
  timeout: int | None = None,
484
484
  streaming: bool = True,
485
+ use_responses_api: bool | None = None,
485
486
  default_headers: dict[str, str] | None = None,
486
487
  **kwargs: Any,
487
488
  ) -> Any:
@@ -490,6 +491,8 @@ def create_chat_openai(
490
491
  `model=None`, an empty string, or `model="auto"` uses the catalog default. The
491
492
  returned ChatOpenAI receives the current user token as its OpenAI-compatible API
492
493
  key, so deployed projects do not need LLM_MODEL/OPENAI_MODEL env injection.
494
+ A catalog `response_url` selects Responses by default. Set
495
+ `use_responses_api=False` to use Chat Completions explicitly.
493
496
  """
494
497
  try:
495
498
  from langchain_core.callbacks import BaseCallbackHandler
@@ -500,6 +503,11 @@ def create_chat_openai(
500
503
 
501
504
  runtime = _model_runtime(base_url=base_url, user_token=user_token, user_id=user_id, workspace_id=workspace_id)
502
505
  endpoint = _resolve_chat_endpoint(runtime, model_group=model_group, requested_model=model)
506
+ if use_responses_api is None:
507
+ use_responses_api = bool(endpoint.response_url)
508
+ if use_responses_api and not endpoint.response_url:
509
+ raise ConfigurationError(f"Model {endpoint.model!r} has no response_url configured")
510
+ endpoint_url = endpoint.response_url if use_responses_api else endpoint.base_url
503
511
  headers = _without_authorization(
504
512
  _merge_headers(
505
513
  endpoint.request_headers or endpoint.required_headers,
@@ -540,7 +548,8 @@ def create_chat_openai(
540
548
  )
541
549
  chat_kwargs: dict[str, Any] = {
542
550
  "model": endpoint.model,
543
- "base_url": _chat_completions_sdk_base_url(endpoint.base_url),
551
+ "base_url": _openai_sdk_base_url(endpoint_url, use_responses_api=use_responses_api),
552
+ "use_responses_api": use_responses_api,
544
553
  "api_key": api_key or runtime.token,
545
554
  "streaming": streaming,
546
555
  **_not_none(
@@ -622,9 +631,10 @@ def _LangChainUsageHandler(
622
631
  return UsageHandler()
623
632
 
624
633
 
625
- def _chat_completions_sdk_base_url(url: str) -> str:
634
+ def _openai_sdk_base_url(url: str, *, use_responses_api: bool) -> str:
626
635
  trimmed = str(url or "").strip().rstrip("/")
627
- return trimmed.removesuffix("/chat/completions").rstrip("/")
636
+ suffix = "/responses" if use_responses_api else "/chat/completions"
637
+ return trimmed.removesuffix(suffix).rstrip("/")
628
638
 
629
639
 
630
640
  def resolve_vision_endpoint(
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from pathlib import Path
6
+ import sys
7
+ import unittest
8
+ from unittest.mock import patch
9
+
10
+ import httpx
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 model
16
+ from licos_platform_sdk._runtime import ConfigurationError
17
+
18
+
19
+ CHAT_URL = "https://chat.example/compatible/v1/chat/completions"
20
+ RESPONSES_URL = "https://responses.example/api/v1/responses"
21
+
22
+
23
+ class LangChainProtocolTests(unittest.TestCase):
24
+ def setUp(self):
25
+ runtime = model.ModelRuntime(base_url="https://platform.example", token="user-token")
26
+ self.runtime_patch = patch.object(model, "_model_runtime", return_value=runtime)
27
+ self.usage_patch = patch.object(model, "_enqueue_model_usage")
28
+ self.runtime_patch.start()
29
+ self.usage = self.usage_patch.start()
30
+ self.addCleanup(self.runtime_patch.stop)
31
+ self.addCleanup(self.usage_patch.stop)
32
+
33
+ def endpoint(self, response_url):
34
+ return model.ModelEndpoint(
35
+ provider="test", capability="chat", model="test-model", base_url=CHAT_URL,
36
+ response_url=response_url, request_headers={"X-Provider-Route": "route-1"},
37
+ )
38
+
39
+ def response(self, request):
40
+ if str(request.url) == RESPONSES_URL:
41
+ body = {
42
+ "id": "resp_1", "object": "response", "created_at": 1,
43
+ "model": "test-model", "status": "completed", "error": None,
44
+ "output": [{"id": "msg_1", "type": "message", "role": "assistant", "status": "completed",
45
+ "content": [{"type": "output_text", "text": "answer", "annotations": []}]}],
46
+ "usage": {"input_tokens": 3, "output_tokens": 2, "total_tokens": 5},
47
+ }
48
+ else:
49
+ self.assertEqual(str(request.url), CHAT_URL)
50
+ body = {
51
+ "id": "chat_1", "object": "chat.completion", "created": 1, "model": "test-model",
52
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": "answer"}, "finish_reason": "stop"}],
53
+ "usage": {"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5},
54
+ }
55
+ return httpx.Response(200, json=body)
56
+
57
+ def test_catalog_selects_actual_protocol_and_address(self):
58
+ for response_url, override, expected in [
59
+ (RESPONSES_URL, None, RESPONSES_URL),
60
+ (None, None, CHAT_URL),
61
+ (RESPONSES_URL, False, CHAT_URL),
62
+ (RESPONSES_URL, True, RESPONSES_URL),
63
+ ]:
64
+ for asynchronous in (False, True):
65
+ with self.subTest(response_url=response_url, override=override, asynchronous=asynchronous):
66
+ requests = []
67
+
68
+ def handle(request):
69
+ requests.append(request)
70
+ return self.response(request)
71
+
72
+ transport = httpx.MockTransport(handle)
73
+ sync_client = httpx.Client(transport=transport)
74
+ async_client = httpx.AsyncClient(transport=transport)
75
+ try:
76
+ with patch.object(model, "_resolve_chat_endpoint", return_value=self.endpoint(response_url)):
77
+ options = {} if override is None else {"use_responses_api": override}
78
+ chat = model.create_chat_openai(streaming=False, http_client=sync_client,
79
+ http_async_client=async_client, **options)
80
+ result = asyncio.run(chat.ainvoke("question")) if asynchronous else chat.invoke("question")
81
+ self.assertEqual(len(requests), 1)
82
+ self.assertEqual(str(requests[0].url), expected)
83
+ payload = json.loads(requests[0].content)
84
+ self.assertIn("input" if expected == RESPONSES_URL else "messages", payload)
85
+ self.assertEqual(requests[0].headers["Authorization"], "Bearer user-token")
86
+ self.assertEqual(requests[0].headers["X-Provider-Route"], "route-1")
87
+ self.assertTrue(requests[0].headers[model.MODEL_REQUEST_ID_HEADER])
88
+ self.assertEqual(result.text, "answer")
89
+ self.assertEqual(result.usage_metadata["total_tokens"], 5)
90
+ self.assertEqual(chat.use_responses_api, expected == RESPONSES_URL)
91
+ finally:
92
+ sync_client.close()
93
+ asyncio.run(async_client.aclose())
94
+
95
+ def test_explicit_responses_requires_catalog_address(self):
96
+ with patch.object(model, "_resolve_chat_endpoint", return_value=self.endpoint(None)):
97
+ with self.assertRaisesRegex(ConfigurationError, "response_url"):
98
+ model.create_chat_openai(use_responses_api=True)
99
+
100
+ def test_streaming_uses_selected_endpoint_and_keeps_usage(self):
101
+ for response_url in (RESPONSES_URL, None):
102
+ for asynchronous in (False, True):
103
+ with self.subTest(response_url=response_url, asynchronous=asynchronous):
104
+ requests = []
105
+
106
+ def handle(request):
107
+ requests.append(request)
108
+ body = self.response(request).json()
109
+ if response_url:
110
+ events = [
111
+ {"type": "response.created", "response": {**body, "status": "in_progress", "output": []}},
112
+ {"type": "response.output_text.delta", "output_index": 0, "content_index": 0,
113
+ "item_id": "msg_1", "delta": "answer"},
114
+ {"type": "response.output_text.done", "output_index": 0, "content_index": 0,
115
+ "item_id": "msg_1", "text": "answer"},
116
+ {"type": "response.completed", "response": body},
117
+ ]
118
+ text = "".join(f"event: {event['type']}\ndata: {json.dumps({**event, 'sequence_number': i})}\n\n"
119
+ for i, event in enumerate(events))
120
+ else:
121
+ base = {"id": "chat_1", "object": "chat.completion.chunk", "created": 1, "model": "test-model"}
122
+ events = [
123
+ {**base, "choices": [{"index": 0, "delta": {"role": "assistant", "content": "answer"}, "finish_reason": None}]},
124
+ {**base, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
125
+ {**base, "choices": [], "usage": body["usage"]},
126
+ ]
127
+ text = "".join(f"data: {json.dumps(event)}\n\n" for event in events) + "data: [DONE]\n\n"
128
+ return httpx.Response(200, headers={"Content-Type": "text/event-stream"}, content=text)
129
+
130
+ transport = httpx.MockTransport(handle)
131
+ sync_client = httpx.Client(transport=transport)
132
+ async_client = httpx.AsyncClient(transport=transport)
133
+ try:
134
+ with patch.object(model, "_resolve_chat_endpoint", return_value=self.endpoint(response_url)):
135
+ chat = model.create_chat_openai(http_client=sync_client, http_async_client=async_client)
136
+
137
+ async def collect():
138
+ return [chunk async for chunk in chat.astream("question")]
139
+
140
+ chunks = asyncio.run(collect()) if asynchronous else list(chat.stream("question"))
141
+ combined = chunks[0]
142
+ for chunk in chunks[1:]:
143
+ combined += chunk
144
+ self.assertEqual(str(requests[0].url), response_url or CHAT_URL)
145
+ self.assertTrue(json.loads(requests[0].content)["stream"])
146
+ self.assertEqual(combined.text, "answer")
147
+ self.assertEqual(combined.usage_metadata["total_tokens"], 5)
148
+ finally:
149
+ sync_client.close()
150
+ asyncio.run(async_client.aclose())
151
+
152
+
153
+ if __name__ == "__main__":
154
+ unittest.main()
File without changes
File without changes