@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,413 @@
1
+
2
+
3
+ const { BaseFeature } = require('../base/BaseFeature')
4
+
5
+
6
+ // Cost tracking and spend budget. Uses BOTH seams, which is the point of
7
+ // the feature: money is spent per HTTP ATTEMPT (a retried call is charged
8
+ // again, because the upstream API charges it again), but it is owed by an
9
+ // OPERATION. So the transport wrap prices each attempt, and PreDone
10
+ // attributes the running total to `<entity>.<op>` and to the caller
11
+ // (`ctrl.actor`, the same actor the audit feature records).
12
+ //
13
+ // The price of an attempt comes from the first source that answers:
14
+ // a response header (`header` x `perUnit`), the rate table (`rates`, keyed
15
+ // '<entity>.<op>' / '<op>' / '*'), then the flat `unit`. A body figure
16
+ // (`path` x `perUnit`, e.g. 'usage.total_tokens') is read at PreDone
17
+ // instead, from the already-parsed result: the response body is a one-shot
18
+ // stream, and consuming it at the transport seam would leave the pipeline
19
+ // with nothing. A body figure describes the whole call, so it REPLACES the
20
+ // 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. The default (map) order
28
+ // puts cache innermost and cost outside it, so activate them in array form
29
+ // with cost first: [{ name: 'cost' }, { name: 'cache' }].
30
+ class CostFeature extends BaseFeature {
31
+ version = '0.0.1'
32
+ name = 'cost'
33
+ active = true
34
+
35
+ _client
36
+ _options = {}
37
+ _pending = new WeakMap()
38
+ _seq = 0
39
+
40
+
41
+ init(ctx, options) {
42
+ this._client = ctx.client
43
+ this._options = options || {}
44
+ this.active = options.active
45
+ this._pending = new WeakMap()
46
+ this._seq = 0
47
+
48
+ const limit = this._limit()
49
+ const client = this._client
50
+ if (null == client._cost) {
51
+ client._cost = {
52
+ currency: this._options.currency || 'USD',
53
+ total: { calls: 0, attempts: 0, amount: 0, reported: 0, estimated: 0 },
54
+ ops: {},
55
+ actors: {},
56
+ budget: { limit, spent: 0, remaining: limit, exceeded: false },
57
+ last: undefined,
58
+ }
59
+ }
60
+
61
+ if (!this.active) {
62
+ return
63
+ }
64
+
65
+ const self = this
66
+ const utility = ctx.utility
67
+ const inner = utility.fetcher
68
+
69
+ utility.fetcher = async function (ctx2, url, fetchdef) {
70
+ return self._charge(ctx2, url, fetchdef, inner)
71
+ }
72
+ }
73
+
74
+
75
+ // Budget gate. Runs before endpoint resolution, so a refused call costs
76
+ // nothing at all.
77
+ PrePoint(ctx) {
78
+ if (!this.active) {
79
+ return
80
+ }
81
+
82
+ // Mark the context as running through the pipeline, so _charge knows a
83
+ // PreDone is coming and does not commit the spend itself.
84
+ let pending = this._pending.get(ctx)
85
+ if (null == pending) {
86
+ pending = this._newPending()
87
+ this._pending.set(ctx, pending)
88
+ }
89
+ pending.piped = true
90
+
91
+ const limit = this._limit()
92
+ if (0 >= limit) {
93
+ return
94
+ }
95
+
96
+ const client = this._client
97
+ const cost = client._cost
98
+ if (cost.total.amount < limit) {
99
+ return
100
+ }
101
+
102
+ cost.budget.exceeded = true
103
+
104
+ if ('deny' !== this._options.onBudget) {
105
+ return
106
+ }
107
+
108
+ const err = ctx.error('cost_budget',
109
+ 'Cost budget of ' + limit + ' ' + cost.currency + ' is spent (' +
110
+ cost.total.amount + ' ' + cost.currency + ' used)')
111
+ // Short-circuit endpoint resolution; the pipeline surfaces this error.
112
+ ctx.out.point = err
113
+ return err
114
+ }
115
+
116
+
117
+ async _charge(ctx, url, fetchdef, inner) {
118
+ let res
119
+ let threw = false
120
+
121
+ // A rejecting transport still costs an attempt. Without this, a run of
122
+ // connection-level failures under `retry` (which catches the throw and
123
+ // tries again) would be charged nothing at all, and an onBudget: 'deny'
124
+ // ceiling could never stop it.
125
+ try {
126
+ res = await inner(ctx, url, fetchdef)
127
+ }
128
+ catch (err) {
129
+ threw = true
130
+ res = err
131
+ }
132
+
133
+ const priced = this._price(ctx, res)
134
+ const client = this._client
135
+ const cost = client._cost
136
+
137
+ let pending = this._pending.get(ctx)
138
+ if (null == pending) {
139
+ pending = this._newPending()
140
+ this._pending.set(ctx, pending)
141
+ }
142
+
143
+ pending.attempts++
144
+
145
+ // Accumulated here, committed once at PreDone. Adding each attempt to
146
+ // the running total and then subtracting it again when a body figure
147
+ // supersedes it loses precision to catastrophic cancellation
148
+ // (5 + (0.01 - 5) is not 0.01 in binary floating point).
149
+ //
150
+ // Reported and estimated are kept apart per ATTEMPT, not per operation:
151
+ // a 503 priced from the rate table followed by a 200 carrying the cost
152
+ // header is part estimate, part reported, and collapsing both into the
153
+ // final attempt's category would corrupt the split.
154
+ pending.amount += priced.amount
155
+ pending[('header' === priced.source || 'body' === priced.source) ?
156
+ 'reported' : 'estimated'] += priced.amount
157
+ pending.source = priced.source
158
+
159
+ cost.total.attempts++
160
+
161
+ // direct() and graphql() call the transport through _rawRequest, which
162
+ // dispatches no pipeline hooks at all — no PrePoint to gate on, and no
163
+ // PreDone to commit. Their spend is committed here instead, or it would
164
+ // never be counted and could run past an onBudget: 'deny' ceiling
165
+ // indefinitely. `piped` is set by PrePoint, so its absence is the signal.
166
+ if (!pending.piped) {
167
+ this._commit(ctx, pending, '_', 'direct')
168
+ this._pending.delete(ctx)
169
+ }
170
+
171
+ if (threw) {
172
+ throw res
173
+ }
174
+
175
+ return res
176
+ }
177
+
178
+
179
+ _newPending() {
180
+ return { attempts: 0, amount: 0, reported: 0, estimated: 0, source: 'none', piped: false }
181
+ }
182
+
183
+
184
+ // Attribute the operation's spend once the call is finished.
185
+ PreDone(ctx) {
186
+ this._finish(ctx, true)
187
+ }
188
+
189
+
190
+ // A failed operation still spent the money. When the pipeline throws,
191
+ // PreDone never runs, so without this the attempts are counted and the
192
+ // spend is not — and a budget could never see the cost of a call that
193
+ // failed. Committing is once-per-operation either way: whichever hook
194
+ // fires first consumes the pending entry.
195
+ PreUnexpected(ctx) {
196
+ this._finish(ctx, false)
197
+ }
198
+
199
+
200
+ _finish(ctx, done) {
201
+ if (!this.active) {
202
+ return
203
+ }
204
+ const pending = this._pending.get(ctx)
205
+ if (null == pending) {
206
+ return
207
+ }
208
+ this._pending.delete(ctx)
209
+
210
+ // A FAILED operation that made no attempt never reached the network:
211
+ // PrePoint creates the pending entry to mark the context as piped, and
212
+ // then the budget gate refuses the call (rbac, or an unresolvable
213
+ // endpoint, short-circuits just as early). Committing it would count a
214
+ // call that never happened and file a zero-amount record as `last`.
215
+ //
216
+ // A SUCCEEDED operation that made no attempt is the opposite case: it was
217
+ // served from the cache. That is a real call, and the fact that it cost
218
+ // nothing is the whole point of ordering cost inside the cache.
219
+ if (!done && 0 === pending.attempts) {
220
+ return
221
+ }
222
+
223
+ const entity = (ctx.op && ctx.op.entity) || '_'
224
+ const opname = (ctx.op && ctx.op.name) || '_'
225
+
226
+ this._commit(ctx, pending, entity, opname)
227
+ }
228
+
229
+
230
+ // Commit one operation's spend: totals, budget, per-op and per-actor
231
+ // attribution, and the record. Shared by PreDone and the raw-request path
232
+ // in _charge, which has no PreDone to reach.
233
+ _commit(ctx, pending, entity, opname) {
234
+ const client = this._client
235
+ const cost = client._cost
236
+
237
+ let amount = pending.amount
238
+ let reported = pending.reported
239
+ let estimated = pending.estimated
240
+ let source = pending.source
241
+
242
+ // A body figure prices the whole call, so it replaces the per-attempt
243
+ // estimate rather than adding to it — and, being server-stated, the
244
+ // whole amount counts as reported.
245
+ const body = this._body(ctx)
246
+ if (null != body) {
247
+ amount = body
248
+ reported = body
249
+ estimated = 0
250
+ source = 'body'
251
+ }
252
+
253
+ this._spend(cost, amount, reported, estimated)
254
+ const actor = (ctx.ctrl && ctx.ctrl.actor) || this._options.actor || 'anonymous'
255
+
256
+ cost.total.calls++
257
+ this._bump(cost.ops, entity + '.' + opname, amount)
258
+ this._bump(cost.actors, actor, amount)
259
+
260
+ this._seq++
261
+ const record = {
262
+ seq: this._seq,
263
+ entity,
264
+ op: opname,
265
+ actor,
266
+ amount,
267
+ currency: cost.currency,
268
+ source,
269
+ attempts: pending.attempts,
270
+ }
271
+ cost.last = record
272
+
273
+ const sink = this._options.sink
274
+ if ('function' === typeof sink) {
275
+ try { sink(record) } catch (_e) { }
276
+ }
277
+ }
278
+
279
+
280
+ // Price one attempt: a reported header figure, else the rate table, else
281
+ // the flat unit.
282
+ _price(ctx, res) {
283
+ const header = this._options.header
284
+ if ('string' === typeof header && '' !== header) {
285
+ const v = this._header(res, header)
286
+ if (null != v) {
287
+ return { amount: v * this._perUnit(), source: 'header' }
288
+ }
289
+ }
290
+
291
+ const rate = this._rate(ctx)
292
+ if (null != rate) {
293
+ return { amount: rate, source: 'table' }
294
+ }
295
+
296
+ const unit = this._options.unit
297
+ if ('number' === typeof unit && 0 !== unit) {
298
+ return { amount: unit, source: 'unit' }
299
+ }
300
+
301
+ return { amount: 0, source: 'none' }
302
+ }
303
+
304
+
305
+ // The rate table uses the same lookup grammar as rbac's rules:
306
+ // '<entity>.<op>', then '<op>', then '*'.
307
+ _rate(ctx) {
308
+ const rates = this._options.rates || {}
309
+ const entity = (ctx.entity && ctx.entity.name) || (ctx.op && ctx.op.entity) || ''
310
+ const opname = (ctx.op && ctx.op.name) || ''
311
+
312
+ if ('number' === typeof rates[entity + '.' + opname]) {
313
+ return rates[entity + '.' + opname]
314
+ }
315
+ if ('number' === typeof rates[opname]) {
316
+ return rates[opname]
317
+ }
318
+ if ('number' === typeof rates['*']) {
319
+ return rates['*']
320
+ }
321
+ return null
322
+ }
323
+
324
+
325
+ // A usage figure from the parsed result body, priced by perUnit. Read
326
+ // here, not at the transport seam, because the body is one-shot.
327
+ _body(ctx) {
328
+ const path = this._options.path
329
+ if ('string' !== typeof path || '' === path) {
330
+ return null
331
+ }
332
+ const result = ctx.result
333
+ if (null == result || null == result.body || 'object' !== typeof result.body) {
334
+ return null
335
+ }
336
+ const v = ctx.utility.struct.getpath(result.body, path)
337
+ const n = Number(v)
338
+ if (null == v || isNaN(n)) {
339
+ return null
340
+ }
341
+ return n * this._perUnit()
342
+ }
343
+
344
+
345
+ _spend(cost, amount, reported, estimated) {
346
+ cost.total.amount += amount
347
+ cost.total.reported += reported
348
+ cost.total.estimated += estimated
349
+
350
+ const limit = cost.budget.limit
351
+ cost.budget.spent = cost.total.amount
352
+ cost.budget.remaining = 0 < limit ? Math.max(0, limit - cost.total.amount) : 0
353
+ if (0 < limit && cost.total.amount >= limit) {
354
+ cost.budget.exceeded = true
355
+ }
356
+ }
357
+
358
+
359
+ _bump(bucket, key, amount) {
360
+ let b = bucket[key]
361
+ if (null == b) {
362
+ b = bucket[key] = { calls: 0, amount: 0 }
363
+ }
364
+ b.calls++
365
+ b.amount += amount
366
+ }
367
+
368
+
369
+ _header(res, name) {
370
+ if (null == res || null == res.headers) {
371
+ return null
372
+ }
373
+ let v
374
+ if ('function' === typeof res.headers.get) {
375
+ v = res.headers.get(name.toLowerCase())
376
+ }
377
+ else {
378
+ // A plain header map from a custom system.fetch keeps conventional
379
+ // casing ('X-Request-Cost'), and HTTP header names are
380
+ // case-insensitive, so scan rather than index. The go, perl and php
381
+ // ports already do this.
382
+ const lower = name.toLowerCase()
383
+ for (const k of Object.keys(res.headers)) {
384
+ if (k.toLowerCase() === lower) {
385
+ v = res.headers[k]
386
+ break
387
+ }
388
+ }
389
+ }
390
+ if (null == v) {
391
+ return null
392
+ }
393
+ const n = Number(v)
394
+ return isNaN(n) ? null : n
395
+ }
396
+
397
+
398
+ _perUnit() {
399
+ const p = this._options.perUnit
400
+ return 'number' === typeof p ? p : 0
401
+ }
402
+
403
+
404
+ _limit() {
405
+ const b = this._options.budget
406
+ return 'number' === typeof b ? b : 0
407
+ }
408
+ }
409
+
410
+
411
+ module.exports = {
412
+ CostFeature
413
+ }