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