@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,467 @@
1
+ // Cost tracking and spend budget (mirrors ts
2
+ // src/feature/cost/CostFeature.ts). Uses BOTH seams, which is the point of
3
+ // the feature: money is spent per HTTP ATTEMPT (a retried call is charged
4
+ // again, because the upstream API charges it again), but it is owed by an
5
+ // OPERATION. So the transport wrap prices each attempt, and pre_done
6
+ // attributes the running total to `<entity>.<op>` and to the caller (the
7
+ // per-call ctrl actor, the same actor the audit feature records).
8
+ //
9
+ // The price of an attempt comes from the first source that answers: a
10
+ // response header (`header` x `perUnit`), the rate table (`rates`, keyed
11
+ // `<entity>.<op>` / `<op>` / `*`), then the flat `unit`. A body figure
12
+ // (`path` x `perUnit`, e.g. "usage.total_tokens") is read at pre_done
13
+ // instead, from the already-parsed result, and describes the whole call, so
14
+ // it REPLACES the per-attempt estimate rather than adding to it.
15
+ //
16
+ // `budget` caps total spend. With `onBudget: "deny"` a further operation is
17
+ // refused at pre_point (via ctx.out["point"], which MakePoint surfaces),
18
+ // before an endpoint is resolved and before 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. The default (map) order
22
+ // puts cache innermost and cost outside it, so activate them in list form
23
+ // with cost first.
24
+
25
+ use std::cell::RefCell;
26
+ use std::collections::HashMap;
27
+ use std::rc::Rc;
28
+
29
+ use crate::core::context::Context;
30
+ use crate::core::error::ProjectNameError;
31
+ use crate::core::helpers::{call_vfn, get_f64, getp, getpath, jo, setp};
32
+ use crate::core::types::{Feature, FetcherFn, OutVal};
33
+ use crate::feature::support::*;
34
+ use crate::utility::voxgigstruct::Value;
35
+
36
+ const COST_PENDING_KEY: &str = "cost_pending";
37
+
38
+ #[derive(Default, Clone)]
39
+ pub struct CostBucket {
40
+ pub calls: i64,
41
+ pub amount: f64,
42
+ }
43
+
44
+ #[derive(Default)]
45
+ pub struct CostTrack {
46
+ // Aggregates (mirrors the ts client._cost record).
47
+ pub currency: String,
48
+ pub calls: i64,
49
+ pub attempts: i64,
50
+ pub amount: f64,
51
+ pub reported: f64,
52
+ pub estimated: f64,
53
+ pub ops: HashMap<String, CostBucket>,
54
+ pub actors: HashMap<String, CostBucket>,
55
+ pub limit: f64,
56
+ pub spent: f64,
57
+ pub remaining: f64,
58
+ pub exceeded: bool,
59
+ pub last: Value,
60
+ pub seq: i64,
61
+ }
62
+
63
+ pub struct CostFeature {
64
+ pub name: String,
65
+ pub active: bool,
66
+ pub add_opts: Option<Value>,
67
+ options: Value,
68
+ pub track: Rc<RefCell<CostTrack>>,
69
+ }
70
+
71
+ impl CostFeature {
72
+ pub fn new() -> CostFeature {
73
+ CostFeature {
74
+ name: "cost".to_string(),
75
+ active: true,
76
+ add_opts: None,
77
+ options: Value::Noval,
78
+ track: Rc::new(RefCell::new(CostTrack::default())),
79
+ }
80
+ }
81
+ }
82
+
83
+ // Price one attempt: a reported header figure, else the rate table, else the
84
+ // flat unit.
85
+ fn price(options: &Value, ctx: &Rc<Context>, res: &Value) -> (f64, String) {
86
+ let header = fopt_str(options, "header", "");
87
+ if !header.is_empty() {
88
+ if let Some(s) = fres_header(res, &header) {
89
+ if let Ok(n) = s.trim().parse::<f64>() {
90
+ return (n * fopt_num(options, "perUnit", 0.0), "header".to_string());
91
+ }
92
+ }
93
+ }
94
+
95
+ if let Some(rate) = rate(options, ctx) {
96
+ return (rate, "table".to_string());
97
+ }
98
+
99
+ let unit = fopt_num(options, "unit", 0.0);
100
+ if unit != 0.0 {
101
+ return (unit, "unit".to_string());
102
+ }
103
+
104
+ (0.0, "none".to_string())
105
+ }
106
+
107
+ // The rate table uses the same lookup grammar as rbac's rules:
108
+ // `<entity>.<op>`, then `<op>`, then `*`.
109
+ fn rate(options: &Value, ctx: &Rc<Context>) -> Option<f64> {
110
+ let rates = fopt_map(options, "rates");
111
+ if !matches!(rates, Value::Map(_)) {
112
+ return None;
113
+ }
114
+
115
+ let (entity, opname) = {
116
+ let op = ctx.op.borrow();
117
+ (op.entity.clone(), op.name.clone())
118
+ };
119
+
120
+ for key in [format!("{}.{}", entity, opname), opname, "*".to_string()] {
121
+ if let Some(n) = get_f64(&rates, &key) {
122
+ return Some(n);
123
+ }
124
+ }
125
+ None
126
+ }
127
+
128
+ // A usage figure from the parsed result body, priced by perUnit. Read here,
129
+ // not at the transport seam, because the body is one-shot.
130
+ fn body_amount(options: &Value, ctx: &Rc<Context>) -> Option<f64> {
131
+ let path = fopt_str(options, "path", "");
132
+ if path.is_empty() {
133
+ return None;
134
+ }
135
+
136
+ let body = match ctx.result.borrow().clone() {
137
+ Some(r) => r.borrow().body.clone(),
138
+ None => return None,
139
+ };
140
+
141
+ let segs: Vec<&str> = path.split('.').collect();
142
+ let val = getpath(&segs, &body);
143
+
144
+ let n = match &val {
145
+ Value::Num(n) => *n,
146
+ Value::Str(s) => match s.trim().parse::<f64>() {
147
+ Ok(v) => v,
148
+ Err(_) => return None,
149
+ },
150
+ _ => return None,
151
+ };
152
+
153
+ Some(n * fopt_num(options, "perUnit", 0.0))
154
+ }
155
+
156
+ fn spend(t: &mut CostTrack, amount: f64, reported: f64, estimated: f64) {
157
+ t.amount += amount;
158
+ t.reported += reported;
159
+ t.estimated += estimated;
160
+
161
+ t.spent = t.amount;
162
+ if t.limit > 0.0 {
163
+ t.remaining = (t.limit - t.amount).max(0.0);
164
+ if t.amount >= t.limit {
165
+ t.exceeded = true;
166
+ }
167
+ } else {
168
+ t.remaining = 0.0;
169
+ }
170
+ }
171
+
172
+ fn bump(bucket: &mut HashMap<String, CostBucket>, key: String, amount: f64) {
173
+ let entry = bucket.entry(key).or_default();
174
+ entry.calls += 1;
175
+ entry.amount += amount;
176
+ }
177
+
178
+ fn charge(
179
+ track: &Rc<RefCell<CostTrack>>,
180
+ options: &Value,
181
+ ctx: &Rc<Context>,
182
+ url: &str,
183
+ fetchdef: &Value,
184
+ inner: &FetcherFn,
185
+ ) -> Result<Value, ProjectNameError> {
186
+ let out = inner(ctx, url, fetchdef);
187
+
188
+ let res = match &out {
189
+ Ok(r) => r.clone(),
190
+ Err(_) => Value::Noval,
191
+ };
192
+
193
+ let (amount, source) = price(options, ctx, &res);
194
+
195
+ // Accumulated on the context, committed once at pre_done. Adding each
196
+ // attempt to the running total and then subtracting it again when a body
197
+ // figure supersedes it loses precision to catastrophic cancellation.
198
+ let pending = match ctx.out_take(COST_PENDING_KEY) {
199
+ Some(OutVal::Val(v)) if matches!(v, Value::Map(_)) => v,
200
+ other => {
201
+ if let Some(o) = other {
202
+ ctx.out_set(COST_PENDING_KEY, o);
203
+ }
204
+ new_pending()
205
+ }
206
+ };
207
+
208
+ let attempts = get_f64(&pending, "attempts").unwrap_or(0.0) + 1.0;
209
+ let total = get_f64(&pending, "amount").unwrap_or(0.0) + amount;
210
+
211
+ // Reported and estimated are kept apart per ATTEMPT: a 503 priced from the
212
+ // rate table followed by a 200 carrying the cost header is part estimate,
213
+ // part reported, and collapsing both into the final attempt's category
214
+ // would corrupt the split.
215
+ //
216
+ // A failed transport is an Err VALUE here, not a panic, so it already
217
+ // reaches this point and is priced from the table or unit - no separate
218
+ // catch is needed, unlike the ts/js ports.
219
+ let bucket = if "header" == source || "body" == source { "reported" } else { "estimated" };
220
+ let prev = get_f64(&pending, bucket).unwrap_or(0.0);
221
+ setp(&pending, bucket, Value::Num(prev + amount));
222
+
223
+ setp(&pending, "attempts", Value::Num(attempts));
224
+ setp(&pending, "amount", Value::Num(total));
225
+ setp(&pending, "source", Value::str(source));
226
+
227
+ let piped = matches!(getp(&pending, "piped"), Value::Bool(true));
228
+
229
+ track.borrow_mut().attempts += 1;
230
+
231
+ // direct() and graphql() reach the transport without dispatching any
232
+ // pipeline hooks, so there is no pre_point to gate on and no pre_done to
233
+ // commit. Their spend is committed here, or it would never be counted.
234
+ if !piped {
235
+ commit(track, options, ctx, &pending, "_", "direct");
236
+ ctx.out_take(COST_PENDING_KEY);
237
+ } else {
238
+ ctx.out_set(COST_PENDING_KEY, OutVal::Val(pending));
239
+ }
240
+
241
+ out
242
+ }
243
+
244
+
245
+ fn new_pending() -> Value {
246
+ jo(vec![
247
+ ("attempts", Value::Num(0.0)),
248
+ ("amount", Value::Num(0.0)),
249
+ ("reported", Value::Num(0.0)),
250
+ ("estimated", Value::Num(0.0)),
251
+ ("source", Value::str("none".to_string())),
252
+ ("piped", Value::Bool(false)),
253
+ ])
254
+ }
255
+
256
+
257
+ // Commit one operation's spend: totals, budget, per-op and per-actor
258
+ // attribution, and the record. Shared by finish and the raw-request path in
259
+ // charge, which has no pre_done to reach.
260
+ fn commit(
261
+ track: &Rc<RefCell<CostTrack>>,
262
+ options: &Value,
263
+ ctx: &Rc<Context>,
264
+ pending: &Value,
265
+ entity: &str,
266
+ opname: &str,
267
+ ) {
268
+ let attempts = get_f64(pending, "attempts").unwrap_or(0.0);
269
+ let mut amount = get_f64(pending, "amount").unwrap_or(0.0);
270
+ let mut reported = get_f64(pending, "reported").unwrap_or(0.0);
271
+ let mut estimated = get_f64(pending, "estimated").unwrap_or(0.0);
272
+ let mut source = match getp(pending, "source") {
273
+ Value::Str(s) => s,
274
+ _ => "none".to_string(),
275
+ };
276
+
277
+ // A body figure prices the whole call, so it replaces the per-attempt
278
+ // estimate rather than adding to it, and being server-stated the whole
279
+ // amount counts as reported.
280
+ if let Some(b) = body_amount(options, ctx) {
281
+ amount = b;
282
+ reported = b;
283
+ estimated = 0.0;
284
+ source = "body".to_string();
285
+ }
286
+
287
+ let mut actor = "anonymous".to_string();
288
+ let opt_actor = fopt_str(options, "actor", "");
289
+ if !opt_actor.is_empty() {
290
+ actor = opt_actor;
291
+ }
292
+ {
293
+ let ctrl = ctx.ctrl.borrow().clone();
294
+ let c = ctrl.borrow();
295
+ if !c.actor.is_empty() {
296
+ actor = c.actor.clone();
297
+ }
298
+ }
299
+
300
+ let record = jo(vec![]);
301
+ {
302
+ let mut t = track.borrow_mut();
303
+ spend(&mut t, amount, reported, estimated);
304
+ t.calls += 1;
305
+ t.seq += 1;
306
+
307
+ bump(&mut t.ops, format!("{}.{}", entity, opname), amount);
308
+ bump(&mut t.actors, actor.clone(), amount);
309
+
310
+ setp(&record, "seq", Value::Num(t.seq as f64));
311
+ setp(&record, "entity", Value::str(entity.to_string()));
312
+ setp(&record, "op", Value::str(opname.to_string()));
313
+ setp(&record, "actor", Value::str(actor));
314
+ setp(&record, "amount", Value::Num(amount));
315
+ setp(&record, "currency", Value::str(t.currency.clone()));
316
+ setp(&record, "source", Value::str(source));
317
+ setp(&record, "attempts", Value::Num(attempts));
318
+
319
+ t.last = record.clone();
320
+ }
321
+
322
+ let sink = getp(options, "sink");
323
+ if let Value::Func(_) = sink {
324
+ call_vfn(&sink, &record);
325
+ }
326
+ }
327
+
328
+ impl Feature for CostFeature {
329
+ fn name(&self) -> String {
330
+ self.name.clone()
331
+ }
332
+ fn active(&self) -> bool {
333
+ self.active
334
+ }
335
+ fn add_options(&self) -> Option<Value> {
336
+ self.add_opts.clone()
337
+ }
338
+
339
+ fn init(&mut self, ctx: &Rc<Context>, options: &Value) {
340
+ self.options = options.clone();
341
+ self.active = fopt_bool(options, "active", false);
342
+
343
+ let limit = fopt_num(options, "budget", 0.0);
344
+ {
345
+ let mut t = self.track.borrow_mut();
346
+ *t = CostTrack::default();
347
+ t.currency = fopt_str(options, "currency", "USD");
348
+ t.limit = limit;
349
+ t.remaining = limit;
350
+ t.last = Value::Noval;
351
+ }
352
+
353
+ if !self.active {
354
+ return;
355
+ }
356
+
357
+ let util = ctx.util();
358
+ let inner: FetcherFn = util.fetcher.borrow().clone();
359
+ let track = self.track.clone();
360
+ let options = options.clone();
361
+
362
+ *util.fetcher.borrow_mut() = Rc::new(move |ctx2, url, fetchdef| {
363
+ charge(&track, &options, ctx2, url, fetchdef, &inner)
364
+ });
365
+ }
366
+
367
+ // The budget gate. Runs before endpoint resolution, so a refused call
368
+ // costs nothing at all.
369
+ fn pre_point(&mut self, ctx: &Rc<Context>) {
370
+ if !self.active {
371
+ return;
372
+ }
373
+
374
+ let (limit, amount, currency) = {
375
+ let t = self.track.borrow();
376
+ (t.limit, t.amount, t.currency.clone())
377
+ };
378
+
379
+ // Mark the context as running through the pipeline, so charge knows a
380
+ // pre_done is coming and does not commit the spend itself.
381
+ let pending = match ctx.out_take(COST_PENDING_KEY) {
382
+ Some(OutVal::Val(v)) if matches!(v, Value::Map(_)) => v,
383
+ other => {
384
+ if let Some(o) = other {
385
+ ctx.out_set(COST_PENDING_KEY, o);
386
+ }
387
+ new_pending()
388
+ }
389
+ };
390
+ setp(&pending, "piped", Value::Bool(true));
391
+ ctx.out_set(COST_PENDING_KEY, OutVal::Val(pending));
392
+
393
+ if limit <= 0.0 || amount < limit {
394
+ return;
395
+ }
396
+
397
+ self.track.borrow_mut().exceeded = true;
398
+
399
+ if "deny" != fopt_str(&self.options, "onBudget", "warn") {
400
+ return;
401
+ }
402
+
403
+ let err = ctx.make_error(
404
+ "cost_budget",
405
+ &format!(
406
+ "Cost budget of {} {} is spent ({} {} used)",
407
+ limit, currency, amount, currency
408
+ ),
409
+ );
410
+
411
+ // Short-circuit endpoint resolution; MakePoint surfaces this error
412
+ // before any network activity.
413
+ ctx.out_set("point", OutVal::Err(err));
414
+ }
415
+
416
+ // Attribute the operation's spend once the call is finished.
417
+ fn pre_done(&mut self, ctx: &Rc<Context>) {
418
+ self.finish(ctx, true);
419
+ }
420
+
421
+ // A failed operation still spent the money. When the pipeline errors,
422
+ // pre_done never runs, so without this the attempts are counted and the
423
+ // spend is not, and a budget could never see the cost of a failed call.
424
+ // Whichever hook fires first consumes the pending entry, so it commits
425
+ // exactly once.
426
+ fn pre_unexpected(&mut self, ctx: &Rc<Context>) {
427
+ self.finish(ctx, false);
428
+ }
429
+ }
430
+
431
+ impl CostFeature {
432
+ fn finish(&mut self, ctx: &Rc<Context>, done: bool) {
433
+ if !self.active {
434
+ return;
435
+ }
436
+
437
+ let pending = match ctx.out_take(COST_PENDING_KEY) {
438
+ Some(OutVal::Val(v)) if matches!(v, Value::Map(_)) => v,
439
+ other => {
440
+ if let Some(o) = other {
441
+ ctx.out_set(COST_PENDING_KEY, o);
442
+ }
443
+ return;
444
+ }
445
+ };
446
+
447
+ // A FAILED operation that made no attempt never reached the network:
448
+ // PrePoint creates the pending entry to mark the context as piped, and
449
+ // then the budget gate refuses the call (rbac, or an unresolvable
450
+ // endpoint, short-circuits just as early). Committing it would count a
451
+ // call that never happened and file a zero-amount record as `last`.
452
+ //
453
+ // A SUCCEEDED operation that made no attempt is the opposite case: it was
454
+ // served from the cache. That is a real call, and the fact that it cost
455
+ // nothing is the whole point of ordering cost inside the cache.
456
+ if !done && 0.0 == get_f64(&pending, "attempts").unwrap_or(0.0) {
457
+ return;
458
+ }
459
+
460
+ let (entity, opname) = {
461
+ let op = ctx.op.borrow();
462
+ (op.entity.clone(), op.name.clone())
463
+ };
464
+
465
+ commit(&self.track, &self.options, ctx, &pending, &entity, &opname);
466
+ }
467
+ }
File without changes