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