diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock
index 926949e77..e19e16701 100644
--- a/.speakeasy/gen.lock
+++ b/.speakeasy/gen.lock
@@ -1,19 +1,19 @@
lockVersion: 2.0.0
id: 2d045ec7-2ebb-4f4d-ad25-40953b132161
management:
- docChecksum: 88f8765f297eba24ea252acea6ae0346
+ docChecksum: cfe6944e067faa7851832c66c9b4b340
docVersion: 1.0.0
speakeasyVersion: 1.796.4
generationVersion: 2.935.1
- releaseVersion: 3.5.0
- configChecksum: 90c7e2935bf5d87ac9bc5e574594582d
+ releaseVersion: 3.6.0
+ configChecksum: e989c703205a842171b583faeec42be4
repoURL: https://github.com/mistralai/client-python.git
installationURL: https://github.com/mistralai/client-python.git
published: true
persistentEdits:
- generation_id: a76cbf96-e5c8-4cdd-a264-aa1e028d7774
- pristine_commit_hash: 386ad02302add3e05953b33916d1bddff2e4c3d3
- pristine_tree_hash: 6eb983533a728d0f19c4efc5ac7f43054c6228df
+ generation_id: 53136f90-a575-4cae-b8d9-b64cd15c6143
+ pristine_commit_hash: d18bece5899675225816b484a4335ed500dc94e1
+ pristine_tree_hash: 6fc1736b4a815dcf15ba0bfe80ed6804ad66aac2
features:
python:
acceptHeaders: 3.0.0
@@ -820,10 +820,6 @@ trackedFiles:
id: 458248f721bb
last_write_checksum: sha1:d4591e7b6f6e841cffc67d1821bd0b3258ff4b1d
pristine_git_object: e979caf1e36471e2fc485ed096fd29e3cda3582e
- docs/models/completiontokensdetails.md:
- id: 72beafa9a4c8
- last_write_checksum: sha1:79d4cd281e9247b16f381800938fb302752df5b2
- pristine_git_object: fec4195551396423a67aab09606a7fdc1b26a3f1
docs/models/computefilteredrunstatisticsv2v1observabilityevaluationrunsv2runidstatisticscomputepostrequest.md:
id: 44374d81fb36
last_write_checksum: sha1:3f11891cf8ffc16b10cb348eebb0c0113faa8b94
@@ -846,8 +842,8 @@ trackedFiles:
pristine_git_object: 0a2e25d13afd8d8507afc3c2cbcf6e8656817eee
docs/models/connectionpreference.md:
id: be469697273b
- last_write_checksum: sha1:f59d85167c69837a67f05e190a8efdd2feaa723f
- pristine_git_object: e069233ff9f42a6b13e356985e3bc3819128b970
+ last_write_checksum: sha1:69a6cefd1f899906a65af2effcc9b9c5d5ed7ba3
+ pristine_git_object: 48347b9568a6772d6bfaffb1ae44d549d7f89ca9
docs/models/connector.md:
id: cbf7c2c53983
last_write_checksum: sha1:48b79253125e022074f325f25dd122329890d34c
@@ -3252,10 +3248,6 @@ trackedFiles:
id: 3dd409ff05ba
last_write_checksum: sha1:f7dd5b03360f01417c32a33fd8131e6899ec6430
pristine_git_object: 504aa9de165ea21ffd7178dd092b7532fc3f9e6a
- docs/models/messagetokens.md:
- id: c348c2448aaa
- last_write_checksum: sha1:745a41b9189a9a48cbde2838bf9432e60bb9df62
- pristine_git_object: 79b126d9a6a72f84fa491417bff0f891206f59b4
docs/models/metadata.md:
id: ad63dba5a4f8
last_write_checksum: sha1:ff2c890fd9527ba5ecae6fc1f28609eac06c44d9
@@ -3442,8 +3434,8 @@ trackedFiles:
pristine_git_object: 22e2804a4424325ad07c264a61463635299fc254
docs/models/observabilityerrorcode.md:
id: 61d16ff95b87
- last_write_checksum: sha1:dfa87efabba4a093f43605b803bf8fe16d9adfe9
- pristine_git_object: 314a516353e9a844677a397ffb4f53273704a025
+ last_write_checksum: sha1:c7ad15b7169b61165ce5ec96048f4810c862fc47
+ pristine_git_object: ee72a767a23a06437ce3ed94518ac979964131e7
docs/models/observabilityerrordetail.md:
id: 38061447dfbb
last_write_checksum: sha1:1da7bf6e72c2f972b0e22f0201a1cb9b76f53ccd
@@ -3856,10 +3848,6 @@ trackedFiles:
id: 327de0066e86
last_write_checksum: sha1:db07389f762ea8e69bd6f661d9e7f14f38093806
pristine_git_object: f46e47a0e9389830cda6385a6f9cddf4bcdc9aac
- docs/models/prompttokensdetails.md:
- id: 1391892fce0a
- last_write_checksum: sha1:d6409b4ded3c282050b3751d1c535f6f428ccacb
- pristine_git_object: 9614a436bf49c19500bc829f2ac1d65929eb7375
docs/models/promptvariable.md:
id: 2e9f9dbaf1a3
last_write_checksum: sha1:6fbb6fe14fcaad4e86189c876c0a9338f4edba9a
@@ -3916,6 +3904,54 @@ trackedFiles:
id: 1323016410bd
last_write_checksum: sha1:553cf2fa061b2ac0cd52d04c287927d7d8f01b10
pristine_git_object: 657af9de97a1863eef9f9484303b939ad854fe01
+ docs/models/realtimespeecherror.md:
+ id: db5ede717a0c
+ last_write_checksum: sha1:10d7e6f4da88cb3294dbe16bc6d70697ef2a43f5
+ pristine_git_object: 24ea9672a70c1a2e5d1bd8636c0e266584722ecb
+ docs/models/realtimespeecherrordetail.md:
+ id: 69e8335daf17
+ last_write_checksum: sha1:363a4b197b646d459064db72b4afe46fff924b86
+ pristine_git_object: 393fbd62410fbc56724d113d5b462b7fb60ffa77
+ docs/models/realtimespeecherrordetailmessage.md:
+ id: 161fc46cd8c0
+ last_write_checksum: sha1:ce0869376d388813c0ef0170e0cfd37c60a6dc7c
+ pristine_git_object: 281c50060dd37bca3d71564185ea535d75b2fe40
+ docs/models/realtimespeechinputtextappend.md:
+ id: 381fbbedd861
+ last_write_checksum: sha1:c6b1d339dd8a8a8aaf74d6c684233c8620dcbefa
+ pristine_git_object: 96d5f813e54b92a52df1aa6ccd61fc6ac717f8d8
+ docs/models/realtimespeechinputtextend.md:
+ id: 702831e13b28
+ last_write_checksum: sha1:0c442cb7685c3e4b8ea3c9836a40604bf2e74612
+ pristine_git_object: 936a64bd469c5bbbb8d86476546d2c3a5108126b
+ docs/models/realtimespeechinputtextflush.md:
+ id: 8d69f70645f1
+ last_write_checksum: sha1:d693df10221d0eea6e4c210033d24132ecd353c9
+ pristine_git_object: 48041c5d5d1662dd28f1c450d41dcc86db496dcd
+ docs/models/realtimespeechsession.md:
+ id: 28b2f5a82537
+ last_write_checksum: sha1:e9ceb29414b8e0d2e7772ad9e93b2a710f2972c3
+ pristine_git_object: 256b2bfdd18c406cc2dfaef3d08861ca112cad7e
+ docs/models/realtimespeechsessioncreated.md:
+ id: 6a5abd07b3f5
+ last_write_checksum: sha1:22de922e7be85645946e42aedab732f3a2a4be5b
+ pristine_git_object: 169a9c017d095a9dc1f95420f552302de0fef391
+ docs/models/realtimespeechsessionupdated.md:
+ id: e47d1889c765
+ last_write_checksum: sha1:042f6076307379a2811499d93cceb902e4f932e5
+ pristine_git_object: 0a38f78f9b77e29e6a1333a5f284c034631c94e5
+ docs/models/realtimespeechsessionupdatemessage.md:
+ id: b4270a54bbb9
+ last_write_checksum: sha1:4fc5a99f2b24db294ffd8cd4139c644625eac212
+ pristine_git_object: 6f96b67c2b112ef3b4253d5eb856c19ce1ec59b2
+ docs/models/realtimespeechsessionupdatepayload.md:
+ id: a9ee3a4cd31a
+ last_write_checksum: sha1:66967ecf8d6d7be1f2a39ff84ebba68895764c7d
+ pristine_git_object: b931680851ddd15e5c22caae3284956c77dafb1a
+ docs/models/realtimespeechstreamingmode.md:
+ id: 0fde6b6275cd
+ last_write_checksum: sha1:d376a19ffa871b3375b33a517e17527bc46454d5
+ pristine_git_object: 2c388e6f5886e7eef5adab87df07b07b092136d1
docs/models/realtimetranscriptionerror.md:
id: 4bc5e819565b
last_write_checksum: sha1:c93e4b19a0aa68723ea69973a9f22a581c7b2ff6
@@ -4536,10 +4572,6 @@ trackedFiles:
id: ac567924689c
last_write_checksum: sha1:7534c5ec5f1ae1e750c8f610f81f2106587e81a9
pristine_git_object: f1280f8862e9d3212a5cfccd9453884b4055710a
- docs/models/roles.md:
- id: 2af79e204ed6
- last_write_checksum: sha1:10354a9bcbe03a498848de9874b3163cde0fcc05
- pristine_git_object: d722e7654427a013da70deb45f1bdb65b55add30
docs/models/rootscapability.md:
id: 7cc1a16f90b4
last_write_checksum: sha1:68d66251fe9a079c0c0c78853699009c378ddef5
@@ -4978,8 +5010,8 @@ trackedFiles:
pristine_git_object: 3a1725529f737b69ecfe147f1619c05896d0c2a4
docs/models/speechstreamdone.md:
id: 660654bb6961
- last_write_checksum: sha1:460b0891989440add3f7f0e802b5744e343249cb
- pristine_git_object: d9b5e0c0c9deef93a2ff79d46a026766c4aef673
+ last_write_checksum: sha1:b22cea97446a13b5be8bd1bce302eabf77d69130
+ pristine_git_object: 12c38c7e2fc0ce775a3f728b62f901afd039f688
docs/models/speechstreamevents.md:
id: d071f3e7ecda
last_write_checksum: sha1:48806c50f933cdf9331fafe5a622717147fe9427
@@ -5764,10 +5796,6 @@ trackedFiles:
id: ec6fe65028a9
last_write_checksum: sha1:19977a2bb11140ff61aaf762e5ebf8a60c8a050f
pristine_git_object: fb460774fe5e9affe5d1c0e6795ed6255cf06587
- docs/models/usageinfodollardefs.md:
- id: db2412f29163
- last_write_checksum: sha1:618cb870e5cb3bc6e612908b118c1bce075391e0
- pristine_git_object: 8f1f228d20f855505f9dfd33c627a6f0d81b88d5
docs/models/useridentity.md:
id: 959b5b6bb365
last_write_checksum: sha1:57c19208b5cccac6058c850e34f98bdb7d6b04e9
@@ -6434,8 +6462,8 @@ trackedFiles:
pristine_git_object: 5052bb072d14c65419bf49bdc1a47500cb03ae56
src/mistralai/client/_version.py:
id: cc807b30de19
- last_write_checksum: sha1:7c09a778d62000c4bca913828c94da1591087072
- pristine_git_object: b073af7fd1283d3fa1c1ea82ade8815167e43f62
+ last_write_checksum: sha1:eb2f38c76b6186585c38f1a15cb08e729b316504
+ pristine_git_object: 729425f91764a66b7771d3c38a92436dc4c640f5
src/mistralai/client/accesses.py:
id: 76fc53bfcf59
last_write_checksum: sha1:414c7375fcd63266d196840fa8d1274d60aae2dc
@@ -6598,16 +6626,16 @@ trackedFiles:
pristine_git_object: e7755cdc9230bbb9a87fff16930099911959c558
src/mistralai/client/managed_indexes.py:
id: b6dd0fca653d
- last_write_checksum: sha1:f61b7f33f06494f6dc687165204c57558e6af6b4
- pristine_git_object: 4ffd52d4a9b964a9d98b91eeacaca0c938acc432
+ last_write_checksum: sha1:26c6d9a04c1b87e60221efbb1cef9a4b69bb0c38
+ pristine_git_object: 0e5b80e29cb9acf864055c4ae8394c9421bc4a30
src/mistralai/client/metrics.py:
id: 937cb03f8130
last_write_checksum: sha1:461f34f2e6b943241208de01bbbf5f57ec6a31b4
pristine_git_object: 41d7f799e8a2afa860b4193f7752e094f1c624b2
src/mistralai/client/models/__init__.py:
id: e0e8dad92725
- last_write_checksum: sha1:5ed9fa555fef2f9d62f14e1498d40cf9ccf6ebfc
- pristine_git_object: 67090a69a82505390159fc39af19be1cf24609ef
+ last_write_checksum: sha1:f9019f990040b4bdc89401030f4a2d489214cd8a
+ pristine_git_object: 4c21494c59eefea68c1c910af8094ab9654a2a25
src/mistralai/client/models/accepteddocumentresponse.py:
id: f529b784daff
last_write_checksum: sha1:2eb3c7dc0d7165987aefda841b1ceada6698c15d
@@ -7168,10 +7196,6 @@ trackedFiles:
id: fdd27ba32f7c
last_write_checksum: sha1:6b4a5aa8545f6807625ad44d645551a054ccd9cc
pristine_git_object: 5b0688a0398b51e50fa4c8ea9b741c61d2beb150
- src/mistralai/client/models/completiontokensdetails.py:
- id: 8e822f11a1b2
- last_write_checksum: sha1:ac75a68cd2d87926a4f751833b036098accf67ed
- pristine_git_object: f253decf8e24fff25256a323b7288b27a33fe7ca
? src/mistralai/client/models/compute_filtered_run_statistics_v2_v1_observability_evaluation_runs_v2_run_id_statistics_compute_postop.py
: id: a9bcc643cf6a
last_write_checksum: sha1:6c3f7e18a33fe11a4604600398a7b317e210a71e
@@ -7186,8 +7210,8 @@ trackedFiles:
pristine_git_object: 13a20107f4a4e60dd569db435df4a9c610e9a993
src/mistralai/client/models/connectionpreference.py:
id: 6b9389ac91e6
- last_write_checksum: sha1:586106310d6ec2ce38e7d1aefb0a37cb71534ee8
- pristine_git_object: fbca39a4f13d80041161d4f0a35efbc170d3eea6
+ last_write_checksum: sha1:50cb67daee6c0f20e5eca35466ab61b5b06ee3ea
+ pristine_git_object: 8e0bcf220c25b1df84a7584faac325873cab1972
src/mistralai/client/models/connector.py:
id: 1a4facac922d
last_write_checksum: sha1:ab248d0d9facc036b568078130ab576601a07bdb
@@ -9060,10 +9084,6 @@ trackedFiles:
id: 6b388bc155dd
last_write_checksum: sha1:668d73bde9650a7642fd71c61b7383cb9be8abf6
pristine_git_object: 42fb7b7fcc14ec779408bbcbce2a2151ad7536f5
- src/mistralai/client/models/messagetokens.py:
- id: e2a5171d27c5
- last_write_checksum: sha1:af37f98ca55f6ba5373ed538e68f88e1e9f3e612
- pristine_git_object: 7326311b6d45c56cbc6c2c513e27826a3e2b943e
src/mistralai/client/models/metadatafilter.py:
id: 119677f7c52c
last_write_checksum: sha1:1cefa7882d18ad41b6ff6887c16ba56932006ed0
@@ -9214,8 +9234,8 @@ trackedFiles:
pristine_git_object: 91c2360d5d2c02e4391590a61ea4f6b0af5e24ec
src/mistralai/client/models/observabilityerrorcode.py:
id: ae572b470a30
- last_write_checksum: sha1:a2152859dba9f1bf6b891312b59c37bccc135e21
- pristine_git_object: 8f3bf3e8f29dd23db264513760de6c8fd1d738b0
+ last_write_checksum: sha1:5776ff19686908459ab073616c4a51da5988eb12
+ pristine_git_object: e036b0d3fe4fd59533a69320c96cedab5184de8f
src/mistralai/client/models/observabilityerrordetail.py:
id: cb6e8a484a38
last_write_checksum: sha1:f77796b6fbcb1e7e7a87dc25dd52cf1f9474510c
@@ -9568,10 +9588,6 @@ trackedFiles:
id: 15c552f53adf
last_write_checksum: sha1:40c135b89809978ca26628e66af4277138f662f8
pristine_git_object: c75e8c237ef7be829227b332d53dc5938bf13d45
- src/mistralai/client/models/prompttokensdetails.py:
- id: e6be33f2cd2d
- last_write_checksum: sha1:c2cd2bab5863224ea26f932b180e37b9992d2d74
- pristine_git_object: d9204fcdca4ec825f60e40e6de46bdd8162c9916
src/mistralai/client/models/promptvariable.py:
id: c86c2b89e3ce
last_write_checksum: sha1:0efc25a3bc4aa3c103a79e606519873ec2440356
@@ -9620,6 +9636,50 @@ trackedFiles:
id: 8ef0ba975e25
last_write_checksum: sha1:b74353dfa67c9c373adc473c5ffb9d31f819169b
pristine_git_object: d318e18465c8eaac9a61b2c75cd30c30749b0456
+ src/mistralai/client/models/realtimespeecherror.py:
+ id: f3233c538690
+ last_write_checksum: sha1:315f9e974b81afbb292c66f5d3feaed6d83492bb
+ pristine_git_object: 1dc232a9d990b2d14bd5ef7b37bc1df86a3cdc1d
+ src/mistralai/client/models/realtimespeecherrordetail.py:
+ id: 852eea0ef272
+ last_write_checksum: sha1:c80a509db977d1a5da5b223f7491c3478f5de35f
+ pristine_git_object: f3e747f5f4de58bc3a19080ce88191f54ea85c1c
+ src/mistralai/client/models/realtimespeechinputtextappend.py:
+ id: 95c5a18e566b
+ last_write_checksum: sha1:c182504907843c01747944e8b9c4c99caceeddaf
+ pristine_git_object: 994e160ceb70248dcaa9530deb778864c921d42e
+ src/mistralai/client/models/realtimespeechinputtextend.py:
+ id: 797090a803c8
+ last_write_checksum: sha1:ab6a036e00bcc0ad599c9a2986e9b30e335e7e1c
+ pristine_git_object: 34b6b97c8e334f2857b456bdb1dcf90b66284313
+ src/mistralai/client/models/realtimespeechinputtextflush.py:
+ id: ff816674b0d7
+ last_write_checksum: sha1:f87e61ecae4a55fa50c922fbd965a68fcff1fe73
+ pristine_git_object: 1dec12ab5f01c68089eac3922b98f5de88317247
+ src/mistralai/client/models/realtimespeechsession.py:
+ id: 0b8c41568fce
+ last_write_checksum: sha1:d2fc2309cf8f7d58f7da925e1f65ee8dc3af3d24
+ pristine_git_object: 8a4ea97c2badc51ea7588085e7378fcb7930810d
+ src/mistralai/client/models/realtimespeechsessioncreated.py:
+ id: 46cd4ef13e0e
+ last_write_checksum: sha1:c165056e010506590c0cb10976a2e9301481bf35
+ pristine_git_object: 28e26ee28670ab5308038d4a28d95a17aeea4f6c
+ src/mistralai/client/models/realtimespeechsessionupdated.py:
+ id: ecd9359dee85
+ last_write_checksum: sha1:cc0ca6ffe38d81798b93bbc9da7240aeb78e4bac
+ pristine_git_object: e5043ae5a54c009000ea894da3f63863db1c4629
+ src/mistralai/client/models/realtimespeechsessionupdatemessage.py:
+ id: f2c064a777b2
+ last_write_checksum: sha1:80dba67b81bee6a9bb1f41fe0ec62058be46e1b5
+ pristine_git_object: fbb541fa5615d4dff2823c09fd5da35c219c1578
+ src/mistralai/client/models/realtimespeechsessionupdatepayload.py:
+ id: 3f0e12441b91
+ last_write_checksum: sha1:ade5311cddbbac00ed935236ba17b40112ced229
+ pristine_git_object: 0feae7e820a84c72bb28a97a268a3c48b3d578d7
+ src/mistralai/client/models/realtimespeechstreamingmode.py:
+ id: ee52fdab8618
+ last_write_checksum: sha1:3702c8d3b3706fd1cac8eceaf7d6750b4fd443b3
+ pristine_git_object: e4c169cf76f9b2d77fb9de2f52ad1167756c45ac
src/mistralai/client/models/realtimetranscriptionerror.py:
id: 8c2267378f48
last_write_checksum: sha1:baf9963ef1a7c5ed3ca3958859237b3d6b309dbb
@@ -9980,10 +10040,6 @@ trackedFiles:
id: 6fefa90ca351
last_write_checksum: sha1:6727373da253fb3da395681c167264141771d9cd
pristine_git_object: eaaf010c5556472fa94983033984faec79fd2afa
- src/mistralai/client/models/roles.py:
- id: e41c04cf7422
- last_write_checksum: sha1:67cf30898ea50742c1271ea6d825abafaca630f1
- pristine_git_object: 42263b5d1c4317d7e991182f386ab83d797478f4
src/mistralai/client/models/rootscapability.py:
id: f1514b6dd416
last_write_checksum: sha1:8be6c7b90f486f80843394e31dc4daceb64336de
@@ -10358,8 +10414,8 @@ trackedFiles:
pristine_git_object: 7fdd1b5a52cbbeeea563cec24fc09d59a06e5ef4
src/mistralai/client/models/speechstreamdone.py:
id: 2164b6526a81
- last_write_checksum: sha1:ff4611cb50b27b1dfed76434e6e849154a4659da
- pristine_git_object: 5d29dd86cd0dfc592f6f51662ebf8d0f276e824f
+ last_write_checksum: sha1:b66df6bdce8af49e74146db00c516fc22f48cc2b
+ pristine_git_object: c435a4ae0ec0e3c0b3531d3036362ae1c94e0f62
src/mistralai/client/models/speechstreameventtypes.py:
id: b76eee4fdf6a
last_write_checksum: sha1:b58401574b545377bd720689987ce1f4e0039781
@@ -10956,10 +11012,6 @@ trackedFiles:
id: 54adb9a3af16
last_write_checksum: sha1:a4a4fcf6064ad55e9495258241e495a0a50f1c46
pristine_git_object: e8978e8522246ca127853f907968f290635783a3
- src/mistralai/client/models/usageinfo_dollar_defs.py:
- id: d543506af998
- last_write_checksum: sha1:79b2cdf708c61b623780e3bef926022b61da0763
- pristine_git_object: 25ffc4e40bd84e0ace846975a6c0140bb3ee3e24
src/mistralai/client/models/useridentity.py:
id: 8b1effdfe1ba
last_write_checksum: sha1:2b131fbb269cce463f9758cd2a97025aff0aef26
@@ -15986,7 +16038,78 @@ examples:
application/json: {"input_tokens": 363814, "object": "response.input_tokens"}
examplesVersion: 1.0.2
generatedTests: {}
-releaseNotes: "## Python SDK Changes:\n* `mistral.beta.conversations.start()`: \n * `request.completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.conversations.list()`: `response.[].union(ModelConversation).completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.conversations.get()`: `response.union(ModelConversation).completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.conversations.append()`: \n * `request.completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.conversations.restart()`: \n * `request.completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.agents.create()`: \n * `request.completion_args.reasoning_effort.enum(max)` **Added**\n * `response.completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.agents.list()`: `response.[].completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.agents.list_pages()`: `response.data[].completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.agents.get()`: `response.completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.agents.update()`: \n * `request.completion_args.reasoning_effort.enum(max)` **Added**\n * `response.completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.agents.update_version()`: `response.completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.agents.list_versions()`: `response.[].completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.beta.agents.get_version()`: `response.completion_args.reasoning_effort.enum(max)` **Added**\n* `mistral.chat.complete()`: \n * `request.reasoning_effort.enum(max)` **Added**\n* `mistral.agents.complete()`: \n * `request.reasoning_effort.enum(max)` **Added**\n"
+releaseNotes: |
+ ## Python SDK Changes:
+ * `mistral.beta.observability.judges.create()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.judges.list()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.judges.fetch()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.judges.delete()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.judges.update()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.judges.judge_conversation()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.create()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.list()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.fetch()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.delete()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.update()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.list_records()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.create_record()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.import_from_file()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.import_from_playground()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.import_from_spans()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.import_from_dataset_records()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.export_to_jsonl()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.fetch_task()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.list_tasks()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.records.fetch()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.records.delete()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.records.bulk_delete()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.records.judge()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.records.update_payload()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.datasets.records.update_properties()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.create_pipeline()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.list_pipelines()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.get_pipeline()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.update_pipeline()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.delete_pipeline()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.list()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.get()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.projects.list()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.projects.get()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.runs.search()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.runs.search_all()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.runs.get()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.runs.get_statistics()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.runs.compute_statistics()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.records.get()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.records.search()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.optimizations.list()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.optimizations.get()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.evaluations.optimizations.list_trials()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.logs.search()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.logs.list()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.logs.fetch_options()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.traces.search()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.traces.aggregate()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.traces.get_trace_fields()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.traces.get_trace_by_id()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.traces.get_trace_spans()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.traces.fetch_options()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.traces.get_span_by_id()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.search_spans()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.aggregate()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.aggregate_span_evaluations()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.search_span_evaluations()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.search_latest_span_evaluations()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.search_pipeline_results()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.list_span_fields()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.list_span_eval_fields()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.fetch_span_field_options()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.observability.spans.fetch_span_eval_field_options()`: `error.detail.error_code.enum(optimization_trial_observation_already_exists)` **Added**
+ * `mistral.beta.connectors.create()`: `response.union(mcp).connection_preferences[].name` **Changed**
+ * `mistral.beta.connectors.list()`: `response.items[].union(mcp).connection_preferences[].name` **Changed**
+ * `mistral.beta.connectors.get()`: `response.union(mcp).connection_preferences[].name` **Changed**
+ * `mistral.beta.connectors.update()`: `response.union(http).connection_preferences[].name` **Changed**
+ * `mistral.beta.rag.managed_indexes.create()`: `error.status[403]` **Added**
generatedFiles:
- .gitattributes
- .vscode/settings.json
diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml
index 924436fc9..99e0b379a 100644
--- a/.speakeasy/gen.yaml
+++ b/.speakeasy/gen.yaml
@@ -32,7 +32,7 @@ generation:
generateNewTests: false
skipResponseBodyAssertions: false
python:
- version: 3.5.0
+ version: 3.6.0
additionalDependencies:
dev:
pytest: ^8.2.2
diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock
index 603fbdc5e..904917c0b 100644
--- a/.speakeasy/workflow.lock
+++ b/.speakeasy/workflow.lock
@@ -16,8 +16,8 @@ sources:
- speakeasy-mistralai-gcp-sdk-26292830187-1
mistral-openapi:
sourceNamespace: mistral-openapi
- sourceRevisionDigest: sha256:2e71945d25a8d5ef853793bde7979e1bef1749311f1200ada89fcd6ee5348e07
- sourceBlobDigest: sha256:861833fe29d784afb8f378e9c7addea0b7239aebae2bbd5d6da9a08cca6b9e8e
+ sourceRevisionDigest: sha256:a4f1649fc6d293f9cf3652df5b6dea61d16c4824bbc429194f9ec20b9f798a8c
+ sourceBlobDigest: sha256:16df92d5f5fb961cb5677c2db17092202548f7d2aea908d800653d1b5596cd78
tags:
- latest
- 1.0.0
@@ -39,10 +39,10 @@ targets:
mistralai-sdk:
source: mistral-openapi
sourceNamespace: mistral-openapi
- sourceRevisionDigest: sha256:2e71945d25a8d5ef853793bde7979e1bef1749311f1200ada89fcd6ee5348e07
- sourceBlobDigest: sha256:861833fe29d784afb8f378e9c7addea0b7239aebae2bbd5d6da9a08cca6b9e8e
+ sourceRevisionDigest: sha256:a4f1649fc6d293f9cf3652df5b6dea61d16c4824bbc429194f9ec20b9f798a8c
+ sourceBlobDigest: sha256:16df92d5f5fb961cb5677c2db17092202548f7d2aea908d800653d1b5596cd78
codeSamplesNamespace: mistral-openapi-python-code-samples
- codeSamplesRevisionDigest: sha256:cfa2c195f3262d7efbde0b3c45037f0c372faa9baa9c980c6d2c0f5b714e4c93
+ codeSamplesRevisionDigest: sha256:0768cbd3d8153d3e6f7de9fc36f22055898671d601895f2f55b95c0fec812490
workflow:
workflowVersion: 1.0.0
speakeasyVersion: 1.796.4
diff --git a/RELEASES.md b/RELEASES.md
index 031990d52..1e0cae9e3 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -1596,3 +1596,83 @@ Based on:
- [python v3.5.0] .
### Releases
- [PyPI v3.5.0] https://pypi.org/project/mistralai/3.5.0 - .
+
+## 2026-10-27 16:51:27
+### Generated SDK baseline differences
+The published SDK or generator baseline differs from the baseline used by the OpenAPI changelog.
+
+
+Generated surface details
+
+#### Stable compatibility changes
+Removed from the stable API surface (27):
+- `CompletionTokensDetails`
+- `MessageTokens`
+- `PromptTokensDetails`
+- `UsageInfoDollarDefs`
+- `CompletionTokensDetails.reasoning_tokens`
+- `MessageTokens.role`
+- `MessageTokens.settings_tokens`
+- `MessageTokens.total_tokens`
+- `MessageTokens.truncated`
+- `MessageTokens.usage_count`
+- `PromptTokensDetails.audio_tokens`
+- `PromptTokensDetails.cached_tokens`
+- `PromptTokensDetails.messages`
+- `UsageInfoDollarDefs.completion_tokens`
+- `UsageInfoDollarDefs.completion_tokens_details`
+- `UsageInfoDollarDefs.num_cached_tokens`
+- `UsageInfoDollarDefs.prompt_audio_seconds`
+- `UsageInfoDollarDefs.prompt_token_details`
+- `UsageInfoDollarDefs.prompt_tokens`
+- `UsageInfoDollarDefs.prompt_tokens_details`
+- `UsageInfoDollarDefs.request_count`
+- `UsageInfoDollarDefs.service_tier`
+- `UsageInfoDollarDefs.total_tokens`
+- `CompletionTokensDetails.serialize_model`
+- `MessageTokens.serialize_model`
+- ...and 2 more
+#### Stable type changes
+Changed types or signatures on the stable surface (2):
+- `SpeechStreamDone.usage: UsageInfoDollarDefs -> UsageInfo`
+- `SpeechStreamDoneTypedDict.usage: UsageInfoDollarDefsTypedDict -> UsageInfoTypedDict`
+#### Beta surface changes
+Changed types or signatures in beta (3):
+- `ConnectionPreference.is_default: OptionalNullable[bool] -> Annotated[OptionalNullable[bool], pydantic.Field(deprecated='warning: ** DEPRECATED ** - This will be removed in a future release, please migrate away from it as soon as possible.')]`
+- `ConnectionPreference.name: str -> Annotated[Optional[str], pydantic.Field(deprecated='warning: ** DEPRECATED ** - This will be removed in a future release, please migrate away from it as soon as possible.')]`
+- `ConnectionPreferenceTypedDict.name: str -> NotRequired[str]`
+
+
+### API changes
+#### Breaking changes
+
+- Removed fields `num_cached_tokens`, `prompt_tokens_details`, `completion_tokens_details`, `prompt_token_details`, and `request_count` from response `data.union(speech.audio.done).usage` in `sdk.audio.speech.complete()`.
+
+#### Added operations
+
+Stable:
+
+- `sdk.audio.realtime_speech.connect()`
+- `sdk.audio.realtime_speech.speak_stream()`
+
+#### Schema changes
+
+- Added enum value `optimization_trial_observation_already_exists` across beta observability judges, beta observability datasets, beta observability datasets records, beta observability evaluations, beta observability evaluations projects, beta observability evaluations runs, beta observability evaluations records, beta observability evaluations optimizations, beta observability logs, beta observability traces, beta observability spans (65 operations).
+- Changed response field `name` in `sdk.beta.connectors.list()`, `sdk.beta.connectors.create()`, `sdk.beta.connectors.get()`, and `sdk.beta.connectors.update()`.
+- Added field `status[403]` in `sdk.beta.rag.managed_indexes.create()`.
+- Added request fields `diarize`, `context_bias`, `language`, `speaker_references`, and `turn_detection` in `sdk.audio.realtime.connect()`, `sdk.audio.realtime.transcribe_stream()`, and `sdk.audio.realtime.connect().update_session()`.
+- Added response variants `TurnDetectionStartEvent`, `TurnDetectionEagerEndEvent`, `TurnDetectionResumeEvent`, and `TurnDetectionEndEvent` in `sdk.audio.realtime.connect()` and `sdk.audio.realtime.transcribe_stream()`.
+
+#### Fixes
+
+- `sdk.workflows.schedules.schedule_workflow()` and `sdk.workflows.schedules.update_schedule()` now encrypt `schedule.input` when payload encryption is enabled; it was previously sent in cleartext. `sdk.workflows.schedules.get_schedule()` and `sdk.workflows.schedules.get_schedules()` decode it.
+- Partially encrypted workflow payloads are now serialized as compact JSON, matching the TypeScript SDK.
+
+### Changes
+Based on:
+- OpenAPI Doc
+- Speakeasy CLI 1.796.4 (2.935.1) https://github.com/speakeasy-api/speakeasy
+### Generated
+- [python v3.6.0] .
+### Releases
+- [PyPI v3.6.0] https://pypi.org/project/mistralai/3.6.0 - .
diff --git a/docs/models/completiontokensdetails.md b/docs/models/completiontokensdetails.md
deleted file mode 100644
index fec419555..000000000
--- a/docs/models/completiontokensdetails.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# CompletionTokensDetails
-
-Token usage details for the completion.
-
-``reasoning_tokens`` is ``None`` when the provider did not report a count.
-Preserve that distinction instead of inventing a measured zero: ``0`` means
-"reasoning off, confirmed by the provider", ``None`` means "unknown".
-
-
-## Fields
-
-| Field | Type | Required | Description |
-| ----------------------- | ----------------------- | ----------------------- | ----------------------- |
-| `reasoning_tokens` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/docs/models/connectionpreference.md b/docs/models/connectionpreference.md
index e069233ff..48347b956 100644
--- a/docs/models/connectionpreference.md
+++ b/docs/models/connectionpreference.md
@@ -3,9 +3,9 @@
## Fields
-| Field | Type | Required | Description |
-| ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
-| `name` | *str* | :heavy_check_mark: | N/A |
-| `tool_configuration` | [models.ToolExecutionConfiguration](../models/toolexecutionconfiguration.md) | :heavy_check_mark: | N/A |
-| `is_default` | *OptionalNullable[bool]* | :heavy_minus_sign: | N/A |
-| `consumer_type` | [OptionalNullable[models.ConsumerType]](../models/consumertype.md) | :heavy_minus_sign: | N/A |
\ No newline at end of file
+| Field | Type | Required | Description |
+| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `tool_configuration` | [models.ToolExecutionConfiguration](../models/toolexecutionconfiguration.md) | :heavy_check_mark: | N/A |
+| `consumer_type` | [OptionalNullable[models.ConsumerType]](../models/consumertype.md) | :heavy_minus_sign: | N/A |
+| ~~`name`~~ | *Optional[str]* | :heavy_minus_sign: | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible.
Deprecated: always `default`, a connection holds a single preference. |
+| ~~`is_default`~~ | *OptionalNullable[bool]* | :heavy_minus_sign: | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible.
Deprecated: always `true`. |
\ No newline at end of file
diff --git a/docs/models/messagetokens.md b/docs/models/messagetokens.md
deleted file mode 100644
index 79b126d9a..000000000
--- a/docs/models/messagetokens.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# MessageTokens
-
-Information on a single message included in a tokenized prompt as part of an InstructRequest.
-
-
-## Fields
-
-| Field | Type | Required | Description |
-| ---------------------------------- | ---------------------------------- | ---------------------------------- | ---------------------------------- |
-| `role` | [models.Roles](../models/roles.md) | :heavy_check_mark: | N/A |
-| `total_tokens` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A |
-| `settings_tokens` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A |
-| `truncated` | *Optional[bool]* | :heavy_minus_sign: | N/A |
-| `usage_count` | *Optional[int]* | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/docs/models/observabilityerrorcode.md b/docs/models/observabilityerrorcode.md
index 314a51635..ee72a767a 100644
--- a/docs/models/observabilityerrorcode.md
+++ b/docs/models/observabilityerrorcode.md
@@ -63,6 +63,7 @@ This is an open enum. Unrecognized values will not fail type checks.
- `"EVALUATION_NAME_ALREADY_EXISTS"`
- `"OPTIMIZATION_NOT_FOUND"`
- `"OPTIMIZATION_TRIAL_KEY_ALREADY_EXISTS"`
+- `"OPTIMIZATION_TRIAL_OBSERVATION_ALREADY_EXISTS"`
- `"TRACES_FILTER_QUERY_PARSE_ERROR"`
- `"TRACE_NOT_FOUND"`
- `"SPAN_NOT_FOUND"`
diff --git a/docs/models/prompttokensdetails.md b/docs/models/prompttokensdetails.md
deleted file mode 100644
index 9614a436b..000000000
--- a/docs/models/prompttokensdetails.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# PromptTokensDetails
-
-Token usage details for the prompt.
-
-
-## Fields
-
-| Field | Type | Required | Description |
-| -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- |
-| `messages` | List[[models.MessageTokens](../models/messagetokens.md)] | :heavy_minus_sign: | N/A |
-| `cached_tokens` | *Optional[int]* | :heavy_minus_sign: | N/A |
-| `audio_tokens` | *Optional[int]* | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeecherror.md b/docs/models/realtimespeecherror.md
new file mode 100644
index 000000000..24ea9672a
--- /dev/null
+++ b/docs/models/realtimespeecherror.md
@@ -0,0 +1,9 @@
+# RealtimeSpeechError
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
+| `type` | *Optional[Literal["error"]]* | :heavy_minus_sign: | N/A |
+| `error` | [models.RealtimeSpeechErrorDetail](../models/realtimespeecherrordetail.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeecherrordetail.md b/docs/models/realtimespeecherrordetail.md
new file mode 100644
index 000000000..393fbd624
--- /dev/null
+++ b/docs/models/realtimespeecherrordetail.md
@@ -0,0 +1,9 @@
+# RealtimeSpeechErrorDetail
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| `message` | [models.RealtimeSpeechErrorDetailMessage](../models/realtimespeecherrordetailmessage.md) | :heavy_check_mark: | Human-readable error message. |
+| `code` | *int* | :heavy_check_mark: | Internal error code for debugging. |
\ No newline at end of file
diff --git a/docs/models/realtimespeecherrordetailmessage.md b/docs/models/realtimespeecherrordetailmessage.md
new file mode 100644
index 000000000..281c50060
--- /dev/null
+++ b/docs/models/realtimespeecherrordetailmessage.md
@@ -0,0 +1,19 @@
+# RealtimeSpeechErrorDetailMessage
+
+Human-readable error message.
+
+
+## Supported Types
+
+### `str`
+
+```python
+value: str = /* values here */
+```
+
+### `Dict[str, Any]`
+
+```python
+value: Dict[str, Any] = /* values here */
+```
+
diff --git a/docs/models/realtimespeechinputtextappend.md b/docs/models/realtimespeechinputtextappend.md
new file mode 100644
index 000000000..96d5f813e
--- /dev/null
+++ b/docs/models/realtimespeechinputtextappend.md
@@ -0,0 +1,9 @@
+# RealtimeSpeechInputTextAppend
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- |
+| `type` | *Optional[Literal["input_text.append"]]* | :heavy_minus_sign: | N/A |
+| `text` | *str* | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeechinputtextend.md b/docs/models/realtimespeechinputtextend.md
new file mode 100644
index 000000000..936a64bd4
--- /dev/null
+++ b/docs/models/realtimespeechinputtextend.md
@@ -0,0 +1,8 @@
+# RealtimeSpeechInputTextEnd
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------- | ------------------------------------- | ------------------------------------- | ------------------------------------- |
+| `type` | *Optional[Literal["input_text.end"]]* | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeechinputtextflush.md b/docs/models/realtimespeechinputtextflush.md
new file mode 100644
index 000000000..48041c5d5
--- /dev/null
+++ b/docs/models/realtimespeechinputtextflush.md
@@ -0,0 +1,9 @@
+# RealtimeSpeechInputTextFlush
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| --------------------------------------- | --------------------------------------- | --------------------------------------- | --------------------------------------- |
+| `type` | *Optional[Literal["input_text.flush"]]* | :heavy_minus_sign: | N/A |
+| `text` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeechsession.md b/docs/models/realtimespeechsession.md
new file mode 100644
index 000000000..256b2bfdd
--- /dev/null
+++ b/docs/models/realtimespeechsession.md
@@ -0,0 +1,14 @@
+# RealtimeSpeechSession
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
+| `request_id` | *str* | :heavy_check_mark: | N/A |
+| `model` | *str* | :heavy_check_mark: | N/A |
+| `voice` | *OptionalNullable[str]* | :heavy_minus_sign: | N/A |
+| `is_ref_audio` | *bool* | :heavy_check_mark: | N/A |
+| `streaming_mode` | [models.RealtimeSpeechStreamingMode](../models/realtimespeechstreamingmode.md) | :heavy_check_mark: | N/A |
+| `response_format` | [models.SpeechOutputFormat](../models/speechoutputformat.md) | :heavy_check_mark: | N/A |
+| `expressiveness` | *float* | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeechsessioncreated.md b/docs/models/realtimespeechsessioncreated.md
new file mode 100644
index 000000000..169a9c017
--- /dev/null
+++ b/docs/models/realtimespeechsessioncreated.md
@@ -0,0 +1,9 @@
+# RealtimeSpeechSessionCreated
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| `type` | *Optional[Literal["session.created"]]* | :heavy_minus_sign: | N/A |
+| `session` | [models.RealtimeSpeechSession](../models/realtimespeechsession.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeechsessionupdated.md b/docs/models/realtimespeechsessionupdated.md
new file mode 100644
index 000000000..0a38f78f9
--- /dev/null
+++ b/docs/models/realtimespeechsessionupdated.md
@@ -0,0 +1,9 @@
+# RealtimeSpeechSessionUpdated
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
+| `type` | *Optional[Literal["session.updated"]]* | :heavy_minus_sign: | N/A |
+| `session` | [models.RealtimeSpeechSession](../models/realtimespeechsession.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeechsessionupdatemessage.md b/docs/models/realtimespeechsessionupdatemessage.md
new file mode 100644
index 000000000..6f96b67c2
--- /dev/null
+++ b/docs/models/realtimespeechsessionupdatemessage.md
@@ -0,0 +1,9 @@
+# RealtimeSpeechSessionUpdateMessage
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
+| `type` | *Optional[Literal["session.update"]]* | :heavy_minus_sign: | N/A |
+| `session` | [models.RealtimeSpeechSessionUpdatePayload](../models/realtimespeechsessionupdatepayload.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/docs/models/realtimespeechsessionupdatepayload.md b/docs/models/realtimespeechsessionupdatepayload.md
new file mode 100644
index 000000000..b93168085
--- /dev/null
+++ b/docs/models/realtimespeechsessionupdatepayload.md
@@ -0,0 +1,12 @@
+# RealtimeSpeechSessionUpdatePayload
+
+
+## Fields
+
+| Field | Type | Required | Description |
+| ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| `voice` | *OptionalNullable[str]* | :heavy_minus_sign: | Preset voice identifier. Mutually exclusive with ref_audio and voice_profile. |
+| `ref_audio` | *OptionalNullable[str]* | :heavy_minus_sign: | Base64-encoded reference audio. Mutually exclusive with voice and voice_profile. |
+| `streaming_mode` | [OptionalNullable[models.RealtimeSpeechStreamingMode]](../models/realtimespeechstreamingmode.md) | :heavy_minus_sign: | responsive (default): short chunks, for conversational turns. long_form: chunks grow as the text goes on, for smoother delivery of long texts. |
+| `response_format` | [OptionalNullable[models.SpeechOutputFormat]](../models/speechoutputformat.md) | :heavy_minus_sign: | Audio encoding of speech.audio.delta events. pcm is raw 32-bit float little-endian mono. |
+| `expressiveness` | *OptionalNullable[float]* | :heavy_minus_sign: | Higher values make the delivery more expressive. Minimum 1.0. |
\ No newline at end of file
diff --git a/docs/models/realtimespeechstreamingmode.md b/docs/models/realtimespeechstreamingmode.md
new file mode 100644
index 000000000..2c388e6f5
--- /dev/null
+++ b/docs/models/realtimespeechstreamingmode.md
@@ -0,0 +1,14 @@
+# RealtimeSpeechStreamingMode
+
+## Example Usage
+
+```python
+from mistralai.client.models import RealtimeSpeechStreamingMode
+value: RealtimeSpeechStreamingMode = "responsive"
+```
+
+
+## Values
+
+- `"responsive"`
+- `"long_form"`
diff --git a/docs/models/roles.md b/docs/models/roles.md
deleted file mode 100644
index d722e7654..000000000
--- a/docs/models/roles.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Roles
-
-## Example Usage
-
-```python
-from mistralai.client.models import Roles
-
-# Open enum: unrecognized values are captured as UnrecognizedStr
-value: Roles = "system"
-```
-
-
-## Values
-
-This is an open enum. Unrecognized values will not fail type checks.
-
-- `"system"`
-- `"user"`
-- `"assistant"`
-- `"tool"`
diff --git a/docs/models/speechstreamdone.md b/docs/models/speechstreamdone.md
index d9b5e0c0c..12c38c7e2 100644
--- a/docs/models/speechstreamdone.md
+++ b/docs/models/speechstreamdone.md
@@ -3,7 +3,7 @@
## Fields
-| Field | Type | Required | Description |
-| -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
-| `type` | *Literal["speech.audio.done"]* | :heavy_check_mark: | N/A |
-| `usage` | [models.UsageInfoDollarDefs](../models/usageinfodollardefs.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
+| Field | Type | Required | Description |
+| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ |
+| `type` | *Literal["speech.audio.done"]* | :heavy_check_mark: | N/A |
+| `usage` | [models.UsageInfo](../models/usageinfo.md) | :heavy_check_mark: | N/A |
\ No newline at end of file
diff --git a/docs/models/usageinfodollardefs.md b/docs/models/usageinfodollardefs.md
deleted file mode 100644
index 8f1f228d2..000000000
--- a/docs/models/usageinfodollardefs.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# UsageInfoDollarDefs
-
-
-## Fields
-
-| Field | Type | Required | Description |
-| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
-| `prompt_audio_seconds` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A |
-| `prompt_tokens` | *Optional[int]* | :heavy_minus_sign: | N/A |
-| `total_tokens` | *Optional[int]* | :heavy_minus_sign: | N/A |
-| `completion_tokens` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A |
-| `request_count` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A |
-| `prompt_tokens_details` | [OptionalNullable[models.PromptTokensDetails]](../models/prompttokensdetails.md) | :heavy_minus_sign: | N/A |
-| `completion_tokens_details` | [OptionalNullable[models.CompletionTokensDetails]](../models/completiontokensdetails.md) | :heavy_minus_sign: | N/A |
-| `service_tier` | *OptionalNullable[str]* | :heavy_minus_sign: | The service tier at which the request was processed: standard or priority. |
-| `prompt_token_details` | [OptionalNullable[models.PromptTokensDetails]](../models/prompttokensdetails.md) | :heavy_minus_sign: | N/A |
-| `num_cached_tokens` | *OptionalNullable[int]* | :heavy_minus_sign: | N/A |
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index fd54cf084..5296829e5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "mistralai"
-version = "3.5.0"
+version = "3.6.0"
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 b798423b4..6f8b22108 100644
--- a/src/mistralai/client/_hooks/workflow_encoding_hook.py
+++ b/src/mistralai/client/_hooks/workflow_encoding_hook.py
@@ -1,5 +1,7 @@
import asyncio
import concurrent.futures
+import hashlib
+import hmac
import json
import logging
import re
@@ -9,6 +11,7 @@
import httpx2 as httpx
from httpx2._types import AsyncByteStream
+from pydantic_core import to_json
from .types import (
AfterSuccessContext,
@@ -19,7 +22,11 @@
from mistralai.client.sdkconfiguration import SDKConfiguration
from mistralai.extra.workflows.encoding.config import WorkflowEncodingConfig
from mistralai.extra.workflows.encoding.helpers import generate_two_part_id
-from mistralai.extra.workflows.encoding.models import WorkflowContext
+from mistralai.extra.workflows.encoding.models import (
+ NetworkEncodedInput,
+ NetworkEncodedScheduleInput,
+ WorkflowContext,
+)
from mistralai.extra.workflows.encoding.payload_encoder import PayloadEncoder
logger = logging.getLogger(__name__)
@@ -86,12 +93,20 @@ def _get_encoding_config(
)
EXECUTE_WORKFLOW_REGISTRATION_OPERATION_ID = "execute_workflow_registration_v1_workflows_registrations__workflow_registration_id__execute_post"
SCHEDULE_WORKFLOW_OPERATION_ID = "schedule_workflow_v1_workflows_schedules_post"
+UPDATE_SCHEDULE_OPERATION_ID = (
+ "update_schedule_v1_workflows_schedules__schedule_id__patch"
+)
EXECUTE_OPERATIONS = {
EXECUTE_WORKFLOW_OPERATION_ID,
EXECUTE_WORKFLOW_REGISTRATION_OPERATION_ID,
}
+SCHEDULE_OPERATIONS = {
+ SCHEDULE_WORKFLOW_OPERATION_ID,
+ UPDATE_SCHEDULE_OPERATION_ID,
+}
+
OPERATIONS_ENCODE_INPUT = {
EXECUTE_WORKFLOW_OPERATION_ID,
EXECUTE_WORKFLOW_REGISTRATION_OPERATION_ID,
@@ -99,6 +114,7 @@ def _get_encoding_config(
"query_workflow_execution_v1_workflows_executions__execution_id__queries_post",
"update_workflow_execution_v1_workflows_executions__execution_id__updates_post",
SCHEDULE_WORKFLOW_OPERATION_ID,
+ UPDATE_SCHEDULE_OPERATION_ID,
}
OPERATIONS_DECODE_RESULT = {
@@ -109,6 +125,11 @@ def _get_encoding_config(
"update_workflow_execution_v1_workflows_executions__execution_id__updates_post",
}
+OPERATIONS_DECODE_SCHEDULE_INPUT = {
+ "get_schedule_v1_workflows_schedules__schedule_id__get",
+ "get_schedules_v1_workflows_schedules_get",
+}
+
# Operations that return event data that may need decryption
OPERATIONS_DECODE_EVENTS = {
"get_workflow_events_v1_workflows_events_list_get",
@@ -122,6 +143,8 @@ def _get_encoding_config(
SCHEDULE_CORRELATION_ID_PLACEHOLDER = "__scheduled_workflow__"
+SCHEDULE_INPUT_HASH_KEY_LABEL = b"schedule-input-hash"
+
def _is_payload_type(value: Any) -> bool:
"""Check if a value is a JSONPayload or JSONPatchPayload by its structure.
@@ -213,6 +236,34 @@ async def _decrypt_events_in_response(
return body
+def _is_encoded_schedule_input(value: Any, payload_encoder: PayloadEncoder) -> bool:
+ return (
+ isinstance(value, dict)
+ and bool(value.get("encoding_options"))
+ and payload_encoder.check_is_payload_encoded(value)
+ )
+
+
+async def _decode_schedule_inputs(
+ schedule_defs: list[Dict[str, Any]],
+ payload_encoder: PayloadEncoder,
+) -> None:
+ for schedule_def in schedule_defs:
+ try:
+ decoded_input = await payload_encoder.decode_network_result(
+ schedule_def["input"]
+ )
+ except Exception as e:
+ logger.warning(
+ "WorkflowEncodingHook: Failed to decode input of schedule %s: %s",
+ schedule_def.get("schedule_id"),
+ e,
+ )
+ continue
+ if not isinstance(decoded_input, bytes):
+ schedule_def["input"] = decoded_input
+
+
def _decrypt_sse_line(line: bytes, payload_encoder: PayloadEncoder) -> bytes:
"""Decrypt event payloads in an SSE data line."""
if not line.startswith(b"data:"):
@@ -325,7 +376,17 @@ def before_request(
try:
body = json.loads(request.content)
- input_data = body.get("input")
+ if not isinstance(body, dict):
+ return request
+ if hook_ctx.operation_id in SCHEDULE_OPERATIONS:
+ input_holder = body.get("schedule")
+ if not isinstance(input_holder, dict):
+ raise ValueError(
+ f"WorkflowEncoding: {hook_ctx.operation_id} request has no schedule object to encode"
+ )
+ else:
+ input_holder = body
+ input_data = input_holder.get("input")
if input_data is None:
return request
@@ -338,10 +399,7 @@ def before_request(
execution_id = generate_two_part_id(seed)
body["execution_id"] = execution_id
- if (
- not execution_id
- and hook_ctx.operation_id == SCHEDULE_WORKFLOW_OPERATION_ID
- ):
+ if not execution_id and hook_ctx.operation_id in SCHEDULE_OPERATIONS:
execution_id = SCHEDULE_CORRELATION_ID_PLACEHOLDER
if not execution_id:
@@ -354,20 +412,50 @@ def before_request(
execution_id=execution_id,
)
- encoded_input = _run_async(
- encoding_config.payload_encoder.encode_network_input(
- input_data, context
+ allow_offloading = hook_ctx.operation_id not in SCHEDULE_OPERATIONS
+
+ encoded_data, encoding_options = _run_async(
+ encoding_config.payload_encoder.encode_payload_content(
+ to_json(input_data),
+ context,
+ allow_offloading=allow_offloading,
)
)
+ encoded_input = NetworkEncodedInput.from_data(
+ encoded_data, encoding_options
+ )
+
+ encryption_config = encoding_config.payload_encoder.encryption_config
+ if hook_ctx.operation_id == SCHEDULE_WORKFLOW_OPERATION_ID:
+ canonical_input = json.dumps(
+ input_data,
+ sort_keys=True,
+ ensure_ascii=False,
+ separators=(",", ":"),
+ ).encode("utf-8")
+ if (
+ encryption_config is not None
+ and encryption_config.main_key is not None
+ ):
+ hash_key = hmac.new(
+ bytes.fromhex(encryption_config.main_key.get_secret_value()),
+ SCHEDULE_INPUT_HASH_KEY_LABEL,
+ hashlib.sha256,
+ ).digest()
+ input_hash = hmac.new(
+ hash_key, canonical_input, hashlib.sha256
+ ).hexdigest()
+ else:
+ input_hash = hashlib.sha256(canonical_input).hexdigest()
+ encoded_input = NetworkEncodedScheduleInput(
+ **encoded_input.model_dump(), input_hash=input_hash
+ )
- # Update body based on operation type:
- # - Execute operations: use separate `encoded_input` field and set `input` to None
- # - Signal/Query/Update: put encoded input directly in `input` field
if hook_ctx.operation_id in EXECUTE_OPERATIONS:
body["encoded_input"] = encoded_input.model_dump(mode="json")
body["input"] = None
else:
- body["input"] = encoded_input.model_dump(mode="json")
+ input_holder["input"] = encoded_input.model_dump(mode="json")
new_content = json.dumps(body).encode("utf-8")
new_headers = httpx.Headers(request.headers)
@@ -455,4 +543,40 @@ def after_success(
logger.error("WorkflowEncodingHook: Failed to decode events: %s", e)
raise
+ elif hook_ctx.operation_id in OPERATIONS_DECODE_SCHEDULE_INPUT:
+ try:
+ body = json.loads(response.content)
+ if not isinstance(body, dict):
+ return response
+ schedule_defs = body.get("schedules")
+ if not isinstance(schedule_defs, list):
+ schedule_defs = [body]
+ encoded_schedule_defs = [
+ schedule_def
+ for schedule_def in schedule_defs
+ if isinstance(schedule_def, dict)
+ and _is_encoded_schedule_input(
+ schedule_def.get("input"), encoding_config.payload_encoder
+ )
+ ]
+ if encoded_schedule_defs:
+ _run_async(
+ _decode_schedule_inputs(
+ encoded_schedule_defs, encoding_config.payload_encoder
+ )
+ )
+ new_content = json.dumps(body).encode("utf-8")
+ response = httpx.Response(
+ status_code=response.status_code,
+ headers=_strip_content_encoding_header(response.headers),
+ content=new_content,
+ request=response.request,
+ extensions=response.extensions,
+ )
+ except Exception as e:
+ logger.error(
+ "WorkflowEncodingHook: Failed to decode schedule input: %s", e
+ )
+ raise
+
return response
diff --git a/src/mistralai/client/_version.py b/src/mistralai/client/_version.py
index b073af7fd..729425f91 100644
--- a/src/mistralai/client/_version.py
+++ b/src/mistralai/client/_version.py
@@ -3,10 +3,10 @@
import importlib.metadata
__title__: str = "mistralai"
-__version__: str = "3.5.0"
+__version__: str = "3.6.0"
__openapi_doc_version__: str = "1.0.0"
__gen_version__: str = "2.935.1"
-__user_agent__: str = "speakeasy-sdk/python 3.5.0 2.935.1 1.0.0 mistralai"
+__user_agent__: str = "speakeasy-sdk/python 3.6.0 2.935.1 1.0.0 mistralai"
try:
if __package__ is not None:
diff --git a/src/mistralai/client/audio.py b/src/mistralai/client/audio.py
index f3ce69ba7..b90dfc5a9 100644
--- a/src/mistralai/client/audio.py
+++ b/src/mistralai/client/audio.py
@@ -13,7 +13,7 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
- from mistralai.extra.realtime import RealtimeTranscription
+ from mistralai.extra.realtime import RealtimeSpeech, RealtimeTranscription
# endregion imports
@@ -54,4 +54,13 @@ def realtime(self) -> "RealtimeTranscription":
return self._realtime
+ @property
+ def realtime_speech(self) -> "RealtimeSpeech":
+ if not hasattr(self, "_realtime_speech"):
+ from mistralai.extra.realtime import RealtimeSpeech # pylint: disable=import-outside-toplevel
+
+ self._realtime_speech = RealtimeSpeech(self.sdk_configuration) # pylint: disable=attribute-defined-outside-init
+
+ return self._realtime_speech
+
# endregion sdk-class-body
diff --git a/src/mistralai/client/managed_indexes.py b/src/mistralai/client/managed_indexes.py
index 4ffd52d4a..0e5b80e29 100644
--- a/src/mistralai/client/managed_indexes.py
+++ b/src/mistralai/client/managed_indexes.py
@@ -113,7 +113,7 @@ def create(
errors.HTTPValidationErrorData, http_res
)
raise errors.HTTPValidationError(response_data, http_res)
- if utils.match_response(http_res, ["409", "4XX"], "*"):
+ if utils.match_response(http_res, ["403", "409", "4XX"], "*"):
http_res_text = utils.stream_to_text(http_res)
raise errors.SDKError("API error occurred", http_res, http_res_text)
if utils.match_response(http_res, "5XX", "*"):
@@ -224,7 +224,7 @@ async def create_async(
errors.HTTPValidationErrorData, http_res
)
raise errors.HTTPValidationError(response_data, http_res)
- if utils.match_response(http_res, ["409", "4XX"], "*"):
+ if utils.match_response(http_res, ["403", "409", "4XX"], "*"):
http_res_text = await utils.stream_to_text_async(http_res)
raise errors.SDKError("API error occurred", http_res, http_res_text)
if utils.match_response(http_res, "5XX", "*"):
diff --git a/src/mistralai/client/models/__init__.py b/src/mistralai/client/models/__init__.py
index 67090a69a..4c21494c5 100644
--- a/src/mistralai/client/models/__init__.py
+++ b/src/mistralai/client/models/__init__.py
@@ -514,10 +514,6 @@
CompletionsCapability,
CompletionsCapabilityTypedDict,
)
- from .completiontokensdetails import (
- CompletionTokensDetails,
- CompletionTokensDetailsTypedDict,
- )
from .compute_filtered_run_statistics_v2_v1_observability_evaluation_runs_v2_run_id_statistics_compute_postop import (
ComputeFilteredRunStatisticsV2V1ObservabilityEvaluationRunsV2RunIDStatisticsComputePostRequest,
ComputeFilteredRunStatisticsV2V1ObservabilityEvaluationRunsV2RunIDStatisticsComputePostRequestTypedDict,
@@ -2233,7 +2229,6 @@
MessageOutputEventTypedDict,
)
from .messageresponse import MessageResponse, MessageResponseTypedDict
- from .messagetokens import MessageTokens, MessageTokensTypedDict
from .metadatafilter import (
MetadataFilter,
MetadataFilterTypedDict,
@@ -2565,7 +2560,6 @@
UpdatePromptRequestTypedDict,
)
from .promptscapability import PromptsCapability, PromptsCapabilityTypedDict
- from .prompttokensdetails import PromptTokensDetails, PromptTokensDetailsTypedDict
from .promptvariable import PromptVariable, PromptVariableTypedDict
from .promptversion import PromptVersion, PromptVersionTypedDict
from .publicauthenticationmethod import (
@@ -2606,6 +2600,46 @@
ReadIndexV1ContextSearchIndexesIndexNameReadPostRequestTypedDict,
)
from .readrequest import ReadRequest, ReadRequestTypedDict
+ from .realtimespeecherror import RealtimeSpeechError, RealtimeSpeechErrorTypedDict
+ from .realtimespeecherrordetail import (
+ RealtimeSpeechErrorDetail,
+ RealtimeSpeechErrorDetailMessage,
+ RealtimeSpeechErrorDetailMessageTypedDict,
+ RealtimeSpeechErrorDetailTypedDict,
+ )
+ from .realtimespeechinputtextappend import (
+ RealtimeSpeechInputTextAppend,
+ RealtimeSpeechInputTextAppendTypedDict,
+ )
+ from .realtimespeechinputtextend import (
+ RealtimeSpeechInputTextEnd,
+ RealtimeSpeechInputTextEndTypedDict,
+ )
+ from .realtimespeechinputtextflush import (
+ RealtimeSpeechInputTextFlush,
+ RealtimeSpeechInputTextFlushTypedDict,
+ )
+ from .realtimespeechsession import (
+ RealtimeSpeechSession,
+ RealtimeSpeechSessionTypedDict,
+ )
+ from .realtimespeechsessioncreated import (
+ RealtimeSpeechSessionCreated,
+ RealtimeSpeechSessionCreatedTypedDict,
+ )
+ from .realtimespeechsessionupdated import (
+ RealtimeSpeechSessionUpdated,
+ RealtimeSpeechSessionUpdatedTypedDict,
+ )
+ from .realtimespeechsessionupdatemessage import (
+ RealtimeSpeechSessionUpdateMessage,
+ RealtimeSpeechSessionUpdateMessageTypedDict,
+ )
+ from .realtimespeechsessionupdatepayload import (
+ RealtimeSpeechSessionUpdatePayload,
+ RealtimeSpeechSessionUpdatePayloadTypedDict,
+ )
+ from .realtimespeechstreamingmode import RealtimeSpeechStreamingMode
from .realtimetranscriptionerror import (
RealtimeTranscriptionError,
RealtimeTranscriptionErrorTypedDict,
@@ -3002,7 +3036,6 @@
RetrieveModelV1ModelsModelIDGetRequestTypedDict,
UnknownResponseRetrieveModelV1ModelsModelIDGet,
)
- from .roles import Roles
from .rootscapability import RootsCapability, RootsCapabilityTypedDict
from .rrfretriever import (
RRFRetriever,
@@ -3772,7 +3805,6 @@
URLElicitationCapabilityTypedDict,
)
from .usageinfo import UsageInfo, UsageInfoTypedDict
- from .usageinfo_dollar_defs import UsageInfoDollarDefs, UsageInfoDollarDefsTypedDict
from .useridentity import UserIdentity, UserIdentityTypedDict
from .useridentityapikey import UserIdentityAPIKey, UserIdentityAPIKeyTypedDict
from .useridentityorganization import (
@@ -4472,8 +4504,6 @@
"CompletionResponseStreamChoice",
"CompletionResponseStreamChoiceFinishReason",
"CompletionResponseStreamChoiceTypedDict",
- "CompletionTokensDetails",
- "CompletionTokensDetailsTypedDict",
"CompletionsCapability",
"CompletionsCapabilityTypedDict",
"ComputeFilteredRunStatisticsV2V1ObservabilityEvaluationRunsV2RunIDStatisticsComputePostRequest",
@@ -5615,8 +5645,6 @@
"MessageOutputEventTypedDict",
"MessageResponse",
"MessageResponseTypedDict",
- "MessageTokens",
- "MessageTokensTypedDict",
"Metadata",
"MetadataFilter",
"MetadataFilterGroup",
@@ -5874,8 +5902,6 @@
"PromptCacheRetention",
"PromptDefinition",
"PromptDefinitionTypedDict",
- "PromptTokensDetails",
- "PromptTokensDetailsTypedDict",
"PromptTypedDict",
"PromptVariable",
"PromptVariableTypedDict",
@@ -5932,6 +5958,29 @@
"ReadIndexV1ContextSearchIndexesIndexNameReadPostRequestTypedDict",
"ReadRequest",
"ReadRequestTypedDict",
+ "RealtimeSpeechError",
+ "RealtimeSpeechErrorDetail",
+ "RealtimeSpeechErrorDetailMessage",
+ "RealtimeSpeechErrorDetailMessageTypedDict",
+ "RealtimeSpeechErrorDetailTypedDict",
+ "RealtimeSpeechErrorTypedDict",
+ "RealtimeSpeechInputTextAppend",
+ "RealtimeSpeechInputTextAppendTypedDict",
+ "RealtimeSpeechInputTextEnd",
+ "RealtimeSpeechInputTextEndTypedDict",
+ "RealtimeSpeechInputTextFlush",
+ "RealtimeSpeechInputTextFlushTypedDict",
+ "RealtimeSpeechSession",
+ "RealtimeSpeechSessionCreated",
+ "RealtimeSpeechSessionCreatedTypedDict",
+ "RealtimeSpeechSessionTypedDict",
+ "RealtimeSpeechSessionUpdateMessage",
+ "RealtimeSpeechSessionUpdateMessageTypedDict",
+ "RealtimeSpeechSessionUpdatePayload",
+ "RealtimeSpeechSessionUpdatePayloadTypedDict",
+ "RealtimeSpeechSessionUpdated",
+ "RealtimeSpeechSessionUpdatedTypedDict",
+ "RealtimeSpeechStreamingMode",
"RealtimeTranscriptionError",
"RealtimeTranscriptionErrorDetail",
"RealtimeTranscriptionErrorDetailMessage",
@@ -6215,7 +6264,6 @@
"ResumeScheduleV1WorkflowsSchedulesScheduleIDResumePostRequestTypedDict",
"RetrieveModelV1ModelsModelIDGetRequest",
"RetrieveModelV1ModelsModelIDGetRequestTypedDict",
- "Roles",
"RootsCapability",
"RootsCapabilityTypedDict",
"RunEvaluationSummary",
@@ -6811,8 +6859,6 @@
"UpdateWorkflowV1WorkflowsWorkflowIdentifierPutRequest",
"UpdateWorkflowV1WorkflowsWorkflowIdentifierPutRequestTypedDict",
"UsageInfo",
- "UsageInfoDollarDefs",
- "UsageInfoDollarDefsTypedDict",
"UsageInfoTypedDict",
"UserIdentity",
"UserIdentityAPIKey",
@@ -7369,8 +7415,6 @@
"CompletionResponseStreamChoiceTypedDict": ".completionresponsestreamchoice",
"CompletionsCapability": ".completionscapability",
"CompletionsCapabilityTypedDict": ".completionscapability",
- "CompletionTokensDetails": ".completiontokensdetails",
- "CompletionTokensDetailsTypedDict": ".completiontokensdetails",
"ComputeFilteredRunStatisticsV2V1ObservabilityEvaluationRunsV2RunIDStatisticsComputePostRequest": ".compute_filtered_run_statistics_v2_v1_observability_evaluation_runs_v2_run_id_statistics_compute_postop",
"ComputeFilteredRunStatisticsV2V1ObservabilityEvaluationRunsV2RunIDStatisticsComputePostRequestTypedDict": ".compute_filtered_run_statistics_v2_v1_observability_evaluation_runs_v2_run_id_statistics_compute_postop",
"ConfusionMatrix": ".confusionmatrix",
@@ -8562,8 +8606,6 @@
"MessageOutputEventTypedDict": ".messageoutputevent",
"MessageResponse": ".messageresponse",
"MessageResponseTypedDict": ".messageresponse",
- "MessageTokens": ".messagetokens",
- "MessageTokensTypedDict": ".messagetokens",
"MetadataFilter": ".metadatafilter",
"MetadataFilterTypedDict": ".metadatafilter",
"MetadataFilterValue": ".metadatafilter",
@@ -8833,8 +8875,6 @@
"UpdatePromptRequestTypedDict": ".prompts_updateop",
"PromptsCapability": ".promptscapability",
"PromptsCapabilityTypedDict": ".promptscapability",
- "PromptTokensDetails": ".prompttokensdetails",
- "PromptTokensDetailsTypedDict": ".prompttokensdetails",
"PromptVariable": ".promptvariable",
"PromptVariableTypedDict": ".promptvariable",
"PromptVersion": ".promptversion",
@@ -8868,6 +8908,29 @@
"ReadIndexV1ContextSearchIndexesIndexNameReadPostRequestTypedDict": ".read_index_v1_context_search_indexes_index_name_read_postop",
"ReadRequest": ".readrequest",
"ReadRequestTypedDict": ".readrequest",
+ "RealtimeSpeechError": ".realtimespeecherror",
+ "RealtimeSpeechErrorTypedDict": ".realtimespeecherror",
+ "RealtimeSpeechErrorDetail": ".realtimespeecherrordetail",
+ "RealtimeSpeechErrorDetailMessage": ".realtimespeecherrordetail",
+ "RealtimeSpeechErrorDetailMessageTypedDict": ".realtimespeecherrordetail",
+ "RealtimeSpeechErrorDetailTypedDict": ".realtimespeecherrordetail",
+ "RealtimeSpeechInputTextAppend": ".realtimespeechinputtextappend",
+ "RealtimeSpeechInputTextAppendTypedDict": ".realtimespeechinputtextappend",
+ "RealtimeSpeechInputTextEnd": ".realtimespeechinputtextend",
+ "RealtimeSpeechInputTextEndTypedDict": ".realtimespeechinputtextend",
+ "RealtimeSpeechInputTextFlush": ".realtimespeechinputtextflush",
+ "RealtimeSpeechInputTextFlushTypedDict": ".realtimespeechinputtextflush",
+ "RealtimeSpeechSession": ".realtimespeechsession",
+ "RealtimeSpeechSessionTypedDict": ".realtimespeechsession",
+ "RealtimeSpeechSessionCreated": ".realtimespeechsessioncreated",
+ "RealtimeSpeechSessionCreatedTypedDict": ".realtimespeechsessioncreated",
+ "RealtimeSpeechSessionUpdated": ".realtimespeechsessionupdated",
+ "RealtimeSpeechSessionUpdatedTypedDict": ".realtimespeechsessionupdated",
+ "RealtimeSpeechSessionUpdateMessage": ".realtimespeechsessionupdatemessage",
+ "RealtimeSpeechSessionUpdateMessageTypedDict": ".realtimespeechsessionupdatemessage",
+ "RealtimeSpeechSessionUpdatePayload": ".realtimespeechsessionupdatepayload",
+ "RealtimeSpeechSessionUpdatePayloadTypedDict": ".realtimespeechsessionupdatepayload",
+ "RealtimeSpeechStreamingMode": ".realtimespeechstreamingmode",
"RealtimeTranscriptionError": ".realtimetranscriptionerror",
"RealtimeTranscriptionErrorTypedDict": ".realtimetranscriptionerror",
"RealtimeTranscriptionErrorDetail": ".realtimetranscriptionerrordetail",
@@ -9125,7 +9188,6 @@
"RetrieveModelV1ModelsModelIDGetRequest": ".retrieve_model_v1_models_model_id_getop",
"RetrieveModelV1ModelsModelIDGetRequestTypedDict": ".retrieve_model_v1_models_model_id_getop",
"UnknownResponseRetrieveModelV1ModelsModelIDGet": ".retrieve_model_v1_models_model_id_getop",
- "Roles": ".roles",
"RootsCapability": ".rootscapability",
"RootsCapabilityTypedDict": ".rootscapability",
"RRFRetriever": ".rrfretriever",
@@ -9694,8 +9756,6 @@
"URLElicitationCapabilityTypedDict": ".urlelicitationcapability",
"UsageInfo": ".usageinfo",
"UsageInfoTypedDict": ".usageinfo",
- "UsageInfoDollarDefs": ".usageinfo_dollar_defs",
- "UsageInfoDollarDefsTypedDict": ".usageinfo_dollar_defs",
"UserIdentity": ".useridentity",
"UserIdentityTypedDict": ".useridentity",
"UserIdentityAPIKey": ".useridentityapikey",
diff --git a/src/mistralai/client/models/connectionpreference.py b/src/mistralai/client/models/connectionpreference.py
index fbca39a4f..8e0bcf220 100644
--- a/src/mistralai/client/models/connectionpreference.py
+++ b/src/mistralai/client/models/connectionpreference.py
@@ -13,30 +13,46 @@
UNSET,
UNSET_SENTINEL,
)
+import pydantic
from pydantic import model_serializer
-from typing_extensions import NotRequired, TypedDict
+from typing import Optional
+from typing_extensions import Annotated, NotRequired, TypedDict
class ConnectionPreferenceTypedDict(TypedDict):
- name: str
tool_configuration: ToolExecutionConfigurationTypedDict
- is_default: NotRequired[Nullable[bool]]
consumer_type: NotRequired[Nullable[ConsumerType]]
+ name: NotRequired[str]
+ r"""Deprecated: always `default`, a connection holds a single preference."""
+ is_default: NotRequired[Nullable[bool]]
+ r"""Deprecated: always `true`."""
class ConnectionPreference(BaseModel):
- name: str
-
tool_configuration: ToolExecutionConfiguration
- is_default: OptionalNullable[bool] = UNSET
-
consumer_type: OptionalNullable[ConsumerType] = UNSET
+ name: Annotated[
+ Optional[str],
+ pydantic.Field(
+ deprecated="warning: ** DEPRECATED ** - This will be removed in a future release, please migrate away from it as soon as possible."
+ ),
+ ] = "default"
+ r"""Deprecated: always `default`, a connection holds a single preference."""
+
+ is_default: Annotated[
+ OptionalNullable[bool],
+ pydantic.Field(
+ deprecated="warning: ** DEPRECATED ** - This will be removed in a future release, please migrate away from it as soon as possible."
+ ),
+ ] = UNSET
+ r"""Deprecated: always `true`."""
+
@model_serializer(mode="wrap")
def serialize_model(self, handler):
- optional_fields = set(["is_default", "consumer_type"])
- nullable_fields = set(["is_default", "consumer_type"])
+ optional_fields = set(["consumer_type", "name", "is_default"])
+ nullable_fields = set(["consumer_type", "is_default"])
serialized = handler(self)
m = {}
diff --git a/src/mistralai/client/models/observabilityerrorcode.py b/src/mistralai/client/models/observabilityerrorcode.py
index 8f3bf3e8f..e036b0d3f 100644
--- a/src/mistralai/client/models/observabilityerrorcode.py
+++ b/src/mistralai/client/models/observabilityerrorcode.py
@@ -56,6 +56,7 @@
"EVALUATION_NAME_ALREADY_EXISTS",
"OPTIMIZATION_NOT_FOUND",
"OPTIMIZATION_TRIAL_KEY_ALREADY_EXISTS",
+ "OPTIMIZATION_TRIAL_OBSERVATION_ALREADY_EXISTS",
"TRACES_FILTER_QUERY_PARSE_ERROR",
"TRACE_NOT_FOUND",
"SPAN_NOT_FOUND",
diff --git a/src/mistralai/client/models/prompttokensdetails.py b/src/mistralai/client/models/prompttokensdetails.py
deleted file mode 100644
index d9204fcdc..000000000
--- a/src/mistralai/client/models/prompttokensdetails.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
-
-from __future__ import annotations
-from .messagetokens import MessageTokens, MessageTokensTypedDict
-from mistralai.client.types import BaseModel, UNSET_SENTINEL
-from pydantic import model_serializer
-from typing import List, Optional
-from typing_extensions import NotRequired, TypedDict
-
-
-class PromptTokensDetailsTypedDict(TypedDict):
- r"""Token usage details for the prompt."""
-
- messages: NotRequired[List[MessageTokensTypedDict]]
- cached_tokens: NotRequired[int]
- audio_tokens: NotRequired[int]
-
-
-class PromptTokensDetails(BaseModel):
- r"""Token usage details for the prompt."""
-
- messages: Optional[List[MessageTokens]] = None
-
- cached_tokens: Optional[int] = 0
-
- audio_tokens: Optional[int] = 0
-
- @model_serializer(mode="wrap")
- def serialize_model(self, handler):
- optional_fields = set(["messages", "cached_tokens", "audio_tokens"])
- serialized = handler(self)
- m = {}
-
- for n, f in type(self).model_fields.items():
- k = f.alias or n
- val = serialized.get(k, serialized.get(n))
-
- if val != UNSET_SENTINEL:
- if val is not None or k not in optional_fields:
- m[k] = val
-
- return m
diff --git a/src/mistralai/client/models/realtimespeecherror.py b/src/mistralai/client/models/realtimespeecherror.py
new file mode 100644
index 000000000..1dc232a9d
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeecherror.py
@@ -0,0 +1,50 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from .realtimespeecherrordetail import (
+ RealtimeSpeechErrorDetail,
+ RealtimeSpeechErrorDetailTypedDict,
+)
+from mistralai.client.types import BaseModel, UNSET_SENTINEL
+from mistralai.client.utils import validate_const
+import pydantic
+from pydantic import model_serializer
+from pydantic.functional_validators import AfterValidator
+from typing import Literal, Optional
+from typing_extensions import Annotated, TypedDict
+
+
+class RealtimeSpeechErrorTypedDict(TypedDict):
+ error: RealtimeSpeechErrorDetailTypedDict
+ type: Literal["error"]
+
+
+class RealtimeSpeechError(BaseModel):
+ error: RealtimeSpeechErrorDetail
+
+ type: Annotated[
+ Annotated[Optional[Literal["error"]], AfterValidator(validate_const("error"))],
+ pydantic.Field(alias="type"),
+ ] = "error"
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = set(["type"])
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k, serialized.get(n))
+
+ if val != UNSET_SENTINEL:
+ if val is not None or k not in optional_fields:
+ m[k] = val
+
+ return m
+
+
+try:
+ RealtimeSpeechError.model_rebuild()
+except NameError:
+ pass
diff --git a/src/mistralai/client/models/realtimespeecherrordetail.py b/src/mistralai/client/models/realtimespeecherrordetail.py
new file mode 100644
index 000000000..f3e747f5f
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeecherrordetail.py
@@ -0,0 +1,33 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from mistralai.client.types import BaseModel
+from typing import Any, Dict, Union
+from typing_extensions import TypeAliasType, TypedDict
+
+
+RealtimeSpeechErrorDetailMessageTypedDict = TypeAliasType(
+ "RealtimeSpeechErrorDetailMessageTypedDict", Union[str, Dict[str, Any]]
+)
+r"""Human-readable error message."""
+
+
+RealtimeSpeechErrorDetailMessage = TypeAliasType(
+ "RealtimeSpeechErrorDetailMessage", Union[str, Dict[str, Any]]
+)
+r"""Human-readable error message."""
+
+
+class RealtimeSpeechErrorDetailTypedDict(TypedDict):
+ message: RealtimeSpeechErrorDetailMessageTypedDict
+ r"""Human-readable error message."""
+ code: int
+ r"""Internal error code for debugging."""
+
+
+class RealtimeSpeechErrorDetail(BaseModel):
+ message: RealtimeSpeechErrorDetailMessage
+ r"""Human-readable error message."""
+
+ code: int
+ r"""Internal error code for debugging."""
diff --git a/src/mistralai/client/models/realtimespeechinputtextappend.py b/src/mistralai/client/models/realtimespeechinputtextappend.py
new file mode 100644
index 000000000..994e160ce
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeechinputtextappend.py
@@ -0,0 +1,49 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from mistralai.client.types import BaseModel, UNSET_SENTINEL
+from mistralai.client.utils import validate_const
+import pydantic
+from pydantic import model_serializer
+from pydantic.functional_validators import AfterValidator
+from typing import Literal, Optional
+from typing_extensions import Annotated, TypedDict
+
+
+class RealtimeSpeechInputTextAppendTypedDict(TypedDict):
+ text: str
+ type: Literal["input_text.append"]
+
+
+class RealtimeSpeechInputTextAppend(BaseModel):
+ text: str
+
+ type: Annotated[
+ Annotated[
+ Optional[Literal["input_text.append"]],
+ AfterValidator(validate_const("input_text.append")),
+ ],
+ pydantic.Field(alias="type"),
+ ] = "input_text.append"
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = set(["type"])
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k, serialized.get(n))
+
+ if val != UNSET_SENTINEL:
+ if val is not None or k not in optional_fields:
+ m[k] = val
+
+ return m
+
+
+try:
+ RealtimeSpeechInputTextAppend.model_rebuild()
+except NameError:
+ pass
diff --git a/src/mistralai/client/models/realtimespeechinputtextend.py b/src/mistralai/client/models/realtimespeechinputtextend.py
new file mode 100644
index 000000000..34b6b97c8
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeechinputtextend.py
@@ -0,0 +1,46 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from mistralai.client.types import BaseModel, UNSET_SENTINEL
+from mistralai.client.utils import validate_const
+import pydantic
+from pydantic import model_serializer
+from pydantic.functional_validators import AfterValidator
+from typing import Literal, Optional
+from typing_extensions import Annotated, TypedDict
+
+
+class RealtimeSpeechInputTextEndTypedDict(TypedDict):
+ type: Literal["input_text.end"]
+
+
+class RealtimeSpeechInputTextEnd(BaseModel):
+ type: Annotated[
+ Annotated[
+ Optional[Literal["input_text.end"]],
+ AfterValidator(validate_const("input_text.end")),
+ ],
+ pydantic.Field(alias="type"),
+ ] = "input_text.end"
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = set(["type"])
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k, serialized.get(n))
+
+ if val != UNSET_SENTINEL:
+ if val is not None or k not in optional_fields:
+ m[k] = val
+
+ return m
+
+
+try:
+ RealtimeSpeechInputTextEnd.model_rebuild()
+except NameError:
+ pass
diff --git a/src/mistralai/client/models/completiontokensdetails.py b/src/mistralai/client/models/realtimespeechinputtextflush.py
similarity index 51%
rename from src/mistralai/client/models/completiontokensdetails.py
rename to src/mistralai/client/models/realtimespeechinputtextflush.py
index f253decf8..1dec12ab5 100644
--- a/src/mistralai/client/models/completiontokensdetails.py
+++ b/src/mistralai/client/models/realtimespeechinputtextflush.py
@@ -8,35 +8,34 @@
UNSET,
UNSET_SENTINEL,
)
+from mistralai.client.utils import validate_const
+import pydantic
from pydantic import model_serializer
-from typing_extensions import NotRequired, TypedDict
+from pydantic.functional_validators import AfterValidator
+from typing import Literal, Optional
+from typing_extensions import Annotated, NotRequired, TypedDict
-class CompletionTokensDetailsTypedDict(TypedDict):
- r"""Token usage details for the completion.
+class RealtimeSpeechInputTextFlushTypedDict(TypedDict):
+ type: Literal["input_text.flush"]
+ text: NotRequired[Nullable[str]]
- ``reasoning_tokens`` is ``None`` when the provider did not report a count.
- Preserve that distinction instead of inventing a measured zero: ``0`` means
- \"reasoning off, confirmed by the provider\", ``None`` means \"unknown\".
- """
- reasoning_tokens: NotRequired[Nullable[int]]
+class RealtimeSpeechInputTextFlush(BaseModel):
+ type: Annotated[
+ Annotated[
+ Optional[Literal["input_text.flush"]],
+ AfterValidator(validate_const("input_text.flush")),
+ ],
+ pydantic.Field(alias="type"),
+ ] = "input_text.flush"
-
-class CompletionTokensDetails(BaseModel):
- r"""Token usage details for the completion.
-
- ``reasoning_tokens`` is ``None`` when the provider did not report a count.
- Preserve that distinction instead of inventing a measured zero: ``0`` means
- \"reasoning off, confirmed by the provider\", ``None`` means \"unknown\".
- """
-
- reasoning_tokens: OptionalNullable[int] = UNSET
+ text: OptionalNullable[str] = UNSET
@model_serializer(mode="wrap")
def serialize_model(self, handler):
- optional_fields = set(["reasoning_tokens"])
- nullable_fields = set(["reasoning_tokens"])
+ optional_fields = set(["type", "text"])
+ nullable_fields = set(["text"])
serialized = handler(self)
m = {}
@@ -57,3 +56,9 @@ def serialize_model(self, handler):
m[k] = val
return m
+
+
+try:
+ RealtimeSpeechInputTextFlush.model_rebuild()
+except NameError:
+ pass
diff --git a/src/mistralai/client/models/messagetokens.py b/src/mistralai/client/models/realtimespeechsession.py
similarity index 54%
rename from src/mistralai/client/models/messagetokens.py
rename to src/mistralai/client/models/realtimespeechsession.py
index 7326311b6..8a4ea97c2 100644
--- a/src/mistralai/client/models/messagetokens.py
+++ b/src/mistralai/client/models/realtimespeechsession.py
@@ -1,7 +1,8 @@
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
from __future__ import annotations
-from .roles import Roles
+from .realtimespeechstreamingmode import RealtimeSpeechStreamingMode
+from .speechoutputformat import SpeechOutputFormat
from mistralai.client.types import (
BaseModel,
Nullable,
@@ -10,39 +11,38 @@
UNSET_SENTINEL,
)
from pydantic import model_serializer
-from typing import Optional
from typing_extensions import NotRequired, TypedDict
-class MessageTokensTypedDict(TypedDict):
- r"""Information on a single message included in a tokenized prompt as part of an InstructRequest."""
+class RealtimeSpeechSessionTypedDict(TypedDict):
+ request_id: str
+ model: str
+ is_ref_audio: bool
+ streaming_mode: RealtimeSpeechStreamingMode
+ response_format: SpeechOutputFormat
+ expressiveness: float
+ voice: NotRequired[Nullable[str]]
- role: Roles
- total_tokens: NotRequired[Nullable[int]]
- settings_tokens: NotRequired[Nullable[int]]
- truncated: NotRequired[bool]
- usage_count: NotRequired[int]
+class RealtimeSpeechSession(BaseModel):
+ request_id: str
-class MessageTokens(BaseModel):
- r"""Information on a single message included in a tokenized prompt as part of an InstructRequest."""
+ model: str
- role: Roles
+ is_ref_audio: bool
- total_tokens: OptionalNullable[int] = UNSET
+ streaming_mode: RealtimeSpeechStreamingMode
- settings_tokens: OptionalNullable[int] = UNSET
+ response_format: SpeechOutputFormat
- truncated: Optional[bool] = False
+ expressiveness: float
- usage_count: Optional[int] = 1
+ voice: OptionalNullable[str] = UNSET
@model_serializer(mode="wrap")
def serialize_model(self, handler):
- optional_fields = set(
- ["total_tokens", "settings_tokens", "truncated", "usage_count"]
- )
- nullable_fields = set(["total_tokens", "settings_tokens"])
+ optional_fields = set(["voice"])
+ nullable_fields = set(["voice"])
serialized = handler(self)
m = {}
diff --git a/src/mistralai/client/models/realtimespeechsessioncreated.py b/src/mistralai/client/models/realtimespeechsessioncreated.py
new file mode 100644
index 000000000..28e26ee28
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeechsessioncreated.py
@@ -0,0 +1,50 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from .realtimespeechsession import RealtimeSpeechSession, RealtimeSpeechSessionTypedDict
+from mistralai.client.types import BaseModel, UNSET_SENTINEL
+from mistralai.client.utils import validate_const
+import pydantic
+from pydantic import model_serializer
+from pydantic.functional_validators import AfterValidator
+from typing import Literal, Optional
+from typing_extensions import Annotated, TypedDict
+
+
+class RealtimeSpeechSessionCreatedTypedDict(TypedDict):
+ session: RealtimeSpeechSessionTypedDict
+ type: Literal["session.created"]
+
+
+class RealtimeSpeechSessionCreated(BaseModel):
+ session: RealtimeSpeechSession
+
+ type: Annotated[
+ Annotated[
+ Optional[Literal["session.created"]],
+ AfterValidator(validate_const("session.created")),
+ ],
+ pydantic.Field(alias="type"),
+ ] = "session.created"
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = set(["type"])
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k, serialized.get(n))
+
+ if val != UNSET_SENTINEL:
+ if val is not None or k not in optional_fields:
+ m[k] = val
+
+ return m
+
+
+try:
+ RealtimeSpeechSessionCreated.model_rebuild()
+except NameError:
+ pass
diff --git a/src/mistralai/client/models/realtimespeechsessionupdated.py b/src/mistralai/client/models/realtimespeechsessionupdated.py
new file mode 100644
index 000000000..e5043ae5a
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeechsessionupdated.py
@@ -0,0 +1,50 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from .realtimespeechsession import RealtimeSpeechSession, RealtimeSpeechSessionTypedDict
+from mistralai.client.types import BaseModel, UNSET_SENTINEL
+from mistralai.client.utils import validate_const
+import pydantic
+from pydantic import model_serializer
+from pydantic.functional_validators import AfterValidator
+from typing import Literal, Optional
+from typing_extensions import Annotated, TypedDict
+
+
+class RealtimeSpeechSessionUpdatedTypedDict(TypedDict):
+ session: RealtimeSpeechSessionTypedDict
+ type: Literal["session.updated"]
+
+
+class RealtimeSpeechSessionUpdated(BaseModel):
+ session: RealtimeSpeechSession
+
+ type: Annotated[
+ Annotated[
+ Optional[Literal["session.updated"]],
+ AfterValidator(validate_const("session.updated")),
+ ],
+ pydantic.Field(alias="type"),
+ ] = "session.updated"
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = set(["type"])
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k, serialized.get(n))
+
+ if val != UNSET_SENTINEL:
+ if val is not None or k not in optional_fields:
+ m[k] = val
+
+ return m
+
+
+try:
+ RealtimeSpeechSessionUpdated.model_rebuild()
+except NameError:
+ pass
diff --git a/src/mistralai/client/models/realtimespeechsessionupdatemessage.py b/src/mistralai/client/models/realtimespeechsessionupdatemessage.py
new file mode 100644
index 000000000..fbb541fa5
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeechsessionupdatemessage.py
@@ -0,0 +1,53 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from .realtimespeechsessionupdatepayload import (
+ RealtimeSpeechSessionUpdatePayload,
+ RealtimeSpeechSessionUpdatePayloadTypedDict,
+)
+from mistralai.client.types import BaseModel, UNSET_SENTINEL
+from mistralai.client.utils import validate_const
+import pydantic
+from pydantic import model_serializer
+from pydantic.functional_validators import AfterValidator
+from typing import Literal, Optional
+from typing_extensions import Annotated, TypedDict
+
+
+class RealtimeSpeechSessionUpdateMessageTypedDict(TypedDict):
+ session: RealtimeSpeechSessionUpdatePayloadTypedDict
+ type: Literal["session.update"]
+
+
+class RealtimeSpeechSessionUpdateMessage(BaseModel):
+ session: RealtimeSpeechSessionUpdatePayload
+
+ type: Annotated[
+ Annotated[
+ Optional[Literal["session.update"]],
+ AfterValidator(validate_const("session.update")),
+ ],
+ pydantic.Field(alias="type"),
+ ] = "session.update"
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = set(["type"])
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k, serialized.get(n))
+
+ if val != UNSET_SENTINEL:
+ if val is not None or k not in optional_fields:
+ m[k] = val
+
+ return m
+
+
+try:
+ RealtimeSpeechSessionUpdateMessage.model_rebuild()
+except NameError:
+ pass
diff --git a/src/mistralai/client/models/realtimespeechsessionupdatepayload.py b/src/mistralai/client/models/realtimespeechsessionupdatepayload.py
new file mode 100644
index 000000000..0feae7e82
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeechsessionupdatepayload.py
@@ -0,0 +1,85 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from .realtimespeechstreamingmode import RealtimeSpeechStreamingMode
+from .speechoutputformat import SpeechOutputFormat
+from mistralai.client.types import (
+ BaseModel,
+ Nullable,
+ OptionalNullable,
+ UNSET,
+ UNSET_SENTINEL,
+)
+from pydantic import model_serializer
+from typing_extensions import NotRequired, TypedDict
+
+
+class RealtimeSpeechSessionUpdatePayloadTypedDict(TypedDict):
+ voice: NotRequired[Nullable[str]]
+ r"""Preset voice identifier. Mutually exclusive with ref_audio and voice_profile."""
+ ref_audio: NotRequired[Nullable[str]]
+ r"""Base64-encoded reference audio. Mutually exclusive with voice and voice_profile."""
+ streaming_mode: NotRequired[Nullable[RealtimeSpeechStreamingMode]]
+ r"""responsive (default): short chunks, for conversational turns. long_form: chunks grow as the text goes on, for smoother delivery of long texts."""
+ response_format: NotRequired[Nullable[SpeechOutputFormat]]
+ r"""Audio encoding of speech.audio.delta events. pcm is raw 32-bit float little-endian mono."""
+ expressiveness: NotRequired[Nullable[float]]
+ r"""Higher values make the delivery more expressive. Minimum 1.0."""
+
+
+class RealtimeSpeechSessionUpdatePayload(BaseModel):
+ voice: OptionalNullable[str] = UNSET
+ r"""Preset voice identifier. Mutually exclusive with ref_audio and voice_profile."""
+
+ ref_audio: OptionalNullable[str] = UNSET
+ r"""Base64-encoded reference audio. Mutually exclusive with voice and voice_profile."""
+
+ streaming_mode: OptionalNullable[RealtimeSpeechStreamingMode] = UNSET
+ r"""responsive (default): short chunks, for conversational turns. long_form: chunks grow as the text goes on, for smoother delivery of long texts."""
+
+ response_format: OptionalNullable[SpeechOutputFormat] = UNSET
+ r"""Audio encoding of speech.audio.delta events. pcm is raw 32-bit float little-endian mono."""
+
+ expressiveness: OptionalNullable[float] = UNSET
+ r"""Higher values make the delivery more expressive. Minimum 1.0."""
+
+ @model_serializer(mode="wrap")
+ def serialize_model(self, handler):
+ optional_fields = set(
+ [
+ "voice",
+ "ref_audio",
+ "streaming_mode",
+ "response_format",
+ "expressiveness",
+ ]
+ )
+ nullable_fields = set(
+ [
+ "voice",
+ "ref_audio",
+ "streaming_mode",
+ "response_format",
+ "expressiveness",
+ ]
+ )
+ serialized = handler(self)
+ m = {}
+
+ for n, f in type(self).model_fields.items():
+ k = f.alias or n
+ val = serialized.get(k, serialized.get(n))
+ is_nullable_and_explicitly_set = (
+ k in nullable_fields
+ and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
+ )
+
+ if val != UNSET_SENTINEL:
+ if (
+ val is not None
+ or k not in optional_fields
+ or is_nullable_and_explicitly_set
+ ):
+ m[k] = val
+
+ return m
diff --git a/src/mistralai/client/models/realtimespeechstreamingmode.py b/src/mistralai/client/models/realtimespeechstreamingmode.py
new file mode 100644
index 000000000..e4c169cf7
--- /dev/null
+++ b/src/mistralai/client/models/realtimespeechstreamingmode.py
@@ -0,0 +1,10 @@
+"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
+
+from __future__ import annotations
+from typing import Literal
+
+
+RealtimeSpeechStreamingMode = Literal[
+ "responsive",
+ "long_form",
+]
diff --git a/src/mistralai/client/models/roles.py b/src/mistralai/client/models/roles.py
deleted file mode 100644
index 42263b5d1..000000000
--- a/src/mistralai/client/models/roles.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
-
-from __future__ import annotations
-from mistralai.client.types import UnrecognizedStr
-from typing import Literal, Union
-
-
-Roles = Union[
- Literal[
- "system",
- "user",
- "assistant",
- "tool",
- ],
- UnrecognizedStr,
-]
diff --git a/src/mistralai/client/models/speechstreamdone.py b/src/mistralai/client/models/speechstreamdone.py
index 5d29dd86c..c435a4ae0 100644
--- a/src/mistralai/client/models/speechstreamdone.py
+++ b/src/mistralai/client/models/speechstreamdone.py
@@ -1,7 +1,7 @@
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
from __future__ import annotations
-from .usageinfo_dollar_defs import UsageInfoDollarDefs, UsageInfoDollarDefsTypedDict
+from .usageinfo import UsageInfo, UsageInfoTypedDict
from mistralai.client.types import BaseModel
from mistralai.client.utils import validate_const
import pydantic
@@ -11,12 +11,12 @@
class SpeechStreamDoneTypedDict(TypedDict):
- usage: UsageInfoDollarDefsTypedDict
+ usage: UsageInfoTypedDict
type: Literal["speech.audio.done"]
class SpeechStreamDone(BaseModel):
- usage: UsageInfoDollarDefs
+ usage: UsageInfo
type: Annotated[
Annotated[
diff --git a/src/mistralai/client/models/usageinfo_dollar_defs.py b/src/mistralai/client/models/usageinfo_dollar_defs.py
deleted file mode 100644
index 25ffc4e40..000000000
--- a/src/mistralai/client/models/usageinfo_dollar_defs.py
+++ /dev/null
@@ -1,104 +0,0 @@
-"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
-
-from __future__ import annotations
-from .completiontokensdetails import (
- CompletionTokensDetails,
- CompletionTokensDetailsTypedDict,
-)
-from .prompttokensdetails import PromptTokensDetails, PromptTokensDetailsTypedDict
-from mistralai.client.types import (
- BaseModel,
- Nullable,
- OptionalNullable,
- UNSET,
- UNSET_SENTINEL,
-)
-from pydantic import model_serializer
-from typing import Optional
-from typing_extensions import NotRequired, TypedDict
-
-
-class UsageInfoDollarDefsTypedDict(TypedDict):
- prompt_audio_seconds: NotRequired[Nullable[int]]
- prompt_tokens: NotRequired[int]
- total_tokens: NotRequired[int]
- completion_tokens: NotRequired[Nullable[int]]
- request_count: NotRequired[Nullable[int]]
- prompt_tokens_details: NotRequired[Nullable[PromptTokensDetailsTypedDict]]
- completion_tokens_details: NotRequired[Nullable[CompletionTokensDetailsTypedDict]]
- service_tier: NotRequired[Nullable[str]]
- r"""The service tier at which the request was processed: standard or priority."""
- prompt_token_details: NotRequired[Nullable[PromptTokensDetailsTypedDict]]
- num_cached_tokens: NotRequired[Nullable[int]]
-
-
-class UsageInfoDollarDefs(BaseModel):
- prompt_audio_seconds: OptionalNullable[int] = UNSET
-
- prompt_tokens: Optional[int] = 0
-
- total_tokens: Optional[int] = 0
-
- completion_tokens: OptionalNullable[int] = UNSET
-
- request_count: OptionalNullable[int] = UNSET
-
- prompt_tokens_details: OptionalNullable[PromptTokensDetails] = UNSET
-
- completion_tokens_details: OptionalNullable[CompletionTokensDetails] = UNSET
-
- service_tier: OptionalNullable[str] = UNSET
- r"""The service tier at which the request was processed: standard or priority."""
-
- prompt_token_details: OptionalNullable[PromptTokensDetails] = UNSET
-
- num_cached_tokens: OptionalNullable[int] = UNSET
-
- @model_serializer(mode="wrap")
- def serialize_model(self, handler):
- optional_fields = set(
- [
- "prompt_audio_seconds",
- "prompt_tokens",
- "total_tokens",
- "completion_tokens",
- "request_count",
- "prompt_tokens_details",
- "completion_tokens_details",
- "service_tier",
- "prompt_token_details",
- "num_cached_tokens",
- ]
- )
- nullable_fields = set(
- [
- "prompt_audio_seconds",
- "completion_tokens",
- "request_count",
- "prompt_tokens_details",
- "completion_tokens_details",
- "service_tier",
- "prompt_token_details",
- "num_cached_tokens",
- ]
- )
- serialized = handler(self)
- m = {}
-
- for n, f in type(self).model_fields.items():
- k = f.alias or n
- val = serialized.get(k, serialized.get(n))
- is_nullable_and_explicitly_set = (
- k in nullable_fields
- and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
- )
-
- if val != UNSET_SENTINEL:
- if (
- val is not None
- or k not in optional_fields
- or is_nullable_and_explicitly_set
- ):
- m[k] = val
-
- return m
diff --git a/src/mistralai/extra/__init__.py b/src/mistralai/extra/__init__.py
index cabda728f..fa4231c6c 100644
--- a/src/mistralai/extra/__init__.py
+++ b/src/mistralai/extra/__init__.py
@@ -18,6 +18,25 @@
RealtimeTranscriptionSessionCreated,
RealtimeTranscriptionSessionUpdated,
RealtimeTranscription,
+ TurnDetectionConfig,
+ TurnDetectionConfigTypedDict,
+ TurnDetectionEagerEndEvent,
+ TurnDetectionEndEvent,
+ TurnDetectionEndReason,
+ TurnDetectionResumeEvent,
+ TurnDetectionStartEvent,
+ RealtimeSpeech,
+ RealtimeSpeechConnection,
+ RealtimeSpeechEvent,
+ RealtimeSpeechError,
+ RealtimeSpeechErrorDetail,
+ RealtimeSpeechSession,
+ RealtimeSpeechSessionCreated,
+ RealtimeSpeechSessionUpdated,
+ RealtimeSpeechStreamingMode,
+ SpeechOutputFormat,
+ SpeechStreamAudioDelta,
+ SpeechStreamDone,
UnknownRealtimeEvent,
)
@@ -32,6 +51,25 @@
"RealtimeTranscriptionSession",
"RealtimeTranscriptionSessionCreated",
"RealtimeTranscriptionSessionUpdated",
+ "TurnDetectionConfig",
+ "TurnDetectionConfigTypedDict",
+ "TurnDetectionEagerEndEvent",
+ "TurnDetectionEndEvent",
+ "TurnDetectionEndReason",
+ "TurnDetectionResumeEvent",
+ "TurnDetectionStartEvent",
+ "RealtimeSpeech",
+ "RealtimeSpeechConnection",
+ "RealtimeSpeechEvent",
+ "RealtimeSpeechError",
+ "RealtimeSpeechErrorDetail",
+ "RealtimeSpeechSession",
+ "RealtimeSpeechSessionCreated",
+ "RealtimeSpeechSessionUpdated",
+ "RealtimeSpeechStreamingMode",
+ "SpeechOutputFormat",
+ "SpeechStreamAudioDelta",
+ "SpeechStreamDone",
}
@@ -58,4 +96,23 @@ def __getattr__(name: str):
"RealtimeTranscriptionSession",
"RealtimeTranscriptionSessionCreated",
"RealtimeTranscriptionSessionUpdated",
+ "TurnDetectionConfig",
+ "TurnDetectionConfigTypedDict",
+ "TurnDetectionEagerEndEvent",
+ "TurnDetectionEndEvent",
+ "TurnDetectionEndReason",
+ "TurnDetectionResumeEvent",
+ "TurnDetectionStartEvent",
+ "RealtimeSpeech",
+ "RealtimeSpeechConnection",
+ "RealtimeSpeechEvent",
+ "RealtimeSpeechError",
+ "RealtimeSpeechErrorDetail",
+ "RealtimeSpeechSession",
+ "RealtimeSpeechSessionCreated",
+ "RealtimeSpeechSessionUpdated",
+ "RealtimeSpeechStreamingMode",
+ "SpeechOutputFormat",
+ "SpeechStreamAudioDelta",
+ "SpeechStreamDone",
]
diff --git a/src/mistralai/extra/exceptions.py b/src/mistralai/extra/exceptions.py
index 82507f2c1..d15ad02f5 100644
--- a/src/mistralai/extra/exceptions.py
+++ b/src/mistralai/extra/exceptions.py
@@ -12,7 +12,7 @@
)
if TYPE_CHECKING:
- from mistralai.client.models import RealtimeTranscriptionError
+ from mistralai.client.models import RealtimeSpeechError, RealtimeTranscriptionError
from mistralai.extra.run.result import RunOutputEntries
@@ -313,3 +313,32 @@ def __init__(
)
self.payload_typed = payload
self.payload_raw = raw
+
+
+class RealtimeSpeechException(MistralClientException):
+ def __init__(
+ self,
+ message: str,
+ *,
+ code: Optional[int] = None,
+ payload: Optional[object] = None,
+ ) -> None:
+ super().__init__(message)
+ self.code = code
+ self.payload = payload
+
+
+class RealtimeSpeechWSError(RealtimeSpeechException):
+ def __init__(
+ self,
+ message: str,
+ *,
+ payload: Optional["RealtimeSpeechError"] = None,
+ raw: Optional[object] = None,
+ ) -> None:
+ code = payload.error.code if payload is not None else None
+ super().__init__(
+ message, code=code, payload=payload if payload is not None else raw
+ )
+ self.payload_typed = payload
+ self.payload_raw = raw
diff --git a/src/mistralai/extra/realtime/__init__.py b/src/mistralai/extra/realtime/__init__.py
index 7b80e0451..aa18bc2ae 100644
--- a/src/mistralai/extra/realtime/__init__.py
+++ b/src/mistralai/extra/realtime/__init__.py
@@ -1,14 +1,36 @@
from mistralai.client.models import (
AudioEncoding,
AudioFormat,
+ RealtimeSpeechError,
+ RealtimeSpeechErrorDetail,
+ RealtimeSpeechSession,
+ RealtimeSpeechSessionCreated,
+ RealtimeSpeechSessionUpdated,
+ RealtimeSpeechStreamingMode,
RealtimeTranscriptionError,
RealtimeTranscriptionErrorDetail,
RealtimeTranscriptionSession,
RealtimeTranscriptionSessionCreated,
RealtimeTranscriptionSessionUpdated,
+ SpeechOutputFormat,
+ SpeechStreamAudioDelta,
+ SpeechStreamDone,
+ TurnDetectionConfig,
+ TurnDetectionConfigTypedDict,
+ TurnDetectionEagerEndEvent,
+ TurnDetectionEndEvent,
+ TurnDetectionEndReason,
+ TurnDetectionResumeEvent,
+ TurnDetectionStartEvent,
)
from .connection import UnknownRealtimeEvent, RealtimeConnection
+from .speech import (
+ RealtimeSpeech,
+ RealtimeSpeechConnection,
+ RealtimeSpeechEvent,
+ audio_bytes,
+)
from .transcription import RealtimeTranscription
__all__ = [
@@ -19,7 +41,27 @@
"RealtimeTranscriptionSession",
"RealtimeTranscriptionSessionCreated",
"RealtimeTranscriptionSessionUpdated",
+ "TurnDetectionConfig",
+ "TurnDetectionConfigTypedDict",
+ "TurnDetectionEagerEndEvent",
+ "TurnDetectionEndEvent",
+ "TurnDetectionEndReason",
+ "TurnDetectionResumeEvent",
+ "TurnDetectionStartEvent",
"RealtimeConnection",
"RealtimeTranscription",
"UnknownRealtimeEvent",
+ "RealtimeSpeech",
+ "RealtimeSpeechConnection",
+ "RealtimeSpeechEvent",
+ "RealtimeSpeechError",
+ "RealtimeSpeechErrorDetail",
+ "RealtimeSpeechSession",
+ "RealtimeSpeechSessionCreated",
+ "RealtimeSpeechSessionUpdated",
+ "RealtimeSpeechStreamingMode",
+ "SpeechOutputFormat",
+ "SpeechStreamAudioDelta",
+ "SpeechStreamDone",
+ "audio_bytes",
]
diff --git a/src/mistralai/extra/realtime/connection.py b/src/mistralai/extra/realtime/connection.py
index 6547052b0..082ca1db8 100644
--- a/src/mistralai/extra/realtime/connection.py
+++ b/src/mistralai/extra/realtime/connection.py
@@ -4,7 +4,7 @@
import json
from asyncio import CancelledError
from collections import deque
-from typing import Any, AsyncIterator, Deque, Optional, Union
+from typing import Any, AsyncIterator, Deque, Dict, List, Optional, Union
from pydantic import ValidationError, BaseModel
@@ -31,8 +31,13 @@
TranscriptionStreamLanguage,
TranscriptionStreamSegmentDelta,
TranscriptionStreamTextDelta,
+ TurnDetectionConfig,
+ TurnDetectionConfigTypedDict,
+ TurnDetectionEagerEndEvent,
+ TurnDetectionEndEvent,
+ TurnDetectionResumeEvent,
+ TurnDetectionStartEvent,
)
-from mistralai.client.types import UNSET
class UnknownRealtimeEvent(BaseModel):
@@ -59,6 +64,11 @@ class UnknownRealtimeEvent(BaseModel):
TranscriptionStreamSegmentDelta,
TranscriptionStreamTextDelta,
TranscriptionStreamDone,
+ # turn detection events
+ TurnDetectionStartEvent,
+ TurnDetectionEagerEndEvent,
+ TurnDetectionResumeEvent,
+ TurnDetectionEndEvent,
# forward-compat fallback
UnknownRealtimeEvent,
]
@@ -71,6 +81,10 @@ class UnknownRealtimeEvent(BaseModel):
"transcription.segment": TranscriptionStreamSegmentDelta,
"transcription.text.delta": TranscriptionStreamTextDelta,
"transcription.done": TranscriptionStreamDone,
+ "turn.start": TurnDetectionStartEvent,
+ "turn.eager_end": TurnDetectionEagerEndEvent,
+ "turn.resume": TurnDetectionResumeEvent,
+ "turn.end": TurnDetectionEndEvent,
}
@@ -156,20 +170,32 @@ async def update_session(
audio_format: Optional[AudioFormat] = None,
*,
target_streaming_delay_ms: Optional[int] = None,
+ diarize: Optional[bool] = None,
+ context_bias: Optional[List[str]] = None,
+ language: Optional[str] = None,
+ speaker_references: Optional[Dict[str, str]] = None,
+ turn_detection: Optional[
+ Union[TurnDetectionConfig, TurnDetectionConfigTypedDict]
+ ] = None,
) -> None:
if self._closed:
raise RuntimeError("Connection is closed")
- if audio_format is None and target_streaming_delay_ms is None:
+ fields = {
+ "audio_format": audio_format,
+ "target_streaming_delay_ms": target_streaming_delay_ms,
+ "diarize": diarize,
+ "context_bias": context_bias,
+ "language": language,
+ "speaker_references": speaker_references,
+ "turn_detection": turn_detection,
+ }
+ fields = {name: value for name, value in fields.items() if value is not None}
+ if not fields:
raise ValueError("At least one session field must be provided")
message = RealtimeTranscriptionSessionUpdateMessage(
- session=RealtimeTranscriptionSessionUpdatePayload(
- audio_format=audio_format if audio_format is not None else UNSET,
- target_streaming_delay_ms=target_streaming_delay_ms
- if target_streaming_delay_ms is not None
- else UNSET,
- )
+ session=RealtimeTranscriptionSessionUpdatePayload.model_validate(fields)
)
await self._websocket.send(message.model_dump_json())
diff --git a/src/mistralai/extra/realtime/speech.py b/src/mistralai/extra/realtime/speech.py
new file mode 100644
index 000000000..7aa90b22e
--- /dev/null
+++ b/src/mistralai/extra/realtime/speech.py
@@ -0,0 +1,460 @@
+from __future__ import annotations
+
+import asyncio
+import base64
+import json
+import time
+from asyncio import CancelledError
+from collections import deque
+from typing import Any, AsyncIterator, Deque, Mapping, Optional, Union
+from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
+
+try:
+ from websockets.asyncio.client import (
+ ClientConnection,
+ connect,
+ ) # websockets >= 13.0
+ from websockets.exceptions import ConnectionClosed, ConnectionClosedError
+except ImportError as exc:
+ raise ImportError(
+ "The `websockets` package (>=13.0) is required for real-time speech. "
+ "Install with: pip install 'mistralai[realtime]'"
+ ) from exc
+
+from pydantic import ValidationError
+
+from mistralai.client import models, utils
+from mistralai.client._hooks.service_account_auth import (
+ bearer_header,
+ read_service_account_token,
+)
+from mistralai.client.models import (
+ RealtimeSpeechError,
+ RealtimeSpeechInputTextAppend,
+ RealtimeSpeechInputTextEnd,
+ RealtimeSpeechInputTextFlush,
+ RealtimeSpeechSession,
+ RealtimeSpeechSessionCreated,
+ RealtimeSpeechSessionUpdated,
+ RealtimeSpeechSessionUpdateMessage,
+ RealtimeSpeechSessionUpdatePayload,
+ RealtimeSpeechStreamingMode,
+ SpeechOutputFormat,
+ SpeechStreamAudioDelta,
+ SpeechStreamDone,
+)
+from mistralai.client.sdkconfiguration import SDKConfiguration
+from mistralai.client.utils import generate_url, get_security, get_security_from_env
+
+from ..exceptions import RealtimeSpeechException, RealtimeSpeechWSError
+from .connection import UnknownRealtimeEvent
+
+RealtimeSpeechEvent = Union[
+ RealtimeSpeechSessionCreated,
+ RealtimeSpeechSessionUpdated,
+ RealtimeSpeechError,
+ SpeechStreamAudioDelta,
+ SpeechStreamDone,
+ UnknownRealtimeEvent,
+]
+
+_MESSAGE_MODELS: dict[str, Any] = {
+ "session.created": RealtimeSpeechSessionCreated,
+ "session.updated": RealtimeSpeechSessionUpdated,
+ "error": RealtimeSpeechError,
+ "speech.audio.delta": SpeechStreamAudioDelta,
+ "speech.audio.done": SpeechStreamDone,
+}
+
+
+def parse_realtime_speech_event(payload: Any) -> RealtimeSpeechEvent:
+ if not isinstance(payload, dict):
+ return UnknownRealtimeEvent(
+ type=None, content=payload, error="expected JSON object"
+ )
+
+ msg_type = payload.get("type")
+ if not isinstance(msg_type, str):
+ return UnknownRealtimeEvent(
+ type=None, content=payload, error="missing/invalid 'type'"
+ )
+
+ model_cls = _MESSAGE_MODELS.get(msg_type)
+ if model_cls is None:
+ return UnknownRealtimeEvent(
+ type=msg_type, content=payload, error="unknown event type"
+ )
+ try:
+ return model_cls.model_validate(payload)
+ except ValidationError as exc:
+ return UnknownRealtimeEvent(type=msg_type, content=payload, error=str(exc))
+
+
+def _parse_message(msg: Union[str, bytes, bytearray]) -> RealtimeSpeechEvent:
+ text = (
+ msg.decode("utf-8", errors="replace")
+ if isinstance(msg, (bytes, bytearray))
+ else msg
+ )
+ try:
+ data = json.loads(text)
+ except Exception as exc:
+ return UnknownRealtimeEvent(
+ type=None, content=text, error=f"invalid JSON: {exc}"
+ )
+ return parse_realtime_speech_event(data)
+
+
+class RealtimeSpeechConnection:
+ def __init__(
+ self,
+ websocket: ClientConnection,
+ session: RealtimeSpeechSession,
+ *,
+ initial_events: Optional[list[RealtimeSpeechEvent]] = None,
+ ) -> None:
+ self._websocket = websocket
+ self._session = session
+ self._closed = False
+ self._initial_events: Deque[RealtimeSpeechEvent] = deque(initial_events or [])
+
+ @property
+ def request_id(self) -> str:
+ return self._session.request_id
+
+ @property
+ def session(self) -> RealtimeSpeechSession:
+ return self._session
+
+ @property
+ def is_closed(self) -> bool:
+ return self._closed
+
+ async def send_text(self, text: str) -> None:
+ if self._closed:
+ raise RuntimeError("Connection is closed")
+ await self._websocket.send(
+ RealtimeSpeechInputTextAppend(text=text).model_dump_json()
+ )
+
+ async def flush(self, text: Optional[str] = None) -> None:
+ if self._closed:
+ raise RuntimeError("Connection is closed")
+ message = (
+ RealtimeSpeechInputTextFlush()
+ if text is None
+ else RealtimeSpeechInputTextFlush(text=text)
+ )
+ await self._websocket.send(message.model_dump_json())
+
+ async def end(self) -> None:
+ if self._closed:
+ return
+ await self._websocket.send(RealtimeSpeechInputTextEnd().model_dump_json())
+
+ async def update_session(
+ self,
+ *,
+ voice_id: Optional[str] = None,
+ ref_audio: Optional[str] = None,
+ response_format: Optional[SpeechOutputFormat] = None,
+ streaming_mode: Optional[RealtimeSpeechStreamingMode] = None,
+ expressiveness: Optional[float] = None,
+ ) -> None:
+ if self._closed:
+ raise RuntimeError("Connection is closed")
+
+ fields = {
+ "voice": voice_id,
+ "ref_audio": ref_audio,
+ "response_format": response_format,
+ "streaming_mode": streaming_mode,
+ "expressiveness": expressiveness,
+ }
+ fields = {name: value for name, value in fields.items() if value is not None}
+ if not fields:
+ raise ValueError("At least one session field must be provided")
+
+ message = RealtimeSpeechSessionUpdateMessage(
+ session=RealtimeSpeechSessionUpdatePayload.model_validate(fields)
+ )
+ await self._websocket.send(message.model_dump_json())
+
+ async def close(self, *, code: int = 1000, reason: str = "") -> None:
+ if self._closed:
+ return
+ self._closed = True
+ await self._websocket.close(code=code, reason=reason)
+
+ async def __aenter__(self) -> "RealtimeSpeechConnection":
+ return self
+
+ async def __aexit__(self, exc_type, exc, tb) -> None:
+ await self.close()
+
+ def __aiter__(self) -> AsyncIterator[RealtimeSpeechEvent]:
+ return self.events()
+
+ async def events(self) -> AsyncIterator[RealtimeSpeechEvent]:
+ while self._initial_events:
+ ev = self._initial_events.popleft()
+ self._apply_session_updates(ev)
+ yield ev
+
+ try:
+ async for msg in self._websocket:
+ ev = _parse_message(msg)
+ self._apply_session_updates(ev)
+ yield ev
+ except CancelledError:
+ pass
+ except ConnectionClosedError as exc:
+ # Unknown voices etc. are rejected with a close frame, not an error event.
+ raise RealtimeSpeechException(
+ exc.rcvd.reason if exc.rcvd else "Connection closed by the server",
+ code=exc.rcvd.code if exc.rcvd else None,
+ ) from exc
+ finally:
+ await self.close()
+
+ def _apply_session_updates(self, ev: RealtimeSpeechEvent) -> None:
+ if isinstance(ev, (RealtimeSpeechSessionCreated, RealtimeSpeechSessionUpdated)):
+ self._session = ev.session
+
+
+class RealtimeSpeech:
+ def __init__(self, sdk_config: SDKConfiguration) -> None:
+ self._sdk_config = sdk_config
+
+ def _build_url(
+ self,
+ model: str,
+ *,
+ server_url: Optional[str],
+ query_params: Mapping[str, str],
+ ) -> str:
+ if server_url is not None:
+ base_url = utils.remove_suffix(server_url, "/")
+ else:
+ base_url, _ = self._sdk_config.get_server_details()
+
+ url = generate_url(base_url, "/v1/audio/speech/realtime", None)
+
+ parsed = urlparse(url)
+ merged = dict(parse_qsl(parsed.query, keep_blank_values=True))
+ merged["model"] = model
+ merged.update(dict(query_params))
+
+ return urlunparse(parsed._replace(query=urlencode(merged)))
+
+ async def connect(
+ self,
+ model: str,
+ *,
+ voice_id: Optional[str] = None,
+ ref_audio: Optional[str] = None,
+ response_format: Optional[SpeechOutputFormat] = None,
+ streaming_mode: Optional[RealtimeSpeechStreamingMode] = None,
+ expressiveness: Optional[float] = None,
+ server_url: Optional[str] = None,
+ timeout_ms: Optional[int] = None,
+ http_headers: Optional[Mapping[str, str]] = None,
+ ) -> RealtimeSpeechConnection:
+ if timeout_ms is None:
+ timeout_ms = self._sdk_config.timeout_ms
+
+ security = self._sdk_config.security
+ if security is not None and callable(security):
+ security = security()
+
+ resolved_security = get_security_from_env(security, models.Security)
+
+ headers: dict[str, str] = {}
+ query_params: dict[str, str] = {}
+
+ if resolved_security is not None:
+ security_headers, security_query = get_security(resolved_security)
+ headers |= security_headers
+ for key, values in security_query.items():
+ if values:
+ query_params[key] = values[-1]
+
+ # Websocket handshake bypasses SDK hooks: apply service-account auth here.
+ caller_authorization = any(
+ name.lower() == "authorization" for name in (http_headers or {})
+ )
+ if self._sdk_config.security is None and not caller_authorization:
+ token = read_service_account_token()
+ if token is not None:
+ headers["Authorization"] = bearer_header(token)
+
+ if http_headers is not None:
+ headers |= dict(http_headers)
+
+ url = self._build_url(model, server_url=server_url, query_params=query_params)
+
+ parsed = urlparse(url)
+ if parsed.scheme == "https":
+ parsed = parsed._replace(scheme="wss")
+ elif parsed.scheme == "http":
+ parsed = parsed._replace(scheme="ws")
+ ws_url = urlunparse(parsed)
+ open_timeout = None if timeout_ms is None else timeout_ms / 1000.0
+
+ websocket: Optional[ClientConnection] = None
+ try:
+ websocket = await connect(
+ ws_url,
+ additional_headers=dict(headers),
+ open_timeout=open_timeout,
+ user_agent_header=self._sdk_config.user_agent,
+ )
+
+ created_session, initial_events = await _recv_handshake(
+ websocket, timeout_ms=timeout_ms
+ )
+ connection = RealtimeSpeechConnection(
+ websocket=websocket,
+ session=created_session,
+ initial_events=initial_events,
+ )
+
+ session_fields = (
+ voice_id,
+ ref_audio,
+ response_format,
+ streaming_mode,
+ expressiveness,
+ )
+ if any(value is not None for value in session_fields):
+ await connection.update_session(
+ voice_id=voice_id,
+ ref_audio=ref_audio,
+ response_format=response_format,
+ streaming_mode=streaming_mode,
+ expressiveness=expressiveness,
+ )
+
+ return connection
+
+ except RealtimeSpeechException:
+ if websocket is not None:
+ await websocket.close()
+ raise
+ except Exception as exc:
+ if websocket is not None:
+ await websocket.close()
+ raise RealtimeSpeechException(f"Failed to connect: {exc}") from exc
+
+ async def speak_stream(
+ self,
+ text_stream: AsyncIterator[str],
+ model: str,
+ *,
+ voice_id: Optional[str] = None,
+ ref_audio: Optional[str] = None,
+ response_format: Optional[SpeechOutputFormat] = None,
+ streaming_mode: Optional[RealtimeSpeechStreamingMode] = None,
+ expressiveness: Optional[float] = None,
+ server_url: Optional[str] = None,
+ timeout_ms: Optional[int] = None,
+ http_headers: Optional[Mapping[str, str]] = None,
+ ) -> AsyncIterator[RealtimeSpeechEvent]:
+ async with await self.connect(
+ model,
+ voice_id=voice_id,
+ ref_audio=ref_audio,
+ response_format=response_format,
+ streaming_mode=streaming_mode,
+ expressiveness=expressiveness,
+ server_url=server_url,
+ timeout_ms=timeout_ms,
+ http_headers=http_headers,
+ ) as connection:
+
+ async def _send() -> None:
+ try:
+ async for text in text_stream:
+ if connection.is_closed:
+ break
+ await connection.send_text(text)
+ await connection.end()
+ except Exception:
+ await connection.close()
+ raise
+
+ send_task = asyncio.create_task(_send())
+
+ try:
+ async for event in connection:
+ yield event
+
+ if isinstance(event, (RealtimeSpeechError, SpeechStreamDone)):
+ break
+ finally:
+ send_task.cancel()
+ try:
+ await send_task
+ except (asyncio.CancelledError, ConnectionClosed):
+ pass
+ await connection.close()
+
+
+def audio_bytes(event: SpeechStreamAudioDelta) -> bytes:
+ return base64.b64decode(event.audio_data)
+
+
+def _extract_error_message(payload: dict) -> str:
+ err = payload.get("error")
+ if isinstance(err, dict):
+ msg = err.get("message")
+ if isinstance(msg, str):
+ return msg
+ if isinstance(msg, dict):
+ detail = msg.get("detail")
+ if isinstance(detail, str):
+ return detail
+ return "Realtime speech error"
+
+
+async def _recv_handshake(
+ websocket: ClientConnection,
+ *,
+ timeout_ms: Optional[int],
+) -> tuple[RealtimeSpeechSession, list[RealtimeSpeechEvent]]:
+ timeout_s = None if timeout_ms is None else timeout_ms / 1000.0
+ deadline = None if timeout_s is None else (time.monotonic() + timeout_s)
+
+ initial_events: list[RealtimeSpeechEvent] = []
+
+ def remaining() -> Optional[float]:
+ if deadline is None:
+ return None
+ return max(0.0, deadline - time.monotonic())
+
+ try:
+ while True:
+ raw = await asyncio.wait_for(websocket.recv(), timeout=remaining())
+ event = _parse_message(raw)
+ initial_events.append(event)
+
+ if isinstance(event, RealtimeSpeechError):
+ raise RealtimeSpeechWSError(
+ _extract_error_message(event.model_dump()), payload=event
+ )
+ if isinstance(event, UnknownRealtimeEvent) and event.type == "error":
+ raw_payload = event.content if isinstance(event.content, dict) else {}
+ raise RealtimeSpeechWSError(
+ _extract_error_message(raw_payload), raw=event.content
+ )
+ if isinstance(event, RealtimeSpeechSessionCreated):
+ return event.session, initial_events
+
+ except asyncio.TimeoutError as exc:
+ raise RealtimeSpeechException("Timeout waiting for session creation.") from exc
+ except RealtimeSpeechException:
+ raise
+ except Exception as exc:
+ raise RealtimeSpeechException(
+ f"Unexpected websocket handshake failure: {exc}"
+ ) from exc
diff --git a/src/mistralai/extra/realtime/transcription.py b/src/mistralai/extra/realtime/transcription.py
index dd4008bf4..6f616f08e 100644
--- a/src/mistralai/extra/realtime/transcription.py
+++ b/src/mistralai/extra/realtime/transcription.py
@@ -3,7 +3,7 @@
import asyncio
import json
import time
-from typing import AsyncIterator, Mapping, Optional
+from typing import AsyncIterator, Dict, List, Mapping, Optional, Union
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
try:
@@ -23,6 +23,8 @@
RealtimeTranscriptionError,
RealtimeTranscriptionSession,
RealtimeTranscriptionSessionCreated,
+ TurnDetectionConfig,
+ TurnDetectionConfigTypedDict,
)
from mistralai.client.sdkconfiguration import SDKConfiguration
from mistralai.client.utils import generate_url, get_security, get_security_from_env
@@ -75,6 +77,14 @@ async def connect(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
+ *,
+ diarize: Optional[bool] = None,
+ context_bias: Optional[List[str]] = None,
+ language: Optional[str] = None,
+ speaker_references: Optional[Dict[str, str]] = None,
+ turn_detection: Optional[
+ Union[TurnDetectionConfig, TurnDetectionConfigTypedDict]
+ ] = None,
) -> RealtimeConnection:
if timeout_ms is None:
timeout_ms = self._sdk_config.timeout_ms
@@ -128,19 +138,33 @@ async def connect(
user_agent_header=user_agent,
)
- session, initial_events = await _recv_handshake(
+ created_session, initial_events = await _recv_handshake(
websocket, timeout_ms=timeout_ms
)
connection = RealtimeConnection(
websocket=websocket,
- session=session,
+ session=created_session,
initial_events=initial_events,
)
- if audio_format is not None or target_streaming_delay_ms is not None:
+ session_fields = (
+ audio_format,
+ target_streaming_delay_ms,
+ diarize,
+ context_bias,
+ language,
+ speaker_references,
+ turn_detection,
+ )
+ if any(value is not None for value in session_fields):
await connection.update_session(
audio_format,
target_streaming_delay_ms=target_streaming_delay_ms,
+ diarize=diarize,
+ context_bias=context_bias,
+ language=language,
+ speaker_references=speaker_references,
+ turn_detection=turn_detection,
)
return connection
@@ -163,6 +187,14 @@ async def transcribe_stream(
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
+ *,
+ diarize: Optional[bool] = None,
+ context_bias: Optional[List[str]] = None,
+ language: Optional[str] = None,
+ speaker_references: Optional[Dict[str, str]] = None,
+ turn_detection: Optional[
+ Union[TurnDetectionConfig, TurnDetectionConfigTypedDict]
+ ] = None,
) -> AsyncIterator[RealtimeEvent]:
"""
Flow
@@ -177,6 +209,11 @@ async def transcribe_stream(
server_url=server_url,
timeout_ms=timeout_ms,
http_headers=http_headers,
+ diarize=diarize,
+ context_bias=context_bias,
+ language=language,
+ speaker_references=speaker_references,
+ turn_detection=turn_detection,
) as connection:
async def _send() -> None:
diff --git a/src/mistralai/extra/tests/fixtures/workflow_encryption_parity.py b/src/mistralai/extra/tests/fixtures/workflow_encryption_parity.py
index 3e92fb308..479c22b07 100644
--- a/src/mistralai/extra/tests/fixtures/workflow_encryption_parity.py
+++ b/src/mistralai/extra/tests/fixtures/workflow_encryption_parity.py
@@ -115,6 +115,7 @@ def _encrypted(data: str) -> dict[str, str]:
),
"duplicate_keys": '{"k": 1, "j": 2, "k": {"field_type": "__encrypted_str__", "data": "v"}}',
"escapes": r'{"s": "é😀\/\b\f", "e": {"field_type": "__encrypted_str__", "data": "\u0000"}}',
+ "lone_surrogates": r'{"s": "\ud800 \udc00 \ud83d\ude00", "e": {"field_type": "__encrypted_str__", "data": "x"}}',
"no_encrypted_fields": '{"a":1,"b":[true,false,null]}',
"not_json": "this is not json",
}
diff --git a/src/mistralai/extra/tests/test_realtime.py b/src/mistralai/extra/tests/test_realtime.py
new file mode 100644
index 000000000..31ecdabd8
--- /dev/null
+++ b/src/mistralai/extra/tests/test_realtime.py
@@ -0,0 +1,221 @@
+import inspect
+import json
+import unittest
+from typing import Any, AsyncIterator, cast
+from unittest import mock
+
+from mistralai.client.models import (
+ AudioFormat,
+ RealtimeTranscriptionSession,
+ RealtimeTranscriptionSessionUpdatePayload,
+ TurnDetectionConfig,
+ TurnDetectionEagerEndEvent,
+ TurnDetectionEndEvent,
+ TurnDetectionResumeEvent,
+ TurnDetectionStartEvent,
+)
+from mistralai.extra.realtime.connection import (
+ RealtimeConnection,
+ UnknownRealtimeEvent,
+ parse_realtime_event,
+)
+from mistralai.extra.realtime.transcription import RealtimeTranscription
+
+MODEL = "voxtral-mini-transcribe-realtime-3"
+PCM_16K = {"encoding": "pcm_s16le", "sample_rate": 16000}
+SESSION = {"request_id": "ws-123", "model": MODEL, "audio_format": PCM_16K}
+SESSION_CREATED = {"type": "session.created", "session": SESSION}
+DEFAULT_TURN_DETECTION = TurnDetectionConfig().model_dump()
+
+
+class FakeWebSocket:
+ def __init__(self, incoming: list[dict[str, Any]] | None = None) -> None:
+ self.sent: list[dict[str, Any]] = []
+ self._incoming = [json.dumps(message) for message in incoming or []]
+
+ async def send(self, message: str) -> None:
+ self.sent.append(json.loads(message))
+
+ async def recv(self) -> str:
+ return self._incoming.pop(0)
+
+ async def close(self, code: int = 1000, reason: str = "") -> None:
+ pass
+
+ def __aiter__(self) -> AsyncIterator[str]:
+ return self._iter()
+
+ async def _iter(self) -> AsyncIterator[str]:
+ while self._incoming:
+ yield self._incoming.pop(0)
+
+
+def _connection(ws: FakeWebSocket) -> RealtimeConnection:
+ session = RealtimeTranscriptionSession.model_validate(SESSION)
+ return RealtimeConnection(websocket=cast(Any, ws), session=session)
+
+
+def _session_update(session: dict[str, Any]) -> dict[str, Any]:
+ return {"type": "session.update", "session": session}
+
+
+class TestParseTurnDetectionEvents(unittest.TestCase):
+ def test_turn_events_are_typed(self) -> None:
+ cases = [
+ ({"type": "turn.start", "time_s": 1.84}, TurnDetectionStartEvent),
+ ({"type": "turn.eager_end", "time_s": 2.5}, TurnDetectionEagerEndEvent),
+ ({"type": "turn.resume", "time_s": 2.9}, TurnDetectionResumeEvent),
+ (
+ {"type": "turn.end", "time_s": 4.4, "end_reason": "threshold"},
+ TurnDetectionEndEvent,
+ ),
+ ]
+ for payload, expected_cls in cases:
+ with self.subTest(event=payload["type"]):
+ event = parse_realtime_event(payload)
+ self.assertIsInstance(event, expected_cls)
+ self.assertEqual(getattr(event, "time_s"), payload["time_s"])
+ end_event = parse_realtime_event(cases[-1][0])
+ assert isinstance(end_event, TurnDetectionEndEvent)
+ self.assertEqual(end_event.end_reason, "threshold")
+
+ def test_invalid_turn_event_falls_back_to_unknown(self) -> None:
+ event = parse_realtime_event({"type": "turn.end", "time_s": 4.4})
+ self.assertIsInstance(event, UnknownRealtimeEvent)
+ self.assertEqual(event.type, "turn.end")
+
+
+class TestUpdateSession(unittest.IsolatedAsyncioTestCase):
+ async def test_sends_audio_format_only(self) -> None:
+ ws = FakeWebSocket()
+ await _connection(ws).update_session(
+ AudioFormat(encoding="pcm_s16le", sample_rate=24000)
+ )
+ self.assertEqual(
+ ws.sent,
+ [
+ _session_update(
+ {"audio_format": {"encoding": "pcm_s16le", "sample_rate": 24000}}
+ )
+ ],
+ )
+
+ async def test_sends_voxtral3_session_fields(self) -> None:
+ ws = FakeWebSocket()
+ await _connection(ws).update_session(
+ diarize=True,
+ context_bias=["Voxtral", "Harmattan"],
+ language="en",
+ speaker_references={"alice": "UklGRg=="},
+ )
+ self.assertEqual(
+ ws.sent,
+ [
+ _session_update(
+ {
+ "diarize": True,
+ "context_bias": ["Voxtral", "Harmattan"],
+ "language": "en",
+ "speaker_references": {"alice": "UklGRg=="},
+ }
+ )
+ ],
+ )
+
+ async def test_sends_turn_detection_dict(self) -> None:
+ ws = FakeWebSocket()
+ await _connection(ws).update_session(
+ turn_detection={"start_threshold": 0.8, "end_threshold": 0.2}
+ )
+ expected_turn_detection = {
+ **DEFAULT_TURN_DETECTION,
+ "start_threshold": 0.8,
+ "end_threshold": 0.2,
+ }
+ self.assertEqual(
+ ws.sent, [_session_update({"turn_detection": expected_turn_detection})]
+ )
+
+ async def test_rejects_empty_update(self) -> None:
+ ws = FakeWebSocket()
+ with self.assertRaisesRegex(ValueError, "At least one session field"):
+ await _connection(ws).update_session()
+ self.assertEqual(ws.sent, [])
+
+
+class TestConnect(unittest.IsolatedAsyncioTestCase):
+ def _client(self) -> RealtimeTranscription:
+ sdk_config = mock.Mock(timeout_ms=None, security=None, user_agent="test-agent")
+ sdk_config.get_server_details.return_value = ("https://example.com", {})
+ return RealtimeTranscription(sdk_config)
+
+ def _patch(self, ws: FakeWebSocket) -> Any:
+ return mock.patch.multiple(
+ "mistralai.extra.realtime.transcription",
+ connect=mock.AsyncMock(return_value=ws),
+ read_service_account_token=mock.Mock(return_value=None),
+ )
+
+ async def test_sends_session_after_handshake(self) -> None:
+ ws = FakeWebSocket([SESSION_CREATED])
+ with self._patch(ws):
+ await self._client().connect(
+ MODEL,
+ audio_format=AudioFormat(encoding="pcm_s16le", sample_rate=16000),
+ language="fr",
+ turn_detection=TurnDetectionConfig(),
+ )
+ self.assertEqual(
+ ws.sent,
+ [
+ _session_update(
+ {
+ "audio_format": PCM_16K,
+ "language": "fr",
+ "turn_detection": DEFAULT_TURN_DETECTION,
+ }
+ )
+ ],
+ )
+
+ async def test_sends_nothing_without_session_fields(self) -> None:
+ ws = FakeWebSocket([SESSION_CREATED])
+ with self._patch(ws):
+ await self._client().connect(MODEL)
+ self.assertEqual(ws.sent, [])
+
+ async def test_transcribe_stream_forwards_session(self) -> None:
+ ws = FakeWebSocket([SESSION_CREATED])
+
+ async def audio() -> AsyncIterator[bytes]:
+ yield b"\x00\x00"
+
+ with self._patch(ws):
+ async for _ in self._client().transcribe_stream(
+ audio(), MODEL, diarize=True
+ ):
+ pass
+ self.assertEqual(ws.sent[0], _session_update({"diarize": True}))
+
+
+class TestSessionKwargsMatchGeneratedPayload(unittest.TestCase):
+ """Fails when a regen adds or removes a session field the helper doesn't expose."""
+
+ def test_helpers_expose_every_session_field(self) -> None:
+ fields = set(RealtimeTranscriptionSessionUpdatePayload.model_fields)
+ update_params = set(
+ inspect.signature(RealtimeConnection.update_session).parameters
+ )
+ self.assertEqual(update_params - {"self"}, fields)
+
+ for method in (
+ RealtimeTranscription.connect,
+ RealtimeTranscription.transcribe_stream,
+ ):
+ with self.subTest(method=method.__qualname__):
+ params = set(inspect.signature(method).parameters)
+ self.assertEqual(fields - params, set())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/mistralai/extra/tests/test_realtime_speech.py b/src/mistralai/extra/tests/test_realtime_speech.py
new file mode 100644
index 000000000..9a8b2b23e
--- /dev/null
+++ b/src/mistralai/extra/tests/test_realtime_speech.py
@@ -0,0 +1,350 @@
+import asyncio
+import inspect
+import json
+import unittest
+from typing import Any, AsyncIterator, cast
+from unittest import mock
+
+from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
+from websockets.frames import Close
+
+from mistralai.client.models import (
+ RealtimeSpeechError,
+ RealtimeSpeechSession,
+ RealtimeSpeechSessionCreated,
+ RealtimeSpeechSessionUpdated,
+ RealtimeSpeechSessionUpdatePayload,
+ SpeechStreamAudioDelta,
+ SpeechStreamDone,
+)
+from mistralai.extra.exceptions import RealtimeSpeechException, RealtimeSpeechWSError
+from mistralai.extra.realtime.connection import UnknownRealtimeEvent
+from mistralai.extra.realtime.speech import (
+ RealtimeSpeech,
+ RealtimeSpeechConnection,
+ audio_bytes,
+ parse_realtime_speech_event,
+)
+
+MODEL = "voxtral-mini-tts-3"
+SESSION = {
+ "request_id": "ws-123",
+ "model": MODEL,
+ "voice": "voice_447",
+ "is_ref_audio": False,
+ "is_voice_profile": False,
+ "streaming_mode": "responsive",
+ "response_format": "wav",
+ "expressiveness": 2.0,
+}
+SESSION_CREATED = {"type": "session.created", "session": SESSION}
+AUDIO_DELTA = {"type": "speech.audio.delta", "audio_data": "AAAAAA=="}
+AUDIO_DONE = {
+ "type": "speech.audio.done",
+ "usage": {"prompt_tokens": 3, "completion_tokens": 10, "total_tokens": 13},
+}
+
+
+class FakeWebSocket:
+ def __init__(
+ self,
+ incoming: list[dict[str, Any]] | None = None,
+ close_frame: Close | None = None,
+ text_send_error: Exception | None = None,
+ wait_for_close: bool = False,
+ ) -> None:
+ self.sent: list[dict[str, Any]] = []
+ self.closed = False
+ self._incoming = [json.dumps(message) for message in incoming or []]
+ self._close_frame = close_frame
+ self._text_send_error = text_send_error
+ self._wait_for_close = wait_for_close
+ self._closed_event = asyncio.Event()
+
+ async def send(self, message: str) -> None:
+ payload = json.loads(message)
+ if self._text_send_error and payload["type"] == "input_text.append":
+ self._closed_event.set()
+ raise self._text_send_error
+ self.sent.append(payload)
+
+ async def recv(self) -> str:
+ return self._incoming.pop(0)
+
+ async def close(self, code: int = 1000, reason: str = "") -> None:
+ self.closed = True
+ self._closed_event.set()
+
+ def __aiter__(self) -> AsyncIterator[str]:
+ return self._iter()
+
+ async def _iter(self) -> AsyncIterator[str]:
+ while self._incoming:
+ yield self._incoming.pop(0)
+ if self._wait_for_close:
+ await self._closed_event.wait()
+ if self._close_frame is not None:
+ await asyncio.sleep(0)
+ raise ConnectionClosedError(self._close_frame, None)
+
+
+def _connection(ws: FakeWebSocket) -> RealtimeSpeechConnection:
+ session = RealtimeSpeechSession.model_validate(SESSION)
+ return RealtimeSpeechConnection(websocket=cast(Any, ws), session=session)
+
+
+class TestParseEvents(unittest.TestCase):
+ def test_events_are_typed(self) -> None:
+ cases = [
+ (SESSION_CREATED, RealtimeSpeechSessionCreated),
+ (
+ {**SESSION_CREATED, "type": "session.updated"},
+ RealtimeSpeechSessionUpdated,
+ ),
+ (AUDIO_DELTA, SpeechStreamAudioDelta),
+ (AUDIO_DONE, SpeechStreamDone),
+ (
+ {"type": "error", "error": {"message": "bad voice", "code": 3001}},
+ RealtimeSpeechError,
+ ),
+ ]
+ for payload, expected_cls in cases:
+ with self.subTest(event=expected_cls.__name__):
+ self.assertIsInstance(
+ parse_realtime_speech_event(payload), expected_cls
+ )
+
+ def test_unknown_and_invalid_events_fall_back(self) -> None:
+ for payload in (
+ {"type": "speech.surprise"},
+ {"type": "speech.audio.delta"},
+ ["not", "an", "object"],
+ ):
+ with self.subTest(payload=payload):
+ self.assertIsInstance(
+ parse_realtime_speech_event(payload), UnknownRealtimeEvent
+ )
+
+ def test_audio_bytes_decodes_delta(self) -> None:
+ event = parse_realtime_speech_event(AUDIO_DELTA)
+ assert isinstance(event, SpeechStreamAudioDelta)
+ self.assertEqual(audio_bytes(event), b"\x00\x00\x00\x00")
+
+
+class TestConnection(unittest.IsolatedAsyncioTestCase):
+ async def test_sends_text_messages(self) -> None:
+ ws = FakeWebSocket()
+ connection = _connection(ws)
+ await connection.send_text("Hello, ")
+ await connection.flush("world.")
+ await connection.flush()
+ await connection.end()
+ self.assertEqual(
+ ws.sent,
+ [
+ {"type": "input_text.append", "text": "Hello, "},
+ {"type": "input_text.flush", "text": "world."},
+ {"type": "input_text.flush"},
+ {"type": "input_text.end"},
+ ],
+ )
+
+ async def test_update_session_sends_set_fields(self) -> None:
+ ws = FakeWebSocket()
+ await _connection(ws).update_session(
+ voice_id="my-voice", response_format="pcm", expressiveness=1.5
+ )
+ self.assertEqual(
+ ws.sent,
+ [
+ {
+ "type": "session.update",
+ "session": {
+ "voice": "my-voice",
+ "response_format": "pcm",
+ "expressiveness": 1.5,
+ },
+ }
+ ],
+ )
+
+ async def test_rejects_empty_update(self) -> None:
+ ws = FakeWebSocket()
+ with self.assertRaisesRegex(ValueError, "At least one session field"):
+ await _connection(ws).update_session()
+ self.assertEqual(ws.sent, [])
+
+ async def test_events_track_session_updates(self) -> None:
+ updated = {**SESSION, "response_format": "pcm"}
+ ws = FakeWebSocket([{"type": "session.updated", "session": updated}])
+ connection = _connection(ws)
+ events = [event async for event in connection.events()]
+ self.assertIsInstance(events[0], RealtimeSpeechSessionUpdated)
+ self.assertEqual(connection.session.response_format, "pcm")
+ self.assertTrue(ws.closed)
+
+
+class TestRealtimeSpeech(unittest.IsolatedAsyncioTestCase):
+ def _client(self) -> RealtimeSpeech:
+ sdk_config = mock.Mock(timeout_ms=None, security=None, user_agent="test-agent")
+ sdk_config.get_server_details.return_value = ("https://example.com", {})
+ return RealtimeSpeech(sdk_config)
+
+ def _patch(
+ self, ws: FakeWebSocket, connect_mock: mock.AsyncMock | None = None
+ ) -> Any:
+ return mock.patch.multiple(
+ "mistralai.extra.realtime.speech",
+ connect=connect_mock or mock.AsyncMock(return_value=ws),
+ read_service_account_token=mock.Mock(return_value=None),
+ )
+
+ async def test_connect_url_and_session_update(self) -> None:
+ ws = FakeWebSocket([SESSION_CREATED])
+ connect_mock = mock.AsyncMock(return_value=ws)
+ with self._patch(ws, connect_mock):
+ connection = await self._client().connect(
+ MODEL, voice_id="my-voice", response_format="pcm"
+ )
+ self.assertEqual(
+ connect_mock.call_args.args[0],
+ "wss://example.com/v1/audio/speech/realtime?model=voxtral-mini-tts-3",
+ )
+ self.assertEqual(
+ ws.sent,
+ [
+ {
+ "type": "session.update",
+ "session": {"voice": "my-voice", "response_format": "pcm"},
+ }
+ ],
+ )
+ self.assertEqual(connection.request_id, "ws-123")
+
+ async def test_connect_sends_nothing_without_session_fields(self) -> None:
+ ws = FakeWebSocket([SESSION_CREATED])
+ with self._patch(ws):
+ await self._client().connect(MODEL)
+ self.assertEqual(ws.sent, [])
+
+ async def test_connect_raises_on_handshake_error(self) -> None:
+ error = {"type": "error", "error": {"message": "Invalid model", "code": 1500}}
+ ws = FakeWebSocket([error])
+ with self._patch(ws):
+ with self.assertRaisesRegex(RealtimeSpeechWSError, "Invalid model") as ctx:
+ await self._client().connect(MODEL)
+ self.assertEqual(ctx.exception.code, 1500)
+ self.assertTrue(ws.closed)
+
+ async def test_speak_stream_sends_text_and_stops_on_done(self) -> None:
+ ws = FakeWebSocket([SESSION_CREATED, AUDIO_DELTA, AUDIO_DONE, AUDIO_DELTA])
+
+ async def text() -> AsyncIterator[str]:
+ yield "Hello, "
+ yield "world."
+
+ with self._patch(ws):
+ events = [
+ event
+ async for event in self._client().speak_stream(
+ text(), MODEL, voice_id="my-voice"
+ )
+ ]
+
+ self.assertEqual(
+ [type(event) for event in events],
+ [RealtimeSpeechSessionCreated, SpeechStreamAudioDelta, SpeechStreamDone],
+ )
+ self.assertEqual(
+ ws.sent[0],
+ {"type": "session.update", "session": {"voice": "my-voice"}},
+ )
+ self.assertTrue(ws.closed)
+
+ async def test_server_close_is_raised(self) -> None:
+ async def text() -> AsyncIterator[str]:
+ yield "Hi."
+
+ close_frame = Close(1008, "Voice 'nope' not found.")
+ for fail_text_send in (False, True):
+ with self.subTest(fail_text_send=fail_text_send):
+ ws = FakeWebSocket(
+ [SESSION_CREATED],
+ close_frame=close_frame,
+ text_send_error=(
+ ConnectionClosedError(close_frame, None)
+ if fail_text_send
+ else None
+ ),
+ wait_for_close=fail_text_send,
+ )
+ with self._patch(ws):
+ with self.assertRaisesRegex(
+ RealtimeSpeechException, "Voice 'nope' not found."
+ ) as ctx:
+ async for _ in self._client().speak_stream(
+ text(), MODEL, voice_id="nope"
+ ):
+ pass
+ self.assertEqual(ctx.exception.code, 1008)
+
+ async def test_clean_server_close_while_sending_ends_quietly(self) -> None:
+ ws = FakeWebSocket(
+ [SESSION_CREATED],
+ text_send_error=ConnectionClosedOK(Close(1000, ""), None),
+ wait_for_close=True,
+ )
+
+ async def text() -> AsyncIterator[str]:
+ yield "Hi."
+
+ with self._patch(ws):
+ events = [
+ event async for event in self._client().speak_stream(text(), MODEL)
+ ]
+ self.assertEqual(
+ [type(event) for event in events], [RealtimeSpeechSessionCreated]
+ )
+
+ async def test_text_stream_error_is_raised(self) -> None:
+ ws = FakeWebSocket([SESSION_CREATED], wait_for_close=True)
+
+ async def text() -> AsyncIterator[str]:
+ yield "Hi."
+ raise ValueError("LLM stream failed")
+
+ async def consume() -> None:
+ async for _ in self._client().speak_stream(text(), MODEL):
+ pass
+
+ with self._patch(ws):
+ task = asyncio.create_task(consume())
+ await asyncio.wait({task}, timeout=1)
+ self.assertTrue(task.done())
+ with self.assertRaisesRegex(ValueError, "LLM stream failed"):
+ await task
+ self.assertTrue(ws.closed)
+
+
+class TestSessionKwargsMatchGeneratedPayload(unittest.TestCase):
+ """Fails when a regen adds or removes a session field the helper doesn't expose."""
+
+ def test_helpers_expose_every_session_field(self) -> None:
+ kwarg_names = {"voice": "voice_id"}
+ fields = {
+ kwarg_names.get(name, name)
+ for name in RealtimeSpeechSessionUpdatePayload.model_fields
+ }
+ update_params = set(
+ inspect.signature(RealtimeSpeechConnection.update_session).parameters
+ )
+ self.assertEqual(update_params - {"self"}, fields)
+
+ for method in (RealtimeSpeech.connect, RealtimeSpeech.speak_stream):
+ with self.subTest(method=method.__qualname__):
+ params = set(inspect.signature(method).parameters)
+ self.assertEqual(fields - params, set())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/mistralai/extra/tests/test_workflow_encoding.py b/src/mistralai/extra/tests/test_workflow_encoding.py
index 5210324b5..324c1be54 100644
--- a/src/mistralai/extra/tests/test_workflow_encoding.py
+++ b/src/mistralai/extra/tests/test_workflow_encoding.py
@@ -1,20 +1,29 @@
"""Tests for workflow encoding configuration lifecycle."""
import gc
+import hashlib
+import hmac
+import inspect
import json
from typing import cast
+import httpx2 as httpx
import msgpack
import pytest
import zstandard
from pydantic import SecretStr
from mistralai.client import Mistral
+from mistralai.client._hooks.types import BeforeRequestContext, HookContext
from mistralai.client._hooks.workflow_encoding_hook import (
+ _run_async,
_workflow_configs,
_ENCODING_CONFIG_ID_ATTR,
+ SCHEDULE_WORKFLOW_OPERATION_ID,
+ WorkflowEncodingHook,
configure_workflow_encoding,
)
+from mistralai.client.utils import BackoffStrategy, RetryConfig
from mistralai.extra.workflows import (
BlobStorageConfig,
EncryptedStrField,
@@ -676,7 +685,7 @@ async def test_payload_encoder_encodes_event_content_without_offloading():
decoder = PayloadEncoder(
encoding_config=WorkflowEncodingConfig(payload_encryption=encryption_config)
)
- payload = json.dumps({"data": "x" * 20_000}).encode()
+ payload = json.dumps({"data": "x" * 20_000}, separators=(",", ":")).encode()
encoded, encoding_options = await encoder.encode_event_payload_content(payload)
decoded = await decoder.decode_payload_content(encoded, encoding_options)
@@ -843,3 +852,428 @@ async def test_encode_payload_content_force_offload_is_idempotent(monkeypatch):
await encoder.encode_payload_content(b"same", context, force_offload=True)
assert len(storage.blobs) == 1
+
+
+@pytest.fixture
+def make_schedule_client():
+ http_clients: list[httpx.Client] = []
+
+ def make(
+ config: WorkflowEncodingConfig, response_body: dict | None = None
+ ) -> tuple[Mistral, list[httpx.Request]]:
+ requests: list[httpx.Request] = []
+
+ def handle(request: httpx.Request) -> httpx.Response:
+ requests.append(request)
+ return httpx.Response(
+ 201 if request.method == "POST" else 200,
+ json=response_body or {"schedule_id": "sched-1"},
+ )
+
+ http_client = httpx.Client(transport=httpx.MockTransport(handle))
+ http_clients.append(http_client)
+ client = Mistral(
+ api_key="test-key",
+ server_url="https://api.mistral.ai",
+ client=http_client,
+ )
+ configure_workflow_encoding(
+ config,
+ namespace="test-namespace",
+ sdk_config=client.sdk_configuration,
+ )
+ return client, requests
+
+ yield make
+ for http_client in http_clients:
+ http_client.close()
+ gc.collect()
+
+
+def _before_request_context(client: Mistral, operation_id: str) -> BeforeRequestContext:
+ return BeforeRequestContext(
+ HookContext(
+ config=client.sdk_configuration,
+ base_url="https://api.mistral.ai",
+ operation_id=operation_id,
+ oauth2_scopes=[],
+ security_source=None,
+ tags=None,
+ extensions=None,
+ )
+ )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("use_async", [False, True])
+@pytest.mark.parametrize("encryption_mode", [None, *PayloadEncryptionMode])
+async def test_encoding_hook_preserves_schedule_input_identity_on_retry(
+ use_async, encryption_mode
+):
+ requests = []
+
+ def handle(request):
+ requests.append(request)
+ if len(requests) == 1:
+ raise httpx.ReadError("Response lost after schedule creation")
+ return httpx.Response(201, json={"schedule_id": "sched-1"})
+
+ config = WorkflowEncodingConfig(
+ payload_encryption=PayloadEncryptionConfig(
+ mode=encryption_mode, main_key=SecretStr("0" * 64)
+ )
+ if encryption_mode is not None
+ else None,
+ )
+ async with httpx.AsyncClient(transport=httpx.MockTransport(handle)) as async_client:
+ with httpx.Client(transport=httpx.MockTransport(handle)) as http_client:
+ client = Mistral(
+ api_key="test-key", client=http_client, async_client=async_client
+ )
+ configure_workflow_encoding(
+ config, namespace="test-namespace", sdk_config=client.sdk_configuration
+ )
+ create = (
+ client.workflows.schedules.schedule_workflow_async
+ if use_async
+ else client.workflows.schedules.schedule_workflow
+ )
+ retries = RetryConfig(
+ "backoff", BackoffStrategy(0, 0, 1, 1000, jitter_ms=0), True
+ )
+ for secret in ["s3cr3t", "s3cr3t", "changed"]:
+ input_data = {
+ "secret": EncryptedStrField(data=secret).model_dump(),
+ "other": "value",
+ }
+ if len(requests) == 2:
+ input_data = dict(reversed(list(input_data.items())))
+ result = create(
+ schedule={
+ "input": input_data,
+ "cron_expressions": ["0 * * * *"],
+ },
+ workflow_identifier="my-workflow",
+ retries=retries,
+ )
+ if inspect.isawaitable(result):
+ await result
+
+ bodies = [json.loads(request.content) for request in requests]
+ inputs = [body["schedule"]["input"] for body in bodies]
+ assert len(inputs) == 4
+ assert inputs[0]["input_hash"] == inputs[1]["input_hash"] == inputs[2]["input_hash"]
+ assert inputs[3]["input_hash"] != inputs[0]["input_hash"]
+ if encryption_mode is not None:
+ assert len({value["b64payload"] for value in inputs}) == 4
+ assert all(
+ "schedule_id" not in body and "schedule_id" not in body["schedule"]
+ for body in bodies
+ )
+ encoder = PayloadEncoder(config)
+ assert await encoder.decode_network_result(inputs[0]) == {
+ "secret": EncryptedStrField(data="s3cr3t").model_dump(),
+ "other": "value",
+ }
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "operation_id",
+ [
+ "execute_workflow_v1_workflows__workflow_identifier__execute_post",
+ "execute_workflow_registration_v1_workflows_registrations__workflow_registration_id__execute_post",
+ "signal_workflow_execution_v1_workflows_executions__execution_id__signals_post",
+ "query_workflow_execution_v1_workflows_executions__execution_id__queries_post",
+ "update_workflow_execution_v1_workflows_executions__execution_id__updates_post",
+ "schedule_workflow_v1_workflows_schedules_post",
+ "update_schedule_v1_workflows_schedules__schedule_id__patch",
+ ],
+)
+@pytest.mark.parametrize("encryption_mode", list(PayloadEncryptionMode))
+async def test_encoding_hook_keeps_large_unicode_inputs_under_payload_limit(
+ make_schedule_client, operation_id, encryption_mode
+):
+ config = WorkflowEncodingConfig(
+ payload_encryption=PayloadEncryptionConfig(
+ mode=encryption_mode, main_key=SecretStr("0" * 64)
+ )
+ )
+ client, _ = make_schedule_client(config)
+ payload = {"text": "é" * 350_000}
+ is_schedule = "schedule" in operation_id
+ body = (
+ {"schedule": {"input": payload}}
+ if is_schedule
+ else {"input": payload, "execution_id": "exec-1"}
+ )
+ request = httpx.Request(
+ "POST", "https://api.mistral.ai/v1/workflows/my-workflow/execute", json=body
+ )
+ result = WorkflowEncodingHook().before_request(
+ _before_request_context(client, operation_id), request
+ )
+ assert isinstance(result, httpx.Request)
+ sent = json.loads(result.content)
+ input_holder = sent["schedule"] if is_schedule else sent
+ input_field = "encoded_input" if operation_id.startswith("execute_") else "input"
+ encoded = NetworkEncodedInput.model_validate(
+ input_holder[input_field],
+ )
+
+ assert len(encoded.get_payload()) < 1_992_294
+ raw = await PayloadEncoder(config).decode_payload_content(
+ encoded.get_payload(), encoded.encoding_options
+ )
+ assert raw == b'{"text":"' + "é".encode("utf-8") * 350_000 + b'"}'
+
+
+@pytest.mark.asyncio
+async def test_encoding_hook_encodes_schedule_input_on_create(
+ make_schedule_client, encryption_config
+):
+ client, requests = make_schedule_client(encryption_config)
+
+ client.workflows.schedules.schedule_workflow(
+ schedule={"input": {"secret": "s3cr3t"}, "cron_expressions": ["0 * * * *"]},
+ workflow_identifier="my-workflow",
+ )
+
+ sent = json.loads(requests[0].content)
+ assert "input" not in sent
+ assert sent["schedule"]["cron_expressions"] == ["0 * * * *"]
+ assert sent["schedule"]["input"]["encoding_options"] == ["encrypted"]
+ assert "s3cr3t" not in json.dumps(sent)
+ encoder = PayloadEncoder(encoding_config=encryption_config)
+ decoded = await encoder.decode_network_result(sent["schedule"]["input"])
+ assert decoded == {"secret": "s3cr3t"}
+
+
+def test_encoding_hook_does_not_hash_schedule_input_with_encryption_key(
+ make_schedule_client, encryption_config
+):
+ client, requests = make_schedule_client(encryption_config)
+
+ client.workflows.schedules.schedule_workflow(
+ schedule={"input": {"secret": "s3cr3t"}},
+ workflow_identifier="my-workflow",
+ )
+
+ input_hash = json.loads(requests[0].content)["schedule"]["input"]["input_hash"]
+ encryption_key = bytes.fromhex(
+ encryption_config.payload_encryption.main_key.get_secret_value()
+ )
+ assert (
+ input_hash
+ != hmac.new(encryption_key, b'{"secret":"s3cr3t"}', hashlib.sha256).hexdigest()
+ )
+
+
+@pytest.mark.asyncio
+async def test_encoding_hook_encodes_schedule_input_on_update(
+ make_schedule_client, encryption_config
+):
+ client, requests = make_schedule_client(encryption_config)
+
+ client.workflows.schedules.update_schedule(
+ schedule_id="sched-1",
+ schedule={"input": {"secret": "s3cr3t"}},
+ )
+
+ sent = json.loads(requests[0].content)
+ assert sent["schedule"]["input"]["encoding_options"] == ["encrypted"]
+ assert "s3cr3t" not in json.dumps(sent)
+ encoder = PayloadEncoder(encoding_config=encryption_config)
+ decoded = await encoder.decode_network_result(sent["schedule"]["input"])
+ assert decoded == {"secret": "s3cr3t"}
+
+
+@pytest.mark.asyncio
+async def test_encoding_hook_leaves_schedule_update_without_input_unchanged(
+ make_schedule_client, encryption_config
+):
+ client, requests = make_schedule_client(encryption_config)
+
+ client.workflows.schedules.update_schedule(
+ schedule_id="sched-1",
+ schedule={"cron_expressions": ["0 * * * *"]},
+ )
+
+ assert json.loads(requests[0].content) == {
+ "schedule": {"cron_expressions": ["0 * * * *"]}
+ }
+
+
+@pytest.mark.asyncio
+async def test_encoding_hook_does_not_offload_schedule_input(
+ make_schedule_client, monkeypatch
+):
+ storage = InMemoryBlobStorage()
+ monkeypatch.setattr(
+ "mistralai.extra.workflows.encoding.payload_encoder.get_blob_storage",
+ lambda _: storage,
+ )
+ config = WorkflowEncodingConfig(
+ payload_encryption=PayloadEncryptionConfig(
+ mode=PayloadEncryptionMode.FULL,
+ main_key=SecretStr("0" * 64),
+ ),
+ payload_offloading=PayloadOffloadingConfig(
+ min_size_bytes=1,
+ storage_config=BlobStorageConfig(
+ storage_provider=StorageProvider.S3,
+ bucket_name="test-bucket",
+ ),
+ ),
+ )
+ client, requests = make_schedule_client(config)
+
+ client.workflows.schedules.schedule_workflow(
+ schedule={"input": {"secret": "s3cr3t"}},
+ workflow_identifier="my-workflow",
+ )
+
+ sent = json.loads(requests[0].content)
+ assert sent["schedule"]["input"]["encoding_options"] == ["encrypted"]
+ assert (
+ EncodedPayloadOptions.OFFLOADED
+ not in sent["schedule"]["input"]["encoding_options"]
+ )
+ assert storage.blobs == {}
+ encoder = PayloadEncoder(encoding_config=config)
+ decoded = await encoder.decode_network_result(sent["schedule"]["input"])
+ assert decoded == {"secret": "s3cr3t"}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("exclude", [{"empty"}, None])
+async def test_encoding_hook_decodes_schedule_input_on_get(
+ make_schedule_client, encryption_config, exclude
+):
+ encoder = PayloadEncoder(encoding_config=encryption_config)
+ encoded_input = await encoder.encode_network_input(
+ {"secret": "s3cr3t"},
+ WorkflowContext(namespace="test-namespace", execution_id="sched-1"),
+ )
+ client, _ = make_schedule_client(
+ encryption_config,
+ {
+ "schedule_id": "sched-1",
+ "workflow_name": "my-workflow",
+ "paused": False,
+ "input": encoded_input.model_dump(mode="json", exclude=exclude),
+ },
+ )
+
+ schedule = client.workflows.schedules.get_schedule(schedule_id="sched-1")
+
+ assert schedule.input == {"secret": "s3cr3t"}
+
+
+@pytest.mark.parametrize(
+ "plain_input",
+ [
+ {"b64payload": "Zmlyc3Q=", "foo": "bar"},
+ {"b64payload": "not base64!"},
+ {"b64payload": "//4=", "encoding_options": []},
+ {"b64payload": "Zmlyc3Q=", "encoding_options": ["encrypted"]},
+ ],
+)
+def test_encoding_hook_leaves_plain_schedule_input_unchanged_on_get(
+ make_schedule_client, encryption_config, plain_input
+):
+ client, _ = make_schedule_client(
+ encryption_config,
+ {
+ "schedule_id": "sched-1",
+ "workflow_name": "my-workflow",
+ "paused": False,
+ "input": plain_input,
+ },
+ )
+
+ schedule = client.workflows.schedules.get_schedule(schedule_id="sched-1")
+
+ assert schedule.input == plain_input
+
+
+@pytest.mark.asyncio
+async def test_encoding_hook_decodes_schedule_inputs_on_list(
+ make_schedule_client, encryption_config, monkeypatch
+):
+ encoder = PayloadEncoder(encoding_config=encryption_config)
+ foreign_encoder = PayloadEncoder(
+ encoding_config=WorkflowEncodingConfig(
+ payload_encryption=PayloadEncryptionConfig(
+ mode=PayloadEncryptionMode.FULL, main_key=SecretStr("1" * 64)
+ )
+ )
+ )
+ context = WorkflowContext(namespace="test-namespace", execution_id="sched-1")
+ plain_input = {"b64payload": "Zmlyc3Q=", "foo": "bar"}
+ foreign_input = (
+ await foreign_encoder.encode_network_input({"n": 3}, context)
+ ).model_dump(mode="json", exclude={"empty"})
+ non_json_input = NetworkEncodedInput.from_data(
+ *await encoder.encode_payload_content(b"not json", context)
+ ).model_dump(mode="json", exclude={"empty"})
+ schedule_inputs = [
+ (await encoder.encode_network_input({"n": n}, context)).model_dump(
+ mode="json", exclude={"empty"}
+ )
+ for n in range(3)
+ ]
+ client, _ = make_schedule_client(
+ encryption_config,
+ {
+ "schedules": [
+ {
+ "schedule_id": f"sched-{index}",
+ "workflow_name": "my-workflow",
+ "paused": False,
+ "input": schedule_input,
+ }
+ for index, schedule_input in enumerate(
+ [*schedule_inputs, foreign_input, non_json_input, plain_input]
+ )
+ ]
+ },
+ )
+ run_async_calls = []
+
+ def counting_run_async(coro):
+ run_async_calls.append(coro)
+ return _run_async(coro)
+
+ monkeypatch.setattr(
+ "mistralai.client._hooks.workflow_encoding_hook._run_async",
+ counting_run_async,
+ )
+
+ schedules = client.workflows.schedules.get_schedules()
+
+ assert [schedule.input for schedule in schedules.result.schedules] == [
+ {"n": 0},
+ {"n": 1},
+ {"n": 2},
+ foreign_input,
+ non_json_input,
+ plain_input,
+ ]
+ assert len(run_async_calls) == 1
+
+
+def test_encoding_hook_refuses_schedule_operation_without_schedule_object(
+ make_schedule_client, encryption_config
+):
+ client, _ = make_schedule_client(encryption_config)
+ request = httpx.Request(
+ "POST",
+ "https://api.mistral.ai/v1/workflows/schedules",
+ json={"input": {"secret": "s3cr3t"}},
+ )
+
+ with pytest.raises(ValueError, match="no schedule object"):
+ WorkflowEncodingHook().before_request(
+ _before_request_context(client, SCHEDULE_WORKFLOW_OPERATION_ID), request
+ )
diff --git a/src/mistralai/extra/workflows/encoding/models.py b/src/mistralai/extra/workflows/encoding/models.py
index 1f1c85462..68b58571d 100644
--- a/src/mistralai/extra/workflows/encoding/models.py
+++ b/src/mistralai/extra/workflows/encoding/models.py
@@ -83,6 +83,13 @@ def from_data(
)
+class NetworkEncodedScheduleInput(NetworkEncodedInput):
+ input_hash: str = Field(
+ description="Stable hash of the input for schedule identity, keyed when encryption is enabled",
+ pattern=r"^[0-9a-f]{64}$",
+ )
+
+
class NetworkEncodedResult(NetworkEncodedBase):
@staticmethod
def from_encoded_payload(encoded_payload: EncodedPayload) -> "NetworkEncodedResult":
diff --git a/src/mistralai/extra/workflows/encoding/payload_encoder.py b/src/mistralai/extra/workflows/encoding/payload_encoder.py
index 6699a7e4c..a22e53bf5 100644
--- a/src/mistralai/extra/workflows/encoding/payload_encoder.py
+++ b/src/mistralai/extra/workflows/encoding/payload_encoder.py
@@ -283,7 +283,8 @@ async def _partially_encrypt_fields(self, data: bytes) -> tuple[bytes, bool]:
encrypted_data = self._encrypt(encrypted_field["data"].encode())
encrypted_field["data"] = base64.b64encode(encrypted_data).decode()
- return json.dumps(obj).encode(), len(encrypted_fields) > 0
+ serialized = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
+ return serialized.encode("utf-8", "backslashreplace"), len(encrypted_fields) > 0
async def _partially_decrypt_fields(self, data: bytes) -> tuple[bytes, bool]:
try:
diff --git a/uv.lock b/uv.lock
index 69b1c5aed..49ed4b80f 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1275,7 +1275,7 @@ wheels = [
[[package]]
name = "mistralai"
-version = "3.5.0"
+version = "3.6.0"
source = { editable = "." }
dependencies = [
{ name = "eval-type-backport" },