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