From 09858518e9312ca72238efd596cc0313927c26e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:41:50 +0000 Subject: [PATCH 1/3] feat(api): update via SDK Studio --- .stats.yml | 2 +- README.md | 26 ++++++++++++++++++++++ src/opencode_ai/_client.py | 4 ++++ src/opencode_ai/resources/event.py | 35 +++++++++++++++--------------- tests/api_resources/test_event.py | 28 ++++++++++-------------- tests/test_client.py | 24 ++++++++++++++++++++ 6 files changed, 84 insertions(+), 35 deletions(-) diff --git a/.stats.yml b/.stats.yml index 69d1c49..ef8559f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-b4a3f35e4a44e5a5034508ced15d7b44c1924000062e0f5293797413d26ee412.yml openapi_spec_hash: f17b1091020f90126e6cefc2d38ff85f -config_hash: 1156f6f6fb7245e7b021daddf23153e3 +config_hash: e2d21e779cfc4e26a99b9e4e75de3f50 diff --git a/README.md b/README.md index 5066f50..47fa0b6 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,32 @@ async def main() -> None: asyncio.run(main()) ``` +## Streaming responses + +We provide support for streaming responses using Server Side Events (SSE). + +```python +from opencode_ai import Opencode + +client = Opencode() + +stream = client.event.list() +for events in stream: + print(events) +``` + +The async client uses the exact same interface. + +```python +from opencode_ai import AsyncOpencode + +client = AsyncOpencode() + +stream = await client.event.list() +async for events in stream: + print(events) +``` + ## Using types Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: diff --git a/src/opencode_ai/_client.py b/src/opencode_ai/_client.py index 0271192..c97685d 100644 --- a/src/opencode_ai/_client.py +++ b/src/opencode_ai/_client.py @@ -92,6 +92,8 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self._default_stream_cls = Stream + self.event = event.EventResource(self) self.app = app.AppResource(self) self.file = file.FileResource(self) @@ -247,6 +249,8 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self._default_stream_cls = AsyncStream + self.event = event.AsyncEventResource(self) self.app = app.AsyncAppResource(self) self.file = file.AsyncFileResource(self) diff --git a/src/opencode_ai/resources/event.py b/src/opencode_ai/resources/event.py index 2025eb3..3e9baa3 100644 --- a/src/opencode_ai/resources/event.py +++ b/src/opencode_ai/resources/event.py @@ -15,6 +15,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._streaming import Stream, AsyncStream from .._base_client import make_request_options from ..types.event_list_response import EventListResponse @@ -50,17 +51,16 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> EventListResponse: + ) -> Stream[EventListResponse]: """Get events""" - return cast( - EventListResponse, - self._get( - "/event", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=cast(Any, EventListResponse), # Union types cannot be passed in as arguments in the type system + return self._get( + "/event", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), + cast_to=cast(Any, EventListResponse), # Union types cannot be passed in as arguments in the type system + stream=True, + stream_cls=Stream[EventListResponse], ) @@ -93,17 +93,16 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> EventListResponse: + ) -> AsyncStream[EventListResponse]: """Get events""" - return cast( - EventListResponse, - await self._get( - "/event", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=cast(Any, EventListResponse), # Union types cannot be passed in as arguments in the type system + return await self._get( + "/event", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), + cast_to=cast(Any, EventListResponse), # Union types cannot be passed in as arguments in the type system + stream=True, + stream_cls=AsyncStream[EventListResponse], ) diff --git a/tests/api_resources/test_event.py b/tests/api_resources/test_event.py index 21c167c..ec909e9 100644 --- a/tests/api_resources/test_event.py +++ b/tests/api_resources/test_event.py @@ -8,8 +8,6 @@ import pytest from opencode_ai import Opencode, AsyncOpencode -from tests.utils import assert_matches_type -from opencode_ai.types import EventListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -20,18 +18,17 @@ class TestEvent: @pytest.mark.skip() @parametrize def test_method_list(self, client: Opencode) -> None: - event = client.event.list() - assert_matches_type(EventListResponse, event, path=["response"]) + event_stream = client.event.list() + event_stream.response.close() @pytest.mark.skip() @parametrize def test_raw_response_list(self, client: Opencode) -> None: response = client.event.with_raw_response.list() - assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - event = response.parse() - assert_matches_type(EventListResponse, event, path=["response"]) + stream = response.parse() + stream.close() @pytest.mark.skip() @parametrize @@ -40,8 +37,8 @@ def test_streaming_response_list(self, client: Opencode) -> None: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - event = response.parse() - assert_matches_type(EventListResponse, event, path=["response"]) + stream = response.parse() + stream.close() assert cast(Any, response.is_closed) is True @@ -54,18 +51,17 @@ class TestAsyncEvent: @pytest.mark.skip() @parametrize async def test_method_list(self, async_client: AsyncOpencode) -> None: - event = await async_client.event.list() - assert_matches_type(EventListResponse, event, path=["response"]) + event_stream = await async_client.event.list() + await event_stream.response.aclose() @pytest.mark.skip() @parametrize async def test_raw_response_list(self, async_client: AsyncOpencode) -> None: response = await async_client.event.with_raw_response.list() - assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - event = await response.parse() - assert_matches_type(EventListResponse, event, path=["response"]) + stream = await response.parse() + await stream.close() @pytest.mark.skip() @parametrize @@ -74,7 +70,7 @@ async def test_streaming_response_list(self, async_client: AsyncOpencode) -> Non assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - event = await response.parse() - assert_matches_type(EventListResponse, event, path=["response"]) + stream = await response.parse() + await stream.close() assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 0e900dd..0cf7c41 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -24,6 +24,7 @@ from opencode_ai import Opencode, AsyncOpencode, APIResponseValidationError from opencode_ai._types import Omit from opencode_ai._models import BaseModel, FinalRequestOptions +from opencode_ai._streaming import Stream, AsyncStream from opencode_ai._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError from opencode_ai._base_client import ( DEFAULT_TIMEOUT, @@ -624,6 +625,17 @@ def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): Opencode(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None)) + @pytest.mark.respx(base_url=base_url) + def test_default_stream_cls(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + stream = self.client.post("/foo", cast_to=Model, stream=True, stream_cls=Stream[Model]) + assert isinstance(stream, Stream) + stream.response.close() + @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): @@ -1390,6 +1402,18 @@ async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): AsyncOpencode(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None)) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_default_stream_cls(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + stream = await self.client.post("/foo", cast_to=Model, stream=True, stream_cls=AsyncStream[Model]) + assert isinstance(stream, AsyncStream) + await stream.response.aclose() + @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: From e1cb382c5391eb135a31ad98c7301c061191c563 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:17:12 +0000 Subject: [PATCH 2/3] feat(api): update via SDK Studio --- .stats.yml | 4 +- src/opencode_ai/types/app.py | 2 - src/opencode_ai/types/config.py | 2 + src/opencode_ai/types/event_list_response.py | 82 +++++++++++++------- src/opencode_ai/types/mcp_local.py | 3 + src/opencode_ai/types/mcp_remote.py | 4 + src/opencode_ai/types/model.py | 2 + 7 files changed, 67 insertions(+), 32 deletions(-) diff --git a/.stats.yml b/.stats.yml index ef8559f..3857fb8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-b4a3f35e4a44e5a5034508ced15d7b44c1924000062e0f5293797413d26ee412.yml -openapi_spec_hash: f17b1091020f90126e6cefc2d38ff85f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-384a94f70b48c84af9eddcac72bbe12952c3ae3bd7fededfa1c63b203d12d828.yml +openapi_spec_hash: e47ad28d646736d5d79d2dd1086d517d config_hash: e2d21e779cfc4e26a99b9e4e75de3f50 diff --git a/src/opencode_ai/types/app.py b/src/opencode_ai/types/app.py index e45419f..de3df28 100644 --- a/src/opencode_ai/types/app.py +++ b/src/opencode_ai/types/app.py @@ -30,8 +30,6 @@ class App(BaseModel): path: Path - project: str - time: Time user: str diff --git a/src/opencode_ai/types/config.py b/src/opencode_ai/types/config.py index d0b4417..8159f70 100644 --- a/src/opencode_ai/types/config.py +++ b/src/opencode_ai/types/config.py @@ -81,6 +81,8 @@ class ProviderModels(BaseModel): reasoning: Optional[bool] = None + release_date: Optional[str] = None + temperature: Optional[bool] = None tool_call: Optional[bool] = None diff --git a/src/opencode_ai/types/event_list_response.py b/src/opencode_ai/types/event_list_response.py index 346430b..79e2e64 100644 --- a/src/opencode_ai/types/event_list_response.py +++ b/src/opencode_ai/types/event_list_response.py @@ -15,15 +15,17 @@ __all__ = [ "EventListResponse", - "EventStorageWrite", - "EventStorageWriteProperties", - "EventInstallationUpdated", - "EventInstallationUpdatedProperties", "EventLspClientDiagnostics", "EventLspClientDiagnosticsProperties", "EventPermissionUpdated", "EventPermissionUpdatedProperties", "EventPermissionUpdatedPropertiesTime", + "EventFileEdited", + "EventFileEditedProperties", + "EventStorageWrite", + "EventStorageWriteProperties", + "EventInstallationUpdated", + "EventInstallationUpdatedProperties", "EventMessageUpdated", "EventMessageUpdatedProperties", "EventMessagePartUpdated", @@ -32,6 +34,8 @@ "EventSessionUpdatedProperties", "EventSessionDeleted", "EventSessionDeletedProperties", + "EventSessionIdle", + "EventSessionIdleProperties", "EventSessionError", "EventSessionErrorProperties", "EventSessionErrorPropertiesError", @@ -39,28 +43,6 @@ ] -class EventStorageWriteProperties(BaseModel): - key: str - - content: Optional[object] = None - - -class EventStorageWrite(BaseModel): - properties: EventStorageWriteProperties - - type: Literal["storage.write"] - - -class EventInstallationUpdatedProperties(BaseModel): - version: str - - -class EventInstallationUpdated(BaseModel): - properties: EventInstallationUpdatedProperties - - type: Literal["installation.updated"] - - class EventLspClientDiagnosticsProperties(BaseModel): path: str @@ -95,6 +77,38 @@ class EventPermissionUpdated(BaseModel): type: Literal["permission.updated"] +class EventFileEditedProperties(BaseModel): + file: str + + +class EventFileEdited(BaseModel): + properties: EventFileEditedProperties + + type: Literal["file.edited"] + + +class EventStorageWriteProperties(BaseModel): + key: str + + content: Optional[object] = None + + +class EventStorageWrite(BaseModel): + properties: EventStorageWriteProperties + + type: Literal["storage.write"] + + +class EventInstallationUpdatedProperties(BaseModel): + version: str + + +class EventInstallationUpdated(BaseModel): + properties: EventInstallationUpdatedProperties + + type: Literal["installation.updated"] + + class EventMessageUpdatedProperties(BaseModel): info: Message @@ -139,6 +153,16 @@ class EventSessionDeleted(BaseModel): type: Literal["session.deleted"] +class EventSessionIdleProperties(BaseModel): + session_id: str = FieldInfo(alias="sessionID") + + +class EventSessionIdle(BaseModel): + properties: EventSessionIdleProperties + + type: Literal["session.idle"] + + class EventSessionErrorPropertiesErrorMessageOutputLengthError(BaseModel): data: object @@ -163,14 +187,16 @@ class EventSessionError(BaseModel): EventListResponse: TypeAlias = Annotated[ Union[ - EventStorageWrite, - EventInstallationUpdated, EventLspClientDiagnostics, EventPermissionUpdated, + EventFileEdited, + EventStorageWrite, + EventInstallationUpdated, EventMessageUpdated, EventMessagePartUpdated, EventSessionUpdated, EventSessionDeleted, + EventSessionIdle, EventSessionError, ], PropertyInfo(discriminator="type"), diff --git a/src/opencode_ai/types/mcp_local.py b/src/opencode_ai/types/mcp_local.py index e62dce6..5296819 100644 --- a/src/opencode_ai/types/mcp_local.py +++ b/src/opencode_ai/types/mcp_local.py @@ -15,5 +15,8 @@ class McpLocal(BaseModel): type: Literal["local"] """Type of MCP server connection""" + enabled: Optional[bool] = None + """Enable or disable the MCP server on startup""" + environment: Optional[Dict[str, str]] = None """Environment variables to set when running the MCP server""" diff --git a/src/opencode_ai/types/mcp_remote.py b/src/opencode_ai/types/mcp_remote.py index 363be49..83a02a2 100644 --- a/src/opencode_ai/types/mcp_remote.py +++ b/src/opencode_ai/types/mcp_remote.py @@ -1,5 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional from typing_extensions import Literal from .._models import BaseModel @@ -13,3 +14,6 @@ class McpRemote(BaseModel): url: str """URL of the remote MCP server""" + + enabled: Optional[bool] = None + """Enable or disable the MCP server on startup""" diff --git a/src/opencode_ai/types/model.py b/src/opencode_ai/types/model.py index 694f2cc..32fc15a 100644 --- a/src/opencode_ai/types/model.py +++ b/src/opencode_ai/types/model.py @@ -38,6 +38,8 @@ class Model(BaseModel): reasoning: bool + release_date: str + temperature: bool tool_call: bool From a99560d343ce8ae217705f1e1bb7888293b9e079 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:17:27 +0000 Subject: [PATCH 3/3] release: 0.1.0-alpha.6 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- src/opencode_ai/_version.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e8285b7..4f9005e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.5" + ".": "0.1.0-alpha.6" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aee826..80a92df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.1.0-alpha.6 (2025-06-30) + +Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-python/compare/v0.1.0-alpha.5...v0.1.0-alpha.6) + +### Features + +* **api:** update via SDK Studio ([e1cb382](https://github.com/sst/opencode-sdk-python/commit/e1cb382c5391eb135a31ad98c7301c061191c563)) +* **api:** update via SDK Studio ([0985851](https://github.com/sst/opencode-sdk-python/commit/09858518e9312ca72238efd596cc0313927c26e3)) + ## 0.1.0-alpha.5 (2025-06-30) Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencode-sdk-python/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) diff --git a/pyproject.toml b/pyproject.toml index 8be2aab..6c7f406 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "opencode-ai" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" description = "The official Python library for the opencode API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/opencode_ai/_version.py b/src/opencode_ai/_version.py index f8d9288..3df8319 100644 --- a/src/opencode_ai/_version.py +++ b/src/opencode_ai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "opencode_ai" -__version__ = "0.1.0-alpha.5" # x-release-please-version +__version__ = "0.1.0-alpha.6" # x-release-please-version