From cfd22fbb463c0d1f69e2d744c5e682156454da19 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Mon, 4 May 2026 11:21:29 +0530 Subject: [PATCH 1/9] Added supoport for new params --- runware/base.py | 2 ++ runware/types.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/runware/base.py b/runware/base.py index 499838d..927d436 100644 --- a/runware/base.py +++ b/runware/base.py @@ -2661,6 +2661,8 @@ async def _requestText(self, requestText: ITextInference) -> Union[List[IText], inputs.images = await self._process_media_list(inputs.images) if inputs.videos: inputs.videos = await self._process_media_list(inputs.videos) + if inputs.documents: + inputs.documents = await self._process_media_list(inputs.documents) request_object = self._buildTextRequest(requestText) diff --git a/runware/types.py b/runware/types.py index 76a62fa..e8bfa35 100644 --- a/runware/types.py +++ b/runware/types.py @@ -120,6 +120,8 @@ class OperationState(Enum): IOutputType = Literal["base64Data", "dataURI", "URL"] IOutputFormat = Literal["JPG", "PNG", "WEBP", "SVG"] IAudioOutputFormat = Literal["wav", "mp3", "pcm", "opus", "aac", "flac", "MP3"] +TextInferenceCacheScope = Literal["system", "system+history"] +TextInferenceCacheTtl = Literal["5m", "1h"] @dataclass @@ -821,13 +823,47 @@ def request_key(self) -> str: return "texSlat" +@dataclass +class ITextInferenceCache(SerializableMixin): + + scope: Optional[TextInferenceCacheScope] = None + ttl: Optional[TextInferenceCacheTtl] = None + + @property + def request_key(self) -> str: + return "cache" + + @dataclass class ITextInferenceTool(SerializableMixin): """Tool definition for text inference (e.g. function-calling / JSON-schema tools).""" name: str description: str - input_schema: Dict[str, Any] + schema: Optional[Dict[str, Any]] = None + input_schema: Optional[Dict[str, Any]] = field(default=None, repr=False) + type: Optional[str] = field(default=None, repr=False) + toolType: Optional[str] = None + + def __post_init__(self) -> None: + if self.schema is None and self.input_schema is not None: + object.__setattr__(self, "schema", self.input_schema) + object.__setattr__(self, "input_schema", None) + if self.schema is None: + raise ValueError("ITextInferenceTool requires 'schema' or 'input_schema'") + if self.toolType is None: + object.__setattr__( + self, + "toolType", + self.type if self.type is not None else "function", + ) + object.__setattr__(self, "type", None) + + def serialize(self) -> Dict[str, Any]: + data = super().serialize() + tt = data.pop("toolType", None) or "function" + data["type"] = tt + return data @dataclass @@ -965,6 +1001,7 @@ class ISettings(SerializableMixin): stopSequences: Optional[List[str]] = None tools: Optional[List[Union[ITextInferenceTool, Dict[str, Any]]]] = None toolChoice: Optional[Union[ITextInferenceToolChoice, Dict[str, Any]]] = None + cache: Optional[Union[ITextInferenceCache, Dict[str, Any]]] = None # Image upscale steps: Optional[int] = None seed: Optional[int] = None @@ -992,6 +1029,8 @@ def __post_init__(self): ITextInferenceTool(**t) if isinstance(t, dict) else t for t in self.tools ] + if self.cache is not None and isinstance(self.cache, dict): + self.cache = ITextInferenceCache(**self.cache) if self.toolChoice is not None and isinstance(self.toolChoice, dict): self.toolChoice = ITextInferenceToolChoice(**self.toolChoice) if self.editRegions is not None: @@ -1083,6 +1122,7 @@ def __post_init__(self): class ITextInputs(SerializableMixin): images: Optional[List[Union[str, File]]] = None videos: Optional[List[Union[str, File]]] = None + documents: Optional[List[Union[str, File]]] = None @property def request_key(self) -> str: From 7e1168c0ecb4eb801fba08fd31ff3cf0113e098c Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Tue, 5 May 2026 19:36:14 +0530 Subject: [PATCH 2/9] Fixed types.py --- runware/types.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/runware/types.py b/runware/types.py index e8bfa35..2401a1c 100644 --- a/runware/types.py +++ b/runware/types.py @@ -842,27 +842,16 @@ class ITextInferenceTool(SerializableMixin): description: str schema: Optional[Dict[str, Any]] = None input_schema: Optional[Dict[str, Any]] = field(default=None, repr=False) - type: Optional[str] = field(default=None, repr=False) toolType: Optional[str] = None - def __post_init__(self) -> None: - if self.schema is None and self.input_schema is not None: - object.__setattr__(self, "schema", self.input_schema) - object.__setattr__(self, "input_schema", None) - if self.schema is None: - raise ValueError("ITextInferenceTool requires 'schema' or 'input_schema'") - if self.toolType is None: - object.__setattr__( - self, - "toolType", - self.type if self.type is not None else "function", - ) - object.__setattr__(self, "type", None) - def serialize(self) -> Dict[str, Any]: data = super().serialize() - tt = data.pop("toolType", None) or "function" - data["type"] = tt + if self.schema is None and self.input_schema is not None: + data["schema"] = self.input_schema + data.pop("input_schema", None) + if self.toolType is not None: + data["type"] = self.toolType + data.pop("toolType", None) return data From 26c9bd746a98bab8b61c7b0b77f3c1e3a0d6f6c4 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Tue, 5 May 2026 19:50:24 +0530 Subject: [PATCH 3/9] Unified text-input media preprocessing in runware/base.py via _processTextInputs() --- CHANGELOG.md | 12 ++++++++++++ runware/base.py | 32 ++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02022b5..46297da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,17 @@ All notable changes to this project will be documented in this file. - `negativePrompt: Optional[str]` - `I3dOutputFormat` now supports: - `OBJ` +- Added text inference cache support: + - `TextInferenceCacheScope = Literal["system", "system+history"]` + - `TextInferenceCacheTtl = Literal["5m", "1h"]` + - `ITextInferenceCache` dataclass (`scope`, `ttl`) with request key `cache` + - `ISettings.cache: Optional[Union[ITextInferenceCache, Dict[str, Any]]]` plus dict-to-dataclass normalization in `ISettings.__post_init__` +- `ITextInputs` now includes: + - `documents: Optional[List[Union[str, File]]]` +- `ITextInferenceTool` now supports: + - `schema: Optional[Dict[str, Any]]` + - backward-compatible `input_schema` alias (serialized as `schema` when `schema` is not provided) + - optional `toolType` (serialized as `type`) ### Changed @@ -57,6 +68,7 @@ All notable changes to this project will be documented in this file. - Kept `process_image()` as a backward-compatible alias to `process_media()`. - Replaced `type` usage in video reference dataclasses with `refType` to avoid reserved-name ambiguity while preserving API payload compatibility (`type` is still sent on wire via `serialize()`). - `I3dOutputFormat` now supports Meshy 6 target formats: `GLB`, `OBJ`, `FBX`, `STL`, `USDZ`, `3MF` (while keeping `PLY` for compatibility). +- Unified text-input media preprocessing in `runware/base.py` via `_processTextInputs()` and applied it to both `_requestText()` and `_requestTextStream()`, so `images`/`videos`/`documents` are normalized consistently for sync/async/stream text requests. ## [0.5.9] diff --git a/runware/base.py b/runware/base.py index 927d436..065d43b 100644 --- a/runware/base.py +++ b/runware/base.py @@ -2584,6 +2584,7 @@ async def _requestTextStream( self, requestText: ITextInference ) -> AsyncIterator[Union[str, IText]]: requestText.taskUUID = requestText.taskUUID or getUUID() + await self._processTextInputs(requestText) request_object = self._buildTextRequest(requestText) body = [request_object] http_url = get_http_url_from_ws_url(self._url or "") @@ -2649,20 +2650,7 @@ async def _requestTextStream( async def _requestText(self, requestText: ITextInference) -> Union[List[IText], IAsyncTaskResponse]: await self.ensureConnection() requestText.taskUUID = requestText.taskUUID or getUUID() - - - if requestText.inputs: - inputs = requestText.inputs - if isinstance(inputs, dict): - inputs = ITextInputs(**inputs) - requestText.inputs = inputs - - if inputs.images: - inputs.images = await self._process_media_list(inputs.images) - if inputs.videos: - inputs.videos = await self._process_media_list(inputs.videos) - if inputs.documents: - inputs.documents = await self._process_media_list(inputs.documents) + await self._processTextInputs(requestText) request_object = self._buildTextRequest(requestText) @@ -2764,6 +2752,22 @@ def is_text_complete(r: "Dict[str, Any]") -> bool: finally: await self._unregister_pending_operation(task_uuid) + async def _processTextInputs(self, requestText: ITextInference) -> None: + if not requestText.inputs: + return + + inputs = requestText.inputs + if isinstance(inputs, dict): + inputs = ITextInputs(**inputs) + requestText.inputs = inputs + + if inputs.images: + inputs.images = await self._process_media_list(inputs.images) + if inputs.videos: + inputs.videos = await self._process_media_list(inputs.videos) + if inputs.documents: + inputs.documents = await self._process_media_list(inputs.documents) + def _buildImageRequest(self, requestImage: IImageInference, prompt: Optional[str], control_net_data_dicts: List[Dict], instant_id_data: Optional[Dict], ip_adapters_data: Optional[List[Dict]], ace_plus_plus_data: Optional[Dict], pulid_data: Optional[Dict], photo_maker_data: Optional[Dict]) -> Dict[str, Any]: request_object = { "taskType": ETaskType.IMAGE_INFERENCE.value, From abaa0a4e1108e9d49a254bc989c008afa99c713e Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Wed, 6 May 2026 11:39:05 +0530 Subject: [PATCH 4/9] Fixed toolChoice --- runware/base.py | 1 + runware/types.py | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/runware/base.py b/runware/base.py index 065d43b..8650e75 100644 --- a/runware/base.py +++ b/runware/base.py @@ -2545,6 +2545,7 @@ def _buildTextRequest(self, requestText: ITextInference) -> Dict[str, Any]: request_object["includeUsage"] = requestText.includeUsage if requestText.numberResults is not None: request_object["numberResults"] = requestText.numberResults + self._addOptionalField(request_object, requestText.toolChoice) self._addOptionalField(request_object, requestText.settings) self._addOptionalField(request_object, requestText.inputs) self._addProviderSettings(request_object, requestText) diff --git a/runware/types.py b/runware/types.py index 2401a1c..d2c9a3a 100644 --- a/runware/types.py +++ b/runware/types.py @@ -859,9 +859,20 @@ def serialize(self) -> Dict[str, Any]: class ITextInferenceToolChoice(SerializableMixin): """Selects how tools are used (provider-specific shape, e.g. type + name).""" - type: str + toolType: str name: Optional[str] = None + @property + def request_key(self) -> str: + return "toolChoice" + + def serialize(self) -> Dict[str, Any]: + data = super().serialize() + if self.toolType is not None: + data["type"] = self.toolType + data.pop("toolType", None) + return data + @dataclass class IColorPaletteEntry(SerializableMixin): @@ -989,7 +1000,6 @@ class ISettings(SerializableMixin): topK: Optional[int] = None stopSequences: Optional[List[str]] = None tools: Optional[List[Union[ITextInferenceTool, Dict[str, Any]]]] = None - toolChoice: Optional[Union[ITextInferenceToolChoice, Dict[str, Any]]] = None cache: Optional[Union[ITextInferenceCache, Dict[str, Any]]] = None # Image upscale steps: Optional[int] = None @@ -1020,8 +1030,6 @@ def __post_init__(self): ] if self.cache is not None and isinstance(self.cache, dict): self.cache = ITextInferenceCache(**self.cache) - if self.toolChoice is not None and isinstance(self.toolChoice, dict): - self.toolChoice = ITextInferenceToolChoice(**self.toolChoice) if self.editRegions is not None: self.editRegions = [ [ @@ -2167,12 +2175,15 @@ class ITextInference: seed: Optional[int] = None includeCost: Optional[bool] = None includeUsage: Optional[bool] = None + toolChoice: Optional[Union[ITextInferenceToolChoice, Dict[str, Any]]] = None settings: Optional[Union[ISettings, Dict[str, Any]]] = None inputs: Optional[Union[ITextInputs, Dict[str, Any]]] = None providerSettings: Optional[TextProviderSettings] = None webhookURL: Optional[str] = None def __post_init__(self) -> None: + if self.toolChoice is not None and isinstance(self.toolChoice, dict): + self.toolChoice = ITextInferenceToolChoice(**self.toolChoice) if self.settings is not None and isinstance(self.settings, dict): self.settings = ISettings(**self.settings) if self.inputs is not None and isinstance(self.inputs, dict): From e652eb7f45fea0572e9ccff2b0ee5c48fa9a4ed0 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Wed, 6 May 2026 11:40:26 +0530 Subject: [PATCH 5/9] Discarded changelog temp --- CHANGELOG.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46297da..02022b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,17 +49,6 @@ All notable changes to this project will be documented in this file. - `negativePrompt: Optional[str]` - `I3dOutputFormat` now supports: - `OBJ` -- Added text inference cache support: - - `TextInferenceCacheScope = Literal["system", "system+history"]` - - `TextInferenceCacheTtl = Literal["5m", "1h"]` - - `ITextInferenceCache` dataclass (`scope`, `ttl`) with request key `cache` - - `ISettings.cache: Optional[Union[ITextInferenceCache, Dict[str, Any]]]` plus dict-to-dataclass normalization in `ISettings.__post_init__` -- `ITextInputs` now includes: - - `documents: Optional[List[Union[str, File]]]` -- `ITextInferenceTool` now supports: - - `schema: Optional[Dict[str, Any]]` - - backward-compatible `input_schema` alias (serialized as `schema` when `schema` is not provided) - - optional `toolType` (serialized as `type`) ### Changed @@ -68,7 +57,6 @@ All notable changes to this project will be documented in this file. - Kept `process_image()` as a backward-compatible alias to `process_media()`. - Replaced `type` usage in video reference dataclasses with `refType` to avoid reserved-name ambiguity while preserving API payload compatibility (`type` is still sent on wire via `serialize()`). - `I3dOutputFormat` now supports Meshy 6 target formats: `GLB`, `OBJ`, `FBX`, `STL`, `USDZ`, `3MF` (while keeping `PLY` for compatibility). -- Unified text-input media preprocessing in `runware/base.py` via `_processTextInputs()` and applied it to both `_requestText()` and `_requestTextStream()`, so `images`/`videos`/`documents` are normalized consistently for sync/async/stream text requests. ## [0.5.9] From 7bcf4aff52f522176112aef3137666c1e707ad76 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 7 May 2026 09:03:30 +0530 Subject: [PATCH 6/9] Fixed backward compatibility --- runware/types.py | 14 ++++++++++---- runware/utils.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/runware/types.py b/runware/types.py index d2c9a3a..c6d5489 100644 --- a/runware/types.py +++ b/runware/types.py @@ -846,9 +846,9 @@ class ITextInferenceTool(SerializableMixin): def serialize(self) -> Dict[str, Any]: data = super().serialize() - if self.schema is None and self.input_schema is not None: - data["schema"] = self.input_schema - data.pop("input_schema", None) + input_schema = data.pop("input_schema", None) + if data.get("schema") is None and input_schema is not None: + data["schema"] = input_schema if self.toolType is not None: data["type"] = self.toolType data.pop("toolType", None) @@ -1025,7 +1025,13 @@ def __post_init__(self): self.texSlat = ITexSlat(**self.texSlat) if self.tools is not None: self.tools = [ - ITextInferenceTool(**t) if isinstance(t, dict) else t + ITextInferenceTool( + **( + {**t, "toolType": t["type"]} + if isinstance(t, dict) and "toolType" not in t and "type" in t + else t + ) + ) if isinstance(t, dict) else t for t in self.tools ] if self.cache is not None and isinstance(self.cache, dict): diff --git a/runware/utils.py b/runware/utils.py index 9c05871..704fa94 100644 --- a/runware/utils.py +++ b/runware/utils.py @@ -39,7 +39,7 @@ mimetypes.add_type("image/webp", ".webp") BASE_RUNWARE_URLS = { - Environment.PRODUCTION: "wss://ws-api.runware.ai/v1", + Environment.PRODUCTION: "wss://ws-api.runware.dev/v1", Environment.TEST: "ws://localhost:8080", } From 7ac52b4ccbd22f4ee5f4d8634b7c009f96f87451 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 7 May 2026 09:08:30 +0530 Subject: [PATCH 7/9] Fixed backward compatibility for ISettings.toolChoice --- runware/types.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/runware/types.py b/runware/types.py index c6d5489..b9dbb89 100644 --- a/runware/types.py +++ b/runware/types.py @@ -2188,10 +2188,23 @@ class ITextInference: webhookURL: Optional[str] = None def __post_init__(self) -> None: - if self.toolChoice is not None and isinstance(self.toolChoice, dict): - self.toolChoice = ITextInferenceToolChoice(**self.toolChoice) if self.settings is not None and isinstance(self.settings, dict): - self.settings = ISettings(**self.settings) + settings_data = dict(self.settings) + legacy_tool_choice = settings_data.pop("toolChoice", None) + if legacy_tool_choice is not None: + warnings.warn( + "settings.toolChoice is deprecated; use ITextInference.toolChoice instead.", + DeprecationWarning, + stacklevel=2, + ) + if self.toolChoice is None: + self.toolChoice = legacy_tool_choice + self.settings = ISettings(**settings_data) + if self.toolChoice is not None and isinstance(self.toolChoice, dict): + tool_choice_data = dict(self.toolChoice) + if "toolType" not in tool_choice_data and "type" in tool_choice_data: + tool_choice_data["toolType"] = tool_choice_data.pop("type") + self.toolChoice = ITextInferenceToolChoice(**tool_choice_data) if self.inputs is not None and isinstance(self.inputs, dict): self.inputs = ITextInputs(**self.inputs) From 3ffbc0a30edb2048117fda3c130980b525babaee Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 7 May 2026 09:26:55 +0530 Subject: [PATCH 8/9] Fixed backward comp for type and toolType --- runware/types.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/runware/types.py b/runware/types.py index b9dbb89..b349d3e 100644 --- a/runware/types.py +++ b/runware/types.py @@ -859,20 +859,23 @@ def serialize(self) -> Dict[str, Any]: class ITextInferenceToolChoice(SerializableMixin): """Selects how tools are used (provider-specific shape, e.g. type + name).""" - toolType: str + toolType: Optional[str] = None + type: InitVar[Optional[str]] = None name: Optional[str] = None + def __post_init__(self, type: Optional[str] = None) -> None: + if self.toolType is None and type is not None: + warnings.warn( + "ITextInferenceToolChoice(type=...) is deprecated; use toolType=... instead.", + DeprecationWarning, + stacklevel=2, + ) + self.toolType = type + @property def request_key(self) -> str: return "toolChoice" - def serialize(self) -> Dict[str, Any]: - data = super().serialize() - if self.toolType is not None: - data["type"] = self.toolType - data.pop("toolType", None) - return data - @dataclass class IColorPaletteEntry(SerializableMixin): @@ -1027,7 +1030,7 @@ def __post_init__(self): self.tools = [ ITextInferenceTool( **( - {**t, "toolType": t["type"]} + {**{k: v for k, v in t.items() if k != "type"}, "toolType": t["type"]} if isinstance(t, dict) and "toolType" not in t and "type" in t else t ) From b61615a75fa133dd08f76cef3c457c6fb77dc150 Mon Sep 17 00:00:00 2001 From: Sirshendu Ganguly Date: Thu, 7 May 2026 13:53:08 +0530 Subject: [PATCH 9/9] Added backward compat for toolChoice as InitVar --- runware/types.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/runware/types.py b/runware/types.py index b349d3e..f1e577c 100644 --- a/runware/types.py +++ b/runware/types.py @@ -876,6 +876,13 @@ def __post_init__(self, type: Optional[str] = None) -> None: def request_key(self) -> str: return "toolChoice" + def serialize(self) -> Dict[str, Any]: + data = super().serialize() + if self.toolType is not None: + data["type"] = self.toolType + data.pop("toolType", None) + return data + @dataclass class IColorPaletteEntry(SerializableMixin): @@ -1003,6 +1010,7 @@ class ISettings(SerializableMixin): topK: Optional[int] = None stopSequences: Optional[List[str]] = None tools: Optional[List[Union[ITextInferenceTool, Dict[str, Any]]]] = None + toolChoice: InitVar[Optional[Union["ITextInferenceToolChoice", Dict[str, Any]]]] = None cache: Optional[Union[ITextInferenceCache, Dict[str, Any]]] = None # Image upscale steps: Optional[int] = None @@ -1019,7 +1027,14 @@ class ISettings(SerializableMixin): enhanceDetails: Optional[bool] = None realism: Optional[bool] = None - def __post_init__(self): + def __post_init__(self, toolChoice: Optional[Union["ITextInferenceToolChoice", Dict[str, Any]]] = None): + if toolChoice is not None: + warnings.warn( + "ISettings(toolChoice=...) is deprecated; use ITextInference.toolChoice instead.", + DeprecationWarning, + stacklevel=2, + ) + self.__dict__["toolChoice"] = toolChoice if self.sparseStructure is not None and isinstance(self.sparseStructure, dict): self.sparseStructure = ISparseStructure(**self.sparseStructure) if self.shapeSlat is not None and isinstance(self.shapeSlat, dict): @@ -2203,6 +2218,16 @@ def __post_init__(self) -> None: if self.toolChoice is None: self.toolChoice = legacy_tool_choice self.settings = ISettings(**settings_data) + elif self.settings is not None: + legacy_tool_choice = getattr(self.settings, "toolChoice", None) + if legacy_tool_choice is not None: + warnings.warn( + "settings.toolChoice is deprecated; use ITextInference.toolChoice instead.", + DeprecationWarning, + stacklevel=2, + ) + if self.toolChoice is None: + self.toolChoice = legacy_tool_choice if self.toolChoice is not None and isinstance(self.toolChoice, dict): tool_choice_data = dict(self.toolChoice) if "toolType" not in tool_choice_data and "type" in tool_choice_data: