This commit is contained in:
2025-09-23 20:24:15 +08:00
commit bde8c44874
22 changed files with 846 additions and 0 deletions

60
models/common_openai.py Normal file
View File

@@ -0,0 +1,60 @@
from collections.abc import Mapping
import openai
from httpx import Timeout
from dify_plugin.errors.model import (
InvokeAuthorizationError,
InvokeBadRequestError,
InvokeConnectionError,
InvokeError,
InvokeRateLimitError,
InvokeServerUnavailableError,
)
class _CommonOpenAI:
def _to_credential_kwargs(self, credentials: Mapping) -> dict:
"""
Transform credentials to kwargs for model instance
:param credentials:
:return:
"""
credentials_kwargs = {
"api_key": credentials["openai_api_key"],
"timeout": Timeout(315.0, read=300.0, write=10.0, connect=5.0),
"max_retries": 1,
}
if credentials.get("openai_api_base"):
openai_api_base = credentials["openai_api_base"].rstrip("/")
credentials_kwargs["base_url"] = openai_api_base + "/v1"
if "openai_organization" in credentials:
credentials_kwargs["organization"] = credentials["openai_organization"]
return credentials_kwargs
@property
def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
"""
Map model invoke error to unified error
The key is the error type thrown to the caller
The value is the error type thrown by the model,
which needs to be converted into a unified error type for the caller.
:return: Invoke error mapping
"""
return {
InvokeConnectionError: [openai.APIConnectionError, openai.APITimeoutError],
InvokeServerUnavailableError: [openai.InternalServerError],
InvokeRateLimitError: [openai.RateLimitError],
InvokeAuthorizationError: [openai.AuthenticationError, openai.PermissionDeniedError],
InvokeBadRequestError: [
openai.BadRequestError,
openai.NotFoundError,
openai.UnprocessableEntityError,
openai.APIError,
],
}

167
models/llm/llm.py Normal file
View File

@@ -0,0 +1,167 @@
import re
from contextlib import suppress
from typing import Mapping, Optional, Union, Generator
from dify_plugin.entities.model import (
AIModelEntity,
DefaultParameterName,
I18nObject,
ModelFeature,
ParameterRule,
ParameterType,
)
from dify_plugin.entities.model.llm import LLMResult
from dify_plugin.entities.model.message import (
PromptMessage,
PromptMessageRole,
PromptMessageTool,
SystemPromptMessage,
AssistantPromptMessage,
)
from dify_plugin.interfaces.model.openai_compatible.llm import OAICompatLargeLanguageModel
from typing import List
class OpenAILargeLanguageModel(OAICompatLargeLanguageModel):
# Pre-compiled regex for better performance
_THINK_PATTERN = re.compile(r"^<think>.*?</think>\s*", re.DOTALL)
def get_customizable_model_schema(
self, model: str, credentials: Mapping | dict
) -> AIModelEntity:
entity = super().get_customizable_model_schema(model, credentials)
agent_though_support = credentials.get("agent_though_support", "not_supported")
if agent_though_support == "supported":
try:
entity.features.index(ModelFeature.AGENT_THOUGHT)
except ValueError:
entity.features.append(ModelFeature.AGENT_THOUGHT)
structured_output_support = credentials.get("structured_output_support", "not_supported")
if structured_output_support == "supported":
# ----
# The following section should be added after the new version of `dify-plugin-sdks`
# is released.
# Related Commit:
# https://github.com/langgenius/dify-plugin-sdks/commit/0690573a879caf43f92494bf411f45a1835d96f6
# ----
# try:
# entity.features.index(ModelFeature.STRUCTURED_OUTPUT)
# except ValueError:
# entity.features.append(ModelFeature.STRUCTURED_OUTPUT)
entity.parameter_rules.append(
ParameterRule(
name=DefaultParameterName.RESPONSE_FORMAT.value,
label=I18nObject(en_US="Response Format", zh_Hans="回复格式"),
help=I18nObject(
en_US="Specifying the format that the model must output.",
zh_Hans="指定模型必须输出的格式。",
),
type=ParameterType.STRING,
options=["text", "json_object", "json_schema"],
required=False,
)
)
entity.parameter_rules.append(
ParameterRule(
name=DefaultParameterName.JSON_SCHEMA.value,
use_template=DefaultParameterName.JSON_SCHEMA.value,
)
)
if "display_name" in credentials and credentials["display_name"] != "":
entity.label = I18nObject(
en_US=credentials["display_name"], zh_Hans=credentials["display_name"]
)
entity.parameter_rules += [
ParameterRule(
name="enable_thinking",
label=I18nObject(en_US="Thinking mode", zh_Hans="思考模式"),
help=I18nObject(
en_US="Whether to enable thinking mode, applicable to various thinking mode models deployed on reasoning frameworks such as vLLM and SGLang, for example Qwen3.",
zh_Hans="是否开启思考模式适用于vLLM和SGLang等推理框架部署的多种思考模式模型例如Qwen3。",
),
type=ParameterType.BOOLEAN,
required=False,
)
]
return entity
@classmethod
def _drop_analyze_channel(cls, prompt_messages: List[PromptMessage]) -> None:
"""
Remove thinking content from assistant messages for better performance.
Uses early exit and pre-compiled regex to minimize overhead.
Args:
prompt_messages:
Returns:
"""
for p in prompt_messages:
# Early exit conditions
if not isinstance(p, AssistantPromptMessage):
continue
if not isinstance(p.content, str):
continue
# Quick check to avoid regex if not needed
if not p.content.startswith("<think>"):
continue
# Only perform regex substitution when necessary
new_content = cls._THINK_PATTERN.sub("", p.content, count=1)
# Only update if changed
if new_content != p.content:
p.content = new_content
def _invoke(
self,
model: str,
credentials: dict,
prompt_messages: list[PromptMessage],
model_parameters: dict,
tools: Optional[list[PromptMessageTool]] = None,
stop: Optional[list[str]] = None,
stream: bool = True,
user: Optional[str] = None,
) -> Union[LLMResult, Generator]:
# Compatibility adapter for Dify's 'json_schema' structured output mode.
# The base class does not natively handle the 'json_schema' parameter. This block
# translates it into a standard OpenAI-compatible request by:
# 1. Injecting the JSON schema directly into the system prompt to guide the model.
# This ensures models like gpt-4o produce the correct structured output.
if model_parameters.get("response_format") == "json_schema":
# Use .get() instead of .pop() for safety
json_schema_str = model_parameters.get("json_schema")
if json_schema_str:
structured_output_prompt = (
"Your response must be a JSON object that validates against the following JSON schema, and nothing else.\n"
f"JSON Schema: ```json\n{json_schema_str}\n```"
)
existing_system_prompt = next(
(p for p in prompt_messages if p.role == PromptMessageRole.SYSTEM), None
)
if existing_system_prompt:
existing_system_prompt.content = (
structured_output_prompt + "\n\n" + existing_system_prompt.content
)
else:
prompt_messages.insert(0, SystemPromptMessage(content=structured_output_prompt))
enable_thinking = model_parameters.pop("enable_thinking", None)
if enable_thinking is not None:
model_parameters["chat_template_kwargs"] = {"enable_thinking": bool(enable_thinking)}
# Remove thinking content from assistant messages for better performance.
with suppress(Exception):
self._drop_analyze_channel(prompt_messages)
return super()._invoke(
model, credentials, prompt_messages, model_parameters, tools, stop, stream, user
)

45
models/rerank/rerank.py Normal file
View File

@@ -0,0 +1,45 @@
from typing import Mapping
from dify_plugin.entities.model import AIModelEntity, I18nObject
from dify_plugin.interfaces.model.openai_compatible.rerank import OAICompatRerankModel
from dify_plugin.errors.model import CredentialsValidateFailedError
class OpenAIRerankModel(OAICompatRerankModel):
def validate_credentials(self, model: str, credentials: dict) -> None:
"""
Validate model credentials
:param model: model name
:param credentials: model credentials
:return:
"""
try:
self._invoke(
model=model,
credentials=credentials,
query="What is the capital of the United States?",
docs=[
"Carson City is the capital city of the American state of Nevada. At the 2010 United States "
"Census, Carson City had a population of 55,274.",
"The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that "
"are a political division controlled by the United States. Its capital is Saipan.",
],
score_threshold=0.8,
top_n=3,
)
except Exception as ex:
raise CredentialsValidateFailedError(str(ex)) from ex
def get_customizable_model_schema(
self, model: str, credentials: Mapping | dict
) -> AIModelEntity:
entity = super().get_customizable_model_schema(model, credentials)
if "display_name" in credentials and credentials["display_name"] != "":
entity.label = I18nObject(
en_US=credentials["display_name"], zh_Hans=credentials["display_name"]
)
return entity

View File

@@ -0,0 +1,27 @@
from typing import Optional
from dify_plugin.entities.model import AIModelEntity, FetchFrom, I18nObject, ModelType
from dify_plugin.interfaces.model.openai_compatible.speech2text import OAICompatSpeech2TextModel
class OpenAISpeech2TextModel(OAICompatSpeech2TextModel):
def get_customizable_model_schema(
self, model: str, credentials: dict
) -> Optional[AIModelEntity]:
"""
used to define customizable model schema
"""
entity = AIModelEntity(
model=model,
label=I18nObject(en_US=model),
fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
model_type=ModelType.SPEECH2TEXT,
model_properties={},
parameter_rules=[],
)
if "display_name" in credentials and credentials["display_name"] != "":
entity.label = I18nObject(
en_US=credentials["display_name"], zh_Hans=credentials["display_name"]
)
return entity

View File

@@ -0,0 +1,21 @@
from typing import Mapping
from dify_plugin.entities.model import AIModelEntity, I18nObject
from dify_plugin.interfaces.model.openai_compatible.text_embedding import OAICompatEmbeddingModel
class OpenAITextEmbeddingModel(OAICompatEmbeddingModel):
def get_customizable_model_schema(
self, model: str, credentials: Mapping | dict
) -> AIModelEntity:
credentials = credentials or {}
entity = super().get_customizable_model_schema(model, credentials)
if "display_name" in credentials and credentials["display_name"] != "":
entity.label = I18nObject(
en_US=credentials["display_name"], zh_Hans=credentials["display_name"]
)
return entity

20
models/tts/tts.py Normal file
View File

@@ -0,0 +1,20 @@
from typing import Mapping
from dify_plugin.entities.model import AIModelEntity, I18nObject
from dify_plugin.interfaces.model.openai_compatible.tts import OAICompatText2SpeechModel
class OpenAIText2SpeechModel(OAICompatText2SpeechModel):
def get_customizable_model_schema(
self, model: str, credentials: Mapping | dict
) -> AIModelEntity:
entity = super().get_customizable_model_schema(model, credentials)
if "display_name" in credentials and credentials["display_name"] != "":
entity.label = I18nObject(
en_US=credentials["display_name"], zh_Hans=credentials["display_name"]
)
return entity