@ryanfw/prompt-orchestration-pipeline 0.5.0 → 0.7.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/README.md +1 -2
- package/package.json +1 -2
- package/src/api/validators/json.js +39 -0
- package/src/components/DAGGrid.jsx +392 -303
- package/src/components/JobCard.jsx +14 -12
- package/src/components/JobDetail.jsx +54 -51
- package/src/components/JobTable.jsx +72 -23
- package/src/components/Layout.jsx +145 -42
- package/src/components/LiveText.jsx +47 -0
- package/src/components/PageSubheader.jsx +75 -0
- package/src/components/TaskDetailSidebar.jsx +216 -0
- package/src/components/TimerText.jsx +82 -0
- package/src/components/UploadSeed.jsx +0 -70
- package/src/components/ui/Logo.jsx +16 -0
- package/src/components/ui/RestartJobModal.jsx +140 -0
- package/src/components/ui/toast.jsx +138 -0
- package/src/config/models.js +322 -0
- package/src/config/statuses.js +119 -0
- package/src/core/config.js +4 -34
- package/src/core/file-io.js +13 -28
- package/src/core/module-loader.js +54 -40
- package/src/core/pipeline-runner.js +65 -26
- package/src/core/status-writer.js +213 -58
- package/src/core/symlink-bridge.js +57 -0
- package/src/core/symlink-utils.js +94 -0
- package/src/core/task-runner.js +321 -437
- package/src/llm/index.js +258 -86
- package/src/pages/Code.jsx +351 -0
- package/src/pages/PipelineDetail.jsx +124 -15
- package/src/pages/PromptPipelineDashboard.jsx +20 -88
- package/src/providers/anthropic.js +83 -69
- package/src/providers/base.js +52 -0
- package/src/providers/deepseek.js +20 -21
- package/src/providers/gemini.js +226 -0
- package/src/providers/openai.js +36 -106
- package/src/providers/zhipu.js +136 -0
- package/src/ui/client/adapters/job-adapter.js +42 -28
- package/src/ui/client/api.js +134 -0
- package/src/ui/client/hooks/useJobDetailWithUpdates.js +65 -179
- package/src/ui/client/index.css +15 -0
- package/src/ui/client/index.html +2 -1
- package/src/ui/client/main.jsx +19 -14
- package/src/ui/client/time-store.js +161 -0
- package/src/ui/config-bridge.js +15 -24
- package/src/ui/config-bridge.node.js +15 -24
- package/src/ui/dist/assets/{index-CxcrauYR.js → index-DqkbzXZ1.js} +2132 -1086
- package/src/ui/dist/assets/style-DBF9NQGk.css +62 -0
- package/src/ui/dist/index.html +4 -3
- package/src/ui/job-reader.js +0 -108
- package/src/ui/public/favicon.svg +12 -0
- package/src/ui/server.js +252 -0
- package/src/ui/sse-enhancer.js +0 -1
- package/src/ui/transformers/list-transformer.js +32 -12
- package/src/ui/transformers/status-transformer.js +29 -42
- package/src/utils/dag.js +8 -4
- package/src/utils/duration.js +13 -19
- package/src/utils/formatters.js +27 -0
- package/src/utils/geometry-equality.js +83 -0
- package/src/utils/pipelines.js +5 -1
- package/src/utils/time-utils.js +40 -0
- package/src/utils/token-cost-calculator.js +294 -0
- package/src/utils/ui.jsx +18 -20
- package/src/components/ui/select.jsx +0 -27
- package/src/lib/utils.js +0 -6
- package/src/ui/client/hooks/useTicker.js +0 -26
- package/src/ui/config-bridge.browser.js +0 -149
- package/src/ui/dist/assets/style-D6K_oQ12.css +0 -62
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractMessages,
|
|
3
|
+
isRetryableError,
|
|
4
|
+
sleep,
|
|
5
|
+
tryParseJSON,
|
|
6
|
+
ensureJsonResponseFormat,
|
|
7
|
+
ProviderJsonParseError,
|
|
8
|
+
} from "./base.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Google Gemini provider implementation
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options - Provider options
|
|
14
|
+
* @param {Array} options.messages - Message array with system and user roles
|
|
15
|
+
* @param {string} options.model - Model name (default: "gemini-2.5-flash")
|
|
16
|
+
* @param {number} options.temperature - Temperature for sampling (default: 0.7)
|
|
17
|
+
* @param {number} options.maxTokens - Maximum tokens in response
|
|
18
|
+
* @param {string|Object} options.responseFormat - Response format ("json" or schema object)
|
|
19
|
+
* @param {number} options.topP - Top-p sampling parameter
|
|
20
|
+
* @param {string} options.stop - Stop sequence
|
|
21
|
+
* @param {number} options.maxRetries - Maximum retry attempts (default: 3)
|
|
22
|
+
* @returns {Promise<Object>} Provider response with content, text, usage, and raw response
|
|
23
|
+
*/
|
|
24
|
+
export async function geminiChat(options) {
|
|
25
|
+
const {
|
|
26
|
+
messages,
|
|
27
|
+
model = "gemini-2.5-flash",
|
|
28
|
+
temperature = 0.7,
|
|
29
|
+
maxTokens,
|
|
30
|
+
responseFormat,
|
|
31
|
+
topP,
|
|
32
|
+
frequencyPenalty,
|
|
33
|
+
presencePenalty,
|
|
34
|
+
stop,
|
|
35
|
+
maxRetries = 3
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
// Validate response format (Gemini only supports JSON mode)
|
|
39
|
+
ensureJsonResponseFormat(responseFormat, "Gemini");
|
|
40
|
+
|
|
41
|
+
// Check API key
|
|
42
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
43
|
+
throw new Error("Gemini API key not configured");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract system and user messages
|
|
47
|
+
const { systemMsg, userMsg } = extractMessages(messages);
|
|
48
|
+
|
|
49
|
+
// Build system instruction for JSON enforcement
|
|
50
|
+
let systemInstruction = systemMsg;
|
|
51
|
+
if (responseFormat === "json" || responseFormat?.type === "json_object") {
|
|
52
|
+
systemInstruction = `${systemMsg}\n\nYou must output strict JSON only with no extra text.`;
|
|
53
|
+
}
|
|
54
|
+
if (responseFormat?.json_schema) {
|
|
55
|
+
systemInstruction = `${systemMsg}\n\nYou must output strict JSON only matching this schema (no extra text):\n${JSON.stringify(responseFormat.json_schema)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Prepare request body
|
|
59
|
+
const requestBody = {
|
|
60
|
+
contents: [
|
|
61
|
+
{
|
|
62
|
+
parts: [
|
|
63
|
+
{
|
|
64
|
+
text: userMsg,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
generationConfig: {
|
|
70
|
+
temperature,
|
|
71
|
+
maxOutputTokens: maxTokens,
|
|
72
|
+
topP,
|
|
73
|
+
stopSequences: stop ? [stop] : undefined,
|
|
74
|
+
},
|
|
75
|
+
safetySettings: [
|
|
76
|
+
{
|
|
77
|
+
category: "HARM_CATEGORY_HARASSMENT",
|
|
78
|
+
threshold: "BLOCK_NONE",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
category: "HARM_CATEGORY_HATE_SPEECH",
|
|
82
|
+
threshold: "BLOCK_NONE",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
86
|
+
threshold: "BLOCK_NONE",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
90
|
+
threshold: "BLOCK_NONE",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Add system instruction if provided
|
|
96
|
+
if (systemInstruction.trim()) {
|
|
97
|
+
requestBody.systemInstruction = {
|
|
98
|
+
parts: [
|
|
99
|
+
{
|
|
100
|
+
text: systemInstruction,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Remove undefined values
|
|
107
|
+
if (topP === undefined) delete requestBody.generationConfig.topP;
|
|
108
|
+
if (stop === undefined) delete requestBody.generationConfig.stopSequences;
|
|
109
|
+
|
|
110
|
+
let lastError;
|
|
111
|
+
const baseUrl =
|
|
112
|
+
process.env.GEMINI_BASE_URL ||
|
|
113
|
+
"https://generativelanguage.googleapis.com/v1beta";
|
|
114
|
+
const url = `${baseUrl}/models/${model}:generateContent?key=${process.env.GEMINI_API_KEY}`;
|
|
115
|
+
|
|
116
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
117
|
+
if (attempt > 0) {
|
|
118
|
+
await sleep(2 ** attempt * 1000); // Exponential backoff
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
console.log(
|
|
123
|
+
`[Gemini] Starting geminiChat call (attempt ${attempt + 1}/${maxRetries + 1})`
|
|
124
|
+
);
|
|
125
|
+
console.log(`[Gemini] Model: ${model}`);
|
|
126
|
+
console.log(`[Gemini] Response format:`, responseFormat);
|
|
127
|
+
console.log(
|
|
128
|
+
`[Gemini] System instruction length: ${systemInstruction.length}`
|
|
129
|
+
);
|
|
130
|
+
console.log(`[Gemini] User message length: ${userMsg.length}`);
|
|
131
|
+
|
|
132
|
+
const response = await fetch(url, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(requestBody),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const errorData = await response.json().catch(() => ({}));
|
|
142
|
+
const error = new Error(
|
|
143
|
+
errorData.error?.message || `Gemini API error: ${response.statusText}`
|
|
144
|
+
);
|
|
145
|
+
error.status = response.status;
|
|
146
|
+
error.data = errorData;
|
|
147
|
+
|
|
148
|
+
// Don't retry on authentication errors
|
|
149
|
+
if (response.status === 401) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Retry on retryable errors
|
|
154
|
+
if (isRetryableError(error) && attempt < maxRetries) {
|
|
155
|
+
console.log(`[Gemini] Retryable error, retrying...`);
|
|
156
|
+
lastError = error;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const data = await response.json();
|
|
164
|
+
console.log(
|
|
165
|
+
`[Gemini] Response received, candidates length: ${data.candidates?.length || 0}`
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Extract text from response
|
|
169
|
+
const candidate = data.candidates?.[0];
|
|
170
|
+
if (!candidate?.content?.parts?.[0]?.text) {
|
|
171
|
+
throw new Error("No content returned from Gemini API");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const text = candidate.content.parts[0].text;
|
|
175
|
+
console.log(`[Gemini] Text length: ${text.length}`);
|
|
176
|
+
|
|
177
|
+
// Parse JSON if required
|
|
178
|
+
const parsed = tryParseJSON(text);
|
|
179
|
+
if (responseFormat && !parsed) {
|
|
180
|
+
throw new ProviderJsonParseError(
|
|
181
|
+
"Gemini",
|
|
182
|
+
model,
|
|
183
|
+
text.substring(0, 200),
|
|
184
|
+
"Failed to parse JSON response from Gemini API"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Normalize usage metrics
|
|
189
|
+
const usage = data.usageMetadata
|
|
190
|
+
? {
|
|
191
|
+
prompt_tokens: data.usageMetadata.promptTokenCount,
|
|
192
|
+
completion_tokens: data.usageMetadata.candidatesTokenCount,
|
|
193
|
+
total_tokens: data.usageMetadata.totalTokenCount,
|
|
194
|
+
}
|
|
195
|
+
: undefined;
|
|
196
|
+
|
|
197
|
+
console.log(`[Gemini] Usage:`, usage);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
content: parsed || text,
|
|
201
|
+
text,
|
|
202
|
+
...(usage ? { usage } : {}),
|
|
203
|
+
raw: data,
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(`[Gemini] Error occurred: ${error.message}`);
|
|
207
|
+
console.error(`[Gemini] Error status: ${error.status}`);
|
|
208
|
+
|
|
209
|
+
lastError = error;
|
|
210
|
+
|
|
211
|
+
// Don't retry on authentication errors
|
|
212
|
+
if (error.status === 401) {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Continue retrying for other errors
|
|
217
|
+
if (attempt < maxRetries) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
throw lastError;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw lastError;
|
|
226
|
+
}
|
package/src/providers/openai.js
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
isRetryableError,
|
|
5
5
|
sleep,
|
|
6
6
|
tryParseJSON,
|
|
7
|
+
ensureJsonResponseFormat,
|
|
8
|
+
ProviderJsonParseError,
|
|
7
9
|
} from "./base.js";
|
|
8
10
|
|
|
9
11
|
let client = null;
|
|
@@ -31,9 +33,7 @@ export async function openaiChat({
|
|
|
31
33
|
temperature,
|
|
32
34
|
maxTokens,
|
|
33
35
|
max_tokens, // Explicitly destructure to prevent it from being in ...rest
|
|
34
|
-
responseFormat,
|
|
35
|
-
tools,
|
|
36
|
-
toolChoice,
|
|
36
|
+
responseFormat,
|
|
37
37
|
seed,
|
|
38
38
|
stop,
|
|
39
39
|
topP,
|
|
@@ -46,6 +46,9 @@ export async function openaiChat({
|
|
|
46
46
|
console.log("[OpenAI] Model:", model);
|
|
47
47
|
console.log("[OpenAI] Response format:", responseFormat);
|
|
48
48
|
|
|
49
|
+
// Enforce JSON mode - reject calls without proper JSON responseFormat
|
|
50
|
+
ensureJsonResponseFormat(responseFormat, "OpenAI");
|
|
51
|
+
|
|
49
52
|
const openai = getClient();
|
|
50
53
|
if (!openai) throw new Error("OpenAI API key not configured");
|
|
51
54
|
|
|
@@ -106,22 +109,19 @@ export async function openaiChat({
|
|
|
106
109
|
total_tokens: promptTokens + completionTokens,
|
|
107
110
|
};
|
|
108
111
|
|
|
109
|
-
// Parse JSON
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
lastError = new Error("Failed to parse JSON response");
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
112
|
+
// Parse JSON - this is now required for all calls
|
|
113
|
+
const parsed = tryParseJSON(text);
|
|
114
|
+
if (!parsed) {
|
|
115
|
+
throw new ProviderJsonParseError(
|
|
116
|
+
"OpenAI",
|
|
117
|
+
model,
|
|
118
|
+
text.substring(0, 200),
|
|
119
|
+
"Failed to parse JSON response from Responses API"
|
|
120
|
+
);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
console.log("[OpenAI] Returning response from Responses API");
|
|
124
|
-
return { content: parsed
|
|
124
|
+
return { content: parsed, text, usage, raw: resp };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// ---------- CLASSIC CHAT COMPLETIONS path (non-GPT-5) ----------
|
|
@@ -136,8 +136,6 @@ export async function openaiChat({
|
|
|
136
136
|
presence_penalty: presencePenalty,
|
|
137
137
|
seed,
|
|
138
138
|
stop,
|
|
139
|
-
tools,
|
|
140
|
-
tool_choice: toolChoice,
|
|
141
139
|
stream: false,
|
|
142
140
|
};
|
|
143
141
|
|
|
@@ -158,31 +156,19 @@ export async function openaiChat({
|
|
|
158
156
|
classicText.length
|
|
159
157
|
);
|
|
160
158
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
let classicParsed = null;
|
|
172
|
-
if (
|
|
173
|
-
responseFormat?.json_schema ||
|
|
174
|
-
responseFormat?.type === "json_object" ||
|
|
175
|
-
responseFormat === "json"
|
|
176
|
-
) {
|
|
177
|
-
classicParsed = tryParseJSON(classicText);
|
|
178
|
-
if (!classicParsed && attempt < maxRetries) {
|
|
179
|
-
lastError = new Error("Failed to parse JSON response");
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
159
|
+
// Parse JSON - this is now required for all calls
|
|
160
|
+
const classicParsed = tryParseJSON(classicText);
|
|
161
|
+
if (!classicParsed) {
|
|
162
|
+
throw new ProviderJsonParseError(
|
|
163
|
+
"OpenAI",
|
|
164
|
+
model,
|
|
165
|
+
classicText.substring(0, 200),
|
|
166
|
+
"Failed to parse JSON response from Classic API"
|
|
167
|
+
);
|
|
182
168
|
}
|
|
183
169
|
|
|
184
170
|
return {
|
|
185
|
-
content: classicParsed
|
|
171
|
+
content: classicParsed,
|
|
186
172
|
text: classicText,
|
|
187
173
|
usage: classicRes?.usage,
|
|
188
174
|
raw: classicRes,
|
|
@@ -211,8 +197,6 @@ export async function openaiChat({
|
|
|
211
197
|
presence_penalty: presencePenalty,
|
|
212
198
|
seed,
|
|
213
199
|
stop,
|
|
214
|
-
tools,
|
|
215
|
-
tool_choice: toolChoice,
|
|
216
200
|
stream: false,
|
|
217
201
|
};
|
|
218
202
|
|
|
@@ -227,17 +211,19 @@ export async function openaiChat({
|
|
|
227
211
|
const classicRes = await openai.chat.completions.create(classicReq);
|
|
228
212
|
const text = classicRes?.choices?.[0]?.message?.content ?? "";
|
|
229
213
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
214
|
+
// Parse JSON - this is now required for all calls
|
|
215
|
+
const parsed = tryParseJSON(text);
|
|
216
|
+
if (!parsed) {
|
|
217
|
+
throw new ProviderJsonParseError(
|
|
218
|
+
"OpenAI",
|
|
219
|
+
model,
|
|
220
|
+
text.substring(0, 200),
|
|
221
|
+
"Failed to parse JSON response from fallback Classic API"
|
|
222
|
+
);
|
|
237
223
|
}
|
|
238
224
|
|
|
239
225
|
return {
|
|
240
|
-
content: parsed
|
|
226
|
+
content: parsed,
|
|
241
227
|
text,
|
|
242
228
|
usage: classicRes?.usage,
|
|
243
229
|
raw: classicRes,
|
|
@@ -256,59 +242,3 @@ export async function openaiChat({
|
|
|
256
242
|
|
|
257
243
|
throw lastError || new Error(`Failed after ${maxRetries + 1} attempts`);
|
|
258
244
|
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Convenience helper used widely in the codebase.
|
|
262
|
-
* For tests, this hits the Responses API directly (even for non-GPT-5).
|
|
263
|
-
* Always attempts to coerce JSON on return (falls back to string).
|
|
264
|
-
*/
|
|
265
|
-
export async function queryChatGPT(system, prompt, options = {}) {
|
|
266
|
-
console.log("\n[OpenAI] Starting queryChatGPT call");
|
|
267
|
-
console.log("[OpenAI] Model:", options.model || "gpt-5-chat-latest");
|
|
268
|
-
|
|
269
|
-
const openai = getClient();
|
|
270
|
-
if (!openai) throw new Error("OpenAI API key not configured");
|
|
271
|
-
|
|
272
|
-
const { systemMsg, userMsg } = extractMessages([
|
|
273
|
-
{ role: "system", content: system },
|
|
274
|
-
{ role: "user", content: prompt },
|
|
275
|
-
]);
|
|
276
|
-
console.log("[OpenAI] System message length:", systemMsg.length);
|
|
277
|
-
console.log("[OpenAI] User message length:", userMsg.length);
|
|
278
|
-
|
|
279
|
-
const req = {
|
|
280
|
-
model: options.model || "gpt-5-chat-latest",
|
|
281
|
-
instructions: systemMsg,
|
|
282
|
-
input: userMsg,
|
|
283
|
-
max_output_tokens: options.maxTokens ?? 25000,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
// Note: Responses API does not support temperature, top_p, frequency_penalty,
|
|
287
|
-
// presence_penalty, seed, or stop parameters. These are only for Chat Completions API.
|
|
288
|
-
|
|
289
|
-
// Response format / schema mapping for Responses API
|
|
290
|
-
if (options.schema) {
|
|
291
|
-
req.text = {
|
|
292
|
-
format: {
|
|
293
|
-
type: "json_schema",
|
|
294
|
-
name: options.schemaName || "Response",
|
|
295
|
-
schema: options.schema,
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
} else if (
|
|
299
|
-
options.response_format?.type === "json_object" ||
|
|
300
|
-
options.response_format === "json"
|
|
301
|
-
) {
|
|
302
|
-
req.text = { format: { type: "json_object" } };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
console.log("[OpenAI] Calling responses.create...");
|
|
306
|
-
const resp = await openai.responses.create(req);
|
|
307
|
-
const text = resp.output_text ?? "";
|
|
308
|
-
console.log("[OpenAI] Response received, text length:", text.length);
|
|
309
|
-
|
|
310
|
-
// Always try to parse JSON; fall back to string
|
|
311
|
-
const parsed = tryParseJSON(text);
|
|
312
|
-
console.log("[OpenAI] Parsed result:", parsed ? "JSON" : "text");
|
|
313
|
-
return parsed ?? text;
|
|
314
|
-
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractMessages,
|
|
3
|
+
isRetryableError,
|
|
4
|
+
sleep,
|
|
5
|
+
tryParseJSON,
|
|
6
|
+
ensureJsonResponseFormat,
|
|
7
|
+
ProviderJsonParseError,
|
|
8
|
+
} from "./base.js";
|
|
9
|
+
|
|
10
|
+
export async function zhipuChat({
|
|
11
|
+
messages,
|
|
12
|
+
model = "glm-4-plus",
|
|
13
|
+
temperature = 0.7,
|
|
14
|
+
maxTokens = 8192,
|
|
15
|
+
responseFormat = "json",
|
|
16
|
+
topP,
|
|
17
|
+
stop,
|
|
18
|
+
maxRetries = 3,
|
|
19
|
+
}) {
|
|
20
|
+
console.log("\n[Zhipu] Starting zhipuChat call");
|
|
21
|
+
console.log("[Zhipu] Model:", model);
|
|
22
|
+
console.log("[Zhipu] Response format:", responseFormat);
|
|
23
|
+
|
|
24
|
+
// Enforce JSON mode - reject calls without proper JSON responseFormat
|
|
25
|
+
ensureJsonResponseFormat(responseFormat, "Zhipu");
|
|
26
|
+
|
|
27
|
+
if (!process.env.ZHIPU_API_KEY) {
|
|
28
|
+
throw new Error("Zhipu API key not configured");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { systemMsg, userMsg } = extractMessages(messages);
|
|
32
|
+
console.log("[Zhipu] System message length:", systemMsg.length);
|
|
33
|
+
console.log("[Zhipu] User message length:", userMsg.length);
|
|
34
|
+
|
|
35
|
+
// Build system guard for JSON enforcement
|
|
36
|
+
let system = systemMsg;
|
|
37
|
+
|
|
38
|
+
if (responseFormat === "json" || responseFormat?.type === "json_object") {
|
|
39
|
+
system = `${systemMsg}\n\nYou must output strict JSON only with no extra text.`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (responseFormat?.json_schema) {
|
|
43
|
+
system = `${systemMsg}\n\nYou must output strict JSON only matching this schema (no extra text):\n${JSON.stringify(responseFormat.json_schema)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let lastError;
|
|
47
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
48
|
+
if (attempt > 0) {
|
|
49
|
+
await sleep(Math.pow(2, attempt) * 1000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
console.log(`[Zhipu] Attempt ${attempt + 1}/${maxRetries + 1}`);
|
|
54
|
+
|
|
55
|
+
const requestBody = {
|
|
56
|
+
model,
|
|
57
|
+
messages: [
|
|
58
|
+
...(system ? [{ role: "system", content: system }] : []),
|
|
59
|
+
{ role: "user", content: userMsg },
|
|
60
|
+
],
|
|
61
|
+
temperature,
|
|
62
|
+
max_tokens: maxTokens,
|
|
63
|
+
...(topP !== undefined ? { top_p: topP } : {}),
|
|
64
|
+
...(stop !== undefined ? { stop: stop } : {}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
console.log("[Zhipu] Calling Zhipu API...");
|
|
68
|
+
const response = await fetch("https://api.z.ai/api/coding/paas/v4", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
Authorization: `Bearer ${process.env.ZHIPU_API_KEY}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(requestBody),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const error = await response
|
|
79
|
+
.json()
|
|
80
|
+
.catch(() => ({ error: response.statusText }));
|
|
81
|
+
throw { status: response.status, ...error };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
console.log("[Zhipu] Response received from Zhipu API");
|
|
86
|
+
|
|
87
|
+
// Extract text from response
|
|
88
|
+
const text = data?.choices?.[0]?.message?.content || "";
|
|
89
|
+
console.log("[Zhipu] Response text length:", text.length);
|
|
90
|
+
|
|
91
|
+
// Parse JSON - this is required for all calls
|
|
92
|
+
const parsed = tryParseJSON(text);
|
|
93
|
+
if (!parsed) {
|
|
94
|
+
throw new ProviderJsonParseError(
|
|
95
|
+
"Zhipu",
|
|
96
|
+
model,
|
|
97
|
+
text.substring(0, 200),
|
|
98
|
+
"Failed to parse JSON response from Zhipu API"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Normalize usage (if provided)
|
|
103
|
+
const prompt_tokens = data?.usage?.prompt_tokens;
|
|
104
|
+
const completion_tokens = data?.usage?.completion_tokens;
|
|
105
|
+
const total_tokens = (prompt_tokens ?? 0) + (completion_tokens ?? 0);
|
|
106
|
+
const usage =
|
|
107
|
+
prompt_tokens != null && completion_tokens != null
|
|
108
|
+
? { prompt_tokens, completion_tokens, total_tokens }
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
111
|
+
console.log("[Zhipu] Returning response from Zhipu API");
|
|
112
|
+
return {
|
|
113
|
+
content: parsed,
|
|
114
|
+
text,
|
|
115
|
+
...(usage ? { usage } : {}),
|
|
116
|
+
raw: data,
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
lastError = error;
|
|
120
|
+
const msg = error?.error?.message || error?.message || "";
|
|
121
|
+
console.error("[Zhipu] Error occurred:", msg);
|
|
122
|
+
console.error("[Zhipu] Error status:", error?.status);
|
|
123
|
+
|
|
124
|
+
if (error.status === 401) throw error;
|
|
125
|
+
|
|
126
|
+
if (isRetryableError(error) && attempt < maxRetries) {
|
|
127
|
+
console.log("[Zhipu] Retrying due to retryable error");
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (attempt === maxRetries) throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw lastError || new Error(`Failed after ${maxRetries + 1} attempts`);
|
|
136
|
+
}
|