@juspay/neurolink 9.67.0 → 9.67.2
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/CHANGELOG.md +4 -0
- package/dist/browser/neurolink.min.js +376 -370
- package/dist/lib/providers/googleVertex.js +8 -7
- package/dist/lib/providers/litellm.d.ts +31 -24
- package/dist/lib/providers/litellm.js +590 -391
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +67 -0
- package/dist/lib/providers/openaiChatCompletionsClient.js +526 -0
- package/dist/lib/providers/openaiCompatible.d.ts +46 -19
- package/dist/lib/providers/openaiCompatible.js +559 -171
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/middleware.d.ts +1 -1
- package/dist/lib/types/openaiCompatible.d.ts +250 -0
- package/dist/lib/types/openaiCompatible.js +2 -0
- package/dist/lib/types/providers.d.ts +2 -0
- package/dist/providers/googleVertex.js +8 -7
- package/dist/providers/litellm.d.ts +31 -24
- package/dist/providers/litellm.js +590 -391
- package/dist/providers/openaiChatCompletionsClient.d.ts +67 -0
- package/dist/providers/openaiChatCompletionsClient.js +525 -0
- package/dist/providers/openaiCompatible.d.ts +46 -19
- package/dist/providers/openaiCompatible.js +559 -171
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/middleware.d.ts +1 -1
- package/dist/types/openaiCompatible.d.ts +250 -0
- package/dist/types/openaiCompatible.js +1 -0
- package/dist/types/providers.d.ts +2 -0
- package/package.json +2 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OpenAI chat-completions wire-format helpers used by providers that
|
|
3
|
+
* talk to an OpenAI-shaped /chat/completions endpoint (openai-compatible,
|
|
4
|
+
* litellm, groq, perplexity, xai, fireworks, togetherAi, cohere, cloudflare,
|
|
5
|
+
* huggingFace, llamaCpp, lmStudio, deepseek, nvidiaNim, and openAI itself).
|
|
6
|
+
*
|
|
7
|
+
* Everything in this module is provider-agnostic: pure functions that convert
|
|
8
|
+
* between NeuroLink-shaped values and the OpenAI wire format, plus the SSE
|
|
9
|
+
* parser + queue primitives a streaming provider needs. Provider classes own
|
|
10
|
+
* their own orchestration (executeStream + runStreamLoop) for now — that
|
|
11
|
+
* extraction is a follow-up PR.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here imports from "ai" or "@ai-sdk/*". The whole point of this
|
|
14
|
+
* module is to be the native replacement for the AI SDK's OpenAI wrapper.
|
|
15
|
+
*/
|
|
16
|
+
import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, Tool } from "../types/index.js";
|
|
17
|
+
export declare const stripTrailingSlash: (s: string) => string;
|
|
18
|
+
export declare const safeStringify: (value: unknown) => string;
|
|
19
|
+
export declare const stringifyToolInput: (input: unknown) => string;
|
|
20
|
+
export declare const stringifyToolOutput: (output: unknown) => string;
|
|
21
|
+
export declare const imageDataToURL: (data: unknown) => string | undefined;
|
|
22
|
+
export declare const convertContentForOpenAI: (content: unknown) => string | OpenAICompatMessageContent[];
|
|
23
|
+
export declare const messageBuilderToOpenAI: (messages: ReadonlyArray<OpenAICompatMessage>) => OpenAICompatChatMessage[];
|
|
24
|
+
export declare const buildToolsForOpenAI: (tools: Record<string, Tool>) => OpenAICompatChatTool[] | undefined;
|
|
25
|
+
export declare const v3ToolsToOpenAI: (tools: OpenAICompatV3CallTools | undefined) => OpenAICompatChatTool[] | undefined;
|
|
26
|
+
export declare const v3ToolChoiceToOpenAI: (choice: OpenAICompatV3CallToolChoice) => OpenAICompatToolChoiceWire | undefined;
|
|
27
|
+
export declare const v3ResponseFormatToOpenAI: (rf: {
|
|
28
|
+
type: "text" | "json";
|
|
29
|
+
schema?: Record<string, unknown>;
|
|
30
|
+
name?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
}) => OpenAICompatResponseFormat | undefined;
|
|
33
|
+
export declare const mapNeuroLinkToolChoice: (choice: unknown) => OpenAICompatToolChoiceWire | undefined;
|
|
34
|
+
export declare const buildBody: (args: OpenAICompatBuildBodyArgs) => OpenAICompatChatRequest;
|
|
35
|
+
export declare const parseSSEStream: (body: ReadableStream<Uint8Array>, onTextDelta: (delta: string) => void) => Promise<OpenAICompatSSEResult>;
|
|
36
|
+
export declare const buildAPIError: (url: string, body: OpenAICompatChatRequest, res: Response) => Promise<Error>;
|
|
37
|
+
export declare const createDeferredAnalytics: () => {
|
|
38
|
+
usagePromise: Promise<{
|
|
39
|
+
promptTokens: number;
|
|
40
|
+
completionTokens: number;
|
|
41
|
+
totalTokens: number;
|
|
42
|
+
}>;
|
|
43
|
+
finishPromise: Promise<string>;
|
|
44
|
+
resolveUsage: (u: {
|
|
45
|
+
promptTokens: number;
|
|
46
|
+
completionTokens: number;
|
|
47
|
+
totalTokens: number;
|
|
48
|
+
}) => void;
|
|
49
|
+
resolveFinish: (reason: string) => void;
|
|
50
|
+
};
|
|
51
|
+
export declare const createChunkQueue: () => {
|
|
52
|
+
pushChunk: (c: OpenAICompatStreamChunk) => void;
|
|
53
|
+
nextChunk: () => Promise<OpenAICompatStreamChunk>;
|
|
54
|
+
};
|
|
55
|
+
export declare const mergeUsage: (a: {
|
|
56
|
+
prompt_tokens?: number;
|
|
57
|
+
completion_tokens?: number;
|
|
58
|
+
total_tokens?: number;
|
|
59
|
+
} | undefined, b: {
|
|
60
|
+
prompt_tokens?: number;
|
|
61
|
+
completion_tokens?: number;
|
|
62
|
+
total_tokens?: number;
|
|
63
|
+
} | undefined) => {
|
|
64
|
+
prompt_tokens?: number;
|
|
65
|
+
completion_tokens?: number;
|
|
66
|
+
total_tokens?: number;
|
|
67
|
+
} | undefined;
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OpenAI chat-completions wire-format helpers used by providers that
|
|
3
|
+
* talk to an OpenAI-shaped /chat/completions endpoint (openai-compatible,
|
|
4
|
+
* litellm, groq, perplexity, xai, fireworks, togetherAi, cohere, cloudflare,
|
|
5
|
+
* huggingFace, llamaCpp, lmStudio, deepseek, nvidiaNim, and openAI itself).
|
|
6
|
+
*
|
|
7
|
+
* Everything in this module is provider-agnostic: pure functions that convert
|
|
8
|
+
* between NeuroLink-shaped values and the OpenAI wire format, plus the SSE
|
|
9
|
+
* parser + queue primitives a streaming provider needs. Provider classes own
|
|
10
|
+
* their own orchestration (executeStream + runStreamLoop) for now — that
|
|
11
|
+
* extraction is a follow-up PR.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here imports from "ai" or "@ai-sdk/*". The whole point of this
|
|
14
|
+
* module is to be the native replacement for the AI SDK's OpenAI wrapper.
|
|
15
|
+
*/
|
|
16
|
+
import { createParser } from "eventsource-parser";
|
|
17
|
+
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
18
|
+
export const stripTrailingSlash = (s) => s.replace(/\/+$/, "");
|
|
19
|
+
export const safeStringify = (value) => {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(value ?? "");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return String(value ?? "");
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const stringifyToolInput = (input) => {
|
|
28
|
+
if (typeof input === "string") {
|
|
29
|
+
return input;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.stringify(input ?? {});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "{}";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
// V3 tool-result `output` is a tagged union ({type:"text"|"json"|...}).
|
|
39
|
+
// Serialize each variant the way an OpenAI-compatible endpoint expects
|
|
40
|
+
// to read it as the `content` of a `role: "tool"` message.
|
|
41
|
+
export const stringifyToolOutput = (output) => {
|
|
42
|
+
if (output === null || output === undefined) {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
if (typeof output === "string") {
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
if (typeof output !== "object") {
|
|
49
|
+
return String(output);
|
|
50
|
+
}
|
|
51
|
+
const o = output;
|
|
52
|
+
switch (o.type) {
|
|
53
|
+
case "text":
|
|
54
|
+
return typeof o.value === "string" ? o.value : safeStringify(o.value);
|
|
55
|
+
case "json":
|
|
56
|
+
return safeStringify(o.value);
|
|
57
|
+
case "execution-denied":
|
|
58
|
+
return `Tool execution denied${o.reason ? `: ${o.reason}` : ""}`;
|
|
59
|
+
case "error-text":
|
|
60
|
+
return typeof o.value === "string" ? o.value : safeStringify(o.value);
|
|
61
|
+
case "error-json":
|
|
62
|
+
return safeStringify(o.value);
|
|
63
|
+
case "content":
|
|
64
|
+
if (Array.isArray(o.value)) {
|
|
65
|
+
return o.value
|
|
66
|
+
.map((p) => {
|
|
67
|
+
if (p &&
|
|
68
|
+
typeof p === "object" &&
|
|
69
|
+
p.type === "text") {
|
|
70
|
+
return String(p.text ?? "");
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
})
|
|
74
|
+
.filter((s) => s.length > 0)
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
default:
|
|
79
|
+
return safeStringify(output);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export const imageDataToURL = (data) => {
|
|
83
|
+
if (typeof data === "string") {
|
|
84
|
+
if (data.startsWith("data:") || /^https?:\/\//i.test(data)) {
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
return `data:image/png;base64,${data}`;
|
|
88
|
+
}
|
|
89
|
+
if (data instanceof URL) {
|
|
90
|
+
return data.toString();
|
|
91
|
+
}
|
|
92
|
+
if (data instanceof Uint8Array) {
|
|
93
|
+
return `data:image/png;base64,${Buffer.from(data).toString("base64")}`;
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
};
|
|
97
|
+
export const convertContentForOpenAI = (content) => {
|
|
98
|
+
if (typeof content === "string") {
|
|
99
|
+
return content;
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(content)) {
|
|
102
|
+
return safeStringify(content);
|
|
103
|
+
}
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const part of content) {
|
|
106
|
+
if (typeof part === "string") {
|
|
107
|
+
out.push({ type: "text", text: part });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!part || typeof part !== "object") {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const p = part;
|
|
114
|
+
if (p.type === "text") {
|
|
115
|
+
out.push({
|
|
116
|
+
type: "text",
|
|
117
|
+
text: part.text ?? "",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else if (p.type === "image" || p.type === "image_url") {
|
|
121
|
+
const data = part.image ??
|
|
122
|
+
part.data ??
|
|
123
|
+
part.url;
|
|
124
|
+
const url = imageDataToURL(data);
|
|
125
|
+
if (url) {
|
|
126
|
+
out.push({ type: "image_url", image_url: { url } });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (out.length === 1 && out[0].type === "text") {
|
|
131
|
+
return out[0].text;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
export const messageBuilderToOpenAI = (messages) => {
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const msg of messages) {
|
|
138
|
+
switch (msg.role) {
|
|
139
|
+
case "system":
|
|
140
|
+
out.push({
|
|
141
|
+
role: "system",
|
|
142
|
+
content: typeof msg.content === "string"
|
|
143
|
+
? msg.content
|
|
144
|
+
: safeStringify(msg.content),
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
case "user":
|
|
148
|
+
out.push({
|
|
149
|
+
role: "user",
|
|
150
|
+
content: convertContentForOpenAI(msg.content),
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
case "assistant": {
|
|
154
|
+
const parts = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
155
|
+
const text = [];
|
|
156
|
+
const toolCalls = [];
|
|
157
|
+
for (const part of parts) {
|
|
158
|
+
if (part && typeof part === "object") {
|
|
159
|
+
const p = part;
|
|
160
|
+
if (p.type === "text") {
|
|
161
|
+
text.push({
|
|
162
|
+
type: "text",
|
|
163
|
+
text: part.text ?? "",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else if (p.type === "tool-call") {
|
|
167
|
+
const tc = part;
|
|
168
|
+
toolCalls.push({
|
|
169
|
+
id: tc.toolCallId ?? "",
|
|
170
|
+
type: "function",
|
|
171
|
+
function: {
|
|
172
|
+
name: tc.toolName ?? "",
|
|
173
|
+
arguments: stringifyToolInput(tc.input),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (typeof part === "string") {
|
|
179
|
+
text.push({ type: "text", text: part });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const flat = text.length === 0
|
|
183
|
+
? null
|
|
184
|
+
: text.length === 1 && text[0].type === "text"
|
|
185
|
+
? text[0].text
|
|
186
|
+
: text;
|
|
187
|
+
out.push({
|
|
188
|
+
role: "assistant",
|
|
189
|
+
content: flat,
|
|
190
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
191
|
+
});
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "tool": {
|
|
195
|
+
// V3 tool messages carry `{ toolCallId, output }` per content[] entry,
|
|
196
|
+
// not at the top-level. Emit one OpenAI `role: "tool"` message per
|
|
197
|
+
// tool-result part so the model can correlate by tool_call_id.
|
|
198
|
+
if (Array.isArray(msg.content)) {
|
|
199
|
+
for (const part of msg.content) {
|
|
200
|
+
if (!part || typeof part !== "object") {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const p = part;
|
|
204
|
+
if (p.type === "tool-result") {
|
|
205
|
+
out.push({
|
|
206
|
+
role: "tool",
|
|
207
|
+
tool_call_id: p.toolCallId ?? "",
|
|
208
|
+
content: stringifyToolOutput(p.output),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (typeof msg.content === "string") {
|
|
214
|
+
out.push({
|
|
215
|
+
role: "tool",
|
|
216
|
+
tool_call_id: msg.toolCallId ?? "",
|
|
217
|
+
content: msg.content,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
};
|
|
226
|
+
export const buildToolsForOpenAI = (tools) => {
|
|
227
|
+
const entries = Object.entries(tools);
|
|
228
|
+
if (entries.length === 0) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const out = [];
|
|
232
|
+
for (const [name, tool] of entries) {
|
|
233
|
+
const t = tool;
|
|
234
|
+
const rawSchema = t.inputSchema ?? t.parameters;
|
|
235
|
+
// tool.inputSchema may be a Zod schema, an AI SDK jsonSchema() wrapper,
|
|
236
|
+
// or plain JSON Schema — convertZodToJsonSchema normalizes all three.
|
|
237
|
+
// Sending raw Zod internals (with `_def`) gets rejected by most
|
|
238
|
+
// OpenAI-compatible endpoints.
|
|
239
|
+
const parameters = rawSchema
|
|
240
|
+
? convertZodToJsonSchema(rawSchema)
|
|
241
|
+
: { type: "object", properties: {} };
|
|
242
|
+
out.push({
|
|
243
|
+
type: "function",
|
|
244
|
+
function: {
|
|
245
|
+
name,
|
|
246
|
+
...(t.description ? { description: t.description } : {}),
|
|
247
|
+
parameters,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
};
|
|
253
|
+
// V3 → OpenAI conversion helpers used by the non-streaming `doGenerate`
|
|
254
|
+
// path that BaseProvider's `generate()` still drives via the AI SDK's
|
|
255
|
+
// `generateText`. The streaming path doesn't need these — it consumes
|
|
256
|
+
// NeuroLink-shaped options directly.
|
|
257
|
+
export const v3ToolsToOpenAI = (tools) => {
|
|
258
|
+
if (!tools || tools.length === 0) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const out = [];
|
|
262
|
+
for (const t of tools) {
|
|
263
|
+
if (t.type === "function") {
|
|
264
|
+
out.push({
|
|
265
|
+
type: "function",
|
|
266
|
+
function: {
|
|
267
|
+
name: t.name,
|
|
268
|
+
...(t.description ? { description: t.description } : {}),
|
|
269
|
+
parameters: t.inputSchema,
|
|
270
|
+
...(t.strict !== undefined ? { strict: t.strict } : {}),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// provider-defined V3 tools are silently dropped here — they have no
|
|
275
|
+
// OpenAI chat-completions equivalent.
|
|
276
|
+
}
|
|
277
|
+
return out.length > 0 ? out : undefined;
|
|
278
|
+
};
|
|
279
|
+
export const v3ToolChoiceToOpenAI = (choice) => {
|
|
280
|
+
switch (choice.type) {
|
|
281
|
+
case "auto":
|
|
282
|
+
case "none":
|
|
283
|
+
case "required":
|
|
284
|
+
return choice.type;
|
|
285
|
+
case "tool":
|
|
286
|
+
return { type: "function", function: { name: choice.toolName } };
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
export const v3ResponseFormatToOpenAI = (rf) => {
|
|
290
|
+
if (rf.type === "text") {
|
|
291
|
+
return { type: "text" };
|
|
292
|
+
}
|
|
293
|
+
if (!rf.schema) {
|
|
294
|
+
return { type: "json_object" };
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
type: "json_schema",
|
|
298
|
+
json_schema: {
|
|
299
|
+
name: rf.name ?? "response",
|
|
300
|
+
schema: rf.schema,
|
|
301
|
+
...(rf.description ? { description: rf.description } : {}),
|
|
302
|
+
strict: true,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
export const mapNeuroLinkToolChoice = (choice) => {
|
|
307
|
+
if (!choice) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
if (choice === "auto" || choice === "none" || choice === "required") {
|
|
311
|
+
return choice;
|
|
312
|
+
}
|
|
313
|
+
if (typeof choice === "object" && choice !== null) {
|
|
314
|
+
const c = choice;
|
|
315
|
+
if (c.type === "tool" && c.toolName) {
|
|
316
|
+
return { type: "function", function: { name: c.toolName } };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
};
|
|
321
|
+
export const buildBody = (args) => {
|
|
322
|
+
const { modelId, messages, options, tools, toolChoice, streaming, responseFormat, } = args;
|
|
323
|
+
const body = {
|
|
324
|
+
model: modelId,
|
|
325
|
+
messages,
|
|
326
|
+
...(streaming ? { stream: true } : {}),
|
|
327
|
+
...(streaming ? { stream_options: { include_usage: true } } : {}),
|
|
328
|
+
};
|
|
329
|
+
if (options.maxTokens !== undefined && options.maxTokens !== null) {
|
|
330
|
+
body.max_tokens = options.maxTokens;
|
|
331
|
+
}
|
|
332
|
+
if (options.temperature !== undefined && options.temperature !== null) {
|
|
333
|
+
body.temperature = options.temperature;
|
|
334
|
+
}
|
|
335
|
+
if (options.topP !== undefined && options.topP !== null) {
|
|
336
|
+
body.top_p = options.topP;
|
|
337
|
+
}
|
|
338
|
+
if (options.presencePenalty !== undefined &&
|
|
339
|
+
options.presencePenalty !== null) {
|
|
340
|
+
body.presence_penalty = options.presencePenalty;
|
|
341
|
+
}
|
|
342
|
+
if (options.frequencyPenalty !== undefined &&
|
|
343
|
+
options.frequencyPenalty !== null) {
|
|
344
|
+
body.frequency_penalty = options.frequencyPenalty;
|
|
345
|
+
}
|
|
346
|
+
if (options.seed !== undefined && options.seed !== null) {
|
|
347
|
+
body.seed = options.seed;
|
|
348
|
+
}
|
|
349
|
+
if (options.stopSequences && options.stopSequences.length > 0) {
|
|
350
|
+
body.stop = options.stopSequences;
|
|
351
|
+
}
|
|
352
|
+
if (tools) {
|
|
353
|
+
body.tools = tools;
|
|
354
|
+
}
|
|
355
|
+
if (toolChoice !== undefined) {
|
|
356
|
+
body.tool_choice = toolChoice;
|
|
357
|
+
}
|
|
358
|
+
if (responseFormat) {
|
|
359
|
+
body.response_format = responseFormat;
|
|
360
|
+
}
|
|
361
|
+
return body;
|
|
362
|
+
};
|
|
363
|
+
export const parseSSEStream = async (body, onTextDelta) => {
|
|
364
|
+
const result = {
|
|
365
|
+
text: "",
|
|
366
|
+
toolCalls: new Map(),
|
|
367
|
+
finishReason: null,
|
|
368
|
+
usage: undefined,
|
|
369
|
+
};
|
|
370
|
+
const decoder = new TextDecoder();
|
|
371
|
+
let parseErr;
|
|
372
|
+
const handleEvent = (msg) => {
|
|
373
|
+
const data = msg.data;
|
|
374
|
+
if (!data || data === "[DONE]") {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
let chunk;
|
|
378
|
+
try {
|
|
379
|
+
chunk = JSON.parse(data);
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
parseErr = err instanceof Error ? err : new Error(String(err));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (chunk.usage) {
|
|
386
|
+
result.usage = chunk.usage;
|
|
387
|
+
}
|
|
388
|
+
const choice = chunk.choices?.[0];
|
|
389
|
+
if (!choice) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const delta = choice.delta;
|
|
393
|
+
if (delta?.content) {
|
|
394
|
+
result.text += delta.content;
|
|
395
|
+
onTextDelta(delta.content);
|
|
396
|
+
}
|
|
397
|
+
if (delta?.tool_calls) {
|
|
398
|
+
for (const tc of delta.tool_calls) {
|
|
399
|
+
let state = result.toolCalls.get(tc.index);
|
|
400
|
+
if (!state) {
|
|
401
|
+
state = {
|
|
402
|
+
id: tc.id ?? `call_${tc.index}_${Date.now()}`,
|
|
403
|
+
name: tc.function?.name ?? "",
|
|
404
|
+
argsBuffered: "",
|
|
405
|
+
};
|
|
406
|
+
result.toolCalls.set(tc.index, state);
|
|
407
|
+
}
|
|
408
|
+
else if (tc.id) {
|
|
409
|
+
state.id = tc.id;
|
|
410
|
+
}
|
|
411
|
+
if (tc.function?.name) {
|
|
412
|
+
state.name = tc.function.name;
|
|
413
|
+
}
|
|
414
|
+
if (tc.function?.arguments) {
|
|
415
|
+
state.argsBuffered += tc.function.arguments;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (choice.finish_reason) {
|
|
420
|
+
result.finishReason = choice.finish_reason;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const parser = createParser({ onEvent: handleEvent });
|
|
424
|
+
const reader = body.getReader();
|
|
425
|
+
try {
|
|
426
|
+
for (;;) {
|
|
427
|
+
const { done, value } = await reader.read();
|
|
428
|
+
if (done) {
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
432
|
+
}
|
|
433
|
+
parser.feed(decoder.decode());
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
reader.releaseLock();
|
|
437
|
+
}
|
|
438
|
+
if (parseErr) {
|
|
439
|
+
throw parseErr;
|
|
440
|
+
}
|
|
441
|
+
return result;
|
|
442
|
+
};
|
|
443
|
+
export const buildAPIError = async (url, body, res) => {
|
|
444
|
+
let bodyText;
|
|
445
|
+
let parsed;
|
|
446
|
+
try {
|
|
447
|
+
bodyText = await res.text();
|
|
448
|
+
parsed = bodyText
|
|
449
|
+
? JSON.parse(bodyText)
|
|
450
|
+
: undefined;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
parsed = undefined;
|
|
454
|
+
}
|
|
455
|
+
const msg = parsed?.error?.message ??
|
|
456
|
+
`OpenAI-compatible request failed with status ${res.status}`;
|
|
457
|
+
const err = new Error(msg);
|
|
458
|
+
err.statusCode = res.status;
|
|
459
|
+
err.url = url;
|
|
460
|
+
// Redacted summary only — never attach raw prompts, tool definitions, or
|
|
461
|
+
// tool arguments to the thrown error. Anything serialized by upstream
|
|
462
|
+
// logging would leak them otherwise.
|
|
463
|
+
err.requestBody = {
|
|
464
|
+
model: body.model,
|
|
465
|
+
stream: body.stream === true,
|
|
466
|
+
tool_count: body.tools?.length ?? 0,
|
|
467
|
+
};
|
|
468
|
+
if (bodyText !== undefined) {
|
|
469
|
+
err.responseBody = bodyText;
|
|
470
|
+
}
|
|
471
|
+
return err;
|
|
472
|
+
};
|
|
473
|
+
// Deferred-promise pair for `usage` and `finishReason` so the analytics
|
|
474
|
+
// collector resolves with the actual aggregated values after the multi-step
|
|
475
|
+
// loop ends, not the zeros they had at result-construction time.
|
|
476
|
+
export const createDeferredAnalytics = () => {
|
|
477
|
+
let resolveUsage = () => { };
|
|
478
|
+
const usagePromise = new Promise((r) => {
|
|
479
|
+
resolveUsage = r;
|
|
480
|
+
});
|
|
481
|
+
let resolveFinish = () => { };
|
|
482
|
+
const finishPromise = new Promise((r) => {
|
|
483
|
+
resolveFinish = r;
|
|
484
|
+
});
|
|
485
|
+
return { usagePromise, finishPromise, resolveUsage, resolveFinish };
|
|
486
|
+
};
|
|
487
|
+
// Single-producer / single-consumer chunk queue. The streaming loop pushes
|
|
488
|
+
// `{content}` deltas as they arrive from SSE and a final `{done:true}` when
|
|
489
|
+
// it finishes; the consumer's AsyncIterable pulls from `nextChunk()`.
|
|
490
|
+
export const createChunkQueue = () => {
|
|
491
|
+
const chunkQueue = [];
|
|
492
|
+
let pendingResolve;
|
|
493
|
+
const pushChunk = (c) => {
|
|
494
|
+
if (pendingResolve) {
|
|
495
|
+
const r = pendingResolve;
|
|
496
|
+
pendingResolve = undefined;
|
|
497
|
+
r(c);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
chunkQueue.push(c);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const nextChunk = () => new Promise((resolve) => {
|
|
504
|
+
if (chunkQueue.length > 0) {
|
|
505
|
+
resolve(chunkQueue.shift());
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
pendingResolve = resolve;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
return { pushChunk, nextChunk };
|
|
512
|
+
};
|
|
513
|
+
export const mergeUsage = (a, b) => {
|
|
514
|
+
if (!a) {
|
|
515
|
+
return b;
|
|
516
|
+
}
|
|
517
|
+
if (!b) {
|
|
518
|
+
return a;
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
prompt_tokens: (a.prompt_tokens ?? 0) + (b.prompt_tokens ?? 0),
|
|
522
|
+
completion_tokens: (a.completion_tokens ?? 0) + (b.completion_tokens ?? 0),
|
|
523
|
+
total_tokens: (a.total_tokens ?? 0) + (b.total_tokens ?? 0),
|
|
524
|
+
};
|
|
525
|
+
};
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { AIProviderName } from "../constants/enums.js";
|
|
2
2
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
3
|
-
import type { StreamOptions, StreamResult, ZodUnknownSchema } from "../types/index.js";
|
|
4
|
-
import type { LanguageModel, Schema } from "../types/index.js";
|
|
3
|
+
import type { LanguageModel, Schema, StreamOptions, StreamResult, ZodUnknownSchema } from "../types/index.js";
|
|
5
4
|
/**
|
|
6
|
-
* OpenAI Compatible Provider
|
|
7
|
-
*
|
|
5
|
+
* OpenAI Compatible Provider — direct HTTP, no AI SDK.
|
|
6
|
+
*
|
|
7
|
+
* Talks to any OpenAI chat-completions-shaped endpoint (LiteLLM, vLLM,
|
|
8
|
+
* OpenRouter, etc.). The entire request/stream/tool-loop is inline above;
|
|
9
|
+
* no `streamText`, no `LanguageModelV3`, no `@ai-sdk/openai`.
|
|
8
10
|
*/
|
|
9
11
|
export declare class OpenAICompatibleProvider extends BaseProvider {
|
|
10
|
-
private model?;
|
|
11
12
|
private config;
|
|
13
|
+
private resolvedModel?;
|
|
12
14
|
private discoveredModel?;
|
|
13
|
-
private customOpenAI;
|
|
14
15
|
constructor(modelName?: string, sdk?: unknown, _region?: string, credentials?: {
|
|
15
16
|
apiKey?: string;
|
|
16
17
|
baseURL?: string;
|
|
@@ -18,33 +19,59 @@ export declare class OpenAICompatibleProvider extends BaseProvider {
|
|
|
18
19
|
protected getProviderName(): AIProviderName;
|
|
19
20
|
protected getDefaultModel(): string;
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* Abstract from BaseProvider — used by the parent's generate() path which
|
|
23
|
+
* still goes through `generateText`. Returns a thin LanguageModelV3-shaped
|
|
24
|
+
* object that delegates to the same HTTP helpers used by executeStream.
|
|
25
|
+
* Stays inside this file so no AI-SDK-named import is needed here.
|
|
23
26
|
*/
|
|
24
27
|
protected getAISDKModel(): Promise<LanguageModel>;
|
|
25
|
-
|
|
28
|
+
private resolveModelName;
|
|
26
29
|
/**
|
|
27
|
-
*
|
|
30
|
+
* Returns a minimal V3-shaped model. Only used by BaseProvider's
|
|
31
|
+
* `generate()` non-streaming path which still relies on the parent's
|
|
32
|
+
* `generateText`. The streaming path bypasses this entirely.
|
|
28
33
|
*/
|
|
34
|
+
private buildDelegatingModel;
|
|
35
|
+
protected formatProviderError(error: unknown): Error;
|
|
29
36
|
supportsTools(): boolean;
|
|
30
37
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
38
|
+
* Streaming path — drives the OpenAI endpoint directly. No streamText,
|
|
39
|
+
* no AI SDK orchestrator. Tool calls, multi-step loops, telemetry,
|
|
40
|
+
* abort handling all inline.
|
|
33
41
|
*/
|
|
34
42
|
protected executeStream(options: StreamOptions, _analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
|
|
35
43
|
/**
|
|
36
|
-
*
|
|
44
|
+
* Multi-step streaming orchestrator. One iteration per model turn:
|
|
37
45
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
46
|
+
* 1. POST /chat/completions with stream:true
|
|
47
|
+
* 2. Parse SSE; push text deltas to the consumer queue
|
|
48
|
+
* 3. If the step emitted tool_calls → execute each, append to
|
|
49
|
+
* conversation, loop again
|
|
50
|
+
* 4. Otherwise resolve the deferred analytics promises and exit
|
|
51
|
+
*
|
|
52
|
+
* Bounded by `args.maxSteps`. Any thrown error rejects loopPromise and
|
|
53
|
+
* is surfaced to the consumer via `await loopPromise` in the stream
|
|
54
|
+
* generator.
|
|
40
55
|
*/
|
|
41
|
-
|
|
56
|
+
private runStreamLoop;
|
|
42
57
|
/**
|
|
43
|
-
*
|
|
58
|
+
* One streaming round-trip: POST chat-completions, parse SSE, push text
|
|
59
|
+
* deltas to the consumer queue. Returns the accumulated SSE result so
|
|
60
|
+
* the caller can decide whether to run tools and re-stream.
|
|
44
61
|
*/
|
|
45
|
-
|
|
62
|
+
private streamOneStep;
|
|
46
63
|
/**
|
|
47
|
-
*
|
|
64
|
+
* Execute every tool_call collected from one streaming step:
|
|
65
|
+
*
|
|
66
|
+
* - append an `assistant` turn carrying the tool_calls
|
|
67
|
+
* - resolve each tool from the local registry and run it
|
|
68
|
+
* - emit tool:start/tool:end events
|
|
69
|
+
* - push per-execution summaries
|
|
70
|
+
* - append a `tool` turn per result so the next step can see them
|
|
71
|
+
* - mirror BaseProvider's tool-events + storage hooks
|
|
48
72
|
*/
|
|
73
|
+
private executeToolBatch;
|
|
74
|
+
getAvailableModels(): Promise<string[]>;
|
|
75
|
+
getFirstAvailableModel(): Promise<string>;
|
|
49
76
|
private getFallbackModels;
|
|
50
77
|
}
|