@voxgig/sdkgen 4.2.5 → 4.2.7
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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/helpers/naming.js +5 -4
- package/dist/helpers/naming.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/model/feature/cost.aon +43 -0
- package/project/.sdk/model/feature/feature-index.aon +1 -0
- package/project/.sdk/src/cmp/go/utility_go.ts +20 -7
- package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/utility_js.ts +20 -7
- package/project/.sdk/src/cmp/php/ReadmeExamplesTest_php.ts +144 -12
- package/project/.sdk/src/cmp/py/ReadmeExamplesTest_py.ts +98 -3
- package/project/.sdk/src/cmp/rb/ReadmeExamplesTest_rb.ts +83 -10
- package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/utility_ts.ts +20 -7
- package/project/.sdk/tm/c/core/sdk.h +1 -0
- package/project/.sdk/tm/c/feature/cost.c +549 -0
- package/project/.sdk/tm/c/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/clojure/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/cpp/feature/cost.hpp +419 -0
- package/project/.sdk/tm/cpp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/csharp/feature/CostFeature.cs +518 -0
- package/project/.sdk/tm/csharp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/dart/lib/feature/cost/CostFeature.dart +446 -0
- package/project/.sdk/tm/dart/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/elixir/lib/projectname/feature/cost.ex +442 -0
- package/project/.sdk/tm/elixir/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go/feature/cost_feature.go +424 -0
- package/project/.sdk/tm/go/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go-cli/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go-mcp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/java/feature/CostFeature.java +431 -0
- package/project/.sdk/tm/java/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/js/src/feature/cost/CostFeature.js +413 -0
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +264 -0
- package/project/.sdk/tm/kotlin/feature/CostFeature.kt +430 -0
- package/project/.sdk/tm/kotlin/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/lean/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/lua/feature/cost_feature.lua +457 -0
- package/project/.sdk/tm/lua/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/ocaml/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/perl/feature/cost_feature.pm +446 -0
- package/project/.sdk/tm/perl/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/php/feature/CostFeature.php +439 -0
- package/project/.sdk/tm/php/feature/TestFeature.php +31 -1
- package/project/.sdk/tm/php/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/py/pkg/feature/cost_feature.py +381 -0
- package/project/.sdk/tm/py/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/rb/feature/cost_feature.rb +386 -0
- package/project/.sdk/tm/rb/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/rust/feature/cost.rs +467 -0
- package/project/.sdk/tm/rust/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/scala/feature/CostFeature.scala +388 -0
- package/project/.sdk/tm/scala/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/CostFeature.swift +425 -0
- package/project/.sdk/tm/swift/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/ts/src/feature/cost/CostFeature.ts +415 -0
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +266 -0
- package/project/.sdk/tm/zig/feature/cost.zig +457 -0
- package/project/.sdk/tm/zig/src/feature/cost/.gitkeep +0 -0
- package/project/sdkgen-package.json +2 -1
- package/src/helpers/naming.ts +5 -4
|
@@ -0,0 +1,518 @@
|
|
|
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
|
+
using Voxgig.Struct;
|
|
24
|
+
|
|
25
|
+
using static ProjectNameSdk.Feature.FeatureOptions;
|
|
26
|
+
|
|
27
|
+
namespace ProjectNameSdk.Feature;
|
|
28
|
+
|
|
29
|
+
public class CostBucket
|
|
30
|
+
{
|
|
31
|
+
public int Calls;
|
|
32
|
+
public double Amount;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public class CostRecord
|
|
36
|
+
{
|
|
37
|
+
public int Seq;
|
|
38
|
+
public string Entity = "";
|
|
39
|
+
public string Op = "";
|
|
40
|
+
public string Actor = "";
|
|
41
|
+
public double Amount;
|
|
42
|
+
public string Currency = "";
|
|
43
|
+
public string Source = "";
|
|
44
|
+
public int Attempts;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Per-operation accumulator. Held in ctx.Out for the life of one call, the
|
|
48
|
+
// same place metrics keeps its start marker.
|
|
49
|
+
public class CostPending
|
|
50
|
+
{
|
|
51
|
+
public int Attempts;
|
|
52
|
+
public double Amount;
|
|
53
|
+
public double Reported;
|
|
54
|
+
public double Estimated;
|
|
55
|
+
public string Source = "none";
|
|
56
|
+
|
|
57
|
+
// Set by PrePoint. Its absence means the call never entered the
|
|
58
|
+
// pipeline (direct/graphql), so Charge commits the spend itself.
|
|
59
|
+
public bool Piped;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public class CostTotal
|
|
63
|
+
{
|
|
64
|
+
public int Calls;
|
|
65
|
+
public int Attempts;
|
|
66
|
+
public double Amount;
|
|
67
|
+
public double Reported;
|
|
68
|
+
public double Estimated;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public class CostBudget
|
|
72
|
+
{
|
|
73
|
+
public double Limit;
|
|
74
|
+
public double Spent;
|
|
75
|
+
public double Remaining;
|
|
76
|
+
public bool Exceeded;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public class CostFeature : BaseFeature
|
|
80
|
+
{
|
|
81
|
+
private ProjectNameSDK? _client;
|
|
82
|
+
private Dictionary<string, object?>? _options;
|
|
83
|
+
private int _seq;
|
|
84
|
+
|
|
85
|
+
// Aggregates (mirrors the ts client._cost record).
|
|
86
|
+
public string Currency = "USD";
|
|
87
|
+
public CostTotal Total = new();
|
|
88
|
+
public Dictionary<string, CostBucket> Ops = new();
|
|
89
|
+
public Dictionary<string, CostBucket> Actors = new();
|
|
90
|
+
public CostBudget Budget = new();
|
|
91
|
+
public CostRecord? Last;
|
|
92
|
+
|
|
93
|
+
private const string PendingKey = "cost_pending";
|
|
94
|
+
|
|
95
|
+
public CostFeature()
|
|
96
|
+
{
|
|
97
|
+
Version = "0.0.1";
|
|
98
|
+
Name = "cost";
|
|
99
|
+
Active = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public override void Init(Context ctx, Dictionary<string, object?> options)
|
|
103
|
+
{
|
|
104
|
+
_client = ctx.Client;
|
|
105
|
+
_options = options;
|
|
106
|
+
Active = FoptBool(options, "active", false);
|
|
107
|
+
_seq = 0;
|
|
108
|
+
|
|
109
|
+
Currency = FoptStr(options, "currency", "USD");
|
|
110
|
+
Total = new CostTotal();
|
|
111
|
+
Ops = new Dictionary<string, CostBucket>();
|
|
112
|
+
Actors = new Dictionary<string, CostBucket>();
|
|
113
|
+
var limit = Limit();
|
|
114
|
+
Budget = new CostBudget { Limit = limit, Remaining = limit };
|
|
115
|
+
Last = null;
|
|
116
|
+
|
|
117
|
+
if (!Active)
|
|
118
|
+
{
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var inner = ctx.Utility!.Fetcher;
|
|
123
|
+
|
|
124
|
+
ctx.Utility.Fetcher = (ctx2, url, fetchdef) => Charge(ctx2, url, fetchdef, inner);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Budget gate. Runs before endpoint resolution, so a refused call costs
|
|
128
|
+
// nothing at all.
|
|
129
|
+
public override void PrePoint(Context ctx)
|
|
130
|
+
{
|
|
131
|
+
if (!Active)
|
|
132
|
+
{
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Mark the context as running through the pipeline, so Charge knows
|
|
137
|
+
// a PreDone is coming and does not commit the spend itself.
|
|
138
|
+
var pending = Pending(ctx);
|
|
139
|
+
pending.Piped = true;
|
|
140
|
+
|
|
141
|
+
var limit = Limit();
|
|
142
|
+
if (limit <= 0)
|
|
143
|
+
{
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (Total.Amount < limit)
|
|
148
|
+
{
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Budget.Exceeded = true;
|
|
153
|
+
|
|
154
|
+
if (FoptStr(_options, "onBudget", "warn") != "deny")
|
|
155
|
+
{
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
var err = ctx.MakeError("cost_budget",
|
|
160
|
+
"Cost budget of " + limit + " " + Currency + " is spent (" +
|
|
161
|
+
Total.Amount + " " + Currency + " used)");
|
|
162
|
+
|
|
163
|
+
// Short-circuit endpoint resolution; MakePoint surfaces this error
|
|
164
|
+
// before any network activity.
|
|
165
|
+
ctx.Out["point"] = err;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private object? Charge(Context ctx, string url, Dictionary<string, object?> fetchdef,
|
|
169
|
+
FetcherFunc inner)
|
|
170
|
+
{
|
|
171
|
+
object? res = null;
|
|
172
|
+
Exception? err = null;
|
|
173
|
+
|
|
174
|
+
// A throwing transport still costs an attempt. Without this, a run
|
|
175
|
+
// of connection-level failures under `retry` (which catches the
|
|
176
|
+
// throw and tries again) would be charged nothing at all, and an
|
|
177
|
+
// onBudget = "deny" ceiling could never stop it.
|
|
178
|
+
try
|
|
179
|
+
{
|
|
180
|
+
res = inner(ctx, url, fetchdef);
|
|
181
|
+
}
|
|
182
|
+
catch (Exception ex)
|
|
183
|
+
{
|
|
184
|
+
err = ex;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
var (amount, source) = Price(ctx, res);
|
|
188
|
+
|
|
189
|
+
var pending = Pending(ctx);
|
|
190
|
+
pending.Attempts++;
|
|
191
|
+
|
|
192
|
+
// Accumulated here, committed once at PreDone. Adding each attempt
|
|
193
|
+
// to the running total and then subtracting it again when a body
|
|
194
|
+
// figure supersedes it loses precision to catastrophic cancellation
|
|
195
|
+
// (5 + (0.01 - 5) is not 0.01 in binary floating point).
|
|
196
|
+
//
|
|
197
|
+
// Reported and estimated are kept apart per ATTEMPT, not per
|
|
198
|
+
// operation: a 503 priced from the rate table followed by a 200
|
|
199
|
+
// carrying the cost header is part estimate, part reported, and
|
|
200
|
+
// collapsing both into the final attempt's category would corrupt
|
|
201
|
+
// the split.
|
|
202
|
+
pending.Amount += amount;
|
|
203
|
+
if (source == "header" || source == "body")
|
|
204
|
+
{
|
|
205
|
+
pending.Reported += amount;
|
|
206
|
+
}
|
|
207
|
+
else
|
|
208
|
+
{
|
|
209
|
+
pending.Estimated += amount;
|
|
210
|
+
}
|
|
211
|
+
pending.Source = source;
|
|
212
|
+
|
|
213
|
+
Total.Attempts++;
|
|
214
|
+
|
|
215
|
+
// Direct() and Graphql() reach the transport without dispatching
|
|
216
|
+
// any pipeline hooks - no PrePoint to gate on, and no PreDone to
|
|
217
|
+
// commit. Their spend is committed here instead, or it would never
|
|
218
|
+
// be counted and could run past an onBudget = "deny" ceiling
|
|
219
|
+
// indefinitely. Piped is set by PrePoint, so its absence is the
|
|
220
|
+
// signal.
|
|
221
|
+
if (!pending.Piped)
|
|
222
|
+
{
|
|
223
|
+
Commit(ctx, pending, "_", "direct");
|
|
224
|
+
ctx.Out.Remove(PendingKey);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (err != null)
|
|
228
|
+
{
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return res;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private CostPending Pending(Context ctx)
|
|
236
|
+
{
|
|
237
|
+
if (ctx.Out.TryGetValue(PendingKey, out var raw) && raw is CostPending p)
|
|
238
|
+
{
|
|
239
|
+
return p;
|
|
240
|
+
}
|
|
241
|
+
var pending = new CostPending();
|
|
242
|
+
ctx.Out[PendingKey] = pending;
|
|
243
|
+
return pending;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Attribute the operation's spend once the call is finished.
|
|
247
|
+
public override void PreDone(Context ctx)
|
|
248
|
+
{
|
|
249
|
+
Finish(ctx, true);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// A failed operation still spent the money. When the pipeline errors,
|
|
253
|
+
// PreDone never runs, so without this the attempts are counted and the
|
|
254
|
+
// spend is not, and a budget could never see the cost of a failed call.
|
|
255
|
+
// Whichever hook fires first consumes the pending entry, so it commits
|
|
256
|
+
// exactly once.
|
|
257
|
+
public override void PreUnexpected(Context ctx)
|
|
258
|
+
{
|
|
259
|
+
Finish(ctx, false);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private void Finish(Context ctx, bool done)
|
|
263
|
+
{
|
|
264
|
+
if (!Active)
|
|
265
|
+
{
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (!ctx.Out.TryGetValue(PendingKey, out var raw) || raw is not CostPending pending)
|
|
269
|
+
{
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
ctx.Out.Remove(PendingKey);
|
|
273
|
+
|
|
274
|
+
// A FAILED operation that made no attempt never reached the network:
|
|
275
|
+
// PrePoint creates the pending entry to mark the context as piped,
|
|
276
|
+
// and then the budget gate refuses the call (rbac, or an
|
|
277
|
+
// unresolvable endpoint, short-circuits just as early). Committing
|
|
278
|
+
// it would count a call that never happened and file a zero-amount
|
|
279
|
+
// record as Last.
|
|
280
|
+
//
|
|
281
|
+
// A SUCCEEDED operation that made no attempt is the opposite case:
|
|
282
|
+
// it was served from the cache. That is a real call, and the fact
|
|
283
|
+
// that it cost nothing is the whole point of ordering cost inside
|
|
284
|
+
// the cache.
|
|
285
|
+
if (!done && pending.Attempts == 0)
|
|
286
|
+
{
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
var entity = ctx.Op?.Entity ?? "_";
|
|
291
|
+
var opname = ctx.Op?.Name ?? "_";
|
|
292
|
+
if (entity == "")
|
|
293
|
+
{
|
|
294
|
+
entity = "_";
|
|
295
|
+
}
|
|
296
|
+
if (opname == "")
|
|
297
|
+
{
|
|
298
|
+
opname = "_";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Commit(ctx, pending, entity, opname);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Commit one operation's spend: totals, budget, per-op and per-actor
|
|
305
|
+
// attribution, and the record. Shared by Finish and the raw-request
|
|
306
|
+
// path in Charge, which has no PreDone to reach.
|
|
307
|
+
private void Commit(Context ctx, CostPending pending, string entity, string opname)
|
|
308
|
+
{
|
|
309
|
+
var amount = pending.Amount;
|
|
310
|
+
var reported = pending.Reported;
|
|
311
|
+
var estimated = pending.Estimated;
|
|
312
|
+
var source = pending.Source;
|
|
313
|
+
|
|
314
|
+
// A body figure prices the whole call, so it replaces the
|
|
315
|
+
// per-attempt estimate rather than adding to it - and, being
|
|
316
|
+
// server-stated, the whole amount counts as reported.
|
|
317
|
+
var (body, hasBody) = Body(ctx);
|
|
318
|
+
if (hasBody)
|
|
319
|
+
{
|
|
320
|
+
amount = body;
|
|
321
|
+
reported = body;
|
|
322
|
+
estimated = 0;
|
|
323
|
+
source = "body";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
Spend(amount, reported, estimated);
|
|
327
|
+
|
|
328
|
+
var actor = FoptStr(_options, "actor", "");
|
|
329
|
+
if (ctx.Ctrl != null && ctx.Ctrl.Actor != "")
|
|
330
|
+
{
|
|
331
|
+
actor = ctx.Ctrl.Actor;
|
|
332
|
+
}
|
|
333
|
+
if (actor == "")
|
|
334
|
+
{
|
|
335
|
+
actor = "anonymous";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
Total.Calls++;
|
|
339
|
+
Bump(Ops, entity + "." + opname, amount);
|
|
340
|
+
Bump(Actors, actor, amount);
|
|
341
|
+
|
|
342
|
+
_seq++;
|
|
343
|
+
Last = new CostRecord
|
|
344
|
+
{
|
|
345
|
+
Seq = _seq,
|
|
346
|
+
Entity = entity,
|
|
347
|
+
Op = opname,
|
|
348
|
+
Actor = actor,
|
|
349
|
+
Amount = amount,
|
|
350
|
+
Currency = Currency,
|
|
351
|
+
Source = source,
|
|
352
|
+
Attempts = pending.Attempts,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
if (Opt(_options, "sink") is Action<CostRecord> sink)
|
|
356
|
+
{
|
|
357
|
+
try
|
|
358
|
+
{
|
|
359
|
+
sink(Last);
|
|
360
|
+
}
|
|
361
|
+
catch (Exception)
|
|
362
|
+
{
|
|
363
|
+
// A sink must never break the call it is reporting on.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Price one attempt: a reported header figure, else the rate table,
|
|
369
|
+
// else the flat unit.
|
|
370
|
+
private (double, string) Price(Context ctx, object? res)
|
|
371
|
+
{
|
|
372
|
+
var header = FoptStr(_options, "header", "");
|
|
373
|
+
if (header != "")
|
|
374
|
+
{
|
|
375
|
+
var (v, has) = HeaderNum(res, header);
|
|
376
|
+
if (has)
|
|
377
|
+
{
|
|
378
|
+
return (v * PerUnit(), "header");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
var (rate, hasRate) = Rate(ctx);
|
|
383
|
+
if (hasRate)
|
|
384
|
+
{
|
|
385
|
+
return (rate, "table");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
var unit = FoptNum(_options, "unit", 0);
|
|
389
|
+
if (unit != 0)
|
|
390
|
+
{
|
|
391
|
+
return (unit, "unit");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return (0, "none");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// The rate table uses the same lookup grammar as rbac's rules:
|
|
398
|
+
// '<entity>.<op>', then '<op>', then '*'.
|
|
399
|
+
private (double, bool) Rate(Context ctx)
|
|
400
|
+
{
|
|
401
|
+
var rates = FoptMap(_options, "rates");
|
|
402
|
+
if (rates == null)
|
|
403
|
+
{
|
|
404
|
+
return (0, false);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
var entity = ctx.Entity?.GetName() ?? ctx.Op?.Entity ?? "";
|
|
408
|
+
var opname = ctx.Op?.Name ?? "";
|
|
409
|
+
|
|
410
|
+
foreach (var key in new[] { entity + "." + opname, opname, "*" })
|
|
411
|
+
{
|
|
412
|
+
if (rates.TryGetValue(key, out var r))
|
|
413
|
+
{
|
|
414
|
+
var (n, has) = ToNum(r);
|
|
415
|
+
if (has)
|
|
416
|
+
{
|
|
417
|
+
return (n, true);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return (0, false);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// A usage figure from the parsed result body, priced by perUnit. Read
|
|
425
|
+
// here, not at the transport seam, because the body is consumed once.
|
|
426
|
+
private (double, bool) Body(Context ctx)
|
|
427
|
+
{
|
|
428
|
+
var path = FoptStr(_options, "path", "");
|
|
429
|
+
if (path == "" || ctx.Result == null)
|
|
430
|
+
{
|
|
431
|
+
return (0, false);
|
|
432
|
+
}
|
|
433
|
+
if (ctx.Result.Body is not Dictionary<string, object?> body)
|
|
434
|
+
{
|
|
435
|
+
return (0, false);
|
|
436
|
+
}
|
|
437
|
+
var v = StructUtils.GetPath(body, path);
|
|
438
|
+
var (n, has) = ToNum(v);
|
|
439
|
+
if (!has)
|
|
440
|
+
{
|
|
441
|
+
return (0, false);
|
|
442
|
+
}
|
|
443
|
+
return (n * PerUnit(), true);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private void Spend(double amount, double reported, double estimated)
|
|
447
|
+
{
|
|
448
|
+
Total.Amount += amount;
|
|
449
|
+
Total.Reported += reported;
|
|
450
|
+
Total.Estimated += estimated;
|
|
451
|
+
|
|
452
|
+
Budget.Spent = Total.Amount;
|
|
453
|
+
if (Budget.Limit > 0)
|
|
454
|
+
{
|
|
455
|
+
Budget.Remaining = Math.Max(0, Budget.Limit - Total.Amount);
|
|
456
|
+
if (Total.Amount >= Budget.Limit)
|
|
457
|
+
{
|
|
458
|
+
Budget.Exceeded = true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else
|
|
462
|
+
{
|
|
463
|
+
Budget.Remaining = 0;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private static void Bump(Dictionary<string, CostBucket> bucket, string key, double amount)
|
|
468
|
+
{
|
|
469
|
+
if (!bucket.TryGetValue(key, out var b))
|
|
470
|
+
{
|
|
471
|
+
b = new CostBucket();
|
|
472
|
+
bucket[key] = b;
|
|
473
|
+
}
|
|
474
|
+
b.Calls++;
|
|
475
|
+
b.Amount += amount;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// HTTP header names are case-insensitive and a custom transport keeps
|
|
479
|
+
// conventional casing ("X-Request-Cost"), so FresHeader scans rather
|
|
480
|
+
// than indexes.
|
|
481
|
+
private static (double, bool) HeaderNum(object? res, string name)
|
|
482
|
+
{
|
|
483
|
+
var (v, has) = FresHeader(res, name);
|
|
484
|
+
if (!has)
|
|
485
|
+
{
|
|
486
|
+
return (0, false);
|
|
487
|
+
}
|
|
488
|
+
return double.TryParse(v.Trim(),
|
|
489
|
+
System.Globalization.NumberStyles.Float,
|
|
490
|
+
System.Globalization.CultureInfo.InvariantCulture, out var n)
|
|
491
|
+
? (n, true)
|
|
492
|
+
: (0, false);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private static (double, bool) ToNum(object? v)
|
|
496
|
+
{
|
|
497
|
+
return v switch
|
|
498
|
+
{
|
|
499
|
+
int n => (n, true),
|
|
500
|
+
long n => (n, true),
|
|
501
|
+
double n => (n, true),
|
|
502
|
+
float n => (n, true),
|
|
503
|
+
short n => (n, true),
|
|
504
|
+
byte n => (n, true),
|
|
505
|
+
_ => (0, false),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private double PerUnit()
|
|
510
|
+
{
|
|
511
|
+
return FoptNum(_options, "perUnit", 0);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private double Limit()
|
|
515
|
+
{
|
|
516
|
+
return FoptNum(_options, "budget", 0);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
File without changes
|