Provider file
The provider file names each provider, its kind, its model, where its key comes from and where its calls go, and maps
the roles to them. It is intentsharp.providers.json, or its .yaml or .yml spelling, in a document store, and it
starts with intentsharp: providers/1. Its keys are on Provider secrets: secret references, a
resolver of your own, endpoints and plain http, and the edits flagged as widening.
{
"intentsharp": "providers/1",
"providers": [
{ "name": "main", "kind": "anthropic", "secret": "env:ANTHROPIC_API_KEY", "model": "<model id>" },
{ "name": "backup", "kind": "openai", "secret": "env:OPENAI_API_KEY", "model": "<model id>", "embeddingModel": "<embedding model id>" },
{ "name": "local", "kind": "openai", "endpoint": "http://localhost:11434/v1/", "model": "<model id>" }
],
"roles": {
"answer": ["main", "backup"],
"judge": "backup",
"embedding": "backup",
"maintenance": "local"
},
"aliases": {
"quick": { "provider": "main", "model": "<smaller model id>" }
}
}
// "policy" holds intentsharp.providers.json. WithDocumentStore names the same store, so an edit submitted
// through the client is checked by the provider file's validator before it is saved.
using var store = new FolderDocumentStore("policy");
using var client = new IntentClientBuilder()
.UseProviderOptions(store)
.WithDocumentStore(store)
.Build();
Under dependency injection the client reads the provider file of the registered document store
(Providers). The same options can come from a configuration section, with
ConfigurationProviderOptionsSource, or from an IProviderOptionsSource of your own; providers added in code with
UseProvider need no entry.
Entries
| Key | What it says |
|---|---|
name |
The provider's name, unique ignoring case: in results, logs, rate limiting, the roles and AIRequestOptions.Provider. |
kind |
openai, anthropic, chat-client (with IntentSharp.Extensions.AI) or a kind of your own (AddProviderFactory). |
model |
The model the provider asks for when the call and the role name none. |
secret |
Where the key is read from, never the key itself (Provider secrets). An entry with no secret sends no key. |
endpoint |
The address its requests go to, when it is not the vendor's (Provider secrets). |
embeddingModel, fallbackModel |
The model for embeddings, and the model when the entry answers as a fallback. |
capabilities |
What the model accepts, such as { "temperature": true }. |
key |
For the chat-client kind, the service key of the chat client that receives the calls. |
allowInsecureHttp |
true lets an entry send its key over plain http (Provider secrets). false when absent. |
A field that holds a key itself, such as apiKey, does not load (Provider secrets).
Roles and chains
A role names one provider or an ordered chain of them. answer serves the calls; judge serves the judge, and
defaults to the first provider of answer; embedding serves embedding calls; maintenance serves the library's own
calls, such as compiling a rule written as intent, and also defaults to the first provider of answer. A role of
another name serves the calls that name it in AIRequestOptions.Role. A link of a chain is a provider, such as
"main", a provider and a model, such as "main@<model id>", or an alias, such as "@quick". A call moves to the
next link of the chain after a failure that falls back, or when a rule or setting refused the link before it
(Providers).
Model aliases
aliases gives a short name to a provider and a model. A call that names an alias as its model goes to that provider
with that model; an alias the file does not define throws ArgumentException:
// "@quick" is an alias of the provider file: it chooses the provider main and the model the alias names.
var options = new AIRequestOptions { Model = "@quick" };
var summary = await email.AIAsync(client, "Summarize this email in one sentence.", options);
IntentSharp ships no model id: a call whose provider has no model in the call, the alias, the role or the entry throws
ProviderConfigurationException, naming the provider and the role, and nothing is sent.
The hosts a call may reach
A rule on request.host, at the Request checkpoint, pins the hosts a call may reach, whatever the provider file says:
intentsharp: rules/1
set: hosts
checkpoint: request
rules:
- id: known-hosts
subject: request.host
allow: [api.anthropic.com, api.openai.com, localhost]
message: Calls go only to the hosts this application uses.
// hosts.rules.yaml, listed in the store's intentsharp.guardrails.json, is checked before every model call.
var client = new IntentClientBuilder()
.UseProviderOptions(store)
.WithDocumentStore(store)
.WithGuardrailFiles(store, "hosts")
.Build();
The host is in lower case, with no port; an international name is in its xn-- form, and an IPv6 address has no
brackets. For the openai and anthropic kinds, and any provider built on HttpProviderBase, it is the host of the
address the provider sends its requests to. A chat client reports the host its metadata names, which IntentSharp
cannot check against where the client sends its requests. Under a rule on request.host, a provider that reports no
host is refused.
The output ends with two calls the set hosts refuses: one to a host the set does not name, and one whose provider
reports no host. It starts with the refusal and the edits that Provider secrets
explains:
The entry gateway: plain http, a secret and no allowInsecureHttp:
intentsharp.providers.json:4: providers[0].endpoint is a plain http address of a host that is not loopback, so the entry's secret would be sent unencrypted; use https, or set allowInsecureHttp: true on the entry to accept that.
An edit of the model of main:
Status: Applied, WidensSecurity: false
An edit of the endpoint and the secret of backup:
Status: Applied, WidensSecurity: true
providers.backup.endpoint
providers.backup.secret
A call to gateway.example.net, under the set hosts:
Success: false
[Guardrail] attempt 1: The answer call was refused at the Request checkpoint: Calls go only to the hosts this application uses (rule known-hosts in guardrail set "hosts").
A call whose provider reports no host, under the set hosts:
Success: false
[Guardrail] attempt 1: The answer call was refused at the Request checkpoint: Calls go only to the hosts this application uses (rule known-hosts in guardrail set "hosts").
The chat-client kind
With IntentSharp.Extensions.AI, under dependency injection, an entry of the kind chat-client names a chat client you
register under a service key, its key, once AddChatClientProvider() has added the kind
(Microsoft.Extensions.AI). The chat client holds its own configuration, so the
entry has no secret; its key decides which client receives the calls, so a change of it is flagged as widening.
Without a file on disk
The openai and anthropic kinds are made only from provider entries; no builder method creates one in code. With no
files on disk, keep the provider file in an InMemoryDocumentStore. It is read as a file is, its secret stays a
reference, and a new version applies to the next call:
// No file on disk: the provider file is a document of an in-memory store, read as a file would be.
var store = new InMemoryDocumentStore();
await store.SaveAsync(new DocumentSave
{
Id = new DocumentId("intentsharp.providers.json"),
Text = """
{
"intentsharp": "providers/1",
"providers": [ { "name": "main", "kind": "openai", "secret": "env:OPENAI_API_KEY", "model": "<model id>" } ],
"roles": { "answer": "main" }
}
""",
Author = new EditAuthor { Kind = AuthorKind.Developer, Label = "startup" },
});
var client = new IntentClientBuilder()
.UseProviderOptions(store)
.Build();