Metadata-Version: 2.1
Name: model-mesh-ai
Version: 1.0.1
Summary: Model Mesh: Unified AI Gateway SDK with Standardized Requests, Load Balancing, and Rate Limiting
Author: Model Mesh Team
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# Model Mesh (model-mesh-ai)

[![PyPI version](https://img.shields.io/pypi/v/model-mesh-ai.svg)](https://pypi.org/project/model-mesh-ai/)
[![Python versions](https://img.shields.io/pypi/pyversions/model-mesh-ai.svg)](https://pypi.org/project/model-mesh-ai/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

> **Model Mesh** is a unified AI Gateway SDK. It lets you send prompts to **Google Gemini, OpenAI, Anthropic Claude, DeepSeek, and Groq** using **standardized formats**, automatic **sliding-window rate limiting**, and **intelligent load balancing**.

---

## ⚡ Quick Installation

Install from PyPI on Google Colab, Anaconda, VS Code, Linux, Mac, or Windows:

```bash
pip install model-mesh-ai
```

Then in Python:
```python
import model_mesh
```

---

## 📖 Complete Method Reference with Request & Response Examples

Every method is explained in simple English below, showing **What you Send (The Request)** and **What you Get Back (The Response)**.

---

### Module 1: Format Standardization (`Standardizer`)

Use the `Standardizer` so you never have to worry about different vendors using different JSON formats.

---

#### 1. `Standardizer.standardize(prompt, model=...)` 🌟 (Universal Method)
* **What it does:** You give it a normal text prompt and a model name. It automatically converts it into the exact JSON format required by that AI company.
* **The Request (What you run):**
  ```python
  from model_mesh import Standardizer

  # Example A: For Google Gemini
  req_gemini = Standardizer.standardize("Explain gravity in 1 line", model="gemini-3.8-flash")
  
  # Example B: For OpenAI / DeepSeek / Groq
  req_openai = Standardizer.standardize("Explain gravity in 1 line", model="gpt-4o")

  # Example C: For Anthropic Claude
  req_claude = Standardizer.standardize("Explain gravity in 1 line", model="claude-3-5-sonnet")
  ```
* **The Response (What comes out):**
  * *From Gemini:*
    ```json
    {
      "contents": [
        {
          "role": "user",
          "parts": [{"text": "Explain gravity in 1 line"}]
        }
      ]
    }
    ```
  * *From OpenAI:*
    ```json
    {
      "model": "gpt-4o",
      "messages": [
        {"role": "user", "content": "Explain gravity in 1 line"}
      ],
      "stream": false
    }
    ```
  * *From Claude:*
    ```json
    {
      "model": "claude-3-5-sonnet",
      "messages": [
        {"role": "user", "content": "Explain gravity in 1 line"}
      ],
      "max_tokens": 4096
    }
    ```

---

#### 2. `Standardizer.normalize_messages(messages)`
* **What it does:** Takes messy inputs (like a plain string or tuples) and cleans them into standard `[{"role": "user", "content": "..."}]` dictionaries.
* **The Request (What you run):**
  ```python
  from model_mesh import Standardizer

  clean_msgs = Standardizer.normalize_messages("Hello world!")
  ```
* **The Response (What comes out):**
  ```python
  [
    {"role": "user", "content": "Hello world!"}
  ]
  ```

---

#### 3. `Standardizer.to_gemini(messages, model=..., temperature=...)`
* **What it does:** Specifically creates Google Gemini's REST API payload with `contents` and `parts`.
* **The Request (What you run):**
  ```python
  from model_mesh import Standardizer

  gemini_payload = Standardizer.to_gemini(
      "Translate 'thank you' to French",
      model="gemini-3.8-flash",
      temperature=0.2
  )
  ```
* **The Response (What comes out):**
  ```json
  {
    "contents": [
      {
        "role": "user",
        "parts": [{"text": "Translate 'thank you' to French"}]
      }
    ],
    "generationConfig": {
      "temperature": 0.2
    }
  }
  ```

---

#### 4. `Standardizer.to_openai(messages, model=..., stream=...)`
* **What it does:** Specifically creates an OpenAI / Groq / DeepSeek compatible payload.
* **The Request (What you run):**
  ```python
  from model_mesh import Standardizer

  openai_payload = Standardizer.to_openai(
      "Summarize photosynthesis in 5 words",
      model="gpt-4o",
      temperature=0.7
  )
  ```
* **The Response (What comes out):**
  ```json
  {
    "model": "gpt-4o",
    "messages": [
      {"role": "user", "content": "Summarize photosynthesis in 5 words"}
    ],
    "stream": false,
    "temperature": 0.7
  }
  ```

---

#### 5. `Standardizer.to_anthropic(messages, model=..., max_tokens=...)`
* **What it does:** Formats prompts for Claude Messages API, automatically separating any `system` prompt from chat messages.
* **The Request (What you run):**
  ```python
  from model_mesh import Standardizer

  messages = [
      {"role": "system", "content": "You are a poet."},
      {"role": "user", "content": "Write 1 line about rain."}
  ]
  claude_payload = Standardizer.to_anthropic(messages, model="claude-3-5-sonnet")
  ```
* **The Response (What comes out):**
  ```json
  {
    "model": "claude-3-5-sonnet",
    "system": "You are a poet.",
    "messages": [
      {"role": "user", "content": "Write 1 line about rain."}
    ],
    "max_tokens": 4096
  }
  ```

---

#### 6. `Standardizer.normalize_response(raw_api_data)` 🌟 (Universal Parser)
* **What it does:** You give it raw, messy JSON returned by ANY AI vendor (Gemini, Claude, or OpenAI), and it cleans it up into ONE single standardized response object.
* **The Request (What you run with raw Gemini response):**
  ```python
  from model_mesh import Standardizer

  raw_gemini_json = {
      "candidates": [
          {"content": {"parts": [{"text": "Gravity pulls mass toward mass."}]}}
      ],
      "usageMetadata": {
          "promptTokenCount": 10,
          "candidatesTokenCount": 7,
          "totalTokenCount": 17
      }
  }

  response = Standardizer.normalize_response(raw_gemini_json)
  ```
* **The Response (What comes out):**
  ```python
  ChatResponse(
      content="Gravity pulls mass toward mass.",
      model="gemini",
      role="assistant",
      provider="gemini",
      usage={
          "prompt_tokens": 10,
          "completion_tokens": 7,
          "total_tokens": 17
      }
  )
  ```
  *(Now you can simply do `response.content` and `response.usage` for ANY AI!)*

---

### Module 2: Sliding-Window Rate Limiting (`DynamicRateLimiter`)

Protects your API keys and quotas using a sliding-window algorithm.

---

#### 7. `DynamicRateLimiter(limit_rule)`
* **What it does:** Initializes a rate limiter with a rule like `"5/min"`, `"10/sec"`, or `"100/hour"`.
* **The Request (What you run):**
  ```python
  from model_mesh import DynamicRateLimiter

  limiter = DynamicRateLimiter("5/min")
  ```
* **The Response (What comes out):**
  ```python
  <DynamicRateLimiter max_requests=5 window_seconds=60.0>
  ```

---

#### 8. `limiter.check(client_id)` (Non-Destructive Probe)
* **What it does:** Checks if a user is allowed to make a request and how many calls they have left, **without** consuming any quota.
* **The Request (What you run):**
  ```python
  allowed, remaining, reset_in = limiter.check("student_user_1")
  print(f"Allowed: {allowed}, Remaining: {remaining}, Reset In: {reset_in}s")
  ```
* **The Response (What comes out):**
  ```python
  # (allowed: True, remaining: 5, reset_in: 0.0)
  Allowed: True, Remaining: 5, Reset In: 0.0s
  ```

---

#### 9. `limiter.acquire(client_id)` (Consume Permit)
* **What it does:** Consumes 1 request permit. If the user sent too many requests, it immediately raises a `RateLimitExceededError`.
* **The Request (What you run - Under quota):**
  ```python
  allowed, remaining, reset_in = limiter.acquire("student_user_1")
  print(f"Allowed: {allowed}, Calls left: {remaining}")
  ```
* **The Response (What comes out - Success):**
  ```python
  Allowed: True, Calls left: 4
  ```
* **The Request (What happens when quota is breached on the 6th call):**
  ```python
  from model_mesh import RateLimitExceededError

  try:
      limiter.acquire("student_user_1")
  except RateLimitExceededError as e:
      print(f"Blocked! {e.message}")
      print(f"Retry after: {e.retry_after} seconds")
  ```
* **The Response (What comes out - Error):**
  ```text
  Blocked! Rate limit of 5/min exceeded for client 'student_user_1'. Retry after 58.2 seconds.
  Retry after: 58.2 seconds
  ```

---

#### 10. `limiter.reset(client_id=None)`
* **What it does:** Resets the rate limit window for a specific user or for all users.
* **The Request (What you run):**
  ```python
  limiter.reset("student_user_1") # Resets this specific user
  limiter.reset()                 # Resets all users
  ```
* **The Response (What comes out):**
  ```python
  None # Quota is completely refreshed back to 5
  ```

---

### Module 3: Load Balancing & Fault Tolerance (`LoadBalancer`)

Distributes requests across multiple providers and handles automatic failover.

---

#### 11. `LoadBalancer(strategy="round_robin" | "weight" | "priority")`
* **What it does:** Creates a load balancer with your chosen routing strategy.
* **The Request (What you run):**
  ```python
  from model_mesh import LoadBalancer

  balancer = LoadBalancer(strategy="round_robin")
  ```
* **The Response (What comes out):**
  ```python
  <LoadBalancer strategy='round_robin' channels=0>
  ```

---

#### 12. `balancer.add_channel(name, base_url, api_keys, models, weight, priority)`
* **What it does:** Adds an AI provider channel with its keys, models, weight, and priority.
* **The Request (What you run):**
  ```python
  # Add primary OpenAI channel
  balancer.add_channel(
      name="primary-openai",
      base_url="https://api.openai.com/v1",
      api_keys=["sk-key-1", "sk-key-2"],
      models=["gpt-4o"],
      weight=3,
      priority=1
  )

  # Add backup Gemini channel
  balancer.add_channel(
      name="backup-gemini",
      base_url="https://generativelanguage.googleapis.com/v1",
      api_keys=["AIzaKey1"],
      models=["gpt-4o", "gemini-3.8-flash"],
      weight=1,
      priority=2
  )
  ```
* **The Response (What comes out):**
  ```python
  Channel(name='primary-openai', weight=3, priority=1, healthy=True)
  ```

---

#### 13. `balancer.select_channel(model)`
* **What it does:** Selects the best channel and API key to use for the model.
* **The Request (What you run):**
  ```python
  channel, api_key = balancer.select_channel("gpt-4o")
  print(f"Routed to channel: {channel.name} using key: {api_key}")
  ```
* **The Response (What comes out):**
  ```text
  Routed to channel: primary-openai using key: sk-key-1
  ```

---

#### 14. `balancer.mark_channel_status(channel_name, healthy=False)` (Circuit Breaker)
* **What it does:** Tells the balancer that a channel failed (e.g., 500 error). It trips the circuit breaker and puts that channel into cooldown, routing subsequent calls to the backup.
* **The Request (What you run):**
  ```python
  # Primary channel failed! Trip circuit breaker:
  balancer.mark_channel_status("primary-openai", healthy=False)

  # Next call automatically routes to backup:
  channel, api_key = balancer.select_channel("gpt-4o")
  print(f"Fallback routed to: {channel.name}")
  ```
* **The Response (What comes out):**
  ```text
  Fallback routed to: backup-gemini
  ```

---

#### 15. `balancer.get_health_report()`
* **What it does:** Returns a real-time health and error report for all channels.
* **The Request (What you run):**
  ```python
  report = balancer.get_health_report()
  print(report)
  ```
* **The Response (What comes out):**
  ```json
  {
    "primary-openai": {
      "healthy": false,
      "failure_count": 1,
      "in_cooldown": true
    },
    "backup-gemini": {
      "healthy": true,
      "failure_count": 0,
      "in_cooldown": false
    }
  }
  ```

---

### Module 4: Unified Gateway Client (`Client`)

Combines standardization, rate limiting, and balancing into a single clean client.

---

#### 16. `client.chat(model, prompt=...)`
* **What it does:** Sends a prompt, checks rate limits, selects the channel, and returns the response.
* **The Request (What you run):**
  ```python
  import model_mesh

  client = model_mesh.Client()
  res = client.chat(model="gpt-4o", prompt="What is 2 + 2?")
  print("AI Response:", res.content)
  print("Tokens Used:", res.usage)
  ```
* **The Response (What comes out):**
  ```text
  AI Response: 2 + 2 = 4.
  Tokens Used: {'prompt_tokens': 12, 'completion_tokens': 6, 'total_tokens': 18}
  ```

---

#### 17. `client.balance(model)`
* **What it does:** Tests and simulates which channel a model will use.
* **The Request (What you run):**
  ```python
  route = client.balance("gpt-4o")
  print(route)
  ```
* **The Response (What comes out):**
  ```json
  {
    "model": "gpt-4o",
    "selected_channel": "openai-primary",
    "strategy": "round_robin"
  }
  ```

---

#### 18. `client.check_rate_limit(client_id)`
* **What it does:** Checks how much quota a specific client has remaining.
* **The Request (What you run):**
  ```python
  status = client.check_rate_limit("user_123")
  print(status)
  ```
* **The Response (What comes out):**
  ```json
  {
    "allowed": true,
    "remaining_requests": 59,
    "reset_in_seconds": 0.0
  }
  ```

---

#### 19. `client.set_rate_limit(rule)` & `client.set_load_balancing(strategy)`
* **What it does:** Dynamically changes the rate limit or balancing algorithm at runtime.
* **The Request (What you run):**
  ```python
  client.set_rate_limit("120/min")
  client.set_load_balancing("weight")
  ```
* **The Response (What comes out):**
  ```python
  None # Policies updated live without restarting your app
  ```

---

## 📋 Quick Method Summary

| Method | What You Send (Input) | What You Get Back (Output) |
| :--- | :--- | :--- |
| `Standardizer.standardize(prompt, model)` | Prompt string + model name | Native vendor wire format JSON |
| `Standardizer.normalize_messages(msgs)` | String or list of tuples | Clean `[{"role": "user", "content": "..."}]` |
| `Standardizer.to_gemini(msgs, model)` | Prompt + Gemini model | Google Gemini REST schema |
| `Standardizer.to_openai(msgs, model)` | Prompt + OpenAI model | OpenAI wire format schema |
| `Standardizer.to_anthropic(msgs, model)` | Prompt + Claude model | Anthropic Messages schema |
| `Standardizer.normalize_response(raw)` | Raw vendor JSON | Clean `ChatResponse` object |
| `limiter.check(client_id)` | Client ID string | `(allowed, remaining, reset_in)` tuple |
| `limiter.acquire(client_id)` | Client ID string | `(allowed, remaining, reset_in)` or raises error |
| `limiter.reset(client_id)` | Optional client ID | Clears history |
| `balancer.add_channel(...)` | Name, URL, keys, models | New `Channel` registered |
| `balancer.select_channel(model)` | Target model name | `(channel, api_key)` tuple |
| `balancer.mark_channel_status(...)` | Channel name + healthy bool | Trips or resets circuit breaker |
| `balancer.get_health_report()` | None | Dict of all channel states |
| `client.chat(model, prompt)` | Model + text prompt | Unified `ChatResponse` |
| `client.balance(model)` | Model name | Dict of routing decision |
| `client.check_rate_limit(id)` | Client ID string | Dict of quota status |

---

## 📄 License
MIT License.
