@clocknext/sdk 0.7.0 → 0.10.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 +93 -18
- package/dist/index.cjs +174 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +464 -66
- package/dist/index.d.ts +464 -66
- package/dist/index.js +174 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,16 @@ await cnk.flush();
|
|
|
41
41
|
## Recording signals
|
|
42
42
|
|
|
43
43
|
There are three signal types, matching ClockNext's billable primitives. Each has
|
|
44
|
-
a typed helper that enforces the right required fields at compile time
|
|
44
|
+
a typed helper that enforces the right required fields at compile time, and each
|
|
45
|
+
posts to **its own route** — `/api/v1/signal/credit`, `/signal/wallet`,
|
|
46
|
+
`/signal/outcome`. The signal's `type` is what picks that route; from 0.10 it is
|
|
47
|
+
no longer sent in the body.
|
|
48
|
+
|
|
49
|
+
> **Migrating from 0.9 or earlier?** Your calling code does not change — same
|
|
50
|
+
> methods, same arguments. What changed is the wire: the SDK now targets the
|
|
51
|
+
> `/api/v1/signal/*` family instead of the deprecated `POST /api/v1/usage`, and
|
|
52
|
+
> the `metric` field is now `composite` (see [Composites](#composites)). Bump the
|
|
53
|
+
> package and you are migrated.
|
|
45
54
|
|
|
46
55
|
`model` is always the **catalog model id** enabled in your org (matched
|
|
47
56
|
case-insensitively against `OrgModel.modelId`). Credits and outcomes are
|
|
@@ -76,23 +85,54 @@ await cnk.signals.outcome({ customerId, model, agentKey: "resolved_ticket.verify
|
|
|
76
85
|
await cnk.signals.outcome({ customerId, model, agentKey: "resolved_ticket.resolve", runId, complete: true, tokens: { input: 500, output: 200 } });
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
###
|
|
88
|
+
### Composites
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
A **composite** bundles several credits / outcomes / units and is priced and
|
|
91
|
+
billed as one thing. Tag a signal with `composite` to group it into one
|
|
92
|
+
*occurrence* — `ref` is the composite's ref ID and `value` is your own
|
|
93
|
+
correlation id for that occurrence (a session, a call, a job):
|
|
84
94
|
|
|
85
95
|
```ts
|
|
86
96
|
await cnk.signals.credit({
|
|
87
97
|
customerId, model: "llama-3.3-70b-versatile", agentKey: "api_credit",
|
|
88
98
|
tokens: { input: 1200, output: 340 },
|
|
89
|
-
|
|
99
|
+
composite: { ref: "voice_ai", value: "call_5f3a" },
|
|
90
100
|
});
|
|
91
101
|
```
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
An occurrence opens on the first tagged signal and costs nothing. **Closing it
|
|
104
|
+
is what bills it** — one charge for the bundle, however many signals it
|
|
105
|
+
contained:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
await cnk.signals.completeComposite({
|
|
109
|
+
customerId,
|
|
110
|
+
composite: { ref: "voice_ai", value: "call_5f3a" },
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Only your code knows when the work ended, so closing is a declaration, not
|
|
115
|
+
something ClockNext infers from which signals have arrived: tagged signals may
|
|
116
|
+
arrive in any order and the occurrence stays open and unbilled until you say
|
|
117
|
+
otherwise. Put the call on the line before your own `return`.
|
|
118
|
+
|
|
119
|
+
An **outcome never needs it** — its own `complete: true` closes its run and the
|
|
120
|
+
matching occurrence together. Credits and units have no "done" signal of their
|
|
121
|
+
own, so this is theirs.
|
|
122
|
+
|
|
123
|
+
Closing twice is safe (`ALREADY_COMPLETED`, and it cannot bill again), and so is
|
|
124
|
+
closing *early* — before every entitlement in the bundle has reported, or while
|
|
125
|
+
a prepaid slot pool is full: the server parks the request and applies it when
|
|
126
|
+
the blocker clears, rather than failing it.
|
|
127
|
+
|
|
128
|
+
A tag that names no live composite is ignored and never fails the signal — the
|
|
129
|
+
signal carries money, the tag only decides what it is grouped under. Tags on
|
|
130
|
+
wallet signals are ignored.
|
|
131
|
+
|
|
132
|
+
> **Renamed in 0.10.** This field was called `metric` and was documented as
|
|
133
|
+
> "reporting only, never changes what's billed". That was wrong: where the plan
|
|
134
|
+
> sells the composite, the occurrence is the billable unit. Rename `metric:` to
|
|
135
|
+
> `composite:` — the shape (`{ ref, value }`) is unchanged.
|
|
96
136
|
|
|
97
137
|
### Async vs sync
|
|
98
138
|
|
|
@@ -102,21 +142,31 @@ buffer on a size/interval trigger. This keeps billing telemetry off your LLM hot
|
|
|
102
142
|
path.
|
|
103
143
|
|
|
104
144
|
> **"Batching" here means timing, not a bulk request.** Each signal is still its
|
|
105
|
-
> own
|
|
106
|
-
> how many run at once (`batch.maxConcurrency`). There is
|
|
107
|
-
> call, so size your request budget per signal.
|
|
145
|
+
> own POST to its own `/api/v1/signal/*` route; the flusher just groups *when*
|
|
146
|
+
> sends happen and caps how many run at once (`batch.maxConcurrency`). There is
|
|
147
|
+
> no single bulk-ingest call, so size your request budget per signal.
|
|
108
148
|
|
|
109
|
-
To
|
|
149
|
+
To await the request itself (rather than buffering it), either:
|
|
110
150
|
|
|
111
151
|
```ts
|
|
112
152
|
// per call
|
|
113
153
|
const res = await cnk.signals.credit({ … }, { wait: true });
|
|
114
|
-
console.log(res.usageLog);
|
|
115
154
|
|
|
116
155
|
// or globally
|
|
117
156
|
const cnk = new ClockNext({ apiKey, mode: "sync" });
|
|
118
157
|
```
|
|
119
158
|
|
|
159
|
+
> **Awaiting is not the same as billed.** Where the server has a durable queue
|
|
160
|
+
> configured it answers `202` and prices the signal in a worker, so you get
|
|
161
|
+
> `{ queued: true, messageId, usageLog: null }` — accepted, not yet recorded, and
|
|
162
|
+
> validated only later (a bad `agentKey` is dropped there, silently). Only when
|
|
163
|
+
> the server prices inline do you get `{ queued: false, usageLog }`. Branch on
|
|
164
|
+
> `queued`; never assume `usageLog` is populated.
|
|
165
|
+
>
|
|
166
|
+
> To validate and price a signal synchronously, use `signals.verify()` (a dry
|
|
167
|
+
> run — it always returns the projected log). To confirm a real signal landed,
|
|
168
|
+
> read it back with `signals.list({ customerId })`.
|
|
169
|
+
|
|
120
170
|
> In `async` mode, server-side rejections (e.g. insufficient allowance, `422`)
|
|
121
171
|
> surface via the `onError` hook or `flush()`, not from the `track()` call.
|
|
122
172
|
|
|
@@ -150,7 +200,7 @@ const cnk = new ClockNext({ apiKey, mode: "sync" });
|
|
|
150
200
|
const c = await cnk.customers.create({ name: "Acme", email: "ops@acme.com" });
|
|
151
201
|
const one = await cnk.customers.get(c.id);
|
|
152
202
|
const page = await cnk.customers.list({ limit: 50, q: "ac" });
|
|
153
|
-
await cnk.customers.update(c.id, {
|
|
203
|
+
await cnk.customers.update(c.id, { legalName: "Acme Inc." });
|
|
154
204
|
await cnk.customers.delete(c.id);
|
|
155
205
|
|
|
156
206
|
// iterate every customer (auto-follows the cursor)
|
|
@@ -166,7 +216,7 @@ await cnk.customers.revenue(c.id, { range: "30d" });
|
|
|
166
216
|
### Members
|
|
167
217
|
|
|
168
218
|
```ts
|
|
169
|
-
await cnk.customers.addMember(c.id, { name: "Alice",
|
|
219
|
+
await cnk.customers.addMember(c.id, { name: "Alice", email: "alice@acme.com" });
|
|
170
220
|
const members = await cnk.customers.listMembers(c.id);
|
|
171
221
|
await cnk.customers.updateMember(c.id, members[0].id, { name: "Alice R." });
|
|
172
222
|
await cnk.customers.removeMember(c.id, members[0].id);
|
|
@@ -230,9 +280,12 @@ Define the billable *types*; record usage against them with `signals.*`.
|
|
|
230
280
|
await cnk.credits.create({ name: "API", agentKey: "api_credit", basePrice: 1, marginPercent: 20, pricePerCredit: 0.01 });
|
|
231
281
|
await cnk.credits.list({ active: true });
|
|
232
282
|
|
|
233
|
-
// outcome type — a multi-step workflow
|
|
283
|
+
// outcome type — a multi-step workflow. TWO kinds of agentKey, both required and
|
|
284
|
+
// both unique per org: the outcome's own (its identity) and each step's (what
|
|
285
|
+
// signals.outcome() reports against). They are separate namespaces.
|
|
234
286
|
await cnk.outcomes.create({
|
|
235
287
|
name: "Resolved ticket",
|
|
288
|
+
agentKey: "resolved_ticket",
|
|
236
289
|
basePrice: 1, marginPercent: 20, pricePerOutcome: 2,
|
|
237
290
|
steps: [{ name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 }],
|
|
238
291
|
});
|
|
@@ -243,13 +296,35 @@ await cnk.units.create({ name: "Seats", agentKey: "seats", pricingType: "FLAT",
|
|
|
243
296
|
|
|
244
297
|
### Unit metering
|
|
245
298
|
|
|
246
|
-
Record a unit event (no tokens) and read a customer's unit balances:
|
|
299
|
+
Record a unit event (no tokens, no model) and read a customer's unit balances:
|
|
247
300
|
|
|
248
301
|
```ts
|
|
249
302
|
await cnk.signals.unit({ customerId: c.id, agentKey: "seats" });
|
|
303
|
+
|
|
304
|
+
// several at once, instead of N separate calls
|
|
305
|
+
await cnk.signals.unit({ customerId: c.id, agentKey: "seats", quantity: 3 });
|
|
306
|
+
|
|
307
|
+
// part of a composite, and safe to retry
|
|
308
|
+
await cnk.signals.unit({
|
|
309
|
+
customerId: c.id, agentKey: "seats",
|
|
310
|
+
composite: { ref: "voice_ai", value: "call_5f3a" },
|
|
311
|
+
idempotencyKey: `seats:${requestId}`,
|
|
312
|
+
});
|
|
313
|
+
|
|
250
314
|
await cnk.signals.unitUsage({ customerId: c.id });
|
|
251
315
|
```
|
|
252
316
|
|
|
317
|
+
`quantity` defaults to 1. On a prepaid (ADVANCE) plan the draw is
|
|
318
|
+
all-or-nothing: asking for more than remains is refused in full rather than
|
|
319
|
+
partially filled.
|
|
320
|
+
|
|
321
|
+
> **Changed in 0.10.** `signals.unit()` moved to `POST /api/v1/signal/unit`,
|
|
322
|
+
> which is **durable** (the report is written down before it is priced, so a
|
|
323
|
+
> failure parks for retry instead of losing a billable event) and **idempotent**
|
|
324
|
+
> (on the old route a retried report drew the balance down twice). Because it can
|
|
325
|
+
> now answer `202`, this resolves `{ queued, unitUsage, rawId }` rather than a
|
|
326
|
+
> bare `{ unitUsage }` — branch on `queued`, don't assume `unitUsage`.
|
|
327
|
+
|
|
253
328
|
## Webhooks
|
|
254
329
|
|
|
255
330
|
Threshold alerts — at most one per kind (`CREDIT` / `OUTCOME` / `WALLET`):
|
package/dist/index.cjs
CHANGED
|
@@ -121,7 +121,8 @@ var Flusher = class {
|
|
|
121
121
|
try {
|
|
122
122
|
await this.transport.request({
|
|
123
123
|
method: "POST",
|
|
124
|
-
|
|
124
|
+
// Per-item, not per-Flusher: one route per meter. See `QueuedSignal`.
|
|
125
|
+
path: item.path,
|
|
125
126
|
body: item.body,
|
|
126
127
|
// Best-effort metering opts into retries on transient failures.
|
|
127
128
|
retry: true
|
|
@@ -229,7 +230,7 @@ function sleep(ms) {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
// src/http.ts
|
|
232
|
-
var SDK_VERSION = "0.
|
|
233
|
+
var SDK_VERSION = "0.10.0" ;
|
|
233
234
|
var Transport = class {
|
|
234
235
|
constructor(cfg) {
|
|
235
236
|
this.cfg = cfg;
|
|
@@ -324,6 +325,36 @@ var Transport = class {
|
|
|
324
325
|
}
|
|
325
326
|
};
|
|
326
327
|
|
|
328
|
+
// src/resources/composites.ts
|
|
329
|
+
var Composites = class {
|
|
330
|
+
constructor(transport) {
|
|
331
|
+
this.transport = transport;
|
|
332
|
+
}
|
|
333
|
+
transport;
|
|
334
|
+
/** List composites. Pass `{ active: true }` for only the ones a signal can
|
|
335
|
+
* still be tagged with — the default list INCLUDES archived ones, which
|
|
336
|
+
* keep every row they already own. */
|
|
337
|
+
async list(params = {}) {
|
|
338
|
+
const res = await this.transport.request({
|
|
339
|
+
method: "GET",
|
|
340
|
+
path: "/api/v1/composites",
|
|
341
|
+
query: {
|
|
342
|
+
active: params.active === void 0 ? void 0 : String(params.active)
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
return res.composites;
|
|
346
|
+
}
|
|
347
|
+
/** Create a composite. `POST /api/v1/composites`. */
|
|
348
|
+
async create(input) {
|
|
349
|
+
const res = await this.transport.request({
|
|
350
|
+
method: "POST",
|
|
351
|
+
path: "/api/v1/composites",
|
|
352
|
+
body: input
|
|
353
|
+
});
|
|
354
|
+
return res.composite;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
327
358
|
// src/resources/credits.ts
|
|
328
359
|
var Credits = class {
|
|
329
360
|
constructor(transport) {
|
|
@@ -506,7 +537,7 @@ var Customers = class {
|
|
|
506
537
|
});
|
|
507
538
|
return res.members;
|
|
508
539
|
}
|
|
509
|
-
/** Add a member to a customer. Duplicate `
|
|
540
|
+
/** Add a member to a customer. Duplicate `email` → ConflictError. */
|
|
510
541
|
async addMember(id, input) {
|
|
511
542
|
const res = await this.transport.request({
|
|
512
543
|
method: "POST",
|
|
@@ -515,7 +546,7 @@ var Customers = class {
|
|
|
515
546
|
});
|
|
516
547
|
return res.member;
|
|
517
548
|
}
|
|
518
|
-
/** Update a member's profile
|
|
549
|
+
/** Update a member's profile. Duplicate `email` → ConflictError. `PATCH …/:id/members/:memberId`. */
|
|
519
550
|
async updateMember(id, memberId, input) {
|
|
520
551
|
const res = await this.transport.request({
|
|
521
552
|
method: "PATCH",
|
|
@@ -524,12 +555,13 @@ var Customers = class {
|
|
|
524
555
|
});
|
|
525
556
|
return res.member;
|
|
526
557
|
}
|
|
527
|
-
/** Remove a member from a customer. `DELETE …/:id/members/:memberId`. */
|
|
558
|
+
/** Remove a member from a customer, returning who was removed. `DELETE …/:id/members/:memberId`. */
|
|
528
559
|
async removeMember(id, memberId) {
|
|
529
|
-
await this.transport.request({
|
|
560
|
+
const res = await this.transport.request({
|
|
530
561
|
method: "DELETE",
|
|
531
562
|
path: `/api/v1/customers/${encodeURIComponent(id)}/members/${encodeURIComponent(memberId)}`
|
|
532
563
|
});
|
|
564
|
+
return res.member;
|
|
533
565
|
}
|
|
534
566
|
// --- Wallet & billing config --------------------------------------------
|
|
535
567
|
/** The customer's wallet balance + transactions. `GET …/:id/wallet`. */
|
|
@@ -855,18 +887,28 @@ function newKey() {
|
|
|
855
887
|
}
|
|
856
888
|
|
|
857
889
|
// src/resources/signals.ts
|
|
890
|
+
var SIGNAL_PATHS = {
|
|
891
|
+
credit: "/api/v1/signal/credit",
|
|
892
|
+
outcome: "/api/v1/signal/outcome",
|
|
893
|
+
wallet: "/api/v1/signal/wallet"
|
|
894
|
+
};
|
|
895
|
+
var UNIT_PATH = "/api/v1/signal/unit";
|
|
896
|
+
var COMPOSITE_COMPLETE_PATH = "/api/v1/signal/composite/complete";
|
|
858
897
|
function signalToWire(signal) {
|
|
898
|
+
const t = signal.tokens;
|
|
859
899
|
const body = {
|
|
860
900
|
customerId: signal.customerId,
|
|
861
|
-
|
|
862
|
-
|
|
901
|
+
// `usage` is required on all three model-priced kinds. input/output are
|
|
902
|
+
// always sent; `cacheTokens` only when provided (the server defaults 0).
|
|
903
|
+
usage: {
|
|
904
|
+
model: signal.model,
|
|
905
|
+
inputTokens: t.input,
|
|
906
|
+
outputTokens: t.output,
|
|
907
|
+
...t.cache != null ? { cacheTokens: t.cache } : {}
|
|
908
|
+
}
|
|
863
909
|
};
|
|
864
910
|
if (signal.member) body.member = signal.member;
|
|
865
911
|
if (signal.custom) body.custom = signal.custom;
|
|
866
|
-
const t = signal.tokens;
|
|
867
|
-
body.inputTokens = t.input;
|
|
868
|
-
body.outputTokens = t.output;
|
|
869
|
-
if (t.cache != null) body.cacheTokens = t.cache;
|
|
870
912
|
if (signal.type === "credit" || signal.type === "outcome") {
|
|
871
913
|
body.agentKey = signal.agentKey;
|
|
872
914
|
}
|
|
@@ -874,12 +916,18 @@ function signalToWire(signal) {
|
|
|
874
916
|
body.runId = signal.runId;
|
|
875
917
|
if (signal.complete != null) body.complete = signal.complete;
|
|
876
918
|
}
|
|
877
|
-
if (signal.type !== "wallet" && signal.
|
|
878
|
-
body[signal.
|
|
919
|
+
if (signal.type !== "wallet" && signal.composite) {
|
|
920
|
+
body.composite = { [signal.composite.ref]: signal.composite.value };
|
|
879
921
|
}
|
|
880
922
|
if (signal.idempotencyKey) body.idempotencyKey = signal.idempotencyKey;
|
|
881
923
|
return body;
|
|
882
924
|
}
|
|
925
|
+
function handles(res) {
|
|
926
|
+
return {
|
|
927
|
+
...res.messageId != null ? { messageId: res.messageId } : {},
|
|
928
|
+
...res.rawId != null ? { rawId: res.rawId } : {}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
883
931
|
var Signals = class {
|
|
884
932
|
constructor(transport, flusher, cfg) {
|
|
885
933
|
this.transport = transport;
|
|
@@ -889,56 +937,79 @@ var Signals = class {
|
|
|
889
937
|
transport;
|
|
890
938
|
flusher;
|
|
891
939
|
cfg;
|
|
940
|
+
/**
|
|
941
|
+
* Record one usage signal, to the route its `type` selects:
|
|
942
|
+
* `POST /api/v1/signal/credit` | `/signal/outcome` | `/signal/wallet`.
|
|
943
|
+
*
|
|
944
|
+
* Returns as soon as the signal is buffered (async mode) or the request has
|
|
945
|
+
* been answered (`{ wait: true }` / sync mode). See {@link TrackResult}: a
|
|
946
|
+
* resolved promise means "accepted", which is not the same as "billed".
|
|
947
|
+
*/
|
|
892
948
|
async track(signal, opts = {}) {
|
|
893
949
|
const body = signalToWire(signal);
|
|
894
950
|
if (body.idempotencyKey == null) body.idempotencyKey = newKey();
|
|
951
|
+
const path = SIGNAL_PATHS[signal.type];
|
|
895
952
|
const sendNow = opts.wait || this.cfg.mode === "sync";
|
|
896
953
|
if (!sendNow) {
|
|
897
|
-
this.flusher.enqueue({ body, signal });
|
|
954
|
+
this.flusher.enqueue({ path, body, signal });
|
|
898
955
|
return { queued: true };
|
|
899
956
|
}
|
|
900
957
|
const res = await this.transport.request({
|
|
901
958
|
method: "POST",
|
|
902
|
-
path
|
|
959
|
+
path,
|
|
903
960
|
body,
|
|
904
961
|
// Best-effort metering opts into retries on transient failures.
|
|
905
962
|
retry: true
|
|
906
963
|
});
|
|
907
964
|
return {
|
|
908
|
-
queued:
|
|
909
|
-
usageLog: res.usageLog ?? null
|
|
965
|
+
queued: res.queued === true,
|
|
966
|
+
usageLog: res.usageLog ?? null,
|
|
967
|
+
...handles(res)
|
|
910
968
|
};
|
|
911
969
|
}
|
|
912
970
|
/**
|
|
913
971
|
* Validate + price a signal WITHOUT recording it (server `dryRun`). Returns
|
|
914
972
|
* the projected usage log so an integration can preflight before sending real
|
|
915
973
|
* traffic — no inbox row, wallet / outcome writes, or webhooks fire.
|
|
974
|
+
*
|
|
975
|
+
* Only the model-priced kinds support this. A dry run is not offered for unit
|
|
976
|
+
* reports or composite completions, and the server REJECTS the flag on those
|
|
977
|
+
* routes rather than accepting and ignoring it — which is why there is no
|
|
978
|
+
* dry-run path through `unit()` or `completeComposite()`.
|
|
916
979
|
*/
|
|
917
980
|
async verify(signal) {
|
|
918
981
|
const body = signalToWire(signal);
|
|
919
982
|
body.dryRun = true;
|
|
920
983
|
const res = await this.transport.request({
|
|
921
984
|
method: "POST",
|
|
922
|
-
path:
|
|
985
|
+
path: SIGNAL_PATHS[signal.type],
|
|
923
986
|
body,
|
|
924
987
|
// A dry run writes nothing, so it is always safe to retry.
|
|
925
988
|
retry: true
|
|
926
989
|
});
|
|
927
990
|
return res.usageLog ?? null;
|
|
928
991
|
}
|
|
929
|
-
/** Meter a named credit. */
|
|
992
|
+
/** Meter a named credit. `POST /api/v1/signal/credit`. */
|
|
930
993
|
credit(input, opts) {
|
|
931
994
|
return this.track({ ...input, type: "credit" }, opts);
|
|
932
995
|
}
|
|
933
|
-
/** Debit the customer's wallet at model cost. */
|
|
996
|
+
/** Debit the customer's wallet at model cost. `POST /api/v1/signal/wallet`. */
|
|
934
997
|
wallet(input, opts) {
|
|
935
998
|
return this.track({ ...input, type: "wallet" }, opts);
|
|
936
999
|
}
|
|
937
|
-
/** Advance one step of an outcome workflow. */
|
|
1000
|
+
/** Advance one step of an outcome workflow. `POST /api/v1/signal/outcome`. */
|
|
938
1001
|
outcome(input, opts) {
|
|
939
1002
|
return this.track({ ...input, type: "outcome" }, opts);
|
|
940
1003
|
}
|
|
941
|
-
/**
|
|
1004
|
+
/**
|
|
1005
|
+
* Recent usage logs + totals for a customer. `GET /api/v1/usage`.
|
|
1006
|
+
*
|
|
1007
|
+
* Still the legacy path, deliberately. The `/api/v1/signal/*` family is
|
|
1008
|
+
* POST-only — it replaced how usage is INGESTED, not how it is read — so
|
|
1009
|
+
* `GET /api/v1/usage` has no successor and is not deprecated. It is also the
|
|
1010
|
+
* only way to prove a signal landed, which matters more now that ingestion
|
|
1011
|
+
* answers `202` for everything.
|
|
1012
|
+
*/
|
|
942
1013
|
async list(params) {
|
|
943
1014
|
const res = await this.transport.request({
|
|
944
1015
|
method: "GET",
|
|
@@ -950,22 +1021,94 @@ var Signals = class {
|
|
|
950
1021
|
/**
|
|
951
1022
|
* Record one unit-metered event (a fixed-price meter with no tokens),
|
|
952
1023
|
* addressed to a catalog unit by its `agentKey`. Sent immediately — unit
|
|
953
|
-
* events are not buffered. `POST /api/v1/
|
|
1024
|
+
* events are not buffered. `POST /api/v1/signal/unit`.
|
|
1025
|
+
*
|
|
1026
|
+
* Two things changed with the move off `POST /api/v1/units`, and both show up
|
|
1027
|
+
* in the return type. The route is now durable (the report lands in an inbox
|
|
1028
|
+
* before it is priced, so a failure parks as Pending and can be retried
|
|
1029
|
+
* instead of losing a billable event) and it answers `202` when it queues —
|
|
1030
|
+
* so this resolves {@link UnitTrackResult}, not a bare `{ unitUsage }`.
|
|
1031
|
+
* Branch on `queued`.
|
|
954
1032
|
*/
|
|
955
1033
|
async unit(input) {
|
|
956
|
-
|
|
1034
|
+
const res = await this.transport.request({
|
|
957
1035
|
method: "POST",
|
|
958
|
-
path:
|
|
1036
|
+
path: UNIT_PATH,
|
|
959
1037
|
body: {
|
|
960
1038
|
customerId: input.customerId,
|
|
961
1039
|
agentKey: input.agentKey,
|
|
962
|
-
...input.
|
|
1040
|
+
...input.quantity != null ? { quantity: input.quantity } : {},
|
|
1041
|
+
...input.member ? { member: input.member } : {},
|
|
1042
|
+
...input.composite ? { composite: { [input.composite.ref]: input.composite.value } } : {},
|
|
1043
|
+
// Assigned HERE, before the send, so the SAME key rides every
|
|
1044
|
+
// transport-level retry below. Without it a single 500 followed by a
|
|
1045
|
+
// success is TWO un-keyed reports, and the server dedups on the key —
|
|
1046
|
+
// so it bills the allowance down twice, up to `retry.maxAttempts`
|
|
1047
|
+
// times. The route gained idempotency precisely to stop that; not
|
|
1048
|
+
// using it by default would leave the double-charge reachable from the
|
|
1049
|
+
// client instead of the server.
|
|
1050
|
+
idempotencyKey: input.idempotencyKey ?? newKey()
|
|
963
1051
|
},
|
|
964
1052
|
// Best-effort metering opts into retries on transient failures.
|
|
965
1053
|
retry: true
|
|
966
1054
|
});
|
|
1055
|
+
return {
|
|
1056
|
+
queued: res.queued === true,
|
|
1057
|
+
unitUsage: res.unitUsage ?? null,
|
|
1058
|
+
...handles(res)
|
|
1059
|
+
};
|
|
967
1060
|
}
|
|
968
|
-
/**
|
|
1061
|
+
/**
|
|
1062
|
+
* Close ONE occurrence of a composite — a distinct tag value.
|
|
1063
|
+
* `POST /api/v1/signal/composite/complete`.
|
|
1064
|
+
*
|
|
1065
|
+
* A composite bundles several credits / outcomes / units and is billed as one
|
|
1066
|
+
* thing. The occurrence opens on the first tagged signal and costs nothing;
|
|
1067
|
+
* this call is the moment it is billed. Only your code knows when the
|
|
1068
|
+
* workflow ended, so this is a DECLARATION, not a hint: tagged signals may
|
|
1069
|
+
* arrive in any order and the occurrence stays open and unbilled until you
|
|
1070
|
+
* say otherwise. Put it on the line before your own `return`.
|
|
1071
|
+
*
|
|
1072
|
+
* An OUTCOME never needs this — its own `complete: true` closes its run and
|
|
1073
|
+
* the matching occurrence together. Credits and units have no "done" signal
|
|
1074
|
+
* of their own, so this is theirs.
|
|
1075
|
+
*
|
|
1076
|
+
* Closing twice is safe: the second call answers `ALREADY_COMPLETED` and
|
|
1077
|
+
* cannot bill again. Calling it EARLY is also safe — before every entitlement
|
|
1078
|
+
* the composite is restricted to has reported, or while an ADVANCE pool is
|
|
1079
|
+
* full — the server parks the ping and applies it when the blocker clears,
|
|
1080
|
+
* rather than failing it.
|
|
1081
|
+
*/
|
|
1082
|
+
async completeComposite(input) {
|
|
1083
|
+
const res = await this.transport.request({
|
|
1084
|
+
method: "POST",
|
|
1085
|
+
path: COMPOSITE_COMPLETE_PATH,
|
|
1086
|
+
body: {
|
|
1087
|
+
customerId: input.customerId,
|
|
1088
|
+
composite: { [input.composite.ref]: input.composite.value },
|
|
1089
|
+
// Auto-assigned like the others. A retry here could not double-BILL
|
|
1090
|
+
// even without it — closing a closed occurrence is a server-side
|
|
1091
|
+
// no-op — but that safety comes from occurrence-level dedup, not from
|
|
1092
|
+
// idempotency, and an un-keyed replay still writes a redundant inbox
|
|
1093
|
+
// row. Keying it collapses the retry into the one attempt it is.
|
|
1094
|
+
idempotencyKey: input.idempotencyKey ?? newKey()
|
|
1095
|
+
},
|
|
1096
|
+
retry: true
|
|
1097
|
+
});
|
|
1098
|
+
return {
|
|
1099
|
+
queued: res.queued === true,
|
|
1100
|
+
...res.status != null ? { status: res.status } : {},
|
|
1101
|
+
...res.composite !== void 0 ? { composite: res.composite } : {},
|
|
1102
|
+
...res.value !== void 0 ? { value: res.value } : {},
|
|
1103
|
+
...handles(res)
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* A customer's unit balances + recent unit events. `GET /api/v1/units`.
|
|
1108
|
+
*
|
|
1109
|
+
* Legacy path for the same reason as {@link list}: the signal family is
|
|
1110
|
+
* POST-only, so the read side has no successor and is not deprecated.
|
|
1111
|
+
*/
|
|
969
1112
|
unitUsage(params) {
|
|
970
1113
|
return this.transport.request({
|
|
971
1114
|
method: "GET",
|
|
@@ -1109,6 +1252,8 @@ var ClockNext = class {
|
|
|
1109
1252
|
payments;
|
|
1110
1253
|
/** Manage the credit-type catalog. */
|
|
1111
1254
|
credits;
|
|
1255
|
+
/** Manage the composite catalog — bundles billed as one thing. */
|
|
1256
|
+
composites;
|
|
1112
1257
|
/** Manage the outcome-type catalog. */
|
|
1113
1258
|
outcomes;
|
|
1114
1259
|
/** Manage the unit-type catalog. */
|
|
@@ -1137,6 +1282,7 @@ var ClockNext = class {
|
|
|
1137
1282
|
this.invoices = new Invoices(transport);
|
|
1138
1283
|
this.payments = new Payments(transport);
|
|
1139
1284
|
this.credits = new Credits(transport);
|
|
1285
|
+
this.composites = new Composites(transport);
|
|
1140
1286
|
this.outcomes = new Outcomes(transport);
|
|
1141
1287
|
this.units = new Units(transport);
|
|
1142
1288
|
this.webhooks = new Webhooks(transport);
|