@ryanfw/prompt-orchestration-pipeline 0.16.4 → 0.17.0
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/package.json +1 -1
- package/src/config/models.js +63 -1
- package/src/core/orchestrator.js +28 -56
- package/src/core/pipeline-runner.js +51 -1
- package/src/core/task-runner.js +17 -7
- package/src/llm/index.js +148 -0
- package/src/pages/Code.jsx +201 -2
- package/src/providers/anthropic.js +3 -2
- package/src/providers/base.js +19 -0
- package/src/providers/deepseek.js +3 -2
- package/src/providers/moonshot.js +218 -0
- package/src/ui/dist/assets/{index-DI_nRqVI.js → index-xx8otyG0.js} +142 -1
- package/src/ui/dist/assets/{index-DI_nRqVI.js.map → index-xx8otyG0.js.map} +1 -1
- package/src/ui/dist/index.html +1 -1
package/src/pages/Code.jsx
CHANGED
|
@@ -24,11 +24,75 @@ import {
|
|
|
24
24
|
const sections = [
|
|
25
25
|
{ id: "environment", label: "Environment", icon: Key },
|
|
26
26
|
{ id: "getting-started", label: "Getting Started", icon: FileText },
|
|
27
|
+
{ id: "pipeline-config", label: "Pipeline Config", icon: Folder },
|
|
27
28
|
{ id: "io-api", label: "IO API", icon: Database },
|
|
28
29
|
{ id: "llm-api", label: "LLM API", icon: Cpu },
|
|
29
30
|
{ id: "validation", label: "Validation", icon: Shield },
|
|
30
31
|
];
|
|
31
32
|
|
|
33
|
+
// Sample pipeline.json for documentation
|
|
34
|
+
const samplePipelineJson = {
|
|
35
|
+
name: "content-generation",
|
|
36
|
+
version: "1.0.0",
|
|
37
|
+
description: "Demo pipeline showcasing multi-stage LLM workflows",
|
|
38
|
+
tasks: ["research", "analysis", "synthesis", "formatting"],
|
|
39
|
+
taskConfig: {
|
|
40
|
+
research: {
|
|
41
|
+
maxRetries: 3,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
llm: {
|
|
45
|
+
provider: "anthropic",
|
|
46
|
+
model: "claude-sonnet-4-20250514",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Pipeline.json field definitions
|
|
51
|
+
const pipelineFields = [
|
|
52
|
+
{
|
|
53
|
+
name: "name",
|
|
54
|
+
required: true,
|
|
55
|
+
type: "string",
|
|
56
|
+
description:
|
|
57
|
+
"Unique identifier for the pipeline. Used to reference this pipeline from seed files.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "version",
|
|
61
|
+
required: false,
|
|
62
|
+
type: "string",
|
|
63
|
+
description:
|
|
64
|
+
'Semantic version of the pipeline (e.g., "1.0.0"). Useful for tracking changes.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "description",
|
|
68
|
+
required: false,
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Human-readable description of what this pipeline does.",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "tasks",
|
|
74
|
+
required: true,
|
|
75
|
+
type: "string[]",
|
|
76
|
+
description:
|
|
77
|
+
"Ordered array of task names to execute. Each task must be registered in the task index.",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "taskConfig",
|
|
81
|
+
required: false,
|
|
82
|
+
type: "object",
|
|
83
|
+
description:
|
|
84
|
+
"Per-task configuration overrides. Keys are task names, values are config objects passed to stages.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "llm",
|
|
88
|
+
required: false,
|
|
89
|
+
type: "{ provider, model }",
|
|
90
|
+
description:
|
|
91
|
+
"Pipeline-level LLM override. When set, ALL task LLM calls are routed to this provider/model.",
|
|
92
|
+
isNew: true,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
32
96
|
// IO Functions organized by category
|
|
33
97
|
const writeFunctions = [
|
|
34
98
|
{
|
|
@@ -125,6 +189,7 @@ const envVars = [
|
|
|
125
189
|
{ name: "GEMINI_API_KEY", provider: "Google Gemini" },
|
|
126
190
|
{ name: "DEEPSEEK_API_KEY", provider: "DeepSeek" },
|
|
127
191
|
{ name: "ZHIPU_API_KEY", provider: "Zhipu" },
|
|
192
|
+
{ name: "MOONSHOT_API_KEY", provider: "Moonshot" },
|
|
128
193
|
];
|
|
129
194
|
|
|
130
195
|
// Collapsible Section Component
|
|
@@ -231,7 +296,7 @@ export default function CodePage() {
|
|
|
231
296
|
}
|
|
232
297
|
});
|
|
233
298
|
},
|
|
234
|
-
{ rootMargin: "-100px 0px -66% 0px" }
|
|
299
|
+
{ rootMargin: "-100px 0px -66% 0px" },
|
|
235
300
|
);
|
|
236
301
|
|
|
237
302
|
sections.forEach(({ id }) => {
|
|
@@ -405,6 +470,140 @@ export default function CodePage() {
|
|
|
405
470
|
</div>
|
|
406
471
|
</CollapsibleSection>
|
|
407
472
|
|
|
473
|
+
{/* Pipeline Config Section */}
|
|
474
|
+
<CollapsibleSection
|
|
475
|
+
id="pipeline-config"
|
|
476
|
+
title="Pipeline Configuration (pipeline.json)"
|
|
477
|
+
icon={Folder}
|
|
478
|
+
defaultOpen={true}
|
|
479
|
+
>
|
|
480
|
+
<Text as="p" size="3" className="text-gray-600 mb-4">
|
|
481
|
+
Each pipeline is defined by a <Code size="2">pipeline.json</Code>{" "}
|
|
482
|
+
file in its directory. This file specifies which tasks to run and
|
|
483
|
+
optional configuration overrides.
|
|
484
|
+
</Text>
|
|
485
|
+
|
|
486
|
+
<div className="space-y-6">
|
|
487
|
+
{/* Fields Table */}
|
|
488
|
+
<div>
|
|
489
|
+
<Text size="2" weight="medium" className="mb-3 block">
|
|
490
|
+
Fields
|
|
491
|
+
</Text>
|
|
492
|
+
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
|
493
|
+
<Table.Root>
|
|
494
|
+
<Table.Header>
|
|
495
|
+
<Table.Row>
|
|
496
|
+
<Table.ColumnHeaderCell className="bg-gray-50">
|
|
497
|
+
Field
|
|
498
|
+
</Table.ColumnHeaderCell>
|
|
499
|
+
<Table.ColumnHeaderCell className="bg-gray-50">
|
|
500
|
+
Type
|
|
501
|
+
</Table.ColumnHeaderCell>
|
|
502
|
+
<Table.ColumnHeaderCell className="bg-gray-50">
|
|
503
|
+
Required
|
|
504
|
+
</Table.ColumnHeaderCell>
|
|
505
|
+
<Table.ColumnHeaderCell className="bg-gray-50">
|
|
506
|
+
Description
|
|
507
|
+
</Table.ColumnHeaderCell>
|
|
508
|
+
</Table.Row>
|
|
509
|
+
</Table.Header>
|
|
510
|
+
<Table.Body>
|
|
511
|
+
{pipelineFields.map((field) => (
|
|
512
|
+
<Table.Row key={field.name}>
|
|
513
|
+
<Table.RowHeaderCell>
|
|
514
|
+
<Flex align="center" gap="2">
|
|
515
|
+
<Code size="2">{field.name}</Code>
|
|
516
|
+
{field.isNew && (
|
|
517
|
+
<span className="px-1.5 py-0.5 text-xs font-medium bg-green-100 text-green-700 rounded">
|
|
518
|
+
NEW
|
|
519
|
+
</span>
|
|
520
|
+
)}
|
|
521
|
+
</Flex>
|
|
522
|
+
</Table.RowHeaderCell>
|
|
523
|
+
<Table.Cell>
|
|
524
|
+
<Code size="1" className="text-gray-600">
|
|
525
|
+
{field.type}
|
|
526
|
+
</Code>
|
|
527
|
+
</Table.Cell>
|
|
528
|
+
<Table.Cell>
|
|
529
|
+
{field.required ? (
|
|
530
|
+
<span className="text-red-600 font-medium">
|
|
531
|
+
Yes
|
|
532
|
+
</span>
|
|
533
|
+
) : (
|
|
534
|
+
<span className="text-gray-400">No</span>
|
|
535
|
+
)}
|
|
536
|
+
</Table.Cell>
|
|
537
|
+
<Table.Cell className="text-gray-600 text-sm">
|
|
538
|
+
{field.description}
|
|
539
|
+
</Table.Cell>
|
|
540
|
+
</Table.Row>
|
|
541
|
+
))}
|
|
542
|
+
</Table.Body>
|
|
543
|
+
</Table.Root>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
{/* Example */}
|
|
548
|
+
<div>
|
|
549
|
+
<Text size="2" weight="medium" className="mb-2 block">
|
|
550
|
+
Example
|
|
551
|
+
</Text>
|
|
552
|
+
<CopyableCodeBlock maxHeight="280px">
|
|
553
|
+
{JSON.stringify(samplePipelineJson, null, 2)}
|
|
554
|
+
</CopyableCodeBlock>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
{/* LLM Override Details */}
|
|
558
|
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
559
|
+
<Flex align="center" gap="2" className="mb-2">
|
|
560
|
+
<Cpu className="h-4 w-4 text-blue-600" />
|
|
561
|
+
<Text size="2" weight="medium" className="text-blue-800">
|
|
562
|
+
Pipeline-Level LLM Override
|
|
563
|
+
</Text>
|
|
564
|
+
<span className="px-1.5 py-0.5 text-xs font-medium bg-green-100 text-green-700 rounded">
|
|
565
|
+
NEW
|
|
566
|
+
</span>
|
|
567
|
+
</Flex>
|
|
568
|
+
<Text as="p" size="2" className="text-blue-700 mb-3">
|
|
569
|
+
When the <Code size="2">llm</Code> field is set in
|
|
570
|
+
pipeline.json, ALL LLM calls from task stages are
|
|
571
|
+
automatically routed to the specified provider and model —
|
|
572
|
+
regardless of what the task code requests.
|
|
573
|
+
</Text>
|
|
574
|
+
<ul className="space-y-1 text-sm text-blue-700">
|
|
575
|
+
<li className="flex items-start gap-2">
|
|
576
|
+
<span className="text-blue-500 mt-0.5">•</span>
|
|
577
|
+
<span>
|
|
578
|
+
Tasks calling <Code size="1">llm.deepseek.chat()</Code>{" "}
|
|
579
|
+
will use the override provider/model
|
|
580
|
+
</span>
|
|
581
|
+
</li>
|
|
582
|
+
<li className="flex items-start gap-2">
|
|
583
|
+
<span className="text-blue-500 mt-0.5">•</span>
|
|
584
|
+
<span>
|
|
585
|
+
Original provider/model is preserved in{" "}
|
|
586
|
+
<Code size="1">metadata.originalProvider</Code>
|
|
587
|
+
</span>
|
|
588
|
+
</li>
|
|
589
|
+
<li className="flex items-start gap-2">
|
|
590
|
+
<span className="text-blue-500 mt-0.5">•</span>
|
|
591
|
+
<span>
|
|
592
|
+
Useful for A/B testing, cost control, or switching
|
|
593
|
+
providers during outages
|
|
594
|
+
</span>
|
|
595
|
+
</li>
|
|
596
|
+
</ul>
|
|
597
|
+
</div>
|
|
598
|
+
|
|
599
|
+
{/* File Location */}
|
|
600
|
+
<div className="text-sm text-gray-500">
|
|
601
|
+
<span>Location: </span>
|
|
602
|
+
<Code size="2">{"{pipelineDir}"}/pipeline.json</Code>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
</CollapsibleSection>
|
|
606
|
+
|
|
408
607
|
{/* IO API Section */}
|
|
409
608
|
<CollapsibleSection
|
|
410
609
|
id="io-api"
|
|
@@ -529,7 +728,7 @@ export default function CodePage() {
|
|
|
529
728
|
</Code>
|
|
530
729
|
</Table.Cell>
|
|
531
730
|
</Table.Row>
|
|
532
|
-
))
|
|
731
|
+
)),
|
|
533
732
|
)}
|
|
534
733
|
</Table.Body>
|
|
535
734
|
</Table.Root>
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
tryParseJSON,
|
|
7
7
|
ensureJsonResponseFormat,
|
|
8
8
|
ProviderJsonParseError,
|
|
9
|
+
createProviderError,
|
|
9
10
|
} from "./base.js";
|
|
10
11
|
import { createLogger } from "../core/logger.js";
|
|
11
12
|
|
|
@@ -70,10 +71,10 @@ export async function anthropicChat({
|
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
if (!response.ok) {
|
|
73
|
-
const
|
|
74
|
+
const errorBody = await response
|
|
74
75
|
.json()
|
|
75
76
|
.catch(() => ({ error: response.statusText }));
|
|
76
|
-
throw
|
|
77
|
+
throw createProviderError(response.status, errorBody, response.statusText);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
const data = await response.json();
|
package/src/providers/base.js
CHANGED
|
@@ -89,6 +89,25 @@ export function tryParseJSON(text) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Creates a proper Error instance from an HTTP error response.
|
|
94
|
+
* This ensures errors have proper stack traces and don't cause
|
|
95
|
+
* "UnhandledPromiseRejection: #<Object>" crashes.
|
|
96
|
+
*
|
|
97
|
+
* @param {number} status - HTTP status code
|
|
98
|
+
* @param {Object} errorBody - Parsed error response body
|
|
99
|
+
* @param {string} fallbackMessage - Fallback message if none in errorBody
|
|
100
|
+
* @returns {Error} Error instance with status and details attached
|
|
101
|
+
*/
|
|
102
|
+
export function createProviderError(status, errorBody, fallbackMessage = "Request failed") {
|
|
103
|
+
const message = errorBody?.error?.message || errorBody?.message || fallbackMessage;
|
|
104
|
+
const err = new Error(`[${status}] ${message}`);
|
|
105
|
+
err.status = status;
|
|
106
|
+
err.code = errorBody?.error?.code || errorBody?.code;
|
|
107
|
+
err.details = errorBody;
|
|
108
|
+
return err;
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
/**
|
|
93
112
|
* Error thrown when JSON response format is required but not provided
|
|
94
113
|
*/
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
tryParseJSON,
|
|
7
7
|
ensureJsonResponseFormat,
|
|
8
8
|
ProviderJsonParseError,
|
|
9
|
+
createProviderError,
|
|
9
10
|
} from "./base.js";
|
|
10
11
|
import { createLogger } from "../core/logger.js";
|
|
11
12
|
|
|
@@ -77,10 +78,10 @@ export async function deepseekChat({
|
|
|
77
78
|
);
|
|
78
79
|
|
|
79
80
|
if (!response.ok) {
|
|
80
|
-
const
|
|
81
|
+
const errorBody = await response
|
|
81
82
|
.json()
|
|
82
83
|
.catch(() => ({ error: response.statusText }));
|
|
83
|
-
throw
|
|
84
|
+
throw createProviderError(response.status, errorBody, response.statusText);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// Streaming mode - return async generator for real-time chunks
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractMessages,
|
|
3
|
+
isRetryableError,
|
|
4
|
+
sleep,
|
|
5
|
+
stripMarkdownFences,
|
|
6
|
+
tryParseJSON,
|
|
7
|
+
ProviderJsonParseError,
|
|
8
|
+
createProviderError,
|
|
9
|
+
} from "./base.js";
|
|
10
|
+
import { createLogger } from "../core/logger.js";
|
|
11
|
+
|
|
12
|
+
const logger = createLogger("Moonshot");
|
|
13
|
+
|
|
14
|
+
export async function moonshotChat({
|
|
15
|
+
messages,
|
|
16
|
+
model = "moonshot-v1-128k",
|
|
17
|
+
temperature = 0.7,
|
|
18
|
+
maxTokens,
|
|
19
|
+
responseFormat = "json_object",
|
|
20
|
+
topP,
|
|
21
|
+
frequencyPenalty,
|
|
22
|
+
presencePenalty,
|
|
23
|
+
stop,
|
|
24
|
+
stream = false,
|
|
25
|
+
maxRetries = 3,
|
|
26
|
+
}) {
|
|
27
|
+
const isJsonMode =
|
|
28
|
+
responseFormat?.type === "json_object" ||
|
|
29
|
+
responseFormat?.type === "json_schema" ||
|
|
30
|
+
responseFormat === "json" ||
|
|
31
|
+
responseFormat === "json_object";
|
|
32
|
+
|
|
33
|
+
logger.log("moonshotChat called", { model, stream, maxRetries, isJsonMode });
|
|
34
|
+
|
|
35
|
+
if (!process.env.MOONSHOT_API_KEY) {
|
|
36
|
+
throw new Error("Moonshot API key not configured");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { systemMsg, userMsg } = extractMessages(messages);
|
|
40
|
+
|
|
41
|
+
logger.log("Messages extracted", {
|
|
42
|
+
systemMsgLength: systemMsg?.length,
|
|
43
|
+
userMsgLength: userMsg?.length,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let lastError;
|
|
47
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
48
|
+
if (attempt > 0) {
|
|
49
|
+
const sleepMs = Math.pow(2, attempt) * 1000;
|
|
50
|
+
logger.log("Retry attempt", { attempt, sleepMs });
|
|
51
|
+
await sleep(sleepMs);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
logger.log("Sending request to Moonshot API", { attempt, model });
|
|
56
|
+
// Thinking models only accept temperature=1
|
|
57
|
+
//const isThinkingModel = model.includes("thinking");
|
|
58
|
+
//const effectiveTemperature = isThinkingModel ? 1 : temperature;
|
|
59
|
+
|
|
60
|
+
const requestBody = {
|
|
61
|
+
model,
|
|
62
|
+
messages: [
|
|
63
|
+
{ role: "system", content: systemMsg },
|
|
64
|
+
{ role: "user", content: userMsg },
|
|
65
|
+
],
|
|
66
|
+
temperature: 1,
|
|
67
|
+
max_tokens: maxTokens,
|
|
68
|
+
top_p: topP,
|
|
69
|
+
frequency_penalty: frequencyPenalty,
|
|
70
|
+
presence_penalty: presencePenalty,
|
|
71
|
+
stop,
|
|
72
|
+
stream,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (isJsonMode && !stream) {
|
|
76
|
+
requestBody.response_format = { type: "json_object" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
logger.log("About to call fetch...");
|
|
80
|
+
const response = await fetch(
|
|
81
|
+
"https://api.moonshot.ai/v1/chat/completions",
|
|
82
|
+
{
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
Authorization: `Bearer ${process.env.MOONSHOT_API_KEY}`,
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(requestBody),
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
logger.log("Fetch returned", {
|
|
92
|
+
status: response.status,
|
|
93
|
+
ok: response.ok,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const errorBody = await response
|
|
98
|
+
.json()
|
|
99
|
+
.catch(() => ({ error: response.statusText }));
|
|
100
|
+
|
|
101
|
+
// Provide more helpful error message for authentication failures
|
|
102
|
+
if (response.status === 401) {
|
|
103
|
+
const enhancedError = createProviderError(
|
|
104
|
+
response.status,
|
|
105
|
+
errorBody,
|
|
106
|
+
"Invalid Moonshot API key. Please verify your MOONSHOT_API_KEY environment variable is correct and has not expired. Get your API key at https://platform.moonshot.ai/",
|
|
107
|
+
);
|
|
108
|
+
throw enhancedError;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw createProviderError(
|
|
112
|
+
response.status,
|
|
113
|
+
errorBody,
|
|
114
|
+
response.statusText,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Step 6: Handle streaming response path
|
|
119
|
+
if (stream) {
|
|
120
|
+
logger.log("Handling streaming response");
|
|
121
|
+
return createStreamGenerator(response.body);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Step 7: Handle non-streaming response parsing
|
|
125
|
+
logger.log("Parsing JSON response...");
|
|
126
|
+
const data = await response.json();
|
|
127
|
+
logger.log("JSON parsed successfully", {
|
|
128
|
+
hasChoices: !!data.choices,
|
|
129
|
+
choicesCount: data.choices?.length,
|
|
130
|
+
});
|
|
131
|
+
const rawContent = data.choices[0].message.content;
|
|
132
|
+
|
|
133
|
+
const content = stripMarkdownFences(rawContent);
|
|
134
|
+
|
|
135
|
+
if (isJsonMode) {
|
|
136
|
+
const parsed = tryParseJSON(content);
|
|
137
|
+
if (!parsed) {
|
|
138
|
+
throw new ProviderJsonParseError(
|
|
139
|
+
"Moonshot",
|
|
140
|
+
model,
|
|
141
|
+
content.substring(0, 200),
|
|
142
|
+
"Failed to parse JSON response from Moonshot API",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
content: parsed,
|
|
147
|
+
usage: data.usage,
|
|
148
|
+
raw: data,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
content,
|
|
154
|
+
usage: data.usage,
|
|
155
|
+
raw: data,
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
lastError = error;
|
|
159
|
+
logger.warn("Attempt failed", {
|
|
160
|
+
attempt,
|
|
161
|
+
errorMessage: error.message || error,
|
|
162
|
+
errorStatus: error.status,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (error.status === 401) throw error;
|
|
166
|
+
|
|
167
|
+
if (isRetryableError(error) && attempt < maxRetries) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (attempt === maxRetries) throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw lastError || new Error(`Failed after ${maxRetries + 1} attempts`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create async generator for streaming Moonshot responses.
|
|
180
|
+
* Moonshot uses Server-Sent Events format with "data:" prefix.
|
|
181
|
+
*/
|
|
182
|
+
async function* createStreamGenerator(stream) {
|
|
183
|
+
const decoder = new TextDecoder();
|
|
184
|
+
const reader = stream.getReader();
|
|
185
|
+
let buffer = "";
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
while (true) {
|
|
189
|
+
const { done, value } = await reader.read();
|
|
190
|
+
if (done) break;
|
|
191
|
+
|
|
192
|
+
buffer += decoder.decode(value, { stream: true });
|
|
193
|
+
const lines = buffer.split("\n");
|
|
194
|
+
buffer = lines.pop(); // Keep incomplete line
|
|
195
|
+
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
if (line.startsWith("data: ")) {
|
|
198
|
+
const data = line.slice(6);
|
|
199
|
+
if (data === "[DONE]") continue;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(data);
|
|
203
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
204
|
+
// Skip only truly empty chunks; preserve whitespace-only content
|
|
205
|
+
if (content !== undefined && content !== null && content !== "") {
|
|
206
|
+
yield { content };
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// Skip malformed JSON
|
|
210
|
+
logger.warn("Failed to parse stream chunk:", e);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
reader.releaseLock();
|
|
217
|
+
}
|
|
218
|
+
}
|