unique-search-proxy 2026.44.0.dev1__tar.gz → 2026.44.0.dev3__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.
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/PKG-INFO +25 -3
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/README.md +22 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/pyproject.toml +3 -3
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/__init__.py +14 -0
- unique_search_proxy-2026.44.0.dev3/unique_search_proxy_client/web/core/search_engines/custom_api/__init__.py +5 -0
- unique_search_proxy-2026.44.0.dev3/unique_search_proxy_client/web/core/search_engines/custom_api/service.py +173 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/factory.py +5 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/generator/introspect.py +13 -2
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/url_safety_registration.py +2 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/startup_report.py +6 -5
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/health.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/v1/context_headers.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/v1/crawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/api/v1/search.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/app.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/context.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/bing/cleanup.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/client/azure_transport.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/client/credentials.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/client/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/provider_response.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/providers.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/error_handlers.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/generator/schema.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/generator/template.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/metadata.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/helm/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/logging_config.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/middleware/context.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/presets/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/presets/common.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/presets/crawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/presets/search.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/presets/types.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/app.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/base.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/client.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/unique_search_proxy_client/web/utils/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev1 → unique_search_proxy-2026.44.0.dev3}/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.
|
|
3
|
+
Version: 2026.44.0.dev3
|
|
4
4
|
Summary: Web Search Proxy implementation
|
|
5
5
|
Author: ThePhilAz
|
|
6
6
|
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
@@ -20,8 +20,8 @@ Requires-Dist: certifi>=2025.11.12,<2027
|
|
|
20
20
|
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
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.
|
|
24
|
-
Requires-Dist: unique-search-proxy-core>=2026.44.0.
|
|
23
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev7,<2026.44.0rc0
|
|
24
|
+
Requires-Dist: unique-search-proxy-core>=2026.44.0.dev3,<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
|
|
@@ -172,6 +172,23 @@ Response includes **curated** normalised results and opaque **raw** provider pay
|
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
+
Customized API endpoints must return a JSON object with a `results` array:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"results": [
|
|
180
|
+
{
|
|
181
|
+
"url": "https://example.com",
|
|
182
|
+
"title": "Example",
|
|
183
|
+
"snippet": "Result summary",
|
|
184
|
+
"content": "Optional full content"
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Each result requires `url`, `title`, and `snippet`; `content` is optional.
|
|
191
|
+
|
|
175
192
|
### 5.3 Agent search (`POST /v1/agent-search`)
|
|
176
193
|
|
|
177
194
|
Thin egress — proxy returns opaque agent text; callers own parsing and citation extraction:
|
|
@@ -248,6 +265,11 @@ proxy routing fails during startup. The application cannot verify that the
|
|
|
248
265
|
proxy implements equivalent URL-safety controls, so this state is highlighted
|
|
249
266
|
in the Prometheus metric and Grafana dashboard.
|
|
250
267
|
|
|
268
|
+
`CUSTOM_WEB_SEARCH_API_TRUSTED_PRIVATE_HOSTS` is a JSON array of exact
|
|
269
|
+
operator-approved Customized API hostnames or IPs that may use RFC 1918 or IPv6
|
|
270
|
+
unique-local addresses. It does not apply to crawler targets, permit localhost
|
|
271
|
+
or cloud metadata targets, or configure network routing or private DNS.
|
|
272
|
+
|
|
251
273
|
With `WORKERS > 1`, the entrypoint sets `PROMETHEUS_MULTIPROC_DIR` for correct metric aggregation.
|
|
252
274
|
|
|
253
275
|
---
|
|
@@ -144,6 +144,23 @@ Response includes **curated** normalised results and opaque **raw** provider pay
|
|
|
144
144
|
}
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
Customized API endpoints must return a JSON object with a `results` array:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"results": [
|
|
152
|
+
{
|
|
153
|
+
"url": "https://example.com",
|
|
154
|
+
"title": "Example",
|
|
155
|
+
"snippet": "Result summary",
|
|
156
|
+
"content": "Optional full content"
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Each result requires `url`, `title`, and `snippet`; `content` is optional.
|
|
163
|
+
|
|
147
164
|
### 5.3 Agent search (`POST /v1/agent-search`)
|
|
148
165
|
|
|
149
166
|
Thin egress — proxy returns opaque agent text; callers own parsing and citation extraction:
|
|
@@ -220,6 +237,11 @@ proxy routing fails during startup. The application cannot verify that the
|
|
|
220
237
|
proxy implements equivalent URL-safety controls, so this state is highlighted
|
|
221
238
|
in the Prometheus metric and Grafana dashboard.
|
|
222
239
|
|
|
240
|
+
`CUSTOM_WEB_SEARCH_API_TRUSTED_PRIVATE_HOSTS` is a JSON array of exact
|
|
241
|
+
operator-approved Customized API hostnames or IPs that may use RFC 1918 or IPv6
|
|
242
|
+
unique-local addresses. It does not apply to crawler targets, permit localhost
|
|
243
|
+
or cloud metadata targets, or configure network routing or private DNS.
|
|
244
|
+
|
|
223
245
|
With `WORKERS > 1`, the entrypoint sets `PROMETHEUS_MULTIPROC_DIR` for correct metric aggregation.
|
|
224
246
|
|
|
225
247
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy"
|
|
3
|
-
version = "2026.44.0.
|
|
3
|
+
version = "2026.44.0.dev3"
|
|
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.
|
|
26
|
-
"unique-search-proxy-core>=2026.44.0.
|
|
25
|
+
"unique-toolkit[monitoring]>=2026.44.0.dev7,<2026.44.0rc0",
|
|
26
|
+
"unique-search-proxy-core>=2026.44.0.dev3,<2026.44.0rc0",
|
|
27
27
|
"openai>=1.105.0,<3",
|
|
28
28
|
]
|
|
29
29
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
from unique_search_proxy_core.search_engines.base import SearchEngineType
|
|
2
2
|
from unique_search_proxy_core.search_engines.brave.schema import BraveConfig
|
|
3
|
+
from unique_search_proxy_core.search_engines.custom_api.schema import CustomApiConfig
|
|
3
4
|
from unique_search_proxy_core.search_engines.google.schema import GoogleConfig
|
|
4
5
|
from unique_search_proxy_core.search_engines.perplexity.schema import PerplexityConfig
|
|
5
6
|
|
|
6
7
|
from unique_search_proxy_client.web.core.search_engines.brave.service import (
|
|
7
8
|
BraveSearchService,
|
|
8
9
|
)
|
|
10
|
+
from unique_search_proxy_client.web.core.search_engines.custom_api.service import (
|
|
11
|
+
CustomApiSearchService,
|
|
12
|
+
)
|
|
9
13
|
from unique_search_proxy_client.web.core.search_engines.descriptor import (
|
|
10
14
|
SearchEngineDescriptor,
|
|
11
15
|
)
|
|
@@ -47,11 +51,21 @@ def register_builtin_search_engines() -> None:
|
|
|
47
51
|
service_cls=PerplexitySearchService,
|
|
48
52
|
),
|
|
49
53
|
)
|
|
54
|
+
register_search_engine(
|
|
55
|
+
SearchEngineType.CUSTOM_API.value,
|
|
56
|
+
CustomApiSearchService,
|
|
57
|
+
descriptor=SearchEngineDescriptor(
|
|
58
|
+
config_model=CustomApiConfig,
|
|
59
|
+
service_cls=CustomApiSearchService,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
50
62
|
|
|
51
63
|
|
|
52
64
|
__all__ = [
|
|
53
65
|
"BraveConfig",
|
|
54
66
|
"BraveSearchService",
|
|
67
|
+
"CustomApiConfig",
|
|
68
|
+
"CustomApiSearchService",
|
|
55
69
|
"GoogleConfig",
|
|
56
70
|
"GoogleSearchService",
|
|
57
71
|
"PerplexityConfig",
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
from unique_search_proxy_core.errors import (
|
|
10
|
+
BadRequestProxyError,
|
|
11
|
+
ForbiddenTargetError,
|
|
12
|
+
UpstreamError,
|
|
13
|
+
UpstreamTimeoutError,
|
|
14
|
+
)
|
|
15
|
+
from unique_search_proxy_core.schema import SearchEngineRaw, WebSearchResults
|
|
16
|
+
from unique_search_proxy_core.search_engines.base import (
|
|
17
|
+
SearchEngineType,
|
|
18
|
+
get_search_engine_mode,
|
|
19
|
+
)
|
|
20
|
+
from unique_search_proxy_core.search_engines.custom_api.schema import (
|
|
21
|
+
CustomApiRequestMethod,
|
|
22
|
+
CustomApiSearchRequest,
|
|
23
|
+
)
|
|
24
|
+
from unique_search_proxy_core.url_safety import (
|
|
25
|
+
CustomApiTargetValidationError,
|
|
26
|
+
UrlSafetyService,
|
|
27
|
+
pinned_httpx_get_args,
|
|
28
|
+
url_safety_settings,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from unique_search_proxy_client.web.core.provider_response import (
|
|
32
|
+
raise_for_upstream_response,
|
|
33
|
+
transport_error_raw,
|
|
34
|
+
)
|
|
35
|
+
from unique_search_proxy_client.web.core.search_engines.service_base import (
|
|
36
|
+
SearchEngineService,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_LOGGER = logging.getLogger(__name__)
|
|
40
|
+
_CUSTOM_API_PROVIDER_LABEL = "Custom API"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_json_object(value: str, *, field_name: str) -> dict[str, Any]:
|
|
44
|
+
try:
|
|
45
|
+
parsed = json.loads(value)
|
|
46
|
+
except (TypeError, json.JSONDecodeError) as exc:
|
|
47
|
+
raise BadRequestProxyError(
|
|
48
|
+
f"{field_name} must be a valid JSON object",
|
|
49
|
+
) from exc
|
|
50
|
+
if not isinstance(parsed, dict):
|
|
51
|
+
raise BadRequestProxyError(f"{field_name} must be a JSON object")
|
|
52
|
+
return parsed
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_headers(value: str) -> dict[str, str]:
|
|
56
|
+
parsed = _parse_json_object(value, field_name="apiHeaders")
|
|
57
|
+
if not all(
|
|
58
|
+
isinstance(key, str) and isinstance(item, str) for key, item in parsed.items()
|
|
59
|
+
):
|
|
60
|
+
raise BadRequestProxyError("apiHeaders keys and values must be strings")
|
|
61
|
+
return parsed
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CustomApiSearchService(SearchEngineService[CustomApiSearchRequest]):
|
|
65
|
+
"""Tenant-configured API provider with fail-closed outbound URL controls."""
|
|
66
|
+
|
|
67
|
+
engine_id = SearchEngineType.CUSTOM_API.value
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def mode(self) -> str:
|
|
71
|
+
return get_search_engine_mode(SearchEngineType.CUSTOM_API).value
|
|
72
|
+
|
|
73
|
+
async def search(
|
|
74
|
+
self,
|
|
75
|
+
request: CustomApiSearchRequest, # type: ignore[valid-type]
|
|
76
|
+
) -> tuple[SearchEngineRaw, WebSearchResults]:
|
|
77
|
+
client = self._http_client
|
|
78
|
+
if client is None:
|
|
79
|
+
raise RuntimeError("HTTP client is required for Custom API search")
|
|
80
|
+
|
|
81
|
+
# Admin configuration must not grant access to unapproved private targets.
|
|
82
|
+
try:
|
|
83
|
+
resolved_target = await UrlSafetyService.resolve_custom_api_target(
|
|
84
|
+
request.api_endpoint,
|
|
85
|
+
trusted_private_hosts=(
|
|
86
|
+
url_safety_settings.custom_api_trusted_private_hosts
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
except CustomApiTargetValidationError as exc:
|
|
90
|
+
blocked = exc.blocked_targets[0]
|
|
91
|
+
raise ForbiddenTargetError(
|
|
92
|
+
"Custom API endpoint is blocked by URL safety policy",
|
|
93
|
+
details=[
|
|
94
|
+
{
|
|
95
|
+
"category": blocked.category,
|
|
96
|
+
"reason": blocked.reason,
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
) from exc
|
|
100
|
+
|
|
101
|
+
request_url, pin_headers, extensions = pinned_httpx_get_args(resolved_target)
|
|
102
|
+
headers = _parse_headers(request.api_headers)
|
|
103
|
+
headers.update(pin_headers)
|
|
104
|
+
params = _parse_json_object(
|
|
105
|
+
request.api_additional_query_params,
|
|
106
|
+
field_name="apiAdditionalQueryParams",
|
|
107
|
+
)
|
|
108
|
+
body = _parse_json_object(
|
|
109
|
+
request.api_additional_body_params,
|
|
110
|
+
field_name="apiAdditionalBodyParams",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
if request.api_request_method == CustomApiRequestMethod.GET:
|
|
115
|
+
params["query"] = request.query
|
|
116
|
+
response = await client.request(
|
|
117
|
+
method=CustomApiRequestMethod.GET,
|
|
118
|
+
url=request_url,
|
|
119
|
+
headers=headers,
|
|
120
|
+
params=params,
|
|
121
|
+
json=body,
|
|
122
|
+
extensions=extensions or None,
|
|
123
|
+
timeout=request.timeout,
|
|
124
|
+
follow_redirects=False,
|
|
125
|
+
)
|
|
126
|
+
elif request.api_request_method == CustomApiRequestMethod.POST:
|
|
127
|
+
body["query"] = request.query
|
|
128
|
+
response = await client.post(
|
|
129
|
+
url=request_url,
|
|
130
|
+
headers=headers,
|
|
131
|
+
params=params,
|
|
132
|
+
json=body,
|
|
133
|
+
extensions=extensions or None,
|
|
134
|
+
timeout=request.timeout,
|
|
135
|
+
follow_redirects=False,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
raise BadRequestProxyError("Unsupported Custom API request method")
|
|
139
|
+
except httpx.TimeoutException as exc:
|
|
140
|
+
raise UpstreamTimeoutError(
|
|
141
|
+
f"Custom API search timed out after {request.timeout}s",
|
|
142
|
+
upstream_raw=transport_error_raw(exc),
|
|
143
|
+
) from exc
|
|
144
|
+
except httpx.HTTPError as exc:
|
|
145
|
+
raise UpstreamError(
|
|
146
|
+
"Custom API search request failed",
|
|
147
|
+
upstream_raw=transport_error_raw(exc),
|
|
148
|
+
) from exc
|
|
149
|
+
|
|
150
|
+
if response.is_redirect:
|
|
151
|
+
raise ForbiddenTargetError("Custom API redirects are not allowed")
|
|
152
|
+
raise_for_upstream_response(
|
|
153
|
+
response,
|
|
154
|
+
provider_label=_CUSTOM_API_PROVIDER_LABEL,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
payload = response.json()
|
|
159
|
+
except ValueError as exc:
|
|
160
|
+
raise UpstreamError("Custom API returned invalid JSON") from exc
|
|
161
|
+
if not isinstance(payload, dict):
|
|
162
|
+
raise UpstreamError("Custom API response must be a JSON object")
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
curated = WebSearchResults.model_validate(payload)
|
|
166
|
+
except ValidationError as exc:
|
|
167
|
+
raise UpstreamError("Custom API returned an invalid result schema") from exc
|
|
168
|
+
|
|
169
|
+
_LOGGER.info("Custom API search returned %s curated results", len(curated))
|
|
170
|
+
return SearchEngineRaw(pages=[payload]), curated
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = ["CustomApiSearchService"]
|
|
@@ -7,6 +7,9 @@ from unique_search_proxy_core.search_engines.base import SearchEngine, SearchEng
|
|
|
7
7
|
from unique_search_proxy_client.web.core.search_engines.brave.service import (
|
|
8
8
|
BraveSearchService,
|
|
9
9
|
)
|
|
10
|
+
from unique_search_proxy_client.web.core.search_engines.custom_api.service import (
|
|
11
|
+
CustomApiSearchService,
|
|
12
|
+
)
|
|
10
13
|
from unique_search_proxy_client.web.core.search_engines.google.service import (
|
|
11
14
|
GoogleSearchService,
|
|
12
15
|
)
|
|
@@ -31,6 +34,8 @@ def get_search_engine_service(
|
|
|
31
34
|
return BraveSearchService(http_client=http_client)
|
|
32
35
|
case SearchEngineType.PERPLEXITY:
|
|
33
36
|
return PerplexitySearchService(http_client=http_client)
|
|
37
|
+
case SearchEngineType.CUSTOM_API:
|
|
38
|
+
return CustomApiSearchService(http_client=http_client)
|
|
34
39
|
case _:
|
|
35
40
|
raise ValueError(f"Unsupported search engine: {engine}")
|
|
36
41
|
|
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
from typing import Any, Literal, cast, get_args, get_origin
|
|
8
8
|
|
|
9
9
|
from pydantic import SecretStr
|
|
10
|
+
from pydantic.aliases import AliasChoices
|
|
10
11
|
from pydantic.fields import FieldInfo
|
|
11
12
|
|
|
12
13
|
from unique_search_proxy_client.web.settings.secret_str import (
|
|
@@ -25,7 +26,17 @@ def snake_to_camel(name: str) -> str:
|
|
|
25
26
|
return parts[0] + "".join(part.capitalize() for part in parts[1:])
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def env_var_name(
|
|
29
|
+
def env_var_name(
|
|
30
|
+
field_name: str,
|
|
31
|
+
env_prefix: str,
|
|
32
|
+
field_info: FieldInfo | None = None,
|
|
33
|
+
) -> str:
|
|
34
|
+
if field_info is not None:
|
|
35
|
+
aliases = field_info.validation_alias
|
|
36
|
+
choices = aliases.choices if isinstance(aliases, AliasChoices) else [aliases]
|
|
37
|
+
for choice in choices:
|
|
38
|
+
if isinstance(choice, str) and choice.isupper():
|
|
39
|
+
return choice
|
|
29
40
|
return f"{env_prefix}{field_name.upper()}"
|
|
30
41
|
|
|
31
42
|
|
|
@@ -201,7 +212,7 @@ def iter_helm_fields(
|
|
|
201
212
|
spec = HelmFieldSpec(
|
|
202
213
|
python_name=field_name,
|
|
203
214
|
helm_name=helm_name,
|
|
204
|
-
env_var=env_var_name(field_name, env_prefix),
|
|
215
|
+
env_var=env_var_name(field_name, env_prefix, field_info),
|
|
205
216
|
sensitive=sensitive,
|
|
206
217
|
required_when_enabled=required_when_enabled,
|
|
207
218
|
schema_ref=schema_ref,
|
|
@@ -33,6 +33,7 @@ _URL_SAFETY_SECTIONS: dict[str, list[str]] = {
|
|
|
33
33
|
"allowed_schemes",
|
|
34
34
|
"localhost_hosts",
|
|
35
35
|
"metadata_hosts",
|
|
36
|
+
"custom_api_trusted_private_hosts",
|
|
36
37
|
],
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -76,6 +77,7 @@ def configure_url_safety_helm_model(model: type[UrlSafetySettings]) -> None:
|
|
|
76
77
|
"allowed_schemes": {"overridable": True},
|
|
77
78
|
"localhost_hosts": {"overridable": True},
|
|
78
79
|
"metadata_hosts": {"overridable": True},
|
|
80
|
+
"custom_api_trusted_private_hosts": {"overridable": True},
|
|
79
81
|
},
|
|
80
82
|
)
|
|
81
83
|
|
|
@@ -6,6 +6,7 @@ from collections import defaultdict
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
9
|
+
from unique_search_proxy_client.web.helm.generator.introspect import env_var_name
|
|
9
10
|
from unique_search_proxy_client.web.helm.registry import startup_report_groups
|
|
10
11
|
from unique_search_proxy_client.web.settings.secret_str import (
|
|
11
12
|
field_has_not_provided_default,
|
|
@@ -15,8 +16,8 @@ from unique_search_proxy_client.web.settings.secret_str import (
|
|
|
15
16
|
_LOGGER = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
def _env_var_name(field_name: str, env_prefix: str) -> str:
|
|
19
|
-
return
|
|
19
|
+
def _env_var_name(model: BaseModel, field_name: str, env_prefix: str) -> str:
|
|
20
|
+
return env_var_name(field_name, env_prefix, model.model_fields[field_name])
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _field_status(
|
|
@@ -26,7 +27,7 @@ def _field_status(
|
|
|
26
27
|
) -> str:
|
|
27
28
|
value = getattr(model, field_name)
|
|
28
29
|
field_info = model.model_fields[field_name]
|
|
29
|
-
env_var = _env_var_name(field_name, env_prefix)
|
|
30
|
+
env_var = _env_var_name(model, field_name, env_prefix)
|
|
30
31
|
|
|
31
32
|
if field_has_not_provided_default(field_info):
|
|
32
33
|
return "configured" if is_secret_configured(value) else "missing"
|
|
@@ -46,7 +47,7 @@ def _group_fields_by_status(
|
|
|
46
47
|
) -> dict[str, list[str]]:
|
|
47
48
|
grouped: dict[str, list[str]] = defaultdict(list)
|
|
48
49
|
for field_name in model.model_fields:
|
|
49
|
-
env_var = _env_var_name(field_name, env_prefix)
|
|
50
|
+
env_var = _env_var_name(model, field_name, env_prefix)
|
|
50
51
|
status = _field_status(model, field_name, env_prefix)
|
|
51
52
|
grouped[status].append(env_var)
|
|
52
53
|
return grouped
|
|
@@ -72,7 +73,7 @@ def _format_group(title: str, model: BaseModel, env_prefix: str) -> list[str]:
|
|
|
72
73
|
grouped = _group_fields_by_status(model, env_prefix)
|
|
73
74
|
lines = [f" [{title}] {_group_summary(grouped)}"]
|
|
74
75
|
for field_name in model.model_fields:
|
|
75
|
-
env_var = _env_var_name(field_name, env_prefix)
|
|
76
|
+
env_var = _env_var_name(model, field_name, env_prefix)
|
|
76
77
|
value = getattr(model, field_name)
|
|
77
78
|
lines.append(f" {env_var}={_format_settings_value(value)}")
|
|
78
79
|
return lines
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|