@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,424 @@
1
+ package feature
2
+
3
+ import (
4
+ "strconv"
5
+ "strings"
6
+
7
+ vs "github.com/voxgig/struct"
8
+
9
+ "GOMODULE/core"
10
+ )
11
+
12
+ // Cost tracking and spend budget. Uses BOTH seams, which is the point of
13
+ // the feature: money is spent per HTTP ATTEMPT (a retried call is charged
14
+ // again, because the upstream API charges it again), but it is owed by an
15
+ // OPERATION. So the transport wrap prices each attempt, and PreDone
16
+ // attributes the running total to `<entity>.<op>` and to the caller
17
+ // (`ctrl.actor`, the same actor the audit feature records).
18
+ //
19
+ // The price of an attempt comes from the first source that answers: a
20
+ // response header (`header` x `perUnit`), the rate table (`rates`, keyed
21
+ // `<entity>.<op>` / `<op>` / `*`), then the flat `unit`. A body figure
22
+ // (`path` x `perUnit`, e.g. "usage.total_tokens") is read at PreDone
23
+ // instead, from the already-parsed result, and describes the whole call, so
24
+ // it REPLACES the per-attempt estimate rather than adding to it.
25
+ //
26
+ // `budget` caps total spend. With `onBudget: "deny"` a further operation is
27
+ // refused at PrePoint (via ctx.Out["point"], which MakePoint surfaces),
28
+ // before an endpoint is resolved and before anything reaches the network.
29
+ //
30
+ // ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
31
+ // cache is charged for money that was never spent. The default (map) order
32
+ // puts cache innermost and cost outside it, so activate them in array form
33
+ // with cost first.
34
+ type CostFeature struct {
35
+ BaseFeature
36
+ client *core.ProjectNameSDK
37
+ options map[string]any
38
+
39
+ // Aggregates (mirrors the ts client._cost record).
40
+ Currency string
41
+ Total *CostTotal
42
+ Ops map[string]*CostBucket
43
+ Actors map[string]*CostBucket
44
+ Budget *CostBudget
45
+ Last map[string]any
46
+
47
+ seq int
48
+ }
49
+
50
+ type CostTotal struct {
51
+ Calls int
52
+ Attempts int
53
+ Amount float64
54
+ Reported float64
55
+ Estimated float64
56
+ }
57
+
58
+ type CostBucket struct {
59
+ Calls int
60
+ Amount float64
61
+ }
62
+
63
+ type CostBudget struct {
64
+ Limit float64
65
+ Spent float64
66
+ Remaining float64
67
+ Exceeded bool
68
+ }
69
+
70
+ // Per-operation accumulator, carried on ctx.Out between the transport wrap
71
+ // and PreDone.
72
+ type costPending struct {
73
+ attempts int
74
+ amount float64
75
+ reported float64
76
+ estimated float64
77
+ source string
78
+ // Set by PrePoint. Its absence means the call never entered the pipeline
79
+ // (direct/graphql), so charge commits the spend itself.
80
+ piped bool
81
+ }
82
+
83
+ const costPendingKey = "cost_pending"
84
+
85
+ func NewCostFeature() *CostFeature {
86
+ return &CostFeature{
87
+ BaseFeature: BaseFeature{
88
+ Version: "0.0.1",
89
+ Name: "cost",
90
+ Active: true,
91
+ },
92
+ }
93
+ }
94
+
95
+ func (f *CostFeature) Init(ctx *core.Context, options map[string]any) {
96
+ f.client = ctx.Client
97
+ f.options = options
98
+ f.Active = foptBool(options, "active", false)
99
+
100
+ limit := foptNum(options, "budget", 0)
101
+
102
+ f.Currency = foptStr(options, "currency", "USD")
103
+ f.Total = &CostTotal{}
104
+ f.Ops = map[string]*CostBucket{}
105
+ f.Actors = map[string]*CostBucket{}
106
+ f.Budget = &CostBudget{Limit: limit, Remaining: limit}
107
+ f.seq = 0
108
+
109
+ if !f.Active {
110
+ return
111
+ }
112
+
113
+ inner := ctx.Utility.Fetcher
114
+
115
+ ctx.Utility.Fetcher = func(ctx2 *core.Context, url string, fetchdef map[string]any) (any, error) {
116
+ return f.charge(ctx2, url, fetchdef, inner)
117
+ }
118
+ }
119
+
120
+ // PrePoint is the budget gate. It runs before endpoint resolution, so a
121
+ // refused call costs nothing at all.
122
+ func (f *CostFeature) PrePoint(ctx *core.Context) {
123
+ if !f.Active {
124
+ return
125
+ }
126
+
127
+ // Mark the context as running through the pipeline, so charge knows a
128
+ // PreDone is coming and does not commit the spend itself.
129
+ pending, ok := ctx.Out[costPendingKey].(*costPending)
130
+ if !ok || pending == nil {
131
+ pending = &costPending{source: "none"}
132
+ ctx.Out[costPendingKey] = pending
133
+ }
134
+ pending.piped = true
135
+
136
+ limit := f.Budget.Limit
137
+ if limit <= 0 {
138
+ return
139
+ }
140
+ if f.Total.Amount < limit {
141
+ return
142
+ }
143
+
144
+ f.Budget.Exceeded = true
145
+
146
+ if foptStr(f.options, "onBudget", "warn") != "deny" {
147
+ return
148
+ }
149
+
150
+ err := ctx.MakeError("cost_budget",
151
+ "Cost budget of "+fcostNumStr(limit)+" "+f.Currency+" is spent ("+
152
+ fcostNumStr(f.Total.Amount)+" "+f.Currency+" used)")
153
+
154
+ // Short-circuit endpoint resolution; MakePoint surfaces this error
155
+ // before any network activity.
156
+ ctx.Out["point"] = err
157
+ }
158
+
159
+ func (f *CostFeature) charge(ctx *core.Context, url string, fetchdef map[string]any,
160
+ inner core.FetcherFunc) (any, error) {
161
+
162
+ res, err := inner(ctx, url, fetchdef)
163
+
164
+ amount, source := f.price(ctx, res)
165
+
166
+ pending, ok := ctx.Out[costPendingKey].(*costPending)
167
+ if !ok || pending == nil {
168
+ pending = &costPending{source: "none"}
169
+ ctx.Out[costPendingKey] = pending
170
+ }
171
+
172
+ pending.attempts++
173
+
174
+ // Accumulated here, committed once at PreDone. Adding each attempt to
175
+ // the running total and then subtracting it again when a body figure
176
+ // supersedes it loses precision to catastrophic cancellation.
177
+ //
178
+ // Reported and estimated are kept apart per ATTEMPT: a 503 priced from
179
+ // the rate table followed by a 200 carrying the cost header is part
180
+ // estimate, part reported, and collapsing both into the final attempt's
181
+ // category would corrupt the split.
182
+ //
183
+ // A failed transport is an error VALUE here, not a panic, so it already
184
+ // reaches this point and is priced from the table or unit — no separate
185
+ // rescue is needed, unlike the ts/js ports.
186
+ pending.amount += amount
187
+ if source == "header" || source == "body" {
188
+ pending.reported += amount
189
+ } else {
190
+ pending.estimated += amount
191
+ }
192
+ pending.source = source
193
+
194
+ f.Total.Attempts++
195
+
196
+ // direct() and graphql() reach the transport without dispatching any
197
+ // pipeline hooks, so there is no PrePoint to gate on and no PreDone to
198
+ // commit. Their spend is committed here, or it would never be counted.
199
+ if !pending.piped {
200
+ f.commit(ctx, pending, "_", "direct")
201
+ delete(ctx.Out, costPendingKey)
202
+ }
203
+
204
+ return res, err
205
+ }
206
+
207
+ // PreDone attributes the operation's spend once the call is finished.
208
+ func (f *CostFeature) PreDone(ctx *core.Context) {
209
+ f.finish(ctx, true)
210
+ }
211
+
212
+ // PreUnexpected commits a FAILED operation's spend. When the pipeline errors,
213
+ // PreDone never runs, so without this the attempts are counted and the spend
214
+ // is not, and a budget could never see the cost of a failed call. Whichever
215
+ // hook fires first consumes the pending entry, so it commits exactly once.
216
+ func (f *CostFeature) PreUnexpected(ctx *core.Context) {
217
+ f.finish(ctx, false)
218
+ }
219
+
220
+ func (f *CostFeature) finish(ctx *core.Context, done bool) {
221
+ if !f.Active {
222
+ return
223
+ }
224
+ pending, ok := ctx.Out[costPendingKey].(*costPending)
225
+ if !ok || pending == nil {
226
+ return
227
+ }
228
+ delete(ctx.Out, costPendingKey)
229
+
230
+ // A FAILED operation that made no attempt never reached the network:
231
+ // PrePoint creates the pending entry to mark the context as piped, and
232
+ // then the budget gate refuses the call (rbac, or an unresolvable
233
+ // endpoint, short-circuits just as early). Committing it would count a
234
+ // call that never happened and file a zero-amount record as Last.
235
+ //
236
+ // A SUCCEEDED operation that made no attempt is the opposite case: it was
237
+ // served from the cache. That is a real call, and the fact that it cost
238
+ // nothing is the whole point of ordering cost inside the cache.
239
+ if !done && pending.attempts == 0 {
240
+ return
241
+ }
242
+
243
+ entity := "_"
244
+ opname := "_"
245
+ if ctx.Op != nil {
246
+ entity = ctx.Op.Entity
247
+ opname = ctx.Op.Name
248
+ }
249
+
250
+ f.commit(ctx, pending, entity, opname)
251
+ }
252
+
253
+ // commit records one operation's spend: totals, budget, per-op and per-actor
254
+ // attribution, and the record. Shared by finish and the raw-request path in
255
+ // charge, which has no PreDone to reach.
256
+ func (f *CostFeature) commit(ctx *core.Context, pending *costPending, entity, opname string) {
257
+ amount := pending.amount
258
+ reported := pending.reported
259
+ estimated := pending.estimated
260
+ source := pending.source
261
+
262
+ // A body figure prices the whole call, so it replaces the per-attempt
263
+ // estimate rather than adding to it, and being server-stated the whole
264
+ // amount counts as reported.
265
+ if body, has := f.body(ctx); has {
266
+ amount = body
267
+ reported = body
268
+ estimated = 0
269
+ source = "body"
270
+ }
271
+
272
+ f.spend(amount, reported, estimated)
273
+
274
+ actor := "anonymous"
275
+ if a := foptStr(f.options, "actor", ""); a != "" {
276
+ actor = a
277
+ }
278
+ if ctx.Ctrl != nil && ctx.Ctrl.Actor != "" {
279
+ actor = ctx.Ctrl.Actor
280
+ }
281
+
282
+ f.Total.Calls++
283
+ f.bump(f.Ops, entity+"."+opname, amount)
284
+ f.bump(f.Actors, actor, amount)
285
+
286
+ f.seq++
287
+ record := map[string]any{
288
+ "seq": f.seq,
289
+ "entity": entity,
290
+ "op": opname,
291
+ "actor": actor,
292
+ "amount": amount,
293
+ "currency": f.Currency,
294
+ "source": source,
295
+ "attempts": pending.attempts,
296
+ }
297
+ f.Last = record
298
+
299
+ if sink, ok := f.options["sink"].(func(map[string]any)); ok {
300
+ sink(record)
301
+ }
302
+ }
303
+
304
+ // price returns the cost of one attempt: a reported header figure, else the
305
+ // rate table, else the flat unit.
306
+ func (f *CostFeature) price(ctx *core.Context, res any) (float64, string) {
307
+ if header := foptStr(f.options, "header", ""); header != "" {
308
+ if s, ok := fresHeader(res, header); ok {
309
+ if n, err := strconv.ParseFloat(strings.TrimSpace(s), 64); err == nil {
310
+ return n * f.perUnit(), "header"
311
+ }
312
+ }
313
+ }
314
+
315
+ if rate, ok := f.rate(ctx); ok {
316
+ return rate, "table"
317
+ }
318
+
319
+ if unit := foptNum(f.options, "unit", 0); unit != 0 {
320
+ return unit, "unit"
321
+ }
322
+
323
+ return 0, "none"
324
+ }
325
+
326
+ // rate uses the same lookup grammar as rbac's rules: `<entity>.<op>`, then
327
+ // `<op>`, then `*`.
328
+ func (f *CostFeature) rate(ctx *core.Context) (float64, bool) {
329
+ rates := foptMap(f.options, "rates")
330
+ if rates == nil {
331
+ return 0, false
332
+ }
333
+
334
+ entity := ""
335
+ if ctx.Entity != nil {
336
+ entity = ctx.Entity.GetName()
337
+ } else if ctx.Op != nil {
338
+ entity = ctx.Op.Entity
339
+ }
340
+ opname := ""
341
+ if ctx.Op != nil {
342
+ opname = ctx.Op.Name
343
+ }
344
+
345
+ for _, key := range []string{entity + "." + opname, opname, "*"} {
346
+ if v, has := rates[key]; has {
347
+ switch n := v.(type) {
348
+ case int:
349
+ return float64(n), true
350
+ case int64:
351
+ return float64(n), true
352
+ case float64:
353
+ return n, true
354
+ case float32:
355
+ return float64(n), true
356
+ }
357
+ }
358
+ }
359
+ return 0, false
360
+ }
361
+
362
+ // body reads a usage figure from the parsed result body, priced by perUnit.
363
+ // Read here, not at the transport seam, because the body is one-shot.
364
+ func (f *CostFeature) body(ctx *core.Context) (float64, bool) {
365
+ path := foptStr(f.options, "path", "")
366
+ if path == "" || ctx.Result == nil || ctx.Result.Body == nil {
367
+ return 0, false
368
+ }
369
+
370
+ switch n := vs.GetPath(path, ctx.Result.Body).(type) {
371
+ case int:
372
+ return float64(n) * f.perUnit(), true
373
+ case int64:
374
+ return float64(n) * f.perUnit(), true
375
+ case float64:
376
+ return n * f.perUnit(), true
377
+ case float32:
378
+ return float64(n) * f.perUnit(), true
379
+ case string:
380
+ if v, err := strconv.ParseFloat(strings.TrimSpace(n), 64); err == nil {
381
+ return v * f.perUnit(), true
382
+ }
383
+ }
384
+ return 0, false
385
+ }
386
+
387
+ func (f *CostFeature) spend(amount, reported, estimated float64) {
388
+ f.Total.Amount += amount
389
+ f.Total.Reported += reported
390
+ f.Total.Estimated += estimated
391
+
392
+ limit := f.Budget.Limit
393
+ f.Budget.Spent = f.Total.Amount
394
+ if limit > 0 {
395
+ f.Budget.Remaining = limit - f.Total.Amount
396
+ if f.Budget.Remaining < 0 {
397
+ f.Budget.Remaining = 0
398
+ }
399
+ if f.Total.Amount >= limit {
400
+ f.Budget.Exceeded = true
401
+ }
402
+ } else {
403
+ f.Budget.Remaining = 0
404
+ }
405
+ }
406
+
407
+ func (f *CostFeature) bump(bucket map[string]*CostBucket, key string, amount float64) {
408
+ b := bucket[key]
409
+ if b == nil {
410
+ b = &CostBucket{}
411
+ bucket[key] = b
412
+ }
413
+ b.Calls++
414
+ b.Amount += amount
415
+ }
416
+
417
+ func (f *CostFeature) perUnit() float64 {
418
+ return foptNum(f.options, "perUnit", 0)
419
+ }
420
+
421
+ // fcostNumStr renders a money amount without an exponent or trailing zeros.
422
+ func fcostNumStr(n float64) string {
423
+ return strconv.FormatFloat(n, 'f', -1, 64)
424
+ }
File without changes