marginfuse 0.2.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +18 -8
- data/lib/marginfuse/client.rb +37 -8
- data/lib/marginfuse/version.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cae1d3d92e08b1a32116bacfdb72d19db66577325fb861231642792c57269499
|
|
4
|
+
data.tar.gz: 06f74aaad11706184f3bb865b4138e1a960ecc21db9ea88f94a805aabc8fed2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea248283d17a1d94b6344cb75f2369622b98dd58c8f972c206100bf92fb29b7735ce6eb7eae33cfdc39bff4485f94a1dae7a7f768e2a438c452e391ea0b00494
|
|
7
|
+
data.tar.gz: 3394d7d8f8376c3721686bf11a2cf83687434d369a5e13856d95785b34da97ac720601063ca065915721cc446cad3c2241611300fbc04e36ba7eecd87363b2a5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
- Validate successful decision responses before using routing fields. Malformed
|
|
6
|
+
responses fail open to the original provider and model, mark the decision
|
|
7
|
+
degraded, and invoke the error hook. Valid blocks without an id still block.
|
|
8
|
+
- Correct OpenAI examples to separate cached input tokens from fresh input,
|
|
9
|
+
and clarify provider dispatch for cross-provider downgrade policies.
|
|
10
|
+
|
|
11
|
+
|
|
3
12
|
All notable changes to this project are documented here. The format follows
|
|
4
13
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
14
|
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
15
|
|
|
16
|
+
## [0.3.0]
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- A downgrade that crosses vendors is reported against the vendor that actually
|
|
21
|
+
ran it. `guard()` already ran the model the server chose, but the usage event
|
|
22
|
+
still named the requested provider, so the call was priced from the wrong
|
|
23
|
+
catalog and the saving the downgrade exists to prove was computed against the
|
|
24
|
+
wrong basis. An `allow` is unchanged, because the decision already defaults
|
|
25
|
+
its provider to the requested one.
|
|
26
|
+
- A downgrade whose provider call then fails is acknowledged as
|
|
27
|
+
`used_downgrade_model` rather than `proceeded_as_requested`. The cheaper model
|
|
28
|
+
did run; what failed came after. Reporting otherwise told reconciliation the
|
|
29
|
+
policy never applied, which skewed realized-savings attribution on the error
|
|
30
|
+
path.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Pinned contract v2, whose new scenarios cover both corrections above and add
|
|
35
|
+
a privacy check that hands the SDK content-bearing fields and scans the bytes
|
|
36
|
+
that actually leave the process.
|
|
37
|
+
|
|
7
38
|
## [0.2.0]
|
|
8
39
|
|
|
9
40
|
### Added
|
data/README.md
CHANGED
|
@@ -26,7 +26,9 @@ bundle add marginfuse
|
|
|
26
26
|
|
|
27
27
|
## Track an AI call
|
|
28
28
|
|
|
29
|
-
Monitoring. One call after each AI request, metadata only.
|
|
29
|
+
Monitoring. One call after each AI request, metadata only. OpenAI prompt tokens
|
|
30
|
+
include cached reads: subtract them from input and report them separately to
|
|
31
|
+
avoid pricing cached tokens at the full input rate.
|
|
30
32
|
|
|
31
33
|
```ruby
|
|
32
34
|
require "marginfuse"
|
|
@@ -34,6 +36,7 @@ require "marginfuse"
|
|
|
34
36
|
mf = MarginFuse.new(api_key: ENV.fetch("MARGINFUSE_KEY"))
|
|
35
37
|
|
|
36
38
|
response = client.chat(model: "gpt-4.1", messages: messages)
|
|
39
|
+
cached = response.usage.prompt_tokens_details&.cached_tokens || 0
|
|
37
40
|
|
|
38
41
|
mf.track(
|
|
39
42
|
customer_id: "cus_8x2m91", # your Stripe customer id, or your own
|
|
@@ -41,7 +44,8 @@ mf.track(
|
|
|
41
44
|
provider: "openai",
|
|
42
45
|
model: "gpt-4.1",
|
|
43
46
|
usage: {
|
|
44
|
-
input_tokens: response.usage.prompt_tokens,
|
|
47
|
+
input_tokens: [0, response.usage.prompt_tokens - cached].max,
|
|
48
|
+
cached_input_tokens: cached,
|
|
45
49
|
output_tokens: response.usage.completion_tokens
|
|
46
50
|
}
|
|
47
51
|
)
|
|
@@ -53,7 +57,9 @@ or the last events go with it.
|
|
|
53
57
|
|
|
54
58
|
## Guard a call
|
|
55
59
|
|
|
56
|
-
Protection. Ask before the call runs, and act on the answer.
|
|
60
|
+
Protection. Ask before the call runs, and act on the answer. This example assumes
|
|
61
|
+
same-provider downgrades; cross-provider policies require dispatching to the
|
|
62
|
+
client named by the decision provider.
|
|
57
63
|
|
|
58
64
|
```ruby
|
|
59
65
|
outcome = mf.guard(
|
|
@@ -64,10 +70,12 @@ outcome = mf.guard(
|
|
|
64
70
|
) do |decision|
|
|
65
71
|
# decision.model is the one to call: a downgrade verdict changes it.
|
|
66
72
|
response = client.chat(model: decision.model, messages: messages)
|
|
73
|
+
cached = response.usage.prompt_tokens_details&.cached_tokens || 0
|
|
67
74
|
{
|
|
68
75
|
result: response,
|
|
69
76
|
usage: {
|
|
70
|
-
input_tokens: response.usage.prompt_tokens,
|
|
77
|
+
input_tokens: [0, response.usage.prompt_tokens - cached].max,
|
|
78
|
+
cached_input_tokens: cached,
|
|
71
79
|
output_tokens: response.usage.completion_tokens
|
|
72
80
|
}
|
|
73
81
|
}
|
|
@@ -98,9 +106,11 @@ must never become your outage. Transport failures go to `on_error`.
|
|
|
98
106
|
|
|
99
107
|
## Tell MarginFuse what a customer pays
|
|
100
108
|
|
|
101
|
-
Margin needs a revenue side
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
Margin needs a revenue side: Stripe for web billing, RevenueCat for App Store
|
|
110
|
+
and Google Play proceeds, or declared plan prices. RevenueCat joins by App User
|
|
111
|
+
ID; use that same ID in your events. Without a billing connection, declare your
|
|
112
|
+
plans in MarginFuse and say which plan each customer is on. Declared revenue
|
|
113
|
+
is unverified and does not confirm payment:
|
|
104
114
|
|
|
105
115
|
```ruby
|
|
106
116
|
result = mf.identify(
|
|
@@ -182,7 +192,7 @@ MARGINFUSE = MarginFuse.new(
|
|
|
182
192
|
Everything, and nothing else:
|
|
183
193
|
|
|
184
194
|
```
|
|
185
|
-
event_id customer_id feature provider model requested_model
|
|
195
|
+
event_id customer_id feature provider model requested_model plan
|
|
186
196
|
usage(input_tokens, output_tokens, cached_input_tokens,
|
|
187
197
|
cache_creation_tokens, images, audio_seconds)
|
|
188
198
|
cost_usd occurred_at outcome decision_id retry_of_event_id corrects_event_id
|
data/lib/marginfuse/client.rb
CHANGED
|
@@ -81,6 +81,10 @@ module MarginFuse
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
parsed = JSON.parse(response.body)
|
|
84
|
+
unless valid_wire_decision?(parsed)
|
|
85
|
+
report(RuntimeError.new("decide: invalid response payload"), "decide")
|
|
86
|
+
return fail_open(provider, model, "invalid response")
|
|
87
|
+
end
|
|
84
88
|
Decision.new(
|
|
85
89
|
id: parsed["id"],
|
|
86
90
|
action: Decision.action_from_wire(parsed["action"]),
|
|
@@ -208,7 +212,8 @@ module MarginFuse
|
|
|
208
212
|
#
|
|
209
213
|
# Yields the decision to the block, which must return a hash with +:usage+
|
|
210
214
|
# and optionally +:result+, +:cost_usd+ and +:outcome+. Use
|
|
211
|
-
# +decision.model+: a downgrade verdict changes
|
|
215
|
+
# +decision.model+ and +decision.provider+: a downgrade verdict changes
|
|
216
|
+
# them, and it can send you to another vendor.
|
|
212
217
|
#
|
|
213
218
|
# It yields rather than returning a decision for you to act on, because
|
|
214
219
|
# enforcement must not depend on the caller remembering to check anything.
|
|
@@ -234,27 +239,34 @@ module MarginFuse
|
|
|
234
239
|
return GuardOutcome.new(kind: :topup_required, decision: decision)
|
|
235
240
|
end
|
|
236
241
|
|
|
242
|
+
# A downgrade can cross vendors, so the vendor that ran the call comes
|
|
243
|
+
# from the decision exactly as the model does. Reporting the caller's
|
|
244
|
+
# would price the call from the wrong catalog and credit the saving to the
|
|
245
|
+
# wrong vendor. The decision defaults both to what was asked for, so
|
|
246
|
+
# anything but a downgrade reports what it always did.
|
|
237
247
|
model_used = decision.action == :downgrade ? decision.model : model
|
|
248
|
+
provider_used = decision.action == :downgrade ? decision.provider : provider
|
|
249
|
+
# A downgrade whose provider call then fails is still a downgrade: the
|
|
250
|
+
# cheaper model is the one that ran, so both paths acknowledge the same.
|
|
251
|
+
acknowledgment =
|
|
252
|
+
decision.action == :downgrade ? :used_downgrade_model : :proceeded_as_requested
|
|
238
253
|
|
|
239
254
|
begin
|
|
240
255
|
call = yield(decision)
|
|
241
256
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
242
|
-
track(customer_id: customer_id, plan: plan, feature: feature, provider:
|
|
257
|
+
track(customer_id: customer_id, plan: plan, feature: feature, provider: provider_used,
|
|
243
258
|
model: model_used, requested_model: model, usage: {},
|
|
244
259
|
outcome: :provider_error, decision_id: decision.id)
|
|
245
|
-
acknowledge(decision.id,
|
|
260
|
+
acknowledge(decision.id, acknowledgment) if decision.id
|
|
246
261
|
raise e
|
|
247
262
|
end
|
|
248
263
|
|
|
249
264
|
call ||= {}
|
|
250
|
-
track(customer_id: customer_id, plan: plan, feature: feature, provider:
|
|
265
|
+
track(customer_id: customer_id, plan: plan, feature: feature, provider: provider_used,
|
|
251
266
|
model: model_used, requested_model: model, usage: call[:usage],
|
|
252
267
|
cost_usd: call[:cost_usd], outcome: call[:outcome] || :success,
|
|
253
268
|
decision_id: decision.id)
|
|
254
|
-
if decision.id
|
|
255
|
-
acknowledge(decision.id,
|
|
256
|
-
decision.action == :downgrade ? :used_downgrade_model : :proceeded_as_requested)
|
|
257
|
-
end
|
|
269
|
+
acknowledge(decision.id, acknowledgment) if decision.id
|
|
258
270
|
|
|
259
271
|
GuardOutcome.new(kind: :completed, decision: decision, result: call[:result])
|
|
260
272
|
end
|
|
@@ -274,6 +286,23 @@ module MarginFuse
|
|
|
274
286
|
|
|
275
287
|
private
|
|
276
288
|
|
|
289
|
+
def valid_wire_decision?(value)
|
|
290
|
+
return false unless value.is_a?(Hash)
|
|
291
|
+
return false unless %w[allow block topup_required downgrade].include?(value["action"])
|
|
292
|
+
|
|
293
|
+
%w[id model provider topupContext degradedReason].each do |key|
|
|
294
|
+
return false if value.key?(key) && !value[key].is_a?(String)
|
|
295
|
+
end
|
|
296
|
+
if value.key?("degraded") && value["degraded"] != true && value["degraded"] != false
|
|
297
|
+
return false
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
%w[model provider].each do |key|
|
|
301
|
+
return false if value.key?(key) && value[key].strip.empty?
|
|
302
|
+
end
|
|
303
|
+
value["action"] != "downgrade" || value.key?("model")
|
|
304
|
+
end
|
|
305
|
+
|
|
277
306
|
def fail_open(provider, model, reason)
|
|
278
307
|
Decision.new(id: nil, action: :allow, model: model, provider: provider,
|
|
279
308
|
topup_context: nil, degraded: true, degraded_reason: reason)
|
data/lib/marginfuse/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MarginFuse
|
|
4
|
-
VERSION = "0.
|
|
4
|
+
VERSION = "0.3.1"
|
|
5
5
|
|
|
6
6
|
# The version of the shared SDK contract this build was verified against.
|
|
7
7
|
#
|
|
@@ -12,5 +12,5 @@ module MarginFuse
|
|
|
12
12
|
# same vectors.
|
|
13
13
|
#
|
|
14
14
|
# See github.com/marginfuse/sdk-contract
|
|
15
|
-
CONTRACT_VERSION =
|
|
15
|
+
CONTRACT_VERSION = 2
|
|
16
16
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: marginfuse
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pemira Labs
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |-
|
|
14
14
|
MarginFuse server-side SDK. Connect revenue to per-request AI cost, see gross
|