unique_orchestrator 2026.22.0.dev6__tar.gz → 2026.22.0.dev7__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 (31) hide show
  1. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/PKG-INFO +3 -4
  2. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/pyproject.toml +3 -4
  3. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/_builders/skill_setup.py +18 -82
  4. unique_orchestrator-2026.22.0.dev7/unique_orchestrator/tests/test_utils_resolve_other_options.py +261 -0
  5. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/unique_ai.py +4 -1
  6. unique_orchestrator-2026.22.0.dev7/unique_orchestrator/utils.py +133 -0
  7. unique_orchestrator-2026.22.0.dev6/unique_orchestrator/utils.py +0 -26
  8. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/README.md +0 -0
  9. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/__init__.py +0 -0
  10. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/_builders/__init__.py +0 -0
  11. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
  12. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
  13. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/_builders/open_file_setup.py +0 -0
  14. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/config.py +0 -0
  15. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
  16. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
  17. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
  18. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/settings.py +0 -0
  19. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
  20. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_config_services_validators.py +0 -0
  21. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
  22. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
  23. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
  24. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
  25. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
  26. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
  27. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
  28. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -0
  29. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
  30. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
  31. {unique_orchestrator-2026.22.0.dev6 → unique_orchestrator-2026.22.0.dev7}/unique_orchestrator/unique_ai_builder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-orchestrator
3
- Version: 2026.22.0.dev6
3
+ Version: 2026.22.0.dev7
4
4
  Summary:
5
5
  Author: Andreas Hauri
6
6
  Author-email: Andreas Hauri <andreas.hauri@unique.ai>
@@ -9,16 +9,15 @@ Requires-Dist: pydantic>=2.8.2,<3
9
9
  Requires-Dist: pydantic-settings>=2.10.1,<3
10
10
  Requires-Dist: typing-extensions>=4.9.0,<5
11
11
  Requires-Dist: jinja2>=3.1.0,<4
12
- Requires-Dist: unique-toolkit>=2026.22.0.dev7,<2026.22.0rc0
12
+ Requires-Dist: unique-toolkit>=2026.22.0.dev11,<2026.22.0rc0
13
13
  Requires-Dist: unique-stock-ticker>=2026.22.0.dev2,<2026.22.0rc0
14
14
  Requires-Dist: unique-follow-up-questions>=2026.22.0.dev2,<2026.22.0rc0
15
15
  Requires-Dist: unique-internal-search>=2026.22.0.dev3,<2026.22.0rc0
16
16
  Requires-Dist: unique-deep-research>=2026.22.0.dev2,<2026.22.0rc0
17
17
  Requires-Dist: unique-web-search>=2026.22.0.dev4,<2026.22.0rc0
18
18
  Requires-Dist: unique-swot>=2026.22.0.dev2,<2026.22.0rc0
19
- Requires-Dist: unique-skill-tool>=2026.22.0.dev4,<2026.22.0rc0
19
+ Requires-Dist: unique-skill-tool>=2026.22.0.dev6,<2026.22.0rc0
20
20
  Requires-Dist: openai>=1.109.1,<3
21
- Requires-Dist: python-frontmatter>=1.1.0,<2
22
21
  Requires-Python: >=3.12, <4
23
22
  Description-Content-Type: text/markdown
24
23
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_orchestrator"
3
- version = "2026.22.0.dev6"
3
+ version = "2026.22.0.dev7"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "Proprietary" }
@@ -13,16 +13,15 @@ dependencies = [
13
13
  "pydantic-settings>=2.10.1,<3",
14
14
  "typing-extensions>=4.9.0,<5",
15
15
  "jinja2>=3.1.0,<4",
16
- "unique-toolkit>=2026.22.0.dev7,<2026.22.0rc0",
16
+ "unique-toolkit>=2026.22.0.dev11,<2026.22.0rc0",
17
17
  "unique-stock-ticker>=2026.22.0.dev2,<2026.22.0rc0",
18
18
  "unique-follow-up-questions>=2026.22.0.dev2,<2026.22.0rc0",
19
19
  "unique-internal-search>=2026.22.0.dev3,<2026.22.0rc0",
20
20
  "unique-deep-research>=2026.22.0.dev2,<2026.22.0rc0",
21
21
  "unique-web-search>=2026.22.0.dev4,<2026.22.0rc0",
22
22
  "unique-swot>=2026.22.0.dev2,<2026.22.0rc0",
23
- "unique-skill-tool>=2026.22.0.dev4,<2026.22.0rc0",
23
+ "unique-skill-tool>=2026.22.0.dev6,<2026.22.0rc0",
24
24
  "openai>=1.109.1,<3",
25
- "python-frontmatter>=1.1.0,<2",
26
25
  ]
27
26
 
28
27
  [build-system]
@@ -43,11 +43,10 @@ from __future__ import annotations
43
43
 
44
44
  import asyncio
45
45
  from logging import Logger
46
- from typing import TYPE_CHECKING, Any
46
+ from typing import TYPE_CHECKING
47
47
 
48
- import frontmatter
49
- from pydantic import ValidationError
50
48
  from unique_skill_tool.config import SkillToolConfig
49
+ from unique_skill_tool.loader import parse_skill_file
51
50
  from unique_skill_tool.schemas import SkillDefinition
52
51
  from unique_skill_tool.service import SkillTool
53
52
  from unique_toolkit.agentic.tools.config import ToolBuildConfig
@@ -76,29 +75,6 @@ def _find_skill_tool_build_config(
76
75
  return None
77
76
 
78
77
 
79
- def _parse_frontmatter(*, text: str) -> tuple[dict[str, Any], str]:
80
- """Split YAML frontmatter from the markdown body.
81
-
82
- Thin wrapper around ``python-frontmatter``. On a YAML parse error,
83
- or when the frontmatter parses to a non-mapping (e.g. a top-level
84
- YAML list), we fall back to ``({}, <stripped body>)`` so a broken
85
- skill file never leaks its raw ``---\\nname: ...\\n---`` block into
86
- the LLM prompt, but also never crashes ``_build_skill`` with an
87
- unhandled ``TypeError``/``ValueError`` from ``dict(...)``.
88
- """
89
- try:
90
- post = frontmatter.loads(text)
91
- metadata = post.metadata
92
- body = post.content
93
- except Exception:
94
- return {}, text
95
-
96
- if not isinstance(metadata, dict):
97
- return {}, body
98
-
99
- return dict(metadata), body
100
-
101
-
102
78
  def normalize_available_skills_for_tool(
103
79
  available_skills: list[SkillReference],
104
80
  ) -> list[SkillReference]:
@@ -125,56 +101,6 @@ def normalize_available_skills_for_tool(
125
101
  return out
126
102
 
127
103
 
128
- def _build_skill(
129
- *,
130
- content_id: str,
131
- content_key: str,
132
- file_text: str,
133
- logger: Logger,
134
- ) -> SkillDefinition | None:
135
- """Build a ``SkillDefinition`` from the raw file text of a KB document.
136
-
137
- Parses YAML frontmatter for ``name`` and ``description``. A malformed
138
- ``name`` (non-kebab-case, contains whitespace/punctuation, too long)
139
- is rejected by ``SkillDefinition`` rather than silently flowing into
140
- the OpenAI tool enum where the model could never emit it verbatim.
141
-
142
- ``content_id`` is stored on the returned definition so it can be used
143
- as a lookup key to load the SKILL.md.
144
- """
145
- if not file_text.strip():
146
- return None
147
-
148
- metadata, body = _parse_frontmatter(text=file_text)
149
-
150
- name = metadata.get("name")
151
- description = metadata.get("description")
152
-
153
- if not name or not description:
154
- logger.warning(
155
- "Skipping '%s' (%s): wrong skill format.",
156
- content_key,
157
- content_id,
158
- )
159
- return None
160
-
161
- try:
162
- return SkillDefinition(
163
- name=name,
164
- description=description,
165
- content=body,
166
- content_id=content_id,
167
- )
168
- except ValidationError as exc:
169
- logger.warning(
170
- "Skipping '%s' (%s): invalid skill definition: %s",
171
- content_key,
172
- content_id,
173
- exc.errors(include_url=False),
174
- )
175
- return None
176
-
177
-
178
104
  async def load_selectable_skills(
179
105
  *,
180
106
  content_service: ContentService,
@@ -224,12 +150,22 @@ async def load_selectable_skills(
224
150
  )
225
151
  continue
226
152
 
227
- skill = _build_skill(
228
- content_id=entry.content_id,
229
- content_key=label,
230
- file_text=file_text,
231
- logger=logger,
232
- )
153
+ try:
154
+ skill = parse_skill_file(
155
+ file_text=file_text,
156
+ content_id=entry.content_id,
157
+ source_label=label,
158
+ logger=logger,
159
+ )
160
+ except Exception:
161
+ logger.warning(
162
+ "Unexpected error building selectable skill '%s' (%s) — skipping.",
163
+ label,
164
+ entry.content_id,
165
+ exc_info=True,
166
+ )
167
+ continue
168
+
233
169
  if skill is None:
234
170
  logger.debug(
235
171
  "Skipping selectable skill '%s' (%s): empty or invalid file.",
@@ -0,0 +1,261 @@
1
+ """Tests for utils.resolve_other_options().
2
+
3
+ Verifies that the highest effort level is selected across
4
+ ``additional_llm_options`` from config and the ``thinking_level`` hints
5
+ declared on skills activated in the current run, and that the effort is
6
+ written in the format required by the active API.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from unittest.mock import MagicMock
12
+
13
+ from unique_skill_tool.service import SkillTool
14
+
15
+ from unique_orchestrator.utils import resolve_other_options
16
+
17
+
18
+ def _make_config(
19
+ additional_llm_options: dict,
20
+ use_responses_api: bool = False,
21
+ supported_reasoning_efforts: list[str] | None = None,
22
+ ) -> MagicMock:
23
+ config = MagicMock()
24
+ config.agent.experimental.additional_llm_options = additional_llm_options
25
+ config.agent.experimental.responses_api_config.use_responses_api = use_responses_api
26
+ config.agent.experimental.use_responses_api = use_responses_api
27
+ config.space.language_model.supported_reasoning_efforts = (
28
+ supported_reasoning_efforts
29
+ )
30
+ return config
31
+
32
+
33
+ def _make_tool_manager(skill_tool: SkillTool | None = None) -> MagicMock:
34
+ tool_manager = MagicMock()
35
+ tool_manager.get_tool_by_name.return_value = skill_tool
36
+ return tool_manager
37
+
38
+
39
+ def _make_skill_tool_with_max(max_level: str | None) -> SkillTool:
40
+ tool = MagicMock(spec=SkillTool)
41
+ tool.max_thinking_level = max_level
42
+ return tool
43
+
44
+
45
+ def _call(
46
+ additional_llm_options: dict,
47
+ skill_tool: SkillTool | None = None,
48
+ use_responses_api: bool = False,
49
+ supported_reasoning_efforts: list[str] | None = None,
50
+ ) -> dict:
51
+ return resolve_other_options(
52
+ config=_make_config(
53
+ additional_llm_options, use_responses_api, supported_reasoning_efforts
54
+ ),
55
+ tool_manager=_make_tool_manager(skill_tool),
56
+ logger=MagicMock(),
57
+ )
58
+
59
+
60
+ class TestResolveOtherOptions:
61
+ def test_returns_config_options_unchanged_when_no_skill_tool(self) -> None:
62
+ result = _call({"temperature": 0.5})
63
+ assert result == {"temperature": 0.5}
64
+
65
+ def test_returns_config_options_unchanged_when_skill_tool_has_no_hint(self) -> None:
66
+ skill_tool = _make_skill_tool_with_max(None)
67
+ result = _call({"temperature": 0.5}, skill_tool=skill_tool)
68
+ assert result == {"temperature": 0.5}
69
+
70
+ def test_injects_skill_hint_when_config_has_no_reasoning_effort(self) -> None:
71
+ skill_tool = _make_skill_tool_with_max("high")
72
+ result = _call({}, skill_tool=skill_tool)
73
+ assert result["reasoning_effort"] == "high"
74
+
75
+ def test_config_effort_wins_when_higher_than_skill(self) -> None:
76
+ skill_tool = _make_skill_tool_with_max("low")
77
+ result = _call({"reasoning_effort": "high"}, skill_tool=skill_tool)
78
+ assert result["reasoning_effort"] == "high"
79
+
80
+ def test_skill_hint_wins_when_higher_than_config(self) -> None:
81
+ skill_tool = _make_skill_tool_with_max("high")
82
+ result = _call({"reasoning_effort": "low"}, skill_tool=skill_tool)
83
+ assert result["reasoning_effort"] == "high"
84
+
85
+ def test_equal_levels_preserved(self) -> None:
86
+ skill_tool = _make_skill_tool_with_max("medium")
87
+ result = _call({"reasoning_effort": "medium"}, skill_tool=skill_tool)
88
+ assert result["reasoning_effort"] == "medium"
89
+
90
+ def test_other_options_keys_passed_through_unchanged(self) -> None:
91
+ skill_tool = _make_skill_tool_with_max("high")
92
+ result = _call(
93
+ {"reasoning_effort": "low", "responseFormat": {"type": "json_object"}},
94
+ skill_tool=skill_tool,
95
+ )
96
+ assert result["reasoning_effort"] == "high"
97
+ assert result["responseFormat"] == {"type": "json_object"}
98
+
99
+ def test_does_not_mutate_config_dict(self) -> None:
100
+ original = {"reasoning_effort": "low"}
101
+ skill_tool = _make_skill_tool_with_max("high")
102
+ _call(original, skill_tool=skill_tool)
103
+ assert original == {"reasoning_effort": "low"}
104
+
105
+ # ── Responses API ─────────────────────────────────────────────────────────
106
+
107
+ def test_reads_effort_from_nested_reasoning_dict_for_responses_api(self) -> None:
108
+ skill_tool = _make_skill_tool_with_max("high")
109
+ result = _call(
110
+ {"reasoning": {"effort": "low"}},
111
+ skill_tool=skill_tool,
112
+ use_responses_api=True,
113
+ )
114
+ assert result["reasoning"] == {"effort": "high"}
115
+ assert "reasoning_effort" not in result
116
+
117
+ def test_nested_effort_config_wins_when_higher_than_skill(self) -> None:
118
+ skill_tool = _make_skill_tool_with_max("low")
119
+ result = _call(
120
+ {"reasoning": {"effort": "high"}},
121
+ skill_tool=skill_tool,
122
+ use_responses_api=True,
123
+ )
124
+ assert result["reasoning"] == {"effort": "high"}
125
+
126
+ def test_skill_hint_injected_when_no_nested_effort_config(self) -> None:
127
+ skill_tool = _make_skill_tool_with_max("high")
128
+ result = _call({}, skill_tool=skill_tool, use_responses_api=True)
129
+ assert result["reasoning"] == {"effort": "high"}
130
+ assert "reasoning_effort" not in result
131
+
132
+ def test_flat_reasoning_effort_ignored_when_responses_api_active(self) -> None:
133
+ """``reasoning_effort`` (flat) is in the wrong format for the responses API
134
+ and must not be picked up as the config effort value."""
135
+ skill_tool = _make_skill_tool_with_max("high")
136
+ result = _call(
137
+ {"reasoning_effort": "low"},
138
+ skill_tool=skill_tool,
139
+ use_responses_api=True,
140
+ )
141
+ assert result["reasoning"] == {"effort": "high"}
142
+ assert result.get("reasoning_effort") == "low"
143
+
144
+ def test_extra_reasoning_subkeys_preserved_for_responses_api(self) -> None:
145
+ """Other keys inside ``reasoning`` (e.g. ``summary``) must survive."""
146
+ skill_tool = _make_skill_tool_with_max("high")
147
+ result = _call(
148
+ {"reasoning": {"effort": "low", "summary": "auto"}},
149
+ skill_tool=skill_tool,
150
+ use_responses_api=True,
151
+ )
152
+ assert result["reasoning"] == {"effort": "high", "summary": "auto"}
153
+
154
+ def test_json_string_reasoning_is_parsed_for_responses_api(self) -> None:
155
+ """``reasoning`` stored as a JSON string must be parsed before use."""
156
+ skill_tool = _make_skill_tool_with_max("high")
157
+ result = _call(
158
+ {"reasoning": '{"effort": "low"}'},
159
+ skill_tool=skill_tool,
160
+ use_responses_api=True,
161
+ )
162
+ assert result["reasoning"] == {"effort": "high"}
163
+
164
+ def test_invalid_json_string_reasoning_treated_as_empty_for_responses_api(
165
+ self,
166
+ ) -> None:
167
+ """An unparseable string must not crash; skill max is used as effort."""
168
+ skill_tool = _make_skill_tool_with_max("medium")
169
+ result = _call(
170
+ {"reasoning": "not-json"},
171
+ skill_tool=skill_tool,
172
+ use_responses_api=True,
173
+ )
174
+ assert result["reasoning"] == {"effort": "medium"}
175
+
176
+ # ── Completions API ───────────────────────────────────────────────────────
177
+
178
+ def test_nested_reasoning_effort_ignored_when_completions_api_active(self) -> None:
179
+ """``reasoning: {effort: ...}`` is in the wrong format for the completions API
180
+ and must not be picked up as the config effort value."""
181
+ skill_tool = _make_skill_tool_with_max("high")
182
+ result = _call(
183
+ {"reasoning": {"effort": "low"}},
184
+ skill_tool=skill_tool,
185
+ use_responses_api=False,
186
+ )
187
+ assert result["reasoning_effort"] == "high"
188
+ assert result.get("reasoning") == {"effort": "low"}
189
+
190
+ # ── Invalid config effort ─────────────────────────────────────────────────
191
+
192
+ def test_unrecognised_config_effort_falls_back_to_skill_max(self) -> None:
193
+ """An unrecognised effort string must not crash; skill max is used instead."""
194
+ skill_tool = _make_skill_tool_with_max("high")
195
+ result = _call({"reasoning_effort": "very_high"}, skill_tool=skill_tool)
196
+ assert result["reasoning_effort"] == "high"
197
+
198
+ def test_unrecognised_config_effort_passes_through_when_no_skill_max(self) -> None:
199
+ """When no skill thinking_level is active the invalid value passes through unchanged."""
200
+ skill_tool = _make_skill_tool_with_max(None)
201
+ result = _call({"reasoning_effort": "very_high"}, skill_tool=skill_tool)
202
+ assert result["reasoning_effort"] == "very_high"
203
+
204
+ def test_unrecognised_nested_effort_falls_back_to_skill_max_responses_api(
205
+ self,
206
+ ) -> None:
207
+ """Same guard for the responses-API nested path."""
208
+ skill_tool = _make_skill_tool_with_max("medium")
209
+ result = _call(
210
+ {"reasoning": {"effort": "very_high"}},
211
+ skill_tool=skill_tool,
212
+ use_responses_api=True,
213
+ )
214
+ assert result["reasoning"] == {"effort": "medium"}
215
+
216
+ # ── Supported reasoning efforts ───────────────────────────────────────────
217
+
218
+ def test_effort_written_when_within_supported_list(self) -> None:
219
+ """Resolved effort is written when it appears in the supported list."""
220
+ skill_tool = _make_skill_tool_with_max("high")
221
+ result = _call(
222
+ {},
223
+ skill_tool=skill_tool,
224
+ supported_reasoning_efforts=["low", "medium", "high"],
225
+ )
226
+ assert result["reasoning_effort"] == "high"
227
+
228
+ def test_effort_skipped_when_model_does_not_support_reasoning(self) -> None:
229
+ """Empty supported list means the model does not support reasoning_effort at all."""
230
+ skill_tool = _make_skill_tool_with_max("high")
231
+ result = _call({}, skill_tool=skill_tool, supported_reasoning_efforts=[])
232
+ assert "reasoning_effort" not in result
233
+
234
+ def test_effort_skipped_when_resolved_effort_not_in_supported_list(self) -> None:
235
+ """Resolved effort outside the supported list is not written."""
236
+ skill_tool = _make_skill_tool_with_max("high")
237
+ result = _call(
238
+ {},
239
+ skill_tool=skill_tool,
240
+ supported_reasoning_efforts=["low"],
241
+ )
242
+ assert "reasoning_effort" not in result
243
+
244
+ def test_original_options_preserved_when_effort_skipped(self) -> None:
245
+ """Other options survive when the effort is skipped due to model constraints."""
246
+ skill_tool = _make_skill_tool_with_max("high")
247
+ result = _call(
248
+ {"temperature": 0.5},
249
+ skill_tool=skill_tool,
250
+ supported_reasoning_efforts=[],
251
+ )
252
+ assert result == {"temperature": 0.5}
253
+
254
+ # ── No effort resolved ────────────────────────────────────────────────────
255
+
256
+ def test_no_effort_resolved_leaves_options_unchanged(self) -> None:
257
+ """When neither config nor skill provides an effort, options pass through."""
258
+ result = _call({"responseFormat": {"type": "json_object"}})
259
+ assert result == {"responseFormat": {"type": "json_object"}}
260
+ assert "reasoning_effort" not in result
261
+ assert "reasoning" not in result
@@ -57,6 +57,7 @@ from unique_orchestrator._builders.inject_tool_reminders import (
57
57
  from unique_orchestrator._builders.skill_setup import preload_invoked_skills
58
58
  from unique_orchestrator.config import UniqueAIConfig
59
59
  from unique_orchestrator.settings import env_settings
60
+ from unique_orchestrator.utils import resolve_other_options
60
61
 
61
62
 
62
63
  class UniqueAI:
@@ -345,7 +346,9 @@ class UniqueAI:
345
346
  debug_info=self._debug_info_manager.get(),
346
347
  temperature=self._config.agent.experimental.temperature,
347
348
  tool_choices=self._tool_manager.get_forced_tools(), # type: ignore (as above)
348
- other_options=self._config.agent.experimental.additional_llm_options,
349
+ other_options=resolve_other_options(
350
+ self._config, self._tool_manager, self._logger
351
+ ),
349
352
  )
350
353
 
351
354
  # Experimental Feature UN-17905
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from logging import Logger
5
+
6
+ from unique_skill_tool.service import SkillTool
7
+ from unique_toolkit.agentic.feature_flags import feature_flags
8
+ from unique_toolkit.agentic.tools.tool_manager import (
9
+ ResponsesApiToolManager,
10
+ ToolManager,
11
+ )
12
+ from unique_toolkit.app.schemas import ChatEventAdditionalParameters
13
+ from unique_toolkit.content import Content
14
+ from unique_toolkit.language_model.schemas import (
15
+ REASONING_EFFORT_ORDER,
16
+ to_reasoning_effort,
17
+ )
18
+
19
+ from unique_orchestrator.config import UniqueAIConfig
20
+
21
+
22
+ def resolve_other_options(
23
+ config: UniqueAIConfig,
24
+ tool_manager: ToolManager | ResponsesApiToolManager,
25
+ logger: Logger,
26
+ ) -> dict:
27
+ """Build ``other_options`` for the LLM call.
28
+
29
+ Merges ``additional_llm_options`` from config with the highest
30
+ ``thinking_level`` declared across all skills activated in this run.
31
+ The final effort level is the highest value among the config setting
32
+ (if any) and the skill hint (if any).
33
+
34
+ Determines the active API first, then reads the effort from the
35
+ matching format and writes the resolved value back in that same format:
36
+ - completions API: ``reasoning_effort: "high"``
37
+ - responses API: ``reasoning: {"effort": "high"}``
38
+ """
39
+
40
+ options: dict = dict(config.agent.experimental.additional_llm_options)
41
+
42
+ skill_tool = tool_manager.get_tool_by_name(SkillTool.name)
43
+ if not isinstance(skill_tool, SkillTool) or skill_tool.max_thinking_level is None:
44
+ return options
45
+
46
+ skill_max: str = skill_tool.max_thinking_level
47
+
48
+ use_responses_api: bool = (
49
+ config.agent.experimental.responses_api_config.use_responses_api
50
+ or config.agent.experimental.use_responses_api
51
+ )
52
+
53
+ if use_responses_api:
54
+ reasoning_raw = options.get("reasoning")
55
+ if isinstance(reasoning_raw, str):
56
+ try:
57
+ reasoning_dict: dict = json.loads(reasoning_raw)
58
+ except (json.JSONDecodeError, ValueError):
59
+ reasoning_dict = {}
60
+ elif isinstance(reasoning_raw, dict):
61
+ reasoning_dict = reasoning_raw
62
+ else:
63
+ reasoning_dict = {}
64
+ config_effort: str | None = reasoning_dict.get("effort")
65
+ else:
66
+ config_effort = options.get("reasoning_effort")
67
+
68
+ valid_config_effort: str | None = None
69
+ if config_effort is not None:
70
+ try:
71
+ valid_config_effort = to_reasoning_effort(config_effort)
72
+ except ValueError:
73
+ logger.warning(
74
+ "additional_llm_options contains an unrecognised "
75
+ "reasoning_effort value %r — cannot compare it with "
76
+ "the skill's thinking_level; using skill maximum %r instead.",
77
+ config_effort,
78
+ skill_max,
79
+ )
80
+
81
+ resolved_effort: str = (
82
+ skill_max
83
+ if valid_config_effort is None
84
+ else max([valid_config_effort, skill_max], key=REASONING_EFFORT_ORDER.index)
85
+ )
86
+
87
+ supported_efforts = config.space.language_model.supported_reasoning_efforts
88
+ if supported_efforts is not None:
89
+ if len(supported_efforts) == 0:
90
+ logger.warning(
91
+ "Skipping reasoning_effort %r: model %s does not support reasoning_effort.",
92
+ resolved_effort,
93
+ config.space.language_model.name,
94
+ )
95
+ return options
96
+ if resolved_effort not in supported_efforts:
97
+ logger.warning(
98
+ "Skipping reasoning_effort %r: model %s only supports %s.",
99
+ resolved_effort,
100
+ config.space.language_model.name,
101
+ supported_efforts,
102
+ )
103
+ return options
104
+
105
+ if use_responses_api:
106
+ existing_reasoning: dict = dict(reasoning_dict)
107
+ existing_reasoning["effort"] = resolved_effort
108
+ options["reasoning"] = existing_reasoning
109
+ else:
110
+ options["reasoning_effort"] = resolved_effort
111
+
112
+ return options
113
+
114
+
115
+ def filter_uploaded_documents_by_selection(
116
+ documents: list[Content],
117
+ additional_parameters: ChatEventAdditionalParameters | None,
118
+ company_id: str,
119
+ ) -> list[Content]:
120
+ """Return only the documents the user has actively selected.
121
+
122
+ Returns *all* documents unchanged when:
123
+ - the feature flag is disabled for ``company_id``,
124
+ - ``additional_parameters`` is ``None``.
125
+ """
126
+ if not feature_flags.enable_selected_uploaded_files_un_18215.is_enabled(company_id):
127
+ return documents
128
+
129
+ if additional_parameters is None:
130
+ return documents
131
+
132
+ selected_ids = additional_parameters.selected_uploaded_file_ids
133
+ return [doc for doc in documents if doc.id in selected_ids]
@@ -1,26 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from unique_toolkit.agentic.feature_flags import feature_flags
4
- from unique_toolkit.app.schemas import ChatEventAdditionalParameters
5
- from unique_toolkit.content import Content
6
-
7
-
8
- def filter_uploaded_documents_by_selection(
9
- documents: list[Content],
10
- additional_parameters: ChatEventAdditionalParameters | None,
11
- company_id: str,
12
- ) -> list[Content]:
13
- """Return only the documents the user has actively selected.
14
-
15
- Returns *all* documents unchanged when:
16
- - the feature flag is disabled for ``company_id``,
17
- - ``additional_parameters`` is ``None``.
18
- """
19
- if not feature_flags.enable_selected_uploaded_files_un_18215.is_enabled(company_id):
20
- return documents
21
-
22
- if additional_parameters is None:
23
- return documents
24
-
25
- selected_ids = additional_parameters.selected_uploaded_file_ids
26
- return [doc for doc in documents if doc.id in selected_ids]