Use signed-in user's HF OAuth token for inference
Browse filesThe 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 +1 -0
- app/api/phrases/route.ts +1 -0
- app/api/stories/route.ts +1 -0
- app/api/translate/route.ts +1 -1
- app/api/vision/analyze/route.ts +1 -0
- lib/hf/client.ts +14 -13
- lib/hf/dialogue.ts +2 -1
- lib/hf/gemma-vision.ts +2 -1
- lib/hf/inference.ts +4 -2
- lib/hf/phrases.ts +2 -1
- lib/hf/sentences.ts +2 -1
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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
//
|
| 7 |
-
|
| 8 |
-
if (
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
);
|
| 12 |
}
|
| 13 |
-
}
|
| 14 |
|
| 15 |
-
export function createInferenceClient() {
|
| 16 |
const serverToken = process.env.HF_TOKEN?.trim();
|
| 17 |
if (!serverToken) {
|
| 18 |
throw new Error(
|
| 19 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|