@ynhcj/xiaoyi-channel 0.0.61-next → 0.0.62-next
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.
- package/dist/src/provider.js +105 -36
- package/package.json +1 -1
package/dist/src/provider.js
CHANGED
|
@@ -85,6 +85,108 @@ function buildReplayStream(result) {
|
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Wrap the underlying stream with retry logic while preserving real-time streaming.
|
|
90
|
+
*
|
|
91
|
+
* Strategy:
|
|
92
|
+
* 1. Buffer events until the first content-bearing event is seen.
|
|
93
|
+
* 2. If the stream errors before any content, the buffer is tiny (start + error)
|
|
94
|
+
* and we can safely retry with a fresh API call.
|
|
95
|
+
* 3. Once content events appear, flush the buffer and switch to pass-through mode
|
|
96
|
+
* — the consumer sees every text_delta in real time.
|
|
97
|
+
*/
|
|
98
|
+
function createRetryingStream(createStream, cronJob) {
|
|
99
|
+
let resultResolve;
|
|
100
|
+
const resultPromise = new Promise(resolve => { resultResolve = resolve; });
|
|
101
|
+
const CONTENT_EVENT_TYPES = new Set([
|
|
102
|
+
"text_start", "text_delta", "text_end",
|
|
103
|
+
"thinking_start", "thinking_delta", "thinking_end",
|
|
104
|
+
"toolcall_start", "toolcall_delta", "toolcall_end",
|
|
105
|
+
]);
|
|
106
|
+
async function* retryGenerator() {
|
|
107
|
+
for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
|
|
108
|
+
const stream = await createStream();
|
|
109
|
+
let hasContent = false;
|
|
110
|
+
const buffer = [];
|
|
111
|
+
let errorResult = null;
|
|
112
|
+
for await (const event of stream) {
|
|
113
|
+
const isContent = CONTENT_EVENT_TYPES.has(event.type);
|
|
114
|
+
if (!hasContent && !isContent) {
|
|
115
|
+
// ── Buffer phase (no content yet) ──
|
|
116
|
+
if (event.type === "done") {
|
|
117
|
+
// Success without content — flush buffer and finish
|
|
118
|
+
for (const b of buffer)
|
|
119
|
+
yield b;
|
|
120
|
+
resultResolve(event.message);
|
|
121
|
+
yield event;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (event.type === "error") {
|
|
125
|
+
errorResult = event.error;
|
|
126
|
+
}
|
|
127
|
+
buffer.push(event);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// ── Streaming phase ──
|
|
131
|
+
if (!hasContent) {
|
|
132
|
+
// First content event — flush buffer then yield
|
|
133
|
+
hasContent = true;
|
|
134
|
+
for (const b of buffer)
|
|
135
|
+
yield b;
|
|
136
|
+
}
|
|
137
|
+
yield event;
|
|
138
|
+
if (event.type === "done") {
|
|
139
|
+
resultResolve(event.message);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (event.type === "error") {
|
|
143
|
+
resultResolve(event.error);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Stream ended during buffer phase — decide whether to retry
|
|
149
|
+
if (errorResult?.stopReason === "error" && isRetryableProviderError(errorResult.errorMessage)) {
|
|
150
|
+
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
151
|
+
const delayMs = getRetryDelayMs(attempt + 1, cronJob);
|
|
152
|
+
console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
|
|
153
|
+
`${errorResult.errorMessage} — retrying in ${delayMs}ms`);
|
|
154
|
+
await sleep(delayMs);
|
|
155
|
+
continue; // discard buffer, retry with a new stream
|
|
156
|
+
}
|
|
157
|
+
console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
|
|
158
|
+
}
|
|
159
|
+
else if (errorResult) {
|
|
160
|
+
console.log(`[xiaoyiprovider] non-retryable error: ${errorResult.errorMessage}`);
|
|
161
|
+
}
|
|
162
|
+
// Non-retryable or retries exhausted — yield buffered events
|
|
163
|
+
for (const b of buffer)
|
|
164
|
+
yield b;
|
|
165
|
+
resultResolve(errorResult);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Safety: final fallback attempt
|
|
169
|
+
const lastStream = await createStream();
|
|
170
|
+
for await (const event of lastStream) {
|
|
171
|
+
yield event;
|
|
172
|
+
if (event.type === "done") {
|
|
173
|
+
resultResolve(event.message);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (event.type === "error") {
|
|
177
|
+
resultResolve(event.error);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const gen = retryGenerator();
|
|
183
|
+
return {
|
|
184
|
+
result: () => resultPromise,
|
|
185
|
+
push: () => { },
|
|
186
|
+
end: () => { },
|
|
187
|
+
[Symbol.asyncIterator]: () => gen,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
88
190
|
/**
|
|
89
191
|
* Dynamic header keys injected via extraParams and forwarded to the HTTP request.
|
|
90
192
|
* Correspond to the three fields written to .xiaoyiruntime:
|
|
@@ -216,51 +318,18 @@ export const xiaoyiProvider = {
|
|
|
216
318
|
const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
|
|
217
319
|
context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
|
|
218
320
|
}
|
|
219
|
-
// ── Retry
|
|
321
|
+
// ── Retry-capable streaming ──────────────────────────────
|
|
220
322
|
const cronJob = isCronTriggered(context.messages);
|
|
221
323
|
if (cronJob)
|
|
222
324
|
console.log("[xiaoyiprovider] detected cron-triggered request, using extended retry delays");
|
|
223
|
-
|
|
224
|
-
const stream = await underlying(model, context, {
|
|
225
|
-
...options,
|
|
226
|
-
headers: {
|
|
227
|
-
...options?.headers,
|
|
228
|
-
...dynamicHeaders,
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
// Wait for the stream to settle (done or error) to inspect the result.
|
|
232
|
-
// stream.result() resolves to the final AssistantMessage (even on error).
|
|
233
|
-
const result = await stream.result();
|
|
234
|
-
// Check if this is a retryable error
|
|
235
|
-
if (result.stopReason === "error" && isRetryableProviderError(result.errorMessage)) {
|
|
236
|
-
const delayMs = getRetryDelayMs(attempt + 1, cronJob);
|
|
237
|
-
console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
|
|
238
|
-
`${result.errorMessage} — retrying in ${delayMs}ms`);
|
|
239
|
-
await sleep(delayMs);
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
// Success or non-retryable error — log and return
|
|
243
|
-
if (result.stopReason === "error") {
|
|
244
|
-
console.log(`[xiaoyiprovider] non-retryable error: ${result.errorMessage}`);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
|
|
248
|
-
}
|
|
249
|
-
// The original stream has already been consumed by result().
|
|
250
|
-
// Build a replay stream that delivers the final result.
|
|
251
|
-
return buildReplayStream(result);
|
|
252
|
-
}
|
|
253
|
-
// All retries exhausted — return the last attempt's real error via a new stream
|
|
254
|
-
console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
|
|
255
|
-
const lastStream = await underlying(model, context, {
|
|
325
|
+
const makeStream = () => underlying(model, context, {
|
|
256
326
|
...options,
|
|
257
327
|
headers: {
|
|
258
328
|
...options?.headers,
|
|
259
329
|
...dynamicHeaders,
|
|
260
330
|
},
|
|
261
331
|
});
|
|
262
|
-
|
|
263
|
-
return buildReplayStream(lastResult);
|
|
332
|
+
return createRetryingStream(makeStream, cronJob);
|
|
264
333
|
};
|
|
265
334
|
},
|
|
266
335
|
};
|