Skip to main content
net10.0

iron-prow adapter: ironhive providers as gateway candidates.

Published
Oct 5, 2026
13d ago
Total Downloads
8.0K
8,011

Install

dotnet add package IronProw.IronHive --version 0.15.1
ProductVersions
.NET
net10.0

README

iron-prow

safe-inference gateway โ€” every LLM call's first gate.
provider selection (frontier โˆจ LAN GpuStack) ยท guardrail ยท resilience โ€” delivered as a standard Microsoft.Extensions.AI.IChatClient.

๋‘ ๊ฐˆ๋ž˜ ํ•„์ˆ˜: frontier/LAN ๊ฒŒ์ดํŠธ์›จ์ด(A) and local-provider safety(B).
๋ฒ”์œ„๋Š” ํ•œ ๋ฒˆ์˜ ์•ˆ์ „ํ•œ ํ˜ธ์ถœ(provider ์„ ํƒ ยท guardrail ยท resilience ยท length-bounding)๊นŒ์ง€๋‹ค. ์—ฌ๋Ÿฌ ํ˜ธ์ถœ์„ ์—ฎ๋Š” ์—์ด์ „ํŠธ ๋ฃจํ”„ยท์„ธ์…˜ยทMCPยทHITL ์€ ํ˜ธ์ŠคํŠธ์˜ ๋ชซ์ด๋‹ค.
์ด๋ฏธ ์ž์ฒด ๋ฐฐ์„ (providerยทresilienceยทguardrail)์„ ๊ฐ€์ง„ ์•ฑ์˜ ์ด๊ด€์€ docs/ADOPTION.md ์ฐธ์กฐ.

Packages

Package ์—ญํ• 
IronProw.Core ๊ฒŒ์ดํŠธ์›จ์ด ๊ณ„์•ฝ, resilience, ์„ ํƒ ๋กœ์ง โ€” iyulab ๊ตฌํ˜„ ์˜์กด 0
IronProw.IronHive IronHive provider ์–ด๋Œ‘ํ„ฐ โ€” OpenAI ยท Anthropic ยท GoogleAI (frontier) ยท GpuStack ยท OpenAI-Compatible/Ollama (LAN)
IronProw.FluxGuard FluxGuard guardrail ์–ด๋Œ‘ํ„ฐ (IGuard ๊ตฌํ˜„)
IronProw.LMSupply ๋กœ์ปฌ ์ถ”๋ก  ์•ˆ์ „ ๋ž˜ํผ (length-bounding + readiness gate)
dotnet add package IronProw.Core
dotnet add package IronProw.IronHive    # frontier provider adapters
dotnet add package IronProw.FluxGuard  # guardrail adapter
dotnet add package IronProw.LMSupply   # local-provider safety adapter

Quick Start

A. Frontier / LAN ๊ฒŒ์ดํŠธ์›จ์ด

IronHive ๊ธฐ๋ฐ˜ frontier provider + FluxGuard guardrail์„ ๋“ฑ๋กํ•œ๋‹ค.
DI ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ํ‘œ์ค€ IChatClient๋ฅผ resolveํ•˜๋ฉฐ, ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ selectionยทretryยท์ž…์ถœ๋ ฅ ๊ฒ€์‚ฌ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค(์ŠคํŠธ๋ฆฌ๋ฐ ์ถœ๋ ฅ ๊ฒ€์‚ฌ์˜ ์‹œ์ ์€ ์•„๋ž˜ ยซWhat the guard inspectsยป ์ฐธ์กฐ).

using IronProw.Core;
using IronProw.IronHive;
using IronProw.FluxGuard;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();   // Generic Host ๋ผ๋ฉด builder.Services
services.AddIronProw()
        .AddIronHiveOpenAI(
            id: "openai",
            priority: 10,
            modelId: "gpt-4o",
            configure: cfg => cfg.ApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!)
        .AddIronHiveAnthropic(
            id: "anthropic",
            priority: 5,
            modelId: "claude-opus-4-5",
            configure: cfg => cfg.ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")!)
        .UseFluxGuard();  // Standard preset (L1 regex guards, offline)

// ํ‘œ์ค€ Microsoft.Extensions.AI.IChatClient ๋ฐ˜ํ™˜
await using var provider = services.BuildServiceProvider();
IChatClient chat = provider.GetRequiredService<IChatClient>();
try
{
    var response = await chat.GetResponseAsync("Hello");
    Console.WriteLine(response.Text);
}
catch (GuardException ex)   // ๊ฐ€๋“œ๊ฐ€ ์ž…๋ ฅ์ด๋‚˜ ์ถœ๋ ฅ์„ ๋ง‰์•˜๋‹ค
{
    Console.WriteLine($"blocked: {ex.Reason}");
}

IronProw.IronHive๋Š” ๋‹ค์„ฏ ๊ฐ€์ง€ provider ์–ด๋Œ‘ํ„ฐ์™€ BYO ํ”„๋ฆฌ์…‹ ์นดํƒˆ๋กœ๊ทธ(ByoPresets, ์•„๋ž˜)๋ฅผ ์ œ๊ณตํ•œ๋‹ค:

  • AddIronHiveOpenAI ยท AddIronHiveAnthropic ยท AddIronHiveGoogleAI โ€” frontier (ProviderKind.Frontier)
  • AddIronHiveGpuStack โ€” LAN GpuStack (ProviderKind.Lan, key-optional). cfg => cfg.BaseUrl = "http://gpustack.lan:8080" ํ˜•ํƒœ๋กœ endpoint ์ง€์ •.
  • AddIronHiveOpenAICompatible โ€” LAN generic OpenAI-ํ˜ธํ™˜(OllamaยทLMStudioยทvLLMยทllama.cpp server, ProviderKind.Lan, key-optional). ํ‘œ์ค€ /v1 API ํ‘œ๋ฉด์„ ๋…ธ์ถœํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋ฉฐ ๊ธฐ๋ณธ endpoint๋Š” Ollama์˜ http://localhost:11434. cfg => cfg.BaseUrl = "http://localhost:1234"(LMStudio)์ฒ˜๋Ÿผ override.
services.AddIronProw()
        .AddIronHiveOpenAICompatible(
            id: "ollama",
            priority: 20,               // LAN ์šฐ์„  โ€” frontier๋ณด๋‹ค ๋†’๊ฒŒ ๋‘๋ฉด ๋กœ์ปฌ ๋จผ์ € ์‹œ๋„
            modelId: "llama3.2",
            configure: cfg => cfg.BaseUrl = "http://localhost:11434");  // key ๋ถˆํ•„์š”
์‚ฌ์šฉ์ž ์ง€์ •(BYO) ์—”๋“œํฌ์ธํŠธ โ€” ByoPresets (0.11.0+)

์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ • ํ™”๋ฉด์—์„œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ณ ๋ฅด๋Š” ์•ฑ์€ ํ”„๋ฆฌ์…‹ ์นดํƒˆ๋กœ๊ทธ ํ•˜๋‚˜๋กœ ๊ธฐ๋ณธ๊ฐ’ยท๊ฒ€์ฆยท๋“ฑ๋กยท์—ฐ๊ฒฐ ํ™•์ธ์„ ๋๋‚ธ๋‹ค:

ํ”„๋ฆฌ์…‹ id ์ข…๋ฅ˜ ๊ธฐ๋ณธ base URL ํ‚ค
openai ยท anthropic ยท gemini frontier SDK ๊ธฐ๋ณธ๊ฐ’(๋น„์›Œ ๋‘๋ฉด ๊ทธ๋Œ€๋กœ) ํ•„์ˆ˜
grok frontier (OpenAI-ํ˜ธํ™˜) https://api.x.ai/v1 ํ•„์ˆ˜
ollama ยท gpustack LAN http://localhost:11434 ยท http://localhost:8080 ์„ ํƒ
custom LAN (OpenAI-ํ˜ธํ™˜) ์—†์Œ โ€” ํ•„์ˆ˜ ์„ ํƒ
var userKey = Environment.GetEnvironmentVariable("XAI_API_KEY");
var gatewayHeaders = new Dictionary<string, string> { ["X-Gateway-Token"] = "โ€ฆ" };   // ์—†์œผ๋ฉด ์ƒ๋žต
var endpoint = new ByoEndpoint("grok", BaseUrl: null, ApiKey: userKey,
    Headers: gatewayHeaders);                                          // ์„ ํƒ โ€” ๊ฒŒ์ดํŠธ์›จ์ด ํ† ํฐ ๋“ฑ, ํ”„๋กœ๋ธŒยท์ฑ„ํŒ… ๋‘˜ ๋‹ค์— ์‹ค๋ฆฐ๋‹ค

string? why = ByoPresets.Validate(endpoint);                          // ์ ‘์† ์—†์ด ์ž…๋ ฅ ๊ทœ์น™๋งŒ โ€” null ์ด๋ฉด ์‚ฌ์šฉ ๊ฐ€๋Šฅ
ByoProbeResult probe = await ByoPresets.ProbeAsync(endpoint);         // ๋ชจ๋ธ ๋ชฉ๋ก ์š”์ฒญ 1ํšŒ โ€” ์‹ค์ œ ์ธ์ฆ
// probe.Ok / probe.ModelIds(์„œ๋ฒ„ ์ˆœ์„œ) / probe.ModelCount / probe.StatusCode(401 = ์ž˜๋ชป๋œ ํ‚ค, 404 = ์ž˜๋ชป๋œ ๊ฒฝ๋กœ) / probe.Error
// probe.Lists("grok-4") โ€” ์„œ๋ฒ„๊ฐ€ ๊ทธ ๋ชจ๋ธ์„ ๋‚ด๋†“๋Š”๊ฐ€(๋ณด๋‚ด๊ธฐ ์ „ ยซ์ด ๋ชจ๋ธ์€ ์ œ๊ณต๋˜์ง€ ์•Š์Œยป ๊ฒฝ๊ณ ์šฉ)

if (why is null && probe.Lists("grok-4"))
{
    services.AddIronProw().AddIronHiveByo(id: "user-endpoint", priority: 30, modelId: "grok-4", endpoint);
}
  • ByoPresets.All ์€ ์•ฑ์ด UI ๊ธฐ๋ณธ๊ฐ’ยท๊ฒ€์ฆ์„ ์ฑ„์šฐ๋Š” ์ •๋ณธ์ด๋‹ค(ํ‘œ์‹œ ์ด๋ฆ„ยท๋ฌธ๊ตฌ๋Š” ์•ฑ ๋ชซ). ๋ชจ๋ฅด๋Š” ํ”„๋ฆฌ์…‹ id ๋Š” ์กฐ์šฉํžˆ OpenAI-ํ˜ธํ™˜์œผ๋กœ ๋ณด๋‚ด์ง€ ์•Š๊ณ  Validate ๊ฐ€ ๊ฑฐ๋ถ€ํ•œ๋‹ค.
  • ProbeAsync ๋Š” ๊ฐ provider ์˜ ๋ชจ๋ธ ํŒŒ์ธ๋”(OpenAIModelFinder ยท AnthropicModelFinder ยท GoogleAIModelFinder)๋กœ ํ•œ ๋ฒˆ ์ธ์ฆ ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค โ€” frontier ๋„ "๋ฌด์กฐ๊ฑด ์—ฐ๊ฒฐ๋จ" ์ด ์•„๋‹ˆ๋‹ค. ์š”์ฒญ์€ ํ•œ ๋ฒˆ(SDK ์žฌ์‹œ๋„ ๋” โ€” ๋‹ฟ์ง€ ์•Š๋Š” ํ˜ธ์ŠคํŠธ๋ฅผ ๋„ค ๋ฒˆ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๋Š”๋‹ค), ๊ธฐ๋ณธ ์ œํ•œ์‹œ๊ฐ„ 10์ดˆ, ์‹คํŒจ๋Š” ์˜ˆ์™ธ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฒฐ๊ณผ๋กœ ์˜จ๋‹ค(์ทจ์†Œ๋งŒ ์˜ˆ์™ธ). Error ๋Š” ์›์ธ์„ ๋งํ•œ๋‹ค: provider ์˜ ๊ฑฐ๋ถ€ ๋ฉ”์‹œ์ง€ ยท ์—ฐ๊ฒฐ ์‹คํŒจ ยท ยซNo answer within N s.ยป. ๋‹ค๋งŒ ๊ทธ ๋ฌธ์žฅ์€ OSยทSDK ์˜ ๊ฒƒ์ด๋ผ ์ง€์—ญํ™”๋˜๊ณ  ์ฃผ์†Œ๋‚˜ provider ์ƒ์„ธ๋ฅผ ๋‹ด์„ ์ˆ˜ ์žˆ๋‹ค โ€” ์•ฑ์ด ์ž๊ธฐ ๋ง๋กœ ์•ˆ๋‚ดํ•˜๋ ค๋ฉด Failure(ByoProbeFailure: Unauthorized ยท HttpStatus ยท Timeout ยท Refused ยท HostNotFound ยท Unreachable ยท Transport ยท Invalid)๋กœ ๋ถ„๋ฅ˜ํ•œ๋‹ค. ๊ฑฐ๋ถ€๋œ ์—ฐ๊ฒฐ์€ ์–ด๋А wire ์—์„œ๋“  Refused ๋‹ค.
  • ApiKeyPlacement ๋Š” ApiKey ๋ฅผ ๋ณด๋‚ผ ํ˜•ํƒœ๋‹ค โ€” ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ Authorization: Bearer ๊ฐ€ ์•„๋‹Œ ๊ฒƒ์„ ์š”๊ตฌํ•˜๋ฉด CredentialPlacement.Authorization("Basic")(ํ‚ค๋Š” ์ธ์ฝ”๋”ฉ๋œ user:password) ยท .Authorization(null)(scheme ์—†๋Š” ํ† ํฐ) ยท .InHeader("api-key"). null ์ด๋ฉด Bearer. OpenAI ์™€์ด์–ด ํ”„๋ฆฌ์…‹(openai ยท grok ยท ollama ยท gpustack ยท custom)์— ์ ์šฉ๋˜๊ณ , ๋“ฑ๋ก๊ณผ ProbeAsync ๊ฐ€ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๋ณด๋‚ธ๋‹ค. anthropic ยท gemini ๋Š” ๋ฒค๋” ํ—ค๋”๋ฅผ ์“ฐ๋ฏ€๋กœ Validate ๊ฐ€ ๊ฑฐ๋ถ€ํ•œ๋‹ค.
  • Headers ๋Š” provider ์˜ Headers ์„ค์ •์œผ๋กœ ๊ฐ„๋‹ค โ€” ์ž๊ฒฉ์ฆ๋ช…์„ ์‹ฃ๋Š” ์ด๋ฆ„(OpenAI ๊ณ„์—ด Authorization, Anthropic x-api-key ๋“ฑ)์€ Validate ๊ฐ€ provider ๊ทœ์น™๋Œ€๋กœ ๊ฑฐ๋ถ€ํ•œ๋‹ค(ํ‚ค๋Š” ApiKey ๋กœ).

UseFluxGuard() (ํŒŒ๋ผ๋ฏธํ„ฐ ์—†์Œ) ๋Š” Standard preset(L1 regex, offline)์„ ์ ์šฉํ•œ๋‹ค.
UseFluxGuard(configure: b => โ€ฆ) ๋Š” Standard preset ์œ„์— FluxGuard ๋นŒ๋” ์„ค์ •์„ ๋”ํ•œ๋‹ค(์˜ˆ: b.WithBlockThreshold(0.8)). ์ด๋ฏธ ๋งŒ๋“  FluxGuard ์ธ์Šคํ„ด์Šค๋ฅผ ์ฃผ์ž…ํ•˜๋ ค๋ฉด UseFluxGuard(IFluxGuard) ์˜ค๋ฒ„๋กœ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ๋‹ค๋ฅธ ๊ฐ€๋“œ๋Š” UseGuard(sp => myGuard) ๋กœ IGuard ๋ฅผ ์ง์ ‘ ๊ฝ‚๋Š”๋‹ค.
fail mode: ๊ธฐ๋ณธ์€ fail-closed(๋ถˆํ™•์‹ค verdict ์ฐจ๋‹จ). ๊ฐ€์šฉ์„ฑ์„ ์šฐ์„ ํ•˜๋Š” ์†Œ๋น„์ž๋Š” UseFluxGuard(failMode: FluxGuardFailMode.Open)์œผ๋กœ opt-in โ€” Flagged/NeedsEscalation์„ ํ†ต๊ณผ์‹œํ‚จ๋‹ค(์ •์˜๋œ Block์€ mode ๋ฌด๊ด€ ํ•ญ์ƒ ์ฐจ๋‹จ).

B. Local-provider safety (lm-supply / ONNX / DirectML)

๋กœ์ปฌ ์ถ”๋ก ์„ iron-prow ์•ˆ์ „ ๋ ˆ์ด์–ด๋กœ ๊ฐ์‹ผ๋‹ค. lm-supply ์ƒ์„ฑ์ž(IGeneratorModel/ITextGenerator)๋ฅผ ๊ทธ๋Œ€๋กœ ๋„˜๊ธฐ๋ฉด iron-prow๊ฐ€ ๋‚ด๋ถ€ GeneratorChatClient ๋ธŒ๋ฆฌ์ง€๋กœ IChatClient์— ์ ์‘์‹œํ‚จ๋‹ค โ€” ์†Œ๋น„์ž๊ฐ€ ๋ธŒ๋ฆฌ์ง€๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

using IronProw.Core;
using IronProw.LMSupply;
using IronProw.FluxGuard;
using LMSupply.Generator;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

// generator: lm-supply ๊ฐ€ ๋กœ๋“œํ•œ IGeneratorModel/ITextGenerator โ€” ์ƒ๋ช…์ฃผ๊ธฐ๋Š” ํ˜ธ์ถœ์ž ์†Œ์œ .
await using var generator = await LocalGenerator.LoadAsync("auto");

// probe: IReadinessProbe โ€” ๋ชจ๋ธ์ด ์š”์ฒญ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š”์ง€์™€ ์–ด๋–ค ๋ชจ๋ธ id ๊ฐ€ ์žˆ๋Š”์ง€ ๋ณด๊ณ ํ•œ๋‹ค.
//        LoadAsync ๋กœ ๋‹จ์ผ ๋ชจ๋ธ์„ ์ง์ ‘ ๋กœ๋“œํ–ˆ๋‹ค๋ฉด LazyReadinessProbe, GeneratorPool ์„ ์“ฐ๋ฉด GeneratorPoolProbe.
var probe = new LazyReadinessProbe(() => true, [generator.GetModelInfo().ModelId]);

var services = new ServiceCollection();
services.AddIronProw()
        .AddLMSupplyLocal(
            id: "local-phi3",
            priority: 20,
            generator: generator,
            probe: probe,
            options: new LocalSafetyOptions { DefaultMaxOutputTokens = 1024 })
        .UseFluxGuard();

await using var provider = services.BuildServiceProvider();
IChatClient chat = provider.GetRequiredService<IChatClient>();

AddLMSupplyLocal์ด ์ œ๊ณตํ•˜๋Š” safety(ํ˜ธ์ถœ๋งˆ๋‹ค ์ด ์ˆœ์„œ๋กœ ์ ์šฉ):

  • bridge โ€” GeneratorChatClient๊ฐ€ lm-supply ์ƒ์„ฑ์ž๋ฅผ IChatClient๋กœ ์ ์‘(role ๋งคํ•‘, MaxOutputTokensโ†’MaxNewTokens, sampler/tool ์ „ํŒŒ, streaming flatten). ์ด๋ฏธ ๋ธŒ๋ฆฌ์ง€๋œ IChatClient๋ฅผ ๋ณด์œ ํ•œ ํ˜ธ์ถœ์ž(์˜ˆ: ironhive-host)๋Š” AddLMSupplyLocal(..., IChatClient rawClient, ...) ์˜ค๋ฒ„๋กœ๋“œ๋ฅผ ์“ธ ์ˆ˜ ์žˆ๋‹ค.
  • readiness gate โ€” IReadinessProbe.IsReadyAsync ๊ฐ€ false ๋ฉด InvalidOperationException. ๊ฒŒ์ดํŠธ์›จ์ด๋Š” ์ด๊ฒƒ์„ ๊ทธ provider ์˜ ์‹คํŒจ๋กœ ๋ณด๊ณ  ๋‹ค์Œ provider ๋กœ ๋„˜์–ด๊ฐ„๋‹ค.
  • model-ID preflight โ€” ChatOptions.ModelId ๊ฐ€ ์ง€์ •๋œ ํ˜ธ์ถœ๋งŒ IReadinessProbe.GetAvailableModelIdsAsync ์— ๊ทธ id ๊ฐ€ ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•œ๋‹ค(์—†์œผ๋ฉด InvalidOperationException). โš  ๊ฒŒ์ดํŠธ์›จ์ด๋Š” ๊ฐ™์€ ChatOptions ๋ฅผ ๋ชจ๋“  provider ์— ๋„˜๊ธฐ๋ฏ€๋กœ, frontier ๋ชจ๋ธ id ๋ฅผ ์ง€์ •ํ•œ ํ˜ธ์ถœ์ด local provider ๋กœ fallback ๋˜๋ฉด preflight ์—์„œ ์‹คํŒจํ•˜๊ณ  ๊ทธ provider ์˜ ๊ฑด๊ฐ• ๊ธฐ๋ก์—๋„ ์‹คํŒจ๋กœ ๋‚จ๋Š”๋‹ค โ€” ํ˜ผํ•ฉ ๊ฒŒ์ดํŠธ์›จ์ด์—์„œ๋Š” ModelId ๋ฅผ ๋น„์›Œ ๋‘๊ณ  provider ๋ณ„ ๊ธฐ๋ณธ ๋ชจ๋ธ์„ ์“ด๋‹ค.
  • length-bounding โ€” LocalSafetyOptions.DefaultMaxOutputTokens (๋ฏธ์„ค์ • ํ˜ธ์ถœ์— ์ž๋™ ์ ์šฉ, ๊ธฐ๋ณธ 512)
  • reasoning default โ€” LocalSafetyOptions.DefaultReasoningEffort (๋ฏธ์„ค์ • ํ˜ธ์ถœ์— ์ž๋™ ์ ์šฉ, ๊ธฐ๋ณธ ReasoningEffort.None). thinking ๊ธฐ๋ณธ-on ๋ชจ๋ธ(Gemma 4ยทQwen3)์€ ์ž‘์€ ์˜ˆ์‚ฐ์„ reasoning ์— ์ „๋ถ€ ์จ ๋นˆ ๋‹ต + FinishReason.Length ๋ฅผ ๋Œ๋ ค์ค„ ์ˆ˜ ์žˆ๋‹ค โ€” ์•ˆ์ „ ๋ž˜ํผ์˜ ๊ณ„์•ฝ์€ ยซ์˜ˆ์‚ฐ์€ ๋‹ต์— ์“ด๋‹คยป๋ผ ๊ธฐ๋ณธ์€ off. ๋ชจ๋ธ ๊ธฐ๋ณธ์„ ์œ ์ง€ํ•˜๋ ค๋ฉด null.
  • reasoning ์šด๋ฐ˜ โ€” ๋ธŒ๋ฆฌ์ง€๊ฐ€ ํ‘œ์ค€ ChatOptions.Reasoning ์„ lm-supply ThinkingMode ๋กœ ๋ฒˆ์—ญํ•œ๋‹ค(Effort.Noneโ†’Off, ๊ทธ ์™ธโ†’On, nullโ†’๋ชจ๋ธ ๊ธฐ๋ณธ). ๋ชจ๋ธ์ด ๋‚ธ reasoning ์€ TextReasoningContent ๋กœ ์‘๋‹ต์— ์‹ค๋ฆฐ๋‹ค(ReasoningOutput.None ์ด๋ฉด ๋ฒ„๋ฆผ) โ€” ๋นˆ ๋‹ต์ด ์™œ ๋น„์—ˆ๋Š”์ง€ ์†Œ๋น„์ž๊ฐ€ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  • ๊ณ„์ธก โ€” ChatResponse.ModelIdยทUsage(llama-server ์˜ prompt/completion ํ† ํฐ; ONNX ๋Š” null)ยทGetService<ChatClientMetadata>()(ProviderName = "LMSupply" โ€” ๊ฒฝ๋Ÿ‰ ๊ฒฝ๋กœ BuildLocalSafeClient ๊ฐ€ ๋Œ๋ ค์ค€ client ์—์„œ. ๊ฒŒ์ดํŠธ์›จ์ด๋Š” ํ˜ธ์ถœ๋งˆ๋‹ค provider ๋ฅผ ๊ณ ๋ฅด๋ฏ€๋กœ provider ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋‚ด์ง€ ์•Š๋Š”๋‹ค(null); ์–ด๋А ๋ชจ๋ธ์ด ๋‹ตํ–ˆ๋Š”์ง€๋Š” ์‘๋‹ต์˜ ModelId ๋กœ ๋ณธ๋‹ค).
๊ฒฝ๋Ÿ‰ ๊ฒฝ๋กœ โ€” ๋‹จ์ผ local provider (๊ฒŒ์ดํŠธ์›จ์ด ์—†์ด)

ํด๋ฐฑ ๋Œ€์ƒ 2๋ฒˆ์งธ provider๊ฐ€ ์—†๋Š” local-first ๋‹จ์ผ provider ์†Œ๋น„์ž์—๊ฒŒ๋Š” ๊ฒŒ์ดํŠธ์›จ์ด์˜ registryยทselectionยทresilience ๋ ˆ์ด์–ด๊ฐ€ ์ „๋ถ€ inertํ•˜๋‹ค. ์ด ๊ฒฝ์šฐ BuildLocalSafeClient๊ฐ€ ๋ธŒ๋ฆฌ์ง€+์•ˆ์ „wrap๋งŒ ์กฐ๋ฆฝํ•œ plain IChatClient๋ฅผ ๋“ฑ๋ก ์—†์ด ๋ฐ˜ํ™˜ํ•œ๋‹ค:

using IronProw.LMSupply;
using LMSupply.Generator;
using Microsoft.Extensions.AI;

await using var generator = await LocalGenerator.LoadAsync("auto");
var probe = new LazyReadinessProbe(() => true, [generator.GetModelInfo().ModelId]);

// ๊ฒŒ์ดํŠธ์›จ์ด(AddIronProw/๋นŒ๋”/๋ ˆ์ง€์ŠคํŠธ๋ฆฌ) ์—†์ด ์•ˆ์ „ ๋ž˜ํ•‘๋œ local client ์ง์ ‘ ์กฐ๋ฆฝ โ€” readinessยทpreflightยทlength-bound ๋งŒ,
// ์ž…์ถœ๋ ฅ ๊ฐ€๋“œ(FluxGuard)๋Š” ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค(๊ฐ€๋“œ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ๊ฒŒ์ดํŠธ์›จ์ด ๊ฒฝ๋กœ๋ฅผ ์“ด๋‹ค).
IChatClient chat = LMSupplyExtensions.BuildLocalSafeClient(
    generator,                                                 // lm-supply ITextGenerator (ํ˜ธ์ถœ์ž ์†Œ์œ )
    probe,                                                     // IReadinessProbe
    new LocalSafetyOptions { DefaultMaxOutputTokens = 1024 }); // ์„ ํƒ (๊ธฐ๋ณธ 512)

๋™์ผํ•œ safety(readiness gateยทpreflightยทlength-bounding)๋ฅผ ๋ฐ›๋˜ selection/fallback/resilience ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์—†๋‹ค. ๋‹ค์ค‘ providerยท์šฐ์„ ์ˆœ์œ„ ์„ ํƒยทprovider-level fallback์ด ํ•„์š”ํ•ด์ง€๋ฉด AddLMSupplyLocal๋กœ ์ „ํ™˜ํ•œ๋‹ค.

๋‘ ๊ฐˆ๋ž˜ ์กฐํ•ฉ

๋‘ ๊ฐˆ๋ž˜๋ฅผ ๊ฐ™์€ DI ๋“ฑ๋ก์—์„œ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค. priority๊ฐ€ ๋†’์€ provider๊ฐ€ ๋จผ์ € ์„ ํƒ๋˜๊ณ , fallback ์‹œ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„ provider๋กœ ๊ฐ•๋“ฑ๋œ๋‹ค.

services.AddIronProw()
        .AddIronHiveOpenAI("openai", priority: 10, "gpt-4o",
            cfg => cfg.ApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!)
        .AddLMSupplyLocal("local", priority: 20, generator, probe)
        .UseFluxGuard()
        .Configure(opt =>
        {
            opt.EnableFallback = true;                              // ๊ธฐ๋ณธ๊ฐ’
            opt.Resilience.MaxRetries = 3;                         // ๊ธฐ๋ณธ๊ฐ’: 2
            opt.Resilience.BaseDelay = TimeSpan.FromMilliseconds(300); // ๊ธฐ๋ณธ๊ฐ’: 200ms
            opt.Resilience.FailureThreshold = 3;                   // ์—ฐ์† ์‹คํŒจ NํšŒ๋ฉด cooldown (๊ธฐ๋ณธ๊ฐ’: 3, 0 = ๋”)
            opt.Resilience.Cooldown = TimeSpan.FromSeconds(30);    // cooldown ๋™์•ˆ ํ›„์ˆœ์œ„ (๊ธฐ๋ณธ๊ฐ’: 30s)
            opt.Resilience.MaxRetryAfter = TimeSpan.FromSeconds(10); // provider ์˜ Retry-After ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ƒํ•œ (๊ธฐ๋ณธ๊ฐ’: 10s)
            opt.OnTransition = t =>                                 // retry/fallback/exhausted ์ด๋ฒคํŠธ (UI ์นฉ ๋“ฑ)
                Console.WriteLine($"[{t.Kind}] {t.ProviderId} ({t.ProviderIndex + 1}/{t.TotalProviders})");
        });

provider ๊ฑด๊ฐ• ๊ธฐ์–ต(0.4.0+): ๊ฒŒ์ดํŠธ์›จ์ด๋Š” provider ๋ณ„ ์—ฐ์† ์‹คํŒจ๋ฅผ ๊ธฐ์–ตํ•œ๋‹ค. FailureThreshold ํšŒ ์—ฐ์†์œผ๋กœ ๊ฐ•๋“ฑ ๋Œ€์ƒ ์‹คํŒจ(retry ์†Œ์ง„ยทfallback-eligible)๊ฐ€ ๋‚˜๋ฉด ๊ทธ provider ๋Š” Cooldown ๋™์•ˆ ํ›„์ˆœ์œ„๋กœ ๋ฐ€๋ฆฐ๋‹ค โ€” ์ œ์™ธ๊ฐ€ ์•„๋‹ˆ๋ผ ํ›„์ˆœ์œ„๋ผ ๊ฑด๊ฐ•ํ•œ provider ๊ฐ€ ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ์—ฌ์ „ํžˆ ์‹œ๋„๋˜๊ณ , ํ•œ ๋ฒˆ ์„ฑ๊ณตํ•˜๋ฉด ๊ธฐ๋ก์ด ์ง€์›Œ์ง„๋‹ค. ๊ทธ ์ „์—” ์ฃฝ์€ LAN provider ๊ฐ€ ๋งค ํ˜ธ์ถœ์— retry ์˜ˆ์‚ฐ(๊ธฐ๋ณธ 2ํšŒ + backoff)์„ ๋ฌผ๋ฆฐ ๋’ค์—์•ผ ๋‹ค์Œ provider ๋กœ ๋„˜์–ด๊ฐ”๋‹ค. ๋ฐ€๋ฆฐ provider ๋Š” OnTransition ์— ProwTransitionKind.Skipped ๋กœ ๋ณด๊ณ ๋œ๋‹ค. EnableFallback = false ๋ฉด ๊ฑด๊ฐ• ๊ธฐ์–ต๋„ ๋ผ์šฐํŒ…์— ์“ฐ์ง€ ์•Š๋Š”๋‹ค(๊ทธ ์„ค์ •์˜ ๋œป์ด ยซ์ ˆ๋Œ€ provider ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹คยป๋ผ์„œ). ์—ฐ๊ฒฐ ๊ฑฐ๋ถ€ยท์ด๋ฆ„ ํ•ด์„ ์‹คํŒจ(HttpRequestError.ConnectionError/NameResolutionError)๋Š” retry ๊ฐ€ ์•„๋‹ˆ๋ผ ์ฆ‰์‹œ fallback ์œผ๋กœ ๋ถ„๋ฅ˜๋œ๋‹ค โ€” ์—”๋“œํฌ์ธํŠธ๊ฐ€ ๋ฐ”์œ ๊ฒŒ ์•„๋‹ˆ๋ผ ์—†๋Š” ๊ฒƒ์ด๋ผ์„œ.

HTTP ์ƒํƒœ๋ณ„ ๋ถ„๋ฅ˜(0.5.0+): HTTP ์‹คํŒจ๋Š” ์˜ˆ์™ธ ํƒ€์ž…์ด ์•„๋‹ˆ๋ผ ์ƒํƒœ ์ฝ”๋“œ๋กœ ๋ถ„๋ฅ˜๋œ๋‹ค โ€” OpenAI SDK ์˜ ClientResultException, HttpRequestException.StatusCode, ironhive ์˜ RateLimitException(AddIronHive* ๊ฐ€ ๋“ฑ๋กํ•˜๋Š” IronHiveHttpFailureReader) ๋ชจ๋‘ ๊ฐ™์€ ๊ทœ์น™์ด๋‹ค.

์ƒํƒœ ๋ถ„๋ฅ˜
408 ยท 500 ยท 502 ยท 504 ๊ฐ™์€ provider ์—์„œ retry
429 ยท 503 provider ๊ฐ€ retry ํžŒํŠธ(Retry-After / retry-after-ms)๋ฅผ ๋ณด๋ƒˆ์œผ๋ฉด ๊ทธ๋งŒํผ ๊ธฐ๋‹ค๋ ค retry(MaxRetryAfter ์ดˆ๊ณผ๋ฉด retry ์—†์ด ๋‹ค์Œ provider), ์—†์œผ๋ฉด ์ฆ‰์‹œ ๋‹ค์Œ provider
๊ทธ ๋ฐ–(400 ยท 401 ยท 403 ยท 404 ยท 409 โ€ฆ) ๋‹ค์Œ provider (ํ•œ provider ์˜ ๊ฑฐ์ ˆ์€ ๋‹ค๋ฅธ provider ๋„ ๊ฑฐ์ ˆํ•œ๋‹ค๋Š” ์ฆ๊ฑฐ๊ฐ€ ์•„๋‹ˆ๋‹ค)

retry ๋ฅผ ๋‹ค ์“ด ์‹คํŒจ๋„ ๋‹ค์Œ provider ๋กœ ๋„˜์–ด๊ฐ„๋‹ค. ์ƒํƒœ์— ๋„๋ฉ”์ธ ์˜๋ฏธ๋ฅผ ์ฃผ๋Š” provider(์˜ˆ: ๋‹ค์šด๋กœ๋“œ ์Šน์ธ ์ „๊นŒ์ง€ 409 ๋ฅผ ๋‚ด๋Š” ๋กœ์ปฌ provider)๋Š” ์†Œ๋น„์ž๊ฐ€ IErrorClassifier ๋ฅผ ๋ฐ์ฝ”๋ ˆ์ดํŠธํ•ด ๊ทธ ์ฝ”๋“œ๋งŒ ๋‹ค๋ฅด๊ฒŒ ๋ถ„๋ฅ˜ํ•œ๋‹ค. ๋‹ค๋ฅธ ์˜ˆ์™ธ ํƒ€์ž…์ด HTTP ์‹คํŒจ๋ฅผ ๋‚˜๋ฅด๋ฉด IHttpFailureReader ๋ฅผ TryAddEnumerable ๋กœ ๋“ฑ๋กํ•œ๋‹ค.

using IronProw.Core;
using Microsoft.Extensions.DependencyInjection;

// AddIronProw() ๋’ค์— ๋“ฑ๋กํ•˜๋ฉด ๊ธฐ๋ณธ ๋ถ„๋ฅ˜๊ธฐ๋ฅผ ๋Œ€์‹ ํ•œ๋‹ค โ€” 409 ๋งŒ ๋‹ค๋ฅด๊ฒŒ, ๋‚˜๋จธ์ง€๋Š” ๊ธฐ๋ณธ ๊ทœ์น™์— ๋งก๊ธด๋‹ค.
services.AddSingleton<IErrorClassifier>(sp =>
    new DownloadPendingClassifier(new DefaultErrorClassifier(sp.GetServices<IHttpFailureReader>())));

sealed class DownloadPendingClassifier(IErrorClassifier inner) : IErrorClassifier
{
    public ErrorClassification Classify(Exception exception)
        => exception is HttpRequestException { StatusCode: System.Net.HttpStatusCode.Conflict }
            ? ErrorClassification.FallbackEligible
            : inner.Classify(exception);
}

Provider ์„ ํƒ โ€” ProviderKind ์™€ IProviderSelector

๊ธฐ๋ณธ ์„ ํƒ๊ธฐ(DefaultProviderSelector)๋Š” priority ๋งŒ ๋ณธ๋‹ค โ€” ๋†’์€ ์ˆœ์„œ๋กœ ์‹œ๋„ํ•œ๋‹ค. ๊ฐ ๋“ฑ๋ก์˜ ProviderKind(FrontierยทLanยทLocal, ์–ด๋Œ‘ํ„ฐ๊ฐ€ ์ฑ„์šด๋‹ค)๋Š” ๊ธฐ๋ณธ ์„ ํƒ๊ธฐ๊ฐ€ ์ฝ์ง€ ์•Š๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์ด๊ณ , ์š”์ฒญ์— ๋”ฐ๋ผ ์ˆœ์„œ๋ฅผ ๋ฐ”๊พธ๊ณ  ์‹ถ์€ ์†Œ๋น„์ž๊ฐ€ ์ž๊ธฐ IProviderSelector ์—์„œ ์“ด๋‹ค. AddIronProw() ๋’ค์— ๋“ฑ๋กํ•˜๋ฉด ๊ธฐ๋ณธ ์„ ํƒ๊ธฐ๋ฅผ ๋Œ€์‹ ํ•œ๋‹ค. ์„ ํƒ๊ธฐ๊ฐ€ ๋Œ๋ ค์ค€ ์ˆœ์„œ์— ๊ฑด๊ฐ• ๊ธฐ์–ต(cooldown ์ค‘์ธ provider ๋ฅผ ๋’ค๋กœ)์ด ์ ์šฉ๋œ๋‹ค.

using IronProw.Core;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

services.AddSingleton<IProviderSelector, ShortRequestsStayLocal>();

// ์งง์€ ์š”์ฒญ์€ ๋กœ์ปฌ ๋จผ์ €, ๋‚˜๋จธ์ง€๋Š” priority ์ˆœ์„œ.
sealed class ShortRequestsStayLocal : IProviderSelector
{
    public IReadOnlyList<ProviderRegistration> Order(ChatSelectionContext context)
    {
        var chars = context.Messages.Sum(m => m.Text.Length);
        return chars < 2_000
            ? [.. context.Candidates.OrderByDescending(c => c.Kind == ProviderKind.Local)]   // ์•ˆ์ • ์ •๋ ฌ โ€” ๊ฐ™์€ Kind ์•ˆ์—์„  priority ์ˆœ์„œ ์œ ์ง€
            : context.Candidates;
    }
}

OnTransition(์„ ํƒ)์€ ๊ฐ ๊ฒŒ์ดํŠธ์›จ์ด ์ „ํ™˜(retry / fallback / exhausted / skipped)๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋Š” best-effort ์ฝœ๋ฐฑ์ด๋‹ค. ์†Œ๋น„์ž๊ฐ€ ์–ด๋А provider๋กœ ๊ฐ•๋“ฑ๋๋Š”์ง€ UI์— ํ‘œ์‹œ(์˜ˆ: resilience ์นฉ)ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฝœ๋ฐฑ์ด ๋˜์ง€๋Š” ์˜ˆ์™ธ๋Š” ์‚ผ์ผœ์ง€๋ฉฐ ์ถ”๋ก ์„ ์ ˆ๋Œ€ ๊นจ์ง€ ์•Š๋Š”๋‹ค. ๋ฏธ์„ค์ • ์‹œ ๋™์ž‘์€ ๊ธฐ์กด๊ณผ ๋™์ผ(๋ฌด๋ณด๊ณ ).

์ŠคํŠธ๋ฆฌ๋ฐ ๋™๋“ฑ์„ฑ: retryยทfallback์€ GetResponseAsync์™€ GetStreamingResponseAsync ์–‘์ชฝ์— ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋œ๋‹ค. ์ŠคํŠธ๋ฆฌ๋ฐ์˜ ๋ณต์›๋ ฅ ์ฐฝ์€ "์ฒซ ChatResponseUpdate๊ฐ€ yield๋˜๊ธฐ ์ „" ์ด๋‹ค โ€” ์ฒซ ์ฒญํฌ ์ด์ „์— ๋ฐœ์ƒํ•œ ์‹คํŒจ(์˜ˆ: OpenAI ํ˜ธํ™˜ ํ˜ธ์ถœ์ด ์ฒซ MoveNextAsync์—์„œ ๋˜์ง€๋Š” connection-refused / 404 / model-not-found)๋Š” same-provider retry(Retryable) ๋˜๋Š” next-provider fallback(FallbackEligible)์œผ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค. ์ฒซ ์ฒญํฌ๊ฐ€ emit๋œ ๋’ค์˜ ์‹คํŒจ๋Š” provider๋ฅผ ๋ฐ”๊พธ๋ฉด ์ด์ค‘ emit์ด ๋˜๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ์ „ํŒŒ๋œ๋‹ค.

๋ฉ€ํ‹ฐํ…Œ๋„ŒํŠธ โ€” per-tenant provider resolution

๊ธฐ๋ณธ AddProvider/AddLMSupplyLocal ๊ฒฝ๋กœ๋Š” provider ์ง‘ํ•ฉ์ด ํ”„๋กœ์„ธ์Šค ์ˆ˜๋ช… ๋™์•ˆ ๊ณ ์ •์ธ ์†Œ๋น„์ž(๋ฐ์Šคํฌํƒ‘ ์—์ด์ „ํŠธ, ๋‹จ์ผ ์œ ์ €)๋ฅผ ์œ„ํ•œ ๊ฒƒ์ด๋‹ค. ์›Œํฌ์ŠคํŽ˜์ด์Šค๋งˆ๋‹ค provider ์ง‘ํ•ฉยทconfigยทsecret์ด ๋‹ค๋ฅธ ๋ฉ€ํ‹ฐํ…Œ๋„ŒํŠธ ์„œ๋ฒ„ ์†Œ๋น„์ž๋Š” AddTenantResolver๋กœ per-tenant ๊ฒŒ์ดํŠธ์›จ์ด๋ฅผ ๋Ÿฐํƒ€์ž„์— buildํ•œ๋‹ค.

using IronProw.Core;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

// startup โ€” ๋‹จ์ผ ํ…Œ๋„ŒํŠธ AddProvider ๊ฒฝ๋กœ์™€ ๋ณ‘์กด(๋ฌดํšŒ๊ท€). ๊ฒŒ์ดํŠธ์›จ์ด๋‹น ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•œ๋‹ค(๋‘ ๋ฒˆ์งธ๋Š” InvalidOperationException).
services.AddIronProw()
        .UseFluxGuard()
        .AddTenantResolver((sp, tenant) =>                           // ์‹ ๊ทœ ํ‘œ๋ฉด
            sp.GetRequiredService<ProviderService>()                // consumer ๊ตฌํ˜„
              .ResolveRegistrations(tenant));                       // per-workspace ์ง‘ํ•ฉ โ†’ ProviderRegistration[]

// per-request (์š”์ฒญ ์Šค์ฝ”ํ”„์—์„œ resolve)
IChatClient client = scopedSp.GetRequiredService<IIronProwFactory>().ForTenant(workspaceId);
await client.GetResponseAsync(messages, options, ct);                // guarded: select/retry/fallback/guard

// consumer ๊ตฌํ˜„ โ€” ์›Œํฌ์ŠคํŽ˜์ด์Šค์˜ provider ์ง‘ํ•ฉ(์š”์ฒญ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ๋ฏธ๋ฆฌ ๋ณตํ˜ธํ™”ํ•ด ๋‘” secret ์„ sync ๋กœ ์ฝ๋Š”๋‹ค)
sealed class ProviderService
{
    public IReadOnlyList<ProviderRegistration> ResolveRegistrations(string workspaceId) => [];
}
  • ForTenant(tenant)์€ ํ•ด๋‹น ํ…Œ๋„ŒํŠธ์˜ provider ์ง‘ํ•ฉ์œผ๋กœ SelectingChatClient๋ฅผ ์žฌ์กฐ๋ฆฝํ•œ๋‹ค โ€” selector/guard/classifier/options๋Š” ๊ณต์œ (์žฌ์‚ฌ์šฉ), registry๋งŒ per-tenant. tenant ํ‚ค๋Š” iron-prow์— opaque(resolver๊ฐ€ ํ•ด์„).
  • IIronProwFactory๋Š” scoped๋กœ ๋“ฑ๋ก๋˜๋ฏ€๋กœ ์š”์ฒญ ์Šค์ฝ”ํ”„์—์„œ resolveํ•ด์•ผ resolverยทprovider factory๊ฐ€ ์š”์ฒญ ๋ฒ”์œ„ ์„œ๋น„์Šค(์˜ˆ: ๋ณตํ˜ธํ™”๋œ ์›Œํฌ์ŠคํŽ˜์ด์Šค secret)๋ฅผ ๋ณธ๋‹ค.
  • async๋Š” ์ƒ๋ฅ˜์—์„œ: resolver์™€ ProviderRegistration.ClientFactory๋Š” ๋ชจ๋‘ sync๋‹ค. ์›Œํฌ์ŠคํŽ˜์ด์Šค secret์˜ async DB ๋กœ๋“œยท๋ณตํ˜ธํ™”๋Š” consumer์˜ ์š”์ฒญ ๋ฏธ๋“ค์›จ์–ด์—์„œ ์ˆ˜ํ–‰ํ•ด scoped ์„œ๋น„์Šค์— stashํ•˜๊ณ , resolver๋Š” ๊ทธ๊ฒƒ์„ sync๋กœ ์ฝ๋Š”๋‹ค. (์š”์ฒญ๋‹น async ๋กœ๋“œ๊ฐ€ ํ•„์ˆ˜๋ผ๋ฉด ํ–ฅํ›„ ForTenantAsync ์˜ค๋ฒ„๋กœ๋“œ๊ฐ€ ์ˆœ์ˆ˜ additive๋กœ ์ถ”๊ฐ€๋  ์ˆ˜ ์žˆ๋‹ค.)
  • ๋ฐ˜ํ™˜๋œ client๋Š” ๋งค ์š”์ฒญ build(์—ฐ๊ฒฐ ์—†์Œยท์ €๋น„์šฉ)๋‹ค. consumer๊ฐ€ provider factory์—์„œ HttpClient ๋“ฑ disposable์„ ์ฅ๋ฉด ์ˆ˜๋ช…์€ consumer ์ฑ…์ž„์ด๋‹ค.
  • ๋‹จ์ผ ํ…Œ๋„ŒํŠธ ๊ฒฝ๋กœ(AddProvider/AddLMSupplyLocal, singleton IChatClient)๋Š” ์™„์ „ ๋ฌด๋ณ€๊ฒฝ์œผ๋กœ ๋ณ‘์กดํ•œ๋‹ค.

๋ฐ˜๋ณต ํ‡ดํ™” ์ •์ง€ (0.6.0+, opt-in)

์ž‘์€ ๋กœ์ปฌ ๋ชจ๋ธ์€ ๊ฐ™์€ ๋‹จ์–ดยท๊ตฌ์ ˆ์„ ํ† ํฐ ์ƒํ•œ๊นŒ์ง€ ๋ฐ˜๋ณตํ•˜๋Š” ํ‡ดํ™”์— ๋น ์ง€๊ณค ํ•œ๋‹ค("concisely concisely concisely โ€ฆ"). WithDegenerationStop()์€ ์–ด๋–ค IChatClient๋“ (๊ฒŒ์ดํŠธ์›จ์ด ํฌํ•จ) ๊ฐ์‹ธ์„œ, ์ถœ๋ ฅ ๋์ด ์งง์€ ๋‹จ์œ„(โ‰ค 60์ž, ๊ธ€์ž ํฌํ•จ)์˜ 4ํšŒ ์ด์ƒ ์—ฐ์† ๋ฐ˜๋ณต์ด ๋˜๋ฉด ์ƒ์„ฑ์„ ๋ฉˆ์ถ˜๋‹ค. ๊ฒ€์‚ฌ ๋Œ€์ƒ์€ ์ถœ๋ ฅ์˜ ๋งˆ์ง€๋ง‰ 240์ž๋‹ค. Markdown ๊ตฌ์กฐ์™€ ๊ตฌ๋‘์  ์—ฐ์†(----, | --- |, ====, ....)์€ ๋ฐ˜๋ณต์œผ๋กœ ๋ณด์ง€ ์•Š๋Š”๋‹ค. ํŒ์ •์€ ์ข‹์€ ๋‹ต์„ ๋Š์ง€ ์•Š๋Š” ์ชฝ์œผ๋กœ ์น˜์šฐ์ณ ์žˆ๋‹ค.

using IronProw.Core;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

IChatClient chat = sp.GetRequiredService<IChatClient>().WithDegenerationStop(o => o.MinRepeats = 4);

// ChatClientBuilder ํŒŒ์ดํ”„๋ผ์ธ์—์„œ๋Š”(ChatClientBuilder ๋Š” Microsoft.Extensions.AI ํŒจํ‚ค์ง€์— ์žˆ๋‹ค โ€” IronProw.Core ๋Š” .Abstractions ๋งŒ ์ฐธ์กฐ):
IChatClient piped = new ChatClientBuilder(inner)
    .Use(c => new DegenerationStopChatClient(c, new DegenerationOptions { MinRepeats = 4 }))
    .Build();

DegenerationOptions โ€” Window(๊ฒ€์‚ฌํ•  ์ถœ๋ ฅ ๋ ๊ธธ์ด, ๊ธฐ๋ณธ 240์ž) ยท MaxPeriod(๋ฐ˜๋ณต ๋‹จ์œ„ ์ตœ๋Œ€ ๊ธธ์ด, ๊ธฐ๋ณธ 60์ž) ยท MinRepeats(์—ฐ์† ๋ฐ˜๋ณต ํšŸ์ˆ˜, ๊ธฐ๋ณธ 4).

๋ฉˆ์ถค์€ ์กฐ์šฉํ•œ ๋์ด ์•„๋‹ˆ๋ผ ์‹ ํ˜ธ๋‹ค. ์ŠคํŠธ๋ฆฌ๋ฐ์˜ ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ์™€ ๋น„์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์˜ FinishReason์ด DegenerationStopChatClient.FinishReason("degeneration")์ด ๋˜๊ณ , ๋ฐ˜๋ณต๋œ ๋‹จ์œ„๋Š” AdditionalProperties[DegenerationStopChatClient.RepeatedUnitKey]์— ์‹ค๋ฆฐ๋‹ค. ์ด๋ฏธ ๋‚ด๋ณด๋‚ธ ํ…์ŠคํŠธ๋Š” ๊ทธ๋Œ€๋กœ ๋‘๊ณ , ์•ˆ์ชฝ ์ŠคํŠธ๋ฆผ์„ ๋ฒ„๋ ค ์ƒ์„ฑ์„ ์ทจ์†Œํ•œ๋‹ค. ๋น„์ŠคํŠธ๋ฆฌ๋ฐ ํ˜ธ์ถœ๋„ ์•ˆ์ชฝ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ฐ›์•„ ์กฐ๊ธฐ์— ๋ฉˆ์ถ˜๋‹ค. ํŒ์ •๋งŒ ํ•„์š”ํ•˜๋ฉด DegenerationDetector.FindRepeatingUnit(text)๋ฅผ ์“ด๋‹ค.

์˜ˆ๋ฐฉ ์ชฝ์€ ๋กœ์ปฌ ๊ฒฝ๋กœ(IronProw.LMSupply)์˜ ์ƒ˜ํ”Œ๋Ÿฌ๋‹ค. ChatOptions.FrequencyPenalty/PresencePenalty๊ฐ€ ์ „๋‹ฌ๋˜๊ณ , lm-supply ๊ณ ์œ ์˜ repetition_penalty(๊ธฐ๋ณธ 1.1)๋Š” ChatOptions.AdditionalProperties["repetition_penalty"]๋กœ ์ค€๋‹ค.

Crash-fallback ์ œํ•œ

LocalSafetyChatClient(๊ฐˆ๋ž˜ B)๋Š” IReadinessProbe๋กœ ๋กœ์ปฌ ์ถ”๋ก  ๋ถˆ๊ฐ€๋ฅผ ๊ฐ์ง€ํ•˜๊ณ , ๊ฒŒ์ดํŠธ์›จ์ด SelectingChatClient์˜ provider-level fallback์œผ๋กœ ์Šน๊ฒฉํ•œ๋‹ค. ์ด๊ฒƒ์€ ๊ฒŒ์ดํŠธ์›จ์ด ์ˆ˜์ค€ fallback(M2-4 ๋ฒ”์œ„)์ด๋‹ค.

์ง„์งœ ํ•˜๋“œ์›จ์–ด ์ˆ˜์ค€ fallback(์˜ˆ: ONNX GenAI DirectML โ†’ CPU)์€ upstream lm-supply์˜ ์ฑ…์ž„์ด๋‹ค.
lm-supply 0.35.x ๊ธฐ์ค€, ONNX GenAI ์ƒ์„ฑ ๊ฒฝ๋กœ๋Š” DirectML โ†’ CPU ํด๋ฐฑ์ด ์—†๋‹ค(์ž„๋ฒ ๋”ฉ ๊ฒฝ๋กœยทllama-server ๊ฒฝ๋กœ๋Š” ๋ณด์œ ). iron-prow๋Š” ์ด gap์„ ๊ฐ์‹ธ์ง€ ์•Š๋Š”๋‹ค โ€” upstream์ด ์ˆ˜์ •๋˜๋ฉด iron-prow ๋ณ€๊ฒฝ ์—†์ด ์ด๋“์ด ํก์ˆ˜๋œ๋‹ค.

๋‘ ๊ฐˆ๋ž˜ ํ•„์ˆ˜ ์„ค๊ณ„

iron-prow๋Š” ๋‘ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋…๋ฆฝ์ ์ด๋ฉด์„œ๋„ ์กฐํ•ฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ์ปค๋ฒ„ํ•œ๋‹ค:

๊ฐˆ๋ž˜ ์ง„์ž…์  ์ฑ…์ž„
A. Frontier / LAN AddIronHiveOpenAI ยท AddIronHiveAnthropic ยท AddIronHiveGoogleAI ยท AddIronHiveGpuStack ยท AddIronHiveOpenAICompatible provider ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ, ์šฐ์„ ์ˆœ์œ„ ์„ ํƒ, retry, provider-level fallback, ์ „ํ™˜ ์ด๋ฒคํŠธ(OnTransition)
B. local-provider safety AddLMSupplyLocal model-ID preflight, readiness gate, length-bounding, crashโ†’fallback

๋‘ ๊ฐˆ๋ž˜๋Š” ๋”ฐ๋กœ ์“ธ ์ˆ˜๋„, ํ•œ ๊ฒŒ์ดํŠธ์›จ์ด ์•ˆ์—์„œ ์กฐํ•ฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
UseFluxGuard()๋Š” ๋‘ ๊ฐˆ๋ž˜ ๊ณตํ†ต โ€” ๊ด€๋ฌธ์—์„œ ์ผ๊ด„ ์ ์šฉ๋œ๋‹ค.

โš ๏ธ The guard is opt-in. AddIronProw() installs a default NullGuard that allows all traffic. A gateway without UseFluxGuard() (or a custom UseGuard(...)) performs NO input/output guardrail checks. Always register a guard in production.

What the guard inspects, and when. A blocked input or output throws GuardException (Reason says why).

  • GetResponseAsync โ€” the input before the call, the response after it; a blocked response is never returned.
  • GetStreamingResponseAsync โ€” the input before the call, the aggregated output when the stream ends. Chunks arrive as they are generated, so a blocked output ends the stream with GuardException after its chunks were yielded: a streaming consumer discards or retracts what it showed when the exception arrives. (0.9.0+; before, streamed output was never inspected.)
  • A consumer that stops reading early โ€” WithDegenerationStop() around the gateway, which also serves GetResponseAsync through the stream โ€” still gets the output inspected, on disposal. (0.9.0+; before, WithDegenerationStop() around the gateway turned the output guard off for every call.)

See also

  • docs/ADOPTION.md โ€” ์ž์ฒด ๋ฐฐ์„ (providerยทresilienceยทguardrail)์„ ๊ฐ€์ง„ ์•ฑ์˜ ์ด๊ด€ ๊ฐ€์ด๋“œ

License

MIT

Changes from Previous Version

No structural changes between v0.15.0 and v0.15.1.

Adoption Assessment

Assess this package against its supported frameworks, license, maintenance signals and dependency graph before making it a project standard.

Compatibility metadata lists 1 target framework; confirm the exact target used by your application.

Related Guides & Workflows

Learn how to evaluate, integrate, or audit this package in your .NET project.