@voxgig/sdkgen 4.2.6 → 4.2.8

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 (64) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/helpers/naming.js +5 -4
  3. package/dist/helpers/naming.js.map +1 -1
  4. package/dist/tsconfig.tsbuildinfo +1 -1
  5. package/package.json +1 -1
  6. package/project/.sdk/model/feature/cost.aon +43 -0
  7. package/project/.sdk/model/feature/feature-index.aon +1 -0
  8. package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +2 -1
  9. package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +2 -1
  10. package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +2 -1
  11. package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +2 -1
  12. package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +2 -1
  13. package/project/.sdk/src/cmp/py/ReadmeExamplesTest_py.ts +15 -5
  14. package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +2 -1
  15. package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +2 -1
  16. package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +2 -1
  17. package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +2 -1
  18. package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +2 -1
  19. package/project/.sdk/tm/c/core/sdk.h +1 -0
  20. package/project/.sdk/tm/c/feature/cost.c +549 -0
  21. package/project/.sdk/tm/c/src/feature/cost/.gitkeep +0 -0
  22. package/project/.sdk/tm/clojure/src/feature/cost/.gitkeep +0 -0
  23. package/project/.sdk/tm/cpp/feature/cost.hpp +419 -0
  24. package/project/.sdk/tm/cpp/src/feature/cost/.gitkeep +0 -0
  25. package/project/.sdk/tm/csharp/feature/CostFeature.cs +518 -0
  26. package/project/.sdk/tm/csharp/src/feature/cost/.gitkeep +0 -0
  27. package/project/.sdk/tm/dart/lib/feature/cost/CostFeature.dart +446 -0
  28. package/project/.sdk/tm/dart/src/feature/cost/.gitkeep +0 -0
  29. package/project/.sdk/tm/elixir/lib/projectname/feature/cost.ex +442 -0
  30. package/project/.sdk/tm/elixir/src/feature/cost/.gitkeep +0 -0
  31. package/project/.sdk/tm/go/feature/cost_feature.go +424 -0
  32. package/project/.sdk/tm/go/src/feature/cost/.gitkeep +0 -0
  33. package/project/.sdk/tm/go-cli/src/feature/cost/.gitkeep +0 -0
  34. package/project/.sdk/tm/go-mcp/src/feature/cost/.gitkeep +0 -0
  35. package/project/.sdk/tm/java/feature/CostFeature.java +431 -0
  36. package/project/.sdk/tm/java/src/feature/cost/.gitkeep +0 -0
  37. package/project/.sdk/tm/js/src/feature/cost/CostFeature.js +413 -0
  38. package/project/.sdk/tm/kotlin/feature/CostFeature.kt +430 -0
  39. package/project/.sdk/tm/kotlin/src/feature/cost/.gitkeep +0 -0
  40. package/project/.sdk/tm/lean/src/feature/cost/.gitkeep +0 -0
  41. package/project/.sdk/tm/lua/feature/cost_feature.lua +457 -0
  42. package/project/.sdk/tm/lua/src/feature/cost/.gitkeep +0 -0
  43. package/project/.sdk/tm/ocaml/src/feature/cost/.gitkeep +0 -0
  44. package/project/.sdk/tm/perl/feature/cost_feature.pm +446 -0
  45. package/project/.sdk/tm/perl/src/feature/cost/.gitkeep +0 -0
  46. package/project/.sdk/tm/php/feature/CostFeature.php +439 -0
  47. package/project/.sdk/tm/php/feature/TestFeature.php +31 -1
  48. package/project/.sdk/tm/php/src/feature/cost/.gitkeep +0 -0
  49. package/project/.sdk/tm/py/pkg/feature/cost_feature.py +381 -0
  50. package/project/.sdk/tm/py/src/feature/cost/.gitkeep +0 -0
  51. package/project/.sdk/tm/py-data/src/feature/cost/.gitkeep +0 -0
  52. package/project/.sdk/tm/rb/feature/cost_feature.rb +386 -0
  53. package/project/.sdk/tm/rb/src/feature/cost/.gitkeep +0 -0
  54. package/project/.sdk/tm/rust/feature/cost.rs +467 -0
  55. package/project/.sdk/tm/rust/src/feature/cost/.gitkeep +0 -0
  56. package/project/.sdk/tm/scala/feature/CostFeature.scala +388 -0
  57. package/project/.sdk/tm/scala/src/feature/cost/.gitkeep +0 -0
  58. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/CostFeature.swift +425 -0
  59. package/project/.sdk/tm/swift/src/feature/cost/.gitkeep +0 -0
  60. package/project/.sdk/tm/ts/src/feature/cost/CostFeature.ts +415 -0
  61. package/project/.sdk/tm/zig/feature/cost.zig +457 -0
  62. package/project/.sdk/tm/zig/src/feature/cost/.gitkeep +0 -0
  63. package/project/sdkgen-package.json +2 -1
  64. package/src/helpers/naming.ts +5 -4
@@ -0,0 +1,457 @@
1
+ // Cost tracking and spend budget (mirrors go feature/cost_feature.go / rust
2
+ // feature/cost.rs). Uses BOTH seams, which is the point of the feature: money
3
+ // is spent per HTTP ATTEMPT (a retried call is charged again, because the
4
+ // upstream API charges it again), but it is owed by an OPERATION. So the
5
+ // transport wrap prices each attempt, and PreDone attributes the running
6
+ // total to `<entity>.<op>` and to the caller (`ctrl.actor`, the same actor
7
+ // the audit feature records).
8
+ //
9
+ // The price of an attempt comes from the first source that answers: a
10
+ // response header (`header` x `perUnit`), the rate table (`rates`, keyed
11
+ // '<entity>.<op>' / '<op>' / '*'), then the flat `unit`. A body figure
12
+ // (`path` x `perUnit`, e.g. "usage.total_tokens") is read at PreDone instead,
13
+ // from the already-parsed result. A body figure describes the whole call, so
14
+ // it REPLACES the per-attempt estimate rather than adding to it.
15
+ //
16
+ // `budget` caps total spend. With `onBudget` = "deny" a further operation is
17
+ // refused at PrePoint, before an endpoint is resolved and before anything
18
+ // reaches the network.
19
+ //
20
+ // ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
21
+ // cache is charged for money that was never spent.
22
+ //
23
+ // The per-operation accumulator lives on ctx.out as a Value map (like rust),
24
+ // not as a native pointer: OutVal carries Values, and the transport wrapper
25
+ // and the hooks are different objects that both have to reach it.
26
+
27
+ const std = @import("std");
28
+ const h = @import("../core/helpers.zig");
29
+ const err = @import("../core/error.zig");
30
+ const types = @import("../core/types.zig");
31
+ const sup = @import("support.zig");
32
+
33
+ const Value = h.Value;
34
+ const Context = types.Context;
35
+ const Feature = types.Feature;
36
+ const Fetcher = types.Fetcher;
37
+ const OutVal = types.OutVal;
38
+
39
+ const COST_PENDING_KEY: []const u8 = "cost_pending";
40
+
41
+ pub const CostBucket = struct {
42
+ calls: i64 = 0,
43
+ amount: f64 = 0,
44
+ };
45
+
46
+ pub const CostTotal = struct {
47
+ calls: i64 = 0,
48
+ attempts: i64 = 0,
49
+ amount: f64 = 0,
50
+ reported: f64 = 0,
51
+ estimated: f64 = 0,
52
+ };
53
+
54
+ pub const CostBudget = struct {
55
+ limit: f64 = 0,
56
+ spent: f64 = 0,
57
+ remaining: f64 = 0,
58
+ exceeded: bool = false,
59
+ };
60
+
61
+ // Shared between the feature (hooks) and the transport wrapper, which are
62
+ // separate objects reached through different pointers.
63
+ pub const CostTrack = struct {
64
+ currency: []const u8 = "USD",
65
+ total: CostTotal = .{},
66
+ ops: std.StringHashMap(CostBucket),
67
+ actors: std.StringHashMap(CostBucket),
68
+ budget: CostBudget = .{},
69
+ last: Value,
70
+ seq: i64 = 0,
71
+ };
72
+
73
+ pub const CostFeature = struct {
74
+ name: []const u8 = "cost",
75
+ active: bool = true,
76
+ add_opts: Value = .{ .null = {} },
77
+ options: Value = .{ .null = {} },
78
+ track: *CostTrack,
79
+
80
+ pub fn make() Feature {
81
+ const self = h.A().create(CostFeature) catch unreachable;
82
+ const track = h.A().create(CostTrack) catch unreachable;
83
+ track.* = .{
84
+ .ops = std.StringHashMap(CostBucket).init(h.A()),
85
+ .actors = std.StringHashMap(CostBucket).init(h.A()),
86
+ .last = h.vnull(),
87
+ };
88
+ self.* = .{ .track = track };
89
+ return .{ .ptr = @ptrCast(self), .vtable = &vtable };
90
+ }
91
+
92
+ fn self_of(p: *anyopaque) *CostFeature {
93
+ return @ptrCast(@alignCast(p));
94
+ }
95
+
96
+ fn vname(p: *anyopaque) []const u8 {
97
+ return self_of(p).name;
98
+ }
99
+ fn vactive(p: *anyopaque) bool {
100
+ return self_of(p).active;
101
+ }
102
+ fn vaddopts(p: *anyopaque) Value {
103
+ return self_of(p).add_opts;
104
+ }
105
+ fn vinit(p: *anyopaque, ctx: *Context, options: Value) void {
106
+ const self = self_of(p);
107
+ self.options = options;
108
+ self.active = sup.fopt_bool(options, "active", false);
109
+
110
+ const lim = sup.fopt_num(options, "budget", 0);
111
+ self.track.* = .{
112
+ .currency = sup.fopt_str(options, "currency", "USD"),
113
+ .ops = std.StringHashMap(CostBucket).init(h.A()),
114
+ .actors = std.StringHashMap(CostBucket).init(h.A()),
115
+ .budget = .{ .limit = lim, .remaining = lim },
116
+ .last = h.vnull(),
117
+ };
118
+
119
+ if (!self.active) return;
120
+
121
+ const util = ctx.util();
122
+ const w = h.A().create(WrapCtx) catch unreachable;
123
+ w.* = .{ .inner = util.fetcher, .options = options, .track = self.track };
124
+ util.fetcher = .{ .ctx = @ptrCast(w), .call = wrapCall };
125
+ }
126
+ fn vdispatch(p: *anyopaque, name: []const u8, ctx: *Context) void {
127
+ const self = self_of(p);
128
+ if (std.mem.eql(u8, name, "PrePoint")) {
129
+ self.pre_point(ctx);
130
+ } else if (std.mem.eql(u8, name, "PreDone")) {
131
+ self.finish(ctx, true);
132
+ } else if (std.mem.eql(u8, name, "PreUnexpected")) {
133
+ self.finish(ctx, false);
134
+ }
135
+ }
136
+
137
+ // Budget gate. Runs before endpoint resolution, so a refused call costs
138
+ // nothing at all.
139
+ fn pre_point(self: *CostFeature, ctx: *Context) void {
140
+ if (!self.active) return;
141
+
142
+ // Mark the context as running through the pipeline, so charge knows a
143
+ // PreDone is coming and does not commit the spend itself.
144
+ const pending = pending_for(ctx);
145
+ h.setp(pending, "piped", h.vbool(true));
146
+
147
+ const lim = self.track.budget.limit;
148
+ if (lim <= 0) return;
149
+ if (self.track.total.amount < lim) return;
150
+
151
+ self.track.budget.exceeded = true;
152
+
153
+ if (!std.mem.eql(u8, sup.fopt_str(self.options, "onBudget", "warn"), "deny")) return;
154
+
155
+ const msg = std.fmt.allocPrint(
156
+ h.A(),
157
+ "Cost budget of {d} {s} is spent ({d} {s} used)",
158
+ .{ lim, self.track.currency, self.track.total.amount, self.track.currency },
159
+ ) catch "cost budget spent";
160
+ const e = ctx.make_error("cost_budget", msg);
161
+ // Short-circuit endpoint resolution; make_point surfaces this error.
162
+ ctx.out_set("point", OutVal{ .err = e });
163
+ }
164
+
165
+ fn finish(self: *CostFeature, ctx: *Context, done: bool) void {
166
+ if (!self.active) return;
167
+
168
+ const taken = ctx.out.fetchRemove(COST_PENDING_KEY) orelse return;
169
+ var pending: Value = h.vnull();
170
+ switch (taken.value) {
171
+ .val => |v| if (v == .object) {
172
+ pending = v;
173
+ },
174
+ else => {},
175
+ }
176
+ if (pending != .object) {
177
+ // Put back anything unexpected (should not happen).
178
+ ctx.out_set(COST_PENDING_KEY, taken.value);
179
+ return;
180
+ }
181
+
182
+ // A FAILED operation that made no attempt never reached the network:
183
+ // PrePoint creates the pending entry to mark the context as piped, and
184
+ // then the budget gate refuses the call (rbac, or an unresolvable
185
+ // endpoint, short-circuits just as early). Committing it would count a
186
+ // call that never happened and file a zero-amount record as `last`.
187
+ //
188
+ // A SUCCEEDED operation that made no attempt is the opposite case: it
189
+ // was served from the cache. That is a real call, and the fact that it
190
+ // cost nothing is the whole point of ordering cost inside the cache.
191
+ const tried = h.get_f64(pending, "attempts") orelse 0;
192
+ if (!done and tried == 0) return;
193
+
194
+ const entity0 = ctx.op.entity;
195
+ const opname0 = ctx.op.name;
196
+ const entity = if (entity0.len == 0) "_" else entity0;
197
+ const opname = if (opname0.len == 0) "_" else opname0;
198
+
199
+ commit(self.track, self.options, ctx, pending, entity, opname);
200
+ }
201
+
202
+ const vtable = Feature.VTable{
203
+ .name = vname,
204
+ .active = vactive,
205
+ .add_options = vaddopts,
206
+ .init = vinit,
207
+ .dispatch = vdispatch,
208
+ };
209
+ };
210
+
211
+ const WrapCtx = struct {
212
+ inner: Fetcher,
213
+ options: Value,
214
+ track: *CostTrack,
215
+ };
216
+
217
+ fn wrapCall(p: *anyopaque, ctx: *Context, url: []const u8, fetchdef: Value) err.E!Value {
218
+ const w: *WrapCtx = @ptrCast(@alignCast(p));
219
+ return charge(w.track, w.options, ctx, url, fetchdef, w.inner);
220
+ }
221
+
222
+ fn new_pending() Value {
223
+ const m = h.omap();
224
+ h.setp(m, "attempts", h.vfloat(0));
225
+ h.setp(m, "amount", h.vfloat(0));
226
+ h.setp(m, "reported", h.vfloat(0));
227
+ h.setp(m, "estimated", h.vfloat(0));
228
+ h.setp(m, "source", h.vstr("none"));
229
+ h.setp(m, "piped", h.vbool(false));
230
+ return m;
231
+ }
232
+
233
+ fn pending_for(ctx: *Context) Value {
234
+ if (ctx.out_get(COST_PENDING_KEY)) |ov| {
235
+ switch (ov) {
236
+ .val => |v| if (v == .object) return v,
237
+ else => {},
238
+ }
239
+ }
240
+ const m = new_pending();
241
+ ctx.out_set(COST_PENDING_KEY, OutVal{ .val = m });
242
+ return m;
243
+ }
244
+
245
+ fn charge(track: *CostTrack, options: Value, ctx: *Context, url: []const u8, fetchdef: Value, inner: Fetcher) err.E!Value {
246
+ // A failing transport still costs an attempt. Without this, a run of
247
+ // connection-level failures under `retry` (which retries on an error)
248
+ // would be charged nothing at all, and an onBudget = "deny" ceiling could
249
+ // never stop it.
250
+ var res: Value = h.vnull();
251
+ var failed = false;
252
+ var fail_err: err.E = error.Sdk;
253
+ if (inner.invoke(ctx, url, fetchdef)) |ok| {
254
+ res = ok;
255
+ } else |e| {
256
+ failed = true;
257
+ fail_err = e;
258
+ }
259
+
260
+ const priced = price(options, ctx, if (failed) h.vnull() else res);
261
+
262
+ const pending = pending_for(ctx);
263
+
264
+ // Accumulated here, committed once at PreDone. Adding each attempt to the
265
+ // running total and then subtracting it again when a body figure
266
+ // supersedes it loses precision to catastrophic cancellation
267
+ // (5 + (0.01 - 5) is not 0.01 in binary floating point).
268
+ //
269
+ // Reported and estimated are kept apart per ATTEMPT, not per operation: a
270
+ // 503 priced from the rate table followed by a 200 carrying the cost
271
+ // header is part estimate, part reported, and collapsing both into the
272
+ // final attempt's category would corrupt the split.
273
+ const attempts = (h.get_f64(pending, "attempts") orelse 0) + 1;
274
+ h.setp(pending, "attempts", h.vfloat(attempts));
275
+ h.setp(pending, "amount", h.vfloat((h.get_f64(pending, "amount") orelse 0) + priced.amount));
276
+
277
+ const bucket: []const u8 = if (std.mem.eql(u8, priced.source, "header") or
278
+ std.mem.eql(u8, priced.source, "body")) "reported" else "estimated";
279
+ h.setp(pending, bucket, h.vfloat((h.get_f64(pending, bucket) orelse 0) + priced.amount));
280
+ h.setp(pending, "source", h.vstr(priced.source));
281
+
282
+ track.total.attempts += 1;
283
+
284
+ // direct() and graphql() reach the transport without dispatching any
285
+ // pipeline hooks - no PrePoint to gate on, and no PreDone to commit.
286
+ // Their spend is committed here instead, or it would never be counted and
287
+ // could run past an onBudget = "deny" ceiling indefinitely. `piped` is set
288
+ // by PrePoint, so its absence is the signal.
289
+ if (!(h.get_bool(pending, "piped") orelse false)) {
290
+ commit(track, options, ctx, pending, "_", "direct");
291
+ _ = ctx.out.fetchRemove(COST_PENDING_KEY);
292
+ }
293
+
294
+ if (failed) return fail_err;
295
+ return res;
296
+ }
297
+
298
+ const Priced = struct {
299
+ amount: f64,
300
+ source: []const u8,
301
+ };
302
+
303
+ // Price one attempt: a reported header figure, else the rate table, else the
304
+ // flat unit.
305
+ fn price(options: Value, ctx: *Context, res: Value) Priced {
306
+ const header = sup.fopt_str(options, "header", "");
307
+ if (header.len != 0) {
308
+ if (header_num(res, header)) |v| {
309
+ return .{ .amount = v * per_unit(options), .source = "header" };
310
+ }
311
+ }
312
+
313
+ if (rate(options, ctx)) |r| {
314
+ return .{ .amount = r, .source = "table" };
315
+ }
316
+
317
+ const unit = sup.fopt_num(options, "unit", 0);
318
+ if (unit != 0) {
319
+ return .{ .amount = unit, .source = "unit" };
320
+ }
321
+
322
+ return .{ .amount = 0, .source = "none" };
323
+ }
324
+
325
+ // The rate table uses the same lookup grammar as rbac's rules:
326
+ // '<entity>.<op>', then '<op>', then '*'.
327
+ fn rate(options: Value, ctx: *Context) ?f64 {
328
+ const rates = sup.fopt_map(options, "rates");
329
+ if (rates != .object) return null;
330
+
331
+ const entity: []const u8 = if (ctx.entity) |e| e.get_name() else ctx.op.entity;
332
+ const opname = ctx.op.name;
333
+
334
+ const k0 = std.fmt.allocPrint(h.A(), "{s}.{s}", .{ entity, opname }) catch opname;
335
+ const keys = [_][]const u8{ k0, opname, "*" };
336
+ for (keys) |key| {
337
+ if (h.get_f64(rates, key)) |n| return n;
338
+ }
339
+ return null;
340
+ }
341
+
342
+ // A usage figure from the parsed result body, priced by perUnit. Read here,
343
+ // not at the transport seam, because the body is consumed once.
344
+ fn body_amount(options: Value, ctx: *Context) ?f64 {
345
+ const path = sup.fopt_str(options, "path", "");
346
+ if (path.len == 0) return null;
347
+
348
+ const result = ctx.result orelse return null;
349
+ if (result.body != .object) return null;
350
+
351
+ // Size the segment array from the separator count: a fixed array would
352
+ // silently stop splitting and look up a truncated path, which reads as
353
+ // "no usage figure" rather than as the bug it is. splitScalar yields
354
+ // slices into `path`, so there is nothing to copy.
355
+ var nseg: usize = 1;
356
+ for (path) |c| {
357
+ if (c == '.') nseg += 1;
358
+ }
359
+ const segs = h.A().alloc([]const u8, nseg) catch return null;
360
+
361
+ var n: usize = 0;
362
+ var it = std.mem.splitScalar(u8, path, '.');
363
+ while (it.next()) |seg| {
364
+ segs[n] = seg;
365
+ n += 1;
366
+ }
367
+
368
+ const v = h.getpath(segs[0..n], result.body);
369
+ return switch (v) {
370
+ .integer => |i| @as(f64, @floatFromInt(i)) * per_unit(options),
371
+ .float => |f| f * per_unit(options),
372
+ else => null,
373
+ };
374
+ }
375
+
376
+ // Commit one operation's spend: totals, budget, per-op and per-actor
377
+ // attribution, and the record. Shared by finish and the raw-request path in
378
+ // charge, which has no PreDone to reach.
379
+ fn commit(track: *CostTrack, options: Value, ctx: *Context, pending: Value, entity: []const u8, opname: []const u8) void {
380
+ var amount = h.get_f64(pending, "amount") orelse 0;
381
+ var reported = h.get_f64(pending, "reported") orelse 0;
382
+ var estimated = h.get_f64(pending, "estimated") orelse 0;
383
+ var source = h.get_str(pending, "source") orelse "none";
384
+
385
+ // A body figure prices the whole call, so it replaces the per-attempt
386
+ // estimate rather than adding to it - and, being server-stated, the whole
387
+ // amount counts as reported.
388
+ if (body_amount(options, ctx)) |b| {
389
+ amount = b;
390
+ reported = b;
391
+ estimated = 0;
392
+ source = "body";
393
+ }
394
+
395
+ spend(track, amount, reported, estimated);
396
+
397
+ var actor: []const u8 = "anonymous";
398
+ const opt_actor = sup.fopt_str(options, "actor", "");
399
+ if (opt_actor.len != 0) actor = opt_actor;
400
+ if (ctx.ctrl.actor.len != 0) actor = ctx.ctrl.actor;
401
+
402
+ track.total.calls += 1;
403
+
404
+ const opkey = std.fmt.allocPrint(h.A(), "{s}.{s}", .{ entity, opname }) catch opname;
405
+ bump(&track.ops, opkey, amount);
406
+ bump(&track.actors, actor, amount);
407
+
408
+ track.seq += 1;
409
+ const record = h.omap();
410
+ h.setp(record, "seq", h.vnum(track.seq));
411
+ h.setp(record, "entity", h.vstr(entity));
412
+ h.setp(record, "op", h.vstr(opname));
413
+ h.setp(record, "actor", h.vstr(actor));
414
+ h.setp(record, "amount", h.vfloat(amount));
415
+ h.setp(record, "currency", h.vstr(track.currency));
416
+ h.setp(record, "source", h.vstr(source));
417
+ h.setp(record, "attempts", h.vfloat(h.get_f64(pending, "attempts") orelse 0));
418
+ track.last = record;
419
+
420
+ const sink = h.getp(options, "sink");
421
+ if (sink == .function) {
422
+ _ = h.call_vfn(sink, record);
423
+ }
424
+ }
425
+
426
+ fn spend(track: *CostTrack, amount: f64, reported: f64, estimated: f64) void {
427
+ track.total.amount += amount;
428
+ track.total.reported += reported;
429
+ track.total.estimated += estimated;
430
+
431
+ track.budget.spent = track.total.amount;
432
+ if (track.budget.limit > 0) {
433
+ track.budget.remaining = @max(@as(f64, 0), track.budget.limit - track.total.amount);
434
+ if (track.total.amount >= track.budget.limit) track.budget.exceeded = true;
435
+ } else {
436
+ track.budget.remaining = 0;
437
+ }
438
+ }
439
+
440
+ fn bump(bucket: *std.StringHashMap(CostBucket), key: []const u8, amount: f64) void {
441
+ const gop = bucket.getOrPut(key) catch return;
442
+ if (!gop.found_existing) gop.value_ptr.* = .{};
443
+ gop.value_ptr.calls += 1;
444
+ gop.value_ptr.amount += amount;
445
+ }
446
+
447
+ // HTTP header names are case-insensitive and a custom transport keeps
448
+ // conventional casing ("X-Request-Cost"), so fres_header scans rather than
449
+ // indexes.
450
+ fn header_num(res: Value, name: []const u8) ?f64 {
451
+ const s = sup.fres_header(res, name) orelse return null;
452
+ return std.fmt.parseFloat(f64, std.mem.trim(u8, s, " \t")) catch null;
453
+ }
454
+
455
+ fn per_unit(options: Value) f64 {
456
+ return sup.fopt_num(options, "perUnit", 0);
457
+ }
File without changes
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.2.6",
6
+ "version": "4.2.8",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",
@@ -37,6 +37,7 @@
37
37
  "audit",
38
38
  "cache",
39
39
  "clienttrack",
40
+ "cost",
40
41
  "debug",
41
42
  "idempotency",
42
43
  "log",
@@ -220,10 +220,11 @@ const SWIFT_SDK_TYPES = new Set<string>([
220
220
  'ManualRedirectDelegate',
221
221
  // features
222
222
  'AuditFeature', 'BaseFeature', 'CacheFeature', 'ClienttrackFeature',
223
- 'DebugFeature', 'IdempotencyFeature', 'LogFeature', 'MetricsBucket',
224
- 'MetricsFeature', 'NetsimFeature', 'PagingFeature', 'ProxyFeature',
225
- 'RatelimitFeature', 'RbacFeature', 'RetryFeature', 'StreamingFeature',
226
- 'TelemetryFeature', 'TestFeature', 'TimeoutFeature',
223
+ 'CostBucket', 'CostBudget', 'CostFeature', 'CostPending', 'CostRecord',
224
+ 'CostTotal', 'DebugFeature', 'IdempotencyFeature', 'LogFeature',
225
+ 'MetricsBucket', 'MetricsFeature', 'NetsimFeature', 'PagingFeature',
226
+ 'ProxyFeature', 'RatelimitFeature', 'RbacFeature', 'RetryFeature',
227
+ 'StreamingFeature', 'TelemetryFeature', 'TestFeature', 'TimeoutFeature',
227
228
  ])
228
229
 
229
230