unique-search-proxy 2026.26.0.dev10__tar.gz → 2026.26.0.dev12__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 (100) hide show
  1. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/PKG-INFO +2 -2
  2. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/pyproject.toml +2 -2
  3. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/app.py +18 -15
  4. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +4 -3
  5. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/client/service.py +6 -3
  6. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +4 -3
  7. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/jina/service.py +2 -1
  8. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +2 -1
  9. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/service.py +2 -1
  10. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/service.py +2 -1
  11. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +2 -1
  12. unique_search_proxy-2026.26.0.dev12/unique_search_proxy_client/web/logging_config.py +39 -0
  13. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/base.py +2 -3
  14. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/client.py +5 -4
  15. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/base.py +20 -5
  16. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/brave.py +2 -1
  17. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/firecrawl.py +2 -1
  18. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/google.py +2 -1
  19. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/jina.py +2 -1
  20. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/perplexity.py +2 -1
  21. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/tavily.py +2 -1
  22. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +2 -1
  23. unique_search_proxy-2026.26.0.dev12/unique_search_proxy_client/web/settings/secret_str.py +41 -0
  24. unique_search_proxy-2026.26.0.dev12/unique_search_proxy_client/web/settings/startup_log.py +29 -0
  25. unique_search_proxy-2026.26.0.dev12/unique_search_proxy_client/web/settings/startup_report.py +138 -0
  26. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/README.md +0 -0
  27. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/__init__.py +0 -0
  28. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/__init__.py +0 -0
  29. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/__init__.py +0 -0
  30. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/health.py +0 -0
  31. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
  32. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
  33. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
  34. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/crawl.py +0 -0
  35. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
  36. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/api/v1/search.py +0 -0
  37. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/__init__.py +0 -0
  38. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
  39. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
  40. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
  41. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
  42. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
  43. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/serialization.py +0 -0
  44. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
  45. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
  46. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
  47. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
  48. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
  49. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
  50. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
  51. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
  52. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
  53. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
  54. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
  55. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
  56. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
  57. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
  58. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
  59. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/service.py +0 -0
  60. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
  61. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
  62. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
  63. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
  64. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
  65. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
  66. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
  67. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
  68. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/provider_response.py +0 -0
  69. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/providers.py +0 -0
  70. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/registry.py +0 -0
  71. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
  72. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
  73. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
  74. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
  75. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
  76. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
  77. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
  78. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
  79. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
  80. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
  81. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
  82. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
  83. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
  84. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
  85. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
  86. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/error_handlers.py +0 -0
  87. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
  88. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
  89. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
  90. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/__init__.py +0 -0
  91. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/common.py +0 -0
  92. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/crawl.py +0 -0
  93. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/search.py +0 -0
  94. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/presets/types.py +0 -0
  95. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/__init__.py +0 -0
  96. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
  97. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
  98. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
  99. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/unique_search_proxy_client/web/utils/__init__.py +0 -0
  100. {unique_search_proxy-2026.26.0.dev10 → unique_search_proxy-2026.26.0.dev12}/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.26.0.dev10
3
+ Version: 2026.26.0.dev12
4
4
  Summary: Web Search Proxy implementation
5
5
  Author: ThePhilAz
6
6
  Author-email: ThePhilAz <rami.azouz@philico.com>
@@ -19,7 +19,7 @@ Requires-Dist: certifi>=2025.11.12,<2027
19
19
  Requires-Dist: google-genai>=1.73.0,<2
20
20
  Requires-Dist: google-auth>=2.43.0,<3
21
21
  Requires-Dist: unique-toolkit[monitoring]>=2026.26.0.dev11,<2026.26.0rc0
22
- Requires-Dist: unique-search-proxy-core>=2026.26.0.dev6,<2026.26.0rc0
22
+ Requires-Dist: unique-search-proxy-core>=2026.26.0.dev7,<2026.26.0rc0
23
23
  Requires-Python: >=3.12
24
24
  Description-Content-Type: text/markdown
25
25
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy"
3
- version = "2026.26.0.dev10"
3
+ version = "2026.26.0.dev12"
4
4
  description = "Web Search Proxy implementation"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -21,7 +21,7 @@ dependencies = [
21
21
  "google-genai>=1.73.0,<2",
22
22
  "google-auth>=2.43.0,<3",
23
23
  "unique-toolkit[monitoring]>=2026.26.0.dev11,<2026.26.0rc0",
24
- "unique-search-proxy-core>=2026.26.0.dev6,<2026.26.0rc0",
24
+ "unique-search-proxy-core>=2026.26.0.dev7,<2026.26.0rc0",
25
25
  ]
26
26
 
27
27
  [dependency-groups]
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import os
4
+ import sys
5
5
  from contextlib import asynccontextmanager
6
6
 
7
7
  from dotenv import load_dotenv
@@ -13,22 +13,19 @@ from unique_search_proxy_client.web.api import health_router, v1_router
13
13
  from unique_search_proxy_client.web.core.client.service import create_http_client_pool
14
14
  from unique_search_proxy_client.web.core.providers import register_builtin_providers
15
15
  from unique_search_proxy_client.web.error_handlers import register_exception_handlers
16
+ from unique_search_proxy_client.web.logging_config import (
17
+ build_logging_config,
18
+ configure_logging,
19
+ )
16
20
  from unique_search_proxy_client.web.monitoring import setup_prometheus
21
+ from unique_search_proxy_client.web.settings.startup_report import (
22
+ log_startup_settings_report,
23
+ )
17
24
 
18
- load_dotenv()
25
+ if "pytest" not in sys.modules:
26
+ load_dotenv()
19
27
 
20
-
21
- def _configure_logging() -> None:
22
- level_name = os.getenv("LOG_LEVEL", "INFO").upper()
23
- level = getattr(logging, level_name, logging.INFO)
24
- logging.basicConfig(
25
- level=level,
26
- format="%(levelname)s %(name)s: %(message)s",
27
- force=True,
28
- )
29
-
30
-
31
- _configure_logging()
28
+ configure_logging()
32
29
  suppress_httpx_request_logs()
33
30
 
34
31
  _LOGGER = logging.getLogger(__name__)
@@ -52,6 +49,7 @@ logging.getLogger("uvicorn.access").addFilter(HealthCheckFilter())
52
49
  @asynccontextmanager
53
50
  async def lifespan(app: FastAPI):
54
51
  _LOGGER.info("Starting Unique Search Proxy...")
52
+ log_startup_settings_report(_LOGGER)
55
53
  pool = await create_http_client_pool()
56
54
  app.state.http_client_pool = pool
57
55
  try:
@@ -95,4 +93,9 @@ app = create_app()
95
93
  if __name__ == "__main__":
96
94
  import uvicorn
97
95
 
98
- uvicorn.run(app, host="0.0.0.0", port=2349)
96
+ uvicorn.run(
97
+ app,
98
+ host="0.0.0.0",
99
+ port=2349,
100
+ log_config=build_logging_config(),
101
+ )
@@ -8,6 +8,7 @@ from google.auth import load_credentials_from_dict
8
8
  from google.genai._api_client import BaseApiClient
9
9
  from google.genai.client import AsyncClient
10
10
 
11
+ from unique_search_proxy_client.web.settings.providers.base import read_secret
11
12
  from unique_search_proxy_client.web.settings.providers.vertexai_agent import (
12
13
  vertexai_agent_credentials,
13
14
  )
@@ -22,9 +23,9 @@ def _get_base_api_client_from_service_account() -> BaseApiClient:
22
23
  "https://www.googleapis.com/auth/cloud-platform",
23
24
  ]
24
25
  service_account_info = json.loads(
25
- b64decode(vertexai_agent_credentials.service_account_credentials).decode(
26
- "utf-8"
27
- ),
26
+ b64decode(
27
+ read_secret(vertexai_agent_credentials.service_account_credentials)
28
+ ).decode("utf-8"),
28
29
  )
29
30
  credentials, project_id = load_credentials_from_dict(
30
31
  service_account_info,
@@ -14,6 +14,8 @@ from unique_search_proxy_client.web.settings.client import (
14
14
  ProxyConfig,
15
15
  http_client_settings,
16
16
  )
17
+ from unique_search_proxy_client.web.settings.providers.base import read_secret
18
+ from unique_search_proxy_client.web.settings.secret_str import read_secret_headers
17
19
 
18
20
  if TYPE_CHECKING:
19
21
  from fastapi import FastAPI
@@ -51,7 +53,8 @@ def _build_proxy_url_with_username_password(settings: HttpClientSettings) -> str
51
53
  raise ValueError("Proxy username and password are required")
52
54
  return (
53
55
  f"{settings.proxy_protocol}://"
54
- f"{proxy_username}:{proxy_password}@{proxy_host}:{proxy_port}"
56
+ f"{read_secret(proxy_username)}:{read_secret(proxy_password)}"
57
+ f"@{proxy_host}:{proxy_port}"
55
58
  )
56
59
 
57
60
 
@@ -92,7 +95,7 @@ def _get_username_password_proxy_kwargs(settings: HttpClientSettings) -> ProxyCo
92
95
  )
93
96
  return ProxyConfig(
94
97
  proxy=proxy_url,
95
- headers=settings.proxy_headers,
98
+ headers=read_secret_headers(settings.proxy_headers) or None,
96
99
  verify=settings.proxy_ssl_ca_bundle_path or True,
97
100
  )
98
101
 
@@ -104,7 +107,7 @@ def _get_ssl_tls_proxy_kwargs(settings: HttpClientSettings) -> ProxyConfig:
104
107
  return ProxyConfig(
105
108
  proxy=proxy_url,
106
109
  cert=cert_args,
107
- headers=settings.proxy_headers,
110
+ headers=read_secret_headers(settings.proxy_headers) or None,
108
111
  verify=settings.proxy_ssl_ca_bundle_path or True,
109
112
  )
110
113
 
@@ -25,6 +25,7 @@ from unique_search_proxy_client.web.core.provider_response import (
25
25
  upstream_error_message,
26
26
  upstream_response_raw,
27
27
  )
28
+ from unique_search_proxy_client.web.settings.providers.base import read_secret
28
29
  from unique_search_proxy_client.web.settings.providers.firecrawl import (
29
30
  firecrawl_crawl_credentials as credentials,
30
31
  )
@@ -90,7 +91,7 @@ class FirecrawlCrawlerService(BaseCrawler[FirecrawlCrawlRequest]):
90
91
  response = await client.post(
91
92
  credentials.scrape_endpoint,
92
93
  json=body,
93
- headers=_firecrawl_headers(credentials.api_key),
94
+ headers=_firecrawl_headers(read_secret(credentials.api_key)),
94
95
  timeout=timeout,
95
96
  )
96
97
  except httpx.TimeoutException as exc:
@@ -149,7 +150,7 @@ class FirecrawlCrawlerService(BaseCrawler[FirecrawlCrawlRequest]):
149
150
  start_response = await client.post(
150
151
  credentials.batch_scrape_endpoint,
151
152
  json=body,
152
- headers=_firecrawl_headers(credentials.api_key),
153
+ headers=_firecrawl_headers(read_secret(credentials.api_key)),
153
154
  timeout=timeout,
154
155
  )
155
156
  except httpx.TimeoutException as exc:
@@ -200,7 +201,7 @@ class FirecrawlCrawlerService(BaseCrawler[FirecrawlCrawlRequest]):
200
201
  final_payload = await poll_batch_scrape(
201
202
  client,
202
203
  status_url=status_url,
203
- api_key=credentials.api_key,
204
+ api_key=read_secret(credentials.api_key),
204
205
  deadline=deadline,
205
206
  )
206
207
  except TimeoutError as exc:
@@ -18,6 +18,7 @@ from unique_search_proxy_client.web.core.provider_response import (
18
18
  transport_error_raw,
19
19
  upstream_response_raw,
20
20
  )
21
+ from unique_search_proxy_client.web.settings.providers.base import read_secret
21
22
  from unique_search_proxy_client.web.settings.providers.jina import (
22
23
  jina_crawl_credentials as credentials,
23
24
  )
@@ -70,7 +71,7 @@ class JinaCrawlerService(BaseCrawler[JinaCrawlRequest]):
70
71
 
71
72
  urls = list(request.urls)
72
73
  timeout = request.timeout
73
- headers = _jina_headers(credentials.api_key)
74
+ headers = _jina_headers(read_secret(credentials.api_key))
74
75
  semaphore = asyncio.Semaphore(request.max_concurrent_requests)
75
76
 
76
77
  async def crawl_one(url: str) -> CrawlUrlResult:
@@ -19,6 +19,7 @@ from unique_search_proxy_client.web.core.provider_response import (
19
19
  upstream_error_message,
20
20
  upstream_response_raw,
21
21
  )
22
+ from unique_search_proxy_client.web.settings.providers.base import read_secret
22
23
  from unique_search_proxy_client.web.settings.providers.tavily import (
23
24
  tavily_crawl_credentials as credentials,
24
25
  )
@@ -132,7 +133,7 @@ class TavilyCrawlerService(BaseCrawler[TavilyCrawlRequest]):
132
133
  response = await client.post(
133
134
  credentials.extract_endpoint,
134
135
  json=body,
135
- headers=_tavily_headers(credentials.api_key),
136
+ headers=_tavily_headers(read_secret(credentials.api_key)),
136
137
  timeout=timeout,
137
138
  )
138
139
  except httpx.TimeoutException as exc:
@@ -37,6 +37,7 @@ from unique_search_proxy_client.web.core.search_engines.pagination import PageRe
37
37
  from unique_search_proxy_client.web.core.search_engines.service_base import (
38
38
  SearchEngineService,
39
39
  )
40
+ from unique_search_proxy_client.web.settings.providers.base import read_secret
40
41
  from unique_search_proxy_client.web.settings.providers.brave import (
41
42
  brave_search_credentials as credentials,
42
43
  )
@@ -71,7 +72,7 @@ class BraveSearchService(SearchEngineService[BraveSearchRequest]):
71
72
  break
72
73
  page = await self._fetch_page(
73
74
  request=request,
74
- api_key=credentials.api_key,
75
+ api_key=read_secret(credentials.api_key),
75
76
  api_endpoint=credentials.api_endpoint,
76
77
  page=page_request,
77
78
  timeout=timeout,
@@ -37,6 +37,7 @@ from unique_search_proxy_client.web.core.search_engines.pagination import PageRe
37
37
  from unique_search_proxy_client.web.core.search_engines.service_base import (
38
38
  SearchEngineService,
39
39
  )
40
+ from unique_search_proxy_client.web.settings.providers.base import read_secret
40
41
  from unique_search_proxy_client.web.settings.providers.google import (
41
42
  google_search_credentials as credentials,
42
43
  )
@@ -71,7 +72,7 @@ class GoogleSearchService(SearchEngineService[GoogleSearchRequest]):
71
72
  for page_request in iter_google_page_requests(fetch_size):
72
73
  page = await self._fetch_page(
73
74
  request=request,
74
- api_key=credentials.api_key,
75
+ api_key=read_secret(credentials.api_key),
75
76
  search_engine_id=search_engine_id,
76
77
  api_endpoint=credentials.api_endpoint,
77
78
  page=page_request,
@@ -36,6 +36,7 @@ from unique_search_proxy_client.web.core.search_engines.service_base import (
36
36
  from unique_search_proxy_client.web.settings.providers import (
37
37
  perplexity_search_credentials as credentials,
38
38
  )
39
+ from unique_search_proxy_client.web.settings.providers.base import read_secret
39
40
 
40
41
  _LOGGER = logging.getLogger(__name__)
41
42
  _PERPLEXITY_PROVIDER_LABEL = "Perplexity Search API"
@@ -67,7 +68,7 @@ class PerplexitySearchService(SearchEngineService[PerplexitySearchRequest]):
67
68
  response = await client.post(
68
69
  credentials.api_endpoint,
69
70
  json=body,
70
- headers=_perplexity_headers(credentials.api_key),
71
+ headers=_perplexity_headers(read_secret(credentials.api_key)),
71
72
  timeout=timeout,
72
73
  )
73
74
  except httpx.TimeoutException as exc:
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import logging
5
+ import logging.config
6
+ import os
7
+ from typing import Any
8
+
9
+ from uvicorn.config import LOG_LEVELS, LOGGING_CONFIG
10
+
11
+ _APP_LOGGER_NAMES = (
12
+ "unique_search_proxy_client",
13
+ "unique_search_proxy_core",
14
+ )
15
+
16
+
17
+ def build_logging_config(log_level: str | None = None) -> dict[str, Any]:
18
+ """Extend Uvicorn's logging config with application loggers."""
19
+ level_key = (log_level or os.getenv("LOG_LEVEL", "info")).lower()
20
+ if level_key not in LOG_LEVELS:
21
+ level_key = "info"
22
+ level_name = level_key.upper()
23
+
24
+ config = copy.deepcopy(LOGGING_CONFIG)
25
+ for logger_name in _APP_LOGGER_NAMES:
26
+ config["loggers"][logger_name] = {
27
+ "handlers": ["default"],
28
+ "level": level_name,
29
+ "propagate": False,
30
+ }
31
+ config["loggers"]["uvicorn"]["level"] = level_name
32
+ config["loggers"]["uvicorn.error"]["level"] = level_name
33
+ config["loggers"]["uvicorn.access"]["level"] = level_name
34
+ return config
35
+
36
+
37
+ def configure_logging(log_level: str | None = None) -> None:
38
+ """Configure app and Uvicorn loggers with Uvicorn's colored formatter."""
39
+ logging.config.dictConfig(build_logging_config(log_level))
@@ -36,9 +36,8 @@ def _is_test_runtime() -> bool:
36
36
 
37
37
  def get_settings(cls: type[T], *, env_prefix: str) -> T:
38
38
  """Load a settings model from env; uses ``tests/test.env`` under pytest."""
39
- config = _settings_config(env_prefix=env_prefix)
40
- if _is_test_runtime():
41
- config = _settings_config(env_prefix=env_prefix, test=True)
39
+ use_test_env = _is_test_runtime()
40
+ config = _settings_config(env_prefix=env_prefix, test=use_test_env)
42
41
 
43
42
  class Settings(cls):
44
43
  model_config = config
@@ -1,9 +1,10 @@
1
1
  from typing import Literal
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, Field
4
4
  from pydantic_settings import BaseSettings
5
5
 
6
6
  from unique_search_proxy_client.web.settings.base import get_settings
7
+ from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
7
8
 
8
9
  HTTP_CLIENT_ENV_PREFIX = "HTTP_CLIENT_"
9
10
 
@@ -30,10 +31,10 @@ class HttpClientSettings(BaseSettings):
30
31
  proxy_protocol: ProxyProtocol = "http"
31
32
  proxy_host: str | None = None
32
33
  proxy_port: int | None = None
33
- proxy_headers: dict[str, str] = {}
34
+ proxy_headers: dict[str, LogSecretStr] = Field(default_factory=dict)
34
35
  proxy_ssl_ca_bundle_path: str | None = None
35
- proxy_username: str | None = None
36
- proxy_password: str | None = None
36
+ proxy_username: LogSecretStr | None = None
37
+ proxy_password: LogSecretStr | None = None
37
38
  proxy_ssl_cert_path: str | None = None
38
39
  proxy_ssl_key_path: str | None = None
39
40
 
@@ -4,7 +4,7 @@ from collections.abc import Callable, Sequence
4
4
  from functools import lru_cache
5
5
  from typing import ClassVar, TypeVar
6
6
 
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, SecretStr
8
8
  from pydantic_settings import BaseSettings
9
9
  from unique_search_proxy_core.errors import EngineNotConfiguredError
10
10
 
@@ -36,10 +36,25 @@ def provider_credentials(env_prefix: str) -> Callable[[T], T]:
36
36
  return decorate
37
37
 
38
38
 
39
- def _is_secret_configured(value: str | None) -> bool:
39
+ def read_secret(value: str | SecretStr | None) -> str:
40
40
  if value is None:
41
- return False
42
- normalized = value.strip()
41
+ return ""
42
+ if isinstance(value, SecretStr):
43
+ return value.get_secret_value()
44
+ return value
45
+
46
+
47
+ def _field_has_not_provided_default(field_info: object) -> bool:
48
+ default = getattr(field_info, "default", None)
49
+ if default is NOT_PROVIDED:
50
+ return True
51
+ if isinstance(default, SecretStr):
52
+ return default.get_secret_value() == NOT_PROVIDED
53
+ return False
54
+
55
+
56
+ def _is_secret_configured(value: str | SecretStr | None) -> bool:
57
+ normalized = read_secret(value).strip()
43
58
  if not normalized:
44
59
  return False
45
60
  return normalized != NOT_PROVIDED
@@ -53,7 +68,7 @@ def _iter_not_provided_credential_fields(model_cls: type[BaseModel]) -> tuple[st
53
68
  return tuple(
54
69
  name
55
70
  for name, field_info in model_cls.model_fields.items()
56
- if field_info.default is NOT_PROVIDED
71
+ if _field_has_not_provided_default(field_info)
57
72
  )
58
73
 
59
74
 
@@ -8,6 +8,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
8
8
  ProviderCredentials,
9
9
  provider_credentials,
10
10
  )
11
+ from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
11
12
 
12
13
  _DEFAULT_BRAVE_API_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
13
14
  _ENV_PREFIX = "BRAVE_SEARCH_"
@@ -17,7 +18,7 @@ _ENV_PREFIX = "BRAVE_SEARCH_"
17
18
  class _BraveCredentials(ProviderCredentials):
18
19
  """Environment-backed credentials for Brave Web Search."""
19
20
 
20
- api_key: str = Field(default=NOT_PROVIDED)
21
+ api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
21
22
  api_endpoint: str = Field(default=_DEFAULT_BRAVE_API_ENDPOINT)
22
23
 
23
24
 
@@ -10,6 +10,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
10
10
  ProviderCredentials,
11
11
  provider_credentials,
12
12
  )
13
+ from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
13
14
  from unique_search_proxy_client.web.utils.url import join_url_path
14
15
 
15
16
  _FIRECRAWL_API_BASE = "https://api.firecrawl.dev"
@@ -28,7 +29,7 @@ _ENV_PREFIX = "FIRECRAWL_"
28
29
  class _FirecrawlCredentials(ProviderCredentials):
29
30
  """Environment-backed credentials for Firecrawl v2 APIs."""
30
31
 
31
- api_key: str = Field(default=NOT_PROVIDED)
32
+ api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
32
33
  api_endpoint: str = Field(default=_FIRECRAWL_API_BASE)
33
34
  api_version: FirecrawlApiVersion = Field(default=_DEFAULT_FIRECRAWL_API_VERSION)
34
35
 
@@ -8,6 +8,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
8
8
  ProviderCredentials,
9
9
  provider_credentials,
10
10
  )
11
+ from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
11
12
 
12
13
  _DEFAULT_GOOGLE_API_ENDPOINT = "https://www.googleapis.com/customsearch/v1"
13
14
  _ENV_PREFIX = "GOOGLE_SEARCH_"
@@ -17,7 +18,7 @@ _ENV_PREFIX = "GOOGLE_SEARCH_"
17
18
  class _GoogleCredentials(ProviderCredentials):
18
19
  """Environment-backed credentials for Google Custom Search."""
19
20
 
20
- api_key: str = Field(default=NOT_PROVIDED)
21
+ api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
21
22
  api_endpoint: str = Field(default=_DEFAULT_GOOGLE_API_ENDPOINT)
22
23
  engine_id: str = Field(default=NOT_PROVIDED)
23
24
 
@@ -10,6 +10,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
10
10
  ProviderCredentials,
11
11
  provider_credentials,
12
12
  )
13
+ from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
13
14
 
14
15
  _JINA_DOMAIN = "jina.ai"
15
16
  _JINA_SUBDOMAINS: dict[str, dict[str, str]] = {
@@ -25,7 +26,7 @@ _ENV_PREFIX = "JINA_"
25
26
  class _JinaCredentials(ProviderCredentials):
26
27
  """Environment-backed credentials for Jina Reader and Search."""
27
28
 
28
- api_key: str = Field(default=NOT_PROVIDED)
29
+ api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
29
30
  deployment: JinaDeployment = Field(default=_DEFAULT_JINA_DEPLOYMENT)
30
31
  api_domain: str = Field(default=_JINA_DOMAIN)
31
32
 
@@ -8,6 +8,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
8
8
  ProviderCredentials,
9
9
  provider_credentials,
10
10
  )
11
+ from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
11
12
 
12
13
  _DEFAULT_PERPLEXITY_API_ENDPOINT = "https://api.perplexity.ai/search"
13
14
  _ENV_PREFIX = "PERPLEXITY_SEARCH_"
@@ -17,7 +18,7 @@ _ENV_PREFIX = "PERPLEXITY_SEARCH_"
17
18
  class _PerplexityCredentials(ProviderCredentials):
18
19
  """Environment-backed credentials for Perplexity Search."""
19
20
 
20
- api_key: str = Field(default=NOT_PROVIDED)
21
+ api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
21
22
  api_endpoint: str = Field(default=_DEFAULT_PERPLEXITY_API_ENDPOINT)
22
23
 
23
24
 
@@ -10,6 +10,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
10
10
  ProviderCredentials,
11
11
  provider_credentials,
12
12
  )
13
+ from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
13
14
  from unique_search_proxy_client.web.utils.url import join_url_path
14
15
 
15
16
  _TAVILY_API_BASE = "https://api.tavily.com"
@@ -25,7 +26,7 @@ _ENV_PREFIX = "TAVILY_"
25
26
  class _TavilyCredentials(ProviderCredentials):
26
27
  """Environment-backed credentials for Tavily APIs."""
27
28
 
28
- api_key: str = Field(default=NOT_PROVIDED)
29
+ api_key: LogSecretStr = Field(default=LogSecretStr(NOT_PROVIDED))
29
30
  api_endpoint: str = Field(default=_TAVILY_API_BASE)
30
31
 
31
32
  def _endpoint(self, operation: TavilyOperation) -> str:
@@ -7,6 +7,7 @@ from unique_search_proxy_client.web.settings.providers.base import (
7
7
  ProviderCredentials,
8
8
  provider_credentials,
9
9
  )
10
+ from unique_search_proxy_client.web.settings.secret_str import LogSecretStr
10
11
 
11
12
  _ENV_PREFIX = "VERTEXAI_AGENT_"
12
13
 
@@ -15,7 +16,7 @@ _ENV_PREFIX = "VERTEXAI_AGENT_"
15
16
  class _VertexAIAgentCredentials(ProviderCredentials):
16
17
  """Environment-backed credentials for Vertex AI grounding (Google GenAI)."""
17
18
 
18
- service_account_credentials: str | None = Field(default=None)
19
+ service_account_credentials: LogSecretStr | None = Field(default=None)
19
20
  service_account_scopes: list[str] | None = Field(default=None)
20
21
 
21
22
 
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+
5
+ from pydantic import SecretStr
6
+
7
+ _NOT_PROVIDED = "NOT_PROVIDED"
8
+
9
+
10
+ def _secret_log_suffix_len() -> int:
11
+ from unique_search_proxy_client.web.settings.startup_log import (
12
+ startup_log_settings,
13
+ )
14
+
15
+ return startup_log_settings.secret_suffix_len
16
+
17
+
18
+ def _mask_secret_for_display(value: str) -> str:
19
+ if not value:
20
+ return ""
21
+ if value == _NOT_PROVIDED:
22
+ return _NOT_PROVIDED
23
+ suffix_len = _secret_log_suffix_len()
24
+ if suffix_len <= 0:
25
+ return "*" * 10
26
+ # 20% of the value length is the max suffix length
27
+ if len(value) * 0.10 <= suffix_len:
28
+ return "*" * 10
29
+ return f"**********{value[-suffix_len:]}"
30
+
31
+
32
+ class LogSecretStr(SecretStr):
33
+ """SecretStr that masks values in str()/repr() with a configurable suffix."""
34
+
35
+ def _display(self) -> str:
36
+ return _mask_secret_for_display(self._secret_value)
37
+
38
+
39
+ def read_secret_headers(headers: Mapping[str, LogSecretStr]) -> dict[str, str]:
40
+ """Unwrap proxy header secrets for httpx."""
41
+ return {name: value.get_secret_value() for name, value in headers.items()}
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings
5
+
6
+ from unique_search_proxy_client.web.settings.base import get_settings
7
+
8
+ STARTUP_LOG_ENV_PREFIX = "STARTUP_LOG_"
9
+
10
+
11
+ class StartupLogSettings(BaseSettings):
12
+ """Startup log formatting options."""
13
+
14
+ secret_suffix_len: int = Field(
15
+ default=0,
16
+ ge=0,
17
+ le=32,
18
+ description=(
19
+ "Number of trailing characters to show when logging secret values "
20
+ "at startup (0 hides the secret entirely)."
21
+ ),
22
+ )
23
+
24
+
25
+ def _get_startup_log_settings() -> StartupLogSettings:
26
+ return get_settings(StartupLogSettings, env_prefix=STARTUP_LOG_ENV_PREFIX)
27
+
28
+
29
+ startup_log_settings = _get_startup_log_settings()
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from collections import defaultdict
6
+ from collections.abc import Iterable
7
+
8
+ from pydantic import BaseModel
9
+ from unique_search_proxy_core.url_safety.settings import url_safety_settings
10
+
11
+ from unique_search_proxy_client.web.settings.client import http_client_settings
12
+ from unique_search_proxy_client.web.settings.monitoring import prometheus_settings
13
+ from unique_search_proxy_client.web.settings.providers.base import (
14
+ _field_has_not_provided_default,
15
+ _is_secret_configured,
16
+ )
17
+ from unique_search_proxy_client.web.settings.providers.bing_agent import (
18
+ bing_agent_credentials,
19
+ )
20
+ from unique_search_proxy_client.web.settings.providers.brave import (
21
+ brave_search_credentials,
22
+ )
23
+ from unique_search_proxy_client.web.settings.providers.firecrawl import (
24
+ firecrawl_crawl_credentials,
25
+ )
26
+ from unique_search_proxy_client.web.settings.providers.google import (
27
+ google_search_credentials,
28
+ )
29
+ from unique_search_proxy_client.web.settings.providers.jina import (
30
+ jina_crawl_credentials,
31
+ )
32
+ from unique_search_proxy_client.web.settings.providers.perplexity import (
33
+ perplexity_search_credentials,
34
+ )
35
+ from unique_search_proxy_client.web.settings.providers.tavily import (
36
+ tavily_crawl_credentials,
37
+ )
38
+ from unique_search_proxy_client.web.settings.providers.vertexai_agent import (
39
+ vertexai_agent_credentials,
40
+ )
41
+
42
+ _LOGGER = logging.getLogger(__name__)
43
+
44
+ SettingsGroup = tuple[str, BaseModel, str]
45
+
46
+
47
+ def _env_var_name(field_name: str, env_prefix: str) -> str:
48
+ return f"{env_prefix}{field_name.upper()}"
49
+
50
+
51
+ def _field_status(
52
+ model: BaseModel,
53
+ field_name: str,
54
+ env_prefix: str,
55
+ ) -> str:
56
+ value = getattr(model, field_name)
57
+ field_info = model.model_fields[field_name]
58
+ env_var = _env_var_name(field_name, env_prefix)
59
+
60
+ if _field_has_not_provided_default(field_info):
61
+ return "configured" if _is_secret_configured(value) else "missing"
62
+
63
+ if value is None:
64
+ return "unset"
65
+
66
+ if env_var in os.environ:
67
+ return "set"
68
+
69
+ return "default"
70
+
71
+
72
+ def _group_fields_by_status(
73
+ model: BaseModel,
74
+ env_prefix: str,
75
+ ) -> dict[str, list[str]]:
76
+ grouped: dict[str, list[str]] = defaultdict(list)
77
+ for field_name in model.model_fields:
78
+ env_var = _env_var_name(field_name, env_prefix)
79
+ status = _field_status(model, field_name, env_prefix)
80
+ grouped[status].append(env_var)
81
+ return grouped
82
+
83
+
84
+ def _group_summary(grouped: dict[str, list[str]]) -> str:
85
+ missing = grouped.get("missing", [])
86
+ if missing:
87
+ return f"incomplete ({len(missing)} missing)"
88
+ return "ready"
89
+
90
+
91
+ def _format_settings_value(value: object) -> str:
92
+ if isinstance(value, dict):
93
+ if not value:
94
+ return "{}"
95
+ pairs = ", ".join(f"{key!r}: {secret}" for key, secret in value.items())
96
+ return "{" + pairs + "}"
97
+ return str(value)
98
+
99
+
100
+ def _format_group(title: str, model: BaseModel, env_prefix: str) -> list[str]:
101
+ grouped = _group_fields_by_status(model, env_prefix)
102
+ lines = [f" [{title}] {_group_summary(grouped)}"]
103
+ for field_name in model.model_fields:
104
+ env_var = _env_var_name(field_name, env_prefix)
105
+ value = getattr(model, field_name)
106
+ lines.append(f" {env_var}={_format_settings_value(value)}")
107
+ return lines
108
+
109
+
110
+ def _settings_groups() -> Iterable[SettingsGroup]:
111
+ return (
112
+ ("Google Search", google_search_credentials, "GOOGLE_SEARCH_"),
113
+ ("Brave Search", brave_search_credentials, "BRAVE_SEARCH_"),
114
+ ("Perplexity Search", perplexity_search_credentials, "PERPLEXITY_SEARCH_"),
115
+ ("Bing Agent", bing_agent_credentials, "BING_AGENT_"),
116
+ ("VertexAI Agent", vertexai_agent_credentials, "VERTEXAI_AGENT_"),
117
+ ("Tavily", tavily_crawl_credentials, "TAVILY_"),
118
+ ("Jina", jina_crawl_credentials, "JINA_"),
119
+ ("Firecrawl", firecrawl_crawl_credentials, "FIRECRAWL_"),
120
+ ("HTTP Client", http_client_settings, "HTTP_CLIENT_"),
121
+ ("Prometheus", prometheus_settings, "PROMETHEUS_"),
122
+ ("URL Safety", url_safety_settings, "URL_SAFETY_"),
123
+ )
124
+
125
+
126
+ def build_startup_settings_report() -> str:
127
+ """Build a multi-line startup settings summary for logging."""
128
+ lines = ["Search Proxy settings at startup:"]
129
+ for title, model, env_prefix in _settings_groups():
130
+ lines.extend(_format_group(title, model, env_prefix))
131
+ lines.append(f" [Runtime] LOG_LEVEL={os.getenv('LOG_LEVEL', 'INFO')}")
132
+ return "\n".join(lines)
133
+
134
+
135
+ def log_startup_settings_report(logger: logging.Logger | None = None) -> None:
136
+ """Log configured vs missing/default env-backed settings at pod startup."""
137
+ log = logger or _LOGGER
138
+ log.info("%s", build_startup_settings_report())