Skip to content
Open
115 changes: 115 additions & 0 deletions docs/HostedConversation-ProviderMapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# IHostedConversationClient — Provider Mapping Report

## Overview

`IHostedConversationClient` is an abstraction for managing server-side (hosted) conversation state across AI providers. It provides a common interface for creating, retrieving, deleting, and managing messages within persistent conversations, decoupling application code from provider-specific conversation/thread/session APIs. Each provider maps these operations to its native primitives, with escape hatches (`RawRepresentation`, `RawRepresentationFactory`, `AdditionalProperties`) for accessing provider-specific features.

## Interface Operations

| Operation | Description | Return Type |
|-----------|-------------|-------------|
| `CreateAsync` | Creates a new hosted conversation | `HostedConversation` |
| `GetAsync` | Retrieves conversation by ID | `HostedConversation` |
| `DeleteAsync` | Deletes a conversation | `void` (Task) |
| `AddMessagesAsync` | Adds messages to a conversation | `void` (Task) |
| `GetMessagesAsync` | Lists messages in a conversation | `IAsyncEnumerable<ChatMessage>` |

## Provider Mapping

### OpenAI (Implemented)

- Maps to `ConversationClient` in `OpenAI.Conversations` namespace
- Full CRUD support via protocol-level APIs
- `RawRepresentation` set to `ClientResult` objects
- ConversationId integrates with `ChatOptions.ConversationId` for inference via `OpenAIResponsesChatClient`
- Metadata limited to 16 key-value pairs (max 64 char keys, 512 char values)

### Azure AI Foundry

- Maps to Thread/Message APIs in Agent Service SDK
- `CreateAsync` → `threads.create()`
Comment thread
rogerbarreto marked this conversation as resolved.
Outdated
- `GetAsync` → `threads.get()`
- `DeleteAsync` → `threads.delete()`
- `AddMessagesAsync` → `messages.create()` (one per message)
- `GetMessagesAsync` → `messages.list()`
- **Gaps**: Thread model includes Run/Agent concepts not in our abstraction; use `AdditionalProperties` for agent-specific metadata

### AWS Bedrock

- Maps to Session Management APIs
- `CreateAsync` → `CreateSession` with optional encryption/metadata
- `GetAsync` → `GetSession`
- `DeleteAsync` → `DeleteSession`
- `AddMessagesAsync` → `PutInvocationStep` (different item model)
- `GetMessagesAsync` → `GetInvocationSteps` (requires translation)
- **Gaps**: Session status (ACTIVE/EXPIRED/ENDED) not in abstraction; encryption config is provider-specific; use `AdditionalProperties` or `RawRepresentationFactory`

### Google Gemini

- Maps to Interactions API
- `CreateAsync` → `interactions.create()` (creates an interaction, not a "conversation" per se)
- `GetAsync` → `interactions.get()`
- `DeleteAsync` → `interactions.delete()`
- `AddMessagesAsync` → No direct equivalent; use `interactions.create()` with `previous_interaction_id` chain
- `GetMessagesAsync` → `interactions.get().outputs` (retrieves outputs, not full message history)
- **Gaps**: Interactions are individual turns, not conversation containers. AddMessages requires creating new interactions chained via `previous_interaction_id`. Provider adapter would need to manage this mapping.

### Anthropic

- **No native conversation CRUD API** — requires local adapter
- Server-side features that CAN assist:
- **Prompt Caching** (`cache_control`): Stores KV cache of message prefixes (5min/1hr TTL). Adapter should auto-apply cache breakpoints.
- **Context Compaction** (beta): Server-side summarization when conversations exceed token threshold
- **Files API** (beta): Store documents server-side for reference across requests
- **Containers** (beta): Server-side execution state with reusable IDs
- Implementation approach: `LocalHostedConversationClient<TStore>` using local storage (in-memory, SQLite, Redis) with automatic prompt caching optimization
- **Gaps**: All operations are simulated client-side. No server-side conversation persistence.

### Ollama / Local Models

- **No server-side state** at all
- Implementation: Same local adapter pattern as Anthropic but without prompt caching optimization
- **Gaps**: Same as Anthropic — entirely client-side simulation

## Escape Hatches for Provider-Specific Features

### RawRepresentation

Every `HostedConversation` response carries `RawRepresentation` (the underlying provider object). This gives access to 100% of provider functionality:

```csharp
var conversation = await client.CreateAsync();
var openAIResult = (ClientResult)conversation.RawRepresentation; // Access any OpenAI-specific data
```

### RawRepresentationFactory

`HostedConversationCreationOptions.RawRepresentationFactory` allows passing provider-specific creation options:

```csharp
var options = new HostedConversationCreationOptions
{
RawRepresentationFactory = client => new ConversationCreationOptions
{
// Any provider-specific settings
}
};
Comment thread
rogerbarreto marked this conversation as resolved.
```

### AdditionalProperties

`HostedConversation.AdditionalProperties` and `HostedConversationCreationOptions.AdditionalProperties` carry provider-specific data that doesn't fit the common abstraction.

Comment thread
rogerbarreto marked this conversation as resolved.
## Feature Coverage Matrix

| Feature | OpenAI | Azure | Bedrock | Gemini | Anthropic | Ollama |
|---------|--------|-------|---------|--------|-----------|--------|
| Create | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ⚠️ Local | ⚠️ Local |
| Get | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ⚠️ Local | ⚠️ Local |
| Delete | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ⚠️ Local | ⚠️ Local |
| AddMessages | ✅ Native | ✅ Native | ⚠️ Translated | ⚠️ Chained | ⚠️ Local | ⚠️ Local |
| GetMessages | ✅ Native | ✅ Native | ⚠️ Translated | ⚠️ Partial | ⚠️ Local | ⚠️ Local |
| Metadata | ✅ 16 KV | ✅ | ✅ | ✅ | ⚠️ Local | ⚠️ Local |
| RawRepresentation | ✅ ClientResult | ✅ AgentThread | ✅ Session | ✅ Interaction | N/A | N/A |

Legend: ✅ = Direct mapping, ⚠️ = Requires translation/local adapter
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Provides an optional base class for an <see cref="IHostedConversationClient"/> that passes through calls to another instance.
/// </summary>
/// <remarks>
/// This is recommended as a base type when building clients that can be chained around an underlying <see cref="IHostedConversationClient"/>.
/// The default implementation simply passes each call to the inner client instance.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIHostedConversation, UrlFormat = DiagnosticIds.UrlFormat)]
public class DelegatingHostedConversationClient : IHostedConversationClient
{
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingHostedConversationClient"/> class.
/// </summary>
/// <param name="innerClient">The wrapped client instance.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerClient"/> is <see langword="null"/>.</exception>
protected DelegatingHostedConversationClient(IHostedConversationClient innerClient)
{
InnerClient = Throw.IfNull(innerClient);
}

/// <inheritdoc />
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <summary>Gets the inner <see cref="IHostedConversationClient" />.</summary>
protected IHostedConversationClient InnerClient { get; }

/// <inheritdoc />
public virtual Task<HostedConversation> CreateAsync(
HostedConversationCreationOptions? options = null,
CancellationToken cancellationToken = default) =>
InnerClient.CreateAsync(options, cancellationToken);

/// <inheritdoc />
public virtual Task<HostedConversation> GetAsync(
string conversationId,
CancellationToken cancellationToken = default) =>
InnerClient.GetAsync(conversationId, cancellationToken);

/// <inheritdoc />
public virtual Task DeleteAsync(
string conversationId,
CancellationToken cancellationToken = default) =>
InnerClient.DeleteAsync(conversationId, cancellationToken);

/// <inheritdoc />
public virtual Task AddMessagesAsync(
string conversationId,
IEnumerable<ChatMessage> messages,
CancellationToken cancellationToken = default) =>
InnerClient.AddMessagesAsync(conversationId, messages, cancellationToken);

/// <inheritdoc />
public virtual IAsyncEnumerable<ChatMessage> GetMessagesAsync(
string conversationId,
CancellationToken cancellationToken = default) =>
InnerClient.GetMessagesAsync(conversationId, cancellationToken);

/// <inheritdoc />
public virtual object? GetService(Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(serviceType);

// If the key is non-null, we don't know what it means so pass through to the inner service.
return
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
InnerClient.GetService(serviceType, serviceKey);
}

/// <summary>Provides a mechanism for releasing unmanaged resources.</summary>
/// <param name="disposing"><see langword="true"/> if being called from <see cref="Dispose()"/>; otherwise, <see langword="false"/>.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
InnerClient.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>Represents a hosted conversation.</summary>
[Experimental(DiagnosticIds.Experiments.AIHostedConversation, UrlFormat = DiagnosticIds.UrlFormat)]
public class HostedConversation
{
/// <summary>Gets or sets the conversation identifier.</summary>
public string? ConversationId { get; set; }

/// <summary>Gets or sets the creation timestamp.</summary>
public DateTimeOffset? CreatedAt { get; set; }

/// <summary>Gets or sets metadata associated with the conversation.</summary>
public AdditionalPropertiesDictionary<string>? Metadata { get; set; }
Comment thread
rogerbarreto marked this conversation as resolved.
Outdated

/// <summary>Gets or sets the raw representation of the conversation from the underlying provider.</summary>
/// <remarks>
/// If a <see cref="HostedConversation"/> is created to represent some underlying object from another object
/// model, this property can be used to store that original object. This can be useful for debugging or
/// for enabling a consumer to access the underlying object model if needed.
/// </remarks>
[JsonIgnore]
public object? RawRepresentation { get; set; }

/// <summary>Gets or sets any additional properties associated with the conversation.</summary>
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.AI;

/// <summary>Provides a collection of static methods for extending <see cref="IHostedConversationClient"/> instances.</summary>
[Experimental(DiagnosticIds.Experiments.AIHostedConversation, UrlFormat = DiagnosticIds.UrlFormat)]
public static class HostedConversationClientExtensions
{
/// <summary>Asks the <see cref="IHostedConversationClient"/> for an object of type <typeparamref name="TService"/>.</summary>
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
/// <param name="client">The client.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="IHostedConversationClient"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public static TService? GetService<TService>(this IHostedConversationClient client, object? serviceKey = null)
{
_ = Throw.IfNull(client);

return client.GetService(typeof(TService), serviceKey) is TService service ? service : default;
}

/// <summary>
/// Asks the <see cref="IHostedConversationClient"/> for an object of the specified type <paramref name="serviceType"/>
/// and throws an exception if one isn't available.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="serviceType">The type of object being requested.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object.</returns>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">No service of the requested type for the specified key is available.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of services that are required to be provided by the <see cref="IHostedConversationClient"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public static object GetRequiredService(this IHostedConversationClient client, Type serviceType, object? serviceKey = null)
{
_ = Throw.IfNull(client);
_ = Throw.IfNull(serviceType);

return
client.GetService(serviceType, serviceKey) ??
throw Throw.CreateMissingServiceException(serviceType, serviceKey);
}

/// <summary>
/// Asks the <see cref="IHostedConversationClient"/> for an object of type <typeparamref name="TService"/>
/// and throws an exception if one isn't available.
/// </summary>
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
/// <param name="client">The client.</param>
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
/// <returns>The found object.</returns>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">No service of the requested type for the specified key is available.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly typed services that are required to be provided by the <see cref="IHostedConversationClient"/>,
/// including itself or any services it might be wrapping.
/// </remarks>
public static TService GetRequiredService<TService>(this IHostedConversationClient client, object? serviceKey = null)
{
_ = Throw.IfNull(client);

if (client.GetService(typeof(TService), serviceKey) is not TService service)
{
throw Throw.CreateMissingServiceException(typeof(TService), serviceKey);
}

return service;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>Provides metadata about an <see cref="IHostedConversationClient"/>.</summary>
[Experimental(DiagnosticIds.Experiments.AIHostedConversation, UrlFormat = DiagnosticIds.UrlFormat)]
public class HostedConversationClientMetadata
{
/// <summary>Initializes a new instance of the <see cref="HostedConversationClientMetadata"/> class.</summary>
/// <param name="providerName">
/// The name of the hosted conversation provider, if applicable. Where possible, this should map to the
/// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems.
/// </param>
/// <param name="providerUri">The URL for accessing the hosted conversation provider, if applicable.</param>
public HostedConversationClientMetadata(string? providerName = null, Uri? providerUri = null)
{
ProviderName = providerName;
ProviderUri = providerUri;
}

/// <summary>Gets the name of the hosted conversation provider.</summary>
/// <remarks>
/// Where possible, this maps to the appropriate name defined in the
/// OpenTelemetry Semantic Conventions for Generative AI systems.
/// </remarks>
public string? ProviderName { get; }

/// <summary>Gets the URL for accessing the hosted conversation provider.</summary>
public Uri? ProviderUri { get; }
}
Loading
Loading