unique_web_search 2026.44.0.dev0__tar.gz → 2026.44.0.dev2__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 (126) hide show
  1. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/PKG-INFO +8 -6
  2. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/README.md +4 -2
  3. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/pyproject.toml +4 -4
  4. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/metrics.py +5 -0
  5. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/README.md +17 -6
  6. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/base.py +22 -12
  7. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/custom_api.py +107 -8
  8. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/settings.py +1 -0
  9. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/__init__.py +0 -0
  10. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/client_settings.py +0 -0
  11. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/config.py +0 -0
  12. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/invocation_stats.py +0 -0
  13. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/prompts.py +0 -0
  14. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/schema.py +0 -0
  15. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/service.py +0 -0
  16. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/_registry.py +0 -0
  17. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  18. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/config.py +0 -0
  19. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  20. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  21. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  22. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  23. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  24. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  25. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  26. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/argument_screening/service.py +0 -0
  27. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/client/http_client.py +0 -0
  28. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  29. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  30. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  31. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  32. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  33. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/config.py +0 -0
  34. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  35. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  36. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
  37. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
  38. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
  39. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  40. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  41. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  42. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  43. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  44. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  45. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  46. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  47. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  48. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  49. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  50. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/content_processing/service.py +0 -0
  51. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/README.md +0 -0
  52. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  53. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/base.py +0 -0
  54. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/basic.py +0 -0
  55. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
  56. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/jina.py +0 -0
  57. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/registry.py +0 -0
  58. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/tavily.py +0 -0
  59. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  60. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/crawlers/utils.py +0 -0
  61. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/README.md +0 -0
  62. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/__init__.py +0 -0
  63. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/base_config.py +0 -0
  64. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/base_executor.py +0 -0
  65. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/context.py +0 -0
  66. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/modes.py +0 -0
  67. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  68. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/config.py +0 -0
  69. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/executor.py +0 -0
  70. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  71. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  72. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  73. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  74. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  75. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
  76. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  77. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/config.py +0 -0
  78. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/executor.py +0 -0
  79. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  80. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  81. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  82. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  83. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  84. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
  85. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  86. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v3/config.py +0 -0
  87. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v3/executor.py +0 -0
  88. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  89. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  90. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  91. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  92. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
  93. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/helpers.py +0 -0
  94. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/message_log.py +0 -0
  95. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/proxy/__init__.py +0 -0
  96. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/proxy/bridge.py +0 -0
  97. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/proxy/mappers.py +0 -0
  98. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/query_elicitation.py +0 -0
  99. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  100. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/bing.py +0 -0
  101. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/brave.py +0 -0
  102. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/google.py +0 -0
  103. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  104. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/registry.py +0 -0
  105. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/schema.py +0 -0
  106. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  107. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  108. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  109. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  110. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  111. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
  112. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  113. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
  114. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  115. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  116. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  117. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  118. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  119. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/search_engine/vertexai.py +0 -0
  120. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  121. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  122. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  123. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  124. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/services/structured_llm.py +0 -0
  125. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/skills/web-search-v3.md +0 -0
  126. {unique_web_search-2026.44.0.dev0 → unique_web_search-2026.44.0.dev2}/src/unique_web_search/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-web-search
3
- Version: 2026.44.0.dev0
3
+ Version: 2026.44.0.dev2
4
4
  Summary:
5
5
  Author: Andreas Hauri, Gustav Hartz, Rami Azouz
6
6
  Author-email: Andreas Hauri <andreas@unique.ch>, Gustav Hartz <gustav.hartz.ext@unique.ch>, Rami Azouz <rami.ext@unique.ch>
@@ -16,7 +16,7 @@ Requires-Dist: tavily-python>=0.7.11,<1
16
16
  Requires-Dist: unidecode>=1.4.0,<2
17
17
  Requires-Dist: azure-ai-projects>=2.3,<3
18
18
  Requires-Dist: azure-identity>=1.25.0,<2
19
- Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev0,<2026.44.0rc0
19
+ Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev9,<2026.44.0rc0
20
20
  Requires-Dist: azure-core>=1.36.0,<2
21
21
  Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
22
22
  Requires-Dist: google-auth>=2.43.0,<3
@@ -25,8 +25,8 @@ Requires-Dist: rapidfuzz>=3.0.0,<4
25
25
  Requires-Dist: httpx>=0.28.1
26
26
  Requires-Dist: certifi>=2025.11.12
27
27
  Requires-Dist: jinja2>=3.1.6
28
- Requires-Dist: unique-search-proxy-core>=2026.44.0.dev0,<2026.44.0rc0
29
- Requires-Dist: unique-search-proxy-sdk>=2026.44.0.dev0,<2026.44.0rc0
28
+ Requires-Dist: unique-search-proxy-core>=2026.44.0.dev4,<2026.44.0rc0
29
+ Requires-Dist: unique-search-proxy-sdk>=2026.44.0.dev2,<2026.44.0rc0
30
30
  Requires-Dist: openai>=1.105.0,<3
31
31
  Requires-Python: >=3.12, <4.0
32
32
  Description-Content-Type: text/markdown
@@ -45,7 +45,9 @@ Outbound web access should go through **Unique Search Proxy** (`connectors/uniqu
45
45
  | **Legacy** | URL unset | Direct provider calls from this process (`_legacy_search` / `_legacy_crawl`) |
46
46
 
47
47
  - **New standard engines (Brave, Perplexity) are proxy-only** — legacy raises.
48
- - **Custom API** always runs locally (never routed through the proxy).
48
+ - **Custom API** uses proxy egress only when
49
+ `FEATURE_FLAG_SEARCH_PROXY_ENABLE_CUSTOM_API_UN_26736` is enabled; otherwise
50
+ the legacy path reports URL-safety outcomes without blocking.
49
51
  - Future provider work targets Search Proxy only; legacy is preserved for migrated engines.
50
52
 
51
53
  Packages involved: [`unique-search-proxy-core`](../../connectors/unique_search_proxy/unique_search_proxy_core/README.md) (schemas), [`unique-search-proxy-sdk`](../../connectors/unique_search_proxy/unique_search_proxy_sdk/README.md) (HTTP), [`unique-search-proxy`](../../connectors/unique_search_proxy/unique_search_proxy_client/README.md) (server). System overview: [`connectors/unique_search_proxy/README.md`](../../connectors/unique_search_proxy/README.md).
@@ -65,7 +67,7 @@ Query → normalised results (URL / title / snippet / optional content). Optiona
65
67
  | Google | legacy + proxy | Requires scraping by default |
66
68
  | Brave | **proxy-only** | Rich snippets; `ExposableParam` knobs |
67
69
  | Perplexity | **proxy-only** | Content extraction knobs; `ExposableParam` |
68
- | Custom API | always local | Your REST endpoint |
70
+ | Custom API | legacy + proxy | Your REST endpoint; proxy mode rejects redirects |
69
71
 
70
72
  Standard engines that live in proxy-core use **`ExposableParam`** (`expose` + `value`) for optional provider knobs. See [Search Engines README](./src/unique_web_search/services/search_engine/README.md).
71
73
 
@@ -12,7 +12,9 @@ Outbound web access should go through **Unique Search Proxy** (`connectors/uniqu
12
12
  | **Legacy** | URL unset | Direct provider calls from this process (`_legacy_search` / `_legacy_crawl`) |
13
13
 
14
14
  - **New standard engines (Brave, Perplexity) are proxy-only** — legacy raises.
15
- - **Custom API** always runs locally (never routed through the proxy).
15
+ - **Custom API** uses proxy egress only when
16
+ `FEATURE_FLAG_SEARCH_PROXY_ENABLE_CUSTOM_API_UN_26736` is enabled; otherwise
17
+ the legacy path reports URL-safety outcomes without blocking.
16
18
  - Future provider work targets Search Proxy only; legacy is preserved for migrated engines.
17
19
 
18
20
  Packages involved: [`unique-search-proxy-core`](../../connectors/unique_search_proxy/unique_search_proxy_core/README.md) (schemas), [`unique-search-proxy-sdk`](../../connectors/unique_search_proxy/unique_search_proxy_sdk/README.md) (HTTP), [`unique-search-proxy`](../../connectors/unique_search_proxy/unique_search_proxy_client/README.md) (server). System overview: [`connectors/unique_search_proxy/README.md`](../../connectors/unique_search_proxy/README.md).
@@ -32,7 +34,7 @@ Query → normalised results (URL / title / snippet / optional content). Optiona
32
34
  | Google | legacy + proxy | Requires scraping by default |
33
35
  | Brave | **proxy-only** | Rich snippets; `ExposableParam` knobs |
34
36
  | Perplexity | **proxy-only** | Content extraction knobs; `ExposableParam` |
35
- | Custom API | always local | Your REST endpoint |
37
+ | Custom API | legacy + proxy | Your REST endpoint; proxy mode rejects redirects |
36
38
 
37
39
  Standard engines that live in proxy-core use **`ExposableParam`** (`expose` + `value`) for optional provider knobs. See [Search Engines README](./src/unique_web_search/services/search_engine/README.md).
38
40
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_web_search"
3
- version = "2026.44.0.dev0"
3
+ version = "2026.44.0.dev2"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -22,7 +22,7 @@ dependencies = [
22
22
  "unidecode>=1.4.0,<2",
23
23
  "azure-ai-projects>=2.3,<3",
24
24
  "azure-identity>=1.25.0,<2",
25
- "unique-toolkit[monitoring]>=2026.44.0.dev0,<2026.44.0rc0",
25
+ "unique-toolkit[monitoring]>=2026.44.0.dev9,<2026.44.0rc0",
26
26
  "azure-core>=1.36.0,<2",
27
27
  "google-cloud-aiplatform>=1.128.0,<2",
28
28
  "google-auth>=2.43.0,<3",
@@ -31,8 +31,8 @@ dependencies = [
31
31
  "httpx>=0.28.1",
32
32
  "certifi>=2025.11.12",
33
33
  "jinja2>=3.1.6",
34
- "unique-search-proxy-core>=2026.44.0.dev0,<2026.44.0rc0",
35
- "unique-search-proxy-sdk>=2026.44.0.dev0,<2026.44.0rc0",
34
+ "unique-search-proxy-core>=2026.44.0.dev4,<2026.44.0rc0",
35
+ "unique-search-proxy-sdk>=2026.44.0.dev2,<2026.44.0rc0",
36
36
  "openai>=1.105.0,<3",
37
37
  ]
38
38
 
@@ -10,6 +10,11 @@ search_total = m.counter("search_total", "Search calls by engine", ["engine"])
10
10
  search_errors = m.counter(
11
11
  "search_errors_total", "Search failures", ["engine", "error_type"]
12
12
  )
13
+ custom_api_url_safety_report = m.counter(
14
+ "custom_api_url_safety_report_total",
15
+ "Report-only URL safety outcomes for legacy Custom API searches",
16
+ ["outcome", "reason_category"],
17
+ )
13
18
 
14
19
  # End-to-End Tool (P1)
15
20
  tool_duration = m.histogram(
@@ -6,7 +6,11 @@ Implementations wrap standard search APIs and grounding (agent) providers behind
6
6
 
7
7
  ```python
8
8
  # SearchEngine.search (services/search_engine/base.py)
9
- if search_proxy_client_enabled and engine is proxy-supported:
9
+ if (
10
+ search_proxy_client_enabled
11
+ and engine is proxy-supported # config.engine is SearchEngineType / AgentEngineType
12
+ and await self._proxy_routing_enabled() # per-engine gate; Custom API checks its feature flag
13
+ ):
10
14
  return await self._proxy_search(...) # standard → /v1/search; agent → /v1/agent-search
11
15
  return await self._legacy_search(...) # direct provider call from this process
12
16
  ```
@@ -15,7 +19,7 @@ return await self._legacy_search(...) # direct provider call from this proc
15
19
  |------|---------|-------|--------|
16
20
  | **Standard** | Google | yes | yes |
17
21
  | **Standard** | Brave, Perplexity | yes | **raises** (proxy-only) |
18
- | **Standard** | Custom API | never | always |
22
+ | **Standard** | Custom API | yes | yes |
19
23
  | **Agent** | Bing, VertexAI | yes | yes |
20
24
 
21
25
  Deployment configs for Google / Brave / Perplexity (and agent base fields) live in **`unique_search_proxy_core`**. This package registers tool wrappers and adds web-only fields where needed (e.g. `requires_scraping` on Bing/VertexAI).
@@ -127,13 +131,20 @@ Config: `unique_search_proxy_core.search_engines.perplexity.schema.PerplexityCon
127
131
  PERPLEXITY_API_KEY=...
128
132
  ```
129
133
 
130
- ### Custom API Search (always local)
134
+ ### Custom API Search
131
135
 
132
- **Provider:** your REST endpoint · **Never proxy-routed**
136
+ **Provider:** your REST endpoint · **Proxy:** legacy + proxy
133
137
 
134
- Config: `unique_web_search.services.search_engine.custom_api.CustomAPIConfig` (`LocalSearchEngineType.CUSTOM_API`)
138
+ Config: `unique_web_search.services.search_engine.custom_api.CustomAPIConfig` (engine `SearchEngineType.CUSTOM_API`; registered under `LocalSearchEngineType.CUSTOM_API`)
135
139
 
136
- Response must be JSON with a `results` array of `{url, title, snippet, content?}`.
140
+ Response must be JSON with a `results` or legacy `curated` array of
141
+ `{url, title, snippet, content?}`. When
142
+ `FEATURE_FLAG_SEARCH_PROXY_ENABLE_CUSTOM_API_UN_26736` is enabled, the endpoint
143
+ is validated against the Search Proxy URL-safety policy, DNS is pinned, and
144
+ redirects are rejected. Exact private hosts can be operator-approved with
145
+ `CUSTOM_WEB_SEARCH_API_TRUSTED_PRIVATE_HOSTS`; this exception applies only to
146
+ Customized API endpoints. Use the same value in assistants-core for accurate
147
+ report-only metrics before proxy routing is enabled.
137
148
 
138
149
  ```bash
139
150
  CUSTOM_WEB_SEARCH_API_ENDPOINT=https://your-api.example.com/search
@@ -8,14 +8,14 @@ from pydantic import BaseModel
8
8
  from unique_search_proxy_core.agent_engines.base import AgentEngineType
9
9
  from unique_search_proxy_core.agent_engines.config_types import ENGINE_NAME_TO_CONFIG
10
10
  from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
11
+ from unique_search_proxy_core.param_policy.exposable_config import (
12
+ ExposableParamsConfig,
13
+ )
11
14
  from unique_search_proxy_core.param_policy.exposable_param import (
12
15
  resolve_exposable_value,
13
16
  )
14
17
  from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
15
- from unique_search_proxy_core.search_engines.base import (
16
- BaseSearchEngineConfig,
17
- SearchEngineType,
18
- )
18
+ from unique_search_proxy_core.search_engines.base import SearchEngineType
19
19
 
20
20
  from unique_web_search.services.proxy.bridge import (
21
21
  open_search_proxy_client,
@@ -45,8 +45,7 @@ class LocalSearchEngineType(StrEnum):
45
45
  CUSTOM_API = "custom_api"
46
46
 
47
47
 
48
- # Engine ids that the search proxy can serve. Every other engine
49
- # (e.g. ``LocalSearchEngineType``) is routed to the local legacy implementation.
48
+ # Engine ids that the search proxy can serve.
50
49
  ProxyEngineType = SearchEngineType | AgentEngineType
51
50
 
52
51
  # Fields present on agent-engine configs that are not accepted as proxy call kwargs.
@@ -69,8 +68,7 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
69
68
  provide the direct ``_legacy_search`` implementation.
70
69
 
71
70
  ``search`` uses the proxy whenever it is enabled and the engine is supported by
72
- it, otherwise it reroutes to the local legacy implementation. Engines that the
73
- proxy cannot serve (e.g. custom APIs) always fall back to ``_legacy_search``.
71
+ it, otherwise it reroutes to the local legacy implementation.
74
72
  """
75
73
 
76
74
  # Set by agent engines (Bing/VertexAI) and consumed by the agent proxy path.
@@ -90,6 +88,14 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
90
88
  """Whether the search engine requires scraping."""
91
89
  return False
92
90
 
91
+ async def _proxy_routing_enabled(self) -> bool:
92
+ """Whether this engine may use Search Proxy for the current request."""
93
+ return True
94
+
95
+ @property
96
+ def _standard_proxy_client_timeout(self) -> float:
97
+ return _STANDARD_PROXY_TIMEOUT
98
+
93
99
  @property
94
100
  def _proxy_engine(self) -> ProxyEngineType | None:
95
101
  """The proxy engine id for this config, or ``None`` if not proxy-supported."""
@@ -105,7 +111,11 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
105
111
  ) -> list[WebSearchResult]:
106
112
  """Search the web for the given query using the search engine."""
107
113
  proxy_engine = self._proxy_engine
108
- if search_proxy_client_enabled and proxy_engine is not None:
114
+ if (
115
+ search_proxy_client_enabled
116
+ and proxy_engine is not None
117
+ and await self._proxy_routing_enabled()
118
+ ):
109
119
  return await self._proxy_search(query, params, proxy_engine)
110
120
  return await self._legacy_search(query=query, params=params)
111
121
 
@@ -127,9 +137,9 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
127
137
  engine: SearchEngineType,
128
138
  ) -> list[WebSearchResult]:
129
139
  """Merge deployment config with per-call params and dispatch via the proxy SDK."""
130
- if not isinstance(self.config, BaseSearchEngineConfig):
140
+ if not isinstance(self.config, ExposableParamsConfig):
131
141
  raise TypeError(
132
- f"Standard proxy search requires BaseSearchEngineConfig, "
142
+ f"Standard proxy search requires ExposableParamsConfig, "
133
143
  f"got {type(self.config).__name__}"
134
144
  )
135
145
  overrides = (
@@ -140,7 +150,7 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
140
150
  invocation.pop("engine", None)
141
151
  invocation.pop("query", None)
142
152
  async with open_search_proxy_client(
143
- timeout=_STANDARD_PROXY_TIMEOUT,
153
+ timeout=self._standard_proxy_client_timeout,
144
154
  context=self._request_context,
145
155
  ) as client:
146
156
  response = await client.search.search(
@@ -1,13 +1,27 @@
1
1
  import json
2
- from typing import Any, Literal, TypeVar, override
2
+ import logging
3
+ from typing import Any, TypeVar, override
3
4
 
4
5
  from httpx import AsyncClient
5
- from pydantic import BaseModel, Field
6
+ from pydantic import BaseModel, Field, model_validator
6
7
  from pydantic.json_schema import SkipJsonSchema
7
8
  from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
8
9
  from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
10
+ from unique_search_proxy_core.search_engines.custom_api.schema import (
11
+ CustomApiConfig as ProxyCustomApiConfig,
12
+ )
13
+ from unique_search_proxy_core.search_engines.custom_api.schema import (
14
+ CustomApiSearchRequest,
15
+ )
16
+ from unique_search_proxy_core.url_safety import (
17
+ CustomApiTargetValidationError,
18
+ UrlSafetyService,
19
+ )
20
+ from unique_toolkit.agentic.feature_flags import FeatureFlagNames
9
21
  from unique_toolkit.agentic.tools.config import get_configuration_dict
22
+ from unique_toolkit.experimental.resources.feature_flags import is_flag_enabled
10
23
 
24
+ from unique_web_search.metrics import custom_api_url_safety_report
11
25
  from unique_web_search.services.search_engine.base import (
12
26
  LocalSearchEngineType,
13
27
  SearchEngine,
@@ -20,6 +34,8 @@ from unique_web_search.services.search_engine.schema import (
20
34
  )
21
35
  from unique_web_search.settings import CUSTOM_API_REQUEST_METHOD, env_settings
22
36
 
37
+ _LOGGER = logging.getLogger(__name__)
38
+
23
39
  T = TypeVar("T")
24
40
 
25
41
 
@@ -64,11 +80,9 @@ ApiRequestMethodType, ApiRequestMethodField = conditional_type(
64
80
  )
65
81
 
66
82
 
67
- class CustomAPIConfig(BaseModel):
83
+ class CustomAPIConfig(ProxyCustomApiConfig):
68
84
  model_config = get_configuration_dict(title="Customized API")
69
85
 
70
- engine: Literal[LocalSearchEngineType.CUSTOM_API] = LocalSearchEngineType.CUSTOM_API
71
-
72
86
  api_endpoint: ApiEndpointType = ApiEndpointField # type: ignore (Dynamic type generation)
73
87
  api_headers: ApiHeadersType = ApiHeadersField # type: ignore (Dynamic type generation)
74
88
  api_additional_query_params: ApiAdditionalQueryParamsType = ( # type: ignore (Dynamic type generation)
@@ -88,6 +102,43 @@ class CustomAPIConfig(BaseModel):
88
102
  )
89
103
  timeout: int = Field(default=120, description="The timeout of the custom API")
90
104
 
105
+ @model_validator(mode="before")
106
+ @classmethod
107
+ def apply_operator_config_precedence(cls, value: Any) -> Any:
108
+ if not isinstance(value, dict):
109
+ return value
110
+
111
+ resolved_value = dict(value)
112
+ operator_values = {
113
+ "apiEndpoint": env_settings.custom_web_search_api_endpoint,
114
+ "apiHeaders": env_settings.custom_web_search_api_headers,
115
+ "apiAdditionalQueryParams": (
116
+ env_settings.custom_web_search_api_additional_query_params
117
+ ),
118
+ "apiAdditionalBodyParams": (
119
+ env_settings.custom_web_search_api_additional_body_params
120
+ ),
121
+ "apiRequestMethod": env_settings.custom_web_search_api_method,
122
+ }
123
+ aliases = {
124
+ "apiEndpoint": "api_endpoint",
125
+ "apiHeaders": "api_headers",
126
+ "apiAdditionalQueryParams": "api_additional_query_params",
127
+ "apiAdditionalBodyParams": "api_additional_body_params",
128
+ "apiRequestMethod": "api_request_method",
129
+ }
130
+ for alias, operator_value in operator_values.items():
131
+ if operator_value is None:
132
+ continue
133
+ resolved_value.pop(aliases[alias], None)
134
+ resolved_value[alias] = operator_value
135
+ return resolved_value
136
+
137
+ @classmethod
138
+ def request_model(cls) -> type[BaseModel]:
139
+ """Use the proxy's wire contract, excluding tool-only configuration fields."""
140
+ return CustomApiSearchRequest
141
+
91
142
 
92
143
  @register_search_engine(
93
144
  name="custom_api",
@@ -107,6 +158,17 @@ class CustomAPI(SearchEngine[CustomAPIConfig]):
107
158
  self.api_endpoint = config.api_endpoint
108
159
  self.is_configured = True # No possibility to check if the API is configured from our side. So we assume it is configured.
109
160
 
161
+ async def _proxy_routing_enabled(self) -> bool:
162
+ return await is_flag_enabled(
163
+ FeatureFlagNames.search_proxy_enable_custom_api_un_26736,
164
+ company_id=self._request_context.company_id,
165
+ user_id=self._request_context.user_id,
166
+ )
167
+
168
+ @property
169
+ def _standard_proxy_client_timeout(self) -> float:
170
+ return float(self.config.timeout) + 5.0
171
+
110
172
  @override
111
173
  async def _legacy_search(
112
174
  self,
@@ -114,10 +176,10 @@ class CustomAPI(SearchEngine[CustomAPIConfig]):
114
176
  params: ExposedParams | None,
115
177
  ) -> list[WebSearchResult]:
116
178
  del params
179
+ await self._report_url_safety()
117
180
  params_dict, body = self._prepare_request_params_and_body(query)
118
- async_client_params = self._client_config | {
119
- "timeout": self.config.timeout,
120
- }
181
+ async_client_params: dict[str, Any] = dict(self._client_config)
182
+ async_client_params["timeout"] = self.config.timeout
121
183
  async with AsyncClient(**async_client_params) as client:
122
184
  response = await client.request(
123
185
  method=self._request_method,
@@ -135,6 +197,43 @@ class CustomAPI(SearchEngine[CustomAPIConfig]):
135
197
  validated_response = WebSearchResults.model_validate(response.json())
136
198
  return validated_response.results
137
199
 
200
+ async def _report_url_safety(self) -> None:
201
+ try:
202
+ await UrlSafetyService.resolve_custom_api_target(
203
+ self.config.api_endpoint,
204
+ trusted_private_hosts=(
205
+ env_settings.custom_web_search_api_trusted_private_hosts
206
+ ),
207
+ )
208
+ except CustomApiTargetValidationError as exc:
209
+ reason_category = exc.blocked_targets[0].category
210
+ custom_api_url_safety_report.labels(
211
+ outcome="would_block",
212
+ reason_category=reason_category,
213
+ ).inc()
214
+ _LOGGER.warning(
215
+ "Search Proxy URL safety would block this Custom API endpoint; "
216
+ "continuing through the legacy direct path because proxy routing "
217
+ "is disabled company_id=%s category=%s",
218
+ self._request_context.company_id,
219
+ reason_category,
220
+ )
221
+ except Exception as exc:
222
+ custom_api_url_safety_report.labels(
223
+ outcome="validation_error",
224
+ reason_category="unexpected",
225
+ ).inc()
226
+ _LOGGER.warning(
227
+ "Custom API URL safety report failed company_id=%s error_type=%s",
228
+ self._request_context.company_id,
229
+ type(exc).__name__,
230
+ )
231
+ else:
232
+ custom_api_url_safety_report.labels(
233
+ outcome="allowed",
234
+ reason_category="none",
235
+ ).inc()
236
+
138
237
  @property
139
238
  def requires_scraping(self) -> bool:
140
239
  return self.config.requires_scraping
@@ -77,6 +77,7 @@ class Base(BaseSettings):
77
77
  custom_web_search_api_additional_query_params: str | None = None
78
78
  custom_web_search_api_additional_body_params: str | None = None
79
79
  custom_web_search_api_client_config: str | None = None
80
+ custom_web_search_api_trusted_private_hosts: list[str] = []
80
81
 
81
82
  # Proxy settings
82
83
  ## Shared settings