rightmodeler 0.2.1 → 0.4.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.
@@ -0,0 +1,89 @@
1
+ const DATED_SNAPSHOT = /^\d{2,4}(?:-?\d{2}){1,2}$/;
2
+ const PORTKEY_CACHE_HITS = new Set(["hit", "semantic hit"]);
3
+ function objectOf(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value)
5
+ ? value
6
+ : undefined;
7
+ }
8
+ function nonEmptyString(value) {
9
+ return typeof value === "string" && value.length > 0 ? value : undefined;
10
+ }
11
+ export function servedModel(body) {
12
+ return nonEmptyString(objectOf(body)?.model);
13
+ }
14
+ function modelParts(id) {
15
+ const segments = id.toLowerCase().split("/");
16
+ return {
17
+ name: segments[segments.length - 1],
18
+ ...(segments.length > 1 ? { vendor: segments[segments.length - 2] } : {}),
19
+ };
20
+ }
21
+ export function sameModel(requested, served) {
22
+ const want = modelParts(requested);
23
+ const got = modelParts(served);
24
+ if (want.vendor !== undefined &&
25
+ got.vendor !== undefined &&
26
+ want.vendor !== got.vendor) {
27
+ return false;
28
+ }
29
+ if (got.name === want.name)
30
+ return true;
31
+ const prefix = `${want.name}-`;
32
+ return (got.name.startsWith(prefix) &&
33
+ DATED_SNAPSHOT.test(got.name.slice(prefix.length)));
34
+ }
35
+ function header(headers, name) {
36
+ if (headers instanceof Headers)
37
+ return headers.get(name) ?? undefined;
38
+ const value = headers[name];
39
+ return typeof value === "string" ? value : value?.join(", ");
40
+ }
41
+ function substitution(kind, evidence) {
42
+ return { kind, evidence: evidence.slice(0, 200) };
43
+ }
44
+ export function responseSubstitution(input) {
45
+ const body = objectOf(input.body);
46
+ const extra = objectOf(body?.extra_fields);
47
+ const served = servedModel(body);
48
+ if (served !== undefined && !sameModel(input.requestedModel, served)) {
49
+ return substitution("model", `served ${served} for requested ${input.requestedModel}`);
50
+ }
51
+ const fallback = nonEmptyString(objectOf(extra?.routing_info)?.server_side_fallback_model);
52
+ if (fallback !== undefined) {
53
+ return substitution("model", `bifrost server-side fallback served ${fallback}`);
54
+ }
55
+ const cacheStatus = header(input.headers, "x-portkey-cache-status");
56
+ if (cacheStatus !== undefined &&
57
+ PORTKEY_CACHE_HITS.has(cacheStatus.toLowerCase())) {
58
+ return substitution("cache", `x-portkey-cache-status: ${cacheStatus}`);
59
+ }
60
+ const cacheDebug = objectOf(extra?.cache_debug);
61
+ if (cacheDebug?.cache_hit === true) {
62
+ const hitType = cacheDebug.hit_type;
63
+ return substitution("cache", `bifrost cache hit${typeof hitType === "string" ? ` (${hitType})` : ""}`);
64
+ }
65
+ const hookResults = objectOf(body?.hook_results);
66
+ const transformed = [
67
+ hookResults?.before_request_hooks,
68
+ hookResults?.after_request_hooks,
69
+ ]
70
+ .flatMap((hooks) => (Array.isArray(hooks) ? hooks : []))
71
+ .map(objectOf)
72
+ .find((hook) => hook?.transformed === true);
73
+ if (transformed !== undefined) {
74
+ const id = nonEmptyString(transformed.id) ?? "unnamed";
75
+ return substitution("request", `portkey hook ${id} transformed the call`);
76
+ }
77
+ const dropped = [
78
+ extra?.dropped_compat_plugin_params,
79
+ extra?.dropped_unsupported_tools,
80
+ ].flatMap((items) => (Array.isArray(items) ? items : []));
81
+ if (dropped.length > 0) {
82
+ return substitution("request", `bifrost dropped ${dropped.join(", ")}`);
83
+ }
84
+ const converted = nonEmptyString(extra?.converted_request_type);
85
+ if (converted !== undefined) {
86
+ return substitution("request", `bifrost converted the request to ${converted}`);
87
+ }
88
+ return undefined;
89
+ }
@@ -30,6 +30,11 @@ const heartbeat = setInterval(
30
30
  500,
31
31
  );
32
32
  heartbeat.unref();
33
+ const deadlineMs = Number(process.env.RM_DEADLINE_MS ?? 0);
34
+ let active = null;
35
+ if (deadlineMs > 0) {
36
+ setTimeout(() => active?.kill("SIGKILL"), deadlineMs).unref();
37
+ }
33
38
 
34
39
  function start(command, stdoutPath, stderrPath, flags = "w") {
35
40
  const stdout = createWriteStream(stdoutPath, { flags });
@@ -38,6 +43,7 @@ function start(command, stdoutPath, stderrPath, flags = "w") {
38
43
  cwd: APP_ROOT,
39
44
  stdio: ["ignore", "pipe", "pipe"],
40
45
  });
46
+ active = child;
41
47
  child.stdout.pipe(stdout);
42
48
  child.stderr.pipe(stderr);
43
49
  const closed = new Promise((resolve, reject) => {
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import { PassThrough } from "node:stream";
13
13
 
14
14
  import { hopByHopHeaders } from "./headers.js";
15
+ import { responseSubstitution, servedModel } from "../provenance.js";
15
16
  import { classifyStream } from "../transport/stream.js";
16
17
 
17
18
  const maxRequestBytes = 10 * 1024 * 1024;
@@ -39,6 +40,12 @@ function isObject(value) {
39
40
  return typeof value === "object" && value !== null && !Array.isArray(value);
40
41
  }
41
42
 
43
+ // The same 1e-12 snap as roundUsd in ../budget.ts, which this runtime ships without:
44
+ // lease sums then do not depend on the order requests settle in.
45
+ function roundUsd(value) {
46
+ return Number(value.toFixed(12));
47
+ }
48
+
42
49
  function parseConfig() {
43
50
  const rawSwapPolicy = jsonEnv("RM_SWAP_POLICY");
44
51
  if (
@@ -69,7 +76,32 @@ function parseConfig() {
69
76
  "RM_PRICING_TABLE values must contain non-negative input and output prices",
70
77
  );
71
78
  }
72
- pricingTable[model] = { input: pricing.input, output: pricing.output };
79
+ if (
80
+ pricing.maxOutputTokens !== undefined &&
81
+ (!Number.isSafeInteger(pricing.maxOutputTokens) ||
82
+ pricing.maxOutputTokens <= 0)
83
+ ) {
84
+ throw new Error(
85
+ "RM_PRICING_TABLE maxOutputTokens must be a positive integer",
86
+ );
87
+ }
88
+ pricingTable[model] = {
89
+ input: pricing.input,
90
+ output: pricing.output,
91
+ ...(pricing.maxOutputTokens === undefined
92
+ ? {}
93
+ : { maxOutputTokens: pricing.maxOutputTokens }),
94
+ };
95
+ }
96
+
97
+ const defaultMaxOutputTokens = Number(
98
+ requiredEnv("RM_DEFAULT_MAX_OUTPUT_TOKENS"),
99
+ );
100
+ if (
101
+ !Number.isSafeInteger(defaultMaxOutputTokens) ||
102
+ defaultMaxOutputTokens <= 0
103
+ ) {
104
+ throw new Error("RM_DEFAULT_MAX_OUTPUT_TOKENS must be a positive integer");
73
105
  }
74
106
 
75
107
  const lease = jsonEnv("RM_BUDGET_LEASE");
@@ -88,6 +120,21 @@ function parseConfig() {
88
120
  throw new Error("RM_EGRESS_URL must use http or https");
89
121
  }
90
122
 
123
+ const requestHeaders =
124
+ process.env.RM_REQUEST_HEADERS === undefined
125
+ ? {}
126
+ : jsonEnv("RM_REQUEST_HEADERS");
127
+ if (
128
+ !isObject(requestHeaders) ||
129
+ Object.entries(requestHeaders).some(
130
+ ([name, value]) => name.length === 0 || typeof value !== "string",
131
+ )
132
+ ) {
133
+ throw new Error(
134
+ "RM_REQUEST_HEADERS must map header names to string values",
135
+ );
136
+ }
137
+
91
138
  return {
92
139
  runId: requiredEnv("RM_RUN_ID"),
93
140
  caseId: requiredEnv("RM_CASE_ID"),
@@ -98,7 +145,9 @@ function parseConfig() {
98
145
  egressUrl,
99
146
  swapPolicy,
100
147
  pricingTable,
148
+ defaultMaxOutputTokens,
101
149
  lease,
150
+ requestHeaders,
102
151
  };
103
152
  }
104
153
 
@@ -184,11 +233,11 @@ function loadState(spoolPath, checkpointPath) {
184
233
  state.lastAttemptGroup = Math.max(state.lastAttemptGroup, row.attemptGroup);
185
234
  state.groups.set(row.logicalCallId, row.attemptGroup);
186
235
  state.attemptIds.add(row.attemptId);
187
- state.spentUsd += row.costUsd;
236
+ state.spentUsd = roundUsd(state.spentUsd + row.costUsd);
188
237
  }
189
238
 
190
239
  for (const reservation of state.reservations.values()) {
191
- state.spentUsd += reservation.reservedUsd;
240
+ state.spentUsd = roundUsd(state.spentUsd + reservation.reservedUsd);
192
241
  }
193
242
 
194
243
  return state;
@@ -222,12 +271,13 @@ function responseHeaders(headers) {
222
271
  return forwarded;
223
272
  }
224
273
 
225
- function requestHeaders(headers, bodyLength) {
274
+ function requestHeaders(headers, bodyLength, configured) {
226
275
  const forwarded = {};
227
276
  for (const [name, value] of Object.entries(headers)) {
228
277
  if (
229
278
  value !== undefined &&
230
279
  !hopByHopHeaders.has(name) &&
280
+ name !== "accept-encoding" &&
231
281
  name !== "authorization" &&
232
282
  name !== "host" &&
233
283
  name !== "content-length"
@@ -235,6 +285,8 @@ function requestHeaders(headers, bodyLength) {
235
285
  forwarded[name] = value;
236
286
  }
237
287
  }
288
+ Object.assign(forwarded, configured);
289
+ forwarded["accept-encoding"] = "identity";
238
290
  forwarded["content-length"] = String(bodyLength);
239
291
  return forwarded;
240
292
  }
@@ -288,7 +340,11 @@ function requestUpstream(
288
340
  return new Promise((resolve, reject) => {
289
341
  const request = send(
290
342
  url,
291
- { method, path: requestTarget, headers },
343
+ {
344
+ method,
345
+ path: `${url.pathname.replace(/\/$/, "")}${requestTarget}`,
346
+ headers,
347
+ },
292
348
  (response) => {
293
349
  clearTimeout(deadline);
294
350
  resolve(response);
@@ -437,6 +493,7 @@ async function forwardStreaming(upstream, outgoing, status, spoolSink) {
437
493
  spoolPath: result.spoolPath ?? null,
438
494
  finishedWithoutSentinel: result.finishedWithoutSentinel === true,
439
495
  upstreamFailed,
496
+ model: result.model ?? null,
440
497
  };
441
498
  }
442
499
 
@@ -489,6 +546,7 @@ async function forwardNonStreaming(upstream, outgoing, status) {
489
546
  streamOutcome: "completed",
490
547
  usage: isObject(body) ? (body.usage ?? null) : null,
491
548
  upstreamFailed: false,
549
+ body,
492
550
  };
493
551
  } catch {
494
552
  return { streamOutcome: "truncated", usage: null, upstreamFailed: false };
@@ -624,13 +682,14 @@ async function main() {
624
682
  return;
625
683
  }
626
684
 
627
- const maxTokens = parsed?.max_tokens;
685
+ const requestedLimit =
686
+ parsed?.max_completion_tokens ?? parsed?.max_tokens ?? undefined;
628
687
  if (
629
688
  !isObject(parsed) ||
630
689
  typeof parsed.model !== "string" ||
631
690
  parsed.model.length === 0 ||
632
- !Number.isSafeInteger(maxTokens) ||
633
- maxTokens < 0
691
+ (requestedLimit !== undefined &&
692
+ (!Number.isSafeInteger(requestedLimit) || requestedLimit < 0))
634
693
  ) {
635
694
  recordLost({
636
695
  attemptGroup,
@@ -640,13 +699,26 @@ async function main() {
640
699
  startedAt,
641
700
  });
642
701
  sendJson(outgoing, 400, {
643
- error: "Request body requires model and a non-negative max_tokens.",
702
+ error:
703
+ "Request body requires model; max_completion_tokens or max_tokens must be a non-negative integer when present.",
644
704
  });
645
705
  return;
646
706
  }
647
707
 
708
+ const swapped = stepId in config.swapPolicy;
648
709
  const model = config.swapPolicy[stepId] ?? parsed.model;
649
- const rewritten = { ...parsed, model };
710
+ const rewritten = {
711
+ ...parsed,
712
+ model,
713
+ ...(parsed.stream === true
714
+ ? {
715
+ stream_options: {
716
+ ...(isObject(parsed.stream_options) ? parsed.stream_options : {}),
717
+ include_usage: true,
718
+ },
719
+ }
720
+ : {}),
721
+ };
650
722
  const forwardedBody = Buffer.from(JSON.stringify(rewritten));
651
723
  const pricing = config.pricingTable[model];
652
724
  if (pricing === undefined) {
@@ -662,13 +734,18 @@ async function main() {
662
734
  });
663
735
  return;
664
736
  }
737
+ const maxTokens =
738
+ requestedLimit ??
739
+ pricing.maxOutputTokens ??
740
+ config.defaultMaxOutputTokens;
665
741
 
666
742
  const estimatedInputTokens = forwardedBody.length;
667
743
  const estimatedWorstCaseUsd =
668
744
  estimatedInputTokens * pricing.input + maxTokens * pricing.output;
669
- const requiredLeaseUsd =
670
- state.spentUsd + reservedUsd + estimatedWorstCaseUsd;
671
- if (requiredLeaseUsd > config.lease.maxUsd) {
745
+ const requiredLeaseUsd = roundUsd(
746
+ state.spentUsd + reservedUsd + estimatedWorstCaseUsd,
747
+ );
748
+ if (requiredLeaseUsd > roundUsd(config.lease.maxUsd)) {
672
749
  appendRow(spoolPath, {
673
750
  kind: "blocked",
674
751
  runId: config.runId,
@@ -693,7 +770,7 @@ async function main() {
693
770
  return;
694
771
  }
695
772
 
696
- reservedUsd += estimatedWorstCaseUsd;
773
+ reservedUsd = roundUsd(reservedUsd + estimatedWorstCaseUsd);
697
774
  const attemptId = nextAttemptId(state);
698
775
  const responseSpoolPath = join(streamDirectory, `${attemptId}.txt`);
699
776
  appendRow(spoolPath, {
@@ -726,21 +803,25 @@ async function main() {
726
803
  upstreamSource: null,
727
804
  upstreamFailed: false,
728
805
  };
806
+ let provenance = {};
729
807
  try {
730
808
  const upstream = await requestUpstream(
731
809
  config.egressUrl,
732
810
  incoming.url ?? "/",
733
811
  incoming.method ?? "POST",
734
- requestHeaders(incoming.headers, forwardedBody.length),
812
+ requestHeaders(
813
+ incoming.headers,
814
+ forwardedBody.length,
815
+ config.requestHeaders,
816
+ ),
735
817
  forwardedBody,
736
818
  streamHardDeadlineMs,
737
819
  );
738
820
  const status = upstream.statusCode ?? 502;
739
- const declaredSource = upstream.headers[egressSourceHeader];
740
821
  const upstreamSource =
741
- declaredSource === "provider" || declaredSource === "egress"
742
- ? declaredSource
743
- : null;
822
+ upstream.headers[egressSourceHeader] === "egress"
823
+ ? "egress"
824
+ : "provider";
744
825
  const forwarded =
745
826
  rewritten.stream === true && status < 400
746
827
  ? await forwardStreaming(upstream, outgoing, status, {
@@ -755,6 +836,23 @@ async function main() {
755
836
  upstreamStatus: status,
756
837
  upstreamSource: forwarded.upstreamFailed ? "egress" : upstreamSource,
757
838
  };
839
+ if (swapped && status < 400) {
840
+ const answered =
841
+ forwarded.body ??
842
+ (typeof forwarded.model === "string"
843
+ ? { model: forwarded.model }
844
+ : undefined);
845
+ const served = servedModel(answered);
846
+ const substitution = responseSubstitution({
847
+ requestedModel: model,
848
+ headers: upstream.headers,
849
+ body: answered,
850
+ });
851
+ provenance = {
852
+ ...(served === undefined ? {} : { servedModel: served }),
853
+ ...(substitution === undefined ? {} : { substitution }),
854
+ };
855
+ }
758
856
  } catch {
759
857
  if (!outgoing.headersSent) {
760
858
  sendJson(outgoing, 502, { error: "Egress request failed." });
@@ -790,6 +888,7 @@ async function main() {
790
888
  ...(result.finishedWithoutSentinel
791
889
  ? { finishedWithoutSentinel: true }
792
890
  : {}),
891
+ ...provenance,
793
892
  usage,
794
893
  responseSpoolPath: result.spoolPath,
795
894
  costUsd: leaseChargeUsd,
@@ -800,10 +899,10 @@ async function main() {
800
899
  endedAt: new Date().toISOString(),
801
900
  });
802
901
  state.reservations.delete(attemptId);
803
- state.spentUsd += leaseChargeUsd;
902
+ state.spentUsd = roundUsd(state.spentUsd + leaseChargeUsd);
804
903
  if (!outgoing.destroyed && !outgoing.writableEnded) outgoing.end();
805
904
  } finally {
806
- reservedUsd -= estimatedWorstCaseUsd;
905
+ reservedUsd = roundUsd(reservedUsd - estimatedWorstCaseUsd);
807
906
  }
808
907
  }
809
908
 
@@ -65,13 +65,13 @@ function parseUsage(value) {
65
65
  if (!isRecord(value) ||
66
66
  typeof value.prompt_tokens !== "number" ||
67
67
  typeof value.completion_tokens !== "number" ||
68
- typeof value.total_tokens !== "number") {
68
+ (value.total_tokens !== undefined && typeof value.total_tokens !== "number")) {
69
69
  return null;
70
70
  }
71
71
  return {
72
72
  inputTokens: value.prompt_tokens,
73
73
  outputTokens: value.completion_tokens,
74
- totalTokens: value.total_tokens,
74
+ totalTokens: value.total_tokens ?? value.prompt_tokens + value.completion_tokens,
75
75
  };
76
76
  }
77
77
  function parseEvent(data) {
@@ -121,6 +121,9 @@ function parseEvent(data) {
121
121
  usage: value.usage === undefined || value.usage === null
122
122
  ? null
123
123
  : parseUsage(value.usage),
124
+ ...(typeof value.model === "string" && value.model.length > 0
125
+ ? { model: value.model }
126
+ : {}),
124
127
  };
125
128
  }
126
129
  function eventData(event) {
@@ -144,6 +147,7 @@ export async function classifyStream(byteStream, options) {
144
147
  const collector = new ContentCollector(options.spoolSink, processingController.signal);
145
148
  let chunks = 0;
146
149
  let usage = null;
150
+ let model;
147
151
  let sawFinish = false;
148
152
  let selectedResult;
149
153
  const result = (outcome, reason, finishedWithoutSentinel = false) => {
@@ -154,6 +158,7 @@ export async function classifyStream(byteStream, options) {
154
158
  usage,
155
159
  chunks,
156
160
  ...(finishedWithoutSentinel ? { finishedWithoutSentinel: true } : {}),
161
+ ...(model === undefined ? {} : { model }),
157
162
  };
158
163
  return selectedResult;
159
164
  };
@@ -283,6 +288,7 @@ export async function classifyStream(byteStream, options) {
283
288
  throw error;
284
289
  }
285
290
  usage = event.usage ?? usage;
291
+ model ??= event.model;
286
292
  sawFinish ||= event.finished;
287
293
  }
288
294
  }