बग रिपोर्ट में तीन शब्द थे: "Opus टूट गया है।" वह टूटा नहीं था। जो टूटा वह एक धारणा थी। हमारे पास कोड का एक रास्ता था जो हर उस Claude मॉडल पर thinking.type: "enabled" सेट कर देता था जो विस्तारित रीज़निंग सपोर्ट करता था। Anthropic ने Opus 4.7 शिप किया, जो ठीक उसी फ़ील्ड को 400 के साथ अस्वीकार कर देता है। यह केवल thinking.type: "adaptive" स्वीकार करता है। एक मॉडल, एक नाम बदला हुआ enum मान, और उसकी ओर रूट किया गया हर रिक्वेस्ट तार पर ही मर गया।
यही एक एकीकृत LLM प्रोवाइडर लेयर का पूरा काम है, एक ही घटना में संपीड़ित। आप कोई HTTP क्लाइंट नहीं लिख रहे। आप इस बात का एक जीवित मॉडल बनाए रख रहे हैं कि उन्नीस अलग-अलग वेंडर एक ही चार अवधारणाओं — मैसेज, टूल, टोकन और एरर — पर कैसे असहमत होते हैं, और उनके किए हर बदलाव को सोख रहे हैं ताकि आपके ऊपर का कोड कभी इसकी परवाह न करे।
यह octolib की इंजीनियरिंग गहराई से पड़ताल है — वह ओपन-सोर्स Rust लाइब्रेरी जो Octocode, Octomind और Octobrain के लिए यह सोखने का काम करती है। मैं उन सबकों से गुज़रूँगा जिनकी कीमत हमें सबसे ज़्यादा चुकानी पड़ी, हर एक कोड के एक असली टुकड़े से बंधा हुआ। अगर आपने octolib का परिचय या एक AI एजेंट को कई मॉडलों पर चलाने वाला सहयोगी लेख पढ़ा है, तो यह दोनों के नीचे की लेयर है।
एब्स्ट्रैक्शन का आकार
कॉन्ट्रैक्ट एक trait है। उसके नीचे सब कुछ एक प्रोवाइडर है जो उसे लागू करता है।
#[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 स्ट्रिंग सौंपता है:
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 में रजिस्टर होते हैं:
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 है:
#[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 की शाखा वहीं है जहाँ सबक रहता है:
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 यहाँ भी मायने रखता है:
serde_json::from_str(&call.function.arguments).unwrap_or_else(|_| {
serde_json::json!({"raw_arguments": call.function.arguments})
})
टूटे हुए tool call को खोने के बजाय, हम कच्ची स्ट्रिंग को raw_arguments कुंजी के नीचे संरक्षित रखते हैं। मॉडल कभी-कभी अपना ख़ुद का टूटा हुआ आउटपुट देखकर उबर सकता है। वह उस कॉल से कभी नहीं उबर सकता जो चुपचाप ग़ायब हो गई।
सबक 2: टोकन उपयोग वह जगह है जहाँ पैसा चुपचाप रिसता है
हर प्रोवाइडर उपयोग की रिपोर्ट करता है। कोई दो एक जैसी रिपोर्ट नहीं करते। अगर आपको प्रति-रिक्वेस्ट लागत चाहिए — और एक दर्जन प्रोवाइडरों पर हज़ारों रिक्वेस्ट पर, चाहिए — तो आपको एक सचमुच गंदे इनपुट को एक ही struct में सामान्य करना पड़ता है:
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 ऑब्जेक्ट को एक साथ हर बोली बर्दाश्त करनी पड़ती है:
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 हेल्पर में है:
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 टोकन से रद्द-योग्य:
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() सभी को कनेक्शन एरर मानना, और अगले प्रयास से पहले एक ताज़ा क्लाइंट परमाणुक रूप से बदल देना:
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 स्लाइस हैं:
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 में से कौन-कौन मॉडल स्वीकार भी करेगा:
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 — एक उच्च-प्रदर्शन LLM प्रॉक्सी सर्वर जो सचमुच octolib है, एक HTTP चेहरे और एक डेटाबेस के साथ।
OctoHub का प्रॉक्सी इंजन किसी भी प्रोवाइडर को फिर से लागू नहीं करता। यह octolib पर निर्भर है और उसी फ़ैक्टरी को कॉल करता है:
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 और OctoHub Apache-2.0 के तहत ओपन-सोर्स हैं। अगर आपको चाहिए कोई प्रोवाइडर ग़ायब है, टेम्पलेट src/llm/providers/openai.rs में है — एक issue या PR खोलें.



