# huggingface
Haskell bindings to the [Hugging Face Hub](https://huggingface.co/docs/hub/api)
and [Inference Providers](https://huggingface.co/docs/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
```console
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`:
```cabal
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:
```haskell
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
```haskell
{-# 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.
```haskell
{-# 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:
```console
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:
```haskell
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](https://hackage.haskell.org/package/mtl-2.3.1/docs/Control-Monad-Except.html).
`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:
```haskell
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.