@voxgig/sdkgen 4.2.5 → 4.2.7

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 (71) 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/go/utility_go.ts +20 -7
  9. package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +2 -1
  10. package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +2 -1
  11. package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +2 -1
  12. package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +2 -1
  13. package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +2 -1
  14. package/project/.sdk/src/cmp/js/utility_js.ts +20 -7
  15. package/project/.sdk/src/cmp/php/ReadmeExamplesTest_php.ts +144 -12
  16. package/project/.sdk/src/cmp/py/ReadmeExamplesTest_py.ts +98 -3
  17. package/project/.sdk/src/cmp/rb/ReadmeExamplesTest_rb.ts +83 -10
  18. package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +2 -1
  19. package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +2 -1
  20. package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +2 -1
  21. package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +2 -1
  22. package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +2 -1
  23. package/project/.sdk/src/cmp/ts/utility_ts.ts +20 -7
  24. package/project/.sdk/tm/c/core/sdk.h +1 -0
  25. package/project/.sdk/tm/c/feature/cost.c +549 -0
  26. package/project/.sdk/tm/c/src/feature/cost/.gitkeep +0 -0
  27. package/project/.sdk/tm/clojure/src/feature/cost/.gitkeep +0 -0
  28. package/project/.sdk/tm/cpp/feature/cost.hpp +419 -0
  29. package/project/.sdk/tm/cpp/src/feature/cost/.gitkeep +0 -0
  30. package/project/.sdk/tm/csharp/feature/CostFeature.cs +518 -0
  31. package/project/.sdk/tm/csharp/src/feature/cost/.gitkeep +0 -0
  32. package/project/.sdk/tm/dart/lib/feature/cost/CostFeature.dart +446 -0
  33. package/project/.sdk/tm/dart/src/feature/cost/.gitkeep +0 -0
  34. package/project/.sdk/tm/elixir/lib/projectname/feature/cost.ex +442 -0
  35. package/project/.sdk/tm/elixir/src/feature/cost/.gitkeep +0 -0
  36. package/project/.sdk/tm/go/feature/cost_feature.go +424 -0
  37. package/project/.sdk/tm/go/src/feature/cost/.gitkeep +0 -0
  38. package/project/.sdk/tm/go-cli/src/feature/cost/.gitkeep +0 -0
  39. package/project/.sdk/tm/go-mcp/src/feature/cost/.gitkeep +0 -0
  40. package/project/.sdk/tm/java/feature/CostFeature.java +431 -0
  41. package/project/.sdk/tm/java/src/feature/cost/.gitkeep +0 -0
  42. package/project/.sdk/tm/js/src/feature/cost/CostFeature.js +413 -0
  43. package/project/.sdk/tm/js/test/feature/Corpus.test.js +264 -0
  44. package/project/.sdk/tm/kotlin/feature/CostFeature.kt +430 -0
  45. package/project/.sdk/tm/kotlin/src/feature/cost/.gitkeep +0 -0
  46. package/project/.sdk/tm/lean/src/feature/cost/.gitkeep +0 -0
  47. package/project/.sdk/tm/lua/feature/cost_feature.lua +457 -0
  48. package/project/.sdk/tm/lua/src/feature/cost/.gitkeep +0 -0
  49. package/project/.sdk/tm/ocaml/src/feature/cost/.gitkeep +0 -0
  50. package/project/.sdk/tm/perl/feature/cost_feature.pm +446 -0
  51. package/project/.sdk/tm/perl/src/feature/cost/.gitkeep +0 -0
  52. package/project/.sdk/tm/php/feature/CostFeature.php +439 -0
  53. package/project/.sdk/tm/php/feature/TestFeature.php +31 -1
  54. package/project/.sdk/tm/php/src/feature/cost/.gitkeep +0 -0
  55. package/project/.sdk/tm/py/pkg/feature/cost_feature.py +381 -0
  56. package/project/.sdk/tm/py/src/feature/cost/.gitkeep +0 -0
  57. package/project/.sdk/tm/py-data/src/feature/cost/.gitkeep +0 -0
  58. package/project/.sdk/tm/rb/feature/cost_feature.rb +386 -0
  59. package/project/.sdk/tm/rb/src/feature/cost/.gitkeep +0 -0
  60. package/project/.sdk/tm/rust/feature/cost.rs +467 -0
  61. package/project/.sdk/tm/rust/src/feature/cost/.gitkeep +0 -0
  62. package/project/.sdk/tm/scala/feature/CostFeature.scala +388 -0
  63. package/project/.sdk/tm/scala/src/feature/cost/.gitkeep +0 -0
  64. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/CostFeature.swift +425 -0
  65. package/project/.sdk/tm/swift/src/feature/cost/.gitkeep +0 -0
  66. package/project/.sdk/tm/ts/src/feature/cost/CostFeature.ts +415 -0
  67. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +266 -0
  68. package/project/.sdk/tm/zig/feature/cost.zig +457 -0
  69. package/project/.sdk/tm/zig/src/feature/cost/.gitkeep +0 -0
  70. package/project/sdkgen-package.json +2 -1
  71. package/src/helpers/naming.ts +5 -4
@@ -0,0 +1,518 @@
1
+ // Cost tracking and spend budget. Uses BOTH seams, which is the point of
2
+ // the feature: money is spent per HTTP ATTEMPT (a retried call is charged
3
+ // again, because the upstream API charges it again), but it is owed by an
4
+ // OPERATION. So the transport wrap prices each attempt, and PreDone
5
+ // attributes the running total to `<entity>.<op>` and to the caller
6
+ // (`ctrl.actor`, the same actor the audit feature records).
7
+ //
8
+ // The price of an attempt comes from the first source that answers: a
9
+ // response header (`header` x `perUnit`), the rate table (`rates`, keyed
10
+ // '<entity>.<op>' / '<op>' / '*'), then the flat `unit`. A body figure
11
+ // (`path` x `perUnit`, e.g. "usage.total_tokens") is read at PreDone
12
+ // instead, from the already-parsed result. A body figure describes the
13
+ // whole call, so it REPLACES the per-attempt estimate rather than adding
14
+ // to it.
15
+ //
16
+ // `budget` caps total spend. With `onBudget` = "deny" a further operation
17
+ // is refused at PrePoint, before an endpoint is resolved and before
18
+ // anything 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
+ using Voxgig.Struct;
24
+
25
+ using static ProjectNameSdk.Feature.FeatureOptions;
26
+
27
+ namespace ProjectNameSdk.Feature;
28
+
29
+ public class CostBucket
30
+ {
31
+ public int Calls;
32
+ public double Amount;
33
+ }
34
+
35
+ public class CostRecord
36
+ {
37
+ public int Seq;
38
+ public string Entity = "";
39
+ public string Op = "";
40
+ public string Actor = "";
41
+ public double Amount;
42
+ public string Currency = "";
43
+ public string Source = "";
44
+ public int Attempts;
45
+ }
46
+
47
+ // Per-operation accumulator. Held in ctx.Out for the life of one call, the
48
+ // same place metrics keeps its start marker.
49
+ public class CostPending
50
+ {
51
+ public int Attempts;
52
+ public double Amount;
53
+ public double Reported;
54
+ public double Estimated;
55
+ public string Source = "none";
56
+
57
+ // Set by PrePoint. Its absence means the call never entered the
58
+ // pipeline (direct/graphql), so Charge commits the spend itself.
59
+ public bool Piped;
60
+ }
61
+
62
+ public class CostTotal
63
+ {
64
+ public int Calls;
65
+ public int Attempts;
66
+ public double Amount;
67
+ public double Reported;
68
+ public double Estimated;
69
+ }
70
+
71
+ public class CostBudget
72
+ {
73
+ public double Limit;
74
+ public double Spent;
75
+ public double Remaining;
76
+ public bool Exceeded;
77
+ }
78
+
79
+ public class CostFeature : BaseFeature
80
+ {
81
+ private ProjectNameSDK? _client;
82
+ private Dictionary<string, object?>? _options;
83
+ private int _seq;
84
+
85
+ // Aggregates (mirrors the ts client._cost record).
86
+ public string Currency = "USD";
87
+ public CostTotal Total = new();
88
+ public Dictionary<string, CostBucket> Ops = new();
89
+ public Dictionary<string, CostBucket> Actors = new();
90
+ public CostBudget Budget = new();
91
+ public CostRecord? Last;
92
+
93
+ private const string PendingKey = "cost_pending";
94
+
95
+ public CostFeature()
96
+ {
97
+ Version = "0.0.1";
98
+ Name = "cost";
99
+ Active = true;
100
+ }
101
+
102
+ public override void Init(Context ctx, Dictionary<string, object?> options)
103
+ {
104
+ _client = ctx.Client;
105
+ _options = options;
106
+ Active = FoptBool(options, "active", false);
107
+ _seq = 0;
108
+
109
+ Currency = FoptStr(options, "currency", "USD");
110
+ Total = new CostTotal();
111
+ Ops = new Dictionary<string, CostBucket>();
112
+ Actors = new Dictionary<string, CostBucket>();
113
+ var limit = Limit();
114
+ Budget = new CostBudget { Limit = limit, Remaining = limit };
115
+ Last = null;
116
+
117
+ if (!Active)
118
+ {
119
+ return;
120
+ }
121
+
122
+ var inner = ctx.Utility!.Fetcher;
123
+
124
+ ctx.Utility.Fetcher = (ctx2, url, fetchdef) => Charge(ctx2, url, fetchdef, inner);
125
+ }
126
+
127
+ // Budget gate. Runs before endpoint resolution, so a refused call costs
128
+ // nothing at all.
129
+ public override void PrePoint(Context ctx)
130
+ {
131
+ if (!Active)
132
+ {
133
+ return;
134
+ }
135
+
136
+ // Mark the context as running through the pipeline, so Charge knows
137
+ // a PreDone is coming and does not commit the spend itself.
138
+ var pending = Pending(ctx);
139
+ pending.Piped = true;
140
+
141
+ var limit = Limit();
142
+ if (limit <= 0)
143
+ {
144
+ return;
145
+ }
146
+
147
+ if (Total.Amount < limit)
148
+ {
149
+ return;
150
+ }
151
+
152
+ Budget.Exceeded = true;
153
+
154
+ if (FoptStr(_options, "onBudget", "warn") != "deny")
155
+ {
156
+ return;
157
+ }
158
+
159
+ var err = ctx.MakeError("cost_budget",
160
+ "Cost budget of " + limit + " " + Currency + " is spent (" +
161
+ Total.Amount + " " + Currency + " used)");
162
+
163
+ // Short-circuit endpoint resolution; MakePoint surfaces this error
164
+ // before any network activity.
165
+ ctx.Out["point"] = err;
166
+ }
167
+
168
+ private object? Charge(Context ctx, string url, Dictionary<string, object?> fetchdef,
169
+ FetcherFunc inner)
170
+ {
171
+ object? res = null;
172
+ Exception? err = null;
173
+
174
+ // A throwing transport still costs an attempt. Without this, a run
175
+ // of connection-level failures under `retry` (which catches the
176
+ // throw and tries again) would be charged nothing at all, and an
177
+ // onBudget = "deny" ceiling could never stop it.
178
+ try
179
+ {
180
+ res = inner(ctx, url, fetchdef);
181
+ }
182
+ catch (Exception ex)
183
+ {
184
+ err = ex;
185
+ }
186
+
187
+ var (amount, source) = Price(ctx, res);
188
+
189
+ var pending = Pending(ctx);
190
+ pending.Attempts++;
191
+
192
+ // Accumulated here, committed once at PreDone. Adding each attempt
193
+ // to the running total and then subtracting it again when a body
194
+ // figure supersedes it loses precision to catastrophic cancellation
195
+ // (5 + (0.01 - 5) is not 0.01 in binary floating point).
196
+ //
197
+ // Reported and estimated are kept apart per ATTEMPT, not per
198
+ // operation: a 503 priced from the rate table followed by a 200
199
+ // carrying the cost header is part estimate, part reported, and
200
+ // collapsing both into the final attempt's category would corrupt
201
+ // the split.
202
+ pending.Amount += amount;
203
+ if (source == "header" || source == "body")
204
+ {
205
+ pending.Reported += amount;
206
+ }
207
+ else
208
+ {
209
+ pending.Estimated += amount;
210
+ }
211
+ pending.Source = source;
212
+
213
+ Total.Attempts++;
214
+
215
+ // Direct() and Graphql() reach the transport without dispatching
216
+ // any pipeline hooks - no PrePoint to gate on, and no PreDone to
217
+ // commit. Their spend is committed here instead, or it would never
218
+ // be counted and could run past an onBudget = "deny" ceiling
219
+ // indefinitely. Piped is set by PrePoint, so its absence is the
220
+ // signal.
221
+ if (!pending.Piped)
222
+ {
223
+ Commit(ctx, pending, "_", "direct");
224
+ ctx.Out.Remove(PendingKey);
225
+ }
226
+
227
+ if (err != null)
228
+ {
229
+ throw err;
230
+ }
231
+
232
+ return res;
233
+ }
234
+
235
+ private CostPending Pending(Context ctx)
236
+ {
237
+ if (ctx.Out.TryGetValue(PendingKey, out var raw) && raw is CostPending p)
238
+ {
239
+ return p;
240
+ }
241
+ var pending = new CostPending();
242
+ ctx.Out[PendingKey] = pending;
243
+ return pending;
244
+ }
245
+
246
+ // Attribute the operation's spend once the call is finished.
247
+ public override void PreDone(Context ctx)
248
+ {
249
+ Finish(ctx, true);
250
+ }
251
+
252
+ // A failed operation still spent the money. When the pipeline errors,
253
+ // PreDone never runs, so without this the attempts are counted and the
254
+ // spend is not, and a budget could never see the cost of a failed call.
255
+ // Whichever hook fires first consumes the pending entry, so it commits
256
+ // exactly once.
257
+ public override void PreUnexpected(Context ctx)
258
+ {
259
+ Finish(ctx, false);
260
+ }
261
+
262
+ private void Finish(Context ctx, bool done)
263
+ {
264
+ if (!Active)
265
+ {
266
+ return;
267
+ }
268
+ if (!ctx.Out.TryGetValue(PendingKey, out var raw) || raw is not CostPending pending)
269
+ {
270
+ return;
271
+ }
272
+ ctx.Out.Remove(PendingKey);
273
+
274
+ // A FAILED operation that made no attempt never reached the network:
275
+ // PrePoint creates the pending entry to mark the context as piped,
276
+ // and then the budget gate refuses the call (rbac, or an
277
+ // unresolvable endpoint, short-circuits just as early). Committing
278
+ // it would count a call that never happened and file a zero-amount
279
+ // record as Last.
280
+ //
281
+ // A SUCCEEDED operation that made no attempt is the opposite case:
282
+ // it was served from the cache. That is a real call, and the fact
283
+ // that it cost nothing is the whole point of ordering cost inside
284
+ // the cache.
285
+ if (!done && pending.Attempts == 0)
286
+ {
287
+ return;
288
+ }
289
+
290
+ var entity = ctx.Op?.Entity ?? "_";
291
+ var opname = ctx.Op?.Name ?? "_";
292
+ if (entity == "")
293
+ {
294
+ entity = "_";
295
+ }
296
+ if (opname == "")
297
+ {
298
+ opname = "_";
299
+ }
300
+
301
+ Commit(ctx, pending, entity, opname);
302
+ }
303
+
304
+ // Commit one operation's spend: totals, budget, per-op and per-actor
305
+ // attribution, and the record. Shared by Finish and the raw-request
306
+ // path in Charge, which has no PreDone to reach.
307
+ private void Commit(Context ctx, CostPending pending, string entity, string opname)
308
+ {
309
+ var amount = pending.Amount;
310
+ var reported = pending.Reported;
311
+ var estimated = pending.Estimated;
312
+ var source = pending.Source;
313
+
314
+ // A body figure prices the whole call, so it replaces the
315
+ // per-attempt estimate rather than adding to it - and, being
316
+ // server-stated, the whole amount counts as reported.
317
+ var (body, hasBody) = Body(ctx);
318
+ if (hasBody)
319
+ {
320
+ amount = body;
321
+ reported = body;
322
+ estimated = 0;
323
+ source = "body";
324
+ }
325
+
326
+ Spend(amount, reported, estimated);
327
+
328
+ var actor = FoptStr(_options, "actor", "");
329
+ if (ctx.Ctrl != null && ctx.Ctrl.Actor != "")
330
+ {
331
+ actor = ctx.Ctrl.Actor;
332
+ }
333
+ if (actor == "")
334
+ {
335
+ actor = "anonymous";
336
+ }
337
+
338
+ Total.Calls++;
339
+ Bump(Ops, entity + "." + opname, amount);
340
+ Bump(Actors, actor, amount);
341
+
342
+ _seq++;
343
+ Last = new CostRecord
344
+ {
345
+ Seq = _seq,
346
+ Entity = entity,
347
+ Op = opname,
348
+ Actor = actor,
349
+ Amount = amount,
350
+ Currency = Currency,
351
+ Source = source,
352
+ Attempts = pending.Attempts,
353
+ };
354
+
355
+ if (Opt(_options, "sink") is Action<CostRecord> sink)
356
+ {
357
+ try
358
+ {
359
+ sink(Last);
360
+ }
361
+ catch (Exception)
362
+ {
363
+ // A sink must never break the call it is reporting on.
364
+ }
365
+ }
366
+ }
367
+
368
+ // Price one attempt: a reported header figure, else the rate table,
369
+ // else the flat unit.
370
+ private (double, string) Price(Context ctx, object? res)
371
+ {
372
+ var header = FoptStr(_options, "header", "");
373
+ if (header != "")
374
+ {
375
+ var (v, has) = HeaderNum(res, header);
376
+ if (has)
377
+ {
378
+ return (v * PerUnit(), "header");
379
+ }
380
+ }
381
+
382
+ var (rate, hasRate) = Rate(ctx);
383
+ if (hasRate)
384
+ {
385
+ return (rate, "table");
386
+ }
387
+
388
+ var unit = FoptNum(_options, "unit", 0);
389
+ if (unit != 0)
390
+ {
391
+ return (unit, "unit");
392
+ }
393
+
394
+ return (0, "none");
395
+ }
396
+
397
+ // The rate table uses the same lookup grammar as rbac's rules:
398
+ // '<entity>.<op>', then '<op>', then '*'.
399
+ private (double, bool) Rate(Context ctx)
400
+ {
401
+ var rates = FoptMap(_options, "rates");
402
+ if (rates == null)
403
+ {
404
+ return (0, false);
405
+ }
406
+
407
+ var entity = ctx.Entity?.GetName() ?? ctx.Op?.Entity ?? "";
408
+ var opname = ctx.Op?.Name ?? "";
409
+
410
+ foreach (var key in new[] { entity + "." + opname, opname, "*" })
411
+ {
412
+ if (rates.TryGetValue(key, out var r))
413
+ {
414
+ var (n, has) = ToNum(r);
415
+ if (has)
416
+ {
417
+ return (n, true);
418
+ }
419
+ }
420
+ }
421
+ return (0, false);
422
+ }
423
+
424
+ // A usage figure from the parsed result body, priced by perUnit. Read
425
+ // here, not at the transport seam, because the body is consumed once.
426
+ private (double, bool) Body(Context ctx)
427
+ {
428
+ var path = FoptStr(_options, "path", "");
429
+ if (path == "" || ctx.Result == null)
430
+ {
431
+ return (0, false);
432
+ }
433
+ if (ctx.Result.Body is not Dictionary<string, object?> body)
434
+ {
435
+ return (0, false);
436
+ }
437
+ var v = StructUtils.GetPath(body, path);
438
+ var (n, has) = ToNum(v);
439
+ if (!has)
440
+ {
441
+ return (0, false);
442
+ }
443
+ return (n * PerUnit(), true);
444
+ }
445
+
446
+ private void Spend(double amount, double reported, double estimated)
447
+ {
448
+ Total.Amount += amount;
449
+ Total.Reported += reported;
450
+ Total.Estimated += estimated;
451
+
452
+ Budget.Spent = Total.Amount;
453
+ if (Budget.Limit > 0)
454
+ {
455
+ Budget.Remaining = Math.Max(0, Budget.Limit - Total.Amount);
456
+ if (Total.Amount >= Budget.Limit)
457
+ {
458
+ Budget.Exceeded = true;
459
+ }
460
+ }
461
+ else
462
+ {
463
+ Budget.Remaining = 0;
464
+ }
465
+ }
466
+
467
+ private static void Bump(Dictionary<string, CostBucket> bucket, string key, double amount)
468
+ {
469
+ if (!bucket.TryGetValue(key, out var b))
470
+ {
471
+ b = new CostBucket();
472
+ bucket[key] = b;
473
+ }
474
+ b.Calls++;
475
+ b.Amount += amount;
476
+ }
477
+
478
+ // HTTP header names are case-insensitive and a custom transport keeps
479
+ // conventional casing ("X-Request-Cost"), so FresHeader scans rather
480
+ // than indexes.
481
+ private static (double, bool) HeaderNum(object? res, string name)
482
+ {
483
+ var (v, has) = FresHeader(res, name);
484
+ if (!has)
485
+ {
486
+ return (0, false);
487
+ }
488
+ return double.TryParse(v.Trim(),
489
+ System.Globalization.NumberStyles.Float,
490
+ System.Globalization.CultureInfo.InvariantCulture, out var n)
491
+ ? (n, true)
492
+ : (0, false);
493
+ }
494
+
495
+ private static (double, bool) ToNum(object? v)
496
+ {
497
+ return v switch
498
+ {
499
+ int n => (n, true),
500
+ long n => (n, true),
501
+ double n => (n, true),
502
+ float n => (n, true),
503
+ short n => (n, true),
504
+ byte n => (n, true),
505
+ _ => (0, false),
506
+ };
507
+ }
508
+
509
+ private double PerUnit()
510
+ {
511
+ return FoptNum(_options, "perUnit", 0);
512
+ }
513
+
514
+ private double Limit()
515
+ {
516
+ return FoptNum(_options, "budget", 0);
517
+ }
518
+ }