mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-29 12:09:31 +08:00
feat: add GreenPT model provider (#17447)
## Summary GreenPT is a European AI provider with an OpenAI-compatible API, optimized infrastructure, and datacenters powered by 100% renewable energy. This adds native GreenPT support across RAGFlow’s Go-first provider system and its Python compatibility layer: - discovers the current catalog from `GET /v1/models` - features `glm-5.2` and `kimi-k2.7-code` for chat and coding - supports `green-embedding` through `/v1/embeddings` - supports `green-rerank` through `/v1/rerank` - supports `green-s` and `green-s-pro` speech-to-text through `/v1/listen` - adds provider configuration, UI icon, and supported-provider documentation
This commit is contained in:
@@ -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": "",
|
||||
|
||||
68
conf/models/greenpt.json
Normal file
68
conf/models/greenpt.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
145
internal/entity/models/greenpt.go
Normal file
145
internal/entity/models/greenpt.go
Normal file
@@ -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
|
||||
}
|
||||
115
internal/entity/models/greenpt_test.go
Normal file
115
internal/entity/models/greenpt_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
105
test/unit_test/rag/llm/test_greenpt.py
Normal file
105
test/unit_test/rag/llm/test_greenpt.py
Normal file
@@ -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
|
||||
4
web/src/assets/svg/llm/greenpt.svg
Normal file
4
web/src/assets/svg/llm/greenpt.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 512 512" fill="none">
|
||||
<path fill="#9BE755" d="M255.746 52.773c-112.1 0-202.973 90.874-202.973 202.973S143.646 458.72 255.746 458.72 458.72 367.846 458.72 255.746 367.846 52.773 255.746 52.773Zm6.427 313.932c-60.72 0-109.944-49.527-109.944-110.62s49.224-110.62 109.944-110.62 109.944 49.526 109.944 110.62-49.224 110.62-109.944 110.62Z"/>
|
||||
<path fill="#4B7F2B" d="M319.923 424.89c-92.214 0-166.969-75.729-166.969-169.144 0-93.416 74.755-169.144 166.969-169.144 20.616 0 40.275 3.965 58.505 10.887-34.322-27.922-77.847-44.716-125.293-44.716-110.658 0-200.362 90.874-200.362 202.973S142.477 458.72 253.135 458.72c47.446 0 90.971-16.794 125.293-44.716-18.23 6.921-37.889 10.887-58.505 10.887Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 778 B |
@@ -109,6 +109,7 @@ const svgIcons = [
|
||||
LLMFactory.TokenHub,
|
||||
LLMFactory.FunASR,
|
||||
LLMFactory.AIMLAPI,
|
||||
LLMFactory.GreenPT,
|
||||
];
|
||||
|
||||
export const LlmIcon = ({
|
||||
|
||||
@@ -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<string, string> = {
|
||||
@@ -198,6 +200,7 @@ export const FieldToModelType: Record<string, string> = {
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user