marginfuse 0.3.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 +9 -0
- data/README.md +18 -8
- data/lib/marginfuse/client.rb +21 -0
- data/lib/marginfuse/version.rb +1 -1
- 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,5 +1,14 @@
|
|
|
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).
|
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"]),
|
|
@@ -282,6 +286,23 @@ module MarginFuse
|
|
|
282
286
|
|
|
283
287
|
private
|
|
284
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
|
+
|
|
285
306
|
def fail_open(provider, model, reason)
|
|
286
307
|
Decision.new(id: nil, action: :allow, model: model, provider: provider,
|
|
287
308
|
topup_context: nil, degraded: true, degraded_reason: reason)
|
data/lib/marginfuse/version.rb
CHANGED
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.3.
|
|
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
|