unique-search-proxy-core 2026.44.0.dev8__tar.gz → 2026.44.0.dev10__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 (67) hide show
  1. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/PKG-INFO +3 -3
  2. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/README.md +1 -1
  3. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/pyproject.toml +2 -2
  4. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/__init__.py +4 -0
  5. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/base.py +3 -2
  6. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/http_client/__init__.py +2 -0
  7. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/http_client/response_cap.py +54 -6
  8. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/schema.py +40 -2
  9. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/__init__.py +0 -0
  10. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/bing/__init__.py +0 -0
  11. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/bing/enums.py +0 -0
  12. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/bing/grounding.py +0 -0
  13. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/bing/schema.py +0 -0
  14. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/bing/settings.py +0 -0
  15. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/config_types.py +0 -0
  16. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/output_schema.py +0 -0
  17. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/resolve.py +0 -0
  18. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/response_parsing.py +0 -0
  19. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/vertexai/__init__.py +0 -0
  20. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/vertexai/schema.py +0 -0
  21. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/agent_engines/vertexai/settings.py +0 -0
  22. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/context.py +0 -0
  23. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/__init__.py +0 -0
  24. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/base.py +0 -0
  25. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/basic/__init__.py +0 -0
  26. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/basic/content_types.py +0 -0
  27. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/basic/processing/policy.py +0 -0
  28. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/basic/schema.py +0 -0
  29. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/config_types.py +0 -0
  30. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/firecrawl/schema.py +0 -0
  31. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/jina/schema.py +0 -0
  32. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/crawlers/tavily/schema.py +0 -0
  33. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/errors.py +0 -0
  34. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/http_client/client.py +0 -0
  35. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/http_client/credentials.py +0 -0
  36. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/http_client/secrets.py +0 -0
  37. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/http_client/settings.py +0 -0
  38. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/logging.py +0 -0
  39. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/param_policy/__init__.py +0 -0
  40. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/param_policy/annotations.py +0 -0
  41. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/param_policy/derive.py +0 -0
  42. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/param_policy/exposable_config.py +0 -0
  43. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/param_policy/exposable_param.py +0 -0
  44. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/param_policy/exposed_params.py +0 -0
  45. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/param_policy/request_base.py +0 -0
  46. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/providers/schema.py +0 -0
  47. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/__init__.py +0 -0
  48. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/base.py +0 -0
  49. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/brave/__init__.py +0 -0
  50. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/brave/schema.py +0 -0
  51. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/config_types.py +0 -0
  52. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/custom_api/__init__.py +0 -0
  53. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/custom_api/schema.py +0 -0
  54. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/google/__init__.py +0 -0
  55. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/google/schema.py +0 -0
  56. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/perplexity/__init__.py +0 -0
  57. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/search_engines/perplexity/schema.py +0 -0
  58. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/__init__.py +0 -0
  59. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/dns.py +0 -0
  60. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/egress.py +0 -0
  61. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/models.py +0 -0
  62. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/netloc.py +0 -0
  63. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/policy.py +0 -0
  64. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/redirect.py +0 -0
  65. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/resolver.py +0 -0
  66. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/unique_search_proxy_core/url_safety/service.py +0 -0
  67. {unique_search_proxy_core-2026.44.0.dev8 → unique_search_proxy_core-2026.44.0.dev10}/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.44.0.dev8
3
+ Version: 2026.44.0.dev10
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.44.0.dev9,<2026.44.0rc0
11
+ Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.0rc0
12
12
  Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
14
 
@@ -150,7 +150,7 @@ flowchart TB
150
150
 
151
151
  | Module | Responsibility |
152
152
  |--------|----------------|
153
- | `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse`, `CrawlResponse`, `WebSearchResult`, `ErrorResponse`, SSE events |
153
+ | `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse` (+ `AnswerSpanCitation`), `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
156
  | `param_policy/exposable_config.py` | `ExposableParamsConfig` — the `exposed_params_model()` / `merge()` lifecycle shared by search and agent config bases |
@@ -136,7 +136,7 @@ flowchart TB
136
136
 
137
137
  | Module | Responsibility |
138
138
  |--------|----------------|
139
- | `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse`, `CrawlResponse`, `WebSearchResult`, `ErrorResponse`, SSE events |
139
+ | `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse` (+ `AnswerSpanCitation`), `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
142
  | `param_policy/exposable_config.py` | `ExposableParamsConfig` — the `exposed_params_model()` / `merge()` lifecycle shared by search and agent config bases |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy-core"
3
- version = "2026.44.0.dev8"
3
+ version = "2026.44.0.dev10"
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.44.0.dev9,<2026.44.0rc0",
13
+ "unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.0rc0",
14
14
 
15
15
  ]
16
16
 
@@ -19,6 +19,8 @@ from unique_search_proxy_core.errors import (
19
19
  ValidationProxyError,
20
20
  )
21
21
  from unique_search_proxy_core.schema import (
22
+ AgentSearchResponse,
23
+ AnswerSpanCitation,
22
24
  CrawlResponse,
23
25
  ErrorDetail,
24
26
  ErrorResponse,
@@ -35,6 +37,8 @@ __all__ = [
35
37
  "RequestContext",
36
38
  "USER_ID_HEADER",
37
39
  "BadRequestProxyError",
40
+ "AgentSearchResponse",
41
+ "AnswerSpanCitation",
38
42
  "CrawlResponse",
39
43
  "EmptySearchResultsError",
40
44
  "EngineNotConfiguredError",
@@ -112,8 +112,9 @@ class AgentSearchEngine(ABC, Generic[AgentRequestT]):
112
112
  """Agent (grounded) search engine contract for v1 providers.
113
113
 
114
114
  Implementations call the upstream agent/grounding provider and return opaque
115
- ``answer`` text plus ``raw`` provider payload. Content resolution is left to
116
- consumers; this layer stays provider-egress only.
115
+ ``answer`` text, the ``citations`` the provider attached to it, and ``raw``
116
+ provider payload. Content resolution is left to consumers; this layer stays
117
+ provider-egress only.
117
118
  """
118
119
 
119
120
  engine_id: str
@@ -19,6 +19,7 @@ from unique_search_proxy_core.http_client.credentials import (
19
19
  )
20
20
  from unique_search_proxy_core.http_client.response_cap import (
21
21
  MAX_RESPONSE_BYTES_EXTENSION,
22
+ ByteBudget,
22
23
  install_response_size_cap,
23
24
  read_capped_body,
24
25
  response_size_cap_hook,
@@ -36,6 +37,7 @@ from unique_search_proxy_core.http_client.settings import (
36
37
 
37
38
  __all__ = [
38
39
  "MAX_RESPONSE_BYTES_EXTENSION",
40
+ "ByteBudget",
39
41
  "DirectRoute",
40
42
  "EgressRoute",
41
43
  "HttpClientRegistry",
@@ -115,10 +115,45 @@ def install_response_size_cap(
115
115
  return client
116
116
 
117
117
 
118
+ class ByteBudget:
119
+ """Shared allowance of decoded body bytes across several responses.
120
+
121
+ Bounds what one caller keeps in memory at once, e.g. every page of a crawl
122
+ request, which a per-response limit alone cannot do. Not thread-safe; share
123
+ it only between tasks of one event loop.
124
+ """
125
+
126
+ def __init__(self, max_bytes: int) -> None:
127
+ self._max_bytes = max_bytes
128
+ self._used = 0
129
+
130
+ @property
131
+ def exhausted(self) -> bool:
132
+ return self._used >= self._max_bytes
133
+
134
+ def consume(self, num_bytes: int) -> None:
135
+ """Reserve bytes, raising once the budget is exceeded.
136
+
137
+ Raises:
138
+ ResponseTooLargeError: When the reservation exceeds the budget.
139
+ """
140
+ self._used += num_bytes
141
+ if self._used > self._max_bytes:
142
+ raise ResponseTooLargeError(
143
+ f"Combined upstream responses exceeded the {self._max_bytes} "
144
+ "byte budget",
145
+ )
146
+
147
+ def release(self, num_bytes: int) -> None:
148
+ """Return bytes from a body that was discarded."""
149
+ self._used = max(0, self._used - num_bytes)
150
+
151
+
118
152
  async def read_capped_body(
119
153
  response: httpx.Response,
120
154
  *,
121
155
  max_bytes: int | None,
156
+ budget: ByteBudget | None = None,
122
157
  ) -> bytes:
123
158
  """Read a streamed response body, limiting its decompressed size.
124
159
 
@@ -129,25 +164,38 @@ async def read_capped_body(
129
164
  response (httpx.Response): Response opened with ``stream=True``.
130
165
  max_bytes (int | None): Largest decoded body allowed. ``None`` reads
131
166
  the whole body.
167
+ budget (ByteBudget | None): Shared budget the body is charged against.
168
+ Bytes are returned to it when the read fails.
132
169
 
133
170
  Returns:
134
171
  bytes: The decoded response body.
135
172
 
136
173
  Raises:
137
- ResponseTooLargeError: When the decoded body exceeds ``max_bytes``.
174
+ ResponseTooLargeError: When the decoded body exceeds ``max_bytes`` or
175
+ the shared budget.
138
176
  """
139
177
  chunks: list[bytes] = []
140
178
  received = 0
141
- async for chunk in response.aiter_bytes():
142
- received += len(chunk)
143
- if max_bytes is not None and received > max_bytes:
144
- raise ResponseTooLargeError.for_limit(max_bytes)
145
- chunks.append(chunk)
179
+ charged = 0
180
+ try:
181
+ async for chunk in response.aiter_bytes():
182
+ received += len(chunk)
183
+ if max_bytes is not None and received > max_bytes:
184
+ raise ResponseTooLargeError.for_limit(max_bytes)
185
+ if budget is not None:
186
+ charged += len(chunk)
187
+ budget.consume(len(chunk))
188
+ chunks.append(chunk)
189
+ except BaseException:
190
+ if budget is not None:
191
+ budget.release(charged)
192
+ raise
146
193
  return b"".join(chunks)
147
194
 
148
195
 
149
196
  __all__ = [
150
197
  "MAX_RESPONSE_BYTES_EXTENSION",
198
+ "ByteBudget",
151
199
  "ResponseHook",
152
200
  "install_response_size_cap",
153
201
  "read_capped_body",
@@ -186,11 +186,42 @@ class SearchResponse(BaseModel):
186
186
  curated: list[WebSearchResult]
187
187
 
188
188
 
189
+ class AnswerSpanCitation(BaseModel):
190
+ """A source cited at a specific span of the answer text.
191
+
192
+ Grounding with Bing Search reports these as ``url_citation`` annotations:
193
+ the agent writes a marker such as ``【4:0†source】`` into its answer, and
194
+ Foundry attaches the source to that marker's character span.
195
+ """
196
+
197
+ model_config = camelized_model_config
198
+
199
+ url: str = Field(description="URL of the cited source")
200
+ title: str | None = Field(
201
+ default=None,
202
+ description="Title of the cited source",
203
+ )
204
+ marker: str = Field(
205
+ description="Answer text between start_index and end_index",
206
+ )
207
+ start_index: int = Field(
208
+ description="Character offset in the answer where the marker starts",
209
+ )
210
+ end_index: int = Field(
211
+ description="Character offset in the answer just past the marker",
212
+ )
213
+ output_item_id: str | None = Field(
214
+ default=None,
215
+ description="Output item the citation is attached to, if the provider reports one",
216
+ )
217
+
218
+
189
219
  class AgentSearchResponse(BaseModel):
190
220
  """Result of an agent-based (grounded) search.
191
221
 
192
- Thin egress contract: opaque ``answer`` text from the provider plus ``raw``
193
- for debugging. Consumers interpret ``answer`` (JSON parsing, citations, etc.).
222
+ Thin egress contract: opaque ``answer`` text from the provider, the
223
+ citations the provider attached to it, and ``raw`` for debugging. Consumers
224
+ interpret ``answer`` (JSON parsing, mapping citations, etc.).
194
225
  """
195
226
 
196
227
  model_config = camelized_model_config
@@ -201,6 +232,13 @@ class AgentSearchResponse(BaseModel):
201
232
  default="",
202
233
  description="Agent response text as returned by the provider (opaque to the proxy)",
203
234
  )
235
+ citations: list[AnswerSpanCitation] = Field(
236
+ default_factory=list,
237
+ description=(
238
+ "Sources cited in the answer. Only Grounding with Bing Search "
239
+ "reports citations; other engines return an empty list."
240
+ ),
241
+ )
204
242
  raw: Any = Field(default=None, description="Opaque provider payload")
205
243
 
206
244