margin-meter 0.1.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/README.md +110 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +205 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# margin-meter (TypeScript SDK)
|
|
2
|
+
|
|
3
|
+
The tiny client a **TypeScript/JavaScript** project imports to connect to
|
|
4
|
+
Margin — the TS twin of the Python [`margin-meter`](../python) package. It wraps
|
|
5
|
+
your LLM calls and records their outcomes, emitting each one **over HTTP** to a
|
|
6
|
+
Margin **ingest API** (`POST /api/ingest/calls` / `/api/ingest/outcomes`),
|
|
7
|
+
authenticated with a per-project ingest key.
|
|
8
|
+
|
|
9
|
+
- **Dependency-free.** Uses the global `fetch` — nothing to install at runtime.
|
|
10
|
+
- **Fail-safe.** A failed emit resolves to `{ ok: false, … }` instead of throwing.
|
|
11
|
+
Pass `raiseOnError: true` for strict/CI behaviour.
|
|
12
|
+
- **Provenance-honest.** `isSimulated` is carried through untouched; the written
|
|
13
|
+
`source` is forced server-side to your key's project — you cannot spoof another
|
|
14
|
+
project's economics.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Published as a git-installable subpath of the Margin repo (no npm registry
|
|
19
|
+
account needed pre-launch):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install margin-meter
|
|
23
|
+
# or, pinning the subdirectory with your package manager of choice, e.g. pnpm:
|
|
24
|
+
# pnpm add "https://gitpkg.now.sh/subhsubh24/Margin.ai/sdk/ts?main"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Most TS projects import the source directly (their bundler compiles it); the
|
|
28
|
+
package `exports` point at `src/index.ts` with types.
|
|
29
|
+
|
|
30
|
+
## Configure
|
|
31
|
+
|
|
32
|
+
Two environment variables — the deployed API base and your project's key:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export MARGIN_INGEST_URL="https://margin-ai-rho.vercel.app"
|
|
36
|
+
export MARGIN_INGEST_KEY="mgk_…" # issued by the Margin owner
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The Margin owner issues your project a key with the provisioning CLI in the
|
|
40
|
+
Margin repo (`python3 scripts/issue_ingest_key.py <your-project-slug>`). The raw
|
|
41
|
+
`mgk_…` key is shown once — only its hash is stored.
|
|
42
|
+
|
|
43
|
+
## Use
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { MarginMeter } from "margin-meter";
|
|
47
|
+
|
|
48
|
+
const meter = new MarginMeter(); // reads MARGIN_INGEST_URL + MARGIN_INGEST_KEY
|
|
49
|
+
|
|
50
|
+
// 1) Wrap the LLM call — latency is timed automatically, cost computed server-side.
|
|
51
|
+
const answer = await meter.measure(
|
|
52
|
+
{ workflowId: "cart-parse", provider: "google", model: "gemini-2.5-flash" },
|
|
53
|
+
async (m) => {
|
|
54
|
+
const resp = await callTheModel(/* … */);
|
|
55
|
+
m.setTokens({ inputTokens: 1200, outputTokens: 300, cacheReadTokens: 800 });
|
|
56
|
+
return resp;
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// 2) Record the outcome it produced (the unit of productivity).
|
|
61
|
+
await meter.recordOutcome({
|
|
62
|
+
workflowId: "cart-parse",
|
|
63
|
+
passed: true,
|
|
64
|
+
qualityScore: 0.94,
|
|
65
|
+
qualityMethod: "ground_truth",
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or record a call directly:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const res = await meter.recordCall({
|
|
73
|
+
workflowId: "cart-parse",
|
|
74
|
+
provider: "google",
|
|
75
|
+
model: "gemini-2.5-flash",
|
|
76
|
+
inputTokens: 1200,
|
|
77
|
+
outputTokens: 300,
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) console.warn(`margin ingest failed: ${res.error} (${res.statusCode})`);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
| Method | Emits to | Notes |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `recordCall(input)` | `POST /api/ingest/calls` | cost computed from the pricing table when `costUsd` omitted |
|
|
87
|
+
| `recordOutcome(input)` | `POST /api/ingest/outcomes` | `qualityMethod` records HOW the score was graded |
|
|
88
|
+
| `measure(input, fn)` | `recordCall` on completion | times latency; `status:"error"` + rethrow if `fn` throws; returns `fn`'s value, emit result on `m.result` |
|
|
89
|
+
|
|
90
|
+
Every method resolves to an `IngestResult { ok, statusCode, body?, error? }`.
|
|
91
|
+
`ok` is true only on HTTP 200; `body` holds `call_id`/`outcome_id` + `source`.
|
|
92
|
+
|
|
93
|
+
## Testing against the ingest contract (no network)
|
|
94
|
+
|
|
95
|
+
The network boundary is a single `post(path, { json, headers })` protocol, so a
|
|
96
|
+
test can inject a fake that mimics the ingest API's documented 200/401/422/429
|
|
97
|
+
responses:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const meter = new MarginMeter({ apiKey: "mgk_…", transport: fakeIngest, raiseOnError: true });
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
See `test/meter.test.mjs`. Run with `npm test`
|
|
104
|
+
(`node --experimental-strip-types --test test/*.test.mjs` — zero deps).
|
|
105
|
+
|
|
106
|
+
## Rate + validation bounds
|
|
107
|
+
|
|
108
|
+
Ingest is auth'd, validated, and rate-bounded server-side: bad key → 401,
|
|
109
|
+
malformed/implausible row → 422, full rolling per-project window → 429. In
|
|
110
|
+
fail-safe mode these come back as `{ ok: false, statusCode }`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* margin-meter — the TypeScript SDK a project imports to connect to Margin.
|
|
3
|
+
*
|
|
4
|
+
* The TS twin of the Python `margin_meter` package: a tiny, dependency-free
|
|
5
|
+
* client the three TS siblings (GroceryManager, HighlightMagic, AptDesignerAI)
|
|
6
|
+
* — or any real customer — drop in to wrap their LLM calls and record the
|
|
7
|
+
* outcomes, emitting each one OVER HTTP to Margin's ingest API
|
|
8
|
+
* (`POST /api/ingest/calls` / `/api/ingest/outcomes`), authenticated with a
|
|
9
|
+
* per-project ingest key.
|
|
10
|
+
*
|
|
11
|
+
* Design goals (mirroring the Python SDK):
|
|
12
|
+
* - **Dependency-free.** Uses the global `fetch` — no runtime deps to install.
|
|
13
|
+
* - **Pluggable transport (hermetically testable).** The network boundary is a
|
|
14
|
+
* single `post(path, { json, headers })` protocol, so a test can inject a
|
|
15
|
+
* fake that faithfully mimics the ingest API's documented 200/401/422/429
|
|
16
|
+
* responses. Production uses `fetch`.
|
|
17
|
+
* - **Fail-safe by default.** Telemetry must never crash the host, so a failed
|
|
18
|
+
* emit resolves to an `IngestResult { ok: false, ... }` instead of throwing.
|
|
19
|
+
* Pass `raiseOnError: true` for strict/CI behaviour.
|
|
20
|
+
* - **Provenance-honest.** `isSimulated` is carried through untouched; the
|
|
21
|
+
* written `source` is forced server-side to the key's project slug — the
|
|
22
|
+
* client cannot spoof another project's economics.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CALLS_PATH = "/api/ingest/calls";
|
|
25
|
+
export declare const OUTCOMES_PATH = "/api/ingest/outcomes";
|
|
26
|
+
export declare const KEY_HEADER = "X-Margin-Key";
|
|
27
|
+
export declare const DEFAULT_INGEST_URL = "http://127.0.0.1:8000";
|
|
28
|
+
export declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
29
|
+
/** The outcome of one emit. Returned by every record call. */
|
|
30
|
+
export interface IngestResult {
|
|
31
|
+
/** True only on an HTTP 200. */
|
|
32
|
+
ok: boolean;
|
|
33
|
+
/** The HTTP status, or 0 for a transport/config failure. */
|
|
34
|
+
statusCode: number;
|
|
35
|
+
/** The parsed success payload (`call_id`/`outcome_id` + `source`) when ok. */
|
|
36
|
+
body?: Record<string, unknown>;
|
|
37
|
+
/** The failure reason, when not ok. */
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
/** The response shape a transport returns (a subset of the `fetch` Response). */
|
|
41
|
+
export interface TransportResponse {
|
|
42
|
+
status: number;
|
|
43
|
+
json(): Promise<unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** The single network-boundary protocol. `fetch` (via FetchTransport) and any
|
|
46
|
+
* test fake both satisfy it. */
|
|
47
|
+
export interface Transport {
|
|
48
|
+
post(path: string, opts: {
|
|
49
|
+
json: unknown;
|
|
50
|
+
headers: Record<string, string>;
|
|
51
|
+
}): Promise<TransportResponse>;
|
|
52
|
+
}
|
|
53
|
+
/** Raised (only in strict mode) when a row is rejected by the ingest API. */
|
|
54
|
+
export declare class MarginIngestError extends Error {
|
|
55
|
+
statusCode: number;
|
|
56
|
+
detail: string;
|
|
57
|
+
constructor(statusCode: number, detail: string);
|
|
58
|
+
}
|
|
59
|
+
/** Raised (only in strict mode) when the client is missing its ingest key. */
|
|
60
|
+
export declare class MarginConfigError extends Error {
|
|
61
|
+
constructor(message: string);
|
|
62
|
+
}
|
|
63
|
+
/** The default transport: POST JSON over HTTP with the global `fetch`. */
|
|
64
|
+
export declare class FetchTransport implements Transport {
|
|
65
|
+
baseUrl: string;
|
|
66
|
+
timeoutMs: number;
|
|
67
|
+
constructor(baseUrl: string, timeoutMs?: number);
|
|
68
|
+
post(path: string, opts: {
|
|
69
|
+
json: unknown;
|
|
70
|
+
headers: Record<string, string>;
|
|
71
|
+
}): Promise<TransportResponse>;
|
|
72
|
+
}
|
|
73
|
+
export interface MarginMeterOptions {
|
|
74
|
+
ingestUrl?: string;
|
|
75
|
+
apiKey?: string;
|
|
76
|
+
transport?: Transport;
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
raiseOnError?: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface RecordCallInput {
|
|
81
|
+
workflowId: string;
|
|
82
|
+
provider: string;
|
|
83
|
+
model: string;
|
|
84
|
+
inputTokens?: number;
|
|
85
|
+
outputTokens?: number;
|
|
86
|
+
cacheReadTokens?: number;
|
|
87
|
+
latencyMs?: number;
|
|
88
|
+
status?: string;
|
|
89
|
+
isRetry?: boolean;
|
|
90
|
+
sessionId?: string;
|
|
91
|
+
promptId?: string;
|
|
92
|
+
costUsd?: number;
|
|
93
|
+
isSimulated?: boolean;
|
|
94
|
+
}
|
|
95
|
+
export interface RecordOutcomeInput {
|
|
96
|
+
workflowId: string;
|
|
97
|
+
passed: boolean;
|
|
98
|
+
qualityScore?: number;
|
|
99
|
+
qualityMethod?: string;
|
|
100
|
+
link?: string;
|
|
101
|
+
isSimulated?: boolean;
|
|
102
|
+
}
|
|
103
|
+
/** A per-project client that emits measured economics to a Margin ingest API. */
|
|
104
|
+
export declare class MarginMeter {
|
|
105
|
+
ingestUrl: string;
|
|
106
|
+
apiKey?: string;
|
|
107
|
+
raiseOnError: boolean;
|
|
108
|
+
private transport;
|
|
109
|
+
constructor(options?: MarginMeterOptions);
|
|
110
|
+
private emit;
|
|
111
|
+
/** Emit one measured LLM call. Cost is computed server-side when `costUsd`
|
|
112
|
+
* is omitted. Never throws unless `raiseOnError`. */
|
|
113
|
+
recordCall(input: RecordCallInput): Promise<IngestResult>;
|
|
114
|
+
/** Emit one outcome (a unit of productivity). `qualityMethod` records HOW the
|
|
115
|
+
* score was graded so a self-report is never mistaken for a graded number. */
|
|
116
|
+
recordOutcome(input: RecordOutcomeInput): Promise<IngestResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Time an async call and emit it on completion. The TS analogue of the
|
|
119
|
+
* Python `measure` context manager: latency is timed automatically, and the
|
|
120
|
+
* call is emitted with `status="error"` (and the error re-thrown) if `fn`
|
|
121
|
+
* throws. Set token counts inside `fn` via `m.setTokens(...)`. Returns the
|
|
122
|
+
* value `fn` produced; the emit's `IngestResult` is available as `m.result`.
|
|
123
|
+
*/
|
|
124
|
+
measure<T>(input: Omit<RecordCallInput, "latencyMs" | "status">, fn: (m: Measurement) => Promise<T> | T): Promise<T>;
|
|
125
|
+
}
|
|
126
|
+
/** Token accumulator passed into `measure`'s callback. */
|
|
127
|
+
export declare class Measurement {
|
|
128
|
+
inputTokens: number;
|
|
129
|
+
outputTokens: number;
|
|
130
|
+
cacheReadTokens: number;
|
|
131
|
+
result?: IngestResult;
|
|
132
|
+
setTokens(tokens: {
|
|
133
|
+
inputTokens?: number;
|
|
134
|
+
outputTokens?: number;
|
|
135
|
+
cacheReadTokens?: number;
|
|
136
|
+
}): void;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* margin-meter — the TypeScript SDK a project imports to connect to Margin.
|
|
3
|
+
*
|
|
4
|
+
* The TS twin of the Python `margin_meter` package: a tiny, dependency-free
|
|
5
|
+
* client the three TS siblings (GroceryManager, HighlightMagic, AptDesignerAI)
|
|
6
|
+
* — or any real customer — drop in to wrap their LLM calls and record the
|
|
7
|
+
* outcomes, emitting each one OVER HTTP to Margin's ingest API
|
|
8
|
+
* (`POST /api/ingest/calls` / `/api/ingest/outcomes`), authenticated with a
|
|
9
|
+
* per-project ingest key.
|
|
10
|
+
*
|
|
11
|
+
* Design goals (mirroring the Python SDK):
|
|
12
|
+
* - **Dependency-free.** Uses the global `fetch` — no runtime deps to install.
|
|
13
|
+
* - **Pluggable transport (hermetically testable).** The network boundary is a
|
|
14
|
+
* single `post(path, { json, headers })` protocol, so a test can inject a
|
|
15
|
+
* fake that faithfully mimics the ingest API's documented 200/401/422/429
|
|
16
|
+
* responses. Production uses `fetch`.
|
|
17
|
+
* - **Fail-safe by default.** Telemetry must never crash the host, so a failed
|
|
18
|
+
* emit resolves to an `IngestResult { ok: false, ... }` instead of throwing.
|
|
19
|
+
* Pass `raiseOnError: true` for strict/CI behaviour.
|
|
20
|
+
* - **Provenance-honest.** `isSimulated` is carried through untouched; the
|
|
21
|
+
* written `source` is forced server-side to the key's project slug — the
|
|
22
|
+
* client cannot spoof another project's economics.
|
|
23
|
+
*/
|
|
24
|
+
export const CALLS_PATH = "/api/ingest/calls";
|
|
25
|
+
export const OUTCOMES_PATH = "/api/ingest/outcomes";
|
|
26
|
+
export const KEY_HEADER = "X-Margin-Key";
|
|
27
|
+
export const DEFAULT_INGEST_URL = "http://127.0.0.1:8000";
|
|
28
|
+
export const DEFAULT_TIMEOUT_MS = 10_000;
|
|
29
|
+
/** Raised (only in strict mode) when a row is rejected by the ingest API. */
|
|
30
|
+
export class MarginIngestError extends Error {
|
|
31
|
+
statusCode;
|
|
32
|
+
detail;
|
|
33
|
+
constructor(statusCode, detail) {
|
|
34
|
+
super(`ingest rejected (${statusCode}): ${detail}`);
|
|
35
|
+
this.name = "MarginIngestError";
|
|
36
|
+
this.statusCode = statusCode;
|
|
37
|
+
this.detail = detail;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Raised (only in strict mode) when the client is missing its ingest key. */
|
|
41
|
+
export class MarginConfigError extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "MarginConfigError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** The default transport: POST JSON over HTTP with the global `fetch`. */
|
|
48
|
+
export class FetchTransport {
|
|
49
|
+
baseUrl;
|
|
50
|
+
timeoutMs;
|
|
51
|
+
constructor(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
52
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
53
|
+
this.timeoutMs = timeoutMs;
|
|
54
|
+
}
|
|
55
|
+
async post(path, opts) {
|
|
56
|
+
return await fetch(this.baseUrl + path, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json", ...opts.headers },
|
|
59
|
+
body: JSON.stringify(opts.json),
|
|
60
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** A per-project client that emits measured economics to a Margin ingest API. */
|
|
65
|
+
export class MarginMeter {
|
|
66
|
+
ingestUrl;
|
|
67
|
+
apiKey;
|
|
68
|
+
raiseOnError;
|
|
69
|
+
transport;
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
const env = (typeof process !== "undefined" && process.env) || {};
|
|
72
|
+
this.ingestUrl = options.ingestUrl || env.MARGIN_INGEST_URL || DEFAULT_INGEST_URL;
|
|
73
|
+
this.apiKey = options.apiKey || env.MARGIN_INGEST_KEY;
|
|
74
|
+
this.raiseOnError = options.raiseOnError ?? false;
|
|
75
|
+
this.transport =
|
|
76
|
+
options.transport ||
|
|
77
|
+
new FetchTransport(this.ingestUrl, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
78
|
+
}
|
|
79
|
+
async emit(path, payload) {
|
|
80
|
+
if (!this.apiKey) {
|
|
81
|
+
const error = "no ingest key (set MARGIN_INGEST_KEY or pass apiKey)";
|
|
82
|
+
if (this.raiseOnError)
|
|
83
|
+
throw new MarginConfigError(error);
|
|
84
|
+
return { ok: false, statusCode: 0, error };
|
|
85
|
+
}
|
|
86
|
+
let resp;
|
|
87
|
+
try {
|
|
88
|
+
resp = await this.transport.post(path, {
|
|
89
|
+
json: payload,
|
|
90
|
+
headers: { [KEY_HEADER]: this.apiKey },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
const error = `transport error: ${e.message}`;
|
|
95
|
+
if (this.raiseOnError)
|
|
96
|
+
throw new MarginIngestError(0, e.message);
|
|
97
|
+
return { ok: false, statusCode: 0, error };
|
|
98
|
+
}
|
|
99
|
+
if (resp.status === 200) {
|
|
100
|
+
const body = (await resp.json());
|
|
101
|
+
return { ok: true, statusCode: 200, body };
|
|
102
|
+
}
|
|
103
|
+
const detail = await extractDetail(resp);
|
|
104
|
+
if (this.raiseOnError)
|
|
105
|
+
throw new MarginIngestError(resp.status, detail);
|
|
106
|
+
return { ok: false, statusCode: resp.status, error: detail };
|
|
107
|
+
}
|
|
108
|
+
/** Emit one measured LLM call. Cost is computed server-side when `costUsd`
|
|
109
|
+
* is omitted. Never throws unless `raiseOnError`. */
|
|
110
|
+
async recordCall(input) {
|
|
111
|
+
const payload = {
|
|
112
|
+
workflow_id: input.workflowId,
|
|
113
|
+
provider: input.provider,
|
|
114
|
+
model: input.model,
|
|
115
|
+
input_tokens: input.inputTokens ?? 0,
|
|
116
|
+
output_tokens: input.outputTokens ?? 0,
|
|
117
|
+
cache_read_tokens: input.cacheReadTokens ?? 0,
|
|
118
|
+
latency_ms: input.latencyMs ?? 0,
|
|
119
|
+
status: input.status ?? "ok",
|
|
120
|
+
is_retry: input.isRetry ?? false,
|
|
121
|
+
is_simulated: input.isSimulated ?? false,
|
|
122
|
+
};
|
|
123
|
+
if (input.sessionId !== undefined)
|
|
124
|
+
payload.session_id = input.sessionId;
|
|
125
|
+
if (input.promptId !== undefined)
|
|
126
|
+
payload.prompt_id = input.promptId;
|
|
127
|
+
if (input.costUsd !== undefined)
|
|
128
|
+
payload.cost_usd = input.costUsd;
|
|
129
|
+
return await this.emit(CALLS_PATH, payload);
|
|
130
|
+
}
|
|
131
|
+
/** Emit one outcome (a unit of productivity). `qualityMethod` records HOW the
|
|
132
|
+
* score was graded so a self-report is never mistaken for a graded number. */
|
|
133
|
+
async recordOutcome(input) {
|
|
134
|
+
const payload = {
|
|
135
|
+
workflow_id: input.workflowId,
|
|
136
|
+
passed: input.passed,
|
|
137
|
+
is_simulated: input.isSimulated ?? false,
|
|
138
|
+
};
|
|
139
|
+
if (input.qualityScore !== undefined)
|
|
140
|
+
payload.quality_score = input.qualityScore;
|
|
141
|
+
if (input.qualityMethod !== undefined)
|
|
142
|
+
payload.quality_method = input.qualityMethod;
|
|
143
|
+
if (input.link !== undefined)
|
|
144
|
+
payload.link = input.link;
|
|
145
|
+
return await this.emit(OUTCOMES_PATH, payload);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Time an async call and emit it on completion. The TS analogue of the
|
|
149
|
+
* Python `measure` context manager: latency is timed automatically, and the
|
|
150
|
+
* call is emitted with `status="error"` (and the error re-thrown) if `fn`
|
|
151
|
+
* throws. Set token counts inside `fn` via `m.setTokens(...)`. Returns the
|
|
152
|
+
* value `fn` produced; the emit's `IngestResult` is available as `m.result`.
|
|
153
|
+
*/
|
|
154
|
+
async measure(input, fn) {
|
|
155
|
+
const m = new Measurement();
|
|
156
|
+
const start = now();
|
|
157
|
+
let threw = false;
|
|
158
|
+
try {
|
|
159
|
+
return await fn(m);
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
threw = true;
|
|
163
|
+
throw e;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
const latencyMs = Math.round(now() - start);
|
|
167
|
+
m.result = await this.recordCall({
|
|
168
|
+
...input,
|
|
169
|
+
inputTokens: m.inputTokens,
|
|
170
|
+
outputTokens: m.outputTokens,
|
|
171
|
+
cacheReadTokens: m.cacheReadTokens,
|
|
172
|
+
latencyMs,
|
|
173
|
+
status: threw ? "error" : "ok",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Token accumulator passed into `measure`'s callback. */
|
|
179
|
+
export class Measurement {
|
|
180
|
+
inputTokens = 0;
|
|
181
|
+
outputTokens = 0;
|
|
182
|
+
cacheReadTokens = 0;
|
|
183
|
+
result;
|
|
184
|
+
setTokens(tokens) {
|
|
185
|
+
this.inputTokens = tokens.inputTokens ?? 0;
|
|
186
|
+
this.outputTokens = tokens.outputTokens ?? 0;
|
|
187
|
+
this.cacheReadTokens = tokens.cacheReadTokens ?? 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function extractDetail(resp) {
|
|
191
|
+
try {
|
|
192
|
+
const body = await resp.json();
|
|
193
|
+
if (body && typeof body === "object" && "detail" in body) {
|
|
194
|
+
return String(body.detail);
|
|
195
|
+
}
|
|
196
|
+
return JSON.stringify(body);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return "unparseable error body";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function now() {
|
|
203
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "margin-meter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tiny dependency-free client that emits LLM call + outcome economics to a Margin ingest API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "README.md"],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.build.json",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"test": "node --experimental-strip-types --test test/*.test.mjs",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["llm", "cost", "observability", "finops", "margin"],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": { "type": "git", "url": "https://github.com/subhsubh24/Margin.ai.git", "directory": "sdk/ts" },
|
|
26
|
+
"engines": { "node": ">=18" },
|
|
27
|
+
"devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.4.0" }
|
|
28
|
+
}
|