From 7ee50a828d91c84133359e1ed2c8221069261597 Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Mon, 4 May 2026 09:45:36 +0200 Subject: [PATCH 1/7] Workflows: Event decryption --- .speakeasy/gen.lock | 16 +- .speakeasy/gen.yaml | 2 +- .speakeasy/workflow.lock | 2 +- pyproject.toml | 2 +- .../client/_hooks/workflow_encoding_hook.py | 163 ++++++++++++++---- src/mistralai/client/_version.py | 4 +- .../workflows/encoding/payload_encoder.py | 43 +++++ uv.lock | 2 +- 8 files changed, 189 insertions(+), 45 deletions(-) diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index b62a204c..7a1d3ff8 100644 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -5,15 +5,15 @@ management: docVersion: 1.0.0 speakeasyVersion: 1.761.1 generationVersion: 2.879.6 - releaseVersion: 2.4.4 - configChecksum: d237fa5a6bc2a67b0977a61520bd85fb + releaseVersion: 2.4.5 + configChecksum: f6852fb59e3bcc9a7750210521f2c2d4 repoURL: https://github.com/mistralai/client-python.git installationURL: https://github.com/mistralai/client-python.git published: true persistentEdits: - generation_id: 8315974b-9a57-4a9f-b6b1-a75627dc6b85 - pristine_commit_hash: 969db46e6806db3556c93296ed4c87a2b6a0c709 - pristine_tree_hash: b900e343a3b555d433748ed99095b1468250aa44 + generation_id: e62d9764-a475-4217-b15c-339df5940f3f + pristine_commit_hash: 081e214c8cf02efca9607c9f2c0e224a453b9f25 + pristine_tree_hash: 1e5d4803268ab0dfc34daf3e0d00a5a950cd7737 features: python: acceptHeaders: 3.0.0 @@ -61,7 +61,6 @@ trackedFiles: id: 89aa447020cd last_write_checksum: sha1:f84632c81029fcdda8c3b0c768d02b836fc80526 pristine_git_object: 8d79f0abb72526f1fb34a4c03e5bba612c6ba2ae - deleted: true USAGE.md: id: 3aed33ce6e6f last_write_checksum: sha1:d172deb3ee1630f16b279de22aec1f8f68d7565f @@ -3344,8 +3343,8 @@ trackedFiles: pristine_git_object: 036d44b8cfc51599873bd5c401a6aed30450536c src/mistralai/client/_version.py: id: cc807b30de19 - last_write_checksum: sha1:9096662e60738fa15fd58b70880e1e8dff442403 - pristine_git_object: 026fdee0423c494d6d525bd1a67d566a76c4fd7d + last_write_checksum: sha1:6b2772cd63b60cddf4ea95d94cfc44f81a878a73 + pristine_git_object: 9f9ae8c6224af17b3e22410b33f5a5ad37389e33 src/mistralai/client/accesses.py: id: 76fc53bfcf59 last_write_checksum: sha1:de197fbbfea8bc95f44b4e7ee1b39e68fdde8bc7 @@ -8704,7 +8703,6 @@ examples: application/json: {} examplesVersion: 1.0.2 generatedTests: {} -releaseNotes: "## Python SDK Changes:\n* `mistral.beta.libraries.update()`: `request.name` **Changed** (Breaking ⚠️)\n* `mistral.beta.libraries.documents.update()`: `request.name` **Changed** (Breaking ⚠️)\n* `mistral.beta.rag.ingestion_pipeline_configurations.update_run_info()`: **Added**\n* `mistral.workflows.schedules.pause_schedule()`: **Added**\n* `mistral.workflows.schedules.resume_schedule()`: **Added**\n* `mistral.beta.rag.ingestion_pipeline_configurations.list()`: `response.[]` **Changed**\n* `mistral.beta.rag.ingestion_pipeline_configurations.register()`: \n * `request.pipeline_composition` **Added**\n * `response` **Changed**\n* `mistral.workflows.schedules.get_schedules()`: `response.schedules[]` **Changed**\n* `mistral.workflows.schedules.schedule_workflow()`: \n * `request.schedule.max_executions` **Added**\n" generatedFiles: - .gitattributes - .vscode/settings.json diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index 465afa47..a246d5a7 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -32,7 +32,7 @@ generation: generateNewTests: false skipResponseBodyAssertions: false python: - version: 2.4.4 + version: 2.4.5 additionalDependencies: dev: pytest: ^8.2.2 diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index 265ea37e..7f4be3ae 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -41,7 +41,7 @@ targets: sourceRevisionDigest: sha256:1f3eeb2513538e3ae8b5483477b559a460ce3dac0cdd41c31d47db9167acec60 sourceBlobDigest: sha256:b743e6e51b23d2d5207d1f4348076d22f130052fea4e1676095a2ddc654822a9 codeSamplesNamespace: mistral-openapi-code-samples - codeSamplesRevisionDigest: sha256:fa3aa9d30da8a70f9e5d8c74ceab2cd4a043bfa5c5dacfe0035b09d907f41705 + codeSamplesRevisionDigest: sha256:1b67135a0ef0b6ae9bd28992b5cb642d7868eeed2c46b3f552cb940f6c635688 workflow: workflowVersion: 1.0.0 speakeasyVersion: 1.761.1 diff --git a/pyproject.toml b/pyproject.toml index 674de675..2b2a5d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mistralai" -version = "2.4.4" +version = "2.4.2" description = "Python Client SDK for the Mistral AI API." authors = [{ name = "Mistral" }] requires-python = ">=3.10" diff --git a/src/mistralai/client/_hooks/workflow_encoding_hook.py b/src/mistralai/client/_hooks/workflow_encoding_hook.py index f383842d..51c519da 100644 --- a/src/mistralai/client/_hooks/workflow_encoding_hook.py +++ b/src/mistralai/client/_hooks/workflow_encoding_hook.py @@ -28,9 +28,7 @@ class _WorkflowEncodingConfig: - def __init__( - self, payload_encoder: PayloadEncoder, namespace: str - ) -> None: + def __init__(self, payload_encoder: PayloadEncoder, namespace: str) -> None: self.payload_encoder = payload_encoder self.namespace = namespace @@ -64,7 +62,9 @@ def configure_workflow_encoding( ) -def _get_encoding_config(sdk_config: SDKConfiguration) -> Optional[_WorkflowEncodingConfig]: +def _get_encoding_config( + sdk_config: SDKConfiguration, +) -> Optional[_WorkflowEncodingConfig]: """Get workflow encoding config for a client.""" config_id = getattr(sdk_config, _ENCODING_CONFIG_ID_ATTR, None) if config_id is None: @@ -100,9 +100,26 @@ def _get_encoding_config(sdk_config: SDKConfiguration) -> Optional[_WorkflowEnco "update_workflow_execution_v1_workflows_executions__execution_id__updates_post", } +# Operations that return event data that may need decryption +OPERATIONS_DECODE_EVENTS = { + "get_workflow_events_v1_workflows_events_list_get", + "get_workflow_execution_history_v1_workflows_executions__execution_id__history_get", +} + SCHEDULE_CORRELATION_ID_PLACEHOLDER = "__scheduled_workflow__" +def _is_payload_type(value: Any) -> bool: + """Check if a value is a JSONPayload or JSONPatchPayload by its structure. + + Payload types have: {"type": "json" | "json_patch", "value": ...} + """ + if not isinstance(value, dict): + return False + payload_type = value.get("type") + return payload_type in ("json", "json_patch") and "value" in value + + _T = TypeVar("_T") @@ -143,6 +160,47 @@ def _extract_execution_id_from_body(body: Dict[str, Any]) -> Optional[str]: return body.get("execution_id") +async def _decrypt_event_attributes( + attributes: Dict[str, Any], + payload_encoder: PayloadEncoder, +) -> Dict[str, Any]: + """Decrypt payload fields in event attributes. + + Identifies encryptable fields by their type structure (JSONPayload or JSONPatchPayload) + rather than by field name. + """ + for field_name, field_value in attributes.items(): + if not _is_payload_type(field_value): + continue + + # Check if it has encoding_options (meaning it's encrypted) + if not field_value.get("encoding_options"): + continue + + # Decrypt the payload + decrypted = await payload_encoder.decode_event_payload(field_value) + attributes[field_name] = decrypted + + return attributes + + +async def _decrypt_events_in_response( + body: Dict[str, Any], + payload_encoder: PayloadEncoder, +) -> Dict[str, Any]: + """Decrypt payload fields in events within a response body.""" + events = body.get("events", []) + if not events: + return body + + for event in events: + attributes = event.get("attributes") + if isinstance(attributes, dict): + event["attributes"] = await _decrypt_event_attributes( + attributes, payload_encoder + ) + + return body class WorkflowEncodingHook(BeforeRequestHook, AfterSuccessHook): @@ -208,7 +266,9 @@ def before_request( ) encoded_input = _run_async( - encoding_config.payload_encoder.encode_network_input(input_data, context) + encoding_config.payload_encoder.encode_network_input( + input_data, context + ) ) # Update body based on operation type: @@ -241,40 +301,83 @@ def after_success( hook_ctx: AfterSuccessContext, response: httpx.Response, ) -> Union[httpx.Response, Exception]: - """Intercept responses to decode workflow result payloads.""" + """Intercept responses to decode workflow result payloads and event payloads.""" + logger.debug( + "WorkflowEncodingHook.after_success called for %s", + hook_ctx.operation_id, + ) encoding_config = _get_encoding_config(hook_ctx.config) if not encoding_config: - return response - - if hook_ctx.operation_id not in OPERATIONS_DECODE_RESULT: + logger.debug( + "WorkflowEncodingHook: No encoding config for %s", + hook_ctx.operation_id, + ) return response content_type = response.headers.get("content-type", "") if "application/json" not in content_type: return response - try: - body = json.loads(response.content) - result = body.get("result") - if result is None or not encoding_config.payload_encoder.check_is_payload_encoded(result): - return response + # Handle workflow result decoding + if hook_ctx.operation_id in OPERATIONS_DECODE_RESULT: + try: + body = json.loads(response.content) + result = body.get("result") + if ( + result is None + or not encoding_config.payload_encoder.check_is_payload_encoded( + result + ) + ): + return response + + logger.debug( + "WorkflowEncodingHook: Decoding result for %s", + hook_ctx.operation_id, + ) - logger.debug( - "WorkflowEncodingHook: Decoding result for %s", hook_ctx.operation_id - ) + decoded_result = _run_async( + encoding_config.payload_encoder.decode_network_result(result) + ) - decoded_result = _run_async(encoding_config.payload_encoder.decode_network_result(result)) + body["result"] = decoded_result + new_content = json.dumps(body).encode("utf-8") - body["result"] = decoded_result - new_content = json.dumps(body).encode("utf-8") + return httpx.Response( + status_code=response.status_code, + headers=response.headers, + content=new_content, + request=response.request, + extensions=response.extensions, + ) + except Exception as e: + logger.error("WorkflowEncodingHook: Failed to decode result: %s", e) + raise + + # Handle event payload decoding + if hook_ctx.operation_id in OPERATIONS_DECODE_EVENTS: + try: + body = json.loads(response.content) + + logger.debug( + "WorkflowEncodingHook: Decoding events for %s", + hook_ctx.operation_id, + ) - return httpx.Response( - status_code=response.status_code, - headers=response.headers, - content=new_content, - request=response.request, - extensions=response.extensions, - ) - except Exception as e: - logger.error("WorkflowEncodingHook: Failed to decode result: %s", e) - raise + body = _run_async( + _decrypt_events_in_response(body, encoding_config.payload_encoder) + ) + new_content = json.dumps(body).encode("utf-8") + + return httpx.Response( + status_code=response.status_code, + headers=response.headers, + content=new_content, + request=response.request, + extensions=response.extensions, + ) + except Exception as e: + logger.error("WorkflowEncodingHook: Failed to decode events: %s", e) + raise + + return response diff --git a/src/mistralai/client/_version.py b/src/mistralai/client/_version.py index 026fdee0..9f9ae8c6 100644 --- a/src/mistralai/client/_version.py +++ b/src/mistralai/client/_version.py @@ -4,10 +4,10 @@ import importlib.metadata __title__: str = "mistralai" -__version__: str = "2.4.4" +__version__: str = "2.4.5" __openapi_doc_version__: str = "1.0.0" __gen_version__: str = "2.879.6" -__user_agent__: str = "speakeasy-sdk/python 2.4.4 2.879.6 1.0.0 mistralai" +__user_agent__: str = "speakeasy-sdk/python 2.4.5 2.879.6 1.0.0 mistralai" try: if __package__ is not None: diff --git a/src/mistralai/extra/workflows/encoding/payload_encoder.py b/src/mistralai/extra/workflows/encoding/payload_encoder.py index 802ae41b..d0c31572 100644 --- a/src/mistralai/extra/workflows/encoding/payload_encoder.py +++ b/src/mistralai/extra/workflows/encoding/payload_encoder.py @@ -263,6 +263,23 @@ async def encode_payload_content( return data, encoding_options + async def encode_payload_content_full( + self, data: Union[bytes, str] + ) -> tuple[bytes, list[EncodedPayloadOptions]]: + """Encrypt payload with full encryption, regardless of configured mode. + + This is used for payloads like json_patch that don't contain EncryptedStrField + markers and must always use full encryption to avoid leaking sensitive data. + """ + if isinstance(data, str): + data = data.encode() + + if self.encryption_config is None: + return data, [] + + encrypted_data = self._encrypt(data) + return encrypted_data, [EncodedPayloadOptions.ENCRYPTED] + async def decode_payload_content( self, data: bytes, encoding_options: List[EncodedPayloadOptions] ) -> bytes: @@ -294,6 +311,32 @@ async def decode_payload_content( return data + async def decode_event_payload( + self, payload_data: Dict[str, Any] + ) -> Dict[str, Any]: + """Decrypt an event payload's value if it has encoding_options. + + Args: + payload_data: Dict with 'type', 'value', and 'encoding_options' fields + + Returns: + Dict with decrypted 'value' and empty 'encoding_options' + """ + encoding_options_strs = payload_data.get("encoding_options", []) + if not encoding_options_strs: + return payload_data + + encoding_options = [EncodedPayloadOptions(opt) for opt in encoding_options_strs] + encrypted_bytes = base64.b64decode(payload_data["value"]) + decrypted_bytes = await self.decode_payload_content(encrypted_bytes, encoding_options) + decrypted_value = json.loads(decrypted_bytes) + + return { + "type": payload_data["type"], + "value": decrypted_value, + "encoding_options": [], + } + async def encode_network_input( self, data: Optional[Dict[str, Any]], context: WorkflowContext ) -> NetworkEncodedInput: diff --git a/uv.lock b/uv.lock index 8f07ac6b..52b3440f 100644 --- a/uv.lock +++ b/uv.lock @@ -1015,7 +1015,7 @@ wheels = [ [[package]] name = "mistralai" -version = "2.4.4" +version = "2.4.2" source = { editable = "." } dependencies = [ { name = "eval-type-backport" }, From 32398481ac4d864a66116e5b09acc03104c7e77b Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Mon, 4 May 2026 11:36:38 +0200 Subject: [PATCH 2/7] Improvements --- pyproject.toml | 2 +- .../workflows/encoding/payload_encoder.py | 26 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b2a5d07..674de675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mistralai" -version = "2.4.2" +version = "2.4.4" description = "Python Client SDK for the Mistral AI API." authors = [{ name = "Mistral" }] requires-python = ">=3.10" diff --git a/src/mistralai/extra/workflows/encoding/payload_encoder.py b/src/mistralai/extra/workflows/encoding/payload_encoder.py index d0c31572..611f33fa 100644 --- a/src/mistralai/extra/workflows/encoding/payload_encoder.py +++ b/src/mistralai/extra/workflows/encoding/payload_encoder.py @@ -263,13 +263,17 @@ async def encode_payload_content( return data, encoding_options - async def encode_payload_content_full( - self, data: Union[bytes, str] + async def encode_event_payload_content( + self, data: Union[bytes, str], force_full_encryption: bool = False ) -> tuple[bytes, list[EncodedPayloadOptions]]: - """Encrypt payload with full encryption, regardless of configured mode. + """Encrypt event payload content. - This is used for payloads like json_patch that don't contain EncryptedStrField - markers and must always use full encryption to avoid leaking sensitive data. + Unlike encode_payload_content, this only handles encryption (no offloading). + + Args: + data: The payload data to encrypt. + force_full_encryption: Force full encryption regardless of configured mode. + Use for payloads like json_patch that don't support partial encryption. """ if isinstance(data, str): data = data.encode() @@ -277,8 +281,16 @@ async def encode_payload_content_full( if self.encryption_config is None: return data, [] - encrypted_data = self._encrypt(data) - return encrypted_data, [EncodedPayloadOptions.ENCRYPTED] + if force_full_encryption or self.encryption_config.mode == PayloadEncryptionMode.FULL: + encrypted_data = self._encrypt(data) + return encrypted_data, [EncodedPayloadOptions.ENCRYPTED] + + # Partial encryption mode + data, partially_encrypted = await self._partially_encrypt_fields(data) + if partially_encrypted: + return data, [EncodedPayloadOptions.PARTIALLY_ENCRYPTED] + + return data, [] async def decode_payload_content( self, data: bytes, encoding_options: List[EncodedPayloadOptions] From b407fb818c601e635756f234a0dabb5c1f975204 Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Mon, 4 May 2026 13:07:51 +0200 Subject: [PATCH 3/7] Improvements --- .../client/_hooks/workflow_encoding_hook.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/src/mistralai/client/_hooks/workflow_encoding_hook.py b/src/mistralai/client/_hooks/workflow_encoding_hook.py index 51c519da..1a71731f 100644 --- a/src/mistralai/client/_hooks/workflow_encoding_hook.py +++ b/src/mistralai/client/_hooks/workflow_encoding_hook.py @@ -165,9 +165,6 @@ async def _decrypt_event_attributes( payload_encoder: PayloadEncoder, ) -> Dict[str, Any]: """Decrypt payload fields in event attributes. - - Identifies encryptable fields by their type structure (JSONPayload or JSONPatchPayload) - rather than by field name. """ for field_name, field_value in attributes.items(): if not _is_payload_type(field_value): @@ -261,10 +258,6 @@ def before_request( execution_id=execution_id, ) - logger.debug( - "WorkflowEncodingHook: Encoding input for %s", hook_ctx.operation_id - ) - encoded_input = _run_async( encoding_config.payload_encoder.encode_network_input( input_data, context @@ -302,16 +295,8 @@ def after_success( response: httpx.Response, ) -> Union[httpx.Response, Exception]: """Intercept responses to decode workflow result payloads and event payloads.""" - logger.debug( - "WorkflowEncodingHook.after_success called for %s", - hook_ctx.operation_id, - ) encoding_config = _get_encoding_config(hook_ctx.config) if not encoding_config: - logger.debug( - "WorkflowEncodingHook: No encoding config for %s", - hook_ctx.operation_id, - ) return response content_type = response.headers.get("content-type", "") @@ -331,11 +316,6 @@ def after_success( ): return response - logger.debug( - "WorkflowEncodingHook: Decoding result for %s", - hook_ctx.operation_id, - ) - decoded_result = _run_async( encoding_config.payload_encoder.decode_network_result(result) ) @@ -358,12 +338,6 @@ def after_success( if hook_ctx.operation_id in OPERATIONS_DECODE_EVENTS: try: body = json.loads(response.content) - - logger.debug( - "WorkflowEncodingHook: Decoding events for %s", - hook_ctx.operation_id, - ) - body = _run_async( _decrypt_events_in_response(body, encoding_config.payload_encoder) ) From 2804d5e44386c206a55ffbada85ddc57ddaabc88 Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Mon, 4 May 2026 17:02:12 +0200 Subject: [PATCH 4/7] Decryption for event stream endpoint --- .../client/_hooks/workflow_encoding_hook.py | 105 +++++++++++++++++- uv.lock | 2 +- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/mistralai/client/_hooks/workflow_encoding_hook.py b/src/mistralai/client/_hooks/workflow_encoding_hook.py index 1a71731f..debf0d67 100644 --- a/src/mistralai/client/_hooks/workflow_encoding_hook.py +++ b/src/mistralai/client/_hooks/workflow_encoding_hook.py @@ -5,9 +5,10 @@ import re import uuid import weakref -from typing import Any, Coroutine, Dict, Optional, TypeVar, Union +from typing import Any, AsyncIterator, Coroutine, Dict, Optional, TypeVar, Union import httpx +from httpx._types import AsyncByteStream from .types import ( AfterSuccessContext, @@ -103,7 +104,11 @@ def _get_encoding_config( # Operations that return event data that may need decryption OPERATIONS_DECODE_EVENTS = { "get_workflow_events_v1_workflows_events_list_get", - "get_workflow_execution_history_v1_workflows_executions__execution_id__history_get", +} + +# Streaming operations that return SSE event data that may need decryption +OPERATIONS_DECODE_EVENTS_STREAM = { + "get_stream_events_v1_workflows_events_stream_get", } SCHEDULE_CORRELATION_ID_PLACEHOLDER = "__scheduled_workflow__" @@ -164,8 +169,7 @@ async def _decrypt_event_attributes( attributes: Dict[str, Any], payload_encoder: PayloadEncoder, ) -> Dict[str, Any]: - """Decrypt payload fields in event attributes. - """ + """Decrypt payload fields in event attributes.""" for field_name, field_value in attributes.items(): if not _is_payload_type(field_value): continue @@ -200,6 +204,90 @@ async def _decrypt_events_in_response( return body +def _decrypt_sse_line(line: bytes, payload_encoder: PayloadEncoder) -> bytes: + """Decrypt event payloads in an SSE data line.""" + if not line.startswith(b"data:"): + return line + + try: + data_part = line[5:].strip() + if not data_part: + return line + + event_wrapper = json.loads(data_part) + data = event_wrapper.get("data") + if not isinstance(data, dict): + return line + + attributes = data.get("attributes") + if not isinstance(attributes, dict): + return line + + # Decrypt in place - _decrypt_event_attributes modifies attributes dict + _run_async(_decrypt_event_attributes(attributes, payload_encoder)) + + return b"data: " + json.dumps(event_wrapper).encode("utf-8") + except (json.JSONDecodeError, Exception) as e: + logger.debug("SSE line decryption failed: %s", e) + return line + + +class _DecryptingAsyncByteStream(AsyncByteStream): + """Async byte stream wrapper that decrypts SSE event payloads.""" + + def __init__(self, original_stream: Any, payload_encoder: PayloadEncoder): + self._original = original_stream + self._payload_encoder = payload_encoder + self._buffer = b"" + + async def __aiter__(self) -> AsyncIterator[bytes]: + async for chunk in self._original: + for processed in self._process_chunk(chunk): + yield processed + # Flush remaining buffer + if self._buffer: + yield _decrypt_sse_line(self._buffer, self._payload_encoder) + + def _process_chunk(self, chunk: bytes): + self._buffer += chunk + lines = self._buffer.split(b"\n") + # Keep last incomplete line in buffer + self._buffer = lines[-1] + for line in lines[:-1]: + yield _decrypt_sse_line(line, self._payload_encoder) + b"\n" + + async def aclose(self) -> None: + if hasattr(self._original, "aclose"): + await self._original.aclose() + + +def _wrap_sse_response_with_decryption( + response: httpx.Response, + payload_encoder: PayloadEncoder, +) -> httpx.Response: + """Wrap an SSE response to decrypt event payloads as they stream. + + Creates a new response with a custom stream that decrypts payloads on-the-fly. + """ + # Get the original stream from the response + original_stream = response.stream + + # Create wrapped stream + decrypting_stream = _DecryptingAsyncByteStream(original_stream, payload_encoder) + + # Create new response with wrapped stream + # Use internal _content to avoid reading stream + new_response = httpx.Response( + status_code=response.status_code, + headers=response.headers, + stream=decrypting_stream, + request=response.request, + extensions=response.extensions, + ) + + return new_response + + class WorkflowEncodingHook(BeforeRequestHook, AfterSuccessHook): """Hook for encoding/decoding workflow event payloads. @@ -300,6 +388,15 @@ def after_success( return response content_type = response.headers.get("content-type", "") + + # Handle SSE stream decryption + if hook_ctx.operation_id in OPERATIONS_DECODE_EVENTS_STREAM: + if "text/event-stream" in content_type: + return _wrap_sse_response_with_decryption( + response, encoding_config.payload_encoder + ) + return response + if "application/json" not in content_type: return response diff --git a/uv.lock b/uv.lock index 52b3440f..8f07ac6b 100644 --- a/uv.lock +++ b/uv.lock @@ -1015,7 +1015,7 @@ wheels = [ [[package]] name = "mistralai" -version = "2.4.2" +version = "2.4.4" source = { editable = "." } dependencies = [ { name = "eval-type-backport" }, From 69d5d924f2cd3d91f65900bd5a148891d2868d5f Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Mon, 4 May 2026 17:23:07 +0200 Subject: [PATCH 5/7] Add new route to stream decoding --- src/mistralai/client/_hooks/workflow_encoding_hook.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mistralai/client/_hooks/workflow_encoding_hook.py b/src/mistralai/client/_hooks/workflow_encoding_hook.py index debf0d67..03dd54bb 100644 --- a/src/mistralai/client/_hooks/workflow_encoding_hook.py +++ b/src/mistralai/client/_hooks/workflow_encoding_hook.py @@ -109,6 +109,7 @@ def _get_encoding_config( # Streaming operations that return SSE event data that may need decryption OPERATIONS_DECODE_EVENTS_STREAM = { "get_stream_events_v1_workflows_events_stream_get", + "stream_v1_workflows_executions__execution_id__stream_get", } SCHEDULE_CORRELATION_ID_PLACEHOLDER = "__scheduled_workflow__" From cbcabdcf7e2f73f1537464ef65ae3c4f13ede410 Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Tue, 5 May 2026 15:58:35 +0200 Subject: [PATCH 6/7] Fix a generation issue --- .speakeasy/gen.lock | 14 ++++---- .speakeasy/gen.yaml | 2 +- .speakeasy/workflow.lock | 2 +- .../client/_hooks/workflow_encoding_hook.py | 34 +++++++++---------- src/mistralai/client/_version.py | 4 +-- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index 7a1d3ff8..03b5bafa 100644 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -5,15 +5,15 @@ management: docVersion: 1.0.0 speakeasyVersion: 1.761.1 generationVersion: 2.879.6 - releaseVersion: 2.4.5 - configChecksum: f6852fb59e3bcc9a7750210521f2c2d4 + releaseVersion: 2.4.6 + configChecksum: 49adf68e99fa9f97295f7d2573756c6b repoURL: https://github.com/mistralai/client-python.git installationURL: https://github.com/mistralai/client-python.git published: true persistentEdits: - generation_id: e62d9764-a475-4217-b15c-339df5940f3f - pristine_commit_hash: 081e214c8cf02efca9607c9f2c0e224a453b9f25 - pristine_tree_hash: 1e5d4803268ab0dfc34daf3e0d00a5a950cd7737 + generation_id: 5269a0d2-aab8-4e30-89b0-34c4a1e06a03 + pristine_commit_hash: 86fdb3deecc042da4373f0001c0d5a4d4b30a806 + pristine_tree_hash: 1956575b1839536573ed965cede30916dd3b6a5a features: python: acceptHeaders: 3.0.0 @@ -3343,8 +3343,8 @@ trackedFiles: pristine_git_object: 036d44b8cfc51599873bd5c401a6aed30450536c src/mistralai/client/_version.py: id: cc807b30de19 - last_write_checksum: sha1:6b2772cd63b60cddf4ea95d94cfc44f81a878a73 - pristine_git_object: 9f9ae8c6224af17b3e22410b33f5a5ad37389e33 + last_write_checksum: sha1:b60d9f81024f0a37e4137751a7e7e9e323f2b9b8 + pristine_git_object: d4969426e57c58605d8874bcb7418d7eecdd9dd7 src/mistralai/client/accesses.py: id: 76fc53bfcf59 last_write_checksum: sha1:de197fbbfea8bc95f44b4e7ee1b39e68fdde8bc7 diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index a246d5a7..ff877688 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -32,7 +32,7 @@ generation: generateNewTests: false skipResponseBodyAssertions: false python: - version: 2.4.5 + version: 2.4.6 additionalDependencies: dev: pytest: ^8.2.2 diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index 7f4be3ae..974d3b5b 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -41,7 +41,7 @@ targets: sourceRevisionDigest: sha256:1f3eeb2513538e3ae8b5483477b559a460ce3dac0cdd41c31d47db9167acec60 sourceBlobDigest: sha256:b743e6e51b23d2d5207d1f4348076d22f130052fea4e1676095a2ddc654822a9 codeSamplesNamespace: mistral-openapi-code-samples - codeSamplesRevisionDigest: sha256:1b67135a0ef0b6ae9bd28992b5cb642d7868eeed2c46b3f552cb940f6c635688 + codeSamplesRevisionDigest: sha256:f6f821363470675b275221f114275f0f7adf6f09dce005effa439ce0b883ea32 workflow: workflowVersion: 1.0.0 speakeasyVersion: 1.761.1 diff --git a/src/mistralai/client/_hooks/workflow_encoding_hook.py b/src/mistralai/client/_hooks/workflow_encoding_hook.py index 03dd54bb..d65c3ff4 100644 --- a/src/mistralai/client/_hooks/workflow_encoding_hook.py +++ b/src/mistralai/client/_hooks/workflow_encoding_hook.py @@ -407,33 +407,31 @@ def after_success( body = json.loads(response.content) result = body.get("result") if ( - result is None - or not encoding_config.payload_encoder.check_is_payload_encoded( + result is not None + and encoding_config.payload_encoder.check_is_payload_encoded( result ) ): - return response - - decoded_result = _run_async( - encoding_config.payload_encoder.decode_network_result(result) - ) + decoded_result = _run_async( + encoding_config.payload_encoder.decode_network_result(result) + ) - body["result"] = decoded_result - new_content = json.dumps(body).encode("utf-8") + body["result"] = decoded_result + new_content = json.dumps(body).encode("utf-8") - return httpx.Response( - status_code=response.status_code, - headers=response.headers, - content=new_content, - request=response.request, - extensions=response.extensions, - ) + response = httpx.Response( + status_code=response.status_code, + headers=response.headers, + content=new_content, + request=response.request, + extensions=response.extensions, + ) except Exception as e: logger.error("WorkflowEncodingHook: Failed to decode result: %s", e) raise # Handle event payload decoding - if hook_ctx.operation_id in OPERATIONS_DECODE_EVENTS: + elif hook_ctx.operation_id in OPERATIONS_DECODE_EVENTS: try: body = json.loads(response.content) body = _run_async( @@ -441,7 +439,7 @@ def after_success( ) new_content = json.dumps(body).encode("utf-8") - return httpx.Response( + response = httpx.Response( status_code=response.status_code, headers=response.headers, content=new_content, diff --git a/src/mistralai/client/_version.py b/src/mistralai/client/_version.py index 9f9ae8c6..d4969426 100644 --- a/src/mistralai/client/_version.py +++ b/src/mistralai/client/_version.py @@ -4,10 +4,10 @@ import importlib.metadata __title__: str = "mistralai" -__version__: str = "2.4.5" +__version__: str = "2.4.6" __openapi_doc_version__: str = "1.0.0" __gen_version__: str = "2.879.6" -__user_agent__: str = "speakeasy-sdk/python 2.4.5 2.879.6 1.0.0 mistralai" +__user_agent__: str = "speakeasy-sdk/python 2.4.6 2.879.6 1.0.0 mistralai" try: if __package__ is not None: From baac6dd7f1e451b940eb70650558bfdad7d43409 Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Wed, 6 May 2026 11:11:48 +0200 Subject: [PATCH 7/7] Do not trigger SDK generation --- .speakeasy/gen.lock | 16 +++++++++------- .speakeasy/gen.yaml | 2 +- .speakeasy/workflow.lock | 2 +- src/mistralai/client/_version.py | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index 03b5bafa..b62a204c 100644 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -5,15 +5,15 @@ management: docVersion: 1.0.0 speakeasyVersion: 1.761.1 generationVersion: 2.879.6 - releaseVersion: 2.4.6 - configChecksum: 49adf68e99fa9f97295f7d2573756c6b + releaseVersion: 2.4.4 + configChecksum: d237fa5a6bc2a67b0977a61520bd85fb repoURL: https://github.com/mistralai/client-python.git installationURL: https://github.com/mistralai/client-python.git published: true persistentEdits: - generation_id: 5269a0d2-aab8-4e30-89b0-34c4a1e06a03 - pristine_commit_hash: 86fdb3deecc042da4373f0001c0d5a4d4b30a806 - pristine_tree_hash: 1956575b1839536573ed965cede30916dd3b6a5a + generation_id: 8315974b-9a57-4a9f-b6b1-a75627dc6b85 + pristine_commit_hash: 969db46e6806db3556c93296ed4c87a2b6a0c709 + pristine_tree_hash: b900e343a3b555d433748ed99095b1468250aa44 features: python: acceptHeaders: 3.0.0 @@ -61,6 +61,7 @@ trackedFiles: id: 89aa447020cd last_write_checksum: sha1:f84632c81029fcdda8c3b0c768d02b836fc80526 pristine_git_object: 8d79f0abb72526f1fb34a4c03e5bba612c6ba2ae + deleted: true USAGE.md: id: 3aed33ce6e6f last_write_checksum: sha1:d172deb3ee1630f16b279de22aec1f8f68d7565f @@ -3343,8 +3344,8 @@ trackedFiles: pristine_git_object: 036d44b8cfc51599873bd5c401a6aed30450536c src/mistralai/client/_version.py: id: cc807b30de19 - last_write_checksum: sha1:b60d9f81024f0a37e4137751a7e7e9e323f2b9b8 - pristine_git_object: d4969426e57c58605d8874bcb7418d7eecdd9dd7 + last_write_checksum: sha1:9096662e60738fa15fd58b70880e1e8dff442403 + pristine_git_object: 026fdee0423c494d6d525bd1a67d566a76c4fd7d src/mistralai/client/accesses.py: id: 76fc53bfcf59 last_write_checksum: sha1:de197fbbfea8bc95f44b4e7ee1b39e68fdde8bc7 @@ -8703,6 +8704,7 @@ examples: application/json: {} examplesVersion: 1.0.2 generatedTests: {} +releaseNotes: "## Python SDK Changes:\n* `mistral.beta.libraries.update()`: `request.name` **Changed** (Breaking ⚠️)\n* `mistral.beta.libraries.documents.update()`: `request.name` **Changed** (Breaking ⚠️)\n* `mistral.beta.rag.ingestion_pipeline_configurations.update_run_info()`: **Added**\n* `mistral.workflows.schedules.pause_schedule()`: **Added**\n* `mistral.workflows.schedules.resume_schedule()`: **Added**\n* `mistral.beta.rag.ingestion_pipeline_configurations.list()`: `response.[]` **Changed**\n* `mistral.beta.rag.ingestion_pipeline_configurations.register()`: \n * `request.pipeline_composition` **Added**\n * `response` **Changed**\n* `mistral.workflows.schedules.get_schedules()`: `response.schedules[]` **Changed**\n* `mistral.workflows.schedules.schedule_workflow()`: \n * `request.schedule.max_executions` **Added**\n" generatedFiles: - .gitattributes - .vscode/settings.json diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index ff877688..465afa47 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -32,7 +32,7 @@ generation: generateNewTests: false skipResponseBodyAssertions: false python: - version: 2.4.6 + version: 2.4.4 additionalDependencies: dev: pytest: ^8.2.2 diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index 974d3b5b..265ea37e 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -41,7 +41,7 @@ targets: sourceRevisionDigest: sha256:1f3eeb2513538e3ae8b5483477b559a460ce3dac0cdd41c31d47db9167acec60 sourceBlobDigest: sha256:b743e6e51b23d2d5207d1f4348076d22f130052fea4e1676095a2ddc654822a9 codeSamplesNamespace: mistral-openapi-code-samples - codeSamplesRevisionDigest: sha256:f6f821363470675b275221f114275f0f7adf6f09dce005effa439ce0b883ea32 + codeSamplesRevisionDigest: sha256:fa3aa9d30da8a70f9e5d8c74ceab2cd4a043bfa5c5dacfe0035b09d907f41705 workflow: workflowVersion: 1.0.0 speakeasyVersion: 1.761.1 diff --git a/src/mistralai/client/_version.py b/src/mistralai/client/_version.py index d4969426..026fdee0 100644 --- a/src/mistralai/client/_version.py +++ b/src/mistralai/client/_version.py @@ -4,10 +4,10 @@ import importlib.metadata __title__: str = "mistralai" -__version__: str = "2.4.6" +__version__: str = "2.4.4" __openapi_doc_version__: str = "1.0.0" __gen_version__: str = "2.879.6" -__user_agent__: str = "speakeasy-sdk/python 2.4.6 2.879.6 1.0.0 mistralai" +__user_agent__: str = "speakeasy-sdk/python 2.4.4 2.879.6 1.0.0 mistralai" try: if __package__ is not None: