Add YandexGPT API support
diff --git a/docs/model_support.md b/docs/model_support.md
index 50ef527aa8..123daf6730 100644
--- a/docs/model_support.md
+++ b/docs/model_support.md
@@ -105,7 +105,7 @@ After these steps, the new model should be compatible with most FastChat feature
 ## API-Based Models
 To support an API-based model, consider learning from the existing OpenAI example.
 If the model is compatible with OpenAI APIs, then a configuration file is all that's needed without any additional code.
-For custom protocols, implementation of a streaming generator in [fastchat/serve/api_provider.py](https://github.com/lm-sys/FastChat/blob/main/fastchat/serve/api_provider.py) is required, following the provided examples. Currently, FastChat is compatible with OpenAI, Anthropic, Google Vertex AI, Mistral, and Nvidia NGC.
+For custom protocols, implementation of a streaming generator in [fastchat/serve/api_provider.py](https://github.com/lm-sys/FastChat/blob/main/fastchat/serve/api_provider.py) is required, following the provided examples. Currently, FastChat is compatible with OpenAI, Anthropic, Google Vertex AI, Mistral, Nvidia NGC and YandexGPT.
 
 ### Steps to Launch a WebUI with an API Model
 1. Specify the endpoint information in a JSON configuration file. For instance, create a file named `api_endpoints.json`:
@@ -120,7 +120,7 @@ For custom protocols, implementation of a streaming generator in [fastchat/serve
   }
 }
 ```
-  - "api_type" can be one of the following: openai, anthropic, gemini, or mistral. For custom APIs, add a new type and implement it accordingly.
+  - "api_type" can be one of the following: openai, anthropic, gemini, mistral or yandexgpt. For custom APIs, add a new type and implement it accordingly.
   - "anony_only" indicates whether to display this model in anonymous mode only.
 
 2. Launch the Gradio web server with the argument `--register api_endpoints.json`:
diff --git a/fastchat/conversation.py b/fastchat/conversation.py
index 99eb32a442..63f867f1e2 100644
--- a/fastchat/conversation.py
+++ b/fastchat/conversation.py
@@ -1620,6 +1620,16 @@ def get_conv_template(name: str) -> Conversation:
     )
 )
 
+register_conv_template(
+    Conversation(
+        name="yandexgpt",
+        system_message="",
+        roles=("user", "assistant"),
+        sep_style=None,
+        sep=None,
+    )
+)
+
 
 if __name__ == "__main__":
     from fastchat.conversation import get_conv_template
diff --git a/fastchat/model/model_adapter.py b/fastchat/model/model_adapter.py
index f5fb57cb58..4807703d27 100644
--- a/fastchat/model/model_adapter.py
+++ b/fastchat/model/model_adapter.py
@@ -2276,6 +2276,16 @@ def get_default_conv_template(self, model_path: str) -> Conversation:
         return get_conv_template("gemma")
 
 
+class YandexGPTAdapter(BaseModelAdapter):
+    """The model adapter for YandexGPT"""
+
+    def match(self, model_path: str):
+        return "yandexgpt" in model_path.lower()
+
+    def get_default_conv_template(self, model_path: str) -> Conversation:
+        return get_conv_template("yandexgpt")
+
+
 class CllmAdapter(BaseModelAdapter):
     """The model adapter for CLLM"""
 
@@ -2397,6 +2407,7 @@ def get_default_conv_template(self, model_path: str) -> Conversation:
 register_model_adapter(LlavaAdapter)
 register_model_adapter(YuanAdapter)
 register_model_adapter(GemmaAdapter)
+register_model_adapter(YandexGPTAdapter)
 register_model_adapter(CllmAdapter)
 
 # After all adapters, try the default base adapter.
diff --git a/fastchat/serve/api_provider.py b/fastchat/serve/api_provider.py
index b0611773cc..cba95eacec 100644
--- a/fastchat/serve/api_provider.py
+++ b/fastchat/serve/api_provider.py
@@ -83,6 +83,31 @@ def get_api_provider_stream_iter(
             api_base=model_api_dict["api_base"],
             api_key=model_api_dict["api_key"],
         )
+    elif model_api_dict["api_type"] == "yandexgpt":
+        # note: top_p parameter is unused by yandexgpt
+
+        messages = []
+        if conv.system_message:
+            messages.append({"role": "system", "text": conv.system_message})
+        messages += [
+            {"role": role, "text": text}
+            for role, text in conv.messages
+            if text is not None
+        ]
+
+        fixed_temperature = model_api_dict.get("fixed_temperature")
+        if fixed_temperature is not None:
+            temperature = fixed_temperature
+
+        stream_iter = yandexgpt_api_stream_iter(
+            model_name=model_api_dict["model_name"],
+            messages=messages,
+            temperature=temperature,
+            max_tokens=max_new_tokens,
+            api_base=model_api_dict["api_base"],
+            api_key=model_api_dict.get("api_key"),
+            folder_id=model_api_dict.get("folder_id"),
+        )
     elif model_api_dict["api_type"] == "cohere":
         messages = conv.to_openai_api_messages()
         stream_iter = cohere_api_stream_iter(
@@ -467,6 +492,47 @@ def nvidia_api_stream_iter(model_name, messages, temp, top_p, max_tokens, api_ba
             yield {"text": text, "error_code": 0}
 
 
+def yandexgpt_api_stream_iter(
+    model_name, messages, temperature, max_tokens, api_base, api_key, folder_id
+):
+    api_key = api_key or os.environ["YANDEXGPT_API_KEY"]
+    headers = {
+        "Authorization": f"Api-Key {api_key}",
+        "content-type": "application/json",
+    }
+
+    payload = {
+        "modelUri": f"gpt://{folder_id}/{model_name}",
+        "completionOptions": {
+            "temperature": temperature,
+            "max_tokens": max_tokens,
+            "stream": True,
+        },
+        "messages": messages,
+    }
+    logger.info(f"==== request ====\n{payload}")
+
+    # https://llm.api.cloud.yandex.net/foundationModels/v1/completion
+    response = requests.post(
+        api_base, headers=headers, json=payload, stream=True, timeout=60
+    )
+    text = ""
+    for line in response.iter_lines():
+        if line:
+            data = json.loads(line.decode("utf-8"))
+            data = data["result"]
+            top_alternative = data["alternatives"][0]
+            text = top_alternative["message"]["text"]
+            yield {"text": text, "error_code": 0}
+
+            status = top_alternative["status"]
+            if status in (
+                "ALTERNATIVE_STATUS_FINAL",
+                "ALTERNATIVE_STATUS_TRUNCATED_FINAL",
+            ):
+                break
+
+
 def cohere_api_stream_iter(
     client_name: str,
     model_id: str,

<!-- Thank you for your contribution! -->

<!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. -->

## Why are these changes needed?

<!-- Please give a short summary of the change and the problem this solves. -->

## Related issue number (if applicable)

<!-- For example: "Closes #1234" -->

## Checks

- [x] I've run `format.sh` to lint the changes in this PR.
- [x] I've included any doc changes needed.
- [x] I've made sure the relevant tests are passing (if applicable).
