unique-search-proxy-core 2026.30.0.dev2__tar.gz → 2026.30.0.dev4__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 (54) hide show
  1. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/PKG-INFO +5 -14
  2. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/README.md +3 -12
  3. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/pyproject.toml +2 -2
  4. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/__init__.py +0 -2
  5. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/base.py +7 -2
  6. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/basic/schema.py +9 -2
  7. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/firecrawl/schema.py +31 -9
  8. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/jina/schema.py +34 -12
  9. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/tavily/schema.py +17 -6
  10. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/request_base.py +3 -10
  11. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/schema.py +0 -9
  12. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/redirect.py +12 -1
  13. unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/crawlers/params.py +0 -49
  14. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/__init__.py +0 -0
  15. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/__init__.py +0 -0
  16. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/base.py +0 -0
  17. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/bing/__init__.py +0 -0
  18. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/bing/schema.py +0 -0
  19. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/config_types.py +0 -0
  20. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/output_schema.py +0 -0
  21. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/resolve.py +0 -0
  22. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/response_parsing.py +0 -0
  23. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/vertexai/__init__.py +0 -0
  24. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/vertexai/schema.py +0 -0
  25. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/basic/__init__.py +0 -0
  26. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/basic/content_types.py +0 -0
  27. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/basic/processing/policy.py +0 -0
  28. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/config_types.py +0 -0
  29. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/errors.py +0 -0
  30. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/logging.py +0 -0
  31. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/__init__.py +0 -0
  32. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/annotations.py +0 -0
  33. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/derive.py +0 -0
  34. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/exposable_param.py +0 -0
  35. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/exposed_params.py +0 -0
  36. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/providers/schema.py +0 -0
  37. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/__init__.py +0 -0
  38. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/base.py +0 -0
  39. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/brave/__init__.py +0 -0
  40. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/brave/schema.py +0 -0
  41. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/config_types.py +0 -0
  42. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/google/__init__.py +0 -0
  43. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/google/schema.py +0 -0
  44. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/perplexity/__init__.py +0 -0
  45. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/perplexity/schema.py +0 -0
  46. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/__init__.py +0 -0
  47. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/dns.py +0 -0
  48. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/egress.py +0 -0
  49. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/models.py +0 -0
  50. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/netloc.py +0 -0
  51. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/policy.py +0 -0
  52. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/resolver.py +0 -0
  53. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/service.py +0 -0
  54. {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-search-proxy-core
3
- Version: 2026.30.0.dev2
3
+ Version: 2026.30.0.dev4
4
4
  Summary: Shared Pydantic types for the Unique Search Proxy API
5
5
  Author: ThePhilAz
6
6
  Author-email: ThePhilAz <rami.azouz@philico.com>
@@ -8,7 +8,7 @@ Requires-Dist: httpx>=0.28.0,<0.29
8
8
  Requires-Dist: pydantic>=2.12.5,<3.0.0
9
9
  Requires-Dist: pydantic-settings>=2.10.1,<3
10
10
  Requires-Dist: pyhumps>=3.8.0,<4
11
- Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev10,<2026.30.0rc0
11
+ Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev15,<2026.30.0rc0
12
12
  Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
14
 
@@ -82,7 +82,7 @@ Admin JSON → GoogleConfig {expose, value}
82
82
  | `merge(overrides, *, query)` (instance) | search only | deployment defaults + LLM overrides + query → validated `request_model()` instance |
83
83
  | `provider_query_params(request)` (classmethod) | search only | request serialized for the upstream provider, minus `_provider_param_exclude_fields` (Google adds `search_engine_id`) |
84
84
 
85
- Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()`; crawlers additionally keep `merge_crawler_config_and_invocation`.
85
+ Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()`. Crawlers have no exposable params and no `merge` — a crawl request is simply the deployment config fields plus `urls`.
86
86
 
87
87
  ### 3.2 ExposableParam — search-engine only
88
88
 
@@ -121,15 +121,6 @@ request = google_config.merge({"gl": "de"}, query="EU AI Act")
121
121
 
122
122
  Deactivated knobs (`value=None`) are dropped, overrides win over admin defaults, `engine` always comes from the config. The proxy receives a flat body; it does not resolve deployment config over HTTP.
123
123
 
124
- ### 3.5 merge_crawler_config_and_invocation — crawler only
125
-
126
- ```python
127
- from unique_search_proxy_core.crawlers import merge_crawler_config_and_invocation
128
-
129
- request = merge_crawler_config_and_invocation(basic_config, {"urls": ["https://example.com"]})
130
- # → validated BasicCrawlRequest ready for POST /v1/crawl
131
- ```
132
-
133
124
  ---
134
125
 
135
126
  ## 4. Architecture (modules)
@@ -169,7 +160,7 @@ flowchart TB
169
160
  | `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
170
161
  | `search_engines/` | Config models with the config-owned API (`request_model` / `exposed_params_model` / `merge` / `provider_query_params`), request union |
171
162
  | `agent_engines/` | Agent config/request models (`request_model`), output schema |
172
- | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies, `merge_crawler_config_and_invocation` |
163
+ | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies (`request_model()`) |
173
164
 
174
165
  ---
175
166
 
@@ -234,7 +225,7 @@ from unique_search_proxy_core import (
234
225
  - Discriminated provider configs (`engine`, `crawler` Literal discriminators)
235
226
  - **Search-only:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` / `provider_query_params`
236
227
  - **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`)
237
- - **Crawl:** `BasicConfig.request_model()` (injects `urls`); `merge_crawler_config_and_invocation`
228
+ - **Crawl:** `BasicConfig.request_model()` (injects `urls`); no exposable params / no merge
238
229
  - CamelCase JSON aliases on all models
239
230
  - Zero server dependencies (import-linter enforced in the client package)
240
231
 
@@ -68,7 +68,7 @@ Admin JSON → GoogleConfig {expose, value}
68
68
  | `merge(overrides, *, query)` (instance) | search only | deployment defaults + LLM overrides + query → validated `request_model()` instance |
69
69
  | `provider_query_params(request)` (classmethod) | search only | request serialized for the upstream provider, minus `_provider_param_exclude_fields` (Google adds `search_engine_id`) |
70
70
 
71
- Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()`; crawlers additionally keep `merge_crawler_config_and_invocation`.
71
+ Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()`. Crawlers have no exposable params and no `merge` — a crawl request is simply the deployment config fields plus `urls`.
72
72
 
73
73
  ### 3.2 ExposableParam — search-engine only
74
74
 
@@ -107,15 +107,6 @@ request = google_config.merge({"gl": "de"}, query="EU AI Act")
107
107
 
108
108
  Deactivated knobs (`value=None`) are dropped, overrides win over admin defaults, `engine` always comes from the config. The proxy receives a flat body; it does not resolve deployment config over HTTP.
109
109
 
110
- ### 3.5 merge_crawler_config_and_invocation — crawler only
111
-
112
- ```python
113
- from unique_search_proxy_core.crawlers import merge_crawler_config_and_invocation
114
-
115
- request = merge_crawler_config_and_invocation(basic_config, {"urls": ["https://example.com"]})
116
- # → validated BasicCrawlRequest ready for POST /v1/crawl
117
- ```
118
-
119
110
  ---
120
111
 
121
112
  ## 4. Architecture (modules)
@@ -155,7 +146,7 @@ flowchart TB
155
146
  | `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
156
147
  | `search_engines/` | Config models with the config-owned API (`request_model` / `exposed_params_model` / `merge` / `provider_query_params`), request union |
157
148
  | `agent_engines/` | Agent config/request models (`request_model`), output schema |
158
- | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies, `merge_crawler_config_and_invocation` |
149
+ | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies (`request_model()`) |
159
150
 
160
151
  ---
161
152
 
@@ -220,7 +211,7 @@ from unique_search_proxy_core import (
220
211
  - Discriminated provider configs (`engine`, `crawler` Literal discriminators)
221
212
  - **Search-only:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` / `provider_query_params`
222
213
  - **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`)
223
- - **Crawl:** `BasicConfig.request_model()` (injects `urls`); `merge_crawler_config_and_invocation`
214
+ - **Crawl:** `BasicConfig.request_model()` (injects `urls`); no exposable params / no merge
224
215
  - CamelCase JSON aliases on all models
225
216
  - Zero server dependencies (import-linter enforced in the client package)
226
217
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy-core"
3
- version = "2026.30.0.dev2"
3
+ version = "2026.30.0.dev4"
4
4
  description = "Shared Pydantic types for the Unique Search Proxy API"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -10,7 +10,7 @@ dependencies = [
10
10
  "pydantic>=2.12.5,<3.0.0",
11
11
  "pydantic-settings>=2.10.1,<3",
12
12
  "pyhumps>=3.8.0,<4",
13
- "unique-toolkit[monitoring]>=2026.30.0.dev10,<2026.30.0rc0",
13
+ "unique-toolkit[monitoring]>=2026.30.0.dev15,<2026.30.0rc0",
14
14
 
15
15
  ]
16
16
 
@@ -17,7 +17,6 @@ from unique_search_proxy_core.crawlers.config_types import (
17
17
  parse_crawl_request,
18
18
  parse_crawler_config,
19
19
  )
20
- from unique_search_proxy_core.crawlers.params import merge_crawler_config_and_invocation
21
20
 
22
21
  __all__ = [
23
22
  "BaseCrawler",
@@ -31,7 +30,6 @@ __all__ = [
31
30
  "CrawlerType",
32
31
  "build_crawl_request_union",
33
32
  "crawler_config_from_request",
34
- "merge_crawler_config_and_invocation",
35
33
  "parse_crawl_request",
36
34
  "parse_crawler_config",
37
35
  ]
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
8
8
 
9
9
  from unique_search_proxy_core.param_policy.derive import derive_request_model
10
10
  from unique_search_proxy_core.param_policy.request_base import CrawlRequestBase
11
- from unique_search_proxy_core.schema import CrawlUrlResult, deployment_model_config
11
+ from unique_search_proxy_core.schema import CrawlUrlResult, camelized_model_config
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from httpx import AsyncClient
@@ -29,7 +29,12 @@ class CrawlerType(StrEnum):
29
29
  class BaseCrawlerConfig(BaseModel, Generic[CrawlerTypeT]):
30
30
  """Deployment config for a crawler; each crawler narrows ``crawler`` with a Literal."""
31
31
 
32
- model_config = deployment_model_config
32
+ # Mirrors ``BaseSearchEngineConfig``: no ``extra='forbid'``. Emitting
33
+ # ``additionalProperties: false`` on every branch of the ``crawlerConfig``
34
+ # discriminated ``oneOf`` breaks the RJSF admin form, which carries sibling
35
+ # default fields across branches and then matches zero subschemas. Runtime
36
+ # ``/v1/crawl`` request bodies stay strict via ``CrawlRequestBase``.
37
+ model_config = camelized_model_config
33
38
 
34
39
  #: Name of the derived request model; set by every concrete crawler config.
35
40
  _request_model_name: ClassVar[str]
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import ClassVar, Literal
3
+ from typing import Annotated, ClassVar, Literal
4
4
 
5
5
  from pydantic import Field
6
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
6
7
 
7
8
  from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig, CrawlerType
8
9
  from unique_search_proxy_core.crawlers.basic.content_types import ContentTypeToggles
@@ -13,7 +14,13 @@ class BasicConfig(BaseCrawlerConfig[CrawlerType.BASIC]):
13
14
 
14
15
  _request_model_name: ClassVar[str] = "BasicCrawlRequest"
15
16
 
16
- crawler: Literal[CrawlerType.BASIC] = CrawlerType.BASIC
17
+ crawler: Annotated[
18
+ Literal[CrawlerType.BASIC], RJSFMetaTag.SpecialWidget.hidden()
19
+ ] = Field(
20
+ default=CrawlerType.BASIC,
21
+ title="Crawler",
22
+ description="Provider discriminator; must be `Basic` for this config.",
23
+ )
17
24
 
18
25
  content_types: ContentTypeToggles = Field(
19
26
  default_factory=ContentTypeToggles,
@@ -1,20 +1,44 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import ClassVar, Literal
3
+ from typing import Annotated, ClassVar, Literal, TypeAlias
4
4
 
5
5
  from pydantic import Field
6
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
6
7
 
7
8
  from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig, CrawlerType
9
+ from unique_search_proxy_core.schema import DeactivatedNone
8
10
 
9
11
  FirecrawlProxyMode = Literal["basic", "enhanced", "auto"]
10
12
 
13
+ MaxConcurrencyOrNone: TypeAlias = (
14
+ Annotated[int, Field(title="Integer", ge=1)] | DeactivatedNone
15
+ )
16
+ TagListOrNone: TypeAlias = (
17
+ Annotated[
18
+ list[str],
19
+ Field(title="Tags"),
20
+ RJSFMetaTag({"ui:options": {"orderable": False}}),
21
+ ]
22
+ | DeactivatedNone
23
+ )
24
+ ScrapeHeadersOrNone: TypeAlias = (
25
+ Annotated[dict[str, str], Field(title="Headers")] | DeactivatedNone
26
+ )
27
+ MaxAgeOrNone: TypeAlias = Annotated[int, Field(title="Integer", ge=0)] | DeactivatedNone
28
+
11
29
 
12
30
  class FirecrawlConfig(BaseCrawlerConfig[CrawlerType.FIRECRAWL]):
13
31
  """Deployment config for Firecrawl (scrape or batch scrape)."""
14
32
 
15
33
  _request_model_name: ClassVar[str] = "FirecrawlCrawlRequest"
16
34
 
17
- crawler: Literal[CrawlerType.FIRECRAWL] = CrawlerType.FIRECRAWL
35
+ crawler: Annotated[
36
+ Literal[CrawlerType.FIRECRAWL], RJSFMetaTag.SpecialWidget.hidden()
37
+ ] = Field(
38
+ default=CrawlerType.FIRECRAWL,
39
+ title="Crawler",
40
+ description="Provider discriminator; must be `Firecrawl` for this config.",
41
+ )
18
42
 
19
43
  only_main_content: bool = Field(
20
44
  default=True,
@@ -26,9 +50,8 @@ class FirecrawlConfig(BaseCrawlerConfig[CrawlerType.FIRECRAWL]):
26
50
  title="Only clean content",
27
51
  description="LLM pass to remove residual boilerplate from markdown.",
28
52
  )
29
- max_concurrency: int | None = Field(
53
+ max_concurrency: MaxConcurrencyOrNone = Field(
30
54
  default=None,
31
- ge=1,
32
55
  title="Max concurrency",
33
56
  description="Maximum concurrent scrapes for this batch job.",
34
57
  )
@@ -63,24 +86,23 @@ class FirecrawlConfig(BaseCrawlerConfig[CrawlerType.FIRECRAWL]):
63
86
  title="Proxy mode",
64
87
  description="Firecrawl proxy tier: `basic`, `enhanced`, or `auto`.",
65
88
  )
66
- include_tags: list[str] | None = Field(
89
+ include_tags: TagListOrNone = Field(
67
90
  default=None,
68
91
  title="Include tags",
69
92
  description="HTML tags to include in scrape output.",
70
93
  )
71
- exclude_tags: list[str] | None = Field(
94
+ exclude_tags: TagListOrNone = Field(
72
95
  default=None,
73
96
  title="Exclude tags",
74
97
  description="HTML tags to exclude from scrape output.",
75
98
  )
76
- scrape_headers: dict[str, str] | None = Field(
99
+ scrape_headers: ScrapeHeadersOrNone = Field(
77
100
  default=None,
78
101
  title="Scrape headers",
79
102
  description="Headers sent to the target page (cookies, user-agent, etc.).",
80
103
  )
81
- max_age: int | None = Field(
104
+ max_age: MaxAgeOrNone = Field(
82
105
  default=None,
83
- ge=0,
84
106
  title="Max age",
85
107
  description=(
86
108
  "Return cached page content younger than this age in milliseconds."
@@ -1,22 +1,46 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import ClassVar, Literal
3
+ from typing import Annotated, ClassVar, Literal, TypeAlias
4
4
 
5
5
  from pydantic import Field
6
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
6
7
 
7
8
  from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig, CrawlerType
9
+ from unique_search_proxy_core.schema import DeactivatedNone
8
10
 
9
11
  JinaReturnFormat = Literal["markdown", "html", "text", "screenshot", "pageshot"]
10
12
  JinaEngine = Literal["auto", "browser", "direct", "cf-browser-rendering"]
11
13
  JinaRetainImages = Literal["none", "all", "alt", "all_p", "alt_p"]
12
14
 
15
+ StrOrNone: TypeAlias = Annotated[str, Field(title="String")] | DeactivatedNone
16
+ PageTimeoutOrNone: TypeAlias = (
17
+ Annotated[int, Field(title="Integer", ge=1, le=180)] | DeactivatedNone
18
+ )
19
+ CssSelectorListOrNone: TypeAlias = (
20
+ Annotated[
21
+ list[str],
22
+ Field(title="CSS Selectors"),
23
+ RJSFMetaTag({"ui:options": {"orderable": False}}),
24
+ ]
25
+ | DeactivatedNone
26
+ )
27
+ RetainImagesOrNone: TypeAlias = (
28
+ Annotated[JinaRetainImages, Field(title="Retain Images")] | DeactivatedNone
29
+ )
30
+
13
31
 
14
32
  class JinaConfig(BaseCrawlerConfig[CrawlerType.JINA]):
15
33
  """Deployment config for the Jina Reader crawler."""
16
34
 
17
35
  _request_model_name: ClassVar[str] = "JinaCrawlRequest"
18
36
 
19
- crawler: Literal[CrawlerType.JINA] = CrawlerType.JINA
37
+ crawler: Annotated[
38
+ Literal[CrawlerType.JINA], RJSFMetaTag.SpecialWidget.hidden()
39
+ ] = Field(
40
+ default=CrawlerType.JINA,
41
+ title="Crawler",
42
+ description="Provider discriminator; must be `Jina` for this config.",
43
+ )
20
44
 
21
45
  return_format: JinaReturnFormat = Field(
22
46
  default="markdown",
@@ -34,10 +58,8 @@ class JinaConfig(BaseCrawlerConfig[CrawlerType.JINA]):
34
58
  "`cf-browser-rendering` for JS-heavy sites."
35
59
  ),
36
60
  )
37
- page_timeout: int | None = Field(
61
+ page_timeout: PageTimeoutOrNone = Field(
38
62
  default=None,
39
- ge=1,
40
- le=180,
41
63
  title="Page load timeout",
42
64
  description=(
43
65
  "Max seconds to wait for the page to load (Jina Reader). "
@@ -56,17 +78,17 @@ class JinaConfig(BaseCrawlerConfig[CrawlerType.JINA]):
56
78
  title="Bypass cache",
57
79
  description="Bypass Jina cache and fetch fresh content.",
58
80
  )
59
- target_selector: list[str] | None = Field(
81
+ target_selector: CssSelectorListOrNone = Field(
60
82
  default=None,
61
83
  title="Target selector",
62
84
  description="CSS selectors to focus extraction on specific page elements.",
63
85
  )
64
- wait_for_selector: list[str] | None = Field(
86
+ wait_for_selector: CssSelectorListOrNone = Field(
65
87
  default=None,
66
88
  title="Wait for selector",
67
89
  description="CSS selectors to wait for before returning content.",
68
90
  )
69
- remove_selector: list[str] | None = Field(
91
+ remove_selector: CssSelectorListOrNone = Field(
70
92
  default=None,
71
93
  title="Remove selector",
72
94
  description="CSS selectors for page regions to strip (headers, footers, etc.).",
@@ -91,22 +113,22 @@ class JinaConfig(BaseCrawlerConfig[CrawlerType.JINA]):
91
113
  title="Include iframes",
92
114
  description="Include iframe content in the reader response.",
93
115
  )
94
- retain_images: JinaRetainImages | None = Field(
116
+ retain_images: RetainImagesOrNone = Field(
95
117
  default=None,
96
118
  title="Retain images",
97
119
  description="Control which images are retained in markdown output.",
98
120
  )
99
- locale: str | None = Field(
121
+ locale: StrOrNone = Field(
100
122
  default=None,
101
123
  title="Locale",
102
124
  description="Browser locale used to render the page (e.g. `en-US`).",
103
125
  )
104
- referer: str | None = Field(
126
+ referer: StrOrNone = Field(
105
127
  default=None,
106
128
  title="Referer",
107
129
  description="Referer header sent when fetching the target URL.",
108
130
  )
109
- proxy_url: str | None = Field(
131
+ proxy_url: StrOrNone = Field(
110
132
  default=None,
111
133
  title="Proxy URL",
112
134
  description="Proxy URL used by Jina Reader to access the target page.",
@@ -1,21 +1,34 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import ClassVar, Literal
3
+ from typing import Annotated, ClassVar, Literal, TypeAlias
4
4
 
5
5
  from pydantic import Field, model_validator
6
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
6
7
 
7
8
  from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig, CrawlerType
9
+ from unique_search_proxy_core.schema import DeactivatedNone
8
10
 
9
11
  TavilyExtractDepth = Literal["basic", "advanced"]
10
12
  TavilyExtractFormat = Literal["markdown", "text"]
11
13
 
14
+ StrOrNone: TypeAlias = Annotated[str, Field(title="String")] | DeactivatedNone
15
+ ChunksPerSourceOrNone: TypeAlias = (
16
+ Annotated[int, Field(title="Integer", ge=1, le=5)] | DeactivatedNone
17
+ )
18
+
12
19
 
13
20
  class TavilyConfig(BaseCrawlerConfig[CrawlerType.TAVILY]):
14
21
  """Deployment config for the Tavily Extract crawler."""
15
22
 
16
23
  _request_model_name: ClassVar[str] = "TavilyCrawlRequest"
17
24
 
18
- crawler: Literal[CrawlerType.TAVILY] = CrawlerType.TAVILY
25
+ crawler: Annotated[
26
+ Literal[CrawlerType.TAVILY], RJSFMetaTag.SpecialWidget.hidden()
27
+ ] = Field(
28
+ default=CrawlerType.TAVILY,
29
+ title="Crawler",
30
+ description="Provider discriminator; must be `Tavily` for this config.",
31
+ )
19
32
 
20
33
  extract_depth: TavilyExtractDepth = Field(
21
34
  default="advanced",
@@ -30,7 +43,7 @@ class TavilyConfig(BaseCrawlerConfig[CrawlerType.TAVILY]):
30
43
  title="Output format",
31
44
  description="Extracted content format: `markdown` or `text`.",
32
45
  )
33
- query: str | None = Field(
46
+ query: StrOrNone = Field(
34
47
  default=None,
35
48
  title="Rerank query",
36
49
  description=(
@@ -38,10 +51,8 @@ class TavilyConfig(BaseCrawlerConfig[CrawlerType.TAVILY]):
38
51
  "`chunks_per_source` may be used."
39
52
  ),
40
53
  )
41
- chunks_per_source: int | None = Field(
54
+ chunks_per_source: ChunksPerSourceOrNone = Field(
42
55
  default=None,
43
- ge=1,
44
- le=5,
45
56
  title="Chunks per source",
46
57
  description=(
47
58
  "Max relevant chunks per URL when `query` is set (1–5). "
@@ -9,10 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  from pydantic import BaseModel, Field
11
11
 
12
- from unique_search_proxy_core.schema import (
13
- camelized_model_config,
14
- deployment_model_config,
15
- )
12
+ from unique_search_proxy_core.schema import camelized_model_config
16
13
 
17
14
 
18
15
  class SearchRequestBase(BaseModel):
@@ -27,11 +24,7 @@ class AgentRequestBase(SearchRequestBase):
27
24
 
28
25
 
29
26
  class CrawlRequestBase(BaseModel):
30
- """Base for derived ``POST /v1/crawl`` bodies: carries the required ``urls``.
27
+ """Base for derived ``POST /v1/crawl`` bodies: carries the required ``urls``."""
31
28
 
32
- Uses ``deployment_model_config`` (``extra='forbid'``) to match the crawler
33
- configs' schema (``additionalProperties: false``).
34
- """
35
-
36
- model_config = deployment_model_config
29
+ model_config = camelized_model_config
37
30
  urls: list[str] = Field(..., min_length=1, description="URLs to crawl")
@@ -35,15 +35,6 @@ def get_model_config(title: str | None = None) -> ConfigDict:
35
35
  camelized_model_config = get_model_config()
36
36
 
37
37
 
38
- deployment_model_config = ConfigDict(
39
- alias_generator=to_camel,
40
- field_title_generator=field_title_generator,
41
- model_title_generator=model_title_generator,
42
- populate_by_name=True,
43
- extra="forbid",
44
- )
45
-
46
-
47
38
  # Marks the deactivated branch of an optional config value. Locally declared so
48
39
  # core has no runtime dependency on unique-toolkit for this one-line alias.
49
40
  DeactivatedNone = Annotated[
@@ -17,6 +17,13 @@ _LOGGER = logging.getLogger(__name__)
17
17
 
18
18
  _REDIRECT_STATUS_CODES = frozenset({301, 302, 303, 307, 308})
19
19
 
20
+ # Browser-like UA avoids CDN bot stalls (e.g. Akamai) that make fail-closed
21
+ # redirect probing falsely block legitimate public pages.
22
+ _REDIRECT_PROBE_USER_AGENT = (
23
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
24
+ "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
25
+ )
26
+
20
27
  ValidateUrlFn = Callable[[str], Awaitable[tuple[str, str] | None]]
21
28
 
22
29
 
@@ -32,7 +39,11 @@ async def resolve_redirect_chain(
32
39
  """
33
40
  current = url
34
41
  timeout = url_safety_settings.redirect_timeout_seconds
35
- async with httpx.AsyncClient(follow_redirects=False, timeout=timeout) as client:
42
+ async with httpx.AsyncClient(
43
+ follow_redirects=False,
44
+ timeout=timeout,
45
+ headers={"User-Agent": _REDIRECT_PROBE_USER_AGENT},
46
+ ) as client:
36
47
  for _ in range(url_safety_settings.max_redirect_hops):
37
48
  error = await validate_url(current)
38
49
  if error is not None:
@@ -1,49 +0,0 @@
1
- """Crawler request merge helpers (no HTTP)."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any
6
-
7
- from pydantic import BaseModel
8
-
9
- from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig
10
- from unique_search_proxy_core.crawlers.config_types import crawler_config_from_request
11
- from unique_search_proxy_core.param_policy import URLS_FIELD
12
-
13
- CRAWLER_FIELD = "crawler"
14
- TIMEOUT_FIELD = "timeout"
15
-
16
- _DEPLOYMENT_DEFAULT_EXCLUDED_FIELDS = frozenset({CRAWLER_FIELD, URLS_FIELD})
17
-
18
-
19
- def crawler_config_defaults(config: BaseModel) -> dict[str, Any]:
20
- """Deployment defaults merged into each flat crawl request."""
21
- defaults: dict[str, Any] = {}
22
- for field_name in type(config).model_fields:
23
- if field_name in _DEPLOYMENT_DEFAULT_EXCLUDED_FIELDS:
24
- continue
25
- defaults[field_name] = getattr(config, field_name)
26
- return defaults
27
-
28
-
29
- def merge_crawler_config_and_invocation(
30
- config: BaseCrawlerConfig,
31
- invocation: dict[str, Any],
32
- ) -> BaseModel:
33
- """Merge deployment config defaults with caller/LLM args into a flat crawl request."""
34
- request_model = config.request_model()
35
- defaults = crawler_config_defaults(config)
36
- merged: dict[str, Any] = {**defaults, **invocation}
37
- merged[CRAWLER_FIELD] = getattr(config, CRAWLER_FIELD)
38
- request = request_model.model_validate(merged)
39
- crawler_config_from_request(request)
40
- return request
41
-
42
-
43
- __all__ = [
44
- "CRAWLER_FIELD",
45
- "TIMEOUT_FIELD",
46
- "URLS_FIELD",
47
- "crawler_config_defaults",
48
- "merge_crawler_config_and_invocation",
49
- ]