Route HuggingFace calls through backend proxy via huggingface_hub
Browse files- Frontend HF calls now use /api/proxy/hf backend endpoint
- Backend uses huggingface_hub.InferenceClient with internal routing
(bypasses api-inference.huggingface.co DNS issue on HF infra)
- Removed browser-direct HF fetch (hfFetch + CORS proxies)
- app.py +96 -1
- index.html +24 -61
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -8,6 +8,13 @@ import sys
|
|
| 8 |
app = Flask(__name__)
|
| 9 |
STATIC_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
@app.route('/')
|
| 12 |
def index():
|
| 13 |
return send_from_directory(STATIC_DIR, 'index.html')
|
|
@@ -24,7 +31,6 @@ def proxy():
|
|
| 24 |
|
| 25 |
method = data.get('method', 'POST').upper()
|
| 26 |
headers = data.get('headers', {})
|
| 27 |
-
# Filter out hop-by-hop / browser-only headers
|
| 28 |
filtered_headers = {}
|
| 29 |
skip = {'origin', 'referer', 'host', 'cookie', 'set-cookie', 'cf-connecting-ip', 'cf-ray', 'cf-worker', 'x-forwarded-for', 'x-forwarded-proto', 'x-real-ip'}
|
| 30 |
for k, v in headers.items():
|
|
@@ -89,6 +95,95 @@ def proxy():
|
|
| 89 |
)
|
| 90 |
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
if __name__ == '__main__':
|
| 93 |
port = int(os.environ.get('PORT', 7860))
|
| 94 |
app.run(host='0.0.0.0', port=port, debug=False)
|
|
|
|
| 8 |
app = Flask(__name__)
|
| 9 |
STATIC_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 10 |
|
| 11 |
+
# Import huggingface_hub if available (pre-installed on HF Spaces)
|
| 12 |
+
try:
|
| 13 |
+
from huggingface_hub import InferenceClient
|
| 14 |
+
HAS_HF_HUB = True
|
| 15 |
+
except ImportError:
|
| 16 |
+
HAS_HF_HUB = False
|
| 17 |
+
|
| 18 |
@app.route('/')
|
| 19 |
def index():
|
| 20 |
return send_from_directory(STATIC_DIR, 'index.html')
|
|
|
|
| 31 |
|
| 32 |
method = data.get('method', 'POST').upper()
|
| 33 |
headers = data.get('headers', {})
|
|
|
|
| 34 |
filtered_headers = {}
|
| 35 |
skip = {'origin', 'referer', 'host', 'cookie', 'set-cookie', 'cf-connecting-ip', 'cf-ray', 'cf-worker', 'x-forwarded-for', 'x-forwarded-proto', 'x-real-ip'}
|
| 36 |
for k, v in headers.items():
|
|
|
|
| 95 |
)
|
| 96 |
|
| 97 |
|
| 98 |
+
@app.route('/api/proxy/hf', methods=['POST', 'OPTIONS'])
|
| 99 |
+
def proxy_hf():
|
| 100 |
+
"""Proxy HuggingFace Inference API via huggingface_hub internal routing."""
|
| 101 |
+
if request.method == 'OPTIONS':
|
| 102 |
+
return Response(headers={'Access-Control-Allow-Origin': '*'})
|
| 103 |
+
|
| 104 |
+
data = request.get_json(force=True)
|
| 105 |
+
model = data.get('model', '')
|
| 106 |
+
body = data.get('body', {})
|
| 107 |
+
token = data.get('token', '')
|
| 108 |
+
is_chat = data.get('is_chat', False)
|
| 109 |
+
stream = data.get('stream', False)
|
| 110 |
+
|
| 111 |
+
if not model:
|
| 112 |
+
return {'error': 'Missing model'}, 400
|
| 113 |
+
|
| 114 |
+
if not HAS_HF_HUB:
|
| 115 |
+
return {'error': 'huggingface_hub not available on server'}, 500
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
client = InferenceClient(token=token)
|
| 119 |
+
|
| 120 |
+
if is_chat and not stream:
|
| 121 |
+
messages = body.get('messages', [])
|
| 122 |
+
temperature = body.get('temperature', 0.7)
|
| 123 |
+
max_tokens = body.get('max_tokens', 2048)
|
| 124 |
+
|
| 125 |
+
result = client.chat.completions.create(
|
| 126 |
+
model=model,
|
| 127 |
+
messages=messages,
|
| 128 |
+
temperature=temperature,
|
| 129 |
+
max_tokens=max_tokens,
|
| 130 |
+
stream=False,
|
| 131 |
+
)
|
| 132 |
+
return Response(
|
| 133 |
+
json.dumps(result.to_dict()),
|
| 134 |
+
status=200,
|
| 135 |
+
headers={'Content-Type': 'application/json'}
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
elif stream:
|
| 139 |
+
messages = body.get('messages', [])
|
| 140 |
+
temperature = body.get('temperature', 0.7)
|
| 141 |
+
max_tokens = body.get('max_tokens', 2048)
|
| 142 |
+
|
| 143 |
+
result = client.chat.completions.create(
|
| 144 |
+
model=model,
|
| 145 |
+
messages=messages,
|
| 146 |
+
temperature=temperature,
|
| 147 |
+
max_tokens=max_tokens,
|
| 148 |
+
stream=True,
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
def generate():
|
| 152 |
+
try:
|
| 153 |
+
for chunk in result:
|
| 154 |
+
yield f'data: {json.dumps(chunk.to_dict())}\n\n'
|
| 155 |
+
yield 'data: [DONE]\n\n'
|
| 156 |
+
except Exception as e:
|
| 157 |
+
print(f'HF stream error: {e}', file=sys.stderr, flush=True)
|
| 158 |
+
return Response(
|
| 159 |
+
stream_with_context(generate()),
|
| 160 |
+
status=200,
|
| 161 |
+
headers={'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
else:
|
| 165 |
+
# Text generation via _post to bypass model metadata lookup
|
| 166 |
+
resp = client._post(
|
| 167 |
+
path=f"/models/{model}",
|
| 168 |
+
json=body,
|
| 169 |
+
stream=False,
|
| 170 |
+
)
|
| 171 |
+
return Response(
|
| 172 |
+
resp.content,
|
| 173 |
+
status=resp.status_code,
|
| 174 |
+
headers={k: v for k, v in resp.headers.items() if k.lower() not in {'content-encoding', 'transfer-encoding', 'content-length'}},
|
| 175 |
+
content_type=resp.headers.get('Content-Type', 'application/json'),
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
except ImportError:
|
| 179 |
+
return {'error': 'huggingface_hub library not found'}, 500
|
| 180 |
+
except Exception as e:
|
| 181 |
+
tb = traceback.format_exc()
|
| 182 |
+
print(f'HF proxy error for {model}: {e}\n{tb}', file=sys.stderr, flush=True)
|
| 183 |
+
error_msg = f'{type(e).__name__}: {e}'
|
| 184 |
+
return {'error': error_msg}, 500
|
| 185 |
+
|
| 186 |
+
|
| 187 |
if __name__ == '__main__':
|
| 188 |
port = int(os.environ.get('PORT', 7860))
|
| 189 |
app.run(host='0.0.0.0', port=port, debug=False)
|
index.html
CHANGED
|
@@ -1962,41 +1962,24 @@ async function streamPollinations(messages, systemPrompt, signal, onChunk) {
|
|
| 1962 |
async function callHuggingFace(messages, systemPrompt, signal, onChunk) {
|
| 1963 |
const apiKey = state.apiKeys['huggingface'] || '';
|
| 1964 |
const model = state.currentModel;
|
| 1965 |
-
const headers = {'Content-Type':'application/json'};
|
| 1966 |
-
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
| 1967 |
console.log('callHuggingFace: model=%s hasKey=%s', model, !!apiKey);
|
| 1968 |
|
| 1969 |
-
// Try chat completions
|
| 1970 |
try {
|
| 1971 |
-
return await
|
| 1972 |
} catch(e) {
|
| 1973 |
-
if (e.message?.includes('HTTP 404') || e.message?.includes('HTTP 400')) {
|
| 1974 |
console.warn('Chat completions not supported for', model, 'trying text-generation');
|
| 1975 |
} else {
|
| 1976 |
throw e;
|
| 1977 |
}
|
| 1978 |
}
|
| 1979 |
|
| 1980 |
-
// Fallback: text-generation
|
| 1981 |
-
return await
|
| 1982 |
-
}
|
| 1983 |
-
|
| 1984 |
-
async function hfFetch(url, options = {}) {
|
| 1985 |
-
// Try direct browser fetch first, then CORS proxies
|
| 1986 |
-
try {
|
| 1987 |
-
return await fetch(url, options);
|
| 1988 |
-
} catch(e) {
|
| 1989 |
-
if (!isNetworkError(e)) throw e;
|
| 1990 |
-
const proxies = ['https://corsproxy.io/?url=', 'https://api.allorigins.win/raw?url=', 'https://thingproxy.freeboard.io/fetch/'];
|
| 1991 |
-
for (const proxy of proxies) {
|
| 1992 |
-
try { return await fetch(proxy + encodeURIComponent(url), options); } catch(e2) { if (!isNetworkError(e2)) throw e2; }
|
| 1993 |
-
}
|
| 1994 |
-
throw new Error('Ne mogu dosegnuti HuggingFace. Provjeri internet konekciju ili probaj Pollinations provider.');
|
| 1995 |
-
}
|
| 1996 |
}
|
| 1997 |
|
| 1998 |
-
async function
|
| 1999 |
-
const url = `https://api-inference.huggingface.co/models/${model}/v1/chat/completions`;
|
| 2000 |
const body = {
|
| 2001 |
model,
|
| 2002 |
messages: [
|
|
@@ -2005,26 +1988,31 @@ async function hfChatCompletionsBrowser(model, messages, systemPrompt, headers,
|
|
| 2005 |
],
|
| 2006 |
temperature: state.settings.temperature,
|
| 2007 |
max_tokens: state.settings.maxTokens,
|
| 2008 |
-
stream: false, // non-streaming for reliability
|
| 2009 |
};
|
| 2010 |
|
| 2011 |
-
const res = await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2012 |
if (!res.ok) {
|
| 2013 |
let err = '';
|
| 2014 |
try { err = await res.text(); } catch(_) {}
|
| 2015 |
let msg = `HuggingFace HTTP ${res.status}`;
|
| 2016 |
-
try { const j = JSON.parse(err); msg += ': ' + (j.error
|
| 2017 |
throw new Error(msg);
|
| 2018 |
}
|
|
|
|
| 2019 |
const json = await res.json();
|
| 2020 |
const fullText = json.choices?.[0]?.message?.content || '';
|
| 2021 |
if (fullText) onChunk(fullText);
|
| 2022 |
return fullText;
|
| 2023 |
}
|
| 2024 |
|
| 2025 |
-
async function
|
| 2026 |
const prompt = buildPrompt(messages, systemPrompt);
|
| 2027 |
-
const url = `https://api-inference.huggingface.co/models/${model}`;
|
| 2028 |
const body = {
|
| 2029 |
inputs: prompt,
|
| 2030 |
parameters: {
|
|
@@ -2034,7 +2022,13 @@ async function hfTextGenerationBrowser(model, messages, systemPrompt, headers, s
|
|
| 2034 |
}
|
| 2035 |
};
|
| 2036 |
|
| 2037 |
-
const res = await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2038 |
if (!res.ok) {
|
| 2039 |
let err = '';
|
| 2040 |
try { err = await res.text(); } catch(_) {}
|
|
@@ -2042,44 +2036,13 @@ async function hfTextGenerationBrowser(model, messages, systemPrompt, headers, s
|
|
| 2042 |
try { const j = JSON.parse(err); msg += ': ' + (j.error || err.slice(0,300)); } catch(_) { if (err) msg += ': ' + err.slice(0,300); }
|
| 2043 |
throw new Error(msg);
|
| 2044 |
}
|
|
|
|
| 2045 |
const arr = await res.json();
|
| 2046 |
const fullText = Array.isArray(arr) ? (arr[0]?.generated_text || '') : '';
|
| 2047 |
if (fullText) onChunk(fullText);
|
| 2048 |
return fullText;
|
| 2049 |
}
|
| 2050 |
|
| 2051 |
-
async function hfChatCompletions(model, messages, systemPrompt, headers, signal, onChunk) {
|
| 2052 |
-
const url = `https://api-inference.huggingface.co/models/${model}/v1/chat/completions`;
|
| 2053 |
-
const body = {
|
| 2054 |
-
model,
|
| 2055 |
-
messages: [
|
| 2056 |
-
{role:'system', content: systemPrompt},
|
| 2057 |
-
...messages
|
| 2058 |
-
],
|
| 2059 |
-
temperature: state.settings.temperature,
|
| 2060 |
-
max_tokens: state.settings.maxTokens,
|
| 2061 |
-
};
|
| 2062 |
-
|
| 2063 |
-
let res;
|
| 2064 |
-
try {
|
| 2065 |
-
res = await apiFetch(url, {method:'POST',headers,body:JSON.stringify(body),signal});
|
| 2066 |
-
} catch(e) {
|
| 2067 |
-
if (signal?.aborted) throw e;
|
| 2068 |
-
throw new Error('Ne mogu dosegnuti HuggingFace.');
|
| 2069 |
-
}
|
| 2070 |
-
if (!res.ok) {
|
| 2071 |
-
let err = '';
|
| 2072 |
-
try { err = await res.text(); } catch(_) {}
|
| 2073 |
-
let msg = `HuggingFace HTTP ${res.status}`;
|
| 2074 |
-
try { const j = JSON.parse(err); msg += ': ' + (j.error?.message || err.slice(0,300)); } catch(_) { if (err) msg += ': ' + err.slice(0,300); }
|
| 2075 |
-
throw new Error(msg);
|
| 2076 |
-
}
|
| 2077 |
-
const json = await res.json();
|
| 2078 |
-
const fullText = json.choices?.[0]?.message?.content || '';
|
| 2079 |
-
if (fullText) onChunk(fullText);
|
| 2080 |
-
return fullText;
|
| 2081 |
-
}
|
| 2082 |
-
|
| 2083 |
function buildPrompt(messages, systemPrompt) {
|
| 2084 |
let prompt = `System: ${systemPrompt}\n\n`;
|
| 2085 |
for (const m of messages) {
|
|
|
|
| 1962 |
async function callHuggingFace(messages, systemPrompt, signal, onChunk) {
|
| 1963 |
const apiKey = state.apiKeys['huggingface'] || '';
|
| 1964 |
const model = state.currentModel;
|
|
|
|
|
|
|
| 1965 |
console.log('callHuggingFace: model=%s hasKey=%s', model, !!apiKey);
|
| 1966 |
|
| 1967 |
+
// Try chat completions via backend proxy (uses huggingface_hub internal routing)
|
| 1968 |
try {
|
| 1969 |
+
return await hfChatCompletionsBackend(model, messages, systemPrompt, apiKey, signal, onChunk);
|
| 1970 |
} catch(e) {
|
| 1971 |
+
if (e.message?.includes('HTTP 404') || e.message?.includes('HTTP 400') || e.message?.includes('model_not_supported')) {
|
| 1972 |
console.warn('Chat completions not supported for', model, 'trying text-generation');
|
| 1973 |
} else {
|
| 1974 |
throw e;
|
| 1975 |
}
|
| 1976 |
}
|
| 1977 |
|
| 1978 |
+
// Fallback: text-generation via backend proxy
|
| 1979 |
+
return await hfTextGenerationBackend(model, messages, systemPrompt, apiKey, signal, onChunk);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1980 |
}
|
| 1981 |
|
| 1982 |
+
async function hfChatCompletionsBackend(model, messages, systemPrompt, apiKey, signal, onChunk) {
|
|
|
|
| 1983 |
const body = {
|
| 1984 |
model,
|
| 1985 |
messages: [
|
|
|
|
| 1988 |
],
|
| 1989 |
temperature: state.settings.temperature,
|
| 1990 |
max_tokens: state.settings.maxTokens,
|
|
|
|
| 1991 |
};
|
| 1992 |
|
| 1993 |
+
const res = await fetch('/api/proxy/hf', {
|
| 1994 |
+
method: 'POST',
|
| 1995 |
+
headers: {'Content-Type':'application/json'},
|
| 1996 |
+
body: JSON.stringify({model, token: apiKey, is_chat: true, stream: false, body}),
|
| 1997 |
+
signal,
|
| 1998 |
+
});
|
| 1999 |
+
|
| 2000 |
if (!res.ok) {
|
| 2001 |
let err = '';
|
| 2002 |
try { err = await res.text(); } catch(_) {}
|
| 2003 |
let msg = `HuggingFace HTTP ${res.status}`;
|
| 2004 |
+
try { const j = JSON.parse(err); msg += ': ' + (j.error || err.slice(0,300)); } catch(_) { if (err) msg += ': ' + err.slice(0,300); }
|
| 2005 |
throw new Error(msg);
|
| 2006 |
}
|
| 2007 |
+
|
| 2008 |
const json = await res.json();
|
| 2009 |
const fullText = json.choices?.[0]?.message?.content || '';
|
| 2010 |
if (fullText) onChunk(fullText);
|
| 2011 |
return fullText;
|
| 2012 |
}
|
| 2013 |
|
| 2014 |
+
async function hfTextGenerationBackend(model, messages, systemPrompt, apiKey, signal, onChunk) {
|
| 2015 |
const prompt = buildPrompt(messages, systemPrompt);
|
|
|
|
| 2016 |
const body = {
|
| 2017 |
inputs: prompt,
|
| 2018 |
parameters: {
|
|
|
|
| 2022 |
}
|
| 2023 |
};
|
| 2024 |
|
| 2025 |
+
const res = await fetch('/api/proxy/hf', {
|
| 2026 |
+
method: 'POST',
|
| 2027 |
+
headers: {'Content-Type':'application/json'},
|
| 2028 |
+
body: JSON.stringify({model, token: apiKey, is_chat: false, stream: false, body}),
|
| 2029 |
+
signal,
|
| 2030 |
+
});
|
| 2031 |
+
|
| 2032 |
if (!res.ok) {
|
| 2033 |
let err = '';
|
| 2034 |
try { err = await res.text(); } catch(_) {}
|
|
|
|
| 2036 |
try { const j = JSON.parse(err); msg += ': ' + (j.error || err.slice(0,300)); } catch(_) { if (err) msg += ': ' + err.slice(0,300); }
|
| 2037 |
throw new Error(msg);
|
| 2038 |
}
|
| 2039 |
+
|
| 2040 |
const arr = await res.json();
|
| 2041 |
const fullText = Array.isArray(arr) ? (arr[0]?.generated_text || '') : '';
|
| 2042 |
if (fullText) onChunk(fullText);
|
| 2043 |
return fullText;
|
| 2044 |
}
|
| 2045 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2046 |
function buildPrompt(messages, systemPrompt) {
|
| 2047 |
let prompt = `System: ${systemPrompt}\n\n`;
|
| 2048 |
for (const m of messages) {
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
flask>=3.0
|
| 2 |
requests>=2.31
|
| 3 |
gunicorn>=21.2
|
|
|
|
|
|
| 1 |
flask>=3.0
|
| 2 |
requests>=2.31
|
| 3 |
gunicorn>=21.2
|
| 4 |
+
huggingface_hub>=0.26
|