diff --git a/README.md b/README.md index 07bc9b4d..1866518a 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ The Foundry Local SDK makes it easy to integrate local AI models into your appli 2. Use the SDK in your application as follows: ```csharp using Microsoft.AI.Foundry.Local; + using Microsoft.AI.Foundry.Local.OpenAI; var config = new Configuration { AppName = "foundry_local_samples" }; await FoundryLocalManager.CreateAsync(config); @@ -154,7 +155,7 @@ The Foundry Local SDK makes it easy to integrate local AI models into your appli var chatClient = await model.GetChatClientAsync(); var messages = new List { - new() { Role = "user", Content = "What is the golden ratio?" } + new() { Role = ChatMessageRole.User, Content = "What is the golden ratio?" } }; await foreach (var chunk in chatClient.CompleteChatStreamingAsync(messages)) diff --git a/samples/cs/Directory.Packages.props b/samples/cs/Directory.Packages.props index e5ba306b..b1d67c33 100644 --- a/samples/cs/Directory.Packages.props +++ b/samples/cs/Directory.Packages.props @@ -6,8 +6,7 @@ - - + diff --git a/samples/cs/model-management-example/Program.cs b/samples/cs/model-management-example/Program.cs index a34d2737..f22d5211 100644 --- a/samples/cs/model-management-example/Program.cs +++ b/samples/cs/model-management-example/Program.cs @@ -1,5 +1,5 @@ using Microsoft.AI.Foundry.Local; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Microsoft.AI.Foundry.Local.OpenAI; using System.Diagnostics; CancellationToken ct = new CancellationToken(); @@ -112,7 +112,7 @@ await model.DownloadAsync(progress => // Create a chat message List messages = new() { - new ChatMessage { Role = "user", Content = "Why is the sky blue?" } + new ChatMessage { Role = ChatMessageRole.User, Content = "Why is the sky blue?" } }; // You can adjust settings on the chat client diff --git a/samples/cs/native-chat-completions/Program.cs b/samples/cs/native-chat-completions/Program.cs index 033786b1..8369ef96 100644 --- a/samples/cs/native-chat-completions/Program.cs +++ b/samples/cs/native-chat-completions/Program.cs @@ -1,7 +1,7 @@ -// +// // using Microsoft.AI.Foundry.Local; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Microsoft.AI.Foundry.Local.OpenAI; // // @@ -90,7 +90,7 @@ await model.DownloadAsync(progress => // Create a chat message List messages = new() { - new ChatMessage { Role = "user", Content = "Why is the sky blue?" } + new ChatMessage { Role = ChatMessageRole.User, Content = "Why is the sky blue?" } }; // Get a streaming chat completion response diff --git a/samples/cs/tool-calling-foundry-local-sdk/Program.cs b/samples/cs/tool-calling-foundry-local-sdk/Program.cs index 8ac96369..1050e075 100644 --- a/samples/cs/tool-calling-foundry-local-sdk/Program.cs +++ b/samples/cs/tool-calling-foundry-local-sdk/Program.cs @@ -1,9 +1,7 @@ -// +// // using Microsoft.AI.Foundry.Local; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; -using Betalgo.Ranul.OpenAI.ObjectModels.SharedModels; +using Microsoft.AI.Foundry.Local.OpenAI; using System.Text.Json; // @@ -59,14 +57,14 @@ await model.DownloadAsync(progress => // Get a chat client var chatClient = await model.GetChatClientAsync(); -chatClient.Settings.ToolChoice = ToolChoice.Required; // Force the model to make a tool call +chatClient.Settings.ToolChoice = ToolChoice.CreateRequiredChoice(); // Force the model to make a tool call // Prepare messages List messages = [ - new ChatMessage { Role = "system", Content = "You are a helpful AI assistant. If necessary, you can use any provided tools to answer the question." }, - new ChatMessage { Role = "user", Content = "What is the answer to 7 multiplied by 6?" } + new ChatMessage { Role = ChatMessageRole.System, Content = "You are a helpful AI assistant. If necessary, you can use any provided tools to answer the question." }, + new ChatMessage { Role = ChatMessageRole.User, Content = "What is the answer to 7 multiplied by 6?" } ]; @@ -76,7 +74,7 @@ await model.DownloadAsync(progress => [ new ToolDefinition { - Type = "function", + Type = ToolType.Function, Function = new FunctionDefinition() { Name = "multiply_numbers", @@ -99,7 +97,7 @@ await model.DownloadAsync(progress => // // Get a streaming chat completion response -var toolCallResponses = new List(); +var toolCallResponses = new List(); Console.WriteLine("Chat completion response:"); var streamingResponse = chatClient.CompleteChatStreamingAsync(messages, tools, ct); await foreach (var chunk in streamingResponse) @@ -108,7 +106,7 @@ await model.DownloadAsync(progress => Console.Write(content); Console.Out.Flush(); - if (chunk.Choices[0].FinishReason == "tool_calls") + if (chunk.Choices[0].FinishReason == FinishReason.ToolCalls) { toolCallResponses.Add(chunk); } @@ -119,7 +117,7 @@ await model.DownloadAsync(progress => // Invoke tools called and append responses to the chat foreach (var chunk in toolCallResponses) { - var call = chunk?.Choices[0].Message.ToolCalls?[0].FunctionCall; + var call = chunk?.Choices[0].Message.ToolCalls?[0].Function; if (call?.Name == "multiply_numbers") { var arguments = JsonSerializer.Deserialize>(call.Arguments!)!; @@ -132,7 +130,7 @@ await model.DownloadAsync(progress => var response = new ChatMessage { - Role = "tool", + Role = ChatMessageRole.Tool, Content = result.ToString(), }; messages.Add(response); @@ -142,12 +140,12 @@ await model.DownloadAsync(progress => // Prompt the model to continue the conversation after the tool call -messages.Add(new ChatMessage { Role = "system", Content = "Respond only with the answer generated by the tool." }); +messages.Add(new ChatMessage { Role = ChatMessageRole.System, Content = "Respond only with the answer generated by the tool." }); // Set tool calling back to auto so that the model can decide whether to call // the tool again or continue the conversation based on the new user prompt -chatClient.Settings.ToolChoice = ToolChoice.Auto; +chatClient.Settings.ToolChoice = ToolChoice.CreateAutoChoice(); // Run the next turn of the conversation diff --git a/samples/cs/tool-calling-foundry-local-web-server/Program.cs b/samples/cs/tool-calling-foundry-local-web-server/Program.cs index 48ee6c6f..666d9212 100644 --- a/samples/cs/tool-calling-foundry-local-web-server/Program.cs +++ b/samples/cs/tool-calling-foundry-local-web-server/Program.cs @@ -1,4 +1,4 @@ -// +// using Microsoft.AI.Foundry.Local; using OpenAI; using OpenAI.Chat; diff --git a/samples/cs/tutorial-chat-assistant/Program.cs b/samples/cs/tutorial-chat-assistant/Program.cs index 10e9a63b..68c47d7d 100644 --- a/samples/cs/tutorial-chat-assistant/Program.cs +++ b/samples/cs/tutorial-chat-assistant/Program.cs @@ -1,7 +1,7 @@ // // using Microsoft.AI.Foundry.Local; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Microsoft.AI.Foundry.Local.OpenAI; using Microsoft.Extensions.Logging; // @@ -48,7 +48,7 @@ await model.DownloadAsync(progress => { new ChatMessage { - Role = "system", + Role = ChatMessageRole.System, Content = "You are a helpful, friendly assistant. Keep your responses " + "concise and conversational. If you don't know something, say so." } @@ -70,7 +70,7 @@ await model.DownloadAsync(progress => } // Add the user's message to conversation history - messages.Add(new ChatMessage { Role = "user", Content = userInput }); + messages.Add(new ChatMessage { Role = ChatMessageRole.User, Content = userInput }); // // Stream the response token by token @@ -91,7 +91,7 @@ await model.DownloadAsync(progress => // // Add the complete response to conversation history - messages.Add(new ChatMessage { Role = "assistant", Content = fullResponse }); + messages.Add(new ChatMessage { Role = ChatMessageRole.Assistant, Content = fullResponse }); } // diff --git a/samples/cs/tutorial-chat-assistant/TutorialChatAssistant.csproj b/samples/cs/tutorial-chat-assistant/TutorialChatAssistant.csproj index a3533047..e48c209d 100644 --- a/samples/cs/tutorial-chat-assistant/TutorialChatAssistant.csproj +++ b/samples/cs/tutorial-chat-assistant/TutorialChatAssistant.csproj @@ -42,7 +42,6 @@ - diff --git a/samples/cs/tutorial-document-summarizer/Program.cs b/samples/cs/tutorial-document-summarizer/Program.cs index bc5546f6..14d0e7a1 100644 --- a/samples/cs/tutorial-document-summarizer/Program.cs +++ b/samples/cs/tutorial-document-summarizer/Program.cs @@ -1,7 +1,7 @@ // // using Microsoft.AI.Foundry.Local; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Microsoft.AI.Foundry.Local.OpenAI; using Microsoft.Extensions.Logging; // @@ -75,8 +75,8 @@ async Task SummarizeFileAsync( var fileContent = await File.ReadAllTextAsync(filePath, token); var messages = new List { - new ChatMessage { Role = "system", Content = prompt }, - new ChatMessage { Role = "user", Content = fileContent } + new ChatMessage { Role = ChatMessageRole.System, Content = prompt }, + new ChatMessage { Role = ChatMessageRole.User, Content = fileContent } }; var response = await client.CompleteChatAsync(messages, token); diff --git a/samples/cs/tutorial-document-summarizer/TutorialDocumentSummarizer.csproj b/samples/cs/tutorial-document-summarizer/TutorialDocumentSummarizer.csproj index a3533047..e48c209d 100644 --- a/samples/cs/tutorial-document-summarizer/TutorialDocumentSummarizer.csproj +++ b/samples/cs/tutorial-document-summarizer/TutorialDocumentSummarizer.csproj @@ -42,7 +42,6 @@ - diff --git a/samples/cs/tutorial-tool-calling/Program.cs b/samples/cs/tutorial-tool-calling/Program.cs index 74f137db..761556a7 100644 --- a/samples/cs/tutorial-tool-calling/Program.cs +++ b/samples/cs/tutorial-tool-calling/Program.cs @@ -2,9 +2,7 @@ // using System.Text.Json; using Microsoft.AI.Foundry.Local; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; -using Betalgo.Ranul.OpenAI.ObjectModels.SharedModels; +using Microsoft.AI.Foundry.Local.OpenAI; using Microsoft.Extensions.Logging; // @@ -16,7 +14,7 @@ [ new ToolDefinition { - Type = "function", + Type = ToolType.Function, Function = new FunctionDefinition() { Name = "get_weather", @@ -35,7 +33,7 @@ }, new ToolDefinition { - Type = "function", + Type = ToolType.Function, Function = new FunctionDefinition() { Name = "calculate", @@ -136,13 +134,13 @@ await model.DownloadAsync(progress => Console.WriteLine("Model loaded and ready."); var chatClient = await model.GetChatClientAsync(); -chatClient.Settings.ToolChoice = ToolChoice.Auto; +chatClient.Settings.ToolChoice = ToolChoice.CreateAutoChoice(); var messages = new List { new ChatMessage { - Role = "system", + Role = ChatMessageRole.System, Content = "You are a helpful assistant with access to tools. " + "Use them when needed to answer questions accurately." } @@ -165,7 +163,7 @@ await model.DownloadAsync(progress => messages.Add(new ChatMessage { - Role = "user", + Role = ChatMessageRole.User, Content = userInput }); @@ -182,18 +180,18 @@ await model.DownloadAsync(progress => foreach (var toolCall in choice.ToolCalls) { var toolArgs = JsonDocument.Parse( - toolCall.FunctionCall.Arguments + toolCall.Function.Arguments ).RootElement; Console.WriteLine( - $" Tool call: {toolCall.FunctionCall.Name}({toolArgs})" + $" Tool call: {toolCall.Function.Name}({toolArgs})" ); var result = ExecuteTool( - toolCall.FunctionCall.Name, toolArgs + toolCall.Function.Name, toolArgs ); messages.Add(new ChatMessage { - Role = "tool", + Role = ChatMessageRole.Tool, ToolCallId = toolCall.Id, Content = result }); @@ -205,7 +203,7 @@ await model.DownloadAsync(progress => var answer = finalResponse.Choices[0].Message.Content ?? ""; messages.Add(new ChatMessage { - Role = "assistant", + Role = ChatMessageRole.Assistant, Content = answer }); Console.WriteLine($"Assistant: {answer}\n"); @@ -215,7 +213,7 @@ await model.DownloadAsync(progress => var answer = choice.Content ?? ""; messages.Add(new ChatMessage { - Role = "assistant", + Role = ChatMessageRole.Assistant, Content = answer }); Console.WriteLine($"Assistant: {answer}\n"); diff --git a/samples/cs/tutorial-tool-calling/TutorialToolCalling.csproj b/samples/cs/tutorial-tool-calling/TutorialToolCalling.csproj index a3533047..e48c209d 100644 --- a/samples/cs/tutorial-tool-calling/TutorialToolCalling.csproj +++ b/samples/cs/tutorial-tool-calling/TutorialToolCalling.csproj @@ -42,7 +42,6 @@ - diff --git a/samples/cs/tutorial-voice-to-text/Program.cs b/samples/cs/tutorial-voice-to-text/Program.cs index 976b44e4..84882fa6 100644 --- a/samples/cs/tutorial-voice-to-text/Program.cs +++ b/samples/cs/tutorial-voice-to-text/Program.cs @@ -1,7 +1,7 @@ // // using Microsoft.AI.Foundry.Local; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Microsoft.AI.Foundry.Local.OpenAI; using Microsoft.Extensions.Logging; using System.Text; // @@ -81,14 +81,14 @@ await chatModel.DownloadAsync(progress => { new ChatMessage { - Role = "system", + Role = ChatMessageRole.System, Content = "You are a note-taking assistant. Summarize " + "the following transcription into organized, " + "concise notes with bullet points." }, new ChatMessage { - Role = "user", + Role = ChatMessageRole.User, Content = transcriptionText.ToString() } }; diff --git a/samples/cs/tutorial-voice-to-text/TutorialVoiceToText.csproj b/samples/cs/tutorial-voice-to-text/TutorialVoiceToText.csproj index a3533047..e48c209d 100644 --- a/samples/cs/tutorial-voice-to-text/TutorialVoiceToText.csproj +++ b/samples/cs/tutorial-voice-to-text/TutorialVoiceToText.csproj @@ -42,7 +42,6 @@ - diff --git a/samples/rust/tool-calling-foundry-local/src/main.rs b/samples/rust/tool-calling-foundry-local/src/main.rs index 1ccda1e8..9928224e 100644 --- a/samples/rust/tool-calling-foundry-local/src/main.rs +++ b/samples/rust/tool-calling-foundry-local/src/main.rs @@ -189,7 +189,7 @@ async fn main() -> Result<(), Box> { messages.push(assistant_msg); messages.push( ChatCompletionRequestToolMessage { - content: result.into(), + content: result, tool_call_id: tc["id"].as_str().unwrap_or_default().to_string(), } .into(), diff --git a/samples/rust/tutorial-tool-calling/src/main.rs b/samples/rust/tutorial-tool-calling/src/main.rs index f4476643..356303e2 100644 --- a/samples/rust/tutorial-tool-calling/src/main.rs +++ b/samples/rust/tutorial-tool-calling/src/main.rs @@ -294,7 +294,7 @@ async fn main() -> anyhow::Result<()> { execute_tool(function_name, &arguments); messages.push( ChatCompletionRequestToolMessage { - content: result.to_string().into(), + content: result.to_string(), tool_call_id: tool_call.id.clone(), } .into(), diff --git a/sdk/cs/README.md b/sdk/cs/README.md index 3efdc242..ff03e6e6 100644 --- a/sdk/cs/README.md +++ b/sdk/cs/README.md @@ -106,7 +106,7 @@ Catalog access no longer blocks on EP downloads. Call `DownloadAndRegisterEpsAsy using Microsoft.AI.Foundry.Local; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Microsoft.AI.Foundry.Local.OpenAI; // 1. Initialize the singleton manager await FoundryLocalManager.CreateAsync( @@ -273,7 +273,7 @@ audioClient.Settings.Temperature = 0.0f; For real-time microphone-to-text transcription, use `CreateLiveTranscriptionSession()`. Audio is pushed as raw PCM chunks and transcription results stream back as an `IAsyncEnumerable`. -The streaming result type (`LiveAudioTranscriptionResponse`) extends `ConversationItem` from the Betalgo OpenAI SDK's Realtime models, so it's compatible with the OpenAI Realtime API pattern. Access transcribed text via `result.Content[0].Text` or `result.Content[0].Transcript`. +The streaming result type (`LiveAudioTranscriptionResponse`) is compatible with the OpenAI Realtime API pattern and provides access to transcribed text via `result.Content[0].Text` or `result.Content[0].Transcript`. ```csharp var audioClient = await model.GetAudioClientAsync(); @@ -314,7 +314,6 @@ await session.StopAsync(); | `IsFinal` | `bool` | Whether this is a final or interim result. Nemotron always returns `true`. | | `StartTime` | `double?` | Start time offset in the audio stream (seconds). | | `EndTime` | `double?` | End time offset in the audio stream (seconds). | -| `Id` | `string?` | Unique identifier for this result (if available). | #### Session Lifecycle diff --git a/sdk/cs/docs/api/microsoft.ai.foundry.local.imodel.md b/sdk/cs/docs/api/microsoft.ai.foundry.local.imodel.md index 861386a8..e57c8391 100644 --- a/sdk/cs/docs/api/microsoft.ai.foundry.local.imodel.md +++ b/sdk/cs/docs/api/microsoft.ai.foundry.local.imodel.md @@ -188,7 +188,7 @@ Optional cancellation token. #### Returns [Task<OpenAIChatClient>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-OpenAI.ChatClient +OpenAIChatClient ### **GetAudioClientAsync(Nullable<CancellationToken>)** @@ -206,7 +206,7 @@ Optional cancellation token. #### Returns [Task<OpenAIAudioClient>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-OpenAI.AudioClient +OpenAIAudioClient ### **SelectVariant(IModel)** diff --git a/sdk/cs/docs/api/microsoft.ai.foundry.local.openaiaudioclient.md b/sdk/cs/docs/api/microsoft.ai.foundry.local.openaiaudioclient.md index b1b60bd8..77b6bc4c 100644 --- a/sdk/cs/docs/api/microsoft.ai.foundry.local.openaiaudioclient.md +++ b/sdk/cs/docs/api/microsoft.ai.foundry.local.openaiaudioclient.md @@ -3,7 +3,7 @@ Namespace: Microsoft.AI.Foundry.Local Audio Client that uses the OpenAI API. - Implemented using Betalgo.Ranul.OpenAI SDK types. + Implemented using custom OpenAI-compatible types. ```csharp public class OpenAIAudioClient @@ -33,7 +33,7 @@ public AudioSettings Settings { get; } Transcribe audio from a file. ```csharp -public Task TranscribeAudioAsync(string audioFilePath, Nullable ct) +public Task TranscribeAudioAsync(string audioFilePath, Nullable ct) ``` #### Parameters @@ -47,7 +47,7 @@ Optional cancellation token. #### Returns -[Task<AudioCreateTranscriptionResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+[Task<AudioTranscriptionResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
Transcription response. ### **TranscribeAudioStreamingAsync(String, CancellationToken)** @@ -55,7 +55,7 @@ Transcription response. Transcribe audio from a file with streamed output. ```csharp -public IAsyncEnumerable TranscribeAudioStreamingAsync(string audioFilePath, CancellationToken ct) +public IAsyncEnumerable TranscribeAudioStreamingAsync(string audioFilePath, CancellationToken ct) ``` #### Parameters @@ -69,7 +69,7 @@ Cancellation token. #### Returns -[IAsyncEnumerable<AudioCreateTranscriptionResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1)
+[IAsyncEnumerable<AudioTranscriptionResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1)
An asynchronous enumerable of transcription responses. ### **CreateLiveTranscriptionSession()** diff --git a/sdk/cs/docs/api/microsoft.ai.foundry.local.openaichatclient.md b/sdk/cs/docs/api/microsoft.ai.foundry.local.openaichatclient.md index 43e00f6d..f0d78442 100644 --- a/sdk/cs/docs/api/microsoft.ai.foundry.local.openaichatclient.md +++ b/sdk/cs/docs/api/microsoft.ai.foundry.local.openaichatclient.md @@ -3,7 +3,7 @@ Namespace: Microsoft.AI.Foundry.Local Chat Client that uses the OpenAI API. - Implemented using Betalgo.Ranul.OpenAI SDK types. + Implemented using custom OpenAI-compatible types. ```csharp public class OpenAIChatClient @@ -35,7 +35,7 @@ Execute a chat completion request. To continue a conversation, add the ChatMessage from the previous response and new prompt to the messages. ```csharp -public Task CompleteChatAsync(IEnumerable messages, Nullable ct) +public Task CompleteChatAsync(IEnumerable messages, Nullable ct) ``` #### Parameters @@ -48,7 +48,7 @@ Optional cancellation token. #### Returns -[Task<ChatCompletionCreateResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+[Task<ChatCompletionResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
Chat completion response. ### **CompleteChatAsync(IEnumerable<ChatMessage>, IEnumerable<ToolDefinition>, Nullable<CancellationToken>)** @@ -58,7 +58,7 @@ Execute a chat completion request. To continue a conversation, add the ChatMessage from the previous response and new prompt to the messages. ```csharp -public Task CompleteChatAsync(IEnumerable messages, IEnumerable tools, Nullable ct) +public Task CompleteChatAsync(IEnumerable messages, IEnumerable tools, Nullable ct) ``` #### Parameters @@ -74,7 +74,7 @@ Optional cancellation token. #### Returns -[Task<ChatCompletionCreateResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+[Task<ChatCompletionResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
Chat completion response. ### **CompleteChatStreamingAsync(IEnumerable<ChatMessage>, CancellationToken)** @@ -84,7 +84,7 @@ Execute a chat completion request with streamed output. To continue a conversation, add the ChatMessage from the previous response and new prompt to the messages. ```csharp -public IAsyncEnumerable CompleteChatStreamingAsync(IEnumerable messages, CancellationToken ct) +public IAsyncEnumerable CompleteChatStreamingAsync(IEnumerable messages, CancellationToken ct) ``` #### Parameters @@ -97,7 +97,7 @@ Cancellation token. #### Returns -[IAsyncEnumerable<ChatCompletionCreateResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1)
+[IAsyncEnumerable<ChatCompletionResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1)
Async enumerable of chat completion responses. ### **CompleteChatStreamingAsync(IEnumerable<ChatMessage>, IEnumerable<ToolDefinition>, CancellationToken)** @@ -107,7 +107,7 @@ Execute a chat completion request with streamed output. To continue a conversation, add the ChatMessage from the previous response and new prompt to the messages. ```csharp -public IAsyncEnumerable CompleteChatStreamingAsync(IEnumerable messages, IEnumerable tools, CancellationToken ct) +public IAsyncEnumerable CompleteChatStreamingAsync(IEnumerable messages, IEnumerable tools, CancellationToken ct) ``` #### Parameters @@ -123,5 +123,5 @@ Cancellation token. #### Returns -[IAsyncEnumerable<ChatCompletionCreateResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1)
+[IAsyncEnumerable<ChatCompletionResponse>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1)
Async enumerable of chat completion responses. diff --git a/sdk/cs/src/Catalog.cs b/sdk/cs/src/Catalog.cs index f33dcaff..0f939c0b 100644 --- a/sdk/cs/src/Catalog.cs +++ b/sdk/cs/src/Catalog.cs @@ -5,6 +5,7 @@ // -------------------------------------------------------------------------------------------------------------------- namespace Microsoft.AI.Foundry.Local; + using System; using System.Collections.Generic; using System.Text.Json; @@ -180,7 +181,7 @@ private async Task GetLatestVersionImplAsync(IModel modelOrModelVariant, } // variants are sorted by version, so the first one matching the name is the latest version for that variant. - var latest = model!.Variants.FirstOrDefault(v => v.Info.Name == modelOrModelVariant.Info.Name) ?? + var latest = model.Variants.FirstOrDefault(v => v.Info.Name == modelOrModelVariant.Info.Name) ?? // should not be possible given we internally manage all the state involved throw new FoundryLocalException($"Internal error. Mismatch between model (alias:{model.Alias}) and " + $"model variant (alias:{modelOrModelVariant.Alias}).", _logger); diff --git a/sdk/cs/src/Detail/AsyncLock.cs b/sdk/cs/src/Detail/AsyncLock.cs index 921d7f98..c8b174d1 100644 --- a/sdk/cs/src/Detail/AsyncLock.cs +++ b/sdk/cs/src/Detail/AsyncLock.cs @@ -5,6 +5,7 @@ // -------------------------------------------------------------------------------------------------------------------- namespace Microsoft.AI.Foundry.Local.Detail; + using System; using System.Threading.Tasks; diff --git a/sdk/cs/src/Detail/IModelLoadManager.cs b/sdk/cs/src/Detail/IModelLoadManager.cs index a96c6697..cd11f1f3 100644 --- a/sdk/cs/src/Detail/IModelLoadManager.cs +++ b/sdk/cs/src/Detail/IModelLoadManager.cs @@ -5,6 +5,7 @@ // -------------------------------------------------------------------------------------------------------------------- namespace Microsoft.AI.Foundry.Local.Detail; + using System.Threading.Tasks; /// diff --git a/sdk/cs/src/Detail/JsonSerializationContext.cs b/sdk/cs/src/Detail/JsonSerializationContext.cs index 37cc81ac..94cb2f06 100644 --- a/sdk/cs/src/Detail/JsonSerializationContext.cs +++ b/sdk/cs/src/Detail/JsonSerializationContext.cs @@ -1,4 +1,4 @@ -// -------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) Microsoft. All rights reserved. // @@ -10,33 +10,39 @@ namespace Microsoft.AI.Foundry.Local.Detail; using System.Text.Json; using System.Text.Json.Serialization; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; -using Betalgo.Ranul.OpenAI.ObjectModels.SharedModels; - using Microsoft.AI.Foundry.Local.OpenAI; [JsonSerializable(typeof(ModelInfo))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(CoreInteropRequest))] -[JsonSerializable(typeof(ChatCompletionCreateRequestExtended))] -[JsonSerializable(typeof(ChatCompletionCreateResponse))] -[JsonSerializable(typeof(AudioCreateTranscriptionRequest))] -[JsonSerializable(typeof(AudioCreateTranscriptionResponse))] +[JsonSerializable(typeof(ChatCompletionRequest))] +[JsonSerializable(typeof(ChatCompletionResponse))] +[JsonSerializable(typeof(ChatChoice))] +[JsonSerializable(typeof(ChatMessage))] +[JsonSerializable(typeof(ChatMessageRole))] +[JsonSerializable(typeof(ToolType))] +[JsonSerializable(typeof(FinishReason))] +[JsonSerializable(typeof(CompletionUsage))] +[JsonSerializable(typeof(ResponseError))] +[JsonSerializable(typeof(AudioTranscriptionRequest))] +[JsonSerializable(typeof(AudioTranscriptionResponse))] [JsonSerializable(typeof(string[]))] // list loaded or cached models [JsonSerializable(typeof(EpInfo[]))] [JsonSerializable(typeof(EpDownloadResult))] [JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(ResponseFormat))] [JsonSerializable(typeof(ResponseFormatExtended))] [JsonSerializable(typeof(ToolChoice))] +[JsonSerializable(typeof(ToolChoice.FunctionTool))] [JsonSerializable(typeof(ToolDefinition))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(FunctionDefinition))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(PropertyDefinition))] [JsonSerializable(typeof(IList))] -// --- Audio streaming types (LiveAudioTranscriptionResponse inherits ConversationItem -// which has AOT-incompatible JsonConverters, so we only register the raw deserialization type) --- +[JsonSerializable(typeof(ToolCall))] +[JsonSerializable(typeof(FunctionCall))] +[JsonSerializable(typeof(JsonSchema))] [JsonSerializable(typeof(LiveAudioTranscriptionRaw))] [JsonSerializable(typeof(CoreErrorResponse))] [JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, diff --git a/sdk/cs/src/EpInfo.cs b/sdk/cs/src/EpInfo.cs index d170ac0e..db853c69 100644 --- a/sdk/cs/src/EpInfo.cs +++ b/sdk/cs/src/EpInfo.cs @@ -1,4 +1,4 @@ -// -------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) Microsoft. All rights reserved. // diff --git a/sdk/cs/src/FoundryLocalException.cs b/sdk/cs/src/FoundryLocalException.cs index d6e606c9..553c7ee8 100644 --- a/sdk/cs/src/FoundryLocalException.cs +++ b/sdk/cs/src/FoundryLocalException.cs @@ -5,6 +5,7 @@ // -------------------------------------------------------------------------------------------------------------------- namespace Microsoft.AI.Foundry.Local; + using System; using System.Diagnostics; diff --git a/sdk/cs/src/ICatalog.cs b/sdk/cs/src/ICatalog.cs index 4dca8e7d..8848d9ce 100644 --- a/sdk/cs/src/ICatalog.cs +++ b/sdk/cs/src/ICatalog.cs @@ -5,6 +5,7 @@ // -------------------------------------------------------------------------------------------------------------------- namespace Microsoft.AI.Foundry.Local; + using System.Collections.Generic; public interface ICatalog diff --git a/sdk/cs/src/IModel.cs b/sdk/cs/src/IModel.cs index a27f3a3d..afe8ff1c 100644 --- a/sdk/cs/src/IModel.cs +++ b/sdk/cs/src/IModel.cs @@ -60,14 +60,14 @@ Task DownloadAsync(Action? downloadProgress = null, /// Get an OpenAI API based ChatClient /// /// Optional cancellation token. - /// OpenAI.ChatClient + /// OpenAIChatClient Task GetChatClientAsync(CancellationToken? ct = null); /// /// Get an OpenAI API based AudioClient /// /// Optional cancellation token. - /// OpenAI.AudioClient + /// OpenAIAudioClient Task GetAudioClientAsync(CancellationToken? ct = null); /// diff --git a/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj b/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj index e8a7b755..4dc678e0 100644 --- a/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj +++ b/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj @@ -121,7 +121,6 @@ - diff --git a/sdk/cs/src/OpenAI/AudioClient.cs b/sdk/cs/src/OpenAI/AudioClient.cs index a8cbc1d7..e5ce01a4 100644 --- a/sdk/cs/src/OpenAI/AudioClient.cs +++ b/sdk/cs/src/OpenAI/AudioClient.cs @@ -1,4 +1,4 @@ -// -------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) Microsoft. All rights reserved. // @@ -8,8 +8,6 @@ namespace Microsoft.AI.Foundry.Local; using System.Runtime.CompilerServices; using System.Threading.Channels; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; using Microsoft.AI.Foundry.Local.Detail; using Microsoft.AI.Foundry.Local.OpenAI; @@ -17,7 +15,6 @@ namespace Microsoft.AI.Foundry.Local; /// /// Audio Client that uses the OpenAI API. -/// Implemented using Betalgo.Ranul.OpenAI SDK types. /// public class OpenAIAudioClient { @@ -54,7 +51,7 @@ public record AudioSettings /// /// Optional cancellation token. /// Transcription response. - public async Task TranscribeAudioAsync(string audioFilePath, + public async Task TranscribeAudioAsync(string audioFilePath, CancellationToken? ct = null) { return await Utils.CallWithExceptionHandling(() => TranscribeAudioImplAsync(audioFilePath, ct), @@ -71,7 +68,7 @@ public async Task TranscribeAudioAsync(string /// /// Cancellation token. /// An asynchronous enumerable of transcription responses. - public async IAsyncEnumerable TranscribeAudioStreamingAsync( + public async IAsyncEnumerable TranscribeAudioStreamingAsync( string audioFilePath, [EnumeratorCancellation] CancellationToken ct) { var enumerable = Utils.CallWithExceptionHandling( @@ -94,10 +91,10 @@ public LiveAudioTranscriptionSession CreateLiveTranscriptionSession() return new LiveAudioTranscriptionSession(_modelId); } - private async Task TranscribeAudioImplAsync(string audioFilePath, + private async Task TranscribeAudioImplAsync(string audioFilePath, CancellationToken? ct) { - var openaiRequest = AudioTranscriptionCreateRequestExtended.FromUserInput(_modelId, audioFilePath, Settings); + var openaiRequest = AudioTranscriptionExtensions.FromUserInput(_modelId, audioFilePath, Settings); var request = new CoreInteropRequest @@ -117,10 +114,10 @@ private async Task TranscribeAudioImplAsync(st return output; } - private async IAsyncEnumerable TranscribeAudioStreamingImplAsync( + private async IAsyncEnumerable TranscribeAudioStreamingImplAsync( string audioFilePath, [EnumeratorCancellation] CancellationToken ct) { - var openaiRequest = AudioTranscriptionCreateRequestExtended.FromUserInput(_modelId, audioFilePath, Settings); + var openaiRequest = AudioTranscriptionExtensions.FromUserInput(_modelId, audioFilePath, Settings); var request = new CoreInteropRequest { @@ -130,7 +127,7 @@ private async IAsyncEnumerable TranscribeAudio } }; - var channel = Channel.CreateUnbounded( + var channel = Channel.CreateUnbounded( new UnboundedChannelOptions { SingleWriter = true, @@ -138,7 +135,7 @@ private async IAsyncEnumerable TranscribeAudio AllowSynchronousContinuations = true }); - // The callback will push AudioCreateTranscriptionResponse objects into the channel. + // The callback will push AudioTranscriptionResponse objects into the channel. // The channel reader will return the values to the user. // This setup prevents the user from blocking the thread generating the responses. _ = Task.Run(async () => diff --git a/sdk/cs/src/OpenAI/AudioTranscriptionRequestResponseTypes.cs b/sdk/cs/src/OpenAI/AudioTranscriptionExtensions.cs similarity index 57% rename from sdk/cs/src/OpenAI/AudioTranscriptionRequestResponseTypes.cs rename to sdk/cs/src/OpenAI/AudioTranscriptionExtensions.cs index 4ba28336..8ccb7fef 100644 --- a/sdk/cs/src/OpenAI/AudioTranscriptionRequestResponseTypes.cs +++ b/sdk/cs/src/OpenAI/AudioTranscriptionExtensions.cs @@ -8,29 +8,19 @@ namespace Microsoft.AI.Foundry.Local.OpenAI; using System.Globalization; using System.Text.Json; -using System.Text.Json.Serialization; - -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; using Microsoft.AI.Foundry.Local; using Microsoft.AI.Foundry.Local.Detail; using Microsoft.Extensions.Logging; -internal record AudioTranscriptionCreateRequestExtended : AudioCreateTranscriptionRequest +internal static class AudioTranscriptionExtensions { - // Valid entries: - // int language - // int temperature - [JsonPropertyName("metadata")] - public Dictionary? Metadata { get; set; } - - internal static AudioTranscriptionCreateRequestExtended FromUserInput(string modelId, - string audioFilePath, - OpenAIAudioClient.AudioSettings settings) + internal static AudioTranscriptionRequest FromUserInput(string modelId, + string audioFilePath, + OpenAIAudioClient.AudioSettings settings) { - var request = new AudioTranscriptionCreateRequestExtended + var request = new AudioTranscriptionRequest { Model = modelId, FileName = audioFilePath, @@ -57,18 +47,16 @@ internal static AudioTranscriptionCreateRequestExtended FromUserInput(string mod request.Metadata = metadata; } - return request; } -} -internal static class AudioTranscriptionRequestResponseExtensions -{ - internal static string ToJson(this AudioCreateTranscriptionRequest request) + + internal static string ToJson(this AudioTranscriptionRequest request) { - return JsonSerializer.Serialize(request, JsonSerializationContext.Default.AudioCreateTranscriptionRequest); + return JsonSerializer.Serialize(request, JsonSerializationContext.Default.AudioTranscriptionRequest); } - internal static AudioCreateTranscriptionResponse ToAudioTranscription(this ICoreInterop.Response response, - ILogger logger) + + internal static AudioTranscriptionResponse ToAudioTranscription(this ICoreInterop.Response response, + ILogger logger) { if (response.Error != null) { @@ -79,14 +67,14 @@ internal static AudioCreateTranscriptionResponse ToAudioTranscription(this ICore return response.Data!.ToAudioTranscription(logger); } - internal static AudioCreateTranscriptionResponse ToAudioTranscription(this string responseData, ILogger logger) + internal static AudioTranscriptionResponse ToAudioTranscription(this string responseData, ILogger logger) { - var typeInfo = JsonSerializationContext.Default.AudioCreateTranscriptionResponse; + var typeInfo = JsonSerializationContext.Default.AudioTranscriptionResponse; var response = JsonSerializer.Deserialize(responseData, typeInfo); if (response == null) { - logger.LogError("Failed to deserialize AudioCreateTranscriptionResponse. Json={Data}", responseData); - throw new FoundryLocalException("Failed to deserialize AudioCreateTranscriptionResponse"); + logger.LogError("Failed to deserialize AudioTranscriptionResponse. Json={Data}", responseData); + throw new FoundryLocalException("Failed to deserialize AudioTranscriptionResponse"); } return response; diff --git a/sdk/cs/src/OpenAI/AudioTypes.cs b/sdk/cs/src/OpenAI/AudioTypes.cs new file mode 100644 index 00000000..a10e36af --- /dev/null +++ b/sdk/cs/src/OpenAI/AudioTypes.cs @@ -0,0 +1,50 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.OpenAI; + +using System.Text.Json.Serialization; + +/// +/// Response from an audio transcription request. +/// +public class AudioTranscriptionResponse +{ + /// The transcribed text. + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// The task performed (e.g. "transcribe"). + [JsonPropertyName("task")] + public string? Task { get; set; } + + /// The language of the audio. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// The duration of the audio in seconds. + [JsonPropertyName("duration")] + public float? Duration { get; set; } +} + +/// +/// Internal request DTO for audio transcription. Properties use PascalCase +/// (the default with no JsonPropertyName) for native Core interop communication. +/// Metadata is the exception — Core expects it as lowercase "metadata". +/// +internal class AudioTranscriptionRequest +{ + public string? Model { get; set; } + public string? FileName { get; set; } + public byte[]? File { get; set; } + public string? Language { get; set; } + public string? Prompt { get; set; } + public string? ResponseFormat { get; set; } + public float? Temperature { get; set; } + + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; set; } +} diff --git a/sdk/cs/src/OpenAI/ChatClient.cs b/sdk/cs/src/OpenAI/ChatClient.cs index b9f889f2..5e6463e7 100644 --- a/sdk/cs/src/OpenAI/ChatClient.cs +++ b/sdk/cs/src/OpenAI/ChatClient.cs @@ -10,16 +10,12 @@ namespace Microsoft.AI.Foundry.Local; using System.Runtime.CompilerServices; using System.Threading.Channels; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; - using Microsoft.AI.Foundry.Local.Detail; using Microsoft.AI.Foundry.Local.OpenAI; using Microsoft.Extensions.Logging; /// /// Chat Client that uses the OpenAI API. -/// Implemented using Betalgo.Ranul.OpenAI SDK types. /// public class OpenAIChatClient { @@ -40,10 +36,12 @@ public record ChatSettings { public float? FrequencyPenalty { get; set; } public int? MaxTokens { get; set; } + public int? MaxCompletionTokens { get; set; } public int? N { get; set; } public float? Temperature { get; set; } public float? PresencePenalty { get; set; } public int? RandomSeed { get; set; } + public string? Stop { get; set; } internal bool? Stream { get; set; } // this is set internally based on the API used public int? TopK { get; set; } public float? TopP { get; set; } @@ -65,7 +63,7 @@ public record ChatSettings /// Chat messages. The system message is automatically added. /// Optional cancellation token. /// Chat completion response. - public Task CompleteChatAsync(IEnumerable messages, + public Task CompleteChatAsync(IEnumerable messages, CancellationToken? ct = null) { return CompleteChatAsync(messages: messages, tools: null, ct: ct); @@ -80,7 +78,7 @@ public Task CompleteChatAsync(IEnumerableOptional tool definitions to include in the request. /// Optional cancellation token. /// Chat completion response. - public async Task CompleteChatAsync(IEnumerable messages, + public async Task CompleteChatAsync(IEnumerable messages, IEnumerable? tools, CancellationToken? ct = null) { @@ -97,7 +95,7 @@ public async Task CompleteChatAsync(IEnumerableChat messages. The system message is automatically added. /// Cancellation token. /// Async enumerable of chat completion responses. - public IAsyncEnumerable CompleteChatStreamingAsync(IEnumerable messages, + public IAsyncEnumerable CompleteChatStreamingAsync(IEnumerable messages, CancellationToken ct) { return CompleteChatStreamingAsync(messages: messages, tools: null, ct: ct); @@ -112,7 +110,7 @@ public IAsyncEnumerable CompleteChatStreamingAsync /// Optional tool definitions to include in the request. /// Cancellation token. /// Async enumerable of chat completion responses. - public async IAsyncEnumerable CompleteChatStreamingAsync(IEnumerable messages, + public async IAsyncEnumerable CompleteChatStreamingAsync(IEnumerable messages, IEnumerable? tools, [EnumeratorCancellation] CancellationToken ct) { @@ -126,13 +124,13 @@ public async IAsyncEnumerable CompleteChatStreamin } } - private async Task CompleteChatImplAsync(IEnumerable messages, + private async Task CompleteChatImplAsync(IEnumerable messages, IEnumerable? tools, CancellationToken? ct) { Settings.Stream = false; - var chatRequest = ChatCompletionCreateRequestExtended.FromUserInput(_modelId, messages, tools, Settings); + var chatRequest = ChatCompletionRequest.FromUserInput(_modelId, messages, tools, Settings); var chatRequestJson = chatRequest.ToJson(); var request = new CoreInteropRequest { Params = new() { { "OpenAICreateRequest", chatRequestJson } } }; @@ -144,17 +142,17 @@ private async Task CompleteChatImplAsync(IEnumerab return chatCompletion; } - private async IAsyncEnumerable ChatStreamingImplAsync(IEnumerable messages, + private async IAsyncEnumerable ChatStreamingImplAsync(IEnumerable messages, IEnumerable? tools, [EnumeratorCancellation] CancellationToken ct) { Settings.Stream = true; - var chatRequest = ChatCompletionCreateRequestExtended.FromUserInput(_modelId, messages, tools, Settings); + var chatRequest = ChatCompletionRequest.FromUserInput(_modelId, messages, tools, Settings); var chatRequestJson = chatRequest.ToJson(); var request = new CoreInteropRequest { Params = new() { { "OpenAICreateRequest", chatRequestJson } } }; - var channel = Channel.CreateUnbounded( + var channel = Channel.CreateUnbounded( new UnboundedChannelOptions { SingleWriter = true, @@ -230,4 +228,4 @@ private async IAsyncEnumerable ChatStreamingImplAs yield return item; } } -} \ No newline at end of file +} diff --git a/sdk/cs/src/OpenAI/ChatCompletionRequestResponseTypes.cs b/sdk/cs/src/OpenAI/ChatCompletionExtensions.cs similarity index 55% rename from sdk/cs/src/OpenAI/ChatCompletionRequestResponseTypes.cs rename to sdk/cs/src/OpenAI/ChatCompletionExtensions.cs index cfd3e08c..c09838fc 100644 --- a/sdk/cs/src/OpenAI/ChatCompletionRequestResponseTypes.cs +++ b/sdk/cs/src/OpenAI/ChatCompletionExtensions.cs @@ -6,37 +6,73 @@ namespace Microsoft.AI.Foundry.Local.OpenAI; +using System.Collections.Generic; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; - using Microsoft.AI.Foundry.Local; using Microsoft.AI.Foundry.Local.Detail; using Microsoft.Extensions.Logging; // https://platform.openai.com/docs/api-reference/chat/create -// Using the Betalgo ChatCompletionCreateRequest and extending with the `metadata` field for additional parameters -// which is part of the OpenAI spec but for some reason not part of the Betalgo request object. -internal class ChatCompletionCreateRequestExtended : ChatCompletionCreateRequest +internal class ChatCompletionRequest { + [JsonPropertyName("model")] + public string? Model { get; set; } + + [JsonPropertyName("messages")] + public List? Messages { get; set; } + + [JsonPropertyName("temperature")] + public float? Temperature { get; set; } + + [JsonPropertyName("max_tokens")] + public int? MaxTokens { get; set; } + + [JsonPropertyName("max_completion_tokens")] + public int? MaxCompletionTokens { get; set; } + + [JsonPropertyName("n")] + public int? N { get; set; } + + [JsonPropertyName("stream")] + public bool? Stream { get; set; } + + [JsonPropertyName("top_p")] + public float? TopP { get; set; } + + [JsonPropertyName("frequency_penalty")] + public float? FrequencyPenalty { get; set; } + + [JsonPropertyName("presence_penalty")] + public float? PresencePenalty { get; set; } + + [JsonPropertyName("stop")] + public string? Stop { get; set; } + + [JsonPropertyName("tools")] + public List? Tools { get; set; } + + [JsonPropertyName("tool_choice")] + public ToolChoice? ToolChoice { get; set; } + + [JsonPropertyName("response_format")] + public ResponseFormatExtended? ResponseFormat { get; set; } + + // Extension: additional parameters passed via metadata // Valid entries: // int top_k // int random_seed [JsonPropertyName("metadata")] public Dictionary? Metadata { get; set; } - [JsonPropertyName("response_format")] - public new ResponseFormatExtended? ResponseFormat { get; set; } - - internal static ChatCompletionCreateRequestExtended FromUserInput(string modelId, - IEnumerable messages, - IEnumerable? tools, - OpenAIChatClient.ChatSettings settings) + internal static ChatCompletionRequest FromUserInput(string modelId, + IEnumerable messages, + IEnumerable? tools, + OpenAIChatClient.ChatSettings settings) { - var request = new ChatCompletionCreateRequestExtended + var request = new ChatCompletionRequest { Model = modelId, Messages = messages.ToList(), @@ -44,9 +80,11 @@ internal static ChatCompletionCreateRequestExtended FromUserInput(string modelId // Apply our specific settings FrequencyPenalty = settings.FrequencyPenalty, MaxTokens = settings.MaxTokens, + MaxCompletionTokens = settings.MaxCompletionTokens, N = settings.N, Temperature = settings.Temperature, PresencePenalty = settings.PresencePenalty, + Stop = settings.Stop, Stream = settings.Stream, TopP = settings.TopP, // Apply tool calling and structured output settings @@ -71,19 +109,18 @@ internal static ChatCompletionCreateRequestExtended FromUserInput(string modelId request.Metadata = metadata; } - return request; } } -internal static class ChatCompletionsRequestResponseExtensions +internal static class ChatCompletionExtensions { - internal static string ToJson(this ChatCompletionCreateRequestExtended request) + internal static string ToJson(this ChatCompletionRequest request) { - return JsonSerializer.Serialize(request, JsonSerializationContext.Default.ChatCompletionCreateRequestExtended); + return JsonSerializer.Serialize(request, JsonSerializationContext.Default.ChatCompletionRequest); } - internal static ChatCompletionCreateResponse ToChatCompletion(this ICoreInterop.Response response, ILogger logger) + internal static ChatCompletionResponse ToChatCompletion(this ICoreInterop.Response response, ILogger logger) { if (response.Error != null) { @@ -94,9 +131,9 @@ internal static ChatCompletionCreateResponse ToChatCompletion(this ICoreInterop. return response.Data!.ToChatCompletion(logger); } - internal static ChatCompletionCreateResponse ToChatCompletion(this string responseData, ILogger logger) + internal static ChatCompletionResponse ToChatCompletion(this string responseData, ILogger logger) { - var output = JsonSerializer.Deserialize(responseData, JsonSerializationContext.Default.ChatCompletionCreateResponse); + var output = JsonSerializer.Deserialize(responseData, JsonSerializationContext.Default.ChatCompletionResponse); if (output == null) { logger.LogError("Failed to deserialize chat completion response: {ResponseData}", responseData); diff --git a/sdk/cs/src/OpenAI/ChatCompletionResponse.cs b/sdk/cs/src/OpenAI/ChatCompletionResponse.cs new file mode 100644 index 00000000..e499705e --- /dev/null +++ b/sdk/cs/src/OpenAI/ChatCompletionResponse.cs @@ -0,0 +1,137 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.OpenAI; + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +/// +/// Reason the model stopped generating tokens. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum FinishReason +{ + /// The model finished naturally or hit a stop sequence. + [JsonStringEnumMemberName("stop")] + Stop, + + /// The model hit the maximum token limit. + [JsonStringEnumMemberName("length")] + Length, + + /// The model is requesting tool calls. + [JsonStringEnumMemberName("tool_calls")] + ToolCalls, + + /// Content was filtered by safety policy. + [JsonStringEnumMemberName("content_filter")] + ContentFilter, + + /// The model called a function (deprecated in favor of tool_calls). + [JsonStringEnumMemberName("function_call")] + FunctionCall +} + +/// +/// Response from a chat completion request. +/// +public class ChatCompletionResponse +{ + /// A unique identifier for the completion. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// The object type (e.g. "chat.completion"). + [JsonPropertyName("object")] + public string? ObjectType { get; set; } + + /// The Unix timestamp when the completion was created. + [JsonPropertyName("created")] + public long Created { get; set; } + + /// The model used for the completion. + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// The list of completion choices. + [JsonPropertyName("choices")] + public List? Choices { get; set; } + + /// Token usage statistics for the request. + [JsonPropertyName("usage")] + public CompletionUsage? Usage { get; set; } + + /// A fingerprint representing the backend configuration. + [JsonPropertyName("system_fingerprint")] + public string? SystemFingerprint { get; set; } + + /// Error information, if the request failed. + [JsonPropertyName("error")] + public ResponseError? Error { get; set; } +} + +/// +/// A single completion choice in a chat completion response. +/// +public class ChatChoice +{ + /// The index of this choice. + [JsonPropertyName("index")] + public int Index { get; set; } + + /// The chat message generated by the model (non-streaming). + [JsonPropertyName("message")] + public ChatMessage? Message { get; set; } + + /// The delta content for streaming responses. + [JsonPropertyName("delta")] + public ChatMessage? Delta { get; set; } + + /// The reason the model stopped generating. + [JsonPropertyName("finish_reason")] + public FinishReason? FinishReason { get; set; } +} + +/// +/// Token usage statistics for a completion request. +/// +public class CompletionUsage +{ + /// Number of tokens in the prompt. + [JsonPropertyName("prompt_tokens")] + public int PromptTokens { get; set; } + + /// Number of tokens in the completion. + [JsonPropertyName("completion_tokens")] + public int? CompletionTokens { get; set; } + + /// Total number of tokens used. + [JsonPropertyName("total_tokens")] + public int TotalTokens { get; set; } +} + +/// +/// Error information returned by the API. +/// +public class ResponseError +{ + /// The error code. + [JsonPropertyName("code")] + public string? Code { get; set; } + + /// A human-readable error message. + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// The error type (e.g. "invalid_request_error", "server_error"). + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// The parameter that caused the error. + [JsonPropertyName("param")] + public string? Param { get; set; } +} diff --git a/sdk/cs/src/OpenAI/ChatMessage.cs b/sdk/cs/src/OpenAI/ChatMessage.cs new file mode 100644 index 00000000..ddf7da5e --- /dev/null +++ b/sdk/cs/src/OpenAI/ChatMessage.cs @@ -0,0 +1,114 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.OpenAI; + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +/// +/// Role of a chat message author. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ChatMessageRole +{ + /// System instruction message. + [JsonStringEnumMemberName("system")] + System, + + /// User input message. + [JsonStringEnumMemberName("user")] + User, + + /// Assistant response message. + [JsonStringEnumMemberName("assistant")] + Assistant, + + /// Tool result message. + [JsonStringEnumMemberName("tool")] + Tool, + + /// Developer instruction message (replaces system for reasoning models). + [JsonStringEnumMemberName("developer")] + Developer +} + +/// +/// Type of tool call or tool definition. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ToolType +{ + /// A function tool. + [JsonStringEnumMemberName("function")] + Function +} + +/// +/// Represents a chat message in a conversation. +/// +public class ChatMessage +{ + /// The role of the message author. + [JsonPropertyName("role")] + public ChatMessageRole? Role { get; set; } + + /// The text content of the message. + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Optional name of the author of the message. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Tool call ID this message is responding to. + [JsonPropertyName("tool_call_id")] + public string? ToolCallId { get; set; } + + /// Tool calls generated by the model. + [JsonPropertyName("tool_calls")] + public IList? ToolCalls { get; set; } + + /// Deprecated function call generated by the model. + [JsonPropertyName("function_call")] + public FunctionCall? FunctionCall { get; set; } +} + +/// +/// Represents a tool call generated by the model. +/// +public class ToolCall +{ + /// The index of this tool call in the list (streaming only). + [JsonPropertyName("index")] + public int? Index { get; set; } + + /// The unique ID of the tool call. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// The type of tool call. + [JsonPropertyName("type")] + public ToolType? Type { get; set; } + + /// The function that the model called. + [JsonPropertyName("function")] + public FunctionCall? Function { get; set; } +} + +/// +/// Represents a function call with name and arguments. +/// +public class FunctionCall +{ + /// The name of the function to call. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// The arguments to pass to the function, as a JSON string. + [JsonPropertyName("arguments")] + public string? Arguments { get; set; } +} diff --git a/sdk/cs/src/OpenAI/LiveAudioTranscriptionClient.cs b/sdk/cs/src/OpenAI/LiveAudioTranscriptionClient.cs index 6da4d076..1d44e45c 100644 --- a/sdk/cs/src/OpenAI/LiveAudioTranscriptionClient.cs +++ b/sdk/cs/src/OpenAI/LiveAudioTranscriptionClient.cs @@ -1,4 +1,4 @@ -// -------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) Microsoft. All rights reserved. // @@ -6,9 +6,10 @@ namespace Microsoft.AI.Foundry.Local.OpenAI; -using System.Runtime.CompilerServices; using System.Globalization; +using System.Runtime.CompilerServices; using System.Threading.Channels; + using Microsoft.AI.Foundry.Local; using Microsoft.AI.Foundry.Local.Detail; using Microsoft.Extensions.Logging; @@ -382,4 +383,4 @@ public async ValueTask DisposeAsync() _lock.Dispose(); } } -} \ No newline at end of file +} diff --git a/sdk/cs/src/OpenAI/LiveAudioTranscriptionTypes.cs b/sdk/cs/src/OpenAI/LiveAudioTranscriptionTypes.cs index a0e98542..872216bc 100644 --- a/sdk/cs/src/OpenAI/LiveAudioTranscriptionTypes.cs +++ b/sdk/cs/src/OpenAI/LiveAudioTranscriptionTypes.cs @@ -1,19 +1,41 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + namespace Microsoft.AI.Foundry.Local.OpenAI; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; -using Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; + using Microsoft.AI.Foundry.Local; using Microsoft.AI.Foundry.Local.Detail; +/// +/// A content part within a transcription result, following the OpenAI Realtime +/// ConversationItem pattern. Access transcribed text via or +/// . +/// +public class TranscriptionContentPart +{ + /// The transcribed text. + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// The transcript (same as Text for transcription results). + [JsonPropertyName("transcript")] + public string? Transcript { get; set; } +} + /// /// Transcription result for real-time audio streaming sessions. -/// Extends the OpenAI Realtime API's so that +/// Follows the OpenAI Realtime API ConversationItem pattern so that /// customers access text via result.Content[0].Text or -/// result.Content[0].Transcript, ensuring forward compatibility -/// when the transport layer moves to WebSocket. +/// result.Content[0].Transcript. /// -public class LiveAudioTranscriptionResponse : ConversationItem +public class LiveAudioTranscriptionResponse { /// /// Whether this is a final or partial (interim) result. @@ -32,6 +54,10 @@ public class LiveAudioTranscriptionResponse : ConversationItem [JsonPropertyName("end_time")] public double? EndTime { get; init; } + /// Content parts. Access text via Content[0].Text or Content[0].Transcript. + [JsonPropertyName("content")] + public List? Content { get; set; } + internal static LiveAudioTranscriptionResponse FromJson(string json) { var raw = JsonSerializer.Deserialize(json, @@ -45,7 +71,7 @@ internal static LiveAudioTranscriptionResponse FromJson(string json) EndTime = raw.EndTime, Content = [ - new ContentPart + new TranscriptionContentPart { Text = raw.Text, Transcript = raw.Text @@ -102,4 +128,4 @@ internal record CoreErrorResponse return null; // unstructured error — treat as permanent } } -} \ No newline at end of file +} diff --git a/sdk/cs/src/OpenAI/ToolCallingExtensions.cs b/sdk/cs/src/OpenAI/ToolCallingExtensions.cs index fb3a72bf..f683b71e 100644 --- a/sdk/cs/src/OpenAI/ToolCallingExtensions.cs +++ b/sdk/cs/src/OpenAI/ToolCallingExtensions.cs @@ -6,18 +6,23 @@ namespace Microsoft.AI.Foundry.Local.OpenAI; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; - using System.Text.Json.Serialization; -// Extend response format beyond the OpenAI spec for LARK grammars +/// +/// Extended response format that adds LARK grammar support beyond the OpenAI spec. +/// +/// +/// Supported formats: +/// +/// {"type": "text"} +/// {"type": "json_object"} +/// {"type": "json_schema", "json_schema": ...} +/// {"type": "lark_grammar", "lark_grammar": ...} +/// +/// public class ResponseFormatExtended : ResponseFormat { - // Ex: - // 1. {"type": "text"} - // 2. {"type": "json_object"} - // 3. {"type": "json_schema", "json_schema": } - // 4. {"type": "lark_grammar", "lark_grammar": } + /// LARK grammar string when type is "lark_grammar". [JsonPropertyName("lark_grammar")] public string? LarkGrammar { get; set; } } diff --git a/sdk/cs/src/OpenAI/ToolCallingTypes.cs b/sdk/cs/src/OpenAI/ToolCallingTypes.cs new file mode 100644 index 00000000..1326fca8 --- /dev/null +++ b/sdk/cs/src/OpenAI/ToolCallingTypes.cs @@ -0,0 +1,229 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.OpenAI; + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +using Microsoft.AI.Foundry.Local.Detail; + +/// +/// Definition of a tool the model may call. +/// +public class ToolDefinition +{ + /// The type of tool. Defaults to . + [JsonPropertyName("type")] + public ToolType Type { get; set; } = ToolType.Function; + + /// The function definition. + [JsonPropertyName("function")] + public required FunctionDefinition Function { get; set; } +} + +/// +/// Definition of a function the model may call. +/// +public class FunctionDefinition +{ + /// The name of the function. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// A description of what the function does. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// The parameters the function accepts, described as a JSON Schema object. + [JsonPropertyName("parameters")] + public PropertyDefinition? Parameters { get; set; } + + /// Whether to enable strict schema adherence. + [JsonPropertyName("strict")] + public bool? Strict { get; set; } +} + +/// +/// JSON Schema property definition used to describe function parameters and structured outputs. +/// +public class PropertyDefinition +{ + /// The data type (e.g. "object", "string", "integer", "array"). + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// A description of the property. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Allowed values for enum types. + [JsonPropertyName("enum")] + public IList? Enum { get; set; } + + /// Nested properties for object types. + [JsonPropertyName("properties")] + public IDictionary? Properties { get; set; } + + /// Required property names for object types. + [JsonPropertyName("required")] + public IList? Required { get; set; } + + /// Schema for array item types. + [JsonPropertyName("items")] + public PropertyDefinition? Items { get; set; } + + /// Whether additional properties are allowed. + [JsonPropertyName("additionalProperties")] + public bool? AdditionalProperties { get; set; } +} + +/// +/// Response format specification for chat completions. +/// +public class ResponseFormat +{ + /// The format type (e.g. "text", "json_object", "json_schema"). + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// JSON Schema specification when type is "json_schema". + [JsonPropertyName("json_schema")] + public JsonSchema? JsonSchema { get; set; } +} + +/// +/// JSON Schema definition for structured output. +/// +public class JsonSchema +{ + /// The name of the schema. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Whether to enable strict schema adherence. + [JsonPropertyName("strict")] + public bool? Strict { get; set; } + + /// The JSON Schema definition. + [JsonPropertyName("schema")] + public PropertyDefinition? Schema { get; set; } +} + +/// +/// Controls which tool the model should use. +/// Use static methods , , +/// , or . +/// +[JsonConverter(typeof(ToolChoiceConverter))] +public class ToolChoice +{ + /// The tool choice type. + public string? Type { get; internal set; } + + /// Specifies a particular function to call. + public FunctionTool? Function { get; internal set; } + + /// Creates a choice indicating the model will not call any tool. + public static ToolChoice CreateNoneChoice() => new() { Type = "none" }; + + /// Creates a choice indicating the model can choose whether to call a tool. + public static ToolChoice CreateAutoChoice() => new() { Type = "auto" }; + + /// Creates a choice indicating the model must call one or more tools. + public static ToolChoice CreateRequiredChoice() => new() { Type = "required" }; + + /// Creates a choice indicating the model must call the specified function. + /// is null. + /// is empty. + public static ToolChoice CreateFunctionChoice(string functionName) + { + ArgumentNullException.ThrowIfNullOrEmpty(functionName, nameof(functionName)); + return new() { Type = "function", Function = new FunctionTool { Name = functionName } }; + } + + /// + /// Specifies a specific function tool to call. + /// + public class FunctionTool + { + /// The name of the function to call. + [JsonPropertyName("name")] + public string? Name { get; set; } + } +} + +/// +/// Custom JSON converter for that serializes +/// simple choices ("none", "auto", "required") as plain strings +/// and specific function choices as objects. +/// +internal class ToolChoiceConverter : JsonConverter +{ + public override ToolChoice? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType == JsonTokenType.String) + { + return new ToolChoice { Type = reader.GetString() }; + } + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException($"Unexpected token type {reader.TokenType} when deserializing ToolChoice."); + } + + var choice = new ToolChoice(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType != JsonTokenType.PropertyName) + { + continue; + } + + var prop = reader.GetString(); + reader.Read(); + switch (prop) + { + case "type": + choice.Type = reader.GetString(); + break; + case "function": + choice.Function = JsonSerializer.Deserialize(ref reader, JsonSerializationContext.Default.FunctionTool); + break; + default: + reader.Skip(); + break; + } + } + return choice; + } + + public override void Write(Utf8JsonWriter writer, ToolChoice value, JsonSerializerOptions options) + { + if (value.Function == null) + { + if (value.Type == null) + { + throw new JsonException("ToolChoice.Type must not be null when serializing."); + } + writer.WriteStringValue(value.Type); + return; + } + + writer.WriteStartObject(); + writer.WriteString("type", value.Type ?? "function"); + writer.WritePropertyName("function"); + JsonSerializer.Serialize(writer, value.Function, JsonSerializationContext.Default.FunctionTool); + writer.WriteEndObject(); + } +} diff --git a/sdk/cs/test/FoundryLocal.Tests/CatalogTests.cs b/sdk/cs/test/FoundryLocal.Tests/CatalogTests.cs index d270ac15..68267b03 100644 --- a/sdk/cs/test/FoundryLocal.Tests/CatalogTests.cs +++ b/sdk/cs/test/FoundryLocal.Tests/CatalogTests.cs @@ -5,6 +5,7 @@ // -------------------------------------------------------------------------------------------------------------------- namespace Microsoft.AI.Foundry.Local.Tests; + using System.Collections.Generic; using System.Linq; using System.Text.Json; diff --git a/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs b/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs index 2624f98a..b12d5d23 100644 --- a/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs +++ b/sdk/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs @@ -1,4 +1,4 @@ -// -------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) Microsoft. All rights reserved. // @@ -9,9 +9,7 @@ namespace Microsoft.AI.Foundry.Local.Tests; using System.Text; using System.Threading.Tasks; -using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; -using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; -using Betalgo.Ranul.OpenAI.ObjectModels.SharedModels; +using Microsoft.AI.Foundry.Local.OpenAI; internal sealed class ChatCompletionsTests { @@ -45,7 +43,7 @@ public async Task DirectChat_NoStreaming_Succeeds() List messages = [ // System prompt is setup by GenAI - new ChatMessage { Role = "user", Content = "You are a calculator. Be precise. What is the answer to 7 multiplied by 6?" } + new ChatMessage { Role = ChatMessageRole.User, Content = "You are a calculator. Be precise. What is the answer to 7 multiplied by 6?" } ]; var response = await chatClient.CompleteChatAsync(messages).ConfigureAwait(false); @@ -54,16 +52,16 @@ public async Task DirectChat_NoStreaming_Succeeds() await Assert.That(response.Choices).IsNotNull().And.IsNotEmpty(); var message = response.Choices[0].Message; await Assert.That(message).IsNotNull(); - await Assert.That(message.Role).IsEqualTo("assistant"); + await Assert.That(message.Role).IsEqualTo(ChatMessageRole.Assistant); await Assert.That(message.Content).IsNotNull(); await Assert.That(message.Content).Contains("42"); Console.WriteLine($"Response: {message.Content}"); - messages.Add(new ChatMessage { Role = "assistant", Content = message.Content }); + messages.Add(new ChatMessage { Role = ChatMessageRole.Assistant, Content = message.Content }); messages.Add(new ChatMessage { - Role = "user", + Role = ChatMessageRole.User, Content = "Is the answer a real number?" }); @@ -84,7 +82,7 @@ public async Task DirectChat_Streaming_Succeeds() List messages = [ - new ChatMessage { Role = "user", Content = "You are a calculator. Be precise. What is the answer to 7 multiplied by 6?" } + new ChatMessage { Role = ChatMessageRole.User, Content = "You are a calculator. Be precise. What is the answer to 7 multiplied by 6?" } ]; var updates = chatClient.CompleteChatStreamingAsync(messages, CancellationToken.None).ConfigureAwait(false); @@ -96,7 +94,7 @@ public async Task DirectChat_Streaming_Succeeds() await Assert.That(response.Choices).IsNotNull().And.IsNotEmpty(); var message = response.Choices[0].Message; await Assert.That(message).IsNotNull(); - await Assert.That(message.Role).IsEqualTo("assistant"); + await Assert.That(message.Role).IsEqualTo(ChatMessageRole.Assistant); await Assert.That(message.Content).IsNotNull(); responseMessage.Append(message.Content); } @@ -105,10 +103,10 @@ public async Task DirectChat_Streaming_Succeeds() Console.WriteLine(fullResponse); await Assert.That(fullResponse).Contains("42"); - messages.Add(new ChatMessage { Role = "assistant", Content = fullResponse }); + messages.Add(new ChatMessage { Role = ChatMessageRole.Assistant, Content = fullResponse }); messages.Add(new ChatMessage { - Role = "user", + Role = ChatMessageRole.User, Content = "Add 25 to the previous answer. Think hard to be sure of the answer." }); @@ -120,7 +118,7 @@ public async Task DirectChat_Streaming_Succeeds() await Assert.That(response.Choices).IsNotNull().And.IsNotEmpty(); var message = response.Choices[0].Message; await Assert.That(message).IsNotNull(); - await Assert.That(message.Role).IsEqualTo("assistant"); + await Assert.That(message.Role).IsEqualTo(ChatMessageRole.Assistant); await Assert.That(message.Content).IsNotNull(); responseMessage.Append(message.Content); } @@ -138,19 +136,19 @@ public async Task DirectTool_NoStreaming_Succeeds() chatClient.Settings.MaxTokens = 500; chatClient.Settings.Temperature = 0.0f; // for deterministic results - chatClient.Settings.ToolChoice = ToolChoice.Required; // Force the model to make a tool call + chatClient.Settings.ToolChoice = ToolChoice.CreateRequiredChoice(); // Force the model to make a tool call // Prepare messages and tools List messages = [ - new ChatMessage { Role = "system", Content = "You are a helpful AI assistant. If necessary, you can use any provided tools to answer the question." }, - new ChatMessage { Role = "user", Content = "What is the answer to 7 multiplied by 6?" } + new ChatMessage { Role = ChatMessageRole.System, Content = "You are a helpful AI assistant. If necessary, you can use any provided tools to answer the question." }, + new ChatMessage { Role = ChatMessageRole.User, Content = "What is the answer to 7 multiplied by 6?" } ]; List tools = [ new ToolDefinition { - Type = "function", + Type = ToolType.Function, Function = new FunctionDefinition() { Name = "multiply_numbers", @@ -177,7 +175,7 @@ public async Task DirectTool_NoStreaming_Succeeds() await Assert.That(response).IsNotNull(); await Assert.That(response.Choices).IsNotNull().And.IsNotEmpty(); await Assert.That(response.Choices.Count).IsEqualTo(1); - await Assert.That(response.Choices[0].FinishReason).IsEqualTo("tool_calls"); + await Assert.That(response.Choices[0].FinishReason).IsEqualTo(FinishReason.ToolCalls); await Assert.That(response.Choices[0].Message).IsNotNull(); await Assert.That(response.Choices[0].Message.ToolCalls).IsNotNull().And.IsNotEmpty(); @@ -188,23 +186,23 @@ public async Task DirectTool_NoStreaming_Succeeds() var expectedResponse = "" + toolCall + ""; await Assert.That(response.Choices[0].Message.Content).IsEqualTo(expectedResponse); - await Assert.That(response.Choices[0].Message.ToolCalls?[0].Type).IsEqualTo("function"); - await Assert.That(response.Choices[0].Message.ToolCalls?[0].FunctionCall?.Name).IsEqualTo("multiply_numbers"); + await Assert.That(response.Choices[0].Message.ToolCalls?[0].Type).IsEqualTo(ToolType.Function); + await Assert.That(response.Choices[0].Message.ToolCalls?[0].Function?.Name).IsEqualTo("multiply_numbers"); var expectedArguments = /*lang=json*/ "{\r\n \"first\": 7,\r\n \"second\": 6\r\n}"; expectedArguments = OperatingSystemConverter.ToJson(expectedArguments); - await Assert.That(response.Choices[0].Message.ToolCalls?[0].FunctionCall?.Arguments).IsEqualTo(expectedArguments); + await Assert.That(response.Choices[0].Message.ToolCalls?[0].Function?.Arguments).IsEqualTo(expectedArguments); // Add the response from invoking the tool call to the conversation and check if the model can continue correctly var toolCallResponse = "7 x 6 = 42."; - messages.Add(new ChatMessage { Role = "tool", Content = toolCallResponse }); + messages.Add(new ChatMessage { Role = ChatMessageRole.Tool, Content = toolCallResponse }); // Prompt the model to continue the conversation after the tool call - messages.Add(new ChatMessage { Role = "system", Content = "Respond only with the answer generated by the tool." }); + messages.Add(new ChatMessage { Role = ChatMessageRole.System, Content = "Respond only with the answer generated by the tool." }); // Set tool calling back to auto so that the model can decide whether to call // the tool again or continue the conversation based on the new user prompt - chatClient.Settings.ToolChoice = ToolChoice.Auto; + chatClient.Settings.ToolChoice = ToolChoice.CreateAutoChoice(); // Run the next turn of the conversation response = await chatClient.CompleteChatAsync(messages, tools).ConfigureAwait(false); @@ -222,19 +220,19 @@ public async Task DirectTool_Streaming_Succeeds() chatClient.Settings.MaxTokens = 500; chatClient.Settings.Temperature = 0.0f; // for deterministic results - chatClient.Settings.ToolChoice = ToolChoice.Required; // Force the model to make a tool call + chatClient.Settings.ToolChoice = ToolChoice.CreateRequiredChoice(); // Force the model to make a tool call // Prepare messages and tools List messages = [ - new ChatMessage { Role = "system", Content = "You are a helpful AI assistant. If necessary, you can use any provided tools to answer the question." }, - new ChatMessage { Role = "user", Content = "What is the answer to 7 multiplied by 6?" } + new ChatMessage { Role = ChatMessageRole.System, Content = "You are a helpful AI assistant. If necessary, you can use any provided tools to answer the question." }, + new ChatMessage { Role = ChatMessageRole.User, Content = "What is the answer to 7 multiplied by 6?" } ]; List tools = [ new ToolDefinition { - Type = "function", + Type = ToolType.Function, Function = new FunctionDefinition() { Name = "multiply_numbers", @@ -259,7 +257,7 @@ public async Task DirectTool_Streaming_Succeeds() // Check that each response chunk contains the expected information StringBuilder responseMessage = new(); var numTokens = 0; - ChatCompletionCreateResponse? toolCallResponse = null; + ChatCompletionResponse? toolCallResponse = null; await foreach (var response in updates) { await Assert.That(response).IsNotNull(); @@ -273,7 +271,7 @@ public async Task DirectTool_Streaming_Succeeds() responseMessage.Append(content); numTokens += 1; } - if (response.Choices[0].FinishReason == "tool_calls") + if (response.Choices[0].FinishReason == FinishReason.ToolCalls) { toolCallResponse = response; } @@ -289,26 +287,26 @@ public async Task DirectTool_Streaming_Succeeds() await Assert.That(fullResponse).IsNotNull(); await Assert.That(fullResponse).IsEqualTo(expectedResponse); await Assert.That(toolCallResponse?.Choices.Count).IsEqualTo(1); - await Assert.That(toolCallResponse?.Choices[0].FinishReason).IsEqualTo("tool_calls"); + await Assert.That(toolCallResponse?.Choices[0].FinishReason).IsEqualTo(FinishReason.ToolCalls); await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls).IsNotNull(); await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls?.Count).IsEqualTo(1); - await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls?[0].Type).IsEqualTo("function"); - await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls?[0].FunctionCall?.Name).IsEqualTo("multiply_numbers"); + await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls?[0].Type).IsEqualTo(ToolType.Function); + await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls?[0].Function?.Name).IsEqualTo("multiply_numbers"); var expectedArguments = /*lang=json*/ "{\r\n \"first\": 7,\r\n \"second\": 6\r\n}"; expectedArguments = OperatingSystemConverter.ToJson(expectedArguments); - await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls?[0].FunctionCall?.Arguments).IsEqualTo(expectedArguments); + await Assert.That(toolCallResponse?.Choices[0].Message.ToolCalls?[0].Function?.Arguments).IsEqualTo(expectedArguments); // Add the response from invoking the tool call to the conversation and check if the model can continue correctly var toolResponse = "7 x 6 = 42."; - messages.Add(new ChatMessage { Role = "tool", Content = toolResponse }); + messages.Add(new ChatMessage { Role = ChatMessageRole.Tool, Content = toolResponse }); // Prompt the model to continue the conversation after the tool call - messages.Add(new ChatMessage { Role = "system", Content = "Respond only with the answer generated by the tool." }); + messages.Add(new ChatMessage { Role = ChatMessageRole.System, Content = "Respond only with the answer generated by the tool." }); // Set tool calling back to auto so that the model can decide whether to call // the tool again or continue the conversation based on the new user prompt - chatClient.Settings.ToolChoice = ToolChoice.Auto; + chatClient.Settings.ToolChoice = ToolChoice.CreateAutoChoice(); // Run the next turn of the conversation updates = chatClient.CompleteChatStreamingAsync(messages, tools, CancellationToken.None).ConfigureAwait(false); diff --git a/sdk/cs/test/FoundryLocal.Tests/EndToEnd.cs b/sdk/cs/test/FoundryLocal.Tests/EndToEnd.cs index 56c70769..3e5bacdc 100644 --- a/sdk/cs/test/FoundryLocal.Tests/EndToEnd.cs +++ b/sdk/cs/test/FoundryLocal.Tests/EndToEnd.cs @@ -5,6 +5,7 @@ // -------------------------------------------------------------------------------------------------------------------- namespace Microsoft.AI.Foundry.Local.Tests; + using System; using System.Threading.Tasks; diff --git a/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj b/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj index fe0dfcd2..3d0c44fb 100644 --- a/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj +++ b/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj @@ -47,7 +47,6 @@ - diff --git a/sdk/cs/test/FoundryLocal.Tests/SkipInCIAttribute.cs b/sdk/cs/test/FoundryLocal.Tests/SkipInCIAttribute.cs index c4d17e5b..69db2f85 100644 --- a/sdk/cs/test/FoundryLocal.Tests/SkipInCIAttribute.cs +++ b/sdk/cs/test/FoundryLocal.Tests/SkipInCIAttribute.cs @@ -6,10 +6,10 @@ namespace Microsoft.AI.Foundry.Local.Tests; -using TUnit.Core; - using System.Threading.Tasks; +using TUnit.Core; + public class SkipInCIAttribute() : SkipAttribute("This test is only supported locally. Skipped on CIs.") { public override Task ShouldSkip(TestRegisteredContext context) diff --git a/sdk/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs b/sdk/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs index 2136a8eb..4720b76e 100644 --- a/sdk/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs +++ b/sdk/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs @@ -5,6 +5,7 @@ // -------------------------------------------------------------------------------------------------------------------- namespace Microsoft.AI.Foundry.Local.Tests; + using System.Threading.Tasks; internal static class TestAssemblySetupCleanup diff --git a/sdk/cs/test/FoundryLocal.Tests/Utils.cs b/sdk/cs/test/FoundryLocal.Tests/Utils.cs index 9611d0d4..c10ea484 100644 --- a/sdk/cs/test/FoundryLocal.Tests/Utils.cs +++ b/sdk/cs/test/FoundryLocal.Tests/Utils.cs @@ -21,15 +21,15 @@ namespace Microsoft.AI.Foundry.Local.Tests; internal static class Utils { - internal struct TestCatalogInfo + internal readonly struct TestCatalogInfo { - internal readonly List TestCatalog { get; } - internal readonly string ModelListJson { get; } + internal List TestCatalog { get; } + internal string ModelListJson { get; } internal TestCatalogInfo(bool includeCuda) { - TestCatalog = Utils.BuildTestCatalog(includeCuda); + TestCatalog = BuildTestCatalog(includeCuda); ModelListJson = JsonSerializer.Serialize(TestCatalog, JsonSerializationContext.Default.ListModelInfo); } } @@ -96,7 +96,7 @@ public static void AssemblyInit(AssemblyHookContext _) FoundryLocalManager.CreateAsync(config, logger).GetAwaiter().GetResult(); // standalone instance for testing individual components that skips the 'initialize' command - CoreInterop = new CoreInterop(logger); + CoreInterop = new CoreInterop(logger); } internal static ICoreInterop CoreInterop { get; private set; } = default!; @@ -234,7 +234,7 @@ private static List BuildTestCatalog(bool includeCuda = true) PromptTemplate = common.PromptTemplate, Publisher = common.Publisher, Task = common.Task, FileSizeMb = common.FileSizeMb - 10, // smaller so default chosen in test that sorts on this - ModelSettings = common.ModelSettings, + ModelSettings = common.ModelSettings, SupportsToolCalling = common.SupportsToolCalling, License = common.License, LicenseDescription = common.LicenseDescription, @@ -444,7 +444,9 @@ private static string GetRepoRoot() while (dir != null) { if (Directory.Exists(Path.Combine(dir.FullName, ".git"))) + { return dir.FullName; + } dir = dir.Parent; } diff --git a/sdk/js/src/openai/chatClient.ts b/sdk/js/src/openai/chatClient.ts index f844da41..fc2c84aa 100644 --- a/sdk/js/src/openai/chatClient.ts +++ b/sdk/js/src/openai/chatClient.ts @@ -4,6 +4,7 @@ import { ResponseFormat, ToolChoice } from '../types.js'; export class ChatClientSettings { frequencyPenalty?: number; maxTokens?: number; + maxCompletionTokens?: number; n?: number; temperature?: number; presencePenalty?: number; @@ -31,6 +32,7 @@ export class ChatClientSettings { const result: any = { frequency_penalty: this.frequencyPenalty, max_tokens: this.maxTokens, + max_completion_tokens: this.maxCompletionTokens, n: this.n, presence_penalty: this.presencePenalty, temperature: this.temperature, diff --git a/sdk/python/src/detail/model_data_types.py b/sdk/python/src/detail/model_data_types.py index 46525dc7..95132f9e 100644 --- a/sdk/python/src/detail/model_data_types.py +++ b/sdk/python/src/detail/model_data_types.py @@ -12,6 +12,7 @@ class DeviceType(StrEnum): """Device types supported by model variants.""" + Invalid = "Invalid" CPU = "CPU" GPU = "GPU" NPU = "NPU" diff --git a/sdk/python/src/openai/chat_client.py b/sdk/python/src/openai/chat_client.py index 0b0d58bc..4a214661 100644 --- a/sdk/python/src/openai/chat_client.py +++ b/sdk/python/src/openai/chat_client.py @@ -34,6 +34,7 @@ def __init__( self, frequency_penalty: Optional[float] = None, max_tokens: Optional[int] = None, + max_completion_tokens: Optional[int] = None, n: Optional[int] = None, temperature: Optional[float] = None, presence_penalty: Optional[float] = None, @@ -45,6 +46,7 @@ def __init__( ): self.frequency_penalty = frequency_penalty self.max_tokens = max_tokens + self.max_completion_tokens = max_completion_tokens self.n = n self.temperature = temperature self.presence_penalty = presence_penalty @@ -63,6 +65,7 @@ def _serialize(self) -> Dict[str, Any]: k: v for k, v in { "frequency_penalty": self.frequency_penalty, "max_tokens": self.max_tokens, + "max_completion_tokens": self.max_completion_tokens, "n": self.n, "presence_penalty": self.presence_penalty, "temperature": self.temperature, diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml index 2a6292b7..2f168bf8 100644 --- a/sdk/rust/Cargo.toml +++ b/sdk/rust/Cargo.toml @@ -24,13 +24,11 @@ tokio-stream = "0.1" futures-core = "0.3" reqwest = { version = "0.12", features = ["json"] } urlencoding = "2" -async-openai = { version = "0.33", default-features = false, features = ["chat-completion-types"] } [build-dependencies] ureq = "3" zip = "2" serde_json = "1" -serde = { version = "1", features = ["derive"] } [[example]] name = "chat_completion" diff --git a/sdk/rust/GENERATE-DOCS.md b/sdk/rust/GENERATE-DOCS.md index f02b5d99..1be5d6a1 100644 --- a/sdk/rust/GENERATE-DOCS.md +++ b/sdk/rust/GENERATE-DOCS.md @@ -26,8 +26,8 @@ The SDK re-exports all public types from the crate root. Key modules: | `ModelVariant` | Single variant — download, load, unload | | `ChatClient` | OpenAI-compatible chat completions (sync + streaming) | | `AudioClient` | OpenAI-compatible audio transcription (sync + streaming) | -| `CreateChatCompletionResponse` | Typed chat completion response (from `async-openai`) | -| `CreateChatCompletionStreamResponse` | Typed streaming chat chunk (from `async-openai`) | +| `CreateChatCompletionResponse` | Typed chat completion response | +| `CreateChatCompletionStreamResponse` | Typed streaming chat chunk | | `AudioTranscriptionResponse` | Typed audio transcription response | | `FoundryLocalError` | Error enum with variants for all failure modes | diff --git a/sdk/rust/docs/api.md b/sdk/rust/docs/api.md index 278402fb..aa6926a0 100644 --- a/sdk/rust/docs/api.md +++ b/sdk/rust/docs/api.md @@ -517,7 +517,7 @@ Implements: `Display`, `Error`, `From`, `From ## Re-exported OpenAI Types -The following types from `async_openai` are re-exported at the crate root for convenience: +The following OpenAI-compatible types are re-exported at the crate root for convenience: **Request types:** - `ChatCompletionRequestMessage` @@ -526,8 +526,6 @@ The following types from `async_openai` are re-exported at the crate root for co - `ChatCompletionRequestAssistantMessage` - `ChatCompletionRequestToolMessage` - `ChatCompletionTools` -- `ChatCompletionToolChoiceOption` -- `ChatCompletionNamedToolChoice` - `FunctionObject` **Response types:** @@ -546,3 +544,9 @@ The following types from `async_openai` are re-exported at the crate root for co - `ChatCompletionMessageToolCalls` - `FunctionCall` - `FunctionCallStream` + +**Audio types:** +- `AudioTranscriptionResponse` +- `AudioTranscriptionStream` +- `TranscriptionSegment` +- `TranscriptionWord` diff --git a/sdk/rust/examples/interactive_chat.rs b/sdk/rust/examples/interactive_chat.rs index bd230155..44ea1887 100644 --- a/sdk/rust/examples/interactive_chat.rs +++ b/sdk/rust/examples/interactive_chat.rs @@ -96,7 +96,7 @@ async fn main() -> Result<(), Box> { // Add assistant reply to history for multi-turn conversation messages.push( ChatCompletionRequestAssistantMessage { - content: Some(full_response.into()), + content: Some(full_response), ..Default::default() } .into(), diff --git a/sdk/rust/examples/tool_calling.rs b/sdk/rust/examples/tool_calling.rs index fecf6bc5..ea8bce5a 100644 --- a/sdk/rust/examples/tool_calling.rs +++ b/sdk/rust/examples/tool_calling.rs @@ -167,7 +167,7 @@ async fn main() -> Result<()> { messages.push(assistant_msg); messages.push( ChatCompletionRequestToolMessage { - content: result.into(), + content: result, tool_call_id: tc["id"].as_str().unwrap_or_default().to_string(), } .into(), diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs index 872a875c..1d47a52e 100644 --- a/sdk/rust/src/lib.rs +++ b/sdk/rust/src/lib.rs @@ -22,22 +22,21 @@ pub use self::types::{ }; // Re-export OpenAI request types so callers can construct typed messages. -pub use async_openai::types::chat::{ - ChatCompletionNamedToolChoice, ChatCompletionRequestAssistantMessage, - ChatCompletionRequestMessage, ChatCompletionRequestSystemMessage, - ChatCompletionRequestToolMessage, ChatCompletionRequestUserMessage, - ChatCompletionToolChoiceOption, ChatCompletionTools, FunctionObject, +pub use crate::openai::chat_types::{ + ChatCompletionRequestAssistantMessage, ChatCompletionRequestMessage, + ChatCompletionRequestSystemMessage, ChatCompletionRequestToolMessage, + ChatCompletionRequestUserMessage, ChatCompletionTools, FunctionObject, }; // Re-export OpenAI response types for convenience. -pub use crate::openai::{ - AudioTranscriptionResponse, AudioTranscriptionStream, ChatCompletionStream, - TranscriptionSegment, TranscriptionWord, -}; -pub use async_openai::types::chat::{ +pub use crate::openai::chat_types::{ ChatChoice, ChatChoiceStream, ChatCompletionMessageToolCall, ChatCompletionMessageToolCallChunk, ChatCompletionMessageToolCalls, ChatCompletionResponseMessage, ChatCompletionStreamResponseDelta, CompletionUsage, CreateChatCompletionResponse, CreateChatCompletionStreamResponse, FinishReason, FunctionCall, FunctionCallStream, }; +pub use crate::openai::{ + AudioTranscriptionResponse, AudioTranscriptionStream, ChatCompletionStream, + TranscriptionSegment, TranscriptionWord, +}; diff --git a/sdk/rust/src/openai/chat_client.rs b/sdk/rust/src/openai/chat_client.rs index 6597de82..a5e84e45 100644 --- a/sdk/rust/src/openai/chat_client.rs +++ b/sdk/rust/src/openai/chat_client.rs @@ -3,10 +3,11 @@ use std::collections::HashMap; use std::sync::Arc; -use async_openai::types::chat::{ +use super::chat_types::{ ChatCompletionRequestMessage, ChatCompletionTools, CreateChatCompletionResponse, CreateChatCompletionStreamResponse, }; +use serde::Serialize; use serde_json::{json, Value}; use crate::detail::core_interop::CoreInterop; @@ -24,83 +25,39 @@ use super::json_stream::JsonStream; /// .temperature(0.7) /// .max_tokens(256); /// ``` -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] pub struct ChatClientSettings { + #[serde(skip_serializing_if = "Option::is_none")] frequency_penalty: Option, + #[serde(skip_serializing_if = "Option::is_none")] max_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_completion_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] n: Option, + #[serde(skip_serializing_if = "Option::is_none")] temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] presence_penalty: Option, + #[serde(skip_serializing_if = "Option::is_none")] top_p: Option, + #[serde(skip)] top_k: Option, + #[serde(skip)] random_seed: Option, + #[serde(skip_serializing_if = "Option::is_none")] response_format: Option, + #[serde(skip_serializing_if = "Option::is_none")] tool_choice: Option, } impl ChatClientSettings { fn serialize(&self) -> Value { - let mut map = serde_json::Map::new(); - - if let Some(v) = self.frequency_penalty { - map.insert("frequency_penalty".into(), json!(v)); - } - if let Some(v) = self.max_tokens { - map.insert("max_tokens".into(), json!(v)); - } - if let Some(v) = self.n { - map.insert("n".into(), json!(v)); - } - if let Some(v) = self.presence_penalty { - map.insert("presence_penalty".into(), json!(v)); - } - if let Some(v) = self.temperature { - map.insert("temperature".into(), json!(v)); - } - if let Some(v) = self.top_p { - map.insert("top_p".into(), json!(v)); - } - - if let Some(ref rf) = self.response_format { - let mut rf_map = serde_json::Map::new(); - match rf { - ChatResponseFormat::Text => { - rf_map.insert("type".into(), json!("text")); - } - ChatResponseFormat::JsonObject => { - rf_map.insert("type".into(), json!("json_object")); - } - ChatResponseFormat::JsonSchema(schema) => { - rf_map.insert("type".into(), json!("json_schema")); - rf_map.insert("jsonSchema".into(), json!(schema)); - } - ChatResponseFormat::LarkGrammar(grammar) => { - rf_map.insert("type".into(), json!("lark_grammar")); - rf_map.insert("larkGrammar".into(), json!(grammar)); - } - } - map.insert("response_format".into(), Value::Object(rf_map)); - } - - if let Some(ref tc) = self.tool_choice { - let mut tc_map = serde_json::Map::new(); - match tc { - ChatToolChoice::None => { - tc_map.insert("type".into(), json!("none")); - } - ChatToolChoice::Auto => { - tc_map.insert("type".into(), json!("auto")); - } - ChatToolChoice::Required => { - tc_map.insert("type".into(), json!("required")); - } - ChatToolChoice::Function(name) => { - tc_map.insert("type".into(), json!("function")); - tc_map.insert("name".into(), json!(name)); - } - } - map.insert("tool_choice".into(), Value::Object(tc_map)); - } + let mut value = + serde_json::to_value(self).expect("ChatClientSettings should always be serializable"); + let map = value + .as_object_mut() + .expect("ChatClientSettings serializes to a JSON object"); // Foundry-specific metadata for settings that don't map directly to // the OpenAI spec. @@ -115,7 +72,7 @@ impl ChatClientSettings { map.insert("metadata".into(), json!(metadata)); } - Value::Object(map) + value } } @@ -152,6 +109,12 @@ impl ChatClient { self } + /// Set the maximum number of completion tokens to generate (newer OpenAI field). + pub fn max_completion_tokens(mut self, v: u32) -> Self { + self.settings.max_completion_tokens = Some(v); + self + } + /// Set the number of completions to generate. pub fn n(mut self, v: u32) -> Self { self.settings.n = Some(v); diff --git a/sdk/rust/src/openai/chat_types.rs b/sdk/rust/src/openai/chat_types.rs new file mode 100644 index 00000000..003d5d2c --- /dev/null +++ b/sdk/rust/src/openai/chat_types.rs @@ -0,0 +1,301 @@ +//! OpenAI-compatible chat completion types. + +use serde::{Deserialize, Serialize}; + +// ─── Request types ─────────────────────────────────────────────────────────── + +/// A chat completion request message, internally tagged by the `role` field. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(tag = "role")] +pub enum ChatCompletionRequestMessage { + #[serde(rename = "system")] + System(ChatCompletionRequestSystemMessage), + #[serde(rename = "user")] + User(ChatCompletionRequestUserMessage), + #[serde(rename = "assistant")] + Assistant(ChatCompletionRequestAssistantMessage), + #[serde(rename = "tool")] + Tool(ChatCompletionRequestToolMessage), + #[serde(rename = "developer")] + Developer(ChatCompletionRequestDeveloperMessage), +} + +/// A system message in a chat completion request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionRequestSystemMessage { + pub content: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl From<&str> for ChatCompletionRequestSystemMessage { + fn from(s: &str) -> Self { + Self::from(s.to_owned()) + } +} + +impl From for ChatCompletionRequestSystemMessage { + fn from(s: String) -> Self { + Self { + content: s, + name: None, + } + } +} + +impl From for ChatCompletionRequestMessage { + fn from(msg: ChatCompletionRequestSystemMessage) -> Self { + Self::System(msg) + } +} + +/// A user message in a chat completion request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionRequestUserMessage { + pub content: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl From<&str> for ChatCompletionRequestUserMessage { + fn from(s: &str) -> Self { + Self::from(s.to_owned()) + } +} + +impl From for ChatCompletionRequestUserMessage { + fn from(s: String) -> Self { + Self { + content: s, + name: None, + } + } +} + +impl From for ChatCompletionRequestMessage { + fn from(msg: ChatCompletionRequestUserMessage) -> Self { + Self::User(msg) + } +} + +/// An assistant message in a chat completion request. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ChatCompletionRequestAssistantMessage { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub function_call: Option, +} + +impl From for ChatCompletionRequestMessage { + fn from(msg: ChatCompletionRequestAssistantMessage) -> Self { + Self::Assistant(msg) + } +} + +/// A tool result message in a chat completion request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionRequestToolMessage { + pub content: String, + pub tool_call_id: String, +} + +impl From for ChatCompletionRequestMessage { + fn from(msg: ChatCompletionRequestToolMessage) -> Self { + Self::Tool(msg) + } +} + +/// A developer message in a chat completion request (replaces system for reasoning models). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionRequestDeveloperMessage { + pub content: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl From<&str> for ChatCompletionRequestDeveloperMessage { + fn from(s: &str) -> Self { + Self::from(s.to_owned()) + } +} + +impl From for ChatCompletionRequestDeveloperMessage { + fn from(s: String) -> Self { + Self { + content: s, + name: None, + } + } +} + +impl From for ChatCompletionRequestMessage { + fn from(msg: ChatCompletionRequestDeveloperMessage) -> Self { + Self::Developer(msg) + } +} + +/// A tool definition for a chat completion request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionTools { + #[serde(rename = "type")] + pub r#type: String, + pub function: FunctionObject, +} + +/// Description of a function that the model can call. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FunctionObject { + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parameters: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub strict: Option, +} + +// ─── Response types ────────────────────────────────────────────────────────── + +/// Response object for a non-streaming chat completion. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateChatCompletionResponse { + pub id: String, + pub object: String, + pub created: u64, + pub model: String, + pub choices: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub usage: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub system_fingerprint: Option, +} + +/// A single choice within a non-streaming chat completion response. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatChoice { + pub index: u32, + pub message: ChatCompletionResponseMessage, + #[serde(default)] + pub finish_reason: Option, +} + +/// The assistant's message inside a [`ChatChoice`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionResponseMessage { + #[serde(default)] + pub role: Option, + #[serde(default)] + pub content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub function_call: Option, +} + +/// Token usage statistics. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CompletionUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + pub total_tokens: u32, +} + +/// Reason the model stopped generating tokens. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "snake_case")] +pub enum FinishReason { + Stop, + Length, + ToolCalls, + ContentFilter, + FunctionCall, +} + +/// A tool call within a response message, tagged by `type`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(tag = "type")] +pub enum ChatCompletionMessageToolCalls { + #[serde(rename = "function")] + Function(ChatCompletionMessageToolCall), +} + +/// A single function tool call (id + function payload). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionMessageToolCall { + pub id: String, + pub function: FunctionCall, +} + +/// A resolved function call with name and JSON-encoded arguments. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct FunctionCall { + pub name: String, + pub arguments: String, +} + +// ─── Streaming response types ──────────────────────────────────────────────── + +/// Response object for a streaming chat completion chunk. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateChatCompletionStreamResponse { + pub id: String, + pub object: String, + pub created: u64, + pub model: String, + pub choices: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub usage: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub system_fingerprint: Option, +} + +/// A single choice within a streaming response chunk. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatChoiceStream { + pub index: u32, + pub delta: ChatCompletionStreamResponseDelta, + #[serde(default)] + pub finish_reason: Option, +} + +/// The delta payload inside a streaming [`ChatChoiceStream`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionStreamResponseDelta { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub role: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub function_call: Option, +} + +/// A partial tool call chunk received during streaming. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatCompletionMessageToolCallChunk { + pub index: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")] + pub r#type: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub function: Option, +} + +/// A partial function call received during streaming. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct FunctionCallStream { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub arguments: Option, +} diff --git a/sdk/rust/src/openai/mod.rs b/sdk/rust/src/openai/mod.rs index c3d4a645..f3ad0931 100644 --- a/sdk/rust/src/openai/mod.rs +++ b/sdk/rust/src/openai/mod.rs @@ -1,5 +1,6 @@ mod audio_client; mod chat_client; +pub mod chat_types; mod json_stream; pub use self::audio_client::{ diff --git a/sdk/rust/src/types.rs b/sdk/rust/src/types.rs index 28b37ed2..9e1ffb63 100644 --- a/sdk/rust/src/types.rs +++ b/sdk/rust/src/types.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; /// Hardware device type for model execution. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -113,6 +113,36 @@ pub enum ChatResponseFormat { LarkGrammar(String), } +impl Serialize for ChatResponseFormat { + fn serialize(&self, serializer: S) -> std::result::Result { + use serde::ser::SerializeMap; + match self { + ChatResponseFormat::Text => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("type", "text")?; + map.end() + } + ChatResponseFormat::JsonObject => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("type", "json_object")?; + map.end() + } + ChatResponseFormat::JsonSchema(schema) => { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("type", "json_schema")?; + map.serialize_entry("json_schema", schema)?; + map.end() + } + ChatResponseFormat::LarkGrammar(grammar) => { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("type", "lark_grammar")?; + map.serialize_entry("lark_grammar", grammar)?; + map.end() + } + } + } +} + /// Tool choice configuration for chat completions. #[derive(Debug, Clone)] pub enum ChatToolChoice { @@ -126,6 +156,24 @@ pub enum ChatToolChoice { Function(String), } +impl Serialize for ChatToolChoice { + fn serialize(&self, serializer: S) -> std::result::Result { + use serde::ser::SerializeMap; + match self { + ChatToolChoice::None => serializer.serialize_str("none"), + ChatToolChoice::Auto => serializer.serialize_str("auto"), + ChatToolChoice::Required => serializer.serialize_str("required"), + ChatToolChoice::Function(name) => { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("type", "function")?; + let func = serde_json::json!({ "name": name }); + map.serialize_entry("function", &func)?; + map.end() + } + } + } +} + /// Information about an available execution provider bootstrapper. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] diff --git a/sdk/rust/tests/integration/chat_client_test.rs b/sdk/rust/tests/integration/chat_client_test.rs index b24f3804..e7758ad5 100644 --- a/sdk/rust/tests/integration/chat_client_test.rs +++ b/sdk/rust/tests/integration/chat_client_test.rs @@ -208,7 +208,7 @@ async fn should_perform_tool_calling_chat_completion_non_streaming() { messages.push(assistant_msg); messages.push( ChatCompletionRequestToolMessage { - content: product.to_string().into(), + content: product.to_string(), tool_call_id: tool_call_id.clone(), } .into(), @@ -302,7 +302,7 @@ async fn should_perform_tool_calling_chat_completion_streaming() { messages.push(assistant_msg); messages.push( ChatCompletionRequestToolMessage { - content: product.to_string().into(), + content: product.to_string(), tool_call_id: tool_call_id.clone(), } .into(),