unique-search-proxy-core 2026.30.0.dev0__tar.gz → 2026.30.0.dev2__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 (66) hide show
  1. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/PKG-INFO +59 -90
  2. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/README.md +57 -88
  3. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/pyproject.toml +2 -2
  4. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/base.py +26 -1
  5. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/bing/schema.py +6 -9
  6. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/config_types.py +3 -6
  7. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/vertexai/schema.py +5 -9
  8. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/base.py +20 -1
  9. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/basic/schema.py +4 -3
  10. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/config_types.py +2 -7
  11. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/firecrawl/schema.py +4 -3
  12. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/jina/schema.py +4 -3
  13. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/params.py +4 -6
  14. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/tavily/schema.py +4 -3
  15. unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/param_policy/__init__.py +24 -0
  16. unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/param_policy/annotations.py +135 -0
  17. unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/param_policy/derive.py +119 -0
  18. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/param_policy/exposable_param.py +15 -26
  19. unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/param_policy/exposed_params.py +66 -0
  20. unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/param_policy/request_base.py +37 -0
  21. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/search_engines/__init__.py +0 -8
  22. unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/search_engines/base.py +218 -0
  23. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/search_engines/brave/__init__.py +0 -2
  24. unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/search_engines/brave/schema.py +332 -0
  25. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/search_engines/config_types.py +3 -4
  26. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/search_engines/google/__init__.py +0 -2
  27. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/search_engines/google/schema.py +28 -48
  28. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/search_engines/perplexity/__init__.py +0 -2
  29. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/search_engines/perplexity/schema.py +36 -71
  30. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/agent_engines/projection.py +0 -47
  31. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/crawlers/projection.py +0 -43
  32. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/model_derivation/__init__.py +0 -21
  33. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/model_derivation/derive.py +0 -52
  34. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/model_derivation/fields.py +0 -144
  35. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/param_policy/__init__.py +0 -21
  36. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/param_policy/policy.py +0 -3
  37. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/projection.py +0 -133
  38. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/search_engines/base.py +0 -114
  39. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/search_engines/brave/schema.py +0 -178
  40. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/search_engines/call_schema.py +0 -78
  41. unique_search_proxy_core-2026.30.0.dev0/unique_search_proxy_core/search_engines/params.py +0 -99
  42. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/__init__.py +0 -0
  43. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/__init__.py +0 -0
  44. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/bing/__init__.py +0 -0
  45. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/output_schema.py +0 -0
  46. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/resolve.py +0 -0
  47. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/response_parsing.py +0 -0
  48. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/agent_engines/vertexai/__init__.py +0 -0
  49. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/__init__.py +0 -0
  50. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/basic/__init__.py +0 -0
  51. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/basic/content_types.py +0 -0
  52. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/crawlers/basic/processing/policy.py +0 -0
  53. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/errors.py +0 -0
  54. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/logging.py +0 -0
  55. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/providers/schema.py +0 -0
  56. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/schema.py +0 -0
  57. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/__init__.py +0 -0
  58. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/dns.py +0 -0
  59. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/egress.py +0 -0
  60. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/models.py +0 -0
  61. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/netloc.py +0 -0
  62. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/policy.py +0 -0
  63. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/redirect.py +0 -0
  64. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/resolver.py +0 -0
  65. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/unique_search_proxy_core/url_safety/service.py +0 -0
  66. {unique_search_proxy_core-2026.30.0.dev0 → unique_search_proxy_core-2026.30.0.dev2}/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.dev0
3
+ Version: 2026.30.0.dev2
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.dev0,<2026.30.0rc0
11
+ Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev10,<2026.30.0rc0
12
12
  Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
14
 
@@ -46,17 +46,15 @@ flowchart TB
46
46
 
47
47
  subgraph core["unique_search_proxy_core"]
48
48
  Contracts["Response & error models"]
49
- Config["*Config deployment models"]
50
- Project["Schema projection & merge"]
49
+ Config["*Config deployment models\n(request_model / exposed_params_model / merge)"]
51
50
  end
52
51
 
53
52
  AC --> Config
54
- AC --> Project
55
53
  Client --> Contracts
56
54
  Client --> Config
57
55
  SDK --> Contracts
58
- Project -->|"flat request body"| SDK
59
- Project -->|"flat request body"| Client
56
+ Config -->|"flat request body"| SDK
57
+ Config -->|"flat request body"| Client
60
58
  ```
61
59
 
62
60
  System overview → [../README.md](../README.md)
@@ -65,81 +63,63 @@ System overview → [../README.md](../README.md)
65
63
 
66
64
  ## 3. Key concepts
67
65
 
68
- Core uses **three different config patterns** depending on provider kind. Only search engines use `ExposableParam` and the full projection pipeline.
66
+ **The config class owns its entire parameter lifecycle.** Every derived surface is a method on the deployment config — there are no standalone projection or resolver modules.
69
67
 
70
- ### 3.1 Three provider patterns (search vs agent vs crawl)
68
+ ### 3.1 Config-owned API (search engines)
71
69
 
72
- | Kind | Config → request | `ExposableParam` | LLM call schema | Config/invocation merge |
73
- |------|------------------|------------------|-----------------|-------------------------|
74
- | **Search engines** | `GoogleConfig` → `GoogleSearchRequest` via `projection.build_request_model` | Yes | `build_llm_call_model` + `call_schema.resolve_*` | `merge_config_and_invocation` |
75
- | **Agent engines** | `BingAgentConfig` → `BingAgentSearchRequest` via `agent_engines/projection.build_agent_request_model` | No (plain fields) | Not implemented in core | Not implemented in core |
76
- | **Crawlers** | `BasicConfig` → `BasicCrawlRequest` via `crawlers/projection.build_crawl_request_model` | No | Not implemented in core | `merge_crawler_config_and_invocation` |
77
-
78
- ```mermaid
79
- flowchart TB
80
- subgraph search["Search engines only"]
81
- GC["GoogleConfig\n(ExposableParam knobs)"]
82
- Proj["projection.py\nbuild_request_model\nbuild_llm_call_model"]
83
- GSR["GoogleSearchRequest"]
84
- LLM["LLM call schema"]
85
- GC --> Proj
86
- Proj --> GSR
87
- Proj --> LLM
88
- end
89
-
90
- subgraph agent["Agent engines"]
91
- BC["BingAgentConfig\n(plain fields)"]
92
- AProj["agent_engines/projection.py\nbuild_agent_request_model"]
93
- BSR["BingAgentSearchRequest"]
94
- BC --> AProj
95
- AProj -->|"via model_derivation"| BSR
96
- end
97
-
98
- subgraph crawl["Crawlers"]
99
- BCrawl["BasicConfig\n(no urls)"]
100
- CProj["crawlers/projection.py\nbuild_crawl_request_model"]
101
- BCR["BasicCrawlRequest\n(urls injected)"]
102
- BCrawl --> CProj
103
- CProj -->|"via model_derivation"| BCR
104
- end
70
+ ```
71
+ Admin JSON → GoogleConfig {expose, value}
72
+ ├─ GoogleConfig.request_model() → GoogleSearchRequest (HTTP body, query required)
73
+ ├─ config.exposed_params_model() → GoogleExposedParams (LLM knobs; tool inherits it)
74
+ ├─ config.merge(llm_args, query=…) → validated GoogleSearchRequest
75
+ └─ GoogleConfig.provider_query_params(req) → upstream provider dict
105
76
  ```
106
77
 
107
- ### 3.2 Search engines — three surfaces from one config
108
-
109
- Search is the only kind where a single `*Config` fans out into three derived surfaces:
110
-
111
- | Surface | Audience | Example |
112
- |---------|----------|---------|
113
- | **Deployment config** | Admin / deployment UI | `{ "engine": "google", "gl": { "expose": true, "value": "de" }, … }` |
114
- | **LLM call schema** | Tool manifest shown to the model | `{ "query", "gl" }` — only fields where `expose: true` |
115
- | **HTTP request body** | `POST /v1/search` wire format | `{ "engine": "google", "query": "…", "gl": "de", "fetchSize": 10 }` |
78
+ | Method | Kind | Returns |
79
+ |--------|------|---------|
80
+ | `request_model()` (classmethod, cached) | search / agent / crawl | `SearchRequestBase` (required `query`) or `CrawlRequestBase` (required `urls`) + config fields; `ExposableParam` knobs unwrapped to optional plain types |
81
+ | `exposed_params_model()` (instance) | search only | `ExposedParams` subclass with exactly the `expose=True` knobs (camelCase aliases, description-only schema), or `None` |
82
+ | `merge(overrides, *, query)` (instance) | search only | deployment defaults + LLM overrides + query → validated `request_model()` instance |
83
+ | `provider_query_params(request)` (classmethod) | search only | request serialized for the upstream provider, minus `_provider_param_exclude_fields` (Google adds `search_engine_id`) |
116
84
 
117
- `projection.py` and `search_engines/params.py` derive the latter two from the first. Agent and crawl use `model_derivation.derive_request_model` via per-kind projection modules to inject `query` or `urls`.
85
+ Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()`; crawlers additionally keep `merge_crawler_config_and_invocation`.
118
86
 
119
- ### 3.3 ExposableParam — search-engine only
87
+ ### 3.2 ExposableParam — search-engine only
120
88
 
121
89
  Optional **search** parameters use `ExposableParam[T]`:
122
90
 
123
91
  - **`value`** — admin default merged into every request (`null` = deactivated)
124
- - **`expose`** — when `true`, the parameter appears on the LLM call schema
92
+ - **`expose`** — when `true`, the parameter appears on the LLM-facing exposed-params model
125
93
 
126
94
  ```python
127
95
  gl: ExposableParam[str | None] = ExposableParam(expose=False, value="de") # admin-fixed
128
96
  gl: ExposableParam[str | None] = ExposableParam(expose=True, value=None) # LLM-overridable
129
97
  ```
130
98
 
131
- No agent or crawler schema imports `ExposableParam` today.
99
+ Bare scalars are **not** coerced — deployment JSON must use the explicit `{"expose": …, "value": …}` shape.
100
+
101
+ ### 3.3 ExposedParams — the LLM tool-schema contract
132
102
 
133
- ### 3.4 merge_config_and_invocation — search-engine only
103
+ `config.exposed_params_model()` returns a plain Pydantic model class. Tool-parameter models graft the knobs on by **ordinary inheritance** — no field-def plumbing, no stamped attributes:
134
104
 
135
105
  ```python
136
- from unique_search_proxy_core.search_engines import merge_config_and_invocation
106
+ Exposed = config.exposed_params_model() # GoogleExposedParams | None
107
+ class ToolParams(ToolParamsBase, Exposed): ... # or create_model(__base__=(...))
108
+
109
+ ToolParams.model_json_schema() # camelCase knobs, no title/default noise
110
+ Exposed.model_fields # the exposed field names
111
+ ```
112
+
113
+ The shared `ExposedParams` base owns the single JSON-schema concern: its `model_json_schema` strips Pydantic's auto-`title` and `default` noise, so admin defaults never leak into what the LLM sees.
137
114
 
138
- request = merge_config_and_invocation(google_config, {"query": "EU AI Act"})
115
+ ### 3.4 merge — deployment defaults + LLM overrides
116
+
117
+ ```python
118
+ request = google_config.merge({"gl": "de"}, query="EU AI Act")
139
119
  # → validated GoogleSearchRequest ready for POST /v1/search
140
120
  ```
141
121
 
142
- The proxy receives a flat body; it does not resolve deployment config over HTTP.
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.
143
123
 
144
124
  ### 3.5 merge_crawler_config_and_invocation — crawler only
145
125
 
@@ -159,25 +139,16 @@ flowchart TB
159
139
  subgraph core_pkg["unique_search_proxy_core"]
160
140
  Schema["schema.py"]
161
141
  Errors["errors.py"]
162
- MD["model_derivation/"]
163
- PP["param_policy/"]
164
- Proj["projection.py\n(search + LLM projection)"]
165
- AProj["agent_engines/projection.py"]
166
- CProj["crawlers/projection.py"]
142
+ PP["param_policy/\nExposableParam · ExposedParams\nrequest bases · derive"]
167
143
  Prov["providers/schema.py"]
168
144
  SE["search_engines/"]
169
145
  AE["agent_engines/"]
170
146
  CR["crawlers/"]
171
147
  end
172
148
 
173
- PP --> Proj
174
149
  PP --> SE
175
- MD --> Proj
176
- MD --> AProj
177
- MD --> CProj
178
- Proj --> SE
179
- AProj --> AE
180
- CProj --> CR
150
+ PP --> AE
151
+ PP --> CR
181
152
  Prov --> SE
182
153
  Prov --> CR
183
154
  SE --> Schema
@@ -190,15 +161,15 @@ flowchart TB
190
161
  |--------|----------------|
191
162
  | `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse`, `CrawlResponse`, `WebSearchResult`, `ErrorResponse`, SSE events |
192
163
  | `errors.py` | `ProxyError` hierarchy and stable `ProxyErrorCode` enum |
193
- | `model_derivation/` | Shared `derive_request_model` + field/annotation helpers used by all three provider kinds |
194
- | `param_policy/` | `ExposableParam` — used by **search engine** config models only |
195
- | `projection.py` | Search-only: `build_request_model`, `build_llm_call_model`, `project_call_schema` |
196
- | `agent_engines/projection.py` | Agent-only: `build_agent_request_model` (injects `query`; excludes `output_schema`) |
197
- | `crawlers/projection.py` | Crawler-only: `build_crawl_request_model` (injects `urls`) |
164
+ | `param_policy/exposable_param.py` | `ExposableParam` value object, factory-default merge, type introspection, OpenAPI naming |
165
+ | `param_policy/exposed_params.py` | `ExposedParams` base for LLM-facing parameter models (schema noise stripping) |
166
+ | `param_policy/request_base.py` | `SearchRequestBase` / `AgentRequestBase` / `CrawlRequestBase` (required leading fields) |
167
+ | `param_policy/annotations.py` | Annotation unwrapping helpers (private, used by `derive`) |
168
+ | `param_policy/derive.py` | `derive_request_model` / `derive_exposed_params_model` factories called by the config base classes |
198
169
  | `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
199
- | `search_engines/` | Config models, request union, `merge_config_and_invocation`, call-schema resolution |
200
- | `agent_engines/` | Agent config/request models, output schema |
201
- | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` HTTP bodies |
170
+ | `search_engines/` | Config models with the config-owned API (`request_model` / `exposed_params_model` / `merge` / `provider_query_params`), request union |
171
+ | `agent_engines/` | Agent config/request models (`request_model`), output schema |
172
+ | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies, `merge_crawler_config_and_invocation` |
202
173
 
203
174
  ---
204
175
 
@@ -230,21 +201,19 @@ schema = provider_config_json_schema("search_engine", "google")
230
201
  defaults = provider_default_config("search_engine", "google")
231
202
  ```
232
203
 
233
- ### Tool manifest — LLM call schema
204
+ ### Tool manifest — exposed LLM knobs
234
205
 
235
206
  ```python
236
- from unique_search_proxy_core.search_engines.call_schema import resolve_search_call_schema
237
-
238
- descriptor = resolve_search_call_schema("google", config=google_config, strict=False)
239
- # descriptor.call_schema → JSON Schema for the LLM tool
207
+ Exposed = google_config.exposed_params_model() # GoogleExposedParams | None
208
+ class ToolParams(ToolParamsBase, Exposed): ... # graft knobs by inheritance
209
+ ToolParams.model_json_schema() # camelCase, no title/default noise
240
210
  ```
241
211
 
242
212
  ### Runtime — build flat request before HTTP call
243
213
 
244
214
  ```python
245
- from unique_search_proxy_core.search_engines import merge_config_and_invocation
246
-
247
- body = merge_config_and_invocation(config, llm_invocation_dict)
215
+ request = google_config.merge(llm_invocation_dict, query="EU AI Act")
216
+ body = GoogleConfig.provider_query_params(request) # upstream provider dict
248
217
  ```
249
218
 
250
219
  ### Shared types and errors
@@ -263,9 +232,9 @@ from unique_search_proxy_core import (
263
232
  ## 7. Features summary
264
233
 
265
234
  - Discriminated provider configs (`engine`, `crawler` Literal discriminators)
266
- - **Search-only:** `ExposableParam` policy, three-surface projection, LLM call schema, `merge_config_and_invocation`
267
- - **Agent:** config → request derivation via `model_derivation` + `build_agent_request_model`
268
- - **Crawl:** `*Config` + `build_crawl_request_model` (injects `urls`); `merge_crawler_config_and_invocation`
235
+ - **Search-only:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` / `provider_query_params`
236
+ - **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`)
237
+ - **Crawl:** `BasicConfig.request_model()` (injects `urls`); `merge_crawler_config_and_invocation`
269
238
  - CamelCase JSON aliases on all models
270
239
  - Zero server dependencies (import-linter enforced in the client package)
271
240
 
@@ -32,17 +32,15 @@ flowchart TB
32
32
 
33
33
  subgraph core["unique_search_proxy_core"]
34
34
  Contracts["Response & error models"]
35
- Config["*Config deployment models"]
36
- Project["Schema projection & merge"]
35
+ Config["*Config deployment models\n(request_model / exposed_params_model / merge)"]
37
36
  end
38
37
 
39
38
  AC --> Config
40
- AC --> Project
41
39
  Client --> Contracts
42
40
  Client --> Config
43
41
  SDK --> Contracts
44
- Project -->|"flat request body"| SDK
45
- Project -->|"flat request body"| Client
42
+ Config -->|"flat request body"| SDK
43
+ Config -->|"flat request body"| Client
46
44
  ```
47
45
 
48
46
  System overview → [../README.md](../README.md)
@@ -51,81 +49,63 @@ System overview → [../README.md](../README.md)
51
49
 
52
50
  ## 3. Key concepts
53
51
 
54
- Core uses **three different config patterns** depending on provider kind. Only search engines use `ExposableParam` and the full projection pipeline.
52
+ **The config class owns its entire parameter lifecycle.** Every derived surface is a method on the deployment config — there are no standalone projection or resolver modules.
55
53
 
56
- ### 3.1 Three provider patterns (search vs agent vs crawl)
54
+ ### 3.1 Config-owned API (search engines)
57
55
 
58
- | Kind | Config → request | `ExposableParam` | LLM call schema | Config/invocation merge |
59
- |------|------------------|------------------|-----------------|-------------------------|
60
- | **Search engines** | `GoogleConfig` → `GoogleSearchRequest` via `projection.build_request_model` | Yes | `build_llm_call_model` + `call_schema.resolve_*` | `merge_config_and_invocation` |
61
- | **Agent engines** | `BingAgentConfig` → `BingAgentSearchRequest` via `agent_engines/projection.build_agent_request_model` | No (plain fields) | Not implemented in core | Not implemented in core |
62
- | **Crawlers** | `BasicConfig` → `BasicCrawlRequest` via `crawlers/projection.build_crawl_request_model` | No | Not implemented in core | `merge_crawler_config_and_invocation` |
63
-
64
- ```mermaid
65
- flowchart TB
66
- subgraph search["Search engines only"]
67
- GC["GoogleConfig\n(ExposableParam knobs)"]
68
- Proj["projection.py\nbuild_request_model\nbuild_llm_call_model"]
69
- GSR["GoogleSearchRequest"]
70
- LLM["LLM call schema"]
71
- GC --> Proj
72
- Proj --> GSR
73
- Proj --> LLM
74
- end
75
-
76
- subgraph agent["Agent engines"]
77
- BC["BingAgentConfig\n(plain fields)"]
78
- AProj["agent_engines/projection.py\nbuild_agent_request_model"]
79
- BSR["BingAgentSearchRequest"]
80
- BC --> AProj
81
- AProj -->|"via model_derivation"| BSR
82
- end
83
-
84
- subgraph crawl["Crawlers"]
85
- BCrawl["BasicConfig\n(no urls)"]
86
- CProj["crawlers/projection.py\nbuild_crawl_request_model"]
87
- BCR["BasicCrawlRequest\n(urls injected)"]
88
- BCrawl --> CProj
89
- CProj -->|"via model_derivation"| BCR
90
- end
56
+ ```
57
+ Admin JSON → GoogleConfig {expose, value}
58
+ ├─ GoogleConfig.request_model() → GoogleSearchRequest (HTTP body, query required)
59
+ ├─ config.exposed_params_model() → GoogleExposedParams (LLM knobs; tool inherits it)
60
+ ├─ config.merge(llm_args, query=…) → validated GoogleSearchRequest
61
+ └─ GoogleConfig.provider_query_params(req) → upstream provider dict
91
62
  ```
92
63
 
93
- ### 3.2 Search engines — three surfaces from one config
94
-
95
- Search is the only kind where a single `*Config` fans out into three derived surfaces:
96
-
97
- | Surface | Audience | Example |
98
- |---------|----------|---------|
99
- | **Deployment config** | Admin / deployment UI | `{ "engine": "google", "gl": { "expose": true, "value": "de" }, … }` |
100
- | **LLM call schema** | Tool manifest shown to the model | `{ "query", "gl" }` — only fields where `expose: true` |
101
- | **HTTP request body** | `POST /v1/search` wire format | `{ "engine": "google", "query": "…", "gl": "de", "fetchSize": 10 }` |
64
+ | Method | Kind | Returns |
65
+ |--------|------|---------|
66
+ | `request_model()` (classmethod, cached) | search / agent / crawl | `SearchRequestBase` (required `query`) or `CrawlRequestBase` (required `urls`) + config fields; `ExposableParam` knobs unwrapped to optional plain types |
67
+ | `exposed_params_model()` (instance) | search only | `ExposedParams` subclass with exactly the `expose=True` knobs (camelCase aliases, description-only schema), or `None` |
68
+ | `merge(overrides, *, query)` (instance) | search only | deployment defaults + LLM overrides + query → validated `request_model()` instance |
69
+ | `provider_query_params(request)` (classmethod) | search only | request serialized for the upstream provider, minus `_provider_param_exclude_fields` (Google adds `search_engine_id`) |
102
70
 
103
- `projection.py` and `search_engines/params.py` derive the latter two from the first. Agent and crawl use `model_derivation.derive_request_model` via per-kind projection modules to inject `query` or `urls`.
71
+ Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()`; crawlers additionally keep `merge_crawler_config_and_invocation`.
104
72
 
105
- ### 3.3 ExposableParam — search-engine only
73
+ ### 3.2 ExposableParam — search-engine only
106
74
 
107
75
  Optional **search** parameters use `ExposableParam[T]`:
108
76
 
109
77
  - **`value`** — admin default merged into every request (`null` = deactivated)
110
- - **`expose`** — when `true`, the parameter appears on the LLM call schema
78
+ - **`expose`** — when `true`, the parameter appears on the LLM-facing exposed-params model
111
79
 
112
80
  ```python
113
81
  gl: ExposableParam[str | None] = ExposableParam(expose=False, value="de") # admin-fixed
114
82
  gl: ExposableParam[str | None] = ExposableParam(expose=True, value=None) # LLM-overridable
115
83
  ```
116
84
 
117
- No agent or crawler schema imports `ExposableParam` today.
85
+ Bare scalars are **not** coerced — deployment JSON must use the explicit `{"expose": …, "value": …}` shape.
86
+
87
+ ### 3.3 ExposedParams — the LLM tool-schema contract
118
88
 
119
- ### 3.4 merge_config_and_invocation — search-engine only
89
+ `config.exposed_params_model()` returns a plain Pydantic model class. Tool-parameter models graft the knobs on by **ordinary inheritance** — no field-def plumbing, no stamped attributes:
120
90
 
121
91
  ```python
122
- from unique_search_proxy_core.search_engines import merge_config_and_invocation
92
+ Exposed = config.exposed_params_model() # GoogleExposedParams | None
93
+ class ToolParams(ToolParamsBase, Exposed): ... # or create_model(__base__=(...))
94
+
95
+ ToolParams.model_json_schema() # camelCase knobs, no title/default noise
96
+ Exposed.model_fields # the exposed field names
97
+ ```
98
+
99
+ The shared `ExposedParams` base owns the single JSON-schema concern: its `model_json_schema` strips Pydantic's auto-`title` and `default` noise, so admin defaults never leak into what the LLM sees.
123
100
 
124
- request = merge_config_and_invocation(google_config, {"query": "EU AI Act"})
101
+ ### 3.4 merge — deployment defaults + LLM overrides
102
+
103
+ ```python
104
+ request = google_config.merge({"gl": "de"}, query="EU AI Act")
125
105
  # → validated GoogleSearchRequest ready for POST /v1/search
126
106
  ```
127
107
 
128
- The proxy receives a flat body; it does not resolve deployment config over HTTP.
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.
129
109
 
130
110
  ### 3.5 merge_crawler_config_and_invocation — crawler only
131
111
 
@@ -145,25 +125,16 @@ flowchart TB
145
125
  subgraph core_pkg["unique_search_proxy_core"]
146
126
  Schema["schema.py"]
147
127
  Errors["errors.py"]
148
- MD["model_derivation/"]
149
- PP["param_policy/"]
150
- Proj["projection.py\n(search + LLM projection)"]
151
- AProj["agent_engines/projection.py"]
152
- CProj["crawlers/projection.py"]
128
+ PP["param_policy/\nExposableParam · ExposedParams\nrequest bases · derive"]
153
129
  Prov["providers/schema.py"]
154
130
  SE["search_engines/"]
155
131
  AE["agent_engines/"]
156
132
  CR["crawlers/"]
157
133
  end
158
134
 
159
- PP --> Proj
160
135
  PP --> SE
161
- MD --> Proj
162
- MD --> AProj
163
- MD --> CProj
164
- Proj --> SE
165
- AProj --> AE
166
- CProj --> CR
136
+ PP --> AE
137
+ PP --> CR
167
138
  Prov --> SE
168
139
  Prov --> CR
169
140
  SE --> Schema
@@ -176,15 +147,15 @@ flowchart TB
176
147
  |--------|----------------|
177
148
  | `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse`, `CrawlResponse`, `WebSearchResult`, `ErrorResponse`, SSE events |
178
149
  | `errors.py` | `ProxyError` hierarchy and stable `ProxyErrorCode` enum |
179
- | `model_derivation/` | Shared `derive_request_model` + field/annotation helpers used by all three provider kinds |
180
- | `param_policy/` | `ExposableParam` — used by **search engine** config models only |
181
- | `projection.py` | Search-only: `build_request_model`, `build_llm_call_model`, `project_call_schema` |
182
- | `agent_engines/projection.py` | Agent-only: `build_agent_request_model` (injects `query`; excludes `output_schema`) |
183
- | `crawlers/projection.py` | Crawler-only: `build_crawl_request_model` (injects `urls`) |
150
+ | `param_policy/exposable_param.py` | `ExposableParam` value object, factory-default merge, type introspection, OpenAPI naming |
151
+ | `param_policy/exposed_params.py` | `ExposedParams` base for LLM-facing parameter models (schema noise stripping) |
152
+ | `param_policy/request_base.py` | `SearchRequestBase` / `AgentRequestBase` / `CrawlRequestBase` (required leading fields) |
153
+ | `param_policy/annotations.py` | Annotation unwrapping helpers (private, used by `derive`) |
154
+ | `param_policy/derive.py` | `derive_request_model` / `derive_exposed_params_model` factories called by the config base classes |
184
155
  | `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
185
- | `search_engines/` | Config models, request union, `merge_config_and_invocation`, call-schema resolution |
186
- | `agent_engines/` | Agent config/request models, output schema |
187
- | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` HTTP bodies |
156
+ | `search_engines/` | Config models with the config-owned API (`request_model` / `exposed_params_model` / `merge` / `provider_query_params`), request union |
157
+ | `agent_engines/` | Agent config/request models (`request_model`), output schema |
158
+ | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies, `merge_crawler_config_and_invocation` |
188
159
 
189
160
  ---
190
161
 
@@ -216,21 +187,19 @@ schema = provider_config_json_schema("search_engine", "google")
216
187
  defaults = provider_default_config("search_engine", "google")
217
188
  ```
218
189
 
219
- ### Tool manifest — LLM call schema
190
+ ### Tool manifest — exposed LLM knobs
220
191
 
221
192
  ```python
222
- from unique_search_proxy_core.search_engines.call_schema import resolve_search_call_schema
223
-
224
- descriptor = resolve_search_call_schema("google", config=google_config, strict=False)
225
- # descriptor.call_schema → JSON Schema for the LLM tool
193
+ Exposed = google_config.exposed_params_model() # GoogleExposedParams | None
194
+ class ToolParams(ToolParamsBase, Exposed): ... # graft knobs by inheritance
195
+ ToolParams.model_json_schema() # camelCase, no title/default noise
226
196
  ```
227
197
 
228
198
  ### Runtime — build flat request before HTTP call
229
199
 
230
200
  ```python
231
- from unique_search_proxy_core.search_engines import merge_config_and_invocation
232
-
233
- body = merge_config_and_invocation(config, llm_invocation_dict)
201
+ request = google_config.merge(llm_invocation_dict, query="EU AI Act")
202
+ body = GoogleConfig.provider_query_params(request) # upstream provider dict
234
203
  ```
235
204
 
236
205
  ### Shared types and errors
@@ -249,9 +218,9 @@ from unique_search_proxy_core import (
249
218
  ## 7. Features summary
250
219
 
251
220
  - Discriminated provider configs (`engine`, `crawler` Literal discriminators)
252
- - **Search-only:** `ExposableParam` policy, three-surface projection, LLM call schema, `merge_config_and_invocation`
253
- - **Agent:** config → request derivation via `model_derivation` + `build_agent_request_model`
254
- - **Crawl:** `*Config` + `build_crawl_request_model` (injects `urls`); `merge_crawler_config_and_invocation`
221
+ - **Search-only:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` / `provider_query_params`
222
+ - **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`)
223
+ - **Crawl:** `BasicConfig.request_model()` (injects `urls`); `merge_crawler_config_and_invocation`
255
224
  - CamelCase JSON aliases on all models
256
225
  - Zero server dependencies (import-linter enforced in the client package)
257
226
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy-core"
3
- version = "2026.30.0.dev0"
3
+ version = "2026.30.0.dev2"
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.dev0,<2026.30.0rc0",
13
+ "unique-toolkit[monitoring]>=2026.30.0.dev10,<2026.30.0rc0",
14
14
 
15
15
  ]
16
16
 
@@ -2,13 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from enum import StrEnum
5
- from typing import TYPE_CHECKING, Annotated, AsyncIterator, Generic, TypeVar
5
+ from typing import TYPE_CHECKING, Annotated, AsyncIterator, ClassVar, Generic, TypeVar
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
  from pydantic.json_schema import SkipJsonSchema
9
9
  from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
10
10
 
11
11
  from unique_search_proxy_core.agent_engines.output_schema import AgentSearchOutput
12
+ from unique_search_proxy_core.param_policy.derive import derive_request_model
13
+ from unique_search_proxy_core.param_policy.request_base import AgentRequestBase
12
14
  from unique_search_proxy_core.schema import (
13
15
  AgentSearchResponse,
14
16
  AgentSearchStreamEvent,
@@ -42,11 +44,34 @@ class AgentEngineType(StrEnum):
42
44
  VERTEXAI = "vertexai"
43
45
 
44
46
 
47
+ #: Config fields not accepted on the agent-search request body.
48
+ _AGENT_REQUEST_EXCLUDED_FIELDS = frozenset({"output_schema"})
49
+
50
+
45
51
  class BaseAgentEngineConfig(BaseModel, Generic[T]):
46
52
  """Shared agent-engine config; each engine narrows ``engine`` with a Literal."""
47
53
 
48
54
  model_config = camelized_model_config
49
55
 
56
+ #: Name of the derived request model; set by every concrete agent config.
57
+ _request_model_name: ClassVar[str]
58
+
59
+ @classmethod
60
+ def request_model(cls) -> type[BaseModel]:
61
+ """HTTP request body model, cached per config class.
62
+
63
+ :class:`AgentRequestBase` (required ``query``) + this config's fields,
64
+ excluding ``output_schema`` (server-side concern, not a request knob).
65
+
66
+ Example: ``BingAgentConfig.request_model()`` -> ``BingAgentSearchRequest``.
67
+ """
68
+ return derive_request_model(
69
+ cls,
70
+ base=AgentRequestBase,
71
+ name=cls._request_model_name,
72
+ exclude=_AGENT_REQUEST_EXCLUDED_FIELDS,
73
+ )
74
+
50
75
  engine: T
51
76
  generation_instructions: Annotated[
52
77
  str,
@@ -1,20 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Annotated, Literal
3
+ from typing import Annotated, ClassVar, Literal
4
4
 
5
- from pydantic import BaseModel, Field
5
+ from pydantic import Field
6
6
  from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
7
7
 
8
8
  from unique_search_proxy_core.agent_engines.base import (
9
9
  AgentEngineType,
10
10
  BaseAgentEngineConfig,
11
11
  )
12
- from unique_search_proxy_core.agent_engines.projection import build_agent_request_model
13
12
 
14
13
 
15
14
  class BingAgentConfig(BaseAgentEngineConfig[Literal[AgentEngineType.BING]]):
16
15
  """Deployment + request defaults for Bing grounding via Azure AI Projects."""
17
16
 
17
+ _request_model_name: ClassVar[str] = "BingAgentSearchRequest"
18
+
18
19
  engine: Annotated[
19
20
  Literal[AgentEngineType.BING], RJSFMetaTag.SpecialWidget.hidden()
20
21
  ] = Field(
@@ -37,11 +38,7 @@ class BingAgentConfig(BaseAgentEngineConfig[Literal[AgentEngineType.BING]]):
37
38
  )
38
39
 
39
40
 
40
- def bing_agent_request_model() -> type[BaseModel]:
41
- return build_agent_request_model(BingAgentConfig)
42
-
43
-
44
- BingAgentSearchRequest = bing_agent_request_model()
41
+ BingAgentSearchRequest = BingAgentConfig.request_model()
45
42
 
46
43
 
47
- __all__ = ["BingAgentConfig", "BingAgentSearchRequest", "bing_agent_request_model"]
44
+ __all__ = ["BingAgentConfig", "BingAgentSearchRequest"]
@@ -11,7 +11,6 @@ from unique_search_proxy_core.agent_engines.base import (
11
11
  BaseAgentEngineConfig,
12
12
  )
13
13
  from unique_search_proxy_core.agent_engines.bing.schema import BingAgentConfig
14
- from unique_search_proxy_core.agent_engines.projection import build_agent_request_model
15
14
  from unique_search_proxy_core.agent_engines.vertexai.schema import VertexAIAgentConfig
16
15
 
17
16
  AgentEngineConfigTypes: TypeAlias = BingAgentConfig | VertexAIAgentConfig
@@ -48,17 +47,15 @@ def get_agent_engine_config_types_from_names(
48
47
 
49
48
 
50
49
  def _union_members_from_mapping(
51
- mapping: Mapping[str, type[BaseModel]],
52
- ) -> tuple[type[BaseModel], ...]:
50
+ mapping: Mapping[str, type[BaseAgentEngineConfig]],
51
+ ) -> tuple[type[BaseAgentEngineConfig], ...]:
53
52
  return tuple(mapping.values())
54
53
 
55
54
 
56
55
  def build_agent_search_request_union() -> Any:
57
56
  """Discriminated union of flat ``POST /v1/agent-search`` bodies."""
58
57
  members = _union_members_from_mapping(ENGINE_NAME_TO_CONFIG)
59
- request_models = tuple(
60
- build_agent_request_model(config_cls) for config_cls in members
61
- )
58
+ request_models = tuple(config_cls.request_model() for config_cls in members)
62
59
  if len(request_models) == 1:
63
60
  return request_models[0]
64
61
  return Annotated[