# OctoHub 0.8.0: प्रॉक्सी में एकीकृत मीडिया API उग आई

> OctoHub 0.8.0 इमेज, वीडियो, स्पीच और ट्रांसक्रिप्शन को उसी सेल्फ-होस्टेड प्रवेश द्वार के पीछे ले आता है जिसके पीछे आपके chat completions हैं — चार टास्क और पाँच प्रोवाइडरों के लिए एक ही JSON envelope, ऐसी कतारबद्ध jobs जो रीस्टार्ट भी झेल जाती हैं, और प्रति-अनुरोध लागत जो शून्य का अंदाज़ा लगाने के बजाय "कीमत अज्ञात" कहती है। octolib 0.36.1 पर बना। ओपन सोर्स, Rust, Apache-2.0.

# OctoHub 0.8.0: प्रॉक्सी में एकीकृत मीडिया API उग आई

एक महीने पहले जिस सवाल ने हमसे [OctoHub](/blog/introducing-octohub-llm-proxy-for-observability) बनवाया, वह यह था: आपके एजेंट ने चालीस मॉडल कॉल कीं — उसने क्या भेजा, उसकी कीमत क्या रही, और किस अपस्ट्रीम ने जवाब दिया?

अब वही सवाल, बस पैसा किसी और जगह खिसक गया है। आपके एजेंट ने एक डिज़ाइन पर काम करते हुए ग्यारह इमेज बनाईं, फिर नौ सेकंड का एक वीडियो, फिर सारांश को एक सिंथेटिक आवाज़ में पढ़कर सुनाया। इसमें चार प्रोवाइडर शामिल थे। उनमें से तीन ऐसी इकाइयों में बिल बनाते हैं जो टोकन नहीं हैं — GPU-सेकंड, वीडियो-सेकंड, अक्षर। और उनमें से एक ने तो बताया ही नहीं कि लागत क्या रही। अब बताइए: बिल किस ग्राहक को भेजेंगे?

chat completions के लिए हम इसका जवाब दे चुके थे। बाकी हर चीज़ के लिए जवाब अब भी एक स्प्रेडशीट था।

**OctoHub 0.8.0 यह खाई पाट देता है।** इमेज जनरेशन, वीडियो जनरेशन, स्पीच सिंथेसिस और ट्रांसक्रिप्शन अब उसी प्रॉक्सी, उन्हीं क्लाइंट कुंजियों, उन्हीं अनुमति-सूचियों, उसी अनुरोध लॉग और उसी लागत कॉलम से होकर गुज़रते हैं जिनसे `/v1/completions` गुज़रता है। पाँच प्रोवाइडर — fal, ElevenLabs, Replicate, Runway और OpenRouter — एक ही JSON envelope के पीछे।

---

## वह श्रम-विभाजन जिसने इसे छोटा रखा

यह सब [octolib](/blog/octolib-the-engine-behind-our-ai-stack) **0.36.1** के ऊपर बना, जो उसी दिन दोपहर को टैग हुई थी और जहाँ असल मीडिया स्टैक रहता है: चारों टास्क के लिए typed request structs, प्रोवाइडर अडैप्टर, job lifecycle, capability descriptors, और एक reference rate table उन चीज़ों की कीमत लगाने के लिए जिनकी कीमत प्रोवाइडर खुद नहीं बताते। उस रिलीज़ को हमने [सितंबर की शुरुआत के राउंड-अप](/blog/release-round-early-september-2026) में कवर किया था।

OctoHub वाला हिस्सा एक ही नियम से निकलता है: **जो कुछ मॉडलों के बारे में है वह octolib में, जो कुछ टेनेंट्स के बारे में है वह यहाँ।** रूटिंग व्याकरण, अडैप्टर, job handles और प्राइसिंग octolib के हैं। कुंजियाँ, कोटा, persistence, मेट्रिक्स और wire API OctoHub के।

इसी नियम की वजह से कॉन्फ़िग में कोई नया सिंटैक्स नहीं आया। मीडिया अलियास वही चीज़ है जो मॉडल अलियास है:

```toml
[media_models]
"flux" = ["fal:fal-ai/flux/dev", "replicate:black-forest-labs/flux-1.1-pro"]
"veo"  = ["openrouter:google/veo-3.1"]
"tts"  = ["elevenlabs:eleven_flash_v2_5"]

[providers.fal]        # concurrency and rate windows, unchanged
concurrency = 8
requests_per_minute = 60
```

वही `provider:model` व्याकरण, वही अर्थ कि सूची का मतलब लोड बैलेंसिंग है, वही प्रति-प्रोवाइडर लिमिटर। मीडिया `tokens_per_minute` को कभी _फीड_ नहीं करता — मीडिया प्रोवाइडर कोई टोकन रिपोर्ट ही नहीं करते — लेकिन ये विंडो प्रोवाइडर के नाम से बँधी हैं और completions के साथ साझा हैं, इसलिए जिस प्रोवाइडर को आप दोनों कामों के लिए इस्तेमाल करते हैं, वहाँ जो टोकन बजट आपका chat ट्रैफ़िक पहले ही खर्च कर चुका है, वह किसी मीडिया अनुरोध को लौटा देगा।

खराब कॉन्फ़िगरेशन पहले पैसे वाले अनुरोध पर नहीं, बूट पर ही फेल होता है: अनजान प्रोवाइडर, बिगड़ा हुआ `provider:model`, खाली मिरर सूची, या `[models]` से टकराने वाला अलियास — इनमें से कोई भी सर्वर को शुरू नहीं होने देता।

---

## चार टास्क, एक envelope

```
POST /v1/images/generations     generate | edit | inpaint | variation
POST /v1/videos                 text_to_video | image_to_video | reference_to_video | extend | edit
POST /v1/audio/speech
POST /v1/audio/transcriptions
GET  /v1/media/{id}             fetch or advance a job
POST /v1/media/{id}/cancel
GET  /v1/media/models           capabilities, parameters, reference price
```

ये क्लाइंट एंडपॉइंट हैं, ठीक completions की तरह ऑथेंटिकेट होते हैं — वही bearer कुंजी, वही प्रति-कुंजी मॉडल अनुमति-सूची, वही `X-Request-Id` से जुड़ाव:

```bash
curl -sX POST http://127.0.0.1:8080/v1/images/generations \
  -H "Authorization: Bearer <client-key>" \
  -d '{"model":"flux","prompt":"a red panda astronaut","count":2,"size":"1024x1024"}'
```

हर प्रतिक्रिया — इमेज, वीडियो, आवाज़, ट्रांसक्रिप्ट, पूरी हो चुकी हो या अभी चल रही — एक ही ऑब्जेक्ट होती है:

```jsonc
{
  "id": "med_9f3c1e0b…", "object": "media", "task": "text_to_image",
  "status": "succeeded", "model": "fal-ai/flux/dev", "provider": "fal",
  "progress": 1.0,
  "artifacts": [ { "kind": "image", "media_type": "image/png",
                   "source": { "type": "url", "value": "https://…" },
                   "size_bytes": 812345, "expires_at": 1767225600 } ],
  "usage": { "cost": 0.08, "cost_source": "provider", "currency": "USD", … },
  "warnings": [], "safety": { "status": "passed", … }, "error": null
}
```

एक ही आकार का मतलब है persistence के लिए एक ही कोड पथ, लॉग के लिए एक ही row फ़ॉर्मैट, और आपके क्लाइंट को parse करने के लिए एक ही चीज़। ट्रांसक्रिप्शन इकलौता टास्क है जिसका payload कोई artifact नहीं है, इसलिए वह `text`, `language`, `segments` और `words` वाला एक `result` ऑब्जेक्ट जोड़ देता है।

OpenAI के API से दो विचलन जानबूझकर किए गए हैं, और हम उन्हें पहले ही बता देते हैं, बजाय इसके कि आप उन्हें बाद में खोजें: **सब कुछ JSON है, कभी `multipart/form-data` नहीं** — बाइनरी इनपुट `{"type":"url"…}` या `{"type":"base64"…}` ऑब्जेक्ट होते हैं — और इमेज edit, inpaint तथा variation अलग-अलग पथ नहीं, बल्कि एक `mode` फ़ील्ड हैं। `/v1/images/generations` OpenAI का पथ और उसकी `model` / `prompt` / `size` वाली वर्तनी उधार लेता है, मगर उसका wire फ़ॉर्मैट नहीं — इमेज की गिनती `count` है, `n` नहीं, और जवाब में OpenAI के `{"created", "data"}` की जगह ऊपर वाला envelope आता है। कोई OpenAI SDK इसे parse नहीं कर पाएगा; इसे सादे HTTP से या एक पतले रैपर से कॉल कीजिए।

---

## वह job जो उसे शुरू करने वाले अनुरोध से ज़्यादा जीती है

यही वह हिस्सा है जो chat completion को प्रॉक्सी करने से सचमुच अलग है, और डिज़ाइन के फ़ैसले भी यहीं हैं।

एक completion एक कॉल है जो या तो लौटती है या फेल होती है। **मीडिया job जिस पल प्रोवाइडर उसे स्वीकार करता है, उसी पल अपस्ट्रीम पैसा खर्च कर देती है, और उसके बाद मिनटों तक चल सकती है।** वीडियो कोई धीमा अनुरोध नहीं है; वह एक खरीद है जिसके बाद इंतज़ार आता है। बाकी सब कुछ इसी बात को गंभीरता से लेने से निकलता है:

**Row इंतज़ार से पहले लिखी जाती है, बाद में नहीं।** कतार-आधारित प्रोवाइडर के स्वीकार करते ही — fal, Replicate, Runway, OpenRouter वीडियो — OctoHub रिकॉर्ड और credential-रहित `JobHandle` सहेज देता है, और उसके बाद ही इंतज़ार शुरू करता है। रीस्टार्ट, टाइमआउट, या कनेक्शन छोड़ देने वाला क्लाइंट: इनमें से कोई भी उस job को अनाथ नहीं कर सकता जिसका पैसा आप चुका चुके हैं। Handle आपके डेटाबेस में है और उसी से job को दोबारा आगे बढ़ाया जा सकता है। (ElevenLabs और OpenRouter के सिंक्रोनस एंडपॉइंट के पास कोई कतार ही नहीं है जो बदले में handle थमाए — वे पूरा काम submit कॉल के भीतर ही निपटा देते हैं, इसलिए बीच में टूटने की कोई गुंजाइश ही नहीं बचती और दोबारा शुरू करने को कुछ रहता ही नहीं।)

**`202` कोई विफलता नहीं है।** `wait: false` भेजिए और प्रोवाइडर के स्वीकार करते ही id मिल जाती है। `wait: true` भेजिए और `server.upstream_timeout_secs` पार कर जाइए, तो वही `202` मिलता है, `status: "queued"` या `"running"` के साथ। रिमोट काम चलता रहता है; id ज़िंदा है; कुछ खोया नहीं। जब तैयार हों, `GET /v1/media/{id}` पर poll कर लीजिए।

**job को आगे polling ही बढ़ाती है — कोई background worker नहीं है।** यह जानबूझकर तय किया गया non-goal है: worker का मतलब होता एक शेड्यूलर, leases, और उन jobs के लिए एक दूसरा failure mode जिनका कोई इंतज़ार ही नहीं कर रहा। नतीजा छिपाया नहीं गया, डॉक्स में लिखा है: जिस job को आप कभी poll नहीं करते वह `queued` पड़ी रहती है और उसकी लागत कभी दर्ज नहीं होती। पहले से पूरी हो चुकी job को पढ़ना मुफ़्त है और दोबारा बिल कभी नहीं बनाता — terminal row डेटाबेस से परोसी जाती है, कोई अपस्ट्रीम कॉल होती ही नहीं।

**प्रोवाइडर परमिट सिर्फ़ submit पर लागू होता है।** OctoHub का प्रति-प्रोवाइडर कॉन्करेंसी लिमिटर submit कॉल की पहरेदारी करता है, फिर छोड़ देता है। चार मिनट का वीडियो आपके आठ fal स्लॉटों में से एक को चार मिनट तक नहीं बाँधे रखता। हाँ, प्रति-टेनेंट स्लॉट पूरे अनुरोध भर पकड़ा रहता है, इसलिए किसी ग्राहक का मीडिया काम उसी बजट से कटता है जिससे उसके completions कटते हैं।

**Failover submit पर होता है, जहाँ वह सुरक्षित है।** `server.failover_on_error` चालू कीजिए — completions की तरह यह भी डिफ़ॉल्ट रूप से बंद है — और submit पर आई प्रोवाइडर की गड़बड़ी उस कैंडिडेट को हटा देती है और अनुरोध अलियास के अगले मिरर को सौंप देती है। वह गड़बड़ी उस प्रोवाइडर की लगातार विफलताओं में भी गिनी जाती है: `server.provider_error_cooldown_secs` सेट कीजिए (डिफ़ॉल्ट `0`, यानी बंद) और प्रोवाइडर की तरफ़ से लगातार तीन विफलताएँ उसे कूलडाउन में डाल देती हैं, जो उसे रोकता नहीं, बस स्वस्थ कैंडिडेटों के पीछे कर देता है। डिफ़ॉल्ट पर छोड़ दीजिए तो गड़बड़ी सीधे कॉल करने वाले के पास लौट जाती है। दोनों ही सूरतों में, job के _स्वीकार_ हो जाने के बाद failover करने को कुछ बचता ही नहीं — उसका पैसा जा चुका है।

रिकॉर्ड उसी कुंजी तक सीमित हैं जिसने उन्हें बनाया। किसी दूसरे टेनेंट की id पर `404` मिलता है, `403` नहीं — आपको यह जानने को नहीं मिलता कि वह id मौजूद भी है।

---

## पैरामीटर वाली समस्या, और उसका ईमानदार जवाब

हर मीडिया प्रोवाइडर की अपनी राय है कि अनुरोध दिखता कैसा है। fal को `num_inference_steps` और `guidance_scale` चाहिए; कुछ एंडपॉइंट प्रॉम्प्ट को `text` कहते हैं; Runway क्रेडिट बेचता है और अपने ही मॉडल नामों में सोचता है। एकीकृत API को इस बारे में कुछ तय करना ही पड़ता है, और दो खराब जवाब मौजूद हैं: सिर्फ़ साझा हिस्सा उजागर कीजिए (बेकार), या एक अनुवाद परत गढ़ लीजिए जो दिखावा करे कि सब एक जैसा है (झूठ, वह भी महँगा)।

OctoHub का जवाब तीन हिस्सों में है:

**एक portable कोर, हर जगह एक ही वर्तनी** — `prompt`, `count`, `seed`, `size`, `duration_secs`, `negative_prompt`, `output_format`। `size` `"1024x1024"` या `"16:9"` लेता है; इसके अलावा कुछ भी `400` है। Portable का मतलब एक ही नाम है, हर जगह सपोर्ट नहीं: Runway के पास `count`, `negative_prompt` या `output_format` का कोई समकक्ष नहीं है, और OpenRouter के वीडियो एंडपॉइंट के पास भी नहीं, इसलिए डिफ़ॉल्ट सख़्त नीति के तहत उन प्रोवाइडरों पर ये चुपचाप नज़रअंदाज़ होने के बजाय `400` बनकर लौटते हैं — और वह नीति ही नीचे वाला तीसरा हिस्सा है।

**एक escape hatch जो कुछ भी जस का तस आगे पहुँचा देता है**, प्रोवाइडर के हिसाब से namespace किया हुआ:

```jsonc
"provider_options": {
  "fal": { "input": { "num_inference_steps": 28, "guidance_scale": 3.5 },
           "field_map": { "prompt": "text" } }
}
```

`field_map` किसी portable नाम को उस नाम पर मैप कर देता है जो एंडपॉइंट असल में इस्तेमाल करता है — इसलिए portable `prompt` उस एंडपॉइंट पर भी चलता रहता है जिसका फ़ील्ड `text` है। जब कोई अलियास कई प्रोवाइडरों पर फैला हो, तो सारे namespace एक साथ भेज दीजिए; सिर्फ़ जीतने वाले कैंडिडेट का namespace आगे जाता है और बाकी गिरा दिए जाते हैं — यही चीज़ मल्टी-प्रोवाइडर अलियास को इस्तेमाल लायक बनाती है।

**एक नीति कि जब कोई पैरामीटर पूरा न किया जा सके तो क्या हो।** `unsupported_parameters: "error"` (डिफ़ॉल्ट) _पैसा खर्च होने से पहले_ फेल होता है — प्रोडक्शन के लिए यही सही है। `"warn_and_drop"` उसे गिरा देता है और एक चेतावनी लौटाता है — तब काम का, जब एक अलियास असमान सपोर्ट वाले प्रोवाइडरों पर पंखे की तरह फैलता हो। चुनाव आप प्रति अनुरोध करते हैं, क्योंकि आपके अलावा कोई नहीं जानता कि आपका मतलब कौन-सा था।

और `GET /v1/media/models` आपको कुछ भी खर्च करने से पहले बता देता है कि कौन क्या है: हर कॉन्फ़िगर किए गए कैंडिडेट के execution और पैरामीटर capability फ़्लैग, सीमाएँ, उसके अडैप्टर का अपना `provider_options` JSON Schema, और reference कीमत। 0.8.0 में दो खुरदुरे किनारे, जो आपको वैसे भी मिल ही जाते: discovery हर कैंडिडेट को इमेज अडैप्टर के ज़रिए टटोलती है, इसलिए वीडियो अलियास पर भी `tasks` फ़ील्ड में `["text_to_image"]` ही पढ़ने को मिलता है, और किसी `elevenlabs` कैंडिडेट के पास इमेज अडैप्टर है ही नहीं — वह कीमत के साथ लौटता है, मगर descriptor `null` रहता है। कई capability फ़ील्डों में ईमानदारी से `unknown` लिखा होता है — अडैप्टर हर एंडपॉइंट का स्कीमा जान ही नहीं सकते, और यह कह देना आत्मविश्वास से भरे गलत जवाब से बेहतर है। escape hatch ठीक इसीलिए मौजूद है।

---

## वह लागत जो अंदाज़ा लगाने से इनकार करती है

असल में यह रिलीज़ इसी फ़ीचर के बारे में है, और यही वह जगह है जहाँ हम सबसे ज़्यादा ज़िद्दी रहे।

`usage.cost` वह संख्या है जिसका बिल बना। `usage.cost_source` बताता है कि वह आई कहाँ से:

| `cost_source` | मतलब                                                                |
| ------------- | ------------------------------------------------------------------- |
| `provider`    | अपस्ट्रीम ने असली डॉलर लौटाए। OpenRouter और Replicate ऐसा करते हैं। |
| `estimate`    | octolib की reference rate table से लोकली निकाली गई।                 |
| `unavailable` | कोई इसकी कीमत लगा ही नहीं सका — `cost` `null` है।                   |

**`null` शून्य नहीं है।** जिस अनुरोध की कीमत कोई नहीं लगा सका, वह बिना कीमत के दर्ज होता है, मुफ़्त के रूप में कभी नहीं। वह `octohub_media_cost_unknown_total` में दिखता है और `cost_unavailable` चेतावनी साथ लाता है — बजाय इसके कि वह चुपचाप आपके कुल खर्च को नीचे खींचे और डैशबोर्ड को हकीकत से बेहतर दिखाए।

अनुमान octolib की reference table से आते हैं, जो इकाइयों को समझती है क्योंकि प्रोवाइडर भी समझते हैं: ElevenLabs अक्षरों का बिल बनाता है, Runway क्रेडिट से बदले गए वीडियो-सेकंड बेचता है, fal GPU wall-clock का सहारा लेता है क्योंकि उसकी queue मेट्रिक्स सिर्फ़ यही मात्रा बताती हैं। जहाँ rate एक अंदाज़ा भर होता, वहाँ कोई rate है ही नहीं — Replicate के कम्युनिटी मॉडल किसी अनजान GPU क्लास पर GPU-सेकंड का बिल बनाते हैं, इसलिए उनके लिए कोई rate निकलता ही नहीं और वे किसी भरोसेमंद दिखने वाली संख्या की मुहर लगवाने के बजाय बिना कीमत के रह जाते हैं।

एक ही ज्ञात कमी, खुलकर कही जा रही है: **ElevenLabs की ट्रांसक्रिप्शन बिना कीमत के है।** Scribe इनपुट-ऑडियो की अवधि का बिल बनाता है, जो वापस रिपोर्ट नहीं होती, और कोई reference rate उसे ढँकता नहीं। बाकी जगह ट्रांसक्रिप्शन को संख्या मिल ही जाती है — fal अपने प्रति-GPU-सेकंड वाले सर्वग्राही नियम पर आ जाता है, और Replicate तथा OpenRouter अपस्ट्रीम जो डॉलर बताता है उसी से कीमत लगाते हैं। जब हम Scribe की कीमत लगा सकेंगे, लगाएँगे; तब तक वह `unavailable` है, `0.00` नहीं।

एग्रीगेट की तरफ़, `GET /v1/admin/usage` को `media_count` और `total_cost` मिलते हैं — जो अब completions, embeddings _और_ मीडिया को जोड़कर वही एक संख्या बनाते हैं जिसे आप सचमुच किसी इनवॉइस पर लिखेंगे। `GET /v1/admin/media` अलग-अलग रिकॉर्ड सूचीबद्ध करता है, बाकी दोनों जैसे ही फ़िल्टरों के साथ, नए पहले, और चल रही jobs भी शामिल — गैर-टर्मिनल स्टेटस और `null` `completed_at` के साथ। job _अपना ही रिकॉर्ड है_, बस पहले वाले स्टेटस पर; जाँचने के लिए कोई अलग कतार है ही नहीं।

Prometheus, प्रति task, model और provider — और `metrics.per_key` चालू होने पर request काउंटर पर एक `api_key_id` लेबल भी। लागत वाले काउंटर कुंजी के हिसाब से बिना लेबल के रहते हैं; प्रति-टेनेंट खर्च `GET /v1/admin/usage` से आता है, जहाँ वह सैंपल किया हुआ नहीं, सटीक होता है:

```
octohub_media_requests_total{task,model,provider,status}
octohub_media_duration_seconds{task,model,provider}
octohub_media_cost_microusd_total{task,model,provider,source}
octohub_media_cost_unknown_total{task,model,provider}
```

लागतें माइक्रो-USD में गिनी जाती हैं, क्योंकि $0.003 वाली इमेज के लिए डॉलर का काउंटर राउंडिंग-एरर पैदा करने की मशीन है। बकाया jobs का कोई gauge जानबूझकर नहीं है: सही gauge को डेटाबेस की गैर-टर्मिनल rows गिननी पड़तीं, और इन-प्रोसेस काउंटर उसी पल गलत हो जाता जब किसी job को कोई दूसरी replica poll कर ले या वह रीस्टार्ट झेलकर बच जाए।

---

## जिन चीज़ों को हमने ना कहा

जो फ़ीचर यहाँ नहीं हैं, वे उतना ही भार उठाते हैं जितना वे जो हैं:

**OctoHub blob स्टोर नहीं बनता।** न कोई ऑब्जेक्ट स्टोर है, न CDN, न चलाने को कोई artifact lifecycle, और वह आपकी तरफ़ से कभी कोई artifact लाकर नहीं देता। जो प्रोवाइडर URL लौटाता है, वह URL के रूप में ही सहेजा जाता है — row में एक लिंक और मेटाडेटा रहता है, बस इतना ही, और ज़्यादातर ट्रैफ़िक यही है। जो प्रोवाइडर payload खुद लौटाता है, वही वह अपवाद है जिसके लिए आपको जगह का हिसाब रखना चाहिए: ElevenLabs की स्पीच हमेशा ऐसा करती है, और fal, Replicate या OpenRouter कभी-कभी। उन बाइट्स को base64 में बदलकर प्रतिक्रिया में भेजा जाता है, और वही base64 रिकॉर्ड के `result` कॉलम में सहेजा भी जाता है — क्योंकि बाद में आने वाला `GET /v1/media/{id}` अपस्ट्रीम को छुए बिना ठीक उसी row को दोबारा परोसता है। `media` टेबल में पड़ी एक MP3 अपने असल आकार से करीब 4/3 गुना जगह खाती है, इसलिए भारी टेक्स्ट-टू-स्पीच डिप्लॉयमेंट को ऐसी टेबल के लिए तैयार रहना चाहिए जो आधी बहीखाता है, आधी मीडिया लाइब्रेरी।

**क्लाइंट के दिए फ़ाइल पथ अस्वीकार होते हैं।** octolib का `MediaSource` `file`, `provider_file` और `object_storage` सपोर्ट करता है; यहाँ तीनों `400` लौटाते हैं। किसी _सर्वर_ को भेजे अनुरोध में पड़ा पथ दरअसल सर्वर के फ़ाइल सिस्टम को पढ़ने की माँग है — एक SSRF/LFI छेद, कोई फ़ीचर नहीं। Inline base64 का आकार किसी प्रोवाइडर तक कुछ भी पहुँचने से पहले `media.max_source_bytes` (डिफ़ॉल्ट 20 MiB) के हिसाब से जाँचा जाता है, और payload कभी डेटाबेस में नहीं उतरता: सहेजा गया अनुरोध आकार और बाइट गिनती रखता है, बाइट नहीं।

**अपस्ट्रीम क्रेडेंशियल सर्वर पर ही रहते हैं।** प्रोवाइडर कुंजियाँ सर्वर के एनवायरनमेंट से आती हैं (`FAL_API_KEY`, `ELEVENLABS_API_KEY`, वगैरह) और क्लाइंट से कभी स्वीकार नहीं की जातीं। `provider_options.<provider>.cost_estimate` सिरे से अस्वीकार है — कीमत सर्वर की तरफ़ तय होती है, और क्लाइंट को यह बताने का हक़ नहीं कि उस पर आपका कितना बकाया है।

0.8.0 में यह भी नहीं है: स्ट्रीमिंग TTS, और `multipart/form-data`। जब किसी को सचमुच इनकी ज़रूरत पड़ेगी, दोनों बाद में जोड़े जा सकते हैं — हालाँकि स्ट्रीमिंग को पहले लागत का अपना जवाब चाहिए होगा, क्योंकि octolib का स्ट्रीमिंग स्पीच पथ कोई usage रिपोर्ट करता ही नहीं।

---

## अपग्रेड

`media` टेबल बाकी टेबलों के साथ स्टार्टअप पर ही बन जाती है — SQLite, MySQL और PostgreSQL पर, बिना किसी माइग्रेशन कदम के। मौजूदा कॉन्फ़िग चलता रहता है: अगर आप कोई `[media_models]` नहीं जोड़ते, तो नए एंडपॉइंट के पास रूट करने को कुछ नहीं होता और बाकी प्रॉक्सी ठीक वैसे ही बर्ताव करती है जैसे 0.7 में करती थी।

```bash
# Linux x86_64, static musl build
curl -fsSL https://github.com/Muvon/octohub/releases/download/0.8.0/octohub-0.8.0-x86_64-unknown-linux-musl.tar.gz | tar xz
./octohub
```

बाइनरी छह targets के लिए आती हैं (Linux musl, macOS, Windows — x86_64 और ARM64), या स्रोत से `cargo build --release`।

रिलीज़ एक ही वजह से breaking चिह्नित है: `Storage` trait में मीडिया मेथड उग आए। यह सिर्फ़ तभी मायने रखता है जब आप OctoHub के internals के हिसाब से अपना storage बैकएंड बनाए रखते हैं; अगर आप बाइनरी चलाते हैं, तो बदलने को कुछ नहीं है। एक छोटी बात जानने लायक: 0.7.12 से OctoHub attribution हेडर शुरू से आख़िर तक फ़ॉरवर्ड करता है, और इस साइकल से वह अपस्ट्रीम पर खुद को octolib के सामान्य डिफ़ॉल्ट के बजाय `Octohub/<version>` बताता है — ताकि प्रोवाइडर की तरफ़ के डैशबोर्ड उस प्रॉक्सी का नाम लें जिसने कॉल की।

---

## असल बात

अपने मॉडलों के आगे प्रॉक्सी रखने की वजह कभी रूटिंग नहीं थी। वजह यह थी कि जिस एक जगह से हर अनुरोध गुज़रता है, वही अकेली जगह है जहाँ पूरा सच मौजूद होता है। जब अनुरोध टोकन के बजाय पिक्सेल और ऑडियो लौटाने लगते हैं तो यह दलील कमज़ोर नहीं पड़ती — और मज़बूत हो जाती है, क्योंकि मीडिया ही वह जगह है जहाँ प्रति-अनुरोध लागत राउंडिंग एरर होना छोड़कर असली बिल बन जाती है।

0.8.0 से "यह वीडियो किस ग्राहक ने बनाया, किस प्रोवाइडर पर, और उसकी कीमत क्या रही" का जवाब आपके अपने डेटाबेस की एक row है — chat completions के ठीक बगल में, उसी मुद्रा में, और एक ऐसे कॉलम के साथ जो किसी को पता न होने पर यह बात साफ़-साफ़ कह देता है।

— Don

_OctoHub Apache-2.0 के तहत ओपन सोर्स है, [Muvon Un Limited](https://muvon.io) द्वारा विकसित। इसे [GitHub](https://github.com/Muvon/octohub) पर लीजिए — issues और pull requests का स्वागत है। पूरा मीडिया डॉक्युमेंटेशन: [doc/11-media.md](https://github.com/Muvon/octohub/blob/master/doc/11-media.md)।_
