diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 3f63a67..7f3f5c8 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.22.1"
+ ".": "0.23.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index f09aad2..d7f028a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 22
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/context-dev/context.dev-c4e3d32c831ceda41c21a08b227addc6969457f9b2824f5afb4297d81ce6b379.yml
-openapi_spec_hash: 8e825842065c24f11fd3f51fdd705337
-config_hash: 70354330f92ce169beabc98696ebb9a3
+configured_endpoints: 23
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/context-dev/context.dev-24aa0941b29bf383cfb7dfb8cafd75999346bffb07b33738a89c3c9bbeb798a9.yml
+openapi_spec_hash: 5530a1589a20c8c337e7f6ebaaa0aecb
+config_hash: f481fca4de4b1657023180d4fc47ef22
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e6482b..5288f36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.23.0 (2026-05-16)
+
+Full Changelog: [v0.22.1...v0.23.0](https://github.com/context-dot-dev/context-python-sdk/compare/v0.22.1...v0.23.0)
+
+### Features
+
+* **api:** manual updates ([bce4c91](https://github.com/context-dot-dev/context-python-sdk/commit/bce4c91688d28054c361ee403be2333229a33f41))
+
## 0.22.1 (2026-05-13)
Full Changelog: [v0.22.0...v0.22.1](https://github.com/context-dot-dev/context-python-sdk/compare/v0.22.0...v0.22.1)
diff --git a/api.md b/api.md
index 3dd9e9a..04e3161 100644
--- a/api.md
+++ b/api.md
@@ -7,6 +7,7 @@ from context.dev.types import (
WebExtractFontsResponse,
WebExtractStyleguideResponse,
WebScreenshotResponse,
+ WebSearchResponse,
WebWebCrawlMdResponse,
WebWebScrapeHTMLResponse,
WebWebScrapeImagesResponse,
@@ -20,6 +21,7 @@ Methods:
- client.web.extract_fonts(\*\*params) -> WebExtractFontsResponse
- client.web.extract_styleguide(\*\*params) -> WebExtractStyleguideResponse
- client.web.screenshot(\*\*params) -> WebScreenshotResponse
+- client.web.search(\*\*params) -> WebSearchResponse
- client.web.web_crawl_md(\*\*params) -> WebWebCrawlMdResponse
- client.web.web_scrape_html(\*\*params) -> WebWebScrapeHTMLResponse
- client.web.web_scrape_images(\*\*params) -> WebWebScrapeImagesResponse
diff --git a/pyproject.toml b/pyproject.toml
index ee2a59f..a714cf7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "context.dev"
-version = "0.22.1"
+version = "0.23.0"
description = "The official Python library for the context.dev API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/context/dev/_version.py b/src/context/dev/_version.py
index bc205aa..6992694 100644
--- a/src/context/dev/_version.py
+++ b/src/context/dev/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "context.dev"
-__version__ = "0.22.1" # x-release-please-version
+__version__ = "0.23.0" # x-release-please-version
diff --git a/src/context/dev/resources/web.py b/src/context/dev/resources/web.py
index 61b54bb..c8b66f6 100644
--- a/src/context/dev/resources/web.py
+++ b/src/context/dev/resources/web.py
@@ -7,6 +7,7 @@
import httpx
from ..types import (
+ web_search_params,
web_screenshot_params,
web_web_crawl_md_params,
web_extract_fonts_params,
@@ -16,7 +17,7 @@
web_extract_styleguide_params,
web_web_scrape_sitemap_params,
)
-from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
+from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from .._utils import maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
@@ -27,6 +28,7 @@
async_to_streamed_response_wrapper,
)
from .._base_client import make_request_options
+from ..types.web_search_response import WebSearchResponse
from ..types.web_screenshot_response import WebScreenshotResponse
from ..types.web_web_crawl_md_response import WebWebCrawlMdResponse
from ..types.web_extract_fonts_response import WebExtractFontsResponse
@@ -266,6 +268,73 @@ def screenshot(
cast_to=WebScreenshotResponse,
)
+ def search(
+ self,
+ *,
+ query: str,
+ exclude_domains: SequenceNotStr[str] | Omit = omit,
+ freshness: Literal["last_24_hours", "last_week", "last_month", "last_year"] | Omit = omit,
+ include_domains: SequenceNotStr[str] | Omit = omit,
+ markdown_options: web_search_params.MarkdownOptions | Omit = omit,
+ query_fanout: bool | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> WebSearchResponse:
+ """
+ Search the web and optionally scrape each result to Markdown in one round-trip.
+
+ Args:
+ query: Natural-language search query.
+
+ exclude_domains: Blocklist — drop results from these domains. Example: ["pinterest.com",
+ "reddit.com"].
+
+ freshness: Restrict results to content published within this window.
+
+ include_domains: Allowlist — only return results from these domains. Example: ["arxiv.org",
+ "github.com"].
+
+ markdown_options: Inline Markdown scraping for each result. Set `enabled: true` to activate.
+
+ query_fanout: Expand the query into multiple parallel variants for broader recall.
+
+ timeout_ms: Optional timeout in milliseconds for the request. If the request takes longer
+ than this value, it will be aborted with a 408 status code. Maximum allowed
+ value is 300000ms (5 minutes).
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return self._post(
+ "/web/search",
+ body=maybe_transform(
+ {
+ "query": query,
+ "exclude_domains": exclude_domains,
+ "freshness": freshness,
+ "include_domains": include_domains,
+ "markdown_options": markdown_options,
+ "query_fanout": query_fanout,
+ "timeout_ms": timeout_ms,
+ },
+ web_search_params.WebSearchParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebSearchResponse,
+ )
+
def web_crawl_md(
self,
*,
@@ -882,6 +951,73 @@ async def screenshot(
cast_to=WebScreenshotResponse,
)
+ async def search(
+ self,
+ *,
+ query: str,
+ exclude_domains: SequenceNotStr[str] | Omit = omit,
+ freshness: Literal["last_24_hours", "last_week", "last_month", "last_year"] | Omit = omit,
+ include_domains: SequenceNotStr[str] | Omit = omit,
+ markdown_options: web_search_params.MarkdownOptions | Omit = omit,
+ query_fanout: bool | Omit = omit,
+ timeout_ms: int | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> WebSearchResponse:
+ """
+ Search the web and optionally scrape each result to Markdown in one round-trip.
+
+ Args:
+ query: Natural-language search query.
+
+ exclude_domains: Blocklist — drop results from these domains. Example: ["pinterest.com",
+ "reddit.com"].
+
+ freshness: Restrict results to content published within this window.
+
+ include_domains: Allowlist — only return results from these domains. Example: ["arxiv.org",
+ "github.com"].
+
+ markdown_options: Inline Markdown scraping for each result. Set `enabled: true` to activate.
+
+ query_fanout: Expand the query into multiple parallel variants for broader recall.
+
+ timeout_ms: Optional timeout in milliseconds for the request. If the request takes longer
+ than this value, it will be aborted with a 408 status code. Maximum allowed
+ value is 300000ms (5 minutes).
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ return await self._post(
+ "/web/search",
+ body=await async_maybe_transform(
+ {
+ "query": query,
+ "exclude_domains": exclude_domains,
+ "freshness": freshness,
+ "include_domains": include_domains,
+ "markdown_options": markdown_options,
+ "query_fanout": query_fanout,
+ "timeout_ms": timeout_ms,
+ },
+ web_search_params.WebSearchParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=WebSearchResponse,
+ )
+
async def web_crawl_md(
self,
*,
@@ -1284,6 +1420,9 @@ def __init__(self, web: WebResource) -> None:
self.screenshot = to_raw_response_wrapper(
web.screenshot,
)
+ self.search = to_raw_response_wrapper(
+ web.search,
+ )
self.web_crawl_md = to_raw_response_wrapper(
web.web_crawl_md,
)
@@ -1314,6 +1453,9 @@ def __init__(self, web: AsyncWebResource) -> None:
self.screenshot = async_to_raw_response_wrapper(
web.screenshot,
)
+ self.search = async_to_raw_response_wrapper(
+ web.search,
+ )
self.web_crawl_md = async_to_raw_response_wrapper(
web.web_crawl_md,
)
@@ -1344,6 +1486,9 @@ def __init__(self, web: WebResource) -> None:
self.screenshot = to_streamed_response_wrapper(
web.screenshot,
)
+ self.search = to_streamed_response_wrapper(
+ web.search,
+ )
self.web_crawl_md = to_streamed_response_wrapper(
web.web_crawl_md,
)
@@ -1374,6 +1519,9 @@ def __init__(self, web: AsyncWebResource) -> None:
self.screenshot = async_to_streamed_response_wrapper(
web.screenshot,
)
+ self.search = async_to_streamed_response_wrapper(
+ web.search,
+ )
self.web_crawl_md = async_to_streamed_response_wrapper(
web.web_crawl_md,
)
diff --git a/src/context/dev/types/__init__.py b/src/context/dev/types/__init__.py
index 60c8ac7..8c237b0 100644
--- a/src/context/dev/types/__init__.py
+++ b/src/context/dev/types/__init__.py
@@ -2,7 +2,9 @@
from __future__ import annotations
+from .web_search_params import WebSearchParams as WebSearchParams
from .ai_ai_query_params import AIAIQueryParams as AIAIQueryParams
+from .web_search_response import WebSearchResponse as WebSearchResponse
from .ai_ai_query_response import AIAIQueryResponse as AIAIQueryResponse
from .brand_retrieve_params import BrandRetrieveParams as BrandRetrieveParams
from .web_screenshot_params import WebScreenshotParams as WebScreenshotParams
diff --git a/src/context/dev/types/web_search_params.py b/src/context/dev/types/web_search_params.py
new file mode 100644
index 0000000..5c3e89f
--- /dev/null
+++ b/src/context/dev/types/web_search_params.py
@@ -0,0 +1,103 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, Annotated, TypedDict
+
+from .._types import SequenceNotStr
+from .._utils import PropertyInfo
+
+__all__ = ["WebSearchParams", "MarkdownOptions", "MarkdownOptionsPdf"]
+
+
+class WebSearchParams(TypedDict, total=False):
+ query: Required[str]
+ """Natural-language search query."""
+
+ exclude_domains: Annotated[SequenceNotStr[str], PropertyInfo(alias="excludeDomains")]
+ """Blocklist — drop results from these domains.
+
+ Example: ["pinterest.com", "reddit.com"].
+ """
+
+ freshness: Literal["last_24_hours", "last_week", "last_month", "last_year"]
+ """Restrict results to content published within this window."""
+
+ include_domains: Annotated[SequenceNotStr[str], PropertyInfo(alias="includeDomains")]
+ """Allowlist — only return results from these domains.
+
+ Example: ["arxiv.org", "github.com"].
+ """
+
+ markdown_options: Annotated[MarkdownOptions, PropertyInfo(alias="markdownOptions")]
+ """Inline Markdown scraping for each result. Set `enabled: true` to activate."""
+
+ query_fanout: Annotated[bool, PropertyInfo(alias="queryFanout")]
+ """Expand the query into multiple parallel variants for broader recall."""
+
+ timeout_ms: Annotated[int, PropertyInfo(alias="timeoutMS")]
+ """Optional timeout in milliseconds for the request.
+
+ If the request takes longer than this value, it will be aborted with a 408
+ status code. Maximum allowed value is 300000ms (5 minutes).
+ """
+
+
+class MarkdownOptionsPdf(TypedDict, total=False):
+ """PDF handling. Use start/end to bound text extraction and OCR to a page range."""
+
+ end: int
+ """Last PDF page to parse (1-based, inclusive).
+
+ Defaults to the final page. Must be >= start.
+ """
+
+ should_parse: Annotated[bool, PropertyInfo(alias="shouldParse")]
+ """Parse PDF URLs. When false, PDF results are skipped with WEBSITE_ACCESS_ERROR."""
+
+ start: int
+ """First PDF page to parse (1-based, inclusive). Defaults to page 1."""
+
+
+class MarkdownOptions(TypedDict, total=False):
+ """Inline Markdown scraping for each result. Set `enabled: true` to activate."""
+
+ enabled: bool
+ """Scrape each result to Markdown. Off by default to keep search cheap and fast."""
+
+ include_frames: Annotated[bool, PropertyInfo(alias="includeFrames")]
+ """Render iframe contents into the Markdown."""
+
+ include_images: Annotated[bool, PropertyInfo(alias="includeImages")]
+ """Emit image references in the Markdown."""
+
+ include_links: Annotated[bool, PropertyInfo(alias="includeLinks")]
+ """Keep hyperlinks in the Markdown."""
+
+ max_age_ms: Annotated[int, PropertyInfo(alias="maxAgeMs")]
+ """Cache TTL in ms for scraped Markdown keyed by URL + options.
+
+ Default 1 day, max 30 days. Set to 0 to force a fresh scrape.
+ """
+
+ pdf: MarkdownOptionsPdf
+ """PDF handling. Use start/end to bound text extraction and OCR to a page range."""
+
+ shorten_base64_images: Annotated[bool, PropertyInfo(alias="shortenBase64Images")]
+ """Truncate inline base64 image payloads to keep responses small."""
+
+ timeout_ms: Annotated[int, PropertyInfo(alias="timeoutMS")]
+ """Optional timeout in milliseconds for the request.
+
+ If the request takes longer than this value, it will be aborted with a 408
+ status code. Maximum allowed value is 300000ms (5 minutes).
+ """
+
+ use_main_content_only: Annotated[bool, PropertyInfo(alias="useMainContentOnly")]
+ """Strip nav, header, footer, and sidebar — keep only the primary article content."""
+
+ wait_for_ms: Annotated[int, PropertyInfo(alias="waitForMs")]
+ """Extra wait after page load before rendering, in ms (0–30000).
+
+ Useful for JS-heavy pages.
+ """
diff --git a/src/context/dev/types/web_search_response.py b/src/context/dev/types/web_search_response.py
new file mode 100644
index 0000000..2e55d97
--- /dev/null
+++ b/src/context/dev/types/web_search_response.py
@@ -0,0 +1,45 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["WebSearchResponse", "Result", "ResultMarkdown"]
+
+
+class ResultMarkdown(BaseModel):
+ """Markdown scrape status and content for this result."""
+
+ code: Literal["SUCCESS", "NOT_REQUESTED", "TIMEOUT", "WEBSITE_ACCESS_ERROR", "ERROR"]
+ """Per-result scrape outcome. Inspect this before reading `markdown`."""
+
+ markdown: Optional[str] = None
+ """GFM Markdown of the page.
+
+ Null unless markdownOptions.enabled is true and scraping succeeded.
+ """
+
+
+class Result(BaseModel):
+ description: str
+ """Snippet excerpt from the page."""
+
+ markdown: ResultMarkdown
+ """Markdown scrape status and content for this result."""
+
+ relevance: Literal["high", "medium", "low"]
+ """Model-judged relevance to the original query."""
+
+ title: str
+ """Page title."""
+
+ url: str
+ """Canonical result URL."""
+
+
+class WebSearchResponse(BaseModel):
+ query: str
+ """Echo of the original query (useful when fanout was enabled)."""
+
+ results: List[Result]
diff --git a/tests/api_resources/test_web.py b/tests/api_resources/test_web.py
index 51f2720..ddb16a2 100644
--- a/tests/api_resources/test_web.py
+++ b/tests/api_resources/test_web.py
@@ -10,6 +10,7 @@
from context.dev import ContextDev, AsyncContextDev
from tests.utils import assert_matches_type
from context.dev.types import (
+ WebSearchResponse,
WebScreenshotResponse,
WebWebCrawlMdResponse,
WebWebScrapeMdResponse,
@@ -149,6 +150,69 @@ def test_streaming_response_screenshot(self, client: ContextDev) -> None:
assert cast(Any, response.is_closed) is True
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_search(self, client: ContextDev) -> None:
+ web = client.web.search(
+ query="x",
+ )
+ assert_matches_type(WebSearchResponse, web, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_search_with_all_params(self, client: ContextDev) -> None:
+ web = client.web.search(
+ query="x",
+ exclude_domains=["string"],
+ freshness="last_24_hours",
+ include_domains=["string"],
+ markdown_options={
+ "enabled": True,
+ "include_frames": True,
+ "include_images": True,
+ "include_links": True,
+ "max_age_ms": 0,
+ "pdf": {
+ "end": 1,
+ "should_parse": True,
+ "start": 1,
+ },
+ "shorten_base64_images": True,
+ "timeout_ms": 1000,
+ "use_main_content_only": True,
+ "wait_for_ms": 0,
+ },
+ query_fanout=True,
+ timeout_ms=1000,
+ )
+ assert_matches_type(WebSearchResponse, web, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_search(self, client: ContextDev) -> None:
+ response = client.web.with_raw_response.search(
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ web = response.parse()
+ assert_matches_type(WebSearchResponse, web, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_search(self, client: ContextDev) -> None:
+ with client.web.with_streaming_response.search(
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ web = response.parse()
+ assert_matches_type(WebSearchResponse, web, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_web_crawl_md(self, client: ContextDev) -> None:
@@ -540,6 +604,69 @@ async def test_streaming_response_screenshot(self, async_client: AsyncContextDev
assert cast(Any, response.is_closed) is True
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_search(self, async_client: AsyncContextDev) -> None:
+ web = await async_client.web.search(
+ query="x",
+ )
+ assert_matches_type(WebSearchResponse, web, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_search_with_all_params(self, async_client: AsyncContextDev) -> None:
+ web = await async_client.web.search(
+ query="x",
+ exclude_domains=["string"],
+ freshness="last_24_hours",
+ include_domains=["string"],
+ markdown_options={
+ "enabled": True,
+ "include_frames": True,
+ "include_images": True,
+ "include_links": True,
+ "max_age_ms": 0,
+ "pdf": {
+ "end": 1,
+ "should_parse": True,
+ "start": 1,
+ },
+ "shorten_base64_images": True,
+ "timeout_ms": 1000,
+ "use_main_content_only": True,
+ "wait_for_ms": 0,
+ },
+ query_fanout=True,
+ timeout_ms=1000,
+ )
+ assert_matches_type(WebSearchResponse, web, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_search(self, async_client: AsyncContextDev) -> None:
+ response = await async_client.web.with_raw_response.search(
+ query="x",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ web = await response.parse()
+ assert_matches_type(WebSearchResponse, web, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_search(self, async_client: AsyncContextDev) -> None:
+ async with async_client.web.with_streaming_response.search(
+ query="x",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ web = await response.parse()
+ assert_matches_type(WebSearchResponse, web, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_web_crawl_md(self, async_client: AsyncContextDev) -> None: