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.