@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,430 @@
1
+ package KOTLINPACKAGE.feature
2
+
3
+ import KOTLINPACKAGE.core.Context
4
+ import KOTLINPACKAGE.core.FetcherFn
5
+ import KOTLINPACKAGE.core.SdkClient
6
+ import KOTLINPACKAGE.utility.struct.Struct
7
+
8
+ // Cost tracking and spend budget. Uses BOTH seams, which is the point of the
9
+ // feature: money is spent per HTTP ATTEMPT (a retried call is charged again,
10
+ // because the upstream API charges it again), but it is owed by an
11
+ // OPERATION. So the transport wrap prices each attempt, and preDone
12
+ // attributes the running total to `<entity>.<op>` and to the caller
13
+ // (`ctrl.actor`, the same actor the audit feature records).
14
+ //
15
+ // The price of an attempt comes from the first source that answers: a
16
+ // response header (`header` x `perUnit`), the rate table (`rates`, keyed
17
+ // '<entity>.<op>' / '<op>' / '*'), then the flat `unit`. A body figure
18
+ // (`path` x `perUnit`, e.g. "usage.total_tokens") is read at preDone
19
+ // instead, from the already-parsed result. A body figure describes the whole
20
+ // call, so it REPLACES the per-attempt estimate rather than adding to it.
21
+ //
22
+ // `budget` caps total spend. With `onBudget` = "deny" a further operation is
23
+ // refused at prePoint, before an endpoint is resolved and before anything
24
+ // reaches the network.
25
+ //
26
+ // ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
27
+ // cache is charged for money that was never spent.
28
+ @Suppress("UNCHECKED_CAST")
29
+ class CostFeature : BaseFeature("cost", "0.0.1", true) {
30
+
31
+ private var client: SdkClient? = null
32
+ private var options: MutableMap<String, Any?>? = null
33
+ private var seq = 0
34
+
35
+ // Aggregates (mirrors the ts client._cost record).
36
+ var currency = "USD"
37
+ var total = CostTotal()
38
+ var ops: MutableMap<String, CostBucket> = linkedMapOf()
39
+ var actors: MutableMap<String, CostBucket> = linkedMapOf()
40
+ var budget = CostBudget()
41
+ var last: CostRecord? = null
42
+
43
+ class CostBucket {
44
+ var calls = 0
45
+ var amount = 0.0
46
+ }
47
+
48
+ class CostTotal {
49
+ var calls = 0
50
+ var attempts = 0
51
+ var amount = 0.0
52
+ var reported = 0.0
53
+ var estimated = 0.0
54
+ }
55
+
56
+ class CostBudget {
57
+ var limit = 0.0
58
+ var spent = 0.0
59
+ var remaining = 0.0
60
+ var exceeded = false
61
+ }
62
+
63
+ class CostRecord {
64
+ var seq = 0
65
+ var entity = ""
66
+ var op = ""
67
+ var actor = ""
68
+ var amount = 0.0
69
+ var currency = ""
70
+ var source = ""
71
+ var attempts = 0
72
+ }
73
+
74
+ // Per-operation accumulator. Held in ctx.out for the life of one call, the
75
+ // same place metrics keeps its start marker.
76
+ class CostPending {
77
+ var attempts = 0
78
+ var amount = 0.0
79
+ var reported = 0.0
80
+ var estimated = 0.0
81
+ var source = "none"
82
+
83
+ // Set by prePoint. Its absence means the call never entered the pipeline
84
+ // (direct/graphql), so charge commits the spend itself.
85
+ var piped = false
86
+ }
87
+
88
+ override fun init(ctx: Context, options: MutableMap<String, Any?>) {
89
+ this.client = ctx.client
90
+ this.options = options
91
+ this.active = FeatureOptions.foptBool(options, "active", false)
92
+ this.seq = 0
93
+
94
+ this.currency = FeatureOptions.foptStr(options, "currency", "USD")
95
+ this.total = CostTotal()
96
+ this.ops = linkedMapOf()
97
+ this.actors = linkedMapOf()
98
+ this.budget = CostBudget()
99
+ this.budget.limit = limit()
100
+ this.budget.remaining = this.budget.limit
101
+ this.last = null
102
+
103
+ if (!this.active) {
104
+ return
105
+ }
106
+
107
+ val inner: FetcherFn = ctx.utility!!.fetcher
108
+
109
+ ctx.utility!!.fetcher = { ctx2, url, fetchdef -> charge(ctx2, url, fetchdef, inner) }
110
+ }
111
+
112
+ // Budget gate. Runs before endpoint resolution, so a refused call costs
113
+ // nothing at all.
114
+ override fun prePoint(ctx: Context) {
115
+ if (!this.active) {
116
+ return
117
+ }
118
+
119
+ // Mark the context as running through the pipeline, so charge knows a
120
+ // preDone is coming and does not commit the spend itself.
121
+ val pending = pending(ctx)
122
+ pending.piped = true
123
+
124
+ val lim = limit()
125
+ if (lim <= 0) {
126
+ return
127
+ }
128
+
129
+ if (this.total.amount < lim) {
130
+ return
131
+ }
132
+
133
+ this.budget.exceeded = true
134
+
135
+ if ("deny" != FeatureOptions.foptStr(this.options, "onBudget", "warn")) {
136
+ return
137
+ }
138
+
139
+ val err = ctx.makeError(
140
+ "cost_budget",
141
+ "Cost budget of " + lim + " " + this.currency + " is spent (" +
142
+ this.total.amount + " " + this.currency + " used)",
143
+ )
144
+
145
+ // Short-circuit endpoint resolution; makePoint surfaces this error
146
+ // before any network activity.
147
+ ctx.out["point"] = err
148
+ }
149
+
150
+ private fun charge(
151
+ ctx: Context,
152
+ url: String,
153
+ fetchdef: MutableMap<String, Any?>,
154
+ inner: FetcherFn,
155
+ ): Any? {
156
+ var res: Any? = null
157
+ var err: RuntimeException? = null
158
+
159
+ // A throwing transport still costs an attempt. Without this, a run of
160
+ // connection-level failures under `retry` (which catches the throw and
161
+ // tries again) would be charged nothing at all, and an onBudget = "deny"
162
+ // ceiling could never stop it.
163
+ try {
164
+ res = inner(ctx, url, fetchdef)
165
+ } catch (e: RuntimeException) {
166
+ err = e
167
+ }
168
+
169
+ val priced = price(ctx, res)
170
+ val amount = priced.first
171
+ val source = priced.second
172
+
173
+ val pending = pending(ctx)
174
+ pending.attempts++
175
+
176
+ // Accumulated here, committed once at preDone. Adding each attempt to
177
+ // the running total and then subtracting it again when a body figure
178
+ // supersedes it loses precision to catastrophic cancellation
179
+ // (5 + (0.01 - 5) is not 0.01 in binary floating point).
180
+ //
181
+ // Reported and estimated are kept apart per ATTEMPT, not per operation:
182
+ // a 503 priced from the rate table followed by a 200 carrying the cost
183
+ // header is part estimate, part reported, and collapsing both into the
184
+ // final attempt's category would corrupt the split.
185
+ pending.amount += amount
186
+ if ("header" == source || "body" == source) {
187
+ pending.reported += amount
188
+ } else {
189
+ pending.estimated += amount
190
+ }
191
+ pending.source = source
192
+
193
+ this.total.attempts++
194
+
195
+ // direct() and graphql() reach the transport without dispatching any
196
+ // pipeline hooks - no prePoint to gate on, and no preDone to commit.
197
+ // Their spend is committed here instead, or it would never be counted
198
+ // and could run past an onBudget = "deny" ceiling indefinitely. `piped`
199
+ // is set by prePoint, so its absence is the signal.
200
+ if (!pending.piped) {
201
+ commit(ctx, pending, "_", "direct")
202
+ ctx.out.remove(COST_PENDING_KEY)
203
+ }
204
+
205
+ if (err != null) {
206
+ throw err
207
+ }
208
+
209
+ return res
210
+ }
211
+
212
+ private fun pending(ctx: Context): CostPending {
213
+ val raw = ctx.out[COST_PENDING_KEY]
214
+ if (raw is CostPending) {
215
+ return raw
216
+ }
217
+ val pending = CostPending()
218
+ ctx.out[COST_PENDING_KEY] = pending
219
+ return pending
220
+ }
221
+
222
+ // Attribute the operation's spend once the call is finished.
223
+ override fun preDone(ctx: Context) {
224
+ finish(ctx, true)
225
+ }
226
+
227
+ // A failed operation still spent the money. When the pipeline errors,
228
+ // preDone never runs, so without this the attempts are counted and the
229
+ // spend is not, and a budget could never see the cost of a failed call.
230
+ // Whichever hook fires first consumes the pending entry, so it commits
231
+ // exactly once.
232
+ override fun preUnexpected(ctx: Context) {
233
+ finish(ctx, false)
234
+ }
235
+
236
+ private fun finish(ctx: Context, done: Boolean) {
237
+ if (!this.active) {
238
+ return
239
+ }
240
+ val raw = ctx.out[COST_PENDING_KEY]
241
+ if (raw !is CostPending) {
242
+ return
243
+ }
244
+ ctx.out.remove(COST_PENDING_KEY)
245
+
246
+ // A FAILED operation that made no attempt never reached the network:
247
+ // prePoint creates the pending entry to mark the context as piped, and
248
+ // then the budget gate refuses the call (rbac, or an unresolvable
249
+ // endpoint, short-circuits just as early). Committing it would count a
250
+ // call that never happened and file a zero-amount record as `last`.
251
+ //
252
+ // A SUCCEEDED operation that made no attempt is the opposite case: it
253
+ // was served from the cache. That is a real call, and the fact that it
254
+ // cost nothing is the whole point of ordering cost inside the cache.
255
+ if (!done && 0 == raw.attempts) {
256
+ return
257
+ }
258
+
259
+ val entity = if ("" == ctx.op.entity) "_" else ctx.op.entity
260
+ val opname = if ("" == ctx.op.name) "_" else ctx.op.name
261
+
262
+ commit(ctx, raw, entity, opname)
263
+ }
264
+
265
+ // Commit one operation's spend: totals, budget, per-op and per-actor
266
+ // attribution, and the record. Shared by finish and the raw-request path
267
+ // in charge, which has no preDone to reach.
268
+ private fun commit(ctx: Context, pending: CostPending, entity: String, opname: String) {
269
+ var amount = pending.amount
270
+ var reported = pending.reported
271
+ var estimated = pending.estimated
272
+ var source = pending.source
273
+
274
+ // A body figure prices the whole call, so it replaces the per-attempt
275
+ // estimate rather than adding to it - and, being server-stated, the
276
+ // whole amount counts as reported.
277
+ val body = bodyAmount(ctx)
278
+ if (body != null) {
279
+ amount = body
280
+ reported = body
281
+ estimated = 0.0
282
+ source = "body"
283
+ }
284
+
285
+ spend(amount, reported, estimated)
286
+
287
+ var actor = "anonymous"
288
+ val optActor = FeatureOptions.foptStr(this.options, "actor", "")
289
+ if ("" != optActor) {
290
+ actor = optActor
291
+ }
292
+ if ("" != ctx.ctrl.actor) {
293
+ actor = ctx.ctrl.actor
294
+ }
295
+
296
+ this.total.calls++
297
+ bump(this.ops, "$entity.$opname", amount)
298
+ bump(this.actors, actor, amount)
299
+
300
+ this.seq++
301
+ val record = CostRecord()
302
+ record.seq = this.seq
303
+ record.entity = entity
304
+ record.op = opname
305
+ record.actor = actor
306
+ record.amount = amount
307
+ record.currency = this.currency
308
+ record.source = source
309
+ record.attempts = pending.attempts
310
+ this.last = record
311
+
312
+ val sink = this.options?.get("sink")
313
+ if (sink is java.util.function.Consumer<*>) {
314
+ try {
315
+ (sink as java.util.function.Consumer<CostRecord>).accept(record)
316
+ } catch (e: RuntimeException) {
317
+ // A sink must never break the call it is reporting on.
318
+ }
319
+ }
320
+ }
321
+
322
+ // Price one attempt: a reported header figure, else the rate table, else
323
+ // the flat unit.
324
+ private fun price(ctx: Context, res: Any?): Pair<Double, String> {
325
+ val header = FeatureOptions.foptStr(this.options, "header", "")
326
+ if ("" != header) {
327
+ val v = headerNum(res, header)
328
+ if (v != null) {
329
+ return Pair(v * perUnit(), "header")
330
+ }
331
+ }
332
+
333
+ val rate = rate(ctx)
334
+ if (rate != null) {
335
+ return Pair(rate, "table")
336
+ }
337
+
338
+ val unit = FeatureOptions.foptNum(this.options, "unit", 0.0)
339
+ if (0.0 != unit) {
340
+ return Pair(unit, "unit")
341
+ }
342
+
343
+ return Pair(0.0, "none")
344
+ }
345
+
346
+ // The rate table uses the same lookup grammar as rbac's rules:
347
+ // '<entity>.<op>', then '<op>', then '*'.
348
+ private fun rate(ctx: Context): Double? {
349
+ val rates = FeatureOptions.foptMap(this.options, "rates") ?: return null
350
+
351
+ val entity = if (ctx.entity != null) ctx.entity!!.name else ctx.op.entity
352
+ val opname = ctx.op.name
353
+
354
+ for (key in arrayOf("$entity.$opname", opname, "*")) {
355
+ val r = rates[key]
356
+ if (r is Number) {
357
+ return r.toDouble()
358
+ }
359
+ }
360
+ return null
361
+ }
362
+
363
+ // A usage figure from the parsed result body, priced by perUnit. Read
364
+ // here, not at the transport seam, because the body is consumed once.
365
+ private fun bodyAmount(ctx: Context): Double? {
366
+ val path = FeatureOptions.foptStr(this.options, "path", "")
367
+ if ("" == path) {
368
+ return null
369
+ }
370
+ val body = ctx.result?.body as? Map<String, Any?> ?: return null
371
+ val v = Struct.getpath(body, path) ?: return null
372
+ if (v !is Number) {
373
+ return null
374
+ }
375
+ return v.toDouble() * perUnit()
376
+ }
377
+
378
+ private fun spend(amount: Double, reported: Double, estimated: Double) {
379
+ this.total.amount += amount
380
+ this.total.reported += reported
381
+ this.total.estimated += estimated
382
+
383
+ this.budget.spent = this.total.amount
384
+ if (this.budget.limit > 0) {
385
+ this.budget.remaining = Math.max(0.0, this.budget.limit - this.total.amount)
386
+ if (this.total.amount >= this.budget.limit) {
387
+ this.budget.exceeded = true
388
+ }
389
+ } else {
390
+ this.budget.remaining = 0.0
391
+ }
392
+ }
393
+
394
+ private fun bump(bucket: MutableMap<String, CostBucket>, key: String, amount: Double) {
395
+ var b = bucket[key]
396
+ if (b == null) {
397
+ b = CostBucket()
398
+ bucket[key] = b
399
+ }
400
+ b.calls++
401
+ b.amount += amount
402
+ }
403
+
404
+ // HTTP header names are case-insensitive and a custom transport keeps
405
+ // conventional casing ("X-Request-Cost"), so fresHeader scans rather than
406
+ // indexes.
407
+ private fun headerNum(res: Any?, name: String): Double? {
408
+ val v = FeatureOptions.fresHeader(res, name)
409
+ if ("" == v) {
410
+ return null
411
+ }
412
+ return try {
413
+ v.trim().toDouble()
414
+ } catch (e: RuntimeException) {
415
+ null
416
+ }
417
+ }
418
+
419
+ private fun perUnit(): Double {
420
+ return FeatureOptions.foptNum(this.options, "perUnit", 0.0)
421
+ }
422
+
423
+ private fun limit(): Double {
424
+ return FeatureOptions.foptNum(this.options, "budget", 0.0)
425
+ }
426
+
427
+ companion object {
428
+ private const val COST_PENDING_KEY = "cost_pending"
429
+ }
430
+ }
File without changes