Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Umbraco.AI.HuggingFace/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog - Umbraco.AI.HuggingFace

All notable changes to Umbraco.AI.HuggingFace will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-04-24

Initial release.

[1.0.0]: https://github.com/umbraco/Umbraco.AI/releases/tag/Umbraco.AI.HuggingFace@1.0.0
104 changes: 104 additions & 0 deletions Umbraco.AI.HuggingFace/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

> **Note:** This is the Umbraco.AI.HuggingFace provider package. See the [root CLAUDE.md](../CLAUDE.md) for shared
> coding standards, build commands, and repository-wide conventions that apply to all packages.

## Build Commands

```bash
# Build the solution
dotnet build Umbraco.AI.HuggingFace.slnx

# Run tests (no test project exists yet — see "Testing" below)
dotnet test Umbraco.AI.HuggingFace.slnx
```

## Architecture Overview

`Umbraco.AI.HuggingFace` is a chat-only provider plugin that targets the [Hugging Face Inference Providers
router](https://huggingface.co/docs/inference-providers/index). The router exposes a drop-in OpenAI-compatible
`/v1/chat/completions` and `/v1/models` API at `https://router.huggingface.co/v1`, so we reuse
`Microsoft.Extensions.AI.OpenAI` instead of pulling in a vendor-specific SDK.

The package does **not** depend on `Umbraco.AI.OpenAI`; both packages happen to consume the same
`Microsoft.Extensions.AI.OpenAI` NuGet but neither references the other.

### Provider Implementation

```csharp
[AIProvider("huggingface", "Hugging Face")]
public class HuggingFaceProvider : AIProviderBase<HuggingFaceProviderSettings>
{
public HuggingFaceProvider(IAIProviderInfrastructure infrastructure, IMemoryCache cache)
: base(infrastructure)
{
WithCapability<HuggingFaceChatCapability>();
}
}
```

### Capabilities

**Chat Capability** (`HuggingFaceChatCapability`):

- Extends `AIChatCapabilityBase<HuggingFaceProviderSettings>`
- Builds an `IChatClient` via `OpenAIClient.GetChatClient(modelId).AsIChatClient()` (note: NOT
`GetResponsesClient` — the router does not implement OpenAI's Responses API, only the classic Chat Completions
API)
- Lists models via `GetOpenAIModelClient().GetModelsAsync()`, cached for one hour per API-key + endpoint pair
- Filters to `vendor/name`-shaped IDs and excludes obvious image/audio/embedding artefacts

### Routing Suffixes

HF model IDs may include a `:suffix` to control routing, e.g. `openai/gpt-oss-120b:fastest`,
`deepseek-ai/DeepSeek-R1:sambanova`. The capability and the model utility both treat the suffix as part of the
model ID; the `/v1/chat/completions` payload passes it through verbatim.

### Settings

```csharp
public class HuggingFaceProviderSettings
{
[AIField(IsSensitive = true)]
[Required]
public string? ApiKey { get; set; }

[AIField]
public string? Endpoint { get; set; } = "https://router.huggingface.co/v1";
}
```

## Future Work

The router endpoint is **chat only**. To add embeddings, image generation, or speech, we would need to either:

- Call Hugging Face's native task-specific Inference API directly (custom HTTP client), or
- Pull in the community `tryAGI/HuggingFace` SDK, which exposes `IEmbeddingGenerator` over HF's TEI endpoints.

Neither is in scope for the initial release.

## Dependencies

- Umbraco CMS 17.x
- Umbraco.AI 1.x
- Microsoft.Extensions.AI.OpenAI

## Provider Discovery

The provider is automatically discovered by Umbraco.AI through:

1. `[AIProvider]` attribute on the provider class
2. Assembly scanning during Umbraco startup
3. Registration in the `AIProvidersCollectionBuilder`

## Testing

There is no dedicated test project — by convention provider packages are validated manually via the demo site.
The csproj declares `InternalsVisibleTo "Umbraco.AI.HuggingFace.Tests.Unit"` for parity with the other providers.

## Contributing

See [CONTRIBUTING.md](../CONTRIBUTING.md) for contribution guidelines and the root [CLAUDE.md](../CLAUDE.md) for
coding standards.
50 changes: 50 additions & 0 deletions Umbraco.AI.HuggingFace/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Company>Umbraco HQ</Company>
<Authors>Umbraco</Authors>
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>
<Product>Umbraco AI Hugging Face Provider</Product>
<PackageProjectUrl>https://github.com/umbraco/Umbraco.AI/tree/main/Umbraco.AI.HuggingFace</PackageProjectUrl>
<PackageIcon>logo-128.png</PackageIcon>
<PackageTags>umbraco ai huggingface umbraco-marketplace</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<NeutralLanguage>en-US</NeutralLanguage>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<!-- NuGet packages lock -->
<PropertyGroup>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DefaultItemExcludes>$(DefaultItemExcludes);packages.lock.json</DefaultItemExcludes>
</PropertyGroup>

<!-- SourceLink -->
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<!-- Package validation -->
<PropertyGroup>
<EnablePackageValidation>false</EnablePackageValidation>
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
</PropertyGroup>

<ItemGroup>
<Content Include="$(MSBuildThisFileDirectory)\..\assets\logo-128.png" Pack="true" PackagePath="" Visible="false" />
<Content Include="$(MSBuildThisFileDirectory)\..\LICENSE.md" Pack="true" PackagePath="" Visible="false" />
</ItemGroup>

<PropertyGroup>
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
</PropertyGroup>
</Project>
77 changes: 77 additions & 0 deletions Umbraco.AI.HuggingFace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Umbraco.AI.HuggingFace

[![NuGet](https://img.shields.io/nuget/v/Umbraco.AI.HuggingFace.svg?style=flat&label=nuget)](https://www.nuget.org/packages/Umbraco.AI.HuggingFace/)

Hugging Face provider plugin for Umbraco.AI, giving access to hundreds of open-weights models served by the
[Hugging Face Inference Providers](https://huggingface.co/docs/inference-providers/index) router.

## Features

- **Inference Providers Router** — single endpoint (`https://router.huggingface.co/v1`) routes to Cerebras, Together, Fireworks, SambaNova, Groq, Replicate, and more
- **Chat Completions** — streaming and non-streaming chat against any conversational model on the Hub
- **Model Discovery** — fetches the list of available chat models directly from `/v1/models`
- **Routing Suffixes** — supports `model-id:fastest`, `:cheapest`, `:preferred`, or `:provider-name` to control how the request is routed
- **Custom Endpoint** — point at a self-hosted OpenAI-compatible gateway if needed

The provider has no dependency on `Umbraco.AI.OpenAI`; it talks to the Hugging Face router via the OpenAI-compatible
schema using `Microsoft.Extensions.AI.OpenAI`.

## Monorepo Context

This package is part of the [Umbraco.AI monorepo](../README.md). For local development, see the monorepo setup
instructions in the root README.

## Installation

```bash
dotnet add package Umbraco.AI.HuggingFace
```

## Requirements

- Umbraco CMS 17.0.0+
- Umbraco.AI 1.0.0+
- .NET 10.0
- A [Hugging Face access token](https://huggingface.co/settings/tokens) with the
*Make calls to Inference Providers* permission

## Configuration

After installation, create a connection in the Umbraco backoffice:

1. Navigate to the AI section
2. Create a new Hugging Face connection
3. Paste your Hugging Face access token
4. Create a profile that uses this connection

### API Configuration

```json
{
"ApiKey": "hf_..."
}
```

## Supported Models

The full list comes back live from `GET /v1/models` on the router and varies as Hugging Face partners add or
retire models. Examples that have been broadly available:

- `openai/gpt-oss-120b`
- `meta-llama/Meta-Llama-3.1-70B-Instruct`
- `deepseek-ai/DeepSeek-R1`
- `Qwen/Qwen2.5-72B-Instruct`
- `mistralai/Mistral-Small-24B-Instruct-2501`

Append a routing suffix to influence provider selection, e.g. `openai/gpt-oss-120b:fastest` or
`deepseek-ai/DeepSeek-R1:sambanova`.

## Documentation

- **[CLAUDE.md](CLAUDE.md)** — Development guide and technical details
- **[Root CLAUDE.md](../CLAUDE.md)** — Shared coding standards and conventions
- **[Contributing Guide](../CONTRIBUTING.md)** — How to contribute to the monorepo

## License

This project is licensed under the MIT License. See [LICENSE.md](../LICENSE.md) for details.
3 changes: 3 additions & 0 deletions Umbraco.AI.HuggingFace/Umbraco.AI.HuggingFace.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Solution>
<Project Path="src/Umbraco.AI.HuggingFace/Umbraco.AI.HuggingFace.csproj" />
</Solution>
3 changes: 3 additions & 0 deletions Umbraco.AI.HuggingFace/changelog.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"scopes": ["huggingface"]
}
2 changes: 2 additions & 0 deletions Umbraco.AI.HuggingFace/src/Umbraco.AI.HuggingFace/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Include wwwroot (overrides root .gitignore exclusion)
!wwwroot/
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.AI;
using Umbraco.AI.Core.Models;
using Umbraco.AI.Core.Providers;
using Umbraco.AI.Extensions;

namespace Umbraco.AI.HuggingFace;

/// <summary>
/// AI chat capability for the Hugging Face provider.
/// </summary>
public class HuggingFaceChatCapability(HuggingFaceProvider provider) : AIChatCapabilityBase<HuggingFaceProviderSettings>(provider)
{
private const string DefaultChatModel = "openai/gpt-oss-120b";

private new HuggingFaceProvider Provider => (HuggingFaceProvider)base.Provider;

// HF model IDs are formatted "vendor/model-name", optionally with a ":provider" or
// ":fastest|cheapest|preferred" routing suffix. The router's /v1/models endpoint
// returns chat-capable models; the slash check is enough to drop anything malformed.
private static readonly Regex IncludePattern =
new(@"^[A-Za-z0-9._-]+/[A-Za-z0-9._:-]+$", RegexOptions.Compiled);

// Drop obvious non-chat artefacts in case the router ever lists them.
private static readonly Regex[] ExcludePatterns =
[
new(@"(?:^|/)(flux|sdxl|sd-|stable-diffusion)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
new(@"-(embed|embedding|reranker|tts|whisper|asr)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled),
];

/// <inheritdoc />
protected override async Task<IReadOnlyList<AIModelDescriptor>> GetModelsAsync(
HuggingFaceProviderSettings settings,
CancellationToken cancellationToken = default)
{
var allModels = await Provider.GetAvailableModelIdsAsync(settings, cancellationToken);

return allModels
.Where(IsChatModel)
.Select(id => new AIModelDescriptor(
new AIModelRef(Provider.Id, id),
HuggingFaceModelUtilities.FormatDisplayName(id)))
.ToList();
}

/// <inheritdoc />
protected override IChatClient CreateClient(HuggingFaceProviderSettings settings, string? modelId)
=> HuggingFaceProvider.CreateOpenAIClient(settings)
.GetChatClient(modelId ?? DefaultChatModel)
.AsIChatClient();

private static bool IsChatModel(string modelId)
=> IncludePattern.IsMatch(modelId)
&& !ExcludePatterns.Any(p => p.IsMatch(modelId));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace Umbraco.AI.Extensions;

/// <summary>
/// Utility methods for working with Hugging Face model identifiers.
/// </summary>
internal static class HuggingFaceModelUtilities
{
/// <summary>
/// Formats a Hugging Face model ID into a human-readable display name.
/// </summary>
/// <param name="modelId">
/// The model ID, typically "vendor/model-name" with an optional ":provider" or
/// ":fastest|cheapest|preferred" routing suffix
/// (e.g., "meta-llama/Meta-Llama-3.1-70B-Instruct", "openai/gpt-oss-120b:fastest").
/// </param>
/// <returns>A formatted display name (e.g., "meta-llama / Meta Llama 3.1 70B Instruct (fastest)").</returns>
public static string FormatDisplayName(string modelId)
{
if (string.IsNullOrWhiteSpace(modelId))
{
return modelId;
}

var routingSuffix = string.Empty;
var withoutSuffix = modelId;
var colonIndex = modelId.IndexOf(':');
if (colonIndex > 0 && colonIndex < modelId.Length - 1)
{
routingSuffix = modelId[(colonIndex + 1)..];
withoutSuffix = modelId[..colonIndex];
}

var slashIndex = withoutSuffix.IndexOf('/');
var vendor = slashIndex > 0 ? withoutSuffix[..slashIndex] : null;
var name = slashIndex > 0 ? withoutSuffix[(slashIndex + 1)..] : withoutSuffix;

var formattedName = FormatModelName(name);
var displayName = vendor is null ? formattedName : $"{vendor} / {formattedName}";

return string.IsNullOrEmpty(routingSuffix) ? displayName : $"{displayName} ({routingSuffix})";
}

private static string FormatModelName(string name)
{
var parts = name.Split('-', StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < parts.Length; i++)
{
var part = parts[i];
if (part.Length == 0 || part.Any(char.IsDigit))
{
continue;
}

parts[i] = char.ToUpperInvariant(part[0]) + part[1..];
}

return string.Join(' ', parts);
}
}
Loading
Loading