@rulvar/openai 1.125.0 → 1.126.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/dist/index.d.ts CHANGED
@@ -200,6 +200,20 @@ interface ReconcileStatementOptions {
200
200
  totalToleranceUsd?: number;
201
201
  /** Provider-side model name of a served ref; default strips the adapter prefix. */
202
202
  modelOf?: (servedBy: ModelRef) => string;
203
+ /**
204
+ * How provider-reported token counts weigh on the verdict (RV903).
205
+ * 'verdict' (default): any token disagreement between the export and
206
+ * our recorded usage is a divergence, because our counts ARE the
207
+ * provider's own wire-reported numbers, so an export that disagrees
208
+ * with them describes a different request than the wire served, and
209
+ * dollars derived from either cannot be trusted to mean the same
210
+ * thing. 'informational' preserves the pre-v1.126 dollar-only
211
+ * verdict for exports whose token semantics legitimately differ from
212
+ * the wire's (a different cache accounting, rounded aggregates):
213
+ * mismatches are still counted and sampled, but only dollar deltas
214
+ * decide.
215
+ */
216
+ tokenComparison?: "verdict" | "informational";
203
217
  }
204
218
  /** One (model, component) line of the reconciliation. */
205
219
  interface ComponentDelta {
@@ -244,7 +258,12 @@ interface StatementReconciliation {
244
258
  components: ComponentDelta[];
245
259
  /** The lines beyond tolerance, largest |delta| first: the named divergences. */
246
260
  divergent: ComponentDelta[];
247
- /** Sample of token disagreements between the export and our recorded usage (requests mode). */
261
+ /**
262
+ * Token disagreements between the export and our recorded usage
263
+ * (requests mode). Under the default tokenComparison 'verdict' any
264
+ * mismatch makes the verdict 'divergence'; under 'informational' the
265
+ * count and sample still report, advisory only (RV903).
266
+ */
248
267
  tokenMismatches: number;
249
268
  tokenMismatchSample: Array<{
250
269
  responseId: string;
@@ -264,8 +283,11 @@ interface StatementReconciliation {
264
283
  * journal-free; see the module doc for the contract. Throws a typed
265
284
  * ConfigError on inputs that cannot be evidence: an empty statement (a
266
285
  * headline total with no rows), a request row without a response id, a
267
- * duplicate response id (an ambiguous join), or a request export whose
268
- * rows carry neither dollars, components, nor usage.
286
+ * duplicate response id (an ambiguous join), a request export whose
287
+ * rows carry neither dollars, components, nor usage, any non-finite or
288
+ * negative dollar amount, any non-integer or negative token count, or
289
+ * a non-finite or negative tolerance (RV903: a statement that cannot
290
+ * be summed must refuse loudly, never verdict 'match' on NaN totals).
269
291
  */
270
292
  declare function reconcileStatement(invoice: {
271
293
  rows: readonly InvoiceRow[];
package/dist/index.js CHANGED
@@ -1084,17 +1084,40 @@ const defaultModelOf = (servedBy) => {
1084
1084
  return colon === -1 ? servedBy : servedBy.slice(colon + 1);
1085
1085
  };
1086
1086
  /**
1087
+ * A statement dollar amount must be a finite nonnegative number
1088
+ * (RV903). The thirteenth experiment's probe fed `usd: NaN` and got
1089
+ * verdict 'match' with NaN totals: NaN flowed through the sums and
1090
+ * `Math.abs(NaN) > tolerance` is false, so the divergence check
1091
+ * silently disarmed. Negative amounts are refused too: provider
1092
+ * credits and adjustments are real, but they are not per-request or
1093
+ * per-component BILLING evidence, and folding them into the join would
1094
+ * let an adjustment mask a rate divergence of the same size.
1095
+ */
1096
+ function assertStatementUsd(where, field, value) {
1097
+ if (!Number.isFinite(value)) throw new ConfigError(`statement reconciliation refused: ${where} carries ${field} ${String(value)}, which cannot be summed; a statement whose dollars are not finite is not evidence`);
1098
+ if (value < 0) throw new ConfigError(`statement reconciliation refused: ${where} carries negative ${field} ${String(value)}; credits and adjustments reconcile separately, never as negative statement rows`);
1099
+ }
1100
+ /** A provider-reported token count must be a nonnegative integer (RV903). */
1101
+ function assertTokenCount(where, field, value) {
1102
+ if (!Number.isInteger(value) || value < 0) throw new ConfigError(`statement reconciliation refused: ${where} carries ${field} ${String(value)}; provider-reported token counts are nonnegative integers`);
1103
+ }
1104
+ /**
1087
1105
  * Reconciles the invoice against a normalized provider export. Pure and
1088
1106
  * journal-free; see the module doc for the contract. Throws a typed
1089
1107
  * ConfigError on inputs that cannot be evidence: an empty statement (a
1090
1108
  * headline total with no rows), a request row without a response id, a
1091
- * duplicate response id (an ambiguous join), or a request export whose
1092
- * rows carry neither dollars, components, nor usage.
1109
+ * duplicate response id (an ambiguous join), a request export whose
1110
+ * rows carry neither dollars, components, nor usage, any non-finite or
1111
+ * negative dollar amount, any non-integer or negative token count, or
1112
+ * a non-finite or negative tolerance (RV903: a statement that cannot
1113
+ * be summed must refuse loudly, never verdict 'match' on NaN totals).
1093
1114
  */
1094
1115
  function reconcileStatement(invoice, statement, options) {
1116
+ for (const [name, value] of [["componentToleranceUsd", options.componentToleranceUsd], ["totalToleranceUsd", options.totalToleranceUsd]]) if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new ConfigError(`statement reconciliation refused: ${name} ${String(value)} is not a finite nonnegative dollar tolerance`);
1095
1117
  const componentToleranceUsd = options.componentToleranceUsd ?? .005;
1096
1118
  const totalToleranceUsd = options.totalToleranceUsd ?? .01;
1097
1119
  const modelOf = options.modelOf ?? defaultModelOf;
1120
+ const tokenComparison = options.tokenComparison ?? "verdict";
1098
1121
  if (statement.rows.length === 0) throw new ConfigError("statement reconciliation refused: the statement carries no rows. A headline total is not evidence (dashboard aggregates are eventually consistent); export per-request rows or per-component categories and reconcile those");
1099
1122
  const billable = [];
1100
1123
  let usageUnknownRows = 0;
@@ -1122,6 +1145,21 @@ function reconcileStatement(invoice, statement, options) {
1122
1145
  for (const row of statement.rows) {
1123
1146
  if (row.responseId === "") throw new ConfigError("statement reconciliation refused: a per-request export row has no response id, the join key; normalize the export or reconcile per-component categories instead");
1124
1147
  if (byId.has(row.responseId)) throw new ConfigError(`statement reconciliation refused: duplicate response id '${row.responseId}' in the export makes the join ambiguous`);
1148
+ const where = `row '${row.responseId}'`;
1149
+ if (row.usd !== void 0) assertStatementUsd(where, "usd", row.usd);
1150
+ if (row.componentsUsd !== void 0) for (const component of COMPONENTS) {
1151
+ const usd = row.componentsUsd[component];
1152
+ if (usd !== void 0) assertStatementUsd(where, `componentsUsd.${component}`, usd);
1153
+ }
1154
+ if (row.usage !== void 0) for (const field of [
1155
+ "inputTokens",
1156
+ "cachedInputTokens",
1157
+ "cacheWriteTokens",
1158
+ "outputTokens"
1159
+ ]) {
1160
+ const count = row.usage[field];
1161
+ if (count !== void 0) assertTokenCount(where, `usage.${field}`, count);
1162
+ }
1125
1163
  byId.set(row.responseId, row);
1126
1164
  if (row.usd !== void 0 || row.componentsUsd !== void 0 || row.usage !== void 0) carriesAnything = true;
1127
1165
  }
@@ -1202,6 +1240,7 @@ function reconcileStatement(invoice, statement, options) {
1202
1240
  statementComponents = /* @__PURE__ */ new Map();
1203
1241
  let total = 0;
1204
1242
  for (const row of statement.rows) {
1243
+ assertStatementUsd(`category row '${row.model}' ${row.component}`, "usd", row.usd);
1205
1244
  const sums = statementComponents.get(row.model) ?? {};
1206
1245
  sums[row.component] = (sums[row.component] ?? 0) + row.usd;
1207
1246
  statementComponents.set(row.model, sums);
@@ -1279,8 +1318,9 @@ function reconcileStatement(invoice, statement, options) {
1279
1318
  const totalsDelta = statementTotalUsd === void 0 ? void 0 : statementTotalUsd - ourUsd;
1280
1319
  const totalsDivergent = statementComponents === void 0 && totalsDelta !== void 0 && Math.abs(totalsDelta) > totalToleranceUsd;
1281
1320
  const coverageComplete = unmatchedRows === 0 && statementOnlyRows === 0 && unpricedModels.size === 0 && (statement.kind === "categories" || matchedRows === rowsWithResponseId && rowsWithResponseId === billable.length) && (statement.kind === "requests" || components.every((line) => line.statementUsd !== void 0));
1321
+ const tokensDivergent = tokenComparison === "verdict" && tokenMismatches > 0;
1282
1322
  let verdict;
1283
- if (divergent.length > 0 || totalsDivergent) verdict = "divergence";
1323
+ if (divergent.length > 0 || totalsDivergent || tokensDivergent) verdict = "divergence";
1284
1324
  else if (matchedRows === 0) verdict = "no-overlap";
1285
1325
  else if (!coverageComplete) verdict = "partial-coverage";
1286
1326
  else verdict = "match";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/openai",
3
- "version": "1.125.0",
3
+ "version": "1.126.0",
4
4
  "description": "Rulvar first-class provider adapter for the OpenAI Responses API, plus the openaiCompatible factory.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -23,13 +23,13 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "openai": "^6.49.0",
26
- "@rulvar/core": "1.125.0"
26
+ "@rulvar/core": "1.126.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.20.1",
30
30
  "tsdown": "^0.22.14",
31
31
  "typescript": "~6.0.3",
32
- "@rulvar/testing": "1.125.0"
32
+ "@rulvar/testing": "1.126.0"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",