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