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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0b1ac060efeb37a1cff83a21bfec10f5e5972c67622a637100b00746a9defbe
4
- data.tar.gz: '0058b2089e6d46488e0db908ad66e675f83e1b4a8116abdb2edef4a557e7445f'
3
+ metadata.gz: cae1d3d92e08b1a32116bacfdb72d19db66577325fb861231642792c57269499
4
+ data.tar.gz: 06f74aaad11706184f3bb865b4138e1a960ecc21db9ea88f94a805aabc8fed2f
5
5
  SHA512:
6
- metadata.gz: 79059326eec63bb75f28c62b93d144d236d14450d477100be60423b7a6a655dfba9556277f9452ae55f204b8c90e60bb43fcc9008fe3bfedc5b4dbf8f0c334a8
7
- data.tar.gz: 0f8bea6add4ee66ac6badb04073b362b08f1ab22524479407b9cd1b475b7d2f152e69d020a41c712eadef3dcf5fd667bc82ce005edb8e39b3682bc4f7734ceb9
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. With Stripe connected it comes from there. Without
102
- one, you declare your plans in MarginFuse and say which plan each customer is
103
- on:
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
@@ -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)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MarginFuse
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
 
6
6
  # The version of the shared SDK contract this build was verified against.
7
7
  #
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.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-04 00:00:00.000000000 Z
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