@gradio/client 2.3.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +4 -2
  3. package/dist/__vite-browser-external-Cgmn0awE.js +7 -0
  4. package/dist/browser.js +1949 -1963
  5. package/dist/client.d.ts +5 -0
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/constants.d.ts +4 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/helpers/api_info.d.ts.map +1 -1
  10. package/dist/helpers/data.d.ts.map +1 -1
  11. package/dist/helpers/init_helpers.d.ts +11 -2
  12. package/dist/helpers/init_helpers.d.ts.map +1 -1
  13. package/dist/index.cjs +2584 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2471 -2466
  17. package/dist/index.min.js +1962 -0
  18. package/dist/types.d.ts +29 -0
  19. package/dist/types.d.ts.map +1 -1
  20. package/dist/utils/duplicate.d.ts.map +1 -1
  21. package/dist/utils/predict.d.ts.map +1 -1
  22. package/dist/utils/run_history.d.ts +64 -0
  23. package/dist/utils/run_history.d.ts.map +1 -0
  24. package/dist/utils/stream.d.ts.map +1 -1
  25. package/dist/utils/submit.d.ts.map +1 -1
  26. package/package.json +11 -4
  27. package/scripts/verify_dist.mjs +46 -0
  28. package/src/client.ts +54 -5
  29. package/src/constants.ts +10 -0
  30. package/src/helpers/api_info.ts +46 -19
  31. package/src/helpers/data.ts +8 -3
  32. package/src/helpers/init_helpers.ts +54 -7
  33. package/src/index.ts +12 -0
  34. package/src/test/api_info.test.ts +77 -12
  35. package/src/test/data.test.ts +26 -3
  36. package/src/test/init.test.ts +21 -3
  37. package/src/test/init_helpers.test.ts +67 -1
  38. package/src/test/refresh.test.ts +31 -0
  39. package/src/test/run_history.test.ts +345 -0
  40. package/src/test/submit.test.ts +1 -3
  41. package/src/types.ts +29 -0
  42. package/src/utils/duplicate.ts +2 -0
  43. package/src/utils/predict.ts +14 -1
  44. package/src/utils/run_history.ts +520 -0
  45. package/src/utils/stream.ts +17 -4
  46. package/src/utils/submit.ts +105 -12
  47. package/vite.config.ts +20 -4
  48. package/dist/__vite-browser-external-DYxpcVy9.js +0 -4
@@ -0,0 +1,345 @@
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import {
4
+ clear_run_history,
5
+ consume_run_history_replay,
6
+ delete_run_history,
7
+ on_run_history_change,
8
+ read_run_history,
9
+ run_history_url,
10
+ stage_run_history_replay,
11
+ start_run_history,
12
+ update_run_history,
13
+ update_run_inputs
14
+ } from "../utils/run_history";
15
+
16
+ const app_id = "app-under-test";
17
+ const other_app = "some-other-app";
18
+ const scope = { app_id };
19
+ const other_scope = { app_id: other_app };
20
+ const replacement_apps = Array.from(
21
+ { length: 8 },
22
+ (_, index) => `replacement-app-${index}`
23
+ );
24
+ const in_browser = typeof window !== "undefined";
25
+
26
+ afterEach(() => {
27
+ clear_run_history(scope);
28
+ clear_run_history(other_scope);
29
+ for (const replacement_app of replacement_apps) {
30
+ clear_run_history({ app_id: replacement_app });
31
+ }
32
+ vi.useRealTimers();
33
+ });
34
+
35
+ describe.skipIf(!in_browser)("run history", () => {
36
+ test("stores runs per app id with the page and inputs", () => {
37
+ const id = start_run_history({
38
+ app_id,
39
+ endpoint: "/predict",
40
+ api_name: "/predict",
41
+ fn_index: 0,
42
+ inputs: ["hello"],
43
+ input_components: [
44
+ {
45
+ type: "textbox",
46
+ component_class_id: "textbox-id",
47
+ props: { label: "Prompt" }
48
+ }
49
+ ]
50
+ });
51
+
52
+ const runs = read_run_history(scope);
53
+ expect(runs).toHaveLength(1);
54
+ expect(runs[0]).toMatchObject({
55
+ id,
56
+ endpoint: "/predict",
57
+ api_name: "/predict",
58
+ inputs: ["hello"],
59
+ status: "running",
60
+ input_components: [
61
+ {
62
+ type: "textbox",
63
+ component_class_id: "textbox-id",
64
+ props: { label: "Prompt" }
65
+ }
66
+ ],
67
+ page: `${window.location.pathname}${window.location.search}`
68
+ });
69
+ expect(read_run_history(other_scope)).toEqual([]);
70
+ });
71
+
72
+ test("replaces inputs with their upload-processed values", () => {
73
+ const id = start_run_history({
74
+ app_id,
75
+ endpoint: "/predict",
76
+ api_name: "/predict",
77
+ fn_index: 0,
78
+ inputs: [new File(["hello"], "hello.txt")]
79
+ });
80
+
81
+ update_run_inputs(scope, id, [
82
+ { path: "/tmp/hello.txt", url: "/gradio_api/file=/tmp/hello.txt" }
83
+ ]);
84
+
85
+ expect(read_run_history(scope)[0].inputs).toEqual([
86
+ { path: "/tmp/hello.txt", url: "/gradio_api/file=/tmp/hello.txt" }
87
+ ]);
88
+ });
89
+
90
+ test("updates outputs and completion state", () => {
91
+ const id = start_run_history({
92
+ app_id,
93
+ endpoint: "/predict",
94
+ api_name: "/predict",
95
+ fn_index: 0,
96
+ inputs: ["hello"]
97
+ });
98
+
99
+ update_run_history(scope, id, {
100
+ type: "data",
101
+ endpoint: "/predict",
102
+ fn_index: 0,
103
+ data: ["hello hello"]
104
+ });
105
+ update_run_history(scope, id, {
106
+ type: "status",
107
+ endpoint: "/predict",
108
+ fn_index: 0,
109
+ queue: false,
110
+ stage: "complete",
111
+ time: new Date("2025-05-16T21:45:00Z")
112
+ });
113
+
114
+ expect(read_run_history(scope)[0]).toMatchObject({
115
+ outputs: ["hello hello"],
116
+ status: "completed",
117
+ completed_at: "2025-05-16T21:45:00.000Z"
118
+ });
119
+ });
120
+
121
+ test("records the runtime reported by the server", () => {
122
+ const id = start_run_history({
123
+ app_id,
124
+ endpoint: "/predict",
125
+ api_name: "/predict",
126
+ fn_index: 0,
127
+ inputs: ["hello"]
128
+ });
129
+
130
+ update_run_history(scope, id, {
131
+ type: "status",
132
+ endpoint: "/predict",
133
+ fn_index: 0,
134
+ queue: true,
135
+ stage: "complete",
136
+ cache_duration: 1.25,
137
+ time: new Date("2025-05-16T21:45:00Z")
138
+ });
139
+
140
+ expect(read_run_history(scope)[0]).toMatchObject({
141
+ duration_ms: 1250,
142
+ completed_at: "2025-05-16T21:45:00.000Z"
143
+ });
144
+ });
145
+
146
+ test("records failed runs and their error message", () => {
147
+ const id = start_run_history({
148
+ app_id,
149
+ endpoint: 3,
150
+ api_name: "Function 3",
151
+ fn_index: 3,
152
+ inputs: [{ circular: null }]
153
+ });
154
+
155
+ update_run_history(scope, id, {
156
+ type: "status",
157
+ endpoint: "/predict",
158
+ fn_index: 3,
159
+ queue: true,
160
+ stage: "error",
161
+ message: "generation failed"
162
+ });
163
+
164
+ expect(read_run_history(scope)[0]).toMatchObject({
165
+ status: "failed",
166
+ error: "generation failed"
167
+ });
168
+ expect(read_run_history(scope)[0].duration_ms).toBeGreaterThanOrEqual(0);
169
+ });
170
+
171
+ test("keeps only the newest 100 runs", () => {
172
+ for (let index = 0; index < 105; index++) {
173
+ start_run_history({
174
+ app_id,
175
+ endpoint: "/predict",
176
+ api_name: "/predict",
177
+ fn_index: 0,
178
+ inputs: [index]
179
+ });
180
+ }
181
+
182
+ const runs = read_run_history(scope);
183
+ expect(runs).toHaveLength(100);
184
+ expect(runs[0].inputs).toEqual([104]);
185
+ expect(runs.at(-1)?.inputs).toEqual([5]);
186
+ });
187
+
188
+ test("deletes an individual run", () => {
189
+ const first = start_run_history({
190
+ app_id,
191
+ endpoint: "/predict",
192
+ api_name: "/predict",
193
+ fn_index: 0,
194
+ inputs: ["first"]
195
+ });
196
+ start_run_history({
197
+ app_id,
198
+ endpoint: "/predict",
199
+ api_name: "/predict",
200
+ fn_index: 0,
201
+ inputs: ["second"]
202
+ });
203
+
204
+ delete_run_history(scope, first!);
205
+
206
+ expect(read_run_history(scope)).toHaveLength(1);
207
+ expect(read_run_history(scope)[0].inputs).toEqual(["second"]);
208
+ });
209
+
210
+ test("notifies this tab when runs are added, deleted or cleared", () => {
211
+ let notified = 0;
212
+ const unsubscribe = on_run_history_change(() => notified++);
213
+
214
+ const id = start_run_history({
215
+ app_id,
216
+ endpoint: "/predict",
217
+ api_name: "/predict",
218
+ fn_index: 0,
219
+ inputs: ["hello"]
220
+ });
221
+ expect(notified).toBe(1);
222
+
223
+ // Progress updates are not structural, so they must not notify.
224
+ update_run_history(scope, id, {
225
+ type: "status",
226
+ endpoint: "/predict",
227
+ fn_index: 0,
228
+ queue: true,
229
+ stage: "complete"
230
+ });
231
+ expect(notified).toBe(1);
232
+
233
+ delete_run_history(scope, id!);
234
+ expect(notified).toBe(2);
235
+
236
+ clear_run_history(scope);
237
+ expect(notified).toBe(3);
238
+
239
+ unsubscribe();
240
+ start_run_history({
241
+ app_id,
242
+ endpoint: "/predict",
243
+ api_name: "/predict",
244
+ fn_index: 0,
245
+ inputs: ["bye"]
246
+ });
247
+ expect(notified).toBe(3);
248
+ });
249
+
250
+ test("keeps a separate history per app id", () => {
251
+ start_run_history({
252
+ app_id,
253
+ endpoint: "/predict",
254
+ api_name: "/predict",
255
+ fn_index: 0,
256
+ inputs: ["mine"]
257
+ });
258
+ start_run_history({
259
+ app_id: other_app,
260
+ endpoint: "/predict",
261
+ api_name: "/predict",
262
+ fn_index: 0,
263
+ inputs: ["theirs"]
264
+ });
265
+
266
+ expect(read_run_history(scope)[0].inputs).toEqual(["mine"]);
267
+ expect(read_run_history(other_scope)[0].inputs).toEqual(["theirs"]);
268
+ });
269
+
270
+ test("keeps a separate history per user of the same app", () => {
271
+ const ada = { app_id, username: "ada" };
272
+ const grace = { app_id, username: "grace" };
273
+
274
+ start_run_history({
275
+ ...ada,
276
+ endpoint: "/predict",
277
+ api_name: "/predict",
278
+ fn_index: 0,
279
+ inputs: ["ada's secret"]
280
+ });
281
+
282
+ expect(read_run_history(grace)).toEqual([]);
283
+ expect(read_run_history(scope)).toEqual([]);
284
+ expect(read_run_history(ada)[0].inputs).toEqual(["ada's secret"]);
285
+
286
+ clear_run_history(ada);
287
+ });
288
+
289
+ test("removes staged replays when pruning an old app", () => {
290
+ vi.useFakeTimers();
291
+ vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
292
+ start_run_history({
293
+ app_id: other_app,
294
+ endpoint: "/predict",
295
+ api_name: "/predict",
296
+ fn_index: 0,
297
+ inputs: ["old"]
298
+ });
299
+ stage_run_history_replay(other_scope, read_run_history(other_scope)[0]);
300
+
301
+ for (const [index, replacement_app] of replacement_apps.entries()) {
302
+ vi.setSystemTime(new Date(Date.UTC(2025, 0, index + 2)));
303
+ start_run_history({
304
+ app_id: replacement_app,
305
+ endpoint: "/predict",
306
+ api_name: "/predict",
307
+ fn_index: 0,
308
+ inputs: [index]
309
+ });
310
+ }
311
+
312
+ expect(read_run_history(other_scope)).toEqual([]);
313
+ expect(consume_run_history_replay(other_scope)).toBeNull();
314
+ });
315
+ });
316
+
317
+ describe("run history url", () => {
318
+ test("joins the app root and the api prefix", () => {
319
+ expect(run_history_url("http://localhost:7860")).toBe(
320
+ "http://localhost:7860/gradio_api/runs"
321
+ );
322
+ expect(run_history_url("http://localhost:7860/my-app/")).toBe(
323
+ "http://localhost:7860/my-app/gradio_api/runs"
324
+ );
325
+ expect(run_history_url("http://localhost:7860", "/api")).toBe(
326
+ "http://localhost:7860/api/runs"
327
+ );
328
+ });
329
+ });
330
+
331
+ describe.skipIf(in_browser)("run history outside the browser", () => {
332
+ test("is a no-op without storage", () => {
333
+ expect(
334
+ start_run_history({
335
+ app_id,
336
+ endpoint: "/predict",
337
+ api_name: "/predict",
338
+ fn_index: 0,
339
+ inputs: ["hello"]
340
+ })
341
+ ).toBeNull();
342
+ expect(read_run_history(scope)).toEqual([]);
343
+ expect(consume_run_history_replay(scope)).toBeNull();
344
+ });
345
+ });
@@ -162,8 +162,6 @@ describe("predict error handling", () => {
162
162
  1000,
163
163
  "predict() never settled for an unknown endpoint"
164
164
  )
165
- ).rejects.toThrow(
166
- "There is no endpoint matching that name of fn_index matching that number."
167
- );
165
+ ).rejects.toThrow('No endpoint matching "nonexistent_endpoint" was found');
168
166
  });
169
167
  });
package/src/types.ts CHANGED
@@ -36,6 +36,8 @@ export interface EndpointInfo<T extends ApiData | JsApiData> {
36
36
  parameters: T[];
37
37
  returns: T[];
38
38
  type?: DependencyTypes;
39
+ /** Set when the endpoint's function takes a `gr.OAuthToken`. */
40
+ oauth_token?: "required" | "optional";
39
41
  }
40
42
 
41
43
  export interface ApiInfo<T extends ApiData | JsApiData> {
@@ -209,6 +211,12 @@ export interface Config {
209
211
  pwa?: boolean;
210
212
  i18n_translations?: Record<string, Record<string, string>> | null;
211
213
  mcp_server?: boolean;
214
+ /**
215
+ * Whether the app permits its runs being saved in the browser. Set from
216
+ * `run_history` on `launch()`; absent on apps served by older versions of
217
+ * Gradio, which is treated as permitted.
218
+ */
219
+ run_history?: boolean;
212
220
  }
213
221
 
214
222
  // todo: DRY up types
@@ -301,6 +309,7 @@ export interface Payload {
301
309
  time?: Date;
302
310
  event_data?: unknown;
303
311
  trigger_id?: number | null;
312
+ oauth_token?: string;
304
313
  }
305
314
 
306
315
  export interface PostResponse {
@@ -323,6 +332,11 @@ export interface DuplicateOptions extends ClientOptions {
323
332
 
324
333
  export interface ClientOptions {
325
334
  token?: `hf_${string}`;
335
+ /**
336
+ * @deprecated Use `token` instead. Kept as an alias so that code written
337
+ * for older versions of the client keeps working.
338
+ */
339
+ hf_token?: `hf_${string}`;
326
340
  status_callback?: SpaceStatusCallback | null;
327
341
  auth?: [string, string] | null;
328
342
  with_null_state?: boolean;
@@ -332,6 +346,21 @@ export interface ClientOptions {
332
346
  session_hash?: string;
333
347
  cookies?: string;
334
348
  credentials?: RequestCredentials;
349
+ /**
350
+ * A Hugging Face token passed to the app's own code, for endpoints whose
351
+ * function takes a `gr.OAuthToken`. Unlike `token`, which only authenticates
352
+ * you to the app, this lets the app act on your behalf, so it is sent only to
353
+ * endpoints that declare they need it.
354
+ */
355
+ oauth_token?: string;
356
+ /**
357
+ * Whether to save each call's inputs and outputs in this browser's local
358
+ * storage, so they can be reviewed on the app's run history page. Defaults
359
+ * to true, and only ever applies in a browser: in Node there is no storage
360
+ * to write to and nothing is recorded. The app can turn it off for everyone
361
+ * with `run_history=False` on `launch()`, which takes precedence over this.
362
+ */
363
+ record_history?: boolean;
335
364
  }
336
365
 
337
366
  export interface FileData {
@@ -8,6 +8,7 @@ import { Client } from "../client";
8
8
  import { SPACE_METADATA_ERROR_MSG } from "../constants";
9
9
  import {
10
10
  get_cookie_header,
11
+ normalise_token_option,
11
12
  parse_and_set_cookies
12
13
  } from "../helpers/init_helpers";
13
14
  import { process_endpoint } from "../helpers/api_info";
@@ -16,6 +17,7 @@ export async function duplicate(
16
17
  app_reference: string,
17
18
  options: DuplicateOptions
18
19
  ): Promise<Client> {
20
+ normalise_token_option(options);
19
21
  const { token, private: _private, hardware, timeout, auth } = options;
20
22
 
21
23
  if (hardware && !hardware_types.includes(hardware)) {
@@ -37,7 +37,20 @@ export async function predict<T = unknown>(
37
37
 
38
38
  if (message.type === "status") {
39
39
  if (message.stage === "error") {
40
- throw message;
40
+ // Throw a real `Error` (rather than the raw status object) so that
41
+ // uncaught failures surface a readable message instead of
42
+ // crashing Node with `ERR_UNHANDLED_REJECTION ... reason "#<Object>"`.
43
+ // The status fields are preserved on the error for callers that
44
+ // inspect them.
45
+ const { message: error_message, ...status } = message;
46
+ const error = new Error(
47
+ (typeof error_message === "string"
48
+ ? error_message
49
+ : error_message && JSON.stringify(error_message)) ||
50
+ "An unknown error occurred while making a prediction."
51
+ );
52
+ Object.assign(error, status);
53
+ throw error;
41
54
  }
42
55
  if (message.stage === "complete") {
43
56
  status_complete = true;