# Rust में एक एकीकृत LLM प्रोवाइडर लेयर बनाने से मिले सबक

> octolib को शिप करते हुए हमने जो सीखा, वह Rust लाइब्रेरी जो हमारे AI स्टैक के पीछे है: कई LLM प्रोवाइडरों को कैसे एब्स्ट्रैक्ट करें, tool calls और टोकन उपयोग को सामान्य करें, और उस दिन को कैसे झेलें जब किसी मॉडल का रीज़निंग फ़ॉर्मैट 400 लौटा दे।

बग रिपोर्ट में तीन शब्द थे: "Opus टूट गया है।" वह टूटा नहीं था। जो टूटा वह एक धारणा थी। हमारे पास कोड का एक रास्ता था जो हर उस Claude मॉडल पर `thinking.type: "enabled"` सेट कर देता था जो विस्तारित रीज़निंग सपोर्ट करता था। Anthropic ने Opus 4.7 शिप किया, जो ठीक उसी फ़ील्ड को `400` के साथ अस्वीकार कर देता है। यह केवल `thinking.type: "adaptive"` स्वीकार करता है। एक मॉडल, एक नाम बदला हुआ enum मान, और उसकी ओर रूट किया गया हर रिक्वेस्ट तार पर ही मर गया।

यही एक एकीकृत LLM प्रोवाइडर लेयर का पूरा काम है, एक ही घटना में संपीड़ित। आप कोई HTTP क्लाइंट नहीं लिख रहे। आप इस बात का एक जीवित मॉडल बनाए रख रहे हैं कि उन्नीस अलग-अलग वेंडर एक ही चार अवधारणाओं — मैसेज, टूल, टोकन और एरर — पर कैसे असहमत होते हैं, और उनके किए हर बदलाव को सोख रहे हैं ताकि आपके ऊपर का कोड कभी इसकी परवाह न करे।

यह [octolib](https://github.com/Muvon/octolib) की इंजीनियरिंग गहराई से पड़ताल है — वह ओपन-सोर्स Rust लाइब्रेरी जो [Octocode](/blog/octolib-the-engine-behind-our-ai-stack), Octomind और Octobrain के लिए यह सोखने का काम करती है। मैं उन सबकों से गुज़रूँगा जिनकी कीमत हमें सबसे ज़्यादा चुकानी पड़ी, हर एक कोड के एक असली टुकड़े से बंधा हुआ। अगर आपने [octolib का परिचय](/blog/octolib-the-engine-behind-our-ai-stack) या [एक AI एजेंट को कई मॉडलों पर चलाने](/blog/running-one-ai-agent-across-many-models) वाला सहयोगी लेख पढ़ा है, तो यह दोनों के नीचे की लेयर है।

---

## एब्स्ट्रैक्शन का आकार

कॉन्ट्रैक्ट एक trait है। उसके नीचे सब कुछ एक प्रोवाइडर है जो उसे लागू करता है।

```rust
#[async_trait::async_trait]
pub trait AiProvider: Send + Sync {
    fn name(&self) -> &str;
    fn supports_model(&self, model: &str) -> bool;
    async fn chat_completion(&self, params: ChatCompletionParams) -> Result<ProviderResponse>;
    fn get_api_key(&self) -> Result<String>;
    // डिफ़ॉल्ट के साथ क्षमता-जाँच: supports_caching, supports_vision,
    // supports_structured_output, supported_sampling_params, get_model_pricing...
}
```

कॉलर कभी सीधे कोई प्रोवाइडर नहीं बनाता। वह फ़ैक्टरी को एक `provider:model` स्ट्रिंग सौंपता है:

```rust
let (provider, model) = ProviderFactory::get_provider_for_model("anthropic:claude-opus-4-8")?;
let response = provider.chat_completion(params).await?;
```

`ProviderFactory::parse_model` पहले कोलन पर विभाजित करता है — प्रोवाइडर प्रीफ़िक्स डिज़ाइन से **अनिवार्य** है। "डिफ़ॉल्ट प्रोवाइडर" का कोई अनुमान नहीं। `anthropic:claude-opus-4-8`, `deepseek:deepseek-chat`, `openrouter:anthropic/claude-opus-4.8`, `cli:claude/...` — प्रीफ़िक्स रूटिंग की कुंजी है और मॉडल का नाम उसके बाद का सब कुछ है। इक्कीस प्रोवाइडर एक ही `match` में रजिस्टर होते हैं:

```rust
match provider_name.to_lowercase().as_str() {
    "openai"    => Ok(Box::new(OpenAiProvider::new())),
    "anthropic" => Ok(Box::new(AnthropicProvider::new())),
    "deepseek"  => Ok(Box::new(DeepSeekProvider::new())),
    "moonshot" | "kimi" => Ok(Box::new(MoonshotProvider::new())),
    "octohub"   => Ok(Box::new(OctoHubProvider::new())),
    // ... cerebras, groq, together, nvidia, cloudflare, fireworks,
    //     featherless, minimax, zai, byteplus, amazon, google,
    //     ollama, local, openrouter
    _ => Err(anyhow::anyhow!("Unsupported provider: {}", provider_name)),
}
```

trait के डिफ़ॉल्ट मेथड इसका अनसुना आधा हिस्सा हैं। जो प्रोवाइडर कुछ ख़ास नहीं करता, उसे समझदार व्यवहार विरासत में मिलता है: `supports_caching` `false` लौटाता है, `supported_sampling_params` `SamplingSupport::ALL` लौटाता है, प्राइसिंग एक रेफ़रेंस टेबल पर गिरती है। जो प्रोवाइडर _वाकई_ ख़ास है वह ठीक उन्हीं मेथड्स को ओवरराइड करता है जहाँ वह अलग होता है और बाकी कुछ नहीं। यही पूरा डिज़ाइन दर्शन है — आम स्थिति को मुफ़्त बनाओ और अजीब स्थिति को स्थानीय।

**सबक शून्य, जिस पर बाकी सब टिका है:** एब्स्ट्रैक्शन "एक LLM API" नहीं है। यह क्षमता-संबंधी सवालों का वह समूह है जो आपको रिक्वेस्ट बनाने से पहले पूछने होंगे। हमने वह सूची पहले ही दिन सही नहीं की। हर बार जब कोई प्रोवाइडर हमें चौंकाता, हम उसे बढ़ाते गए, और trait वह घाव का निशान है।

---

## सबक 1: Tool calls तीन असंगत रूपों में एक ही विचार हैं

अगर आप एक चीज़ ध्यान से एब्स्ट्रैक्ट करें, तो वह tool calling हो। यहीं प्रोवाइडर सबसे ज़्यादा अलग होते हैं और जहाँ एक चुपचाप का बेमेल सबसे ख़राब फ़ेलियर मोड पैदा करता है: मॉडल ने टूल कॉल करने को कहा, आपके कोड ने ध्यान नहीं दिया, एजेंट अटक गया।

दो वेंडर, एक ही फ़ीचर, अलग वायर फ़ॉर्मैट:

- **Anthropic** एक `tool_use` कंटेंट ब्लॉक लौटाता है। आर्गुमेंट एक `input` फ़ील्ड में रहते हैं जो **पहले से ही एक JSON ऑब्जेक्ट है**।
- **OpenAI और हर OpenAI-आकार वाला** एक `tool_calls` ऐरे लौटाता है जहाँ `function.arguments` एक **JSON स्ट्रिंग है जिसे आपको ख़ुद पार्स करना है** — और जो, जब कोई मॉडल छोटा कर देता है या हैलुसिनेट करता है, हमेशा वैध JSON नहीं होती।

octolib इस अंतर को छुपाने के बजाय स्पष्ट रूप से मॉडल करता है। आंतरिक टाइप एक टैग किया हुआ enum है:

```rust
#[serde(tag = "provider")]
pub enum ProviderToolCalls {
    Anthropic  { content: Vec<AnthropicToolUse> },   // input: serde_json::Value
    OpenAI     { tool_calls: Vec<OpenAIToolCall> },   // function.arguments: String
    OpenRouter { tool_calls: Vec<OpenAIToolCall> },
    DeepSeek   { tool_calls: Vec<OpenAIToolCall> },
    Generic    { calls: Vec<GenericToolCall> },
}
```

`extract_from_exchange` प्रोवाइडर के नाम के आधार पर डिस्पैच करता है, और `to_tool_calls()` सभी पाँच वैरिएंट्स को कॉलर के लिए एक सपाट `Vec<ToolCall>` में समेट देता है। OpenAI की शाखा वहीं है जहाँ सबक रहता है:

```rust
let arguments: serde_json::Value = if call.function.arguments.is_empty() {
    serde_json::Value::Object(serde_json::Map::new())
} else {
    serde_json::from_str(&call.function.arguments)
        .map_err(ToolCallError::InvalidArguments)?
};
```

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

एक दूसरी, बारीक जाल भी है: **राउंड-ट्रिपिंग**। जब आप अगले टर्न के लिए बातचीत वापस भेजते हैं, असिस्टेंट के पिछले tool calls को _उस प्रोवाइडर_ के आकार में फिर से एनकोड करना पड़ता है। इसलिए octolib tool calls को हिस्ट्री में एक कैनॉनिकल `GenericToolCall` रूप में रखता है, और हर प्रोवाइडर का `convert_messages` बाहर निकलते समय उसके नेटिव ब्लॉक फिर से बनाता है — Anthropic ऑब्जेक्ट को बरकरार रखते हुए `tool_use` ब्लॉक फिर से बनाता है; OpenAI-संगत रास्ता आर्गुमेंट को _वापस_ एक स्ट्रिंग में सीरियलाइज़ करता है। सहनशील fallback यहाँ भी मायने रखता है:

```rust
serde_json::from_str(&call.function.arguments).unwrap_or_else(|_| {
    serde_json::json!({"raw_arguments": call.function.arguments})
})
```

टूटे हुए tool call को खोने के बजाय, हम कच्ची स्ट्रिंग को `raw_arguments` कुंजी के नीचे संरक्षित रखते हैं। मॉडल कभी-कभी अपना ख़ुद का टूटा हुआ आउटपुट देखकर उबर सकता है। वह उस कॉल से कभी नहीं उबर सकता जो चुपचाप ग़ायब हो गई।

---

## सबक 2: टोकन उपयोग वह जगह है जहाँ पैसा चुपचाप रिसता है

हर प्रोवाइडर उपयोग की रिपोर्ट करता है। कोई दो एक जैसी रिपोर्ट नहीं करते। अगर आपको प्रति-रिक्वेस्ट लागत चाहिए — और एक दर्जन प्रोवाइडरों पर हज़ारों रिक्वेस्ट पर, चाहिए — तो आपको एक सचमुच गंदे इनपुट को एक ही struct में सामान्य करना पड़ता है:

```rust
pub struct TokenUsage {
    pub input_tokens: u64,        // साफ़ — कभी कैश टोकन शामिल नहीं करता
    pub cache_read_tokens: u64,
    pub cache_write_tokens: u64,
    pub output_tokens: u64,
    pub reasoning_tokens: u64,
    pub total_tokens: u64,
    pub cost: Option<f64>,
    pub request_time_ms: Option<u64>,
}
```

OpenAI-संगत प्रोवाइडरों के लिए डीसीरियलाइज़ेशन struct असली कहानी बताता है। एक ही `usage` ऑब्जेक्ट को एक साथ हर बोली बर्दाश्त करनी पड़ती है:

```rust
struct OpenAiCompatUsage {
    input_tokens: Option<u64>,       // कुछ प्रोवाइडर
    prompt_tokens: Option<u64>,      // क्लासिक OpenAI
    completion_tokens: Option<u64>,
    output_tokens: Option<u64>,      // दूसरे
    total_tokens: Option<u64>,
    reasoning_tokens: Option<u64>,
    completion_tokens_details: Option<CompletionTokensDetails>,  // reasoning यहाँ...
    prompt_tokens_details: Option<PromptTokensDetails>,          // cached_tokens यहाँ
    total_cost: Option<f64>, cost: Option<f64>,                  // या यहाँ
    prompt_cost: Option<f64>, completion_cost: Option<f64>,      // ...या इनमें बँटा हुआ
}
```

निष्कर्षण `.or()` fallback की एक श्रृंखला है: input है `input_tokens` **या** `prompt_tokens` **या** Ollama का `prompt_eval_count` **या** शून्य। रीज़निंग टोकन एक टॉप-लेवल फ़ील्ड में छिपते हैं _या_ `completion_tokens_details` के नीचे नेस्टेड। लागत है `total_cost` _या_ `cost` _या_ `prompt_cost + completion_cost`, और केवल तभी जब प्रोवाइडर कॉन्फ़िग upstream लागत पर भरोसा करने का चुनाव करे। जब API कोई लागत फ़ील्ड नहीं लौटाता, octolib उसे प्रति-मॉडल प्राइसिंग टेबल से गणना करता है।

दो चीज़ों ने यहाँ ख़ासतौर पर हमें काटा, दोनों अब कोड में टिप्पणियों में दर्ज हैं क्योंकि हम उन्हें कभी दोबारा नहीं सीखना चाहते:

- **`input_tokens` को "साफ़" होना चाहिए।** कुछ API कैश किए टोकन को इनपुट गिनती में मिला देते हैं; कुछ नहीं। हम `cache_read_tokens` ख़ुद घटाते हैं ताकि फ़ील्ड का हमेशा एक ही मतलब रहे, वरना लागत गणित सस्ते कैश टोकन को महँगी दर पर दोबारा गिन लेती है।
- **Anthropic का कैश TTL विभाजन।** Anthropic 5-मिनट कैश राइट को इनपुट के 1.25× पर और 1-घंटे राइट को 2× पर बिल करता है। यह `cache_creation` में प्रति-TTL ब्रेकडाउन लौटाता है — _लेकिन केवल कभी-कभी_। जब वह नेस्टेड ऑब्जेक्ट अनुपस्थित हो, सुरक्षित धारणा 5m है, 1h नहीं। 1h का अनुमान लगाएँ और आप सबसे आम उपयोग पर अपने ख़ुद के लागत हिसाब को ~60% ओवरचार्ज कर लेते हैं। यह एक-लाइन का डिफ़ॉल्ट है जिसके ऊपर चार-लाइन की टिप्पणी समझाती है कि क्यों।

उबाऊ सबक: लागत ट्रैकिंग ऐसी फ़ीचर नहीं जिसे आप ऊपर से जोड़ देते हैं। यह अंकगणित के भेस में एक सामान्यीकरण समस्या है, और अंकगणित आसान हिस्सा है।

---

## सबक 3: हम स्ट्रीम नहीं करते, और यह सही फ़ैसला था

यह सहज-विरोधी है, इसलिए मुझे सटीक होने दें। जो प्रोवाइडर `stream` फ़्लैग लेते हैं, उन पर octolib `stream: false` भेजता है। DeepSeek एडॉप्टर में टिप्पणी सीधी है: `// We don't support streaming in octolib yet`। हम लौटाने से पहले पूरे रिस्पॉन्स को बफ़र करते हैं।

जिस लाइब्रेरी का काम एजेंट लूप और स्ट्रक्चर्ड आउटपुट है, उसके लिए यह एक फ़ीचर है, कमी नहीं। कारण साझा HTTP हेल्पर में है:

```rust
pub(super) async fn send_and_read(
    request: reqwest::RequestBuilder,
    timeout: Option<Duration>,
) -> anyhow::Result<CapturedResponse> {
    let response = apply_request_timeout(request, timeout).send().await?;
    let status = response.status();
    let headers = response.headers().clone();
    let body = response.text().await?;   // बॉडी रिट्राई की गई इकाई के अंदर पढ़ी जाती है
    Ok(CapturedResponse { status, headers, body })
}
```

`send()` और पूरी बॉडी पढ़ाई दोनों उसी एक awaited इकाई के अंदर होते हैं जिसे रिट्राई लूप लपेटता है। यह मायने रखता है क्योंकि सबसे गंदा ट्रांसपोर्ट फ़ेलियर एक मिड-स्ट्रीम RST है — कनेक्शन हेडर आने के _बाद_ मरता है लेकिन जब बॉडी अभी _आ ही रही है_। पेलोड का निरीक्षण करता फ़ायरवॉल, ओवरलोडेड लोड बैलेंसर, लंबे जेनरेशन पर NAT टाइमआउट। अगर आप स्ट्रीम करते हैं, वह फ़ेलियर आपके टोकन हैंडलर में गहराई में, आधा रिस्पॉन्स पहले ही उत्सर्जित, रिट्राई के साफ़ तरीके के बिना उभरता है। चूँकि हम बॉडी को रिट्राई की गई इकाई के अंदर पढ़ते हैं, `is_connection_error` फ़ेलियर को वर्गीकृत करता है (`is_connect() || is_request() || is_body()`), लूप HTTP क्लाइंट को रिफ़्रेश करता है, और _पूरी रिक्वेस्ट_ साफ़ तौर पर रिट्राई होती है।

ट्रेडऑफ़ ईमानदार है: आपको फ़र्स्ट-टोकन लेटेंसी नहीं मिलती, और आप आउटपुट को क्रमशः रेंडर नहीं कर सकते। चैट UI के लिए, स्ट्रीम करें। ऐसे एजेंट के लिए जो वैसे भी कुछ करने से पहले पूरे रिस्पॉन्स को tool calls या JSON स्कीमा में पार्स करेगा, बफ़रिंग सरल है, अस्थिर नेटवर्क पर ज़्यादा सही है, और आंशिक-स्थिति बग्स का एक पूरा वर्ग हटा देती है। हमने सहीपन चुना। स्ट्रीमिंग सूची में है; यह कभी ज़रूरी नहीं रही।

---

## सबक 4: रिट्राई आसान हैं। यह जानना कि क्या रिट्राई-योग्य है, नहीं।

बैकऑफ़ ख़ुद कुछ ख़ास नहीं — `base_timeout * 2^attempt`, पाँच मिनट पर सीमित, `watch` टोकन से रद्द-योग्य:

```rust
pub fn is_retryable_status(status: u16) -> bool {
    status == 429 || status >= 500
}
```

`429` और `5xx` रिट्राई करें। कभी `4xx` रिट्राई न करें — ख़राब रिक्वेस्ट ख़राब ही रहती है। वह हिस्सा बारह लाइनों का है।

मुश्किल हिस्सा कनेक्शन-एरर का रास्ता है। पूल का एक HTTP कनेक्शन जिसे upstream ने चुपचाप आधा-बंद कर दिया, एक `reqwest` एरर पैदा करता है जो `is_request() = true, is_connect() = false` है। अगर आप केवल `is_connect()` जाँचते हैं, आप पूल कभी रिफ़्रेश नहीं करते, और _हर रिट्राई उसी मरे हुए सॉकेट का दोबारा इस्तेमाल करता है_ — तो आपके सारे रिट्राई एक जैसे फ़ेल होते हैं और आप निष्कर्ष निकालते हैं कि प्रोवाइडर डाउन है जबकि वह नहीं है। हमने यह CN-होस्टेड एंडपॉइंट (DeepSeek, Moonshot) से सीखा जो निष्क्रिय keep-alive कनेक्शन को आक्रामक रूप से बंद कर देते हैं। उपाय है `is_connect()`, `is_request()` और `is_body()` सभी को कनेक्शन एरर मानना, और अगले प्रयास से पहले एक ताज़ा क्लाइंट परमाणुक रूप से बदल देना:

```rust
if on_connection_error(&e) {
    crate::llm::providers::shared::refresh_http_client();  // नया पूल, कोई बासी सॉकेट नहीं
}
```

क्लाइंट ख़ुद ठीक इसी के लिए ट्यून किया गया है: 20s कनेक्ट टाइमआउट (हैंडशेक को सीमित करता है, _जेनरेशन को नहीं_, जो मिनटों ले सकता है), TCP keepalive, HTTP/2 PING फ़्रेम, और एक 30s पूल आइडल टाइमआउट ताकि कनेक्शन को upstream edge द्वारा मारे जाने से पहले निकाल दे। इसमें से कुछ भी ग्लैमरस नहीं। यह सब उस लाइब्रेरी और एक ऐसी लाइब्रेरी के बीच का फ़र्क है जो ख़राब नेटवर्क की दोपहर झेल जाती है और एक जो आपको अलर्ट से जगाती है।

---

## सबक 5: प्रति-मॉडल विचित्रताएँ डेटा में रहती हैं, हर जगह बिखरे `if` में नहीं

शुरुआती घटना पर वापस। उपाय कोई हैक नहीं था — यह स्वीकारना था कि "यह मॉडल कौन से सैंपलिंग और रीज़निंग नॉब स्वीकार करता है" _मॉडलों के बारे में डेटा_ है, और उसे वहाँ रखना जहाँ डेटा जाता है। Anthropic एडॉप्टर में, वह मुट्ठीभर `const` स्लाइस हैं:

```rust
const NO_SAMPLING_MODELS: &[&str]     = &["fable-5", "mythos-5", "opus-4-8", "opus-4-7"];
const ADAPTIVE_ONLY_MODELS: &[&str]   = &["fable-5", "mythos-5", "opus-4-7"];   // मैनुअल thinking → 400
const NO_TOP_P_MODELS: &[&str]        = &["opus-4-1", "opus-4-7", "sonnet-4-5", /* ... */];
const EFFORT_PARAM_MODELS: &[&str]    = &["fable-5", "mythos-5", "opus-4-7", "opus-4-6", /* ... */];
```

जो सामान्य तंत्र इसे संयोजनीय बनाता है वह `SamplingSupport` है — एक छोटा बूलियन मास्क जिसे प्रोवाइडर प्रति मॉडल लौटाता है, घोषित करते हुए कि `temperature`, `top_p`, `top_k` में से कौन-कौन मॉडल स्वीकार भी करेगा:

```rust
pub struct SamplingSupport { pub temperature: bool, pub top_p: bool, pub top_k: bool }
// SamplingSupport::ALL, ::NONE, ::TEMPERATURE_ONLY, ::TEMPERATURE_AND_TOP_P
```

trait उपयोगकर्ता द्वारा अनुरोधित मानों को इस मास्क के साथ `effective_sampling_params` में मिला देता है। जो मॉडल `top_p` अस्वीकार करता है उसके रिक्वेस्ट बॉडी में बस कभी `top_p` होता ही नहीं — कहीं कोई कॉलर `if model == ...` नहीं लिखता। एक संबंधित बारीकी है जो हमें केवल 400 पाकर मिली: कुछ Anthropic मॉडल रीज़निंग चालू होते ही _किसी भी_ ग़ैर-डिफ़ॉल्ट सैंपलिंग मान को अस्वीकार करते हैं, इसलिए एडॉप्टर पहले से `thinking_enabled` तय करता है और तदनुसार सैंपलिंग को क्लैंप करता है, रिक्वेस्ट बनाने से पहले। सबक Anthropic से परे सामान्यीकृत होता है: जिस पल आपके पास एक ही अवधारणा के लिए दो `if model.contains(...)` जाँच हों, आपके पास एक डेटा टेबल जन्म लेने की कोशिश कर रही है। उसे जन्म लेने दें।

---

## OctoHub कहाँ फ़िट होता है: लेयर के ऊपर का प्रॉक्सी

स्वाभाविक सवाल एक बार जब आपके पास एकीकृत क्लाइंट हो: क्या होगा अगर एकीकरण नेटवर्क पर रहे, ताकि ग़ैर-Rust क्लाइंट भी उसे पाएँ? वह है [OctoHub](https://github.com/Muvon/octohub) — एक उच्च-प्रदर्शन LLM प्रॉक्सी सर्वर जो सचमुच octolib है, एक HTTP चेहरे और एक डेटाबेस के साथ।

OctoHub का प्रॉक्सी इंजन किसी भी प्रोवाइडर को फिर से लागू नहीं करता। यह octolib पर निर्भर है और उसी फ़ैक्टरी को कॉल करता है:

```rust
let (provider_name, model) = self.config.resolve_model(&req.model)?;
let provider = ProviderFactory::create_provider(&provider_name)?;
let response = provider.chat_completion(params).await?;
```

OctoHub जो जोड़ता है वह उस कॉल के इर्द-गिर्द का ऑपरेशनल लेयर है: प्रति-टेनेंट API कुंजियाँ, SQLite/MySQL/Postgres में पूरा रिक्वेस्ट/रिस्पॉन्स लॉगिंग, उपयोग एनालिटिक्स, मॉडल-नाम मैपिंग (एक छोटा अलियास `provider:model` स्ट्रिंग की एक सूची में हल होता है, मोटे लोड बैलेंसिंग के लिए एक यादृच्छिक रूप से चुनी जाती है), और upstream टाइमआउट के साथ प्रति-प्रोवाइडर कन्करेंसी सीमाएँ। यह Responses-API आकार (`/v1/completions`, मल्टी-टर्न के लिए `previous_completion_id`) और उन क्लाइंट के लिए एक OpenAI-संगत `/v1/chat/completions` बोलता है जो पहले से वह बोलते हैं।

और फिर यह लूप बंद करता है: octolib _एक `octohub:` प्रोवाइडर भी शिप करता है_। तो octolib का उपयोग करने वाला कोई Rust ऐप एक OctoHub इंस्टेंस के ज़रिए रूट कर सकता है — जो octolib के ज़रिए असली upstream तक वापस बाहर रूट करता है — केंद्रीकृत कुंजियाँ, लॉगिंग और लागत ट्रैकिंग मुफ़्त में पाते हुए, हर जगह उसी `provider:model` स्ट्रिंग के साथ। `octohub` प्रोवाइडर हर क्षमता को समर्थित घोषित करता है (`supports_caching`, `supports_vision`, `supports_structured_output` सभी `true`) ठीक इसलिए क्योंकि _वह जान ही नहीं सकता_ कि प्रॉक्सी के पीछे क्या है; असली upstream एक स्पष्ट एरर लौटाता है अगर अंतर्निहित मॉडल किसी रिक्वेस्ट को पूरा नहीं कर सकता। प्रॉक्सी अपनी ही अनभिज्ञता के बारे में जानबूझकर ईमानदार है।

यही trait सीमा को सही रखने का प्रतिफल है। एक ही एब्स्ट्रैक्शन एक इन-प्रोसेस लाइब्रेरी कॉल और एक नेटवर्क हॉप दोनों की सेवा करता है, और वे बिना यह जाने कि कौन-सी ओर बात हो रही है, संयोजित हो जाते हैं।

---

## जो मैं आज इसे शुरू करने वाले से कहूँगा

| सबक                                              | ठोस बात                                                                                                    |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| क्षमताओं को एब्स्ट्रैक्ट करें, एंडपॉइंट को नहीं  | trait सवालों की सूची है (`supports_caching`, `supported_sampling_params`), केवल `chat_completion` नहीं     |
| tool-call अंतर को स्पष्ट रूप से मॉडल करें        | प्रति प्रोवाइडर आकार एक टैग किया enum एक लॉसी "यूनिवर्सल" struct से बेहतर है                               |
| लागत पर भरोसा करने से पहले उपयोग को सामान्य करें | `prompt_tokens`/`input_tokens`/`prompt_eval_count` में `.or()` श्रृंखलाएँ; `input_tokens` को कैश-साफ़ रखें |
| अगर वैसे भी पार्स कर रहे हैं तो बफ़र करें        | बॉडी को रिट्राई की गई इकाई के अंदर पढ़ना मिड-स्ट्रीम RST को रिट्राई-योग्य बनाता है                         |
| कनेक्शन एरर पर पूल रिफ़्रेश करें                 | `is_request()` और `is_body()` भी कनेक्शन एरर हैं, केवल `is_connect()` नहीं                                 |
| विचित्रताएँ डेटा हैं                             | `const` मॉडल स्लाइस और एक `SamplingSupport` मास्क, कभी बिखरे `if model.contains` नहीं                      |

इनमें से कुछ भी नया नहीं है। यह सब उस तरह की चीज़ है जो आप तभी लिखते हैं जब वह आपको काट चुकी हो, इसीलिए इनमें से अधिकांश सबक असली कोड में एक-लाइन के डिफ़ॉल्ट के ऊपर चार-लाइन की टिप्पणियों के रूप में रहते हैं। एकीकृत LLM प्रोवाइडर लेयर का सबसे मुश्किल हिस्सा एब्स्ट्रैक्शन नहीं है। यह उसके नीचे की चीज़ें सचमुच कितनी अलग हैं इस बारे में ईमानदार रहना है — और एक असली अंतर को एक उम्मीद-भरे डिफ़ॉल्ट से ढाँपने से इनकार करना है।

## अक्सर पूछे जाने वाले सवाल

**क्या octolib एक Rust LLM लाइब्रेरी है जिसे मैं स्वतंत्र रूप से इस्तेमाल कर सकता हूँ?**

हाँ। यह Apache-2.0 के तहत ओपन-सोर्स है, `cargo check` से कंपाइल होती है, और उदाहरण केवल एक API कुंजी से चलते हैं। `provider:model` स्ट्रिंग ही पूरा इंटरफ़ेस है। यह LLM इनफ़रेंस, एम्बेडिंग और रीरैंकिंग करती है — और कुछ नहीं, न एजेंट न चेन।

**एकीकृत लेयर कौन से प्रोवाइडर सपोर्ट करता है?**

फ़ैक्टरी में इक्कीस LLM प्रोवाइडर रजिस्टर होते हैं, जिनमें OpenAI, Anthropic, Google, DeepSeek, Moonshot, MiniMax, Z.ai, Groq, Cerebras, Together, NVIDIA, Cloudflare, Fireworks, Featherless, BytePlus, Amazon Bedrock, OpenRouter, OctoHub, Ollama, लोकल एंडपॉइंट, और एक `cli:` बैकएंड शामिल हैं। नेटिव प्रोवाइडर को विशेष एडॉप्टर मिलते हैं; बाकी एक OpenAI-संगत रास्ता साझा करते हैं।

**क्या यह tool calls स्ट्रीम करता है?**

आज नहीं — octolib पूरे रिस्पॉन्स को बफ़र करता है (`stream: false`) ताकि मिड-स्ट्रीम कनेक्शन रीसेट साफ़ तौर पर रिट्राई-योग्य बनें, जो एजेंट लूप और स्ट्रक्चर्ड आउटपुट के लिए फ़र्स्ट-टोकन लेटेंसी से ज़्यादा मायने रखता है। स्ट्रीमिंग रोडमैप पर है।

**OctoHub octolib से कैसे अलग है?**

octolib इन-प्रोसेस Rust लाइब्रेरी है। OctoHub उसके ऊपर बना एक प्रॉक्सी _सर्वर_ है — वही प्रोवाइडर रूटिंग, साथ में मल्टी-टेनेंट कुंजियाँ, रिक्वेस्ट लॉगिंग, उपयोग एनालिटिक्स, और लोड बैलेंसिंग। octolib अपने `octohub:` प्रोवाइडर के ज़रिए OctoHub को कॉल तक कर सकता है।

— Don

---

_[octolib](https://github.com/Muvon/octolib) और [OctoHub](https://github.com/Muvon/octohub) Apache-2.0 के तहत ओपन-सोर्स हैं। अगर आपको चाहिए कोई प्रोवाइडर ग़ायब है, टेम्पलेट `src/llm/providers/openai.rs` में है — [एक issue या PR खोलें](https://github.com/Muvon/octolib/issues)._
