@fallom/trace 0.2.14 → 0.2.16
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/chunk-3HBKT4HK.mjs +827 -0
- package/dist/chunk-XBZ3ESNV.mjs +824 -0
- package/dist/core-4L56QWI7.mjs +21 -0
- package/dist/core-JLHYFVYS.mjs +21 -0
- package/dist/index.d.mts +140 -3
- package/dist/index.d.ts +140 -3
- package/dist/index.js +180 -14
- package/dist/index.mjs +15 -14
- package/package.json +1 -1
- package/dist/chunk-KFD5AQ7V.mjs +0 -308
- package/dist/models-SEFDGZU2.mjs +0 -8
package/dist/index.js
CHANGED
|
@@ -590,9 +590,159 @@ async function datasetFromFallom(datasetKey, version, config) {
|
|
|
590
590
|
);
|
|
591
591
|
return items;
|
|
592
592
|
}
|
|
593
|
+
var EvaluationDataset;
|
|
593
594
|
var init_helpers = __esm({
|
|
594
595
|
"src/evals/helpers.ts"() {
|
|
595
596
|
"use strict";
|
|
597
|
+
EvaluationDataset = class {
|
|
598
|
+
constructor() {
|
|
599
|
+
this._goldens = [];
|
|
600
|
+
this._testCases = [];
|
|
601
|
+
this._datasetKey = null;
|
|
602
|
+
this._datasetName = null;
|
|
603
|
+
this._version = null;
|
|
604
|
+
}
|
|
605
|
+
/** List of golden records (inputs with optional expected outputs). */
|
|
606
|
+
get goldens() {
|
|
607
|
+
return this._goldens;
|
|
608
|
+
}
|
|
609
|
+
/** List of test cases (inputs with actual outputs from your LLM). */
|
|
610
|
+
get testCases() {
|
|
611
|
+
return this._testCases;
|
|
612
|
+
}
|
|
613
|
+
/** The Fallom dataset key if pulled from Fallom. */
|
|
614
|
+
get datasetKey() {
|
|
615
|
+
return this._datasetKey;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Pull a dataset from Fallom.
|
|
619
|
+
*
|
|
620
|
+
* @param alias - The dataset key/alias in Fallom
|
|
621
|
+
* @param version - Specific version to pull (default: latest)
|
|
622
|
+
* @returns Self for chaining
|
|
623
|
+
*/
|
|
624
|
+
async pull(alias, version) {
|
|
625
|
+
const { _apiKey: _apiKey2, _baseUrl: _baseUrl2, _initialized: _initialized2 } = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
626
|
+
if (!_initialized2) {
|
|
627
|
+
throw new Error("Fallom evals not initialized. Call evals.init() first.");
|
|
628
|
+
}
|
|
629
|
+
const params = new URLSearchParams({ include_entries: "true" });
|
|
630
|
+
if (version !== void 0) {
|
|
631
|
+
params.set("version", String(version));
|
|
632
|
+
}
|
|
633
|
+
const url = `${_baseUrl2}/api/datasets/${encodeURIComponent(alias)}?${params}`;
|
|
634
|
+
const response = await fetch(url, {
|
|
635
|
+
headers: {
|
|
636
|
+
Authorization: `Bearer ${_apiKey2}`,
|
|
637
|
+
"Content-Type": "application/json"
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
if (response.status === 404) {
|
|
641
|
+
throw new Error(`Dataset '${alias}' not found`);
|
|
642
|
+
} else if (response.status === 403) {
|
|
643
|
+
throw new Error(`Access denied to dataset '${alias}'`);
|
|
644
|
+
}
|
|
645
|
+
if (!response.ok) {
|
|
646
|
+
throw new Error(`Failed to fetch dataset: ${response.statusText}`);
|
|
647
|
+
}
|
|
648
|
+
const data = await response.json();
|
|
649
|
+
this._datasetKey = alias;
|
|
650
|
+
this._datasetName = data.dataset?.name || alias;
|
|
651
|
+
this._version = data.version?.version || null;
|
|
652
|
+
this._goldens = [];
|
|
653
|
+
for (const entry of data.entries || []) {
|
|
654
|
+
this._goldens.push({
|
|
655
|
+
input: entry.input || "",
|
|
656
|
+
expectedOutput: entry.output,
|
|
657
|
+
systemMessage: entry.systemMessage,
|
|
658
|
+
metadata: entry.metadata
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
console.log(
|
|
662
|
+
`\u2713 Pulled dataset '${this._datasetName}' (version ${this._version}) with ${this._goldens.length} goldens`
|
|
663
|
+
);
|
|
664
|
+
return this;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Add a golden record manually.
|
|
668
|
+
* @param golden - A Golden object
|
|
669
|
+
* @returns Self for chaining
|
|
670
|
+
*/
|
|
671
|
+
addGolden(golden) {
|
|
672
|
+
this._goldens.push(golden);
|
|
673
|
+
return this;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Add multiple golden records.
|
|
677
|
+
* @param goldens - Array of Golden objects
|
|
678
|
+
* @returns Self for chaining
|
|
679
|
+
*/
|
|
680
|
+
addGoldens(goldens) {
|
|
681
|
+
this._goldens.push(...goldens);
|
|
682
|
+
return this;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Add a test case with actual LLM output.
|
|
686
|
+
* @param testCase - An LLMTestCase object
|
|
687
|
+
* @returns Self for chaining
|
|
688
|
+
*/
|
|
689
|
+
addTestCase(testCase) {
|
|
690
|
+
this._testCases.push(testCase);
|
|
691
|
+
return this;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Add multiple test cases.
|
|
695
|
+
* @param testCases - Array of LLMTestCase objects
|
|
696
|
+
* @returns Self for chaining
|
|
697
|
+
*/
|
|
698
|
+
addTestCases(testCases) {
|
|
699
|
+
this._testCases.push(...testCases);
|
|
700
|
+
return this;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Automatically generate test cases by running all goldens through your LLM app.
|
|
704
|
+
*
|
|
705
|
+
* @param llmApp - A callable that takes messages and returns response
|
|
706
|
+
* @param options - Configuration options
|
|
707
|
+
* @returns Self for chaining
|
|
708
|
+
*/
|
|
709
|
+
async generateTestCases(llmApp, options = {}) {
|
|
710
|
+
const { includeContext = false } = options;
|
|
711
|
+
console.log(`Generating test cases for ${this._goldens.length} goldens...`);
|
|
712
|
+
for (let i = 0; i < this._goldens.length; i++) {
|
|
713
|
+
const golden = this._goldens[i];
|
|
714
|
+
const messages = [];
|
|
715
|
+
if (golden.systemMessage) {
|
|
716
|
+
messages.push({ role: "system", content: golden.systemMessage });
|
|
717
|
+
}
|
|
718
|
+
messages.push({ role: "user", content: golden.input });
|
|
719
|
+
const response = await llmApp(messages);
|
|
720
|
+
const testCase = {
|
|
721
|
+
input: golden.input,
|
|
722
|
+
actualOutput: response.content,
|
|
723
|
+
expectedOutput: golden.expectedOutput,
|
|
724
|
+
systemMessage: golden.systemMessage,
|
|
725
|
+
context: includeContext ? response.context : golden.context,
|
|
726
|
+
metadata: golden.metadata
|
|
727
|
+
};
|
|
728
|
+
this._testCases.push(testCase);
|
|
729
|
+
console.log(
|
|
730
|
+
` [${i + 1}/${this._goldens.length}] Generated output for: ${golden.input.slice(0, 50)}...`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
console.log(`\u2713 Generated ${this._testCases.length} test cases`);
|
|
734
|
+
return this;
|
|
735
|
+
}
|
|
736
|
+
/** Clear all test cases (useful for re-running with different LLM). */
|
|
737
|
+
clearTestCases() {
|
|
738
|
+
this._testCases = [];
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
/** Return the number of goldens. */
|
|
742
|
+
get length() {
|
|
743
|
+
return this._goldens.length;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
596
746
|
}
|
|
597
747
|
});
|
|
598
748
|
|
|
@@ -707,9 +857,22 @@ async function evaluate(options) {
|
|
|
707
857
|
name,
|
|
708
858
|
description,
|
|
709
859
|
verbose = true,
|
|
860
|
+
testCases,
|
|
710
861
|
_skipUpload = false
|
|
711
862
|
} = options;
|
|
712
|
-
|
|
863
|
+
let dataset;
|
|
864
|
+
if (testCases !== void 0 && testCases.length > 0) {
|
|
865
|
+
dataset = testCases.map((tc) => ({
|
|
866
|
+
input: tc.input,
|
|
867
|
+
output: tc.actualOutput,
|
|
868
|
+
systemMessage: tc.systemMessage,
|
|
869
|
+
metadata: tc.metadata
|
|
870
|
+
}));
|
|
871
|
+
} else if (datasetInput !== void 0) {
|
|
872
|
+
dataset = await resolveDataset(datasetInput);
|
|
873
|
+
} else {
|
|
874
|
+
throw new Error("Either 'dataset' or 'testCases' must be provided");
|
|
875
|
+
}
|
|
713
876
|
for (const m of metrics) {
|
|
714
877
|
if (typeof m === "string" && !AVAILABLE_METRICS.includes(m)) {
|
|
715
878
|
throw new Error(
|
|
@@ -775,6 +938,9 @@ async function compareModels(options) {
|
|
|
775
938
|
description,
|
|
776
939
|
verbose = true
|
|
777
940
|
} = options;
|
|
941
|
+
if (!datasetInput) {
|
|
942
|
+
throw new Error("'dataset' is required for compareModels()");
|
|
943
|
+
}
|
|
778
944
|
const dataset = await resolveDataset(datasetInput);
|
|
779
945
|
const results = {};
|
|
780
946
|
if (includeProduction) {
|
|
@@ -1035,7 +1201,7 @@ var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otl
|
|
|
1035
1201
|
// node_modules/@opentelemetry/resources/build/esm/Resource.js
|
|
1036
1202
|
var import_api = require("@opentelemetry/api");
|
|
1037
1203
|
|
|
1038
|
-
// node_modules/@opentelemetry/
|
|
1204
|
+
// node_modules/@opentelemetry/semantic-conventions/build/esm/resource/SemanticResourceAttributes.js
|
|
1039
1205
|
var SemanticResourceAttributes = {
|
|
1040
1206
|
/**
|
|
1041
1207
|
* Name of the cloud provider.
|
|
@@ -2474,10 +2640,9 @@ function createGenerateTextWrapper(aiModule, sessionCtx, debug = false) {
|
|
|
2474
2640
|
const result = await aiModule.generateText(wrappedParams);
|
|
2475
2641
|
const endTime = Date.now();
|
|
2476
2642
|
if (debug || isDebugMode()) {
|
|
2477
|
-
console.log(
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
);
|
|
2643
|
+
console.log("\n\u{1F50D} [Fallom Debug] generateText result:");
|
|
2644
|
+
console.log(" toolCalls:", result?.toolCalls?.length || 0);
|
|
2645
|
+
console.log(" steps:", result?.steps?.length || 0);
|
|
2481
2646
|
}
|
|
2482
2647
|
const modelId = result?.response?.modelId || params?.model?.modelId || String(params?.model || "unknown");
|
|
2483
2648
|
const attributes = {
|
|
@@ -2496,15 +2661,15 @@ function createGenerateTextWrapper(aiModule, sessionCtx, debug = false) {
|
|
|
2496
2661
|
const mapToolCall = (tc) => ({
|
|
2497
2662
|
toolCallId: tc?.toolCallId,
|
|
2498
2663
|
toolName: tc?.toolName,
|
|
2499
|
-
args: tc?.args,
|
|
2500
|
-
//
|
|
2664
|
+
args: tc?.args ?? tc?.input,
|
|
2665
|
+
// v4: args, v5: input
|
|
2501
2666
|
type: tc?.type
|
|
2502
2667
|
});
|
|
2503
2668
|
const mapToolResult = (tr) => ({
|
|
2504
2669
|
toolCallId: tr?.toolCallId,
|
|
2505
2670
|
toolName: tr?.toolName,
|
|
2506
|
-
result: tr?.result,
|
|
2507
|
-
//
|
|
2671
|
+
result: tr?.result ?? tr?.output,
|
|
2672
|
+
// v4: result, v5: output
|
|
2508
2673
|
type: tr?.type
|
|
2509
2674
|
});
|
|
2510
2675
|
attributes["fallom.raw.response"] = JSON.stringify({
|
|
@@ -2817,15 +2982,15 @@ function createStreamTextWrapper(aiModule, sessionCtx, debug = false) {
|
|
|
2817
2982
|
const mapToolCall = (tc) => ({
|
|
2818
2983
|
toolCallId: tc?.toolCallId,
|
|
2819
2984
|
toolName: tc?.toolName,
|
|
2820
|
-
args: tc?.args,
|
|
2821
|
-
//
|
|
2985
|
+
args: tc?.args ?? tc?.input,
|
|
2986
|
+
// v4: args, v5: input
|
|
2822
2987
|
type: tc?.type
|
|
2823
2988
|
});
|
|
2824
2989
|
const mapToolResult = (tr) => ({
|
|
2825
2990
|
toolCallId: tr?.toolCallId,
|
|
2826
2991
|
toolName: tr?.toolName,
|
|
2827
|
-
result: tr?.result,
|
|
2828
|
-
//
|
|
2992
|
+
result: tr?.result ?? tr?.output,
|
|
2993
|
+
// v4: result, v5: output
|
|
2829
2994
|
type: tr?.type
|
|
2830
2995
|
});
|
|
2831
2996
|
attributes["fallom.raw.request"] = JSON.stringify({
|
|
@@ -3544,6 +3709,7 @@ var evals_exports = {};
|
|
|
3544
3709
|
__export(evals_exports, {
|
|
3545
3710
|
AVAILABLE_METRICS: () => AVAILABLE_METRICS,
|
|
3546
3711
|
DEFAULT_JUDGE_MODEL: () => DEFAULT_JUDGE_MODEL,
|
|
3712
|
+
EvaluationDataset: () => EvaluationDataset,
|
|
3547
3713
|
METRIC_PROMPTS: () => METRIC_PROMPTS,
|
|
3548
3714
|
compareModels: () => compareModels,
|
|
3549
3715
|
createCustomModel: () => createCustomModel,
|
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
AVAILABLE_METRICS,
|
|
7
7
|
DEFAULT_JUDGE_MODEL,
|
|
8
|
+
EvaluationDataset,
|
|
8
9
|
METRIC_PROMPTS,
|
|
9
10
|
compareModels,
|
|
10
11
|
createCustomModel,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
init as init2,
|
|
19
20
|
isCustomMetric,
|
|
20
21
|
uploadResultsPublic
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-3HBKT4HK.mjs";
|
|
22
23
|
import {
|
|
23
24
|
__export
|
|
24
25
|
} from "./chunk-7P6ASYW6.mjs";
|
|
@@ -40,7 +41,7 @@ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
|
40
41
|
// node_modules/@opentelemetry/resources/build/esm/Resource.js
|
|
41
42
|
import { diag } from "@opentelemetry/api";
|
|
42
43
|
|
|
43
|
-
// node_modules/@opentelemetry/
|
|
44
|
+
// node_modules/@opentelemetry/semantic-conventions/build/esm/resource/SemanticResourceAttributes.js
|
|
44
45
|
var SemanticResourceAttributes = {
|
|
45
46
|
/**
|
|
46
47
|
* Name of the cloud provider.
|
|
@@ -1479,10 +1480,9 @@ function createGenerateTextWrapper(aiModule, sessionCtx, debug = false) {
|
|
|
1479
1480
|
const result = await aiModule.generateText(wrappedParams);
|
|
1480
1481
|
const endTime = Date.now();
|
|
1481
1482
|
if (debug || isDebugMode()) {
|
|
1482
|
-
console.log(
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
);
|
|
1483
|
+
console.log("\n\u{1F50D} [Fallom Debug] generateText result:");
|
|
1484
|
+
console.log(" toolCalls:", result?.toolCalls?.length || 0);
|
|
1485
|
+
console.log(" steps:", result?.steps?.length || 0);
|
|
1486
1486
|
}
|
|
1487
1487
|
const modelId = result?.response?.modelId || params?.model?.modelId || String(params?.model || "unknown");
|
|
1488
1488
|
const attributes = {
|
|
@@ -1501,15 +1501,15 @@ function createGenerateTextWrapper(aiModule, sessionCtx, debug = false) {
|
|
|
1501
1501
|
const mapToolCall = (tc) => ({
|
|
1502
1502
|
toolCallId: tc?.toolCallId,
|
|
1503
1503
|
toolName: tc?.toolName,
|
|
1504
|
-
args: tc?.args,
|
|
1505
|
-
//
|
|
1504
|
+
args: tc?.args ?? tc?.input,
|
|
1505
|
+
// v4: args, v5: input
|
|
1506
1506
|
type: tc?.type
|
|
1507
1507
|
});
|
|
1508
1508
|
const mapToolResult = (tr) => ({
|
|
1509
1509
|
toolCallId: tr?.toolCallId,
|
|
1510
1510
|
toolName: tr?.toolName,
|
|
1511
|
-
result: tr?.result,
|
|
1512
|
-
//
|
|
1511
|
+
result: tr?.result ?? tr?.output,
|
|
1512
|
+
// v4: result, v5: output
|
|
1513
1513
|
type: tr?.type
|
|
1514
1514
|
});
|
|
1515
1515
|
attributes["fallom.raw.response"] = JSON.stringify({
|
|
@@ -1822,15 +1822,15 @@ function createStreamTextWrapper(aiModule, sessionCtx, debug = false) {
|
|
|
1822
1822
|
const mapToolCall = (tc) => ({
|
|
1823
1823
|
toolCallId: tc?.toolCallId,
|
|
1824
1824
|
toolName: tc?.toolName,
|
|
1825
|
-
args: tc?.args,
|
|
1826
|
-
//
|
|
1825
|
+
args: tc?.args ?? tc?.input,
|
|
1826
|
+
// v4: args, v5: input
|
|
1827
1827
|
type: tc?.type
|
|
1828
1828
|
});
|
|
1829
1829
|
const mapToolResult = (tr) => ({
|
|
1830
1830
|
toolCallId: tr?.toolCallId,
|
|
1831
1831
|
toolName: tr?.toolName,
|
|
1832
|
-
result: tr?.result,
|
|
1833
|
-
//
|
|
1832
|
+
result: tr?.result ?? tr?.output,
|
|
1833
|
+
// v4: result, v5: output
|
|
1834
1834
|
type: tr?.type
|
|
1835
1835
|
});
|
|
1836
1836
|
attributes["fallom.raw.request"] = JSON.stringify({
|
|
@@ -2546,6 +2546,7 @@ var evals_exports = {};
|
|
|
2546
2546
|
__export(evals_exports, {
|
|
2547
2547
|
AVAILABLE_METRICS: () => AVAILABLE_METRICS,
|
|
2548
2548
|
DEFAULT_JUDGE_MODEL: () => DEFAULT_JUDGE_MODEL,
|
|
2549
|
+
EvaluationDataset: () => EvaluationDataset,
|
|
2549
2550
|
METRIC_PROMPTS: () => METRIC_PROMPTS,
|
|
2550
2551
|
compareModels: () => compareModels,
|
|
2551
2552
|
createCustomModel: () => createCustomModel,
|
package/package.json
CHANGED
package/dist/chunk-KFD5AQ7V.mjs
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __export = (target, all) => {
|
|
3
|
-
for (var name in all)
|
|
4
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
// src/models.ts
|
|
8
|
-
var models_exports = {};
|
|
9
|
-
__export(models_exports, {
|
|
10
|
-
get: () => get,
|
|
11
|
-
init: () => init
|
|
12
|
-
});
|
|
13
|
-
import { createHash } from "crypto";
|
|
14
|
-
var apiKey = null;
|
|
15
|
-
var baseUrl = "https://configs.fallom.com";
|
|
16
|
-
var initialized = false;
|
|
17
|
-
var syncInterval = null;
|
|
18
|
-
var debugMode = false;
|
|
19
|
-
var configCache = /* @__PURE__ */ new Map();
|
|
20
|
-
var SYNC_TIMEOUT = 2e3;
|
|
21
|
-
var RECORD_TIMEOUT = 1e3;
|
|
22
|
-
function log(msg) {
|
|
23
|
-
if (debugMode) {
|
|
24
|
-
console.log(`[Fallom] ${msg}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function evaluateTargeting(targeting, customerId, context) {
|
|
28
|
-
if (!targeting || targeting.enabled === false) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
const evalContext = {
|
|
32
|
-
...context || {},
|
|
33
|
-
...customerId ? { customerId } : {}
|
|
34
|
-
};
|
|
35
|
-
log(`Evaluating targeting with context: ${JSON.stringify(evalContext)}`);
|
|
36
|
-
if (targeting.individualTargets) {
|
|
37
|
-
for (const target of targeting.individualTargets) {
|
|
38
|
-
const fieldValue = evalContext[target.field];
|
|
39
|
-
if (fieldValue === target.value) {
|
|
40
|
-
log(`Individual target matched: ${target.field}=${target.value} -> variant ${target.variantIndex}`);
|
|
41
|
-
return target.variantIndex;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (targeting.rules) {
|
|
46
|
-
for (const rule of targeting.rules) {
|
|
47
|
-
const allConditionsMatch = rule.conditions.every((condition) => {
|
|
48
|
-
const fieldValue = evalContext[condition.field];
|
|
49
|
-
if (fieldValue === void 0) return false;
|
|
50
|
-
switch (condition.operator) {
|
|
51
|
-
case "eq":
|
|
52
|
-
return fieldValue === condition.value;
|
|
53
|
-
case "neq":
|
|
54
|
-
return fieldValue !== condition.value;
|
|
55
|
-
case "in":
|
|
56
|
-
return Array.isArray(condition.value) && condition.value.includes(fieldValue);
|
|
57
|
-
case "nin":
|
|
58
|
-
return Array.isArray(condition.value) && !condition.value.includes(fieldValue);
|
|
59
|
-
case "contains":
|
|
60
|
-
return typeof condition.value === "string" && fieldValue.includes(condition.value);
|
|
61
|
-
case "startsWith":
|
|
62
|
-
return typeof condition.value === "string" && fieldValue.startsWith(condition.value);
|
|
63
|
-
case "endsWith":
|
|
64
|
-
return typeof condition.value === "string" && fieldValue.endsWith(condition.value);
|
|
65
|
-
default:
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
if (allConditionsMatch) {
|
|
70
|
-
log(`Rule matched: ${JSON.stringify(rule.conditions)} -> variant ${rule.variantIndex}`);
|
|
71
|
-
return rule.variantIndex;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
log("No targeting rules matched, falling back to weighted random");
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
function init(options = {}) {
|
|
79
|
-
apiKey = options.apiKey || process.env.FALLOM_API_KEY || null;
|
|
80
|
-
baseUrl = options.baseUrl || process.env.FALLOM_CONFIGS_URL || process.env.FALLOM_BASE_URL || "https://configs.fallom.com";
|
|
81
|
-
initialized = true;
|
|
82
|
-
if (!apiKey) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
fetchConfigs().catch(() => {
|
|
86
|
-
});
|
|
87
|
-
if (!syncInterval) {
|
|
88
|
-
syncInterval = setInterval(() => {
|
|
89
|
-
fetchConfigs().catch(() => {
|
|
90
|
-
});
|
|
91
|
-
}, 3e4);
|
|
92
|
-
syncInterval.unref();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function ensureInit() {
|
|
96
|
-
if (!initialized) {
|
|
97
|
-
try {
|
|
98
|
-
init();
|
|
99
|
-
} catch {
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
async function fetchConfigs(timeout = SYNC_TIMEOUT) {
|
|
104
|
-
if (!apiKey) {
|
|
105
|
-
log("_fetchConfigs: No API key, skipping");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
log(`Fetching configs from ${baseUrl}/configs`);
|
|
110
|
-
const controller = new AbortController();
|
|
111
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
112
|
-
const resp = await fetch(`${baseUrl}/configs`, {
|
|
113
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
114
|
-
signal: controller.signal
|
|
115
|
-
});
|
|
116
|
-
clearTimeout(timeoutId);
|
|
117
|
-
log(`Response status: ${resp.status}`);
|
|
118
|
-
if (resp.ok) {
|
|
119
|
-
const data = await resp.json();
|
|
120
|
-
const configs = data.configs || [];
|
|
121
|
-
log(`Got ${configs.length} configs: ${configs.map((c) => c.key)}`);
|
|
122
|
-
for (const c of configs) {
|
|
123
|
-
const key = c.key;
|
|
124
|
-
const version = c.version || 1;
|
|
125
|
-
log(`Config '${key}' v${version}: ${JSON.stringify(c.variants)}`);
|
|
126
|
-
if (!configCache.has(key)) {
|
|
127
|
-
configCache.set(key, { versions: /* @__PURE__ */ new Map(), latest: null });
|
|
128
|
-
}
|
|
129
|
-
const cached = configCache.get(key);
|
|
130
|
-
cached.versions.set(version, c);
|
|
131
|
-
cached.latest = version;
|
|
132
|
-
}
|
|
133
|
-
} else {
|
|
134
|
-
log(`Fetch failed: ${resp.statusText}`);
|
|
135
|
-
}
|
|
136
|
-
} catch (e) {
|
|
137
|
-
log(`Fetch exception: ${e}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
async function fetchSpecificVersion(configKey, version, timeout = SYNC_TIMEOUT) {
|
|
141
|
-
if (!apiKey) return null;
|
|
142
|
-
try {
|
|
143
|
-
const controller = new AbortController();
|
|
144
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
145
|
-
const resp = await fetch(
|
|
146
|
-
`${baseUrl}/configs/${configKey}/version/${version}`,
|
|
147
|
-
{
|
|
148
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
149
|
-
signal: controller.signal
|
|
150
|
-
}
|
|
151
|
-
);
|
|
152
|
-
clearTimeout(timeoutId);
|
|
153
|
-
if (resp.ok) {
|
|
154
|
-
const config = await resp.json();
|
|
155
|
-
if (!configCache.has(configKey)) {
|
|
156
|
-
configCache.set(configKey, { versions: /* @__PURE__ */ new Map(), latest: null });
|
|
157
|
-
}
|
|
158
|
-
configCache.get(configKey).versions.set(version, config);
|
|
159
|
-
return config;
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
async function get(configKey, sessionId, options = {}) {
|
|
166
|
-
const { version, fallback, customerId, context, debug = false } = options;
|
|
167
|
-
debugMode = debug;
|
|
168
|
-
ensureInit();
|
|
169
|
-
log(
|
|
170
|
-
`get() called: configKey=${configKey}, sessionId=${sessionId}, fallback=${fallback}`
|
|
171
|
-
);
|
|
172
|
-
try {
|
|
173
|
-
let configData = configCache.get(configKey);
|
|
174
|
-
log(
|
|
175
|
-
`Cache lookup for '${configKey}': ${configData ? "found" : "not found"}`
|
|
176
|
-
);
|
|
177
|
-
if (!configData) {
|
|
178
|
-
log("Not in cache, fetching...");
|
|
179
|
-
await fetchConfigs(SYNC_TIMEOUT);
|
|
180
|
-
configData = configCache.get(configKey);
|
|
181
|
-
log(
|
|
182
|
-
`After fetch, cache lookup: ${configData ? "found" : "still not found"}`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
if (!configData) {
|
|
186
|
-
log(`Config not found, using fallback: ${fallback}`);
|
|
187
|
-
if (fallback) {
|
|
188
|
-
console.warn(
|
|
189
|
-
`[Fallom WARNING] Config '${configKey}' not found, using fallback model: ${fallback}`
|
|
190
|
-
);
|
|
191
|
-
return returnModel(configKey, sessionId, fallback, 0);
|
|
192
|
-
}
|
|
193
|
-
throw new Error(
|
|
194
|
-
`Config '${configKey}' not found. Check that it exists in your Fallom dashboard.`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
let config;
|
|
198
|
-
let targetVersion;
|
|
199
|
-
if (version !== void 0) {
|
|
200
|
-
config = configData.versions.get(version);
|
|
201
|
-
if (!config) {
|
|
202
|
-
config = await fetchSpecificVersion(configKey, version, SYNC_TIMEOUT) || void 0;
|
|
203
|
-
}
|
|
204
|
-
if (!config) {
|
|
205
|
-
if (fallback) {
|
|
206
|
-
console.warn(
|
|
207
|
-
`[Fallom WARNING] Config '${configKey}' version ${version} not found, using fallback: ${fallback}`
|
|
208
|
-
);
|
|
209
|
-
return returnModel(configKey, sessionId, fallback, 0);
|
|
210
|
-
}
|
|
211
|
-
throw new Error(`Config '${configKey}' version ${version} not found.`);
|
|
212
|
-
}
|
|
213
|
-
targetVersion = version;
|
|
214
|
-
} else {
|
|
215
|
-
targetVersion = configData.latest;
|
|
216
|
-
config = configData.versions.get(targetVersion);
|
|
217
|
-
if (!config) {
|
|
218
|
-
if (fallback) {
|
|
219
|
-
console.warn(
|
|
220
|
-
`[Fallom WARNING] Config '${configKey}' has no cached version, using fallback: ${fallback}`
|
|
221
|
-
);
|
|
222
|
-
return returnModel(configKey, sessionId, fallback, 0);
|
|
223
|
-
}
|
|
224
|
-
throw new Error(`Config '${configKey}' has no cached version.`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const variantsRaw = config.variants;
|
|
228
|
-
const configVersion = config.version || targetVersion;
|
|
229
|
-
const variants = Array.isArray(variantsRaw) ? variantsRaw : Object.values(variantsRaw);
|
|
230
|
-
log(
|
|
231
|
-
`Config found! Version: ${configVersion}, Variants: ${JSON.stringify(
|
|
232
|
-
variants
|
|
233
|
-
)}`
|
|
234
|
-
);
|
|
235
|
-
const targetedVariantIndex = evaluateTargeting(config.targeting, customerId, context);
|
|
236
|
-
if (targetedVariantIndex !== null && variants[targetedVariantIndex]) {
|
|
237
|
-
const assignedModel2 = variants[targetedVariantIndex].model;
|
|
238
|
-
log(`\u2705 Assigned model via targeting: ${assignedModel2}`);
|
|
239
|
-
return returnModel(configKey, sessionId, assignedModel2, configVersion);
|
|
240
|
-
}
|
|
241
|
-
const hashBytes = createHash("md5").update(sessionId).digest();
|
|
242
|
-
const hashVal = hashBytes.readUInt32BE(0) % 1e6;
|
|
243
|
-
log(`Session hash: ${hashVal} (out of 1,000,000)`);
|
|
244
|
-
let cumulative = 0;
|
|
245
|
-
let assignedModel = variants[variants.length - 1].model;
|
|
246
|
-
for (const v of variants) {
|
|
247
|
-
const oldCumulative = cumulative;
|
|
248
|
-
cumulative += v.weight * 1e4;
|
|
249
|
-
log(
|
|
250
|
-
`Variant ${v.model}: weight=${v.weight}%, range=${oldCumulative}-${cumulative}, hash=${hashVal}, match=${hashVal < cumulative}`
|
|
251
|
-
);
|
|
252
|
-
if (hashVal < cumulative) {
|
|
253
|
-
assignedModel = v.model;
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
log(`\u2705 Assigned model via weighted random: ${assignedModel}`);
|
|
258
|
-
return returnModel(configKey, sessionId, assignedModel, configVersion);
|
|
259
|
-
} catch (e) {
|
|
260
|
-
if (e instanceof Error && e.message.includes("not found")) {
|
|
261
|
-
throw e;
|
|
262
|
-
}
|
|
263
|
-
if (fallback) {
|
|
264
|
-
console.warn(
|
|
265
|
-
`[Fallom WARNING] Error getting model for '${configKey}': ${e}. Using fallback: ${fallback}`
|
|
266
|
-
);
|
|
267
|
-
return returnModel(configKey, sessionId, fallback, 0);
|
|
268
|
-
}
|
|
269
|
-
throw e;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
function returnModel(configKey, sessionId, model, version) {
|
|
273
|
-
if (version > 0) {
|
|
274
|
-
recordSession(configKey, version, sessionId, model).catch(() => {
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
return model;
|
|
278
|
-
}
|
|
279
|
-
async function recordSession(configKey, version, sessionId, model) {
|
|
280
|
-
if (!apiKey) return;
|
|
281
|
-
try {
|
|
282
|
-
const controller = new AbortController();
|
|
283
|
-
const timeoutId = setTimeout(() => controller.abort(), RECORD_TIMEOUT);
|
|
284
|
-
await fetch(`${baseUrl}/sessions`, {
|
|
285
|
-
method: "POST",
|
|
286
|
-
headers: {
|
|
287
|
-
Authorization: `Bearer ${apiKey}`,
|
|
288
|
-
"Content-Type": "application/json"
|
|
289
|
-
},
|
|
290
|
-
body: JSON.stringify({
|
|
291
|
-
config_key: configKey,
|
|
292
|
-
config_version: version,
|
|
293
|
-
session_id: sessionId,
|
|
294
|
-
assigned_model: model
|
|
295
|
-
}),
|
|
296
|
-
signal: controller.signal
|
|
297
|
-
});
|
|
298
|
-
clearTimeout(timeoutId);
|
|
299
|
-
} catch {
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export {
|
|
304
|
-
__export,
|
|
305
|
-
init,
|
|
306
|
-
get,
|
|
307
|
-
models_exports
|
|
308
|
-
};
|