Flask / app.py
huijio's picture
Update app.py
7a7336f verified
from flask import Flask, request, jsonify
import requests
import json
import os
import logging
from typing import Optional, Dict, Any
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Configuration from environment variables
IFLOW_API_KEY = os.getenv("IFLOW_API_KEY")
PROXY_AUTH_KEY = os.getenv("PROXY_AUTH_KEY", "your-proxy-auth-key")
IFLOW_API_URL = 'https://apis.iflow.cn/v1/chat/completions'
DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "glm-4.6")
@app.route('/')
def home():
return "iFlow Proxy for Panda App is running! (Using /groq endpoint)"
@app.route('/groq', methods=['POST'])
def proxy_to_groq():
"""Main proxy endpoint - keeps /groq URL but uses iFlow API internally"""
try:
# Validate proxy authentication from header
auth_header = request.headers.get('X-API-Key') or request.headers.get('Authorization')
if auth_header:
# Check if it's Bearer token format
if auth_header.startswith('Bearer '):
auth_key = auth_header.replace('Bearer ', '')
else:
auth_key = auth_header
if auth_key != PROXY_AUTH_KEY:
logger.warning("Unauthorized request with invalid API key")
return jsonify({
'error': {
'message': 'Invalid authentication credentials',
'type': 'invalid_request_error',
'code': 'invalid_api_key'
}
}), 401
else:
logger.warning("No authentication header provided")
return jsonify({
'error': {
'message': 'You must provide an API key',
'type': 'invalid_request_error',
'code': 'missing_api_key'
}
}), 401
# Get the Gemini format payload from app
gemini_payload = request.get_json()
if not gemini_payload:
logger.error("No JSON payload received")
return jsonify({
'error': {
'message': 'No JSON payload received',
'type': 'invalid_request_error'
}
}), 400
logger.info(f"Received payload: {json.dumps(gemini_payload, indent=2)[:500]}...")
# Check for streaming request
stream = gemini_payload.get('stream', False)
# Transform Gemini format to iFlow (OpenAI-compatible) format
iflow_payload = {
'model': gemini_payload.get('model', DEFAULT_MODEL),
'messages': [],
'stream': stream
}
# Copy common parameters if present
common_params = ['temperature', 'max_tokens', 'top_p', 'frequency_penalty',
'presence_penalty', 'stop', 'seed', 'response_format']
for param in common_params:
if param in gemini_payload:
iflow_payload[param] = gemini_payload[param]
# Convert messages from Gemini parts format to OpenAI content format
messages = gemini_payload.get('messages', [])
if not messages:
logger.error("No messages in payload")
return jsonify({
'error': {
'message': 'No messages in payload',
'type': 'invalid_request_error'
}
}), 400
for msg in messages:
# Extract text from parts array
parts = msg.get('parts', [])
# Handle both Gemini format (parts array) and OpenAI format (content string)
if parts:
# Gemini format: parts array with text
content_parts = []
for part in parts:
text = part.get('text', '')
if text:
content_parts.append(text)
if not content_parts:
logger.warning(f"Message with role '{msg.get('role')}' has no text content")
continue
content = '\n'.join(content_parts)
else:
# OpenAI format: direct content field
content = msg.get('content', '')
if not content:
logger.warning(f"Message with role '{msg.get('role')}' has no content")
continue
# Map Gemini roles to OpenAI roles
role = msg.get('role')
if role == 'model':
role = 'assistant' # OpenAI uses 'assistant', not 'model'
# Handle function calls if present (for OpenAI-compatible format)
message_dict = {
'role': role,
'content': content
}
# Copy any additional fields that might be present
additional_fields = ['name', 'function_call', 'tool_calls', 'tool_call_id']
for field in additional_fields:
if field in msg:
message_dict[field] = msg[field]
iflow_payload['messages'].append(message_dict)
if not iflow_payload['messages']:
logger.error("No valid messages after transformation")
return jsonify({
'error': {
'message': 'No valid messages in payload',
'type': 'invalid_request_error'
}
}), 400
logger.info(f"Transformed payload model: {iflow_payload['model']}")
logger.info(f"Message count: {len(iflow_payload['messages'])}")
logger.info(f"Streaming: {stream}")
# Call iFlow API
headers = {
'Authorization': f'Bearer {IFLOW_API_KEY}',
'Content-Type': 'application/json',
'User-Agent': 'iFlow-Proxy/1.0'
}
if stream:
# Handle streaming response
logger.info("Making streaming request to iFlow API")
response = requests.post(
IFLOW_API_URL,
json=iflow_payload,
headers=headers,
timeout=120,
stream=True
)
if response.status_code != 200:
error_text = response.text[:500]
logger.error(f"iFlow API error: {response.status_code} - {error_text}")
return jsonify({
'error': {
'message': f'iFlow API error: {response.status_code}',
'type': 'api_error'
}
}), response.status_code
# Return streaming response
def generate():
for chunk in response.iter_lines():
if chunk:
logger.debug(f"Stream chunk: {chunk[:200]}...")
yield chunk + b'\n\n'
return app.response_class(generate(), mimetype='text/event-stream')
else:
# Handle non-streaming response
logger.info("Making non-streaming request to iFlow API")
response = requests.post(
IFLOW_API_URL,
json=iflow_payload,
headers=headers,
timeout=60
)
logger.info(f"iFlow response status: {response.status_code}")
if response.status_code != 200:
error_text = response.text[:500]
logger.error(f"iFlow API error: {response.status_code} - {error_text}")
try:
error_json = response.json()
return jsonify(error_json), response.status_code
except:
return jsonify({
'error': {
'message': f'iFlow API error: {response.status_code}',
'type': 'api_error'
}
}), response.status_code
# Parse and return iFlow response
iflow_response = response.json()
logger.info(f"iFlow response received successfully")
# Extract content from iFlow's response
if 'choices' in iflow_response and len(iflow_response['choices']) > 0:
choice = iflow_response['choices'][0]
if 'message' in choice:
content = choice['message'].get('content', '')
if not content:
logger.warning("Empty content in iFlow response")
content = "No response content"
# Return plain text for /groq endpoint (original format)
logger.info("Successfully returning response to app via /groq")
return content, 200, {'Content-Type': 'text/plain'}
else:
logger.error("No message in iFlow response choice")
return jsonify({
'error': {
'message': 'Invalid response format from iFlow API',
'type': 'api_error'
}
}), 500
else:
logger.error("No choices in iFlow response")
return jsonify({
'error': {
'message': 'Invalid response format from iFlow API',
'type': 'api_error'
}
}), 500
except requests.exceptions.Timeout:
logger.error("iFlow API request timeout")
return jsonify({
'error': {
'message': 'Request timeout to iFlow API',
'type': 'timeout_error'
}
}), 504
except requests.exceptions.RequestException as e:
logger.error(f"Error calling iFlow API: {e}")
return jsonify({
'error': {
'message': f'Network error: {str(e)}',
'type': 'network_error'
}
}), 500
except (json.JSONDecodeError, KeyError) as e:
logger.error(f"Error processing request: {e}")
return jsonify({
'error': {
'message': f'Invalid request format: {str(e)}',
'type': 'invalid_request_error'
}
}), 400
except Exception as e:
logger.error(f"Unexpected error: {e}", exc_info=True)
return jsonify({
'error': {
'message': f'Internal server error: {str(e)}',
'type': 'server_error'
}
}), 500
@app.route('/iflow', methods=['POST'])
@app.route('/chat/completions', methods=['POST'])
def proxy_to_iflow():
"""Alternative endpoints for iFlow API (returns JSON format)"""
try:
# Validate proxy authentication from header
auth_header = request.headers.get('X-API-Key') or request.headers.get('Authorization')
if auth_header:
# Check if it's Bearer token format
if auth_header.startswith('Bearer '):
auth_key = auth_header.replace('Bearer ', '')
else:
auth_key = auth_header
if auth_key != PROXY_AUTH_KEY:
logger.warning("Unauthorized request with invalid API key")
return jsonify({
'error': {
'message': 'Invalid authentication credentials',
'type': 'invalid_request_error',
'code': 'invalid_api_key'
}
}), 401
else:
logger.warning("No authentication header provided")
return jsonify({
'error': {
'message': 'You must provide an API key',
'type': 'invalid_request_error',
'code': 'missing_api_key'
}
}), 401
# Get the Gemini format payload from app
gemini_payload = request.get_json()
if not gemini_payload:
logger.error("No JSON payload received")
return jsonify({
'error': {
'message': 'No JSON payload received',
'type': 'invalid_request_error'
}
}), 400
# Check for streaming request
stream = gemini_payload.get('stream', False)
# Transform Gemini format to iFlow (OpenAI-compatible) format
iflow_payload = {
'model': gemini_payload.get('model', DEFAULT_MODEL),
'messages': [],
'stream': stream
}
# Copy common parameters if present
common_params = ['temperature', 'max_tokens', 'top_p', 'frequency_penalty',
'presence_penalty', 'stop', 'seed', 'response_format']
for param in common_params:
if param in gemini_payload:
iflow_payload[param] = gemini_payload[param]
# Convert messages from Gemini parts format to OpenAI content format
messages = gemini_payload.get('messages', [])
if not messages:
logger.error("No messages in payload")
return jsonify({
'error': {
'message': 'No messages in payload',
'type': 'invalid_request_error'
}
}), 400
for msg in messages:
# Extract text from parts array
parts = msg.get('parts', [])
# Handle both Gemini format (parts array) and OpenAI format (content string)
if parts:
# Gemini format: parts array with text
content_parts = []
for part in parts:
text = part.get('text', '')
if text:
content_parts.append(text)
if not content_parts:
logger.warning(f"Message with role '{msg.get('role')}' has no text content")
continue
content = '\n'.join(content_parts)
else:
# OpenAI format: direct content field
content = msg.get('content', '')
if not content:
logger.warning(f"Message with role '{msg.get('role')}' has no content")
continue
# Map Gemini roles to OpenAI roles
role = msg.get('role')
if role == 'model':
role = 'assistant' # OpenAI uses 'assistant', not 'model'
# Handle function calls if present (for OpenAI-compatible format)
message_dict = {
'role': role,
'content': content
}
# Copy any additional fields that might be present
additional_fields = ['name', 'function_call', 'tool_calls', 'tool_call_id']
for field in additional_fields:
if field in msg:
message_dict[field] = msg[field]
iflow_payload['messages'].append(message_dict)
if not iflow_payload['messages']:
logger.error("No valid messages after transformation")
return jsonify({
'error': {
'message': 'No valid messages in payload',
'type': 'invalid_request_error'
}
}), 400
# Call iFlow API
headers = {
'Authorization': f'Bearer {IFLOW_API_KEY}',
'Content-Type': 'application/json',
'User-Agent': 'iFlow-Proxy/1.0'
}
response = requests.post(
IFLOW_API_URL,
json=iflow_payload,
headers=headers,
timeout=60
)
if response.status_code != 200:
error_text = response.text[:500]
logger.error(f"iFlow API error: {response.status_code} - {error_text}")
try:
error_json = response.json()
return jsonify(error_json), response.status_code
except:
return jsonify({
'error': {
'message': f'iFlow API error: {response.status_code}',
'type': 'api_error'
}
}), response.status_code
# Return the iFlow response directly (OpenAI-compatible JSON)
return jsonify(response.json()), 200
except Exception as e:
logger.error(f"Error in iflow endpoint: {e}")
return jsonify({
'error': {
'message': f'Internal server error: {str(e)}',
'type': 'server_error'
}
}), 500
@app.route('/v1/models', methods=['GET'])
def list_models():
"""List available models (OpenAI-compatible endpoint)"""
try:
# Validate authentication
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
auth_key = auth_header.replace('Bearer ', '')
if auth_key != PROXY_AUTH_KEY:
return jsonify({'error': 'Invalid API key'}), 401
# Call iFlow models endpoint
headers = {
'Authorization': f'Bearer {IFLOW_API_KEY}',
'Content-Type': 'application/json'
}
response = requests.get(
'https://apis.iflow.cn/v1/models',
headers=headers,
timeout=10
)
if response.status_code == 200:
return jsonify(response.json()), 200
else:
# Return a default list if iFlow API fails
return jsonify({
'object': 'list',
'data': [
{
'id': 'glm-4.6',
'object': 'model',
'created': 1698100000,
'owned_by': 'iFlow'
},
{
'id': 'TBStars2-200B-A13B',
'object': 'model',
'created': 1698200000,
'owned_by': 'iFlow'
}
]
}), 200
except Exception as e:
logger.error(f"Error listing models: {e}")
return jsonify({
'object': 'list',
'data': [
{
'id': DEFAULT_MODEL,
'object': 'model',
'created': 1698100000,
'owned_by': 'iFlow'
}
]
}), 200
@app.route('/test-transform', methods=['POST'])
def test_transform():
"""Test endpoint to see the transformation without calling iFlow"""
try:
gemini_payload = request.get_json()
if not gemini_payload:
return jsonify({'error': 'No payload received'}), 400
# Transform as in main endpoint
iflow_payload = {
'model': gemini_payload.get('model', DEFAULT_MODEL),
'messages': []
}
messages = gemini_payload.get('messages', [])
for msg in messages:
parts = msg.get('parts', [])
if parts:
content = '\n'.join(part.get('text', '') for part in parts)
else:
content = msg.get('content', '')
role = msg.get('role')
if role == 'model':
role = 'assistant'
iflow_payload['messages'].append({
'role': role,
'content': content
})
return jsonify({
'transformed_payload': iflow_payload,
'message_count': len(iflow_payload['messages']),
'model': iflow_payload['model']
}), 200
except Exception as e:
return jsonify({'error': f'Error: {str(e)}'}), 500
@app.route('/health')
def health_check():
"""Health check endpoint"""
health_status = {
"status": "healthy",
"service": "iflow-proxy",
"default_model": DEFAULT_MODEL,
"iflow_api_key_set": bool(IFLOW_API_KEY),
"proxy_auth_key_set": bool(PROXY_AUTH_KEY),
"endpoints": {
"groq_compat": "/groq (for existing apps)",
"iflow": "/iflow",
"chat_completions": "/chat/completions",
"list_models": "/v1/models",
"test_transform": "/test-transform",
"health": "/health"
}
}
# Test iFlow API connectivity if key is set
if IFLOW_API_KEY:
try:
headers = {
'Authorization': f'Bearer {IFLOW_API_KEY}',
'Content-Type': 'application/json'
}
test_payload = {
'model': DEFAULT_MODEL,
'messages': [{'role': 'user', 'content': 'ping'}],
'max_tokens': 5
}
test_response = requests.post(
'https://apis.iflow.cn/v1/chat/completions',
json=test_payload,
headers=headers,
timeout=15
)
health_status["iflow_api_accessible"] = test_response.status_code == 200
if test_response.status_code == 200:
health_status["iflow_api_test"] = "Connection successful"
except Exception as e:
health_status["iflow_api_accessible"] = False
health_status["iflow_api_error"] = str(e)[:100]
return jsonify(health_status), 200
@app.errorhandler(404)
def not_found(error):
return jsonify({
'error': {
'message': 'Endpoint not found',
'type': 'invalid_request_error'
}
}), 404
@app.errorhandler(405)
def method_not_allowed(error):
return jsonify({
'error': {
'message': 'Method not allowed',
'type': 'invalid_request_error'
}
}), 405
if __name__ == '__main__':
logger.info("=" * 50)
logger.info("Starting iFlow Proxy Server (with /groq compatibility)")
logger.info(f"Default Model: {DEFAULT_MODEL}")
logger.info(f"IFLOW_API_KEY set: {bool(IFLOW_API_KEY)}")
logger.info(f"PROXY_AUTH_KEY set: {bool(PROXY_AUTH_KEY)}")
logger.info(f"API URL: {IFLOW_API_URL}")
logger.info("=" * 50)
app.run(host='0.0.0.0', port=7860, debug=False)