@pi-unipi/unipi 2.2.3 → 2.2.4
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
CHANGED
|
@@ -100,6 +100,7 @@ async function callAnthropic(options: RecognizeOptions): Promise<string> {
|
|
|
100
100
|
body: JSON.stringify({
|
|
101
101
|
model: modelId,
|
|
102
102
|
max_tokens: maxTokens,
|
|
103
|
+
stream: false,
|
|
103
104
|
system: systemPrompt,
|
|
104
105
|
messages: [
|
|
105
106
|
{
|
|
@@ -123,11 +124,14 @@ async function callAnthropic(options: RecognizeOptions): Promise<string> {
|
|
|
123
124
|
|
|
124
125
|
if (!response.ok) throw new Error(await describeHttpError(response));
|
|
125
126
|
|
|
126
|
-
const data =
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
const data = await readJsonOrStream(response);
|
|
128
|
+
|
|
129
|
+
// A streamed response arrives as deltas rather than a `content` array.
|
|
130
|
+
const streamed = collectStreamedText(data);
|
|
131
|
+
if (streamed !== null) return streamed;
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
const typed = data as { content?: Array<{ type?: string; text?: string }> };
|
|
134
|
+
return (typed.content ?? [])
|
|
131
135
|
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
132
136
|
.map((block) => block.text as string)
|
|
133
137
|
.join("\n")
|
|
@@ -137,6 +141,79 @@ async function callAnthropic(options: RecognizeOptions): Promise<string> {
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Read a response body as JSON, tolerating a Server-Sent Events stream.
|
|
146
|
+
*
|
|
147
|
+
* Some OpenAI-compatible gateways (omniroute, for one) reply with
|
|
148
|
+
* `text/event-stream` even when streaming was never requested. Calling
|
|
149
|
+
* `response.json()` on that throws `Unexpected token 'd', "data: {"id"...`,
|
|
150
|
+
* which tells the user nothing. Parse the SSE frames instead and hand back a
|
|
151
|
+
* synthetic payload carrying the concatenated deltas.
|
|
152
|
+
*/
|
|
153
|
+
async function readJsonOrStream(response: Response): Promise<unknown> {
|
|
154
|
+
const body = await response.text();
|
|
155
|
+
const trimmed = body.trimStart();
|
|
156
|
+
|
|
157
|
+
if (!trimmed.startsWith("data:")) {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(body) as unknown;
|
|
160
|
+
} catch {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`The model returned a response that could not be parsed:\n${body.slice(0, 200)}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const parts: string[] = [];
|
|
168
|
+
for (const line of body.split(/\r?\n/)) {
|
|
169
|
+
if (!line.startsWith("data:")) continue;
|
|
170
|
+
const payload = line.slice(5).trim();
|
|
171
|
+
if (!payload || payload === "[DONE]") continue;
|
|
172
|
+
|
|
173
|
+
let frame: unknown;
|
|
174
|
+
try {
|
|
175
|
+
frame = JSON.parse(payload);
|
|
176
|
+
} catch {
|
|
177
|
+
continue; // Ignore a partial or malformed frame rather than failing.
|
|
178
|
+
}
|
|
179
|
+
parts.push(...extractDeltaText(frame));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { __streamedText: parts.join("") };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Pull text out of one SSE frame, in both OpenAI and Anthropic shapes. */
|
|
186
|
+
function extractDeltaText(frame: unknown): string[] {
|
|
187
|
+
if (frame === null || typeof frame !== "object") return [];
|
|
188
|
+
const out: string[] = [];
|
|
189
|
+
|
|
190
|
+
// OpenAI: choices[].delta.content (or a non-streamed message.content)
|
|
191
|
+
const choices = (frame as { choices?: unknown }).choices;
|
|
192
|
+
if (Array.isArray(choices)) {
|
|
193
|
+
for (const choice of choices) {
|
|
194
|
+
if (choice === null || typeof choice !== "object") continue;
|
|
195
|
+
const delta = (choice as { delta?: { content?: unknown } }).delta;
|
|
196
|
+
if (typeof delta?.content === "string") out.push(delta.content);
|
|
197
|
+
const message = (choice as { message?: { content?: unknown } }).message;
|
|
198
|
+
if (typeof message?.content === "string") out.push(message.content);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Anthropic: content_block_delta → delta.text
|
|
203
|
+
const delta = (frame as { delta?: { text?: unknown } }).delta;
|
|
204
|
+
if (typeof delta?.text === "string") out.push(delta.text);
|
|
205
|
+
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Text collected from a streamed body, or null when it was ordinary JSON. */
|
|
210
|
+
function collectStreamedText(data: unknown): string | null {
|
|
211
|
+
if (data === null || typeof data !== "object") return null;
|
|
212
|
+
const streamed = (data as { __streamedText?: unknown }).__streamedText;
|
|
213
|
+
if (typeof streamed !== "string") return null;
|
|
214
|
+
return streamed.trim();
|
|
215
|
+
}
|
|
216
|
+
|
|
140
217
|
/** OpenAI-compatible chat completions — image parts use a data: URL. */
|
|
141
218
|
async function callOpenAICompatible(options: RecognizeOptions): Promise<string> {
|
|
142
219
|
const {
|
|
@@ -159,6 +236,9 @@ async function callOpenAICompatible(options: RecognizeOptions): Promise<string>
|
|
|
159
236
|
body: JSON.stringify({
|
|
160
237
|
model: modelId,
|
|
161
238
|
max_tokens: maxTokens,
|
|
239
|
+
// Ask for a single payload. Gateways may stream regardless, which
|
|
240
|
+
// readJsonOrStream handles.
|
|
241
|
+
stream: false,
|
|
162
242
|
messages: [
|
|
163
243
|
{ role: "system", content: systemPrompt },
|
|
164
244
|
{
|
|
@@ -181,11 +261,16 @@ async function callOpenAICompatible(options: RecognizeOptions): Promise<string>
|
|
|
181
261
|
|
|
182
262
|
if (!response.ok) throw new Error(await describeHttpError(response));
|
|
183
263
|
|
|
184
|
-
const data =
|
|
264
|
+
const data = await readJsonOrStream(response);
|
|
265
|
+
|
|
266
|
+
const streamed = collectStreamedText(data);
|
|
267
|
+
if (streamed !== null) return streamed;
|
|
268
|
+
|
|
269
|
+
const typed = data as {
|
|
185
270
|
choices?: Array<{ message?: { content?: string | Array<{ text?: string }> } }>;
|
|
186
271
|
};
|
|
187
272
|
|
|
188
|
-
const content =
|
|
273
|
+
const content = typed.choices?.[0]?.message?.content;
|
|
189
274
|
if (typeof content === "string") return content.trim();
|
|
190
275
|
if (Array.isArray(content)) {
|
|
191
276
|
return content.map((part) => part?.text ?? "").join("").trim();
|