<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: grahamprice3746</title>
    <description>The latest articles on DEV Community by grahamprice3746 (@grahamprice3746).</description>
    <link>https://dev.to/grahamprice3746</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4072161%2F2f4c10f2-45dd-4471-87bf-6c8049512068.png</url>
      <title>DEV Community: grahamprice3746</title>
      <link>https://dev.to/grahamprice3746</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grahamprice3746"/>
    <language>en</language>
    <item>
      <title>Per-Tenant Cost Ledger for One API Key, Speech-to-Text, and Model Gateway Summaries</title>
      <dc:creator>grahamprice3746</dc:creator>
      <pubDate>Tue, 11 Aug 2026 04:34:09 +0000</pubDate>
      <link>https://dev.to/grahamprice3746/per-tenant-cost-ledger-for-one-api-key-speech-to-text-and-model-gateway-summaries-86k</link>
      <guid>https://dev.to/grahamprice3746/per-tenant-cost-ledger-for-one-api-key-speech-to-text-and-model-gateway-summaries-86k</guid>
      <description>&lt;p&gt;Short answer: use two explicit stages behind one application workflow: a speech-to-text component produces an immutable transcript, then a multi-model gateway summarizes that transcript under a tenant-aware policy. Treat the single API key as an integration convenience, not as proof that audio ingestion, model routing, billing attribution, retention, or compliance belong to one system.&lt;/p&gt;

&lt;p&gt;That distinction matters in a developer-tools support queue. A ticket may contain a screen recording, a short voice note, and a request for a concise incident summary. The useful unit for finance and operations is not the request count; it is the tenant's accepted transcript and summary, with enough evidence to explain who paid for which attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should a speech-to-text and transcript-summaries gateway record per tenant?
&lt;/h2&gt;

&lt;p&gt;The decision record starts with invariants. An audio object gets a content digest before processing. A tenant, audio digest, and transcription policy identify one accepted transcript version, although that work may have several attempts. A tenant, transcript digest, prompt version, and model policy identify one accepted summary. The database enforces those identities; a retry loop alone does not create exactly-once behavior.&lt;/p&gt;

&lt;p&gt;The cost ledger needs a separate event for each attempt and a separate acceptance record. At minimum, retain the tenant identifier, job identifier, stage, provider request identifier when available, selected model policy, input and output usage when available, currency or unit, timestamp, status, and a redacted error category. Do not infer cost from latency or token counts that were never returned. If a provider reports usage asynchronously, mark the amount as pending and reconcile it later rather than publishing a guessed total.&lt;/p&gt;

&lt;p&gt;The most important boundary is the commit between the stages. A summarizer reads only a transcript whose acceptance transaction has committed. The audio object, transcript digest, usage record, and audit event should be linked by stable identifiers, while raw audio and transcript content follow their own retention and access controls. This leaves a narrow, testable question when a tenant disputes a bill: which attempt ran, which policy selected the model, what usage was reported, and which record was accepted?&lt;/p&gt;

&lt;p&gt;Keep it boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture decision record: how can one API key cover speech-to-text and multi-model routing?
&lt;/h2&gt;

&lt;p&gt;One key can reduce secret distribution and client-library variation, but it should terminate at an internal adapter that owns tenant context. The adapter sends audio to the transcription stage, commits the result, and sends text to the summary stage. It never lets a model response bypass the ledger, and it never treats a gateway's compatible request shape as a guarantee of equivalent model behavior.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it simplifies&lt;/th&gt;
&lt;th&gt;What it leaves to the application&lt;/th&gt;
&lt;th&gt;Suitable decision rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One gateway for audio and text&lt;/td&gt;
&lt;td&gt;Credential distribution and one client entry point&lt;/td&gt;
&lt;td&gt;Capability discovery, audio limits, usage reconciliation, retention, and stage-level retries&lt;/td&gt;
&lt;td&gt;Use only when the deployed catalogue and contract cover both stages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated speech-to-text plus text gateway&lt;/td&gt;
&lt;td&gt;Clear ownership of audio and summary boundaries&lt;/td&gt;
&lt;td&gt;Two credentials, two processors, and cross-stage reconciliation&lt;/td&gt;
&lt;td&gt;Default when transcript correctness and tenant cost evidence matter more than a tidy diagram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct model-family access&lt;/td&gt;
&lt;td&gt;Fewer routing decisions for a deliberately narrow policy&lt;/td&gt;
&lt;td&gt;Portability, fallback routing, and its own cost ledger&lt;/td&gt;
&lt;td&gt;Use when one approved family meets the quality, region, and retention requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted transcription plus hosted summarization&lt;/td&gt;
&lt;td&gt;Control over audio handling and predictable local admission&lt;/td&gt;
&lt;td&gt;Capacity planning, model operations, and a second-stage audit trail&lt;/td&gt;
&lt;td&gt;Use when audio residency or volume justifies operational ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second option is my default for support-ticket triage. It makes a failed transcription distinct from a failed summary, prevents an uncommitted transcript from entering downstream prompts, and permits a tenant policy to say, for example, that summaries may use a cheaper model for routine tickets but must use a higher-quality policy for suspected payment incidents. One credential is still useful: the application can expose one internal workflow to the rest of the system, while its adapters preserve the external boundaries.&lt;/p&gt;

&lt;p&gt;The trade-off is not cosmetic. Two systems mean two retention reviews, two rotation paths, and two usage vocabularies. A single gateway is better when the organization has verified that its audio capability, selected models, regions, deletion behavior, and accounting export all satisfy the same control set. It is not suitable when the key merely hides a missing audio capability or prevents the finance team from attributing usage to a tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical path: tenant attribution, retries, and auditability
&lt;/h2&gt;

&lt;p&gt;The following Go example keeps provider details behind interfaces. The important behavior is the transaction order: reserve an idempotency identity, obtain or reuse the accepted transcript, summarize only committed text, and accept the summary with its measured usage. A real repository would make the uniqueness constraints part of the schema and would make &lt;code&gt;Reserve&lt;/code&gt; safe under concurrent workers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"crypto/sha256"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/hex"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Transcript&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TenantID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Digest&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Text&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Usage&lt;/span&gt;    &lt;span class="n"&gt;Usage&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Summary&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Text&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Usage&lt;/span&gt; &lt;span class="n"&gt;Usage&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Usage&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;InputUnits&lt;/span&gt;  &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;OutputUnits&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SpeechToText&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objectDigest&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Summarizer&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Ledger&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alreadyAccepted&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AcceptTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="n"&gt;Transcript&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;AcceptSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objectDigest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;speech&lt;/span&gt; &lt;span class="n"&gt;SpeechToText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summarizer&lt;/span&gt; &lt;span class="n"&gt;Summarizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;Ledger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;transcriptKey&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"transcript-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objectDigest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcriptKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objectDigest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Digest&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"transcription returned no committed artifact"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AcceptTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcriptKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;summarizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;summaryKey&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"summary-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summaryKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AcceptSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summaryKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a subtle concurrency issue in this compact example: &lt;code&gt;Reserve&lt;/code&gt; and &lt;code&gt;AcceptTranscript&lt;/code&gt; must be designed so a crashed worker can be retried without leaving a permanent reservation that looks accepted. In production, use explicit states such as &lt;code&gt;reserved&lt;/code&gt;, &lt;code&gt;accepted&lt;/code&gt;, and &lt;code&gt;failed&lt;/code&gt;, with lease expiry for work ownership and an append-only audit event for every transition. Exactly-once publication still belongs to the database boundary that exposes the summary, not to the model request.&lt;/p&gt;

&lt;p&gt;For streaming progress, Server-Sent Events are adequate for one-way status updates from the job service to a browser, provided the event id can be used to resume from an audit cursor. They do not solve duplicate work, usage reconciliation, or authorization; those remain application responsibilities. A support agent should see a stable job state, not a stream that quietly makes a second billable attempt after reconnecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the real cost of transcript summaries
&lt;/h2&gt;

&lt;p&gt;Per-tenant visibility requires two ledgers: operational usage and financial attribution. Operational usage answers how much audio or text the system processed. Financial attribution answers what the organization is allowed to charge or report. Those values may differ because of free allowances, rounding, minimum units, regional terms, or a provider invoice that arrives after the request. Store the provider-reported usage and the internal allocation separately.&lt;/p&gt;

&lt;p&gt;A useful allocation record has a tenant, stage, model policy, request attempt, measured units, estimated amount, final reconciled amount, and accounting status. The estimate can drive alerts; only the reconciled amount should drive a bill. Group dashboards by tenant and stage, then inspect outliers by job rather than averaging them away. A 30-minute recording hidden in a monthly mean is an incident waiting to happen.&lt;/p&gt;

&lt;p&gt;Consider a concrete support queue. Tenant &lt;code&gt;acme-dev&lt;/code&gt; uploads a 28-minute incident call. The transcription worker times out after the remote service has accepted the audio, so the job is retried with the same object digest. Both attempts belong in the operational ledger, but only one transcript acceptance may enter the tenant's accepted-work total. The summary worker then receives a network timeout after the model has produced a response; it retries with the same transcript digest and prompt version. If the ledger records only the final summary text, finance sees one result and cannot tell whether two billable attempts occurred. If it records only raw requests, a retry can look like two accepted summaries. The correct record keeps both attempt events, one accepted transcript, one accepted summary, and a reconciliation status for each reported usage item. A later invoice adjustment should append a correction event rather than mutate the original amount. This is tedious by design: an auditor, an on-call engineer, and a tenant administrator need different views of the same chain, and those views should agree without relying on a dashboard's cached aggregate. The job's state machine therefore needs explicit ownership and timestamps at every transition, including admission, provider submission, response receipt, acceptance, reconciliation, and publication.&lt;/p&gt;

&lt;p&gt;Budget controls should fail closed at the application boundary. Before admission, check the tenant's remaining budget and the estimated maximum for the audio and summary policies. After completion, record actual usage and emit an alert when the estimate differs materially from the reconciled value. The threshold is a policy choice, not a fact about any provider, so keep it configurable and versioned.&lt;/p&gt;

&lt;p&gt;I'm not sure a single dashboard can be called a ledger unless it can reproduce its figures from immutable events. Your mileage may vary with provider export timing, but the design test is stable: delete the dashboard cache and rebuild the tenant total from the event stream and reconciliation records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejected option and its valid use case
&lt;/h2&gt;

&lt;p&gt;The rejected option is a direct, synchronous request that uploads audio, waits for transcription, immediately asks a selected model for a summary, and records only the final text. It is attractive in a prototype because it has little code. It is unsuitable for a production support queue: a timeout after transcription can cause duplicate audio work, a summary can be attached to the wrong tenant if context is passed loosely, and the final text cannot explain which attempt consumed the reported usage.&lt;/p&gt;

&lt;p&gt;This option becomes valid for a small internal tool when losing a duplicate attempt has no financial or compliance consequence, audio retention is acceptable, and the result is explicitly disposable. That is a different decision. For customer-support triage with tenant-level cost visibility, commit each stage and keep the audit path.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MDN, Using server-sent events: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenRouter documentation: &lt;a href="https://openrouter.ai/docs" rel="noopener noreferrer"&gt;https://openrouter.ai/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Review the gateway's current model catalogue, usage fields, regional terms, and deletion controls before approving an adapter: &lt;a href="https://openrouter.ai/docs" rel="noopener noreferrer"&gt;https://openrouter.ai/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>speechtotext</category>
      <category>transcriptsummaries</category>
      <category>multimodelgateway</category>
    </item>
  </channel>
</rss>
