From 325f3b58bd6bf65d89f0bd6760c0fa9449daf826 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Tue, 19 May 2026 13:52:33 -0700 Subject: [PATCH 1/4] Add discovery SDK surface --- openapi/openapi.yml | 175 ++++++++++++++++ pyproject.toml | 2 +- src/roe/_generated/api/discovery/__init__.py | 1 + .../discovery_agent_engine_types_list.py | 166 +++++++++++++++ .../discovery_supported_models_list.py | 196 ++++++++++++++++++ src/roe/_generated/models/__init__.py | 8 + .../models/agent_engine_type_list.py | 115 ++++++++++ .../_generated/models/supported_llm_model.py | 156 ++++++++++++++ .../models/supported_llm_model_list.py | 111 ++++++++++ .../_generated/models/temporal_workflow.py | 152 ++++++++++++++ src/roe/api/__init__.py | 3 +- src/roe/api/discovery.py | 44 ++++ src/roe/client.py | 7 + tests/unit/test_discovery.py | 65 ++++++ uv.lock | 2 +- 15 files changed, 1200 insertions(+), 3 deletions(-) create mode 100644 src/roe/_generated/api/discovery/__init__.py create mode 100644 src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py create mode 100644 src/roe/_generated/api/discovery/discovery_supported_models_list.py create mode 100644 src/roe/_generated/models/agent_engine_type_list.py create mode 100644 src/roe/_generated/models/supported_llm_model.py create mode 100644 src/roe/_generated/models/supported_llm_model_list.py create mode 100644 src/roe/_generated/models/temporal_workflow.py create mode 100644 src/roe/api/discovery.py create mode 100644 tests/unit/test_discovery.py diff --git a/openapi/openapi.yml b/openapi/openapi.yml index d30f8a1..a42972d 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -1621,6 +1621,48 @@ paths: value: error: Internal server error description: Internal server error + /v1/discovery/agent-engine-types/: + get: + operationId: discovery_agent_engine_types_list + description: Returns the production engine_class_id values accepted by agent + creation APIs, plus human-readable metadata and input schemas. Use this before + create_agent or create_agent_version when choosing an engine and constructing + engine_config. + summary: List supported agent engine types + tags: + - discovery + - sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AgentEngineTypeList' + description: '' + /v1/discovery/supported-models/: + get: + operationId: discovery_supported_models_list + description: Returns non-deprecated text-capable model IDs accepted in engine_config.model, + with capability and context metadata. Use this before create_agent or create_agent_version + when choosing a model. The list is tenant-agnostic and excludes customer-specific + or deployment-specific providers. + summary: List supported model IDs + parameters: + - in: query + name: capability + schema: + type: string + description: 'Optional capability filter: text, image, audio, or video' + tags: + - discovery + - sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SupportedLLMModelList' + description: '' /v1/policies/: get: operationId: policies_list @@ -1964,6 +2006,29 @@ components: - data_type - key - value + AgentEngineTypeList: + type: object + description: Serializer for public agent engine type discovery. + properties: + engine_types: + type: array + items: + type: string + description: Valid agent engine_class_id values accepted by create-agent + APIs + total_count: + type: integer + description: Number of engine types returned + engines: + type: array + items: + $ref: '#/components/schemas/TemporalWorkflow' + description: Production agent engine metadata, including descriptions, input + schemas, and default engine_config values + required: + - engine_types + - engines + - total_count AgentExecutionRequestRequest: type: object description: Serializer for agent execution requests with dynamic input fields. @@ -2734,6 +2799,116 @@ components: - display_name - email - id + SupportedLLMModel: + type: object + description: Serializer for tenant-agnostic supported LLM metadata. + properties: + id: + type: string + description: Model identifier accepted in engine_config.model + providers: + type: array + items: + type: string + description: Non-customer-specific providers registered for this model + capabilities: + type: array + items: + type: string + description: Input capabilities supported by this model + context_window: + type: integer + description: Largest context window across global providers + max_output_tokens: + type: integer + description: Largest max output token limit across global providers + supports_system_message: + type: boolean + supports_temperature: + type: boolean + supports_reasoning_effort: + type: boolean + supports_json_output: + type: boolean + supports_json_schema: + type: boolean + required: + - capabilities + - context_window + - id + - max_output_tokens + - providers + - supports_json_output + - supports_json_schema + - supports_reasoning_effort + - supports_system_message + - supports_temperature + SupportedLLMModelList: + type: object + description: Serializer for non-deprecated LLM discovery. + properties: + models: + type: array + items: + $ref: '#/components/schemas/SupportedLLMModel' + total_count: + type: integer + tenant_scope: + type: string + description: Scope of the model list; this endpoint returns all-tenants + models + required: + - models + - tenant_scope + - total_count + TemporalWorkflow: + type: object + description: |- + Serializer for Temporal Workflow engine information. + + Matches the shape returned by fetch_all_temporal_workflows() in + agents/services/temporal_service.py. + properties: + type: + type: string + description: Engine type discriminator, always 'temporal_workflow' + class_id: + type: string + description: Unique class identifier for this workflow + workflow_type: + type: string + description: The temporal workflow type identifier + display_name: + type: string + description: Human-readable name of the workflow + description: + type: string + description: Detailed description of what the workflow does + summary: + type: string + description: Brief summary of the workflow's capabilities + input_schema: + description: Pydantic JSON Schema describing the workflow's input model + default_values: + description: Default values for the workflow input fields + category: + type: string + description: The workflow category + form_type: + type: string + description: The form type that determines which frontend create form to + render + required: + - category + - class_id + - default_values + - description + - display_name + - form_type + - input_schema + - summary + - type + - workflow_type UpdatePolicy: type: object description: Serializer for updating policy metadata (name, description) diff --git a/pyproject.toml b/pyproject.toml index ecc87c5..f105069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "roe-ai" -version = "1.0.801" +version = "1.0.802" authors = [ { name = "Roe AI", email = "founders@roe-ai.com" }, ] diff --git a/src/roe/_generated/api/discovery/__init__.py b/src/roe/_generated/api/discovery/__init__.py new file mode 100644 index 0000000..c9921b5 --- /dev/null +++ b/src/roe/_generated/api/discovery/__init__.py @@ -0,0 +1 @@ +""" Contains endpoint functions for accessing the API """ diff --git a/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py new file mode 100644 index 0000000..4a07ff8 --- /dev/null +++ b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py @@ -0,0 +1,166 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.agent_engine_type_list import AgentEngineTypeList +from typing import cast + + + +def _get_kwargs( + +) -> dict[str, Any]: + + + + + + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v1/discovery/agent-engine-types/", + } + + + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> AgentEngineTypeList | None: + if response.status_code == 200: + response_200 = AgentEngineTypeList.from_dict(response.json()) + + + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[AgentEngineTypeList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + +) -> Response[AgentEngineTypeList]: + """ List supported agent engine types + + Returns the production engine_class_id values accepted by agent creation APIs, plus human-readable + metadata and input schemas. Use this before create_agent or create_agent_version when choosing an + engine and constructing engine_config. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AgentEngineTypeList] + """ + + + kwargs = _get_kwargs( + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + +) -> AgentEngineTypeList | None: + """ List supported agent engine types + + Returns the production engine_class_id values accepted by agent creation APIs, plus human-readable + metadata and input schemas. Use this before create_agent or create_agent_version when choosing an + engine and constructing engine_config. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AgentEngineTypeList + """ + + + return sync_detailed( + client=client, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + +) -> Response[AgentEngineTypeList]: + """ List supported agent engine types + + Returns the production engine_class_id values accepted by agent creation APIs, plus human-readable + metadata and input schemas. Use this before create_agent or create_agent_version when choosing an + engine and constructing engine_config. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AgentEngineTypeList] + """ + + + kwargs = _get_kwargs( + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + +) -> AgentEngineTypeList | None: + """ List supported agent engine types + + Returns the production engine_class_id values accepted by agent creation APIs, plus human-readable + metadata and input schemas. Use this before create_agent or create_agent_version when choosing an + engine and constructing engine_config. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AgentEngineTypeList + """ + + + return (await asyncio_detailed( + client=client, + + )).parsed diff --git a/src/roe/_generated/api/discovery/discovery_supported_models_list.py b/src/roe/_generated/api/discovery/discovery_supported_models_list.py new file mode 100644 index 0000000..15296db --- /dev/null +++ b/src/roe/_generated/api/discovery/discovery_supported_models_list.py @@ -0,0 +1,196 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.supported_llm_model_list import SupportedLLMModelList +from ...types import UNSET, Unset +from typing import cast + + + +def _get_kwargs( + *, + capability: str | Unset = UNSET, + +) -> dict[str, Any]: + + + + + params: dict[str, Any] = {} + + params["capability"] = capability + + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v1/discovery/supported-models/", + "params": params, + } + + + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> SupportedLLMModelList | None: + if response.status_code == 200: + response_200 = SupportedLLMModelList.from_dict(response.json()) + + + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[SupportedLLMModelList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + capability: str | Unset = UNSET, + +) -> Response[SupportedLLMModelList]: + """ List supported model IDs + + Returns non-deprecated text-capable model IDs accepted in engine_config.model, with capability and + context metadata. Use this before create_agent or create_agent_version when choosing a model. The + list is tenant-agnostic and excludes customer-specific or deployment-specific providers. + + Args: + capability (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[SupportedLLMModelList] + """ + + + kwargs = _get_kwargs( + capability=capability, + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + capability: str | Unset = UNSET, + +) -> SupportedLLMModelList | None: + """ List supported model IDs + + Returns non-deprecated text-capable model IDs accepted in engine_config.model, with capability and + context metadata. Use this before create_agent or create_agent_version when choosing a model. The + list is tenant-agnostic and excludes customer-specific or deployment-specific providers. + + Args: + capability (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + SupportedLLMModelList + """ + + + return sync_detailed( + client=client, +capability=capability, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + capability: str | Unset = UNSET, + +) -> Response[SupportedLLMModelList]: + """ List supported model IDs + + Returns non-deprecated text-capable model IDs accepted in engine_config.model, with capability and + context metadata. Use this before create_agent or create_agent_version when choosing a model. The + list is tenant-agnostic and excludes customer-specific or deployment-specific providers. + + Args: + capability (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[SupportedLLMModelList] + """ + + + kwargs = _get_kwargs( + capability=capability, + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + capability: str | Unset = UNSET, + +) -> SupportedLLMModelList | None: + """ List supported model IDs + + Returns non-deprecated text-capable model IDs accepted in engine_config.model, with capability and + context metadata. Use this before create_agent or create_agent_version when choosing a model. The + list is tenant-agnostic and excludes customer-specific or deployment-specific providers. + + Args: + capability (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + SupportedLLMModelList + """ + + + return (await asyncio_detailed( + client=client, +capability=capability, + + )).parsed diff --git a/src/roe/_generated/models/__init__.py b/src/roe/_generated/models/__init__.py index 7572c7c..75e1f44 100644 --- a/src/roe/_generated/models/__init__.py +++ b/src/roe/_generated/models/__init__.py @@ -1,6 +1,7 @@ """ Contains all the data models used in inputs/outputs """ from .agent_datum import AgentDatum +from .agent_engine_type_list import AgentEngineTypeList from .agent_execution_request_request import AgentExecutionRequestRequest from .agent_input_definition import AgentInputDefinition from .agent_job_delete_data_response import AgentJobDeleteDataResponse @@ -32,12 +33,16 @@ from .policy import Policy from .policy_version import PolicyVersion from .policy_version_created_by import PolicyVersionCreatedBy +from .supported_llm_model import SupportedLLMModel +from .supported_llm_model_list import SupportedLLMModelList +from .temporal_workflow import TemporalWorkflow from .update_policy import UpdatePolicy from .update_policy_request import UpdatePolicyRequest from .user_info import UserInfo __all__ = ( "AgentDatum", + "AgentEngineTypeList", "AgentExecutionRequestRequest", "AgentInputDefinition", "AgentJobDeleteDataResponse", @@ -69,6 +74,9 @@ "Policy", "PolicyVersion", "PolicyVersionCreatedBy", + "SupportedLLMModel", + "SupportedLLMModelList", + "TemporalWorkflow", "UpdatePolicy", "UpdatePolicyRequest", "UserInfo", diff --git a/src/roe/_generated/models/agent_engine_type_list.py b/src/roe/_generated/models/agent_engine_type_list.py new file mode 100644 index 0000000..21597ad --- /dev/null +++ b/src/roe/_generated/models/agent_engine_type_list.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + +if TYPE_CHECKING: + from ..models.temporal_workflow import TemporalWorkflow + + + + + +T = TypeVar("T", bound="AgentEngineTypeList") + + + +@_attrs_define +class AgentEngineTypeList: + """ Serializer for public agent engine type discovery. + + Attributes: + engine_types (list[str]): Valid agent engine_class_id values accepted by create-agent APIs + total_count (int): Number of engine types returned + engines (list[TemporalWorkflow]): Production agent engine metadata, including descriptions, input schemas, and + default engine_config values + """ + + engine_types: list[str] + total_count: int + engines: list[TemporalWorkflow] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.temporal_workflow import TemporalWorkflow + engine_types = self.engine_types + + + + total_count = self.total_count + + engines = [] + for engines_item_data in self.engines: + engines_item = engines_item_data.to_dict() + engines.append(engines_item) + + + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "engine_types": engine_types, + "total_count": total_count, + "engines": engines, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.temporal_workflow import TemporalWorkflow + d = dict(src_dict) + engine_types = cast(list[str], d.pop("engine_types")) + + + total_count = d.pop("total_count") + + engines = [] + _engines = d.pop("engines") + for engines_item_data in (_engines): + engines_item = TemporalWorkflow.from_dict(engines_item_data) + + + + engines.append(engines_item) + + + agent_engine_type_list = cls( + engine_types=engine_types, + total_count=total_count, + engines=engines, + ) + + + agent_engine_type_list.additional_properties = d + return agent_engine_type_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/supported_llm_model.py b/src/roe/_generated/models/supported_llm_model.py new file mode 100644 index 0000000..f6c08bb --- /dev/null +++ b/src/roe/_generated/models/supported_llm_model.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + + + + + + +T = TypeVar("T", bound="SupportedLLMModel") + + + +@_attrs_define +class SupportedLLMModel: + """ Serializer for tenant-agnostic supported LLM metadata. + + Attributes: + id (str): Model identifier accepted in engine_config.model + providers (list[str]): Non-customer-specific providers registered for this model + capabilities (list[str]): Input capabilities supported by this model + context_window (int): Largest context window across global providers + max_output_tokens (int): Largest max output token limit across global providers + supports_system_message (bool): + supports_temperature (bool): + supports_reasoning_effort (bool): + supports_json_output (bool): + supports_json_schema (bool): + """ + + id: str + providers: list[str] + capabilities: list[str] + context_window: int + max_output_tokens: int + supports_system_message: bool + supports_temperature: bool + supports_reasoning_effort: bool + supports_json_output: bool + supports_json_schema: bool + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + id = self.id + + providers = self.providers + + + + capabilities = self.capabilities + + + + context_window = self.context_window + + max_output_tokens = self.max_output_tokens + + supports_system_message = self.supports_system_message + + supports_temperature = self.supports_temperature + + supports_reasoning_effort = self.supports_reasoning_effort + + supports_json_output = self.supports_json_output + + supports_json_schema = self.supports_json_schema + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "id": id, + "providers": providers, + "capabilities": capabilities, + "context_window": context_window, + "max_output_tokens": max_output_tokens, + "supports_system_message": supports_system_message, + "supports_temperature": supports_temperature, + "supports_reasoning_effort": supports_reasoning_effort, + "supports_json_output": supports_json_output, + "supports_json_schema": supports_json_schema, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + id = d.pop("id") + + providers = cast(list[str], d.pop("providers")) + + + capabilities = cast(list[str], d.pop("capabilities")) + + + context_window = d.pop("context_window") + + max_output_tokens = d.pop("max_output_tokens") + + supports_system_message = d.pop("supports_system_message") + + supports_temperature = d.pop("supports_temperature") + + supports_reasoning_effort = d.pop("supports_reasoning_effort") + + supports_json_output = d.pop("supports_json_output") + + supports_json_schema = d.pop("supports_json_schema") + + supported_llm_model = cls( + id=id, + providers=providers, + capabilities=capabilities, + context_window=context_window, + max_output_tokens=max_output_tokens, + supports_system_message=supports_system_message, + supports_temperature=supports_temperature, + supports_reasoning_effort=supports_reasoning_effort, + supports_json_output=supports_json_output, + supports_json_schema=supports_json_schema, + ) + + + supported_llm_model.additional_properties = d + return supported_llm_model + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/supported_llm_model_list.py b/src/roe/_generated/models/supported_llm_model_list.py new file mode 100644 index 0000000..65c2b88 --- /dev/null +++ b/src/roe/_generated/models/supported_llm_model_list.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + +if TYPE_CHECKING: + from ..models.supported_llm_model import SupportedLLMModel + + + + + +T = TypeVar("T", bound="SupportedLLMModelList") + + + +@_attrs_define +class SupportedLLMModelList: + """ Serializer for non-deprecated LLM discovery. + + Attributes: + models (list[SupportedLLMModel]): + total_count (int): + tenant_scope (str): Scope of the model list; this endpoint returns all-tenants models + """ + + models: list[SupportedLLMModel] + total_count: int + tenant_scope: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.supported_llm_model import SupportedLLMModel + models = [] + for models_item_data in self.models: + models_item = models_item_data.to_dict() + models.append(models_item) + + + + total_count = self.total_count + + tenant_scope = self.tenant_scope + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "models": models, + "total_count": total_count, + "tenant_scope": tenant_scope, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.supported_llm_model import SupportedLLMModel + d = dict(src_dict) + models = [] + _models = d.pop("models") + for models_item_data in (_models): + models_item = SupportedLLMModel.from_dict(models_item_data) + + + + models.append(models_item) + + + total_count = d.pop("total_count") + + tenant_scope = d.pop("tenant_scope") + + supported_llm_model_list = cls( + models=models, + total_count=total_count, + tenant_scope=tenant_scope, + ) + + + supported_llm_model_list.additional_properties = d + return supported_llm_model_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/temporal_workflow.py b/src/roe/_generated/models/temporal_workflow.py new file mode 100644 index 0000000..ccb212c --- /dev/null +++ b/src/roe/_generated/models/temporal_workflow.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + + + + + + + +T = TypeVar("T", bound="TemporalWorkflow") + + + +@_attrs_define +class TemporalWorkflow: + """ Serializer for Temporal Workflow engine information. + + Matches the shape returned by fetch_all_temporal_workflows() in + agents/services/temporal_service.py. + + Attributes: + type_ (str): Engine type discriminator, always 'temporal_workflow' + class_id (str): Unique class identifier for this workflow + workflow_type (str): The temporal workflow type identifier + display_name (str): Human-readable name of the workflow + description (str): Detailed description of what the workflow does + summary (str): Brief summary of the workflow's capabilities + input_schema (Any): Pydantic JSON Schema describing the workflow's input model + default_values (Any): Default values for the workflow input fields + category (str): The workflow category + form_type (str): The form type that determines which frontend create form to render + """ + + type_: str + class_id: str + workflow_type: str + display_name: str + description: str + summary: str + input_schema: Any + default_values: Any + category: str + form_type: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + class_id = self.class_id + + workflow_type = self.workflow_type + + display_name = self.display_name + + description = self.description + + summary = self.summary + + input_schema = self.input_schema + + default_values = self.default_values + + category = self.category + + form_type = self.form_type + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "type": type_, + "class_id": class_id, + "workflow_type": workflow_type, + "display_name": display_name, + "description": description, + "summary": summary, + "input_schema": input_schema, + "default_values": default_values, + "category": category, + "form_type": form_type, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + type_ = d.pop("type") + + class_id = d.pop("class_id") + + workflow_type = d.pop("workflow_type") + + display_name = d.pop("display_name") + + description = d.pop("description") + + summary = d.pop("summary") + + input_schema = d.pop("input_schema") + + default_values = d.pop("default_values") + + category = d.pop("category") + + form_type = d.pop("form_type") + + temporal_workflow = cls( + type_=type_, + class_id=class_id, + workflow_type=workflow_type, + display_name=display_name, + description=description, + summary=summary, + input_schema=input_schema, + default_values=default_values, + category=category, + form_type=form_type, + ) + + + temporal_workflow.additional_properties = d + return temporal_workflow + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/api/__init__.py b/src/roe/api/__init__.py index 39f5d6e..e0ebc4e 100644 --- a/src/roe/api/__init__.py +++ b/src/roe/api/__init__.py @@ -1,6 +1,7 @@ """API modules for the Roe AI SDK.""" from roe.api.agents import AgentsAPI +from roe.api.discovery import DiscoveryAPI from roe.api.policies import PoliciesAPI -__all__ = ["AgentsAPI", "PoliciesAPI"] +__all__ = ["AgentsAPI", "DiscoveryAPI", "PoliciesAPI"] diff --git a/src/roe/api/discovery.py b/src/roe/api/discovery.py new file mode 100644 index 0000000..d3577ef --- /dev/null +++ b/src/roe/api/discovery.py @@ -0,0 +1,44 @@ +"""Discovery API wrappers for engine and model selection.""" + +from __future__ import annotations + +from roe._generated.api.discovery import ( + discovery_agent_engine_types_list, + discovery_supported_models_list, +) +from roe._generated.client import AuthenticatedClient +from roe._generated.models.agent_engine_type_list import AgentEngineTypeList +from roe._generated.models.supported_llm_model_list import SupportedLLMModelList +from roe._generated.types import UNSET +from roe.config import RoeConfig +from roe.exceptions import RoeAPIException, translate_response + + +class DiscoveryAPI: + """API for discovering valid agent engine types and model IDs.""" + + def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient): + self.config = config + self._raw = raw_client + + def list_agent_engine_types(self) -> AgentEngineTypeList: + """Return production engine_class_id values accepted by agent creation.""" + response = discovery_agent_engine_types_list.sync_detailed(client=self._raw) + translate_response(response) + if response.parsed is None: + raise RoeAPIException("agent engine discovery returned an empty response") + return response.parsed + + def list_supported_models( + self, + capability: str | None = None, + ) -> SupportedLLMModelList: + """Return non-deprecated model IDs accepted in engine_config.model.""" + response = discovery_supported_models_list.sync_detailed( + client=self._raw, + capability=capability if capability is not None else UNSET, + ) + translate_response(response) + if response.parsed is None: + raise RoeAPIException("model discovery returned an empty response") + return response.parsed diff --git a/src/roe/client.py b/src/roe/client.py index 66aaef5..7e1dfa1 100644 --- a/src/roe/client.py +++ b/src/roe/client.py @@ -4,6 +4,7 @@ from roe._generated.client import AuthenticatedClient as RawClient from roe.api.agents import AgentsAPI +from roe.api.discovery import DiscoveryAPI from roe.api.policies import PoliciesAPI from roe.api.users import UsersAPI from roe.auth import RoeAuth @@ -92,6 +93,7 @@ def __init__( # Create API instances. All APIs delegate to the generated raw client. self._agents = AgentsAPI(self.config, self._raw) + self._discovery = DiscoveryAPI(self.config, self._raw) self._policies = PoliciesAPI(self.config, self._raw) self._users = UsersAPI(self.config, self._raw) @@ -122,6 +124,11 @@ def agents(self) -> AgentsAPI: """ return self._agents + @property + def discovery(self) -> DiscoveryAPI: + """Access discovery APIs for valid engine types and model IDs.""" + return self._discovery + @property def policies(self) -> PoliciesAPI: """Access the policies API for managing policies used by agentic workflows. diff --git a/tests/unit/test_discovery.py b/tests/unit/test_discovery.py new file mode 100644 index 0000000..b740ad1 --- /dev/null +++ b/tests/unit/test_discovery.py @@ -0,0 +1,65 @@ +"""Unit tests for ``roe.api.discovery.DiscoveryAPI``.""" + +from __future__ import annotations + +from http import HTTPStatus +from unittest.mock import MagicMock, patch + +import pytest + +from roe._generated.models.agent_engine_type_list import AgentEngineTypeList +from roe._generated.models.supported_llm_model_list import SupportedLLMModelList +from roe._generated.types import Response +from roe.api.discovery import DiscoveryAPI +from roe.exceptions import BadRequestError + + +def _response(parsed, status: int = 200) -> Response: + return Response( + status_code=HTTPStatus(status), + content=b"{}", + headers={}, + parsed=parsed, + ) + + +def test_list_agent_engine_types_calls_generated_endpoint(): + raw_client = MagicMock() + api = DiscoveryAPI(MagicMock(), raw_client) + payload = AgentEngineTypeList(engine_types=["ResearchEngine"], total_count=1, engines=[]) + + with patch( + "roe.api.discovery.discovery_agent_engine_types_list.sync_detailed", + return_value=_response(payload), + ) as mocked: + result = api.list_agent_engine_types() + + mocked.assert_called_once_with(client=raw_client) + assert result.engine_types == ["ResearchEngine"] + + +def test_list_supported_models_passes_capability_filter(): + raw_client = MagicMock() + api = DiscoveryAPI(MagicMock(), raw_client) + payload = SupportedLLMModelList(models=[], total_count=0, tenant_scope="all_tenants") + + with patch( + "roe.api.discovery.discovery_supported_models_list.sync_detailed", + return_value=_response(payload), + ) as mocked: + result = api.list_supported_models(capability="image") + + mocked.assert_called_once_with(client=raw_client, capability="image") + assert result.tenant_scope == "all_tenants" + + +def test_list_supported_models_translates_bad_request(): + raw_client = MagicMock() + api = DiscoveryAPI(MagicMock(), raw_client) + + with patch( + "roe.api.discovery.discovery_supported_models_list.sync_detailed", + return_value=_response(None, status=400), + ): + with pytest.raises(BadRequestError): + api.list_supported_models(capability="spreadsheet") diff --git a/uv.lock b/uv.lock index eda1dc1..c172a16 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "roe-ai" -version = "1.0.801" +version = "1.0.802" source = { editable = "." } dependencies = [ { name = "attrs" }, From 6cdb72e6bce66d168bc5e58e52de07665b63bdca Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 20 May 2026 10:14:55 -0700 Subject: [PATCH 2/4] Regen discovery SDK against latest roe-main PR 3232 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resync openapi.yml + generated client with the latest roe-main PR 3232 state (commits 1e84e190f rename + 4e3b70241 trim). Two contract changes land here: - `/v1/discovery/{agent-engine-types,supported-models}/` → `/v1/agents/{types,models}/` - `engines` items are now an open dict (six fields: class_id, display_name, description, summary, input_schema, default_values), not TemporalWorkflow The old `TemporalWorkflow.from_dict` popped `form_type`, which the new payload omits — `list_agent_engine_types()` against current backend would have raised `KeyError`. Adds a from_dict test against a realistic six-field engine response to catch this category of drift in CI. Co-Authored-By: Claude Opus 4.7 (1M context) --- openapi/openapi.yml | 102 ++++-------- .../discovery_agent_engine_types_list.py | 2 +- .../discovery_supported_models_list.py | 2 +- src/roe/_generated/models/__init__.py | 4 +- .../models/agent_engine_type_list.py | 14 +- .../agent_engine_type_list_engines_item.py | 65 ++++++++ .../_generated/models/temporal_workflow.py | 152 ------------------ tests/unit/test_discovery.py | 63 ++++++++ 8 files changed, 167 insertions(+), 237 deletions(-) create mode 100644 src/roe/_generated/models/agent_engine_type_list_engines_item.py delete mode 100644 src/roe/_generated/models/temporal_workflow.py diff --git a/openapi/openapi.yml b/openapi/openapi.yml index a42972d..a8963de 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -1209,6 +1209,31 @@ paths: format: uuid description: Organization ID. This is required for access control. It can be provided via query or request body depending on the endpoint. + /v1/agents/models/: + get: + operationId: discovery_supported_models_list + description: Returns non-deprecated text-capable model IDs accepted in engine_config.model, + with capability and context metadata. Use this before create_agent or create_agent_version + when choosing a model. The list is tenant-agnostic and excludes customer-specific + or deployment-specific providers. + summary: List supported model IDs + parameters: + - in: query + name: capability + schema: + type: string + description: 'Optional capability filter: image, audio, or video (text-capable + models are always included)' + tags: + - discovery + - sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SupportedLLMModelList' + description: '' /v1/agents/run/{agent_id}/: post: operationId: agents_run @@ -1621,7 +1646,7 @@ paths: value: error: Internal server error description: Internal server error - /v1/discovery/agent-engine-types/: + /v1/agents/types/: get: operationId: discovery_agent_engine_types_list description: Returns the production engine_class_id values accepted by agent @@ -1639,30 +1664,6 @@ paths: schema: $ref: '#/components/schemas/AgentEngineTypeList' description: '' - /v1/discovery/supported-models/: - get: - operationId: discovery_supported_models_list - description: Returns non-deprecated text-capable model IDs accepted in engine_config.model, - with capability and context metadata. Use this before create_agent or create_agent_version - when choosing a model. The list is tenant-agnostic and excludes customer-specific - or deployment-specific providers. - summary: List supported model IDs - parameters: - - in: query - name: capability - schema: - type: string - description: 'Optional capability filter: text, image, audio, or video' - tags: - - discovery - - sdk - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/SupportedLLMModelList' - description: '' /v1/policies/: get: operationId: policies_list @@ -2022,7 +2023,8 @@ components: engines: type: array items: - $ref: '#/components/schemas/TemporalWorkflow' + type: object + additionalProperties: {} description: Production agent engine metadata, including descriptions, input schemas, and default engine_config values required: @@ -2861,54 +2863,6 @@ components: - models - tenant_scope - total_count - TemporalWorkflow: - type: object - description: |- - Serializer for Temporal Workflow engine information. - - Matches the shape returned by fetch_all_temporal_workflows() in - agents/services/temporal_service.py. - properties: - type: - type: string - description: Engine type discriminator, always 'temporal_workflow' - class_id: - type: string - description: Unique class identifier for this workflow - workflow_type: - type: string - description: The temporal workflow type identifier - display_name: - type: string - description: Human-readable name of the workflow - description: - type: string - description: Detailed description of what the workflow does - summary: - type: string - description: Brief summary of the workflow's capabilities - input_schema: - description: Pydantic JSON Schema describing the workflow's input model - default_values: - description: Default values for the workflow input fields - category: - type: string - description: The workflow category - form_type: - type: string - description: The form type that determines which frontend create form to - render - required: - - category - - class_id - - default_values - - description - - display_name - - form_type - - input_schema - - summary - - type - - workflow_type UpdatePolicy: type: object description: Serializer for updating policy metadata (name, description) diff --git a/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py index 4a07ff8..df4e940 100644 --- a/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py +++ b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py @@ -24,7 +24,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/v1/discovery/agent-engine-types/", + "url": "/v1/agents/types/", } diff --git a/src/roe/_generated/api/discovery/discovery_supported_models_list.py b/src/roe/_generated/api/discovery/discovery_supported_models_list.py index 15296db..d06799e 100644 --- a/src/roe/_generated/api/discovery/discovery_supported_models_list.py +++ b/src/roe/_generated/api/discovery/discovery_supported_models_list.py @@ -33,7 +33,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/v1/discovery/supported-models/", + "url": "/v1/agents/models/", "params": params, } diff --git a/src/roe/_generated/models/__init__.py b/src/roe/_generated/models/__init__.py index 75e1f44..0cf80d3 100644 --- a/src/roe/_generated/models/__init__.py +++ b/src/roe/_generated/models/__init__.py @@ -2,6 +2,7 @@ from .agent_datum import AgentDatum from .agent_engine_type_list import AgentEngineTypeList +from .agent_engine_type_list_engines_item import AgentEngineTypeListEnginesItem from .agent_execution_request_request import AgentExecutionRequestRequest from .agent_input_definition import AgentInputDefinition from .agent_job_delete_data_response import AgentJobDeleteDataResponse @@ -35,7 +36,6 @@ from .policy_version_created_by import PolicyVersionCreatedBy from .supported_llm_model import SupportedLLMModel from .supported_llm_model_list import SupportedLLMModelList -from .temporal_workflow import TemporalWorkflow from .update_policy import UpdatePolicy from .update_policy_request import UpdatePolicyRequest from .user_info import UserInfo @@ -43,6 +43,7 @@ __all__ = ( "AgentDatum", "AgentEngineTypeList", + "AgentEngineTypeListEnginesItem", "AgentExecutionRequestRequest", "AgentInputDefinition", "AgentJobDeleteDataResponse", @@ -76,7 +77,6 @@ "PolicyVersionCreatedBy", "SupportedLLMModel", "SupportedLLMModelList", - "TemporalWorkflow", "UpdatePolicy", "UpdatePolicyRequest", "UserInfo", diff --git a/src/roe/_generated/models/agent_engine_type_list.py b/src/roe/_generated/models/agent_engine_type_list.py index 21597ad..0e07c91 100644 --- a/src/roe/_generated/models/agent_engine_type_list.py +++ b/src/roe/_generated/models/agent_engine_type_list.py @@ -11,7 +11,7 @@ from typing import cast if TYPE_CHECKING: - from ..models.temporal_workflow import TemporalWorkflow + from ..models.agent_engine_type_list_engines_item import AgentEngineTypeListEnginesItem @@ -28,13 +28,13 @@ class AgentEngineTypeList: Attributes: engine_types (list[str]): Valid agent engine_class_id values accepted by create-agent APIs total_count (int): Number of engine types returned - engines (list[TemporalWorkflow]): Production agent engine metadata, including descriptions, input schemas, and - default engine_config values + engines (list[AgentEngineTypeListEnginesItem]): Production agent engine metadata, including descriptions, input + schemas, and default engine_config values """ engine_types: list[str] total_count: int - engines: list[TemporalWorkflow] + engines: list[AgentEngineTypeListEnginesItem] additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -42,7 +42,7 @@ class AgentEngineTypeList: def to_dict(self) -> dict[str, Any]: - from ..models.temporal_workflow import TemporalWorkflow + from ..models.agent_engine_type_list_engines_item import AgentEngineTypeListEnginesItem engine_types = self.engine_types @@ -71,7 +71,7 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - from ..models.temporal_workflow import TemporalWorkflow + from ..models.agent_engine_type_list_engines_item import AgentEngineTypeListEnginesItem d = dict(src_dict) engine_types = cast(list[str], d.pop("engine_types")) @@ -81,7 +81,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: engines = [] _engines = d.pop("engines") for engines_item_data in (_engines): - engines_item = TemporalWorkflow.from_dict(engines_item_data) + engines_item = AgentEngineTypeListEnginesItem.from_dict(engines_item_data) diff --git a/src/roe/_generated/models/agent_engine_type_list_engines_item.py b/src/roe/_generated/models/agent_engine_type_list_engines_item.py new file mode 100644 index 0000000..7fca242 --- /dev/null +++ b/src/roe/_generated/models/agent_engine_type_list_engines_item.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + + + + + + + +T = TypeVar("T", bound="AgentEngineTypeListEnginesItem") + + + +@_attrs_define +class AgentEngineTypeListEnginesItem: + """ + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + agent_engine_type_list_engines_item = cls( + ) + + + agent_engine_type_list_engines_item.additional_properties = d + return agent_engine_type_list_engines_item + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/temporal_workflow.py b/src/roe/_generated/models/temporal_workflow.py deleted file mode 100644 index ccb212c..0000000 --- a/src/roe/_generated/models/temporal_workflow.py +++ /dev/null @@ -1,152 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator - -from attrs import define as _attrs_define -from attrs import field as _attrs_field - -from ..types import UNSET, Unset - - - - - - - -T = TypeVar("T", bound="TemporalWorkflow") - - - -@_attrs_define -class TemporalWorkflow: - """ Serializer for Temporal Workflow engine information. - - Matches the shape returned by fetch_all_temporal_workflows() in - agents/services/temporal_service.py. - - Attributes: - type_ (str): Engine type discriminator, always 'temporal_workflow' - class_id (str): Unique class identifier for this workflow - workflow_type (str): The temporal workflow type identifier - display_name (str): Human-readable name of the workflow - description (str): Detailed description of what the workflow does - summary (str): Brief summary of the workflow's capabilities - input_schema (Any): Pydantic JSON Schema describing the workflow's input model - default_values (Any): Default values for the workflow input fields - category (str): The workflow category - form_type (str): The form type that determines which frontend create form to render - """ - - type_: str - class_id: str - workflow_type: str - display_name: str - description: str - summary: str - input_schema: Any - default_values: Any - category: str - form_type: str - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) - - - - - - def to_dict(self) -> dict[str, Any]: - type_ = self.type_ - - class_id = self.class_id - - workflow_type = self.workflow_type - - display_name = self.display_name - - description = self.description - - summary = self.summary - - input_schema = self.input_schema - - default_values = self.default_values - - category = self.category - - form_type = self.form_type - - - field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) - field_dict.update({ - "type": type_, - "class_id": class_id, - "workflow_type": workflow_type, - "display_name": display_name, - "description": description, - "summary": summary, - "input_schema": input_schema, - "default_values": default_values, - "category": category, - "form_type": form_type, - }) - - return field_dict - - - - @classmethod - def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - d = dict(src_dict) - type_ = d.pop("type") - - class_id = d.pop("class_id") - - workflow_type = d.pop("workflow_type") - - display_name = d.pop("display_name") - - description = d.pop("description") - - summary = d.pop("summary") - - input_schema = d.pop("input_schema") - - default_values = d.pop("default_values") - - category = d.pop("category") - - form_type = d.pop("form_type") - - temporal_workflow = cls( - type_=type_, - class_id=class_id, - workflow_type=workflow_type, - display_name=display_name, - description=description, - summary=summary, - input_schema=input_schema, - default_values=default_values, - category=category, - form_type=form_type, - ) - - - temporal_workflow.additional_properties = d - return temporal_workflow - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/tests/unit/test_discovery.py b/tests/unit/test_discovery.py index b740ad1..bf2f9a9 100644 --- a/tests/unit/test_discovery.py +++ b/tests/unit/test_discovery.py @@ -63,3 +63,66 @@ def test_list_supported_models_translates_bad_request(): ): with pytest.raises(BadRequestError): api.list_supported_models(capability="spreadsheet") + + +def test_agent_engine_type_list_deserializes_public_engine_payload(): + """End-to-end deserialization of the exact backend response shape. + + roe-main PR 3232 trimmed the public engine payload to six fields. Earlier + SDK builds typed `engines` as `TemporalWorkflow[]`, whose `from_dict` + popped fields no longer in the payload (`form_type`) and raised + `KeyError`. This guards against that regression. + """ + backend_response = { + "engine_types": ["ResearchEngine"], + "total_count": 1, + "engines": [ + { + "class_id": "ResearchEngine", + "display_name": "Research Engine", + "description": "Researches things.", + "summary": "Research workflow.", + "input_schema": {"type": "object", "properties": {}}, + "default_values": {}, + } + ], + } + + parsed = AgentEngineTypeList.from_dict(backend_response) + + assert parsed.engine_types == ["ResearchEngine"] + assert parsed.total_count == 1 + assert len(parsed.engines) == 1 + engine = parsed.engines[0] + assert engine["class_id"] == "ResearchEngine" + assert engine["display_name"] == "Research Engine" + assert engine["input_schema"] == {"type": "object", "properties": {}} + assert engine["default_values"] == {} + + +def test_supported_llm_model_list_deserializes_public_model_payload(): + backend_response = { + "models": [ + { + "id": "gpt-5", + "providers": ["openai"], + "capabilities": ["text"], + "context_window": 200000, + "max_output_tokens": 8192, + "supports_system_message": True, + "supports_temperature": True, + "supports_reasoning_effort": False, + "supports_json_output": True, + "supports_json_schema": True, + } + ], + "total_count": 1, + "tenant_scope": "all_tenants", + } + + parsed = SupportedLLMModelList.from_dict(backend_response) + + assert parsed.tenant_scope == "all_tenants" + assert parsed.total_count == 1 + assert parsed.models[0].id == "gpt-5" + assert parsed.models[0].capabilities == ["text"] From 037de94194b5bc5047244a9410c8f5fdff6de4b9 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 20 May 2026 11:50:48 -0700 Subject: [PATCH 3/4] fix: drop unused RoeConfig from DiscoveryAPI and tidy generated imports Discovery endpoints are tenant-agnostic; carrying RoeConfig on the API class was a dead public surface that misled future readers. Also remove the duplicate cast/UNSET and unused quote imports from the regenerated endpoint modules, and add a regression test for the capability=None to UNSET translation path. Greptile P2 review id 33087814. --- .../discovery_agent_engine_types_list.py | 6 ++--- .../discovery_supported_models_list.py | 7 ++---- src/roe/api/discovery.py | 9 +++---- src/roe/client.py | 2 +- tests/unit/test_discovery.py | 24 +++++++++++++++---- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py index df4e940..0d5c05c 100644 --- a/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py +++ b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py @@ -1,15 +1,13 @@ from http import HTTPStatus -from typing import Any, cast -from urllib.parse import quote +from typing import Any import httpx from ...client import AuthenticatedClient, Client -from ...types import Response, UNSET +from ...types import Response from ... import errors from ...models.agent_engine_type_list import AgentEngineTypeList -from typing import cast diff --git a/src/roe/_generated/api/discovery/discovery_supported_models_list.py b/src/roe/_generated/api/discovery/discovery_supported_models_list.py index d06799e..c2cbcd6 100644 --- a/src/roe/_generated/api/discovery/discovery_supported_models_list.py +++ b/src/roe/_generated/api/discovery/discovery_supported_models_list.py @@ -1,16 +1,13 @@ from http import HTTPStatus -from typing import Any, cast -from urllib.parse import quote +from typing import Any import httpx from ...client import AuthenticatedClient, Client -from ...types import Response, UNSET +from ...types import Response, UNSET, Unset from ... import errors from ...models.supported_llm_model_list import SupportedLLMModelList -from ...types import UNSET, Unset -from typing import cast diff --git a/src/roe/api/discovery.py b/src/roe/api/discovery.py index d3577ef..1439b17 100644 --- a/src/roe/api/discovery.py +++ b/src/roe/api/discovery.py @@ -10,15 +10,16 @@ from roe._generated.models.agent_engine_type_list import AgentEngineTypeList from roe._generated.models.supported_llm_model_list import SupportedLLMModelList from roe._generated.types import UNSET -from roe.config import RoeConfig from roe.exceptions import RoeAPIException, translate_response class DiscoveryAPI: - """API for discovering valid agent engine types and model IDs.""" + """API for discovering valid agent engine types and model IDs. - def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient): - self.config = config + Discovery endpoints are tenant-agnostic; no RoeConfig is required. + """ + + def __init__(self, raw_client: AuthenticatedClient): self._raw = raw_client def list_agent_engine_types(self) -> AgentEngineTypeList: diff --git a/src/roe/client.py b/src/roe/client.py index 7e1dfa1..e88da2f 100644 --- a/src/roe/client.py +++ b/src/roe/client.py @@ -93,7 +93,7 @@ def __init__( # Create API instances. All APIs delegate to the generated raw client. self._agents = AgentsAPI(self.config, self._raw) - self._discovery = DiscoveryAPI(self.config, self._raw) + self._discovery = DiscoveryAPI(self._raw) self._policies = PoliciesAPI(self.config, self._raw) self._users = UsersAPI(self.config, self._raw) diff --git a/tests/unit/test_discovery.py b/tests/unit/test_discovery.py index bf2f9a9..8209b13 100644 --- a/tests/unit/test_discovery.py +++ b/tests/unit/test_discovery.py @@ -9,7 +9,7 @@ from roe._generated.models.agent_engine_type_list import AgentEngineTypeList from roe._generated.models.supported_llm_model_list import SupportedLLMModelList -from roe._generated.types import Response +from roe._generated.types import UNSET, Response from roe.api.discovery import DiscoveryAPI from roe.exceptions import BadRequestError @@ -25,7 +25,7 @@ def _response(parsed, status: int = 200) -> Response: def test_list_agent_engine_types_calls_generated_endpoint(): raw_client = MagicMock() - api = DiscoveryAPI(MagicMock(), raw_client) + api = DiscoveryAPI(raw_client) payload = AgentEngineTypeList(engine_types=["ResearchEngine"], total_count=1, engines=[]) with patch( @@ -40,7 +40,7 @@ def test_list_agent_engine_types_calls_generated_endpoint(): def test_list_supported_models_passes_capability_filter(): raw_client = MagicMock() - api = DiscoveryAPI(MagicMock(), raw_client) + api = DiscoveryAPI(raw_client) payload = SupportedLLMModelList(models=[], total_count=0, tenant_scope="all_tenants") with patch( @@ -53,9 +53,25 @@ def test_list_supported_models_passes_capability_filter(): assert result.tenant_scope == "all_tenants" +def test_list_supported_models_translates_none_capability_to_unset(): + # Greptile P2: guard the capability=None to UNSET translation, which is + # the main branch in list_supported_models that the other tests skip. + raw_client = MagicMock() + api = DiscoveryAPI(raw_client) + payload = SupportedLLMModelList(models=[], total_count=0, tenant_scope="all_tenants") + + with patch( + "roe.api.discovery.discovery_supported_models_list.sync_detailed", + return_value=_response(payload), + ) as mocked: + api.list_supported_models() + + mocked.assert_called_once_with(client=raw_client, capability=UNSET) + + def test_list_supported_models_translates_bad_request(): raw_client = MagicMock() - api = DiscoveryAPI(MagicMock(), raw_client) + api = DiscoveryAPI(raw_client) with patch( "roe.api.discovery.discovery_supported_models_list.sync_detailed", From a35e0b56cbb3fef505554012cc44b56acaffc14d Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 20 May 2026 12:09:54 -0700 Subject: [PATCH 4/4] revert: keep generated discovery endpoint files exactly as emitted CI's check-codegen-drift job re-runs codegen and rejects any drift from the committed generated files. The duplicate cast/UNSET and unused quote imports are codegen-tool quirks; cleaning them by hand made the committed tree differ from regen output and broke CI. Restore the files. The codegen-side fix is tracked separately and is not in scope for this PR. --- .../api/discovery/discovery_agent_engine_types_list.py | 6 ++++-- .../api/discovery/discovery_supported_models_list.py | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py index 0d5c05c..df4e940 100644 --- a/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py +++ b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py @@ -1,13 +1,15 @@ from http import HTTPStatus -from typing import Any +from typing import Any, cast +from urllib.parse import quote import httpx from ...client import AuthenticatedClient, Client -from ...types import Response +from ...types import Response, UNSET from ... import errors from ...models.agent_engine_type_list import AgentEngineTypeList +from typing import cast diff --git a/src/roe/_generated/api/discovery/discovery_supported_models_list.py b/src/roe/_generated/api/discovery/discovery_supported_models_list.py index c2cbcd6..d06799e 100644 --- a/src/roe/_generated/api/discovery/discovery_supported_models_list.py +++ b/src/roe/_generated/api/discovery/discovery_supported_models_list.py @@ -1,13 +1,16 @@ from http import HTTPStatus -from typing import Any +from typing import Any, cast +from urllib.parse import quote import httpx from ...client import AuthenticatedClient, Client -from ...types import Response, UNSET, Unset +from ...types import Response, UNSET from ... import errors from ...models.supported_llm_model_list import SupportedLLMModelList +from ...types import UNSET, Unset +from typing import cast