@juspay/neurolink 12.57.0 → 12.58.1

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 (40) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +75 -51
  3. package/dist/browser/neurolink.min.js +419 -416
  4. package/dist/cli/commands/decide.d.ts +4 -1
  5. package/dist/cli/commands/decide.js +6 -3
  6. package/dist/cli/commands/setup.d.ts +1 -1
  7. package/dist/cli/commands/setup.js +1 -1
  8. package/dist/cli/factories/commandFactory.js +1 -1
  9. package/dist/constants/enums.d.ts +4 -0
  10. package/dist/constants/enums.js +5 -0
  11. package/dist/core/factory.d.ts +7 -0
  12. package/dist/core/factory.js +20 -0
  13. package/dist/factories/providerDescriptors.js +96 -7
  14. package/dist/processors/media/VideoProcessor.d.ts +2 -2
  15. package/dist/providers/catalog/index.generated.d.ts +1 -1
  16. package/dist/providers/catalog/index.generated.js +8 -1
  17. package/dist/providers/catalog/openai-decisions.json +75 -0
  18. package/dist/providers/catalog/provider-catalog.schema.json +56 -3
  19. package/dist/providers/catalog/schema.js +92 -11
  20. package/dist/providers/configuredDecision.js +9 -14
  21. package/dist/providers/decide/dialects/index.js +2 -0
  22. package/dist/providers/decide/dialects/openaiDecisions.d.ts +2 -0
  23. package/dist/providers/decide/dialects/openaiDecisions.js +228 -0
  24. package/dist/providers/decide/dialects/systemOne.js +17 -0
  25. package/dist/providers/systemOneDecision.js +6 -1
  26. package/dist/telemetry/attributes.d.ts +10 -3
  27. package/dist/telemetry/attributes.js +142 -10
  28. package/dist/telemetry/index.d.ts +1 -1
  29. package/dist/telemetry/index.js +1 -1
  30. package/dist/types/classifierRouter.d.ts +21 -9
  31. package/dist/types/cli.d.ts +4 -2
  32. package/dist/types/decision.d.ts +67 -3
  33. package/dist/types/processor.d.ts +10 -0
  34. package/dist/types/providerCatalog.d.ts +7 -11
  35. package/dist/types/providerCatalog.generated.d.ts +2 -2
  36. package/dist/types/providers.d.ts +12 -1
  37. package/dist/utils/pricing.js +9 -0
  38. package/docs-site/static/search-index.json +84 -59
  39. package/package.json +6 -2
  40. package/dist/client/reactHooks.tsx +0 -1473
@@ -545,8 +545,50 @@ const decideMediaSchema = z
545
545
  path: ["videoKey"],
546
546
  message: "decide.media.videoKey must differ from imagesKey: one body key cannot carry both",
547
547
  });
548
+ const GUARD_LIST_MAX = 8;
549
+ const ambientGuardEnvNameSchema = z
550
+ .string()
551
+ .max(64)
552
+ .regex(/^[A-Z][A-Z0-9_]*$/, "an environment variable name must be upper-case letters, digits and underscores, and start with a letter");
553
+ // Lower-case scheme, host labels and an optional port, nothing else: the
554
+ // origin is compared after the same parsing a base URL gets, so a spelling
555
+ // that URL would normalise (a trailing slash, a path, an upper-case host) is
556
+ // refused here instead of being quietly accepted.
557
+ const AMBIENT_GUARD_ORIGIN_PATTERN = /^https?:\/\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::[0-9]{1,5})?$/;
558
+ function parsesAsUrl(value) {
559
+ try {
560
+ new URL(value);
561
+ return true;
562
+ }
563
+ catch {
564
+ return false;
565
+ }
566
+ }
567
+ const ambientGuardOriginSchema = z
568
+ .string()
569
+ .max(100)
570
+ .regex(AMBIENT_GUARD_ORIGIN_PATTERN, "a trusted origin must be an http(s) origin: a lower-case scheme, host and optional port, with no path, query, fragment or userinfo")
571
+ .refine(parsesAsUrl, {
572
+ message: "a trusted origin must parse as a URL (a valid host, port 0 to 65535)",
573
+ });
574
+ const ambientGuardCredentialKeySchema = z
575
+ .string()
576
+ .max(64)
577
+ .regex(/^[a-zA-Z][a-zA-Z0-9]*$/, "a credentials key must be letters and digits, and start with a letter");
578
+ const ambientKeyGuardSchema = z.strictObject({
579
+ baseURLEnvVars: z.array(ambientGuardEnvNameSchema).min(1).max(GUARD_LIST_MAX),
580
+ trustedOrigins: z.array(ambientGuardOriginSchema).min(1).max(GUARD_LIST_MAX),
581
+ credentialBaseURLs: z
582
+ .array(z.strictObject({
583
+ key: ambientGuardCredentialKeySchema,
584
+ field: z.literal("baseURL"),
585
+ }))
586
+ .min(1)
587
+ .max(GUARD_LIST_MAX)
588
+ .optional(),
589
+ });
548
590
  const decideBlockSchema = z.strictObject({
549
- dialect: z.enum(["system-one"]),
591
+ dialect: z.enum(["system-one", "openai-decisions"]),
550
592
  order: decidePositiveInt,
551
593
  baseURLRequired: z.boolean().optional(),
552
594
  baseURLNormalize: z
@@ -559,6 +601,7 @@ const decideBlockSchema = z.strictObject({
559
601
  alias: z.string().min(1).optional(),
560
602
  }),
561
603
  media: decideMediaSchema.optional(),
604
+ ambientKeyGuard: ambientKeyGuardSchema.optional(),
562
605
  requestIdHeaders: z.array(decideHeaderNameSchema).optional(),
563
606
  response: z
564
607
  .strictObject({ modelPaths: z.array(decidePathSchema).optional() })
@@ -705,18 +748,34 @@ const decideCatalogJsonSchema = z
705
748
  }
706
749
  const mediaKeys = decide.media;
707
750
  const mediaAllowance = entry.decisionLimits?.media;
708
- if (mediaKeys !== undefined && mediaAllowance === undefined) {
709
- issue(["decide", "media"], "decide.media needs decisionLimits.media: without an allowance the base class refuses every image and video, so these keys would never be written");
710
- }
711
- if (mediaKeys === undefined && mediaAllowance !== undefined) {
712
- issue(["decisionLimits", "media"], "decisionLimits.media needs decide.media: without body keys the engine would prepare a request's media and then leave it out of the body");
751
+ if (decide.dialect === "openai-decisions") {
752
+ // This dialect writes images into the request input itself, so it has no
753
+ // body keys to name, and the input has no video part. The allowance
754
+ // alone is how many images a request may carry.
755
+ if (mediaKeys !== undefined) {
756
+ issue(["decide", "media"], "decide.media is not valid with dialect openai-decisions: that dialect places images in the request input itself and has no body keys for them");
757
+ }
758
+ if (mediaAllowance?.video === true) {
759
+ issue(["decisionLimits", "media", "video"], "decisionLimits.media.video must be false with dialect openai-decisions: its request input has no video part");
760
+ }
761
+ if (placement !== "body") {
762
+ issue(["decide", "model", "placement"], "decide.model.placement must be body with dialect openai-decisions: the model is always a field of the request");
763
+ }
713
764
  }
714
- if (mediaKeys !== undefined && mediaAllowance !== undefined) {
715
- if (mediaAllowance.video && mediaKeys.videoKey === undefined) {
716
- issue(["decide", "media", "videoKey"], "decide.media.videoKey is required when decisionLimits.media.video is true");
765
+ else {
766
+ if (mediaKeys !== undefined && mediaAllowance === undefined) {
767
+ issue(["decide", "media"], "decide.media needs decisionLimits.media: without an allowance the base class refuses every image and video, so these keys would never be written");
717
768
  }
718
- if (!mediaAllowance.video && mediaKeys.videoKey !== undefined) {
719
- issue(["decide", "media", "videoKey"], "decide.media.videoKey must be omitted when decisionLimits.media.video is false");
769
+ if (mediaKeys === undefined && mediaAllowance !== undefined) {
770
+ issue(["decisionLimits", "media"], "decisionLimits.media needs decide.media: without body keys the engine would prepare a request's media and then leave it out of the body");
771
+ }
772
+ if (mediaKeys !== undefined && mediaAllowance !== undefined) {
773
+ if (mediaAllowance.video && mediaKeys.videoKey === undefined) {
774
+ issue(["decide", "media", "videoKey"], "decide.media.videoKey is required when decisionLimits.media.video is true");
775
+ }
776
+ if (!mediaAllowance.video && mediaKeys.videoKey !== undefined) {
777
+ issue(["decide", "media", "videoKey"], "decide.media.videoKey must be omitted when decisionLimits.media.video is false");
778
+ }
720
779
  }
721
780
  }
722
781
  if (decide.baseURLRequired === true &&
@@ -738,6 +797,28 @@ const decideCatalogJsonSchema = z
738
797
  }
739
798
  seenHeaders.add(header.toLowerCase());
740
799
  });
800
+ const guard = decide.ambientKeyGuard;
801
+ if (guard !== undefined) {
802
+ const ownBaseURLVar = wire.envOverrides?.baseURL ??
803
+ `${entry.id.toUpperCase().replace(/-/g, "_")}_BASE_URL`;
804
+ const lists = [
805
+ ["baseURLEnvVars", guard.baseURLEnvVars, "variable"],
806
+ ["trustedOrigins", guard.trustedOrigins, "origin"],
807
+ ];
808
+ for (const [field, values, noun] of lists) {
809
+ if (new Set(values).size !== values.length) {
810
+ issue(["decide", "ambientKeyGuard", field], `lists the same ${noun} twice`);
811
+ }
812
+ }
813
+ const credentialFields = guard.credentialBaseURLs ?? [];
814
+ if (new Set(credentialFields.map((c) => `${c.key}.${c.field}`)).size !==
815
+ credentialFields.length) {
816
+ issue(["decide", "ambientKeyGuard", "credentialBaseURLs"], "lists the same credentials field twice");
817
+ }
818
+ if (guard.baseURLEnvVars.includes(ownBaseURLVar)) {
819
+ issue(["decide", "ambientKeyGuard", "baseURLEnvVars"], `must not list the provider's own base-URL variable ${ownBaseURLVar}: setting it already exempts the key`);
820
+ }
821
+ }
741
822
  for (const key of UNSUPPORTED_DECIDE_WIRE_KEYS) {
742
823
  if (wire[key] !== undefined) {
743
824
  issue(["wire", key], `wire.${key} is not supported on decide entries yet; ConfiguredDecisionProvider does not read it`);
@@ -75,20 +75,15 @@ export class ConfiguredDecisionProvider extends SystemOneDecisionProvider {
75
75
  }
76
76
  buildDecisionBody(state, questions, model, media) {
77
77
  const { placement, alias } = this.entry.decide.model;
78
- const body = placement === "body-unless-alias" && model === alias
79
- ? { state, questions }
80
- : { model, state, questions };
81
- const keys = this.entry.decide.media;
82
- if (!keys || !media) {
83
- return body;
84
- }
85
- // A key is written only for media the request carries: a server may refuse
86
- // an empty `images`.
87
- return {
88
- ...body,
89
- ...(media.images.length > 0 ? { [keys.imagesKey]: media.images } : {}),
90
- ...(media.video && keys.videoKey ? { [keys.videoKey]: media.video } : {}),
91
- };
78
+ return this.dialect().buildBody({
79
+ state,
80
+ questions,
81
+ model: placement === "body-unless-alias" && model === alias
82
+ ? undefined
83
+ : model,
84
+ media,
85
+ mediaKeys: this.entry.decide.media,
86
+ });
92
87
  }
93
88
  parseDecisionError(status, payload, requestId) {
94
89
  return classifyDecisionError(this.entry.decide.errors, {
@@ -1,5 +1,7 @@
1
+ import { openaiDecisionsDialect } from "./openaiDecisions.js";
1
2
  import { systemOneDialect } from "./systemOne.js";
2
3
  const DIALECTS = {
3
4
  "system-one": systemOneDialect,
5
+ "openai-decisions": openaiDecisionsDialect,
4
6
  };
5
7
  export const getDecisionDialect = (name) => DIALECTS[name];
@@ -0,0 +1,2 @@
1
+ import type { DecisionDialect } from "../../../types/index.js";
2
+ export declare const openaiDecisionsDialect: DecisionDialect;
@@ -0,0 +1,228 @@
1
+ import { asNumber, isRecord } from "../guards.js";
2
+ const PROBABILITY_SUM_TOLERANCE = 0.01;
3
+ // A sum one tolerance away (0.99, 1.01) lands a rounding error past the
4
+ // tolerance in floating point; it is within it.
5
+ const SUM_ROUNDING_SLACK = 1e-9;
6
+ const text = (value) => typeof value === "string" ? value : JSON.stringify(value);
7
+ const described = (value) => value === null || value === undefined ? {} : { description: text(value) };
8
+ function encodeQuestion(question) {
9
+ if (question.type === "boolean") {
10
+ // The wire has no per-side text, so each side joins the instructions.
11
+ const { true: whenTrue, false: whenFalse } = question.criteria ?? {};
12
+ return {
13
+ type: "predicate",
14
+ instructions: text(question.instructions) +
15
+ (whenTrue === null || whenTrue === undefined
16
+ ? ""
17
+ : `\n\nAnswer true when: ${text(whenTrue)}`) +
18
+ (whenFalse === null || whenFalse === undefined
19
+ ? ""
20
+ : `\nAnswer false when: ${text(whenFalse)}`),
21
+ };
22
+ }
23
+ if (question.type === "choice") {
24
+ return {
25
+ type: "choice",
26
+ instructions: text(question.instructions),
27
+ choices: Object.entries(question.criteria).map(([value, rubric]) => ({
28
+ value,
29
+ ...described(rubric),
30
+ })),
31
+ };
32
+ }
33
+ // The index is the label so a returned `value` and `label` can never
34
+ // disagree about which level it is; the rubric text rides in `description`.
35
+ return {
36
+ type: "score",
37
+ instructions: text(question.instructions),
38
+ levels: question.criteria.map((rubric, index) => ({
39
+ label: String(index),
40
+ ...described(rubric),
41
+ })),
42
+ };
43
+ }
44
+ const inUnitRange = (value) => value >= 0 && value <= 1;
45
+ /**
46
+ * The answer's distribution keyed by the keys that were asked, or null unless
47
+ * it names each asked key exactly once, with probabilities that sum to one.
48
+ * The result is in asked order, not the server's.
49
+ */
50
+ function readDistribution(raw, asked, keyOf) {
51
+ if (!Array.isArray(raw) || raw.length !== asked.length) {
52
+ return null;
53
+ }
54
+ const expected = new Set(asked);
55
+ const seen = new Map();
56
+ for (const entry of raw) {
57
+ if (!isRecord(entry)) {
58
+ return null;
59
+ }
60
+ const key = keyOf(entry.value);
61
+ const probability = asNumber(entry.probability);
62
+ if (key === undefined ||
63
+ probability === undefined ||
64
+ !inUnitRange(probability) ||
65
+ !expected.has(key) ||
66
+ seen.has(key)) {
67
+ return null;
68
+ }
69
+ seen.set(key, probability);
70
+ }
71
+ let sum = 0;
72
+ for (const probability of seen.values()) {
73
+ sum += probability;
74
+ }
75
+ if (Math.abs(sum - 1) > PROBABILITY_SUM_TOLERANCE + SUM_ROUNDING_SLACK) {
76
+ return null;
77
+ }
78
+ return Object.fromEntries(asked.map((key) => [key, seen.get(key) ?? 0]));
79
+ }
80
+ const peak = (probabilities) => Math.max(...Object.values(probabilities));
81
+ /** The answer's own confidence, else the peak; null when its own is outside 0 to 1. */
82
+ function readConfidence(raw, probabilities) {
83
+ const own = asNumber(raw);
84
+ if (own === undefined) {
85
+ return peak(probabilities);
86
+ }
87
+ return inUnitRange(own) ? own : null;
88
+ }
89
+ function readChoice(question, raw) {
90
+ const options = Object.keys(question.criteria);
91
+ if (typeof raw.choice !== "string" || !options.includes(raw.choice)) {
92
+ return null;
93
+ }
94
+ const probabilities = readDistribution(raw.probabilities, options, (value) => typeof value === "string" ? value : undefined);
95
+ if (!probabilities) {
96
+ return null;
97
+ }
98
+ const confidence = readConfidence(raw.confidence, probabilities);
99
+ return confidence === null
100
+ ? null
101
+ : { type: "choice", choice: raw.choice, confidence, probabilities };
102
+ }
103
+ function readScore(question, raw) {
104
+ const score = asNumber(raw.score);
105
+ const levels = question.criteria.map((_rubric, index) => String(index));
106
+ const probabilities = readDistribution(raw.probabilities, levels, (value) => typeof value === "number" && Number.isInteger(value)
107
+ ? String(value)
108
+ : undefined);
109
+ // The probability-weighted mean of the level indices cannot leave 0 to n-1.
110
+ if (score === undefined ||
111
+ score < 0 ||
112
+ score > levels.length - 1 ||
113
+ !probabilities) {
114
+ return null;
115
+ }
116
+ const confidence = readConfidence(raw.confidence, probabilities);
117
+ if (confidence === null) {
118
+ return null;
119
+ }
120
+ return {
121
+ type: "score",
122
+ score,
123
+ confidence,
124
+ legend: Object.fromEntries(question.criteria.map((rubric, index) => [
125
+ String(index),
126
+ rubric === null || rubric === undefined ? "" : text(rubric),
127
+ ])),
128
+ probabilities,
129
+ };
130
+ }
131
+ function readAnswer(question, raw) {
132
+ if (question.type === "boolean") {
133
+ const probability = asNumber(raw.probability);
134
+ return raw.type === "predicate" &&
135
+ probability !== undefined &&
136
+ inUnitRange(probability)
137
+ ? { type: "boolean", probability }
138
+ : null;
139
+ }
140
+ if (question.type === "choice") {
141
+ return raw.type === "choice" ? readChoice(question, raw) : null;
142
+ }
143
+ return raw.type === "score" ? readScore(question, raw) : null;
144
+ }
145
+ /** Number of answers carrying each asked name, so a repeated one can be refused. */
146
+ function countAskedNames(raw, asked) {
147
+ const counts = new Map();
148
+ for (const item of raw) {
149
+ const name = isRecord(item) ? item.name : undefined;
150
+ if (typeof name === "string" && Object.hasOwn(asked, name)) {
151
+ counts.set(name, (counts.get(name) ?? 0) + 1);
152
+ }
153
+ }
154
+ return counts;
155
+ }
156
+ export const openaiDecisionsDialect = {
157
+ name: "openai-decisions",
158
+ encodeQuestion,
159
+ buildBody: ({ state, questions, model, media }) => {
160
+ const images = media?.images ?? [];
161
+ const stateText = text(state);
162
+ return {
163
+ ...(model === undefined ? {} : { model }),
164
+ input: images.length === 0
165
+ ? stateText
166
+ : [
167
+ {
168
+ role: "user",
169
+ content: [
170
+ ...(stateText === ""
171
+ ? []
172
+ : [{ type: "input_text", text: stateText }]),
173
+ ...images.map((url) => ({
174
+ type: "input_image",
175
+ image_url: url,
176
+ })),
177
+ ],
178
+ },
179
+ ],
180
+ questions: Object.entries(questions).map(([name, question]) => ({
181
+ ...question,
182
+ name,
183
+ })),
184
+ };
185
+ },
186
+ // The wire echoes each question's `name`, and the server's answer is checked
187
+ // against the question asked: one that fails any check is dropped and named,
188
+ // never repaired, so the other answers in the batch survive.
189
+ readAnswers: (decoded, _reportedConfidence, asked) => {
190
+ const raw = decoded.answers;
191
+ if (!Array.isArray(raw)) {
192
+ return undefined;
193
+ }
194
+ const counts = countAskedNames(raw, asked);
195
+ const answers = new Map();
196
+ const dropped = [];
197
+ const refused = [];
198
+ raw.forEach((item, index) => {
199
+ const record = isRecord(item) ? item : undefined;
200
+ const name = typeof record?.name === "string" ? record.name : undefined;
201
+ const question = name !== undefined && Object.hasOwn(asked, name)
202
+ ? asked[name]
203
+ : undefined;
204
+ if (!record || name === undefined || !question) {
205
+ dropped.push(`#${index}`);
206
+ return;
207
+ }
208
+ if ((counts.get(name) ?? 0) > 1) {
209
+ if (!dropped.includes(name)) {
210
+ dropped.push(name);
211
+ }
212
+ return;
213
+ }
214
+ if (record.type === "refusal") {
215
+ refused.push(name);
216
+ return;
217
+ }
218
+ const answer = readAnswer(question, record);
219
+ if (answer) {
220
+ answers.set(name, answer);
221
+ }
222
+ else {
223
+ dropped.push(name);
224
+ }
225
+ });
226
+ return { answers: Object.fromEntries(answers), dropped, refused };
227
+ },
228
+ };
@@ -83,6 +83,23 @@ function encodeQuestion(question) {
83
83
  export const systemOneDialect = {
84
84
  name: "system-one",
85
85
  encodeQuestion,
86
+ buildBody: ({ state, questions, model, media, mediaKeys }) => {
87
+ const body = model === undefined ? { state, questions } : { model, state, questions };
88
+ if (!mediaKeys || !media) {
89
+ return body;
90
+ }
91
+ // A key is written only for media the request carries: a server may refuse
92
+ // an empty `images`.
93
+ return {
94
+ ...body,
95
+ ...(media.images.length > 0
96
+ ? { [mediaKeys.imagesKey]: media.images }
97
+ : {}),
98
+ ...(media.video && mediaKeys.videoKey
99
+ ? { [mediaKeys.videoKey]: media.video }
100
+ : {}),
101
+ };
102
+ },
86
103
  readAnswers: (decoded, reportedConfidence) => {
87
104
  const raw = decoded.answers;
88
105
  if (!isRecord(raw)) {
@@ -297,7 +297,7 @@ export class SystemOneDecisionProvider extends BaseProvider {
297
297
  }
298
298
  const decoded = this.readDecisionPayload(payload);
299
299
  const reading = isRecord(decoded)
300
- ? this.dialect().readAnswers(decoded, this.reportedConfidence(decoded))
300
+ ? this.dialect().readAnswers(decoded, this.reportedConfidence(decoded), request.questions)
301
301
  : undefined;
302
302
  if (!isRecord(decoded) || !reading) {
303
303
  throw this.decisionError({
@@ -313,6 +313,11 @@ export class SystemOneDecisionProvider extends BaseProvider {
313
313
  requestId,
314
314
  });
315
315
  }
316
+ for (const id of reading.refused ?? []) {
317
+ logger.warn(`${label}: the model declined to answer "${id}"`, {
318
+ requestId,
319
+ });
320
+ }
316
321
  const answers = { ...reading.answers };
317
322
  const usage = isRecord(decoded.usage) ? decoded.usage : {};
318
323
  return {
@@ -130,6 +130,12 @@ export declare const LANGFUSE_ATTR: {
130
130
  };
131
131
  /** Default ceiling for serialized span attribute values. */
132
132
  export declare const SPAN_ATTRIBUTE_MAX_CHARS = 40000;
133
+ /**
134
+ * The ceiling in force: `NEUROLINK_SPAN_ATTRIBUTE_MAX_CHARS` when it is a
135
+ * positive whole number (raised to the minimum if lower), otherwise the
136
+ * default. Read on every call, so a change applies to the next span.
137
+ */
138
+ export declare function spanAttributeMaxChars(): number;
133
139
  /**
134
140
  * Returns a copy of `value` with credentials masked: values under keys such as
135
141
  * `access_token`, `password`, `authorization` or `set-cookie`, bearer tokens
@@ -161,8 +167,9 @@ export declare function spanJsonAttribute(value: unknown, maxChars?: number): st
161
167
  *
162
168
  * A head cut keeps the system prompt and drops everything after it, so a long
163
169
  * system prompt hides every message, the user's question included. Here the
164
- * prompt and the newest messages are budgeted first, older messages are
165
- * dropped whole and counted, and the system prompt gets whatever room is left.
166
- * The result is always valid JSON.
170
+ * prompt, the current question and this turn's newest messages are budgeted
171
+ * first, earlier messages are dropped whole and counted, and the system prompt
172
+ * gets whatever room is left. A tool result is never kept without the call it
173
+ * answers. The result is always valid JSON.
167
174
  */
168
175
  export declare function spanModelRequestAttribute(rawRequest: SpanModelRequest, maxChars?: number): string;