iron-prow local-provider safety adapter: model-ID preflight, readiness gate, and length-bounding for lm-supply local inference.
Install
| Product | Versions |
|---|---|
| .NET |
README
iron-prow
safe-inference gateway โ every LLM call's first gate.
provider selection (frontier โจ LAN GpuStack) ยท guardrail ยท resilience โ delivered as a standardMicrosoft.Extensions.AI.IChatClient.
๋ ๊ฐ๋ ํ์: frontier/LAN ๊ฒ์ดํธ์จ์ด(A) and local-provider safety(B).
์ญํ ยท๋ฒ์ยท์ค๊ณ ๊ทผ๊ฑฐ๋ CHARTER.md ์ฐธ์กฐ.
์ด๋ฏธ ์์ฒด ๋ฐฐ์ (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 ์ด๋ํฐ๋ฅผ ์ ๊ณตํ๋ค:
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). ํ์ค/v1API ํ๋ฉด์ ๋ ธ์ถํ๋ ์๋ํฌ์ธํธ๋ฅผ ๋์์ผ๋ก ํ๋ฉฐ ๊ธฐ๋ณธ 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 ๋ถํ์
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-supplyThinkingMode๋ก ๋ฒ์ญํ๋ค(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 ์๋น์(์: textree)์๊ฒ๋ ๊ฒ์ดํธ์จ์ด์ 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, singletonIChatClient)๋ ์์ ๋ฌด๋ณ๊ฒฝ์ผ๋ก ๋ณ์กดํ๋ค.
๋ฐ๋ณต ํดํ ์ ์ง (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 |
์ด๋ ํ ๊ฐ๋๋ง ๊ตฌํํ๋ฉด ์์์ ์ ๋ฐ์ ๋์น๋ค (CHARTER ยง๊ธฐ๋ฅ ํ๋ฉด).UseFluxGuard()๋ ๋ ๊ฐ๋ ๊ณตํต โ ๊ด๋ฌธ์์ ์ผ๊ด ์ ์ฉ๋๋ค.
โ ๏ธ The guard is opt-in.
AddIronProw()installs a defaultNullGuardthat allows all traffic. A gateway withoutUseFluxGuard()(or a customUseGuard(...)) 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 withGuardExceptionafter 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 servesGetResponseAsyncthrough 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
CHARTER.mdโ iron-prow ์ ์ฒด์ฑ, ๋ฒ์, ์์กด ๊ท์น, ๋ก๋๋งต ์ต์ปค
License
MIT
Changes from Previous Version
Adoption Assessment
Assess this package against its supported frameworks, license, maintenance signals and dependency graph before making it a project standard.
Related Guides & Workflows
Learn how to evaluate, integrate, or audit this package in your .NET project.