diff --git a/packages/inference/README.md b/packages/inference/README.md index 21e46625be..5425204d05 100644 --- a/packages/inference/README.md +++ b/packages/inference/README.md @@ -120,6 +120,108 @@ await textGeneration({ This will enable tree-shaking by your bundler. +### Error handling + +The inference package provides specific error types to help you handle different error scenarios effectively. + +#### Error Types + +The package defines several error types that extend the base `Error` class: + +- `InferenceClientError`: Base error class for all Hugging Face Inference errors +- `InferenceClientInputError`: Thrown when there are issues with input parameters +- `InferenceClientProviderApiError`: Thrown when there are API-level errors from providers +- `InferenceClientHubApiError`: Thrown when there are API-levels errors from the Hugging Face Hub +- `InferenceClientProviderOutputError`: Thrown when there are issues with providers' API responses format + +### Example Usage + +```typescript +import { InferenceClient } from "@huggingface/inference"; +import { + InferenceClientError, + InferenceClientProviderApiError, + InferenceClientProviderOutputError, + InferenceClientHubApiError, +} from "@huggingface/inference"; + +const client = new InferenceClient(); + +try { + const result = await client.textGeneration({ + model: "gpt2", + inputs: "Hello, I'm a language model", + }); +} catch (error) { + if (error instanceof InferenceClientProviderApiError) { + // Handle API errors (e.g., rate limits, authentication issues) + console.error("Provider API Error:", error.message); + console.error("HTTP Request details:", error.request); + console.error("HTTP Response details:", error.response); + if (error instanceof InferenceClientHubApiError) { + // Handle API errors (e.g., rate limits, authentication issues) + console.error("Hub API Error:", error.message); + console.error("HTTP Request details:", error.request); + console.error("HTTP Response details:", error.response); + } else if (error instanceof InferenceClientProviderOutputError) { + // Handle malformed responses from providers + console.error("Provider Output Error:", error.message); + } else if (error instanceof InferenceClientInputError) { + // Handle invalid input parameters + console.error("Input Error:", error.message); + } else { + // Handle unexpected errors + console.error("Unexpected error:", error); + } +} + +/// Catch all errors from @huggingface/inference +try { + const result = await client.textGeneration({ + model: "gpt2", + inputs: "Hello, I'm a language model", + }); +} catch (error) { + if (error instanceof InferenceClientError) { + // Handle errors from @huggingface/inference + console.error("Error from InferenceClient:", error); + } else { + // Handle unexpected errors + console.error("Unexpected error:", error); + } +} +``` + +### Error Details + +#### InferenceClientProviderApiError + +This error occurs when there are issues with the API request when performing inference at the selected provider. + +It has several properties: +- `message`: A descriptive error message +- `request`: Details about the failed request (URL, method, headers) +- `response`: Response details including status code and body + +#### InferenceClientHubApiError + +This error occurs when there are issues with the API request when requesting the Hugging Face Hub API. + +It has several properties: +- `message`: A descriptive error message +- `request`: Details about the failed request (URL, method, headers) +- `response`: Response details including status code and body + + +#### InferenceClientProviderOutputError + +This error occurs when a provider returns a response in an unexpected format. + +#### InferenceClientInputError + +This error occurs when input parameters are invalid or missing. The error message describes what's wrong with the input. + + ### Natural Language Processing #### Text Generation diff --git a/packages/inference/src/errors.ts b/packages/inference/src/errors.ts new file mode 100644 index 0000000000..01c23d976e --- /dev/null +++ b/packages/inference/src/errors.ts @@ -0,0 +1,82 @@ +import type { JsonObject } from "./vendor/type-fest/basic.js"; + +/** + * Base class for all inference-related errors. + */ +export abstract class InferenceClientError extends Error { + constructor(message: string) { + super(message); + this.name = "InferenceClientError"; + } +} + +export class InferenceClientInputError extends InferenceClientError { + constructor(message: string) { + super(message); + this.name = "InputError"; + } +} + +interface HttpRequest { + url: string; + method: string; + headers?: Record; + body?: JsonObject; +} + +interface HttpResponse { + requestId: string; + status: number; + body: JsonObject | string; +} + +abstract class InferenceClientHttpRequestError extends InferenceClientError { + httpRequest: HttpRequest; + httpResponse: HttpResponse; + constructor(message: string, httpRequest: HttpRequest, httpResponse: HttpResponse) { + super(message); + this.httpRequest = { + ...httpRequest, + ...(httpRequest.headers + ? { + headers: { + ...httpRequest.headers, + ...("Authorization" in httpRequest.headers ? { Authorization: `Bearer [redacted]` } : undefined), + /// redact authentication in the request headers + }, + } + : undefined), + }; + this.httpResponse = httpResponse; + } +} + +/** + * Thrown when the HTTP request to the provider fails, e.g. due to API issues or server errors. + */ +export class InferenceClientProviderApiError extends InferenceClientHttpRequestError { + constructor(message: string, httpRequest: HttpRequest, httpResponse: HttpResponse) { + super(message, httpRequest, httpResponse); + this.name = "ProviderApiError"; + } +} + +/** + * Thrown when the HTTP request to the hub fails, e.g. due to API issues or server errors. + */ +export class InferenceClientHubApiError extends InferenceClientHttpRequestError { + constructor(message: string, httpRequest: HttpRequest, httpResponse: HttpResponse) { + super(message, httpRequest, httpResponse); + this.name = "HubApiError"; + } +} + +/** + * Thrown when the inference output returned by the provider is invalid / does not match the expectations + */ +export class InferenceClientProviderOutputError extends InferenceClientError { + constructor(message: string) { + super(message); + this.name = "ProviderOutputError"; + } +} diff --git a/packages/inference/src/index.ts b/packages/inference/src/index.ts index 93ed1cd54e..de23f5b6d2 100644 --- a/packages/inference/src/index.ts +++ b/packages/inference/src/index.ts @@ -1,5 +1,5 @@ export { InferenceClient, InferenceClientEndpoint, HfInference } from "./InferenceClient.js"; -export { InferenceOutputError } from "./lib/InferenceOutputError.js"; +export * from "./errors.js"; export * from "./types.js"; export * from "./tasks/index.js"; import * as snippets from "./snippets/index.js"; diff --git a/packages/inference/src/lib/InferenceOutputError.ts b/packages/inference/src/lib/InferenceOutputError.ts deleted file mode 100644 index 0765b99944..0000000000 --- a/packages/inference/src/lib/InferenceOutputError.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class InferenceOutputError extends TypeError { - constructor(message: string) { - super( - `Invalid inference output: ${message}. Use the 'request' method with the same parameters to do a custom call with no type checking.` - ); - this.name = "InferenceOutputError"; - } -} diff --git a/packages/inference/src/lib/getInferenceProviderMapping.ts b/packages/inference/src/lib/getInferenceProviderMapping.ts index 8d2777efb7..29f779742d 100644 --- a/packages/inference/src/lib/getInferenceProviderMapping.ts +++ b/packages/inference/src/lib/getInferenceProviderMapping.ts @@ -4,6 +4,7 @@ import { HARDCODED_MODEL_INFERENCE_MAPPING } from "../providers/consts.js"; import { EQUIVALENT_SENTENCE_TRANSFORMERS_TASKS } from "../providers/hf-inference.js"; import type { InferenceProvider, InferenceProviderOrPolicy, ModelId } from "../types.js"; import { typedInclude } from "../utils/typedInclude.js"; +import { InferenceClientHubApiError, InferenceClientInputError } from "../errors.js"; export const inferenceProviderMappingCache = new Map(); @@ -32,27 +33,46 @@ export async function fetchInferenceProviderMappingForModel( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion inferenceProviderMapping = inferenceProviderMappingCache.get(modelId)!; } else { - const resp = await (options?.fetch ?? fetch)( - `${HF_HUB_URL}/api/models/${modelId}?expand[]=inferenceProviderMapping`, - { - headers: accessToken?.startsWith("hf_") ? { Authorization: `Bearer ${accessToken}` } : {}, + const url = `${HF_HUB_URL}/api/models/${modelId}?expand[]=inferenceProviderMapping`; + const resp = await (options?.fetch ?? fetch)(url, { + headers: accessToken?.startsWith("hf_") ? { Authorization: `Bearer ${accessToken}` } : {}, + }); + if (!resp.ok) { + if (resp.headers.get("Content-Type")?.startsWith("application/json")) { + const error = await resp.json(); + if ("error" in error && typeof error.error === "string") { + throw new InferenceClientHubApiError( + `Failed to fetch inference provider mapping for model ${modelId}: ${error.error}`, + { url, method: "GET" }, + { requestId: resp.headers.get("x-request-id") ?? "", status: resp.status, body: error } + ); + } + } else { + throw new InferenceClientHubApiError( + `Failed to fetch inference provider mapping for model ${modelId}`, + { url, method: "GET" }, + { requestId: resp.headers.get("x-request-id") ?? "", status: resp.status, body: await resp.text() } + ); } - ); - if (resp.status === 404) { - throw new Error(`Model ${modelId} does not exist`); } - inferenceProviderMapping = await resp - .json() - .then((json) => json.inferenceProviderMapping) - .catch(() => null); - - if (inferenceProviderMapping) { - inferenceProviderMappingCache.set(modelId, inferenceProviderMapping); + let payload: { inferenceProviderMapping?: InferenceProviderMapping } | null = null; + try { + payload = await resp.json(); + } catch { + throw new InferenceClientHubApiError( + `Failed to fetch inference provider mapping for model ${modelId}: malformed API response, invalid JSON`, + { url, method: "GET" }, + { requestId: resp.headers.get("x-request-id") ?? "", status: resp.status, body: await resp.text() } + ); } - } - - if (!inferenceProviderMapping) { - throw new Error(`We have not been able to find inference provider information for model ${modelId}.`); + if (!payload?.inferenceProviderMapping) { + throw new InferenceClientHubApiError( + `We have not been able to find inference provider information for model ${modelId}.`, + { url, method: "GET" }, + { requestId: resp.headers.get("x-request-id") ?? "", status: resp.status, body: await resp.text() } + ); + } + inferenceProviderMapping = payload.inferenceProviderMapping; } return inferenceProviderMapping; } @@ -83,7 +103,7 @@ export async function getInferenceProviderMapping( ? EQUIVALENT_SENTENCE_TRANSFORMERS_TASKS : [params.task]; if (!typedInclude(equivalentTasks, providerMapping.task)) { - throw new Error( + throw new InferenceClientInputError( `Model ${params.modelId} is not supported for task ${params.task} and provider ${params.provider}. Supported task: ${providerMapping.task}.` ); } @@ -104,7 +124,7 @@ export async function resolveProvider( ): Promise { if (endpointUrl) { if (provider) { - throw new Error("Specifying both endpointUrl and provider is not supported."); + throw new InferenceClientInputError("Specifying both endpointUrl and provider is not supported."); } /// Defaulting to hf-inference helpers / API return "hf-inference"; @@ -117,13 +137,13 @@ export async function resolveProvider( } if (provider === "auto") { if (!modelId) { - throw new Error("Specifying a model is required when provider is 'auto'"); + throw new InferenceClientInputError("Specifying a model is required when provider is 'auto'"); } const inferenceProviderMapping = await fetchInferenceProviderMappingForModel(modelId); provider = Object.keys(inferenceProviderMapping)[0] as InferenceProvider | undefined; } if (!provider) { - throw new Error(`No Inference Provider available for model ${modelId}.`); + throw new InferenceClientInputError(`No Inference Provider available for model ${modelId}.`); } return provider; } diff --git a/packages/inference/src/lib/getProviderHelper.ts b/packages/inference/src/lib/getProviderHelper.ts index 9afcb8980f..ad0eb89a6d 100644 --- a/packages/inference/src/lib/getProviderHelper.ts +++ b/packages/inference/src/lib/getProviderHelper.ts @@ -48,6 +48,7 @@ import * as Replicate from "../providers/replicate.js"; import * as Sambanova from "../providers/sambanova.js"; import * as Together from "../providers/together.js"; import type { InferenceProvider, InferenceProviderOrPolicy, InferenceTask } from "../types.js"; +import { InferenceClientInputError } from "../errors.js"; export const PROVIDERS: Record>> = { "black-forest-labs": { @@ -281,14 +282,18 @@ export function getProviderHelper( return new HFInference.HFInferenceTask(); } if (!task) { - throw new Error("you need to provide a task name when using an external provider, e.g. 'text-to-image'"); + throw new InferenceClientInputError( + "you need to provide a task name when using an external provider, e.g. 'text-to-image'" + ); } if (!(provider in PROVIDERS)) { - throw new Error(`Provider '${provider}' not supported. Available providers: ${Object.keys(PROVIDERS)}`); + throw new InferenceClientInputError( + `Provider '${provider}' not supported. Available providers: ${Object.keys(PROVIDERS)}` + ); } const providerTasks = PROVIDERS[provider]; if (!providerTasks || !(task in providerTasks)) { - throw new Error( + throw new InferenceClientInputError( `Task '${task}' not supported for provider '${provider}'. Available tasks: ${Object.keys(providerTasks ?? {})}` ); } diff --git a/packages/inference/src/lib/makeRequestOptions.ts b/packages/inference/src/lib/makeRequestOptions.ts index 7973d944ba..8ce29d285f 100644 --- a/packages/inference/src/lib/makeRequestOptions.ts +++ b/packages/inference/src/lib/makeRequestOptions.ts @@ -5,6 +5,7 @@ import type { InferenceProviderModelMapping } from "./getInferenceProviderMappin import { getInferenceProviderMapping } from "./getInferenceProviderMapping.js"; import type { getProviderHelper } from "./getProviderHelper.js"; import { isUrl } from "./isUrl.js"; +import { InferenceClientHubApiError, InferenceClientInputError } from "../errors.js"; /** * Lazy-loaded from huggingface.co/api/tasks when needed @@ -33,10 +34,10 @@ export async function makeRequestOptions( // Validate inputs if (args.endpointUrl && provider !== "hf-inference") { - throw new Error(`Cannot use endpointUrl with a third-party provider.`); + throw new InferenceClientInputError(`Cannot use endpointUrl with a third-party provider.`); } if (maybeModel && isUrl(maybeModel)) { - throw new Error(`Model URLs are no longer supported. Use endpointUrl instead.`); + throw new InferenceClientInputError(`Model URLs are no longer supported. Use endpointUrl instead.`); } if (args.endpointUrl) { @@ -51,14 +52,14 @@ export async function makeRequestOptions( } if (!maybeModel && !task) { - throw new Error("No model provided, and no task has been specified."); + throw new InferenceClientInputError("No model provided, and no task has been specified."); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const hfModel = maybeModel ?? (await loadDefaultModel(task!)); if (providerHelper.clientSideRoutingOnly && !maybeModel) { - throw new Error(`Provider ${provider} requires a model ID to be passed directly.`); + throw new InferenceClientInputError(`Provider ${provider} requires a model ID to be passed directly.`); } const inferenceProviderMapping = providerHelper.clientSideRoutingOnly @@ -82,7 +83,9 @@ export async function makeRequestOptions( { fetch: options?.fetch } ); if (!inferenceProviderMapping) { - throw new Error(`We have not been able to find inference provider information for model ${hfModel}.`); + throw new InferenceClientInputError( + `We have not been able to find inference provider information for model ${hfModel}.` + ); } // Use the sync version with the resolved model @@ -122,9 +125,8 @@ export function makeRequestOptionsFromResolvedModel( if (providerHelper.clientSideRoutingOnly) { // Closed-source providers require an accessToken (cannot be routed). if (accessToken && accessToken.startsWith("hf_")) { - throw new Error(`Provider ${provider} is closed-source and does not support HF tokens.`); + throw new InferenceClientInputError(`Provider ${provider} is closed-source and does not support HF tokens.`); } - return "provider-key"; } if (accessToken) { return accessToken.startsWith("hf_") ? "hf-token" : "provider-key"; @@ -197,23 +199,30 @@ async function loadDefaultModel(task: InferenceTask): Promise { } const taskInfo = tasks[task]; if ((taskInfo?.models.length ?? 0) <= 0) { - throw new Error(`No default model defined for task ${task}, please define the model explicitly.`); + throw new InferenceClientInputError( + `No default model defined for task ${task}, please define the model explicitly.` + ); } return taskInfo.models[0].id; } async function loadTaskInfo(): Promise> { - const res = await fetch(`${HF_HUB_URL}/api/tasks`); + const url = `${HF_HUB_URL}/api/tasks`; + const res = await fetch(url); if (!res.ok) { - throw new Error("Failed to load tasks definitions from Hugging Face Hub."); + throw new InferenceClientHubApiError( + "Failed to load tasks definitions from Hugging Face Hub.", + { url, method: "GET" }, + { requestId: res.headers.get("x-request-id") ?? "", status: res.status, body: await res.text() } + ); } return await res.json(); } function removeProviderPrefix(model: string, provider: string): string { if (!model.startsWith(`${provider}/`)) { - throw new Error(`Models from ${provider} must be prefixed by "${provider}/". Got "${model}".`); + throw new InferenceClientInputError(`Models from ${provider} must be prefixed by "${provider}/". Got "${model}".`); } return model.slice(provider.length + 1); } diff --git a/packages/inference/src/providers/black-forest-labs.ts b/packages/inference/src/providers/black-forest-labs.ts index 8399ccb562..82f275bea9 100644 --- a/packages/inference/src/providers/black-forest-labs.ts +++ b/packages/inference/src/providers/black-forest-labs.ts @@ -14,7 +14,11 @@ * * Thanks! */ -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; +import { + InferenceClientInputError, + InferenceClientProviderApiError, + InferenceClientProviderOutputError, +} from "../errors.js"; import type { BodyParams, HeaderParams, UrlParams } from "../types.js"; import { delay } from "../utils/delay.js"; import { omit } from "../utils/omit.js"; @@ -52,7 +56,7 @@ export class BlackForestLabsTextToImageTask extends TaskProviderHelper implement makeRoute(params: UrlParams): string { if (!params) { - throw new Error("Params are required"); + throw new InferenceClientInputError("Params are required"); } return `/v1/${params.model}`; } @@ -70,7 +74,11 @@ export class BlackForestLabsTextToImageTask extends TaskProviderHelper implement urlObj.searchParams.set("attempt", step.toString(10)); const resp = await fetch(urlObj, { headers: { "Content-Type": "application/json" } }); if (!resp.ok) { - throw new InferenceOutputError("Failed to fetch result from black forest labs API"); + throw new InferenceClientProviderApiError( + "Failed to fetch result from black forest labs API", + { url: urlObj.toString(), method: "GET", headers: { "Content-Type": "application/json" } }, + { requestId: resp.headers.get("x-request-id") ?? "", status: resp.status, body: await resp.text() } + ); } const payload = await resp.json(); if ( @@ -92,6 +100,8 @@ export class BlackForestLabsTextToImageTask extends TaskProviderHelper implement return await image.blob(); } } - throw new InferenceOutputError("Failed to fetch result from black forest labs API"); + throw new InferenceClientProviderOutputError( + `Timed out while waiting for the result from black forest labs API - aborting after 5 attempts` + ); } } diff --git a/packages/inference/src/providers/fal-ai.ts b/packages/inference/src/providers/fal-ai.ts index 975b9cda5c..3420a922ec 100644 --- a/packages/inference/src/providers/fal-ai.ts +++ b/packages/inference/src/providers/fal-ai.ts @@ -17,7 +17,6 @@ import { base64FromBytes } from "../utils/base64FromBytes.js"; import type { AutomaticSpeechRecognitionOutput } from "@huggingface/tasks"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; import { isUrl } from "../lib/isUrl.js"; import type { BodyParams, HeaderParams, ModelId, RequestArgs, UrlParams } from "../types.js"; import { delay } from "../utils/delay.js"; @@ -30,6 +29,11 @@ import { } from "./providerHelper.js"; import { HF_HUB_URL } from "../config.js"; import type { AutomaticSpeechRecognitionArgs } from "../tasks/audio/automaticSpeechRecognition.js"; +import { + InferenceClientInputError, + InferenceClientProviderApiError, + InferenceClientProviderOutputError, +} from "../errors.js"; export interface FalAiQueueOutput { request_id: string; @@ -122,7 +126,7 @@ export class FalAITextToImageTask extends FalAITask implements TextToImageTaskHe return await urlResponse.blob(); } - throw new InferenceOutputError("Expected Fal.ai text-to-image response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Fal.ai text-to-image API"); } } @@ -150,11 +154,13 @@ export class FalAITextToVideoTask extends FalAITask implements TextToVideoTaskHe headers?: Record ): Promise { if (!url || !headers) { - throw new InferenceOutputError("URL and headers are required for text-to-video task"); + throw new InferenceClientInputError("URL and headers are required for text-to-video task"); } const requestId = response.request_id; if (!requestId) { - throw new InferenceOutputError("No request ID found in the response"); + throw new InferenceClientProviderOutputError( + "Received malformed response from Fal.ai text-to-video API: no request ID found in the response" + ); } let status = response.status; @@ -176,12 +182,22 @@ export class FalAITextToVideoTask extends FalAITask implements TextToVideoTaskHe const statusResponse = await fetch(statusUrl, { headers }); if (!statusResponse.ok) { - throw new InferenceOutputError("Failed to fetch response status from fal-ai API"); + throw new InferenceClientProviderApiError( + "Failed to fetch response status from fal-ai API", + { url: statusUrl, method: "GET" }, + { + requestId: statusResponse.headers.get("x-request-id") ?? "", + status: statusResponse.status, + body: await statusResponse.text(), + } + ); } try { status = (await statusResponse.json()).status; } catch (error) { - throw new InferenceOutputError("Failed to parse status response from fal-ai API"); + throw new InferenceClientProviderOutputError( + "Failed to parse status response from fal-ai API: received malformed response" + ); } } @@ -190,7 +206,9 @@ export class FalAITextToVideoTask extends FalAITask implements TextToVideoTaskHe try { result = await resultResponse.json(); } catch (error) { - throw new InferenceOutputError("Failed to parse result response from fal-ai API"); + throw new InferenceClientProviderOutputError( + "Failed to parse result response from fal-ai API: received malformed response" + ); } if ( typeof result === "object" && @@ -205,8 +223,10 @@ export class FalAITextToVideoTask extends FalAITask implements TextToVideoTaskHe const urlResponse = await fetch(result.video.url); return await urlResponse.blob(); } else { - throw new InferenceOutputError( - "Expected { video: { url: string } } result format, got instead: " + JSON.stringify(result) + throw new InferenceClientProviderOutputError( + `Received malformed response from Fal.ai text-to-video API: expected { video: { url: string } } result format, got instead: ${JSON.stringify( + result + )}` ); } } @@ -221,8 +241,10 @@ export class FalAIAutomaticSpeechRecognitionTask extends FalAITask implements Au override async getResponse(response: unknown): Promise { const res = response as FalAIAutomaticSpeechRecognitionOutput; if (typeof res?.text !== "string") { - throw new InferenceOutputError( - `Expected { text: string } format from Fal.ai Automatic Speech Recognition, got: ${JSON.stringify(response)}` + throw new InferenceClientProviderOutputError( + `Received malformed response from Fal.ai Automatic Speech Recognition API: expected { text: string } format, got instead: ${JSON.stringify( + response + )}` ); } return { text: res.text }; @@ -232,12 +254,12 @@ export class FalAIAutomaticSpeechRecognitionTask extends FalAITask implements Au const blob = "data" in args && args.data instanceof Blob ? args.data : "inputs" in args ? args.inputs : undefined; const contentType = blob?.type; if (!contentType) { - throw new Error( + throw new InferenceClientInputError( `Unable to determine the input's content-type. Make sure your are passing a Blob when using provider fal-ai.` ); } if (!FAL_AI_SUPPORTED_BLOB_TYPES.includes(contentType)) { - throw new Error( + throw new InferenceClientInputError( `Provider fal-ai does not support blob type ${contentType} - supported content types are: ${FAL_AI_SUPPORTED_BLOB_TYPES.join( ", " )}` @@ -263,21 +285,35 @@ export class FalAITextToSpeechTask extends FalAITask { override async getResponse(response: unknown): Promise { const res = response as FalAITextToSpeechOutput; if (typeof res?.audio?.url !== "string") { - throw new InferenceOutputError( - `Expected { audio: { url: string } } format from Fal.ai Text-to-Speech, got: ${JSON.stringify(response)}` + throw new InferenceClientProviderOutputError( + `Received malformed response from Fal.ai Text-to-Speech API: expected { audio: { url: string } } format, got instead: ${JSON.stringify( + response + )}` + ); + } + const urlResponse = await fetch(res.audio.url); + if (!urlResponse.ok) { + throw new InferenceClientProviderApiError( + `Failed to fetch audio from ${res.audio.url}: ${urlResponse.statusText}`, + { url: res.audio.url, method: "GET", headers: { "Content-Type": "application/json" } }, + { + requestId: urlResponse.headers.get("x-request-id") ?? "", + status: urlResponse.status, + body: await urlResponse.text(), + } ); } try { - const urlResponse = await fetch(res.audio.url); - if (!urlResponse.ok) { - throw new Error(`Failed to fetch audio from ${res.audio.url}: ${urlResponse.statusText}`); - } return await urlResponse.blob(); } catch (error) { - throw new InferenceOutputError( - `Error fetching or processing audio from Fal.ai Text-to-Speech URL: ${res.audio.url}. ${ - error instanceof Error ? error.message : String(error) - }` + throw new InferenceClientProviderApiError( + `Failed to fetch audio from ${res.audio.url}: ${error instanceof Error ? error.message : String(error)}`, + { url: res.audio.url, method: "GET", headers: { "Content-Type": "application/json" } }, + { + requestId: urlResponse.headers.get("x-request-id") ?? "", + status: urlResponse.status, + body: await urlResponse.text(), + } ); } } diff --git a/packages/inference/src/providers/featherless-ai.ts b/packages/inference/src/providers/featherless-ai.ts index 2c838ce12e..249ee28aa9 100644 --- a/packages/inference/src/providers/featherless-ai.ts +++ b/packages/inference/src/providers/featherless-ai.ts @@ -4,10 +4,10 @@ import type { TextGenerationOutput, TextGenerationOutputFinishReason, } from "@huggingface/tasks"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; import type { BodyParams } from "../types.js"; import { BaseConversationalTask, BaseTextGenerationTask } from "./providerHelper.js"; import { omit } from "../utils/omit.js"; +import { InferenceClientProviderOutputError } from "../errors.js"; interface FeatherlessAITextCompletionOutput extends Omit { choices: Array<{ @@ -58,6 +58,6 @@ export class FeatherlessAITextGenerationTask extends BaseTextGenerationTask { generated_text: completion.text, }; } - throw new InferenceOutputError("Expected Featherless AI text generation response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Featherless AI text generation API"); } } diff --git a/packages/inference/src/providers/hf-inference.ts b/packages/inference/src/providers/hf-inference.ts index 099d50e6da..406592bb1a 100644 --- a/packages/inference/src/providers/hf-inference.ts +++ b/packages/inference/src/providers/hf-inference.ts @@ -34,7 +34,7 @@ import type { ZeroShotImageClassificationOutput, } from "@huggingface/tasks"; import { HF_ROUTER_URL } from "../config.js"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; +import { InferenceClientProviderOutputError } from "../errors.js"; import type { TabularClassificationOutput } from "../tasks/tabular/tabularClassification.js"; import type { BodyParams, RequestArgs, UrlParams } from "../types.js"; import { toArray } from "../utils/toArray.js"; @@ -127,7 +127,9 @@ export class HFInferenceTextToImageTask extends HFInferenceTask implements TextT outputType?: "url" | "blob" ): Promise { if (!response) { - throw new InferenceOutputError("response is undefined"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference text-to-image API: response is undefined" + ); } if (typeof response == "object") { if ("data" in response && Array.isArray(response.data) && response.data[0].b64_json) { @@ -154,7 +156,9 @@ export class HFInferenceTextToImageTask extends HFInferenceTask implements TextT } return response; } - throw new InferenceOutputError("Expected a Blob "); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference text-to-image API: expected a Blob" + ); } } @@ -195,13 +199,14 @@ export class HFInferenceTextGenerationTask extends HFInferenceTask implements Te if (Array.isArray(res) && res.every((x) => "generated_text" in x && typeof x?.generated_text === "string")) { return (res as TextGenerationOutput[])?.[0]; } - throw new InferenceOutputError("Expected Array<{generated_text: string}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference text generation API: expected Array<{generated_text: string}>" + ); } } export class HFInferenceAudioClassificationTask extends HFInferenceTask implements AudioClassificationTaskHelper { override async getResponse(response: unknown): Promise { - // Add type checking/validation for the 'unknown' input if ( Array.isArray(response) && response.every( @@ -209,11 +214,11 @@ export class HFInferenceAudioClassificationTask extends HFInferenceTask implemen typeof x === "object" && x !== null && typeof x.label === "string" && typeof x.score === "number" ) ) { - // If validation passes, it's safe to return as AudioClassificationOutput return response; } - // If validation fails, throw an error - throw new InferenceOutputError("Expected Array<{label: string, score: number}> but received different format"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference audio-classification API: expected Array<{label: string, score: number}> but received different format" + ); } } @@ -238,7 +243,9 @@ export class HFInferenceAutomaticSpeechRecognitionTask export class HFInferenceAudioToAudioTask extends HFInferenceTask implements AudioToAudioTaskHelper { override async getResponse(response: AudioToAudioOutput[]): Promise { if (!Array.isArray(response)) { - throw new InferenceOutputError("Expected Array"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference audio-to-audio API: expected Array" + ); } if ( !response.every((elem): elem is AudioToAudioOutput => { @@ -254,7 +261,9 @@ export class HFInferenceAudioToAudioTask extends HFInferenceTask implements Audi ); }) ) { - throw new InferenceOutputError("Expected Array<{label: string, audio: Blob}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference audio-to-audio API: expected Array<{label: string, audio: Blob}>" + ); } return response; } @@ -281,7 +290,9 @@ export class HFInferenceDocumentQuestionAnsweringTask ) { return response[0]; } - throw new InferenceOutputError("Expected Array<{answer: string, end: number, score: number, start: number}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference document-question-answering API: expected Array<{answer: string, end: number, score: number, start: number}>" + ); } } @@ -298,7 +309,9 @@ export class HFInferenceFeatureExtractionTask extends HFInferenceTask implements if (Array.isArray(response) && isNumArrayRec(response, 3, 0)) { return response; } - throw new InferenceOutputError("Expected Array"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference feature-extraction API: expected Array" + ); } } @@ -307,7 +320,9 @@ export class HFInferenceImageClassificationTask extends HFInferenceTask implemen if (Array.isArray(response) && response.every((x) => typeof x.label === "string" && typeof x.score === "number")) { return response; } - throw new InferenceOutputError("Expected Array<{label: string, score: number}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference image-classification API: expected Array<{label: string, score: number}>" + ); } } @@ -324,14 +339,18 @@ export class HFInferenceImageSegmentationTask extends HFInferenceTask implements ) { return response; } - throw new InferenceOutputError("Expected Array<{label: string, mask: string, score: number}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference image-segmentation API: expected Array<{label: string, mask: string, score: number}>" + ); } } export class HFInferenceImageToTextTask extends HFInferenceTask implements ImageToTextTaskHelper { override async getResponse(response: ImageToTextOutput): Promise { if (typeof response?.generated_text !== "string") { - throw new InferenceOutputError("Expected {generated_text: string}"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference image-to-text API: expected {generated_text: string}" + ); } return response; } @@ -359,7 +378,9 @@ export class HFInferenceImageToImageTask extends HFInferenceTask implements Imag if (response instanceof Blob) { return response; } - throw new InferenceOutputError("Expected Blob"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference image-to-image API: expected Blob" + ); } } @@ -379,8 +400,8 @@ export class HFInferenceObjectDetectionTask extends HFInferenceTask implements O ) { return response; } - throw new InferenceOutputError( - "Expected Array<{label: string, score: number, box: {xmin: number, ymin: number, xmax: number, ymax: number}}>" + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference object-detection API: expected Array<{label: string, score: number, box: {xmin: number, ymin: number, xmax: number, ymax: number}}>" ); } } @@ -393,7 +414,9 @@ export class HFInferenceZeroShotImageClassificationTask if (Array.isArray(response) && response.every((x) => typeof x.label === "string" && typeof x.score === "number")) { return response; } - throw new InferenceOutputError("Expected Array<{label: string, score: number}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference zero-shot-image-classification API: expected Array<{label: string, score: number}>" + ); } } @@ -403,7 +426,9 @@ export class HFInferenceTextClassificationTask extends HFInferenceTask implement if (Array.isArray(output) && output.every((x) => typeof x?.label === "string" && typeof x.score === "number")) { return output; } - throw new InferenceOutputError("Expected Array<{label: string, score: number}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference text-classification API: expected Array<{label: string, score: number}>" + ); } } @@ -431,7 +456,9 @@ export class HFInferenceQuestionAnsweringTask extends HFInferenceTask implements ) { return Array.isArray(response) ? response[0] : response; } - throw new InferenceOutputError("Expected Array<{answer: string, end: number, score: number, start: number}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference question-answering API: expected Array<{answer: string, end: number, score: number, start: number}>" + ); } } @@ -449,8 +476,8 @@ export class HFInferenceFillMaskTask extends HFInferenceTask implements FillMask ) { return response; } - throw new InferenceOutputError( - "Expected Array<{score: number, sequence: string, token: number, token_str: string}>" + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference fill-mask API: expected Array<{score: number, sequence: string, token: number, token_str: string}>" ); } } @@ -470,7 +497,9 @@ export class HFInferenceZeroShotClassificationTask extends HFInferenceTask imple ) { return response; } - throw new InferenceOutputError("Expected Array<{labels: string[], scores: number[], sequence: string}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference zero-shot-classification API: expected Array<{labels: string[], scores: number[], sequence: string}>" + ); } } @@ -479,7 +508,9 @@ export class HFInferenceSentenceSimilarityTask extends HFInferenceTask implement if (Array.isArray(response) && response.every((x) => typeof x === "number")) { return response; } - throw new InferenceOutputError("Expected Array"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference sentence-similarity API: expected Array" + ); } } @@ -510,8 +541,8 @@ export class HFInferenceTableQuestionAnsweringTask extends HFInferenceTask imple ) { return Array.isArray(response) ? response[0] : response; } - throw new InferenceOutputError( - "Expected {aggregator: string, answer: string, cells: string[], coordinates: number[][]}" + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference table-question-answering API: expected {aggregator: string, answer: string, cells: string[], coordinates: number[][]}" ); } } @@ -531,8 +562,8 @@ export class HFInferenceTokenClassificationTask extends HFInferenceTask implemen ) { return response; } - throw new InferenceOutputError( - "Expected Array<{end: number, entity_group: string, score: number, start: number, word: string}>" + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference token-classification API: expected Array<{end: number, entity_group: string, score: number, start: number, word: string}>" ); } } @@ -542,7 +573,9 @@ export class HFInferenceTranslationTask extends HFInferenceTask implements Trans if (Array.isArray(response) && response.every((x) => typeof x?.translation_text === "string")) { return response?.length === 1 ? response?.[0] : response; } - throw new InferenceOutputError("Expected Array<{translation_text: string}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference translation API: expected Array<{translation_text: string}>" + ); } } @@ -551,7 +584,9 @@ export class HFInferenceSummarizationTask extends HFInferenceTask implements Sum if (Array.isArray(response) && response.every((x) => typeof x?.summary_text === "string")) { return response?.[0]; } - throw new InferenceOutputError("Expected Array<{summary_text: string}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference summarization API: expected Array<{summary_text: string}>" + ); } } @@ -566,7 +601,9 @@ export class HFInferenceTabularClassificationTask extends HFInferenceTask implem if (Array.isArray(response) && response.every((x) => typeof x === "number")) { return response; } - throw new InferenceOutputError("Expected Array"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference tabular-classification API: expected Array" + ); } } @@ -584,7 +621,9 @@ export class HFInferenceVisualQuestionAnsweringTask ) { return response[0]; } - throw new InferenceOutputError("Expected Array<{answer: string, score: number}>"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference visual-question-answering API: expected Array<{answer: string, score: number}>" + ); } } @@ -593,7 +632,9 @@ export class HFInferenceTabularRegressionTask extends HFInferenceTask implements if (Array.isArray(response) && response.every((x) => typeof x === "number")) { return response; } - throw new InferenceOutputError("Expected Array"); + throw new InferenceClientProviderOutputError( + "Received malformed response from HF-Inference tabular-regression API: expected Array" + ); } } diff --git a/packages/inference/src/providers/hyperbolic.ts b/packages/inference/src/providers/hyperbolic.ts index 7dffbbe157..cfddb59a58 100644 --- a/packages/inference/src/providers/hyperbolic.ts +++ b/packages/inference/src/providers/hyperbolic.ts @@ -15,7 +15,6 @@ * Thanks! */ import type { ChatCompletionOutput, TextGenerationOutput } from "@huggingface/tasks"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; import type { BodyParams, UrlParams } from "../types.js"; import { omit } from "../utils/omit.js"; import { @@ -24,7 +23,7 @@ import { TaskProviderHelper, type TextToImageTaskHelper, } from "./providerHelper.js"; - +import { InferenceClientProviderOutputError } from "../errors.js"; const HYPERBOLIC_API_BASE_URL = "https://api.hyperbolic.xyz"; export interface HyperbolicTextCompletionOutput extends Omit { @@ -79,7 +78,7 @@ export class HyperbolicTextGenerationTask extends BaseTextGenerationTask { }; } - throw new InferenceOutputError("Expected Hyperbolic text generation response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Hyperbolic text generation API"); } } @@ -121,6 +120,6 @@ export class HyperbolicTextToImageTask extends TaskProviderHelper implements Tex return fetch(`data:image/jpeg;base64,${response.images[0].image}`).then((res) => res.blob()); } - throw new InferenceOutputError("Expected Hyperbolic text-to-image response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Hyperbolic text-to-image API"); } } diff --git a/packages/inference/src/providers/nebius.ts b/packages/inference/src/providers/nebius.ts index a301c15195..a5b31c6cf6 100644 --- a/packages/inference/src/providers/nebius.ts +++ b/packages/inference/src/providers/nebius.ts @@ -15,7 +15,6 @@ * Thanks! */ import type { FeatureExtractionOutput } from "@huggingface/tasks"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; import type { BodyParams } from "../types.js"; import { omit } from "../utils/omit.js"; import { @@ -25,6 +24,7 @@ import { type FeatureExtractionTaskHelper, type TextToImageTaskHelper, } from "./providerHelper.js"; +import { InferenceClientProviderOutputError } from "../errors.js"; const NEBIUS_API_BASE_URL = "https://api.studio.nebius.ai"; @@ -92,7 +92,7 @@ export class NebiusTextToImageTask extends TaskProviderHelper implements TextToI return fetch(`data:image/jpeg;base64,${base64Data}`).then((res) => res.blob()); } - throw new InferenceOutputError("Expected Nebius text-to-image response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Nebius text-to-image API"); } } diff --git a/packages/inference/src/providers/novita.ts b/packages/inference/src/providers/novita.ts index 2009ffa3ac..13c8ee2832 100644 --- a/packages/inference/src/providers/novita.ts +++ b/packages/inference/src/providers/novita.ts @@ -14,7 +14,6 @@ * * Thanks! */ -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; import { isUrl } from "../lib/isUrl.js"; import type { TextToVideoArgs } from "../tasks/index.js"; import type { BodyParams, UrlParams } from "../types.js"; @@ -26,6 +25,11 @@ import { TaskProviderHelper, type TextToVideoTaskHelper, } from "./providerHelper.js"; +import { + InferenceClientInputError, + InferenceClientProviderApiError, + InferenceClientProviderOutputError, +} from "../errors.js"; const NOVITA_API_BASE_URL = "https://api.novita.ai"; @@ -78,11 +82,13 @@ export class NovitaTextToVideoTask extends TaskProviderHelper implements TextToV headers?: Record ): Promise { if (!url || !headers) { - throw new InferenceOutputError("URL and headers are required for text-to-video task"); + throw new InferenceClientInputError("URL and headers are required for text-to-video task"); } const taskId = response.task_id; if (!taskId) { - throw new InferenceOutputError("No task ID found in the response"); + throw new InferenceClientProviderOutputError( + "Received malformed response from Novita text-to-video API: no task ID found in the response" + ); } const parsedUrl = new URL(url); @@ -98,7 +104,15 @@ export class NovitaTextToVideoTask extends TaskProviderHelper implements TextToV await delay(500); const resultResponse = await fetch(resultUrl, { headers }); if (!resultResponse.ok) { - throw new InferenceOutputError("Failed to fetch task result"); + throw new InferenceClientProviderApiError( + "Failed to fetch task result", + { url: resultUrl, method: "GET", headers }, + { + requestId: resultResponse.headers.get("x-request-id") ?? "", + status: resultResponse.status, + body: await resultResponse.text(), + } + ); } try { taskResult = await resultResponse.json(); @@ -113,15 +127,19 @@ export class NovitaTextToVideoTask extends TaskProviderHelper implements TextToV ) { status = taskResult.task.status; } else { - throw new InferenceOutputError("Failed to get task status"); + throw new InferenceClientProviderOutputError( + "Received malformed response from Novita text-to-video API: failed to get task status" + ); } } catch (error) { - throw new InferenceOutputError("Failed to parse task result"); + throw new InferenceClientProviderOutputError( + "Received malformed response from Novita text-to-video API: failed to parse task result" + ); } } if (status === "TASK_STATUS_FAILED") { - throw new InferenceOutputError("Task failed"); + throw new InferenceClientProviderOutputError("Novita text-to-video task failed"); } if ( @@ -139,7 +157,11 @@ export class NovitaTextToVideoTask extends TaskProviderHelper implements TextToV const urlResponse = await fetch(taskResult.videos[0].video_url); return await urlResponse.blob(); } else { - throw new InferenceOutputError("Expected { videos: [{ video_url: string }] }"); + throw new InferenceClientProviderOutputError( + `Received malformed response from Novita text-to-video API: expected { videos: [{ video_url: string }] } format, got instead: ${JSON.stringify( + taskResult + )}` + ); } } } diff --git a/packages/inference/src/providers/nscale.ts b/packages/inference/src/providers/nscale.ts index 92f1b3a9d7..85d1a8d0ff 100644 --- a/packages/inference/src/providers/nscale.ts +++ b/packages/inference/src/providers/nscale.ts @@ -15,10 +15,10 @@ * Thanks! */ import type { TextToImageInput } from "@huggingface/tasks"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; import type { BodyParams } from "../types.js"; import { omit } from "../utils/omit.js"; import { BaseConversationalTask, TaskProviderHelper, type TextToImageTaskHelper } from "./providerHelper.js"; +import { InferenceClientProviderOutputError } from "../errors.js"; const NSCALE_API_BASE_URL = "https://inference.api.nscale.com"; @@ -74,6 +74,6 @@ export class NscaleTextToImageTask extends TaskProviderHelper implements TextToI return fetch(`data:image/jpeg;base64,${base64Data}`).then((res) => res.blob()); } - throw new InferenceOutputError("Expected Nscale text-to-image response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Nscale text-to-image API"); } } diff --git a/packages/inference/src/providers/ovhcloud.ts b/packages/inference/src/providers/ovhcloud.ts index b0e5d21080..ff9c52f961 100644 --- a/packages/inference/src/providers/ovhcloud.ts +++ b/packages/inference/src/providers/ovhcloud.ts @@ -17,10 +17,10 @@ import { BaseConversationalTask, BaseTextGenerationTask } from "./providerHelper.js"; import type { ChatCompletionOutput, TextGenerationOutput, TextGenerationOutputFinishReason } from "@huggingface/tasks"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; import type { BodyParams } from "../types.js"; import { omit } from "../utils/omit.js"; import type { TextGenerationInput } from "@huggingface/tasks"; +import { InferenceClientProviderOutputError } from "../errors.js"; const OVHCLOUD_API_BASE_URL = "https://oai.endpoints.kepler.ai.cloud.ovh.net"; @@ -70,6 +70,6 @@ export class OvhCloudTextGenerationTask extends BaseTextGenerationTask { generated_text: completion.text, }; } - throw new InferenceOutputError("Expected OVHcloud text generation response format"); + throw new InferenceClientProviderOutputError("Received malformed response from OVHcloud text generation API"); } } diff --git a/packages/inference/src/providers/providerHelper.ts b/packages/inference/src/providers/providerHelper.ts index 00923d8a0c..19388ee41f 100644 --- a/packages/inference/src/providers/providerHelper.ts +++ b/packages/inference/src/providers/providerHelper.ts @@ -46,7 +46,7 @@ import type { ZeroShotImageClassificationOutput, } from "@huggingface/tasks"; import { HF_ROUTER_URL } from "../config.js"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; +import { InferenceClientProviderOutputError } from "../errors.js"; import type { AudioToAudioOutput } from "../tasks/audio/audioToAudio.js"; import type { BaseArgs, BodyParams, HeaderParams, InferenceProvider, RequestArgs, UrlParams } from "../types.js"; import { toArray } from "../utils/toArray.js"; @@ -320,7 +320,7 @@ export class BaseConversationalTask extends TaskProviderHelper implements Conver return response; } - throw new InferenceOutputError("Expected ChatCompletionOutput"); + throw new InferenceClientProviderOutputError("Expected ChatCompletionOutput"); } } @@ -353,6 +353,6 @@ export class BaseTextGenerationTask extends TaskProviderHelper implements TextGe return res[0]; } - throw new InferenceOutputError("Expected Array<{generated_text: string}>"); + throw new InferenceClientProviderOutputError("Expected Array<{generated_text: string}>"); } } diff --git a/packages/inference/src/providers/replicate.ts b/packages/inference/src/providers/replicate.ts index 7a908cb617..6e704d93b2 100644 --- a/packages/inference/src/providers/replicate.ts +++ b/packages/inference/src/providers/replicate.ts @@ -14,7 +14,7 @@ * * Thanks! */ -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; +import { InferenceClientProviderOutputError } from "../errors.js"; import { isUrl } from "../lib/isUrl.js"; import type { BodyParams, HeaderParams, UrlParams } from "../types.js"; import { omit } from "../utils/omit.js"; @@ -99,7 +99,7 @@ export class ReplicateTextToImageTask extends ReplicateTask implements TextToIma return await urlResponse.blob(); } - throw new InferenceOutputError("Expected Replicate text-to-image response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Replicate text-to-image API"); } } @@ -132,7 +132,7 @@ export class ReplicateTextToSpeechTask extends ReplicateTask { } } } - throw new InferenceOutputError("Expected Blob or object with output"); + throw new InferenceClientProviderOutputError("Received malformed response from Replicate text-to-speech API"); } } @@ -149,6 +149,6 @@ export class ReplicateTextToVideoTask extends ReplicateTask implements TextToVid return await urlResponse.blob(); } - throw new InferenceOutputError("Expected { output: string }"); + throw new InferenceClientProviderOutputError("Received malformed response from Replicate text-to-video API"); } } diff --git a/packages/inference/src/providers/sambanova.ts b/packages/inference/src/providers/sambanova.ts index efc41fe7da..d198ff53c6 100644 --- a/packages/inference/src/providers/sambanova.ts +++ b/packages/inference/src/providers/sambanova.ts @@ -14,12 +14,11 @@ * * Thanks! */ -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; - import type { FeatureExtractionOutput } from "@huggingface/tasks"; import type { BodyParams } from "../types.js"; import type { FeatureExtractionTaskHelper } from "./providerHelper.js"; import { BaseConversationalTask, TaskProviderHelper } from "./providerHelper.js"; +import { InferenceClientProviderOutputError } from "../errors.js"; export class SambanovaConversationalTask extends BaseConversationalTask { constructor() { @@ -40,8 +39,8 @@ export class SambanovaFeatureExtractionTask extends TaskProviderHelper implement if (typeof response === "object" && "data" in response && Array.isArray(response.data)) { return response.data.map((item) => item.embedding); } - throw new InferenceOutputError( - "Expected Sambanova feature-extraction (embeddings) response format to be {'data' : list of {'embedding' : number[]}}" + throw new InferenceClientProviderOutputError( + "Received malformed response from Sambanova feature-extraction (embeddings) API" ); } diff --git a/packages/inference/src/providers/together.ts b/packages/inference/src/providers/together.ts index 350b3c677d..6777fdb169 100644 --- a/packages/inference/src/providers/together.ts +++ b/packages/inference/src/providers/together.ts @@ -15,7 +15,6 @@ * Thanks! */ import type { ChatCompletionOutput, TextGenerationOutput, TextGenerationOutputFinishReason } from "@huggingface/tasks"; -import { InferenceOutputError } from "../lib/InferenceOutputError.js"; import type { BodyParams } from "../types.js"; import { omit } from "../utils/omit.js"; import { @@ -24,6 +23,7 @@ import { TaskProviderHelper, type TextToImageTaskHelper, } from "./providerHelper.js"; +import { InferenceClientProviderOutputError } from "../errors.js"; const TOGETHER_API_BASE_URL = "https://api.together.xyz"; @@ -74,7 +74,7 @@ export class TogetherTextGenerationTask extends BaseTextGenerationTask { generated_text: completion.text, }; } - throw new InferenceOutputError("Expected Together text generation response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Together text generation API"); } } @@ -113,6 +113,6 @@ export class TogetherTextToImageTask extends TaskProviderHelper implements TextT return fetch(`data:image/jpeg;base64,${base64Data}`).then((res) => res.blob()); } - throw new InferenceOutputError("Expected Together text-to-image response format"); + throw new InferenceClientProviderOutputError("Received malformed response from Together text-to-image API"); } } diff --git a/packages/inference/src/tasks/audio/automaticSpeechRecognition.ts b/packages/inference/src/tasks/audio/automaticSpeechRecognition.ts index 6f169a20f1..a8ce6ebed6 100644 --- a/packages/inference/src/tasks/audio/automaticSpeechRecognition.ts +++ b/packages/inference/src/tasks/audio/automaticSpeechRecognition.ts @@ -1,10 +1,10 @@ import type { AutomaticSpeechRecognitionInput, AutomaticSpeechRecognitionOutput } from "@huggingface/tasks"; import { resolveProvider } from "../../lib/getInferenceProviderMapping.js"; import { getProviderHelper } from "../../lib/getProviderHelper.js"; -import { InferenceOutputError } from "../../lib/InferenceOutputError.js"; import type { BaseArgs, Options } from "../../types.js"; import { innerRequest } from "../../utils/request.js"; import type { LegacyAudioInput } from "./utils.js"; +import { InferenceClientProviderOutputError } from "../../errors.js"; export type AutomaticSpeechRecognitionArgs = BaseArgs & (AutomaticSpeechRecognitionInput | LegacyAudioInput); /** @@ -24,7 +24,7 @@ export async function automaticSpeechRecognition( }); const isValidOutput = typeof res?.text === "string"; if (!isValidOutput) { - throw new InferenceOutputError("Expected {text: string}"); + throw new InferenceClientProviderOutputError("Received malformed response from automatic-speech-recognition API"); } return providerHelper.getResponse(res); } diff --git a/packages/inference/src/utils/request.ts b/packages/inference/src/utils/request.ts index e4aa24073a..9bc0333434 100644 --- a/packages/inference/src/utils/request.ts +++ b/packages/inference/src/utils/request.ts @@ -3,6 +3,8 @@ import { makeRequestOptions } from "../lib/makeRequestOptions.js"; import type { InferenceTask, Options, RequestArgs } from "../types.js"; import type { EventSourceMessage } from "../vendor/fetch-event-source/parse.js"; import { getLines, getMessages } from "../vendor/fetch-event-source/parse.js"; +import { InferenceClientProviderApiError } from "../errors.js"; +import type { JsonObject } from "../vendor/type-fest/basic.js"; export interface ResponseWrapper { data: T; @@ -12,6 +14,17 @@ export interface ResponseWrapper { }; } +function requestArgsToJson(args: RequestArgs): JsonObject { + // Convert the entire args object to a JSON-serializable format + const argsWithData = args as RequestArgs & { data?: Blob | ArrayBuffer }; + return JSON.parse( + JSON.stringify({ + ...argsWithData, + data: argsWithData.data ? "[Blob or ArrayBuffer]" : null, + }) + ) as JsonObject; +} + /** * Primitive to make custom calls to the inference provider */ @@ -39,18 +52,54 @@ export async function innerRequest( if (["application/json", "application/problem+json"].some((ct) => contentType?.startsWith(ct))) { const output = await response.json(); if ([400, 422, 404, 500].includes(response.status) && options?.chatCompletion) { - throw new Error( - `Server ${args.model} does not seem to support chat completion. Error: ${JSON.stringify(output.error)}` + throw new InferenceClientProviderApiError( + `Provider ${args.provider} does not seem to support chat completion for model ${ + args.model + } . Error: ${JSON.stringify(output.error)}`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: output } ); } - if (output.error || output.detail) { - throw new Error(JSON.stringify(output.error ?? output.detail)); + if (typeof output.error === "string" || typeof output.detail === "string") { + throw new InferenceClientProviderApiError( + `Failed to perform inference: ${output.error ?? output.detail}`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: output } + ); } else { - throw new Error(output); + throw new InferenceClientProviderApiError( + `Failed to perform inference: an HTTP error occurred when requesting the provider.`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: output } + ); } } const message = contentType?.startsWith("text/plain;") ? await response.text() : undefined; - throw new Error(message ?? "An error occurred while fetching the blob"); + throw new InferenceClientProviderApiError( + `Failed to perform inference: ${message ?? "an HTTP error occurred when requesting the provider"}`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: message ?? "" } + ); } if (response.headers.get("Content-Type")?.startsWith("application/json")) { @@ -85,26 +134,81 @@ export async function* innerStreamingRequest( if (response.headers.get("Content-Type")?.startsWith("application/json")) { const output = await response.json(); if ([400, 422, 404, 500].includes(response.status) && options?.chatCompletion) { - throw new Error(`Server ${args.model} does not seem to support chat completion. Error: ${output.error}`); + throw new InferenceClientProviderApiError( + `Provider ${args.provider} does not seem to support chat completion for model ${ + args.model + } . Error: ${JSON.stringify(output.error)}`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: output } + ); } if (typeof output.error === "string") { - throw new Error(output.error); + throw new InferenceClientProviderApiError( + `Failed to perform inference: ${output.error}`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: output } + ); } if (output.error && "message" in output.error && typeof output.error.message === "string") { /// OpenAI errors - throw new Error(output.error.message); + throw new InferenceClientProviderApiError( + `Failed to perform inference: ${output.error.message}`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: output } + ); } // Sambanova errors if (typeof output.message === "string") { - throw new Error(output.message); + throw new InferenceClientProviderApiError( + `Failed to perform inference: ${output.message}`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: output } + ); } } - throw new Error(`Server response contains error: ${response.status}`); + throw new InferenceClientProviderApiError( + `Failed to perform inference: an HTTP error occurred when requesting the provider.`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: "" } + ); } if (!response.headers.get("content-type")?.startsWith("text/event-stream")) { - throw new Error( - `Server does not support event stream content type, it returned ` + response.headers.get("content-type") + throw new InferenceClientProviderApiError( + `Failed to perform inference: server does not support event stream content type, it returned ` + + response.headers.get("content-type"), + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: "" } ); } @@ -151,7 +255,16 @@ export async function* innerStreamingRequest( typeof data.error.message === "string" ? data.error.message : JSON.stringify(data.error); - throw new Error(`Error forwarded from backend: ` + errorStr); + throw new InferenceClientProviderApiError( + `Failed to perform inference: an occurred while streaming the response: ${errorStr}`, + { + url, + method: info.method ?? "GET", + headers: info.headers as Record, + body: requestArgsToJson(args), + }, + { requestId: response.headers.get("x-request-id") ?? "", status: response.status, body: data } + ); } yield data as T; } diff --git a/packages/inference/src/vendor/type-fest/basic.ts b/packages/inference/src/vendor/type-fest/basic.ts new file mode 100644 index 0000000000..3fa40a0399 --- /dev/null +++ b/packages/inference/src/vendor/type-fest/basic.ts @@ -0,0 +1,31 @@ +/** +Matches a JSON object. + +This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. Don't use this as a direct return type as the user would have to double-cast it: `jsonObject as unknown as CustomResponse`. Instead, you could extend your CustomResponse type from it to ensure your type only uses JSON-compatible types: `interface CustomResponse extends JsonObject { … }`. + +@category JSON +*/ +export type JsonObject = { [Key in string]: JsonValue } & { [Key in string]?: JsonValue | undefined }; + +/** +Matches a JSON array. + +@category JSON +*/ +export type JsonArray = JsonValue[] | readonly JsonValue[]; + +/** +Matches any valid JSON primitive value. + +@category JSON +*/ +export type JsonPrimitive = string | number | boolean | null; + +/** +Matches any valid JSON value. + +@see `Jsonify` if you need to transform a type to one that is assignable to `JsonValue`. + +@category JSON +*/ +export type JsonValue = JsonPrimitive | JsonObject | JsonArray; diff --git a/packages/inference/src/vendor/type-fest/license-cc0 b/packages/inference/src/vendor/type-fest/license-cc0 new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/packages/inference/src/vendor/type-fest/license-cc0 @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/packages/inference/src/vendor/type-fest/license-mit b/packages/inference/src/vendor/type-fest/license-mit new file mode 100644 index 0000000000..fa7ceba3eb --- /dev/null +++ b/packages/inference/src/vendor/type-fest/license-mit @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.