unique-search-proxy-sdk 2026.26.0.dev4__tar.gz → 2026.28.0.dev0__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 (73) hide show
  1. unique_search_proxy_sdk-2026.28.0.dev0/PKG-INFO +221 -0
  2. unique_search_proxy_sdk-2026.28.0.dev0/README.md +208 -0
  3. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/pyproject.toml +8 -2
  4. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/__init__.py +2 -0
  5. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_endpoint.py +99 -0
  6. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/api/agent_search/agent_search_stream_v1_agent_search_stream_post.py +168 -0
  7. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/api/agent_search/agent_search_v1_agent_search_post.py +170 -0
  8. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/api/crawl/crawl_v1_crawl_post.py +36 -11
  9. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/api/search/__init__.py +1 -0
  10. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/api/search/search_v1_search_post.py +18 -11
  11. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/__init__.py +83 -0
  12. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/agent_search_response.py +93 -0
  13. unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/basic_proxy_crawler.py → unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/basic_crawl_request.py +14 -37
  14. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/bing_agent_search_request.py +137 -0
  15. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/brave_search_request.py +356 -0
  16. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/brave_search_request_safesearch_type_0.py +10 -0
  17. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/brave_search_request_units_type_0.py +9 -0
  18. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/crawl_response.py +6 -6
  19. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/crawl_url_result.py +2 -1
  20. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/firecrawl_crawl_request.py +302 -0
  21. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/firecrawl_crawl_request_proxy_mode.py +10 -0
  22. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/firecrawl_crawl_request_scrape_headers_type_0.py +47 -0
  23. unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/google_request.py → unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/google_search_request.py +24 -20
  24. unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/google_request_safe_search.py → unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/google_search_request_safe_search.py +1 -1
  25. unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/google_request_site_search_filter_type_0.py → unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/google_search_request_site_search_filter_type_0.py +1 -1
  26. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/jina_crawl_request.py +380 -0
  27. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/jina_crawl_request_engine.py +11 -0
  28. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/jina_crawl_request_retain_images_type_0.py +12 -0
  29. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/jina_crawl_request_return_format.py +12 -0
  30. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/perplexity_search_request.py +417 -0
  31. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/perplexity_search_request_search_context_size_type_0.py +10 -0
  32. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/perplexity_search_request_search_recency_filter_type_0.py +12 -0
  33. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/providers_list_response.py +13 -0
  34. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/tavily_crawl_request.py +179 -0
  35. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/tavily_crawl_request_extract_depth.py +9 -0
  36. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/tavily_crawl_request_output_format.py +9 -0
  37. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/models/vertex_ai_agent_search_request.py +127 -0
  38. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_transport.py +6 -0
  39. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_typed_endpoints.py +419 -0
  40. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/agent_search_client.py +120 -0
  41. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/client.py +4 -2
  42. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/converters.py +70 -0
  43. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/crawl_client.py +105 -0
  44. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/errors.py +24 -5
  45. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/search_client.py +83 -0
  46. unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/typing_checks.py +57 -0
  47. unique_search_proxy_sdk-2026.26.0.dev4/PKG-INFO +0 -25
  48. unique_search_proxy_sdk-2026.26.0.dev4/README.md +0 -12
  49. unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/__init__.py +0 -37
  50. unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/converters.py +0 -45
  51. unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/crawl_client.py +0 -74
  52. unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/search_client.py +0 -75
  53. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/__init__.py +0 -0
  54. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/api/__init__.py +0 -0
  55. {unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/api/configuration → unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/api/agent_search}/__init__.py +0 -0
  56. {unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/api/crawl → unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/api/configuration}/__init__.py +0 -0
  57. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/api/configuration/list_providers_v1_configuration_providers_get.py +0 -0
  58. {unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/api/health → unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/api/crawl}/__init__.py +0 -0
  59. {unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/api/search → unique_search_proxy_sdk-2026.28.0.dev0/unique_search_proxy_sdk/_generated/api/health}/__init__.py +0 -0
  60. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/api/health/health_health_get.py +0 -0
  61. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/api/health/ready_ready_get.py +0 -0
  62. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/client.py +0 -0
  63. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/errors.py +0 -0
  64. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/content_types.py +0 -0
  65. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/health_health_get_response_health_health_get.py +0 -0
  66. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/http_validation_error.py +0 -0
  67. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/per_url_error.py +0 -0
  68. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/ready_ready_get_response_ready_ready_get.py +0 -0
  69. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/search_response.py +0 -0
  70. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/validation_error.py +0 -0
  71. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/models/web_search_result.py +0 -0
  72. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_generated/types.py +0 -0
  73. {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.28.0.dev0}/unique_search_proxy_sdk/_http.py +0 -0
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.3
2
+ Name: unique-search-proxy-sdk
3
+ Version: 2026.28.0.dev0
4
+ Summary: Async HTTP client for the Unique Search Proxy API
5
+ Author: ThePhilAz
6
+ Author-email: ThePhilAz <rami.azouz@philico.com>
7
+ Requires-Dist: attrs>=24.3.0,<26
8
+ Requires-Dist: httpx>=0.28.0,<0.29.0
9
+ Requires-Dist: pydantic>=2.12.5,<3.0.0
10
+ Requires-Dist: unique-search-proxy-core>=2026.28.0.dev0,<2026.28.0rc0
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+
14
+ # unique-search-proxy-sdk
15
+
16
+ Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy-sdk`
17
+
18
+ ---
19
+
20
+ ## 1. What this package is
21
+
22
+ **The SDK is the caller's HTTP client.** Platform services (e.g. assistants-core) use it to reach the proxy pod at runtime — search, agent search, and crawl — without hand-crafting httpx calls or duplicating route paths.
23
+
24
+ It depends on [core](../unique_search_proxy_core/README.md) for request validation and typed errors. It does **not** include the FastAPI server.
25
+
26
+ | Package | Question it answers |
27
+ |---------|---------------------|
28
+ | [Core](../unique_search_proxy_core/README.md) | *What* can be configured and *what* does a valid request/response look like? |
29
+ | [Client](../unique_search_proxy_client/README.md) | *How* are provider calls executed at runtime? |
30
+ | **SDK** (this) | *How* do callers reach the proxy over HTTP? |
31
+
32
+ ---
33
+
34
+ ## 2. Role in the system
35
+
36
+ The SDK implements **Path B** (runtime HTTP) from the [system overview](../README.md). Schema work (Path A) stays in core — the SDK does not expose deployment config or LLM call-schema endpoints.
37
+
38
+ ```mermaid
39
+ flowchart LR
40
+ Caller["Caller service"]
41
+ SDK["unique_search_proxy_sdk"]
42
+ Core["unique_search_proxy_core"]
43
+ Proxy["unique_search_proxy_client"]
44
+
45
+ Caller --> SDK
46
+ SDK --> Core
47
+ SDK -->|"httpx"| Proxy
48
+ ```
49
+
50
+ Before a search call, the caller (or SDK) validates the payload against core request models. Non-2xx responses become the same `ProxyError` subclasses defined in core.
51
+
52
+ ---
53
+
54
+ ## 3. Architecture
55
+
56
+ Two layers: a **hand-written facade** over **OpenAPI-generated code**.
57
+
58
+ ```mermaid
59
+ flowchart TB
60
+ subgraph sdk_pkg["unique_search_proxy_sdk"]
61
+ Facade["UniqueSearchProxyClient"]
62
+ SC["SearchClient"]
63
+ ASC["AgentSearchClient"]
64
+ CC["CrawlClient"]
65
+ Transport["OpenapiTransport"]
66
+ Gen["_generated/"]
67
+ Conv["converters.py"]
68
+ Err["errors.py"]
69
+ end
70
+
71
+ Core["unique_search_proxy_core"]
72
+ Proxy["Proxy pod"]
73
+
74
+ Facade --> SC & ASC & CC
75
+ SC & ASC & CC --> Transport
76
+ Transport --> Gen
77
+ SC --> Conv --> Core
78
+ Err --> Core
79
+ Gen -->|"httpx AsyncClient"| Proxy
80
+ ```
81
+
82
+ | Layer | Path | Responsibility |
83
+ |-------|------|----------------|
84
+ | **Facade** | `client.py` | `UniqueSearchProxyClient` — composes sub-clients, `health()`, `ready()` |
85
+ | **Sub-clients** | `search_client.py`, `agent_search_client.py`, `crawl_client.py` | Per-provider typed endpoints + compatibility dispatchers |
86
+ | **Endpoint factory** | `_endpoint.py` | `async_post_endpoint` / `async_sse_endpoint` (ParamSpec from core request models) |
87
+ | **Transport** | `_transport.py`, `_http.py` | Shared httpx lifecycle, base URL, timeout |
88
+ | **Generated** | `_generated/` | Route functions + attrs models from OpenAPI (**do not edit**) |
89
+ | **Converters** | `converters.py` | Core Pydantic models → generated SDK models |
90
+ | **Errors** | `errors.py` | Error envelope → core `ProxyError` subclasses |
91
+
92
+ The client package is the **source of truth** for the HTTP contract. When routes change, regenerate `_generated/` from `unique_search_proxy_client/openapi.json`.
93
+
94
+ ---
95
+
96
+ ## 4. Usage
97
+
98
+ Each provider exposes a typed endpoint whose kwargs mirror the core `*Request` Pydantic model (via `ParamSpec`). Snake_case and camelCase aliases both work (`fetch_size` / `fetchSize`).
99
+
100
+ ```python
101
+ from unique_search_proxy_sdk import UniqueSearchProxyClient
102
+
103
+ async with UniqueSearchProxyClient("http://unique-search-proxy:2349") as client:
104
+ await client.health()
105
+
106
+ # Typed per-provider endpoints (preferred)
107
+ result = await client.search.google(query="unique ag", gl="ch", fetch_size=10)
108
+ brave = await client.search.brave(query="news", country="US", safesearch="strict")
109
+ agent = await client.agent_search.bing(query="EU AI Act timeline", fetch_size=5)
110
+ crawl = await client.crawl.basic(urls=["https://example.com"])
111
+
112
+ # Compatibility dispatchers (engine/crawler string)
113
+ result = await client.search.search("unique ag", engine="google", fetch_size=10)
114
+ agent = await client.agent_search.search("query", engine="bing")
115
+ crawl = await client.crawl.crawl(["https://example.com"], crawler="Basic")
116
+
117
+ # Agent SSE streaming
118
+ async for event in client.agent_search.bing_stream(query="query"):
119
+ ...
120
+
121
+ # Low-level: one generated function per route
122
+ raw_client = client.openapi
123
+ ```
124
+
125
+ Search payloads are validated through core's `parse_search_request()` before serialization. Agent and crawl clients follow the same pattern.
126
+
127
+ ---
128
+
129
+ ## 5. API mapping
130
+
131
+ | Facade method | HTTP |
132
+ |---------------|------|
133
+ | `health()` | `GET /health` |
134
+ | `ready()` | `GET /ready` |
135
+ | `search.google(...)` / `.brave(...)` / `.perplexity(...)` | `POST /v1/search` |
136
+ | `search.search(...)` | `POST /v1/search` (dispatcher) |
137
+ | `agent_search.bing(...)` / `.vertexai(...)` | `POST /v1/agent-search` |
138
+ | `agent_search.bing_stream(...)` / `.vertexai_stream(...)` | `POST /v1/agent-search/stream` (SSE) |
139
+ | `agent_search.search(...)` / `.stream(...)` | same routes (dispatchers) |
140
+ | `crawl.basic(...)` / `.tavily(...)` / `.jina(...)` / `.firecrawl(...)` | `POST /v1/crawl` |
141
+ | `crawl.crawl(...)` | `POST /v1/crawl` (dispatcher) |
142
+ | `openapi` | Low-level generated client (one function per route) |
143
+
144
+ Endpoint payloads and provider ids → [Client README](../unique_search_proxy_client/README.md).
145
+
146
+ ---
147
+
148
+ ## 6. Error handling
149
+
150
+ Non-success responses with a proxy error envelope raise typed exceptions from core:
151
+
152
+ ```python
153
+ from unique_search_proxy_core import EngineNotConfiguredError, UpstreamError
154
+ ```
155
+
156
+ `ENGINE_NOT_CONFIGURED` (503) includes `missing_env_vars` parsed from the error `details` list — useful for operators and LLM tool consumers.
157
+
158
+ Transport failures (non-JSON body, connection errors) raise `UniqueSearchProxyClientError`.
159
+
160
+ ---
161
+
162
+ ## 7. Codegen workflow
163
+
164
+ Generated via [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) from the server's OpenAPI spec.
165
+
166
+ ```bash
167
+ cd ../unique_search_proxy_client
168
+ uv sync
169
+ uv run python scripts/generate_sdk.py
170
+ ```
171
+
172
+ | Path | Role |
173
+ |------|------|
174
+ | `unique_search_proxy_sdk/_generated/` | Regenerated httpx client + attrs models |
175
+ | `unique_search_proxy_client/openapi.json` | Exported spec (codegen input) |
176
+ | `unique_search_proxy_sdk/client.py` | Hand-written facade (not regenerated) |
177
+
178
+ Other codegen tools considered: OpenAPI Generator, datamodel-code-generator, Kiota — see git history in the client README if needed.
179
+
180
+ ---
181
+
182
+ ## 8. Testing
183
+
184
+ Pass an injected `httpx.AsyncClient` with `ASGITransport` for in-process tests against `create_app()`:
185
+
186
+ ```python
187
+ import httpx
188
+ from httpx import ASGITransport
189
+ from unique_search_proxy_client.web.app import create_app
190
+ from unique_search_proxy_sdk import UniqueSearchProxyClient
191
+
192
+ app = create_app()
193
+ async with httpx.AsyncClient(
194
+ transport=ASGITransport(app=app),
195
+ base_url="http://test",
196
+ ) as http:
197
+ async with UniqueSearchProxyClient("http://test", http_client=http) as client:
198
+ ...
199
+ ```
200
+
201
+ Run the app lifespan so the in-process `HttpClientPool` is initialised.
202
+
203
+ ---
204
+
205
+ ## 9. Installation & development
206
+
207
+ ```bash
208
+ cd unique_search_proxy_sdk
209
+ uv sync
210
+ uv run pytest
211
+ uv run ruff check .
212
+ uv run basedpyright
213
+ ```
214
+
215
+ Depends on `unique-search-proxy-core`. Does not depend on `unique-search-proxy` at runtime (only as a dev dependency for integration tests).
216
+
217
+ ---
218
+
219
+ ## License
220
+
221
+ Proprietary — Unique AG
@@ -0,0 +1,208 @@
1
+ # unique-search-proxy-sdk
2
+
3
+ Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy-sdk`
4
+
5
+ ---
6
+
7
+ ## 1. What this package is
8
+
9
+ **The SDK is the caller's HTTP client.** Platform services (e.g. assistants-core) use it to reach the proxy pod at runtime — search, agent search, and crawl — without hand-crafting httpx calls or duplicating route paths.
10
+
11
+ It depends on [core](../unique_search_proxy_core/README.md) for request validation and typed errors. It does **not** include the FastAPI server.
12
+
13
+ | Package | Question it answers |
14
+ |---------|---------------------|
15
+ | [Core](../unique_search_proxy_core/README.md) | *What* can be configured and *what* does a valid request/response look like? |
16
+ | [Client](../unique_search_proxy_client/README.md) | *How* are provider calls executed at runtime? |
17
+ | **SDK** (this) | *How* do callers reach the proxy over HTTP? |
18
+
19
+ ---
20
+
21
+ ## 2. Role in the system
22
+
23
+ The SDK implements **Path B** (runtime HTTP) from the [system overview](../README.md). Schema work (Path A) stays in core — the SDK does not expose deployment config or LLM call-schema endpoints.
24
+
25
+ ```mermaid
26
+ flowchart LR
27
+ Caller["Caller service"]
28
+ SDK["unique_search_proxy_sdk"]
29
+ Core["unique_search_proxy_core"]
30
+ Proxy["unique_search_proxy_client"]
31
+
32
+ Caller --> SDK
33
+ SDK --> Core
34
+ SDK -->|"httpx"| Proxy
35
+ ```
36
+
37
+ Before a search call, the caller (or SDK) validates the payload against core request models. Non-2xx responses become the same `ProxyError` subclasses defined in core.
38
+
39
+ ---
40
+
41
+ ## 3. Architecture
42
+
43
+ Two layers: a **hand-written facade** over **OpenAPI-generated code**.
44
+
45
+ ```mermaid
46
+ flowchart TB
47
+ subgraph sdk_pkg["unique_search_proxy_sdk"]
48
+ Facade["UniqueSearchProxyClient"]
49
+ SC["SearchClient"]
50
+ ASC["AgentSearchClient"]
51
+ CC["CrawlClient"]
52
+ Transport["OpenapiTransport"]
53
+ Gen["_generated/"]
54
+ Conv["converters.py"]
55
+ Err["errors.py"]
56
+ end
57
+
58
+ Core["unique_search_proxy_core"]
59
+ Proxy["Proxy pod"]
60
+
61
+ Facade --> SC & ASC & CC
62
+ SC & ASC & CC --> Transport
63
+ Transport --> Gen
64
+ SC --> Conv --> Core
65
+ Err --> Core
66
+ Gen -->|"httpx AsyncClient"| Proxy
67
+ ```
68
+
69
+ | Layer | Path | Responsibility |
70
+ |-------|------|----------------|
71
+ | **Facade** | `client.py` | `UniqueSearchProxyClient` — composes sub-clients, `health()`, `ready()` |
72
+ | **Sub-clients** | `search_client.py`, `agent_search_client.py`, `crawl_client.py` | Per-provider typed endpoints + compatibility dispatchers |
73
+ | **Endpoint factory** | `_endpoint.py` | `async_post_endpoint` / `async_sse_endpoint` (ParamSpec from core request models) |
74
+ | **Transport** | `_transport.py`, `_http.py` | Shared httpx lifecycle, base URL, timeout |
75
+ | **Generated** | `_generated/` | Route functions + attrs models from OpenAPI (**do not edit**) |
76
+ | **Converters** | `converters.py` | Core Pydantic models → generated SDK models |
77
+ | **Errors** | `errors.py` | Error envelope → core `ProxyError` subclasses |
78
+
79
+ The client package is the **source of truth** for the HTTP contract. When routes change, regenerate `_generated/` from `unique_search_proxy_client/openapi.json`.
80
+
81
+ ---
82
+
83
+ ## 4. Usage
84
+
85
+ Each provider exposes a typed endpoint whose kwargs mirror the core `*Request` Pydantic model (via `ParamSpec`). Snake_case and camelCase aliases both work (`fetch_size` / `fetchSize`).
86
+
87
+ ```python
88
+ from unique_search_proxy_sdk import UniqueSearchProxyClient
89
+
90
+ async with UniqueSearchProxyClient("http://unique-search-proxy:2349") as client:
91
+ await client.health()
92
+
93
+ # Typed per-provider endpoints (preferred)
94
+ result = await client.search.google(query="unique ag", gl="ch", fetch_size=10)
95
+ brave = await client.search.brave(query="news", country="US", safesearch="strict")
96
+ agent = await client.agent_search.bing(query="EU AI Act timeline", fetch_size=5)
97
+ crawl = await client.crawl.basic(urls=["https://example.com"])
98
+
99
+ # Compatibility dispatchers (engine/crawler string)
100
+ result = await client.search.search("unique ag", engine="google", fetch_size=10)
101
+ agent = await client.agent_search.search("query", engine="bing")
102
+ crawl = await client.crawl.crawl(["https://example.com"], crawler="Basic")
103
+
104
+ # Agent SSE streaming
105
+ async for event in client.agent_search.bing_stream(query="query"):
106
+ ...
107
+
108
+ # Low-level: one generated function per route
109
+ raw_client = client.openapi
110
+ ```
111
+
112
+ Search payloads are validated through core's `parse_search_request()` before serialization. Agent and crawl clients follow the same pattern.
113
+
114
+ ---
115
+
116
+ ## 5. API mapping
117
+
118
+ | Facade method | HTTP |
119
+ |---------------|------|
120
+ | `health()` | `GET /health` |
121
+ | `ready()` | `GET /ready` |
122
+ | `search.google(...)` / `.brave(...)` / `.perplexity(...)` | `POST /v1/search` |
123
+ | `search.search(...)` | `POST /v1/search` (dispatcher) |
124
+ | `agent_search.bing(...)` / `.vertexai(...)` | `POST /v1/agent-search` |
125
+ | `agent_search.bing_stream(...)` / `.vertexai_stream(...)` | `POST /v1/agent-search/stream` (SSE) |
126
+ | `agent_search.search(...)` / `.stream(...)` | same routes (dispatchers) |
127
+ | `crawl.basic(...)` / `.tavily(...)` / `.jina(...)` / `.firecrawl(...)` | `POST /v1/crawl` |
128
+ | `crawl.crawl(...)` | `POST /v1/crawl` (dispatcher) |
129
+ | `openapi` | Low-level generated client (one function per route) |
130
+
131
+ Endpoint payloads and provider ids → [Client README](../unique_search_proxy_client/README.md).
132
+
133
+ ---
134
+
135
+ ## 6. Error handling
136
+
137
+ Non-success responses with a proxy error envelope raise typed exceptions from core:
138
+
139
+ ```python
140
+ from unique_search_proxy_core import EngineNotConfiguredError, UpstreamError
141
+ ```
142
+
143
+ `ENGINE_NOT_CONFIGURED` (503) includes `missing_env_vars` parsed from the error `details` list — useful for operators and LLM tool consumers.
144
+
145
+ Transport failures (non-JSON body, connection errors) raise `UniqueSearchProxyClientError`.
146
+
147
+ ---
148
+
149
+ ## 7. Codegen workflow
150
+
151
+ Generated via [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) from the server's OpenAPI spec.
152
+
153
+ ```bash
154
+ cd ../unique_search_proxy_client
155
+ uv sync
156
+ uv run python scripts/generate_sdk.py
157
+ ```
158
+
159
+ | Path | Role |
160
+ |------|------|
161
+ | `unique_search_proxy_sdk/_generated/` | Regenerated httpx client + attrs models |
162
+ | `unique_search_proxy_client/openapi.json` | Exported spec (codegen input) |
163
+ | `unique_search_proxy_sdk/client.py` | Hand-written facade (not regenerated) |
164
+
165
+ Other codegen tools considered: OpenAPI Generator, datamodel-code-generator, Kiota — see git history in the client README if needed.
166
+
167
+ ---
168
+
169
+ ## 8. Testing
170
+
171
+ Pass an injected `httpx.AsyncClient` with `ASGITransport` for in-process tests against `create_app()`:
172
+
173
+ ```python
174
+ import httpx
175
+ from httpx import ASGITransport
176
+ from unique_search_proxy_client.web.app import create_app
177
+ from unique_search_proxy_sdk import UniqueSearchProxyClient
178
+
179
+ app = create_app()
180
+ async with httpx.AsyncClient(
181
+ transport=ASGITransport(app=app),
182
+ base_url="http://test",
183
+ ) as http:
184
+ async with UniqueSearchProxyClient("http://test", http_client=http) as client:
185
+ ...
186
+ ```
187
+
188
+ Run the app lifespan so the in-process `HttpClientPool` is initialised.
189
+
190
+ ---
191
+
192
+ ## 9. Installation & development
193
+
194
+ ```bash
195
+ cd unique_search_proxy_sdk
196
+ uv sync
197
+ uv run pytest
198
+ uv run ruff check .
199
+ uv run basedpyright
200
+ ```
201
+
202
+ Depends on `unique-search-proxy-core`. Does not depend on `unique-search-proxy` at runtime (only as a dev dependency for integration tests).
203
+
204
+ ---
205
+
206
+ ## License
207
+
208
+ Proprietary — Unique AG
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy-sdk"
3
- version = "2026.26.0.dev4"
3
+ version = "2026.28.0.dev0"
4
4
  description = "Async HTTP client for the Unique Search Proxy API"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -9,7 +9,7 @@ dependencies = [
9
9
  "attrs>=24.3.0,<26",
10
10
  "httpx>=0.28.0,<0.29.0",
11
11
  "pydantic>=2.12.5,<3.0.0",
12
- "unique-search-proxy-core>=2026.26.0.dev4,<2026.26.0rc0",
12
+ "unique-search-proxy-core>=2026.28.0.dev0,<2026.28.0rc0",
13
13
  ]
14
14
 
15
15
  [dependency-groups]
@@ -71,3 +71,9 @@ markers = [
71
71
  "asyncio: asyncio tests",
72
72
  "integration: requires unique-search-proxy (FastAPI app) in the environment",
73
73
  ]
74
+
75
+ [tool.coverage.run]
76
+ omit = [
77
+ "unique_search_proxy_sdk/_generated/*",
78
+ "unique_search_proxy_sdk/typing_checks.py",
79
+ ]
@@ -13,6 +13,7 @@ Regenerate after API changes::
13
13
  """
14
14
 
15
15
  from unique_search_proxy_sdk._generated.client import Client as OpenAPIClient
16
+ from unique_search_proxy_sdk.agent_search_client import AgentSearchClient
16
17
  from unique_search_proxy_sdk.client import UniqueSearchProxyClient
17
18
  from unique_search_proxy_sdk.crawl_client import CrawlClient
18
19
  from unique_search_proxy_sdk.errors import UniqueSearchProxyClientError
@@ -20,6 +21,7 @@ from unique_search_proxy_sdk.search_client import SearchClient
20
21
 
21
22
  __all__ = [
22
23
  "CrawlClient",
24
+ "AgentSearchClient",
23
25
  "OpenAPIClient",
24
26
  "SearchClient",
25
27
  "UniqueSearchProxyClient",
@@ -0,0 +1,99 @@
1
+ """Async endpoint factories with ParamSpec signatures from core request models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import AsyncIterator, Awaitable, Callable
7
+ from typing import Any, ParamSpec, Protocol, TypeVar, cast
8
+
9
+ import httpx
10
+ from pydantic import BaseModel
11
+
12
+ from unique_search_proxy_sdk._http import unwrap_response
13
+ from unique_search_proxy_sdk._transport import OpenapiTransport
14
+ from unique_search_proxy_sdk.errors import raise_for_proxy_response
15
+
16
+ ResponseType = TypeVar("ResponseType")
17
+ RequestConstructorSpec = ParamSpec("RequestConstructorSpec")
18
+
19
+
20
+ def _kwargs_without_none(**kwargs: Any) -> dict[str, Any]:
21
+ return {key: value for key, value in kwargs.items() if value is not None}
22
+
23
+
24
+ class SdkBody(Protocol):
25
+ def to_dict(self) -> dict[str, Any]: ...
26
+
27
+
28
+ def async_post_endpoint(
29
+ transport: OpenapiTransport,
30
+ request_model: Callable[RequestConstructorSpec, BaseModel],
31
+ *,
32
+ parse: Callable[[object], BaseModel],
33
+ to_sdk: Callable[[BaseModel], SdkBody],
34
+ post: Callable[..., Awaitable[Any]],
35
+ response_type: type[ResponseType],
36
+ ) -> Callable[RequestConstructorSpec, Awaitable[ResponseType]]:
37
+ """Build an async POST caller whose kwargs match ``request_model``."""
38
+ _ = response_type
39
+
40
+ async def call(
41
+ *args: RequestConstructorSpec.args,
42
+ **kwargs: RequestConstructorSpec.kwargs,
43
+ ) -> ResponseType:
44
+ constructed = request_model(*args, **_kwargs_without_none(**kwargs))
45
+ validated = parse(
46
+ constructed.model_dump(mode="json", by_alias=True, exclude_none=True),
47
+ )
48
+ sdk_body = to_sdk(validated)
49
+ response = await post(client=transport.openapi, body=sdk_body)
50
+ return cast(ResponseType, unwrap_response(response))
51
+
52
+ return call
53
+
54
+
55
+ def async_sse_endpoint(
56
+ transport: OpenapiTransport,
57
+ path: str,
58
+ request_model: Callable[RequestConstructorSpec, BaseModel],
59
+ *,
60
+ parse: Callable[[object], BaseModel],
61
+ to_sdk: Callable[[BaseModel], SdkBody],
62
+ ) -> Callable[RequestConstructorSpec, AsyncIterator[dict[str, Any]]]:
63
+ """Build an async SSE stream caller whose kwargs match ``request_model``."""
64
+
65
+ async def stream(
66
+ *args: RequestConstructorSpec.args,
67
+ **kwargs: RequestConstructorSpec.kwargs,
68
+ ) -> AsyncIterator[dict[str, Any]]:
69
+ constructed = request_model(*args, **_kwargs_without_none(**kwargs))
70
+ validated = parse(
71
+ constructed.model_dump(mode="json", by_alias=True, exclude_none=True),
72
+ )
73
+ sdk_body = to_sdk(validated)
74
+ http_client = transport.openapi.get_async_httpx_client()
75
+ base_url = transport.base_url
76
+ async with http_client.stream(
77
+ "POST",
78
+ f"{base_url}{path}",
79
+ json=sdk_body.to_dict(),
80
+ headers={"Content-Type": "application/json"},
81
+ ) as response:
82
+ if response.status_code >= 400:
83
+ raw = await response.aread()
84
+ raise_for_proxy_response(
85
+ httpx.Response(
86
+ status_code=response.status_code,
87
+ content=raw,
88
+ headers=dict(response.headers),
89
+ ),
90
+ )
91
+ async for line in response.aiter_lines():
92
+ if not line.startswith("data: "):
93
+ continue
94
+ yield json.loads(line.removeprefix("data: "))
95
+
96
+ return stream
97
+
98
+
99
+ __all__ = ["async_post_endpoint", "async_sse_endpoint"]