From 4714dd80472f2c8899a3b7c9045f04b356d95789 Mon Sep 17 00:00:00 2001 From: MWK Date: Sat, 13 Sep 2025 16:07:42 +0900 Subject: [PATCH 01/18] refactor: split UI logic in ChatHeader.razor --- .../Components/Pages/Chat/ChatHeader.razor | 5 ----- .../Components/Pages/Chat/ChatHeader.razor.cs | 9 +++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs diff --git a/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor b/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor index 4fc74244..8028e5cc 100644 --- a/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor +++ b/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor @@ -10,8 +10,3 @@

OpenChat.PlaygroundApp

- -@code { - [Parameter] - public EventCallback OnNewChat { get; set; } -} diff --git a/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs b/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs new file mode 100644 index 00000000..3f755644 --- /dev/null +++ b/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Components; + +namespace OpenChat.PlaygroundApp.Components.Pages.Chat; + +public partial class ChatHeader : ComponentBase +{ + [Parameter] + public EventCallback OnNewChat { get; set; } +} From 04735cc7b39b5dac6b2df4bf05ccf3a647bd2837 Mon Sep 17 00:00:00 2001 From: MWK Date: Sat, 13 Sep 2025 16:25:56 +0900 Subject: [PATCH 02/18] test: add integration tests for NewChat button and icon visibility --- .../Pages/Chat/ChatHeaderUITests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs b/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs index c78a9bd0..49781128 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs @@ -24,6 +24,28 @@ public async Task Given_Root_Page_When_Loaded_Then_Header_Should_Be_Visible(stri title.ShouldBe(expected); } + [Trait("Category", "IntegrationTest")] + [Fact] + public async Task Given_Root_Page_When_Loaded_Then_NewChat_Button_Should_Be_Visible() + { + // Arrange + var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" }); + + // Assert + await Expect(newChatButton).ToBeVisibleAsync(); + } + + [Trait("Category", "IntegrationTest")] + [Fact] + public async Task Given_Header_When_Loaded_Then_NewChat_Icon_Should_Be_Visible() + { + // Arrange + var icon = Page.Locator("button svg.new-chat-icon"); + + // Assert + await Expect(icon).ToBeVisibleAsync(); + } + public override async Task DisposeAsync() { await Page.CloseAsync(); From bb8dcbf5d137b8531adf60de26e8d1927b558649 Mon Sep 17 00:00:00 2001 From: MWK Date: Mon, 13 Oct 2025 23:33:33 +0900 Subject: [PATCH 03/18] feat: Add support for Anthropic API integration and update documentation --- .github/workflows/azure-dev.yml | 2 ++ README.md | 2 +- docs/README.md | 1 + docs/anthropic.md | 0 infra/main.bicep | 5 +++++ infra/main.parameters.json | 6 ++++++ infra/resources.bicep | 20 ++++++++++++++++++++ 7 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 docs/anthropic.md diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index ecfc628f..66bcfeeb 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -31,6 +31,7 @@ jobs: AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} GH_MODELS_TOKEN: ${{ secrets.GH_MODELS_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} steps: @@ -133,6 +134,7 @@ jobs: shell: bash env: GitHubModels__Token: ${{ env.GH_MODELS_TOKEN }} + Anthropic__ApiKey: ${{ env.ANTHROPIC_API_KEY }} OpenAI__ApiKey: ${{ env.OPENAI_API_KEY }} run: | azd provision --no-prompt diff --git a/README.md b/README.md index f6e277e5..ac325a34 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Open Chat Playground (OCP) is a web UI that is able to connect virtually any LLM - [ ] [Foundry Local](https://learn.microsoft.com/azure/ai-foundry/foundry-local/what-is-foundry-local) - [x] [Hugging Face](https://huggingface.co/docs) - [ ] [Ollama](https://github.com/ollama/ollama/tree/main/docs) -- [ ] [Anthropic](https://docs.anthropic.com) +- [x] [Anthropic](https://docs.anthropic.com) - [ ] [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) diff --git a/docs/README.md b/docs/README.md index fa29a638..4890d867 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,5 +3,6 @@ - [Azure AI Foundry](azure-ai-foundry.md) - [GitHub Models](github-models.md) - [Hugging Face](hugging-face.md) +- [Anthropic](anthropic.md) - [LG](lg.md) - [OpenAI](openai.md) diff --git a/docs/anthropic.md b/docs/anthropic.md new file mode 100644 index 00000000..e69de29b diff --git a/infra/main.bicep b/infra/main.bicep index 28ced17c..b930283b 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -29,6 +29,9 @@ param huggingFaceModel string = '' // Ollama param ollamaModel string = '' // Anthropic +param anthropicModel string = '' +@secure() +param anthropicApiKey string = '' // LG param lgModel string = '' // Naver @@ -85,6 +88,8 @@ module resources 'resources.bicep' = { githubModelsToken: githubModelsToken huggingFaceModel: huggingFaceModel ollamaModel: ollamaModel + anthropicModel: anthropicModel + anthropicApiKey: anthropicApiKey lgModel: lgModel openAIModel: openAIModel openAIApiKey: openAIApiKey diff --git a/infra/main.parameters.json b/infra/main.parameters.json index c9c1a5b9..b7ae2e83 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -29,6 +29,12 @@ "huggingFaceModel": { "value": "${HUGGING_FACE_MODEL=hf.co/Qwen/Qwen3-0.6B-GGUF}" }, + "anthropicModel" : { + "value": "${ANTHROPIC_MODEL=claude-sonnet-4-0}" + }, + "anthropicApiKey" : { + "value": "${ANTHROPIC_API_KEY}" + }, "lgModel": { "value": "${LG_MODEL=hf.co/LGAI-EXAONE/EXAONE-4.0-1.2B-GGUF}" }, diff --git a/infra/resources.bicep b/infra/resources.bicep index 0483ad36..043c14eb 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -24,6 +24,9 @@ param huggingFaceModel string = '' // Ollama param ollamaModel string = '' // Anthropic +param anthropicModel string = '' +@secure() +param anthropicApiKey string = '' // LG param lgModel string = '' // Naver @@ -227,6 +230,17 @@ var envOllama = connectorType == 'Ollama' ? concat(ollamaModel != '' ? [ } ] : []) : [] // Anthropic +var envAnthropic = connectorType == 'Anthropic' ? concat(anthropicModel != '' ? [ + { + name: 'Anthropic__Model' + value: anthropicModel + } +] : [], anthropicApiKey != '' ? [ + { + name: 'Anthropic__ApiKey' + secretRef: 'anthropic-api-key' + } +] : []) : [] // LG var envLG = connectorType == 'LG' ? concat(lgModel != '' ? [ { @@ -268,6 +282,11 @@ module openchatPlaygroundApp 'br/public:avm/res/app/container-app:0.18.1' = { name: 'github-models-token' value: githubModelsToken } + ] : [], anthropicApiKey != '' ? [ + { + name: 'anthropic-api-key' + value: anthropicApiKey + } ] : [], openAIApiKey != '' ? [ { name: 'openai-api-key' @@ -301,6 +320,7 @@ module openchatPlaygroundApp 'br/public:avm/res/app/container-app:0.18.1' = { envGitHubModels, envHuggingFace, envOllama, + envAnthropic, envLG, envOpenAI, useOllama == true ? [ { From ebf7b2b0a4771afe51859ab1bf14c8cbcc2662e3 Mon Sep 17 00:00:00 2001 From: MWK Date: Tue, 14 Oct 2025 00:18:29 +0900 Subject: [PATCH 04/18] feat: Add documentation for running OpenChat Playground with Anthropic Claude integration --- docs/anthropic.md | 225 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/docs/anthropic.md b/docs/anthropic.md index e69de29b..3eacc2cf 100644 --- a/docs/anthropic.md +++ b/docs/anthropic.md @@ -0,0 +1,225 @@ +# OpenChat Playground with Anthropic Claude + +This page describes how to run OpenChat Playground (OCP) with [Anthropic Claude](https://www.anthropic.com/claude) integration. + +## Get the repository root + +1. Get the repository root. + + ```bash + # bash/zsh + REPOSITORY_ROOT=$(git rev-parse --show-toplevel) + ``` + + ```powershell + # PowerShell + $REPOSITORY_ROOT = git rev-parse --show-toplevel + ``` + +## Run on local machine + +1. Make sure you are at the repository root. + + ```bash + cd $REPOSITORY_ROOT + ``` + +1. Add Anthropic API Key for Claude connection. Make sure you should replace `{{ANTHROPIC_API_KEY}}` with your Anthropic API key. + + ```bash + # bash/zsh + dotnet user-secrets --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp \ + set Anthropic:ApiKey "{{ANTHROPIC_API_KEY}}" + ``` + + ```bash + # PowerShell + dotnet user-secrets --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp ` + set Anthropic:ApiKey "{{ANTHROPIC_API_KEY}}" + ``` + + > For more details about Anthropic API keys, refer to the doc, [Anthropic API Documentation](https://docs.anthropic.com/claude/reference/getting-started-with-the-api). + +1. Run the app. The default model OCP uses is [Claude 3 Opus](https://www.anthropic.com/claude). + + ```bash + # bash/zsh + dotnet run --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp -- \ + --connector-type Anthropic + ``` + + ```powershell + # PowerShell + dotnet run --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp -- ` + --connector-type Anthropic + ``` + + Alternatively, if you want to run with a different model, say [Claude 3 Sonnet](https://www.anthropic.com/claude), other than the default one, you can specify it as an argument: + + ```bash + # bash/zsh + dotnet run --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp -- \ + --connector-type Anthropic \ + --model claude-3-sonnet-20240229 + ``` + + ```powershell + # PowerShell + dotnet run --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp -- ` + --connector-type Anthropic ` + --model claude-3-sonnet-20240229 + ``` + +1. Open your web browser, navigate to `http://localhost:5280`, and enter prompts. + +## Run in local container + +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. Get Anthropic API Key. + + ```bash + # bash/zsh + API_KEY=$(dotnet user-secrets --project ./src/OpenChat.PlaygroundApp list --json | \ + sed -n '/^\/\//d; p' | jq -r '."Anthropic:ApiKey"') + ``` + + ```bash + # PowerShell + $API_KEY = (dotnet user-secrets --project ./src/OpenChat.PlaygroundApp list --json | ` + Select-String -NotMatch '^//(BEGIN|END)' | ConvertFrom-Json).'Anthropic:ApiKey' + ``` + +1. Run the app. The default model OCP uses is [Claude 3 Opus](https://www.anthropic.com/claude). + + ```bash + # bash/zsh - from locally built container + docker run -i --rm -p 8080:8080 openchat-playground:latest --connector-type Anthropic \ + --api-key $API_KEY + ``` + + ```powershell + # PowerShell - from locally built container + docker run -i --rm -p 8080:8080 openchat-playground:latest --connector-type Anthropic ` + --api-key $API_KEY + ``` + + ```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 Anthropic \ + --api-key $API_KEY + ``` + + ```powershell + # PowerShell - from GitHub Container Registry + docker run -i --rm -p 8080:8080 ghcr.io/aliencube/open-chat-playground/openchat-playground:latest ` + --connector-type Anthropic ` + --api-key $API_KEY + ``` + + Alternatively, if you want to run with a different model, say [Claude 3 Sonnet](https://www.anthropic.com/claude), other than the default one, you can specify it as an argument: + + ```bash + # bash/zsh - from locally built container with custom model + docker run -i --rm -p 8080:8080 openchat-playground:latest --connector-type Anthropic \ + --api-key $API_KEY \ + --model claude-3-sonnet-20240229 + ``` + + ```powershell + # PowerShell - from locally built container with custom model + docker run -i --rm -p 8080:8080 openchat-playground:latest --connector-type Anthropic ` + --api-key $API_KEY ` + --model claude-3-sonnet-20240229 + ``` + +1. Open your web browser, navigate to `http://localhost:8080`, and enter prompts. + +## Run on Azure + +1. Make sure you are at the repository root. + + ```bash + cd $REPOSITORY_ROOT + ``` + +1. Login to Azure. + + ```bash + azd auth login + ``` + +1. Check login status. + + ```bash + azd auth login --check-status + ``` + +1. Initialize `azd` template. + + ```bash + azd init + ``` + + > **NOTE**: You will be asked to provide environment name for provisioning. + +1. Get Anthropic API Key. + + ```bash + # bash/zsh + API_KEY=$(dotnet user-secrets --project ./src/OpenChat.PlaygroundApp list --json | \ + sed -n '/^\/\//d; p' | jq -r '."Anthropic:ApiKey"') + ``` + + ```bash + # PowerShell + $API_KEY = (dotnet user-secrets --project ./src/OpenChat.PlaygroundApp list --json | ` + Select-String -NotMatch '^//(BEGIN|END)' | ConvertFrom-Json).'Anthropic:ApiKey' + ``` + +1. Set Anthropic API Key to azd environment variables. + + ```bash + azd env set ANTHROPIC_API_KEY $API_KEY + ``` + + The default model OCP uses is [Claude 3 Opus](https://www.anthropic.com/claude). If you want to run with a different model, say [Claude 3 Sonnet](https://www.anthropic.com/claude), other than the default one, add it to azd environment variables. + + ```bash + azd env set ANTHROPIC_MODEL claude-3-sonnet-20240229 + ``` + +1. Set the connector type to `Anthropic`. + + ```bash + azd env set CONNECTOR_TYPE Anthropic + ``` + +1. Run the following commands in order to provision and deploy the app. + + ```bash + azd up + ``` + + > **NOTE**: You will be asked to provide Azure subscription and location for deployment. + + Once deployed, you will be able to see the deployed OCP app URL. + +1. Open your web browser, navigate to the OCP app URL, and enter prompts. + +1. Clean up all the resources. + + ```bash + azd down --force --purge + ``` From e06184b79730beb50449698f74409eea2a7e9a59 Mon Sep 17 00:00:00 2001 From: MWK Date: Tue, 14 Oct 2025 00:56:16 +0900 Subject: [PATCH 05/18] feat: Update default model references to Claude Sonnet 4 and adjust alternative model options --- docs/anthropic.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/anthropic.md b/docs/anthropic.md index 3eacc2cf..c829ed2e 100644 --- a/docs/anthropic.md +++ b/docs/anthropic.md @@ -40,7 +40,7 @@ This page describes how to run OpenChat Playground (OCP) with [Anthropic Claude] > For more details about Anthropic API keys, refer to the doc, [Anthropic API Documentation](https://docs.anthropic.com/claude/reference/getting-started-with-the-api). -1. Run the app. The default model OCP uses is [Claude 3 Opus](https://www.anthropic.com/claude). +1. Run the app. The default model OCP uses is [Claude Sonnet 4](https://www-cdn.anthropic.com/6be99a52cb68eb70eb9572b4cafad13df32ed995.pdf). ```bash # bash/zsh @@ -54,20 +54,20 @@ This page describes how to run OpenChat Playground (OCP) with [Anthropic Claude] --connector-type Anthropic ``` - Alternatively, if you want to run with a different model, say [Claude 3 Sonnet](https://www.anthropic.com/claude), other than the default one, you can specify it as an argument: + Alternatively, if you want to run with a different model, say [Claude Opus 4.1](http://www.anthropic.com/claude-opus-4-1-system-card), other than the default one, you can specify it as an argument: ```bash # bash/zsh dotnet run --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp -- \ --connector-type Anthropic \ - --model claude-3-sonnet-20240229 + --model claude-opus-4-1 ``` ```powershell # PowerShell dotnet run --project $REPOSITORY_ROOT/src/OpenChat.PlaygroundApp -- ` --connector-type Anthropic ` - --model claude-3-sonnet-20240229 + --model claude-opus-4-1 ``` 1. Open your web browser, navigate to `http://localhost:5280`, and enter prompts. @@ -100,7 +100,7 @@ This page describes how to run OpenChat Playground (OCP) with [Anthropic Claude] Select-String -NotMatch '^//(BEGIN|END)' | ConvertFrom-Json).'Anthropic:ApiKey' ``` -1. Run the app. The default model OCP uses is [Claude 3 Opus](https://www.anthropic.com/claude). +1. Run the app. The default model OCP uses is [Claude Sonnet 4](https://www-cdn.anthropic.com/6be99a52cb68eb70eb9572b4cafad13df32ed995.pdf). ```bash # bash/zsh - from locally built container @@ -128,20 +128,20 @@ This page describes how to run OpenChat Playground (OCP) with [Anthropic Claude] --api-key $API_KEY ``` - Alternatively, if you want to run with a different model, say [Claude 3 Sonnet](https://www.anthropic.com/claude), other than the default one, you can specify it as an argument: + Alternatively, if you want to run with a different model, say [Claude Opus 4.1](http://www.anthropic.com/claude-opus-4-1-system-card), other than the default one, you can specify it as an argument: ```bash # bash/zsh - from locally built container with custom model docker run -i --rm -p 8080:8080 openchat-playground:latest --connector-type Anthropic \ --api-key $API_KEY \ - --model claude-3-sonnet-20240229 + --model claude-opus-4-1 ``` ```powershell # PowerShell - from locally built container with custom model docker run -i --rm -p 8080:8080 openchat-playground:latest --connector-type Anthropic ` --api-key $API_KEY ` - --model claude-3-sonnet-20240229 + --model claude-opus-4-1 ``` 1. Open your web browser, navigate to `http://localhost:8080`, and enter prompts. @@ -194,10 +194,10 @@ This page describes how to run OpenChat Playground (OCP) with [Anthropic Claude] azd env set ANTHROPIC_API_KEY $API_KEY ``` - The default model OCP uses is [Claude 3 Opus](https://www.anthropic.com/claude). If you want to run with a different model, say [Claude 3 Sonnet](https://www.anthropic.com/claude), other than the default one, add it to azd environment variables. + The default model OCP uses is [Claude Sonnet 4](https://www-cdn.anthropic.com/6be99a52cb68eb70eb9572b4cafad13df32ed995.pdf). If you want to run with a different model, say [Claude Opus 4.1](http://www.anthropic.com/claude-opus-4-1-system-card), other than the default one, add it to azd environment variables. ```bash - azd env set ANTHROPIC_MODEL claude-3-sonnet-20240229 + azd env set ANTHROPIC_MODEL claude-opus-4-1 ``` 1. Set the connector type to `Anthropic`. From e3dba70c33fa5fc5bb66ae0779c5338e0a0086e5 Mon Sep 17 00:00:00 2001 From: MWK Date: Tue, 14 Oct 2025 01:04:44 +0900 Subject: [PATCH 06/18] feat: Add support for Anthropic connector in LanguageModelConnector --- .../Abstractions/LanguageModelConnector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs b/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs index f2857bac..7e6c50e6 100644 --- a/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs +++ b/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs @@ -39,6 +39,7 @@ public static async Task CreateChatClientAsync(AppSettings settings ConnectorType.AzureAIFoundry => new AzureAIFoundryConnector(settings), ConnectorType.GitHubModels => new GitHubModelsConnector(settings), ConnectorType.HuggingFace => new HuggingFaceConnector(settings), + ConnectorType.Anthropic => new AnthropicConnector(settings), ConnectorType.LG => new LGConnector(settings), ConnectorType.OpenAI => new OpenAIConnector(settings), _ => throw new NotSupportedException($"Connector type '{settings.ConnectorType}' is not supported.") From 15881b4fc9f9ae5bdd8c68e26d82bda8985233d3 Mon Sep 17 00:00:00 2001 From: MWK Date: Tue, 14 Oct 2025 17:10:16 +0900 Subject: [PATCH 07/18] feat: Implement Anthropic connector for LanguageModel integration --- .../Connectors/AnthropicConnector.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs diff --git a/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs b/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs new file mode 100644 index 00000000..ec7915f4 --- /dev/null +++ b/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.AI; + +using Anthropic.SDK; + +using OpenChat.PlaygroundApp.Abstractions; +using OpenChat.PlaygroundApp.Configurations; + +namespace OpenChat.PlaygroundApp.Connectors; + +/// +/// This represents the connector entity for Anthropic. +/// +public class AnthropicConnector(AppSettings settings) : LanguageModelConnector(settings.Anthropic) +{ + private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings)); + + /// + public override bool EnsureLanguageModelSettingsValid() + { + if (this.Settings is not AnthropicSettings settings) + { + throw new InvalidOperationException("Missing configuration: Anthropic."); + } + + if (string.IsNullOrWhiteSpace(settings.ApiKey!.Trim()) == true) + { + throw new InvalidOperationException("Missing configuration: Anthropic:ApiKey."); + } + + if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true) + { + throw new InvalidOperationException("Missing configuration: Anthropic:Model."); + } + + return true; + } + + /// + public override async Task GetChatClientAsync() + { + var settings = this.Settings as AnthropicSettings; + + var apiKey = settings?.ApiKey ?? throw new InvalidOperationException("Missing configuration: Anthropic:ApiKey."); + + var client = new AnthropicClient(apiKey); + var chatClient = client.Messages; + + Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}"); + + return await Task.FromResult(chatClient).ConfigureAwait(false); + } +} \ No newline at end of file From 07086857fc6a897c3b15fb7fe90f300aa357f5af Mon Sep 17 00:00:00 2001 From: MWK Date: Tue, 14 Oct 2025 18:50:01 +0900 Subject: [PATCH 08/18] feat: Enhance AnthropicConnector with improved error handling and add unit tests --- .../Connectors/AnthropicConnector.cs | 8 +- .../Connectors/AnthropicConnectorTests.cs | 220 ++++++++++++++++++ 2 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 test/OpenChat.PlaygroundApp.Tests/Connectors/AnthropicConnectorTests.cs diff --git a/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs b/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs index ec7915f4..a5a530fa 100644 --- a/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs +++ b/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs @@ -40,12 +40,16 @@ public override async Task GetChatClientAsync() { var settings = this.Settings as AnthropicSettings; - var apiKey = settings?.ApiKey ?? throw new InvalidOperationException("Missing configuration: Anthropic:ApiKey."); + var apiKey = settings?.ApiKey; + if (string.IsNullOrWhiteSpace(apiKey) == true) + { + throw new InvalidOperationException("Missing configuration: Anthropic:ApiKey."); + } var client = new AnthropicClient(apiKey); var chatClient = client.Messages; - Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}"); + Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings!.Model}"); return await Task.FromResult(chatClient).ConfigureAwait(false); } diff --git a/test/OpenChat.PlaygroundApp.Tests/Connectors/AnthropicConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Connectors/AnthropicConnectorTests.cs new file mode 100644 index 00000000..defc4492 --- /dev/null +++ b/test/OpenChat.PlaygroundApp.Tests/Connectors/AnthropicConnectorTests.cs @@ -0,0 +1,220 @@ +using Microsoft.Extensions.AI; + +using OpenChat.PlaygroundApp.Abstractions; +using OpenChat.PlaygroundApp.Configurations; +using OpenChat.PlaygroundApp.Connectors; + +namespace OpenChat.PlaygroundApp.Tests.Connectors; + +public class AnthropicConnectorTests +{ + private const string ApiKey = "test-api-key"; + private const string Model = "test-model"; + + private static AppSettings BuildAppSettings(string? apiKey = ApiKey, string? model = Model) + { + return new AppSettings + { + ConnectorType = ConnectorType.Anthropic, + Anthropic = new AnthropicSettings + { + ApiKey = apiKey, + Model = model + } + }; + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Null_Settings_When_Instantiated_Then_It_Should_Throw() + { + // Act + Action action = () => new AnthropicConnector(null!); + + // Assert + action.ShouldThrow() + .Message.ShouldContain("settings"); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Settings_When_Instantiated_Then_It_Should_Return() + { + // Arrange + var settings = BuildAppSettings(); + + // Act + var result = new AnthropicConnector(settings); + + // Assert + result.ShouldNotBeNull(); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(typeof(LanguageModelConnector), typeof(AnthropicConnector), true)] + [InlineData(typeof(AnthropicConnector), typeof(LanguageModelConnector), false)] + public void Given_BaseType_Then_It_Should_Be_AssignableFrom_DerivedType(Type baseType, Type derivedType, bool expected) + { + // Act + var result = baseType.IsAssignableFrom(derivedType); + + // Assert + result.ShouldBe(expected); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw() + { + // Arrange + var settings = new AppSettings { ConnectorType = ConnectorType.Anthropic, Anthropic = null }; + var connector = new AnthropicConnector(settings); + + // Act + Action action = () => connector.EnsureLanguageModelSettingsValid(); + + // Assert + action.ShouldThrow() + .Message.ShouldContain("Missing configuration: Anthropic."); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] + [InlineData("", typeof(InvalidOperationException), "Anthropic:ApiKey")] + [InlineData(" ", typeof(InvalidOperationException), "Anthropic:ApiKey")] + [InlineData("\t\n\r", typeof(InvalidOperationException), "Anthropic:ApiKey")] + public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? apiKey, Type expectedException, string expectedMessage) + { + // Arrange + var settings = BuildAppSettings(apiKey: apiKey); + var connector = new AnthropicConnector(settings); + + // Act + Action action = () => connector.EnsureLanguageModelSettingsValid(); + + // Assert + action.ShouldThrow(expectedException) + .Message.ShouldContain(expectedMessage); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] + [InlineData("", typeof(InvalidOperationException), "Anthropic:Model")] + [InlineData(" ", typeof(InvalidOperationException), "Anthropic:Model")] + [InlineData("\t\n\r", typeof(InvalidOperationException), "Anthropic:Model")] + public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedException, string expectedMessage) + { + // Arrange + var settings = BuildAppSettings(apiKey: "valid-key", model: model); + var connector = new AnthropicConnector(settings); + + // Act + Action action = () => connector.EnsureLanguageModelSettingsValid(); + + // Assert + action.ShouldThrow(expectedException) + .Message.ShouldContain(expectedMessage); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Valid_Settings_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Return_True() + { + // Arrange + var settings = BuildAppSettings(); + var connector = new AnthropicConnector(settings); + + // Act + var result = connector.EnsureLanguageModelSettingsValid(); + + // Assert + result.ShouldBeTrue(); + } + + [Trait("Category", "UnitTest")] + [Fact] + public async Task Given_Valid_Settings_When_GetChatClientAsync_Invoked_Then_It_Should_Return_ChatClient() + { + // Arrange + var settings = BuildAppSettings(); + var connector = new AnthropicConnector(settings); + + // Act + var client = await connector.GetChatClientAsync(); + + // Assert + client.ShouldNotBeNull(); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Settings_Is_Null_When_GetChatClientAsync_Invoked_Then_It_Should_Throw() + { + // Arrange + var settings = new AppSettings { ConnectorType = ConnectorType.Anthropic, Anthropic = null }; + var connector = new AnthropicConnector(settings); + + // Act + Func func = async () => await connector.GetChatClientAsync(); + + // Assert + func.ShouldThrow() + .Message.ShouldContain("Missing configuration: Anthropic:ApiKey."); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, typeof(InvalidOperationException), "Anthropic:ApiKey")] + [InlineData("", typeof(InvalidOperationException), "Anthropic:ApiKey")] + public void Given_Missing_ApiKey_When_GetChatClientAsync_Invoked_Then_It_Should_Throw(string? apiKey, Type expectedException, string expectedMessage) + { + // Arrange + var settings = BuildAppSettings(apiKey: apiKey); + var connector = new AnthropicConnector(settings); + + // Act + Func func = async () => await connector.GetChatClientAsync(); + + // Assert + func.ShouldThrow(expectedException) + .Message.ShouldContain(expectedMessage); + } + + [Trait("Category", "UnitTest")] + [Fact] + public async Task Given_Valid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Return_ChatClient() + { + // Arrange + var settings = BuildAppSettings(); + + // Act + var result = await LanguageModelConnector.CreateChatClientAsync(settings); + + // Assert + result.ShouldNotBeNull(); + result.ShouldBeAssignableTo(); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData(null, "claude-sonnet-4-0", typeof(NullReferenceException))] + [InlineData("", "claude-sonnet-4-0", typeof(InvalidOperationException))] + [InlineData(" ", "claude-sonnet-4-0", typeof(InvalidOperationException))] + [InlineData("test-api-key", null, typeof(NullReferenceException))] + [InlineData("test-api-key", "", typeof(InvalidOperationException))] + [InlineData("test-api-key", " ", typeof(InvalidOperationException))] + public void Given_Invalid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Throw(string? apiKey, string? model, Type expectedType) + { + // Arrange + var settings = BuildAppSettings(apiKey: apiKey, model: model); + + // Act + Func func = async () => await LanguageModelConnector.CreateChatClientAsync(settings); + + // Assert + func.ShouldThrow(expectedType); + } +} \ No newline at end of file From 65b2d5fd295997296281250b5bfa9e7af0231752 Mon Sep 17 00:00:00 2001 From: MINWOO KIM Date: Tue, 14 Oct 2025 18:56:13 +0900 Subject: [PATCH 09/18] Adds documentation for Anthropic connector Adds links to the documentation for the Anthropic connector in the README, covering local machine, local container, and Azure deployments. Addresses the need to provide clear instructions for utilizing the new Anthropic connector. Related to #261 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ac325a34..c08a6f80 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,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-on-local-machine) - [Use GitHub Models](./docs/github-models.md#run-on-local-machine) - [Use Hugging Face](./docs/hugging-face.md#run-on-local-machine) +- [Use Anthropic](./docs/anthropic.md#run-on-local-machine) - [Use LG](./docs/lg.md#run-on-local-machine) - [Use OpenAI](./docs/openai.md#run-on-local-machine) @@ -71,6 +72,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 Hugging Face](./docs/hugging-face.md#run-in-local-container) +- [Use Anthropic](./docs/anthropic.md#run-in-local-container) - [Use LG](./docs/lg.md#run-in-local-container) - [Use OpenAI](./docs/openai.md#run-in-local-container) @@ -79,6 +81,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-on-azure) - [Use GitHub Models](./docs/github-models.md#run-on-azure) - [Use Hugging Face](./docs/hugging-face.md#run-on-azure) +- [Use Anthropic](./docs/anthropic.md#run-on-azure) - [Use LG](./docs/lg.md#run-on-azure) - [Use OpenAI](./docs/openai.md#run-on-azure) From c625f1171cd5c56d2ac79b2fb7307fc7f31c2360 Mon Sep 17 00:00:00 2001 From: MINWOO KIM Date: Tue, 14 Oct 2025 19:01:55 +0900 Subject: [PATCH 10/18] Updates documentation to refer to Anthropic models Updates the documentation to refer to Anthropic models instead of the specific Claude model, reflecting the broader range of models now supported. Relates to #261 --- docs/anthropic.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/anthropic.md b/docs/anthropic.md index c829ed2e..6c1fc548 100644 --- a/docs/anthropic.md +++ b/docs/anthropic.md @@ -1,6 +1,6 @@ -# OpenChat Playground with Anthropic Claude +# OpenChat Playground with Anthropic -This page describes how to run OpenChat Playground (OCP) with [Anthropic Claude](https://www.anthropic.com/claude) integration. +This page describes how to run OpenChat Playground (OCP) with [Anthropic models](https://docs.claude.com/en/docs/about-claude/models) integration. ## Get the repository root From 9bc001b306149ca3c72bebee9884b9ee900cab73 Mon Sep 17 00:00:00 2001 From: MWK Date: Tue, 14 Oct 2025 22:43:32 +0900 Subject: [PATCH 11/18] feat: Refactor GetChatClientAsync to improve API client initialization and ensure proper function invocation --- .../Connectors/AnthropicConnector.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs b/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs index a5a530fa..7166ae20 100644 --- a/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs +++ b/src/OpenChat.PlaygroundApp/Connectors/AnthropicConnector.cs @@ -39,15 +39,25 @@ public override bool EnsureLanguageModelSettingsValid() public override async Task GetChatClientAsync() { var settings = this.Settings as AnthropicSettings; - var apiKey = settings?.ApiKey; + if (string.IsNullOrWhiteSpace(apiKey) == true) { throw new InvalidOperationException("Missing configuration: Anthropic:ApiKey."); } - var client = new AnthropicClient(apiKey); - var chatClient = client.Messages; + var client = new AnthropicClient() { Auth = new APIAuthentication(apiKey) }; + + var chatClient = client.Messages + .AsBuilder() + .UseFunctionInvocation() + .Use((messages, options, next, cancellationToken) => + { + options!.ModelId = settings!.Model; + options.MaxOutputTokens ??= 1000; + return next(messages, options, cancellationToken); + }) + .Build(); Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings!.Model}"); From d23c92bff652f1350f03128f0b873d66f3c93924 Mon Sep 17 00:00:00 2001 From: MINWOO KIM Date: Fri, 17 Oct 2025 02:29:51 +0900 Subject: [PATCH 12/18] feat: add max_tokens parameter for Anthropic in appsettings.json Related to : #342 * Add `MaxTokens` parameter to the Anthropic section in appsettings.json (Microsoft.Extensions.Configuration.Binder automatically converts string values to integers when binding configuration) --- src/OpenChat.PlaygroundApp/appsettings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OpenChat.PlaygroundApp/appsettings.json b/src/OpenChat.PlaygroundApp/appsettings.json index 1ee914c5..c971cf06 100644 --- a/src/OpenChat.PlaygroundApp/appsettings.json +++ b/src/OpenChat.PlaygroundApp/appsettings.json @@ -56,7 +56,8 @@ "Anthropic": { "ApiKey": "{{ANTHROPIC_API_KEY}}", - "Model": "claude-sonnet-4-0" + "Model": "claude-sonnet-4-0", + "MaxTokens": "1000" }, "LG": { From 92c3eed0afb10870d88c19b9346c09c10e83d16e Mon Sep 17 00:00:00 2001 From: MINWOO KIM Date: Fri, 17 Oct 2025 02:39:37 +0900 Subject: [PATCH 13/18] feat: add property MaxTokens for Anthropic in AnthropicSettings Relate to : #258 * Add`MaxTokens` property to AnthropicSettings --- .../Configurations/AnthropicSettings.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs b/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs index 5e8ba75f..785a6286 100644 --- a/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs +++ b/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs @@ -17,7 +17,7 @@ public partial class AppSettings public class AnthropicSettings : LanguageModelSettings { /// - /// Gets or sets the API key for Anthropic Claude. + /// Gets or sets the API key for Anthropic. /// public string? ApiKey { get; set; } @@ -25,4 +25,9 @@ public class AnthropicSettings : LanguageModelSettings /// Gets or sets the model name of Anthropic Claude. /// public string? Model { get; set; } + + /// + /// Gets or sets the maximum number of output tokens for Anthropic Claude. + /// + public int? MaxTokens { get; set; } } \ No newline at end of file From 1a58fb78795b5ba5f69f99d183652053c7c5ccc7 Mon Sep 17 00:00:00 2001 From: MWK Date: Fri, 17 Oct 2025 02:57:57 +0900 Subject: [PATCH 14/18] fix: temporarily disable Anthropic * update LanguageModelConnector to include Anthropic in unsupported list * will be re-enabled when Anthropic PR is merged --- .../Abstractions/LanguageModelConnector.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs b/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs index a030d213..006b41cb 100644 --- a/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs +++ b/src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs @@ -39,7 +39,6 @@ public static async Task CreateChatClientAsync(AppSettings settings ConnectorType.AzureAIFoundry => new AzureAIFoundryConnector(settings), ConnectorType.GitHubModels => new GitHubModelsConnector(settings), ConnectorType.HuggingFace => new HuggingFaceConnector(settings), - ConnectorType.Anthropic => new AnthropicConnector(settings), ConnectorType.LG => new LGConnector(settings), ConnectorType.OpenAI => new OpenAIConnector(settings), ConnectorType.Upstage => new UpstageConnector(settings), From 9c51809ca3b3b7470b2445ec5110b835ef001c4b Mon Sep 17 00:00:00 2001 From: MWK Date: Fri, 17 Oct 2025 03:06:32 +0900 Subject: [PATCH 15/18] test: skip all Anthropic connector tests until enabled * mark all AnthropicConnectorTests as skipped to avoid CI failures while connector is unsupported --- .../Connectors/AnthropicConnectorTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/OpenChat.PlaygroundApp.Tests/Connectors/AnthropicConnectorTests.cs b/test/OpenChat.PlaygroundApp.Tests/Connectors/AnthropicConnectorTests.cs index defc4492..a8848c5f 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Connectors/AnthropicConnectorTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Connectors/AnthropicConnectorTests.cs @@ -25,7 +25,7 @@ private static AppSettings BuildAppSettings(string? apiKey = ApiKey, string? mod } [Trait("Category", "UnitTest")] - [Fact] + [Fact(Skip = "Anthropic connector is not enabled yet.")] public void Given_Null_Settings_When_Instantiated_Then_It_Should_Throw() { // Act @@ -37,7 +37,7 @@ public void Given_Null_Settings_When_Instantiated_Then_It_Should_Throw() } [Trait("Category", "UnitTest")] - [Fact] + [Fact(Skip = "Anthropic connector is not enabled yet.")] public void Given_Settings_When_Instantiated_Then_It_Should_Return() { // Arrange @@ -51,7 +51,7 @@ public void Given_Settings_When_Instantiated_Then_It_Should_Return() } [Trait("Category", "UnitTest")] - [Theory] + [Theory(Skip = "Anthropic connector is not enabled yet.")] [InlineData(typeof(LanguageModelConnector), typeof(AnthropicConnector), true)] [InlineData(typeof(AnthropicConnector), typeof(LanguageModelConnector), false)] public void Given_BaseType_Then_It_Should_Be_AssignableFrom_DerivedType(Type baseType, Type derivedType, bool expected) @@ -64,7 +64,7 @@ public void Given_BaseType_Then_It_Should_Be_AssignableFrom_DerivedType(Type bas } [Trait("Category", "UnitTest")] - [Fact] + [Fact(Skip = "Anthropic connector is not enabled yet.")] public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw() { // Arrange @@ -80,7 +80,7 @@ public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked } [Trait("Category", "UnitTest")] - [Theory] + [Theory(Skip = "Anthropic connector is not enabled yet.")] [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] [InlineData("", typeof(InvalidOperationException), "Anthropic:ApiKey")] [InlineData(" ", typeof(InvalidOperationException), "Anthropic:ApiKey")] @@ -100,7 +100,7 @@ public void Given_Invalid_ApiKey_When_EnsureLanguageModelSettingsValid_Invoked_T } [Trait("Category", "UnitTest")] - [Theory] + [Theory(Skip = "Anthropic connector is not enabled yet.")] [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] [InlineData("", typeof(InvalidOperationException), "Anthropic:Model")] [InlineData(" ", typeof(InvalidOperationException), "Anthropic:Model")] @@ -120,7 +120,7 @@ public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Th } [Trait("Category", "UnitTest")] - [Fact] + [Fact(Skip = "Anthropic connector is not enabled yet.")] public void Given_Valid_Settings_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Return_True() { // Arrange @@ -135,7 +135,7 @@ public void Given_Valid_Settings_When_EnsureLanguageModelSettingsValid_Invoked_T } [Trait("Category", "UnitTest")] - [Fact] + [Fact(Skip = "Anthropic connector is not enabled yet.")] public async Task Given_Valid_Settings_When_GetChatClientAsync_Invoked_Then_It_Should_Return_ChatClient() { // Arrange @@ -150,7 +150,7 @@ public async Task Given_Valid_Settings_When_GetChatClientAsync_Invoked_Then_It_S } [Trait("Category", "UnitTest")] - [Fact] + [Fact(Skip = "Anthropic connector is not enabled yet.")] public void Given_Settings_Is_Null_When_GetChatClientAsync_Invoked_Then_It_Should_Throw() { // Arrange @@ -166,7 +166,7 @@ public void Given_Settings_Is_Null_When_GetChatClientAsync_Invoked_Then_It_Shoul } [Trait("Category", "UnitTest")] - [Theory] + [Theory(Skip = "Anthropic connector is not enabled yet.")] [InlineData(null, typeof(InvalidOperationException), "Anthropic:ApiKey")] [InlineData("", typeof(InvalidOperationException), "Anthropic:ApiKey")] public void Given_Missing_ApiKey_When_GetChatClientAsync_Invoked_Then_It_Should_Throw(string? apiKey, Type expectedException, string expectedMessage) @@ -184,7 +184,7 @@ public void Given_Missing_ApiKey_When_GetChatClientAsync_Invoked_Then_It_Should_ } [Trait("Category", "UnitTest")] - [Fact] + [Fact(Skip = "Anthropic connector is not enabled in the factory yet.")] public async Task Given_Valid_Settings_When_CreateChatClientAsync_Invoked_Then_It_Should_Return_ChatClient() { // Arrange @@ -199,7 +199,7 @@ public async Task Given_Valid_Settings_When_CreateChatClientAsync_Invoked_Then_I } [Trait("Category", "UnitTest")] - [Theory] + [Theory(Skip = "Anthropic connector is not enabled in the factory yet.")] [InlineData(null, "claude-sonnet-4-0", typeof(NullReferenceException))] [InlineData("", "claude-sonnet-4-0", typeof(InvalidOperationException))] [InlineData(" ", "claude-sonnet-4-0", typeof(InvalidOperationException))] From 06efab111847837bcdc35d115b1225bb7b1f0426 Mon Sep 17 00:00:00 2001 From: MWK Date: Sun, 19 Oct 2025 02:02:28 +0900 Subject: [PATCH 16/18] refactor: update documentation to remove "Claude" references from Anthropic settings --- .../Configurations/AnthropicSettings.cs | 6 +++--- .../Options/AnthropicArgumentOptions.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs b/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs index 785a6286..fa780396 100644 --- a/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs +++ b/src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs @@ -12,7 +12,7 @@ public partial class AppSettings } /// -/// This represents the app settings entity for Anthropic Claude. +/// This represents the app settings entity for Anthropic. /// public class AnthropicSettings : LanguageModelSettings { @@ -22,12 +22,12 @@ public class AnthropicSettings : LanguageModelSettings public string? ApiKey { get; set; } /// - /// Gets or sets the model name of Anthropic Claude. + /// Gets or sets the model name of Anthropic. /// public string? Model { get; set; } /// - /// Gets or sets the maximum number of output tokens for Anthropic Claude. + /// Gets or sets the maximum number of output tokens for Anthropic. /// public int? MaxTokens { get; set; } } \ No newline at end of file diff --git a/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs b/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs index d3a17e59..4761cb19 100644 --- a/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs +++ b/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs @@ -5,17 +5,17 @@ namespace OpenChat.PlaygroundApp.Options; /// -/// This represents the argument options entity for Anthropic Claude. +/// This represents the argument options entity for Anthropic. /// public class AnthropicArgumentOptions : ArgumentOptions { /// - /// Gets or sets the API key for Anthropic Claude. + /// Gets or sets the API key for Anthropic. /// public string? ApiKey { get; set; } /// - /// Gets or sets the model name of Anthropic Claude. + /// Gets or sets the model name of Anthropic. /// public string? Model { get; set; } From 09ad176ff43a2b1a31cd12e49f15233e2cd7ad1a Mon Sep 17 00:00:00 2001 From: MINWOO KIM Date: Mon, 20 Oct 2025 22:51:38 +0900 Subject: [PATCH 17/18] feat: add option MaxTokens for parsing option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to : #259 * Add option ‘MaxTokens’ to AnthropicArgumentOptions and update parsing logic * Update ArgumentOptions.cs, AnthropicArgumentOptions.cs, AnthropicArgumentOptionsTests.cs --- .../Abstractions/ArgumentOptions.cs | 5 +- .../Options/AnthropicArgumentOptions.cs | 19 +- .../Options/AnthropicArgumentOptionsTests.cs | 236 ++++++++++++++---- 3 files changed, 207 insertions(+), 53 deletions(-) diff --git a/src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs b/src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs index 19eb9867..1fd66859 100644 --- a/src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs +++ b/src/OpenChat.PlaygroundApp/Abstractions/ArgumentOptions.cs @@ -42,6 +42,7 @@ private static readonly (ConnectorType ConnectorType, string Argument, bool IsSw // Anthropic (ConnectorType.Anthropic, ArgumentOptionConstants.Anthropic.ApiKey, false), (ConnectorType.Anthropic, ArgumentOptionConstants.Anthropic.Model, false), + (ConnectorType.Anthropic, ArgumentOptionConstants.Anthropic.MaxTokens, false), // LG (ConnectorType.LG, ArgumentOptionConstants.LG.BaseUrl, false), (ConnectorType.LG, ArgumentOptionConstants.LG.Model, false), @@ -237,6 +238,7 @@ public static AppSettings Parse(IConfiguration config, string[] args) settings.Anthropic ??= new AnthropicSettings(); settings.Anthropic.ApiKey = anthropic.ApiKey ?? settings.Anthropic.ApiKey; settings.Anthropic.Model = anthropic.Model ?? settings.Anthropic.Model; + settings.Anthropic.MaxTokens = anthropic.MaxTokens ?? settings.Anthropic.MaxTokens; settings.Model = anthropic.Model ?? settings.Anthropic.Model; break; @@ -440,7 +442,8 @@ private static void DisplayHelpForAnthropic() Console.ForegroundColor = foregroundColor; Console.WriteLine($" {ArgumentOptionConstants.Anthropic.ApiKey} The Anthropic API key."); - Console.WriteLine($" {ArgumentOptionConstants.Anthropic.Model} The Anthropic model name. Default to 'claude-sonnet-4-0'"); + Console.WriteLine($" {ArgumentOptionConstants.Anthropic.Model} The Anthropic model name. Default to 'claude-sonnet-4-0'"); + Console.WriteLine($" {ArgumentOptionConstants.Anthropic.MaxTokens} The maximum tokens (>= 1). Default to 1000"); Console.WriteLine(); } diff --git a/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs b/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs index 4761cb19..553b0407 100644 --- a/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs +++ b/src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs @@ -19,6 +19,12 @@ public class AnthropicArgumentOptions : ArgumentOptions /// public string? Model { get; set; } + /// + /// Gets or sets the maximum number of tokens for Anthropic. + /// Mirrors the 'max_tokens' option as 'MaxTokens'. + /// + public int? MaxTokens { get; set; } + /// protected override void ParseOptions(IConfiguration config, string[] args) { @@ -26,9 +32,10 @@ protected override void ParseOptions(IConfiguration config, string[] args) config.Bind(settings); var anthropic = settings.Anthropic; - + this.ApiKey ??= anthropic?.ApiKey; this.Model ??= anthropic?.Model; + this.MaxTokens ??= anthropic?.MaxTokens; for (var i = 0; i < args.Length; i++) { @@ -48,6 +55,16 @@ protected override void ParseOptions(IConfiguration config, string[] args) } break; + case ArgumentOptionConstants.Anthropic.MaxTokens: + if (i + 1 < args.Length) + { + if (int.TryParse(args[++i], out var maxTokens)) + { + this.MaxTokens = maxTokens; + } + } + break; + default: break; } diff --git a/test/OpenChat.PlaygroundApp.Tests/Options/AnthropicArgumentOptionsTests.cs b/test/OpenChat.PlaygroundApp.Tests/Options/AnthropicArgumentOptionsTests.cs index a0d8183f..918b6a6b 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Options/AnthropicArgumentOptionsTests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Options/AnthropicArgumentOptionsTests.cs @@ -11,14 +11,18 @@ public class AnthropicArgumentOptionsTests { private const string ApiKey = "test-api-key"; private const string Model = "test-model"; + private const string MaxTokens = "1000"; private const string ApiKeyConfigKey = "Anthropic:ApiKey"; private const string ModelConfigKey = "Anthropic:Model"; + private const string MaxTokensConfigKey = "Anthropic:MaxTokens"; private static IConfiguration BuildConfigWithAnthropic( string? configApiKey = ApiKey, string? configModel = Model, + string? configMaxTokens = MaxTokens, string? envApiKey = null, - string? envModel = null) + string? envModel = null, + string? envMaxTokens = null) { // Base configuration values (lowest priority) var configDict = new Dictionary @@ -34,7 +38,13 @@ private static IConfiguration BuildConfigWithAnthropic( { configDict[ModelConfigKey] = configModel; } - if (string.IsNullOrWhiteSpace(envApiKey) == true && string.IsNullOrWhiteSpace(envModel) == true) + if (string.IsNullOrWhiteSpace(configMaxTokens) == false) + { + configDict[MaxTokensConfigKey] = configMaxTokens; + } + if (string.IsNullOrWhiteSpace(envApiKey) == true && + string.IsNullOrWhiteSpace(envModel) == true && + string.IsNullOrWhiteSpace(envMaxTokens) == true) { return new ConfigurationBuilder() .AddInMemoryCollection(configDict!) @@ -51,12 +61,18 @@ private static IConfiguration BuildConfigWithAnthropic( { envDict[ModelConfigKey] = envModel; } + if (string.IsNullOrWhiteSpace(envMaxTokens) == false) + { + envDict[MaxTokensConfigKey] = envMaxTokens; + } return new ConfigurationBuilder() .AddInMemoryCollection(configDict!) // Base configuration (lowest priority) .AddInMemoryCollection(envDict!) // Environment variables (medium priority) .Build(); } + + private static int? IntValueOf(string? value) => string.IsNullOrWhiteSpace(value) ? null : int.Parse(value); [Trait("Category", "UnitTest")] [Theory] @@ -87,6 +103,7 @@ public void Given_Nothing_When_Parse_Invoked_Then_It_Should_Set_Config() settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(ApiKey); settings.Anthropic.Model.ShouldBe(Model); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(MaxTokens)); } [Trait("Category", "UnitTest")] @@ -108,6 +125,7 @@ public void Given_CLI_ApiKey_When_Parse_Invoked_Then_It_Should_Use_CLI_ApiKey(st settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(cliApiKey); settings.Anthropic.Model.ShouldBe(Model); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(MaxTokens)); } [Trait("Category", "UnitTest")] @@ -129,19 +147,43 @@ public void Given_CLI_Model_When_Parse_Invoked_Then_It_Should_Use_CLI_Model(stri settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(ApiKey); settings.Anthropic.Model.ShouldBe(cliModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(MaxTokens)); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData("2000")] + public void Given_CLI_MaxTokens_When_Parse_Invoked_Then_It_Should_Use_CLI_MaxTokens(string cliMaxTokens) + { + // Arrange + var config = BuildConfigWithAnthropic(); + var args = new[] + { + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens + }; + + // Act + var settings = ArgumentOptions.Parse(config, args); + + // Assert + settings.Anthropic.ShouldNotBeNull(); + settings.Anthropic.ApiKey.ShouldBe(ApiKey); + settings.Anthropic.Model.ShouldBe(Model); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(cliMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("cli-api-key", "cli-model")] - public void Given_All_CLI_Arguments_When_Parse_Invoked_Then_It_Should_Use_CLI(string cliApiKey, string cliModel) + [InlineData("cli-api-key", "cli-model", "2000")] + public void Given_All_CLI_Arguments_When_Parse_Invoked_Then_It_Should_Use_CLI(string cliApiKey, string cliModel, string cliMaxTokens) { // Arrange var config = BuildConfigWithAnthropic(); var args = new[] { ArgumentOptionConstants.Anthropic.ApiKey, cliApiKey, - ArgumentOptionConstants.Anthropic.Model, cliModel + ArgumentOptionConstants.Anthropic.Model, cliModel, + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens }; // Act @@ -151,12 +193,14 @@ public void Given_All_CLI_Arguments_When_Parse_Invoked_Then_It_Should_Use_CLI(st settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(cliApiKey); settings.Anthropic.Model.ShouldBe(cliModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(cliMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] [InlineData(ArgumentOptionConstants.Anthropic.ApiKey)] [InlineData(ArgumentOptionConstants.Anthropic.Model)] + [InlineData(ArgumentOptionConstants.Anthropic.MaxTokens)] public void Given_CLI_ArgumentWithoutValue_When_Parse_Invoked_Then_It_Should_Use_Config(string argument) { // Arrange @@ -170,6 +214,7 @@ public void Given_CLI_ArgumentWithoutValue_When_Parse_Invoked_Then_It_Should_Use settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(ApiKey); settings.Anthropic.Model.ShouldBe(Model); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(MaxTokens)); } [Trait("Category", "UnitTest")] @@ -187,6 +232,7 @@ public void Given_Unrelated_CLI_Arguments_When_Parse_Invoked_Then_It_Should_Use_ settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(ApiKey); settings.Anthropic.Model.ShouldBe(Model); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(MaxTokens)); } [Trait("Category", "UnitTest")] @@ -208,6 +254,29 @@ public void Given_Anthropic_With_ModelName_StartingWith_Dashes_When_Parse_Invoke settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(ApiKey); settings.Anthropic.Model.ShouldBe(cliModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(MaxTokens)); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData("-1")] + public void Given_Anthropic_With_MaxTokens_StartingWith_Dashes_When_Parse_Invoked_Then_It_Should_Treat_As_Value(string cliMaxTokens) + { + // Arrange + var config = BuildConfigWithAnthropic(); + var args = new[] + { + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens + }; + + // Act + var settings = ArgumentOptions.Parse(config, args); + + // Assert + settings.Anthropic.ShouldNotBeNull(); + settings.Anthropic.ApiKey.ShouldBe(ApiKey); + settings.Anthropic.Model.ShouldBe(Model); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(cliMaxTokens)); } [Trait("Category", "UnitTest")] @@ -229,15 +298,16 @@ public void Given_Anthropic_With_ApiKey_StartingWith_Dashes_When_Parse_Invoked_T settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(cliApiKey); settings.Anthropic.Model.ShouldBe(Model); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(MaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("config-api-key", "config-model")] - public void Given_ConfigValues_And_No_CLI_When_Parse_Invoked_Then_It_Should_Use_Config(string configApiKey, string configModel) + [InlineData("config-api-key", "config-model", "1000")] + public void Given_ConfigValues_And_No_CLI_When_Parse_Invoked_Then_It_Should_Use_Config(string configApiKey, string configModel, string configMaxTokens) { // Arrange - var config = BuildConfigWithAnthropic(configApiKey, configModel); + var config = BuildConfigWithAnthropic(configApiKey, configModel, configMaxTokens); var args = Array.Empty(); // Act @@ -247,21 +317,23 @@ public void Given_ConfigValues_And_No_CLI_When_Parse_Invoked_Then_It_Should_Use_ settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(configApiKey); settings.Anthropic.Model.ShouldBe(configModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(configMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("config-api-key", "config-model", "cli-api-key", "cli-model")] + [InlineData("config-api-key", "config-model", "1000", "cli-api-key", "cli-model", "2000")] public void Given_ConfigValues_And_CLI_When_Parse_Invoked_Then_It_Should_Use_CLI( - string configApiKey, string configModel, - string cliApiKey, string cliModel) + string configApiKey, string configModel, string configMaxTokens, + string cliApiKey, string cliModel, string cliMaxTokens) { // Arrange - var config = BuildConfigWithAnthropic(configApiKey, configModel); + var config = BuildConfigWithAnthropic(configApiKey, configModel, configMaxTokens); var args = new[] { ArgumentOptionConstants.Anthropic.ApiKey, cliApiKey, - ArgumentOptionConstants.Anthropic.Model, cliModel + ArgumentOptionConstants.Anthropic.Model, cliModel, + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens }; // Act @@ -271,17 +343,18 @@ public void Given_ConfigValues_And_CLI_When_Parse_Invoked_Then_It_Should_Use_CLI settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(cliApiKey); settings.Anthropic.Model.ShouldBe(cliModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(cliMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("env-api-key", "env-model")] - public void Given_EnvironmentVariables_And_No_Config_When_Parse_Invoked_Then_It_Should_Use_EnvironmentVariables(string envApiKey, string envModel) + [InlineData("env-api-key", "env-model", "1500")] + public void Given_EnvironmentVariables_And_No_Config_When_Parse_Invoked_Then_It_Should_Use_EnvironmentVariables(string envApiKey, string envModel, string envMaxTokens) { // Arrange var config = BuildConfigWithAnthropic( - configApiKey: null, configModel: null, - envApiKey: envApiKey, envModel: envModel); + configApiKey: null, configModel: null, configMaxTokens: null, + envApiKey: envApiKey, envModel: envModel, envMaxTokens: envMaxTokens); var args = Array.Empty(); // Act @@ -291,17 +364,20 @@ public void Given_EnvironmentVariables_And_No_Config_When_Parse_Invoked_Then_It_ settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(envApiKey); settings.Anthropic.Model.ShouldBe(envModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(envMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("config-api-key", "config-model", "env-api-key", "env-model")] + [InlineData("config-api-key", "config-model", "1000", "env-api-key", "env-model", "1500")] public void Given_ConfigValues_And_EnvironmentVariables_When_Parse_Invoked_Then_It_Should_Use_EnvironmentVariables( - string configApiKey, string configModel, - string envApiKey, string envModel) + string configApiKey, string configModel, string configMaxTokens, + string envApiKey, string envModel, string envMaxTokens) { // Arrange - var config = BuildConfigWithAnthropic(configApiKey, configModel, envApiKey, envModel); + var config = BuildConfigWithAnthropic( + configApiKey, configModel, configMaxTokens, + envApiKey, envModel, envMaxTokens); var args = Array.Empty(); // Act @@ -311,22 +387,26 @@ public void Given_ConfigValues_And_EnvironmentVariables_When_Parse_Invoked_Then_ settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(envApiKey); settings.Anthropic.Model.ShouldBe(envModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(envMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("config-api-key", "config-model", "env-api-key", "env-model", "cli-api-key", "cli-model")] + [InlineData("config-api-key", "config-model", "1000", "env-api-key", "env-model", "1500", "cli-api-key", "cli-model", "2000")] public void Given_ConfigValues_And_EnvironmentVariables_And_CLI_When_Parse_Invoked_Then_It_Should_Use_CLI( - string configApiKey, string configModel, - string envApiKey, string envModel, - string cliApiKey, string cliModel) + string configApiKey, string configModel, string configMaxTokens, + string envApiKey, string envModel, string envMaxTokens, + string cliApiKey, string cliModel, string cliMaxTokens) { // Arrange - var config = BuildConfigWithAnthropic(configApiKey, configModel, envApiKey, envModel); + var config = BuildConfigWithAnthropic( + configApiKey, configModel, configMaxTokens, + envApiKey, envModel, envMaxTokens); var args = new[] { ArgumentOptionConstants.Anthropic.ApiKey, cliApiKey, - ArgumentOptionConstants.Anthropic.Model, cliModel + ArgumentOptionConstants.Anthropic.Model, cliModel, + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens }; // Act @@ -336,17 +416,20 @@ public void Given_ConfigValues_And_EnvironmentVariables_And_CLI_When_Parse_Invok settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(cliApiKey); settings.Anthropic.Model.ShouldBe(cliModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(cliMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("config-api-key", "config-model", null, "env-model")] + [InlineData("config-api-key", "config-model", "1000", null, "env-model", null)] public void Given_Partial_EnvironmentVariables_When_Parse_Invoked_Then_It_Should_Mix_Config_And_Environment( - string configApiKey, string configModel, - string? envApiKey, string envModel) + string configApiKey, string configModel, string configMaxTokens, + string? envApiKey, string envModel, string? envMaxTokens) { // Arrange - var config = BuildConfigWithAnthropic(configApiKey, configModel, envApiKey, envModel); + var config = BuildConfigWithAnthropic( + configApiKey, configModel, configMaxTokens, + envApiKey, envModel, envMaxTokens); var args = Array.Empty(); // Act @@ -356,22 +439,26 @@ public void Given_Partial_EnvironmentVariables_When_Parse_Invoked_Then_It_Should settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(configApiKey); settings.Anthropic.Model.ShouldBe(envModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(configMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("config-api-key", "config-model", "env-api-key", null, null, "cli-model")] + [InlineData("config-api-key", "config-model", "1000", "env-api-key", null, null, null, "cli-model", null)] public void Given_Mixed_Priority_Sources_When_Parse_Invoked_Then_It_Should_Respect_Priority_Order( - string configApiKey, string configModel, - string envApiKey, string? envModel, - string? cliApiKey, string cliModel) + string configApiKey, string configModel, string configMaxTokens, + string envApiKey, string? envModel, string? envMaxTokens, + string? cliApiKey, string cliModel, string? cliMaxTokens) { // Arrange - var config = BuildConfigWithAnthropic(configApiKey, configModel, envApiKey, envModel); + var config = BuildConfigWithAnthropic( + configApiKey, configModel, configMaxTokens, + envApiKey, envModel, envMaxTokens); var args = new[] { ArgumentOptionConstants.Anthropic.ApiKey, cliApiKey, - ArgumentOptionConstants.Anthropic.Model, cliModel + ArgumentOptionConstants.Anthropic.Model, cliModel, + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens }; // Act @@ -381,19 +468,21 @@ public void Given_Mixed_Priority_Sources_When_Parse_Invoked_Then_It_Should_Respe settings.Anthropic.ShouldNotBeNull(); settings.Anthropic.ApiKey.ShouldBe(envApiKey); settings.Anthropic.Model.ShouldBe(cliModel); + settings.Anthropic.MaxTokens.ShouldBe(IntValueOf(configMaxTokens)); } [Trait("Category", "UnitTest")] [Theory] - [InlineData("cli-api-key", "cli-model")] - public void Given_Anthropic_With_KnownArguments_When_Parse_Invoked_Then_Help_Should_Be_False(string cliApiKey, string cliModel) + [InlineData("cli-api-key", "cli-model", "2000")] + public void Given_Anthropic_With_KnownArguments_When_Parse_Invoked_Then_Help_Should_Be_False(string cliApiKey, string cliModel, string cliMaxTokens) { // Arrange - var config = BuildConfigWithAnthropic(ApiKey, Model); + var config = BuildConfigWithAnthropic(ApiKey, Model, MaxTokens); var args = new[] { ArgumentOptionConstants.Anthropic.ApiKey, cliApiKey, - ArgumentOptionConstants.Anthropic.Model, cliModel + ArgumentOptionConstants.Anthropic.Model, cliModel, + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens }; // Act @@ -407,6 +496,7 @@ public void Given_Anthropic_With_KnownArguments_When_Parse_Invoked_Then_Help_Sho [Theory] [InlineData(ArgumentOptionConstants.Anthropic.ApiKey)] [InlineData(ArgumentOptionConstants.Anthropic.Model)] + [InlineData(ArgumentOptionConstants.Anthropic.MaxTokens)] public void Given_Anthropic_With_KnownArgument_WithoutValue_When_Parse_Invoked_Then_Help_Should_Be_False(string argument) { // Arrange @@ -423,7 +513,7 @@ public void Given_Anthropic_With_KnownArgument_WithoutValue_When_Parse_Invoked_T [Trait("Category", "UnitTest")] [Theory] [InlineData("cli-api-key", "--unknown-flag")] - public void Given_Anthropic_With_Known_And_Unknown_Argument_When_Parse_Invoked_Then_Help_Should_Be_True( + public void Given_Anthropic_With_Known_ApiKey_And_Unknown_Argument_When_Parse_Invoked_Then_Help_Should_Be_True( string cliApiKey, string argument) { // Arrange @@ -443,15 +533,58 @@ public void Given_Anthropic_With_Known_And_Unknown_Argument_When_Parse_Invoked_T [Trait("Category", "UnitTest")] [Theory] - [InlineData("cli-api-key", "cli-model")] - public void Given_CLI_Only_When_Parse_Invoked_Then_Help_Should_Be_False(string cliApiKey, string cliModel) + [InlineData("cli-model", "--unknown-flag")] + public void Given_Anthropic_With_Known_Model_And_Unknown_Argument_When_Parse_Invoked_Then_Help_Should_Be_True( + string cliModel, string argument) + { + // Arrange + var config = BuildConfigWithAnthropic(); + var args = new[] + { + ArgumentOptionConstants.Anthropic.Model, cliModel, + argument + }; + + // Act + var settings = ArgumentOptions.Parse(config, args); + + // Assert + settings.Help.ShouldBeTrue(); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData("2000", "--unknown-flag")] + public void Given_Anthropic_With_Known_MaxTokens_And_Unknown_Argument_When_Parse_Invoked_Then_Help_Should_Be_True( + string cliMaxTokens, string argument) + { + // Arrange + var config = BuildConfigWithAnthropic(); + var args = new[] + { + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens, + argument + }; + + // Act + var settings = ArgumentOptions.Parse(config, args); + + // Assert + settings.Help.ShouldBeTrue(); + } + + [Trait("Category", "UnitTest")] + [Theory] + [InlineData("cli-api-key", "cli-model", "2000")] + public void Given_CLI_Only_When_Parse_Invoked_Then_Help_Should_Be_False(string cliApiKey, string cliModel, string cliMaxTokens) { // Arrange var config = BuildConfigWithAnthropic(); var args = new[] { ArgumentOptionConstants.Anthropic.ApiKey, cliApiKey, - ArgumentOptionConstants.Anthropic.Model, cliModel + ArgumentOptionConstants.Anthropic.Model, cliModel, + ArgumentOptionConstants.Anthropic.MaxTokens, cliMaxTokens }; // Act @@ -463,13 +596,13 @@ public void Given_CLI_Only_When_Parse_Invoked_Then_Help_Should_Be_False(string c [Trait("Category", "UnitTest")] [Theory] - [InlineData("env-api-key", "env-model")] - public void Given_EnvironmentVariables_Only_When_Parse_Invoked_Then_Help_Should_Be_False(string envApiKey, string envModel) + [InlineData("env-api-key", "env-model", "1500")] + public void Given_EnvironmentVariables_Only_When_Parse_Invoked_Then_Help_Should_Be_False(string envApiKey, string envModel, string envMaxTokens) { // Arrange var config = BuildConfigWithAnthropic( - configApiKey: null, configModel: null, - envApiKey: envApiKey, envModel: envModel); + configApiKey: null, configModel: null, configMaxTokens: null, + envApiKey: envApiKey, envModel: envModel, envMaxTokens: envMaxTokens); var args = Array.Empty(); // Act @@ -481,8 +614,8 @@ public void Given_EnvironmentVariables_Only_When_Parse_Invoked_Then_Help_Should_ [Trait("Category", "UnitTest")] [Theory] - [InlineData(null, null, ConnectorType.Unknown, false)] - public void Given_AnthropicArgumentOptions_When_Creating_Instance_Then_Should_Have_Correct_Properties(string? expectedApiKey, string? expectedModel, ConnectorType expectedConnectorType, bool expectedHelp) + [InlineData(null, null, null, ConnectorType.Unknown, false)] + public void Given_AnthropicArgumentOptions_When_Creating_Instance_Then_Should_Have_Correct_Properties(string? expectedApiKey, string? expectedModel, string? expectedMaxTokens, ConnectorType expectedConnectorType, bool expectedHelp) { // Act var options = new AnthropicArgumentOptions(); @@ -491,6 +624,7 @@ public void Given_AnthropicArgumentOptions_When_Creating_Instance_Then_Should_Ha options.ShouldNotBeNull(); options.ApiKey.ShouldBe(expectedApiKey); options.Model.ShouldBe(expectedModel); + options.MaxTokens.ShouldBe(IntValueOf(expectedMaxTokens)); options.ConnectorType.ShouldBe(expectedConnectorType); options.Help.ShouldBe(expectedHelp); } From 8032506ada35bd164f5933496d4f3ecbc1d734d1 Mon Sep 17 00:00:00 2001 From: MINWOO KIM Date: Mon, 20 Oct 2025 23:12:42 +0900 Subject: [PATCH 18/18] feat: add constant for '--max-tokens' command-line argument --- .../Constants/ArgumentOptionConstants.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs b/src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs index c3f88624..a9170e03 100644 --- a/src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs +++ b/src/OpenChat.PlaygroundApp/Constants/ArgumentOptionConstants.cs @@ -182,6 +182,11 @@ public static class Anthropic /// Defines the constant for '--model'. /// public const string Model = "--model"; + + /// + /// Defines the constant for '--max-tokens'. + /// + public const string MaxTokens = "--max-tokens"; } ///