diff --git a/conf/llm_factories.json b/conf/llm_factories.json
index 36dea54024..aa1f75acc3 100644
--- a/conf/llm_factories.json
+++ b/conf/llm_factories.json
@@ -1,5 +1,57 @@
{
"factory_llm_infos": [
+ {
+ "name": "GreenPT",
+ "logo": "",
+ "tags": "LLM,TEXT EMBEDDING,TEXT RE-RANK,SPEECH2TEXT",
+ "status": "1",
+ "rank": "50",
+ "url": "https://api.greenpt.ai/v1",
+ "llm": [
+ {
+ "llm_name": "glm-5.2",
+ "tags": "LLM,CHAT,1M",
+ "max_tokens": 1000000,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "kimi-k2.7-code",
+ "tags": "LLM,CHAT,256K",
+ "max_tokens": 262144,
+ "model_type": "chat",
+ "is_tools": true
+ },
+ {
+ "llm_name": "green-embedding",
+ "tags": "TEXT EMBEDDING,32K",
+ "max_tokens": 32768,
+ "model_type": "embedding",
+ "is_tools": false
+ },
+ {
+ "llm_name": "green-rerank",
+ "tags": "RE-RANK,32K",
+ "max_tokens": 32768,
+ "model_type": "rerank",
+ "is_tools": false
+ },
+ {
+ "llm_name": "green-s",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 0,
+ "model_type": "speech2text",
+ "is_tools": false
+ },
+ {
+ "llm_name": "green-s-pro",
+ "tags": "SPEECH2TEXT",
+ "max_tokens": 0,
+ "model_type": "speech2text",
+ "is_tools": false
+ }
+ ]
+ },
{
"name": "aimlapi.com",
"logo": "",
diff --git a/conf/models/greenpt.json b/conf/models/greenpt.json
new file mode 100644
index 0000000000..36a61bbd93
--- /dev/null
+++ b/conf/models/greenpt.json
@@ -0,0 +1,68 @@
+{
+ "name": "GreenPT",
+ "url": {
+ "default": "https://api.greenpt.ai"
+ },
+ "url_suffix": {
+ "chat": "v1/chat/completions",
+ "models": "v1/models",
+ "embedding": "v1/embeddings",
+ "rerank": "v1/rerank",
+ "asr": "v1/listen"
+ },
+ "class": "greenpt",
+ "models": [
+ {
+ "name": "glm-5.2",
+ "max_tokens": 1000000,
+ "model_types": [
+ "chat"
+ ],
+ "tools": {
+ "support": true
+ }
+ },
+ {
+ "name": "kimi-k2.7-code",
+ "max_tokens": 262144,
+ "model_types": [
+ "chat"
+ ],
+ "tools": {
+ "support": true
+ }
+ },
+ {
+ "name": "green-embedding",
+ "max_tokens": 32768,
+ "max_dimension": 2560,
+ "dimensions": [
+ 2560
+ ],
+ "model_types": [
+ "embedding"
+ ]
+ },
+ {
+ "name": "green-rerank",
+ "max_tokens": 32768,
+ "model_types": [
+ "rerank"
+ ]
+ },
+ {
+ "name": "green-s",
+ "max_tokens": 0,
+ "model_types": [
+ "asr"
+ ]
+ },
+ {
+ "name": "green-s-pro",
+ "max_tokens": 0,
+ "model_types": [
+ "asr"
+ ]
+ }
+ ]
+}
diff --git a/docs/guides/models/supported_models.mdx b/docs/guides/models/supported_models.mdx
index 7dbc8cc2eb..7515a89164 100644
--- a/docs/guides/models/supported_models.mdx
+++ b/docs/guides/models/supported_models.mdx
@@ -35,6 +35,7 @@ A complete list of model providers supported by RAGFlow, which will continue to
| Google Cloud | `https://cloud.google.com` |
| GPUStack | `https://gpustack.ai` |
| Groq | `https://groq.com` |
+| GreenPT | `https://greenpt.ai` |
| HuggingFace | `https://huggingface.co` |
| Jina | `https://jina.ai` |
| LocalAI | `https://localai.io` |
diff --git a/internal/entity/models/factory.go b/internal/entity/models/factory.go
index 3d3d3acde5..1aa4d1da60 100644
--- a/internal/entity/models/factory.go
+++ b/internal/entity/models/factory.go
@@ -159,6 +159,8 @@ func (f *ModelFactory) CreateModelDriver(providerName string, baseURL map[string
return NewXiaomiModel(baseURL, urlSuffix), nil
case "funasr":
return NewFunASRModel(baseURL, urlSuffix), nil
+ case "greenpt":
+ return NewGreenPTModel(baseURL, urlSuffix), nil
default:
return NewDummyModel(baseURL, urlSuffix), nil
}
diff --git a/internal/entity/models/greenpt.go b/internal/entity/models/greenpt.go
new file mode 100644
index 0000000000..baa57244b2
--- /dev/null
+++ b/internal/entity/models/greenpt.go
@@ -0,0 +1,145 @@
+//
+// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package models
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "ragflow/internal/common"
+ "strings"
+)
+
+// GreenPTModel implements GreenPT's OpenAI-compatible chat, embedding,
+// reranking, and model-list APIs, plus its Deepgram-compatible STT endpoint.
+type GreenPTModel struct {
+ *OpenAIAPICompatibleModel
+}
+
+// NewGreenPTModel creates a GreenPT model driver.
+func NewGreenPTModel(baseURL map[string]string, urlSuffix URLSuffix) *GreenPTModel {
+ return &GreenPTModel{
+ OpenAIAPICompatibleModel: NewOpenAIAPICompatibleModel(baseURL, urlSuffix),
+ }
+}
+
+func (m *GreenPTModel) Name() string {
+ return "GreenPT"
+}
+
+func (m *GreenPTModel) NewInstance(baseURL map[string]string) ModelDriver {
+ return NewGreenPTModel(baseURL, m.baseModel.URLSuffix)
+}
+
+// ListModels uses GreenPT's live /v1/models endpoint and corrects the two
+// speech model IDs, whose names do not contain an ASR hint.
+func (m *GreenPTModel) ListModels(ctx context.Context, apiConfig *APIConfig) ([]ListModelResponse, error) {
+ models, err := m.OpenAIAPICompatibleModel.ListModels(ctx, apiConfig)
+ if err != nil {
+ return nil, err
+ }
+ for i := range models {
+ switch models[i].Name {
+ case "green-s", "green-s-pro":
+ models[i].ModelTypes = []string{"asr"}
+ }
+ }
+ return models, nil
+}
+
+// TranscribeAudio calls GreenPT's Deepgram-compatible /v1/listen endpoint.
+func (m *GreenPTModel) TranscribeAudio(ctx context.Context, modelName *string, file *string, apiConfig *APIConfig, asrConfig *ASRConfig, modelUsage *common.ModelUsage) (*ASRResponse, error) {
+ if err := m.baseModel.APIConfigCheck(apiConfig); err != nil {
+ return nil, err
+ }
+ if modelName == nil || strings.TrimSpace(*modelName) == "" {
+ return nil, fmt.Errorf("model name is required")
+ }
+ if file == nil || strings.TrimSpace(*file) == "" {
+ return nil, fmt.Errorf("file is missing")
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, nonStreamCallTimeout)
+ defer cancel()
+
+ audio, err := os.Open(*file)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open audio file: %w", err)
+ }
+ defer audio.Close()
+
+ baseURL, err := m.baseModel.GetBaseURL(apiConfig)
+ if err != nil {
+ return nil, err
+ }
+ endpoint := fmt.Sprintf("%s/%s", strings.TrimSuffix(baseURL, "/"), strings.TrimPrefix(m.baseModel.URLSuffix.ASR, "/"))
+ query := url.Values{"model": {*modelName}}
+ if asrConfig != nil {
+ for key, value := range asrConfig.Params {
+ query.Set(key, fmt.Sprintf("%v", value))
+ }
+ }
+ endpoint += "?" + query.Encode()
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, audio)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+ contentType := mime.TypeByExtension(strings.ToLower(filepath.Ext(*file)))
+ if contentType == "" {
+ contentType = "application/octet-stream"
+ }
+ req.Header.Set("Content-Type", contentType)
+ req.Header.Set("Authorization", "Token "+*apiConfig.ApiKey)
+
+ resp, err := m.baseModel.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to send request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response: %w", err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("GreenPT ASR API error: %s, body: %s", resp.Status, string(body))
+ }
+
+ var result struct {
+ Results struct {
+ Channels []struct {
+ Alternatives []struct {
+ Transcript string `json:"transcript"`
+ } `json:"alternatives"`
+ } `json:"channels"`
+ } `json:"results"`
+ }
+ if err := json.Unmarshal(body, &result); err != nil {
+ return nil, fmt.Errorf("failed to parse GreenPT ASR response: %w", err)
+ }
+ if len(result.Results.Channels) == 0 || len(result.Results.Channels[0].Alternatives) == 0 {
+ return nil, fmt.Errorf("GreenPT ASR response contains no transcript")
+ }
+ return &ASRResponse{Text: result.Results.Channels[0].Alternatives[0].Transcript}, nil
+}
diff --git a/internal/entity/models/greenpt_test.go b/internal/entity/models/greenpt_test.go
new file mode 100644
index 0000000000..63015bec84
--- /dev/null
+++ b/internal/entity/models/greenpt_test.go
@@ -0,0 +1,115 @@
+//
+// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package models
+
+import (
+ "context"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+)
+
+func TestGreenPTListModelsClassifiesSpeech(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/v1/models" {
+ t.Errorf("unexpected path: %s", r.URL.Path)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = io.WriteString(w, `{"object":"list","data":[{"id":"glm-5.2"},{"id":"green-embedding"},{"id":"green-rerank"},{"id":"green-s"},{"id":"green-s-pro"}]}`)
+ }))
+ defer server.Close()
+
+ driver := NewGreenPTModel(map[string]string{"default": server.URL}, URLSuffix{Models: "v1/models"})
+ key := "test"
+ models, err := driver.ListModels(context.Background(), &APIConfig{ApiKey: &key})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := map[string]string{
+ "glm-5.2": "chat",
+ "green-embedding": "embedding",
+ "green-rerank": "rerank",
+ "green-s": "asr",
+ "green-s-pro": "asr",
+ }
+ if len(models) != len(want) {
+ t.Fatalf("expected %d models, got %d", len(want), len(models))
+ }
+ for _, model := range models {
+ expectedType, ok := want[model.Name]
+ if !ok {
+ t.Fatalf("unexpected model: %s", model.Name)
+ }
+ if len(model.ModelTypes) != 1 || model.ModelTypes[0] != expectedType {
+ t.Fatalf("%s classified as %v", model.Name, model.ModelTypes)
+ }
+ delete(want, model.Name)
+ }
+ if len(want) != 0 {
+ t.Fatalf("missing expected models: %v", want)
+ }
+}
+
+func TestGreenPTTranscribeAudio(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/v1/listen" || r.URL.Query().Get("model") != "green-s" {
+ t.Errorf("unexpected request URL: %s", r.URL.String())
+ return
+ }
+ if r.Header.Get("Authorization") != "Token test-key" {
+ t.Errorf("unexpected authorization header: %s", r.Header.Get("Authorization"))
+ return
+ }
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Errorf("failed to read request body: %v", err)
+ return
+ }
+ if string(body) != "audio" {
+ t.Errorf("unexpected audio body: %q", body)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = io.WriteString(w, `{"results":{"channels":[{"alternatives":[{"transcript":"renewable inference"}]}]}}`)
+ }))
+ defer server.Close()
+
+ audio, err := os.CreateTemp(t.TempDir(), "*.wav")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = audio.WriteString("audio"); err != nil {
+ t.Fatal(err)
+ }
+ if err = audio.Close(); err != nil {
+ t.Fatal(err)
+ }
+
+ driver := NewGreenPTModel(map[string]string{"default": server.URL}, URLSuffix{ASR: "v1/listen"})
+ key, model, path := "test-key", "green-s", audio.Name()
+ response, err := driver.TranscribeAudio(context.Background(), &model, &path, &APIConfig{ApiKey: &key}, nil, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if response.Text != "renewable inference" {
+ t.Fatalf("unexpected transcript: %q", response.Text)
+ }
+}
diff --git a/rag/llm/chat_model.py b/rag/llm/chat_model.py
index 8a75135725..1783b858ee 100644
--- a/rag/llm/chat_model.py
+++ b/rag/llm/chat_model.py
@@ -1563,6 +1563,15 @@ class AIMLAPIChat(Base):
logging.info("[aimlapi.com] Chat initialized with model %s", model_name)
+class GreenPTChat(Base):
+ """GreenPT OpenAI-compatible chat adapter."""
+
+ _FACTORY_NAME = "GreenPT"
+
+ def __init__(self, key, model_name, base_url="https://api.greenpt.ai/v1", **kwargs):
+ super().__init__(key, model_name, base_url or "https://api.greenpt.ai/v1", **kwargs)
+
+
class LiteLLMBase(ABC):
_FACTORY_NAME = [
"Tongyi-Qianwen",
diff --git a/rag/llm/embedding_model.py b/rag/llm/embedding_model.py
index ede09d28b9..005053366f 100644
--- a/rag/llm/embedding_model.py
+++ b/rag/llm/embedding_model.py
@@ -878,6 +878,15 @@ class OpenAI_APIEmbed(OpenAIEmbed):
self.model_name = model_name.split("___")[0]
+class GreenPTEmbed(OpenAIEmbed):
+ """GreenPT OpenAI-compatible embedding adapter."""
+
+ _FACTORY_NAME = "GreenPT"
+
+ def __init__(self, key, model_name="green-embedding", base_url="https://api.greenpt.ai/v1"):
+ super().__init__(key, model_name=model_name, base_url=base_url or "https://api.greenpt.ai/v1")
+
+
class CoHereEmbed(Base):
_FACTORY_NAME = "Cohere"
diff --git a/rag/llm/model_meta.py b/rag/llm/model_meta.py
index 301cf8e119..6a91d4b6ea 100644
--- a/rag/llm/model_meta.py
+++ b/rag/llm/model_meta.py
@@ -18,6 +18,7 @@ import aiohttp
from abc import ABC
from urllib.parse import urlparse
from json.decoder import JSONDecodeError
+from typing import ClassVar
from common.constants import LLMType
@@ -458,6 +459,37 @@ class OpenAIAPICompatible(Base):
return model_list
+class GreenPT(OpenAIAPICompatible):
+ """Discover and classify GreenPT models from the live catalog."""
+
+ _FACTORY_NAME = "GreenPT"
+
+ _MODEL_TYPES: ClassVar[dict[str, list[str]]] = {
+ "green-embedding": [LLMType.EMBEDDING.value],
+ "qwen3-embedding-8b": [LLMType.EMBEDDING.value],
+ "green-rerank": [LLMType.RERANK.value],
+ "green-s": [LLMType.ASR.value],
+ "green-s-pro": [LLMType.ASR.value],
+ }
+ _MAX_TOKENS: ClassVar[dict[str, int]] = {
+ "glm-5.2": 1_000_000,
+ "kimi-k2.7-code": 262_144,
+ "green-embedding": 32_768,
+ "qwen3-embedding-8b": 32_768,
+ "green-rerank": 32_768,
+ }
+
+ def _format_model_list(self, raw_model_list):
+ """Apply GreenPT capability metadata to discovered models."""
+ models = super()._format_model_list(raw_model_list)
+ for model in models:
+ model["model_types"] = self._MODEL_TYPES.get(model["name"], model["model_types"])
+ model["max_tokens"] = self._MAX_TOKENS.get(model["name"], model["max_tokens"])
+ if model["model_types"] == [LLMType.CHAT.value]:
+ model["features"] = ["is_tools"]
+ return models
+
+
class FunASR(Base):
_FACTORY_NAME = "FunASR"
diff --git a/rag/llm/rerank_model.py b/rag/llm/rerank_model.py
index 7778b08b04..2e4d1c43b0 100644
--- a/rag/llm/rerank_model.py
+++ b/rag/llm/rerank_model.py
@@ -116,6 +116,18 @@ class JinaRerank(Base):
return rank, total_token_count_from_response(res)
+class GreenPTRerank(JinaRerank):
+ """GreenPT native reranking adapter."""
+
+ _FACTORY_NAME = "GreenPT"
+
+ def __init__(self, key, model_name="green-rerank", base_url="https://api.greenpt.ai/v1/rerank"):
+ endpoint = (base_url or "https://api.greenpt.ai/v1/rerank").rstrip("/")
+ if not endpoint.endswith("/rerank"):
+ endpoint += "/rerank"
+ super().__init__(key, model_name=model_name, base_url=endpoint)
+
+
class XInferenceRerank(Base):
_FACTORY_NAME = "Xinference"
diff --git a/rag/llm/sequence2txt_model.py b/rag/llm/sequence2txt_model.py
index 6957fb961a..beb684d00d 100644
--- a/rag/llm/sequence2txt_model.py
+++ b/rag/llm/sequence2txt_model.py
@@ -16,12 +16,13 @@
import base64
import io
import json
+import logging
import os
import re
import struct
-from abc import ABC
import tempfile
-import logging
+from abc import ABC
+from collections.abc import Mapping, Sequence
from urllib.parse import urlparse
import requests
@@ -31,6 +32,8 @@ from openai.lib.azure import AzureOpenAI
from common.token_utils import num_tokens_from_string
from rag.utils.url_utils import ensure_v1
+logger = logging.getLogger(__name__)
+
class Base(ABC):
def __init__(self, key, model_name, **kwargs):
@@ -124,6 +127,67 @@ class FuturMixSeq2txt(GPTSeq2txt):
logging.info("[FuturMix] Speech2Text initialized with model %s", model_name)
+class GreenPTSeq2txt(Base):
+ """Transcribe audio with GreenPT's Deepgram-compatible endpoint."""
+
+ _FACTORY_NAME = "GreenPT"
+
+ def __init__(self, key, model_name="green-s", base_url="https://api.greenpt.ai/v1", **kwargs):
+ self.api_key = key
+ self.model_name = model_name
+ self.base_url = (base_url or "https://api.greenpt.ai/v1").rstrip("/")
+
+ def transcription(self, audio_path, **kwargs):
+ """Transcribe audio while logging only non-sensitive request metadata."""
+ params = {"model": self.model_name}
+ params.update(kwargs)
+ logger.info("[GreenPT] Starting speech transcription with model %s", self.model_name)
+ try:
+ with open(audio_path, "rb") as audio_file:
+ response = requests.post(
+ f"{self.base_url}/listen",
+ headers={"Authorization": f"Token {self.api_key}", "Content-Type": "application/octet-stream"},
+ params=params,
+ data=audio_file,
+ timeout=300,
+ )
+ response.raise_for_status()
+ except (OSError, requests.RequestException):
+ logger.exception("[GreenPT] Speech transcription request failed for model %s", self.model_name)
+ raise
+ try:
+ payload = response.json()
+ if not isinstance(payload, Mapping):
+ raise TypeError("response root must be an object")
+ results = payload.get("results")
+ if not isinstance(results, Mapping):
+ raise TypeError("results must be an object")
+ channels = results.get("channels")
+ if not isinstance(channels, Sequence) or isinstance(channels, (str, bytes)) or not channels:
+ raise TypeError("channels must be a non-empty array")
+ channel = channels[0]
+ if not isinstance(channel, Mapping):
+ raise TypeError("channel must be an object")
+ alternatives = channel.get("alternatives")
+ if not isinstance(alternatives, Sequence) or isinstance(alternatives, (str, bytes)) or not alternatives:
+ raise TypeError("alternatives must be a non-empty array")
+ alternative = alternatives[0]
+ if not isinstance(alternative, Mapping):
+ raise TypeError("alternative must be an object")
+ transcript = alternative.get("transcript")
+ if not isinstance(transcript, str) or not (text := transcript.strip()):
+ raise TypeError("transcript must be a non-empty string")
+ except (TypeError, ValueError) as exc:
+ logger.warning("[GreenPT] Invalid speech response; model=%s status=%s", self.model_name, response.status_code)
+ raise ValueError("GreenPT speech response contains no valid transcript") from exc
+ logger.info(
+ "[GreenPT] Speech transcription completed; status=%s transcript_available=%s",
+ response.status_code,
+ bool(text),
+ )
+ return text, num_tokens_from_string(text)
+
+
class QWenSeq2txt(Base):
_FACTORY_NAME = "Tongyi-Qianwen"
_FUN_ASR_FLASH_PREFIX = "fun-asr-flash"
diff --git a/test/unit_test/rag/llm/test_greenpt.py b/test/unit_test/rag/llm/test_greenpt.py
new file mode 100644
index 0000000000..a45d3d5abb
--- /dev/null
+++ b/test/unit_test/rag/llm/test_greenpt.py
@@ -0,0 +1,105 @@
+#
+# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import logging
+from io import BytesIO
+from unittest.mock import Mock, patch
+
+import pytest
+
+from common.constants import LLMType
+from rag.llm.model_meta import GreenPT
+from rag.llm.rerank_model import GreenPTRerank
+from rag.llm.sequence2txt_model import GreenPTSeq2txt
+
+
+def test_greenpt_model_list_classifies_native_endpoints():
+ provider = GreenPT("test", "https://api.greenpt.ai/v1")
+ models = provider._format_model_list(
+ {
+ "data": [
+ {"id": "glm-5.2"},
+ {"id": "green-embedding"},
+ {"id": "green-rerank"},
+ {"id": "green-s"},
+ ]
+ }
+ )
+
+ by_name = {model["name"]: model for model in models}
+ assert by_name["glm-5.2"]["model_types"] == [LLMType.CHAT.value]
+ assert by_name["glm-5.2"]["max_tokens"] == 1_000_000
+ assert by_name["green-embedding"]["model_types"] == [LLMType.EMBEDDING.value]
+ assert by_name["green-rerank"]["model_types"] == [LLMType.RERANK.value]
+ assert by_name["green-s"]["model_types"] == [LLMType.ASR.value]
+
+
+def test_greenpt_rerank_normalizes_endpoint():
+ assert GreenPTRerank("test").base_url == "https://api.greenpt.ai/v1/rerank"
+ assert GreenPTRerank("test", base_url="https://example.com/v1").base_url == "https://example.com/v1/rerank"
+
+
+def test_greenpt_transcription_uses_listen_protocol(caplog):
+ caplog.set_level(logging.INFO)
+ response = Mock()
+ response.status_code = 200
+ response.json.return_value = {"results": {"channels": [{"alternatives": [{"transcript": " renewable inference "}]}]}}
+ response.raise_for_status.return_value = None
+
+ with (
+ patch("builtins.open", return_value=BytesIO(b"audio")),
+ patch("rag.llm.sequence2txt_model.requests.post", return_value=response) as post,
+ ):
+ text, _ = GreenPTSeq2txt("secret").transcription("sample.wav", language="en")
+
+ assert text == "renewable inference"
+ assert post.call_args.args[0] == "https://api.greenpt.ai/v1/listen"
+ assert post.call_args.kwargs["headers"]["Authorization"] == "Token secret"
+ assert post.call_args.kwargs["params"] == {"model": "green-s", "language": "en"}
+ assert "status=200" in caplog.text
+ assert "secret" not in caplog.text
+ assert "sample.wav" not in caplog.text
+
+
+@pytest.mark.parametrize(
+ "payload",
+ [
+ [],
+ {},
+ {"results": []},
+ {"results": {"channels": "invalid"}},
+ {"results": {"channels": [[]]}},
+ {"results": {"channels": [{"alternatives": "invalid"}]}},
+ {"results": {"channels": [{"alternatives": [[]]}]}},
+ {"results": {"channels": [{"alternatives": [{"transcript": 42}]}]}},
+ {"results": {"channels": [{"alternatives": [{"transcript": " "}]}]}},
+ ],
+)
+def test_greenpt_transcription_rejects_malformed_responses(payload, caplog):
+ caplog.set_level(logging.WARNING)
+ response = Mock(status_code=200)
+ response.json.return_value = payload
+
+ with (
+ patch("builtins.open", return_value=BytesIO(b"audio")),
+ patch("rag.llm.sequence2txt_model.requests.post", return_value=response),
+ pytest.raises(ValueError, match="contains no valid transcript"),
+ ):
+ GreenPTSeq2txt("secret").transcription("sample.wav")
+
+ assert "model=green-s status=200" in caplog.text
+ assert "secret" not in caplog.text
+ assert "sample.wav" not in caplog.text
diff --git a/web/src/assets/svg/llm/greenpt.svg b/web/src/assets/svg/llm/greenpt.svg
new file mode 100644
index 0000000000..d446007096
--- /dev/null
+++ b/web/src/assets/svg/llm/greenpt.svg
@@ -0,0 +1,4 @@
+
diff --git a/web/src/components/svg-icon.tsx b/web/src/components/svg-icon.tsx
index 03b44ad1e6..ccbc5e3c31 100644
--- a/web/src/components/svg-icon.tsx
+++ b/web/src/components/svg-icon.tsx
@@ -109,6 +109,7 @@ const svgIcons = [
LLMFactory.TokenHub,
LLMFactory.FunASR,
LLMFactory.AIMLAPI,
+ LLMFactory.GreenPT,
];
export const LlmIcon = ({
diff --git a/web/src/constants/llm.ts b/web/src/constants/llm.ts
index bf7045b5a6..93cd7bc910 100644
--- a/web/src/constants/llm.ts
+++ b/web/src/constants/llm.ts
@@ -88,6 +88,7 @@ export enum LLMFactory {
NewAPI = 'New API',
FunASR = 'FunASR',
AIMLAPI = 'aimlapi.com',
+ GreenPT = 'GreenPT',
}
// Please lowercase the file name
@@ -175,6 +176,7 @@ export const IconMap = {
[LLMFactory.NewAPI]: 'new-api',
[LLMFactory.FunASR]: 'funasr',
[LLMFactory.AIMLAPI]: 'aimlapi',
+ [LLMFactory.GreenPT]: 'greenpt',
};
export const ModelTypeToField: Record = {
@@ -198,6 +200,7 @@ export const FieldToModelType: Record = {
export const APIMapUrl = {
[LLMFactory.OpenAI]: 'https://platform.openai.com/api-keys',
[LLMFactory.AIMLAPI]: 'https://aimlapi.com/app/keys',
+ [LLMFactory.GreenPT]: 'https://greenpt.ai',
[LLMFactory.Anthropic]: 'https://console.anthropic.com/settings/keys',
[LLMFactory.Gemini]: 'https://aistudio.google.com/app/apikey',
[LLMFactory.DeepSeek]: 'https://platform.deepseek.com/api_keys',