Thomas G. Lopes commited on
Commit
8cfdfbf
·
1 Parent(s): 8816e9f

fetch token from parent frame

Browse files
.env.example CHANGED
@@ -1,4 +1,5 @@
1
  PUBLIC_ENABLE_MCP=
 
2
  HYPERBOLIC_API_KEY=
3
  COHERE_API_KEY=
4
  TOGETHER_API_KEY=
 
1
  PUBLIC_ENABLE_MCP=
2
+ PUBLIC_HF_TOKEN=
3
  HYPERBOLIC_API_KEY=
4
  COHERE_API_KEY=
5
  TOGETHER_API_KEY=
src/lib/components/debug-menu.svelte CHANGED
@@ -46,12 +46,6 @@
46
  showQuotaModal();
47
  },
48
  },
49
- {
50
- label: "Show token modal",
51
- cb: () => {
52
- token.showModal = true;
53
- },
54
- },
55
  {
56
  label: "Test toast",
57
  cb: () => {
 
46
  showQuotaModal();
47
  },
48
  },
 
 
 
 
 
 
49
  {
50
  label: "Test toast",
51
  cb: () => {
src/lib/components/inference-playground/hf-token-modal.svelte DELETED
@@ -1,121 +0,0 @@
1
- <script lang="ts">
2
- import { createBubbler, preventDefault } from "svelte/legacy";
3
-
4
- const bubble = createBubbler();
5
- import { clickOutside } from "$lib/attachments/click-outside.js";
6
- import { createEventDispatcher, onDestroy, onMount } from "svelte";
7
-
8
- import IconCross from "~icons/carbon/close";
9
- import { autofocus } from "$lib/attachments/autofocus.js";
10
-
11
- interface Props {
12
- storeLocallyHfToken?: boolean;
13
- }
14
-
15
- let { storeLocallyHfToken = $bindable(false) }: Props = $props();
16
-
17
- let backdropEl = $state<HTMLDivElement>();
18
- let modalEl = $state<HTMLDivElement>();
19
-
20
- const dispatch = createEventDispatcher<{ close: void }>();
21
-
22
- function handleKeydown(event: KeyboardEvent) {
23
- const { key } = event;
24
- if (key === "Escape") {
25
- event.preventDefault();
26
- dispatch("close");
27
- }
28
- }
29
-
30
- onMount(() => {
31
- document.getElementById("app")?.setAttribute("inert", "true");
32
- });
33
-
34
- onDestroy(() => {
35
- // remove inert attribute if this is the last modal
36
- if (document.querySelectorAll('[role="dialog"]:not(#app *)').length === 1) {
37
- document.getElementById("app")?.removeAttribute("inert");
38
- }
39
- });
40
- </script>
41
-
42
- <div
43
- id="default-modal"
44
- aria-hidden="true"
45
- class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-black/85"
46
- bind:this={backdropEl}
47
- >
48
- <div
49
- role="dialog"
50
- tabindex="-1"
51
- class="relative max-h-full w-full max-w-xl p-4 outline-hidden"
52
- bind:this={modalEl}
53
- onkeydown={handleKeydown}
54
- {@attach clickOutside(() => dispatch("close"))}
55
- >
56
- <form onsubmit={preventDefault(bubble("submit"))} class="relative rounded-lg bg-white shadow-sm dark:bg-gray-900">
57
- <div class="flex items-center justify-between rounded-t border-b p-4 md:px-5 md:py-4 dark:border-gray-800">
58
- <h3 class="flex items-center gap-2.5 text-lg font-semibold text-gray-900 dark:text-white">
59
- <img
60
- alt="Hugging Face's logo"
61
- class="w-7"
62
- src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
63
- /> Add a Hugging Face Token
64
- </h3>
65
- <button
66
- type="button"
67
- onclick={() => dispatch("close")}
68
- class="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white"
69
- >
70
- <div class="text-xl">
71
- <IconCross />
72
- </div>
73
- <span class="sr-only">Close modal</span>
74
- </button>
75
- </div>
76
- <!-- Modal body -->
77
- <div class="p-4 md:p-5">
78
- <p class="mb-5 text-base leading-relaxed text-gray-800 2xl:text-balance dark:text-gray-300">
79
- You need a free Hugging Face token to use this application. <strong class="font-semibold"
80
- >Make sure you create a token with Inference API permission.</strong
81
- ><br /> Your token is kept safe by only being used from your browser.
82
- </p>
83
- <div>
84
- <label for="hf-token" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white">
85
- Hugging Face Token
86
- </label>
87
- <input
88
- required
89
- placeholder="Enter HF Token"
90
- type="text"
91
- id="hf-token"
92
- name="hf-token"
93
- class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
94
- {@attach autofocus()}
95
- />
96
- </div>
97
- <label class="mt-4 flex items-center gap-x-1.5 text-gray-900 dark:text-gray-200">
98
- <input type="checkbox" bind:checked={storeLocallyHfToken} />
99
- <p class="text-sm leading-none">Save to local storage for future use</p></label
100
- >
101
- </div>
102
-
103
- <!-- Modal footer -->
104
- <div class="flex items-center justify-between rounded-b border-t border-gray-200 p-4 md:p-5 dark:border-gray-800">
105
- <a
106
- href="https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained"
107
- tabindex="-1"
108
- target="_blank"
109
- class="rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 focus:outline-hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
110
- >Create new token</a
111
- >
112
-
113
- <button
114
- type="submit"
115
- class="rounded-lg bg-black px-5 py-2.5 text-sm font-medium text-white hover:bg-gray-900 focus:ring-4 focus:ring-gray-300 focus:outline-hidden dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700"
116
- >Submit</button
117
- >
118
- </div>
119
- </form>
120
- </div>
121
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/inference-playground/playground.svelte CHANGED
@@ -25,7 +25,6 @@
25
  import PlaygroundConversationHeader from "./conversation-header.svelte";
26
  import PlaygroundConversation from "./conversation.svelte";
27
  import GenerationConfig from "./generation-config.svelte";
28
- import HFTokenModal from "./hf-token-modal.svelte";
29
  import MessageTextarea from "./message-textarea.svelte";
30
  import ModelSelectorModal from "./model-selector-modal.svelte";
31
  import ModelSelector from "./model-selector.svelte";
@@ -39,30 +38,8 @@
39
 
40
  const systemPromptSupported = $derived(conversations.active.some(c => isSystemPromptSupported(c.model)));
41
  const compareActive = $derived(conversations.active.length === 2);
42
-
43
- function handleTokenSubmit(e: Event) {
44
- const form = e.target as HTMLFormElement;
45
- const formData = new FormData(form);
46
- const submittedHfToken = (formData.get("hf-token") as string).trim() ?? "";
47
- const RE_HF_TOKEN = /\bhf_[a-zA-Z0-9]{34}\b/;
48
- if (RE_HF_TOKEN.test(submittedHfToken)) {
49
- token.value = submittedHfToken;
50
- // TODO: Only submit when previous action was trying to submit
51
- // submit();
52
- } else {
53
- alert("Please provide a valid HF token.");
54
- }
55
- }
56
  </script>
57
 
58
- {#if token.showModal}
59
- <HFTokenModal
60
- bind:storeLocallyHfToken={token.writeToLocalStorage}
61
- on:close={() => (token.showModal = false)}
62
- on:submit={handleTokenSubmit}
63
- />
64
- {/if}
65
-
66
  <div
67
  class={[
68
  "motion-safe:animate-fade-in grid h-dvh divide-gray-200 overflow-hidden bg-gray-100/50",
@@ -253,26 +230,6 @@
253
  <IconWaterfall class="text-xs" />
254
  Metrics
255
  </a>
256
- <button
257
- onclick={token.reset}
258
- class="flex items-center gap-1 text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
259
- >
260
- <svg xmlns="http://www.w3.org/2000/svg" class="text-xs" width="1em" height="1em" viewBox="0 0 32 32">
261
- <path
262
- fill="currentColor"
263
- d="M23.216 4H26V2h-7v6h2V5.096A11.96 11.96 0 0 1 28 16c0 6.617-5.383 12-12 12v2c7.72 0 14-6.28 14-14c0-5.009-2.632-9.512-6.784-12"
264
- />
265
- <path fill="currentColor" d="M16 20a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M15 9h2v9h-2z" /><path
266
- fill="currentColor"
267
- d="M16 4V2C8.28 2 2 8.28 2 16c0 4.977 2.607 9.494 6.784 12H6v2h7v-6h-2v2.903A11.97 11.97 0 0 1 4 16C4 9.383 9.383 4 16 4"
268
- />
269
- </svg>
270
- {#if token.value}
271
- Reset token
272
- {:else}
273
- Set token
274
- {/if}
275
- </button>
276
  </div>
277
  </div>
278
 
 
25
  import PlaygroundConversationHeader from "./conversation-header.svelte";
26
  import PlaygroundConversation from "./conversation.svelte";
27
  import GenerationConfig from "./generation-config.svelte";
 
28
  import MessageTextarea from "./message-textarea.svelte";
29
  import ModelSelectorModal from "./model-selector-modal.svelte";
30
  import ModelSelector from "./model-selector.svelte";
 
38
 
39
  const systemPromptSupported = $derived(conversations.active.some(c => isSystemPromptSupported(c.model)));
40
  const compareActive = $derived(conversations.active.length === 2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </script>
42
 
 
 
 
 
 
 
 
 
43
  <div
44
  class={[
45
  "motion-safe:animate-fade-in grid h-dvh divide-gray-200 overflow-hidden bg-gray-100/50",
 
230
  <IconWaterfall class="text-xs" />
231
  Metrics
232
  </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  </div>
234
  </div>
235
 
src/lib/state/conversations.svelte.ts CHANGED
@@ -202,7 +202,7 @@ export class ConversationClass {
202
 
203
  genNextMessage = async () => {
204
  if (!token.value) {
205
- token.showModal = true;
206
  return;
207
  }
208
 
@@ -437,7 +437,7 @@ class Conversations {
437
 
438
  genNextMessages = async (conv: "left" | "right" | "both" | ConversationClass = "both") => {
439
  if (!token.value) {
440
- token.showModal = true;
441
  return;
442
  }
443
 
 
202
 
203
  genNextMessage = async () => {
204
  if (!token.value) {
205
+ token.requestTokenFromParent();
206
  return;
207
  }
208
 
 
437
 
438
  genNextMessages = async (conv: "left" | "right" | "both" | ConversationClass = "both") => {
439
  if (!token.value) {
440
+ token.requestTokenFromParent();
441
  return;
442
  }
443
 
src/lib/state/token.svelte.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { safeParse } from "$lib/utils/json.js";
 
2
  import typia from "typia";
3
 
4
  const key = "hf_token";
@@ -6,12 +7,22 @@ const key = "hf_token";
6
  class Token {
7
  #value = $state("");
8
  writeToLocalStorage = $state(true);
9
- showModal = $state(false);
10
 
11
  constructor() {
 
 
 
 
 
12
  const storedHfToken = localStorage.getItem(key);
13
  const parsed = safeParse(storedHfToken ?? "");
14
- this.value = typia.is<string>(parsed) ? parsed : "";
 
 
 
 
 
 
15
  }
16
 
17
  get value() {
@@ -23,12 +34,34 @@ class Token {
23
  localStorage.setItem(key, JSON.stringify(token));
24
  }
25
  this.#value = token;
26
- this.showModal = !token.length;
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  reset = () => {
30
  this.value = "";
31
  localStorage.removeItem(key);
 
 
 
32
  };
33
  }
34
 
 
1
  import { safeParse } from "$lib/utils/json.js";
2
+ import { PUBLIC_HF_TOKEN } from "$env/static/public";
3
  import typia from "typia";
4
 
5
  const key = "hf_token";
 
7
  class Token {
8
  #value = $state("");
9
  writeToLocalStorage = $state(true);
 
10
 
11
  constructor() {
12
+ if (PUBLIC_HF_TOKEN) {
13
+ this.#value = PUBLIC_HF_TOKEN;
14
+ return;
15
+ }
16
+
17
  const storedHfToken = localStorage.getItem(key);
18
  const parsed = safeParse(storedHfToken ?? "");
19
+ const storedToken = typia.is<string>(parsed) ? parsed : "";
20
+
21
+ if (storedToken) {
22
+ this.#value = storedToken;
23
+ } else {
24
+ this.requestTokenFromParent();
25
+ }
26
  }
27
 
28
  get value() {
 
34
  localStorage.setItem(key, JSON.stringify(token));
35
  }
36
  this.#value = token;
 
37
  }
38
 
39
+ requestTokenFromParent = (): Promise<void> => {
40
+ if (typeof window === "undefined") return Promise.resolve();
41
+
42
+ return new Promise(resolve => {
43
+ const handleMessage = (event: MessageEvent) => {
44
+ if (event.data.type === "INFERENCE_JWT_RESPONSE") {
45
+ const token = event.data.token;
46
+ if (token && typeof token === "string") {
47
+ this.value = token;
48
+ window.removeEventListener("message", handleMessage);
49
+ resolve();
50
+ }
51
+ }
52
+ };
53
+
54
+ window.addEventListener("message", handleMessage);
55
+ window.parent?.postMessage({ type: "INFERENCE_JWT_REQUEST" }, "*");
56
+ });
57
+ };
58
+
59
  reset = () => {
60
  this.value = "";
61
  localStorage.removeItem(key);
62
+ if (!PUBLIC_HF_TOKEN) {
63
+ this.requestTokenFromParent();
64
+ }
65
  };
66
  }
67
 
src/lib/utils/business.svelte.ts CHANGED
@@ -122,6 +122,7 @@ export async function handleStreamingResponse(
122
  conversation: ConversationClass | Conversation,
123
  onChunk: (content: string) => void,
124
  abortController: AbortController,
 
125
  ): Promise<void> {
126
  const data = conversation instanceof ConversationClass ? conversation.data : conversation;
127
  const model = conversation.model;
@@ -149,28 +150,37 @@ export async function handleStreamingResponse(
149
  enabledMCPs: getEnabledMCPs(),
150
  };
151
 
152
- const reader = await StreamReader.fromFetch("/api/generate", {
153
- method: "POST",
154
- headers: {
155
- "Content-Type": "application/json",
156
- },
157
- body: JSON.stringify(requestBody),
158
- signal: abortController.signal,
159
- });
 
160
 
161
- let out = "";
162
- for await (const chunk of reader.read()) {
163
- if (chunk.type === "chunk" && chunk.content) {
164
- out += chunk.content;
165
- onChunk(out);
166
- } else if (chunk.type === "error") {
167
- throw new Error(chunk.error || "Stream error");
 
 
 
 
 
 
168
  }
 
169
  }
170
  }
171
 
172
  export async function handleNonStreamingResponse(
173
  conversation: ConversationClass | Conversation,
 
174
  ): Promise<{ message: ChatCompletionOutputMessage; completion_tokens: number }> {
175
  const data = conversation instanceof ConversationClass ? conversation.data : conversation;
176
  const model = conversation.model;
@@ -207,6 +217,10 @@ export async function handleNonStreamingResponse(
207
  });
208
 
209
  if (!response.ok) {
 
 
 
 
210
  const error = await response.json();
211
  throw new Error(error.error || "Failed to generate response");
212
  }
 
122
  conversation: ConversationClass | Conversation,
123
  onChunk: (content: string) => void,
124
  abortController: AbortController,
125
+ retryCount = 0,
126
  ): Promise<void> {
127
  const data = conversation instanceof ConversationClass ? conversation.data : conversation;
128
  const model = conversation.model;
 
150
  enabledMCPs: getEnabledMCPs(),
151
  };
152
 
153
+ try {
154
+ const reader = await StreamReader.fromFetch("/api/generate", {
155
+ method: "POST",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ },
159
+ body: JSON.stringify(requestBody),
160
+ signal: abortController.signal,
161
+ });
162
 
163
+ let out = "";
164
+ for await (const chunk of reader.read()) {
165
+ if (chunk.type === "chunk" && chunk.content) {
166
+ out += chunk.content;
167
+ onChunk(out);
168
+ } else if (chunk.type === "error") {
169
+ throw new Error(chunk.error || "Stream error");
170
+ }
171
+ }
172
+ } catch (error) {
173
+ if (error instanceof Error && error.message.includes("401") && retryCount === 0) {
174
+ await token.requestTokenFromParent();
175
+ return handleStreamingResponse(conversation, onChunk, abortController, retryCount + 1);
176
  }
177
+ throw error;
178
  }
179
  }
180
 
181
  export async function handleNonStreamingResponse(
182
  conversation: ConversationClass | Conversation,
183
+ retryCount = 0,
184
  ): Promise<{ message: ChatCompletionOutputMessage; completion_tokens: number }> {
185
  const data = conversation instanceof ConversationClass ? conversation.data : conversation;
186
  const model = conversation.model;
 
217
  });
218
 
219
  if (!response.ok) {
220
+ if (response.status === 401 && retryCount === 0) {
221
+ await token.requestTokenFromParent();
222
+ return handleNonStreamingResponse(conversation, retryCount + 1);
223
+ }
224
  const error = await response.json();
225
  throw new Error(error.error || "Failed to generate response");
226
  }
src/lib/utils/stream.ts CHANGED
@@ -49,6 +49,9 @@ export class StreamReader {
49
  static async fromFetch(url: string, options?: RequestInit): Promise<StreamReader> {
50
  const response = await fetch(url, options);
51
  if (!response.ok) {
 
 
 
52
  const error = await response.json();
53
  throw new Error(error.error || "Request failed");
54
  }
 
49
  static async fromFetch(url: string, options?: RequestInit): Promise<StreamReader> {
50
  const response = await fetch(url, options);
51
  if (!response.ok) {
52
+ if (response.status === 401) {
53
+ throw new Error("401 Unauthorized");
54
+ }
55
  const error = await response.json();
56
  throw new Error(error.error || "Request failed");
57
  }
src/routes/api/generate/+server.ts CHANGED
@@ -7,6 +7,7 @@ import { createAdapter, type GenerationArgs } from "./adapter.js";
7
  import { connectToMCPServers, executeMcpTool, type MCPServerConnection } from "./mcp.js";
8
  import type { FinishReason, GenerateRequest } from "./types.js";
9
  import { debugLog } from "./utils.js";
 
10
 
11
  type AssistantResponse = { message: ChatCompletionMessage; finish_reason: FinishReason };
12
 
@@ -197,6 +198,16 @@ export const POST: RequestHandler = async ({ request }) => {
197
  } catch (error) {
198
  debugLog(JSON.stringify(error, null, 2));
199
  console.error("Generation error:", error);
200
- return json({ error: error instanceof Error ? error.message : "Unknown error occurred" }, { status: 500 });
 
 
 
 
 
 
 
 
 
 
201
  }
202
  };
 
7
  import { connectToMCPServers, executeMcpTool, type MCPServerConnection } from "./mcp.js";
8
  import type { FinishReason, GenerateRequest } from "./types.js";
9
  import { debugLog } from "./utils.js";
10
+ import { InferenceClientProviderApiError, InferenceClientHubApiError } from "@huggingface/inference";
11
 
12
  type AssistantResponse = { message: ChatCompletionMessage; finish_reason: FinishReason };
13
 
 
198
  } catch (error) {
199
  debugLog(JSON.stringify(error, null, 2));
200
  console.error("Generation error:", error);
201
+
202
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
203
+ let status = 500;
204
+
205
+ if (error instanceof InferenceClientProviderApiError || error instanceof InferenceClientHubApiError) {
206
+ status = error.httpResponse.status;
207
+ } else if (error && typeof error === "object" && "status" in error && typeof error.status === "number") {
208
+ status = error.status;
209
+ }
210
+
211
+ return json({ error: errorMessage }, { status });
212
  }
213
  };