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