@huggingface/inference
Version:
Typescript client for the Hugging Face Inference Providers and Inference Endpoints
253 lines (219 loc) • 7.75 kB
text/typescript
/**
* See the registered mapping of HF model ID => ZAI model ID here:
*
* https://huggingface.co/api/partners/zai-org/models
*
* This is a publicly available mapping.
*
* If you want to try to run inference for a new model locally before it's registered on huggingface.co,
* you can add it to the dictionary "HARDCODED_MODEL_ID_MAPPING" in consts.ts, for dev purposes.
*
* - If you work at zai and want to update this mapping, please use the model mapping API we provide on huggingface.co
* - If you're a community member and want to add a new supported HF model to zai, please open an issue on the present repo
* and we will tag zai team members.
*
* Thanks!
*/
import type { ImageToTextOutput } from "@huggingface/tasks";
import {
InferenceClientInputError,
InferenceClientProviderApiError,
InferenceClientProviderOutputError,
} from "../errors.js";
import { isUrl } from "../lib/isUrl.js";
import type { BodyParams, HeaderParams, OutputType, RequestArgs } from "../types.js";
import { base64FromBytes } from "../utils/base64FromBytes.js";
import { dataUrlFromBlob } from "../utils/dataUrlFromBlob.js";
import { delay } from "../utils/delay.js";
import { omit } from "../utils/omit.js";
import type { ImageToTextArgs } from "../tasks/cv/imageToText.js";
import {
BaseConversationalTask,
TaskProviderHelper,
type ImageToTextTaskHelper,
type TextToImageTaskHelper,
} from "./providerHelper.js";
const ZAI_API_BASE_URL = "https://api.z.ai";
abstract class ZaiTask extends TaskProviderHelper {
constructor() {
super("zai-org", ZAI_API_BASE_URL);
}
override prepareHeaders(params: HeaderParams, binary: boolean): Record<string, string> {
const headers = super.prepareHeaders(params, binary);
headers["x-source-channel"] = "hugging_face";
headers["accept-language"] = "en-US,en";
return headers;
}
}
export class ZaiConversationalTask extends BaseConversationalTask {
constructor() {
super("zai-org", ZAI_API_BASE_URL);
}
override prepareHeaders(params: HeaderParams, binary: boolean): Record<string, string> {
const headers = super.prepareHeaders(params, binary);
headers["x-source-channel"] = "hugging_face";
headers["accept-language"] = "en-US,en";
return headers;
}
override makeRoute(): string {
return "/api/paas/v4/chat/completions";
}
}
interface ZaiTextToImageResponse {
model: string;
id: string;
request_id: string;
task_status: "PROCESSING" | "SUCCESS" | "FAIL";
}
interface ZaiAsyncResultResponse {
image_result?: Array<{ url: string }>;
model: string;
id: string;
request_id: string;
task_status: "PROCESSING" | "SUCCESS" | "FAIL";
}
const MAX_POLL_ATTEMPTS = 60;
const POLL_INTERVAL_MS = 5000;
export class ZaiTextToImageTask extends ZaiTask implements TextToImageTaskHelper {
override makeRoute(): string {
return "/api/paas/v4/async/images/generations";
}
override preparePayload(params: BodyParams): Record<string, unknown> {
return {
...omit(params.args, ["inputs", "parameters"]),
...(params.args.parameters as Record<string, unknown>),
model: params.model,
prompt: params.args.inputs,
};
}
override async getResponse(
response: ZaiTextToImageResponse,
url?: string,
headers?: Record<string, string>,
outputType?: OutputType,
signal?: AbortSignal,
): Promise<string | Blob | Record<string, unknown>> {
if (!url || !headers) {
throw new InferenceClientInputError(`URL and headers are required for 'text-to-image' task`);
}
if (
typeof response !== "object" ||
!response ||
!("task_status" in response) ||
!("id" in response) ||
typeof response.id !== "string"
) {
throw new InferenceClientProviderOutputError(
`Received malformed response from ZAI text-to-image API: expected { id: string, task_status: string }, got: ${JSON.stringify(
response,
)}`,
);
}
if (response.task_status === "FAIL") {
throw new InferenceClientProviderOutputError("ZAI API returned task status: FAIL");
}
const taskId = response.id;
const parsedUrl = new URL(url);
const baseUrl = `${parsedUrl.protocol}//${parsedUrl.host}${
parsedUrl.host === "router.huggingface.co" ? "/zai-org" : ""
}`;
const pollUrl = `${baseUrl}/api/paas/v4/async-result/${taskId}`;
const pollHeaders: Record<string, string> = {
...headers,
"x-source-channel": "hugging_face",
"accept-language": "en-US,en",
};
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
await delay(POLL_INTERVAL_MS, signal);
const resp = await fetch(pollUrl, {
method: "GET",
headers: pollHeaders,
signal,
});
if (!resp.ok) {
throw new InferenceClientProviderApiError(
`Failed to fetch result from ZAI text-to-image API: ${resp.status}`,
{ url: pollUrl, method: "GET" },
{ requestId: resp.headers.get("x-request-id") ?? "", status: resp.status, body: await resp.text() },
);
}
const result: ZaiAsyncResultResponse = await resp.json();
if (result.task_status === "FAIL") {
throw new InferenceClientProviderOutputError("ZAI text-to-image API task failed");
}
if (result.task_status === "SUCCESS") {
if (
!result.image_result ||
!Array.isArray(result.image_result) ||
result.image_result.length === 0 ||
typeof result.image_result[0]?.url !== "string" ||
!isUrl(result.image_result[0].url)
) {
throw new InferenceClientProviderOutputError(
`Received malformed response from ZAI text-to-image API: expected { image_result: Array<{ url: string }> }, got: ${JSON.stringify(
result,
)}`,
);
}
const imageUrl = result.image_result[0].url;
if (outputType === "json") {
return { ...result };
}
if (outputType === "url") {
return imageUrl;
}
const imageResponse = await fetch(imageUrl, { signal });
const blob = await imageResponse.blob();
return outputType === "dataUrl" ? dataUrlFromBlob(blob) : blob;
}
}
throw new InferenceClientProviderOutputError(
`Timed out while waiting for the result from ZAI API - aborting after ${MAX_POLL_ATTEMPTS} attempts`,
);
}
}
interface ZaiLayoutParsingResponse {
md_results?: string;
}
export class ZaiImageToTextTask extends ZaiTask implements ImageToTextTaskHelper {
override makeRoute(): string {
return "/api/paas/v4/layout_parsing";
}
async preparePayloadAsync(args: ImageToTextArgs, signal?: AbortSignal): Promise<RequestArgs> {
const blob =
"data" in args && args.data instanceof Blob
? args.data
: "inputs" in args
? typeof args.inputs === "string" && isUrl(args.inputs)
? await fetch(args.inputs, { signal }).then((r) => r.blob())
: args.inputs instanceof Blob
? args.inputs
: undefined
: undefined;
if (!blob || !(blob instanceof Blob)) {
throw new InferenceClientInputError("ZAI image-to-text requires a URL string or Blob as inputs");
}
const mimeType = blob.type || "image/png";
const b64 = base64FromBytes(new Uint8Array(await blob.arrayBuffer()));
const file = `data:${mimeType};base64,${b64}`;
return {
...("data" in args ? omit(args, "data") : omit(args, "inputs")),
inputs: file,
} as RequestArgs;
}
override preparePayload(params: BodyParams): Record<string, unknown> {
return {
model: params.model,
file: params.args.inputs,
};
}
override async getResponse(response: ZaiLayoutParsingResponse): Promise<ImageToTextOutput> {
const mdResults = response?.md_results;
if (typeof mdResults !== "string") {
throw new InferenceClientProviderOutputError(
`Received malformed response from ZAI layout_parsing API: expected { md_results: string }, got: ${JSON.stringify(response)}`,
);
}
return { generated_text: mdResults, generatedText: mdResults };
}
}