@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +671 -671
  3. package/dist/cli/commands/setup.d.ts +7 -5
  4. package/dist/cli/commands/setup.js +10 -8
  5. package/dist/constants/enums.d.ts +7 -15
  6. package/dist/constants/enums.js +8 -16
  7. package/dist/core/conversationMemoryManager.js +10 -4
  8. package/dist/core/redisConversationMemoryManager.js +6 -5
  9. package/dist/factories/providerDescriptors.d.ts +17 -3
  10. package/dist/factories/providerDescriptors.js +81 -56
  11. package/dist/factories/providerRegistry.d.ts +9 -1
  12. package/dist/factories/providerRegistry.js +19 -9
  13. package/dist/models/manifestRegistry.js +40 -3
  14. package/dist/neurolink.d.ts +52 -1
  15. package/dist/neurolink.js +195 -0
  16. package/dist/providers/catalog/index.generated.d.ts +3 -1
  17. package/dist/providers/catalog/index.generated.js +11 -0
  18. package/dist/providers/catalog/laya.json +107 -0
  19. package/dist/providers/catalog/loader.d.ts +7 -4
  20. package/dist/providers/catalog/loader.js +10 -3
  21. package/dist/providers/catalog/provider-catalog.schema.json +218 -11
  22. package/dist/providers/catalog/schema.d.ts +11 -9
  23. package/dist/providers/catalog/schema.js +333 -59
  24. package/dist/providers/configuredDecision.d.ts +23 -0
  25. package/dist/providers/configuredDecision.js +111 -0
  26. package/dist/providers/decide/baseUrl.d.ts +2 -0
  27. package/dist/providers/decide/baseUrl.js +14 -0
  28. package/dist/providers/decide/errors.d.ts +8 -0
  29. package/dist/providers/decide/errors.js +57 -0
  30. package/dist/providers/decide/formatters.d.ts +2 -0
  31. package/dist/providers/decide/formatters.js +4 -0
  32. package/dist/providers/decide/paths.d.ts +1 -0
  33. package/dist/providers/decide/paths.js +21 -0
  34. package/dist/providers/decide/template.d.ts +3 -0
  35. package/dist/providers/decide/template.js +13 -0
  36. package/dist/types/conversation.d.ts +109 -0
  37. package/dist/types/livekit.d.ts +246 -3
  38. package/dist/types/providerCatalog.d.ts +74 -1
  39. package/dist/types/providerCatalog.generated.d.ts +2 -2
  40. package/dist/types/providers.d.ts +12 -9
  41. package/dist/types/sdk.d.ts +1 -1
  42. package/dist/types/stream.d.ts +8 -1
  43. package/dist/utils/conversationTurnIdentity.d.ts +24 -0
  44. package/dist/utils/conversationTurnIdentity.js +81 -0
  45. package/dist/utils/modelChoices.js +11 -25
  46. package/dist/utils/providerConfig.d.ts +0 -5
  47. package/dist/utils/providerConfig.js +0 -17
  48. package/dist/voice/livekit/brain.js +10 -7
  49. package/dist/voice/livekit/index.d.ts +2 -0
  50. package/dist/voice/livekit/index.js +2 -0
  51. package/dist/voice/livekit/memoryBinding.d.ts +47 -0
  52. package/dist/voice/livekit/memoryBinding.js +152 -0
  53. package/dist/voice/livekit/realtimeMcpTools.js +20 -1
  54. package/dist/voice/livekit/realtimeMemory.d.ts +22 -0
  55. package/dist/voice/livekit/realtimeMemory.js +111 -0
  56. package/dist/voice/livekit/realtimeVoiceAgent.js +40 -1
  57. package/dist/voice/livekit/turnRecorder.d.ts +38 -0
  58. package/dist/voice/livekit/turnRecorder.js +362 -0
  59. package/dist/voice/livekit/voiceAgent.js +43 -5
  60. package/docs-site/static/search-index.json +8 -7
  61. package/package.json +7 -2
  62. package/dist/models/manifests/laya.d.ts +0 -12
  63. package/dist/models/manifests/laya.js +0 -54
  64. package/dist/providers/laya.d.ts +0 -40
  65. 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
- const catalogWireSchema = z
35
- .strictObject({
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
- model: z.string().optional(),
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
- .optional(),
48
- })
49
- .superRefine((wire, ctx) => {
50
- const hasBaseURL = wire.baseURL !== undefined;
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: ["extraCredentials"],
64
- message: "wire.extraCredentials must have exactly one entry",
56
+ path: ["baseURL"],
57
+ message: "wire requires exactly one of baseURL or baseURLTemplate",
65
58
  });
66
59
  }
67
- else if (!IDENTIFIER_PATTERN.test(wire.extraCredentials[0])) {
68
- // Embedded verbatim in generated credential typing — a non-identifier
69
- // value would emit invalid TypeScript.
70
- ctx.addIssue({
71
- code: "custom",
72
- path: ["extraCredentials", 0],
73
- message: "wire.extraCredentials entry must be a valid identifier",
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: ["extraCredentials"],
80
- message: "wire.extraCredentials is only valid with baseURLTemplate",
88
+ path: ["missingCredentialMessage"],
89
+ message: "wire.missingCredentialMessage is only valid with baseURLTemplate",
81
90
  });
82
91
  }
83
- }
84
- if (wire.missingCredentialMessage !== undefined && !hasTemplate) {
85
- ctx.addIssue({
86
- code: "custom",
87
- path: ["missingCredentialMessage"],
88
- message: "wire.missingCredentialMessage is only valid with baseURLTemplate",
89
- });
90
- }
91
- if (hasTemplate &&
92
- wire.extraCredentials !== undefined &&
93
- wire.extraCredentials.length === 1) {
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
- export const providerCatalogJsonSchema = z.strictObject({
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
- tier: z.literal(2),
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,2 @@
1
+ import type { DecideBaseURLTransform } from "../../types/index.js";
2
+ export declare function normalizeBaseURL(raw: string, transforms: readonly DecideBaseURLTransform[]): string;
@@ -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,2 @@
1
+ import type { DecideFormatterName } from "../../types/index.js";
2
+ export declare const DECIDE_FORMATTERS: Readonly<Record<DecideFormatterName, (detail: unknown) => string>>;
@@ -0,0 +1,4 @@
1
+ import { describeValidationErrors } from "../systemOneDecision.js";
2
+ export const DECIDE_FORMATTERS = {
3
+ "fastapi-validation": (detail) => `Request failed validation — ${describeValidationErrors(Array.isArray(detail) ? detail : [])}`,
4
+ };
@@ -0,0 +1 @@
1
+ export declare function readPath(value: unknown, path: string): unknown;