huggingface: Haskell bindings to the Hugging Face Hub and Inference APIs

[ bsd3, library, machine-learning, network, program ] [ Propose Tags ] [ Report a vulnerability ]

A typed HTTP client for discovering models, datasets, and Spaces on the Hugging Face Hub, retrieving repository metadata and files, and running hosted inference. Includes chat completions, feature extraction, text generation, and classification, with explicit errors and pagination.


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0
Change log CHANGELOG.md
Dependencies aeson (>=2.1 && <2.4), base (>=4.18 && <4.23), bytestring (>=0.11 && <0.13), http-client (>=0.7.13 && <0.8), http-client-tls (>=0.3.6 && <0.5), http-types (>=0.12 && <0.13), huggingface, mtl (>=2.3.1 && <2.4), network-uri (>=2.6 && <2.7), optparse-applicative (>=0.18 && <0.20), parsec (>=3.1.16 && <3.2), text (>=2.0 && <2.3) [details]
Tested with ghc ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.4 || ==9.14.1
License BSD-3-Clause
Copyright (c) 2026 Emily Pillmore
Author Emily Pillmore
Maintainer Emily Pillmore <emilypi@cohomolo.gy>
Uploaded by topos at 2026-10-21T03:32:22Z
Category Network, Machine Learning
Home page https://github.com/emilypi/huggingface
Bug tracker https://github.com/emilypi/huggingface/issues
Source repo head: git clone https://github.com/emilypi/huggingface.git
Distributions
Executables huggingface-example
Downloads 0 total (0 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2026-10-21 [all 1 reports]

Readme for huggingface-0.1.0.0

[back to package description]

huggingface

Haskell bindings to the Hugging Face Hub and Inference Providers.

huggingface provides a reusable HTTP client, typed request and response records, explicit errors, pagination, and streaming downloads. It does not require Python or execute downloaded model code.

Scope

Area Operations
Hub discovery Search and filter models, datasets, and Spaces, one page at a time
Hub metadata Repository metadata and file listings at a branch, tag, or commit
Identity Look up the configured token's owner
Files Buffer small files or stream large files through a callback
Router inference Non-streaming chat completions, multimodal message content, tool calls
HF Inference Sentence embeddings, text classification, text generation, generic JSON tasks

This initial release covers the operations above. It does not implement Hub uploads or commits, snapshot caching, local inference, binary inference tasks, or SSE streaming. infer targets the hf-inference provider; chat completion uses the router and supports provider suffixes. Each model must be served for the requested task. The existence of a Hub repository alone does not imply hosted inference availability.

Build

cabal build all
cabal test all --test-show-details=direct
cabal haddock all
cabal sdist

The package uses Haskell2010 and targets GHC 9.6–9.14. CI builds and tests the unpacked source distribution on Linux across that range and on macOS with GHC 9.14. The local initial implementation was validated with GHC 9.14.1; the CI matrix is not a claim that every compiler was run locally.

To use this checkout from another project, add huggingface to its build-depends and include this directory in that project's cabal.project:

packages: . /path/to/huggingface

A default.nix expression is supplied for Nix users. Choose a nixpkgs set whose Haskell packages satisfy the Cabal bounds; the expression is not pinned.

Modules

Import HuggingFace for the domain API, or use focused imports:

Module Responsibility
HuggingFace.Client Client construction, parsed endpoints, and configuration
HuggingFace.Error Error constructors and redacted diagnostics
HuggingFace.Monad Opaque client computations, runner, and error/IO utilities
HuggingFace.Hub Hub operations and downloads
HuggingFace.Inference Hosted inference operations
HuggingFace.Types.Auth Opaque tokens and their parser
HuggingFace.Types.Repository Repository IDs, owners, names, paths, revisions, and commit SHAs
HuggingFace.Types.Hub Listing options, metadata tags, and Hub response records
HuggingFace.Types.Routing Model targets, provider selection, and resolved model names
HuggingFace.Types.Chat Chat identifiers, messages, requests, responses, and defaults
HuggingFace.Types.Tasks Text-generation parameters and task results
HuggingFace.Types.Parameters Constrained numeric types and smart constructors
HuggingFace.Types.Pagination Pages and opaque continuation handles
HuggingFace.Utils Shared parsing, JSON, and HTTP utilities
HuggingFace.Internal.HTTP Transport implementation for building additional operations

HuggingFace.Types re-exports the domain modules and errors for convenience. Existing imports from HuggingFace, HuggingFace.Types, HuggingFace.Hub, and HuggingFace.Inference remain supported. Domain implementations, parsers, and JSON instances live together. Shared helpers live in HuggingFace.Utils. Every library module is exposed by Cabal, including HuggingFace.Internal.HTTP. Internal identifies transport implementation details, not package visibility; import it and Utils explicitly when extending the client.

Module visibility does not expose raw constructors. Direct newtype accessors are declared as record fields. RepoId, InferenceModel, and Client use records for their independently meaningful components. Exported selectors support normal Haskell record updates. tokenBytes explicitly reveals the credential for custom transports; nextPageURL may contain private query parameters. Token and NextPage retain redacted Show instances. nextPageFromHeaders parses a continuation, and the transport checks its origin when it is consumed. repoIdParser supports composing larger Parsec grammars; parseRepoId remains the entry point for a complete external value.

For example, building a chat request needs no transport imports:

import Data.List.NonEmpty (NonEmpty (..))
import Data.Text (Text)
import HuggingFace.Error (HuggingFaceError)
import HuggingFace.Types.Chat (ChatRequest, defaultChatRequest, userMessage)
import HuggingFace.Types.Routing (parseInferenceModel)

prepareChat :: Text -> Text -> Either HuggingFaceError ChatRequest
prepareChat target prompt = do
    model <- parseInferenceModel target
    pure (defaultChatRequest model (userMessage prompt :| []))

Public Hub reads

{-# LANGUAGE OverloadedStrings #-}

import HuggingFace

main :: IO ()
main = do
    result <- runHuggingFaceM $ do
        limit <- liftEither (mkPositiveInt 10)
        client <- liftIO (newClient defaultClientConfig)
        page <- listModels client defaultListOptions
            { listSearch = Just "bert", listLimit = Just limit }
        liftIO $ mapM_ (print . repoIdText . repoId) (pageItems page)
    either print pure result

listModels, listDatasets, and listSpaces return a single Page RepoInfo. pageNext contains an opaque continuation when another page is available. Call nextPage client continuation to fetch it; the result type can be fixed with HuggingFaceM (Page RepoInfo). Pagination remains explicit so callers control how many requests they make. listLimit is the requested page size, and the server may cap it.

parseRepoId, parseRevision, and parseRepoPath turn external Text into opaque domain values without throwing. For example, modelInfo client repository (Just revision) retrieves a specific revision, while Nothing uses the default branch. Revision slashes are encoded as part of a single URL segment. RepoInfo retains unknown metadata in repoRaw, and optional fields remain Nothing when omitted.

Authentication and chat

The library never reads credentials implicitly. Pass a parsed token in accessToken. The example executable reads HF_TOKEN when it is present.

{-# LANGUAGE OverloadedStrings #-}

import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Text as T
import HuggingFace
import System.Environment (getEnv)

main :: IO ()
main = do
    result <- runHuggingFaceM $ do
        rawToken <- liftIO (T.pack <$> getEnv "HF_TOKEN")
        rawModel <- liftIO (T.pack <$> getEnv "HF_MODEL")
        token <- liftEither (parseToken rawToken)
        model <- liftEither (parseInferenceModel rawModel)
        limit <- liftEither (mkPositiveInt 64)
        client <- liftIO $ newClient defaultClientConfig { accessToken = Just token }
        let request = (defaultChatRequest model (userMessage "Hello!" :| []))
                { chatMaxTokens = Just limit }
        chatCompletion client request
    either print print result

Set HF_MODEL to a currently served model, optionally with a provider suffix. The token needs permission to call Inference Providers. Hosted calls can incur charges. chatExtra accepts provider options such as tools, seed, and response_format; it cannot override reserved typed fields or enable streaming. ChatMessage also accepts JSON content arrays for multimodal requests and preserves tool calls in responses.

The compiled example uses optparse-applicative and offers these commands:

cabal run huggingface-example -- models bert
cabal run huggingface-example -- model google-bert/bert-base-uncased
cabal run huggingface-example -- chat "$HF_MODEL" "Explain a Haskell functor briefly."

Only the last command invokes hosted inference.

Use cabal run huggingface-example -- --help for the command overview, or cabal run huggingface-example -- chat --help for command-specific help. Missing or invalid arguments produce usage errors before reading HF_TOKEN or creating a client. Repository and routed model arguments use the library's existing domain parsers. Use -- before a positional argument beginning with a dash, for example chat "$HF_MODEL" -- "--explain this flag".

Parse once, use typed values

Text received from configuration or JSON is parsed into domain types with hidden constructors. Request functions consume those types without reparsing. Direct accessors such as accountNameText are record selectors; computed renderers such as repoIdText and revisionText remain ordinary functions. Record updates can replace field values, so use the parsers and smart constructors for external input. JSON decoding uses those same parsers. There are no partial IsString or Read instances.

Domain and endpoint grammars use Parsec directly over strict Text; pagination headers use Parsec over ByteString. Parsers require complete input consumption and construct the domain values as they parse. Parser diagnostics are replaced with redacted errors, since raw unexpected tokens may contain credentials. Numeric smart constructors still consume numbers, and Aeson handles JSON.

Domain Types and entry points
Repository identity parseRepoId, or qualifiedRepo with an AccountName and RepoName
Repository content parseRepoPath, parseRevision, mainRevision, commitRevision
Commit identity parseCommitSha parses forty hex digits and normalizes case
Chat target parseInferenceModel, or modelFromRepo / routeModel with an existing RepoId
Routing Fastest, Cheapest, Preferred, or Via a parsed Provider
Trusted origins parseEndpoint retains a parsed request; default origins need no parsing
Metadata Separate HubTag, PipelineTag, LibraryName, SpaceSdk, and SortKey types
Chat identity Separate ChatRole, ToolCallId, ParticipantName, CompletionId, ModelName, and FinishReason types
Constrained numbers mkPositiveInt, mkChatTemperature, mkTopP, mkGenerationTemperature

Returned repoId, repoSha, fileName, and userName fields are typed too. For example, pass repoId info straight to modelInfo, fileName file to downloadFile, or commitRevision sha as a revision. routeModel (repoId info) Fastest selects a chat target without rendering and reparsing its ID.

ChatRequest conversations and featureExtraction batches use NonEmpty: construct one with first :| rest. For a dynamic list, use Data.List.NonEmpty.nonEmpty at the input boundary and handle its Nothing case there. Counts, page sizes, and response timeouts use PositiveInt; sampling settings have their own bounds and reject NaN and infinity.

Free-form prompts, generated text, display names, and classifier labels remain Text. Extension payloads remain JSON. Open server-defined namespaces retain future nonblank values rather than enumerating today's tags, roles, or stop reasons. A provider's resolved ModelName is distinct from a router InferenceModel, since provider aliases need not be Hub repository names. CompletionId preserves empty IDs returned by Hugging Face / TGI. String conversions are confined to dependencies that require them; HTTP headers and file contents remain bytes.

This is an API change from the initial unpublished scaffold: mkToken, mkRepoId, mkRevision, and mkRepoPath are now parseToken, parseRepoId, parseRevision, and parseRepoPath. newClient returns IO Client, and newClientWithManager returns Client; configuration failures are handled while constructing the typed configuration.

Downloads

downloadFile client kind repository revision path buffers a file in memory. Use withDownload for model weights. It supplies an http-client BodyReader to a HuggingFaceM callback and releases the connection on success, typed error, or IO exception:

import qualified Data.ByteString as BS
import HuggingFace.Monad (HuggingFaceM, liftIO)
import Network.HTTP.Client (BodyReader, brRead)
import System.IO (Handle)

copyBody :: Handle -> BodyReader -> HuggingFaceM ()
copyBody handle reader = do
    chunk <- liftIO (brRead reader)
    if BS.null chunk
        then pure ()
        else liftIO (BS.hPut handle chunk) >> copyBody handle reader

Pass copyBody handle as the last argument to withDownload. The caller owns the output handle, path selection, overwrite policy, and cleanup of partially written files. Do not retain the reader after the callback returns. The library follows at most five GET redirects and removes authentication when crossing origins, including ports. HTTPS cannot redirect to HTTP.

Errors and configuration

Client operations return HuggingFaceM a, an opaque newtype over ExceptT HuggingFaceError IO with Functor, Applicative, Monad, MonadIO, and MonadError HuggingFaceError instances. Compose operations in a do block; an error stops subsequent actions. Run the whole computation once with runHuggingFaceM :: HuggingFaceM a -> IO (Either HuggingFaceError a) at the application boundary. The transformer constructor is not exposed.

HuggingFace.Monad and the convenience import HuggingFace provide liftEither for pure smart-constructor results, liftIO for ordinary IO, and throwError and catchError for explicit error handling. These are the standard mtl operations. liftHuggingFaceM adapts existing IO (Either HuggingFaceError a) actions. Pure parsers remain pure, and newClient still returns IO Client.

For example, recover from a missing repository while preserving other errors:

import HuggingFace

findModel :: Client -> RepoId -> HuggingFaceM (Maybe RepoInfo)
findModel client repository =
    (Just <$> modelInfo client repository Nothing) `catchError` \err ->
        case err of
            HttpError {errorStatus = 404} -> pure Nothing
            _ -> throwError err

HTTP errors retain status, request ID, Retry-After, and at most 64 KiB of response content. Show omits response bodies and headers; treat those fields as sensitive when inspecting them yourself. Invalid JSON and schema mismatches return DecodeError. Transport failures are classified without retaining or printing the request. Asynchronous exceptions and non-HTTP callback exceptions propagate; neither catchError nor runHuggingFaceM catches them. A typed error leaving a download callback closes its response before an outer catchError handler runs.

newClient creates a reusable TLS connection pool with transport retries disabled. POSTs are never followed across redirects or automatically retried. This avoids silently replaying a potentially billable request. Callers can implement their own explicit retry policy using the returned status and headers.

newClientWithManager accepts a caller-owned manager. When constructing that manager, set managerRetryableException = const False to retain the no-replay guarantee. The caller is also responsible for its TLS, proxy, and request hooks. Only configure trusted Hub/inference origins, because the configured token is sent to both. HTTPS is required except for loopback HTTP development servers. Parse a custom origin with parseEndpoint before putting it in the configuration. The default response timeout is 30 seconds; set timeoutMicroseconds to a PositiveInt built with mkPositiveInt to change it.

Successful JSON responses and downloadFile are buffered without a size cap. Use bounded page sizes and withDownload for large content. Public requests are subject to Hugging Face rate limits; no automatic background requests occur.