@spendgraph/sdk 0.1.0 → 0.2.1

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.
Files changed (63) hide show
  1. package/dist/ai.js +0 -12
  2. package/dist/client.d.ts +37 -0
  3. package/dist/client.js +36 -0
  4. package/dist/core/client/client.d.ts +30 -0
  5. package/dist/core/client/client.js +128 -0
  6. package/dist/core/client/errors.d.ts +25 -0
  7. package/dist/core/client/errors.js +17 -0
  8. package/dist/core/client/index.d.ts +2 -0
  9. package/dist/core/client/index.js +2 -0
  10. package/dist/core/types.d.ts +28 -0
  11. package/dist/core/types.js +1 -0
  12. package/dist/index.d.ts +9 -124
  13. package/dist/index.js +5 -421
  14. package/dist/langchain.js +3 -17
  15. package/dist/resources/alerts.d.ts +18 -0
  16. package/dist/resources/alerts.js +9 -0
  17. package/dist/resources/credentials.d.ts +22 -0
  18. package/dist/resources/credentials.js +15 -0
  19. package/dist/resources/events.d.ts +41 -0
  20. package/dist/resources/events.js +18 -0
  21. package/dist/resources/index.d.ts +21 -0
  22. package/dist/resources/index.js +12 -0
  23. package/dist/resources/ingest.d.ts +42 -0
  24. package/dist/resources/ingest.js +27 -0
  25. package/dist/resources/keys.d.ts +34 -0
  26. package/dist/resources/keys.js +15 -0
  27. package/dist/resources/playground.d.ts +7 -0
  28. package/dist/resources/playground.js +9 -0
  29. package/dist/resources/pricing.d.ts +49 -0
  30. package/dist/resources/pricing.js +36 -0
  31. package/dist/resources/projects.d.ts +54 -0
  32. package/dist/resources/projects.js +49 -0
  33. package/dist/resources/prompts-admin.d.ts +23 -0
  34. package/dist/resources/prompts-admin.js +21 -0
  35. package/dist/resources/prompts.d.ts +124 -0
  36. package/dist/resources/prompts.js +49 -0
  37. package/dist/resources/stats.d.ts +65 -0
  38. package/dist/resources/stats.js +21 -0
  39. package/dist/resources/tools.d.ts +66 -0
  40. package/dist/resources/tools.js +21 -0
  41. package/dist/rollout/index.d.ts +1 -0
  42. package/dist/rollout/index.js +1 -0
  43. package/dist/rollout/rollout.d.ts +75 -0
  44. package/dist/rollout/rollout.js +1 -0
  45. package/dist/schema/index.d.ts +3 -0
  46. package/dist/schema/index.js +2 -0
  47. package/dist/schema/serialize/index.d.ts +1 -0
  48. package/dist/schema/serialize/index.js +1 -0
  49. package/dist/schema/serialize/serialize.d.ts +12 -0
  50. package/dist/schema/serialize/serialize.js +31 -0
  51. package/dist/schema/types/index.d.ts +1 -0
  52. package/dist/schema/types/index.js +1 -0
  53. package/dist/schema/types/types.d.ts +58 -0
  54. package/dist/schema/types/types.js +1 -0
  55. package/dist/schema/validate/index.d.ts +1 -0
  56. package/dist/schema/validate/index.js +1 -0
  57. package/dist/schema/validate/validate.d.ts +28 -0
  58. package/dist/schema/validate/validate.js +92 -0
  59. package/dist/track/index.d.ts +2 -0
  60. package/dist/track/index.js +1 -0
  61. package/dist/track/track.d.ts +209 -0
  62. package/dist/track/track.js +331 -0
  63. package/package.json +3 -2
@@ -0,0 +1,66 @@
1
+ import type { FieldSpec } from "../schema/index.js";
2
+ import type { Client } from "../core/client/index.js";
3
+ import type { Scoped } from "./prompts.js";
4
+ /** What calling a tool does outside the process. Null means nobody has said. */
5
+ export type ToolEffect = "readonly" | "idempotent" | "destructive";
6
+ /**
7
+ * A tool's declaration, as stored.
8
+ *
9
+ * The handler is not here and never will be: this is what the model is told,
10
+ * and the function it maps to lives in the caller's codebase.
11
+ */
12
+ export interface ToolRow {
13
+ id: string;
14
+ projectId: string;
15
+ name: string;
16
+ description: string;
17
+ args: FieldSpec[];
18
+ effect: ToolEffect | null;
19
+ pinned: boolean;
20
+ archivedAt: string | null;
21
+ createdAt: string;
22
+ updatedAt: string;
23
+ }
24
+ export interface SaveToolInput {
25
+ projectId: string;
26
+ name: string;
27
+ description: string;
28
+ args?: FieldSpec[];
29
+ effect?: ToolEffect | null;
30
+ pinned?: boolean;
31
+ }
32
+ /**
33
+ * Declared tools.
34
+ *
35
+ * `get` and `list` take an API key, because binding a handler happens in a
36
+ * running service. Writing one is a dashboard action and needs a session — the
37
+ * same split prompts have.
38
+ */
39
+ export declare class Tools {
40
+ private readonly client;
41
+ constructor(client: Client);
42
+ list(query?: Scoped & {
43
+ limit?: number;
44
+ cursor?: string;
45
+ archived?: "only" | "active" | "all";
46
+ }): Promise<{
47
+ tools: ToolRow[];
48
+ nextCursor: string | null;
49
+ }>;
50
+ /** One tool, addressed by uuid or by the name the model calls. */
51
+ get(name: string, query?: Scoped): Promise<{
52
+ tool: ToolRow;
53
+ }>;
54
+ create(body: SaveToolInput): Promise<{
55
+ tool: ToolRow;
56
+ }>;
57
+ update(id: string, body: SaveToolInput, query?: Scoped): Promise<{
58
+ tool: ToolRow;
59
+ }>;
60
+ /** Out of the list, not destroyed. `archived: false` brings it back. */
61
+ archive(id: string, archived: boolean, query?: Scoped): Promise<{
62
+ id: string;
63
+ archivedAt: string | null;
64
+ unchanged?: boolean;
65
+ }>;
66
+ }
@@ -0,0 +1,21 @@
1
+ export class Tools {
2
+ client;
3
+ constructor(client) {
4
+ this.client = client;
5
+ }
6
+ list(query = {}) {
7
+ return this.client.get("/api/v1/tools", { ...query });
8
+ }
9
+ get(name, query = {}) {
10
+ return this.client.get(`/api/v1/tools/${encodeURIComponent(name)}`, { ...query });
11
+ }
12
+ create(body) {
13
+ return this.client.post("/api/v1/tools", body);
14
+ }
15
+ update(id, body, query = {}) {
16
+ return this.client.patch(`/api/v1/tools/${encodeURIComponent(id)}`, body, { ...query });
17
+ }
18
+ archive(id, archived, query = {}) {
19
+ return this.client.post(`/api/v1/tools/${encodeURIComponent(id)}/archive`, { archived }, { ...query });
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ export type { RolloutInput, RolloutRecord, RolloutStep } from "./rollout.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ import type { RenderedMessage as Message } from "../resources/prompts.js";
2
+ /**
3
+ * The record every capability writes.
4
+ *
5
+ * A prompt run is a rollout with one step. A graph run is a rollout with
6
+ * several. The `steps` column exists in `prompt_rollouts` from the first
7
+ * migration for exactly that reason, so the shape lives here rather than under
8
+ * `prompt/` — a second copy is how the two capabilities end up disagreeing
9
+ * about what a rollout is.
10
+ */
11
+ /** One unit of work inside a rollout. A prompt run has one; an agent run many. */
12
+ export interface RolloutStep {
13
+ /** Position in the run, so a failure can be attributed to a step. */
14
+ index: number;
15
+ /** What produced it — a prompt id, a tool name, a node name. */
16
+ source: string;
17
+ model?: string;
18
+ input?: unknown;
19
+ output?: string;
20
+ status?: "completed" | "failed";
21
+ error?: string;
22
+ inputTokens?: number;
23
+ outputTokens?: number;
24
+ latencyMs?: number;
25
+ }
26
+ /** What a caller hands back after running something itself. */
27
+ export interface RolloutInput {
28
+ /** Dedupe key. Generated if omitted; supply your own to make a retry safe. */
29
+ rolloutId?: string;
30
+ versionId?: string | null;
31
+ model: string;
32
+ fields: Record<string, string>;
33
+ rendered: Message[];
34
+ output: string;
35
+ status?: "completed" | "failed";
36
+ error?: string;
37
+ inputTokens?: number;
38
+ outputTokens?: number;
39
+ cacheReadTokens?: number;
40
+ cacheWriteTokens?: number;
41
+ citationTokens?: number;
42
+ reasoningTokens?: number;
43
+ latencyMs?: number;
44
+ /**
45
+ * The steps a multi-step run took. Absent for a single prompt call, which is
46
+ * the same thing with one step and nothing worth listing.
47
+ */
48
+ steps?: RolloutStep[];
49
+ /**
50
+ * The tools this run was offered, whether or not it called them.
51
+ *
52
+ * Without it a rollout cannot answer the first question anyone asks of a
53
+ * tool-using agent that got something wrong: was the tool it needed even in
54
+ * front of it? "Chose not to call `refund`" and "was never shown `refund`"
55
+ * produce identical records and want opposite fixes. Selection makes this
56
+ * sharper, not softer, because the offered set now varies per turn.
57
+ */
58
+ offeredTools?: string[];
59
+ /** Set only for evaluation rollouts. Production traffic leaves it unset. */
60
+ caseId?: string;
61
+ seed?: number;
62
+ candidateId?: string;
63
+ parentId?: string;
64
+ generation?: number;
65
+ }
66
+ /** What the server returns once it has priced and stored one. */
67
+ export interface RolloutRecord {
68
+ rolloutId: string;
69
+ status: "completed" | "failed";
70
+ output: string;
71
+ costMicros: number;
72
+ latencyMs: number;
73
+ inputTokens: number;
74
+ outputTokens: number;
75
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export { serializeFields } from "./serialize/index.js";
2
+ export type { FieldSpec, FieldType } from "./types/index.js";
3
+ export { type FieldError, FieldValidationError, validateFields } from "./validate/index.js";
@@ -0,0 +1,2 @@
1
+ export { serializeFields } from "./serialize/index.js";
2
+ export { FieldValidationError, validateFields } from "./validate/index.js";
@@ -0,0 +1 @@
1
+ export { serializeFields } from "./serialize.js";
@@ -0,0 +1 @@
1
+ export { serializeFields } from "./serialize.js";
@@ -0,0 +1,12 @@
1
+ import type { FieldSpec } from "../types/index.js";
2
+ /**
3
+ * Validated values as the flat string map the renderer substitutes.
4
+ *
5
+ * Throws rather than rendering something approximate. Headless there is no
6
+ * human reading the reply, so a missing required field would otherwise buy a
7
+ * paid-for response with a hole in it that nothing downstream can detect.
8
+ *
9
+ * An optional field left unset is omitted, so its `{name}` stays visible — the
10
+ * same behaviour the prompt builder has always had.
11
+ */
12
+ export declare function serializeFields(values: Record<string, unknown>, spec: FieldSpec[]): Record<string, string>;
@@ -0,0 +1,31 @@
1
+ import { FieldValidationError, isMissing, toBoolean, toNumber, validateFields, } from "../validate/index.js";
2
+ function renderOne(field, value) {
3
+ switch (field.type) {
4
+ case "number":
5
+ return String(toNumber(value));
6
+ case "boolean":
7
+ return toBoolean(value) ? (field.trueText ?? "true") : (field.falseText ?? "false");
8
+ case "list":
9
+ return value.map(String).join(field.separator ?? ", ");
10
+ case "json":
11
+ return JSON.stringify(value, null, 2);
12
+ default:
13
+ return String(value);
14
+ }
15
+ }
16
+ export function serializeFields(values, spec) {
17
+ const errors = validateFields(values, spec);
18
+ if (errors.length)
19
+ throw new FieldValidationError(errors);
20
+ const out = {};
21
+ for (const field of spec) {
22
+ const value = values[field.name];
23
+ if (isMissing(value)) {
24
+ if (field.default !== undefined)
25
+ out[field.name] = field.default;
26
+ continue;
27
+ }
28
+ out[field.name] = renderOne(field, value);
29
+ }
30
+ return out;
31
+ }
@@ -0,0 +1 @@
1
+ export type { FieldSpec, FieldType } from "./types.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Typed prompt fields — what a `{placeholder}` accepts, and how it renders.
3
+ *
4
+ * Deliberately a copy of `lib/fields.ts` rather than an import. This package is
5
+ * published and depends on nothing; reaching into the app for a type would make
6
+ * every installed copy break the next time the app moved a file. The two are
7
+ * kept in step by the wire format, which is the only contract that matters.
8
+ *
9
+ * A field type is two things, and the second is the one that gets forgotten: an
10
+ * input contract, and a rendering rule. Everything ends up interpolated into a
11
+ * string, so `{max_words}` given `50` has to become `"50"` and `{tags}` given
12
+ * `["a","b"]` has to become something the prompt author chose rather than
13
+ * whatever the first implementation happened to do.
14
+ *
15
+ * Deliberately not called `Variable`: a tool's arguments and an agent node's
16
+ * inputs are the same problem, and naming this after prompts guarantees it gets
17
+ * written twice more with three different meanings for `required`.
18
+ */
19
+ export type FieldType = "string" | "text" | "number" | "boolean" | "enum" | "list" | "json";
20
+ export interface FieldSpec {
21
+ /** Matches the `{placeholder}` in the blocks or question. */
22
+ name: string;
23
+ type: FieldType;
24
+ /** A missing required field throws before anything is sent. */
25
+ required: boolean;
26
+ /**
27
+ * Substituted verbatim when the field is absent — already-rendered text, not
28
+ * a value to coerce. A `list` default is the joined string, a `json` default
29
+ * is the printed JSON. Authored once by a human rather than typed per run, so
30
+ * asking for it in final form is simpler than parsing it back per type.
31
+ */
32
+ default?: string;
33
+ /**
34
+ * What the field means. Shown in the UI, and given to the optimizer's
35
+ * reflector — knowing what a field is *for* is the difference between a
36
+ * useful prompt mutation and a guess.
37
+ */
38
+ description?: string;
39
+ /** `enum` — the permitted values. */
40
+ options?: string[];
41
+ /** `list` — what joins the items. Defaults to ", ". */
42
+ separator?: string;
43
+ /** `number` — inclusive bounds. */
44
+ min?: number;
45
+ max?: number;
46
+ /** `string` | `text` — rejected above this length. */
47
+ maxLength?: number;
48
+ /** `boolean` — words to render instead of "true"/"false". */
49
+ trueText?: string;
50
+ falseText?: string;
51
+ /**
52
+ * This is the field a dataset varies.
53
+ *
54
+ * A prompt declaring `{question}` plus 200 stored values for it *is* a
55
+ * 200-case dataset; this flag is what says which field those values fill.
56
+ */
57
+ datasetKey?: boolean;
58
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export { type FieldError, FieldValidationError, isMissing, toBoolean, toNumber, validateFields, } from "./validate.js";
@@ -0,0 +1 @@
1
+ export { FieldValidationError, isMissing, toBoolean, toNumber, validateFields, } from "./validate.js";
@@ -0,0 +1,28 @@
1
+ import type { FieldSpec } from "../types/index.js";
2
+ export type FieldError = {
3
+ field: string;
4
+ message: string;
5
+ };
6
+ export declare class FieldValidationError extends Error {
7
+ readonly errors: FieldError[];
8
+ constructor(errors: FieldError[]);
9
+ }
10
+ /** Absent means absent. An empty string is a value someone chose to send. */
11
+ export declare function isMissing(value: unknown): boolean;
12
+ /**
13
+ * Coercions shared with the renderer.
14
+ *
15
+ * Shared deliberately: if the checker and the renderer disagreed, a value could
16
+ * pass validation and then render as something else — a request you paid for
17
+ * and cannot explain.
18
+ */
19
+ export declare function toNumber(value: unknown): number;
20
+ export declare function toBoolean(value: unknown): boolean;
21
+ /**
22
+ * Everything wrong with these values, rather than the first thing.
23
+ *
24
+ * Values with no matching field are ignored, not reported: a stored prompt keeps
25
+ * values for placeholders that were later edited out, and treating a stale key
26
+ * as an error would fail a prompt that renders perfectly well.
27
+ */
28
+ export declare function validateFields(values: Record<string, unknown>, spec: FieldSpec[]): FieldError[];
@@ -0,0 +1,92 @@
1
+ export class FieldValidationError extends Error {
2
+ errors;
3
+ constructor(errors) {
4
+ super(errors.map((e) => `${e.field}: ${e.message}`).join("; "));
5
+ this.name = "FieldValidationError";
6
+ this.errors = errors;
7
+ }
8
+ }
9
+ export function isMissing(value) {
10
+ return value === undefined || value === null;
11
+ }
12
+ export function toNumber(value) {
13
+ return typeof value === "number" ? value : Number(String(value).trim());
14
+ }
15
+ export function toBoolean(value) {
16
+ return typeof value === "boolean" ? value : String(value).toLowerCase() === "true";
17
+ }
18
+ function checkText(field, value) {
19
+ const text = String(value);
20
+ return field.maxLength !== undefined && text.length > field.maxLength
21
+ ? `is ${text.length} characters, over the limit of ${field.maxLength}`
22
+ : null;
23
+ }
24
+ function checkNumber(field, value) {
25
+ const n = toNumber(value);
26
+ if (!Number.isFinite(n))
27
+ return `expects a number, got ${JSON.stringify(value)}`;
28
+ if (field.min !== undefined && n < field.min)
29
+ return `is ${n}, below the minimum of ${field.min}`;
30
+ if (field.max !== undefined && n > field.max)
31
+ return `is ${n}, above the maximum of ${field.max}`;
32
+ return null;
33
+ }
34
+ function checkBoolean(value) {
35
+ if (typeof value === "boolean")
36
+ return null;
37
+ const s = String(value).toLowerCase();
38
+ return s === "true" || s === "false"
39
+ ? null
40
+ : `expects true or false, got ${JSON.stringify(value)}`;
41
+ }
42
+ function checkEnum(field, value) {
43
+ const options = field.options ?? [];
44
+ if (options.length === 0)
45
+ return "is an enum with no options declared";
46
+ return options.includes(String(value))
47
+ ? null
48
+ : `must be one of ${options.join(" | ")}, got ${JSON.stringify(value)}`;
49
+ }
50
+ function checkJson(value) {
51
+ try {
52
+ JSON.stringify(value);
53
+ return null;
54
+ }
55
+ catch (err) {
56
+ return `is not serialisable: ${err.message}`;
57
+ }
58
+ }
59
+ function checkOne(field, value) {
60
+ switch (field.type) {
61
+ case "string":
62
+ case "text":
63
+ return checkText(field, value);
64
+ case "number":
65
+ return checkNumber(field, value);
66
+ case "boolean":
67
+ return checkBoolean(value);
68
+ case "enum":
69
+ return checkEnum(field, value);
70
+ case "list":
71
+ return Array.isArray(value) ? null : `expects an array, got ${typeof value}`;
72
+ case "json":
73
+ return checkJson(value);
74
+ }
75
+ }
76
+ export function validateFields(values, spec) {
77
+ const errors = [];
78
+ for (const field of spec) {
79
+ const value = values[field.name];
80
+ if (isMissing(value)) {
81
+ if (field.default !== undefined)
82
+ continue;
83
+ if (field.required)
84
+ errors.push({ field: field.name, message: "is required" });
85
+ continue;
86
+ }
87
+ const problem = checkOne(field, value);
88
+ if (problem)
89
+ errors.push({ field: field.name, message: problem });
90
+ }
91
+ return errors;
92
+ }
@@ -0,0 +1,2 @@
1
+ export type { SpendGraphOptions, TrackEvent } from "./track.js";
2
+ export { SpendGraph } from "./track.js";
@@ -0,0 +1 @@
1
+ export { SpendGraph } from "./track.js";
@@ -0,0 +1,209 @@
1
+ export interface SpendGraphOptions {
2
+ /** spendgraph API key (sg_…). Without it the SDK is a no-op. */
3
+ apiKey: string | undefined;
4
+ /** Base URL of your deployed spendgraph app, e.g. https://costs.yourco.com */
5
+ baseUrl: string;
6
+ /** Flush at most this often (ms). Default 5000. */
7
+ flushIntervalMs?: number;
8
+ /** Flush when the buffer reaches this size. Default 20. */
9
+ maxBatch?: number;
10
+ }
11
+ export interface TrackEvent {
12
+ model: string;
13
+ /** Uncached input tokens — cache tokens go in the fields below. */
14
+ inputTokens: number;
15
+ outputTokens: number;
16
+ /** Prompt-cache read (hit) tokens, billed at the provider's cache-read rate. */
17
+ cacheReadTokens?: number;
18
+ /** Prompt-cache write (creation) tokens. */
19
+ cacheWriteTokens?: number;
20
+ /**
21
+ * Billed on their own by search-grounded providers — Perplexity Sonar Deep
22
+ * Research charges both at rates that are neither input nor output. Leave
23
+ * them unset for every other provider; OpenAI's `reasoning_tokens` are
24
+ * already counted inside `completion_tokens` and would be billed twice.
25
+ */
26
+ citationTokens?: number;
27
+ reasoningTokens?: number;
28
+ /**
29
+ * Client-side id: retried sends dedupe server-side.
30
+ * Must be 8–64 characters — shorter ids are rejected with a 422.
31
+ */
32
+ eventId?: string;
33
+ /** ISO timestamp; defaults to server receive time. */
34
+ timestamp?: string;
35
+ metadata?: Record<string, string | number | boolean>;
36
+ }
37
+ /**
38
+ * Fire-and-forget token tracking. track() never throws and never blocks;
39
+ * events batch in memory and flush every 5s or 20 events. Network failures
40
+ * retry once, then drop with one console.warn — tracking can never break
41
+ * the host app.
42
+ */
43
+ export declare class SpendGraph {
44
+ private readonly opts;
45
+ private queue;
46
+ private timer;
47
+ private warned;
48
+ /** Events lost in the current run of failures, reset by a successful flush. */
49
+ private dropped;
50
+ /** Unpriced model ids already reported, so each is named at most once. */
51
+ private readonly unpricedSeen;
52
+ private readonly interval;
53
+ private readonly maxBatch;
54
+ /** When the pending flush was scheduled, for the suspend check in track(). */
55
+ private timerAt;
56
+ private suspendWarned;
57
+ private noKeyWarned;
58
+ private streamUsageWarned;
59
+ /** The flush currently on the wire, so a later flush() can wait for it. */
60
+ private inFlight;
61
+ constructor(opts: SpendGraphOptions);
62
+ /**
63
+ * The package's client, given no retries of its own.
64
+ *
65
+ * The policy in `send` stays this class's: a flush is awaited inside
66
+ * serverless handlers, where blocking out a minute-long rate-limit window is
67
+ * worse than losing the batch, and the client's backoff would do exactly that.
68
+ */
69
+ private clientFor;
70
+ /** Record one LLM call. Synchronous, returns void, never throws. */
71
+ track(event: TrackEvent): void;
72
+ /**
73
+ * Says once that tracking is off.
74
+ *
75
+ * Running without a key stays a no-op on purpose — it keeps spendgraph out of
76
+ * tests and local runs without branching at every call site. But an unset
77
+ * SPENDGRAPH_API_KEY is indistinguishable from that choice, and it is the
78
+ * likeliest reason a fresh integration records nothing at all: the code is
79
+ * wired up correctly, the dashboard is empty, and nothing anywhere says why.
80
+ * Every other failure in this class warns once; this was the one that stayed
81
+ * quiet, which made it the hardest to find.
82
+ */
83
+ private warnNoKey;
84
+ /**
85
+ * Notices that the runtime froze with events still buffered.
86
+ *
87
+ * Serverless platforms suspend a function once it returns rather than
88
+ * exiting it, so neither the flush timer nor `beforeExit` ever runs and the
89
+ * queue is lost without a sound — the failure Langfuse documents for Lambda
90
+ * and Vercel. There is no reliable flag for "am I serverless", but there is
91
+ * direct evidence: a pending timer whose deadline passed long ago did not
92
+ * fire, which only happens if the runtime stopped executing between calls.
93
+ *
94
+ * Seeing that, send the stragglers now (they survive into this invocation)
95
+ * and say once what the fix is. Correct callers await flush(), which clears
96
+ * the timer, so this never fires for them.
97
+ */
98
+ private detectSuspendedRuntime;
99
+ /**
100
+ * Send everything buffered now. Call at the end of serverless handlers.
101
+ *
102
+ * Serialised against a flush already running, because this method is not the
103
+ * only thing that starts one: track() flushes by itself the moment the batch
104
+ * is full, and so do the timer, the suspend check and the exit hook. Those
105
+ * empty the queue synchronously, so an awaited flush() used to find nothing
106
+ * left to send and hand back an already-resolved promise while the real
107
+ * batch was still on the wire — and the next thing a serverless handler does
108
+ * after awaiting is return, freezing the runtime and losing exactly the
109
+ * events the await was there to save.
110
+ *
111
+ * The drain still starts synchronously — `drain()` runs as far as its first
112
+ * await before returning a promise, so the queue is taken during this call
113
+ * and not a microtask later. That ordering is load-bearing: the suspend
114
+ * check and the exit hook both fire a flush and then expect the buffer to be
115
+ * gone, and deferring it broke them.
116
+ */
117
+ flush(): Promise<void>;
118
+ /** One pass: stop the timer, take the queue, put it on the wire. */
119
+ private drain;
120
+ /**
121
+ * Wrap an Anthropic or OpenAI client. Use the wrapped client exactly as
122
+ * before — token usage is read off each response and tracked automatically.
123
+ *
124
+ * Streaming is covered too:
125
+ * - helper streams (`anthropic.messages.stream()`,
126
+ * `openai.beta.chat.completions.stream()`) are tracked via their
127
+ * final-message promise — the stream you get back is untouched;
128
+ * - raw streams (`create({ stream: true })`) are tee'd: you receive one
129
+ * branch, usage is accumulated off the other. For OpenAI raw streams,
130
+ * pass `stream_options: { include_usage: true }` or there is no usage
131
+ * to read and the call goes untracked.
132
+ */
133
+ wrap<T extends object>(client: T): T;
134
+ /**
135
+ * The recursive wrapper behind wrap().
136
+ *
137
+ * Two things here are deliberate and were both wrong before.
138
+ *
139
+ * `Reflect.get` is called without a receiver, so a getter runs with the real
140
+ * client as `this`. Passing the proxy — the obvious reading of the Proxy
141
+ * docs — makes any getter that touches a `#private` field throw
142
+ * "Cannot read private member", and the provider SDKs this is built to wrap
143
+ * use private fields. That turned a tracking wrapper into a crash on a
144
+ * client that works perfectly well unwrapped, in an SDK whose whole contract
145
+ * is to fail open.
146
+ *
147
+ * And each wrapper is kept, so `client.messages` and `client.messages.create`
148
+ * are the same object and the same function every time they are read.
149
+ * Rebuilding them per access allocated a proxy and a closure on every call
150
+ * and quietly broke any caller that holds onto a method or compares
151
+ * identity. The cache is per proxy and keyed by property, so a method always
152
+ * applies to the object it was read from, and it re-wraps if the underlying
153
+ * value is ever replaced.
154
+ */
155
+ private proxy;
156
+ private observeResult;
157
+ private interceptStream;
158
+ /** Accumulate usage off a tee'd SSE branch (Anthropic events / OpenAI chunks). */
159
+ private consumeStream;
160
+ /**
161
+ * Says once that a streamed call went untracked.
162
+ *
163
+ * A raw OpenAI stream carries no usage unless the caller asks for it, so the
164
+ * accumulator above finishes with nothing to report and simply returns —
165
+ * correct, and completely silent. That is the same hole warnNoKey was added
166
+ * to close: the integration looks right, the dashboard stays empty for those
167
+ * calls, and nothing anywhere connects the two. Anthropic streams always
168
+ * carry usage, so in practice this names the one option that fixes it.
169
+ *
170
+ * Guarded on having seen at least one chunk, so an empty or aborted stream —
171
+ * where there was never anything to read — stays quiet.
172
+ */
173
+ private warnStreamNoUsage;
174
+ private trackFromResponse;
175
+ private send;
176
+ /**
177
+ * The drop policy, which is this class's rather than the client's.
178
+ *
179
+ * 429 is the one failure the server tells you how to fix: it sends
180
+ * retry-after, and the window is at most a minute wide, so a batch that
181
+ * arrived at the end of one is deliverable a second later. Dropping it
182
+ * immediately threw that away and reported the least useful thing it could —
183
+ * "ingest returned 429" — for the one status with a documented remedy. The
184
+ * wait is capped because flush() is awaited inside serverless handlers, where
185
+ * blocking for a minute is worse than losing the batch.
186
+ */
187
+ private onSendFailed;
188
+ /**
189
+ * Surfaces model ids the server could not price, once each.
190
+ *
191
+ * These are accepted and stored, so nothing here is an error — but they cost
192
+ * $0, and a dashboard reading $0 is indistinguishable from one reading
193
+ * "nothing happened". Naming the id in the integrator's own console is the
194
+ * cheapest possible moment to catch a typo or an unmapped model, and the
195
+ * per-id guard keeps a steady stream of the same unknown model from becoming
196
+ * log noise.
197
+ */
198
+ private reportUnpriced;
199
+ /**
200
+ * Warns once per outage, with a running count of what was lost.
201
+ *
202
+ * Warning on every flush would spam a hot loop, but warning exactly once per
203
+ * process — the previous behaviour — hid a server-side bug that failed every
204
+ * full batch: one line early in a long-lived process, then silence, while the
205
+ * dashboard quietly undercounted. The count is what makes the silence legible
206
+ * when someone does go looking.
207
+ */
208
+ private reportDropped;
209
+ }