@openclaw/mistral-provider 2026.10.2 → 2026.10.5-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -49,13 +49,6 @@ function normalizeMistralRealtimeBaseUrl(value) {
|
|
|
49
49
|
url.pathname = url.pathname.replace(/\/v1\/?$/, "").replace(/\/+$/, "");
|
|
50
50
|
return url.toString().replace(/\/+$/, "");
|
|
51
51
|
}
|
|
52
|
-
function toMistralRealtimeWsUrl(config) {
|
|
53
|
-
const base = new URL(`${normalizeMistralRealtimeBaseUrl(config.baseUrl)}/`);
|
|
54
|
-
const url = new URL("v1/audio/transcriptions/realtime", base);
|
|
55
|
-
url.searchParams.set("model", config.model);
|
|
56
|
-
if (config.targetStreamingDelayMs != null) url.searchParams.set("target_streaming_delay_ms", String(config.targetStreamingDelayMs));
|
|
57
|
-
return url.toString();
|
|
58
|
-
}
|
|
59
52
|
function normalizeProviderConfig(config) {
|
|
60
53
|
const raw = readNestedMistralConfig(config);
|
|
61
54
|
return {
|
|
@@ -87,107 +80,6 @@ function measureTranscriptDeltaBytes(partialText, delta) {
|
|
|
87
80
|
const completesSplitSurrogatePair = previousCodeUnit >= 55296 && previousCodeUnit <= 56319 && nextCodeUnit >= 56320 && nextCodeUnit <= 57343;
|
|
88
81
|
return Buffer.byteLength(delta, "utf8") - (completesSplitSurrogatePair ? 2 : 0);
|
|
89
82
|
}
|
|
90
|
-
function createMistralRealtimeTranscriptionSession(config, createRealtimeTranscriptionWebSocketSession) {
|
|
91
|
-
let partialText = "";
|
|
92
|
-
let partialBytes = 0;
|
|
93
|
-
let hasFinalSegment = false;
|
|
94
|
-
let terminal = false;
|
|
95
|
-
const clearPartial = () => {
|
|
96
|
-
partialText = "";
|
|
97
|
-
partialBytes = 0;
|
|
98
|
-
};
|
|
99
|
-
const emitFinalTranscript = (text, source) => {
|
|
100
|
-
if (!text.trim() || source === "pending" && !MISTRAL_REALTIME_SPEECH_CONTENT.test(text)) return;
|
|
101
|
-
hasFinalSegment ||= source === "segment";
|
|
102
|
-
config.onTranscript?.(text);
|
|
103
|
-
};
|
|
104
|
-
const failTerminal = (error, transport) => {
|
|
105
|
-
if (terminal) return;
|
|
106
|
-
terminal = true;
|
|
107
|
-
clearPartial();
|
|
108
|
-
transport.closeNow();
|
|
109
|
-
try {
|
|
110
|
-
config.onError?.(error);
|
|
111
|
-
} catch {}
|
|
112
|
-
};
|
|
113
|
-
const handleEvent = (event, transport) => {
|
|
114
|
-
if (terminal) return;
|
|
115
|
-
if (event.type === "session.created") {
|
|
116
|
-
clearPartial();
|
|
117
|
-
hasFinalSegment = false;
|
|
118
|
-
transport.sendJson({
|
|
119
|
-
type: "session.update",
|
|
120
|
-
session: { audio_format: {
|
|
121
|
-
encoding: config.encoding,
|
|
122
|
-
sample_rate: config.sampleRate
|
|
123
|
-
} }
|
|
124
|
-
});
|
|
125
|
-
transport.markReady();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (!transport.isReady() && event.type === "error") {
|
|
129
|
-
transport.failConnect(new Error(readErrorDetail(event)));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
switch (event.type) {
|
|
133
|
-
case "transcription.text.delta":
|
|
134
|
-
if (event.text) {
|
|
135
|
-
const deltaBytes = measureTranscriptDeltaBytes(partialText, event.text);
|
|
136
|
-
if (deltaBytes > MISTRAL_REALTIME_MAX_PARTIAL_TRANSCRIPT_BYTES - partialBytes) {
|
|
137
|
-
failTerminal(/* @__PURE__ */ new Error(MISTRAL_REALTIME_PARTIAL_TRANSCRIPT_OVERFLOW_MESSAGE), transport);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
partialText += event.text;
|
|
141
|
-
partialBytes += deltaBytes;
|
|
142
|
-
config.onPartial?.(partialText);
|
|
143
|
-
}
|
|
144
|
-
return;
|
|
145
|
-
case "transcription.segment":
|
|
146
|
-
if (event.text?.trim()) {
|
|
147
|
-
emitFinalTranscript(event.text, "segment");
|
|
148
|
-
clearPartial();
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
case "transcription.done": {
|
|
152
|
-
terminal = true;
|
|
153
|
-
const source = hasFinalSegment ? "pending" : "terminal";
|
|
154
|
-
const terminalText = source === "pending" ? partialText : event.text?.trim() ? event.text : partialText;
|
|
155
|
-
clearPartial();
|
|
156
|
-
try {
|
|
157
|
-
emitFinalTranscript(terminalText, source);
|
|
158
|
-
} finally {
|
|
159
|
-
transport.closeNow();
|
|
160
|
-
}
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
case "error": failTerminal(new Error(readErrorDetail(event)), transport);
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
return createRealtimeTranscriptionWebSocketSession({
|
|
167
|
-
providerId: "mistral",
|
|
168
|
-
callbacks: config,
|
|
169
|
-
url: () => toMistralRealtimeWsUrl(config),
|
|
170
|
-
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
171
|
-
connectTimeoutMs: MISTRAL_REALTIME_CONNECT_TIMEOUT_MS,
|
|
172
|
-
closeTimeoutMs: MISTRAL_REALTIME_CLOSE_TIMEOUT_MS,
|
|
173
|
-
maxReconnectAttempts: MISTRAL_REALTIME_MAX_RECONNECT_ATTEMPTS,
|
|
174
|
-
reconnectDelayMs: MISTRAL_REALTIME_RECONNECT_DELAY_MS,
|
|
175
|
-
maxQueuedBytes: MISTRAL_REALTIME_MAX_QUEUED_BYTES,
|
|
176
|
-
connectTimeoutMessage: "Mistral realtime transcription connection timeout",
|
|
177
|
-
reconnectLimitMessage: "Mistral realtime transcription reconnect limit reached",
|
|
178
|
-
sendAudio: (audio, transport) => {
|
|
179
|
-
transport.sendJson({
|
|
180
|
-
type: "input_audio.append",
|
|
181
|
-
audio: audio.toString("base64")
|
|
182
|
-
});
|
|
183
|
-
},
|
|
184
|
-
onClose: (transport) => {
|
|
185
|
-
transport.sendJson({ type: "input_audio.flush" });
|
|
186
|
-
transport.sendJson({ type: "input_audio.end" });
|
|
187
|
-
},
|
|
188
|
-
onMessage: handleEvent
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
83
|
function buildMistralRealtimeTranscriptionProvider({ createRealtimeTranscriptionWebSocketSession }) {
|
|
192
84
|
return {
|
|
193
85
|
id: "mistral",
|
|
@@ -201,15 +93,112 @@ function buildMistralRealtimeTranscriptionProvider({ createRealtimeTranscription
|
|
|
201
93
|
const config = normalizeProviderConfig(req.providerConfig);
|
|
202
94
|
const apiKey = config.apiKey || normalizeMistralApiKey(process.env.MISTRAL_API_KEY);
|
|
203
95
|
if (!apiKey) throw new Error("Mistral API key missing");
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
96
|
+
const baseUrl = normalizeMistralRealtimeBaseUrl(config.baseUrl);
|
|
97
|
+
const callbacks = { ...req };
|
|
98
|
+
let partialText = "";
|
|
99
|
+
let partialBytes = 0;
|
|
100
|
+
let hasFinalSegment = false;
|
|
101
|
+
let terminal = false;
|
|
102
|
+
const clearPartial = () => {
|
|
103
|
+
partialText = "";
|
|
104
|
+
partialBytes = 0;
|
|
105
|
+
};
|
|
106
|
+
const emitFinalTranscript = (text, source) => {
|
|
107
|
+
if (!text.trim() || source === "pending" && !MISTRAL_REALTIME_SPEECH_CONTENT.test(text)) return;
|
|
108
|
+
hasFinalSegment ||= source === "segment";
|
|
109
|
+
callbacks.onTranscript?.(text);
|
|
110
|
+
};
|
|
111
|
+
const failTerminal = (error, transport) => {
|
|
112
|
+
if (terminal) return;
|
|
113
|
+
terminal = true;
|
|
114
|
+
clearPartial();
|
|
115
|
+
transport.closeNow();
|
|
116
|
+
try {
|
|
117
|
+
callbacks.onError?.(error);
|
|
118
|
+
} catch {}
|
|
119
|
+
};
|
|
120
|
+
return createRealtimeTranscriptionWebSocketSession({
|
|
121
|
+
providerId: "mistral",
|
|
122
|
+
callbacks,
|
|
123
|
+
url: () => {
|
|
124
|
+
const base = new URL(`${normalizeMistralRealtimeBaseUrl(baseUrl)}/`);
|
|
125
|
+
const url = new URL("v1/audio/transcriptions/realtime", base);
|
|
126
|
+
url.searchParams.set("model", config.model ?? MISTRAL_REALTIME_DEFAULT_MODEL);
|
|
127
|
+
url.searchParams.set("target_streaming_delay_ms", String(config.targetStreamingDelayMs ?? MISTRAL_REALTIME_DEFAULT_DELAY_MS));
|
|
128
|
+
return url.toString();
|
|
129
|
+
},
|
|
130
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
131
|
+
connectTimeoutMs: MISTRAL_REALTIME_CONNECT_TIMEOUT_MS,
|
|
132
|
+
closeTimeoutMs: MISTRAL_REALTIME_CLOSE_TIMEOUT_MS,
|
|
133
|
+
maxReconnectAttempts: MISTRAL_REALTIME_MAX_RECONNECT_ATTEMPTS,
|
|
134
|
+
reconnectDelayMs: MISTRAL_REALTIME_RECONNECT_DELAY_MS,
|
|
135
|
+
maxQueuedBytes: MISTRAL_REALTIME_MAX_QUEUED_BYTES,
|
|
136
|
+
connectTimeoutMessage: "Mistral realtime transcription connection timeout",
|
|
137
|
+
reconnectLimitMessage: "Mistral realtime transcription reconnect limit reached",
|
|
138
|
+
sendAudio: (audio, transport) => {
|
|
139
|
+
transport.sendJson({
|
|
140
|
+
type: "input_audio.append",
|
|
141
|
+
audio: audio.toString("base64")
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
onClose: (transport) => {
|
|
145
|
+
transport.sendJson({ type: "input_audio.flush" });
|
|
146
|
+
transport.sendJson({ type: "input_audio.end" });
|
|
147
|
+
},
|
|
148
|
+
onMessage: (event, transport) => {
|
|
149
|
+
if (terminal) return;
|
|
150
|
+
if (event.type === "session.created") {
|
|
151
|
+
clearPartial();
|
|
152
|
+
hasFinalSegment = false;
|
|
153
|
+
transport.sendJson({
|
|
154
|
+
type: "session.update",
|
|
155
|
+
session: { audio_format: {
|
|
156
|
+
encoding: config.encoding ?? MISTRAL_REALTIME_DEFAULT_ENCODING,
|
|
157
|
+
sample_rate: config.sampleRate ?? MISTRAL_REALTIME_DEFAULT_SAMPLE_RATE
|
|
158
|
+
} }
|
|
159
|
+
});
|
|
160
|
+
transport.markReady();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!transport.isReady() && event.type === "error") {
|
|
164
|
+
transport.failConnect(new Error(readErrorDetail(event)));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
switch (event.type) {
|
|
168
|
+
case "transcription.text.delta":
|
|
169
|
+
if (event.text) {
|
|
170
|
+
const deltaBytes = measureTranscriptDeltaBytes(partialText, event.text);
|
|
171
|
+
if (deltaBytes > MISTRAL_REALTIME_MAX_PARTIAL_TRANSCRIPT_BYTES - partialBytes) {
|
|
172
|
+
failTerminal(/* @__PURE__ */ new Error(MISTRAL_REALTIME_PARTIAL_TRANSCRIPT_OVERFLOW_MESSAGE), transport);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
partialText += event.text;
|
|
176
|
+
partialBytes += deltaBytes;
|
|
177
|
+
callbacks.onPartial?.(partialText);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
case "transcription.segment":
|
|
181
|
+
if (event.text?.trim()) {
|
|
182
|
+
emitFinalTranscript(event.text, "segment");
|
|
183
|
+
clearPartial();
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
case "transcription.done": {
|
|
187
|
+
terminal = true;
|
|
188
|
+
const source = hasFinalSegment ? "pending" : "terminal";
|
|
189
|
+
const terminalText = source === "pending" ? partialText : event.text?.trim() ? event.text : partialText;
|
|
190
|
+
clearPartial();
|
|
191
|
+
try {
|
|
192
|
+
emitFinalTranscript(terminalText, source);
|
|
193
|
+
} finally {
|
|
194
|
+
transport.closeNow();
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
case "error": failTerminal(new Error(readErrorDetail(event)), transport);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
213
202
|
}
|
|
214
203
|
};
|
|
215
204
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/mistral-provider",
|
|
3
|
-
"version": "2026.10.2",
|
|
3
|
+
"version": "2026.10.5-beta.2",
|
|
4
4
|
"description": "OpenClaw Mistral provider plugin",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"minHostVersion": ">=2026.7.2"
|
|
19
19
|
},
|
|
20
20
|
"compat": {
|
|
21
|
-
"pluginApi": ">=2026.10.2"
|
|
21
|
+
"pluginApi": ">=2026.10.5-beta.2"
|
|
22
22
|
},
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.10.2",
|
|
24
|
+
"openclawVersion": "2026.10.5-beta.2",
|
|
25
25
|
"bundledDist": false
|
|
26
26
|
},
|
|
27
27
|
"release": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"assets/activity.svg"
|
|
41
41
|
],
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"openclaw": ">=2026.10.2"
|
|
43
|
+
"openclaw": ">=2026.10.5-beta.2"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
46
46
|
"openclaw": {
|