@juspay/neurolink 12.49.20 → 12.51.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 -3
- package/dist/browser/neurolink.min.js +671 -671
- package/dist/cli/commands/setup.d.ts +7 -5
- package/dist/cli/commands/setup.js +10 -8
- package/dist/constants/enums.d.ts +7 -15
- package/dist/constants/enums.js +8 -16
- package/dist/core/conversationMemoryManager.js +10 -4
- package/dist/core/redisConversationMemoryManager.js +6 -5
- package/dist/factories/providerDescriptors.d.ts +17 -3
- package/dist/factories/providerDescriptors.js +81 -56
- package/dist/factories/providerRegistry.d.ts +9 -1
- package/dist/factories/providerRegistry.js +19 -9
- package/dist/models/manifestRegistry.js +40 -3
- package/dist/neurolink.d.ts +52 -1
- package/dist/neurolink.js +195 -0
- package/dist/providers/catalog/index.generated.d.ts +3 -1
- package/dist/providers/catalog/index.generated.js +11 -0
- package/dist/providers/catalog/laya.json +107 -0
- package/dist/providers/catalog/loader.d.ts +7 -4
- package/dist/providers/catalog/loader.js +10 -3
- package/dist/providers/catalog/provider-catalog.schema.json +218 -11
- package/dist/providers/catalog/schema.d.ts +11 -9
- package/dist/providers/catalog/schema.js +333 -59
- package/dist/providers/configuredDecision.d.ts +23 -0
- package/dist/providers/configuredDecision.js +111 -0
- package/dist/providers/decide/baseUrl.d.ts +2 -0
- package/dist/providers/decide/baseUrl.js +14 -0
- package/dist/providers/decide/errors.d.ts +8 -0
- package/dist/providers/decide/errors.js +57 -0
- package/dist/providers/decide/formatters.d.ts +2 -0
- package/dist/providers/decide/formatters.js +4 -0
- package/dist/providers/decide/paths.d.ts +1 -0
- package/dist/providers/decide/paths.js +21 -0
- package/dist/providers/decide/template.d.ts +3 -0
- package/dist/providers/decide/template.js +13 -0
- package/dist/types/conversation.d.ts +109 -0
- package/dist/types/livekit.d.ts +246 -3
- package/dist/types/providerCatalog.d.ts +74 -1
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +12 -9
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/stream.d.ts +8 -1
- package/dist/utils/conversationTurnIdentity.d.ts +24 -0
- package/dist/utils/conversationTurnIdentity.js +81 -0
- package/dist/utils/modelChoices.js +11 -25
- package/dist/utils/providerConfig.d.ts +0 -5
- package/dist/utils/providerConfig.js +0 -17
- package/dist/voice/livekit/brain.js +10 -7
- package/dist/voice/livekit/index.d.ts +2 -0
- package/dist/voice/livekit/index.js +2 -0
- package/dist/voice/livekit/memoryBinding.d.ts +47 -0
- package/dist/voice/livekit/memoryBinding.js +152 -0
- package/dist/voice/livekit/realtimeMcpTools.js +20 -1
- package/dist/voice/livekit/realtimeMemory.d.ts +22 -0
- package/dist/voice/livekit/realtimeMemory.js +111 -0
- package/dist/voice/livekit/realtimeVoiceAgent.js +40 -1
- package/dist/voice/livekit/turnRecorder.d.ts +38 -0
- package/dist/voice/livekit/turnRecorder.js +362 -0
- package/dist/voice/livekit/voiceAgent.js +43 -5
- package/docs-site/static/search-index.json +8 -7
- package/package.json +7 -2
- package/dist/models/manifests/laya.d.ts +0 -12
- package/dist/models/manifests/laya.js +0 -54
- package/dist/providers/laya.d.ts +0 -40
- package/dist/providers/laya.js +0 -152
|
@@ -31,76 +31,89 @@ const catalogModelSpecSchema = z.strictObject({
|
|
|
31
31
|
.regex(IDENTIFIER_PATTERN, "enumMember must be a valid identifier")
|
|
32
32
|
.optional(),
|
|
33
33
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
baseURL: z.string().optional(),
|
|
37
|
-
baseURLTemplate: z.string().optional(),
|
|
38
|
-
extraCredentials: z.array(z.string()).optional(),
|
|
39
|
-
missingCredentialMessage: z.string().optional(),
|
|
40
|
-
apiKeyFallbackEnvVars: z.array(z.string()).optional(),
|
|
41
|
-
envOverrides: z
|
|
34
|
+
function buildWireSchema(requireExactlyOneBaseURL) {
|
|
35
|
+
return z
|
|
42
36
|
.strictObject({
|
|
43
|
-
apiKey: z.string().optional(),
|
|
44
37
|
baseURL: z.string().optional(),
|
|
45
|
-
|
|
38
|
+
baseURLTemplate: z.string().optional(),
|
|
39
|
+
extraCredentials: z.array(z.string()).optional(),
|
|
40
|
+
missingCredentialMessage: z.string().optional(),
|
|
41
|
+
apiKeyFallbackEnvVars: z.array(z.string()).optional(),
|
|
42
|
+
envOverrides: z
|
|
43
|
+
.strictObject({
|
|
44
|
+
apiKey: z.string().optional(),
|
|
45
|
+
baseURL: z.string().optional(),
|
|
46
|
+
model: z.string().optional(),
|
|
47
|
+
})
|
|
48
|
+
.optional(),
|
|
46
49
|
})
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const hasTemplate = wire.baseURLTemplate !== undefined;
|
|
52
|
-
if (hasBaseURL === hasTemplate) {
|
|
53
|
-
ctx.addIssue({
|
|
54
|
-
code: "custom",
|
|
55
|
-
path: ["baseURL"],
|
|
56
|
-
message: "wire requires exactly one of baseURL or baseURLTemplate",
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
if (wire.extraCredentials !== undefined) {
|
|
60
|
-
if (wire.extraCredentials.length !== 1) {
|
|
50
|
+
.superRefine((wire, ctx) => {
|
|
51
|
+
const hasBaseURL = wire.baseURL !== undefined;
|
|
52
|
+
const hasTemplate = wire.baseURLTemplate !== undefined;
|
|
53
|
+
if (requireExactlyOneBaseURL && hasBaseURL === hasTemplate) {
|
|
61
54
|
ctx.addIssue({
|
|
62
55
|
code: "custom",
|
|
63
|
-
path: ["
|
|
64
|
-
message: "wire
|
|
56
|
+
path: ["baseURL"],
|
|
57
|
+
message: "wire requires exactly one of baseURL or baseURLTemplate",
|
|
65
58
|
});
|
|
66
59
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
60
|
+
if (wire.extraCredentials !== undefined) {
|
|
61
|
+
if (wire.extraCredentials.length !== 1) {
|
|
62
|
+
ctx.addIssue({
|
|
63
|
+
code: "custom",
|
|
64
|
+
path: ["extraCredentials"],
|
|
65
|
+
message: "wire.extraCredentials must have exactly one entry",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else if (!IDENTIFIER_PATTERN.test(wire.extraCredentials[0])) {
|
|
69
|
+
// Embedded verbatim in generated credential typing — a non-identifier
|
|
70
|
+
// value would emit invalid TypeScript.
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: "custom",
|
|
73
|
+
path: ["extraCredentials", 0],
|
|
74
|
+
message: "wire.extraCredentials entry must be a valid identifier",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!hasTemplate) {
|
|
78
|
+
ctx.addIssue({
|
|
79
|
+
code: "custom",
|
|
80
|
+
path: ["extraCredentials"],
|
|
81
|
+
message: "wire.extraCredentials is only valid with baseURLTemplate",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
75
84
|
}
|
|
76
|
-
if (!hasTemplate) {
|
|
85
|
+
if (wire.missingCredentialMessage !== undefined && !hasTemplate) {
|
|
77
86
|
ctx.addIssue({
|
|
78
87
|
code: "custom",
|
|
79
|
-
path: ["
|
|
80
|
-
message: "wire.
|
|
88
|
+
path: ["missingCredentialMessage"],
|
|
89
|
+
message: "wire.missingCredentialMessage is only valid with baseURLTemplate",
|
|
81
90
|
});
|
|
82
91
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const placeholder = `{${wire.extraCredentials[0]}}`;
|
|
95
|
-
if (!wire.baseURLTemplate?.includes(placeholder)) {
|
|
96
|
-
ctx.addIssue({
|
|
97
|
-
code: "custom",
|
|
98
|
-
path: ["baseURLTemplate"],
|
|
99
|
-
message: "wire.baseURLTemplate must contain the extraCredentials[0] placeholder",
|
|
100
|
-
});
|
|
92
|
+
if (hasTemplate &&
|
|
93
|
+
wire.extraCredentials !== undefined &&
|
|
94
|
+
wire.extraCredentials.length === 1) {
|
|
95
|
+
const placeholder = `{${wire.extraCredentials[0]}}`;
|
|
96
|
+
if (!wire.baseURLTemplate?.includes(placeholder)) {
|
|
97
|
+
ctx.addIssue({
|
|
98
|
+
code: "custom",
|
|
99
|
+
path: ["baseURLTemplate"],
|
|
100
|
+
message: "wire.baseURLTemplate must contain the extraCredentials[0] placeholder",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
101
103
|
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const catalogWireSchema = buildWireSchema(true);
|
|
107
|
+
const decideWireSchema = buildWireSchema(false);
|
|
108
|
+
// Chat providers read these; ConfiguredDecisionProvider does not. Accepting one
|
|
109
|
+
// would give a descriptor that says "configured" next to a provider that
|
|
110
|
+
// ignores the setting, so each is refused until the engine reads it.
|
|
111
|
+
const UNSUPPORTED_DECIDE_WIRE_KEYS = [
|
|
112
|
+
"baseURLTemplate",
|
|
113
|
+
"extraCredentials",
|
|
114
|
+
"missingCredentialMessage",
|
|
115
|
+
"apiKeyFallbackEnvVars",
|
|
116
|
+
];
|
|
104
117
|
const catalogErrorRuleClassSchema = z.enum([
|
|
105
118
|
"authentication",
|
|
106
119
|
"rate-limit",
|
|
@@ -342,7 +355,7 @@ const catalogModelsSchema = z
|
|
|
342
355
|
}
|
|
343
356
|
}
|
|
344
357
|
});
|
|
345
|
-
|
|
358
|
+
const catalogIdentityShape = {
|
|
346
359
|
$schema: z.string().optional(),
|
|
347
360
|
id: z.string().regex(KEBAB_ID_PATTERN, "id must be kebab-case"),
|
|
348
361
|
displayName: z.string(),
|
|
@@ -355,7 +368,10 @@ export const providerCatalogJsonSchema = z.strictObject({
|
|
|
355
368
|
.regex(IDENTIFIER_PATTERN, "credentialsKey must be a valid identifier")
|
|
356
369
|
.optional(),
|
|
357
370
|
aliases: z.array(z.string()),
|
|
358
|
-
|
|
371
|
+
};
|
|
372
|
+
export const providerCatalogJsonSchema = z.strictObject({
|
|
373
|
+
...catalogIdentityShape,
|
|
374
|
+
tier: z.literal(2).optional(),
|
|
359
375
|
wire: catalogWireSchema,
|
|
360
376
|
models: catalogModelsSchema,
|
|
361
377
|
capabilities: catalogCapabilitiesSchema,
|
|
@@ -366,7 +382,265 @@ export const providerCatalogJsonSchema = z.strictObject({
|
|
|
366
382
|
setup: catalogSetupSchema,
|
|
367
383
|
evidence: catalogEvidenceSchema,
|
|
368
384
|
});
|
|
385
|
+
const decideStatusSchema = z.number().int().min(100).max(599);
|
|
386
|
+
const decidePositiveInt = z.number().int().positive();
|
|
387
|
+
const decidePositiveNumber = z.number().positive();
|
|
388
|
+
const decideErrorKindSchema = z.enum([
|
|
389
|
+
"authentication",
|
|
390
|
+
"invalid_request",
|
|
391
|
+
"max_tokens_exceeded",
|
|
392
|
+
"rate_limit",
|
|
393
|
+
"overloaded",
|
|
394
|
+
"server",
|
|
395
|
+
"timeout",
|
|
396
|
+
"network",
|
|
397
|
+
]);
|
|
398
|
+
const decideErrorRuleSchema = z.strictObject({
|
|
399
|
+
status: z
|
|
400
|
+
.union([decideStatusSchema, z.array(decideStatusSchema).min(1)])
|
|
401
|
+
.optional(),
|
|
402
|
+
statusMin: decideStatusSchema.optional(),
|
|
403
|
+
kind: decideErrorKindSchema,
|
|
404
|
+
retryable: z.boolean().optional(),
|
|
405
|
+
});
|
|
406
|
+
const UNSAFE_PATH_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
|
|
407
|
+
const HTTP_TOKEN_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
408
|
+
const decidePathSchema = z
|
|
409
|
+
.string()
|
|
410
|
+
.refine((path) => /^[^.\s]+(?:\.[^.\s]+)*$/.test(path) &&
|
|
411
|
+
path.split(".").every((segment) => !UNSAFE_PATH_SEGMENTS.has(segment)), {
|
|
412
|
+
message: "path must be dot-separated segments without whitespace, and must not use __proto__, constructor or prototype",
|
|
413
|
+
});
|
|
414
|
+
const decideHeaderNameSchema = z
|
|
415
|
+
.string()
|
|
416
|
+
.regex(HTTP_TOKEN_PATTERN, "header name must be an HTTP token");
|
|
417
|
+
const decideEndpointSchema = z
|
|
418
|
+
.string()
|
|
419
|
+
.refine((endpoint) => endpoint.trim().length > 0 &&
|
|
420
|
+
!/[{}]/.test(endpoint.split("{baseURL}").join("")), {
|
|
421
|
+
message: "endpoint must not be blank and may only use the {baseURL} placeholder",
|
|
422
|
+
});
|
|
423
|
+
const decideBodyShapeSchema = z
|
|
424
|
+
.strictObject({
|
|
425
|
+
path: decidePathSchema,
|
|
426
|
+
isArray: z.boolean().optional(),
|
|
427
|
+
formatter: z.enum(["fastapi-validation"]).optional(),
|
|
428
|
+
kind: decideErrorKindSchema.optional(),
|
|
429
|
+
retryable: z.boolean().optional(),
|
|
430
|
+
})
|
|
431
|
+
.refine((shape) => (shape.formatter !== undefined) === (shape.isArray === true), {
|
|
432
|
+
path: ["formatter"],
|
|
433
|
+
message: "a body shape sets isArray: true together with a formatter, or neither: an array without a formatter never matches, and a formatter on a body that is not an array reads the text as an empty list",
|
|
434
|
+
});
|
|
435
|
+
const decideBlockSchema = z.strictObject({
|
|
436
|
+
dialect: z.enum(["system-one"]),
|
|
437
|
+
order: decidePositiveInt,
|
|
438
|
+
baseURLRequired: z.boolean().optional(),
|
|
439
|
+
baseURLNormalize: z.array(z.enum(["strip-trailing-slash"])).optional(),
|
|
440
|
+
endpoint: decideEndpointSchema,
|
|
441
|
+
model: z.strictObject({
|
|
442
|
+
placement: z.enum(["body", "body-unless-alias"]),
|
|
443
|
+
alias: z.string().min(1).optional(),
|
|
444
|
+
}),
|
|
445
|
+
requestIdHeaders: z.array(decideHeaderNameSchema).optional(),
|
|
446
|
+
response: z
|
|
447
|
+
.strictObject({ modelPaths: z.array(decidePathSchema).optional() })
|
|
448
|
+
.optional(),
|
|
449
|
+
errors: z.strictObject({
|
|
450
|
+
rules: z.array(decideErrorRuleSchema),
|
|
451
|
+
default: decideErrorKindSchema,
|
|
452
|
+
bodyShapes: z.array(decideBodyShapeSchema),
|
|
453
|
+
}),
|
|
454
|
+
});
|
|
455
|
+
// A window of 0 or less would reach every reader of the derived model manifest,
|
|
456
|
+
// and a model without its own window inherits the default's through `??`,
|
|
457
|
+
// which keeps a 0. Chat entries keep the looser catalogModelSpecSchema.
|
|
458
|
+
const decideModelSpecSchema = catalogModelSpecSchema.extend({
|
|
459
|
+
contextWindow: decidePositiveInt.optional(),
|
|
460
|
+
maxOutputTokens: decidePositiveInt.optional(),
|
|
461
|
+
});
|
|
462
|
+
// Here and not in the entry-level refinement below: that one does not run once
|
|
463
|
+
// a sibling field fails a type check, and an author who deletes the TODO lines
|
|
464
|
+
// of a scaffold should see these gaps beside the other errors.
|
|
465
|
+
const decideModelsSchema = z
|
|
466
|
+
.strictObject({
|
|
467
|
+
default: z.string(),
|
|
468
|
+
catalog: z.record(z.string(), decideModelSpecSchema),
|
|
469
|
+
topModels: z.array(z.string()).min(1).optional(),
|
|
470
|
+
testModel: z.string().min(1).optional(),
|
|
471
|
+
})
|
|
472
|
+
.superRefine((models, ctx) => {
|
|
473
|
+
// The "must be a key" rule reports a default that is not an own key.
|
|
474
|
+
if (!Object.hasOwn(models.catalog, models.default)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const base = models.catalog[models.default];
|
|
478
|
+
for (const field of ["contextWindow", "maxOutputTokens"]) {
|
|
479
|
+
if (base[field] === undefined) {
|
|
480
|
+
ctx.addIssue({
|
|
481
|
+
code: "custom",
|
|
482
|
+
path: ["catalog", models.default, field],
|
|
483
|
+
message: `${field} is required on the default model; the derived model manifest reads it`,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
// `media` is deliberately absent: no decide provider migrated yet needs it.
|
|
489
|
+
const decideLimitsSchema = z.strictObject({
|
|
490
|
+
maxStateTokens: decidePositiveInt,
|
|
491
|
+
maxQuestions: decidePositiveInt.optional(),
|
|
492
|
+
nonAsciiTokensPerChar: decidePositiveNumber.optional(),
|
|
493
|
+
digitTokensPerChar: decidePositiveNumber.optional(),
|
|
494
|
+
symbolTokensPerChar: decidePositiveNumber.optional(),
|
|
495
|
+
astralTokensPerChar: decidePositiveNumber.optional(),
|
|
496
|
+
models: z
|
|
497
|
+
.record(z.string(), z.strictObject({
|
|
498
|
+
maxStateTokens: decidePositiveInt,
|
|
499
|
+
nonAsciiTokensPerChar: decidePositiveNumber.optional(),
|
|
500
|
+
}))
|
|
501
|
+
.optional(),
|
|
502
|
+
advisory: z.boolean().optional(),
|
|
503
|
+
});
|
|
504
|
+
const decideCatalogJsonSchema = z
|
|
505
|
+
.strictObject({
|
|
506
|
+
...catalogIdentityShape,
|
|
507
|
+
inferenceKinds: z.tuple([z.literal("decide")]),
|
|
508
|
+
tier: z.literal(2).optional(),
|
|
509
|
+
wire: decideWireSchema,
|
|
510
|
+
models: decideModelsSchema,
|
|
511
|
+
timeouts: z
|
|
512
|
+
.strictObject({ decideMs: decidePositiveInt.optional() })
|
|
513
|
+
.optional(),
|
|
514
|
+
decisionLimits: decideLimitsSchema.optional(),
|
|
515
|
+
decide: decideBlockSchema,
|
|
516
|
+
setup: catalogSetupSchema,
|
|
517
|
+
evidence: catalogEvidenceSchema,
|
|
518
|
+
})
|
|
519
|
+
.superRefine((entry, ctx) => {
|
|
520
|
+
const { wire, decide } = entry;
|
|
521
|
+
const issue = (path, message) => {
|
|
522
|
+
ctx.addIssue({ code: "custom", path, message });
|
|
523
|
+
};
|
|
524
|
+
const seenStatus = new Set();
|
|
525
|
+
const seenStatusMin = new Set();
|
|
526
|
+
decide.errors.rules.forEach((rule, index) => {
|
|
527
|
+
const at = ["decide", "errors", "rules", index];
|
|
528
|
+
if ((rule.status === undefined) === (rule.statusMin === undefined)) {
|
|
529
|
+
issue(at, "errors.rules entry needs exactly one of status or statusMin");
|
|
530
|
+
}
|
|
531
|
+
const statuses = typeof rule.status === "number" ? [rule.status] : (rule.status ?? []);
|
|
532
|
+
for (const status of statuses) {
|
|
533
|
+
if (seenStatus.has(status)) {
|
|
534
|
+
issue([...at, "status"], `errors.rules lists status ${status} more than once`);
|
|
535
|
+
}
|
|
536
|
+
seenStatus.add(status);
|
|
537
|
+
}
|
|
538
|
+
if (rule.statusMin !== undefined) {
|
|
539
|
+
if (seenStatusMin.has(rule.statusMin)) {
|
|
540
|
+
issue([...at, "statusMin"], `errors.rules lists statusMin ${rule.statusMin} more than once`);
|
|
541
|
+
}
|
|
542
|
+
seenStatusMin.add(rule.statusMin);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
const modelKeys = new Set(Object.keys(entry.models.catalog));
|
|
546
|
+
if (!modelKeys.has(entry.models.default)) {
|
|
547
|
+
issue(["models", "default"], "models.default must be a key of models.catalog");
|
|
548
|
+
}
|
|
549
|
+
const seenTopModels = new Set();
|
|
550
|
+
(entry.models.topModels ?? []).forEach((id, index) => {
|
|
551
|
+
if (!modelKeys.has(id)) {
|
|
552
|
+
issue(["models", "topModels", index], "models.topModels entry must be a key of models.catalog");
|
|
553
|
+
}
|
|
554
|
+
if (seenTopModels.has(id)) {
|
|
555
|
+
issue(["models", "topModels", index], "models.topModels must not contain duplicate entries");
|
|
556
|
+
}
|
|
557
|
+
seenTopModels.add(id);
|
|
558
|
+
});
|
|
559
|
+
const { alias, placement } = decide.model;
|
|
560
|
+
if (placement === "body-unless-alias" && alias === undefined) {
|
|
561
|
+
issue(["decide", "model", "alias"], "decide.model.alias is required when placement is body-unless-alias");
|
|
562
|
+
}
|
|
563
|
+
if (placement === "body" && alias !== undefined) {
|
|
564
|
+
issue(["decide", "model", "alias"], "decide.model.alias is only valid with placement body-unless-alias");
|
|
565
|
+
}
|
|
566
|
+
if (alias !== undefined && !modelKeys.has(alias)) {
|
|
567
|
+
issue(["decide", "model", "alias"], "decide.model.alias must be a key of models.catalog");
|
|
568
|
+
}
|
|
569
|
+
for (const id of Object.keys(entry.decisionLimits?.models ?? {})) {
|
|
570
|
+
if (!modelKeys.has(id)) {
|
|
571
|
+
issue(["decisionLimits", "models", id], "decisionLimits.models key must be a key of models.catalog");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (decide.baseURLRequired === true &&
|
|
575
|
+
!decide.endpoint.includes("{baseURL}")) {
|
|
576
|
+
issue(["decide", "endpoint"], "decide.endpoint must contain {baseURL} when decide.baseURLRequired is true");
|
|
577
|
+
}
|
|
578
|
+
const seenShapes = new Set();
|
|
579
|
+
decide.errors.bodyShapes.forEach((shape, index) => {
|
|
580
|
+
const key = `${shape.path}|${shape.isArray === true}`;
|
|
581
|
+
if (seenShapes.has(key)) {
|
|
582
|
+
issue(["decide", "errors", "bodyShapes", index], "errors.bodyShapes lists the same path and isArray twice");
|
|
583
|
+
}
|
|
584
|
+
seenShapes.add(key);
|
|
585
|
+
});
|
|
586
|
+
const seenHeaders = new Set();
|
|
587
|
+
(decide.requestIdHeaders ?? []).forEach((header, index) => {
|
|
588
|
+
if (seenHeaders.has(header.toLowerCase())) {
|
|
589
|
+
issue(["decide", "requestIdHeaders", index], "requestIdHeaders lists the same header twice; header names are case-insensitive");
|
|
590
|
+
}
|
|
591
|
+
seenHeaders.add(header.toLowerCase());
|
|
592
|
+
});
|
|
593
|
+
for (const key of UNSUPPORTED_DECIDE_WIRE_KEYS) {
|
|
594
|
+
if (wire[key] !== undefined) {
|
|
595
|
+
issue(["wire", key], `wire.${key} is not supported on decide entries yet; ConfiguredDecisionProvider does not read it`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const hasBaseURL = wire.baseURL !== undefined;
|
|
599
|
+
const hasTemplate = wire.baseURLTemplate !== undefined;
|
|
600
|
+
if (decide.baseURLRequired === true) {
|
|
601
|
+
if (hasBaseURL || hasTemplate) {
|
|
602
|
+
ctx.addIssue({
|
|
603
|
+
code: "custom",
|
|
604
|
+
path: ["wire", hasBaseURL ? "baseURL" : "baseURLTemplate"],
|
|
605
|
+
message: "wire must not set baseURL or baseURLTemplate when decide.baseURLRequired is true; the caller supplies the base URL",
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else if (hasBaseURL === hasTemplate) {
|
|
610
|
+
ctx.addIssue({
|
|
611
|
+
code: "custom",
|
|
612
|
+
path: ["wire", "baseURL"],
|
|
613
|
+
message: "wire requires exactly one of baseURL or baseURLTemplate unless decide.baseURLRequired is true",
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
function describeIssues(error) {
|
|
618
|
+
return error.issues
|
|
619
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
620
|
+
.join("; ");
|
|
621
|
+
}
|
|
622
|
+
function carriesInferenceKinds(raw) {
|
|
623
|
+
return (typeof raw === "object" &&
|
|
624
|
+
raw !== null &&
|
|
625
|
+
raw.inferenceKinds !== undefined);
|
|
626
|
+
}
|
|
627
|
+
export function parseCatalogEntry(raw, sourcePath = "<in-memory entry>") {
|
|
628
|
+
if (!carriesInferenceKinds(raw)) {
|
|
629
|
+
return parseProviderCatalogJson(raw, sourcePath);
|
|
630
|
+
}
|
|
631
|
+
const result = decideCatalogJsonSchema.safeParse(raw);
|
|
632
|
+
if (!result.success) {
|
|
633
|
+
throw new Error(`Invalid decide catalog file ${sourcePath}: ${describeIssues(result.error)}`);
|
|
634
|
+
}
|
|
635
|
+
return result.data;
|
|
636
|
+
}
|
|
637
|
+
export function isDecideCatalogEntry(entry) {
|
|
638
|
+
return "inferenceKinds" in entry;
|
|
639
|
+
}
|
|
369
640
|
export function parseProviderCatalogJson(raw, sourcePath) {
|
|
641
|
+
if (carriesInferenceKinds(raw)) {
|
|
642
|
+
throw new Error(`Invalid provider catalog file ${sourcePath}: inferenceKinds is set, so this is a decide entry; parse it with parseCatalogEntry`);
|
|
643
|
+
}
|
|
370
644
|
const result = providerCatalogJsonSchema.safeParse(raw);
|
|
371
645
|
if (!result.success) {
|
|
372
646
|
throw new Error(`Invalid provider catalog file ${sourcePath}: ${result.error.issues
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DecideCatalogJson, DecisionDialect, DecisionError, DecisionState, UnknownRecord } from "../types/index.js";
|
|
2
|
+
import { SystemOneDecisionProvider } from "./systemOneDecision.js";
|
|
3
|
+
/** A `decide` provider described entirely by its catalog entry. */
|
|
4
|
+
export declare class ConfiguredDecisionProvider extends SystemOneDecisionProvider {
|
|
5
|
+
private readonly entry;
|
|
6
|
+
private readonly apiKey;
|
|
7
|
+
private readonly baseURL;
|
|
8
|
+
private readonly endpoint;
|
|
9
|
+
constructor(entry: DecideCatalogJson, modelName?: string, sdk?: unknown, credentials?: UnknownRecord);
|
|
10
|
+
protected getDefaultModel(): string;
|
|
11
|
+
protected vendorLabel(): string;
|
|
12
|
+
protected vendorDisplayName(): string;
|
|
13
|
+
protected decisionApiKey(): string;
|
|
14
|
+
protected missingKeyMessage(): string;
|
|
15
|
+
protected missingConfigMessage(): string | undefined;
|
|
16
|
+
protected decisionEndpoint(): string;
|
|
17
|
+
protected decisionHeaders(): Record<string, string>;
|
|
18
|
+
protected buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string): Record<string, unknown>;
|
|
19
|
+
protected parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
|
|
20
|
+
protected readRequestId(headers: Headers): string | undefined;
|
|
21
|
+
protected resolveResponseModel(payload: Record<string, unknown>, requestedModel: string): string;
|
|
22
|
+
protected dialect(): DecisionDialect;
|
|
23
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
import { redactUrlForError } from "../utils/logSanitize.js";
|
|
3
|
+
import { getProviderModel } from "../utils/providerConfig.js";
|
|
4
|
+
import { catalogCredentialsKey, catalogEnvVar } from "./catalog/loader.js";
|
|
5
|
+
import { normalizeBaseURL } from "./decide/baseUrl.js";
|
|
6
|
+
import { getDecisionDialect } from "./decide/dialects/index.js";
|
|
7
|
+
import { classifyDecisionError } from "./decide/errors.js";
|
|
8
|
+
import { isRecord } from "./decide/guards.js";
|
|
9
|
+
import { readPath } from "./decide/paths.js";
|
|
10
|
+
import { fillEndpointTemplate } from "./decide/template.js";
|
|
11
|
+
import { SystemOneDecisionProvider } from "./systemOneDecision.js";
|
|
12
|
+
const readText = (value) => typeof value === "string" ? value.trim() : "";
|
|
13
|
+
/** A `decide` provider described entirely by its catalog entry. */
|
|
14
|
+
export class ConfiguredDecisionProvider extends SystemOneDecisionProvider {
|
|
15
|
+
entry;
|
|
16
|
+
apiKey;
|
|
17
|
+
baseURL;
|
|
18
|
+
endpoint;
|
|
19
|
+
constructor(entry, modelName, sdk, credentials) {
|
|
20
|
+
// BaseProvider's constructor calls getDefaultModel() for a falsy name,
|
|
21
|
+
// before this.entry is assigned, so the name is settled here.
|
|
22
|
+
super(modelName ||
|
|
23
|
+
getProviderModel(catalogEnvVar(entry, "model"), entry.models.default), entry.id, sdk);
|
|
24
|
+
this.entry = entry;
|
|
25
|
+
this.apiKey =
|
|
26
|
+
readText(credentials?.apiKey) ||
|
|
27
|
+
readText(process.env[catalogEnvVar(entry, "apiKey")]);
|
|
28
|
+
// `||`, not `??`, so a blank value counts as unset.
|
|
29
|
+
this.baseURL = normalizeBaseURL(readText(credentials?.baseURL) ||
|
|
30
|
+
readText(process.env[catalogEnvVar(entry, "baseURL")]) ||
|
|
31
|
+
readText(entry.wire.baseURL), entry.decide.baseURLNormalize ?? []);
|
|
32
|
+
this.endpoint = fillEndpointTemplate(entry.decide.endpoint, {
|
|
33
|
+
baseURL: this.baseURL,
|
|
34
|
+
});
|
|
35
|
+
// A self-hosted base URL can carry `user:pass@` or a `?token=`.
|
|
36
|
+
logger.debug(`${entry.displayName} Provider initialized (decide only)`, {
|
|
37
|
+
modelName: this.modelName,
|
|
38
|
+
baseURL: this.baseURL ? redactUrlForError(this.baseURL) : "(not set)",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
getDefaultModel() {
|
|
42
|
+
return getProviderModel(catalogEnvVar(this.entry, "model"), this.entry.models.default);
|
|
43
|
+
}
|
|
44
|
+
vendorLabel() {
|
|
45
|
+
return this.entry.displayName;
|
|
46
|
+
}
|
|
47
|
+
vendorDisplayName() {
|
|
48
|
+
return this.entry.displayName;
|
|
49
|
+
}
|
|
50
|
+
decisionApiKey() {
|
|
51
|
+
return this.apiKey;
|
|
52
|
+
}
|
|
53
|
+
missingKeyMessage() {
|
|
54
|
+
return `${this.entry.displayName} requires an API key. Set ${catalogEnvVar(this.entry, "apiKey")} or pass credentials.${catalogCredentialsKey(this.entry)}.apiKey.`;
|
|
55
|
+
}
|
|
56
|
+
missingConfigMessage() {
|
|
57
|
+
return this.entry.decide.baseURLRequired && !this.baseURL
|
|
58
|
+
? `${this.entry.displayName} requires a base URL. Set ${catalogEnvVar(this.entry, "baseURL")} or pass credentials.${catalogCredentialsKey(this.entry)}.baseURL.`
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
decisionEndpoint() {
|
|
62
|
+
return this.endpoint;
|
|
63
|
+
}
|
|
64
|
+
decisionHeaders() {
|
|
65
|
+
return {
|
|
66
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
buildDecisionBody(state, questions, model) {
|
|
71
|
+
const { placement, alias } = this.entry.decide.model;
|
|
72
|
+
return placement === "body-unless-alias" && model === alias
|
|
73
|
+
? { state, questions }
|
|
74
|
+
: { model, state, questions };
|
|
75
|
+
}
|
|
76
|
+
parseDecisionError(status, payload, requestId) {
|
|
77
|
+
return classifyDecisionError(this.entry.decide.errors, {
|
|
78
|
+
status,
|
|
79
|
+
payload,
|
|
80
|
+
requestId,
|
|
81
|
+
label: this.entry.displayName,
|
|
82
|
+
apiKey: this.apiKey,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
readRequestId(headers) {
|
|
86
|
+
for (const name of this.entry.decide.requestIdHeaders ?? []) {
|
|
87
|
+
const value = headers.get(name);
|
|
88
|
+
if (value !== null) {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
resolveResponseModel(payload, requestedModel) {
|
|
95
|
+
// Defensive: the base class passes a record today. A direct call with
|
|
96
|
+
// null or undefined must not throw here, where the base lookup would.
|
|
97
|
+
if (!isRecord(payload)) {
|
|
98
|
+
return requestedModel;
|
|
99
|
+
}
|
|
100
|
+
for (const path of this.entry.decide.response?.modelPaths ?? []) {
|
|
101
|
+
const value = readPath(payload, path);
|
|
102
|
+
if (typeof value === "string") {
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return super.resolveResponseModel(payload, requestedModel);
|
|
107
|
+
}
|
|
108
|
+
dialect() {
|
|
109
|
+
return getDecisionDialect(this.entry.decide.dialect);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const TRANSFORMS = {
|
|
2
|
+
// A loop, not /\/+$/: that pattern is quadratic on a long run of slashes
|
|
3
|
+
// followed by another character.
|
|
4
|
+
"strip-trailing-slash": (value) => {
|
|
5
|
+
let end = value.length;
|
|
6
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
7
|
+
end -= 1;
|
|
8
|
+
}
|
|
9
|
+
return value.slice(0, end);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
export function normalizeBaseURL(raw, transforms) {
|
|
13
|
+
return transforms.reduce((value, name) => TRANSFORMS[name](value), raw);
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DecideErrors, DecisionError } from "../../types/index.js";
|
|
2
|
+
export declare function classifyDecisionError(spec: DecideErrors, input: {
|
|
3
|
+
status: number;
|
|
4
|
+
payload: unknown;
|
|
5
|
+
requestId: string | undefined;
|
|
6
|
+
label: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
}): DecisionError;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { redactCredentials } from "../systemOneDecision.js";
|
|
2
|
+
import { DECIDE_FORMATTERS } from "./formatters.js";
|
|
3
|
+
import { readPath } from "./paths.js";
|
|
4
|
+
const RETRYABLE_BY_DEFAULT = new Set([
|
|
5
|
+
"rate_limit",
|
|
6
|
+
"overloaded",
|
|
7
|
+
"server",
|
|
8
|
+
]);
|
|
9
|
+
const isRetryableByDefault = (kind) => RETRYABLE_BY_DEFAULT.has(kind);
|
|
10
|
+
function ruleMatches(rule, status) {
|
|
11
|
+
const statuses = rule.status === undefined
|
|
12
|
+
? []
|
|
13
|
+
: typeof rule.status === "number"
|
|
14
|
+
? [rule.status]
|
|
15
|
+
: rule.status;
|
|
16
|
+
return (statuses.includes(status) ||
|
|
17
|
+
(rule.statusMin !== undefined && status >= rule.statusMin));
|
|
18
|
+
}
|
|
19
|
+
function messageFromShape(shape, value) {
|
|
20
|
+
if (shape.isArray) {
|
|
21
|
+
return Array.isArray(value) && shape.formatter
|
|
22
|
+
? DECIDE_FORMATTERS[shape.formatter](value)
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return shape.formatter ? DECIDE_FORMATTERS[shape.formatter](value) : value;
|
|
29
|
+
}
|
|
30
|
+
export function classifyDecisionError(spec, input) {
|
|
31
|
+
const { status, payload, requestId, label, apiKey } = input;
|
|
32
|
+
const rule = spec.rules.find((candidate) => ruleMatches(candidate, status));
|
|
33
|
+
const ruleKind = rule?.kind ?? spec.default;
|
|
34
|
+
const ruleRetryable = rule?.retryable ?? isRetryableByDefault(ruleKind);
|
|
35
|
+
// Redacted once, here: it stands in for a shape's text that redaction
|
|
36
|
+
// empties, and the label is catalog data a key can match. Redaction is not
|
|
37
|
+
// idempotent for a key such as "e" (it matches inside "[redacted]"), so the
|
|
38
|
+
// fallback is never passed through it a second time.
|
|
39
|
+
const fallback = redactCredentials(`${label} request failed with HTTP ${status}`, apiKey);
|
|
40
|
+
const build = (kind, retryable, text) => ({
|
|
41
|
+
kind,
|
|
42
|
+
message: (text && redactCredentials(text, apiKey)) || fallback,
|
|
43
|
+
status,
|
|
44
|
+
requestId,
|
|
45
|
+
retryable,
|
|
46
|
+
});
|
|
47
|
+
for (const shape of spec.bodyShapes) {
|
|
48
|
+
const text = messageFromShape(shape, readPath(payload, shape.path));
|
|
49
|
+
if (text !== undefined) {
|
|
50
|
+
return build(shape.kind ?? ruleKind, shape.retryable ??
|
|
51
|
+
(shape.kind === undefined
|
|
52
|
+
? ruleRetryable
|
|
53
|
+
: isRetryableByDefault(shape.kind)), text);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return build(ruleKind, ruleRetryable);
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readPath(value: unknown, path: string): unknown;
|