Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Open Chat Playground (OCP) is a web UI that is able to connect virtually any LLM
- [x] [Hugging Face](https://huggingface.co/docs)
- [x] [Ollama](https://github.com/ollama/ollama/tree/main/docs)
- [ ] [Anthropic](https://docs.anthropic.com)
- [ ] [Naver](https://api.ncloud-docs.com/docs/ai-naver-clovastudio-summary)
- [ ] ~~[Naver](https://api.ncloud-docs.com/docs/ai-naver-clovastudio-summary)~~
- [x] [LG](https://github.com/LG-AI-EXAONE)
- [x] [OpenAI](https://openai.com/api)
- [x] [Upstage](https://console.upstage.ai/docs/getting-started)
Expand Down Expand Up @@ -77,7 +77,7 @@ Open Chat Playground (OCP) is a web UI that is able to connect virtually any LLM
- [Use Azure AI Foundry](./docs/azure-ai-foundry.md#run-in-local-container)
- [Use GitHub Models](./docs/github-models.md#run-in-local-container)
- [Use Docker Model Runner](./docs/docker-model-runner.md#run-in-local-container)
- ~~Use Foundry Local~~ 👉 NOT SUPPORTED
- [Use Foundry Local](./docs/foundry-local.md#run-in-local-container)
- [Use Hugging Face](./docs/hugging-face.md#run-in-local-container)
- [Use Ollama](./docs/ollama.md#run-on-local-container)
- [Use LG](./docs/lg.md#run-in-local-container)
Expand Down
111 changes: 108 additions & 3 deletions docs/foundry-local.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenChat Playground with Foundry Local

This page describes how to run OpenChat Playground (OCP) with Foundry Local models integration.
This page describes how to run OpenChat Playground (OCP) with [Foundry Local](https://learn.microsoft.com/azure/ai-foundry/foundry-local/what-is-foundry-local) integration.

## Get the repository root

Expand All @@ -18,7 +18,7 @@ This page describes how to run OpenChat Playground (OCP) with Foundry Local mode

## Run on local machine

1. Make sure the Foundry Local server is up and running.
1. Make sure the Foundry Local server is up and running with the following command.

```bash
foundry service start
Expand Down Expand Up @@ -74,4 +74,109 @@ This page describes how to run OpenChat Playground (OCP) with Foundry Local mode
--alias qwen2.5-7b
```

1. Open your web browser, navigate to `http://localhost:5280`, and enter prompts.
1. Open your web browser, navigate to `http://localhost:5280`, and enter prompts.

## Run in local container

1. Make sure the Foundry Local server is up and running.

```bash
foundry service start
```

1. Get the Foundry Local service port.

```bash
# bash/zsh
FL_PORT_NUMBER=$(foundry service set --show true | sed -n '/^{/,/^}/p' | jq -r ".serviceSettings.port")
```

```powershell
# PowerShell
$FL_PORT_NUMBER = (foundry service set --show true | `
ForEach-Object { `
if ($_ -match '^{') { $capture = $true } `
if ($capture) { $_ } `
if ($_ -match '^}') { $capture = $false } `
} | Out-String | ConvertFrom-Json).serviceSettings.port
```

1. Download the Foundry Local model. The default model OCP uses is `phi-4-mini`.

```bash
foundry model download phi-4-mini
```

Alternatively, if you want to run with a different model, say `qwen2.5-7b`, other than the default one, download it first by running the following command.

```bash
foundry model download qwen2.5-7b
```

Make sure to follow the model MUST be selected from the CLI output of `foundry model list`.

1. Load the Foundry Local model. The default model OCP uses is `phi-4-mini`.

```bash
foundry model load phi-4-mini
```

Alternatively, if you want to run with a different model, say `qwen2.5-7b`, other than the default one, download it first by running the following command.

```bash
foundry model load qwen2.5-7b
```

1. Make sure you are at the repository root.

```bash
cd $REPOSITORY_ROOT
```

1. Build a container.

```bash
docker build -f Dockerfile -t openchat-playground:latest .
```

1. Run the app. The `{{Model ID}}` refers to the `Model ID` shown in the output of the `foundry service list` command.

> **NOTE**: Make sure it MUST be the model ID, instead of alias.

```bash
# bash/zsh - from locally built container
docker run -i --rm -p 8080:8080 openchat-playground:latest \
--connector-type FoundryLocal \
--base-url http://host.docker.internal:$FL_PORT_NUMBER/ \
--model "Phi-4-mini-instruct-generic-gpu:4" \
--disable-foundrylocal-manager
```

```powershell
# PowerShell - from locally built container
docker run -i --rm -p 8080:8080 openchat-playground:latest `
--connector-type FoundryLocal `
--base-url http://host.docker.internal:$FL_PORT_NUMBER/ `
--model {{Model ID}} `
--disable-foundrylocal-manager
```

```bash
# bash/zsh - from GitHub Container Registry
docker run -i --rm -p 8080:8080 ghcr.io/aliencube/open-chat-playground/openchat-playground:latest \
--connector-type FoundryLocal \
--base-url http://host.docker.internal:$FL_PORT_NUMBER/ \
--model {{Model ID}} \
--disable-foundrylocal-manager
```

```powershell
# PowerShell - from GitHub Container Registry
docker run -i --rm -p 8080:8080 ghcr.io/aliencube/open-chat-playground/openchat-playground:latest `
--connector-type FoundryLocal `
--base-url http://host.docker.internal:$FL_PORT_NUMBER/ `
--model {{Model ID}} `
--disable-foundrylocal-manager
```

1. Open your web browser, navigate to `http://localhost:8080`, and enter prompts.
28 changes: 19 additions & 9 deletions src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ private static readonly (ConnectorType ConnectorType, string Argument, bool IsSw
(ConnectorType.DockerModelRunner, ArgumentOptionConstants.DockerModelRunner.BaseUrl, false),
(ConnectorType.DockerModelRunner, ArgumentOptionConstants.DockerModelRunner.Model, false),
// Foundry Local
(ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.BaseUrl, false),
(ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.Alias, false),
(ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.Model, false),
(ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManager, true),
(ConnectorType.FoundryLocal, ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManagerInShort, true),
// Hugging Face
(ConnectorType.HuggingFace, ArgumentOptionConstants.HuggingFace.BaseUrl, false),
(ConnectorType.HuggingFace, ArgumentOptionConstants.HuggingFace.Model, false),
Expand Down Expand Up @@ -212,9 +216,11 @@ public static AppSettings Parse(IConfiguration config, string[] args)

case FoundryLocalArgumentOptions foundryLocal:
settings.FoundryLocal ??= new FoundryLocalSettings();
settings.FoundryLocal.Alias = foundryLocal.Alias ?? settings.FoundryLocal.Alias;
settings.FoundryLocal.BaseUrl = foundryLocal.BaseUrl ?? settings.FoundryLocal.BaseUrl;
settings.FoundryLocal.AliasOrModel = foundryLocal.AliasOrModel ?? settings.FoundryLocal.AliasOrModel;
settings.FoundryLocal.DisableFoundryLocalManager = foundryLocal.DisableFoundryLocalManager;

settings.Model = foundryLocal.Alias ?? settings.FoundryLocal.Alias;
settings.Model = foundryLocal.AliasOrModel ?? settings.FoundryLocal.AliasOrModel;
break;

case HuggingFaceArgumentOptions huggingFace:
Expand Down Expand Up @@ -361,10 +367,10 @@ private static void DisplayHelpForAmazonBedrock()
Console.WriteLine(" ** Amazon Bedrock: **");
Console.ForegroundColor = foregroundColor;

Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.AccessKeyId} The AWSCredentials Access Key ID.");
Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.SecretAccessKey} The AWSCredentials Secret Access Key.");
Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.Region} The AWS region.");
Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.ModelId} The model ID. Default to 'anthropic.claude-sonnet-4-20250514-v1:0'");
Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.AccessKeyId} The AWSCredentials Access Key ID.");
Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.SecretAccessKey} The AWSCredentials Secret Access Key.");
Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.Region} The AWS region.");
Console.WriteLine($" {ArgumentOptionConstants.AmazonBedrock.ModelId} The model ID. Default to 'anthropic.claude-sonnet-4-20250514-v1:0'");
Console.WriteLine();
}

Expand Down Expand Up @@ -424,7 +430,10 @@ private static void DisplayHelpForFoundryLocal()
Console.WriteLine(" ** Foundry Local: **");
Console.ForegroundColor = foregroundColor;

Console.WriteLine(" TBD");
Console.WriteLine($" {ArgumentOptionConstants.FoundryLocal.BaseUrl} The endpoint URL. Default to 'http://localhost:<random_port>/'");
Console.WriteLine($" {ArgumentOptionConstants.FoundryLocal.Alias}|{ArgumentOptionConstants.FoundryLocal.Model} The alias or model ID. Default to 'phi-4-mini'");
Console.WriteLine($" {ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManager}|{ArgumentOptionConstants.FoundryLocal.DisableFoundryLocalManagerInShort} Disable the built-in Foundry local manager.");
Console.WriteLine($" When this flag is set, you must specify '--base-url'.");
Console.WriteLine();
}

Expand Down Expand Up @@ -471,7 +480,8 @@ private static void DisplayHelpForLG()
Console.WriteLine(" ** LG: **");
Console.ForegroundColor = foregroundColor;

Console.WriteLine(" TBD");
Console.WriteLine($" {ArgumentOptionConstants.LG.BaseUrl} The baseURL. Default to 'http://localhost:11434'");
Console.WriteLine($" {ArgumentOptionConstants.LG.Model} The model name. Default to 'hf.co/LGAI-EXAONE/EXAONE-4.0-1.2B-GGUF'");
Console.WriteLine();
}

Expand All @@ -493,7 +503,7 @@ private static void DisplayHelpForOpenAI()
Console.WriteLine(" ** OpenAI: **");
Console.ForegroundColor = foregroundColor;

Console.WriteLine($" {ArgumentOptionConstants.OpenAI.ApiKey} The OpenAI API key. (Env: OPENAI_API_KEY)");
Console.WriteLine($" {ArgumentOptionConstants.OpenAI.ApiKey} The OpenAI API key.");
Console.WriteLine($" {ArgumentOptionConstants.OpenAI.Model} The OpenAI model name. Default to 'gpt-4.1-mini'");
Console.WriteLine();
}
Expand Down
14 changes: 12 additions & 2 deletions src/OpenChat.PlaygroundApp/Configurations/FoundryLocalSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@ public partial class AppSettings
public class FoundryLocalSettings : LanguageModelSettings
{
/// <summary>
/// Gets or sets the alias of FoundryLocal.
/// Gets or sets the Base URL of Foundry Local. If `DisableFoundryLocalManager` is set, this value must be provided.
/// </summary>
public string? Alias { get; set; }
public string? BaseUrl { get; set; }

/// <summary>
/// Gets or sets either alias or model ID of Foundry Local.
/// </summary>
public string? AliasOrModel { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to disable the automatic Foundry Local manager and use a manually configured endpoint.
/// </summary>
public bool DisableFoundryLocalManager { get; set; }
}
65 changes: 56 additions & 9 deletions src/OpenChat.PlaygroundApp/Connectors/FoundryLocalConnector.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ClientModel;
using System.Text.RegularExpressions;

using Microsoft.AI.Foundry.Local;
using Microsoft.Extensions.AI;
Expand All @@ -16,6 +17,10 @@ namespace OpenChat.PlaygroundApp.Connectors;
/// <param name="settings"><see cref="AppSettings"/> instance.</param>
public class FoundryLocalConnector(AppSettings settings) : LanguageModelConnector(settings.FoundryLocal)
{
private const string ApiKey = "OPENAI_API_KEY";

private static readonly Regex modelIdSuffix = new(":[0-9]+$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings));

/// <inheritdoc/>
Expand All @@ -26,9 +31,21 @@ public override bool EnsureLanguageModelSettingsValid()
throw new InvalidOperationException("Missing configuration: FoundryLocal.");
}

if (string.IsNullOrWhiteSpace(settings.Alias!.Trim()))
if (settings.DisableFoundryLocalManager == true &&
string.IsNullOrWhiteSpace(settings.BaseUrl!.Trim()) == true)
{
throw new InvalidOperationException("Missing configuration: FoundryLocal:Alias.");
throw new InvalidOperationException("Missing configuration: FoundryLocal:BaseUrl is required when DisableFoundryLocalManager is enabled.");
}

if (string.IsNullOrWhiteSpace(settings.AliasOrModel!.Trim()) == true)
{
throw new InvalidOperationException("Missing configuration: FoundryLocal:AliasOrModel.");
}

if (settings.DisableFoundryLocalManager == true &&
modelIdSuffix.IsMatch(settings.AliasOrModel!.Trim()!) == false)
{
throw new InvalidOperationException("When DisableFoundryLocalManager is true, FoundryLocal:AliasOrModel must be the exact model name with version suffix.");
}

return true;
Expand All @@ -38,23 +55,53 @@ public override bool EnsureLanguageModelSettingsValid()
public override async Task<IChatClient> GetChatClientAsync()
{
var settings = this.Settings as FoundryLocalSettings;
var alias = settings!.Alias!.Trim() ?? throw new InvalidOperationException("Missing configuration: FoundryLocal:Alias.");

var manager = await FoundryLocalManager.StartModelAsync(aliasOrModelId: alias).ConfigureAwait(false);
var model = await manager.GetModelInfoAsync(aliasOrModelId: alias).ConfigureAwait(false);
(Uri? endpoint, string? modelId) = settings!.DisableFoundryLocalManager == true
? ParseFromModelId(settings)
: await ParseFromManagerAsync(settings).ConfigureAwait(false);

var credential = new ApiKeyCredential(manager.ApiKey);
var credential = new ApiKeyCredential(ApiKey);
var options = new OpenAIClientOptions()
{
Endpoint = manager.Endpoint,
Endpoint = endpoint,
};

var client = new OpenAIClient(credential, options);
var chatClient = client.GetChatClient(model?.ModelId)
var chatClient = client.GetChatClient(modelId)
.AsIChatClient();

Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {alias}");
Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {modelId}");

return chatClient;
}

private static (Uri? endpoint, string? modelId) ParseFromModelId(FoundryLocalSettings settings)
{
var baseUrl = settings.BaseUrl!.Trim() ?? throw new InvalidOperationException("Missing configuration: FoundryLocal:BaseUrl.");
if (Uri.IsWellFormedUriString(baseUrl, UriKind.Absolute) == false)
{
throw new UriFormatException($"Invalid URI: The Foundry Local base URL '{baseUrl}' is not a valid URI.");
}

var endpoint = new Uri($"{baseUrl.TrimEnd('/')}/v1");
var modelId = settings.AliasOrModel!.Trim() ?? throw new InvalidOperationException("Missing configuration: FoundryLocal:AliasOrModel.");
if (modelIdSuffix.IsMatch(modelId) == false)
{
throw new InvalidOperationException("When DisableFoundryLocalManager is true, FoundryLocal:AliasOrModel must be the exact model name with version suffix.");
}

return (endpoint, modelId);
}

private static async Task<(Uri? endpoint, string? modelId)> ParseFromManagerAsync(FoundryLocalSettings settings)
{
var alias = settings!.AliasOrModel!.Trim() ?? throw new InvalidOperationException("Missing configuration: FoundryLocal:AliasOrModel.");
var manager = await FoundryLocalManager.StartModelAsync(aliasOrModelId: alias).ConfigureAwait(false);
var model = await manager.GetModelInfoAsync(aliasOrModelId: alias).ConfigureAwait(false);

var endpoint = manager.Endpoint;
var modelId = model!.ModelId;

return (endpoint, modelId);
}
}
20 changes: 20 additions & 0 deletions src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,30 @@ public static class DockerModelRunner
/// </summary>
public static class FoundryLocal
{
/// <summary>
/// Defines the constant for '--base-url'.
/// </summary>
public const string BaseUrl = "--base-url";

/// <summary>
/// Defines the constant for '--alias'.
/// </summary>
public const string Alias = "--alias";

/// <summary>
/// Defines the constant for '--model'.
/// </summary>
public const string Model = "--model";

/// <summary>
/// Defines the constant for '--disable-foundry-local-manager'.
/// </summary>
public const string DisableFoundryLocalManager = "--disable-foundry-local-manager";

/// <summary>
/// Defines the constant for '--disable-flm'.
/// </summary>
public const string DisableFoundryLocalManagerInShort = "--disable-flm";
}

/// <summary>
Expand Down
Loading