| """ |
| core/api_client.py β MiniCPM-V API wrapper. |
| |
| Handles: |
| β’ OpenAI-compatible streaming via the `openai` SDK |
| β’ API key resolution (env var β UI field β public fallback) |
| β’ Friendly error messages for 401 / 429 / network failures |
| """ |
|
|
| import os |
| from collections.abc import Generator |
|
|
| from openai import OpenAI, APIStatusError, APIConnectionError |
| from PIL import Image |
|
|
| from config import API_BASE_URL, PUBLIC_API_KEY, MODELS |
| from core.image_utils import pil_to_data_url |
|
|
|
|
| def _resolve_key(ui_key: str) -> str: |
| """ |
| Priority: env var MINICPM_API_KEY β UI field β hardcoded public key. |
| """ |
| return ( |
| os.environ.get("MINICPM_API_KEY", "").strip() |
| or (ui_key or "").strip() |
| or PUBLIC_API_KEY |
| ) |
|
|
|
|
| def stream_description( |
| image: Image.Image, |
| prompt: str, |
| model_label: str, |
| max_tokens: int, |
| temperature: float, |
| api_key: str, |
| ) -> Generator[str, None, None]: |
| """ |
| Stream the model's response token-by-token. |
| |
| Yields: |
| Cumulative response string β each yield is the full text so far, |
| so Gradio can update the textbox incrementally. |
| """ |
| if image is None: |
| yield "β οΈ Please upload an image first." |
| return |
|
|
| key = _resolve_key(api_key) |
| model_id = MODELS[model_label] |
| data_url = pil_to_data_url(image) |
|
|
| client = OpenAI(api_key=key, base_url=API_BASE_URL) |
|
|
| try: |
| stream = client.chat.completions.create( |
| model=model_id, |
| messages=[{ |
| "role": "user", |
| "content": [ |
| {"type": "image_url", "image_url": {"url": data_url}}, |
| {"type": "text", "text": prompt}, |
| ], |
| }], |
| max_tokens=max_tokens, |
| temperature=temperature, |
| stream=True, |
| ) |
|
|
| result = "" |
| for chunk in stream: |
| delta = chunk.choices[0].delta.content or "" |
| if delta: |
| result += delta |
| yield result |
|
|
| except APIStatusError as e: |
| if e.status_code == 401: |
| yield ( |
| "π **API key rejected (401).**\n\n" |
| "The public key may have hit its rate limit. \n" |
| "Get a free personal key at https://modelbest.cn and paste it " |
| "in the **API Key** field." |
| ) |
| elif e.status_code == 429: |
| yield ( |
| "β³ **Rate limit reached (429).**\n\n" |
| "The free public key is shared β wait a moment and retry, " |
| "or use your own key from https://modelbest.cn" |
| ) |
| else: |
| yield f"β API error {e.status_code}:\n\n```\n{e.message}\n```" |
|
|
| except APIConnectionError: |
| yield ( |
| "β **Cannot reach the API** (`api.modelbest.cn`). \n" |
| "Check your internet connection and try again." |
| ) |
|
|
| except Exception as e: |
| yield f"β Unexpected error:\n\n```\n{e}\n```" |
|
|