@ynhcj/xiaoyi-channel 0.0.94-beta → 0.0.95-beta
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 +150 -37
- package/package.json +1 -1
package/dist/src/provider.js
CHANGED
|
@@ -23,8 +23,29 @@ function isRetryableProviderError(message) {
|
|
|
23
23
|
return true;
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
|
+
/** Check if the request is triggered by a cron job by inspecting the first user message. */
|
|
27
|
+
function isCronTriggered(messages) {
|
|
28
|
+
if (!messages)
|
|
29
|
+
return false;
|
|
30
|
+
const firstUser = messages.find(m => m.role === "user");
|
|
31
|
+
if (!firstUser)
|
|
32
|
+
return false;
|
|
33
|
+
let text = "";
|
|
34
|
+
if (typeof firstUser.content === "string") {
|
|
35
|
+
text = firstUser.content;
|
|
36
|
+
}
|
|
37
|
+
else if (Array.isArray(firstUser.content)) {
|
|
38
|
+
const block = firstUser.content.find(b => b.type === "text" && typeof b.text === "string");
|
|
39
|
+
if (block)
|
|
40
|
+
text = block.text;
|
|
41
|
+
}
|
|
42
|
+
return /^\[cron:/i.test(text.trim());
|
|
43
|
+
}
|
|
26
44
|
/** Compute retry delay in ms for the given 1-based attempt, with up to 10s jitter. */
|
|
27
|
-
function getRetryDelayMs(attempt) {
|
|
45
|
+
function getRetryDelayMs(attempt, isCron = false) {
|
|
46
|
+
if (isCron) {
|
|
47
|
+
return 60_000 + Math.floor(Math.random() * 10_000);
|
|
48
|
+
}
|
|
28
49
|
const base = attempt <= RETRY_DELAYS_MS.length
|
|
29
50
|
? RETRY_DELAYS_MS[attempt - 1]
|
|
30
51
|
: RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1];
|
|
@@ -64,6 +85,128 @@ function buildReplayStream(result) {
|
|
|
64
85
|
},
|
|
65
86
|
};
|
|
66
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
|
+
console.log(`[xiaoyiprovider] stream completed (no content), usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
|
|
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
|
+
console.log("[xiaoyiprovider] first content event received, switching to streaming mode");
|
|
133
|
+
hasContent = true;
|
|
134
|
+
for (const b of buffer)
|
|
135
|
+
yield b;
|
|
136
|
+
}
|
|
137
|
+
// IMPORTANT: resolve result() BEFORE yielding terminal events to avoid deadlock.
|
|
138
|
+
// The SDK calls result() when it sees done/error — if we yield first, the generator
|
|
139
|
+
// suspends and can never reach resolve, causing a permanent deadlock.
|
|
140
|
+
if (event.type === "done") {
|
|
141
|
+
console.log(`[xiaoyiprovider] stream completed, usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
|
|
142
|
+
resultResolve(event.message);
|
|
143
|
+
yield event;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (event.type === "error") {
|
|
147
|
+
console.log(`[xiaoyiprovider] stream error after content: ${event.error?.errorMessage}`);
|
|
148
|
+
resultResolve(event.error);
|
|
149
|
+
yield event;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
yield event;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Stream ended during buffer phase — decide whether to retry
|
|
156
|
+
if (errorResult?.stopReason === "error" && isRetryableProviderError(errorResult.errorMessage)) {
|
|
157
|
+
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
158
|
+
const delayMs = getRetryDelayMs(attempt + 1, cronJob);
|
|
159
|
+
console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
|
|
160
|
+
`${errorResult.errorMessage} — retrying in ${delayMs}ms`);
|
|
161
|
+
await sleep(delayMs);
|
|
162
|
+
continue; // discard buffer, retry with a new stream
|
|
163
|
+
}
|
|
164
|
+
console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
|
|
165
|
+
}
|
|
166
|
+
else if (errorResult) {
|
|
167
|
+
console.log(`[xiaoyiprovider] non-retryable error: ${errorResult.errorMessage}`);
|
|
168
|
+
}
|
|
169
|
+
// Non-retryable or retries exhausted — yield buffered events.
|
|
170
|
+
// Resolve before yielding the terminal event to avoid the same deadlock.
|
|
171
|
+
for (const b of buffer) {
|
|
172
|
+
if (b.type === "done") {
|
|
173
|
+
resultResolve(b.message);
|
|
174
|
+
}
|
|
175
|
+
else if (b.type === "error") {
|
|
176
|
+
resultResolve(b.error);
|
|
177
|
+
}
|
|
178
|
+
yield b;
|
|
179
|
+
}
|
|
180
|
+
if (errorResult && buffer.every(b => b.type !== "done" && b.type !== "error")) {
|
|
181
|
+
resultResolve(errorResult);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Safety: final fallback attempt
|
|
186
|
+
console.log("[xiaoyiprovider] entering final fallback attempt");
|
|
187
|
+
const lastStream = await createStream();
|
|
188
|
+
for await (const event of lastStream) {
|
|
189
|
+
if (event.type === "done") {
|
|
190
|
+
resultResolve(event.message);
|
|
191
|
+
yield event;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (event.type === "error") {
|
|
195
|
+
resultResolve(event.error);
|
|
196
|
+
yield event;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
yield event;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const gen = retryGenerator();
|
|
203
|
+
return {
|
|
204
|
+
result: () => resultPromise,
|
|
205
|
+
push: () => { },
|
|
206
|
+
end: () => { },
|
|
207
|
+
[Symbol.asyncIterator]: () => gen,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
67
210
|
/**
|
|
68
211
|
* Dynamic header keys injected via extraParams and forwarded to the HTTP request.
|
|
69
212
|
* Correspond to the three fields written to .xiaoyiruntime:
|
|
@@ -195,48 +338,18 @@ export const xiaoyiProvider = {
|
|
|
195
338
|
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"`;
|
|
196
339
|
context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
|
|
197
340
|
}
|
|
198
|
-
// ── Retry
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
...options?.headers,
|
|
204
|
-
...dynamicHeaders,
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
// Wait for the stream to settle (done or error) to inspect the result.
|
|
208
|
-
// stream.result() resolves to the final AssistantMessage (even on error).
|
|
209
|
-
const result = await stream.result();
|
|
210
|
-
// Check if this is a retryable error
|
|
211
|
-
if (result.stopReason === "error" && isRetryableProviderError(result.errorMessage)) {
|
|
212
|
-
const delayMs = getRetryDelayMs(attempt);
|
|
213
|
-
console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
|
|
214
|
-
`${result.errorMessage} — retrying in ${delayMs}ms`);
|
|
215
|
-
await sleep(delayMs);
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
// Success or non-retryable error — log and return
|
|
219
|
-
if (result.stopReason === "error") {
|
|
220
|
-
console.log(`[xiaoyiprovider] non-retryable error: ${result.errorMessage}`);
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
|
|
224
|
-
}
|
|
225
|
-
// The original stream has already been consumed by result().
|
|
226
|
-
// Build a replay stream that delivers the final result.
|
|
227
|
-
return buildReplayStream(result);
|
|
228
|
-
}
|
|
229
|
-
// All retries exhausted — return the last attempt's real error via a new stream
|
|
230
|
-
console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
|
|
231
|
-
const lastStream = await underlying(model, context, {
|
|
341
|
+
// ── Retry-capable streaming ──────────────────────────────
|
|
342
|
+
const cronJob = isCronTriggered(context.messages);
|
|
343
|
+
if (cronJob)
|
|
344
|
+
console.log("[xiaoyiprovider] detected cron-triggered request, using extended retry delays");
|
|
345
|
+
const makeStream = () => underlying(model, context, {
|
|
232
346
|
...options,
|
|
233
347
|
headers: {
|
|
234
348
|
...options?.headers,
|
|
235
349
|
...dynamicHeaders,
|
|
236
350
|
},
|
|
237
351
|
});
|
|
238
|
-
|
|
239
|
-
return buildReplayStream(lastResult);
|
|
352
|
+
return createRetryingStream(makeStream, cronJob);
|
|
240
353
|
};
|
|
241
354
|
},
|
|
242
355
|
};
|