<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Billing on niuzj</title>
    <link>https://niuzj.org/tags/billing/</link>
    <description>Recent content in Billing on niuzj</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Tue, 12 May 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://niuzj.org/tags/billing/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>支付重构里，我最后只信 Redis 那一扣</title>
      <link>https://niuzj.org/posts/payment-billing-refactor-redis-mongodb/</link>
      <pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate>
      <guid>https://niuzj.org/posts/payment-billing-refactor-redis-mongodb/</guid>
      <description>&lt;p&gt;这次改支付链路，最开始我想错了一个地方。&lt;/p&gt;&#xA;&lt;p&gt;我以为问题是“Agent 也要扣费”，所以把 Consumer 里那套工作流扣费逻辑搬过来就行。&lt;/p&gt;&#xA;&lt;p&gt;后来发现不是。&lt;/p&gt;&#xA;&lt;p&gt;真正的问题是：&lt;strong&gt;一次生成请求，到底在哪一刻算占用了用户余额？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;这个问题如果不先回答，代码会很快散掉。LLM 在一个地方扣，tool 在另一个地方扣，工作流节点又有一套预扣。每条路都能跑，但每条路都在暗示不同的账务语义。&lt;/p&gt;&#xA;&lt;p&gt;最后我接受的原则很简单：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Redis 决定请求能不能继续跑。MongoDB 负责把这次消费写成账本。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;也就是先过运行期闸门，再补最终账本。&lt;/p&gt;&#xA;&lt;h2 id=&#34;旧结构哪里开始疼&#34;&gt;旧结构哪里开始疼&lt;/h2&gt;&#xA;&lt;p&gt;OpenCreator 原来的重扣费场景主要在 Workflow Consumer。&lt;/p&gt;&#xA;&lt;p&gt;图片、视频、音频这些节点，通常能在调用模型前算出费用。Consumer 会在 Model Gateway 里先走 &lt;code&gt;CreditBalanceCheckServiceV2.check_for_pre_deduction()&lt;/code&gt;，Redis 里扣成功后再调用外部模型。&lt;/p&gt;&#xA;&lt;p&gt;这个结构放在工作流里是顺的。&lt;/p&gt;&#xA;&lt;p&gt;节点要调用供应商，供应商可能会产生真实成本。余额不够，就别调用。调用失败，就 rollback。&lt;/p&gt;&#xA;&lt;p&gt;Agent 接进来以后，事情变得没那么整齐。&lt;/p&gt;&#xA;&lt;p&gt;Agent 一轮对话里有多次 LLM 调用。LLM token 费用要等 usage 回来才知道。tool 也可能返回 &lt;code&gt;cost_usd&lt;/code&gt;，比如搜索工具、渲染工具、媒体分析工具。subagent 还可能继续开一条新的执行链。&lt;/p&gt;&#xA;&lt;p&gt;如果沿着旧思路，把扣费写在 API handler 或 SSE hook 里，肯定会漏路径。&lt;/p&gt;&#xA;&lt;p&gt;后来这部分被下沉到了 &lt;code&gt;creato/core/executor.py&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_check_balance&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ctx &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; get_billing_context()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; ctx &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;manager&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;enabled:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;manager&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;check_balance_or_raise(ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user_id)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_bill_llm&lt;/span&gt;(self, usage: dict[str, int]) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ctx &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; get_billing_context()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; ctx &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;manager&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;enabled &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; usage:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;manager&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;bill_llm(ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user_id, self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;model, usage, ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;session_id)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_bill_tool&lt;/span&gt;(self, tool_name: str, cost_usd: float &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ctx &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; get_billing_context()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; ctx &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;manager&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;enabled &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; cost_usd &lt;span style=&#34;color:#f92672&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;manager&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;bill_tool(ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user_id, tool_name, cost_usd, ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;session_id)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个改动不是单纯挪位置。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
