Reubencf commited on
Commit
a84aedc
·
1 Parent(s): 0b4d29f

Use signed-in user's HF OAuth token for inference

Browse files

The Space README declares `hf_oauth: true` with `inference-api` scope,
so the token granted to each user on sign-in is already authorized for
HF inference. createInferenceClient now prefers that user token over a
server-side HF_TOKEN env var, removing the need to provision a PAT.

Each HF helper accepts opts.accessToken again; routes pass
session.accessToken from the JWT. Server-side HF_TOKEN remains a
fallback (e.g. for background jobs) and still gets the JWT-shape
diagnostic if it's misconfigured.

app/api/dialogues/route.ts CHANGED
@@ -24,6 +24,7 @@ export async function POST(req: NextRequest) {
24
  nativeLang: session.nativeLang ?? "en",
25
  level,
26
  scenario,
 
27
  });
28
 
29
  return NextResponse.json(
 
24
  nativeLang: session.nativeLang ?? "en",
25
  level,
26
  scenario,
27
+ accessToken: session.accessToken,
28
  });
29
 
30
  return NextResponse.json(
app/api/phrases/route.ts CHANGED
@@ -26,6 +26,7 @@ export async function POST(req: Request) {
26
  targetLang: session.targetLang,
27
  nativeLang: session.nativeLang ?? "en",
28
  level: (parsed.data.level ?? session.level) as Level,
 
29
  });
30
 
31
  return NextResponse.json(result);
 
26
  targetLang: session.targetLang,
27
  nativeLang: session.nativeLang ?? "en",
28
  level: (parsed.data.level ?? session.level) as Level,
29
+ accessToken: session.accessToken,
30
  });
31
 
32
  return NextResponse.json(result);
app/api/stories/route.ts CHANGED
@@ -21,6 +21,7 @@ export async function POST(req: Request) {
21
  nativeLang: session.nativeLang ?? "en",
22
  level: session.level as Level,
23
  topic: parsed.data.topic,
 
24
  });
25
 
26
  return NextResponse.json({
 
21
  nativeLang: session.nativeLang ?? "en",
22
  level: session.level as Level,
23
  topic: parsed.data.topic,
24
+ accessToken: session.accessToken,
25
  });
26
 
27
  return NextResponse.json({
app/api/translate/route.ts CHANGED
@@ -30,7 +30,7 @@ export async function POST(req: Request) {
30
  if (cache.has(key)) return NextResponse.json({ translation: cache.get(key), cached: true });
31
 
32
  try {
33
- const translation = await translate({ text, from, to });
34
  if (cache.size >= MAX_CACHE) {
35
  const firstKey = cache.keys().next().value;
36
  if (firstKey) cache.delete(firstKey);
 
30
  if (cache.has(key)) return NextResponse.json({ translation: cache.get(key), cached: true });
31
 
32
  try {
33
+ const translation = await translate({ text, from, to, accessToken: session.accessToken });
34
  if (cache.size >= MAX_CACHE) {
35
  const firstKey = cache.keys().next().value;
36
  if (firstKey) cache.delete(firstKey);
app/api/vision/analyze/route.ts CHANGED
@@ -33,6 +33,7 @@ export async function POST(req: Request) {
33
  targetLang: session.targetLang,
34
  nativeLang: session.nativeLang,
35
  level,
 
36
  });
37
 
38
  const mappedObjects = result.objects.map((o) => ({
 
33
  targetLang: session.targetLang,
34
  nativeLang: session.nativeLang,
35
  level,
36
+ accessToken: session.accessToken,
37
  });
38
 
39
  const mappedObjects = result.objects.map((o) => ({
lib/hf/client.ts CHANGED
@@ -1,24 +1,25 @@
1
  import { InferenceClient } from "@huggingface/inference";
2
 
3
- function assertTokenShape(token: string) {
4
- // OAuth access tokens are JWTs: three base64url segments separated by dots, starting with "eyJ".
5
- // Personal Access Tokens start with "hf_" and never expire. Inference must use a PAT — OAuth
6
- // tokens have an `exp` claim and HF returns 401 once it passes.
7
- const looksLikeJwt = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token);
8
- if (looksLikeJwt) {
9
- throw new Error(
10
- "HF_TOKEN looks like an OAuth access token (JWT). HF inference requires a long-lived Personal Access Token starting with `hf_`. Create one at https://huggingface.co/settings/tokens and set it as the HF_TOKEN secret in your HF Space.",
11
- );
12
  }
13
- }
14
 
15
- export function createInferenceClient() {
16
  const serverToken = process.env.HF_TOKEN?.trim();
17
  if (!serverToken) {
18
  throw new Error(
19
- "HF_TOKEN is not configured for inference. Set it to a Personal Access Token (starts with `hf_`) from https://huggingface.co/settings/tokens.",
 
 
 
 
 
20
  );
21
  }
22
- assertTokenShape(serverToken);
23
  return new InferenceClient(serverToken);
24
  }
 
1
  import { InferenceClient } from "@huggingface/inference";
2
 
3
+ const JWT_SHAPE = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
4
+
5
+ export function createInferenceClient(userAccessToken?: string) {
6
+ // Prefer the signed-in user's HF OAuth access token (granted via the
7
+ // `inference-api` scope on sign-in). Falls back to a server-side
8
+ // HF_TOKEN env var if no user token is present (e.g. background jobs).
9
+ if (userAccessToken) {
10
+ return new InferenceClient(userAccessToken);
 
11
  }
 
12
 
 
13
  const serverToken = process.env.HF_TOKEN?.trim();
14
  if (!serverToken) {
15
  throw new Error(
16
+ "No HuggingFace token available. Sign in to use your OAuth token, or set HF_TOKEN to a Personal Access Token (starts with `hf_`) from https://huggingface.co/settings/tokens.",
17
+ );
18
+ }
19
+ if (JWT_SHAPE.test(serverToken)) {
20
+ throw new Error(
21
+ "HF_TOKEN looks like an OAuth access token (JWT). For server-side inference set HF_TOKEN to a long-lived Personal Access Token starting with `hf_` from https://huggingface.co/settings/tokens, or remove HF_TOKEN and rely on per-user OAuth.",
22
  );
23
  }
 
24
  return new InferenceClient(serverToken);
25
  }
lib/hf/dialogue.ts CHANGED
@@ -18,8 +18,9 @@ export async function generateDialogue(opts: {
18
  nativeLang: string;
19
  level: Level;
20
  scenario?: string;
 
21
  }): Promise<{ title: string; scenario: string; turns: DialogueTurn[] }> {
22
- const hf = createInferenceClient();
23
  const lang = languageName(opts.targetLang);
24
  const native = languageName(opts.nativeLang);
25
  const hint = SCENARIO_HINTS[opts.level];
 
18
  nativeLang: string;
19
  level: Level;
20
  scenario?: string;
21
+ accessToken?: string;
22
  }): Promise<{ title: string; scenario: string; turns: DialogueTurn[] }> {
23
+ const hf = createInferenceClient(opts.accessToken);
24
  const lang = languageName(opts.targetLang);
25
  const native = languageName(opts.nativeLang);
26
  const hint = SCENARIO_HINTS[opts.level];
lib/hf/gemma-vision.ts CHANGED
@@ -39,6 +39,7 @@ export async function analyzeWithGemma(opts: {
39
  targetLang: string;
40
  nativeLang: string;
41
  level: Level;
 
42
  }): Promise<GemmaVisionResult> {
43
  const lang = languageName(opts.targetLang);
44
  const brief = LEVEL_SENTENCE_BRIEF[opts.level];
@@ -71,7 +72,7 @@ Output STRICT JSON:
71
  ]
72
  }`;
73
 
74
- const hf = createInferenceClient();
75
  const res = await hf.chatCompletion({
76
  provider: "novita",
77
  model: GEMMA_MODEL,
 
39
  targetLang: string;
40
  nativeLang: string;
41
  level: Level;
42
+ accessToken?: string;
43
  }): Promise<GemmaVisionResult> {
44
  const lang = languageName(opts.targetLang);
45
  const brief = LEVEL_SENTENCE_BRIEF[opts.level];
 
72
  ]
73
  }`;
74
 
75
+ const hf = createInferenceClient(opts.accessToken);
76
  const res = await hf.chatCompletion({
77
  provider: "novita",
78
  model: GEMMA_MODEL,
lib/hf/inference.ts CHANGED
@@ -19,6 +19,7 @@ export async function generateStory(opts: {
19
  nativeLang: string;
20
  level: Level;
21
  topic?: string;
 
22
  }): Promise<{ title: string; content: string; vocab: StoryVocab[] }> {
23
  const lang = languageName(opts.targetLang);
24
  const native = languageName(opts.nativeLang);
@@ -35,7 +36,7 @@ export async function generateStory(opts: {
35
  `Output STRICT JSON with keys: title (string in ${lang}), content (string in ${lang} - paragraphs separated by \\n\\n), vocab (array of 6-10 objects each with "word" in ${lang} and "gloss" in ${native}).${japaneseRule}`;
36
  const user = `Write a story about: ${topic}.`;
37
 
38
- const hf = createInferenceClient();
39
  const res = await hf.chatCompletion({
40
  provider: "novita",
41
  model: STORY_MODEL,
@@ -67,9 +68,10 @@ export async function translate(opts: {
67
  text: string;
68
  from: string;
69
  to: string;
 
70
  }): Promise<string> {
71
  if (!opts.text.trim()) return "";
72
- const hf = createInferenceClient();
73
  const res = await hf.translation({
74
  model: TRANSLATE_MODEL,
75
  inputs: opts.text,
 
19
  nativeLang: string;
20
  level: Level;
21
  topic?: string;
22
+ accessToken?: string;
23
  }): Promise<{ title: string; content: string; vocab: StoryVocab[] }> {
24
  const lang = languageName(opts.targetLang);
25
  const native = languageName(opts.nativeLang);
 
36
  `Output STRICT JSON with keys: title (string in ${lang}), content (string in ${lang} - paragraphs separated by \\n\\n), vocab (array of 6-10 objects each with "word" in ${lang} and "gloss" in ${native}).${japaneseRule}`;
37
  const user = `Write a story about: ${topic}.`;
38
 
39
+ const hf = createInferenceClient(opts.accessToken);
40
  const res = await hf.chatCompletion({
41
  provider: "novita",
42
  model: STORY_MODEL,
 
68
  text: string;
69
  from: string;
70
  to: string;
71
+ accessToken?: string;
72
  }): Promise<string> {
73
  if (!opts.text.trim()) return "";
74
+ const hf = createInferenceClient(opts.accessToken);
75
  const res = await hf.translation({
76
  model: TRANSLATE_MODEL,
77
  inputs: opts.text,
lib/hf/phrases.ts CHANGED
@@ -53,8 +53,9 @@ export async function analyzePhrase(opts: {
53
  targetLang: string;
54
  nativeLang: string;
55
  level: Level;
 
56
  }): Promise<PhraseAnalysis> {
57
- const hf = createInferenceClient();
58
  const lang = languageName(opts.targetLang);
59
  const native = languageName(opts.nativeLang);
60
  const phrase = opts.text.trim();
 
53
  targetLang: string;
54
  nativeLang: string;
55
  level: Level;
56
+ accessToken?: string;
57
  }): Promise<PhraseAnalysis> {
58
+ const hf = createInferenceClient(opts.accessToken);
59
  const lang = languageName(opts.targetLang);
60
  const native = languageName(opts.nativeLang);
61
  const phrase = opts.text.trim();
lib/hf/sentences.ts CHANGED
@@ -11,6 +11,7 @@ export async function generateSentencesForImage(opts: {
11
  targetLang: string;
12
  nativeLang: string;
13
  level: Level;
 
14
  }): Promise<ExampleSentence[]> {
15
  const target = languageName(opts.targetLang);
16
  const native = languageName(opts.nativeLang);
@@ -20,7 +21,7 @@ export async function generateSentencesForImage(opts: {
20
  `Output STRICT JSON: {"sentences": [{"target": string, "gloss": string}, ...]}.`;
21
  const user = `Caption: ${opts.captionEn}\nObjects: ${opts.objectsEn.join(", ") || "(none)"}\n`;
22
 
23
- const hf = createInferenceClient();
24
  const res = await hf.chatCompletion({
25
  provider: "novita",
26
  model: MODEL,
 
11
  targetLang: string;
12
  nativeLang: string;
13
  level: Level;
14
+ accessToken?: string;
15
  }): Promise<ExampleSentence[]> {
16
  const target = languageName(opts.targetLang);
17
  const native = languageName(opts.nativeLang);
 
21
  `Output STRICT JSON: {"sentences": [{"target": string, "gloss": string}, ...]}.`;
22
  const user = `Caption: ${opts.captionEn}\nObjects: ${opts.objectsEn.join(", ") || "(none)"}\n`;
23
 
24
+ const hf = createInferenceClient(opts.accessToken);
25
  const res = await hf.chatCompletion({
26
  provider: "novita",
27
  model: MODEL,