@voxgig/sdkgen 4.2.6 → 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 (66) 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/js/test/feature/Corpus.test.js +264 -0
  39. package/project/.sdk/tm/kotlin/feature/CostFeature.kt +430 -0
  40. package/project/.sdk/tm/kotlin/src/feature/cost/.gitkeep +0 -0
  41. package/project/.sdk/tm/lean/src/feature/cost/.gitkeep +0 -0
  42. package/project/.sdk/tm/lua/feature/cost_feature.lua +457 -0
  43. package/project/.sdk/tm/lua/src/feature/cost/.gitkeep +0 -0
  44. package/project/.sdk/tm/ocaml/src/feature/cost/.gitkeep +0 -0
  45. package/project/.sdk/tm/perl/feature/cost_feature.pm +446 -0
  46. package/project/.sdk/tm/perl/src/feature/cost/.gitkeep +0 -0
  47. package/project/.sdk/tm/php/feature/CostFeature.php +439 -0
  48. package/project/.sdk/tm/php/feature/TestFeature.php +31 -1
  49. package/project/.sdk/tm/php/src/feature/cost/.gitkeep +0 -0
  50. package/project/.sdk/tm/py/pkg/feature/cost_feature.py +381 -0
  51. package/project/.sdk/tm/py/src/feature/cost/.gitkeep +0 -0
  52. package/project/.sdk/tm/py-data/src/feature/cost/.gitkeep +0 -0
  53. package/project/.sdk/tm/rb/feature/cost_feature.rb +386 -0
  54. package/project/.sdk/tm/rb/src/feature/cost/.gitkeep +0 -0
  55. package/project/.sdk/tm/rust/feature/cost.rs +467 -0
  56. package/project/.sdk/tm/rust/src/feature/cost/.gitkeep +0 -0
  57. package/project/.sdk/tm/scala/feature/CostFeature.scala +388 -0
  58. package/project/.sdk/tm/scala/src/feature/cost/.gitkeep +0 -0
  59. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/CostFeature.swift +425 -0
  60. package/project/.sdk/tm/swift/src/feature/cost/.gitkeep +0 -0
  61. package/project/.sdk/tm/ts/src/feature/cost/CostFeature.ts +415 -0
  62. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +266 -0
  63. package/project/.sdk/tm/zig/feature/cost.zig +457 -0
  64. package/project/.sdk/tm/zig/src/feature/cost/.gitkeep +0 -0
  65. package/project/sdkgen-package.json +2 -1
  66. package/src/helpers/naming.ts +5 -4
@@ -0,0 +1,457 @@
1
+ -- ProjectName SDK cost feature
2
+ --
3
+ -- Cost tracking and spend budget. Uses BOTH seams, which is the point of
4
+ -- the feature: money is spent per HTTP ATTEMPT (a retried call is charged
5
+ -- again, because the upstream API charges it again), but it is owed by an
6
+ -- OPERATION. So the transport wrap prices each attempt, and PreDone
7
+ -- attributes the running total to `<entity>.<op>` and to the caller
8
+ -- (`ctrl.actor`, the same actor the audit feature records).
9
+ --
10
+ -- The price of an attempt comes from the first source that answers:
11
+ -- a response header (`header` x `perUnit`), the rate table (`rates`, keyed
12
+ -- '<entity>.<op>' / '<op>' / '*'), then the flat `unit`. A body figure
13
+ -- (`path` x `perUnit`, e.g. "usage.total_tokens") is read at PreDone
14
+ -- instead, from the already-parsed result. A body figure describes the
15
+ -- whole call, so it REPLACES the per-attempt estimate rather than adding
16
+ -- to it.
17
+ --
18
+ -- `budget` caps total spend. With `onBudget` = "deny" a further operation
19
+ -- is refused at PrePoint, before an endpoint is resolved and before
20
+ -- anything reaches the network.
21
+ --
22
+ -- ORDER MATTERS. Cost must sit INSIDE the cache, or a response served
23
+ -- from cache is charged for money that was never spent. The default (map)
24
+ -- order puts cache innermost and cost outside it, so activate them in
25
+ -- array form with cost first.
26
+
27
+ local BaseFeature = require("feature.base_feature")
28
+ local vs = require("utility.struct.struct")
29
+
30
+ local CostFeature = {}
31
+ CostFeature.__index = CostFeature
32
+ setmetatable(CostFeature, { __index = BaseFeature })
33
+
34
+
35
+ -- Case-insensitive header lookup on a plain header table. HTTP header
36
+ -- names are case-insensitive and a custom transport keeps conventional
37
+ -- casing ("X-Request-Cost"), so scan rather than index.
38
+ local function header_get(headers, name)
39
+ if type(headers) ~= "table" then
40
+ return nil
41
+ end
42
+ local lower = string.lower(name)
43
+ for k, v in pairs(headers) do
44
+ if type(k) == "string" and string.lower(k) == lower then
45
+ return v
46
+ end
47
+ end
48
+ return nil
49
+ end
50
+
51
+
52
+ function CostFeature.new()
53
+ local self = setmetatable(BaseFeature.new(), CostFeature)
54
+ self.version = "0.0.1"
55
+ self.name = "cost"
56
+ self.active = true
57
+ self.client = nil
58
+ self.options = nil
59
+ -- Weak keys: an abandoned context must not leak its accumulator.
60
+ self.pending = setmetatable({}, { __mode = "k" })
61
+ self.seq = 0
62
+ return self
63
+ end
64
+
65
+
66
+ function CostFeature:init(ctx, options)
67
+ self.client = ctx.client
68
+ self.options = options or {}
69
+
70
+ if options["active"] == true then
71
+ self.active = true
72
+ else
73
+ self.active = false
74
+ end
75
+
76
+ self.pending = setmetatable({}, { __mode = "k" })
77
+ self.seq = 0
78
+
79
+ local limit = self:_limit()
80
+ local client = self.client
81
+ if client._cost == nil then
82
+ client._cost = {
83
+ currency = self.options["currency"] or "USD",
84
+ total = { calls = 0, attempts = 0, amount = 0, reported = 0, estimated = 0 },
85
+ ops = {},
86
+ actors = {},
87
+ budget = { limit = limit, spent = 0, remaining = limit, exceeded = false },
88
+ last = nil,
89
+ }
90
+ end
91
+
92
+ if not self.active then
93
+ return
94
+ end
95
+
96
+ local cost_self = self
97
+ local utility = ctx.utility
98
+ local inner = utility.fetcher
99
+
100
+ utility.fetcher = function(fctx, fullurl, fetchdef)
101
+ return cost_self:_charge(fctx, fullurl, fetchdef, inner)
102
+ end
103
+ end
104
+
105
+
106
+ -- Budget gate. Runs before endpoint resolution, so a refused call costs
107
+ -- nothing at all.
108
+ function CostFeature:PrePoint(ctx)
109
+ if not self.active then
110
+ return
111
+ end
112
+
113
+ -- Mark the context as running through the pipeline, so _charge knows a
114
+ -- PreDone is coming and does not commit the spend itself.
115
+ local pending = self.pending[ctx]
116
+ if pending == nil then
117
+ pending = self:_new_pending()
118
+ self.pending[ctx] = pending
119
+ end
120
+ pending.piped = true
121
+
122
+ local limit = self:_limit()
123
+ if limit <= 0 then
124
+ return
125
+ end
126
+
127
+ local cost = self.client._cost
128
+ if cost.total.amount < limit then
129
+ return
130
+ end
131
+
132
+ cost.budget.exceeded = true
133
+
134
+ if self.options["onBudget"] ~= "deny" then
135
+ return
136
+ end
137
+
138
+ local err = ctx:make_error("cost_budget",
139
+ "Cost budget of " .. tostring(limit) .. " " .. cost.currency ..
140
+ " is spent (" .. tostring(cost.total.amount) .. " " .. cost.currency .. " used)")
141
+ -- Short-circuit endpoint resolution; the pipeline surfaces this error.
142
+ ctx.out["point"] = err
143
+ return err
144
+ end
145
+
146
+
147
+ function CostFeature:_charge(ctx, fullurl, fetchdef, inner)
148
+ -- A failing transport still costs an attempt. Without this, a run of
149
+ -- connection-level failures under `retry` (which retries on an error)
150
+ -- would be charged nothing at all, and an onBudget = "deny" ceiling
151
+ -- could never stop it.
152
+ local res, err = inner(ctx, fullurl, fetchdef)
153
+
154
+ local amount, source = self:_price(ctx, res)
155
+ local cost = self.client._cost
156
+
157
+ local pending = self.pending[ctx]
158
+ if pending == nil then
159
+ pending = self:_new_pending()
160
+ self.pending[ctx] = pending
161
+ end
162
+
163
+ pending.attempts = pending.attempts + 1
164
+
165
+ -- Accumulated here, committed once at PreDone. Adding each attempt to
166
+ -- the running total and then subtracting it again when a body figure
167
+ -- supersedes it loses precision to catastrophic cancellation
168
+ -- (5 + (0.01 - 5) is not 0.01 in binary floating point).
169
+ --
170
+ -- Reported and estimated are kept apart per ATTEMPT, not per operation:
171
+ -- a 503 priced from the rate table followed by a 200 carrying the cost
172
+ -- header is part estimate, part reported, and collapsing both into the
173
+ -- final attempt's category would corrupt the split.
174
+ pending.amount = pending.amount + amount
175
+ if source == "header" or source == "body" then
176
+ pending.reported = pending.reported + amount
177
+ else
178
+ pending.estimated = pending.estimated + amount
179
+ end
180
+ pending.source = source
181
+
182
+ cost.total.attempts = cost.total.attempts + 1
183
+
184
+ -- direct() and graphql() reach the transport without dispatching any
185
+ -- pipeline hooks - no PrePoint to gate on, and no PreDone to commit.
186
+ -- Their spend is committed here instead, or it would never be counted
187
+ -- and could run past an onBudget = "deny" ceiling indefinitely. `piped`
188
+ -- is set by PrePoint, so its absence is the signal.
189
+ if not pending.piped then
190
+ self:_commit(ctx, pending, "_", "direct")
191
+ self.pending[ctx] = nil
192
+ end
193
+
194
+ return res, err
195
+ end
196
+
197
+
198
+ function CostFeature:_new_pending()
199
+ return {
200
+ attempts = 0,
201
+ amount = 0,
202
+ reported = 0,
203
+ estimated = 0,
204
+ source = "none",
205
+ piped = false,
206
+ }
207
+ end
208
+
209
+
210
+ -- Attribute the operation's spend once the call is finished.
211
+ function CostFeature:PreDone(ctx)
212
+ self:_finish(ctx, true)
213
+ end
214
+
215
+
216
+ -- A failed operation still spent the money. When the pipeline errors,
217
+ -- PreDone never runs, so without this the attempts are counted and the
218
+ -- spend is not, and a budget could never see the cost of a failed call.
219
+ -- Whichever hook fires first consumes the pending entry, so it commits
220
+ -- exactly once.
221
+ function CostFeature:PreUnexpected(ctx)
222
+ self:_finish(ctx, false)
223
+ end
224
+
225
+
226
+ function CostFeature:_finish(ctx, done)
227
+ if not self.active then
228
+ return
229
+ end
230
+ local pending = self.pending[ctx]
231
+ if pending == nil then
232
+ return
233
+ end
234
+ self.pending[ctx] = nil
235
+
236
+ -- A FAILED operation that made no attempt never reached the network:
237
+ -- PrePoint creates the pending entry to mark the context as piped, and
238
+ -- then the budget gate refuses the call (rbac, or an unresolvable
239
+ -- endpoint, short-circuits just as early). Committing it would count a
240
+ -- call that never happened and file a zero-amount record as `last`.
241
+ --
242
+ -- A SUCCEEDED operation that made no attempt is the opposite case: it
243
+ -- was served from the cache. That is a real call, and the fact that it
244
+ -- cost nothing is the whole point of ordering cost inside the cache.
245
+ if not done and pending.attempts == 0 then
246
+ return
247
+ end
248
+
249
+ local entity = "_"
250
+ local opname = "_"
251
+ if ctx.op ~= nil then
252
+ if type(ctx.op.entity) == "string" and ctx.op.entity ~= "" then
253
+ entity = ctx.op.entity
254
+ end
255
+ if type(ctx.op.name) == "string" and ctx.op.name ~= "" then
256
+ opname = ctx.op.name
257
+ end
258
+ end
259
+
260
+ self:_commit(ctx, pending, entity, opname)
261
+ end
262
+
263
+
264
+ -- Commit one operation's spend: totals, budget, per-op and per-actor
265
+ -- attribution, and the record. Shared by _finish and the raw-request path
266
+ -- in _charge, which has no PreDone to reach.
267
+ function CostFeature:_commit(ctx, pending, entity, opname)
268
+ local cost = self.client._cost
269
+
270
+ local amount = pending.amount
271
+ local reported = pending.reported
272
+ local estimated = pending.estimated
273
+ local source = pending.source
274
+
275
+ -- A body figure prices the whole call, so it replaces the per-attempt
276
+ -- estimate rather than adding to it - and, being server-stated, the
277
+ -- whole amount counts as reported.
278
+ local body = self:_body(ctx)
279
+ if body ~= nil then
280
+ amount = body
281
+ reported = body
282
+ estimated = 0
283
+ source = "body"
284
+ end
285
+
286
+ self:_spend(cost, amount, reported, estimated)
287
+
288
+ local actor = nil
289
+ if ctx.ctrl ~= nil and ctx.ctrl.actor ~= nil then
290
+ actor = ctx.ctrl.actor
291
+ end
292
+ if actor == nil then
293
+ actor = self.options["actor"]
294
+ end
295
+ if actor == nil then
296
+ actor = "anonymous"
297
+ end
298
+
299
+ cost.total.calls = cost.total.calls + 1
300
+ self:_bump(cost.ops, entity .. "." .. opname, amount)
301
+ self:_bump(cost.actors, actor, amount)
302
+
303
+ self.seq = self.seq + 1
304
+ local record = {
305
+ seq = self.seq,
306
+ entity = entity,
307
+ op = opname,
308
+ actor = actor,
309
+ amount = amount,
310
+ currency = cost.currency,
311
+ source = source,
312
+ attempts = pending.attempts,
313
+ }
314
+ cost.last = record
315
+
316
+ local sink = self.options["sink"]
317
+ if type(sink) == "function" then
318
+ pcall(sink, record)
319
+ end
320
+ end
321
+
322
+
323
+ -- Price one attempt: a reported header figure, else the rate table, else
324
+ -- the flat unit. Returns amount, source.
325
+ function CostFeature:_price(ctx, res)
326
+ local header = self.options["header"]
327
+ if type(header) == "string" and header ~= "" then
328
+ local v = self:_header(res, header)
329
+ if v ~= nil then
330
+ return v * self:_per_unit(), "header"
331
+ end
332
+ end
333
+
334
+ local rate = self:_rate(ctx)
335
+ if rate ~= nil then
336
+ return rate, "table"
337
+ end
338
+
339
+ local unit = self.options["unit"]
340
+ if type(unit) == "number" and unit ~= 0 then
341
+ return unit, "unit"
342
+ end
343
+
344
+ return 0, "none"
345
+ end
346
+
347
+
348
+ -- The rate table uses the same lookup grammar as rbac's rules:
349
+ -- '<entity>.<op>', then '<op>', then '*'.
350
+ function CostFeature:_rate(ctx)
351
+ local rates = self.options["rates"] or {}
352
+
353
+ local entity = ""
354
+ if ctx.entity ~= nil then
355
+ if type(ctx.entity.get_name) == "function" then
356
+ entity = ctx.entity:get_name()
357
+ elseif type(ctx.entity.name) == "string" then
358
+ entity = ctx.entity.name
359
+ end
360
+ end
361
+ if entity == "" and ctx.op ~= nil and type(ctx.op.entity) == "string" then
362
+ entity = ctx.op.entity
363
+ end
364
+
365
+ local opname = ""
366
+ if ctx.op ~= nil and type(ctx.op.name) == "string" then
367
+ opname = ctx.op.name
368
+ end
369
+
370
+ if type(rates[entity .. "." .. opname]) == "number" then
371
+ return rates[entity .. "." .. opname]
372
+ end
373
+ if type(rates[opname]) == "number" then
374
+ return rates[opname]
375
+ end
376
+ if type(rates["*"]) == "number" then
377
+ return rates["*"]
378
+ end
379
+ return nil
380
+ end
381
+
382
+
383
+ -- A usage figure from the parsed result body, priced by perUnit. Read
384
+ -- here, not at the transport seam, because the body is consumed once.
385
+ function CostFeature:_body(ctx)
386
+ local path = self.options["path"]
387
+ if type(path) ~= "string" or path == "" then
388
+ return nil
389
+ end
390
+ if ctx.result == nil or type(ctx.result.body) ~= "table" then
391
+ return nil
392
+ end
393
+ local v = vs.getpath(ctx.result.body, path)
394
+ local n = tonumber(v)
395
+ if n == nil then
396
+ return nil
397
+ end
398
+ return n * self:_per_unit()
399
+ end
400
+
401
+
402
+ function CostFeature:_spend(cost, amount, reported, estimated)
403
+ cost.total.amount = cost.total.amount + amount
404
+ cost.total.reported = cost.total.reported + reported
405
+ cost.total.estimated = cost.total.estimated + estimated
406
+
407
+ local limit = cost.budget.limit
408
+ cost.budget.spent = cost.total.amount
409
+ if limit > 0 then
410
+ cost.budget.remaining = math.max(0, limit - cost.total.amount)
411
+ if cost.total.amount >= limit then
412
+ cost.budget.exceeded = true
413
+ end
414
+ else
415
+ cost.budget.remaining = 0
416
+ end
417
+ end
418
+
419
+
420
+ function CostFeature:_bump(bucket, key, amount)
421
+ local b = bucket[key]
422
+ if b == nil then
423
+ b = { calls = 0, amount = 0 }
424
+ bucket[key] = b
425
+ end
426
+ b.calls = b.calls + 1
427
+ b.amount = b.amount + amount
428
+ end
429
+
430
+
431
+ function CostFeature:_header(res, name)
432
+ if res == nil then
433
+ return nil
434
+ end
435
+ return tonumber(header_get(res["headers"], name))
436
+ end
437
+
438
+
439
+ function CostFeature:_per_unit()
440
+ local p = self.options["perUnit"]
441
+ if type(p) == "number" then
442
+ return p
443
+ end
444
+ return 0
445
+ end
446
+
447
+
448
+ function CostFeature:_limit()
449
+ local b = self.options["budget"]
450
+ if type(b) == "number" then
451
+ return b
452
+ end
453
+ return 0
454
+ end
455
+
456
+
457
+ return CostFeature
File without changes