@broberg/ai-sdk 0.41.1 → 0.42.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
@@ -608,8 +608,28 @@ interface ClassifyInput {
608
608
  purpose?: string;
609
609
  }
610
610
  interface ClassifyResult {
611
- label: string;
612
- confidence: number;
611
+ /** The chosen label, or `null` when the model named a label that is not in `labels`
612
+ * (F052).
613
+ *
614
+ * MEASURED, not taken from the report: a reply with no JSON in it at all already
615
+ * THREW (`parseJsonLoose`), and still does. So the silent fallback only ever fired on
616
+ * a *parseable* answer naming an unknown label — a narrower hole than reported, and
617
+ * the more dangerous half, because that answer looks like a real classification.
618
+ *
619
+ * Until 0.41.1 this fell back to `labels[0]`, so "the model chose the first one" and
620
+ * "the model could not answer" were the SAME VALUE at the call site. helpdesk routes
621
+ * an autonomy level off this field — an unclassifiable ticket therefore landed on the
622
+ * tenant's first intent, chosen by the order of a config array, with no error and no
623
+ * log trace. `null` is the whole fix: there is no field to hardcode, because the
624
+ * absence IS the signal. */
625
+ label: string | null;
626
+ /** The model's own answer when it did not match, so a caller can log or route what
627
+ * actually came back instead of only knowing that something did not. */
628
+ rawLabel?: string;
629
+ /** `null` when the model reported no confidence. `0` is a REAL confidence and stays
630
+ * `0` — the two used to be the same number, which made the field unusable as a
631
+ * signal even for a caller who wanted to check it. */
632
+ confidence: number | null;
613
633
  usage: Usage;
614
634
  }
615
635
  interface RerankInput {
@@ -619,10 +639,18 @@ interface RerankInput {
619
639
  purpose?: string;
620
640
  }
621
641
  interface RerankResult {
642
+ /** Every entry is an item FROM `input.items` with a real numeric score. Items the
643
+ * model invented are dropped; items it failed to score are named in `unscored`. */
622
644
  ranked: {
623
645
  item: string;
624
646
  score: number;
625
647
  }[];
648
+ /** Input items the model returned no usable score for. Empty on a clean answer.
649
+ *
650
+ * Not an error — but a caller treating `ranked` as complete must look here (F052).
651
+ * A missing item used to become `""` with score `0`, i.e. a plausible-looking entry
652
+ * ranked last. */
653
+ unscored: string[];
626
654
  usage: Usage;
627
655
  }
628
656
  interface Contracts {
@@ -2338,8 +2366,8 @@ declare const falStubAdapter: ProviderAdapter;
2338
2366
  * wires the live adapters. */
2339
2367
  declare const stubProviders: Record<string, ProviderAdapter>;
2340
2368
 
2341
- declare const VERSION: "0.41.1";
2342
- declare const SDK_TAG: "@broberg/ai-sdk@0.41.1";
2369
+ declare const VERSION: "0.42.0";
2370
+ declare const SDK_TAG: "@broberg/ai-sdk@0.42.0";
2343
2371
 
2344
2372
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2345
2373
  * per-call override.
package/dist/index.js CHANGED
@@ -2694,9 +2694,14 @@ ${input.text}`,
2694
2694
  purpose: input.purpose ?? "contract:classify"
2695
2695
  });
2696
2696
  const parsed = parseJsonLoose(res.text);
2697
- const label = input.labels.includes(parsed.label ?? "") ? parsed.label : input.labels[0] ?? "";
2698
- const confidence = typeof parsed.confidence === "number" ? parsed.confidence : 0;
2699
- return { label, confidence, usage: res.usage };
2697
+ const matched = typeof parsed.label === "string" && input.labels.includes(parsed.label);
2698
+ return {
2699
+ label: matched ? parsed.label : null,
2700
+ ...matched ? {} : { rawLabel: typeof parsed.label === "string" ? parsed.label : res.text.slice(0, 200) },
2701
+ // 0 is a real confidence; "no confidence reported" is not 0.
2702
+ confidence: typeof parsed.confidence === "number" ? parsed.confidence : null,
2703
+ usage: res.usage
2704
+ };
2700
2705
  },
2701
2706
  async rerank(input) {
2702
2707
  const res = await client.chat({
@@ -2709,8 +2714,20 @@ ${JSON.stringify(input.items)}`,
2709
2714
  purpose: input.purpose ?? "contract:rerank"
2710
2715
  });
2711
2716
  const raw = parseJsonLoose(res.text);
2712
- const ranked = (Array.isArray(raw) ? raw : []).map((r) => ({ item: String(r.item ?? ""), score: typeof r.score === "number" ? r.score : 0 })).sort((a, b) => b.score - a.score);
2713
- return { ranked, usage: res.usage };
2717
+ if (!Array.isArray(raw)) {
2718
+ throw new Error(
2719
+ `ai.contracts.rerank: the model did not return a JSON array. Got: ${res.text.slice(0, 200)}${res.text.length > 200 ? "\u2026" : ""}`
2720
+ );
2721
+ }
2722
+ const scored = /* @__PURE__ */ new Map();
2723
+ for (const r of raw) {
2724
+ if (typeof r?.item !== "string" || !input.items.includes(r.item)) continue;
2725
+ if (typeof r?.score !== "number") continue;
2726
+ if (!scored.has(r.item)) scored.set(r.item, r.score);
2727
+ }
2728
+ const ranked = [...scored].map(([item, score]) => ({ item, score })).sort((a, b) => b.score - a.score);
2729
+ const unscored = input.items.filter((i) => !scored.has(i));
2730
+ return { ranked, unscored, usage: res.usage };
2714
2731
  }
2715
2732
  };
2716
2733
  }
@@ -3093,8 +3110,8 @@ var aiConfigSchema = z.object({
3093
3110
  });
3094
3111
 
3095
3112
  // src/version.ts
3096
- var VERSION = "0.41.1";
3097
- var SDK_TAG = "@broberg/ai-sdk@0.41.1";
3113
+ var VERSION = "0.42.0";
3114
+ var SDK_TAG = "@broberg/ai-sdk@0.42.0";
3098
3115
 
3099
3116
  // src/cost/sinks/upmetrics.ts
3100
3117
  function upmetricsSink(config) {