Nerdur commited on
Commit
ea58cf8
·
1 Parent(s): fb15b5d

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)

Files changed (3) hide show
  1. app.py +96 -1
  2. index.html +24 -61
  3. 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 endpoint first (direct fetch + CORS proxies, no backend proxy - DNS doesn't resolve server-side)
1970
  try {
1971
- return await hfChatCompletionsBrowser(model, messages, systemPrompt, headers, signal, onChunk);
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 endpoint (also via browser)
1981
- return await hfTextGenerationBrowser(model, messages, systemPrompt, headers, signal, onChunk);
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 hfChatCompletionsBrowser(model, messages, systemPrompt, headers, signal, onChunk) {
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 hfFetch(url, {method:'POST', headers, body:JSON.stringify(body), signal});
 
 
 
 
 
 
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?.message || err.slice(0,300)); } catch(_) { if (err) msg += ': ' + err.slice(0,300); }
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 hfTextGenerationBrowser(model, messages, systemPrompt, headers, signal, onChunk) {
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 hfFetch(url, {method:'POST', headers, body:JSON.stringify(body), signal});
 
 
 
 
 
 
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