Repository: gradio-app/gradio
Pull request: Add support for passing keyword args to `data` in JS client
## Description

This PR adds support for passing keyword arguments as well as positional args to the `data` param in the JS client. This is backwards compatible, so the two usages will work:

```
const result = await app.predict("/predict", ["Chewbacca"]);
```
```
const result = await app.predict("/predict", { name: "Chewbacca" });
```
Also adds unit tests. 

The `CodeSnippet.svelte` API usage has been updated, which you can see here: https://huggingface.co/spaces/hmb/test_args

Closes: #7831

## 🎯 PRs Should Target Issues

Before your create a PR, please check to see if there is [an existing issue](https://github.com/gradio-app/gradio/issues) for this change. If not, please create an issue before you create this PR, unless the fix is very small. 

Not adhering to this guideline will result in the PR being closed. 

## Tests

1. PRs will only be merged if tests pass on CI. To run the tests locally, please set up [your Gradio environment locally](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md) and run the tests: `bash scripts/run_all_tests.sh`

2. You may need to run the linters: `bash scripts/format_backend.sh` and `bash scripts/format_frontend.sh`
  


Problem statement:
Add support for key-word arguments in JS client
This is already supported in the Python Client, e.g.

```py
from gradio_client import Client, file

client = Client("abidlabs/whisper")

client.predict(
    audio=file("audio_sample.wav")
)
```
```
>> "This is a test of the whisper speech recognition model."
```

Beyond consistency, it would allow the JS client to take advantage of default arguments

Hints from issue discussion:

Commit message: Merge branch 'main' into js-client-kwargs

Patch to review:
diff --git a/.changeset/sad-sides-sing.md b/.changeset/sad-sides-sing.md
new file mode 100644
index 0000000000..57a48a313b
--- /dev/null
+++ b/.changeset/sad-sides-sing.md
@@ -0,0 +1,7 @@
+---
+"@gradio/app": minor
+"@gradio/client": minor
+"gradio": minor
+---
+
+feat:Add support for passing keyword args to `data` in JS client
diff --git a/client/js/README.md b/client/js/README.md
index b453ca0d98..a475295a2a 100644
--- a/client/js/README.md
+++ b/client/js/README.md
@@ -127,7 +127,11 @@ The `payload` argument is generally required but this depends on the API itself.
 import { Client } from "@gradio/client";
 
 const app = await Client.connect("user/space-name");
-const result = await app.predict("/predict", [1, "Hello", "friends"]);
+const result = await app.predict("/predict", {
+	input: 1,
+	word_1: "Hello",
+	word_2: "friends"
+});
 ```
 
 #### `submit`
@@ -138,7 +142,7 @@ The `submit` method provides a more flexible way to call an API endpoint, provid
 import { Client } from "@gradio/client";
 
 const app = await Client.connect("user/space-name");
-const submission = app.submit("/predict", payload);
+const submission = app.submit("/predict", { name: "Chewbacca" });
 ```
 
 The `submit` method accepts the same [`endpoint`](#endpoint) and [`payload`](#payload) arguments as `predict`.
@@ -183,7 +187,7 @@ import { Client } from "@gradio/client";
 
 const app = await Client.connect("user/space-name");
 const submission = app
-	.submit("/predict", payload)
+	.submit("/predict", { name: "Chewbacca" })
 	.on("data", (data) => console.log(data))
 	.on("status", (status: Status) => console.log(status));
 ```
@@ -198,7 +202,7 @@ import { Client } from "@gradio/client";
 const app = await Client.connect("user/space-name");
 const handle_data = (data) => console.log(data);
 
-const submission = app.submit("/predict", payload).on("data", handle_data);
+const submission = app.submit("/predict", { name: "Chewbacca" }).on("data", handle_data);
 
 // later
 submission.off("/predict", handle_data);
@@ -214,7 +218,7 @@ import { Client } from "@gradio/client";
 const app = await Client.connect("user/space-name");
 const handle_data = (data) => console.log(data);
 
-const submission = app.submit("/predict", payload).on("data", handle_data);
+const submission = app.submit("/predict", { name: "Chewbacca" }).on("data", handle_data);
 
 // later
 submission.destroy();
@@ -229,7 +233,7 @@ import { Client } from "@gradio/client";
 
 const app = await Client.connect("user/space-name");
 const submission = app
-	.submit("/predict", payload)
+	.submit("/predict", { name: "Chewbacca" })
 	.on("data", (data) => console.log(data));
 
 // later
diff --git a/client/js/src/client.ts b/client/js/src/client.ts
index 637ee2cf3b..c6cf920e23 100644
--- a/client/js/src/client.ts
+++ b/client/js/src/client.ts
@@ -97,15 +97,15 @@ export class Client {
 	) => Promise<unknown[]>;
 	submit: (
 		endpoint: string | number,
-		data: unknown[],
+		data: unknown[] | Record<string, unknown>,
 		event_data?: unknown,
 		trigger_id?: number | null
 	) => SubmitReturn;
 	predict: (
 		endpoint: string | number,
-		data?: unknown[],
+		data: unknown[] | Record<string, unknown>,
 		event_data?: unknown
-	) => Promise<unknown>;
+	) => Promise<SubmitReturn>;
 	open_stream: () => Promise<void>;
 	private resolve_config: (endpoint: string) => Promise<Config | undefined>;
 	constructor(app_reference: string, options: ClientOptions = {}) {
diff --git a/client/js/src/helpers/api_info.ts b/client/js/src/helpers/api_info.ts
index ca13ac2739..5df18a4e38 100644
--- a/client/js/src/helpers/api_info.ts
+++ b/client/js/src/helpers/api_info.ts
@@ -298,3 +298,72 @@ export function handle_message(
 
 	return { type: "none", status: { stage: "error", queue } };
 }
+
+/**
+ * Maps the provided `data` to the parameters defined by the `/info` endpoint response.
+ * This allows us to support both positional and keyword arguments passed to the client
+ * and ensures that all parameters are either directly provided or have default values assigned.
+ *
+ * @param {unknown[] | Record<string, unknown>} data - The input data for the function,
+ *        which can be either an array of values for positional arguments or an object
+ *        with key-value pairs for keyword arguments.
+ * @param {JsApiData[]} parameters - Array of parameter descriptions retrieved from the
+ *        `/info` endpoint.
+ *
+ * @returns {unknown[]} - Returns an array of resolved data where each element corresponds
+ *         to the expected parameter from the API. The `parameter_default` value is used where
+ *         a value is not provided for a parameter, and optional parameters without defaults are
+ * 		   set to `undefined`.
+ *
+ * @throws {Error} - Throws an error:
+ *         - If more arguments are provided than are defined in the parameters.
+ *         - If an argument is provided that does not match any defined parameter.
+ *         - If a parameter value is specified both positionally and as a keyword.
+ */
+
+export const map_data_to_params = (
+	data: unknown[] | Record<string, unknown>,
+	api_info: ApiInfo<JsApiData | ApiData>
+): unknown[] => {
+	const parameters = Object.values(api_info.named_endpoints).flatMap(
+		(values) => values.parameters
+	);
+
+	if (Array.isArray(data)) {
+		if (data.length > parameters.length) {
+			console.warn("Too many arguments provided for the endpoint.");
+		}
+		return data;
+	}
+
+	const resolved_data: unknown[] = [];
+	const provided_keys = Object.keys(data);
+
+	parameters.forEach((param, index) => {
+		if (data.hasOwnProperty(param.parameter_name)) {
+			resolved_data[index] = data[param.parameter_name];
+		} else if (param.parameter_has_default) {
+			resolved_data[index] = param.parameter_default;
+		} else {
+			resolved_data[index] = undefined;
+		}
+	});
+
+	provided_keys.forEach((key) => {
+		if (!parameters.some((param) => param.parameter_name === key)) {
+			throw new Error(
+				`Parameter \`${key}\` is not a valid keyword argument. Please refer to the API for usage.`
+			);
+		}
+	});
+
+	resolved_data.forEach((value, idx) => {
+		if (value === undefined && !parameters[idx].parameter_has_default) {
+			throw new Error(
+				`No value provided for required parameter: ${parameters[idx].parameter_name}`
+			);
+		}
+	});
+
+	return resolved_data;
+};
diff --git a/client/js/src/test/api_info.test.ts b/client/js/src/test/api_info.test.ts
index 407707424e..2cfca02a67 100644
--- a/client/js/src/test/api_info.test.ts
+++ b/client/js/src/test/api_info.test.ts
@@ -4,9 +4,11 @@ import {
 	handle_message,
 	get_description,
 	get_type,
-	process_endpoint
+	process_endpoint,
+	map_data_to_params
 } from "../helpers/api_info";
 import { initialise_server } from "./server";
+import { transformed_api_info } from "./test_data";
 
 const server = initialise_server();
 
@@ -454,3 +456,112 @@ describe("process_endpoint", () => {
 		expect(result).toEqual(expected);
 	});
 });
+
+describe("map_data_params", () => {
+	let test_data = transformed_api_info;
+
+	test_data.named_endpoints["/predict"].parameters = [
+		{
+			parameter_name: "param1",
+			parameter_has_default: false,
+			label: "",
+			component: "",
+			serializer: "",
+			python_type: {
+				type: "",
+				description: ""
+			},
+			type: {
+				type: "",
+				description: ""
+			}
+		},
+		{
+			parameter_name: "param2",
+			parameter_has_default: false,
+			label: "",
+			type: {
+				type: "",
+				description: ""
+			},
+			component: "",
+			serializer: "",
+			python_type: {
+				type: "",
+				description: ""
+			}
+		},
+		{
+			parameter_name: "param3",
+			parameter_has_default: true,
+			parameter_default: 3,
+			label: "",
+			type: {
+				type: "",
+				description: ""
+			},
+			component: "",
+			serializer: "",
+			python_type: {
+				type: "",
+				description: ""
+			}
+		}
+	];
+
+	it("should return an array of data when data is an array", () => {
+		const data = [1, 2];
+
+		const result = map_data_to_params(data, transformed_api_info);
+		expect(result).toEqual(data);
+	});
+
+	it("should return the data when too many arguments are provided for the endpoint", () => {
+		const data = [1, 2, 3, 4];
+
+		const result = map_data_to_params(data, transformed_api_info);
+		expect(result).toEqual(data);
+	});
+
+	it("should return an array of resolved data when data is an object", () => {
+		const data = {
+			param1: 1,
+			param2: 2,
+			param3: 3
+		};
+
+		const result = map_data_to_params(data, transformed_api_info);
+		expect(result).toEqual([1, 2, 3]);
+	});
+
+	it("should use the default value when a keyword argument is not provided and has a default value", () => {
+		const data = {
+			param1: 1,
+			param2: 2
+		};
+
+		const result = map_data_to_params(data, transformed_api_info);
+		expect(result).toEqual([1, 2, 3]);
+	});
+
+	it("should throw an error when an invalid keyword argument is provided", () => {
+		const data = {
+			param1: 1,
+			param2: 2,
+			param3: 3,
+			param4: 4
+		};
+
+		expect(() => map_data_to_params(data, transformed_api_info)).toThrowError(
+			"Parameter `param4` is not a valid keyword argument. Please refer to the API for usage."
+		);
+	});
+
+	it("should throw an error when no value is provided for a required parameter", () => {
+		const data = {};
+
+		expect(() => map_data_to_params(data, transformed_api_info)).toThrowError(
+			"No value provided for required parameter: param1"
+		);
+	});
+});
diff --git a/client/js/src/types.ts b/client/js/src/types.ts
index 18c175490c..838151b70a 100644
--- a/client/js/src/types.ts
+++ b/client/js/src/types.ts
@@ -4,6 +4,9 @@ import { hardware_types } from "./helpers/spaces";
 
 export interface ApiData {
 	label: string;
+	parameter_name: string;
+	parameter_default?: any;
+	parameter_has_default?: boolean;
 	type: {
 		type: any;
 		description: string;
@@ -16,6 +19,9 @@ export interface ApiData {
 
 export interface JsApiData {
 	label: string;
+	parameter_name: string;
+	parameter_default?: any;
+	parameter_has_default?: boolean;
 	type: string;
 	description: string;
 	component: string;
@@ -43,6 +49,21 @@ export interface BlobRef {
 
 export type DataType = string | Buffer | Record<string, any> | any[];
 
+// Function Signature Types
+
+export type SubmitFunction = (
+	endpoint: string | number,
+	data: unknown[] | Record<string, unknown>,
+	event_data?: unknown,
+	trigger_id?: number | null
+) => SubmitReturn;
+
+export type PredictFunction = (
+	endpoint: string | number,
+	data: unknown[] | Record<string, unknown>,
+	event_data?: unknown
+) => Promise<SubmitReturn>;
+
 // Event and Submission Types
 
 type event = <K extends EventType>(
@@ -50,21 +71,10 @@ type event = <K extends EventType>(
 	listener: EventListener<K>
 ) => SubmitReturn;
 
-type predict = (
-	endpoint: string | number,
-	data?: unknown[],
-	event_data?: unknown
-) => Promise<unknown>;
-
 export type client_return = {
 	config: Config | undefined;
-	predict: predict;
-	submit: (
-		endpoint: string | number,
-		data: unknown[],
-		event_data?: unknown,
-		trigger_id?: number | null
-	) => SubmitReturn;
+	predict: PredictFunction;
+	submit: SubmitFunction;
 	component_server: (
 		component_id: number,
 		fn_name: string,
diff --git a/client/js/src/utils/predict.ts b/client/js/src/utils/predict.ts
index 9fc1912241..9c90602372 100644
--- a/client/js/src/utils/predict.ts
+++ b/client/js/src/utils/predict.ts
@@ -4,7 +4,7 @@ import type { Dependency, SubmitReturn } from "../types";
 export async function predict(
 	this: Client,
 	endpoint: string | number,
-	data?: unknown[]
+	data: unknown[] | Record<string, unknown>
 ): Promise<SubmitReturn> {
 	let data_returned = false;
 	let status_complete = false;
@@ -28,7 +28,7 @@ export async function predict(
 	}
 
 	return new Promise(async (resolve, reject) => {
-		const app = this.submit(endpoint, data || []);
+		const app = this.submit(endpoint, data);
 		let result: unknown;
 
 		app
diff --git a/client/js/src/utils/submit.ts b/client/js/src/utils/submit.ts
index c93cc68e98..c1fe3dab69 100644
--- a/client/js/src/utils/submit.ts
+++ b/client/js/src/utils/submit.ts
@@ -16,7 +16,12 @@ import type {
 
 import { skip_queue, post_message } from "../helpers/data";
 import { resolve_root } from "../helpers/init_helpers";
-import { handle_message, process_endpoint } from "../helpers/api_info";
+import {
+	handle_message,
+	map_data_to_params,
+	process_endpoint
+} from "../helpers/api_info";
+import semiver from "semiver";
 import { BROKEN_CONNECTION_MSG, QUEUE_FULL_MSG } from "../constants";
 import { apply_diff_stream, close_stream } from "./stream";
 import { Client } from "../client";
@@ -24,7 +29,7 @@ import { Client } from "../client";
 export function submit(
 	this: Client,
 	endpoint: string | number,
-	data: unknown[],
+	data: unknown[] | Record<string, unknown>,
 	event_data?: unknown,
 	trigger_id?: number | null
 ): SubmitReturn {
@@ -55,6 +60,8 @@ export function submit(
 			config
 		);
 
+		let resolved_data = map_data_to_params(data, api_info);
+
 		let websocket: WebSocket;
 		let stream: EventSource | null;
 		let protocol = config.protocol ?? "ws";
@@ -155,7 +162,7 @@ export function submit(
 			}
 		}
 
-		this.handle_blob(config.root, data, endpoint_info).then(
+		this.handle_blob(config.root, resolved_data, endpoint_info).then(
 			async (_payload) => {
 				payload = {
 					data: _payload || [],
diff --git a/js/app/src/api_docs/CodeSnippet.svelte b/js/app/src/api_docs/CodeSnippet.svelte
index a8258a143f..74b2d648a7 100644
--- a/js/app/src/api_docs/CodeSnippet.svelte
+++ b/js/app/src/api_docs/CodeSnippet.svelte
@@ -88,12 +88,12 @@ const example{component} = await response_{i}.blob();
 const app = await Client.connect(<span class="token string">"{root}"</span>);
 const result = await app.predict({#if named}<span class="api-name"
 								>"/{dependency.api_name}"</span
-							>{:else}{dependency_index}{/if}, [<!--
+							>{:else}{dependency_index}{/if}, &lbrace; <!--
 -->{#each endpoint_parameters as { label, type, python_type, component, example_input, serializer }, i}<!--
 		-->{#if blob_components.includes(component)}<!--
 	-->
 				<span
-									class="example-inputs">example{component}</span
+									class="example-inputs">{label}: example{component}</span
 								>, <!--
 		--><span class="desc"
 									><!--
@@ -105,7 +105,7 @@ const result = await app.predict({#if named}<span class="api-name"
 		-->{:else}<!--
 	-->		
 				<span class="example-inputs"
-									>{represent_value(
+									>{label}: {represent_value(
 										example_input,
 										python_type.type,
 										"js"
@@ -122,7 +122,7 @@ const result = await app.predict({#if named}<span class="api-name"
 								><!--
 -->{/if}
 						{/each}
-	]);
+	&rbrace;);
 
 console.log(result.data);
 </pre>


Code review comments:
diff_hunk: @@ -298,3 +298,72 @@ export function handle_message(
 
 	return { type: "none", status: { stage: "error", queue } };
 }
+
+/**
+ * Maps the provided `data` to the parameters defined by the `/info` endpoint response.
+ * This allows us to support both positional and keyword arguments passed to the client
+ * and ensures that all parameters are either directly provided or have default values assigned.
+ *
+ * @param {unknown[] | Record<string, unknown>} data - The input data for the function,
+ *        which can be either an array of values for positional arguments or an object
+ *        with key-value pairs for keyword arguments.
+ * @param {JsApiData[]} parameters - Array of parameter descriptions retrieved from the
+ *        `/info` endpoint.
+ *
+ * @returns {unknown[]} - Returns an array of resolved data where each element corresponds
+ *         to the expected parameter from the API. The `parameter_default` value is used where
+ *         a value is not provided for a parameter, and optional parameters without defaults are
+ * 		   set to `undefined`.
+ *
+ * @throws {Error} - Throws an error:
+ *         - If more arguments are provided than are defined in the parameters.
+ *         - If an argument is provided that does not match any defined parameter.
+ *         - If a parameter value is specified both positionally and as a keyword.
+ */
+
+export const map_data_to_params = (
+	data: unknown[] | Record<string, unknown>,
+	api_info: ApiInfo<JsApiData | ApiData>
+): unknown[] => {
+	const parameters = Object.values(api_info.named_endpoints).flatMap(
+		(values) => values.parameters
+	);
+
+	if (Array.isArray(data)) {
+		if (data.length > parameters.length) {
+			console.warn("Too many arguments provided for the endpoint.");
+		}
+		return data;
+	}
+
+	const resolved_data: unknown[] = [];
+	const provided_keys = Object.keys(data);
+
+	parameters.forEach((param, index) => {
+		if (data.hasOwnProperty(param.parameter_name)) {
+			resolved_data[index] = data[param.parameter_name];
+		} else if (param.parameter_has_default) {
+			resolved_data[index] = param.parameter_default;
+		} else {
+			resolved_data[index] = undefined;
path: client/js/src/helpers/api_info.ts
text: @user3:
Should we throw an error in this case? 

@user1 @user2 what does the python client do if no param is given but there is no default?

@user1:
Yes it raises an error saying that a required param is missing