iron-prow adapter: ironhive providers as gateway candidates.
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).
๋ฒ์๋ ํ ๋ฒ์ ์์ ํ ํธ์ถ(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). ํ์ค/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 ๋ถํ์
์ฌ์ฉ์ ์ง์ (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, Anthropicx-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-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 ์๋น์์๊ฒ๋ ๊ฒ์ดํธ์จ์ด์ 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 |
๋ ๊ฐ๋๋ ๋ฐ๋ก ์ธ ์๋, ํ ๊ฒ์ดํธ์จ์ด ์์์ ์กฐํฉํ ์๋ ์๋ค.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
docs/ADOPTION.mdโ ์์ฒด ๋ฐฐ์ (providerยทresilienceยทguardrail)์ ๊ฐ์ง ์ฑ์ ์ด๊ด ๊ฐ์ด๋
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.