@juspay/neurolink 12.23.3 → 12.24.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/CHANGELOG.md +3 -4
- package/dist/browser/neurolink.min.js +367 -367
- package/dist/constants/enums.d.ts +61 -69
- package/dist/constants/enums.js +63 -96
- package/dist/factories/providerDescriptors.d.ts +1 -1
- package/dist/factories/providerDescriptors.js +50 -83
- package/dist/factories/providerRegistry.js +9 -16
- package/dist/models/manifestRegistry.js +11 -2
- package/dist/providers/catalog/deepseek.json +84 -0
- package/dist/providers/catalog/huggingface.json +405 -0
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +6 -0
- package/dist/providers/catalog/loader.js +20 -5
- package/dist/providers/catalog/mistral.json +8 -3
- package/dist/providers/catalog/provider-catalog.schema.json +31 -1
- package/dist/providers/catalog/schema.d.ts +8 -1
- package/dist/providers/catalog/schema.js +9 -1
- package/dist/providers/configuredOpenAICompat.d.ts +14 -5
- package/dist/providers/configuredOpenAICompat.js +19 -4
- package/dist/providers/embeddingResponseParsing.d.ts +1 -0
- package/dist/providers/embeddingResponseParsing.js +47 -0
- package/dist/providers/ideogram.js +7 -1
- package/dist/providers/jina.js +6 -15
- package/dist/providers/llamaCpp.js +4 -15
- package/dist/providers/lmStudio.js +4 -15
- package/dist/providers/localRuntimeOpenAICompat.d.ts +16 -0
- package/dist/providers/localRuntimeOpenAICompat.js +26 -0
- package/dist/providers/ollama/client.js +3 -9
- package/dist/providers/openaiChatCompletionsBase.d.ts +3 -3
- package/dist/providers/openaiChatCompletionsBase.js +3 -3
- package/dist/providers/openaiCompatCatalog.d.ts +16 -12
- package/dist/providers/openaiCompatCatalog.js +16 -12
- package/dist/providers/voyage.js +5 -18
- package/dist/types/providerCatalog.d.ts +46 -1
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +12 -8
- package/dist/utils/modelChoices.d.ts +2 -2
- package/dist/utils/modelChoices.js +10 -38
- package/dist/utils/sizeGuard.d.ts +8 -5
- package/dist/utils/sizeGuard.js +32 -9
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
- package/dist/providers/deepseek.d.ts +0 -47
- package/dist/providers/deepseek.js +0 -120
- package/dist/providers/huggingFace/client.d.ts +0 -54
- package/dist/providers/huggingFace/client.js +0 -120
- package/dist/providers/huggingFace/index.d.ts +0 -1
- package/dist/providers/huggingFace/index.js +0 -1
|
@@ -41,11 +41,12 @@ export function catalogEnvVar(entry, kind) {
|
|
|
41
41
|
? `${base}_BASE_URL`
|
|
42
42
|
: `${base}_MODEL`;
|
|
43
43
|
}
|
|
44
|
-
function interpolate(template, entry, modelName) {
|
|
44
|
+
function interpolate(template, entry, modelName, rawMessage) {
|
|
45
45
|
return template
|
|
46
46
|
.replace(/\{apiKeyEnvVar\}/g, catalogEnvVar(entry, "apiKey"))
|
|
47
47
|
.replace(/\{setupUrl\}/g, entry.setup.url)
|
|
48
|
-
.replace(/\{model\}/g, modelName ?? "")
|
|
48
|
+
.replace(/\{model\}/g, modelName ?? "")
|
|
49
|
+
.replace(/\{message\}/g, rawMessage ?? "");
|
|
49
50
|
}
|
|
50
51
|
export function buildCatalogConfigOptions(entry) {
|
|
51
52
|
return {
|
|
@@ -54,17 +55,21 @@ export function buildCatalogConfigOptions(entry) {
|
|
|
54
55
|
setupUrl: entry.setup.url,
|
|
55
56
|
description: entry.setup.description ?? "API key",
|
|
56
57
|
instructions: entry.setup.instructions.map((line) => interpolate(line, entry)),
|
|
58
|
+
...(entry.wire.apiKeyFallbackEnvVars
|
|
59
|
+
? { fallbackEnvVars: entry.wire.apiKeyFallbackEnvVars }
|
|
60
|
+
: {}),
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
function buildErrorRules(entry) {
|
|
60
64
|
const bespoke = entry.errorRules.map((rule) => {
|
|
61
65
|
const regex = rule.pattern ? new RegExp(rule.pattern, "i") : undefined;
|
|
66
|
+
const needsContext = rule.message.includes("{model}") || rule.message.includes("{message}");
|
|
62
67
|
return {
|
|
63
68
|
match: (ctx) => (rule.status !== undefined && ctx.statusCode === rule.status) ||
|
|
64
69
|
(regex !== undefined && regex.test(ctx.message)),
|
|
65
70
|
errorClass: ERROR_CLASS_MAP[rule.class],
|
|
66
|
-
message:
|
|
67
|
-
? (ctx) => interpolate(rule.message, entry, ctx.modelName)
|
|
71
|
+
message: needsContext
|
|
72
|
+
? (ctx) => interpolate(rule.message, entry, ctx.modelName, ctx.message)
|
|
68
73
|
: interpolate(rule.message, entry),
|
|
69
74
|
};
|
|
70
75
|
});
|
|
@@ -118,7 +123,14 @@ export function buildCatalogEntries() {
|
|
|
118
123
|
entry.models.fallbacks[0],
|
|
119
124
|
fallbackModels: [...entry.models.fallbacks],
|
|
120
125
|
errorRules: buildErrorRules(entry),
|
|
121
|
-
|
|
126
|
+
// "model-dependent" leaves this field unset entirely (not the string
|
|
127
|
+
// itself, which OpenAICompatCatalogEntry.supportsTools types as
|
|
128
|
+
// boolean|undefined) so ConfiguredOpenAICompatProvider.supportsTools()
|
|
129
|
+
// falls through to the model-registry default, same as an entry that
|
|
130
|
+
// never set this field at all.
|
|
131
|
+
...(typeof entry.capabilities.tools === "boolean"
|
|
132
|
+
? { supportsTools: entry.capabilities.tools }
|
|
133
|
+
: {}),
|
|
122
134
|
};
|
|
123
135
|
const { baseURLTemplate } = entry.wire;
|
|
124
136
|
if (baseURLTemplate) {
|
|
@@ -143,6 +155,9 @@ export function buildCatalogEntries() {
|
|
|
143
155
|
if (entry.quirks?.timeoutErrorClass === "provider") {
|
|
144
156
|
base.timeoutErrorClass = ProviderError;
|
|
145
157
|
}
|
|
158
|
+
if (entry.quirks?.responseFormatDowngrade) {
|
|
159
|
+
base.responseFormatDowngrade = entry.quirks.responseFormatDowngrade;
|
|
160
|
+
}
|
|
146
161
|
return base;
|
|
147
162
|
});
|
|
148
163
|
}
|
|
@@ -223,12 +223,17 @@
|
|
|
223
223
|
"quirks": {
|
|
224
224
|
"registryDefaultIgnoresModelEnvVar": true
|
|
225
225
|
},
|
|
226
|
+
"timeouts": {
|
|
227
|
+
"generateMs": 45000,
|
|
228
|
+
"streamMs": 120000
|
|
229
|
+
},
|
|
230
|
+
"autoSelectPriority": 9,
|
|
226
231
|
"setup": {
|
|
227
|
-
"url": "https://console.mistral.ai/",
|
|
228
|
-
"apiKeyFormat":
|
|
232
|
+
"url": "https://console.mistral.ai/api-keys",
|
|
233
|
+
"apiKeyFormat": "^[A-Za-z0-9]{32}$",
|
|
229
234
|
"billingPolicy": "free-with-card",
|
|
230
235
|
"instructions": [
|
|
231
|
-
"1. Visit: https://console.mistral.ai/",
|
|
236
|
+
"1. Visit: https://console.mistral.ai/api-keys",
|
|
232
237
|
"2. Create or sign in to your account",
|
|
233
238
|
"3. Generate a new API key"
|
|
234
239
|
]
|
|
@@ -67,6 +67,13 @@
|
|
|
67
67
|
"missingCredentialMessage": {
|
|
68
68
|
"type": "string"
|
|
69
69
|
},
|
|
70
|
+
"apiKeyFallbackEnvVars": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
},
|
|
75
|
+
"description": "Additional env vars validateApiKey() tries, in order, when the primary apiKeyEnvVar is unset (e.g. HF_TOKEN alongside HUGGINGFACE_API_KEY)."
|
|
76
|
+
},
|
|
70
77
|
"envOverrides": {
|
|
71
78
|
"type": "object",
|
|
72
79
|
"additionalProperties": false,
|
|
@@ -205,7 +212,8 @@
|
|
|
205
212
|
"type": "boolean"
|
|
206
213
|
},
|
|
207
214
|
"tools": {
|
|
208
|
-
"type": "boolean"
|
|
215
|
+
"anyOf": [{ "type": "boolean" }, { "const": "model-dependent" }],
|
|
216
|
+
"description": "\"model-dependent\" when tool support varies per served model and the vendor doesn't reject tools for unsupported ones (e.g. HuggingFace's router)."
|
|
209
217
|
},
|
|
210
218
|
"toolsWithStreaming": {
|
|
211
219
|
"type": "boolean"
|
|
@@ -267,9 +275,31 @@
|
|
|
267
275
|
"type": "string",
|
|
268
276
|
"enum": ["string"],
|
|
269
277
|
"description": "Vendor accepts messages[].content only as a plain string, rejecting OpenAI's content-parts array and the null used on an assistant message with tool_calls. Normalized by ConfiguredOpenAICompatProvider."
|
|
278
|
+
},
|
|
279
|
+
"responseFormatDowngrade": {
|
|
280
|
+
"type": "string",
|
|
281
|
+
"enum": ["json-schema-to-json-object"],
|
|
282
|
+
"description": "Vendor rejects response_format: { type: \"json_schema\" } outright but accepts { type: \"json_object\" }. ConfiguredOpenAICompatProvider downgrades the request so generate({ schema }) keeps working."
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
"timeouts": {
|
|
287
|
+
"type": "object",
|
|
288
|
+
"additionalProperties": false,
|
|
289
|
+
"description": "Descriptor-only generate/stream turn-budget override consumed by buildCatalogDescriptor(); never read by the OpenAI-compat runtime path.",
|
|
290
|
+
"properties": {
|
|
291
|
+
"generateMs": {
|
|
292
|
+
"type": "number"
|
|
293
|
+
},
|
|
294
|
+
"streamMs": {
|
|
295
|
+
"type": "number"
|
|
270
296
|
}
|
|
271
297
|
}
|
|
272
298
|
},
|
|
299
|
+
"autoSelectPriority": {
|
|
300
|
+
"type": "number",
|
|
301
|
+
"description": "Descriptor-only ascending auto-select priority override (lower = tried first in getBestProvider()'s fallback chain). Absent = provider is not part of the auto-select chain."
|
|
302
|
+
},
|
|
273
303
|
"setup": {
|
|
274
304
|
"type": "object",
|
|
275
305
|
"additionalProperties": false,
|
|
@@ -23,6 +23,7 @@ export declare const providerCatalogJsonSchema: z.ZodObject<{
|
|
|
23
23
|
baseURLTemplate: z.ZodOptional<z.ZodString>;
|
|
24
24
|
extraCredentials: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
25
25
|
missingCredentialMessage: z.ZodOptional<z.ZodString>;
|
|
26
|
+
apiKeyFallbackEnvVars: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
27
|
envOverrides: z.ZodOptional<z.ZodObject<{
|
|
27
28
|
apiKey: z.ZodOptional<z.ZodString>;
|
|
28
29
|
baseURL: z.ZodOptional<z.ZodString>;
|
|
@@ -60,7 +61,7 @@ export declare const providerCatalogJsonSchema: z.ZodObject<{
|
|
|
60
61
|
capabilities: z.ZodObject<{
|
|
61
62
|
text: z.ZodBoolean;
|
|
62
63
|
streaming: z.ZodBoolean;
|
|
63
|
-
tools: z.ZodBoolean
|
|
64
|
+
tools: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"model-dependent">]>;
|
|
64
65
|
toolsWithStreaming: z.ZodBoolean;
|
|
65
66
|
structuredOutput: z.ZodBoolean;
|
|
66
67
|
structuredOutputWithTools: z.ZodBoolean;
|
|
@@ -83,7 +84,13 @@ export declare const providerCatalogJsonSchema: z.ZodObject<{
|
|
|
83
84
|
timeoutErrorClass: z.ZodOptional<z.ZodLiteral<"provider">>;
|
|
84
85
|
messageContentFormat: z.ZodOptional<z.ZodLiteral<"string">>;
|
|
85
86
|
registryDefaultIgnoresModelEnvVar: z.ZodOptional<z.ZodBoolean>;
|
|
87
|
+
responseFormatDowngrade: z.ZodOptional<z.ZodLiteral<"json-schema-to-json-object">>;
|
|
86
88
|
}, z.core.$strict>>;
|
|
89
|
+
timeouts: z.ZodOptional<z.ZodObject<{
|
|
90
|
+
generateMs: z.ZodOptional<z.ZodNumber>;
|
|
91
|
+
streamMs: z.ZodOptional<z.ZodNumber>;
|
|
92
|
+
}, z.core.$strict>>;
|
|
93
|
+
autoSelectPriority: z.ZodOptional<z.ZodNumber>;
|
|
87
94
|
setup: z.ZodObject<{
|
|
88
95
|
url: z.ZodString;
|
|
89
96
|
apiKeyFormat: z.ZodNullable<z.ZodString>;
|
|
@@ -36,6 +36,7 @@ const catalogWireSchema = z
|
|
|
36
36
|
baseURLTemplate: z.string().optional(),
|
|
37
37
|
extraCredentials: z.array(z.string()).optional(),
|
|
38
38
|
missingCredentialMessage: z.string().optional(),
|
|
39
|
+
apiKeyFallbackEnvVars: z.array(z.string()).optional(),
|
|
39
40
|
envOverrides: z
|
|
40
41
|
.strictObject({
|
|
41
42
|
apiKey: z.string().optional(),
|
|
@@ -138,6 +139,11 @@ const catalogQuirksSchema = z.strictObject({
|
|
|
138
139
|
timeoutErrorClass: z.literal("provider").optional(),
|
|
139
140
|
messageContentFormat: z.literal("string").optional(),
|
|
140
141
|
registryDefaultIgnoresModelEnvVar: z.boolean().optional(),
|
|
142
|
+
responseFormatDowngrade: z.literal("json-schema-to-json-object").optional(),
|
|
143
|
+
});
|
|
144
|
+
const catalogTimeoutsSchema = z.strictObject({
|
|
145
|
+
generateMs: z.number().optional(),
|
|
146
|
+
streamMs: z.number().optional(),
|
|
141
147
|
});
|
|
142
148
|
const catalogBillingPolicySchema = z.enum([
|
|
143
149
|
"free-tier",
|
|
@@ -172,7 +178,7 @@ const catalogEvidenceSchema = z.strictObject({
|
|
|
172
178
|
const catalogCapabilitiesSchema = z.strictObject({
|
|
173
179
|
text: z.boolean(),
|
|
174
180
|
streaming: z.boolean(),
|
|
175
|
-
tools: z.boolean(),
|
|
181
|
+
tools: z.union([z.boolean(), z.literal("model-dependent")]),
|
|
176
182
|
toolsWithStreaming: z.boolean(),
|
|
177
183
|
structuredOutput: z.boolean(),
|
|
178
184
|
structuredOutputWithTools: z.boolean(),
|
|
@@ -331,6 +337,8 @@ export const providerCatalogJsonSchema = z.strictObject({
|
|
|
331
337
|
capabilities: catalogCapabilitiesSchema,
|
|
332
338
|
errorRules: z.array(catalogErrorRuleJsonSchema),
|
|
333
339
|
quirks: catalogQuirksSchema.optional(),
|
|
340
|
+
timeouts: catalogTimeoutsSchema.optional(),
|
|
341
|
+
autoSelectPriority: z.number().optional(),
|
|
334
342
|
setup: catalogSetupSchema,
|
|
335
343
|
evidence: catalogEvidenceSchema,
|
|
336
344
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AIProviderName } from "../constants/enums.js";
|
|
2
|
-
import type { OpenAICompatCatalogEntry, OpenAICompatChatRequest, OpenAICompatCredentials } from "../types/index.js";
|
|
2
|
+
import type { OpenAICompatCatalogEntry, OpenAICompatChatRequest, OpenAICompatCredentials, OpenAICompatResponseFormat } from "../types/index.js";
|
|
3
3
|
import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
4
4
|
/**
|
|
5
5
|
* Generic OpenAI-compatible provider driven entirely by an
|
|
@@ -11,13 +11,13 @@ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
|
11
11
|
* If a provider needs a real hook override (adjustRequestBody,
|
|
12
12
|
* adjustBodyAfter400, getChatCompletionsURL, getAuthHeaders,
|
|
13
13
|
* suppressResponseFormatWithTools, ...) it does NOT belong in the catalog —
|
|
14
|
-
* write a dedicated subclass instead (see
|
|
14
|
+
* write a dedicated subclass instead (see azureOpenai.ts).
|
|
15
15
|
*
|
|
16
16
|
* The exception is a WIRE DIALECT: a vendor that speaks OpenAI for ordinary
|
|
17
17
|
* chat but encodes one part of the request differently. Expressing that as
|
|
18
|
-
* data (the catalog's `messageContentFormat`
|
|
19
|
-
* one-JSON-file entry instead of promoting it to a
|
|
20
|
-
* over a single incompatibility.
|
|
18
|
+
* data (the catalog's `messageContentFormat` / `responseFormatDowngrade`)
|
|
19
|
+
* keeps the provider a one-JSON-file entry instead of promoting it to a
|
|
20
|
+
* hand-written subclass over a single incompatibility.
|
|
21
21
|
*
|
|
22
22
|
* Today that is Cloudflare Workers AI, whose OpenAI-compatible endpoint
|
|
23
23
|
* accepts `messages[].content` only as a plain string — never the
|
|
@@ -28,6 +28,14 @@ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
|
28
28
|
* every tool round-trip fails. Normalizing content to a string below is the
|
|
29
29
|
* whole fix: tools, tool_choice, the `tool` role and `tool_calls` are all
|
|
30
30
|
* accepted as-is.
|
|
31
|
+
*
|
|
32
|
+
* The same shape covers DeepSeek: its `/chat/completions` rejects
|
|
33
|
+
* `response_format: { type: "json_schema" }` outright ("This response_format
|
|
34
|
+
* type is unavailable now"), but accepts `{ type: "json_object" }`. The
|
|
35
|
+
* catalog's `responseFormatDowngrade` quirk downgrades the request so
|
|
36
|
+
* `generate({ schema })` keeps working, matching the
|
|
37
|
+
* `supportsStructuredOutputs: false` behavior of the `@ai-sdk/openai-compatible`
|
|
38
|
+
* provider this replaced.
|
|
31
39
|
*/
|
|
32
40
|
export declare class ConfiguredOpenAICompatProvider extends OpenAIChatCompletionsProvider {
|
|
33
41
|
private readonly entry;
|
|
@@ -48,5 +56,6 @@ export declare class ConfiguredOpenAICompatProvider extends OpenAIChatCompletion
|
|
|
48
56
|
*/
|
|
49
57
|
supportsTools(): boolean;
|
|
50
58
|
protected adjustRequestBody(body: OpenAICompatChatRequest, modelId: string): OpenAICompatChatRequest;
|
|
59
|
+
protected adjustResponseFormat(rf: OpenAICompatResponseFormat | undefined, modelId: string): OpenAICompatResponseFormat | undefined;
|
|
51
60
|
protected formatProviderError(error: unknown): Error;
|
|
52
61
|
}
|
|
@@ -33,13 +33,13 @@ const flattenMessageContent = (content) => {
|
|
|
33
33
|
* If a provider needs a real hook override (adjustRequestBody,
|
|
34
34
|
* adjustBodyAfter400, getChatCompletionsURL, getAuthHeaders,
|
|
35
35
|
* suppressResponseFormatWithTools, ...) it does NOT belong in the catalog —
|
|
36
|
-
* write a dedicated subclass instead (see
|
|
36
|
+
* write a dedicated subclass instead (see azureOpenai.ts).
|
|
37
37
|
*
|
|
38
38
|
* The exception is a WIRE DIALECT: a vendor that speaks OpenAI for ordinary
|
|
39
39
|
* chat but encodes one part of the request differently. Expressing that as
|
|
40
|
-
* data (the catalog's `messageContentFormat`
|
|
41
|
-
* one-JSON-file entry instead of promoting it to a
|
|
42
|
-
* over a single incompatibility.
|
|
40
|
+
* data (the catalog's `messageContentFormat` / `responseFormatDowngrade`)
|
|
41
|
+
* keeps the provider a one-JSON-file entry instead of promoting it to a
|
|
42
|
+
* hand-written subclass over a single incompatibility.
|
|
43
43
|
*
|
|
44
44
|
* Today that is Cloudflare Workers AI, whose OpenAI-compatible endpoint
|
|
45
45
|
* accepts `messages[].content` only as a plain string — never the
|
|
@@ -50,6 +50,14 @@ const flattenMessageContent = (content) => {
|
|
|
50
50
|
* every tool round-trip fails. Normalizing content to a string below is the
|
|
51
51
|
* whole fix: tools, tool_choice, the `tool` role and `tool_calls` are all
|
|
52
52
|
* accepted as-is.
|
|
53
|
+
*
|
|
54
|
+
* The same shape covers DeepSeek: its `/chat/completions` rejects
|
|
55
|
+
* `response_format: { type: "json_schema" }` outright ("This response_format
|
|
56
|
+
* type is unavailable now"), but accepts `{ type: "json_object" }`. The
|
|
57
|
+
* catalog's `responseFormatDowngrade` quirk downgrades the request so
|
|
58
|
+
* `generate({ schema })` keeps working, matching the
|
|
59
|
+
* `supportsStructuredOutputs: false` behavior of the `@ai-sdk/openai-compatible`
|
|
60
|
+
* provider this replaced.
|
|
53
61
|
*/
|
|
54
62
|
export class ConfiguredOpenAICompatProvider extends OpenAIChatCompletionsProvider {
|
|
55
63
|
entry;
|
|
@@ -115,6 +123,13 @@ export class ConfiguredOpenAICompatProvider extends OpenAIChatCompletionsProvide
|
|
|
115
123
|
})),
|
|
116
124
|
};
|
|
117
125
|
}
|
|
126
|
+
adjustResponseFormat(rf, modelId) {
|
|
127
|
+
if (this.entry.responseFormatDowngrade === "json-schema-to-json-object" &&
|
|
128
|
+
rf?.type === "json_schema") {
|
|
129
|
+
return { type: "json_object" };
|
|
130
|
+
}
|
|
131
|
+
return super.adjustResponseFormat(rf, modelId);
|
|
132
|
+
}
|
|
118
133
|
formatProviderError(error) {
|
|
119
134
|
// classifyProviderError hard-codes TimeoutError -> NetworkError ahead
|
|
120
135
|
// of any rule table and does not allow a per-provider override. An
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseIndexedEmbeddingsResponse(data: unknown, inputCount: number, providerLabel: string, makeError: (message: string) => Error): number[][];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared embeddings-response validation for Voyage and Jina — both call
|
|
3
|
+
* POST /embeddings with an identical `{input, model}` body and get back an
|
|
4
|
+
* identical `{data: [{embedding, index}]}` shape, and both had byte-identical
|
|
5
|
+
* validation logic (same three message templates, same sort-by-index +
|
|
6
|
+
* 0..n-1 coverage check) before this was factored out.
|
|
7
|
+
*
|
|
8
|
+
* A plain function, not a shared base class: Voyage throws typed
|
|
9
|
+
* `ProviderError` here, Jina throws plain `Error` (a real, pre-existing
|
|
10
|
+
* inconsistency between the two — not something this refactor silently
|
|
11
|
+
* fixes). `makeError` preserves each provider's exact existing behavior;
|
|
12
|
+
* `providerLabel` preserves each provider's exact existing message text
|
|
13
|
+
* ("Voyage ..." / "Jina ...").
|
|
14
|
+
*/
|
|
15
|
+
const isIndexedEmbedding = (value) => {
|
|
16
|
+
if (typeof value !== "object" || value === null) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const { embedding, index } = value;
|
|
20
|
+
return (Number.isInteger(index) &&
|
|
21
|
+
Array.isArray(embedding) &&
|
|
22
|
+
embedding.every((n) => typeof n === "number" && Number.isFinite(n)));
|
|
23
|
+
};
|
|
24
|
+
export function parseIndexedEmbeddingsResponse(data, inputCount, providerLabel, makeError) {
|
|
25
|
+
const entries = typeof data === "object" && data !== null
|
|
26
|
+
? data.data
|
|
27
|
+
: undefined;
|
|
28
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
29
|
+
throw makeError(`${providerLabel} embeddings response missing data`);
|
|
30
|
+
}
|
|
31
|
+
if (entries.length !== inputCount) {
|
|
32
|
+
throw makeError(`${providerLabel} embeddings response count mismatch: expected ${inputCount}, got ${entries.length}`);
|
|
33
|
+
}
|
|
34
|
+
const malformed = entries.findIndex((entry) => !isIndexedEmbedding(entry));
|
|
35
|
+
if (malformed !== -1) {
|
|
36
|
+
throw makeError(`${providerLabel} embeddings response entry ${malformed} is not an {index, embedding: number[]} object`);
|
|
37
|
+
}
|
|
38
|
+
const sorted = entries
|
|
39
|
+
.slice()
|
|
40
|
+
.sort((a, b) => a.index - b.index);
|
|
41
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
42
|
+
if (sorted[i].index !== i) {
|
|
43
|
+
throw makeError(`${providerLabel} embeddings response has unexpected index ordering: position ${i} has index ${sorted[i].index}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return sorted.map((d) => d.embedding);
|
|
47
|
+
}
|
|
@@ -5,6 +5,8 @@ import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
|
5
5
|
import { AuthenticationError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
7
|
import { createIdeogramConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
|
|
8
|
+
import { MAX_IMAGE_BYTES, readBoundedBuffer } from "../utils/sizeGuard.js";
|
|
9
|
+
import { assertSafeUrl } from "../utils/ssrfGuard.js";
|
|
8
10
|
const IDEOGRAM_DEFAULT_BASE_URL = "https://api.ideogram.ai";
|
|
9
11
|
const REQUEST_TIMEOUT_MS = 120_000;
|
|
10
12
|
const getIdeogramApiKey = () => validateApiKey(createIdeogramConfig());
|
|
@@ -132,6 +134,10 @@ export class IdeogramProvider extends BaseProvider {
|
|
|
132
134
|
if (!url) {
|
|
133
135
|
throw new Error("Ideogram returned no image URL");
|
|
134
136
|
}
|
|
137
|
+
// Guard the API-returned URL before fetching (provider-returned URLs
|
|
138
|
+
// carry the same SSRF risk as caller-supplied ones) — matches Recraft's
|
|
139
|
+
// equivalent download path.
|
|
140
|
+
await assertSafeUrl(url);
|
|
135
141
|
// Download the image and convert to base64 to match the imageOutput
|
|
136
142
|
// contract used by other image-gen providers. Apply a 60s timeout so the
|
|
137
143
|
// download cannot hang indefinitely.
|
|
@@ -155,7 +161,7 @@ export class IdeogramProvider extends BaseProvider {
|
|
|
155
161
|
if (!dl.ok) {
|
|
156
162
|
throw new Error(`Failed to download Ideogram image: ${dl.status}`);
|
|
157
163
|
}
|
|
158
|
-
const buffer =
|
|
164
|
+
const buffer = await readBoundedBuffer(dl, MAX_IMAGE_BYTES, "Ideogram image");
|
|
159
165
|
const base64 = buffer.toString("base64");
|
|
160
166
|
const generationTimeMs = Date.now() - startTime;
|
|
161
167
|
logger.info(`[IdeogramProvider] Generated image (${buffer.length} bytes) in ${generationTimeMs}ms — model ${this.modelName}`);
|
package/dist/providers/jina.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JinaModels } from "../constants/enums.js";
|
|
2
2
|
import { BaseProvider } from "../core/baseProvider.js";
|
|
3
3
|
import { isNeuroLink } from "../neurolink.js";
|
|
4
|
+
import { parseIndexedEmbeddingsResponse } from "./embeddingResponseParsing.js";
|
|
4
5
|
import { createProxyFetch } from "../proxy/proxyFetch.js";
|
|
5
6
|
import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../types/index.js";
|
|
6
7
|
import { logger } from "../utils/logger.js";
|
|
@@ -190,21 +191,11 @@ export class JinaProvider extends BaseProvider {
|
|
|
190
191
|
throw this.formatProviderError(new Error(`Jina embeddings failed: ${response.status} — ${text}`));
|
|
191
192
|
}
|
|
192
193
|
const data = (await response.json());
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
throw new Error(`Jina embeddings response count mismatch: expected ${inputs.length}, got ${data.data.length}`);
|
|
199
|
-
}
|
|
200
|
-
// Sort by index and verify sequential coverage (0 … n-1).
|
|
201
|
-
const sorted = data.data.slice().sort((a, b) => a.index - b.index);
|
|
202
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
203
|
-
if (sorted[i].index !== i) {
|
|
204
|
-
throw new Error(`Jina embeddings response has unexpected index ordering: position ${i} has index ${sorted[i].index}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return sorted.map((d) => d.embedding);
|
|
194
|
+
// Sorts by index and verifies sequential 0..n-1 coverage. Note: throws
|
|
195
|
+
// plain Error, not ProviderError, unlike Voyage's equivalent — a real,
|
|
196
|
+
// pre-existing inconsistency between the two, preserved as-is rather
|
|
197
|
+
// than silently "fixed" by this refactor.
|
|
198
|
+
return parseIndexedEmbeddingsResponse(data, inputs.length, "Jina", (message) => new Error(message));
|
|
208
199
|
}
|
|
209
200
|
async validateConfiguration() {
|
|
210
201
|
return typeof this.apiKey === "string" && this.apiKey.trim().length > 0;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProviderError } from "../types/index.js";
|
|
2
2
|
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../utils/errorClassifier.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
4
|
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
5
5
|
import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
6
|
+
import { buildLocalUnreachableErrorRule } from "./localRuntimeOpenAICompat.js";
|
|
6
7
|
const LLAMACPP_DEFAULT_BASE_URL = "http://localhost:8080/v1";
|
|
7
8
|
const LLAMACPP_PLACEHOLDER_KEY = "llamacpp";
|
|
8
9
|
const getLlamaCppBaseURL = () => {
|
|
@@ -52,21 +53,9 @@ export class LlamaCppProvider extends OpenAIChatCompletionsProvider {
|
|
|
52
53
|
return ["loaded-model"];
|
|
53
54
|
}
|
|
54
55
|
formatProviderError(error) {
|
|
55
|
-
// `code`/`cause.code` aren't part of ProviderErrorContext, so they're read
|
|
56
|
-
// off the raw error here (mirrors ollama's `responseBody` extraction) for
|
|
57
|
-
// the ECONNREFUSED rule below, which the pre-migration code also checked
|
|
58
|
-
// via a duck-typed error code in addition to the message text.
|
|
59
|
-
const errorRecord = error;
|
|
60
|
-
const cause = errorRecord?.cause ?? {};
|
|
61
|
-
const code = (errorRecord?.code ?? cause?.code);
|
|
62
56
|
const rules = [
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
/ECONNREFUSED|Failed to fetch|fetch failed/.test(ctx.message),
|
|
66
|
-
errorClass: NetworkError,
|
|
67
|
-
message: () => `llama.cpp server not reachable at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
68
|
-
"Start it with: ./llama-server -m model.gguf --port 8080",
|
|
69
|
-
},
|
|
57
|
+
buildLocalUnreachableErrorRule(error, () => `llama.cpp server not reachable at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
58
|
+
"Start it with: ./llama-server -m model.gguf --port 8080"),
|
|
70
59
|
{
|
|
71
60
|
match: (ctx) => /400/.test(ctx.message),
|
|
72
61
|
errorClass: ProviderError,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { InvalidModelError
|
|
1
|
+
import { InvalidModelError } from "../types/index.js";
|
|
2
2
|
import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../utils/errorClassifier.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
4
|
import { redactUrlCredentials } from "../utils/logSanitize.js";
|
|
5
5
|
import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
6
|
+
import { buildLocalUnreachableErrorRule } from "./localRuntimeOpenAICompat.js";
|
|
6
7
|
const LM_STUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1";
|
|
7
8
|
const LM_STUDIO_PLACEHOLDER_KEY = "lm-studio";
|
|
8
9
|
const FALLBACK_MODEL = "local-model";
|
|
@@ -47,21 +48,9 @@ export class LMStudioProvider extends OpenAIChatCompletionsProvider {
|
|
|
47
48
|
return FALLBACK_MODEL;
|
|
48
49
|
}
|
|
49
50
|
formatProviderError(error) {
|
|
50
|
-
// `code`/`cause.code` aren't part of ProviderErrorContext, so they're read
|
|
51
|
-
// off the raw error here (mirrors ollama's `responseBody` extraction) for
|
|
52
|
-
// the ECONNREFUSED rule below, which the pre-migration code also checked
|
|
53
|
-
// via a duck-typed error code in addition to the message text.
|
|
54
|
-
const errorRecord = error;
|
|
55
|
-
const cause = errorRecord?.cause ?? {};
|
|
56
|
-
const code = (errorRecord?.code ?? cause?.code);
|
|
57
51
|
const rules = [
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
/ECONNREFUSED|Failed to fetch|fetch failed/.test(ctx.message),
|
|
61
|
-
errorClass: NetworkError,
|
|
62
|
-
message: () => `LM Studio server not reachable at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
63
|
-
`Open the LM Studio app, load a model, and click "Start Server".`,
|
|
64
|
-
},
|
|
52
|
+
buildLocalUnreachableErrorRule(error, () => `LM Studio server not reachable at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
53
|
+
`Open the LM Studio app, load a model, and click "Start Server".`),
|
|
65
54
|
{
|
|
66
55
|
match: (ctx) => /model_not_found|404/.test(ctx.message),
|
|
67
56
|
errorClass: InvalidModelError,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ProviderErrorRule } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shared error-rule builder for local-runtime OpenAI-compatible providers —
|
|
4
|
+
* Ollama, LM Studio, llama.cpp. All three had byte-identical connection-
|
|
5
|
+
* refused predicates (including the duck-typed `code`/`cause.code`
|
|
6
|
+
* extraction, since none of that lives on ProviderErrorContext) before this
|
|
7
|
+
* factored it out. A plain function, not a shared base class: the
|
|
8
|
+
* `neurolink/provider-base-class` lint rule requires every provider to
|
|
9
|
+
* extend BaseProvider or OpenAIChatCompletionsProvider directly, so the
|
|
10
|
+
* three keep their existing class hierarchy and just call this from their
|
|
11
|
+
* own `formatProviderError()`.
|
|
12
|
+
*
|
|
13
|
+
* `message` stays per-vendor (each provider's own exact wording, unchanged)
|
|
14
|
+
* — only the match predicate and errorClass were ever duplicated.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildLocalUnreachableErrorRule(error: unknown, message: () => string): ProviderErrorRule;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NetworkError } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shared error-rule builder for local-runtime OpenAI-compatible providers —
|
|
4
|
+
* Ollama, LM Studio, llama.cpp. All three had byte-identical connection-
|
|
5
|
+
* refused predicates (including the duck-typed `code`/`cause.code`
|
|
6
|
+
* extraction, since none of that lives on ProviderErrorContext) before this
|
|
7
|
+
* factored it out. A plain function, not a shared base class: the
|
|
8
|
+
* `neurolink/provider-base-class` lint rule requires every provider to
|
|
9
|
+
* extend BaseProvider or OpenAIChatCompletionsProvider directly, so the
|
|
10
|
+
* three keep their existing class hierarchy and just call this from their
|
|
11
|
+
* own `formatProviderError()`.
|
|
12
|
+
*
|
|
13
|
+
* `message` stays per-vendor (each provider's own exact wording, unchanged)
|
|
14
|
+
* — only the match predicate and errorClass were ever duplicated.
|
|
15
|
+
*/
|
|
16
|
+
export function buildLocalUnreachableErrorRule(error, message) {
|
|
17
|
+
const errorRecord = error;
|
|
18
|
+
const cause = errorRecord?.cause ?? {};
|
|
19
|
+
const code = (errorRecord?.code ?? cause?.code);
|
|
20
|
+
return {
|
|
21
|
+
match: (ctx) => code === "ECONNREFUSED" ||
|
|
22
|
+
/ECONNREFUSED|Failed to fetch|fetch failed/.test(ctx.message),
|
|
23
|
+
errorClass: NetworkError,
|
|
24
|
+
message,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -6,6 +6,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
6
6
|
import { redactUrlCredentials } from "../../utils/logSanitize.js";
|
|
7
7
|
import { createTimeoutController, parseTimeout, TimeoutError, } from "../../utils/timeout.js";
|
|
8
8
|
import { OpenAIChatCompletionsProvider } from "../openaiChatCompletionsBase.js";
|
|
9
|
+
import { buildLocalUnreachableErrorRule } from "../localRuntimeOpenAICompat.js";
|
|
9
10
|
import { stripTrailingSlash } from "../openaiChatCompletionsClient.js";
|
|
10
11
|
import { DEFAULT_OLLAMA_MODEL } from "./constants.js";
|
|
11
12
|
// Ollama serves its OpenAI-compatible surface under `/v1` (/v1/chat/completions,
|
|
@@ -97,16 +98,9 @@ export class OllamaProvider extends OpenAIChatCompletionsProvider {
|
|
|
97
98
|
const responseBody = typeof errorRecord?.responseBody === "string"
|
|
98
99
|
? errorRecord.responseBody
|
|
99
100
|
: "";
|
|
100
|
-
const cause = errorRecord?.cause ?? {};
|
|
101
|
-
const code = (errorRecord?.code ?? cause?.code);
|
|
102
101
|
const rules = [
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
/ECONNREFUSED|Failed to fetch|fetch failed/.test(ctx.message),
|
|
106
|
-
errorClass: NetworkError,
|
|
107
|
-
message: () => `Cannot connect to Ollama at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
108
|
-
`Install Ollama (https://ollama.com), start it with 'ollama serve', then try again.`,
|
|
109
|
-
},
|
|
102
|
+
buildLocalUnreachableErrorRule(error, () => `Cannot connect to Ollama at ${redactUrlCredentials(this.config.baseURL)}. ` +
|
|
103
|
+
`Install Ollama (https://ollama.com), start it with 'ollama serve', then try again.`),
|
|
110
104
|
// The base client (buildAPIError) attaches statusCode + responseBody to
|
|
111
105
|
// HTTP failures. Distinguish a genuine missing-model error (give 'ollama
|
|
112
106
|
// pull' guidance) from a bare endpoint-mismatch 404 (wrong base URL / not
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* quirks (env var names, default model, error mapping).
|
|
6
6
|
*
|
|
7
7
|
* Currently extended by:
|
|
8
|
-
* - OpenAICompatibleProvider
|
|
9
|
-
* - LiteLLMProvider
|
|
10
|
-
* -
|
|
8
|
+
* - OpenAICompatibleProvider (generic /v1/chat/completions backend)
|
|
9
|
+
* - LiteLLMProvider (LiteLLM proxy server)
|
|
10
|
+
* - ConfiguredOpenAICompatProvider (JSON-catalog providers, incl. deepseek)
|
|
11
11
|
*
|
|
12
12
|
* Subclasses provide:
|
|
13
13
|
* - getProviderName() / getDefaultModel() / formatProviderError() (abstract)
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* quirks (env var names, default model, error mapping).
|
|
6
6
|
*
|
|
7
7
|
* Currently extended by:
|
|
8
|
-
* - OpenAICompatibleProvider
|
|
9
|
-
* - LiteLLMProvider
|
|
10
|
-
* -
|
|
8
|
+
* - OpenAICompatibleProvider (generic /v1/chat/completions backend)
|
|
9
|
+
* - LiteLLMProvider (LiteLLM proxy server)
|
|
10
|
+
* - ConfiguredOpenAICompatProvider (JSON-catalog providers, incl. deepseek)
|
|
11
11
|
*
|
|
12
12
|
* Subclasses provide:
|
|
13
13
|
* - getProviderName() / getDefaultModel() / formatProviderError() (abstract)
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import type { OpenAICompatCatalogEntry } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
|
-
* Config-driven catalog of the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Config-driven catalog of the 10 OpenAI-compatible providers whose only
|
|
4
|
+
* differences from each other are expressible as named, closed catalog
|
|
5
|
+
* quirks (messageContentFormat, timeoutErrorClass, responseFormatDowngrade)
|
|
6
|
+
* rather than real code. Each entry fully replaces what used to be a
|
|
7
|
+
* hand-written OpenAIChatCompletionsProvider subclass — see
|
|
8
|
+
* ConfiguredOpenAICompatProvider for the class that reads these entries,
|
|
9
|
+
* and providerRegistry.ts for the registration loop that consumes this
|
|
10
|
+
* array.
|
|
8
11
|
*
|
|
9
12
|
* The entries themselves are derived from the JSON catalog
|
|
10
13
|
* (src/lib/providers/catalog/<id>.json) via buildCatalogEntries() — that
|
|
11
|
-
* JSON is now the single source of truth for these
|
|
14
|
+
* JSON is now the single source of truth for these 10 providers' identity,
|
|
12
15
|
* models, error rules and wire config. See
|
|
13
16
|
* src/lib/providers/catalog/loader.ts for the derivation logic and
|
|
14
17
|
* docs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md for the
|
|
15
18
|
* authoring format.
|
|
16
19
|
*
|
|
17
|
-
* To add a new
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* (getProviderName/getDefaultModel/formatProviderError)
|
|
22
|
-
* dedicated subclass instead (see
|
|
20
|
+
* To add a new OpenAI-compatible provider: add one JSON file under
|
|
21
|
+
* src/lib/providers/catalog/ (see provider-catalog.schema.json) and run
|
|
22
|
+
* `pnpm run codegen:catalog`. Do NOT add a provider here if it needs a real
|
|
23
|
+
* hook override beyond the 3 mandatory ones
|
|
24
|
+
* (getProviderName/getDefaultModel/formatProviderError) and the named
|
|
25
|
+
* catalog quirks above — write a dedicated subclass instead (see
|
|
26
|
+
* azureOpenai.ts).
|
|
23
27
|
*/
|
|
24
28
|
export declare const OPENAI_COMPAT_CATALOG: readonly OpenAICompatCatalogEntry[];
|