unique-search-proxy-core 2026.36.0.dev2__tar.gz → 2026.36.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 (60) hide show
  1. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/PKG-INFO +12 -11
  2. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/README.md +10 -9
  3. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/pyproject.toml +6 -2
  4. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/agent_engines/base.py +15 -4
  5. unique_search_proxy_core-2026.36.0.dev4/unique_search_proxy_core/agent_engines/bing/__init__.py +21 -0
  6. unique_search_proxy_core-2026.36.0.dev4/unique_search_proxy_core/agent_engines/bing/grounding.py +74 -0
  7. unique_search_proxy_core-2026.36.0.dev4/unique_search_proxy_core/agent_engines/bing/schema.py +231 -0
  8. unique_search_proxy_core-2026.36.0.dev4/unique_search_proxy_core/agent_engines/vertexai/schema.py +74 -0
  9. unique_search_proxy_core-2026.36.0.dev4/unique_search_proxy_core/agent_engines/vertexai/settings.py +52 -0
  10. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/param_policy/derive.py +3 -2
  11. unique_search_proxy_core-2026.36.0.dev4/unique_search_proxy_core/param_policy/exposable_config.py +107 -0
  12. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/param_policy/exposable_param.py +11 -0
  13. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/base.py +9 -73
  14. unique_search_proxy_core-2026.36.0.dev2/unique_search_proxy_core/agent_engines/bing/__init__.py +0 -6
  15. unique_search_proxy_core-2026.36.0.dev2/unique_search_proxy_core/agent_engines/bing/schema.py +0 -37
  16. unique_search_proxy_core-2026.36.0.dev2/unique_search_proxy_core/agent_engines/vertexai/schema.py +0 -45
  17. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/__init__.py +0 -0
  18. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/agent_engines/__init__.py +0 -0
  19. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/agent_engines/config_types.py +0 -0
  20. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/agent_engines/output_schema.py +0 -0
  21. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/agent_engines/resolve.py +0 -0
  22. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/agent_engines/response_parsing.py +0 -0
  23. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/agent_engines/vertexai/__init__.py +0 -0
  24. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/context.py +0 -0
  25. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/__init__.py +0 -0
  26. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/base.py +0 -0
  27. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/basic/__init__.py +0 -0
  28. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/basic/content_types.py +0 -0
  29. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/basic/processing/policy.py +0 -0
  30. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/basic/schema.py +0 -0
  31. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/config_types.py +0 -0
  32. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/firecrawl/schema.py +0 -0
  33. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/jina/schema.py +0 -0
  34. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/crawlers/tavily/schema.py +0 -0
  35. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/errors.py +0 -0
  36. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/logging.py +0 -0
  37. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/param_policy/__init__.py +0 -0
  38. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/param_policy/annotations.py +0 -0
  39. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/param_policy/exposed_params.py +0 -0
  40. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/param_policy/request_base.py +0 -0
  41. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/providers/schema.py +0 -0
  42. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/schema.py +0 -0
  43. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/__init__.py +0 -0
  44. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/brave/__init__.py +0 -0
  45. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/brave/schema.py +0 -0
  46. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/config_types.py +0 -0
  47. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/google/__init__.py +0 -0
  48. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/google/schema.py +0 -0
  49. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/perplexity/__init__.py +0 -0
  50. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/search_engines/perplexity/schema.py +0 -0
  51. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/__init__.py +0 -0
  52. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/dns.py +0 -0
  53. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/egress.py +0 -0
  54. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/models.py +0 -0
  55. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/netloc.py +0 -0
  56. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/policy.py +0 -0
  57. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/redirect.py +0 -0
  58. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/resolver.py +0 -0
  59. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.0.dev4}/unique_search_proxy_core/url_safety/service.py +0 -0
  60. {unique_search_proxy_core-2026.36.0.dev2 → unique_search_proxy_core-2026.36.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.36.0.dev2
3
+ Version: 2026.36.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.36.0.dev6,<2026.36.0rc0
11
+ Requires-Dist: unique-toolkit[monitoring]>=2026.36.0.dev12,<2026.36.0rc0
12
12
  Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
14
 
@@ -65,7 +65,7 @@ System overview → [../README.md](../README.md)
65
65
 
66
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.
67
67
 
68
- ### 3.1 Config-owned API (search engines)
68
+ ### 3.1 Config-owned API (search + agent engines)
69
69
 
70
70
  ```
71
71
  Admin JSON → GoogleConfig {expose, value}
@@ -78,15 +78,15 @@ Admin JSON → GoogleConfig {expose, value}
78
78
  | Method | Kind | Returns |
79
79
  |--------|------|---------|
80
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 |
81
+ | `exposed_params_model()` (instance) | search / agent | `ExposedParams` subclass with exactly the `expose=True` knobs (camelCase aliases, description-only schema), or `None` |
82
+ | `merge(overrides, *, query)` (instance) | search / agent | 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 have no exposable params and no `merge` — a crawl request is simply the deployment config fields plus `urls`.
85
+ `ExposableParamsConfig` (in `param_policy/exposable_config.py`) implements the exposable-parameter lifecycle once; `BaseSearchEngineConfig` and `BaseAgentEngineConfig` both extend it. Crawlers (`BasicConfig`, …) only implement `request_model()`: they have no exposable params and no `merge` — a crawl request is simply the deployment config fields plus `urls`.
86
86
 
87
- ### 3.2 ExposableParam — search-engine only
87
+ ### 3.2 ExposableParam
88
88
 
89
- Optional **search** parameters use `ExposableParam[T]`:
89
+ Optional **search** and **agent** parameters use `ExposableParam[T]`:
90
90
 
91
91
  - **`value`** — admin default merged into every request (`null` = deactivated)
92
92
  - **`expose`** — when `true`, the parameter appears on the LLM-facing exposed-params model
@@ -153,13 +153,14 @@ flowchart TB
153
153
  | `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse`, `CrawlResponse`, `WebSearchResult`, `ErrorResponse`, SSE events |
154
154
  | `errors.py` | `ProxyError` hierarchy and stable `ProxyErrorCode` enum |
155
155
  | `param_policy/exposable_param.py` | `ExposableParam` value object, factory-default merge, type introspection, OpenAPI naming |
156
+ | `param_policy/exposable_config.py` | `ExposableParamsConfig` — the `exposed_params_model()` / `merge()` lifecycle shared by search and agent config bases |
156
157
  | `param_policy/exposed_params.py` | `ExposedParams` base for LLM-facing parameter models (schema noise stripping) |
157
158
  | `param_policy/request_base.py` | `SearchRequestBase` / `AgentRequestBase` / `CrawlRequestBase` (required leading fields) |
158
159
  | `param_policy/annotations.py` | Annotation unwrapping helpers (private, used by `derive`) |
159
160
  | `param_policy/derive.py` | `derive_request_model` / `derive_exposed_params_model` factories called by the config base classes |
160
161
  | `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
161
162
  | `search_engines/` | Config models with the config-owned API (`request_model` / `exposed_params_model` / `merge` / `provider_query_params`), request union |
162
- | `agent_engines/` | Agent config/request models (`request_model`), output schema |
163
+ | `agent_engines/` | Agent config/request models (`request_model`, `exposed_params_model`, `merge`), output schema, Bing grounding configuration + agent naming |
163
164
  | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies (`request_model()`) |
164
165
 
165
166
  ---
@@ -223,8 +224,8 @@ from unique_search_proxy_core import (
223
224
  ## 7. Features summary
224
225
 
225
226
  - Discriminated provider configs (`engine`, `crawler` Literal discriminators)
226
- - **Search-only:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` / `provider_query_params`
227
- - **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`)
227
+ - **Search & agent:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` (`provider_query_params` is search-only)
228
+ - **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`); Bing exposes `market` / `setLang` / `freshness`, which are baked into the hashed agent name (`market` and `setLang` are `Literal`s over Bing's documented codes, like `BraveCountry`)
228
229
  - **Crawl:** `BasicConfig.request_model()` (injects `urls`); no exposable params / no merge
229
230
  - CamelCase JSON aliases on all models
230
231
  - Zero server dependencies (import-linter enforced in the client package)
@@ -51,7 +51,7 @@ System overview → [../README.md](../README.md)
51
51
 
52
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.
53
53
 
54
- ### 3.1 Config-owned API (search engines)
54
+ ### 3.1 Config-owned API (search + agent engines)
55
55
 
56
56
  ```
57
57
  Admin JSON → GoogleConfig {expose, value}
@@ -64,15 +64,15 @@ Admin JSON → GoogleConfig {expose, value}
64
64
  | Method | Kind | Returns |
65
65
  |--------|------|---------|
66
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 |
67
+ | `exposed_params_model()` (instance) | search / agent | `ExposedParams` subclass with exactly the `expose=True` knobs (camelCase aliases, description-only schema), or `None` |
68
+ | `merge(overrides, *, query)` (instance) | search / agent | 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 have no exposable params and no `merge` — a crawl request is simply the deployment config fields plus `urls`.
71
+ `ExposableParamsConfig` (in `param_policy/exposable_config.py`) implements the exposable-parameter lifecycle once; `BaseSearchEngineConfig` and `BaseAgentEngineConfig` both extend it. Crawlers (`BasicConfig`, …) only implement `request_model()`: they have no exposable params and no `merge` — a crawl request is simply the deployment config fields plus `urls`.
72
72
 
73
- ### 3.2 ExposableParam — search-engine only
73
+ ### 3.2 ExposableParam
74
74
 
75
- Optional **search** parameters use `ExposableParam[T]`:
75
+ Optional **search** and **agent** parameters use `ExposableParam[T]`:
76
76
 
77
77
  - **`value`** — admin default merged into every request (`null` = deactivated)
78
78
  - **`expose`** — when `true`, the parameter appears on the LLM-facing exposed-params model
@@ -139,13 +139,14 @@ flowchart TB
139
139
  | `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse`, `CrawlResponse`, `WebSearchResult`, `ErrorResponse`, SSE events |
140
140
  | `errors.py` | `ProxyError` hierarchy and stable `ProxyErrorCode` enum |
141
141
  | `param_policy/exposable_param.py` | `ExposableParam` value object, factory-default merge, type introspection, OpenAPI naming |
142
+ | `param_policy/exposable_config.py` | `ExposableParamsConfig` — the `exposed_params_model()` / `merge()` lifecycle shared by search and agent config bases |
142
143
  | `param_policy/exposed_params.py` | `ExposedParams` base for LLM-facing parameter models (schema noise stripping) |
143
144
  | `param_policy/request_base.py` | `SearchRequestBase` / `AgentRequestBase` / `CrawlRequestBase` (required leading fields) |
144
145
  | `param_policy/annotations.py` | Annotation unwrapping helpers (private, used by `derive`) |
145
146
  | `param_policy/derive.py` | `derive_request_model` / `derive_exposed_params_model` factories called by the config base classes |
146
147
  | `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
147
148
  | `search_engines/` | Config models with the config-owned API (`request_model` / `exposed_params_model` / `merge` / `provider_query_params`), request union |
148
- | `agent_engines/` | Agent config/request models (`request_model`), output schema |
149
+ | `agent_engines/` | Agent config/request models (`request_model`, `exposed_params_model`, `merge`), output schema, Bing grounding configuration + agent naming |
149
150
  | `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies (`request_model()`) |
150
151
 
151
152
  ---
@@ -209,8 +210,8 @@ from unique_search_proxy_core import (
209
210
  ## 7. Features summary
210
211
 
211
212
  - Discriminated provider configs (`engine`, `crawler` Literal discriminators)
212
- - **Search-only:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` / `provider_query_params`
213
- - **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`)
213
+ - **Search & agent:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` (`provider_query_params` is search-only)
214
+ - **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`); Bing exposes `market` / `setLang` / `freshness`, which are baked into the hashed agent name (`market` and `setLang` are `Literal`s over Bing's documented codes, like `BraveCountry`)
214
215
  - **Crawl:** `BasicConfig.request_model()` (injects `urls`); no exposable params / no merge
215
216
  - CamelCase JSON aliases on all models
216
217
  - Zero server dependencies (import-linter enforced in the client package)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy-core"
3
- version = "2026.36.0.dev2"
3
+ version = "2026.36.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.36.0.dev6,<2026.36.0rc0",
13
+ "unique-toolkit[monitoring]>=2026.36.0.dev12,<2026.36.0rc0",
14
14
 
15
15
  ]
16
16
 
@@ -61,6 +61,10 @@ depcheck = "deptry ."
61
61
  [tool.pytest.ini_options]
62
62
  addopts = "--strict-markers --import-mode=importlib"
63
63
  asyncio_mode = "auto"
64
+ markers = [
65
+ "ai: AI-authored or AI-generated tests",
66
+ "unit: unit tests",
67
+ ]
64
68
 
65
69
  [tool.uv.sources]
66
70
  unique-toolkit = { workspace = true }
@@ -10,11 +10,13 @@ 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
12
  from unique_search_proxy_core.param_policy.derive import derive_request_model
13
+ from unique_search_proxy_core.param_policy.exposable_config import (
14
+ ExposableParamsConfig,
15
+ )
13
16
  from unique_search_proxy_core.param_policy.request_base import AgentRequestBase
14
17
  from unique_search_proxy_core.schema import (
15
18
  AgentSearchResponse,
16
19
  AgentSearchStreamEvent,
17
- camelized_model_config,
18
20
  )
19
21
 
20
22
  if TYPE_CHECKING:
@@ -48,14 +50,23 @@ class AgentEngineType(StrEnum):
48
50
  _AGENT_REQUEST_EXCLUDED_FIELDS = frozenset({"output_schema"})
49
51
 
50
52
 
51
- class BaseAgentEngineConfig(BaseModel, Generic[T]):
52
- """Shared agent-engine config; each engine narrows ``engine`` with a Literal."""
53
+ class BaseAgentEngineConfig(ExposableParamsConfig, Generic[T]):
54
+ """Shared agent-engine config; each engine narrows ``engine`` with a Literal.
53
55
 
54
- model_config = camelized_model_config
56
+ Like the standard search-engine config, this owns the whole parameter
57
+ lifecycle: :meth:`request_model` for the HTTP body, and (from
58
+ :class:`ExposableParamsConfig`) ``exposed_params_model()`` / ``merge()`` for
59
+ engines that publish ``ExposableParam`` knobs.
60
+ """
55
61
 
56
62
  #: Name of the derived request model; set by every concrete agent config.
57
63
  _request_model_name: ClassVar[str]
58
64
 
65
+ #: ``output_schema`` is a server-side concern, never a request default.
66
+ _merge_exclude_fields: ClassVar[frozenset[str]] = (
67
+ ExposableParamsConfig._merge_exclude_fields | _AGENT_REQUEST_EXCLUDED_FIELDS
68
+ )
69
+
59
70
  @classmethod
60
71
  def request_model(cls) -> type[BaseModel]:
61
72
  """HTTP request body model, cached per config class.
@@ -0,0 +1,21 @@
1
+ from unique_search_proxy_core.agent_engines.bing.grounding import (
2
+ BING_AUTO_AGENT_NAME_PREFIX,
3
+ BingGroundingConfiguration,
4
+ bing_agent_config_hash,
5
+ bing_agent_name,
6
+ is_auto_provisioned_bing_agent_name,
7
+ )
8
+ from unique_search_proxy_core.agent_engines.bing.schema import (
9
+ BingAgentConfig,
10
+ BingAgentSearchRequest,
11
+ )
12
+
13
+ __all__ = [
14
+ "BING_AUTO_AGENT_NAME_PREFIX",
15
+ "BingAgentConfig",
16
+ "BingAgentSearchRequest",
17
+ "BingGroundingConfiguration",
18
+ "bing_agent_config_hash",
19
+ "bing_agent_name",
20
+ "is_auto_provisioned_bing_agent_name",
21
+ ]
@@ -0,0 +1,74 @@
1
+ """Bing grounding tool configuration shared by the proxy and legacy runners.
2
+
3
+ Azure bakes these knobs into an *agent version* at creation time — they are not
4
+ sent per request — so every runner derives its agent name from them. Keeping the
5
+ value object and its hash payload here means the proxy client and the web-search
6
+ tool cannot drift on what a given agent name stands for.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ from dataclasses import astuple, dataclass
13
+
14
+ #: Name prefix of every auto-provisioned Bing grounding agent.
15
+ BING_AUTO_AGENT_NAME_PREFIX = "unique-grounding-with-bing"
16
+
17
+ _CONFIG_HASH_LENGTH = 12
18
+ _HASH_FIELD_SEPARATOR = "\0"
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class BingGroundingConfiguration:
23
+ """Knobs forwarded to ``BingGroundingSearchConfiguration`` on the agent tool."""
24
+
25
+ fetch_size: int
26
+ market: str | None = None
27
+ set_lang: str | None = None
28
+ freshness: str | None = None
29
+
30
+ def hash_payload(self) -> str:
31
+ """Stable string identifying this tool configuration."""
32
+ return _HASH_FIELD_SEPARATOR.join(str(value) for value in astuple(self))
33
+
34
+
35
+ def bing_agent_config_hash(
36
+ *,
37
+ model: str,
38
+ instructions: str,
39
+ grounding: BingGroundingConfiguration,
40
+ ) -> str:
41
+ """Return a short hex digest of everything baked into the agent version."""
42
+ payload = _HASH_FIELD_SEPARATOR.join(
43
+ (model, instructions, grounding.hash_payload()),
44
+ ).encode()
45
+ return hashlib.sha256(payload).hexdigest()[:_CONFIG_HASH_LENGTH]
46
+
47
+
48
+ def bing_agent_name(
49
+ *,
50
+ model: str,
51
+ instructions: str,
52
+ grounding: BingGroundingConfiguration,
53
+ ) -> str:
54
+ """Build a Foundry-safe agent name unique to this configuration."""
55
+ digest = bing_agent_config_hash(
56
+ model=model,
57
+ instructions=instructions,
58
+ grounding=grounding,
59
+ )
60
+ return f"{BING_AUTO_AGENT_NAME_PREFIX}-{digest}"
61
+
62
+
63
+ def is_auto_provisioned_bing_agent_name(name: str) -> bool:
64
+ """True for names this module would have generated."""
65
+ return name.startswith(f"{BING_AUTO_AGENT_NAME_PREFIX}-")
66
+
67
+
68
+ __all__ = [
69
+ "BING_AUTO_AGENT_NAME_PREFIX",
70
+ "BingGroundingConfiguration",
71
+ "bing_agent_config_hash",
72
+ "bing_agent_name",
73
+ "is_auto_provisioned_bing_agent_name",
74
+ ]
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, Any, ClassVar, Literal, TypeAlias
4
+
5
+ from pydantic import Field, GetCoreSchemaHandler
6
+ from pydantic_core import CoreSchema, core_schema
7
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
8
+
9
+ from unique_search_proxy_core.agent_engines.base import (
10
+ AgentEngineType,
11
+ BaseAgentEngineConfig,
12
+ )
13
+ from unique_search_proxy_core.param_policy.exposable_param import ExposableParam
14
+ from unique_search_proxy_core.schema import DeactivatedNone
15
+
16
+ _BING_DOCS_BASE_URL = (
17
+ "https://learn.microsoft.com/en-us/previous-versions/bing/search-apis/"
18
+ "bing-web-search/reference"
19
+ )
20
+ _BING_QUERY_PARAMS_DOCS_URL = f"{_BING_DOCS_BASE_URL}/query-parameters"
21
+ _BING_MARKET_CODES_DOCS_URL = f"{_BING_DOCS_BASE_URL}/market-codes"
22
+
23
+ #: Markets Bing serves content for (``mkt``), in the documented order.
24
+ BingMarket: TypeAlias = Literal[
25
+ "es-AR",
26
+ "en-AU",
27
+ "de-AT",
28
+ "nl-BE",
29
+ "fr-BE",
30
+ "pt-BR",
31
+ "en-CA",
32
+ "fr-CA",
33
+ "es-CL",
34
+ "da-DK",
35
+ "fi-FI",
36
+ "fr-FR",
37
+ "de-DE",
38
+ "zh-HK",
39
+ "en-IN",
40
+ "en-ID",
41
+ "it-IT",
42
+ "ja-JP",
43
+ "ko-KR",
44
+ "en-MY",
45
+ "es-MX",
46
+ "nl-NL",
47
+ "en-NZ",
48
+ "no-NO",
49
+ "zh-CN",
50
+ "pl-PL",
51
+ "en-PH",
52
+ "ru-RU",
53
+ "en-ZA",
54
+ "es-ES",
55
+ "sv-SE",
56
+ "fr-CH",
57
+ "de-CH",
58
+ "zh-TW",
59
+ "tr-TR",
60
+ "en-GB",
61
+ "en-US",
62
+ "es-US",
63
+ ]
64
+
65
+ #: Languages Bing localizes its interface strings into (``setLang``).
66
+ BingSetLang: TypeAlias = Literal[
67
+ "ar",
68
+ "eu",
69
+ "bn",
70
+ "bg",
71
+ "ca",
72
+ "zh-hans",
73
+ "zh-hant",
74
+ "hr",
75
+ "cs",
76
+ "da",
77
+ "nl",
78
+ "en",
79
+ "en-gb",
80
+ "et",
81
+ "fi",
82
+ "fr",
83
+ "gl",
84
+ "de",
85
+ "gu",
86
+ "he",
87
+ "hi",
88
+ "hu",
89
+ "is",
90
+ "it",
91
+ "jp",
92
+ "kn",
93
+ "ko",
94
+ "lv",
95
+ "lt",
96
+ "ms",
97
+ "ml",
98
+ "mr",
99
+ "nb",
100
+ "pl",
101
+ "pt-br",
102
+ "pt-pt",
103
+ "pa",
104
+ "ro",
105
+ "ru",
106
+ "sr",
107
+ "sk",
108
+ "sl",
109
+ "es",
110
+ "sv",
111
+ "ta",
112
+ "te",
113
+ "th",
114
+ "tr",
115
+ "uk",
116
+ "vi",
117
+ ]
118
+
119
+ #: Named recency windows accepted by the Bing ``freshness`` knob.
120
+ BingFreshnessPreset: TypeAlias = Literal["Day", "Week", "Month"]
121
+
122
+ _FRESHNESS_DATE_PATTERN = r"^\d{4}-\d{2}-\d{2}(?:\.\.\d{4}-\d{2}-\d{2})?$"
123
+
124
+
125
+ class BingFreshnessDate(str):
126
+ """A single ``YYYY-MM-DD`` day or an inclusive ``YYYY-MM-DD..YYYY-MM-DD`` span.
127
+
128
+ The constraint lives in the type instead of ``Field(pattern=...)`` because
129
+ request-model derivation strips ``Annotated`` metadata off union branches,
130
+ which would leave exactly the surface the LLM writes to unchecked. Freshness
131
+ is baked into the hashed agent name, so free text here would mint a Foundry
132
+ agent version per distinct string while Bing ignored the value itself.
133
+ """
134
+
135
+ @classmethod
136
+ def __get_pydantic_core_schema__(
137
+ cls,
138
+ source: type[Any],
139
+ handler: GetCoreSchemaHandler,
140
+ ) -> CoreSchema:
141
+ return core_schema.str_schema(pattern=_FRESHNESS_DATE_PATTERN)
142
+
143
+
144
+ MarketOrNone: TypeAlias = Annotated[BingMarket, Field(title="Market")] | DeactivatedNone
145
+ SetLangOrNone: TypeAlias = (
146
+ Annotated[BingSetLang, Field(title="Language")] | DeactivatedNone
147
+ )
148
+ FreshnessOrNone: TypeAlias = (
149
+ Annotated[BingFreshnessPreset, Field(title="Preset")]
150
+ | Annotated[BingFreshnessDate, Field(title="Day or range")]
151
+ | DeactivatedNone
152
+ )
153
+
154
+ ExposableMarket = ExposableParam[MarketOrNone]
155
+ ExposableSetLang = ExposableParam[SetLangOrNone]
156
+ ExposableFreshness = ExposableParam[FreshnessOrNone]
157
+
158
+
159
+ class BingAgentConfig(BaseAgentEngineConfig[Literal[AgentEngineType.BING]]):
160
+ """Deployment + request defaults for Bing grounding via Azure AI Projects."""
161
+
162
+ _request_model_name: ClassVar[str] = "BingAgentSearchRequest"
163
+ _exposed_params_model_name: ClassVar[str] = "BingAgentExposedParams"
164
+
165
+ engine: Annotated[
166
+ Literal[AgentEngineType.BING], RJSFMetaTag.SpecialWidget.hidden()
167
+ ] = Field(
168
+ default=AgentEngineType.BING,
169
+ title="Agent engine",
170
+ description="Provider discriminator; must be `bing` for this config.",
171
+ )
172
+ fetch_size: int = Field(
173
+ default=5,
174
+ ge=1,
175
+ le=50,
176
+ description="Maximum number of Bing grounding results per query",
177
+ )
178
+ market: ExposableMarket = Field(
179
+ default=ExposableMarket(expose=False, value=None),
180
+ title="Market",
181
+ description=(
182
+ "Country/region **and** language the results come from (Bing `mkt`), "
183
+ "as `<language>-<country>`: `de-CH` returns German-language Swiss "
184
+ "results, `fr-CH` French-language Swiss ones, `en-GB` UK English. "
185
+ "Set it when the question is about a specific country or expects an "
186
+ "answer in that country's language. Left unset, Bing guesses the "
187
+ "market from the caller and may answer from another country. "
188
+ f"[Market codes]({_BING_MARKET_CODES_DOCS_URL})"
189
+ ),
190
+ )
191
+ set_lang: ExposableSetLang = Field(
192
+ default=ExposableSetLang(expose=False, value=None),
193
+ title="Interface language",
194
+ description=(
195
+ "Language of Bing's own interface strings in the response (Bing "
196
+ "`setLang`), e.g. `de`, `fr`, `pt-br`. It changes neither which "
197
+ "results come back nor the language they are written in — use the "
198
+ "market for that. Bing falls back to English for codes it does not "
199
+ "support. "
200
+ f"[Supported languages]({_BING_MARKET_CODES_DOCS_URL}#bing-supported-language-codes)"
201
+ ),
202
+ )
203
+ freshness: ExposableFreshness = Field(
204
+ default=ExposableFreshness(expose=False, value=None),
205
+ title="Freshness",
206
+ description=(
207
+ "Keep only pages Bing discovered recently (Bing `freshness`): `Day` "
208
+ "(last 24 hours), `Week` (last 7 days), `Month` (last 30 days), a "
209
+ "single `YYYY-MM-DD` day, or an inclusive "
210
+ "`YYYY-MM-DD..YYYY-MM-DD` range. Use it for news and fast-moving "
211
+ "topics; leave it unset for background or reference questions, where "
212
+ "it would hide older pages that are still correct. "
213
+ f"[Accepted values]({_BING_QUERY_PARAMS_DOCS_URL}#freshness)"
214
+ ),
215
+ )
216
+
217
+
218
+ BingAgentSearchRequest = BingAgentConfig.request_model()
219
+
220
+
221
+ __all__ = [
222
+ "BingAgentConfig",
223
+ "BingAgentSearchRequest",
224
+ "BingFreshnessDate",
225
+ "BingFreshnessPreset",
226
+ "BingMarket",
227
+ "BingSetLang",
228
+ "ExposableFreshness",
229
+ "ExposableMarket",
230
+ "ExposableSetLang",
231
+ ]
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, ClassVar, Literal
4
+
5
+ from pydantic import Field, field_validator
6
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
7
+
8
+ from unique_search_proxy_core.agent_engines.base import (
9
+ AgentEngineType,
10
+ BaseAgentEngineConfig,
11
+ )
12
+ from unique_search_proxy_core.agent_engines.vertexai.settings import (
13
+ resolve_enable_enterprise_search,
14
+ vertex_ai_env_settings,
15
+ )
16
+
17
+
18
+ def _get_force_activate_enterprise_search_description() -> str:
19
+ if vertex_ai_env_settings.force_activate_enterprise_search:
20
+ return "This parameter has been enforced by infra team."
21
+
22
+ return (
23
+ "When true, ground answers with Vertex AI Enterprise Web Search "
24
+ "(Agent Platform Search, SEC4-compliant)"
25
+ )
26
+
27
+
28
+ def _enterprise_search_rjsf_tag() -> RJSFMetaTag:
29
+ return RJSFMetaTag.BooleanWidget.checkbox(
30
+ title="Enable Enterprise Search",
31
+ help=_get_force_activate_enterprise_search_description(),
32
+ disabled=vertex_ai_env_settings.force_activate_enterprise_search,
33
+ )
34
+
35
+
36
+ EnableEnterpriseSearch = Annotated[bool, _enterprise_search_rjsf_tag()]
37
+
38
+
39
+ class VertexAIAgentConfig(BaseAgentEngineConfig[Literal[AgentEngineType.VERTEXAI]]):
40
+ """Deployment + request defaults for Vertex AI grounding (Google GenAI)."""
41
+
42
+ _request_model_name: ClassVar[str] = "VertexAIAgentSearchRequest"
43
+ _exposed_params_model_name: ClassVar[str] = "VertexAIAgentExposedParams"
44
+
45
+ engine: Annotated[
46
+ Literal[AgentEngineType.VERTEXAI], RJSFMetaTag.SpecialWidget.hidden()
47
+ ] = Field(
48
+ default=AgentEngineType.VERTEXAI,
49
+ title="Agent engine",
50
+ description="Provider discriminator; must be `vertexai` for this config.",
51
+ )
52
+ vertexai_model_name: str = Field(
53
+ default=vertex_ai_env_settings.default_vertex_ai_model_name,
54
+ description="Gemini model name for grounded generation",
55
+ )
56
+ enable_enterprise_search: EnableEnterpriseSearch = Field(
57
+ default=vertex_ai_env_settings.force_activate_enterprise_search,
58
+ validate_default=True,
59
+ )
60
+
61
+ @field_validator("enable_enterprise_search", mode="before")
62
+ @classmethod
63
+ def validate_enable_enterprise_search(cls, v: object) -> bool:
64
+ """Pin the value on when infra forces enterprise search for ZDR tenants."""
65
+ return resolve_enable_enterprise_search(bool(v) if v is not None else False)
66
+
67
+
68
+ VertexAIAgentSearchRequest = VertexAIAgentConfig.request_model()
69
+
70
+
71
+ __all__ = [
72
+ "VertexAIAgentConfig",
73
+ "VertexAIAgentSearchRequest",
74
+ ]
@@ -0,0 +1,52 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from pydantic import Field
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+ _ENV_PREFIX = "VERTEXAI_AGENT_"
9
+
10
+
11
+ class _VertexAIEnvSettings(BaseSettings):
12
+ default_vertex_ai_model_name: str = Field(
13
+ default="gemini-3-flash-preview",
14
+ description="Default Vertex AI model name for grounded generation",
15
+ )
16
+ force_activate_enterprise_search: bool = Field(
17
+ default=False,
18
+ description=(
19
+ "When true, pin Enable Enterprise Search on for ZDR tenants so the "
20
+ "toggle cannot be turned off in config or at request time."
21
+ ),
22
+ )
23
+
24
+
25
+ def _get_settings() -> _VertexAIEnvSettings:
26
+ if "pytest" in sys.modules:
27
+ env_file = Path(os.getcwd()) / "tests/test.env"
28
+ else:
29
+ env_file = Path(os.getcwd()) / ".env"
30
+
31
+ class _Settings(_VertexAIEnvSettings):
32
+ model_config = SettingsConfigDict(
33
+ env_file=env_file,
34
+ env_prefix=_ENV_PREFIX,
35
+ extra="ignore",
36
+ )
37
+
38
+ return _Settings()
39
+
40
+
41
+ vertex_ai_env_settings = _get_settings()
42
+
43
+
44
+ def resolve_enable_enterprise_search(value: bool | None = False) -> bool:
45
+ """Return the effective enterprise-search flag after infra enforcement.
46
+
47
+ ZDR tenants set ``VERTEXAI_AGENT_FORCE_ACTIVATE_ENTERPRISE_SEARCH=true`` so
48
+ the cheaper standard grounding edition cannot be selected.
49
+ """
50
+ if vertex_ai_env_settings.force_activate_enterprise_search:
51
+ return True
52
+ return bool(value)
@@ -1,8 +1,9 @@
1
1
  """Model factories: deployment config class -> derived Pydantic models.
2
2
 
3
3
  Private-by-convention; the public entry points are the config-class methods
4
- (``BaseSearchEngineConfig.request_model()`` / ``exposed_params_model()`` and the
5
- agent/crawler ``request_model()`` counterparts), which delegate here.
4
+ (``request_model()`` / ``exposed_params_model()`` on the search and agent bases
5
+ through :class:`~unique_search_proxy_core.param_policy.exposable_config.ExposableParamsConfig`,
6
+ and ``request_model()`` on the crawler base), which delegate here.
6
7
 
7
8
  Two factories, both plain ``create_model`` calls over ``model_fields``:
8
9
 
@@ -0,0 +1,107 @@
1
+ """Config base for engines carrying ``ExposableParam`` knobs.
2
+
3
+ Standard search engines and agent engines own the same parameter lifecycle:
4
+ partial exposable JSON is merged with the field's ``default_factory`` before
5
+ validation, the admin-exposed subset is projected into an LLM-facing model, and
6
+ deployment defaults are resolved into a validated request body. That lifecycle
7
+ lives here so :class:`~unique_search_proxy_core.search_engines.base.BaseSearchEngineConfig`
8
+ and :class:`~unique_search_proxy_core.agent_engines.base.BaseAgentEngineConfig`
9
+ share one implementation.
10
+
11
+ Subclasses supply the two pieces this base cannot know: ``request_model()``
12
+ (which request base class the derived body extends) and the
13
+ ``_exposed_params_model_name`` / ``_merge_exclude_fields`` class vars.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, ClassVar, Mapping
19
+
20
+ from pydantic import BaseModel, model_validator
21
+
22
+ from unique_search_proxy_core.param_policy import QUERY_FIELD
23
+ from unique_search_proxy_core.param_policy.derive import derive_exposed_params_model
24
+ from unique_search_proxy_core.param_policy.exposable_param import (
25
+ merge_exposable_params_with_factory_defaults,
26
+ resolve_exposable_value,
27
+ )
28
+ from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
29
+ from unique_search_proxy_core.schema import camelized_model_config
30
+
31
+ #: Provider discriminator field carried by every engine config.
32
+ ENGINE_FIELD = "engine"
33
+
34
+
35
+ class ExposableParamsConfig(BaseModel):
36
+ """Deployment config whose optional knobs may be exposed to the LLM.
37
+
38
+ - :meth:`exposed_params_model` — the LLM-facing knobs of this deployment.
39
+ - :meth:`merge` — deployment defaults + LLM overrides -> validated request.
40
+ """
41
+
42
+ model_config = camelized_model_config
43
+
44
+ #: Name of the derived exposed-params model; set by every concrete config.
45
+ _exposed_params_model_name: ClassVar[str]
46
+
47
+ #: Config fields that never contribute a request default in :meth:`merge`.
48
+ #: The discriminator is set from the config itself; engine bases extend this
49
+ #: with their own server-side-only fields (e.g. the agent ``output_schema``).
50
+ _merge_exclude_fields: ClassVar[frozenset[str]] = frozenset({ENGINE_FIELD})
51
+
52
+ @model_validator(mode="before")
53
+ @classmethod
54
+ def _merge_exposable_factory_defaults(cls, data: Any) -> Any:
55
+ """Merge exposable knobs with field ``default_factory`` when JSON omits ``value``."""
56
+ return merge_exposable_params_with_factory_defaults(cls, data)
57
+
58
+ @classmethod
59
+ def request_model(cls) -> type[BaseModel]:
60
+ """HTTP request body model derived from this config."""
61
+ raise NotImplementedError
62
+
63
+ def exposed_params_model(self) -> type[ExposedParams] | None:
64
+ """Plain model class with exactly the knobs this deployment exposed.
65
+
66
+ One optional field per config field whose ``ExposableParam`` has
67
+ ``expose=True``: camelCase alias, description-only schema, no admin
68
+ defaults. Returns ``None`` when nothing is exposed. Consumers subclass
69
+ the result (or pass it as an extra base to ``create_model``) to graft
70
+ the knobs onto their own tool-parameter models.
71
+
72
+ Example: ``config.exposed_params_model()`` -> ``GoogleExposedParams``.
73
+ """
74
+ return derive_exposed_params_model(
75
+ self,
76
+ name=type(self)._exposed_params_model_name,
77
+ )
78
+
79
+ def merge(self, overrides: Mapping[str, Any], *, query: str) -> BaseModel:
80
+ """Deployment defaults + LLM/caller overrides + query -> validated request.
81
+
82
+ Defaults: plain fields contribute their value; ``ExposableParam`` knobs
83
+ contribute ``value`` when not ``None`` (deactivated knobs are dropped);
84
+ ``engine`` always comes from this config. ``overrides`` win over
85
+ defaults. Validates into :meth:`request_model`.
86
+
87
+ Example: ``config.merge({"date_restrict": "d7"}, query="ai news")``.
88
+ """
89
+ merged: dict[str, Any] = self._merge_defaults()
90
+ merged.update(overrides)
91
+ merged[QUERY_FIELD] = query
92
+ merged[ENGINE_FIELD] = getattr(self, ENGINE_FIELD)
93
+ return type(self).request_model().model_validate(merged)
94
+
95
+ def _merge_defaults(self) -> dict[str, Any]:
96
+ """Plain deployment default values (``ExposableParam`` resolved, ``None`` dropped)."""
97
+ defaults: dict[str, Any] = {}
98
+ for field_name in type(self).model_fields:
99
+ if field_name in type(self)._merge_exclude_fields:
100
+ continue
101
+ value = resolve_exposable_value(getattr(self, field_name))
102
+ if value is not None:
103
+ defaults[field_name] = value
104
+ return defaults
105
+
106
+
107
+ __all__ = ["ENGINE_FIELD", "ExposableParamsConfig"]
@@ -121,6 +121,17 @@ class ExposableParam(BaseModel, Generic[T]):
121
121
  return schema
122
122
 
123
123
 
124
+ def resolve_exposable_value(value: Any) -> Any:
125
+ """Plain value behind a config attribute that may be an ``ExposableParam``.
126
+
127
+ Lets consumers read a deployment default without caring whether the field is
128
+ an exposable knob or a plain value.
129
+ """
130
+ if isinstance(value, ExposableParam):
131
+ return value.value
132
+ return value
133
+
134
+
124
135
  def _exposable_field_keys(field_name: str, field_info: FieldInfo) -> list[str]:
125
136
  keys = [field_name]
126
137
  if field_info.alias is not None:
@@ -2,26 +2,18 @@ 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, Any, ClassVar, Generic, Mapping, TypeVar
5
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar
6
6
 
7
- from pydantic import BaseModel, Field, model_validator
7
+ from pydantic import BaseModel, Field
8
8
 
9
9
  from unique_search_proxy_core.param_policy import QUERY_FIELD
10
- from unique_search_proxy_core.param_policy.derive import (
11
- derive_exposed_params_model,
12
- derive_request_model,
10
+ from unique_search_proxy_core.param_policy.derive import derive_request_model
11
+ from unique_search_proxy_core.param_policy.exposable_config import (
12
+ ENGINE_FIELD,
13
+ ExposableParamsConfig,
13
14
  )
14
- from unique_search_proxy_core.param_policy.exposable_param import (
15
- ExposableParam,
16
- merge_exposable_params_with_factory_defaults,
17
- )
18
- from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
19
15
  from unique_search_proxy_core.param_policy.request_base import SearchRequestBase
20
- from unique_search_proxy_core.schema import (
21
- SearchEngineRaw,
22
- WebSearchResults,
23
- camelized_model_config,
24
- )
16
+ from unique_search_proxy_core.schema import SearchEngineRaw, WebSearchResults
25
17
 
26
18
  if TYPE_CHECKING:
27
19
  from httpx import AsyncClient
@@ -29,7 +21,6 @@ if TYPE_CHECKING:
29
21
  T = TypeVar("T", bound="SearchEngineType")
30
22
  SearchRequestT = TypeVar("SearchRequestT", bound=BaseModel)
31
23
 
32
- ENGINE_FIELD = "engine"
33
24
  FETCH_SIZE_FIELD = "fetch_size"
34
25
  TIMEOUT_FIELD = "timeout"
35
26
 
@@ -64,7 +55,7 @@ def get_search_engine_mode(
64
55
  return _SEARCH_ENGINE_MODE_MAP.get(engine_type, SearchEngineMode.STANDARD)
65
56
 
66
57
 
67
- class BaseSearchEngineConfig(BaseModel, Generic[T]):
58
+ class BaseSearchEngineConfig(ExposableParamsConfig, Generic[T]):
68
59
  """Shared search-engine config; each engine narrows ``engine`` with a Literal.
69
60
 
70
61
  The config class owns its entire parameter lifecycle:
@@ -75,8 +66,6 @@ class BaseSearchEngineConfig(BaseModel, Generic[T]):
75
66
  - :meth:`provider_query_params` — request -> upstream provider dict.
76
67
  """
77
68
 
78
- model_config = camelized_model_config
79
-
80
69
  #: Fields never forwarded to the upstream provider query string: the
81
70
  #: discriminator, request plumbing, and the query itself. Engine configs
82
71
  #: extend this to drop provider-internal fields — e.g. Google adds
@@ -85,9 +74,8 @@ class BaseSearchEngineConfig(BaseModel, Generic[T]):
85
74
  {ENGINE_FIELD, FETCH_SIZE_FIELD, TIMEOUT_FIELD, QUERY_FIELD},
86
75
  )
87
76
 
88
- #: Names of the derived models; set by every concrete engine config.
77
+ #: Name of the derived request model; set by every concrete engine config.
89
78
  _request_model_name: ClassVar[str]
90
- _exposed_params_model_name: ClassVar[str]
91
79
 
92
80
  engine: T
93
81
  fetch_size: int = Field(
@@ -103,12 +91,6 @@ class BaseSearchEngineConfig(BaseModel, Generic[T]):
103
91
  description="Request timeout in seconds",
104
92
  )
105
93
 
106
- @model_validator(mode="before")
107
- @classmethod
108
- def _merge_exposable_factory_defaults(cls, data: Any) -> Any:
109
- """Merge exposable knobs with field ``default_factory`` when JSON omits ``value``."""
110
- return merge_exposable_params_with_factory_defaults(cls, data)
111
-
112
94
  @classmethod
113
95
  def request_model(cls) -> type[BaseModel]:
114
96
  """HTTP request body model, cached per config class.
@@ -124,52 +106,6 @@ class BaseSearchEngineConfig(BaseModel, Generic[T]):
124
106
  name=cls._request_model_name,
125
107
  )
126
108
 
127
- def exposed_params_model(self) -> type[ExposedParams] | None:
128
- """Plain model class with exactly the knobs this deployment exposed.
129
-
130
- One optional field per config field whose ``ExposableParam`` has
131
- ``expose=True``: camelCase alias, description-only schema, no admin
132
- defaults. Returns ``None`` when nothing is exposed. Consumers subclass
133
- the result (or pass it as an extra base to ``create_model``) to graft
134
- the knobs onto their own tool-parameter models.
135
-
136
- Example: ``config.exposed_params_model()`` -> ``GoogleExposedParams``.
137
- """
138
- return derive_exposed_params_model(
139
- self,
140
- name=type(self)._exposed_params_model_name,
141
- )
142
-
143
- def merge(self, overrides: Mapping[str, Any], *, query: str) -> BaseModel:
144
- """Deployment defaults + LLM/caller overrides + query -> validated request.
145
-
146
- Defaults: plain fields contribute their value; ``ExposableParam`` knobs
147
- contribute ``value`` when not ``None`` (deactivated knobs are dropped);
148
- ``engine`` always comes from this config. ``overrides`` win over
149
- defaults. Validates into :meth:`request_model`.
150
-
151
- Example: ``config.merge({"date_restrict": "d7"}, query="ai news")``.
152
- """
153
- merged: dict[str, Any] = self._merge_defaults()
154
- merged.update(overrides)
155
- merged[QUERY_FIELD] = query
156
- merged[ENGINE_FIELD] = self.engine
157
- return type(self).request_model().model_validate(merged)
158
-
159
- def _merge_defaults(self) -> dict[str, Any]:
160
- """Plain deployment default values (``ExposableParam`` resolved, ``None`` dropped)."""
161
- defaults: dict[str, Any] = {}
162
- for field_name in type(self).model_fields:
163
- if field_name == ENGINE_FIELD:
164
- continue
165
- raw = getattr(self, field_name)
166
- if isinstance(raw, ExposableParam):
167
- if raw.value is not None:
168
- defaults[field_name] = raw.value
169
- elif raw is not None:
170
- defaults[field_name] = raw
171
- return defaults
172
-
173
109
  @classmethod
174
110
  def provider_query_params(
175
111
  cls,
@@ -1,6 +0,0 @@
1
- from unique_search_proxy_core.agent_engines.bing.schema import (
2
- BingAgentConfig,
3
- BingAgentSearchRequest,
4
- )
5
-
6
- __all__ = ["BingAgentConfig", "BingAgentSearchRequest"]
@@ -1,37 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Annotated, ClassVar, Literal
4
-
5
- from pydantic import Field
6
- from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
7
-
8
- from unique_search_proxy_core.agent_engines.base import (
9
- AgentEngineType,
10
- BaseAgentEngineConfig,
11
- )
12
-
13
-
14
- class BingAgentConfig(BaseAgentEngineConfig[Literal[AgentEngineType.BING]]):
15
- """Deployment + request defaults for Bing grounding via Azure AI Projects."""
16
-
17
- _request_model_name: ClassVar[str] = "BingAgentSearchRequest"
18
-
19
- engine: Annotated[
20
- Literal[AgentEngineType.BING], RJSFMetaTag.SpecialWidget.hidden()
21
- ] = Field(
22
- default=AgentEngineType.BING,
23
- title="Agent engine",
24
- description="Provider discriminator; must be `bing` for this config.",
25
- )
26
- fetch_size: int = Field(
27
- default=5,
28
- ge=1,
29
- le=50,
30
- description="Maximum number of Bing grounding results per query",
31
- )
32
-
33
-
34
- BingAgentSearchRequest = BingAgentConfig.request_model()
35
-
36
-
37
- __all__ = ["BingAgentConfig", "BingAgentSearchRequest"]
@@ -1,45 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Annotated, ClassVar, Literal
4
-
5
- from pydantic import Field
6
- from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
7
-
8
- from unique_search_proxy_core.agent_engines.base import (
9
- AgentEngineType,
10
- BaseAgentEngineConfig,
11
- )
12
-
13
-
14
- class VertexAIAgentConfig(BaseAgentEngineConfig[Literal[AgentEngineType.VERTEXAI]]):
15
- """Deployment + request defaults for Vertex AI grounding (Google GenAI)."""
16
-
17
- _request_model_name: ClassVar[str] = "VertexAIAgentSearchRequest"
18
-
19
- engine: Annotated[
20
- Literal[AgentEngineType.VERTEXAI], RJSFMetaTag.SpecialWidget.hidden()
21
- ] = Field(
22
- default=AgentEngineType.VERTEXAI,
23
- title="Agent engine",
24
- description="Provider discriminator; must be `vertexai` for this config.",
25
- )
26
- vertexai_model_name: str = Field(
27
- default="gemini-3-flash-preview",
28
- description="Gemini model name for grounded generation",
29
- )
30
- enable_enterprise_search: bool = Field(
31
- default=False,
32
- description=(
33
- "When true, ground answers with Vertex AI Enterprise Web Search "
34
- "(Agent Platform Search, SEC4-compliant)"
35
- ),
36
- )
37
-
38
-
39
- VertexAIAgentSearchRequest = VertexAIAgentConfig.request_model()
40
-
41
-
42
- __all__ = [
43
- "VertexAIAgentConfig",
44
- "VertexAIAgentSearchRequest",
45
- ]