unique-search-proxy 2026.44.0.dev14__tar.gz → 2026.46.0.dev0__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 (122) hide show
  1. unique_search_proxy-2026.44.0.dev14/README.md → unique_search_proxy-2026.46.0.dev0/PKG-INFO +48 -0
  2. unique_search_proxy-2026.44.0.dev14/PKG-INFO → unique_search_proxy-2026.46.0.dev0/README.md +20 -28
  3. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/pyproject.toml +3 -3
  4. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/v1/agent_search.py +36 -1
  5. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/v1/context_headers.py +16 -0
  6. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/v1/crawl.py +14 -0
  7. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/v1/search.py +13 -0
  8. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/context.py +7 -1
  9. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/logging_config.py +16 -1
  10. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/middleware/context.py +33 -1
  11. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/monitoring/metrics.py +31 -26
  12. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/app.py +1 -0
  13. unique_search_proxy-2026.46.0.dev0/unique_search_proxy_client/web/usage.py +40 -0
  14. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/__init__.py +0 -0
  15. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/__init__.py +0 -0
  16. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/__init__.py +0 -0
  17. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/health.py +0 -0
  18. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
  19. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
  20. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
  21. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/app.py +0 -0
  22. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/__init__.py +0 -0
  23. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
  24. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/bing/citation_instructions.j2 +0 -0
  25. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/bing/cleanup.py +0 -0
  26. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
  27. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/bing/instructions.py +0 -0
  28. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
  29. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
  30. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
  31. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
  32. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +0 -0
  33. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
  34. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/vertexai/redirects.py +0 -0
  35. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
  36. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
  37. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/client/azure_transport.py +0 -0
  38. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/client/credentials.py +0 -0
  39. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/client/service.py +0 -0
  40. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawl_limiter.py +0 -0
  41. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
  42. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
  43. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
  44. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
  45. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
  46. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
  47. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
  48. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
  49. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
  50. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
  51. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/service.py +0 -0
  52. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
  53. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
  54. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
  55. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
  56. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
  57. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
  58. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
  59. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
  60. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
  61. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
  62. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
  63. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/provider_response.py +0 -0
  64. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/providers.py +0 -0
  65. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/registry.py +0 -0
  66. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
  67. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
  68. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
  69. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
  70. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
  71. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/custom_api/__init__.py +0 -0
  72. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/custom_api/service.py +0 -0
  73. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
  74. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
  75. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
  76. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
  77. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
  78. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
  79. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
  80. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
  81. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
  82. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
  83. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
  84. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
  85. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
  86. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/error_handlers.py +0 -0
  87. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/__init__.py +0 -0
  88. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
  89. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/generator/introspect.py +0 -0
  90. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/generator/schema.py +0 -0
  91. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/generator/template.py +0 -0
  92. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
  93. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
  94. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/metadata.py +0 -0
  95. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/registry.py +0 -0
  96. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/helm/url_safety_registration.py +0 -0
  97. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
  98. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
  99. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/presets/__init__.py +0 -0
  100. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/presets/common.py +0 -0
  101. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/presets/crawl.py +0 -0
  102. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/presets/search.py +0 -0
  103. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/presets/types.py +0 -0
  104. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/__init__.py +0 -0
  105. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/base.py +0 -0
  106. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/client.py +0 -0
  107. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
  108. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
  109. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
  110. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
  111. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
  112. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
  113. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
  114. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
  115. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
  116. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
  117. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
  118. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/request_limits.py +0 -0
  119. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
  120. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/startup_report.py +0 -0
  121. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/utils/__init__.py +0 -0
  122. {unique_search_proxy-2026.44.0.dev14 → unique_search_proxy-2026.46.0.dev0}/unique_search_proxy_client/web/utils/url.py +0 -0
@@ -1,3 +1,31 @@
1
+ Metadata-Version: 2.3
2
+ Name: unique-search-proxy
3
+ Version: 2026.46.0.dev0
4
+ Summary: Web Search Proxy implementation
5
+ Author: ThePhilAz
6
+ Author-email: ThePhilAz <rami.azouz@philico.com>
7
+ Requires-Dist: aiohttp>=3.14.1,<4
8
+ Requires-Dist: fastapi>=0.133.0,<1.0.0
9
+ Requires-Dist: starlette>=1.3.1,<2.0.0
10
+ Requires-Dist: uvicorn[standard]>=0.32.0,<1.0.0
11
+ Requires-Dist: pydantic>=2.12.5,<3.0.0
12
+ Requires-Dist: httpx>=0.28.0,<0.29.0
13
+ Requires-Dist: python-dotenv>=1.2.1,<2.0.0
14
+ Requires-Dist: pydantic-settings>=2.12.0,<3.0.0
15
+ Requires-Dist: markdownify>=0.14.1,<1
16
+ Requires-Dist: azure-ai-projects>=2.3,<3
17
+ Requires-Dist: azure-identity>=1.25.0,<2
18
+ Requires-Dist: azure-core>=1.36.0,<2
19
+ Requires-Dist: certifi>=2025.11.12,<2027
20
+ Requires-Dist: google-genai>=1.73.0,<2
21
+ Requires-Dist: google-auth>=2.43.0,<3
22
+ Requires-Dist: jinja2>=3.1.6,<4
23
+ Requires-Dist: unique-toolkit[monitoring]>=2026.46.0.dev0,<2026.46.0rc0
24
+ Requires-Dist: unique-search-proxy-core>=2026.46.0.dev0,<2026.46.0rc0
25
+ Requires-Dist: openai>=1.105.0,<3
26
+ Requires-Python: >=3.12
27
+ Description-Content-Type: text/markdown
28
+
1
29
  # unique-search-proxy (client)
2
30
 
3
31
  Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy` · Helm: `unique-search-proxy`
@@ -96,6 +124,25 @@ Built-in providers register in `register_builtin_providers()` during `create_app
96
124
 
97
125
  OpenAPI spec: `openapi.json` (input for [SDK codegen](../unique_search_proxy_sdk/README.md)).
98
126
 
127
+ ### 4.1 Request context and usage
128
+
129
+ `/v1/*` routes read these headers into a request context that prefixes every log line:
130
+
131
+ | Header | Required | Use |
132
+ |--------|----------|-----|
133
+ | `x-unique-company-id`, `x-unique-user-id`, `x-unique-chat-id` | Yes (`REQUIRE_CONTEXT_HEADERS`, default `true`) | Tenant identity |
134
+ | `x-unique-entry-point` | No, defaults to `unknown` | Where the request entered the platform (`chat_tool`, `public_api`, `conduct`, ...) |
135
+ | `x-unique-message-id` | No | Reserved for analytics; recorded in the usage line only |
136
+ | `x-service-id` | No, defaults to `unknown` | Calling service; logged as `caller=`, never enforced |
137
+
138
+ A request whose company or user id is not numeric (`local`, blank, junk) is unattributed: it logs a warning and increments `unique_search_proxy_unattributed_requests_total{entry_point}`. Set `REJECT_UNATTRIBUTED_REQUESTS=true` to return 400 instead. `/v1/configuration/*` skips this check.
139
+
140
+ `/v1/search`, `/v1/agent-search` (sync and stream) and `/v1/crawl` write one JSON usage line per request (`event="web_search_usage"`) and increment `unique_search_proxy_requests_total{company_id, entry_point, endpoint, provider, status}`. `company_id` is `unattributed` unless both ids are numeric. The line holds no query text or URLs.
141
+
142
+ `units` is 1 per search or agent search. For a crawl it is the number of URLs that passed URL safety, so 0 when nothing reached the provider. Failed and timed-out requests keep their units, so filter on `status` when counting billable work. Search records also carry `fetch_size`, the number of results requested: Google and Brave page through results (10 and 20 per call), so one search unit can be several provider calls.
143
+
144
+ The usage line goes through its own logger (`unique_search_proxy_core.usage`) with a bare message format, fixed at INFO, so Loki `| json` can parse it regardless of `LOG_LEVEL`. Nothing verifies these headers: any in-cluster caller can set them.
145
+
99
146
  ---
100
147
 
101
148
  ## 5. Providers
@@ -226,6 +273,7 @@ Settings use pydantic-settings with per-provider env vars. Copy `.env.example` t
226
273
  | Bing agent | `BING_AGENT_` | `BING_AGENT_ENDPOINT`, `BING_AGENT_BING_RESOURCE_CONNECTION_STRING`, optional `BING_AGENT_CLEANUP_ON_START`, `BING_AGENT_DEFAULT_MARKET` |
227
274
  | VertexAI agent | `VERTEXAI_AGENT_` | `VERTEXAI_AGENT_SERVICE_ACCOUNT_CREDENTIALS` (optional) |
228
275
  | HTTP client | `HTTP_CLIENT_` | `HTTP_CLIENT_PROXY_HOST`, `HTTP_CLIENT_POOL_TIMEOUT_SECONDS` |
276
+ | Request context | (none) | `REQUIRE_CONTEXT_HEADERS`, `REJECT_UNATTRIBUTED_REQUESTS` |
229
277
  | Prometheus | `PROMETHEUS_` | `PROMETHEUS_ENABLED` |
230
278
  | URL safety | `URL_SAFETY_` | `URL_SAFETY_ENABLED` |
231
279
  | Container | (shell) | `HOST`, `PORT`, `WORKERS`, `LOG_LEVEL` |
@@ -1,31 +1,3 @@
1
- Metadata-Version: 2.3
2
- Name: unique-search-proxy
3
- Version: 2026.44.0.dev14
4
- Summary: Web Search Proxy implementation
5
- Author: ThePhilAz
6
- Author-email: ThePhilAz <rami.azouz@philico.com>
7
- Requires-Dist: aiohttp>=3.14.1,<4
8
- Requires-Dist: fastapi>=0.133.0,<1.0.0
9
- Requires-Dist: starlette>=1.3.1,<2.0.0
10
- Requires-Dist: uvicorn[standard]>=0.32.0,<1.0.0
11
- Requires-Dist: pydantic>=2.12.5,<3.0.0
12
- Requires-Dist: httpx>=0.28.0,<0.29.0
13
- Requires-Dist: python-dotenv>=1.2.1,<2.0.0
14
- Requires-Dist: pydantic-settings>=2.12.0,<3.0.0
15
- Requires-Dist: markdownify>=0.14.1,<1
16
- Requires-Dist: azure-ai-projects>=2.3,<3
17
- Requires-Dist: azure-identity>=1.25.0,<2
18
- Requires-Dist: azure-core>=1.36.0,<2
19
- Requires-Dist: certifi>=2025.11.12,<2027
20
- Requires-Dist: google-genai>=1.73.0,<2
21
- Requires-Dist: google-auth>=2.43.0,<3
22
- Requires-Dist: jinja2>=3.1.6,<4
23
- Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.0rc0
24
- Requires-Dist: unique-search-proxy-core>=2026.44.0.dev10,<2026.44.0rc0
25
- Requires-Dist: openai>=1.105.0,<3
26
- Requires-Python: >=3.12
27
- Description-Content-Type: text/markdown
28
-
29
1
  # unique-search-proxy (client)
30
2
 
31
3
  Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy` · Helm: `unique-search-proxy`
@@ -124,6 +96,25 @@ Built-in providers register in `register_builtin_providers()` during `create_app
124
96
 
125
97
  OpenAPI spec: `openapi.json` (input for [SDK codegen](../unique_search_proxy_sdk/README.md)).
126
98
 
99
+ ### 4.1 Request context and usage
100
+
101
+ `/v1/*` routes read these headers into a request context that prefixes every log line:
102
+
103
+ | Header | Required | Use |
104
+ |--------|----------|-----|
105
+ | `x-unique-company-id`, `x-unique-user-id`, `x-unique-chat-id` | Yes (`REQUIRE_CONTEXT_HEADERS`, default `true`) | Tenant identity |
106
+ | `x-unique-entry-point` | No, defaults to `unknown` | Where the request entered the platform (`chat_tool`, `public_api`, `conduct`, ...) |
107
+ | `x-unique-message-id` | No | Reserved for analytics; recorded in the usage line only |
108
+ | `x-service-id` | No, defaults to `unknown` | Calling service; logged as `caller=`, never enforced |
109
+
110
+ A request whose company or user id is not numeric (`local`, blank, junk) is unattributed: it logs a warning and increments `unique_search_proxy_unattributed_requests_total{entry_point}`. Set `REJECT_UNATTRIBUTED_REQUESTS=true` to return 400 instead. `/v1/configuration/*` skips this check.
111
+
112
+ `/v1/search`, `/v1/agent-search` (sync and stream) and `/v1/crawl` write one JSON usage line per request (`event="web_search_usage"`) and increment `unique_search_proxy_requests_total{company_id, entry_point, endpoint, provider, status}`. `company_id` is `unattributed` unless both ids are numeric. The line holds no query text or URLs.
113
+
114
+ `units` is 1 per search or agent search. For a crawl it is the number of URLs that passed URL safety, so 0 when nothing reached the provider. Failed and timed-out requests keep their units, so filter on `status` when counting billable work. Search records also carry `fetch_size`, the number of results requested: Google and Brave page through results (10 and 20 per call), so one search unit can be several provider calls.
115
+
116
+ The usage line goes through its own logger (`unique_search_proxy_core.usage`) with a bare message format, fixed at INFO, so Loki `| json` can parse it regardless of `LOG_LEVEL`. Nothing verifies these headers: any in-cluster caller can set them.
117
+
127
118
  ---
128
119
 
129
120
  ## 5. Providers
@@ -254,6 +245,7 @@ Settings use pydantic-settings with per-provider env vars. Copy `.env.example` t
254
245
  | Bing agent | `BING_AGENT_` | `BING_AGENT_ENDPOINT`, `BING_AGENT_BING_RESOURCE_CONNECTION_STRING`, optional `BING_AGENT_CLEANUP_ON_START`, `BING_AGENT_DEFAULT_MARKET` |
255
246
  | VertexAI agent | `VERTEXAI_AGENT_` | `VERTEXAI_AGENT_SERVICE_ACCOUNT_CREDENTIALS` (optional) |
256
247
  | HTTP client | `HTTP_CLIENT_` | `HTTP_CLIENT_PROXY_HOST`, `HTTP_CLIENT_POOL_TIMEOUT_SECONDS` |
248
+ | Request context | (none) | `REQUIRE_CONTEXT_HEADERS`, `REJECT_UNATTRIBUTED_REQUESTS` |
257
249
  | Prometheus | `PROMETHEUS_` | `PROMETHEUS_ENABLED` |
258
250
  | URL safety | `URL_SAFETY_` | `URL_SAFETY_ENABLED` |
259
251
  | Container | (shell) | `HOST`, `PORT`, `WORKERS`, `LOG_LEVEL` |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy"
3
- version = "2026.44.0.dev14"
3
+ version = "2026.46.0.dev0"
4
4
  description = "Web Search Proxy implementation"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -22,8 +22,8 @@ dependencies = [
22
22
  "google-genai>=1.73.0,<2",
23
23
  "google-auth>=2.43.0,<3",
24
24
  "jinja2>=3.1.6,<4",
25
- "unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.0rc0",
26
- "unique-search-proxy-core>=2026.44.0.dev10,<2026.44.0rc0",
25
+ "unique-toolkit[monitoring]>=2026.46.0.dev0,<2026.46.0rc0",
26
+ "unique-search-proxy-core>=2026.46.0.dev0,<2026.46.0rc0",
27
27
  "openai>=1.105.0,<3",
28
28
  ]
29
29
 
@@ -32,6 +32,7 @@ from unique_search_proxy_client.web.monitoring.metrics import (
32
32
  record_agent_search_error,
33
33
  record_agent_search_success,
34
34
  )
35
+ from unique_search_proxy_client.web.usage import record_request_usage
35
36
 
36
37
  router = APIRouter(tags=["agent-search"])
37
38
  _LOGGER = logging.getLogger(__name__)
@@ -79,6 +80,7 @@ async def agent_search(
79
80
  engine_id = engine.value if hasattr(engine, "value") else str(engine)
80
81
  timeout = body.timeout
81
82
  started = time.perf_counter()
83
+ succeeded = False
82
84
  _LOGGER.info(
83
85
  "agent-search start mode=sync engine=%s timeout=%ss",
84
86
  engine_id,
@@ -95,6 +97,7 @@ async def agent_search(
95
97
  )
96
98
  async with asyncio.timeout(timeout):
97
99
  result = await engine_service.search(body)
100
+ succeeded = True
98
101
  except TimeoutError as exc:
99
102
  record_agent_search_error(
100
103
  engine_id,
@@ -138,6 +141,14 @@ async def agent_search(
138
141
  (time.perf_counter() - started) * 1000,
139
142
  )
140
143
  raise
144
+ finally:
145
+ record_request_usage(
146
+ "agent_search",
147
+ engine_id,
148
+ units=1,
149
+ succeeded=succeeded,
150
+ started=started,
151
+ )
141
152
 
142
153
  duration = time.perf_counter() - started
143
154
  record_agent_search_success(engine_id, duration)
@@ -161,6 +172,7 @@ async def agent_search_stream(
161
172
  engine_id = engine.value if hasattr(engine, "value") else str(engine)
162
173
  timeout = body.timeout
163
174
  started = time.perf_counter()
175
+ succeeded = False
164
176
  _LOGGER.info(
165
177
  "agent-search start mode=stream engine=%s timeout=%ss",
166
178
  engine_id,
@@ -181,9 +193,23 @@ async def agent_search_stream(
181
193
  exc.code.value if hasattr(exc.code, "value") else str(exc.code),
182
194
  time.perf_counter() - started,
183
195
  )
196
+ record_request_usage(
197
+ "agent_search",
198
+ engine_id,
199
+ units=1,
200
+ succeeded=succeeded,
201
+ started=started,
202
+ )
184
203
  raise _agent_search_request_context(exc, engine_id=engine_id) from exc
185
204
  except Exception as exc:
186
205
  record_agent_search_error(engine_id, "INTERNAL_ERROR", 0.0)
206
+ record_request_usage(
207
+ "agent_search",
208
+ engine_id,
209
+ units=1,
210
+ succeeded=succeeded,
211
+ started=started,
212
+ )
187
213
  _LOGGER.exception(
188
214
  "agent-search setup error mode=stream engine=%s",
189
215
  engine_id,
@@ -194,11 +220,12 @@ async def agent_search_stream(
194
220
  ) from exc
195
221
 
196
222
  async def event_generator() -> AsyncIterator[str]:
197
- nonlocal started
223
+ nonlocal started, succeeded
198
224
  try:
199
225
  async with asyncio.timeout(timeout):
200
226
  async for event in engine_service.stream(body):
201
227
  yield _format_sse_event(event)
228
+ succeeded = True
202
229
  record_agent_search_success(engine_id, time.perf_counter() - started)
203
230
  _LOGGER.info(
204
231
  "agent-search success mode=stream engine=%s duration=%.0fms",
@@ -256,6 +283,14 @@ async def agent_search_stream(
256
283
  engine_id=engine_id,
257
284
  ),
258
285
  )
286
+ finally:
287
+ record_request_usage(
288
+ "agent_search",
289
+ engine_id,
290
+ units=1,
291
+ succeeded=succeeded,
292
+ started=started,
293
+ )
259
294
 
260
295
  return StreamingResponse(
261
296
  event_generator(),
@@ -8,7 +8,9 @@ from fastapi import Header
8
8
  from unique_search_proxy_core.context import (
9
9
  CHAT_ID_HEADER,
10
10
  COMPANY_ID_HEADER,
11
+ ENTRY_POINT_HEADER,
11
12
  LOCAL_REQUEST_CONTEXT,
13
+ MESSAGE_ID_HEADER,
12
14
  USER_ID_HEADER,
13
15
  )
14
16
 
@@ -35,6 +37,20 @@ async def document_request_context_headers(
35
37
  description="Tenant chat or session identifier.",
36
38
  ),
37
39
  ] = LOCAL_REQUEST_CONTEXT.chat_id,
40
+ x_unique_entry_point: Annotated[
41
+ str,
42
+ Header(
43
+ alias=ENTRY_POINT_HEADER,
44
+ description="Service where the search entered the platform.",
45
+ ),
46
+ ] = LOCAL_REQUEST_CONTEXT.entry_point.value,
47
+ x_unique_message_id: Annotated[
48
+ str | None,
49
+ Header(
50
+ alias=MESSAGE_ID_HEADER,
51
+ description="Message identifier, reserved for analytics.",
52
+ ),
53
+ ] = None,
38
54
  ) -> None:
39
55
  """Declare context headers on /v1 routes for Swagger; enforcement is in middleware."""
40
56
  return None
@@ -40,6 +40,7 @@ from unique_search_proxy_client.web.monitoring.metrics import (
40
40
  from unique_search_proxy_client.web.settings.request_limits import (
41
41
  request_limits_settings,
42
42
  )
43
+ from unique_search_proxy_client.web.usage import record_request_usage
43
44
 
44
45
  router = APIRouter(tags=["crawl"])
45
46
  _LOGGER = logging.getLogger(__name__)
@@ -96,6 +97,8 @@ async def crawl(
96
97
  crawler_id = body.crawler
97
98
  timeout = min(body.timeout, request_limits_settings.max_timeout_seconds)
98
99
  started = time.perf_counter()
100
+ succeeded = False
101
+ units = 0
99
102
  _LOGGER.info(
100
103
  "crawl start crawler=%s urls=%d timeout=%ss",
101
104
  crawler_id,
@@ -112,7 +115,9 @@ async def crawl(
112
115
  body.urls,
113
116
  redirect_http_client=client,
114
117
  )
118
+ units = len(gate.allowed_targets)
115
119
  if not gate.allowed_targets:
120
+ succeeded = True
116
121
  duration = time.perf_counter() - started
117
122
  record_crawl_success(
118
123
  crawler_id,
@@ -155,6 +160,7 @@ async def crawl(
155
160
  )
156
161
  else:
157
162
  crawler_results = await crawler.crawl(crawl_body)
163
+ succeeded = True
158
164
  except TimeoutError as exc:
159
165
  record_crawl_error(
160
166
  crawler_id,
@@ -198,6 +204,14 @@ async def crawl(
198
204
  (time.perf_counter() - started) * 1000,
199
205
  )
200
206
  raise
207
+ finally:
208
+ record_request_usage(
209
+ "crawl",
210
+ crawler_id,
211
+ units=units,
212
+ succeeded=succeeded,
213
+ started=started,
214
+ )
201
215
 
202
216
  duration = time.perf_counter() - started
203
217
  record_crawl_success(crawler_id, len(body.urls), duration)
@@ -34,6 +34,7 @@ from unique_search_proxy_client.web.monitoring.metrics import (
34
34
  from unique_search_proxy_client.web.settings.request_limits import (
35
35
  request_limits_settings,
36
36
  )
37
+ from unique_search_proxy_client.web.usage import record_request_usage
37
38
 
38
39
  router = APIRouter(tags=["search"])
39
40
  _LOGGER = logging.getLogger(__name__)
@@ -68,6 +69,7 @@ async def search(
68
69
  engine_id = engine.value if hasattr(engine, "value") else str(engine)
69
70
  timeout = min(body.timeout, request_limits_settings.max_timeout_seconds)
70
71
  started = time.perf_counter()
72
+ succeeded = False
71
73
  _LOGGER.info("search start engine=%s timeout=%ss", engine_id, timeout)
72
74
 
73
75
  try:
@@ -79,6 +81,7 @@ async def search(
79
81
  )
80
82
  async with asyncio.timeout(timeout):
81
83
  raw, curated = await engine.search(body)
84
+ succeeded = True
82
85
  except TimeoutError as exc:
83
86
  record_search_error(
84
87
  engine_id,
@@ -122,6 +125,16 @@ async def search(
122
125
  (time.perf_counter() - started) * 1000,
123
126
  )
124
127
  raise
128
+ finally:
129
+ record_request_usage(
130
+ "search",
131
+ engine_id,
132
+ # TODO(UN-24394): count provider pages fetched if billing needs exact call counts.
133
+ units=1,
134
+ fetch_size=body.fetch_size,
135
+ succeeded=succeeded,
136
+ started=started,
137
+ )
125
138
 
126
139
  duration = time.perf_counter() - started
127
140
  record_search_success(engine_id, duration)
@@ -13,7 +13,13 @@ _REQUEST_CONTEXT: ContextVar[RequestContext] = ContextVar(
13
13
  default=LOCAL_REQUEST_CONTEXT,
14
14
  )
15
15
 
16
- _CONTEXT_FIELDS: Final[tuple[str, ...]] = ("company_id", "user_id", "chat_id")
16
+ _CONTEXT_FIELDS: Final[tuple[str, ...]] = (
17
+ "company_id",
18
+ "user_id",
19
+ "chat_id",
20
+ "entry_point",
21
+ "caller",
22
+ )
17
23
 
18
24
 
19
25
  def bind_request_context(context: RequestContext) -> Token[RequestContext]:
@@ -15,15 +15,18 @@ _APP_LOGGER_NAMES = (
15
15
  "unique_search_proxy_core",
16
16
  )
17
17
 
18
+ _USAGE_LOGGER_NAME = "unique_search_proxy_core.usage"
19
+
18
20
  _CONTEXT_LOG_FORMAT = (
19
21
  "%(levelprefix)s "
20
22
  "company=%(company_id)s user=%(user_id)s chat=%(chat_id)s "
21
- "%(message)s"
23
+ "entry=%(entry_point)s caller=%(caller)s %(message)s"
22
24
  )
23
25
 
24
26
  _ACCESS_CONTEXT_LOG_FORMAT = (
25
27
  "%(levelprefix)s "
26
28
  "company=%(company_id)s user=%(user_id)s chat=%(chat_id)s "
29
+ "entry=%(entry_point)s caller=%(caller)s "
27
30
  '%(client_addr)s - "%(request_line)s" %(status_code)s'
28
31
  )
29
32
 
@@ -50,6 +53,18 @@ def build_logging_config(log_level: str | None = None) -> dict[str, Any]:
50
53
  "level": level_name,
51
54
  "propagate": False,
52
55
  }
56
+ # The usage line is JSON for Loki ``| json``: no prefix, never filtered by level.
57
+ config["formatters"]["usage"] = {"format": "%(message)s"}
58
+ config["handlers"]["usage"] = {
59
+ "class": "logging.StreamHandler",
60
+ "formatter": "usage",
61
+ "stream": "ext://sys.stderr",
62
+ }
63
+ config["loggers"][_USAGE_LOGGER_NAME] = {
64
+ "handlers": ["usage"],
65
+ "level": "INFO",
66
+ "propagate": False,
67
+ }
53
68
  config["loggers"]["uvicorn"]["level"] = level_name
54
69
  config["loggers"]["uvicorn.error"]["level"] = level_name
55
70
  config["loggers"]["uvicorn.access"]["level"] = level_name
@@ -1,16 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
4
+
3
5
  from starlette.types import ASGIApp, Receive, Scope, Send
4
6
  from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
5
- from unique_search_proxy_core.errors import ValidationProxyError
7
+ from unique_search_proxy_core.errors import BadRequestProxyError, ValidationProxyError
6
8
 
7
9
  from unique_search_proxy_client.web.context import (
8
10
  bind_request_context,
9
11
  reset_request_context,
10
12
  )
11
13
  from unique_search_proxy_client.web.error_handlers import proxy_error_response
14
+ from unique_search_proxy_client.web.monitoring.metrics import (
15
+ record_unattributed_request,
16
+ )
12
17
  from unique_search_proxy_client.web.settings.app import app_settings
13
18
 
19
+ _LOGGER = logging.getLogger(__name__)
20
+
14
21
  _CONTEXT_EXCLUDED_PATHS = frozenset(
15
22
  {
16
23
  "/",
@@ -23,6 +30,8 @@ _CONTEXT_EXCLUDED_PATHS = frozenset(
23
30
  }
24
31
  )
25
32
 
33
+ _ATTRIBUTION_EXEMPT_PREFIX = "/v1/configuration/"
34
+
26
35
 
27
36
  def _headers_from_scope(scope: Scope) -> dict[str, str]:
28
37
  return {
@@ -78,6 +87,29 @@ class RequestContextMiddleware:
78
87
  )
79
88
  token = bind_request_context(context)
80
89
  try:
90
+ # Warn and count by default; reject only when the setting is on.
91
+ if not context.is_attributed and not path.startswith(
92
+ _ATTRIBUTION_EXEMPT_PREFIX
93
+ ):
94
+ invalid = context.invalid_identity_headers
95
+ record_unattributed_request(context.entry_point.value)
96
+ _LOGGER.warning("Unattributed request, invalid headers: %s", invalid)
97
+ if app_settings.reject_unattributed_requests:
98
+ response = proxy_error_response(
99
+ BadRequestProxyError(
100
+ f"Invalid context headers: {', '.join(invalid)}",
101
+ details=[
102
+ {
103
+ "loc": ["header", header_name],
104
+ "msg": "Must be a numeric id",
105
+ "type": "invalid",
106
+ }
107
+ for header_name in invalid
108
+ ],
109
+ ),
110
+ )
111
+ await response(scope, receive, send)
112
+ return
81
113
  await self.app(scope, receive, send)
82
114
  finally:
83
115
  reset_request_context(token)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Iterable
4
4
 
5
+ from unique_search_proxy_core.context import RequestContext
5
6
  from unique_toolkit.monitoring import MetricNamespace
6
7
 
7
8
  m = MetricNamespace("unique_search_proxy")
@@ -117,6 +118,17 @@ agent_search_errors_total = m.counter(
117
118
  ["engine", "error_code"],
118
119
  )
119
120
 
121
+ requests_total = m.counter(
122
+ "requests_total",
123
+ "Requests that reached a handler (company_id is 'unattributed' without numeric ids)",
124
+ ["company_id", "entry_point", "endpoint", "provider", "status"],
125
+ )
126
+ unattributed_requests_total = m.counter(
127
+ "unattributed_requests_total",
128
+ "Requests without a numeric company and user id",
129
+ ["entry_point"],
130
+ )
131
+
120
132
  proxy_errors_total = m.counter(
121
133
  "proxy_errors_total",
122
134
  "Top-level API errors returned to clients",
@@ -124,37 +136,23 @@ proxy_errors_total = m.counter(
124
136
  )
125
137
 
126
138
 
127
- def _metrics_enabled() -> bool:
128
- from unique_search_proxy_client.web.settings.monitoring import prometheus_settings
129
-
130
- return prometheus_settings.enabled
131
-
132
-
133
139
  def record_search_success(engine: str, duration_seconds: float) -> None:
134
- if not _metrics_enabled():
135
- return
136
140
  search_total.labels(engine=engine).inc()
137
141
  search_duration_seconds.labels(engine=engine).observe(duration_seconds)
138
142
 
139
143
 
140
144
  def record_search_error(engine: str, error_code: str, duration_seconds: float) -> None:
141
- if not _metrics_enabled():
142
- return
143
145
  search_errors_total.labels(engine=engine, error_code=error_code).inc()
144
146
  search_duration_seconds.labels(engine=engine).observe(duration_seconds)
145
147
 
146
148
 
147
149
  def record_crawl_success(crawler: str, url_count: int, duration_seconds: float) -> None:
148
- if not _metrics_enabled():
149
- return
150
150
  crawl_total.labels(crawler=crawler).inc()
151
151
  crawl_urls_total.labels(crawler=crawler).inc(url_count)
152
152
  crawl_duration_seconds.labels(crawler=crawler).observe(duration_seconds)
153
153
 
154
154
 
155
155
  def record_crawl_error(crawler: str, error_code: str, duration_seconds: float) -> None:
156
- if not _metrics_enabled():
157
- return
158
156
  crawl_errors_total.labels(crawler=crawler, error_code=error_code).inc()
159
157
  crawl_duration_seconds.labels(crawler=crawler).observe(duration_seconds)
160
158
 
@@ -171,8 +169,6 @@ def record_crawl_url_outcomes(
171
169
  ``http_status`` is the upstream HTTP status (e.g. ``"403"``) when the failure
172
170
  was an HTTP error or ``""`` when no HTTP response was received.
173
171
  """
174
- if not _metrics_enabled():
175
- return
176
172
  for outcome, error_code, http_status in outcomes:
177
173
  crawl_url_outcomes_total.labels(
178
174
  crawler=crawler,
@@ -183,20 +179,14 @@ def record_crawl_url_outcomes(
183
179
 
184
180
 
185
181
  def record_crawl_blocked(reason_category: str, count: int = 1) -> None:
186
- if not _metrics_enabled():
187
- return
188
182
  crawl_blocked_total.labels(reason_category=reason_category).inc(count)
189
183
 
190
184
 
191
185
  def set_url_safety_application_checks_enabled(enabled: bool) -> None:
192
- if not _metrics_enabled():
193
- return
194
186
  url_safety_application_checks_enabled.set(int(enabled))
195
187
 
196
188
 
197
189
  def record_agent_search_success(engine: str, duration_seconds: float) -> None:
198
- if not _metrics_enabled():
199
- return
200
190
  agent_search_total.labels(engine=engine).inc()
201
191
  agent_search_duration_seconds.labels(engine=engine).observe(duration_seconds)
202
192
 
@@ -206,13 +196,28 @@ def record_agent_search_error(
206
196
  error_code: str,
207
197
  duration_seconds: float,
208
198
  ) -> None:
209
- if not _metrics_enabled():
210
- return
211
199
  agent_search_errors_total.labels(engine=engine, error_code=error_code).inc()
212
200
  agent_search_duration_seconds.labels(engine=engine).observe(duration_seconds)
213
201
 
214
202
 
215
203
  def record_proxy_error(error_code: str) -> None:
216
- if not _metrics_enabled():
217
- return
218
204
  proxy_errors_total.labels(error_code=error_code).inc()
205
+
206
+
207
+ def record_request(
208
+ context: RequestContext,
209
+ endpoint: str,
210
+ provider: str,
211
+ status: str,
212
+ ) -> None:
213
+ requests_total.labels(
214
+ company_id=context.company_id if context.is_attributed else "unattributed",
215
+ entry_point=context.entry_point.value,
216
+ endpoint=endpoint,
217
+ provider=provider,
218
+ status=status,
219
+ ).inc()
220
+
221
+
222
+ def record_unattributed_request(entry_point: str) -> None:
223
+ unattributed_requests_total.labels(entry_point=entry_point).inc()
@@ -15,6 +15,7 @@ APP_ENV_PREFIX = ""
15
15
  )
16
16
  class AppSettings(BaseSettings):
17
17
  require_context_headers: bool = True
18
+ reject_unattributed_requests: bool = False
18
19
  expose_docs: bool = False
19
20
 
20
21
 
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Literal
5
+
6
+ from unique_search_proxy_core.usage import UsageRecord, record_usage
7
+
8
+ from unique_search_proxy_client.web.context import get_request_context
9
+ from unique_search_proxy_client.web.monitoring.metrics import record_request
10
+
11
+
12
+ def record_request_usage(
13
+ endpoint: Literal["search", "agent_search", "crawl"],
14
+ provider: str,
15
+ *,
16
+ units: int,
17
+ succeeded: bool,
18
+ started: float,
19
+ fetch_size: int | None = None,
20
+ ) -> None:
21
+ """Emit the usage log line and counter for the current request."""
22
+ context = get_request_context()
23
+ status = "success" if succeeded else "error"
24
+ record_usage(
25
+ UsageRecord(
26
+ company_id=context.company_id,
27
+ user_id=context.user_id,
28
+ chat_id=context.chat_id,
29
+ message_id=context.message_id,
30
+ entry_point=context.entry_point,
31
+ caller=context.caller,
32
+ endpoint=endpoint,
33
+ provider=provider,
34
+ units=units,
35
+ fetch_size=fetch_size,
36
+ status=status,
37
+ duration_ms=round((time.perf_counter() - started) * 1000),
38
+ ),
39
+ )
40
+ record_request(context, endpoint, provider, status)