forked from admin/VolcanicAgenticCore
Streams POST {baseUrl}/chat/completions over SSE (System.Net.ServerSentEvents, no NuGet) and maps delta.content, delta.reasoning_content / delta.reasoning and delta.tool_calls onto AssistantMessageEvents via AssistantMessageBuilder. Configurable through OpenAIProviderOptions so one extension serves OpenAI, OpenRouter, Ollama and other compatible servers; ships the gpt-5 / gpt-5-mini / gpt-4.1 catalogue.
105 lines
4.6 KiB
C#
105 lines
4.6 KiB
C#
using Volcanic.AgenticCore.Abstractions.Providers;
|
|
using Volcanic.AgenticCore.Extensions.OpenAI;
|
|
|
|
namespace Volcanic.AgenticCore.Extensions.Tests.OpenAI;
|
|
|
|
public class OpenAIProviderExtensionTests
|
|
{
|
|
[Fact]
|
|
public async Task Default_registers_openai_with_catalogue_and_env_key()
|
|
{
|
|
var api = new FakeExtensionApi();
|
|
var extension = new OpenAIProviderExtension();
|
|
|
|
Assert.Equal("openai-provider", extension.Name);
|
|
await extension.ActivateAsync(api, CancellationToken.None);
|
|
|
|
var registration = Assert.Single(api.Providers);
|
|
Assert.Equal("openai", registration.Id);
|
|
Assert.Equal("OpenAI", registration.Name);
|
|
Assert.Equal("openai-completions", registration.Api);
|
|
Assert.Equal("https://api.openai.com/v1", registration.BaseUrl);
|
|
Assert.Equal("$OPENAI_API_KEY", registration.ApiKey);
|
|
Assert.True(registration.RequiresApiKey);
|
|
Assert.Null(registration.Headers);
|
|
Assert.NotNull(registration.Stream);
|
|
|
|
Assert.Equal(3, registration.Models.Count);
|
|
Assert.Equal(["gpt-5", "gpt-5-mini", "gpt-4.1"], registration.Models.Select(m => m.Id));
|
|
Assert.All(registration.Models, m => Assert.Equal("openai", m.Provider));
|
|
Assert.All(registration.Models, m => Assert.Equal("openai-completions", m.Api));
|
|
Assert.All(registration.Models, m => Assert.True(m.SupportsImages));
|
|
}
|
|
|
|
[Fact]
|
|
public void Catalogue_has_expected_shape()
|
|
{
|
|
Assert.True(OpenAIModels.Gpt5.Reasoning);
|
|
Assert.Equal(400_000, OpenAIModels.Gpt5.ContextWindow);
|
|
Assert.Equal(128_000, OpenAIModels.Gpt5.MaxTokens);
|
|
Assert.Equal(new ModelCost(1.25m, 10m, 0.125m, 0m), OpenAIModels.Gpt5.Cost);
|
|
Assert.Equal("minimal", OpenAIModels.Gpt5.ThinkingLevelMap![ThinkingLevel.Minimal]);
|
|
Assert.Null(OpenAIModels.Gpt5.ThinkingLevelMap[ThinkingLevel.XHigh]);
|
|
Assert.Null(OpenAIModels.Gpt5.ThinkingLevelMap[ThinkingLevel.Max]);
|
|
|
|
Assert.True(OpenAIModels.Gpt5Mini.Reasoning);
|
|
Assert.Equal(new ModelCost(0.25m, 2m, 0.025m, 0m), OpenAIModels.Gpt5Mini.Cost);
|
|
|
|
Assert.False(OpenAIModels.Gpt41.Reasoning);
|
|
Assert.Equal(1_047_576, OpenAIModels.Gpt41.ContextWindow);
|
|
Assert.Equal(32_768, OpenAIModels.Gpt41.MaxTokens);
|
|
Assert.Null(OpenAIModels.Gpt41.ThinkingLevelMap);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenRouter_options_register_openrouter_id_with_attribution_headers()
|
|
{
|
|
var sonnet = new Model { Id = "anthropic/claude-sonnet-4", Name = "Claude Sonnet 4", Provider = "", Api = "" };
|
|
var options = OpenAIProviderOptions.OpenRouter([sonnet], httpReferer: "https://volcanic.example", title: "Volcanic");
|
|
var api = new FakeExtensionApi();
|
|
var extension = new OpenAIProviderExtension(options);
|
|
|
|
Assert.Equal("openrouter-provider", extension.Name);
|
|
await extension.ActivateAsync(api, CancellationToken.None);
|
|
|
|
var registration = Assert.Single(api.Providers);
|
|
Assert.Equal("openrouter", registration.Id);
|
|
Assert.Equal("OpenRouter", registration.Name);
|
|
Assert.Equal("https://openrouter.ai/api/v1", registration.BaseUrl);
|
|
Assert.Null(registration.ApiKey);
|
|
Assert.True(registration.RequiresApiKey);
|
|
Assert.Equal("https://volcanic.example", registration.Headers!["HTTP-Referer"]);
|
|
Assert.Equal("Volcanic", registration.Headers["X-Title"]);
|
|
Assert.Equal("anthropic/claude-sonnet-4", Assert.Single(registration.Models).Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenRouter_without_attribution_has_no_headers()
|
|
{
|
|
var options = OpenAIProviderOptions.OpenRouter([]);
|
|
Assert.Null(options.Headers);
|
|
Assert.Empty(options.EffectiveModels);
|
|
}
|
|
|
|
[Fact]
|
|
public void Custom_provider_defaults_to_no_key_spec_and_no_models()
|
|
{
|
|
var options = new OpenAIProviderOptions { ProviderId = "groq", BaseUrl = "https://api.groq.com/openai/v1" };
|
|
var registration = new OpenAIProviderExtension(options).CreateRegistration();
|
|
Assert.Equal("groq", registration.Id);
|
|
Assert.Equal("groq", registration.Name);
|
|
Assert.Null(registration.ApiKey);
|
|
Assert.Empty(registration.Models);
|
|
}
|
|
|
|
[Fact]
|
|
public void Explicit_key_spec_and_models_are_passed_through()
|
|
{
|
|
var model = new Model { Id = "custom", Name = "Custom", Provider = "", Api = "" };
|
|
var options = new OpenAIProviderOptions { ApiKeySpec = "!op read secret", Models = [model] };
|
|
var registration = new OpenAIProviderExtension(options).CreateRegistration();
|
|
Assert.Equal("!op read secret", registration.ApiKey);
|
|
Assert.Same(model, Assert.Single(registration.Models));
|
|
}
|
|
}
|