Recursive AI

Microsoft.Extensions.AI bridge

All onellm docs

RecursiveAI.OneLlm.Extensions.AI connects OneLLM to the Microsoft.Extensions.AI abstractions (version 10.11 of Microsoft.Extensions.AI.Abstractions) in both directions. It references the OneLLM core package and that one package.

DirectionWhatEntry point
OneLLM → Microsoft.Extensions.AIA model as IChatClientruntime.AsChatClient("claude-sonnet")
A model as IEmbeddingGenerator<string, Embedding<float>>runtime.AsEmbeddingGenerator(model)
A model as ISpeechToTextClient, ITextToSpeechClient, IImageGeneratorruntime.AsSpeechToTextClient(model), AsTextToSpeechClient, AsImageGenerator
A OneLlm tool as an AIFunctiontool.AsAIFunction(timeProvider)
Microsoft.Extensions.AI → OneLLMAny IChatClient (and an optional embedding generator) as a providerruntime.Providers.AddChatClient("name", client)
An AIFunction as a toolfunction.AsTool()

The speech, text-to-speech and image abstractions are marked experimental by Microsoft.Extensions.AI (MEAI001). The bridge opts in inside one source file; code that uses OneLlmSpeechToTextClient, OneLlmTextToSpeechClient or OneLlmImageGenerator sees the same diagnostic on the Microsoft.Extensions.AI types it touches.

A OneLLM model as a client

C#
var rt = new Runtime();
IChatClient client = rt.AsChatClient("claude-sonnet");
ChatResponse r = await client.GetResponseAsync("Summarize this", new ChatOptions { MaxOutputTokens = 500 });
decimal dollars = (decimal)r.AdditionalProperties![OneLlmProperties.CostTotal]!;

The model resolves through the runtime's catalog, so requests are adjusted to what the model accepts (a parameter it rejects is dropped and reported in OneLlmProperties.Warnings) and every call is priced. ChatOptions.ModelId names another catalog model for one request. The client does not run functions: wrap it with Microsoft.Extensions.AI's function-invoking middleware, or use a OneLLM agent.

Over a bare provider the client sends the model name as given and prices nothing: new OneLlmChatClient(provider, "model-name", timeProvider); pass a ResolvedModel instead of the name to adjust and price.

Options

ChatOptionsOneLLM ChatRequest
Instructions, System/Developer messagesSystem (joined with a blank line, instructions first)
MaxOutputTokens, Temperature, TopP, StopSequencesthe same fields
Tools (AIFunctionDeclaration)Tools; the declaration's strictJsonSchema property sets Strict
Tools (HostedWebSearchTool, HostedCodeInterpreterTool)HostedTools web_search, code_interpreter
ToolModeToolChoice (Auto, None, Required, or the named tool)
AllowMultipleToolCallsParallelToolCalls
ResponseFormatResponseFormat: text, any JSON, or the JSON schema with its name
Reasoning.EffortReasoning.Effort (none, low, medium, high, xhigh)
Reasoning.OutputReasoning.Summary: Summary is auto, Full is detailed. Another spelling goes through onellm.extraParameters.
AdditionalProperties["onellm.cache"], ["onellm.serviceTier"], ["onellm.userId"], ["onellm.extraParameters"]Cache, ServiceTier, UserId, ExtraParameters
Seed, TopK, FrequencyPenalty, PresencePenalty, ConversationIdnot carried; the first four are named in a warning

What comes back

Text, tool calls and reasoning arrive as TextContent, FunctionCallContent and TextReasoningContent in the order the model produced them, so the order of parts inside a turn is the order of the Contents list. Streaming sends text and reasoning as deltas; a tool call arrives complete, once its arguments have finished streaming. A stream ends with one update that carries FinishReason and one UsageContent, so Microsoft.Extensions.AI's ToChatResponse() counts usage once.

OneLLMMicrosoft.Extensions.AI
Usage.TotalInputTokens (new + cache read + cache write)UsageDetails.InputTokenCount
Usage.CacheReadInputTokensCachedInputTokenCount
Usage.CacheCreationInputTokensAdditionalCounts["onellm.cacheCreationInputTokens"]
Usage.Counters["reasoningTokens"]ReasoningTokenCount
Usage.OutputTokensOutputTokenCount
FinishReason Stop, Length, ToolCalls, ContentFilterthe matching ChatFinishReason; Refusal and PauseTurn become refusal and pause_turn
TextPart annotationsCitationAnnotation (title, URL, snippet, span; the original Annotation is RawRepresentation)
AssetPart with inline bytes / a public URLDataContent / UriContent; a stored asset with neither is kept whole in OneLlmPartContent
FileRefPartHostedFileContent
JsonPartTextContent holding the JSON text
RefusalPartTextContent with onellm.refusal = true
ReasoningPartTextReasoningContent: Text is the display text, ProtectedData the provider's opaque JSON, onellm.provider names the provider
hosted tool calls, provider partsOneLlmPartContent carrying the Part

Keys in AdditionalProperties

On the ChatResponse (and on the final UsageContent of a stream), and on an embedding result:

KeyValue
onellm.costthe call's OneLlm.Messages.Cost: line items, each with its counter, quantity, unit price and amount, and the total
onellm.cost.totalthe total as a decimal, in US dollars
onellm.warningsstring[]: stripped parameters, a missing price, options the bridge could not carry

Cost is present only when the model is priced in the catalog. Money is always decimal.

The lossy edges, carried so they survive a round trip through Microsoft.Extensions.AI and back:

KeyOnMeaning
onellm.whenChatMessage, FunctionCallContentthe entry's when expression
onellm.injected, onellm.injectedByFunctionCallContentthe call was injected by the runtime rather than asked for by the model, and by what
onellm.routineFunctionCallContentthe tool source reference the call resolved to
onellm.invalidInputsFunctionCallContentthe raw argument text when the model's arguments were not a JSON object (Arguments is then null)
onellm.isError, onellm.resultPartsFunctionResultContentthe tool reported an error; the original result parts, kept in process

The speech, text-to-speech and image types carry usage on their Usage property; speech-to-text and text-to-speech responses also carry the cost keys. ImageGenerationResponse has no property dictionary, so an image call's cost is not on it: use the runtime's result.

A client as a provider

C#
var rt = new Runtime { Catalog = ModelCatalog.Create([CatalogLayer.FromFile(CatalogLayer.User, "my-models.yaml")]) };
rt.Providers.AddChatClient("acme", someChatClient, someEmbeddingGenerator);   // someEmbeddingGenerator is optional
AskResult a = await rt.AskAsync(new AskRequest { Model = "acme-large", Prompt = "hi" });   // priced from my-models.yaml

The catalog entry supplies what the client cannot: the model's name patterns, context window, capabilities and prices. Its name must be the provider name passed to AddChatClient. The client's usage becomes OneLLM Usage: InputTokenCount is the whole input and CachedInputTokenCount the part read from a cache, so the cached tokens are priced at the cache-read rate and not twice. Tool definitions go out as declarations, results come back as function-result content, and a streamed update list becomes OneLLM stream events that build the same Message a non-streaming call builds.

Tools

tool.AsAIFunction(time) passes the tool's name, description, input schema and output schema through unchanged, so function.JsonSchema equals tool.Definition.InputSchema. The function runs the tool in a context with no capabilities; pass a Func<string, ToolContext> to grant the files, shell or network the tool needs. A tool that reports an error makes the function throw InvalidOperationException with the tool's message, which Microsoft.Extensions.AI's function-invoking client reports to the model. function.AsTool() does the reverse: the schema is read from JsonSchema, an ApprovalRequiredAIFunction becomes a tool with RequiresApproval, and a result becomes text (a string), a JSON part (a JSON value) or parts (a content list). The name must be a valid tool name (letters, digits, _, -, at most 64): a function named has.dot is refused with ArgumentException.

Tools made with AIFunctionFactory.Create use reflection and are not trim or AOT safe; that is a property of Microsoft.Extensions.AI, not of the bridge. Declarations, AsAIFunction and AsTool over a hand-written or generated ITool are.

Reads like

A table for readers who know the Microsoft.Extensions.AI calls. It maps calls in the public documentation to OneLLM calls and makes no comparison.

Microsoft.Extensions.AIOneLLM
chatClient.GetResponseAsync(messages, options)Llm.GenerateAsync(model, prompt, attachments); Runtime.AskAsync(new AskRequest { … }); Llm.Chat(model) for turns
chatClient.GetStreamingResponseAsync(...)Llm.StreamAsync(model, prompt); Llm.StreamTextAsync(model, prompt)
GetResponseAsync<T> (structured output)Llm.AskAsync<T>(model, prompt, typeInfo)
ChatOptions.ModelIdthe model argument: a catalog name, alias or provider:model
ChatOptions.Tools with AIFunctionFactory.Create[Tool] methods (generated, AOT safe) or Tool.Create(delegate); ITool
FunctionInvokingChatClientthe agent runners and their tool loop
UsageDetailsUsage and Cost (line items) on every result
IEmbeddingGenerator<string, Embedding<float>>.GenerateAsyncLlm.EmbedAsync(model, text)
ISpeechToTextClient.GetTextAsyncthe transcription provider capability (ISpeechToTextProvider)
ITextToSpeechClient.GetAudioAsyncthe speech provider capability (ITextToSpeechProvider)
IImageGenerator.GenerateAsyncthe image provider capability (IImageProvider)
ChatClientBuilder pipelinesruntime.AsChatClient(model) as the inner client of any pipeline
AIFunctionITool (function.AsTool(), tool.AsAIFunction(time))
OpenTelemetryChatClientOneLLM emits GenAI telemetry itself; see telemetry.md