unique-search-proxy 2026.44.0.dev8__tar.gz → 2026.44.0.dev10__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 (119) hide show
  1. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/PKG-INFO +2 -2
  2. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/pyproject.toml +2 -2
  3. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/service.py +77 -6
  4. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/registry.py +4 -0
  5. unique_search_proxy-2026.44.0.dev10/unique_search_proxy_client/web/settings/request_limits.py +60 -0
  6. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/README.md +0 -0
  7. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/__init__.py +0 -0
  8. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/__init__.py +0 -0
  9. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/__init__.py +0 -0
  10. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/health.py +0 -0
  11. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
  12. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
  13. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
  14. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/v1/context_headers.py +0 -0
  15. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/v1/crawl.py +0 -0
  16. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
  17. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/api/v1/search.py +0 -0
  18. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/app.py +0 -0
  19. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/context.py +0 -0
  20. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/__init__.py +0 -0
  21. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
  22. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/bing/cleanup.py +0 -0
  23. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
  24. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
  25. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
  26. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
  27. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
  28. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
  29. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +0 -0
  30. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
  31. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/vertexai/redirects.py +0 -0
  32. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
  33. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
  34. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/client/azure_transport.py +0 -0
  35. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/client/credentials.py +0 -0
  36. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/client/service.py +0 -0
  37. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
  38. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
  39. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
  40. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
  41. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
  42. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
  43. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
  44. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
  45. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
  46. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
  47. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
  48. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
  49. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
  50. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
  51. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
  52. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
  53. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
  54. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
  55. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
  56. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
  57. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
  58. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/provider_response.py +0 -0
  59. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/providers.py +0 -0
  60. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/registry.py +0 -0
  61. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
  62. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
  63. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
  64. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
  65. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
  66. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/custom_api/__init__.py +0 -0
  67. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/custom_api/service.py +0 -0
  68. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
  69. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
  70. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
  71. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
  72. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
  73. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
  74. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
  75. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
  76. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
  77. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
  78. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
  79. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
  80. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
  81. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/error_handlers.py +0 -0
  82. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/__init__.py +0 -0
  83. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
  84. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/generator/introspect.py +0 -0
  85. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/generator/schema.py +0 -0
  86. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/generator/template.py +0 -0
  87. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
  88. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
  89. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/metadata.py +0 -0
  90. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/helm/url_safety_registration.py +0 -0
  91. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/logging_config.py +0 -0
  92. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/middleware/context.py +0 -0
  93. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
  94. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
  95. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
  96. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/presets/__init__.py +0 -0
  97. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/presets/common.py +0 -0
  98. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/presets/crawl.py +0 -0
  99. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/presets/search.py +0 -0
  100. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/presets/types.py +0 -0
  101. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/__init__.py +0 -0
  102. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/app.py +0 -0
  103. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/base.py +0 -0
  104. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/client.py +0 -0
  105. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
  106. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
  107. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
  108. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
  109. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
  110. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
  111. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
  112. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
  113. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
  114. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
  115. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
  116. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
  117. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/startup_report.py +0 -0
  118. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/utils/__init__.py +0 -0
  119. {unique_search_proxy-2026.44.0.dev8 → unique_search_proxy-2026.44.0.dev10}/unique_search_proxy_client/web/utils/url.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-search-proxy
3
- Version: 2026.44.0.dev8
3
+ Version: 2026.44.0.dev10
4
4
  Summary: Web Search Proxy implementation
5
5
  Author: ThePhilAz
6
6
  Author-email: ThePhilAz <rami.azouz@philico.com>
@@ -21,7 +21,7 @@ Requires-Dist: google-genai>=1.73.0,<2
21
21
  Requires-Dist: google-auth>=2.43.0,<3
22
22
  Requires-Dist: jinja2>=3.1.6,<4
23
23
  Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev9,<2026.44.0rc0
24
- Requires-Dist: unique-search-proxy-core>=2026.44.0.dev7,<2026.44.0rc0
24
+ Requires-Dist: unique-search-proxy-core>=2026.44.0.dev9,<2026.44.0rc0
25
25
  Requires-Dist: openai>=1.105.0,<3
26
26
  Requires-Python: >=3.12
27
27
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy"
3
- version = "2026.44.0.dev8"
3
+ version = "2026.44.0.dev10"
4
4
  description = "Web Search Proxy implementation"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -23,7 +23,7 @@ dependencies = [
23
23
  "google-auth>=2.43.0,<3",
24
24
  "jinja2>=3.1.6,<4",
25
25
  "unique-toolkit[monitoring]>=2026.44.0.dev9,<2026.44.0rc0",
26
- "unique-search-proxy-core>=2026.44.0.dev7,<2026.44.0rc0",
26
+ "unique-search-proxy-core>=2026.44.0.dev9,<2026.44.0rc0",
27
27
  "openai>=1.105.0,<3",
28
28
  ]
29
29
 
@@ -10,6 +10,8 @@ from unique_search_proxy_core.crawlers.basic.processing.policy import (
10
10
  ContentTypeHandlerPolicy,
11
11
  )
12
12
  from unique_search_proxy_core.crawlers.basic.schema import BasicCrawlRequest
13
+ from unique_search_proxy_core.errors import ResponseTooLargeError
14
+ from unique_search_proxy_core.http_client import ByteBudget, read_capped_body
13
15
  from unique_search_proxy_core.schema import (
14
16
  CrawlUrlResult,
15
17
  ProxyErrorCode,
@@ -18,7 +20,7 @@ from unique_search_proxy_core.url_safety import (
18
20
  CrawlTargetValidationError,
19
21
  ResolvedCrawlTarget,
20
22
  bypass_crawl_target,
21
- safe_pinned_httpx_get,
23
+ safe_pinned_httpx_stream,
22
24
  url_safety_settings,
23
25
  )
24
26
 
@@ -37,9 +39,18 @@ from unique_search_proxy_client.web.core.provider_response import (
37
39
  )
38
40
  from unique_search_proxy_client.web.core.url_safety.gate import AllowedCrawlTarget
39
41
  from unique_search_proxy_client.web.monitoring.metrics import record_crawl_blocked
42
+ from unique_search_proxy_client.web.settings.client import http_client_settings
43
+ from unique_search_proxy_client.web.settings.request_limits import (
44
+ request_limits_settings,
45
+ )
40
46
 
41
47
  _LOGGER = logging.getLogger(__name__)
42
48
 
49
+ # Only encodings whose decoded size read_capped_body can bound in small steps;
50
+ # brotli and zstd reach far higher compression ratios.
51
+ _ACCEPT_ENCODING = "gzip, deflate"
52
+ _COMPRESSING_CONTENT_ENCODINGS = frozenset({"gzip", "deflate"})
53
+
43
54
 
44
55
  def _content_type_from_response(response: httpx.Response) -> str | None:
45
56
  header = response.headers.get("content-type")
@@ -48,6 +59,25 @@ def _content_type_from_response(response: httpx.Response) -> str | None:
48
59
  return str(header).split(";")[0].strip().lower() or None
49
60
 
50
61
 
62
+ def _unsupported_content_encoding(response: httpx.Response) -> str | None:
63
+ encodings = [
64
+ encoding.strip().lower()
65
+ for encoding in response.headers.get_list(
66
+ "content-encoding",
67
+ split_commas=True,
68
+ )
69
+ ]
70
+ for encoding in encodings:
71
+ if encoding not in {"", "identity", *_COMPRESSING_CONTENT_ENCODINGS}:
72
+ return encoding
73
+ # httpx decodes stacked layers in a single step, so a second compressing
74
+ # layer would inflate past read_capped_body before it can check the size.
75
+ compressing = [e for e in encodings if e in _COMPRESSING_CONTENT_ENCODINGS]
76
+ if len(compressing) > 1:
77
+ return ", ".join(compressing)
78
+ return None
79
+
80
+
51
81
  class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
52
82
  """Fetch URLs over HTTP and return the response body plus content type."""
53
83
 
@@ -79,6 +109,7 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
79
109
 
80
110
  timeout = request.timeout
81
111
  semaphore = asyncio.Semaphore(request.max_concurrent_requests)
112
+ budget = ByteBudget(request_limits_settings.max_crawl_response_bytes)
82
113
  return list(
83
114
  await asyncio.gather(
84
115
  *[
@@ -88,6 +119,7 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
88
119
  resolved_target=allowed_target.resolved,
89
120
  timeout=timeout,
90
121
  semaphore=semaphore,
122
+ budget=budget,
91
123
  content_type_handlers=request.content_types.to_handlers(),
92
124
  )
93
125
  for allowed_target in allowed_targets
@@ -103,18 +135,51 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
103
135
  resolved_target: ResolvedCrawlTarget,
104
136
  timeout: int,
105
137
  semaphore: asyncio.Semaphore,
138
+ budget: ByteBudget,
106
139
  content_type_handlers: dict[str, ContentTypeHandlerPolicy],
107
140
  ) -> CrawlUrlResult:
108
141
  async with semaphore:
142
+ if budget.exhausted:
143
+ return crawl_upstream_error(
144
+ display_url,
145
+ "Crawl response budget exhausted before this URL was fetched",
146
+ content_type=None,
147
+ code=ProxyErrorCode.RESPONSE_TOO_LARGE.value,
148
+ )
109
149
  try:
110
- response = await safe_pinned_httpx_get(
150
+ async with safe_pinned_httpx_stream(
111
151
  client,
112
152
  resolved_target,
113
- headers={"User-Agent": random_user_agent()},
153
+ headers={
154
+ "User-Agent": random_user_agent(),
155
+ "Accept-Encoding": _ACCEPT_ENCODING,
156
+ },
114
157
  timeout=Timeout(timeout),
115
158
  max_redirect_hops=url_safety_settings.max_redirect_hops,
116
159
  enforce_url_safety=url_safety_settings.enabled,
117
- )
160
+ ) as response:
161
+ content_type = _content_type_from_response(response)
162
+ unsupported_encoding = _unsupported_content_encoding(response)
163
+ if unsupported_encoding is not None:
164
+ _LOGGER.warning(
165
+ "Basic crawl rejected content encoding %s",
166
+ unsupported_encoding,
167
+ )
168
+ return crawl_upstream_error(
169
+ display_url,
170
+ f"Unsupported content encoding: {unsupported_encoding}",
171
+ content_type=content_type,
172
+ status_code=response.status_code,
173
+ )
174
+ body = await read_capped_body(
175
+ response,
176
+ max_bytes=http_client_settings.max_response_bytes,
177
+ budget=budget,
178
+ )
179
+ raw_body = body.decode(
180
+ response.encoding or "utf-8",
181
+ errors="replace",
182
+ )
118
183
  except CrawlTargetValidationError as exc:
119
184
  blocked = exc.blocked_targets[0]
120
185
  record_crawl_blocked(blocked.category)
@@ -123,6 +188,14 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
123
188
  blocked.category,
124
189
  )
125
190
  return crawl_forbidden_target(display_url, blocked.reason)
191
+ except ResponseTooLargeError as exc:
192
+ _LOGGER.warning("Basic crawl response too large: %s", exc.message)
193
+ return crawl_upstream_error(
194
+ display_url,
195
+ exc.message,
196
+ content_type=None,
197
+ code=ProxyErrorCode.RESPONSE_TOO_LARGE.value,
198
+ )
126
199
  except httpx.TimeoutException as exc:
127
200
  _LOGGER.warning(
128
201
  "Basic crawl timed out error_type=%s",
@@ -147,8 +220,6 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
147
220
  raw=transport_error_raw(exc),
148
221
  )
149
222
 
150
- content_type = _content_type_from_response(response)
151
- raw_body = response.text
152
223
  if response.is_error:
153
224
  _LOGGER.warning(
154
225
  "Basic crawl HTTP error http_status=%s",
@@ -31,6 +31,9 @@ from unique_search_proxy_client.web.settings.providers import (
31
31
  tavily_crawl_credentials,
32
32
  vertexai_agent_credentials,
33
33
  )
34
+ from unique_search_proxy_client.web.settings.request_limits import (
35
+ request_limits_settings,
36
+ )
34
37
 
35
38
 
36
39
  @dataclass(frozen=True)
@@ -61,6 +64,7 @@ _REGISTERED_INSTANCES: tuple[BaseSettings, ...] = (
61
64
  jina_crawl_credentials,
62
65
  firecrawl_crawl_credentials,
63
66
  http_client_settings,
67
+ request_limits_settings,
64
68
  prometheus_settings,
65
69
  )
66
70
 
@@ -0,0 +1,60 @@
1
+ """Server-side ceilings that bound what a single proxy request may consume."""
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+ from unique_search_proxy_client.web.helm.metadata import helm_settings
7
+ from unique_search_proxy_client.web.settings.base import get_settings
8
+
9
+ REQUEST_LIMITS_ENV_PREFIX = "REQUEST_LIMITS_"
10
+
11
+
12
+ @helm_settings(
13
+ title="Request Limits",
14
+ helm_key="requestLimits",
15
+ kind="internal",
16
+ gated=False,
17
+ egress=None,
18
+ env_prefix=REQUEST_LIMITS_ENV_PREFIX,
19
+ sections={
20
+ "crawl": ["max_crawl_response_kib"],
21
+ },
22
+ )
23
+ class RequestLimitsSettings(BaseSettings):
24
+ """Ceilings callers cannot raise, sized against the pod memory limit.
25
+
26
+ Byte sizes are in KiB: Helm renders numbers >= 1e6 in scientific notation,
27
+ which pydantic cannot parse back into an int.
28
+ """
29
+
30
+ model_config = SettingsConfigDict(
31
+ env_prefix=REQUEST_LIMITS_ENV_PREFIX,
32
+ extra="ignore",
33
+ )
34
+
35
+ max_crawl_response_kib: int = Field(
36
+ default=10 * 1024,
37
+ ge=1,
38
+ description=(
39
+ "Combined decoded size, in KiB, of all page bodies one crawl request "
40
+ "may keep. URLs read after the budget is spent fail individually."
41
+ ),
42
+ )
43
+
44
+ @property
45
+ def max_crawl_response_bytes(self) -> int:
46
+ return self.max_crawl_response_kib * 1024
47
+
48
+
49
+ def get_request_limits_settings() -> RequestLimitsSettings:
50
+ return get_settings(RequestLimitsSettings, env_prefix=REQUEST_LIMITS_ENV_PREFIX)
51
+
52
+
53
+ request_limits_settings: RequestLimitsSettings = get_request_limits_settings()
54
+
55
+ __all__ = [
56
+ "REQUEST_LIMITS_ENV_PREFIX",
57
+ "RequestLimitsSettings",
58
+ "get_request_limits_settings",
59
+ "request_limits_settings",
60
+ ]