Compare commits
No commits in common. "5350860f787d7ef68e8a89128abe100544037920" and "67b4558ef013e8ebc13e07bb0ec5db6db0d2a882" have entirely different histories.
5350860f78
...
67b4558ef0
16
.env.example
16
.env.example
@ -1,18 +1,10 @@
|
||||
# Dify API Keys Configuration
|
||||
# Format: Comma-separated list of API keys
|
||||
DIFY_API_KEYS=app-xxxxxxxxxxxxxxxx,app-yyyyyyyyyyyyyyyy,app-zzzzzzzzzzzzzzzz
|
||||
# Dify Model Configurations
|
||||
# Format: JSON string in one line
|
||||
MODEL_CONFIG={"claude-3-5-sonnet-v2":"app-xxxxxxxxxxxxxxxx","gemini-2.0-flash-thinking-exp-1219":"app-xxxxxxxxxxxxxxxx","o1-preview":"app-xxxxxxxxxxxxxxxx"}
|
||||
|
||||
# Dify API Base URL
|
||||
DIFY_API_BASE="https://api.dify.example.com/v1"
|
||||
|
||||
# 会话记忆功能模式
|
||||
# 1: 构造history_message附加到消息中的模式(默认)
|
||||
# 2: 当前的零宽字符模式
|
||||
CONVERSATION_MEMORY_MODE=1
|
||||
|
||||
# Server Configuration
|
||||
SERVER_HOST="127.0.0.1"
|
||||
SERVER_PORT=5000
|
||||
|
||||
# OpenAI compatable API Keys
|
||||
VALID_API_KEYS="sk-abc123,sk-def456"
|
||||
SERVER_PORT=5000
|
||||
231
README.md
231
README.md
@ -4,141 +4,19 @@ OpenDify 是一个将 Dify API 转换为 OpenAI API 格式的代理服务器。
|
||||
|
||||
> 🌟 本项目完全由 Cursor + Claude-3.5 自动生成,未手动编写任何代码(包括此Readme),向 AI 辅助编程的未来致敬!
|
||||
|
||||
[English Version](README_EN.md)
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 完整支持 OpenAI API 格式转换为 Dify API
|
||||
- 支持流式输出(Streaming)
|
||||
- 智能动态延迟控制,提供流畅的输出体验
|
||||
- 支持多种会话记忆模式,包括零宽字符模式和history_message模式
|
||||
- 支持 OpenAI Function Call 和 MCP Server 功能
|
||||
- 支持多个模型配置
|
||||
- 支持Dify Agent应用,处理高级工具调用(如生成图片等)
|
||||
- 完整的错误处理和日志记录
|
||||
- 兼容标准的 OpenAI API 客户端
|
||||
- 自动获取 Dify 应用信息
|
||||
|
||||
## 效果展示
|
||||
|
||||
### Function Call 和 MCP Server 支持
|
||||
|
||||
新增对 OpenAI Function Call 和 MCP Server 的支持,即使 Dify 不支持直接设置系统提示词:
|
||||
|
||||
- 自动检测请求中的 `system` 角色消息
|
||||
- 智能将系统提示词插入到用户查询中
|
||||
- 防止重复插入系统提示词
|
||||
- 完美兼容 OpenAI 的 Function Call 格式
|
||||
|
||||

|
||||
|
||||
*上图展示了 OpenDify 对 Function Call 的支持。即使 Dify 应用不支持直接设置系统提示词,通过 OpenDify 的转换,也能正确处理 MCP Server 及 Function Call 的需求。*
|
||||
|
||||
### Dify Agent应用支持
|
||||
|
||||

|
||||
|
||||
*截图展示了OpenDify代理服务支持的Dify Agent应用界面,可以看到Agent成功地处理了用户的Python多线程用法请求,并返回了相关代码示例。*
|
||||
|
||||
### 会话记忆功能
|
||||
|
||||

|
||||
|
||||
*上图展示了OpenDify的会话记忆功能。当用户提问"今天是什么天气?"时,AI能够记住之前对话中提到"今天是晴天"的上下文信息,并给出相应回复。*
|
||||
|
||||
|
||||
### 会话记忆功能
|
||||
|
||||
该代理支持自动记忆会话上下文,无需客户端进行额外处理。提供了两种会话记忆模式:
|
||||
|
||||
1. **history_message模式**:将历史消息直接附加到当前消息中,支持客户端编辑历史消息(默认)
|
||||
2. **零宽字符模式**:在每个新会话的第一条回复中,会自动嵌入不可见的会话ID,后续消息自动继承上下文
|
||||
|
||||
可以通过环境变量控制此功能:
|
||||
|
||||
```shell
|
||||
# 在 .env 文件中设置会话记忆模式
|
||||
# 1: 构造history_message附加到消息中的模式(默认)
|
||||
# 2: 零宽字符模式
|
||||
CONVERSATION_MEMORY_MODE=1
|
||||
```
|
||||
|
||||
默认情况下使用history_message模式,这种模式更灵活,支持客户端编辑历史消息,并能更好地处理系统提示词。
|
||||
|
||||
> 注意:history_message模式会将所有历史消息追加到当前消息中,可能会消耗更多的token。
|
||||
|
||||
### 流式输出优化
|
||||
|
||||
- 智能缓冲区管理
|
||||
- 动态延迟计算
|
||||
- 平滑的输出体验
|
||||
|
||||
### 配置灵活性
|
||||
|
||||
- 自动获取应用信息
|
||||
- 简化的配置方式
|
||||
- 动态模型名称映射
|
||||
- 灵活的模型配置支持
|
||||
|
||||
## 支持的模型
|
||||
|
||||
支持任意 Dify 应用,系统会自动从 Dify API 获取应用名称和信息。只需在配置文件中添加应用的 API Key 即可。
|
||||
|
||||
## API 使用
|
||||
|
||||
### List Models
|
||||
|
||||
获取所有可用模型列表:
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
openai.api_base = "http://127.0.0.1:5000/v1"
|
||||
openai.api_key = "any" # 可以使用任意值
|
||||
|
||||
# 获取可用模型列表
|
||||
models = openai.Model.list()
|
||||
print(models)
|
||||
|
||||
# 输出示例:
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "My Translation App", # Dify 应用名称
|
||||
"object": "model",
|
||||
"created": 1704603847,
|
||||
"owned_by": "dify"
|
||||
},
|
||||
{
|
||||
"id": "Code Assistant", # 另一个 Dify 应用名称
|
||||
"object": "model",
|
||||
"created": 1704603847,
|
||||
"owned_by": "dify"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
系统会自动从 Dify API 获取应用名称,并用作模型 ID。
|
||||
|
||||
### Chat Completions
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
openai.api_base = "http://127.0.0.1:5000/v1"
|
||||
openai.api_key = "any" # 可以使用任意值
|
||||
|
||||
response = openai.ChatCompletion.create(
|
||||
model="My Translation App", # 使用 Dify 应用的名称
|
||||
messages=[
|
||||
{"role": "user", "content": "你好"}
|
||||
],
|
||||
stream=True
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
print(chunk.choices[0].delta.content or "", end="")
|
||||
```
|
||||
支持任意 Dify 模型,只需在配置文件中添加对应的 API Key 即可。
|
||||
|
||||
## 快速开始
|
||||
|
||||
@ -167,13 +45,13 @@ cp .env.example .env
|
||||
- 发布应用
|
||||
- 进入"访问 API"页面,生成 API 密钥
|
||||
|
||||
> **重要说明**:Dify 不支持在请求时动态传入提示词、切换模型及其他参数。所有这些配置都需要在创建应用时设置好。Dify 会根据 API 密钥来确定使用哪个应用及其对应的配置。系统会自动从 Dify API 获取应用的名称和描述信息。
|
||||
> **重要说明**:Dify 不支持在请求时动态传入提示词、切换模型及其他参数。所有这些配置都需要在创建应用时设置好。Dify 会根据 API 密钥来确定使用哪个应用及其对应的配置。
|
||||
|
||||
3. 在 `.env` 文件中配置你的 Dify API Keys:
|
||||
3. 在 `.env` 文件中配置你的 Dify 模型和 API Keys:
|
||||
```env
|
||||
# Dify API Keys Configuration
|
||||
# Format: Comma-separated list of API keys
|
||||
DIFY_API_KEYS=app-xxxxxxxx,app-yyyyyyyy,app-zzzzzzzz
|
||||
# Dify Model Configurations
|
||||
# 注意:必须是单行的 JSON 字符串格式
|
||||
MODEL_CONFIG={"claude-3-5-sonnet-v2":"your-claude-api-key","custom-model":"your-custom-api-key"}
|
||||
|
||||
# Dify API Base URL
|
||||
DIFY_API_BASE="https://your-dify-api-base-url/v1"
|
||||
@ -183,10 +61,16 @@ SERVER_HOST="127.0.0.1"
|
||||
SERVER_PORT=5000
|
||||
```
|
||||
|
||||
配置说明:
|
||||
- `DIFY_API_KEYS`:以逗号分隔的 API Keys 列表,每个 Key 对应一个 Dify 应用
|
||||
- 系统会自动从 Dify API 获取每个应用的名称和信息
|
||||
- 无需手动配置模型名称和映射关系
|
||||
你可以根据需要添加或删除模型配置,但必须保持 JSON 格式在单行内。这是因为 python-dotenv 的限制。
|
||||
|
||||
每个模型配置的格式为:`"模型名称": "Dify应用的API密钥"`。其中:
|
||||
- 模型名称:可以自定义,用于在 API 调用时识别不同的应用
|
||||
- API 密钥:从 Dify 平台获取的应用 API 密钥
|
||||
|
||||
例如,如果你在 Dify 上创建了一个使用 Claude 的翻译应用和一个使用 Gemini 的代码助手应用,可以这样配置:
|
||||
```env
|
||||
MODEL_CONFIG={"translator":"app-xxxxxx","code-assistant":"app-yyyyyy"}
|
||||
```
|
||||
|
||||
### 运行服务
|
||||
|
||||
@ -196,6 +80,85 @@ python openai_to_dify.py
|
||||
|
||||
服务将在 `http://127.0.0.1:5000` 启动
|
||||
|
||||
## API 使用
|
||||
|
||||
### List Models
|
||||
|
||||
获取所有可用模型列表:
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
openai.api_base = "http://127.0.0.1:5000/v1"
|
||||
openai.api_key = "any" # 可以使用任意值
|
||||
|
||||
# 获取可用模型列表
|
||||
models = openai.Model.list()
|
||||
print(models)
|
||||
|
||||
# 输出示例:
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "claude-3-5-sonnet-v2",
|
||||
"object": "model",
|
||||
"created": 1704603847,
|
||||
"owned_by": "dify"
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-flash-thinking-exp-1219",
|
||||
"object": "model",
|
||||
"created": 1704603847,
|
||||
"owned_by": "dify"
|
||||
},
|
||||
// ... 其他在 MODEL_CONFIG 中配置的模型
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
只有在 `.env` 文件的 `MODEL_CONFIG` 中配置了 API Key 的模型才会出现在列表中。
|
||||
|
||||
### Chat Completions
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
openai.api_base = "http://127.0.0.1:5000/v1"
|
||||
openai.api_key = "any" # 可以使用任意值
|
||||
|
||||
response = openai.ChatCompletion.create(
|
||||
model="claude-3-5-sonnet-v2", # 使用在 MODEL_CONFIG 中配置的模型名称
|
||||
messages=[
|
||||
{"role": "user", "content": "你好"}
|
||||
],
|
||||
stream=True
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
print(chunk.choices[0].delta.content or "", end="")
|
||||
```
|
||||
|
||||
## 特性说明
|
||||
|
||||
### 流式输出优化
|
||||
|
||||
- 智能缓冲区管理
|
||||
- 动态延迟计算
|
||||
- 平滑的输出体验
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 完整的错误捕获和处理
|
||||
- 详细的日志记录
|
||||
- 友好的错误提示
|
||||
|
||||
### 配置灵活性
|
||||
|
||||
- 支持动态添加新模型
|
||||
- 支持 JSON 格式配置
|
||||
- 支持自定义模型名称
|
||||
|
||||
## 贡献指南
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来帮助改进项目。
|
||||
|
||||
210
README_EN.md
210
README_EN.md
@ -1,210 +0,0 @@
|
||||
# OpenDify
|
||||
|
||||
OpenDify is a proxy server that transforms the Dify API into OpenAI API format. It allows direct interaction with Dify services using any OpenAI API client.
|
||||
|
||||
> 🌟 This project was fully generated by Cursor + Claude-3.5, without any manual coding (including this README), salute to the future of AI-assisted programming!
|
||||
|
||||
[中文版本](README.md)
|
||||
|
||||
## Features
|
||||
|
||||
- Full support for converting OpenAI API formats to Dify API
|
||||
- Streaming output support
|
||||
- Intelligent dynamic delay control for smooth output experience
|
||||
- Multiple conversation memory modes, including zero-width character mode and history_message mode
|
||||
- Support for OpenAI Function Call and MCP Server functionality
|
||||
- Support for multiple model configurations
|
||||
- Support for Dify Agent applications with advanced tool calls (like image generation)
|
||||
- Compatible with standard OpenAI API clients
|
||||
- Automatic fetching of Dify application information
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Function Call and MCP Server Support
|
||||
|
||||

|
||||
|
||||
*The above image demonstrates OpenDify's support for Function Call. Even though Dify applications don't support setting system prompts directly, through OpenDify's conversion, it can correctly handle MCP Server and Function Call requirements.*
|
||||
|
||||
### Dify Agent Application Support
|
||||
|
||||

|
||||
|
||||
*The screenshot shows the Dify Agent application interface supported by the OpenDify proxy service. It demonstrates how the Agent successfully processes a user's request about Python multithreading usage and returns relevant code examples.*
|
||||
|
||||
### Conversation Memory Feature
|
||||
|
||||

|
||||
|
||||
*The above image demonstrates the conversation memory feature of OpenDify. When the user asks "What's the weather today?", the AI remembers the context from previous conversations that "today is sunny" and provides an appropriate response.*
|
||||
|
||||
## Detailed Features
|
||||
|
||||
### Function Call and MCP Server Support
|
||||
|
||||
Added support for OpenAI Function Call and MCP Server, even though Dify doesn't support setting system prompts directly:
|
||||
|
||||
- Automatically detects `system` role messages in requests
|
||||
- Intelligently inserts system prompts into user queries
|
||||
- Prevents duplicate insertion of system prompts
|
||||
- Perfectly compatible with OpenAI Function Call format
|
||||
|
||||
### Conversation Memory
|
||||
|
||||
The proxy supports automatic remembering of conversation context without requiring additional processing by the client. It provides three conversation memory modes:
|
||||
|
||||
1. **No conversation memory**: Each conversation is independent, with no context association
|
||||
2. **history_message mode**: Directly appends historical messages to the current message, supporting client-side editing of historical messages
|
||||
3. **Zero-width character mode**: Automatically embeds an invisible session ID in the first reply of each new conversation, and subsequent messages automatically inherit the context
|
||||
|
||||
This feature can be controlled via environment variable:
|
||||
|
||||
```shell
|
||||
# Set conversation memory mode in the .env file
|
||||
# 0: No conversation memory
|
||||
# 1: Construct history_message attached to the message
|
||||
# 2: Zero-width character mode (default)
|
||||
CONVERSATION_MEMORY_MODE=2
|
||||
```
|
||||
|
||||
Zero-width character mode is used by default. For scenarios that need to support client-side editing of historical messages, the history_message mode is recommended.
|
||||
|
||||
> Note: history_message mode will append all historical messages to the current message, which may consume more tokens.
|
||||
|
||||
### Streaming Output Optimization
|
||||
|
||||
- Intelligent buffer management
|
||||
- Dynamic delay calculation
|
||||
- Smooth output experience
|
||||
|
||||
### Configuration Flexibility
|
||||
|
||||
- Automatic application information retrieval
|
||||
- Simplified configuration method
|
||||
- Dynamic model name mapping
|
||||
|
||||
## Supported Models
|
||||
|
||||
Supports any Dify application. The system automatically retrieves application names and information from the Dify API. Simply add the API Key for the application in the configuration file.
|
||||
|
||||
## API Usage
|
||||
|
||||
### List Models
|
||||
|
||||
Get a list of all available models:
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
openai.api_base = "http://127.0.0.1:5000/v1"
|
||||
openai.api_key = "any" # Can use any value
|
||||
|
||||
# Get available models
|
||||
models = openai.Model.list()
|
||||
print(models)
|
||||
|
||||
# Example output:
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "My Translation App", # Dify application name
|
||||
"object": "model",
|
||||
"created": 1704603847,
|
||||
"owned_by": "dify"
|
||||
},
|
||||
{
|
||||
"id": "Code Assistant", # Another Dify application name
|
||||
"object": "model",
|
||||
"created": 1704603847,
|
||||
"owned_by": "dify"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The system automatically retrieves application names from the Dify API and uses them as model IDs.
|
||||
|
||||
### Chat Completions
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
openai.api_base = "http://127.0.0.1:5000/v1"
|
||||
openai.api_key = "any" # Can use any value
|
||||
|
||||
response = openai.ChatCompletion.create(
|
||||
model="My Translation App", # Use the Dify application name
|
||||
messages=[
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
stream=True
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
print(chunk.choices[0].delta.content or "", end="")
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Requirements
|
||||
|
||||
- Python 3.9+
|
||||
- pip
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
1. Copy the `.env.example` file and rename it to `.env`:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Configure your application on the Dify platform:
|
||||
- Log in to the Dify platform and enter the workspace
|
||||
- Click "Create Application" and configure the required models (such as Claude, Gemini, etc.)
|
||||
- Configure the application prompts and other parameters
|
||||
- Publish the application
|
||||
- Go to the "Access API" page and generate an API key
|
||||
|
||||
> **Important Note**: Dify does not support dynamically passing prompts, switching models, or other parameters in requests. All these configurations need to be set when creating the application. Dify determines which application and its corresponding configuration to use based on the API key. The system will automatically retrieve the application's name and description information from the Dify API.
|
||||
|
||||
3. Configure your Dify API Keys in the `.env` file:
|
||||
```env
|
||||
# Dify API Keys Configuration
|
||||
# Format: Comma-separated list of API keys
|
||||
DIFY_API_KEYS=app-xxxxxxxx,app-yyyyyyyy,app-zzzzzzzz
|
||||
|
||||
# Dify API Base URL
|
||||
DIFY_API_BASE="https://your-dify-api-base-url/v1"
|
||||
|
||||
# Server Configuration
|
||||
SERVER_HOST="127.0.0.1"
|
||||
SERVER_PORT=5000
|
||||
```
|
||||
|
||||
Configuration notes:
|
||||
- `DIFY_API_KEYS`: A comma-separated list of API Keys, each corresponding to a Dify application
|
||||
- The system automatically retrieves the name and information of each application from the Dify API
|
||||
- No need to manually configure model names and mapping relationships
|
||||
|
||||
### Running the Service
|
||||
|
||||
```bash
|
||||
python openai_to_dify.py
|
||||
```
|
||||
|
||||
The service will start at `http://127.0.0.1:5000`
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
Issues and Pull Requests are welcome to help improve the project.
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE)
|
||||
BIN
images/1.png
BIN
images/1.png
Binary file not shown.
|
Before Width: | Height: | Size: 300 KiB |
BIN
images/2.png
BIN
images/2.png
Binary file not shown.
|
Before Width: | Height: | Size: 329 KiB |
BIN
images/3.png
BIN
images/3.png
Binary file not shown.
|
Before Width: | Height: | Size: 285 KiB |
517
main.py
517
main.py
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from flask import Flask, request, Response, stream_with_context, jsonify
|
||||
from flask import Flask, request, Response, stream_with_context
|
||||
import httpx
|
||||
import time
|
||||
from dotenv import load_dotenv
|
||||
@ -10,102 +10,62 @@ import ast
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 设置httpx的日志级别为WARNING,减少不必要的输出
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
# 设置 httpx 的日志级别
|
||||
logging.getLogger("httpx").setLevel(logging.DEBUG)
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 从环境变量读取有效的API密钥(逗号分隔)
|
||||
VALID_API_KEYS = [key.strip() for key in os.getenv("VALID_API_KEYS", "").split(",") if key]
|
||||
|
||||
# 获取会话记忆功能模式配置
|
||||
# 1: 构造history_message附加到消息中的模式(默认)
|
||||
# 2: 零宽字符模式
|
||||
CONVERSATION_MEMORY_MODE = int(os.getenv('CONVERSATION_MEMORY_MODE', '1'))
|
||||
|
||||
class DifyModelManager:
|
||||
def __init__(self):
|
||||
self.api_keys = []
|
||||
self.name_to_api_key = {} # 应用名称到API Key的映射
|
||||
self.api_key_to_name = {} # API Key到应用名称的映射
|
||||
self.load_api_keys()
|
||||
|
||||
def load_api_keys(self):
|
||||
"""从环境变量加载API Keys"""
|
||||
api_keys_str = os.getenv('DIFY_API_KEYS', '')
|
||||
if api_keys_str:
|
||||
self.api_keys = [key.strip() for key in api_keys_str.split(',') if key.strip()]
|
||||
logger.info(f"Loaded {len(self.api_keys)} API keys")
|
||||
|
||||
async def fetch_app_info(self, api_key):
|
||||
"""获取Dify应用信息"""
|
||||
def parse_model_config():
|
||||
"""
|
||||
从环境变量解析模型配置
|
||||
返回一个字典 {model_name: api_key}
|
||||
"""
|
||||
try:
|
||||
config_str = os.getenv('MODEL_CONFIG', '{}')
|
||||
# 尝试作为Python字典解析
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
response = await client.get(
|
||||
f"{DIFY_API_BASE}/info",
|
||||
headers=headers,
|
||||
params={"user": "default_user"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
app_info = response.json()
|
||||
return app_info.get("name", "Unknown App")
|
||||
else:
|
||||
logger.error(f"Failed to fetch app info for API key: {api_key[:8]}...")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching app info: {str(e)}")
|
||||
return None
|
||||
return ast.literal_eval(config_str)
|
||||
except (SyntaxError, ValueError) as e:
|
||||
logger.error(f"Failed to parse MODEL_CONFIG as Python dict: {e}")
|
||||
try:
|
||||
# 尝试作为JSON解析
|
||||
return json.loads(config_str)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse MODEL_CONFIG as JSON: {e}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing MODEL_CONFIG: {e}")
|
||||
return {}
|
||||
|
||||
async def refresh_model_info(self):
|
||||
"""刷新所有应用信息"""
|
||||
self.name_to_api_key.clear()
|
||||
self.api_key_to_name.clear()
|
||||
|
||||
for api_key in self.api_keys:
|
||||
app_name = await self.fetch_app_info(api_key)
|
||||
if app_name:
|
||||
self.name_to_api_key[app_name] = api_key
|
||||
self.api_key_to_name[api_key] = app_name
|
||||
logger.info(f"Mapped app '{app_name}' to API key: {api_key[:8]}...")
|
||||
# 从环境变量获取配置
|
||||
MODEL_TO_API_KEY = parse_model_config()
|
||||
|
||||
def get_api_key(self, model_name):
|
||||
"""根据模型名称获取API Key"""
|
||||
return self.name_to_api_key.get(model_name)
|
||||
|
||||
def get_available_models(self):
|
||||
"""获取可用模型列表"""
|
||||
return [
|
||||
{
|
||||
"id": name,
|
||||
"object": "model",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "dify"
|
||||
}
|
||||
for name in self.name_to_api_key.keys()
|
||||
]
|
||||
|
||||
# 创建模型管理器实例
|
||||
model_manager = DifyModelManager()
|
||||
|
||||
# 从环境变量获取API基础URL
|
||||
DIFY_API_BASE = os.getenv("DIFY_API_BASE", "")
|
||||
# 根据MODEL_TO_API_KEY自动生成模型信息
|
||||
AVAILABLE_MODELS = [
|
||||
{
|
||||
"id": model_id,
|
||||
"object": "model",
|
||||
"created": int(time.time()),
|
||||
"owned_by": "dify"
|
||||
}
|
||||
for model_id, api_key in MODEL_TO_API_KEY.items()
|
||||
if api_key is not None # 只包含配置了API Key的模型
|
||||
]
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# 从环境变量获取API基础URL
|
||||
DIFY_API_BASE = os.getenv("DIFY_API_BASE", "https://mify-be.pt.xiaomi.com/api/v1")
|
||||
|
||||
def get_api_key(model_name):
|
||||
"""根据模型名称获取对应的API密钥"""
|
||||
api_key = model_manager.get_api_key(model_name)
|
||||
api_key = MODEL_TO_API_KEY.get(model_name)
|
||||
if not api_key:
|
||||
logger.warning(f"No API key found for model: {model_name}")
|
||||
return api_key
|
||||
@ -117,80 +77,23 @@ def transform_openai_to_dify(openai_request, endpoint):
|
||||
messages = openai_request.get("messages", [])
|
||||
stream = openai_request.get("stream", False)
|
||||
|
||||
# 尝试从历史消息中提取conversation_id
|
||||
conversation_id = None
|
||||
|
||||
# 提取system消息内容
|
||||
system_content = ""
|
||||
system_messages = [msg for msg in messages if msg.get("role") == "system"]
|
||||
if system_messages:
|
||||
system_content = system_messages[0].get("content", "")
|
||||
# 记录找到的system消息
|
||||
logger.info(f"Found system message: {system_content[:100]}{'...' if len(system_content) > 100 else ''}")
|
||||
|
||||
if CONVERSATION_MEMORY_MODE == 2: # 零宽字符模式
|
||||
if len(messages) > 1:
|
||||
# 遍历历史消息,找到最近的assistant消息
|
||||
for msg in reversed(messages[:-1]): # 除了最后一条消息
|
||||
if msg.get("role") == "assistant":
|
||||
content = msg.get("content", "")
|
||||
# 尝试解码conversation_id
|
||||
conversation_id = decode_conversation_id(content)
|
||||
if conversation_id:
|
||||
break
|
||||
|
||||
# 获取最后一条用户消息
|
||||
user_query = messages[-1]["content"] if messages and messages[-1].get("role") != "system" else ""
|
||||
|
||||
# 如果有system消息且是首次对话(没有conversation_id),则将system内容添加到用户查询前
|
||||
if system_content and not conversation_id:
|
||||
user_query = f"系统指令: {system_content}\n\n用户问题: {user_query}"
|
||||
logger.info(f"[零宽字符模式] 首次对话,添加system内容到查询前")
|
||||
|
||||
dify_request = {
|
||||
"inputs": {},
|
||||
"query": user_query,
|
||||
"response_mode": "streaming" if stream else "blocking",
|
||||
"conversation_id": conversation_id,
|
||||
"user": openai_request.get("user", "default_user")
|
||||
}
|
||||
else: # history_message模式(默认)
|
||||
# 获取最后一条用户消息
|
||||
user_query = messages[-1]["content"] if messages and messages[-1].get("role") != "system" else ""
|
||||
|
||||
# 构造历史消息
|
||||
if len(messages) > 1:
|
||||
history_messages = []
|
||||
has_system_in_history = False
|
||||
|
||||
# 检查历史消息中是否已经包含system消息
|
||||
for msg in messages[:-1]: # 除了最后一条消息
|
||||
role = msg.get("role", "")
|
||||
content = msg.get("content", "")
|
||||
if role and content:
|
||||
if role == "system":
|
||||
has_system_in_history = True
|
||||
history_messages.append(f"{role}: {content}")
|
||||
|
||||
# 如果历史中没有system消息但现在有system消息,则添加到历史的最前面
|
||||
if system_content and not has_system_in_history:
|
||||
history_messages.insert(0, f"system: {system_content}")
|
||||
logger.info(f"[history_message模式] 添加system内容到历史消息前")
|
||||
|
||||
# 将历史消息添加到查询中
|
||||
if history_messages:
|
||||
history_context = "\n\n".join(history_messages)
|
||||
user_query = f"<history>\n{history_context}\n</history>\n\n用户当前问题: {user_query}"
|
||||
elif system_content: # 没有历史消息但有system消息
|
||||
user_query = f"系统指令: {system_content}\n\n用户问题: {user_query}"
|
||||
logger.info(f"[history_message模式] 首次对话,添加system内容到查询前")
|
||||
|
||||
dify_request = {
|
||||
"inputs": {},
|
||||
"query": user_query,
|
||||
"response_mode": "streaming" if stream else "blocking",
|
||||
"user": openai_request.get("user", "default_user")
|
||||
}
|
||||
dify_request = {
|
||||
"inputs": {},
|
||||
"query": messages[-1]["content"] if messages else "",
|
||||
"response_mode": "streaming" if stream else "blocking",
|
||||
"conversation_id": openai_request.get("conversation_id", None),
|
||||
"user": openai_request.get("user", "default_user")
|
||||
}
|
||||
|
||||
# 添加历史消息
|
||||
if len(messages) > 1:
|
||||
history = []
|
||||
for msg in messages[:-1]: # 除了最后一条消息
|
||||
history.append({
|
||||
"role": msg["role"],
|
||||
"content": msg["content"]
|
||||
})
|
||||
dify_request["conversation_history"] = history
|
||||
|
||||
return dify_request
|
||||
|
||||
@ -200,55 +103,16 @@ def transform_dify_to_openai(dify_response, model="claude-3-5-sonnet-v2", stream
|
||||
"""将Dify格式的响应转换为OpenAI格式"""
|
||||
|
||||
if not stream:
|
||||
# 首先获取回答内容,支持不同的响应模式
|
||||
answer = ""
|
||||
mode = dify_response.get("mode", "")
|
||||
|
||||
# 普通聊天模式
|
||||
if "answer" in dify_response:
|
||||
answer = dify_response.get("answer", "")
|
||||
|
||||
# 如果是Agent模式,需要从agent_thoughts中提取回答
|
||||
elif "agent_thoughts" in dify_response:
|
||||
# Agent模式下通常最后一个thought包含最终答案
|
||||
agent_thoughts = dify_response.get("agent_thoughts", [])
|
||||
if agent_thoughts:
|
||||
for thought in agent_thoughts:
|
||||
if thought.get("thought"):
|
||||
answer = thought.get("thought", "")
|
||||
|
||||
# 只在零宽字符会话记忆模式时处理conversation_id
|
||||
if CONVERSATION_MEMORY_MODE == 2:
|
||||
conversation_id = dify_response.get("conversation_id", "")
|
||||
history = dify_response.get("conversation_history", [])
|
||||
|
||||
# 检查历史消息中是否已经有会话ID
|
||||
has_conversation_id = False
|
||||
if history:
|
||||
for msg in history:
|
||||
if msg.get("role") == "assistant":
|
||||
content = msg.get("content", "")
|
||||
if decode_conversation_id(content) is not None:
|
||||
has_conversation_id = True
|
||||
break
|
||||
|
||||
# 只在新会话且历史消息中没有会话ID时插入
|
||||
if conversation_id and not has_conversation_id:
|
||||
logger.info(f"[Debug] Inserting conversation_id: {conversation_id}, history_length: {len(history)}")
|
||||
encoded = encode_conversation_id(conversation_id)
|
||||
answer = answer + encoded
|
||||
logger.info(f"[Debug] Response content after insertion: {repr(answer)}")
|
||||
|
||||
return {
|
||||
"id": dify_response.get("message_id", ""),
|
||||
"object": "chat.completion",
|
||||
"created": dify_response.get("created", int(time.time())),
|
||||
"model": model,
|
||||
"model": model, # 使用实际使用的模型
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": answer
|
||||
"content": dify_response.get("answer", "")
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}]
|
||||
@ -273,164 +137,19 @@ def create_openai_stream_response(content, message_id, model="claude-3-5-sonnet-
|
||||
}]
|
||||
}
|
||||
|
||||
def encode_conversation_id(conversation_id):
|
||||
"""将conversation_id编码为不可见的字符序列"""
|
||||
if not conversation_id:
|
||||
return ""
|
||||
|
||||
# 使用Base64编码减少长度
|
||||
import base64
|
||||
encoded = base64.b64encode(conversation_id.encode()).decode()
|
||||
|
||||
# 使用8种不同的零宽字符表示3位数字
|
||||
# 这样可以将编码长度进一步减少
|
||||
char_map = {
|
||||
'0': '\u200b', # 零宽空格
|
||||
'1': '\u200c', # 零宽非连接符
|
||||
'2': '\u200d', # 零宽连接符
|
||||
'3': '\ufeff', # 零宽非断空格
|
||||
'4': '\u2060', # 词组连接符
|
||||
'5': '\u180e', # 蒙古语元音分隔符
|
||||
'6': '\u2061', # 函数应用
|
||||
'7': '\u2062', # 不可见乘号
|
||||
}
|
||||
|
||||
# 将Base64字符串转换为八进制数字
|
||||
result = []
|
||||
for c in encoded:
|
||||
# 将每个字符转换为8进制数字(0-7)
|
||||
if c.isalpha():
|
||||
if c.isupper():
|
||||
val = ord(c) - ord('A')
|
||||
else:
|
||||
val = ord(c) - ord('a') + 26
|
||||
elif c.isdigit():
|
||||
val = int(c) + 52
|
||||
elif c == '+':
|
||||
val = 62
|
||||
elif c == '/':
|
||||
val = 63
|
||||
else: # '='
|
||||
val = 0
|
||||
|
||||
# 每个Base64字符可以产生2个3位数字
|
||||
first = (val >> 3) & 0x7
|
||||
second = val & 0x7
|
||||
result.append(char_map[str(first)])
|
||||
if c != '=': # 不编码填充字符的后半部分
|
||||
result.append(char_map[str(second)])
|
||||
|
||||
return ''.join(result)
|
||||
|
||||
def decode_conversation_id(content):
|
||||
"""从消息内容中解码conversation_id"""
|
||||
try:
|
||||
# 零宽字符到3位数字的映射
|
||||
char_to_val = {
|
||||
'\u200b': '0', # 零宽空格
|
||||
'\u200c': '1', # 零宽非连接符
|
||||
'\u200d': '2', # 零宽连接符
|
||||
'\ufeff': '3', # 零宽非断空格
|
||||
'\u2060': '4', # 词组连接符
|
||||
'\u180e': '5', # 蒙古语元音分隔符
|
||||
'\u2061': '6', # 函数应用
|
||||
'\u2062': '7', # 不可见乘号
|
||||
}
|
||||
|
||||
# 提取最后一段零宽字符序列
|
||||
space_chars = []
|
||||
for c in reversed(content):
|
||||
if c not in char_to_val:
|
||||
break
|
||||
space_chars.append(c)
|
||||
|
||||
if not space_chars:
|
||||
return None
|
||||
|
||||
# 将零宽字符转换回Base64字符串
|
||||
space_chars.reverse()
|
||||
base64_chars = []
|
||||
for i in range(0, len(space_chars), 2):
|
||||
first = int(char_to_val[space_chars[i]], 8)
|
||||
if i + 1 < len(space_chars):
|
||||
second = int(char_to_val[space_chars[i + 1]], 8)
|
||||
val = (first << 3) | second
|
||||
else:
|
||||
val = first << 3
|
||||
|
||||
# 转换回Base64字符
|
||||
if val < 26:
|
||||
base64_chars.append(chr(val + ord('A')))
|
||||
elif val < 52:
|
||||
base64_chars.append(chr(val - 26 + ord('a')))
|
||||
elif val < 62:
|
||||
base64_chars.append(str(val - 52))
|
||||
elif val == 62:
|
||||
base64_chars.append('+')
|
||||
else:
|
||||
base64_chars.append('/')
|
||||
|
||||
# 添加Base64填充
|
||||
padding = len(base64_chars) % 4
|
||||
if padding:
|
||||
base64_chars.extend(['='] * (4 - padding))
|
||||
|
||||
# 解码Base64字符串
|
||||
import base64
|
||||
base64_str = ''.join(base64_chars)
|
||||
return base64.b64decode(base64_str).decode()
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to decode conversation_id: {e}")
|
||||
return None
|
||||
|
||||
@app.route('/v1/chat/completions', methods=['POST'])
|
||||
def chat_completions():
|
||||
try:
|
||||
# 新增:验证API密钥
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header:
|
||||
return jsonify({
|
||||
"error": {
|
||||
"message": "Missing Authorization header",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "invalid_api_key"
|
||||
}
|
||||
}), 401
|
||||
|
||||
parts = auth_header.split()
|
||||
if len(parts) != 2 or parts[0].lower() != 'bearer':
|
||||
return jsonify({
|
||||
"error": {
|
||||
"message": "Invalid Authorization header format. Expected: Bearer <API_KEY>",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "invalid_api_key"
|
||||
}
|
||||
}), 401
|
||||
|
||||
provided_api_key = parts[1]
|
||||
if provided_api_key not in VALID_API_KEYS:
|
||||
return jsonify({
|
||||
"error": {
|
||||
"message": "Invalid API key",
|
||||
"type": "invalid_request_error",
|
||||
"param": None,
|
||||
"code": "invalid_api_key"
|
||||
}
|
||||
}), 401
|
||||
|
||||
# 继续处理原始逻辑
|
||||
openai_request = request.get_json()
|
||||
logger.info(f"Received request: {json.dumps(openai_request, ensure_ascii=False)}")
|
||||
|
||||
model = openai_request.get("model", "claude-3-5-sonnet-v2")
|
||||
logger.info(f"Using model: {model}")
|
||||
|
||||
# 验证模型是否支持
|
||||
api_key = get_api_key(model)
|
||||
if not api_key:
|
||||
error_msg = f"Model {model} is not supported. Available models: {', '.join(model_manager.name_to_api_key.keys())}"
|
||||
error_msg = f"Model {model} is not supported. Available models: {', '.join(MODEL_TO_API_KEY.keys())}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"error": {
|
||||
@ -441,6 +160,7 @@ def chat_completions():
|
||||
}, 404
|
||||
|
||||
dify_request = transform_openai_to_dify(openai_request, "/chat/completions")
|
||||
logger.info(f"Transformed request: {json.dumps(dify_request, ensure_ascii=False)}")
|
||||
|
||||
if not dify_request:
|
||||
logger.error("Failed to transform request")
|
||||
@ -560,63 +280,6 @@ def chat_completions():
|
||||
# 立即继续处理下一个请求
|
||||
continue
|
||||
|
||||
# 处理Agent模式的消息事件
|
||||
elif dify_chunk.get("event") == "agent_message" and "answer" in dify_chunk:
|
||||
current_answer = dify_chunk["answer"]
|
||||
if not current_answer:
|
||||
continue
|
||||
|
||||
message_id = dify_chunk.get("message_id", "")
|
||||
if not generate.message_id:
|
||||
generate.message_id = message_id
|
||||
|
||||
# 将当前批次的字符添加到输出缓冲区
|
||||
for char in current_answer:
|
||||
output_buffer.append((char, generate.message_id))
|
||||
|
||||
# 根据缓冲区大小动态调整输出速度
|
||||
while output_buffer:
|
||||
char, msg_id = output_buffer.pop(0)
|
||||
yield send_char(char, msg_id)
|
||||
# 根据剩余缓冲区大小计算延迟
|
||||
delay = calculate_delay(len(output_buffer))
|
||||
time.sleep(delay)
|
||||
|
||||
# 立即继续处理下一个请求
|
||||
continue
|
||||
|
||||
# 处理Agent的思考过程,记录日志但不输出给用户
|
||||
elif dify_chunk.get("event") == "agent_thought":
|
||||
thought_id = dify_chunk.get("id", "")
|
||||
thought = dify_chunk.get("thought", "")
|
||||
tool = dify_chunk.get("tool", "")
|
||||
tool_input = dify_chunk.get("tool_input", "")
|
||||
observation = dify_chunk.get("observation", "")
|
||||
|
||||
logger.info(f"[Agent Thought] ID: {thought_id}, Tool: {tool}")
|
||||
if thought:
|
||||
logger.info(f"[Agent Thought] Thought: {thought}")
|
||||
if tool_input:
|
||||
logger.info(f"[Agent Thought] Tool Input: {tool_input}")
|
||||
if observation:
|
||||
logger.info(f"[Agent Thought] Observation: {observation}")
|
||||
|
||||
# 获取message_id以关联思考和最终输出
|
||||
message_id = dify_chunk.get("message_id", "")
|
||||
if not generate.message_id and message_id:
|
||||
generate.message_id = message_id
|
||||
|
||||
continue
|
||||
|
||||
# 处理消息中的文件(如图片),记录日志但不直接输出给用户
|
||||
elif dify_chunk.get("event") == "message_file":
|
||||
file_id = dify_chunk.get("id", "")
|
||||
file_type = dify_chunk.get("type", "")
|
||||
file_url = dify_chunk.get("url", "")
|
||||
|
||||
logger.info(f"[Message File] ID: {file_id}, Type: {file_type}, URL: {file_url}")
|
||||
continue
|
||||
|
||||
elif dify_chunk.get("event") == "message_end":
|
||||
# 快速输出剩余内容
|
||||
while output_buffer:
|
||||
@ -624,28 +287,6 @@ def chat_completions():
|
||||
yield send_char(char, msg_id)
|
||||
time.sleep(0.001) # 固定使用最小延迟快速输出剩余内容
|
||||
|
||||
# 只在零宽字符会话记忆模式时处理conversation_id
|
||||
if CONVERSATION_MEMORY_MODE == 2:
|
||||
conversation_id = dify_chunk.get("conversation_id")
|
||||
history = dify_chunk.get("conversation_history", [])
|
||||
|
||||
has_conversation_id = False
|
||||
if history:
|
||||
for msg in history:
|
||||
if msg.get("role") == "assistant":
|
||||
content = msg.get("content", "")
|
||||
if decode_conversation_id(content) is not None:
|
||||
has_conversation_id = True
|
||||
break
|
||||
|
||||
# 只在新会话且历史消息中没有会话ID时插入
|
||||
if conversation_id and not has_conversation_id:
|
||||
logger.info(f"[Debug] Inserting conversation_id in stream: {conversation_id}")
|
||||
encoded = encode_conversation_id(conversation_id)
|
||||
logger.info(f"[Debug] Stream encoded content: {repr(encoded)}")
|
||||
for char in encoded:
|
||||
yield send_char(char, generate.message_id)
|
||||
|
||||
final_chunk = {
|
||||
"id": generate.message_id,
|
||||
"object": "chat.completion.chunk",
|
||||
@ -706,20 +347,8 @@ def chat_completions():
|
||||
|
||||
dify_response = response.json()
|
||||
logger.info(f"Received response from Dify: {json.dumps(dify_response, ensure_ascii=False)}")
|
||||
logger.info(f"[Debug] Response content: {repr(dify_response.get('answer', ''))}")
|
||||
openai_response = transform_dify_to_openai(dify_response, model=model)
|
||||
conversation_id = dify_response.get("conversation_id")
|
||||
if conversation_id:
|
||||
# 在响应头中传递conversation_id
|
||||
return Response(
|
||||
json.dumps(openai_response),
|
||||
content_type='application/json',
|
||||
headers={
|
||||
'Conversation-Id': conversation_id
|
||||
}
|
||||
)
|
||||
else:
|
||||
return openai_response
|
||||
return openai_response
|
||||
except httpx.RequestError as e:
|
||||
error_msg = f"Failed to connect to Dify: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
@ -746,12 +375,11 @@ def chat_completions():
|
||||
def list_models():
|
||||
"""返回可用的模型列表"""
|
||||
logger.info("Listing available models")
|
||||
|
||||
# 刷新模型信息
|
||||
asyncio.run(model_manager.refresh_model_info())
|
||||
|
||||
# 获取可用模型列表
|
||||
available_models = model_manager.get_available_models()
|
||||
# 过滤掉没有API密钥的模型
|
||||
available_models = [
|
||||
model for model in AVAILABLE_MODELS
|
||||
if MODEL_TO_API_KEY.get(model["id"])
|
||||
]
|
||||
|
||||
response = {
|
||||
"object": "list",
|
||||
@ -760,14 +388,7 @@ def list_models():
|
||||
logger.info(f"Available models: {json.dumps(response, ensure_ascii=False)}")
|
||||
return response
|
||||
|
||||
# 在main.py的最后初始化时添加环境变量检查:
|
||||
if __name__ == '__main__':
|
||||
if not VALID_API_KEYS:
|
||||
print("Warning: No API keys configured. Set the VALID_API_KEYS environment variable with comma-separated keys.")
|
||||
|
||||
# 启动时初始化模型信息
|
||||
asyncio.run(model_manager.refresh_model_info())
|
||||
|
||||
host = os.getenv("SERVER_HOST", "127.0.0.1")
|
||||
port = int(os.getenv("SERVER_PORT", 5000))
|
||||
logger.info(f"Starting server on http://{host}:{port}")
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
flask
|
||||
httpx
|
||||
python-dotenv
|
||||
python-dotenv
|
||||
logging
|
||||
Loading…
x
Reference in New Issue
Block a user