diff --git a/.dotnet.azure/CHANGELOG.md b/.dotnet.azure/CHANGELOG.md new file mode 100644 index 000000000..972422632 --- /dev/null +++ b/.dotnet.azure/CHANGELOG.md @@ -0,0 +1,495 @@ +# Release History + +## 2.0.0-beta.3 (2024-08-23) + +This change updates the library for compatibility with the latest `2.0.0-beta.9` of the `OpenAI` package and the `2024-07-01-preview` Azure OpenAI service API version label, as published on 8/5. + +### Features Added + +- The library now directly supports alternative authentication audiences, including Azure Government. This can be specified by providing an appropriate `AzureOpenAIAudience` value to the `AzureOpenAIClientOptions.Audience` property when creating a client. See the client configuration section of the README for more details. + +Additional new features from the `OpenAI` package can be found in [the OpenAI changelog](https://github.com/openai/openai-dotnet/blob/main/CHANGELOG.md). + +**Please note**: Structured Outputs support is not yet available with the `2024-07-01-preview` service API version. This means that attempting to use the feature with this library version will fail with an unrecognized property for either `response_format` or `strict` in request payloads; all existing functionality is unaffected. Azure OpenAI support for Structured Outputs is coming soon. + +### Breaking Changes + +No Azure-specific breaking changes are present in this update. + +The update from `OpenAI` `2.0.0-beta.7` to `2.0.0-beta.9` does bring a number of breaking changes, however, as described in [the OpenAI changelog](https://github.com/openai/openai-dotnet/blob/main/CHANGELOG.md): + +- Removed client constructors that do not explicitly take an API key parameter or an endpoint via an `OpenAIClientOptions` parameter, making it clearer how to appropriately instantiate a client. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Removed the endpoint parameter from all client constructors, making it clearer that an alternative endpoint must be specified via the `OpenAIClientOptions` parameter. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Removed `OpenAIClient`'s `Endpoint` `protected` property. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Made `OpenAIClient`'s constructor that takes a `ClientPipeline` parameter `protected internal` instead of just `protected`. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Renamed the `User` property in applicable Options classes to `EndUserId`, making its purpose clearer. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Changed name of return types from methods returning streaming collections from `ResultCollection` to `CollectionResult`. ([7bdecfd](https://github.com/openai/openai-dotnet/commit/7bdecfd8d294be933c7779c7e5b6435ba8a8eab0)) +- Changed return types from methods returning paginated collections from `PageableCollection` to `PageCollection`. ([7bdecfd](https://github.com/openai/openai-dotnet/commit/7bdecfd8d294be933c7779c7e5b6435ba8a8eab0)) +- Users must now call `GetAllValues` on the collection of pages to enumerate collection items directly. Corresponding protocol methods return `IEnumerable` where each collection item represents a single service response holding a page of values. ([7bdecfd](https://github.com/openai/openai-dotnet/commit/7bdecfd8d294be933c7779c7e5b6435ba8a8eab0)) +- Updated `VectorStoreFileCounts` and `VectorStoreFileAssociationError` types from `readonly struct` to `class`. ([58f93c8](https://github.com/openai/openai-dotnet/commit/58f93c8d5ea080adfee8b37ae3cc034ebb06c79f)) + +### Bugs Fixed + +- Removed an inappropriate null check in `FileClient.GetFiles()` (azure-sdk-for-net 44912) +- Addressed issues with automatic retry behavior, including for HTTP 429 rate limit errors: + - Authorization headers are now appropriately reapplied to retried requests + - Automatic retry behavior will now honor header-based intervals from `Retry-After` and related response headers +- The client will now originate an `x-ms-client-request-id` header to match prior library behavior and facilitate troubleshooting + +Additional, non-Azure-specific bug fixes can be found in [the OpenAI changelog](https://github.com/openai/openai-dotnet/blob/main/CHANGELOG.md). + +## 2.0.0-beta.2 (2024-06-14) + +### Features Added + +- Per changes to the [OpenAI .NET client library](https://github.com/openai/openai-dotnet), most convenience methods now provide the direct ability to provide optional `CancellationTokens`, removing the need to use protocol methods + +### Breaking Changes + +- In support of `CancellationToken`s in methods, an overriden method signature for streaming chat completions was changed and a new minimum version dependency of 2.0.0-beta.5 is established for the OpenAI dependency. These styles of breaks will be extraordinarily rare. + +### Bugs Fixed + +- See breaking changes: when streaming chat completions, an error of "Unrecognized request argument supplied: stream_options" is introduced when using Azure.AI.OpenAI 2.0.0-beta.1 with OpenAI 2.0.0-beta.5+. This is fixed with the new version. + +## 2.0.0-beta.1 (2024-06-07) + +**Please note**: This update brings a *major* set of changes to the Azure.AI.OpenAI library. + +With the release of the official [OpenAI .NET client library](https://github.com/openai/openai-dotnet), the `Azure.AI.OpenAI` library has migrated to become a companion to OpenAI's package that offers Azure client configuration and strongly-typed extension support for Azure-specific request and response models. + +**We'd love your feedback:** our goal is to move the new `OpenAI` .NET library and its refreshed `Azure.AI.OpenAI` companion into a General Availability status as quickly as we can; we've heard loud and clear that the perpetual preview/prerelease status is an adoption blocker. To reach that goal, your feedback -- either on the issues here, in `azure-sdk-for-net`, or the issues on the new `openai-dotnet` OpenAI repository -- will be invaluable. + +### Features Added + +**OpenAI parity**: built on the OpenAI .NET library, full parity support is available for the breadth of common features, including: + +- Assistants V2 with streaming +- Audio transcription/translation and text-to-speech generation +- (Coming soon) Batch +- Chat completion +- Embeddings +- Files +- Fine-tuning +- Image generation with dall-e-3 +- Vector stores + +**Azure OpenAI**: updated to the latest `2024-05-01-preview` service API, new features include: + +- Assistants v2 with streaming +- Improved configuration for On Your Data +- Expanded Responsible AI content filter annotations + +### Breaking Changes + +Given the nature of this update, breaking changes are extensive. Please see the README and the [OpenAI library README](https://github.com/openai/openai-dotnet/blob/master/README.md) for usage details. OpenAI's library carries forward many of the same design concepts as the Azure.AI.OpenAI library used as a standalone library, but considerable improvements have been made to the surface that will require significant code adjustments. + +## 1.0.0-beta.17 (2024-05-03) + +### Features Added + +- Image input support for `gpt-4-turbo` chat completions now works with image data in addition to internet URLs. + Images may be now be used as `gpt-4-turbo` message content items via one of three constructors: + - `ChatMessageImageContent(Uri)` -- the existing constructor, used for URL-based image references + - `ChatMessageImageContent(Stream,string)` -- (new) used with a stream and known MIME type (like `image/png`) + - `ChatMessageImageContent(BinaryData,string)` -- (new) used with a BinaryData instance and known MIME type + Please see the [readme example](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/openai/Azure.AI.OpenAI/README.md#chat-with-images-using-gpt-4-turbo) for more details. + +### Breaking Changes + +- Public visibility of the `ChatMessageImageUrl` type is removed to promote more flexible use of data sources in + `ChatMessageImageContent`. Code that previously created a `ChatMessageImageUrl` using a `Uri` should simply provide + the `Uri` to the `ChatMessageImageContent` constructor directly. + +## 1.0.0-beta.16 (2024-04-11) + +### Features Added + +**Audio** + +- `GetAudioTranscription()` now supports word-level timestamp granularities via `AudioTranscriptionOptions`: + - The `Verbose` option for `ResponseFormat` must be used for any timing information to be populated + - `TimestampGranularityFlags` accepts a combination of the `.Word` and `.Segment` granularity values in + `AudioTimestampGranularity`, joined when needed via the single-pipe `|` operator + - For example, `TimestampGranularityFlags = AudioTimestampGranularity.Word | AudioTimestampGranularity.Segment` + will request that both word-level and segment-level timestamps are provided on the transcription result + - If not otherwise specified, `Verbose` format will default to using segment-level timestamp information + - Corresponding word-level information is found on the `.Words` collection of `AudioTranscription`, peer to the + existing `.Segments` collection + - Note that word-level timing information incurs a small amount of additional processingly latency; segment-level + timestamps do not encounter this behavior +- `GenerateSpeechFromText()` can now use `Wav` and `Pcm` values from `SpeechGenerationResponseFormat`, these new + options providing alternative uncompressed formats to `Flac` + +**Chat** + +- `ChatCompletions` and `StreamingChatCompletionsUpdate` now include the reported `Model` value from the response +- Log probability information is now included in `StreamingChatCompletionsUpdate` when `logprobs` are requested on + `GetChatCompletionsStreaming()` +- [AOAI] Custom Blocklist information in content filter results is now represented in a more structured + `ContentFilterDetailedResults` type +- [AOAI] A new `IndirectAttack` content filter entry is now present on content filter results for prompts + +### Breaking Changes + +- [AOAI] `AzureChatExtensionMessageContext`'s `RequestContentFilterResults` now uses the new + `ContentFilterDetailedResults` type, changed from the previous `IReadOnlyList`. The + previous list is now present on `CustomBlockLists.Details`, supplemented with a new `CustomBlockLists.Filtered` + property. + +### Bugs Fixed + +- [AOAI] An issue that sometimes caused `StreamingChatCompletionUpdates` from Azure OpenAI to inappropriately exclude + top-level information like `Id` and `CreatedAt` has been addressed + +## 1.0.0-beta.15 (2024-03-20) + +This release targets the latest `2024-03-01-preview` service API label and brings support for the `Dimensions` property when using new embedding models. + +### Features Added + +- `EmbeddingsOptions` now includes the `Dimensions` property, new to Azure OpenAI's `2024-03-01-preview` service API. + +### Bugs Fixed + +- Several issues with the `ImageGenerations` response object being treated as writeable are fixed: + - `ImageGenerations` no longer has an erroneous public constructor + - `ImageGenerations.Created` no longer has a public setter + - `ImageGenerations.Data` is now an `IReadOnlyList` instead of an `IList` + - A corresponding replacement factory method for mocks is added to `AzureOpenAIModelFactory` + +## 1.0.0-beta.14 (2024-03-04) + +### Features Added + +- Text-to-speech using OpenAI TTS models is now supported. See [OpenAI's API reference](https://platform.openai.com/docs/api-reference/audio/createSpeech) or the [Azure OpenAI quickstart](https://learn.microsoft.com/azure/ai-services/openai/text-to-speech-quickstart) for detailed overview and background information. + - The new method `GenerateSpeechFromText` exposes this capability on `OpenAIClient`. + - Text-to-speech converts text into lifelike spoken audio in a chosen voice, together with other optional configurations. + - This method works for both Azure OpenAI and non-Azure `api.openai.com` client configurations + +### Breaking Changes + +"On Your Data" changes: + +- Introduced a new type `AzureChatExtensionDataSourceResponseCitation` for a more structured representation of citation data. +- Correspondingly, updated `AzureChatExtensionsMessageContext`: + - Replaced `Messages` with `Citations` of type `AzureChatExtensionDataSourceResponseCitation`. + - Added `Intent` as a string type. +- Renamed "AzureCognitiveSearch" to "AzureSearch": + - `AzureCognitiveSearchChatExtensionConfiguration` is now `AzureSearchChatExtensionConfiguration`. + - `AzureCognitiveSearchIndexFieldMappingOptions` is now `AzureSearchIndexFieldMappingOptions`. +- Check the project README for updated code snippets. + +### Other Changes + +- New properties in `ChatCompletionsOptions`: + - `EnableLogProbabilities`: Allows retrieval of log probabilities (REST: `logprobs`) + - `LogProbabilitiesPerToken`: The number of most likely tokens to return per token (REST: `top_logprobs`) +- Introduced a new property in `CompletionsOptions`: + - `Suffix`: Defines the suffix that follows the completion of inserted text (REST: `suffix`) +- Image generation response now includes content filtering details (specific to Azure OpenAI endpoint): + - `ImageGenerationData.ContentFilterResults`: Information about the content filtering results. (REST: `content_filter_results`) + - `ImageGenerationData.PromptFilterResults`: Information about the content filtering category (REST: `prompt_filter_results`) + +## 1.0.0-beta.13 (2024-02-01) + +### Breaking Changes + +- Removed the setter of the `Functions` property of the `ChatCompletionsOptions` class as per the guidelines for collection properties. + +### Bugs Fixed + +- Addressed an issue with the public constructor for `ChatCompletionsFunctionToolCall` that failed to set the tool call type in the corresponding request. + +## 1.0.0-beta.12 (2023-12-15) + +Like beta.11, beta.12 is another release that brings further refinements and fixes. It remains based on the `2023-12-01-preview` service API version for Azure OpenAI and does not add any new service capabilities. + +### Features Added + +**Updates for using streaming tool calls:** + +- A new .NET-specific `StreamingToolCallUpdate` type has been added to better represent streaming tool call updates + when using chat tools. + - This new type includes an explicit `ToolCallIndex` property, reflecting `index` in the REST schema, to allow + resilient deserialization of parallel function tool calling. +- A convenience constructor has been added for `ChatRequestAssistantMessage` that can automatically populate from a prior + `ChatResponseMessage` when using non-streaming chat completions. +- A public constructor has been added for `ChatCompletionsFunctionToolCall` to allow more intuitive reconstruction of + `ChatCompletionsToolCall` instances for use in `ChatRequestAssistantMessage` instances made from streaming responses. + +**Other additions:** + +- To facilitate reuse of user message contents, `ChatRequestUserMessage` now provides a public `Content` property (`string`) as well as a public `MultimodalContentItems` property (`IList` type is introduced that implicitly exposes an `IAsyncEnumerable` derived from + the underlying response. +- `OpenAI.GetCompletionsStreaming()` now returns a `StreamingResponse` that may be directly + enumerated over. `StreamingCompletions`, `StreamingChoice`, and the corresponding methods are removed. +- Because Chat Completions use a distinct structure for their streaming response messages, a new + `StreamingChatCompletionsUpdate` type is introduced that encapsulates this update data. +- Correspondingly, `OpenAI.GetChatCompletionsStreaming()` now returns a + `StreamingResponse` that may be enumerated over directly. + `StreamingChatCompletions`, `StreamingChatChoice`, and related methods are removed. +- For more information, please see + [the related pull request description](https://github.com/Azure/azure-sdk-for-net/pull/39347) as well as the + updated snippets in the project README. + +#### `deploymentOrModelName` moved to `*Options.DeploymentName` + +`deploymentOrModelName` and related method parameters on `OpenAIClient` have been moved to `DeploymentName` +properties in the corresponding method options. This is intended to promote consistency across scenario, +language, and Azure/non-Azure OpenAI use. + +As an example, the following: + +```csharp +ChatCompletionsOptions chatCompletionsOptions = new() +{ + Messages = { new(ChatRole.User, "Hello, assistant!") }, +}; +Response response = client.GetChatCompletions("gpt-4", chatCompletionsOptions); +``` + +...is now re-written as: + +```csharp +ChatCompletionsOptions chatCompletionsOptions = new() +{ + DeploymentName = "gpt-4", + Messages = { new(ChatRole.User, "Hello, assistant!") }, +}; +Response response = client.GetChatCompletions(chatCompletionsOptions); +``` + +#### Consistency in complex method options type constructors + +With the migration of `DeploymentName` into method complex options types, these options types have now been snapped to +follow a common pattern: each complex options type will feature a default constructor that allows `init`-style setting +of properties as well as a single additional constructor that accepts *all* required parameters for the corresponding +method. Existing constructors that no longer meet that "all" requirement, including those impacted by the addition of +`DeploymentName`, have been removed. The "convenience" constructors that represented required parameter data +differently -- for example, `EmbeddingsOptions(string)`, have also been removed in favor of the consistent "set of +directly provide" choice. + +More exhaustively, *removed* are: + +- `AudioTranscriptionOptions(BinaryData)` +- `AudioTranslationOptions(BinaryData)` +- `ChatCompletionsOptions(IEnumerable)` +- `CompletionsOptions(IEnumerable)` +- `EmbeddingsOptions(string)` +- `EmbeddingsOptions(IEnumerable)` + +And *added* as replacements are: + +- `AudioTranscriptionOptions(string, BinaryData)` +- `AudioTranslationOptions(string, BinaryData)` +- `ChatCompletionsOptions(string, IEnumerable)` +- `CompletionsOptions(string, IEnumerable)` +- `EmbeddingsOptions(string, IEnumerable)` + +#### Embeddings now represented as `ReadOnlyMemory` + +Changed the representation of embeddings (specifically, the type of the `Embedding` property of the `EmbeddingItem` class) +from `IReadOnlyList` to `ReadOnlyMemory` as part of a broader effort to establish consistency across the +.NET ecosystem. + +#### `SearchKey` and `EmbeddingKey` properties replaced by `SetSearchKey` and `SetEmbeddingKey` methods + +Replaced the `SearchKey` and `EmbeddingKey` properties of the `AzureCognitiveSearchChatExtensionConfiguration` class with +new `SetSearchKey` and `SetEmbeddingKey` methods respectively. These methods simplify the configuration of the Azure Cognitive +Search chat extension by receiving a plain string instead of an `AzureKeyCredential`, promote more sensible key and secret +management, and align with the Azure SDK guidelines. + +## 1.0.0-beta.8 (2023-09-21) + +### Features Added + +- Audio Transcription and Audio Translation using OpenAI Whisper models is now supported. See [OpenAI's API + reference](https://platform.openai.com/docs/api-reference/audio) or the [Azure OpenAI + quickstart](https://learn.microsoft.com/azure/ai-services/openai/whisper-quickstart) for detailed overview and + background information. + - The new methods `GetAudioTranscription` and `GetAudioTranscription` expose these capabilities on `OpenAIClient` + - Transcription produces text in the primary, supported, spoken input language of the audio data provided, together + with any optional associated metadata + - Translation produces text, translated to English and reflective of the audio data provided, together with any + optional associated metadata + - These methods work for both Azure OpenAI and non-Azure `api.openai.com` client configurations + +### Breaking Changes + +- The underlying representation of `PromptFilterResults` (for `Completions` and `ChatCompletions`) has had its response + body key changed from `prompt_annotations` to `prompt_filter_results` +- **Prior versions of the `Azure.AI.OpenAI` library may no longer populate `PromptFilterResults` as expected** and it's + highly recommended to upgrade to this version if the use of Azure OpenAI content moderation annotations for input data + is desired +- If a library version upgrade is not immediately possible, it's advised to use `Response.GetRawResponse()` and manually + extract the `prompt_filter_results` object from the top level of the `Completions` or `ChatCompletions` response `Content` + payload + +### Bugs Fixed + +- Support for the described breaking change for `PromptFilterResults` was added and this library version will now again + deserialize `PromptFilterResults` appropriately +- `PromptFilterResults` and `ContentFilterResults` are now exposed on the result classes for streaming Completions and + Chat Completions. `Streaming(Chat)Completions.PromptFilterResults` will report an index-sorted list of all prompt + annotations received so far while `Streaming(Chat)Choice.ContentFilterResults` will reflect the latest-received + content annotations that were populated and received while streaming + +## 1.0.0-beta.7 (2023-08-25) + +### Features Added + +- The Azure OpenAI "using your own data" feature is now supported. See [the Azure OpenAI using your own data quickstart](https://learn.microsoft.com/azure/ai-services/openai/use-your-data-quickstart) for conceptual background and detailed setup instructions. + - Azure OpenAI chat extensions are configured via a new `AzureChatExtensionsOptions` property on `ChatCompletionsOptions`. When an `AzureChatExtensionsOptions` is provided, configured requests will only work with clients configured to use the Azure OpenAI service, as the capabilities are unique to that service target. + - `AzureChatExtensionsOptions` then has `AzureChatExtensionConfiguration` instances added to its `Extensions` property, with these instances representing the supplementary information needed for Azure OpenAI to use desired data sources to supplement chat completions behavior. + - `ChatChoice` instances on a `ChatCompletions` response value that used chat extensions will then also have their `Message` property supplemented by an `AzureChatExtensionMessageContext` instance. This context contains a collection of supplementary `Messages` that describe the behavior of extensions that were used and supplementary response data, such as citations, provided along with the response. + - See the README sample snippet for a simplified example of request/response use with "using your own data" + +## 1.0.0-beta.6 (2023-07-19) + +### Features Added + +- DALL-E image generation is now supported. See [the Azure OpenAI quickstart](https://learn.microsoft.com/azure/cognitive-services/openai/dall-e-quickstart) for conceptual background and detailed setup instructions. + - `OpenAIClient` gains a new `GetImageGenerations` method that accepts an `ImageGenerationOptions` and produces an `ImageGenerations` via its response. This response object encapsulates the temporary storage location of generated images for future retrieval. + - In contrast to other capabilities, DALL-E image generation does not require explicit creation or specification of a deployment or model. Its surface as such does not include this concept. +- Functions for chat completions are now supported: see [OpenAI's blog post on the topic](https://openai.com/blog/function-calling-and-other-api-updates) for much more detail. + - A list of `FunctionDefinition` objects may be populated on `ChatCompletionsOptions` via its `Functions` property. These definitions include a name and description together with a serialized JSON Schema representation of its parameters; these parameters can be generated easily via `BinaryData.FromObjectAsJson` with dynamic objects -- see the README for example usage. + - **NOTE**: Chat Functions requires a minimum of the `-0613` model versions for `gpt-4` and `gpt-3.5-turbo`/`gpt-35-turbo`. Please ensure you're using these later model versions, as Functions are not supported with older model revisions. For Azure OpenAI, you can update a deployment's model version or create a new model deployment with an updated version via the Azure AI Studio interface, also accessible through Azure Portal. +- (Azure OpenAI specific) Completions and Chat Completions responses now include embedded content filter annotations for prompts and responses +- A new `Azure.AI.OpenAI.AzureOpenAIModelFactory` is now present for mocking. + +### Breaking Changes + +- `ChatMessage`'s one-parameter constructor has been replaced with a no-parameter constructor. Please replace any hybrid construction with one of these two options that either completely rely on property setting or completely rely on constructor parameters. + +## 1.0.0-beta.5 (2023-03-22) + +This is a significant release that brings GPT-4 model support (chat) and the ability to use non-Azure OpenAI (not just Azure OpenAI resources) to the .NET library. It also makes a number of clarifying adjustments to request properties for completions. + +### Features Added +- GPT-4 models are now supported via new `GetChatCompletions` and `GetChatCompletionsStreaming` methods on `OpenAIClient`. These use the `/chat/completions` REST endpoint and represent the [OpenAI Chat messages format](https://platform.openai.com/docs/guides/chat). + - The `gpt-3.5-model` can also be used with Chat completions; prior models like text-davinci-003 cannot be used with Chat completions and should still use the `GetCompletions` methods. +- Support for using OpenAI's endpoint via valid API keys obtained from https://platform.openai.com has been added. `OpenAIClient` has new constructors that accept an OpenAI API key instead of an Azure endpoint URI and credential; once configured, Completions, Chat Completions, and Embeddings can be used with identical calling patterns. + +### Breaking Changes + +A number of Completions request properties have been renamed and further documented for clarity. +- `CompletionsOptions` (REST request payload): + - `CacheLevel` and `CompletionConfig` are removed. + - `LogitBias` (REST: `logit_bias`), previously a `` Dictionary, is now an `` Dictionary named `TokenSelectionBiases`. + - `LogProbability` (REST: `logprobs`) is renamed to `LogProbabilityCount`. + - `Model` is removed (in favor of the method-level parameter for deployment or model name) + - `Prompt` is renamed to `Prompts` + - `SnippetCount` (REST: `n`) is renamed to `ChoicesPerPrompt`. + - `Stop` is renamed to `StopSequences`. +- Method and property documentation are broadly updated, with renames from REST schema (like `n` becoming `ChoicesPerPrompt`) specifically noted in ``. + +## 1.0.0-beta.4 (2023-02-23) + +### Bugs fixed +- Addressed issues that sometimes caused `beta.3`'s new `GetStreamingCompletions` method to execute indefinitely + +## 1.0.0-beta.3 (2023-02-17) + +### Features Added +- Support for streaming Completions responses, a capability that parallels setting `stream=true` in the REST API, is now available. A new `GetStreamingCompletions` method on `OpenAIClient` provides a response value `StreamingCompletions` type. This, in turn, exposes a collection of `StreamingChoice` objects as an `IAsyncEnumerable` that will update as a streamed response progresses. `StreamingChoice` further exposes an `IAsyncEnumerable` of streaming text elements via a `GetTextStreaming` method. Used together, this facilitates providing faster, live-updating responses for Completions via the convenient `await foreach` pattern. +- ASP.NET integration via `Microsoft.Extensions.Azure`'s `IAzureClientBuilder` interfaces is available. `OpenAIClient` is now a supported client type for these extension methods. + +### Breaking Changes +- `CompletionsLogProbability.TokenLogProbability`, available on `Choice` elements of a `Completions` response value's `.Choices` collection when a non-zero `LogProbability` value is provided via `CompletionsOptions`, is now an `IReadOnlyList` vs. its previous type of `IReadOnlyList`. This nullability addition accomodates circumstances where some tokens produce expected null values in log probability arrays. + +### Bugs Fixed +- Setting `CompletionsOptions.Echo` to true while also setting a non-zero `CompletionsOptions.LogProbability` no longer results in a deserialization error during response processing. + +## 1.0.0-beta.2 (2023-02-08) +### Bugs Fixed +- Adjusted bad name `finishReason` to `finish_reason` in deserializer class + +## 1.0.0-beta.1 (2023-02-06) + +### Features Added + +- This is the initial preview release for Azure OpenAI inference capabilities, including completions and embeddings. diff --git a/.dotnet.azure/README.md b/.dotnet.azure/README.md new file mode 100644 index 000000000..3a201aa89 --- /dev/null +++ b/.dotnet.azure/README.md @@ -0,0 +1,536 @@ +# Azure OpenAI client library for .NET + +The Azure OpenAI client library for .NET is a companion to the official [OpenAI client library for .NET](https://github.com/openai/openai-dotnet). The Azure OpenAI library configures a client for use with Azure OpenAI and provides additional strongly typed extension support for request and response models specific to Azure OpenAI scenarios. + +Azure OpenAI is a managed service that allows developers to deploy, tune, and generate content from OpenAI models on Azure resources. + + [Source code](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/openai/Azure.AI.OpenAI/src) | [Package (NuGet)](https://www.nuget.org/packages/Azure.AI.OpenAI) | [API reference documentation](https://learn.microsoft.com/azure/cognitive-services/openai/reference) | [Product documentation](https://learn.microsoft.com/azure/cognitive-services/openai/) | [Samples](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/openai/Azure.AI.OpenAI/tests/Samples) + +## Getting started + +### Prerequisites + +To use an Azure OpenAI resource, you must have: + +1. An [Azure subscription](https://azure.microsoft.com/free/dotnet/) +1. [Azure OpenAI access](https://learn.microsoft.com/azure/cognitive-services/openai/overview#how-do-i-get-access-to-azure-openai) + +These prerequisites allow you to create an Azure OpenAI resource and get both a connection URL and API keys. For more information, see [Quickstart: Get started generating text using Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart). + +### Install the package + +Install the client library for .NET with [NuGet](https://www.nuget.org/): + +```dotnetcli +dotnet add package Azure.AI.OpenAI --prerelease +``` + +The `Azure.AI.OpenAI` package builds on the [official OpenAI package](https://www.nuget.org/packages/OpenAI), which is included as a dependency. + +### Authenticate the client + +To interact with Azure OpenAI or OpenAI, create an instance of [AzureOpenAIClient][azure_openai_client_class] with one of the following approaches: + +- [Create client with a Microsoft Entra credential](#create-client-with-a-microsoft-entra-credential) **(Recommended)** +- [Create client with an API key](#create-client-with-an-api-key) + +#### Create client with a Microsoft Entra credential + +A secure, keyless authentication approach is to use Microsoft Entra ID (formerly Azure Active Directory) via the [Azure Identity library][azure_identity]. To use the library: + +1. Install the [Azure.Identity package](https://www.nuget.org/packages/Azure.Identity): + + ```dotnetcli + dotnet add package Azure.Identity + ``` + +1. Use the desired credential type from the library. For example, [DefaultAzureCredential][azure_identity_dac]: + +```C# Snippet:ConfigureClient:WithEntra +AzureOpenAIClient azureClient = new( + new Uri("https://your-azure-openai-resource.com"), + new DefaultAzureCredential()); +ChatClient chatClient = azureClient.GetChatClient("my-gpt-4o-mini-deployment"); +``` + +##### Configure client for Azure sovereign cloud** + +If your Microsoft Entra credentials are issued by an entity other than Azure Public Cloud, you can set the `Audience` property on `OpenAIClientOptions` to modify the token authorization scope used for requests. + +For example, the following will configure the client to authenticate tokens via Azure Government Cloud, using `https://cognitiveservices.azure.us/.default` as the authorization scope: + +```C# Snippet:ConfigureClient:GovernmentAudience +AzureOpenAIClientOptions options = new() +{ + Audience = AzureOpenAIAudience.AzureGovernment, +}; +AzureOpenAIClient azureClient = new( + new Uri("https://your-azure-openai-resource.com"), + new DefaultAzureCredential()); +ChatClient chatClient = azureClient.GetChatClient("my-gpt-4o-mini-deployment"); +``` + +For a custom or non-enumerated value, the authorization scope can be provided directly as the value for `Audience`: + +```C# Snippet:ConfigureClient:CustomAudience +AzureOpenAIClientOptions optionsWithCustomAudience = new() +{ + Audience = "https://cognitiveservices.azure.com/.default", +}; +``` + +#### Create client with an API key + +While not as secure as Microsoft Entra-based authentication, it's possible to authenticate using a client subscription key: + +```C# Snippet:ConfigureClient:WithAOAITopLevelClient +string keyFromEnvironment = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); + +AzureOpenAIClient azureClient = new( + new Uri("https://your-azure-openai-resource.com"), + new AzureKeyCredential(keyFromEnvironment)); +ChatClient chatClient = azureClient.GetChatClient("my-gpt-35-turbo-deployment"); +``` + +## Key concepts + +### Assistants + +See [OpenAI's Assistants API overview](https://platform.openai.com/docs/assistants/overview). + +### Audio transcription/translation and text-to-speech generation + +See [OpenAI Capabilities: Speech to text](https://platform.openai.com/docs/guides/speech-to-text/speech-to-text). + +### Batch + +See [OpenAI's Batch API guide](https://platform.openai.com/docs/guides/batch). + +### Chat completion + +Chat models take a list of messages as input and return a model-generated message as output. Although the chat format is +designed to make multi-turn conversations easy, it's also useful for single-turn tasks without any conversation. + +See [OpenAI Capabilities: Chat completion](https://platform.openai.com/docs/guides/text-generation/chat-completions-api). + +### Image generation + +See [OpenAI Capabilities: Image generation](https://platform.openai.com/docs/guides/images/introduction). + +### Files + +See [OpenAI's Files API reference](https://platform.openai.com/docs/api-reference/files). + +### Text embeddings + +See [OpenAI Capabilities: Embeddings](https://platform.openai.com/docs/guides/embeddings/embeddings). + +### Thread safety + +We guarantee that all client instance methods are thread-safe and independent of each other ([guideline](https://azure.github.io/azure-sdk/dotnet_introduction.html#dotnet-service-methods-thread-safety)). This ensures that the recommendation of reusing client instances is always safe, even across threads. + +### Additional concepts + + +[Client options](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#configuring-service-clients-using-clientoptions) | +[Accessing the response](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#accessing-http-response-details-using-responset) | +[Long-running operations](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#consuming-long-running-operations-using-operationt) | +[Handling failures](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/README.md#reporting-errors-requestfailedexception) | +[Diagnostics](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/Diagnostics.md) | +[Mocking](https://learn.microsoft.com/dotnet/azure/sdk/unit-testing-mocking) | +[Client lifetime](https://devblogs.microsoft.com/azure-sdk/lifetime-management-and-thread-safety-guarantees-of-azure-sdk-net-clients/) + + +## Examples + +You can familiarize yourself with different APIs using [Samples from OpenAI's .NET library](https://github.com/openai/openai-dotnet/tree/main/examples) or [Azure.AI.OpenAI-specific samples](https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/openai/Azure.AI.OpenAI/tests/Samples). Most OpenAI capabilities are available on both Azure OpenAI and OpenAI using the same scenario clients and methods, so not all scenarios are redundantly covered here. + +### Get a chat completion + +```C# Snippet:SimpleChatResponse +AzureOpenAIClient azureClient = new( + new Uri("https://your-azure-openai-resource.com"), + new DefaultAzureCredential()); +ChatClient chatClient = azureClient.GetChatClient("my-gpt-35-turbo-deployment"); + +ChatCompletion completion = chatClient.CompleteChat( + [ + // System messages represent instructions or other guidance about how the assistant should behave + new SystemChatMessage("You are a helpful assistant that talks like a pirate."), + // User messages represent user input, whether historical or the most recen tinput + new UserChatMessage("Hi, can you help me?"), + // Assistant messages in a request represent conversation history for responses + new AssistantChatMessage("Arrr! Of course, me hearty! What can I do for ye?"), + new UserChatMessage("What's the best way to train a parrot?"), + ]); + +Console.WriteLine($"{completion.Role}: {completion.Content[0].Text}"); +``` + +### Stream chat messages + +Streaming chat completions use the `CompleteChatStreaming` and `CompleteChatStreamingAsync` method, which return a `ResultCollection` or `AsyncCollectionResult` instead of a `ClientResult`. These result collections can be iterated over using `foreach` or `await foreach`, with each update arriving as new data is available from the streamed response. + +```C# Snippet:StreamChatMessages +AzureOpenAIClient azureClient = new( + new Uri("https://your-azure-openai-resource.com"), + new DefaultAzureCredential()); +ChatClient chatClient = azureClient.GetChatClient("my-gpt-35-turbo-deployment"); + +CollectionResult completionUpdates = chatClient.CompleteChatStreaming( + [ + new SystemChatMessage("You are a helpful assistant that talks like a pirate."), + new UserChatMessage("Hi, can you help me?"), + new AssistantChatMessage("Arrr! Of course, me hearty! What can I do for ye?"), + new UserChatMessage("What's the best way to train a parrot?"), + ]); + +foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates) +{ + foreach (ChatMessageContentPart contentPart in completionUpdate.ContentUpdate) + { + Console.Write(contentPart.Text); + } +} +``` + +### Use chat tools + +**Tools** extend chat completions by allowing an assistant to invoke defined functions and other capabilities in the +process of fulfilling a chat completions request. To use chat tools, start by defining a function tool. Here, we root the tools in local methods for clarity and convenience: + +```C# Snippet:ChatTools:DefineTool +static string GetCurrentLocation() +{ + // Call the location API here. + return "San Francisco"; +} + +static string GetCurrentWeather(string location, string unit = "celsius") +{ + // Call the weather API here. + return $"31 {unit}"; +} + +ChatTool getCurrentLocationTool = ChatTool.CreateFunctionTool( + functionName: nameof(GetCurrentLocation), + functionDescription: "Get the user's current location" +); + +ChatTool getCurrentWeatherTool = ChatTool.CreateFunctionTool( + functionName: nameof(GetCurrentWeather), + functionDescription: "Get the current weather in a given location", + functionParameters: BinaryData.FromString(""" + { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. Boston, MA" + }, + "unit": { + "type": "string", + "enum": [ "celsius", "fahrenheit" ], + "description": "The temperature unit to use. Infer this from the specified location." + } + }, + "required": [ "location" ] + } + """) +); +``` + +With the tool defined, include that new definition in the options for a chat completions request: + +```C# Snippet:ChatTools:RequestWithFunctions +ChatCompletionOptions options = new() +{ + Tools = { getCurrentLocationTool, getCurrentWeatherTool }, +}; + +List conversationMessages = + [ + new UserChatMessage("What's the weather like in Boston?"), + ]; +ChatCompletion completion = chatClient.CompleteChat(conversationMessages); +``` + +When the assistant decides that one or more tools should be used, the response message includes one or more "tool +calls" that must all be resolved via "tool messages" on the subsequent request. This resolution of tool calls into +new request messages can be thought of as a sort of "callback" for chat completions. + +To provide tool call resolutions to the assistant to allow the request to continue, provide all prior historical +context -- including the original system and user messages, the response from the assistant that included the tool +calls, and the tool messages that resolved each of those tools -- when making a subsequent request. + +```C# Snippet:ChatTools:HandleToolCalls +// Purely for convenience and clarity, this standalone local method handles tool call responses. +string GetToolCallContent(ChatToolCall toolCall) +{ + if (toolCall.FunctionName == getCurrentWeatherTool.FunctionName) + { + // Validate arguments before using them; it's not always guaranteed to be valid JSON! + try + { + using JsonDocument argumentsDocument = JsonDocument.Parse(toolCall.FunctionArguments); + if (!argumentsDocument.RootElement.TryGetProperty("location", out JsonElement locationElement)) + { + // Handle missing required "location" argument + } + else + { + string location = locationElement.GetString(); + if (argumentsDocument.RootElement.TryGetProperty("unit", out JsonElement unitElement)) + { + return GetCurrentWeather(location, unitElement.GetString()); + } + else + { + return GetCurrentWeather(location); + } + } + } + catch (JsonException) + { + // Handle the JsonException (bad arguments) here + } + } + // Handle unexpected tool calls + throw new NotImplementedException(); +} + +if (completion.FinishReason == ChatFinishReason.ToolCalls) +{ + // Add a new assistant message to the conversation history that includes the tool calls + conversationMessages.Add(new AssistantChatMessage(completion)); + + foreach (ChatToolCall toolCall in completion.ToolCalls) + { + conversationMessages.Add(new ToolChatMessage(toolCall.Id, GetToolCallContent(toolCall))); + } + + // Now make a new request with all the messages thus far, including the original +} +``` + +When using tool calls with streaming responses, accumulate tool call details much like you'd accumulate the other +portions of streamed choices, in this case using the accumulated `StreamingToolCallUpdate` data to instantiate new +tool call messages for assistant message history. Note that the model will ignore `ChoiceCount` when providing tools +and that all streamed responses should map to a single, common choice index in the range of `[0..(ChoiceCount - 1)]`. + +```C# Snippet:ChatTools:StreamingChatTools +Dictionary toolCallIdsByIndex = []; +Dictionary functionNamesByIndex = []; +Dictionary functionArgumentBuildersByIndex = []; +StringBuilder contentBuilder = new(); + +foreach (StreamingChatCompletionUpdate streamingChatUpdate + in chatClient.CompleteChatStreaming(conversationMessages, options)) +{ + foreach (ChatMessageContentPart contentPart in streamingChatUpdate.ContentUpdate) + { + contentBuilder.Append(contentPart.Text); + } + foreach (StreamingChatToolCallUpdate toolCallUpdate in streamingChatUpdate.ToolCallUpdates) + { + if (!string.IsNullOrEmpty(toolCallUpdate.Id)) + { + toolCallIdsByIndex[toolCallUpdate.Index] = toolCallUpdate.Id; + } + if (!string.IsNullOrEmpty(toolCallUpdate.FunctionName)) + { + functionNamesByIndex[toolCallUpdate.Index] = toolCallUpdate.FunctionName; + } + if (!string.IsNullOrEmpty(toolCallUpdate.FunctionArgumentsUpdate)) + { + StringBuilder argumentsBuilder + = functionArgumentBuildersByIndex.TryGetValue(toolCallUpdate.Index, out StringBuilder existingBuilder) + ? existingBuilder + : new(); + argumentsBuilder.Append(toolCallUpdate.FunctionArgumentsUpdate); + functionArgumentBuildersByIndex[toolCallUpdate.Index] = argumentsBuilder; + } + } +} + +List toolCalls = []; +foreach (KeyValuePair indexToIdPair in toolCallIdsByIndex) +{ + toolCalls.Add(ChatToolCall.CreateFunctionToolCall( + indexToIdPair.Value, + functionNamesByIndex[indexToIdPair.Key], + functionArgumentBuildersByIndex[indexToIdPair.Key].ToString())); +} + +conversationMessages.Add(new AssistantChatMessage(toolCalls, contentBuilder.ToString())); + +// Placeholder: each tool call must be resolved, like in the non-streaming case +string GetToolCallOutput(ChatToolCall toolCall) => null; + +foreach (ChatToolCall toolCall in toolCalls) +{ + conversationMessages.Add(new ToolChatMessage(toolCall.Id, GetToolCallOutput(toolCall))); +} + +// Repeat with the history and all tool call resolution messages added +``` + +### Use your own data with Azure OpenAI + +The use your own data feature is unique to Azure OpenAI and won't work with a client configured to use the non-Azure service. +See [the Azure OpenAI using your own data quickstart](https://learn.microsoft.com/azure/ai-services/openai/use-your-data-quickstart) for conceptual background and detailed setup instructions. + +**NOTE:** The concurrent use of [Chat Functions](#use-chat-functions) and Azure Chat Extensions on a single request isn't yet supported. Supplying both will result in the Chat Functions information being ignored and the operation behaving as if only the Azure Chat Extensions were provided. To address this limitation, consider separating the evaluation of Chat Functions and Azure Chat Extensions across multiple requests in your solution design. + +```C# Snippet:ChatUsingYourOwnData +// Extension methods to use data sources with options are subject to SDK surface changes. Suppress the +// warning to acknowledge and this and use the subject-to-change AddDataSource method. +#pragma warning disable AOAI001 + +ChatCompletionOptions options = new(); +options.AddDataSource(new AzureSearchChatDataSource() +{ + Endpoint = new Uri("https://your-search-resource.search.windows.net"), + IndexName = "contoso-products-index", + Authentication = DataSourceAuthentication.FromApiKey( + Environment.GetEnvironmentVariable("OYD_SEARCH_KEY")), +}); + +ChatCompletion completion = chatClient.CompleteChat( + [ + new UserChatMessage("What are the best-selling Contoso products this month?"), + ], + options); + +AzureChatMessageContext onYourDataContext = completion.GetAzureMessageContext(); + +if (onYourDataContext?.Intent is not null) +{ + Console.WriteLine($"Intent: {onYourDataContext.Intent}"); +} +foreach (AzureChatCitation citation in onYourDataContext?.Citations ?? []) +{ + Console.WriteLine($"Citation: {citation.Content}"); +} +``` + +### Use Assistants and stream a run + +[Assistants](https://platform.openai.com/docs/assistants/overview) provide a stateful, service-persisted conversational +model that can be enriched with a larger array of tools than Chat Completions. + +Creating an `AssistantClient` is similar to other scenario clients. An important difference is that Assistants features +are marked as `[Experimental]` to reflect the API's beta status, and thus you'll need to suppress the corresponding +warning to instantiate a client. This can be done in the `.csproj` file via the `` element or, as below, in +the code itself with a `#pragma` directive. + +```C# Snippet:Assistants:CreateClient +AzureOpenAIClient azureClient = new( + new Uri("https://your-azure-openai-resource.com"), + new DefaultAzureCredential()); + +// The Assistants feature area is in beta, with API specifics subject to change. +// Suppress the [Experimental] warning via .csproj or, as here, in the code to acknowledge. +#pragma warning disable OPENAI001 +AssistantClient assistantClient = azureClient.GetAssistantClient(); +``` + +With a client, you can then create Assistants, Threads, and new Messages on a thread in preparation to start a run. As is the case for other shared API surfaces, you should use an Azure OpenAI model deployment name wherever a model name is requested. + +```C# Snippet:Assistants:PrepareToRun +Assistant assistant = await assistantClient.CreateAssistantAsync( + model: "my-gpt-4o-deployment", + new AssistantCreationOptions() + { + Name = "My Friendly Test Assistant", + Instructions = "You politely help with math questions. Use the code interpreter tool when asked to " + + "visualize numbers.", + Tools = { ToolDefinition.CreateCodeInterpreter() }, + }); +ThreadInitializationMessage initialMessage = new( + MessageRole.User, + [ + "Hi, Assistant! Draw a graph for a line with a slope of 4 and y-intercept of 9." + ]); +AssistantThread thread = await assistantClient.CreateThreadAsync(new ThreadCreationOptions() +{ + InitialMessages = { initialMessage }, +}); +``` + +You can then start a run and stream updates as they arrive using the `Streaming` method variants, handling the updates +you're interested in using the enumerated kind of event it is and/or one of the several derived types for the streaming +update class, as shown here for content: + +```C# Snippet:Assistants:StreamRun +RunCreationOptions runOptions = new() +{ + AdditionalInstructions = "When possible, talk like a pirate." +}; +await foreach (StreamingUpdate streamingUpdate + in assistantClient.CreateRunStreamingAsync(thread, assistant, runOptions)) +{ + if (streamingUpdate.UpdateKind == StreamingUpdateReason.RunCreated) + { + Console.WriteLine($"--- Run started! ---"); + } + else if (streamingUpdate is MessageContentUpdate contentUpdate) + { + Console.Write(contentUpdate.Text); + if (contentUpdate.ImageFileId is not null) + { + Console.WriteLine($"[Image content file ID: {contentUpdate.ImageFileId}"); + } + } +} +``` + +Remember that things like Assistants, Threads, and Vector Stores are persistent resources. You can save their IDs to +reuse them later or, as demonstrated below, delete them when no longer desired. + +```C# Snippet:Assistants:Cleanup +// Optionally, delete persistent resources that are no longer needed. +_ = await assistantClient.DeleteAssistantAsync(assistant); +_ = await assistantClient.DeleteThreadAsync(thread); +``` + +## Next steps + +## Troubleshooting + +When you interact with Azure OpenAI using the .NET SDK, errors returned by the service correspond to the same HTTP status codes returned for [REST API][openai_rest] requests. + +For example, if you try to create a client using an endpoint that doesn't match your Azure OpenAI Resource endpoint, a `404` error is returned, indicating `Resource Not Found`. + +## Contributing + +See the [OpenAI CONTRIBUTING.md][openai_contrib] for details on building, testing, and contributing to this library. + +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit [cla.microsoft.com][cla]. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct][code_of_conduct]. For more information, see the [Code of Conduct FAQ][code_of_conduct_faq] or contact [opencode@microsoft.com][email_opencode] with any additional questions or comments. + + +[azure_identity]: https://learn.microsoft.com/dotnet/api/overview/azure/identity-readme?view=azure-dotnet +[azure_identity_dac]: https://learn.microsoft.com/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet +[msdocs_openai_chat_quickstart]: https://learn.microsoft.com/azure/ai-services/openai/chatgpt-quickstart?pivots=programming-language-csharp +[msdocs_openai_dalle_quickstart]: https://learn.microsoft.com/azure/ai-services/openai/dall-e-quickstart?pivots=programming-language-csharp +[msdocs_openai_whisper_quickstart]: https://learn.microsoft.com/azure/ai-services/openai/whisper-quickstart +[msdocs_openai_tts_quickstart]: https://learn.microsoft.com/azure/ai-services/openai/text-to-speech-quickstart +[msdocs_openai_completion]: https://learn.microsoft.com/azure/cognitive-services/openai/how-to/completions +[msdocs_openai_embedding]: https://learn.microsoft.com/azure/cognitive-services/openai/concepts/understand-embeddings +[style-guide-msft]: https://docs.microsoft.com/style-guide/capitalization +[style-guide-cloud]: https://aka.ms/azsdk/cloud-style-guide +[azure_openai_client_class]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/openai/Azure.AI.OpenAI/src/Custom/AzureOpenAIClient.cs +[openai_rest]: https://learn.microsoft.com/azure/cognitive-services/openai/reference +[azure_openai_completions_docs]: https://learn.microsoft.com/azure/cognitive-services/openai/how-to/completions +[azure_openai_embeddgings_docs]: https://learn.microsoft.com/azure/cognitive-services/openai/concepts/understand-embeddings +[openai_contrib]: https://github.com/Azure/azure-sdk-for-net/blob/main/CONTRIBUTING.md +[cla]: https://cla.microsoft.com +[code_of_conduct]: https://opensource.microsoft.com/codeofconduct/ +[code_of_conduct_faq]: https://opensource.microsoft.com/codeofconduct/faq/ +[email_opencode]: mailto:opencode@microsoft.com + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-net/sdk/openai/Azure.AI.OpenAI/README.png) \ No newline at end of file diff --git a/.dotnet.azure/assets.json b/.dotnet.azure/assets.json new file mode 100644 index 000000000..39d86235b --- /dev/null +++ b/.dotnet.azure/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "net", + "TagPrefix": "net/openai/Azure.AI.OpenAI", + "Tag": "net/openai/Azure.AI.OpenAI_23ae923738" +} diff --git a/.dotnet.azure/src/Azure.AI.OpenAI.csproj b/.dotnet.azure/src/Azure.AI.OpenAI.csproj index d0c5122a2..68ef5ca2b 100644 --- a/.dotnet.azure/src/Azure.AI.OpenAI.csproj +++ b/.dotnet.azure/src/Azure.AI.OpenAI.csproj @@ -12,7 +12,8 @@ Azure OpenAI's official extension package for using OpenAI's .NET library with the Azure OpenAI Service. Azure.AI.OpenAI Client Library - 2.0.0-beta.3 + 2.0.0 + beta.3 Microsoft Azure OpenAI true diff --git a/.dotnet.azure/src/Custom/Assistants/AzureAssistantClient.Protocol.cs b/.dotnet.azure/src/Custom/Assistants/AzureAssistantClient.Protocol.cs index c21e36c6f..36e30a5d6 100644 --- a/.dotnet.azure/src/Custom/Assistants/AzureAssistantClient.Protocol.cs +++ b/.dotnet.azure/src/Custom/Assistants/AzureAssistantClient.Protocol.cs @@ -107,7 +107,7 @@ public override ClientResult CreateMessage(string threadId, BinaryContent conten return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); } - /// + /// public override IAsyncEnumerable GetMessagesAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); @@ -116,7 +116,6 @@ public override IAsyncEnumerable GetMessagesAsync(string threadId, return PageCollectionHelpers.CreateAsync(enumerator); } - /// public override IEnumerable GetMessages(string threadId, int? limit, string order, string after, string before, RequestOptions options) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); @@ -259,7 +258,6 @@ public override ClientResult CreateRun(string threadId, BinaryContent content, R } } - /// public override IAsyncEnumerable GetRunsAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); @@ -268,7 +266,6 @@ public override IAsyncEnumerable GetRunsAsync(string threadId, int return PageCollectionHelpers.CreateAsync(enumerator); } - /// public override IEnumerable GetRuns(string threadId, int? limit, string order, string after, string before, RequestOptions options) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); @@ -379,7 +376,6 @@ public override ClientResult SubmitToolOutputsToRun(string threadId, string runI } } - /// public override IAsyncEnumerable GetRunStepsAsync(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions options) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); @@ -389,7 +385,6 @@ public override IAsyncEnumerable GetRunStepsAsync(string threadId, return PageCollectionHelpers.CreateAsync(enumerator); } - /// public override IEnumerable GetRunSteps(string threadId, string runId, int? limit, string order, string after, string before, RequestOptions options) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); diff --git a/.dotnet.azure/src/Custom/Assistants/AzureAssistantClient.cs b/.dotnet.azure/src/Custom/Assistants/AzureAssistantClient.cs index cd623fdab..c8b48f925 100644 --- a/.dotnet.azure/src/Custom/Assistants/AzureAssistantClient.cs +++ b/.dotnet.azure/src/Custom/Assistants/AzureAssistantClient.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using OpenAI.Assistants; using System.ClientModel.Primitives; namespace Azure.AI.OpenAI.Assistants; @@ -29,6 +28,5 @@ internal AzureAssistantClient(ClientPipeline pipeline, Uri endpoint, AzureOpenAI } protected AzureAssistantClient() - { - } + { } } diff --git a/.dotnet.azure/src/Custom/Assistants/AzureRunStepDetailsUpdate.cs b/.dotnet.azure/src/Custom/Assistants/AzureRunStepDetailsUpdate.cs deleted file mode 100644 index d70d99394..000000000 --- a/.dotnet.azure/src/Custom/Assistants/AzureRunStepDetailsUpdate.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; - -namespace Azure.AI.OpenAI; - -public static class RunStepDetailsUpdateExtensions -{ - /// - /// Gets a value indicating whether this instance represents a call to a browser tool. - /// - /// The tool call to check the type of. - /// True if the tool call represents a browser tool call, false otherwise. - [Experimental("AOAI001")] - public static bool IsBingSearchKind(this RunStepDetailsUpdate baseUpdate) - { - return baseUpdate?._toolCall?.Type == "browser"; - } -} diff --git a/.dotnet.azure/src/Custom/Assistants/Internal/GeneratorStubs.cs b/.dotnet.azure/src/Custom/Assistants/Internal/GeneratorStubs.cs deleted file mode 100644 index a73cb2016..000000000 --- a/.dotnet.azure/src/Custom/Assistants/Internal/GeneratorStubs.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Azure.AI.OpenAI.Assistants; - -[CodeGenModel("BingSearchToolDefinitionBrowser")] internal partial class InternalBingSearchToolDefinitionBrowser { } -[CodeGenModel("MessageContentTextAnnotationsBingSearchUrlCitation")] internal partial class InternalMessageContentTextAnnotationsBingSearchUrlCitation { } -[CodeGenModel("MessageContentTextAnnotationsBingSearchUrlCitationUrlCitation")] internal partial class InternalMessageContentTextAnnotationsBingSearchUrlCitationUrlCitation { } -[CodeGenModel("MessageDeltaContentTextAnnotationsBingSearchUrlCitation")] internal partial class InternalMessageDeltaContentTextAnnotationsBingSearchUrlCitation { } -[CodeGenModel("MessageDeltaContentTextAnnotationsBingSearchUrlCitationUrlCitation")] internal partial class InternalMessageDeltaContentTextAnnotationsBingSearchUrlCitationUrlCitation { } -[CodeGenModel("RunStepDetailsToolCallsBingSearchObject")] internal partial class InternalRunStepDetailsToolCallsBingSearchObject { } -[CodeGenModel("RunStepDeltaStepDetailsToolCallsBingSearchObject")] internal partial class InternalRunStepDeltaStepDetailsToolCallsBingSearchObject { } diff --git a/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureAssistantsPageEnumerator.cs b/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureAssistantsPageEnumerator.cs index 9b1975a15..f351279a7 100644 --- a/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureAssistantsPageEnumerator.cs +++ b/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureAssistantsPageEnumerator.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.ClientModel; using System.ClientModel.Primitives; diff --git a/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureMessagesPageEnumerator.cs b/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureMessagesPageEnumerator.cs index 46486b06b..79ad28d13 100644 --- a/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureMessagesPageEnumerator.cs +++ b/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureMessagesPageEnumerator.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.ClientModel; using System.ClientModel.Primitives; @@ -11,7 +14,7 @@ internal partial class AzureMessagesPageEnumerator : MessagesPageEnumerator public AzureMessagesPageEnumerator( ClientPipeline pipeline, Uri endpoint, - string threadId, + string threadId, int? limit, string order, string after, string before, string apiVersion, RequestOptions options) diff --git a/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureRunStepsPageEnumerator.cs b/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureRunStepsPageEnumerator.cs index 9d66fbc3a..e7f6ae902 100644 --- a/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureRunStepsPageEnumerator.cs +++ b/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureRunStepsPageEnumerator.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.ClientModel; using System.ClientModel.Primitives; diff --git a/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureRunsPageEnumerator.cs b/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureRunsPageEnumerator.cs index 0b394354a..65afbca20 100644 --- a/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureRunsPageEnumerator.cs +++ b/.dotnet.azure/src/Custom/Assistants/Internal/Pagination/AzureRunsPageEnumerator.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.ClientModel; using System.ClientModel.Primitives; @@ -20,7 +23,6 @@ public AzureRunsPageEnumerator( _apiVersion = apiVersion; } - internal override async Task GetRunsAsync(string threadId, int? limit, string order, string after, string before, RequestOptions options) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); diff --git a/.dotnet.azure/src/Custom/Audio/AzureAudioClient.cs b/.dotnet.azure/src/Custom/Audio/AzureAudioClient.cs index 40c40b1cf..f15480951 100644 --- a/.dotnet.azure/src/Custom/Audio/AzureAudioClient.cs +++ b/.dotnet.azure/src/Custom/Audio/AzureAudioClient.cs @@ -32,6 +32,5 @@ internal AzureAudioClient(ClientPipeline pipeline, string deploymentName, Uri en } protected AzureAudioClient() - { - } + { } } diff --git a/.dotnet.azure/src/Custom/AzureOpenAIAudience.cs b/.dotnet.azure/src/Custom/AzureOpenAIAudience.cs new file mode 100644 index 000000000..719fcdd8b --- /dev/null +++ b/.dotnet.azure/src/Custom/AzureOpenAIAudience.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ComponentModel; + +namespace Azure.AI.OpenAI; + +/// +/// Represents cloud authentication audiences available for Azure OpenAI. +/// These audiences correspond to authorization token authentication scopes. +/// +public readonly partial struct AzureOpenAIAudience : IEquatable +{ + private readonly string _value; + + /// + /// Initializes a new instance of the object. + /// + /// + /// Please consider using one of the known, valid values like or . + /// + /// + /// The Microsoft Entra audience to use when forming authorization scopes. + /// For Azure OpenAI, this value corresponds to a URL that identifies the Azure cloud where the resource is located. + /// For more information: . + /// + /// is null. + public AzureOpenAIAudience(string value) + { + Argument.AssertNotNullOrEmpty(value, nameof(value)); + _value = value; + } + + private const string AzurePublicCloudValue = "https://cognitiveservices.azure.com/.default"; + private const string AzureGovernmentValue = "https://cognitiveservices.azure.us/.default"; + + /// + /// The authorization audience used to connect to the public Azure cloud. Default if not otherwise specified. + /// + public static AzureOpenAIAudience AzurePublicCloud { get; } = new AzureOpenAIAudience(AzurePublicCloudValue); + + /// + /// The authorization audience used to authenticate with the Azure Government cloud. + /// + /// + /// For more information, please refer to + /// . + /// + public static AzureOpenAIAudience AzureGovernment { get; } = new AzureOpenAIAudience(AzureGovernmentValue); + + /// Determines if two values are the same. + public static bool operator ==(AzureOpenAIAudience left, AzureOpenAIAudience right) => left.Equals(right); + /// Determines if two values are not the same. + public static bool operator !=(AzureOpenAIAudience left, AzureOpenAIAudience right) => !left.Equals(right); + /// Converts a string to a . + public static implicit operator AzureOpenAIAudience(string value) => new AzureOpenAIAudience(value); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is AzureOpenAIAudience other && Equals(other); + /// + public bool Equals(AzureOpenAIAudience other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => _value != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(_value) : 0; + /// + public override string ToString() => _value; +} diff --git a/.dotnet.azure/src/Custom/AzureOpenAIClient.cs b/.dotnet.azure/src/Custom/AzureOpenAIClient.cs index 6bdbdea55..9569b4ef6 100644 --- a/.dotnet.azure/src/Custom/AzureOpenAIClient.cs +++ b/.dotnet.azure/src/Custom/AzureOpenAIClient.cs @@ -72,7 +72,6 @@ public partial class AzureOpenAIClient : OpenAIClient /// /// The Azure OpenAI resource endpoint to use. This should not include model deployment or operation information. For example: https://my-resource.openai.azure.com. /// The API key to authenticate with the service. - /// The options to configure the client. public AzureOpenAIClient(Uri endpoint, AzureKeyCredential credential) : this(endpoint, credential, new AzureOpenAIClientOptions()) { } @@ -143,7 +142,7 @@ public AzureOpenAIClient(Uri endpoint, AzureKeyCredential credential, AzureOpenA /// Example: https://my-resource.openai.azure.com /// /// - /// The API key to use when authenticating with the provided endpoint. + /// The API key to use when authenticating with the provided endpoint. /// The scenario-independent options to use. public AzureOpenAIClient(Uri endpoint, TokenCredential credential, AzureOpenAIClientOptions options = null) : this(CreatePipeline(credential, options), endpoint, options) @@ -268,18 +267,18 @@ public override VectorStoreClient GetVectorStoreClient() => new AzureVectorStoreClient(Pipeline, _endpoint, _options); private static ClientPipeline CreatePipeline(PipelinePolicy authenticationPolicy, AzureOpenAIClientOptions options) - { - return ClientPipeline.Create( + => ClientPipeline.Create( options ?? new(), - perCallPolicies: [ + perCallPolicies: + [ + CreateAddUserAgentHeaderPolicy(options), + CreateAddClientRequestIdHeaderPolicy(), ], - perTryPolicies: [ + perTryPolicies: + [ authenticationPolicy, - CreateAddUserAgentHeaderPolicy(options), ], - beforeTransportPolicies: [ - ]); - } + beforeTransportPolicies: []); internal static ClientPipeline CreatePipeline(ApiKeyCredential credential, AzureOpenAIClientOptions options = null) { @@ -290,12 +289,14 @@ internal static ClientPipeline CreatePipeline(ApiKeyCredential credential, Azure internal static ClientPipeline CreatePipeline(TokenCredential credential, AzureOpenAIClientOptions options = null) { Argument.AssertNotNull(credential, nameof(credential)); - return CreatePipeline(new AzureTokenAuthenticationPolicy(credential), options); + string authorizationScope = options?.Audience?.ToString() + ?? AzureOpenAIAudience.AzurePublicCloud.ToString(); + return CreatePipeline(new AzureTokenAuthenticationPolicy(credential, [authorizationScope]), options); } private static PipelinePolicy CreateAddUserAgentHeaderPolicy(AzureOpenAIClientOptions options = null) { - Core.TelemetryDetails telemetryDetails = new(typeof(AzureOpenAIClient).Assembly); + Core.TelemetryDetails telemetryDetails = new(typeof(AzureOpenAIClient).Assembly, options?.ApplicationId); return new GenericActionPipelinePolicy( requestAction: request => { @@ -306,8 +307,23 @@ private static PipelinePolicy CreateAddUserAgentHeaderPolicy(AzureOpenAIClientOp }); } + private static PipelinePolicy CreateAddClientRequestIdHeaderPolicy() + { + return new GenericActionPipelinePolicy(request => + { + if (request?.Headers is not null) + { + string requestId = request.Headers.TryGetValue(s_clientRequestIdHeaderKey, out string existingHeader) == true + ? existingHeader + : Guid.NewGuid().ToString().ToLowerInvariant(); + request.Headers.Set(s_clientRequestIdHeaderKey, requestId); + } + }); + } + private static readonly string s_userAgentHeaderKey = "User-Agent"; - private static PipelineMessageClassifier _pipelineMessageClassifier; + private static readonly string s_clientRequestIdHeaderKey = "x-ms-client-request-id"; + private static PipelineMessageClassifier s_pipelineMessageClassifier; internal static PipelineMessageClassifier PipelineMessageClassifier - => _pipelineMessageClassifier ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 201 }); + => s_pipelineMessageClassifier ??= PipelineMessageClassifier.Create(stackalloc ushort[] { 200, 201 }); } diff --git a/.dotnet.azure/src/Custom/AzureOpenAIClientOptions.cs b/.dotnet.azure/src/Custom/AzureOpenAIClientOptions.cs index 15a0a07c5..1b2989bfb 100644 --- a/.dotnet.azure/src/Custom/AzureOpenAIClientOptions.cs +++ b/.dotnet.azure/src/Custom/AzureOpenAIClientOptions.cs @@ -1,17 +1,46 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using OpenAI; +using System.ClientModel.Primitives; namespace Azure.AI.OpenAI; /// /// Defines the scenario-independent, client-level options for the Azure-specific OpenAI client. /// -public partial class AzureOpenAIClientOptions : OpenAIClientOptions +public partial class AzureOpenAIClientOptions : ClientPipelineOptions { - internal string Version => _version; - private readonly string _version; + internal string Version { get; } + + /// + /// The authorization audience to use when authenticating with Azure authentication tokens + /// + /// + /// By default, the public Azure cloud will be used to authenticate tokens. Modify this value to authenticate tokens + /// with other clouds like Azure Government. + /// + public AzureOpenAIAudience? Audience + { + get => _authorizationAudience; + set + { + AssertNotFrozen(); + _authorizationAudience = value; + } + } + private AzureOpenAIAudience? _authorizationAudience; + + /// + public string ApplicationId + { + get => _applicationId; + set + { + AssertNotFrozen(); + _applicationId = value; + } + } + private string _applicationId; /// /// Initializes a new instance of @@ -21,7 +50,7 @@ public partial class AzureOpenAIClientOptions : OpenAIClientOptions public AzureOpenAIClientOptions(ServiceVersion version = LatestVersion) : base() { - _version = version switch + Version = version switch { ServiceVersion.V2024_04_01_Preview => "2024-04-01-preview", ServiceVersion.V2024_05_01_Preview => "2024-05-01-preview", @@ -29,6 +58,7 @@ public AzureOpenAIClientOptions(ServiceVersion version = LatestVersion) ServiceVersion.V2024_07_01_Preview => "2024-07-01-preview", _ => throw new NotSupportedException() }; + RetryPolicy = new RetryWithDelaysPolicy(); } /// The version of the service to use. @@ -41,5 +71,33 @@ public enum ServiceVersion V2024_07_01_Preview = 10, } + internal class RetryWithDelaysPolicy : ClientRetryPolicy + { + protected override TimeSpan GetNextDelay(PipelineMessage message, int tryCount) + { + TimeSpan? TryGetTimeSpanFromHeader(string headerName, int millisecondsPerValue = 1, bool allowDateTimeOffset = false) + { + if (double.TryParse( + message?.Response?.Headers?.TryGetValue(headerName, out string textValue) == true ? textValue : null, + out double doubleValue) == true) + { + return TimeSpan.FromMilliseconds(millisecondsPerValue * doubleValue); + } + else if (allowDateTimeOffset && DateTimeOffset.TryParse(headerName, out DateTimeOffset delayUntil)) + { + return delayUntil - DateTimeOffset.Now; + } + return null; + } + + TimeSpan? delayFromHeader = + TryGetTimeSpanFromHeader("retry-after-ms") + ?? TryGetTimeSpanFromHeader("x-ms-retry-after-ms") + ?? TryGetTimeSpanFromHeader("Retry-After", millisecondsPerValue: 1000, allowDateTimeOffset: true); + + return delayFromHeader ?? base.GetNextDelay(message, tryCount); + } + } + private const ServiceVersion LatestVersion = ServiceVersion.V2024_07_01_Preview; } diff --git a/.dotnet.azure/src/Custom/AzureTokenAuthenticationPolicy.cs b/.dotnet.azure/src/Custom/AzureTokenAuthenticationPolicy.cs index bf427a097..6fb81cb1a 100644 --- a/.dotnet.azure/src/Custom/AzureTokenAuthenticationPolicy.cs +++ b/.dotnet.azure/src/Custom/AzureTokenAuthenticationPolicy.cs @@ -3,24 +3,32 @@ using Azure.Core; using System.ClientModel.Primitives; +using System.Net; namespace Azure.AI.OpenAI; internal partial class AzureTokenAuthenticationPolicy : PipelinePolicy { private readonly TokenCredential _credential; + private readonly string[] _scopes; + private readonly TimeSpan _refreshOffset; private AccessToken? _currentToken; - public AzureTokenAuthenticationPolicy(TokenCredential credential) + public AzureTokenAuthenticationPolicy(TokenCredential credential, IEnumerable scopes, TimeSpan? refreshOffset = null) { + Argument.AssertNotNull(credential, nameof(credential)); + Argument.AssertNotNull(scopes, nameof(scopes)); + _credential = credential; + _scopes = scopes.ToArray(); + _refreshOffset = refreshOffset ?? s_defaultRefreshOffset; } public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { if (message?.Request is not null) { - if (!_currentToken.HasValue || _currentToken.Value.ExpiresOn < DateTimeOffset.UtcNow + TimeSpan.FromSeconds(30)) + if (!IsTokenFresh()) { TokenRequestContext tokenRequestContext = CreateRequestContext(message.Request); _currentToken = _credential.GetToken(tokenRequestContext, cancellationToken: default); @@ -28,13 +36,17 @@ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { if (message?.Request is not null) { - if (!_currentToken.HasValue || _currentToken.Value.ExpiresOn < DateTimeOffset.UtcNow + TimeSpan.FromSeconds(30)) + if (!IsTokenFresh()) { TokenRequestContext tokenRequestContext = CreateRequestContext(message.Request); _currentToken @@ -43,15 +55,26 @@ public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyL message?.Request?.Headers?.Add("Authorization", $"Bearer {_currentToken.Value.Token}"); } await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + if (message?.Response?.Status == (int)HttpStatusCode.Unauthorized) + { + _currentToken = null; + } + } + + private bool IsTokenFresh() + { + if (!_currentToken.HasValue) return false; + DateTimeOffset refreshAt = _currentToken.Value.RefreshOn ?? (_currentToken.Value.ExpiresOn - _refreshOffset); + return DateTimeOffset.UtcNow < refreshAt; } - private static TokenRequestContext CreateRequestContext(PipelineRequest request) + private TokenRequestContext CreateRequestContext(PipelineRequest request) { string clientRequestId = request.Headers.TryGetValue("x-ms-client-request-id", out string messageClientId) == true ? messageClientId : null; - return new TokenRequestContext(DefaultAuthorizationScopes, clientRequestId); + return new TokenRequestContext(_scopes, clientRequestId); } - private static readonly string[] DefaultAuthorizationScopes = ["https://cognitiveservices.azure.com/.default"]; -} \ No newline at end of file + private static readonly TimeSpan s_defaultRefreshOffset = TimeSpan.FromMinutes(5); +} diff --git a/.dotnet.azure/src/Custom/Batch/AzureBatchClient.cs b/.dotnet.azure/src/Custom/Batch/AzureBatchClient.cs index 74c15b8e4..e04d9daf9 100644 --- a/.dotnet.azure/src/Custom/Batch/AzureBatchClient.cs +++ b/.dotnet.azure/src/Custom/Batch/AzureBatchClient.cs @@ -32,6 +32,5 @@ internal AzureBatchClient(ClientPipeline pipeline, string deploymentName, Uri en } protected AzureBatchClient() - { - } + { } } diff --git a/.dotnet.azure/src/Custom/Chat/AzureChatClient.cs b/.dotnet.azure/src/Custom/Chat/AzureChatClient.cs index 75e131b08..73ee82a03 100644 --- a/.dotnet.azure/src/Custom/Chat/AzureChatClient.cs +++ b/.dotnet.azure/src/Custom/Chat/AzureChatClient.cs @@ -31,11 +31,11 @@ internal AzureChatClient(ClientPipeline pipeline, string deploymentName, Uri end _deploymentName = deploymentName; _apiVersion = options.Version; + _endpoint = endpoint; } protected AzureChatClient() - { - } + { } /// public override AsyncCollectionResult CompleteChatStreamingAsync(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) diff --git a/.dotnet.azure/src/Custom/Chat/AzureChatCompletionOptions.cs b/.dotnet.azure/src/Custom/Chat/AzureChatCompletionOptions.cs index 26d4cc919..54ab5606e 100644 --- a/.dotnet.azure/src/Custom/Chat/AzureChatCompletionOptions.cs +++ b/.dotnet.azure/src/Custom/Chat/AzureChatCompletionOptions.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using Azure.AI.OpenAI.Chat; using Azure.AI.OpenAI.Internal; -using OpenAI.Chat; namespace Azure.AI.OpenAI; @@ -13,6 +12,8 @@ public static partial class AzureChatCompletionOptionsExtensions [Experimental("AOAI001")] public static void AddDataSource(this ChatCompletionOptions options, AzureChatDataSource dataSource) { + options.SerializedAdditionalRawData ??= new Dictionary(); + IList existingSources = AdditionalPropertyHelpers.GetAdditionalListProperty( options.SerializedAdditionalRawData, diff --git a/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureCosmosDBChatDataSourceParameters.cs b/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureCosmosDBChatDataSourceParameters.cs index 66b46bc1a..a3a7a1bc3 100644 --- a/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureCosmosDBChatDataSourceParameters.cs +++ b/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureCosmosDBChatDataSourceParameters.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Azure.AI.OpenAI.Chat; [CodeGenModel("AzureCosmosDBChatDataSourceParameters")] diff --git a/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureMachineLearningIndexDataSourceParameters.cs b/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureMachineLearningIndexDataSourceParameters.cs index b678c72f0..07930fab6 100644 --- a/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureMachineLearningIndexDataSourceParameters.cs +++ b/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureMachineLearningIndexDataSourceParameters.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Azure.AI.OpenAI.Chat; [CodeGenModel("AzureMachineLearningIndexChatDataSourceParameters")] diff --git a/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureSearchChatDataSourceParameters.cs b/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureSearchChatDataSourceParameters.cs index 5f85bd94c..a8cc7c74f 100644 --- a/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureSearchChatDataSourceParameters.cs +++ b/.dotnet.azure/src/Custom/Chat/Internal/InternalAzureSearchChatDataSourceParameters.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Azure.AI.OpenAI.Chat; [CodeGenModel("AzureSearchChatDataSourceParameters")] diff --git a/.dotnet.azure/src/Custom/Chat/Internal/InternalElasticsearchChatDataSourceParameters.cs b/.dotnet.azure/src/Custom/Chat/Internal/InternalElasticsearchChatDataSourceParameters.cs index 7e4e62899..568067e4c 100644 --- a/.dotnet.azure/src/Custom/Chat/Internal/InternalElasticsearchChatDataSourceParameters.cs +++ b/.dotnet.azure/src/Custom/Chat/Internal/InternalElasticsearchChatDataSourceParameters.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Azure.AI.OpenAI.Chat; [CodeGenModel("ElasticsearchChatDataSourceParameters")] diff --git a/.dotnet.azure/src/Custom/Chat/Internal/InternalPineconeChatDataSourceParameters.cs b/.dotnet.azure/src/Custom/Chat/Internal/InternalPineconeChatDataSourceParameters.cs index a0ceb7784..3b9288b15 100644 --- a/.dotnet.azure/src/Custom/Chat/Internal/InternalPineconeChatDataSourceParameters.cs +++ b/.dotnet.azure/src/Custom/Chat/Internal/InternalPineconeChatDataSourceParameters.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Azure.AI.OpenAI.Chat; [CodeGenModel("PineconeChatDataSourceParameters")] diff --git a/.dotnet.azure/src/Custom/Chat/OnYourData/DataSourceFieldMappings.cs b/.dotnet.azure/src/Custom/Chat/OnYourData/DataSourceFieldMappings.cs index 1593f4e1f..999ade6bc 100644 --- a/.dotnet.azure/src/Custom/Chat/OnYourData/DataSourceFieldMappings.cs +++ b/.dotnet.azure/src/Custom/Chat/OnYourData/DataSourceFieldMappings.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Azure.AI.OpenAI.Chat; [CodeGenModel("AzureSearchChatDataSourceParametersFieldsMapping")] diff --git a/.dotnet.azure/src/Custom/Chat/OnYourData/DataSourceOutputContextFlags.Serialization.cs b/.dotnet.azure/src/Custom/Chat/OnYourData/DataSourceOutputContextFlags.Serialization.cs index c6039ce8f..b5a0ff12d 100644 --- a/.dotnet.azure/src/Custom/Chat/OnYourData/DataSourceOutputContextFlags.Serialization.cs +++ b/.dotnet.azure/src/Custom/Chat/OnYourData/DataSourceOutputContextFlags.Serialization.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Azure.AI.OpenAI.Chat; internal static partial class DataSourceOutputContextFlagsExtensions diff --git a/.dotnet.azure/src/Custom/Common/ContentFilterBlocklistResult.cs b/.dotnet.azure/src/Custom/Common/ContentFilterBlocklistResult.cs index 87acefddb..12a3b13e2 100644 --- a/.dotnet.azure/src/Custom/Common/ContentFilterBlocklistResult.cs +++ b/.dotnet.azure/src/Custom/Common/ContentFilterBlocklistResult.cs @@ -3,8 +3,6 @@ #nullable disable -using System.Collections.Generic; - namespace Azure.AI.OpenAI; [CodeGenModel("AzureContentFilterBlocklistResult")] diff --git a/.dotnet.azure/src/Custom/Embeddings/AzureEmbeddingClient.cs b/.dotnet.azure/src/Custom/Embeddings/AzureEmbeddingClient.cs index cb986e80e..7bbdf2477 100644 --- a/.dotnet.azure/src/Custom/Embeddings/AzureEmbeddingClient.cs +++ b/.dotnet.azure/src/Custom/Embeddings/AzureEmbeddingClient.cs @@ -32,6 +32,5 @@ internal AzureEmbeddingClient(ClientPipeline pipeline, string deploymentName, Ur } protected AzureEmbeddingClient() - { - } + { } } diff --git a/.dotnet.azure/src/Custom/Files/AzureFileClient.Protocol.cs b/.dotnet.azure/src/Custom/Files/AzureFileClient.Protocol.cs index 921c3072f..07b8d3ccf 100644 --- a/.dotnet.azure/src/Custom/Files/AzureFileClient.Protocol.cs +++ b/.dotnet.azure/src/Custom/Files/AzureFileClient.Protocol.cs @@ -4,7 +4,6 @@ using System.ClientModel; using System.ClientModel.Primitives; using System.ComponentModel; -using OpenAI.Files; namespace Azure.AI.OpenAI.Files; @@ -67,8 +66,6 @@ public override async Task GetFileAsync(string fileId, RequestOpti [EditorBrowsable(EditorBrowsableState.Never)] public override ClientResult GetFiles(string purpose, RequestOptions options) { - Argument.AssertNotNullOrEmpty(purpose, nameof(purpose)); - using PipelineMessage message = CreateGetFilesRequestMessage(purpose, options); return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); } @@ -76,8 +73,6 @@ public override ClientResult GetFiles(string purpose, RequestOptions options) [EditorBrowsable(EditorBrowsableState.Never)] public override async Task GetFilesAsync(string purpose, RequestOptions options) { - Argument.AssertNotNullOrEmpty(purpose, nameof(purpose)); - using PipelineMessage message = CreateGetFilesRequestMessage(purpose, options); return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); } diff --git a/.dotnet.azure/src/Custom/Files/AzureFileClient.cs b/.dotnet.azure/src/Custom/Files/AzureFileClient.cs index 699219c66..60eb3403f 100644 --- a/.dotnet.azure/src/Custom/Files/AzureFileClient.cs +++ b/.dotnet.azure/src/Custom/Files/AzureFileClient.cs @@ -30,8 +30,7 @@ internal AzureFileClient(ClientPipeline pipeline, Uri endpoint, AzureOpenAIClien } protected AzureFileClient() - { - } + { } /// public override ClientResult UploadFile(Stream file, string filename, FileUploadPurpose purpose, CancellationToken cancellationToken = default) diff --git a/.dotnet.azure/src/Custom/FineTuning/AzureFineTuningClient.cs b/.dotnet.azure/src/Custom/FineTuning/AzureFineTuningClient.cs index afca340b4..5c69e1bdd 100644 --- a/.dotnet.azure/src/Custom/FineTuning/AzureFineTuningClient.cs +++ b/.dotnet.azure/src/Custom/FineTuning/AzureFineTuningClient.cs @@ -29,6 +29,5 @@ internal AzureFineTuningClient(ClientPipeline pipeline, Uri endpoint, AzureOpenA } protected AzureFineTuningClient() - { - } + { } } diff --git a/.dotnet.azure/src/Custom/Images/AzureImageClient.cs b/.dotnet.azure/src/Custom/Images/AzureImageClient.cs index 5ddabe8f3..ebeda9dae 100644 --- a/.dotnet.azure/src/Custom/Images/AzureImageClient.cs +++ b/.dotnet.azure/src/Custom/Images/AzureImageClient.cs @@ -32,6 +32,5 @@ internal AzureImageClient(ClientPipeline pipeline, string deploymentName, Uri en } protected AzureImageClient() - { - } + {} } diff --git a/.dotnet.azure/src/Custom/VectorStores/AzureVectorStoreClient.cs b/.dotnet.azure/src/Custom/VectorStores/AzureVectorStoreClient.cs index 4b46bcde3..52b07ee81 100644 --- a/.dotnet.azure/src/Custom/VectorStores/AzureVectorStoreClient.cs +++ b/.dotnet.azure/src/Custom/VectorStores/AzureVectorStoreClient.cs @@ -31,6 +31,5 @@ internal AzureVectorStoreClient(ClientPipeline pipeline, Uri endpoint, AzureOpen } protected AzureVectorStoreClient() - { - } + { } } diff --git a/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoreFileBatchesPageEnumerator.cs b/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoreFileBatchesPageEnumerator.cs index bfb2bf07c..21ee0a3fb 100644 --- a/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoreFileBatchesPageEnumerator.cs +++ b/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoreFileBatchesPageEnumerator.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.ClientModel; using System.ClientModel.Primitives; diff --git a/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoreFilesPageEnumerator.cs b/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoreFilesPageEnumerator.cs index 5cf8b09ff..4152a4869 100644 --- a/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoreFilesPageEnumerator.cs +++ b/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoreFilesPageEnumerator.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.ClientModel; using System.ClientModel.Primitives; @@ -11,7 +14,7 @@ internal partial class AzureVectorStoreFilesPageEnumerator : VectorStoreFilesPag public AzureVectorStoreFilesPageEnumerator( ClientPipeline pipeline, Uri endpoint, - string vectorStoreId, + string vectorStoreId, int? limit, string order, string after, string before, string filter, string apiVersion, RequestOptions options) diff --git a/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoresPageEnumerator.cs b/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoresPageEnumerator.cs index 823045419..d8a90039f 100644 --- a/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoresPageEnumerator.cs +++ b/.dotnet.azure/src/Custom/VectorStores/Internal/Pagination/AzureVectorStoresPageEnumerator.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System.ClientModel; using System.ClientModel.Primitives; diff --git a/.dotnet.azure/src/Generated/Internal/BinaryContentHelper.cs b/.dotnet.azure/src/Generated/Internal/BinaryContentHelper.cs index 94ae48ee4..e6f35c517 100644 --- a/.dotnet.azure/src/Generated/Internal/BinaryContentHelper.cs +++ b/.dotnet.azure/src/Generated/Internal/BinaryContentHelper.cs @@ -53,7 +53,7 @@ public static BinaryContent FromEnumerable(IEnumerable enumerable) } public static BinaryContent FromEnumerable(ReadOnlySpan span) - where T : notnull + where T : notnull { Utf8JsonBinaryContent content = new Utf8JsonBinaryContent(); content.JsonWriter.WriteStartArray(); diff --git a/.dotnet.azure/src/Utility/CustomSerializationHelpers.cs b/.dotnet.azure/src/Utility/CustomSerializationHelpers.cs index 5691e6a4a..23fc567fa 100644 --- a/.dotnet.azure/src/Utility/CustomSerializationHelpers.cs +++ b/.dotnet.azure/src/Utility/CustomSerializationHelpers.cs @@ -1,8 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + #nullable enable -using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Text.Json; namespace Azure.AI.OpenAI; @@ -127,4 +128,4 @@ internal static void WriteSerializedAdditionalRawData(this Utf8JsonWriter writer } } } -} \ No newline at end of file +} diff --git a/.dotnet.azure/src/Utility/GenericActionPipelinePolicy.cs b/.dotnet.azure/src/Utility/GenericActionPipelinePolicy.cs index 9c67f0c76..79e5ccc30 100644 --- a/.dotnet.azure/src/Utility/GenericActionPipelinePolicy.cs +++ b/.dotnet.azure/src/Utility/GenericActionPipelinePolicy.cs @@ -26,7 +26,7 @@ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { _requestAction?.Invoke(message.Request); - await ProcessNextAsync(message, pipeline, currentIndex); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); _responseAction?.Invoke(message.Response); } } diff --git a/.dotnet.azure/tests/Assets/edit_sample_image.png b/.dotnet.azure/tests/Assets/edit_sample_image.png deleted file mode 100644 index 869bb1e04..000000000 Binary files a/.dotnet.azure/tests/Assets/edit_sample_image.png and /dev/null differ diff --git a/.dotnet.azure/tests/Assets/edit_sample_mask.png b/.dotnet.azure/tests/Assets/edit_sample_mask.png deleted file mode 100644 index 98b9c237c..000000000 Binary files a/.dotnet.azure/tests/Assets/edit_sample_mask.png and /dev/null differ diff --git a/.dotnet.azure/tests/Assets/french.wav b/.dotnet.azure/tests/Assets/french.wav deleted file mode 100644 index 847f3463a..000000000 Binary files a/.dotnet.azure/tests/Assets/french.wav and /dev/null differ diff --git a/.dotnet.azure/tests/Assets/hello_world.m4a b/.dotnet.azure/tests/Assets/hello_world.m4a deleted file mode 100644 index ed8e09c8f..000000000 Binary files a/.dotnet.azure/tests/Assets/hello_world.m4a and /dev/null differ diff --git a/.dotnet.azure/tests/Assets/speed-talking.wav b/.dotnet.azure/tests/Assets/speed-talking.wav deleted file mode 100644 index 2a09e2737..000000000 Binary files a/.dotnet.azure/tests/Assets/speed-talking.wav and /dev/null differ diff --git a/.dotnet.azure/tests/Assets/stop_sign.png b/.dotnet.azure/tests/Assets/stop_sign.png deleted file mode 100644 index 002b3ae1a..000000000 Binary files a/.dotnet.azure/tests/Assets/stop_sign.png and /dev/null differ diff --git a/.dotnet.azure/tests/Assets/test_config.json b/.dotnet.azure/tests/Assets/test_config.json deleted file mode 100644 index c9d484e70..000000000 --- a/.dotnet.azure/tests/Assets/test_config.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "assistants": { - "endpoint_name": "AZURE_OPENAI_TIP_ENDPOINT", - "api_key_name": "AZURE_OPENAI_TIP_API_KEY" - }, - "audio": { - "endpoint_name": "AZURE_OPENAI_ENDPOINT_SWEDENCENTRAL", - "api_key_name": "AZURE_OPENAI_API_KEY_SWEDENCENTRAL", - "deployment": "whisper" - }, - "batch": { - "endpoint_name": "AZURE_OPENAI_ENDPOINT", - "api_key_name": "AZURE_OPENAI_API_KEY", - "deployment": "gpt-35-turbo" - }, - "chat": { - "endpoint_name": "AZURE_OPENAI_ENDPOINT", - "api_key_name": "AZURE_OPENAI_API_KEY", - "deployment": "gpt-35-turbo" - }, - "embeddings": { - "endpoint_name": "AZURE_OPENAI_ENDPOINT", - "api_key_name": "AZURE_OPENAI_API_KEY", - "deployment": "text-embedding-3-small" - }, - "files": { - "endpoint_name": "AZURE_OPENAI_ENDPOINT", - "api_key_name": "AZURE_OPENAI_API_KEY" - }, - "fine_tuning": { - "endpoint_name": "AZURE_OPENAI_ENDPOINT", - "api_key_name": "AZURE_OPENAI_API_KEY" - }, - "images": { - "endpoint_name": "AZURE_OPENAI_ENDPOINT_SWEDENCENTRAL", - "api_key_name": "AZURE_OPENAI_API_KEY_SWEDENCENTRAL", - "deployment": "dall-e-3" - }, - "vector_stores": { - "endpoint_name": "AZURE_OPENAI_TIP_ENDPOINT", - "api_key_name": "AZURE_OPENAI_TIP_API_KEY" - } -} \ No newline at end of file diff --git a/.dotnet.azure/tests/Assets/variation_sample_image.png b/.dotnet.azure/tests/Assets/variation_sample_image.png deleted file mode 100644 index 119a13e8f..000000000 Binary files a/.dotnet.azure/tests/Assets/variation_sample_image.png and /dev/null differ diff --git a/.dotnet.azure/tests/AssistantTests.cs b/.dotnet.azure/tests/AssistantTests.cs deleted file mode 100644 index 70408f170..000000000 --- a/.dotnet.azure/tests/AssistantTests.cs +++ /dev/null @@ -1,602 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using Azure.AI.OpenAI.Assistants; -using OpenAI; -using OpenAI.Assistants; -using OpenAI.Files; -using OpenAI.VectorStores; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Diagnostics; - -namespace Azure.AI.OpenAI.Tests; - -#pragma warning disable OPENAI001 - -public class AssistantTests : TestBase -{ - [Test] - [Category("Smoke")] - public void CanCreateClient() => Assert.That(GetTestClient(), Is.InstanceOf()); - - [Test] - public void BasicAssistantOperationsWork() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-35-turbo-latest"); - Validate(assistant); - Assert.That(assistant.Name, Is.Null.Or.Empty); - assistant = client.ModifyAssistant(assistant.Id, new AssistantModificationOptions() - { - Name = "test assistant name", - }); - Assert.That(assistant.Name, Is.EqualTo("test assistant name")); - bool deleted = client.DeleteAssistant(assistant.Id); - Assert.That(deleted, Is.True); - assistant = client.CreateAssistant("gpt-35-turbo-latest", new AssistantCreationOptions() - { - Metadata = - { - ["testkey"] = "hello!" - }, - }); - Validate(assistant); - Assistant retrievedAssistant = client.GetAssistant(assistant.Id); - Assert.That(retrievedAssistant.Id, Is.EqualTo(assistant.Id)); - Assert.That(retrievedAssistant.Metadata.TryGetValue("testkey", out string metadataValue) && metadataValue == "hello!"); - Assistant modifiedAssistant = client.ModifyAssistant(assistant.Id, new AssistantModificationOptions() - { - Metadata = - { - ["testkey"] = "goodbye!", - }, - }); - Assert.That(modifiedAssistant.Id, Is.EqualTo(assistant.Id)); - PageableCollection recentAssistants = client.GetAssistants(); - Assistant listedAssistant = recentAssistants.FirstOrDefault(pageItem => pageItem.Id == assistant.Id); - Assert.That(listedAssistant, Is.Not.Null); - Assert.That(listedAssistant.Metadata.TryGetValue("testkey", out string newMetadataValue) && newMetadataValue == "goodbye!"); - } - - [Test] - public void BasicThreadOperationsWork() - { - AssistantClient client = GetTestClient(); - AssistantThread thread = client.CreateThread(); - Validate(thread); - Assert.That(thread.CreatedAt, Is.GreaterThan(s_2024)); - bool deleted = client.DeleteThread(thread.Id); - Assert.That(deleted, Is.True); - - ThreadCreationOptions options = new() - { - Metadata = - { - ["threadMetadata"] = "threadMetadataValue", - } - }; - thread = client.CreateThread(options); - Validate(thread); - Assert.That(thread.Metadata.TryGetValue("threadMetadata", out string threadMetadataValue) && threadMetadataValue == "threadMetadataValue"); - AssistantThread retrievedThread = client.GetThread(thread.Id); - Assert.That(retrievedThread.Id, Is.EqualTo(thread.Id)); - thread = client.ModifyThread(thread, new ThreadModificationOptions() - { - Metadata = - { - ["threadMetadata"] = "newThreadMetadataValue", - }, - }); - Assert.That(thread.Metadata.TryGetValue("threadMetadata", out threadMetadataValue) && threadMetadataValue == "newThreadMetadataValue"); - } - - [Test] - public void SettingResponseFormatWorks() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-35-turbo-latest", new() - { - ResponseFormat = AssistantResponseFormat.JsonObject, - }); - Validate(assistant); - Assert.That(assistant.ResponseFormat, Is.EqualTo(AssistantResponseFormat.JsonObject)); - assistant = client.ModifyAssistant(assistant, new() - { - ResponseFormat = AssistantResponseFormat.Text, - }); - Assert.That(assistant.ResponseFormat, Is.EqualTo(AssistantResponseFormat.Text)); - AssistantThread thread = client.CreateThread(); - Validate(thread); - ThreadMessage message = client.CreateMessage(thread, ["Write some JSON for me!"]); - Validate(message); - ThreadRun run = client.CreateRun(thread, assistant, new() - { - ResponseFormat = AssistantResponseFormat.JsonObject, - }); - Validate(run); - Assert.That(run.ResponseFormat, Is.EqualTo(AssistantResponseFormat.JsonObject)); - } - - [TestCase] - public async Task StreamingToolCall() - { - AssistantClient client = GetTestClient(); - FunctionToolDefinition getWeatherTool = new("get_current_weather", "Gets the user's current weather"); - Assistant assistant = await client.CreateAssistantAsync("gpt-35-turbo-latest", new() - { - Tools = { getWeatherTool } - }); - Validate(assistant); - - Stopwatch stopwatch = Stopwatch.StartNew(); - void Print(string message) => Console.WriteLine($"[{stopwatch.ElapsedMilliseconds,6}] {message}"); - - Print(" >>> Beginning call ... "); - AsyncResultCollection asyncResults = client.CreateThreadAndRunStreamingAsync( - assistant, - new() - { - InitialMessages = { new(["What should I wear outside right now?"]), }, - }); - Print(" >>> Starting enumeration ..."); - - ThreadRun run = null; - - do - { - run = null; - List toolOutputs = []; - await foreach (StreamingUpdate update in asyncResults) - { - string message = update.UpdateKind.ToString(); - - if (update is RunUpdate runUpdate) - { - message += $" run_id:{runUpdate.Value.Id}"; - run = runUpdate.Value; - } - if (update is RequiredActionUpdate requiredActionUpdate) - { - Assert.That(requiredActionUpdate.FunctionName, Is.EqualTo(getWeatherTool.FunctionName)); - Assert.That(requiredActionUpdate.GetThreadRun().Status, Is.EqualTo(RunStatus.RequiresAction)); - message += $" {requiredActionUpdate.FunctionName}"; - toolOutputs.Add(new(requiredActionUpdate.ToolCallId, "warm and sunny")); - } - if (update is MessageContentUpdate contentUpdate) - { - message += $" {contentUpdate.Text}"; - } - Print(message); - } - if (toolOutputs.Count > 0) - { - asyncResults = client.SubmitToolOutputsToRunStreamingAsync(run, toolOutputs); - } - } while (run?.Status.IsTerminal == false); - } - - [Test] - public void BasicMessageOperationsWork() - { - AssistantClient client = GetTestClient(); - AssistantThread thread = client.CreateThread(); - Validate(thread); - ThreadMessage message = client.CreateMessage(thread, ["Hello, world!"]); - Validate(message); - Assert.That(message.CreatedAt, Is.GreaterThan(s_2024)); - Assert.That(message.Content?.Count, Is.EqualTo(1)); - Assert.That(message.Content[0], Is.Not.Null); - Assert.That(message.Content[0].Text, Is.EqualTo("Hello, world!")); - - // BUG: Can't currently delete messages on AOAI - bool deleted = client.DeleteMessage(message); - Assert.That(deleted, Is.True); - - message = client.CreateMessage(thread, ["Goodbye, world!"], new MessageCreationOptions() - { - Metadata = - { - ["messageMetadata"] = "messageMetadataValue", - }, - }); - Validate(message); - Assert.That(message.Metadata.TryGetValue("messageMetadata", out string metadataValue) && metadataValue == "messageMetadataValue"); - - ThreadMessage retrievedMessage = client.GetMessage(thread.Id, message.Id); - Assert.That(retrievedMessage.Id, Is.EqualTo(message.Id)); - - message = client.ModifyMessage(message, new MessageModificationOptions() - { - Metadata = - { - ["messageMetadata"] = "newValue", - } - }); - Assert.That(message.Metadata.TryGetValue("messageMetadata", out metadataValue) && metadataValue == "newValue"); - - PageableCollection messagePage = client.GetMessages(thread); - Assert.That(messagePage.Count, Is.EqualTo(2)); - // BUG: Can't currently delete messages - Assert.That(messagePage.Count, Is.EqualTo(1)); - Assert.That(messagePage.ElementAt(0).Id, Is.EqualTo(message.Id)); - Assert.That(messagePage.ElementAt(0).Metadata.TryGetValue("messageMetadata", out metadataValue) && metadataValue == "newValue"); - } - - [Test] - public void ThreadWithInitialMessagesWorks() - { - AssistantClient client = GetTestClient(); - ThreadCreationOptions options = new() - { - InitialMessages = - { - new(["Hello, world!"]), - new( - [ - "Can you describe this image for me?", - MessageContent.FromImageUrl(new Uri("https://test.openai.com/image.png")) - ]) - { - Metadata = - { - ["messageMetadata"] = "messageMetadataValue", - }, - }, - }, - }; - AssistantThread thread = client.CreateThread(options); - Validate(thread); - PageableCollection messagePage = client.GetMessages(thread, resultOrder: ListOrder.OldestFirst); - List messageList = messagePage.ToList(); - Assert.That(messageList.Count, Is.EqualTo(2)); - Assert.That(messageList[0].Role, Is.EqualTo(MessageRole.User)); - Assert.That(messageList[0].Content?.Count, Is.EqualTo(1)); - Assert.That(messageList[0].Content[0].Text, Is.EqualTo("Hello, world!")); - Assert.That(messageList[1].Content?.Count, Is.EqualTo(2)); - Assert.That(messageList[1].Content[0], Is.Not.Null); - Assert.That(messageList[1].Content[0].Text, Is.EqualTo("Can you describe this image for me?")); - Assert.That(messageList[1].Content[1], Is.Not.Null); - Assert.That(messageList[1].Content[1].ImageUrl.AbsoluteUri, Is.EqualTo("https://test.openai.com/image.png")); - } - - [Test] - public void BasicRunOperationsWork() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-35-turbo-latest"); - Validate(assistant); - AssistantThread thread = client.CreateThread(); - Validate(thread); - PageableCollection runPage = client.GetRuns(thread.Id); - Assert.That(runPage.Count, Is.EqualTo(0)); - ThreadMessage message = client.CreateMessage(thread.Id, ["Hello, assistant!"]); - Validate(message); - Thread.Sleep(3000); - ThreadRun run = client.CreateRun(thread.Id, assistant.Id); - Validate(run); - Assert.That(run.Status, Is.EqualTo(RunStatus.Queued)); - Assert.That(run.CreatedAt, Is.GreaterThan(s_2024)); - ThreadRun retrievedRun = client.GetRun(thread.Id, run.Id); - Assert.That(retrievedRun.Id, Is.EqualTo(run.Id)); - runPage = client.GetRuns(thread.Id); - Assert.That(runPage.Count, Is.EqualTo(1)); - Assert.That(runPage.ElementAt(0).Id, Is.EqualTo(run.Id)); - - PageableCollection messages = client.GetMessages(thread); - Assert.That(messages.Count, Is.EqualTo(1)); - - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); - } - Assert.Multiple(() => - { - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - Assert.That(run.CompletedAt, Is.GreaterThan(s_2024)); - Assert.That(run.RequiredActions, Is.Empty); - Assert.That(run.AssistantId, Is.EqualTo(assistant.Id)); - Assert.That(run.FailedAt, Is.Null); - Assert.That(run.IncompleteDetails, Is.Null); - }); - messages = client.GetMessages(thread); - Assert.That(messages.Count, Is.EqualTo(2)); - - Assert.That(messages.ElementAt(0).Role, Is.EqualTo(MessageRole.Assistant)); - Assert.That(messages.ElementAt(1).Role, Is.EqualTo(MessageRole.User)); - Assert.That(messages.ElementAt(1).Id, Is.EqualTo(message.Id)); - } - - [Test] - public void BasicRunStepFunctionalityWorks() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-35-turbo-latest", new AssistantCreationOptions() - { - Tools = { new CodeInterpreterToolDefinition() }, - Instructions = "Call the code interpreter tool when asked to visualize mathematical concepts.", - }); - Validate(assistant); - - AssistantThread thread = client.CreateThread(new() - { - InitialMessages = { new(["Please graph the equation y = 3x + 4"]), }, - }); - Validate(thread); - - ThreadRun run = client.CreateRun(thread, assistant); - Validate(run); - - while (!run.Status.IsTerminal) - { - Thread.Sleep(1000); - run = client.GetRun(run); - } - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - Assert.That(run.Usage?.TotalTokens, Is.GreaterThan(0)); - - PageableCollection runSteps = client.GetRunSteps(run); - Assert.That(runSteps.Count(), Is.GreaterThan(1)); - Assert.Multiple(() => - { - Assert.That(runSteps.ElementAt(0).AssistantId, Is.EqualTo(assistant.Id)); - Assert.That(runSteps.ElementAt(0).ThreadId, Is.EqualTo(thread.Id)); - Assert.That(runSteps.ElementAt(0).RunId, Is.EqualTo(run.Id)); - Assert.That(runSteps.ElementAt(0).CreatedAt, Is.GreaterThan(s_2024)); - Assert.That(runSteps.ElementAt(0).CompletedAt, Is.GreaterThan(s_2024)); - }); - RunStepDetails details = runSteps.ElementAt(0).Details; - Assert.That(details?.CreatedMessageId, Is.Not.Null.Or.Empty); - - details = runSteps.ElementAt(1).Details; - Assert.Multiple(() => - { - Assert.That(details?.ToolCalls.Count, Is.GreaterThan(0)); - Assert.That(details.ToolCalls[0].ToolKind, Is.EqualTo(RunStepToolCallKind.CodeInterpreter)); - Assert.That(details.ToolCalls[0].ToolCallId, Is.Not.Null.Or.Empty); - Assert.That(details.ToolCalls[0].CodeInterpreterInput, Is.Not.Null.Or.Empty); - Assert.That(details.ToolCalls[0].CodeInterpreterOutputs?.Count, Is.GreaterThan(0)); - Assert.That(details.ToolCalls[0].CodeInterpreterOutputs[0].ImageFileId, Is.Not.Null.Or.Empty); - }); - } - - [Test] - public void FunctionToolsWork() - { - AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-35-turbo-latest", new AssistantCreationOptions() - { - Tools = - { - new FunctionToolDefinition() - { - FunctionName = "get_favorite_food_for_day_of_week", - Description = "gets the user's favorite food for a given day of the week, like Tuesday", - Parameters = BinaryData.FromObjectAsJson(new - { - type = "object", - properties = new - { - day_of_week = new - { - type = "string", - description = "a day of the week, like Tuesday or Saturday", - } - } - }), - }, - }, - }); - Validate(assistant); - Assert.That(assistant.Tools?.Count, Is.EqualTo(1)); - - FunctionToolDefinition responseToolDefinition = assistant.Tools[0] as FunctionToolDefinition; - Assert.That(responseToolDefinition?.FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); - Assert.That(responseToolDefinition?.Parameters, Is.Not.Null); - - ThreadRun run = client.CreateThreadAndRun( - assistant, - new ThreadCreationOptions() - { - InitialMessages = { new(["What should I eat on Thursday?"]) }, - }, - new RunCreationOptions() - { - AdditionalInstructions = "Call provided tools when appropriate.", - }); - Validate(run); - Console.WriteLine($" Run status right after creation: {run.Status}"); - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); - } - Assert.That(run.Status, Is.EqualTo(RunStatus.RequiresAction)); - Assert.That(run.RequiredActions?.Count, Is.EqualTo(1)); - Assert.That(run.RequiredActions[0].ToolCallId, Is.Not.Null.Or.Empty); - Assert.That(run.RequiredActions[0].FunctionName, Is.EqualTo("get_favorite_food_for_day_of_week")); - Assert.That(run.RequiredActions[0].FunctionArguments, Is.Not.Null.Or.Empty); - - run = client.SubmitToolOutputsToRun(run, [new(run.RequiredActions[0].ToolCallId, "tacos")]); - Assert.That(run.Status.IsTerminal, Is.False); - - for (int i = 0; i < 10 && !run.Status.IsTerminal; i++) - { - Thread.Sleep(500); - run = client.GetRun(run); - } - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - - PageableCollection messages = client.GetMessages(run.ThreadId, resultOrder: ListOrder.NewestFirst); - Assert.That(messages.Count, Is.GreaterThan(1)); - Assert.That(messages.ElementAt(0).Role, Is.EqualTo(MessageRole.Assistant)); - Assert.That(messages.ElementAt(0).Content?[0], Is.Not.Null); - Assert.That(messages.ElementAt(0).Content?[0].Text, Does.Contain("tacos")); - } - - [Test] - public void BasicFileSearchWorks() - { - // First, we need to upload a simple test file. - AssistantClient client = GetTestClient(); - FileClient fileClient = GetChildTestClient(client); - - OpenAIFileInfo testFile = fileClient.UploadFile( - BinaryData.FromString(""" - This file describes the favorite foods of several people. - - Summanus Ferdinand: tacos - Tekakwitha Effie: pizza - Filip Carola: cake - """).ToStream(), - "favorite_foods.txt", - FileUploadPurpose.Assistants); - Validate(testFile); - - // Create an assistant, using the creation helper to make a new vector store - Assistant assistant = client.CreateAssistant("gpt-35-turbo-latest", new() - { - Tools = { new FileSearchToolDefinition() }, - ToolResources = new() - { - FileSearch = new() - { - NewVectorStores = - { - new VectorStoreCreationHelper([testFile.Id]), - } - } - } - }); - Validate(assistant); - Assert.That(assistant.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); - string createdVectorStoreId = assistant.ToolResources.FileSearch.VectorStoreIds[0]; - ValidateById(createdVectorStoreId); - - // Modify an assistant to use the existing vector store - assistant = client.ModifyAssistant(assistant, new AssistantModificationOptions() - { - ToolResources = new() - { - FileSearch = new() - { - VectorStoreIds = { assistant.ToolResources.FileSearch.VectorStoreIds[0] }, - }, - }, - }); - Assert.That(assistant.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); - Assert.That(assistant.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); - - // Create a thread with an override vector store - AssistantThread thread = client.CreateThread(new ThreadCreationOptions() - { - InitialMessages = { new(["Using the files you have available, what's Filip's favorite food?"]) }, - ToolResources = new() - { - FileSearch = new() - { - NewVectorStores = - { - new VectorStoreCreationHelper([testFile.Id]) - } - } - } - }); - Validate(thread); - Assert.That(thread.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); - createdVectorStoreId = thread.ToolResources.FileSearch.VectorStoreIds[0]; - ValidateById(createdVectorStoreId); - - // Ensure that modifying the thread with an existing vector store works - thread = client.ModifyThread(thread, new ThreadModificationOptions() - { - ToolResources = new() - { - FileSearch = new() - { - VectorStoreIds = { createdVectorStoreId }, - } - } - }); - Assert.That(thread.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); - Assert.That(thread.ToolResources.FileSearch.VectorStoreIds[0], Is.EqualTo(createdVectorStoreId)); - - ThreadRun run = client.CreateRun(thread, assistant); - Validate(run); - do - { - Thread.Sleep(1000); - run = client.GetRun(run); - } while (run?.Status.IsTerminal == false); - Assert.That(run.Status, Is.EqualTo(RunStatus.Completed)); - - PageableCollection messages = client.GetMessages(thread, resultOrder: ListOrder.NewestFirst); - foreach (ThreadMessage message in messages) - { - foreach (MessageContent content in message.Content) - { - Console.WriteLine(content.Text); - foreach (TextAnnotation annotation in content.TextAnnotations) - { - Console.WriteLine($" --> From file: {annotation.InputFileId}, replacement: {annotation.TextToReplace}"); - } - } - } - Assert.That(messages.Count() > 1); - Assert.That(messages.Any(message => message.Content.Any(content => content.Text.ToLower().Contains("cake")))); - } - - [Test] - public async Task StreamingRunWorks() - { - AssistantClient client = GetTestClient(); - Assistant assistant = await client.CreateAssistantAsync("gpt-35-turbo-latest"); - Validate(assistant); - - AssistantThread thread = await client.CreateThreadAsync(new() - { - InitialMessages = { new(["Hello there, assistant! How are you today?"]), }, - }); - Validate(thread); - - Stopwatch stopwatch = Stopwatch.StartNew(); - void Print(string message) => Console.WriteLine($"[{stopwatch.ElapsedMilliseconds,6}] {message}"); - - AsyncResultCollection streamingResult - = client.CreateRunStreamingAsync(thread.Id, assistant.Id); - - Print(">>> Connected <<<"); - - await foreach (StreamingUpdate update in streamingResult) - { - string message = $"{update.UpdateKind} "; - if (update is RunUpdate runUpdate) - { - DateTimeOffset? time = update.UpdateKind switch - { - StreamingUpdateReason.RunCreated => runUpdate.Value.CreatedAt, - StreamingUpdateReason.RunQueued => runUpdate.Value.StartedAt, - StreamingUpdateReason.RunInProgress => runUpdate.Value.StartedAt, - StreamingUpdateReason.RunCompleted => runUpdate.Value.CompletedAt, - _ => null, - }; - message += $"at {time}"; - } - if (update is MessageContentUpdate contentUpdate) - { - if (contentUpdate.Role.HasValue) - { - message += $"[{contentUpdate.Role}]"; - } - message += $"[{contentUpdate.MessageIndex}] {contentUpdate.Text}"; - } - Print(message); - } - Print(">>> Done <<<"); - } - - private static readonly DateTimeOffset s_2024 = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); -} \ No newline at end of file diff --git a/.dotnet.azure/tests/AudioTests.cs b/.dotnet.azure/tests/AudioTests.cs deleted file mode 100644 index 936672f48..000000000 --- a/.dotnet.azure/tests/AudioTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using Azure.Core; -using Azure.Identity; -using OpenAI.Audio; -using OpenAI.Chat; -using System.ClientModel; - -namespace Azure.AI.OpenAI.Tests; - -public class AudioTests : TestBase -{ - [Test] - [Category("Smoke")] - public void CanCreateClient() => Assert.That(GetTestClient(), Is.InstanceOf()); - - [Test] - public void TranscriptionWorks() - { - AudioClient audioClient = GetTestClient("whisper"); - AudioTranscription transcription = audioClient.TranscribeAudio( - Path.Combine("Assets", "hello_world.m4a")); - Assert.That(transcription?.Text, Is.Not.Null.Or.Empty); - } - - [Test] - public void TranslationWorks() - { - AudioClient audioClient = GetTestClient("whisper"); - AudioTranslation translation = audioClient.TranslateAudio( - Path.Combine("Assets", "french.wav")); - Assert.That(translation?.Text, Is.Not.Null.Or.Empty); - } - - [Test] - public void TextToSpeechWorks() - { - AudioClient audioClient = GetTestClient("tts"); - BinaryData ttsData = audioClient.GenerateSpeechFromText( - "hello, world!", - GeneratedSpeechVoice.Alloy); - Assert.That(ttsData, Is.Not.Null); - } -} \ No newline at end of file diff --git a/.dotnet.azure/tests/Azure.AI.OpenAI.Tests.csproj b/.dotnet.azure/tests/Azure.AI.OpenAI.Tests.csproj deleted file mode 100644 index 8cd5fb367..000000000 --- a/.dotnet.azure/tests/Azure.AI.OpenAI.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - net7.0 - - $(NoWarn);CS1591 - - - - - - - - - - - - - diff --git a/.dotnet.azure/tests/BatchTests.cs b/.dotnet.azure/tests/BatchTests.cs deleted file mode 100644 index 101466e0f..000000000 --- a/.dotnet.azure/tests/BatchTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using OpenAI.Batch; - -namespace Azure.AI.OpenAI.Tests; - -public class BatchTests : TestBase -{ - [Test] - [Category("Smoke")] - public void CanCreateClient() => Assert.That(GetTestClient(), Is.InstanceOf()); -} \ No newline at end of file diff --git a/.dotnet.azure/tests/ChatTests.cs b/.dotnet.azure/tests/ChatTests.cs deleted file mode 100644 index eee2f4d7e..000000000 --- a/.dotnet.azure/tests/ChatTests.cs +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using Azure.AI.OpenAI.Chat; -using Azure.Core; -using Azure.Identity; -using OpenAI.Chat; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Text; - -namespace Azure.AI.OpenAI.Tests; - -public class ChatTests : TestBase -{ - [Test] - public void HelloWorldChatWithTopLevelClient() - { - ChatClient chatClient = GetTestClient(); - ClientResult chatCompletion = chatClient.CompleteChat([new UserChatMessage("hello, world!")]); - Assert.That(chatCompletion?.Value, Is.Not.Null); - } - - [Test] - public void HelloWorldStreaming() - { - ChatClient chatClient = GetTestClient("gpt-4"); - StringBuilder contentBuilder = new(); - foreach (StreamingChatCompletionUpdate chatUpdate in chatClient.CompleteChatStreaming( - [new UserChatMessage("Hello, assistant"!)])) - { - foreach (ChatMessageContentPart contentPart in chatUpdate.ContentUpdate) - { - contentBuilder.Append(contentPart.Text); - } - } - Assert.That(contentBuilder.ToString(), Is.Not.Null.Or.Empty); - } - - [Test] - public void BadKeyGivesHelpfulError() - { - string endpointFromEnvironment = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); - Uri endpoint = new(endpointFromEnvironment); - string mockKey = "not-a-valid-key-and-should-still-be-sanitized"; - ApiKeyCredential credential = new(mockKey); - AzureOpenAIClient topLevelClient = new(endpoint, credential); - ChatClient chatClient = topLevelClient.GetChatClient("gpt-35-turbo"); - Exception thrownException = null; - try - { - _ = chatClient.CompleteChat([new UserChatMessage("oops, this won't work with that key!")]); - } - catch (Exception ex) - { - thrownException = ex; - } - Assert.That(thrownException, Is.InstanceOf()); - Assert.That(thrownException.Message, Does.Contain("invalid subscription key")); - Assert.That(thrownException.Message, Does.Not.Contain(mockKey)); - } - - [Test] - public void BadKeyGivesHelpfulErrorStreaming() - { - string endpointFromEnvironment = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); - Uri endpoint = new(endpointFromEnvironment); - string mockKey = "not-a-valid-key-and-should-still-be-sanitized"; - ApiKeyCredential credential = new(mockKey); - AzureOpenAIClient topLevelClient = new(endpoint, credential); - ChatClient chatClient = topLevelClient.GetChatClient("gpt-35-turbo"); - Exception thrownException = null; - try - { - foreach (StreamingChatCompletionUpdate update in chatClient.CompleteChatStreaming( - [new UserChatMessage("oops, this won't work with that key!")])) - {} - } - catch (Exception ex) - { - thrownException = ex; - } - Assert.That(thrownException, Is.InstanceOf()); - Assert.That(thrownException.Message, Does.Contain("invalid subscription key")); - Assert.That(thrownException.Message, Does.Not.Contain(mockKey)); - } - - [Test] - public void DefaultAzureCredentialWorks() - { - ChatClient chatClient = GetTestClient(); - ChatCompletion chatCompletion = chatClient.CompleteChat([ChatMessage.CreateUserMessage("Hello, world!")]); - Assert.That(chatCompletion?.Content, Is.Not.Null); - chatCompletion = chatClient.CompleteChat([ChatMessage.CreateUserMessage("Hello again, world!")]); - Assert.That(chatCompletion?.Content, Is.Not.Null); - } - - [Test] - public void CanGetContentFilterResults() - { - ChatClient client = GetTestClient(); - ClientResult chatCompletionResult = client.CompleteChat([ChatMessage.CreateUserMessage("Hello, world!")]); - Console.WriteLine($"--- RESPONSE ---"); - Console.WriteLine(chatCompletionResult.GetRawResponse().Content.ToString()); - ChatCompletion chatCompletion = chatCompletionResult.Value; -#pragma warning disable OPENAI002 - ContentFilterResultForPrompt promptFilterResult = chatCompletion.GetContentFilterResultForPrompt(); - Assert.That(promptFilterResult, Is.Not.Null); - Assert.That(promptFilterResult.Sexual?.Filtered, Is.False); - Assert.That(promptFilterResult.Sexual?.Severity, Is.EqualTo(ContentFilterSeverity.Safe)); - ContentFilterResultForResponse responseFilterResult = chatCompletion.GetContentFilterResultForResponse(); - Assert.That(responseFilterResult, Is.Not.Null); - Assert.That(responseFilterResult.Hate?.Severity, Is.EqualTo(ContentFilterSeverity.Safe)); - Assert.That(responseFilterResult.ProtectedMaterialCode, Is.Null); - } -#pragma warning restore - - [Test] - [Category("Smoke")] - public void DataSourceSerializationWorks() - { - AzureSearchChatDataSource source = new() - { - Endpoint = new Uri("https://some-search-resource.azure.com"), - Authentication = DataSourceAuthentication.FromApiKey("test-api-key"), - IndexName = "index-name-here", - FieldMappings = new() - { - ContentFieldNames = { "hello" }, - TitleFieldName = "hi", - }, - AllowPartialResult = true, - QueryType = DataSourceQueryType.Simple, - OutputContextFlags = DataSourceOutputContextFlags.AllRetrievedDocuments | DataSourceOutputContextFlags.Citations, - VectorizationSource = DataSourceVectorizer.FromEndpoint( - new Uri("https://my-embedding.com"), - DataSourceAuthentication.FromApiKey("embedding-api-key")), - }; - dynamic serialized = ModelReaderWriter.Write(source).ToDynamicFromJson(); - Assert.That(serialized?.type?.ToString(), Is.EqualTo("azure_search")); - Assert.That(serialized?.parameters?.authentication?.type?.ToString(), Is.EqualTo("api_key")); - Assert.That(serialized?.parameters?.authentication?.key?.ToString(), Does.Contain("test")); - Assert.That(serialized?.parameters?.index_name?.ToString(), Is.EqualTo("index-name-here")); - Assert.That(serialized?.parameters?.fields_mapping?.content_fields?[0]?.ToString(), Is.EqualTo("hello")); - Assert.That(serialized?.parameters?.fields_mapping?.title_field?.ToString(), Is.EqualTo("hi")); - Assert.That(bool.TryParse(serialized?.parameters?.allow_partial_result?.ToString(), out bool parsed) && parsed == true); - Assert.That(serialized?.parameters?.query_type?.ToString(), Is.EqualTo("simple")); - Assert.That(serialized?.parameters?.include_contexts?[0]?.ToString(), Is.EqualTo("citations")); - Assert.That(serialized?.parameters?.include_contexts?[1]?.ToString(), Is.EqualTo("all_retrieved_documents")); - Assert.That(serialized?.parameters?.embedding_dependency?.type?.ToString(), Is.EqualTo("endpoint")); - -#pragma warning disable OPENAI002 - ChatCompletionOptions options = new(); - options.AddDataSource(new ElasticsearchChatDataSource() - { - Authentication = DataSourceAuthentication.FromAccessToken("foo-token"), - Endpoint = new Uri("https://my-elasticsearch.com"), - IndexName = "my-index-name", - InScope = true, - }); - - IReadOnlyList sourcesFromOptions = options.GetDataSources(); - Assert.That(sourcesFromOptions, Has.Count.EqualTo(1)); - Assert.That(sourcesFromOptions[0], Is.InstanceOf()); - Assert.That((sourcesFromOptions[0] as ElasticsearchChatDataSource).IndexName, Is.EqualTo("my-index-name")); - - options.AddDataSource(new AzureCosmosDBChatDataSource() - { - Authentication = DataSourceAuthentication.FromApiKey("api-key"), - ContainerName = "my-container-name", - DatabaseName = "my_database_name", - FieldMappings = new() - { - ContentFieldNames = { "hello", "world" }, - }, - IndexName = "my-index-name", - VectorizationSource = DataSourceVectorizer.FromDeploymentName("my-deployment"), - }); - sourcesFromOptions = options.GetDataSources(); - Assert.That(sourcesFromOptions, Has.Count.EqualTo(2)); - Assert.That(sourcesFromOptions[1], Is.InstanceOf()); - } - - [Test] - public void SearchExtensionWorks() - { - string searchEndpoint = Environment.GetEnvironmentVariable("AOAI_SEARCH_ENDPOINT"); - string searchKey = Environment.GetEnvironmentVariable("AOAI_SEARCH_API_KEY"); - string searchIndex = Environment.GetEnvironmentVariable("AOAI_SEARCH_INDEX_NAME"); - - AzureSearchChatDataSource source = new() - { - Endpoint = new Uri(searchEndpoint), - Authentication = DataSourceAuthentication.FromApiKey(searchKey), - IndexName = searchIndex, - AllowPartialResult = true, - QueryType = DataSourceQueryType.Simple, - }; - ChatCompletionOptions options = new(); - options.AddDataSource(source); - - ChatClient client = GetTestClient("gpt-4"); - ClientResult chatCompletionResult = client.CompleteChat( - [new UserChatMessage("What does the term 'PR complete' mean?")], - options); - Console.WriteLine($"--- RESPONSE CONTENT ---"); - Console.WriteLine(chatCompletionResult.GetRawResponse().Content); - ChatCompletion chatCompletion = chatCompletionResult.Value; - AzureChatMessageContext context = chatCompletion.GetAzureMessageContext(); - Assert.That(context?.Intent, Is.Not.Null.Or.Empty); - Assert.That(context?.Citations, Has.Count.GreaterThan(0)); - Assert.That(context.Citations[0].Filepath, Is.Not.Null.Or.Empty); - Assert.That(context.Citations[0].Content, Is.Not.Null.Or.Empty); - Assert.That(context.Citations[0].ChunkId, Is.Not.Null.Or.Empty); - Assert.That(context.Citations[0].Title, Is.Not.Null.Or.Empty); - Assert.That(context.Citations[0].Url, Is.Not.Null.Or.Empty); - } - - [Test] - public void StreamingSearchExtensionWorks() - { - string searchEndpoint = Environment.GetEnvironmentVariable("AOAI_SEARCH_ENDPOINT"); - string searchKey = Environment.GetEnvironmentVariable("AOAI_SEARCH_API_KEY"); - string searchIndex = Environment.GetEnvironmentVariable("AOAI_SEARCH_INDEX_NAME"); - - AzureSearchChatDataSource source = new() - { - Endpoint = new Uri(searchEndpoint), - Authentication = DataSourceAuthentication.FromApiKey(searchKey), - IndexName = searchIndex, - AllowPartialResult = true, - QueryType = DataSourceQueryType.Simple, - }; - ChatCompletionOptions options = new(); - options.AddDataSource(source); - - ChatClient client = GetTestClient("gpt-4"); - - ResultCollection chatUpdates = client.CompleteChatStreaming( - [new UserChatMessage("What does the term 'PR complete' mean?")], - options); - - StringBuilder contentBuilder = new(); - List contexts = []; - - foreach (StreamingChatCompletionUpdate chatUpdate in chatUpdates) - { - AzureChatMessageContext context = chatUpdate.GetAzureMessageContext(); - if (context is not null) - { - contexts.Add(context); - } - foreach (ChatMessageContentPart contentPart in chatUpdate.ContentUpdate) - { - contentBuilder.Append(contentPart.Text); - } - } - - Assert.That(contentBuilder.ToString(), Is.Not.Null.Or.Empty); - Assert.That(contexts, Has.Count.EqualTo(1)); - Assert.That(contexts[0].Intent, Is.Not.Null.Or.Empty); - Assert.That(contexts[0].Citations, Has.Count.GreaterThan(0)); - Assert.That(contexts[0].Citations[0].Content, Is.Not.Null.Or.Empty); - } -} \ No newline at end of file diff --git a/.dotnet.azure/tests/EmbeddingTests.cs b/.dotnet.azure/tests/EmbeddingTests.cs deleted file mode 100644 index 9a2b6a077..000000000 --- a/.dotnet.azure/tests/EmbeddingTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Azure.AI.OpenAI; -using Azure.AI.OpenAI.Embeddings; -using OpenAI.Chat; -using OpenAI.Embeddings; -using System.ClientModel; - -namespace Azure.AI.OpenAI.Tests; - -public class EmbeddingTests : TestBase -{ - [Test] - [Category("Smoke")] - public void CanCreateClient() => Assert.That(GetTestClient(), Is.InstanceOf()); - - [Test] - public void SimpleEmbeddingWithTopLevelClient() - { - EmbeddingClient embeddingClient = GetTestClient(); - ClientResult embeddingResult = embeddingClient.GenerateEmbedding("sample text to embed"); - Assert.That(embeddingResult?.Value?.Vector.Length, Is.GreaterThan(0)); - } -} \ No newline at end of file diff --git a/.dotnet.azure/tests/FileTests.cs b/.dotnet.azure/tests/FileTests.cs deleted file mode 100644 index ba86a2740..000000000 --- a/.dotnet.azure/tests/FileTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using OpenAI.Files; -using System.ClientModel; - -namespace Azure.AI.OpenAI.Tests; - -public class FileTests : TestBase -{ - [Test] - [Category("Smoke")] - public void CanCreateClient() => Assert.That(GetTestClient(), Is.InstanceOf()); - - [Test] - public void CanUploadAndDeleteFiles() - { - FileClient client = GetTestClient(); - OpenAIFileInfo file = client.UploadFile( - BinaryData.FromString("hello, world!"), - "test_file_delete_me.txt", - FileUploadPurpose.Assistants); - Validate(file); - bool deleted = client.DeleteFile(file); - Assert.IsTrue(deleted); - } -} \ No newline at end of file diff --git a/.dotnet.azure/tests/FineTuningTests.cs b/.dotnet.azure/tests/FineTuningTests.cs deleted file mode 100644 index 248e1373a..000000000 --- a/.dotnet.azure/tests/FineTuningTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using OpenAI.FineTuning; - -namespace Azure.AI.OpenAI.Tests; - -public class FineTuningTests : TestBase -{ - [Test] - [Category("Smoke")] - public void CanCreateClient() => Assert.That(GetTestClient(), Is.InstanceOf()); -} \ No newline at end of file diff --git a/.dotnet.azure/tests/ImageTests.cs b/.dotnet.azure/tests/ImageTests.cs deleted file mode 100644 index e603efc61..000000000 --- a/.dotnet.azure/tests/ImageTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using Azure.AI.OpenAI.Images; -using Azure.Core; -using OpenAI.Images; -using System.ClientModel; - -namespace Azure.AI.OpenAI.Tests; - -public class ImageTests : TestBase -{ - [Test] - [Category("Smoke")] - public void CanCreateClient() => Assert.That(GetTestClient(), Is.InstanceOf()); - - [Test] - public void BadKeyGivesHelpfulError() - { - string endpointFromEnvironment = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); - Uri endpoint = new(endpointFromEnvironment); - string mockKey = "not-a-valid-key-and-should-still-be-sanitized"; - ApiKeyCredential credential = new(mockKey); - AzureOpenAIClient topLevelClient = new(endpoint, credential); - ImageClient client = topLevelClient.GetImageClient("dall-e-3"); - Exception thrownException = null; - try - { - _ = client.GenerateImage("a delightful exception message, in contemporary watercolor"); - } - catch (Exception ex) - { - thrownException = ex; - } - Assert.That(thrownException, Is.InstanceOf()); - Assert.That(thrownException.Message, Does.Contain("invalid subscription key")); - Assert.That(thrownException.Message, Does.Not.Contain(mockKey)); - } - - [Test] - public void CanCreateSimpleImage() - { - ImageClient client = GetTestClient(); - GeneratedImage image = client.GenerateImage("a small watermelon", new() - { - Quality = GeneratedImageQuality.Standard, - Size = GeneratedImageSize.W1024xH1024, - User = "test_user", - ResponseFormat = GeneratedImageFormat.Bytes, - }); - Assert.That(image, Is.Not.Null); - Assert.That(image.ImageBytes, Is.Not.Null); - } - - [Test] - public void CanGetContentFilterResults() - { - ImageClient client = GetTestClient(); - ClientResult imageResult = client.GenerateImage("a small watermelon", new() - { - Quality = GeneratedImageQuality.Standard, - Size = GeneratedImageSize.W1024xH1024, - User = "test_user", - ResponseFormat = GeneratedImageFormat.Uri, - }); - GeneratedImage image = imageResult.Value; - Assert.That(image, Is.Not.Null); - Assert.That(image.ImageUri, Is.Not.Null); - Console.WriteLine($"RESPONSE--\n{imageResult.GetRawResponse().Content}"); -#pragma warning disable OPENAI002 - ImageContentFilterResultForPrompt promptResults = image.GetContentFilterResultForPrompt(); - ImageContentFilterResultForResponse responseResults = image.GetContentFilterResultForResponse(); - Assert.That(promptResults?.Sexual?.Severity, Is.EqualTo(ContentFilterSeverity.Safe)); - Assert.That(responseResults?.Sexual?.Severity, Is.EqualTo(ContentFilterSeverity.Safe)); - } -} \ No newline at end of file diff --git a/.dotnet.azure/tests/TestBase.cs b/.dotnet.azure/tests/TestBase.cs deleted file mode 100644 index 55712e559..000000000 --- a/.dotnet.azure/tests/TestBase.cs +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using Azure.Core; -using Azure.Identity; -using OpenAI.Assistants; -using OpenAI.Audio; -using OpenAI.Batch; -using OpenAI.Chat; -using OpenAI.Embeddings; -using OpenAI.Files; -using OpenAI.FineTuning; -using OpenAI.Images; -using OpenAI.Tests; -using OpenAI.VectorStores; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Net; -using System.Security.Cryptography; -using System.Text.Json; - -namespace Azure.AI.OpenAI.Tests; - -public class TestBase -{ - internal TestConfig TestConfig { get; } - - protected TestBase() - { - TestConfig = new TestConfig(); - } - - internal AzureOpenAIClient GetTestTopLevelClient(TestClientOptions options = null) - => GetExplicitTestTopLevelClient(options); - internal AzureOpenAIClient GetTestTopLevelClient(TestClientOptions options = null) - => GetExplicitTestTopLevelClient(options); - private AzureOpenAIClient GetExplicitTestTopLevelClient(TestClientOptions options = null, bool honorParentClient = true) - { - // If the top-level client is being requested on behalf of another client (e.g. a file client for resources to - // use with an assistant client), then we'll ensure we match the configuration of the dependent client to its - // progenitor. - if (honorParentClient && options?.ParentClientObject is not null) - { - return options.ParentClientObject switch - { -#pragma warning disable OPENAI001 - AssistantClient => GetExplicitTestTopLevelClient(options, false), -#pragma warning restore - BatchClient => GetExplicitTestTopLevelClient(options, false), - ChatClient => GetExplicitTestTopLevelClient(options, false), - EmbeddingClient => GetExplicitTestTopLevelClient(options, false), - FileClient => GetExplicitTestTopLevelClient(options, false), - FineTuningClient => GetExplicitTestTopLevelClient(options, false), - ImageClient => GetExplicitTestTopLevelClient(options, false), -#pragma warning disable OPENAI001 - VectorStoreClient => GetExplicitTestTopLevelClient(options, false), -#pragma warning restore - _ => throw new NotImplementedException() - }; - } - - Uri endpoint = TestConfig.GetEndpointFor(); - - ApiKeyCredential apiKeyCredential = typeof(TCredential) == typeof(ApiKeyCredential) - ? TestConfig.GetApiKeyFromEnvironmentFor() - : null; - TokenCredential tokenCredential = typeof(TCredential) == typeof(TokenCredential) - ? new DefaultAzureCredential() - : null; - - options ??= new(); - Action requestAction = options.ShouldOutputRequests ? DumpRequest : null; - Action responseAction = options.ShouldOutputResponses ? DumpResponse : null; - options.AddPolicy(new TestPipelinePolicy(requestAction, responseAction), PipelinePosition.PerCall); - - AzureOpenAIClient client = - typeof(TCredential) == typeof(ApiKeyCredential) - ? new(endpoint, apiKeyCredential, options) - : (typeof(TCredential) == typeof(TokenCredential)) - ? new(endpoint, tokenCredential, options) - : throw new NotImplementedException(); - - return client; - } - - internal TClient GetTestClient(string overrideDeploymentName, TestClientOptions options = null) - => GetExplicitTestClient(overrideDeploymentName, options); - internal TClient GetTestClient(TestClientOptions options = null) - => GetExplicitTestClient(null, options); - internal TClient GetTestClient(TestClientOptions options = null) - => GetExplicitTestClient(null, options); - internal TChildClient GetChildTestClient(TClient parentClient) - => GetExplicitTestClient(null, new() { ParentClientObject = parentClient }); - private TExplicitClient GetExplicitTestClient(string overrideDeploymentName = null, TestClientOptions options = null) - { - AzureOpenAIClient topLevelClient = GetExplicitTestTopLevelClient(options); - string GetDeployment() => overrideDeploymentName ?? TestConfig.GetDeploymentNameFor(); - object clientObject = null; - switch (typeof(TExplicitClient).Name) - { -#pragma warning disable OPENAI001 - case nameof(AssistantClient): - clientObject = topLevelClient.GetAssistantClient(); - break; -#pragma warning restore - case nameof(AudioClient): - clientObject = topLevelClient.GetAudioClient(GetDeployment()); - break; - case nameof(BatchClient): - clientObject = topLevelClient.GetBatchClient(GetDeployment()); - break; - case nameof(ChatClient): - clientObject = topLevelClient.GetChatClient(GetDeployment()); - break; - case nameof(EmbeddingClient): - clientObject = topLevelClient.GetEmbeddingClient(GetDeployment()); - break; - case nameof(FileClient): - clientObject = topLevelClient.GetFileClient(); - break; - case nameof(FineTuningClient): - clientObject = topLevelClient.GetFineTuningClient(); - break; - case nameof(ImageClient): - clientObject = topLevelClient.GetImageClient(GetDeployment()); - break; -#pragma warning disable OPENAI001 - case nameof(VectorStoreClient): - clientObject = topLevelClient.GetVectorStoreClient(); - break; -#pragma warning restore - default: throw new NotImplementedException($"Test client helpers not yet implemented for {typeof(TExplicitClient)}"); - }; - return (TExplicitClient)clientObject; - } - - private static void DumpRequest(PipelineRequest request) - { - Console.WriteLine($"--- New request ---"); - IEnumerable headerPairs = request.Headers? - .Select(header => $"{header.Key}={(header.Key.ToLower().Contains("auth") ? "***" : header.Value)}"); - string headers = string.Join(',', headerPairs); - Console.WriteLine($"Headers: {headers}"); - Console.WriteLine($"{request.Method} URI: {request?.Uri}"); - if (request.Content is not null) - { - using MemoryStream stream = new(); - request.Content.WriteTo(stream, default); - stream.Position = 0; - using StreamReader reader = new(stream); - Console.WriteLine(reader.ReadToEnd()); - } - } - - private static void DumpResponse(PipelineResponse response) - { - Console.WriteLine($"--- Response --- "); - } - - protected void ValidateById(string id) - { - Assert.That(id, Is.Not.Null.Or.Empty); - switch (typeof(T).Name) - { - case nameof(Assistant): _assistantIdsToDelete.Add(id); break; - case nameof(AssistantThread): _threadIdsToDelete.Add(id); break; - case nameof(OpenAIFileInfo): _fileIdsToDelete.Add(id); break; - case nameof(ThreadRun): break; - case nameof(VectorStore): _vectorStoreIdsToDelete.Add(id); break; - default: throw new NotImplementedException(); - } - } - - protected void ValidateById(string id, string parentId) - { - Assert.That(id, Is.Not.Null.Or.Empty); - Assert.That(parentId, Is.Not.Null.Or.Empty); - switch (typeof(T).Name) - { - case nameof(ThreadMessage): - _threadIdsWithMessageIdsToDelete.Add((parentId, id)); - break; - case nameof(VectorStoreFileAssociation): - _vectorStoreFileAssociationsToRemove.Add((parentId, id)); - break; - default: - throw new NotImplementedException(); - } - } - - /// - /// Performs basic, invariant validation of a target that was just instantiated from its corresponding origination - /// mechanism. If applicable, the instance is recorded into the test run for cleanup of persistent resources. - /// - /// Instance type being validated. - /// The instance to validate. - /// The provided instance type isn't supported. - protected void Validate(T target) - { - if (target is ThreadMessage message) - { - ValidateById(message.Id, message.ThreadId); - } - else if (target is VectorStoreFileAssociation fileAssociation) - { - ValidateById(fileAssociation.VectorStoreId, fileAssociation.FileId); - } - else - { - ValidateById(target switch - { - Assistant assistant => assistant.Id, - AssistantThread thread => thread.Id, - OpenAIFileInfo file => file.Id, - ThreadRun run => run.Id, - VectorStore store => store.Id, - _ => throw new NotImplementedException(), - }); - } - } - - [TearDown] - protected void Cleanup() - { - AzureOpenAIClient topLevelCleanupClient = GetTestTopLevelClient(new() - { - ShouldOutputRequests = false, - ShouldOutputResponses = false, - }); -#pragma warning disable OPENAI001 - AssistantClient client = topLevelCleanupClient.GetAssistantClient(); - VectorStoreClient vectorStoreClient = topLevelCleanupClient.GetVectorStoreClient(); -#pragma warning restore - FileClient fileClient = topLevelCleanupClient.GetFileClient(); - RequestOptions requestOptions = new() { ErrorOptions = ClientErrorBehaviors.NoThrow, }; - foreach ((string threadId, string messageId) in _threadIdsWithMessageIdsToDelete) - { - Console.WriteLine($"Cleanup: {messageId} -> {client.DeleteMessage(threadId, messageId, requestOptions)?.GetRawResponse().Status}"); - } - foreach (string assistantId in _assistantIdsToDelete) - { - Console.WriteLine($"Cleanup: {assistantId} -> {client.DeleteAssistant(assistantId, requestOptions)?.GetRawResponse().Status}"); - } - foreach (string threadId in _threadIdsToDelete) - { - Console.WriteLine($"Cleanup: {threadId} -> {client.DeleteThread(threadId, requestOptions)?.GetRawResponse().Status}"); - } - foreach ((string vectorStoreId, string fileId) in _vectorStoreFileAssociationsToRemove) - { - Console.WriteLine($"Cleanup: {vectorStoreId}<->{fileId} => {vectorStoreClient.RemoveFileFromStore(vectorStoreId, fileId, requestOptions)?.GetRawResponse().Status}"); - } - foreach (string vectorStoreId in _vectorStoreIdsToDelete) - { - Console.WriteLine($"Cleanup: {vectorStoreId} => {vectorStoreClient.DeleteVectorStore(vectorStoreId, requestOptions)?.GetRawResponse().Status}"); - } - foreach (string fileId in _fileIdsToDelete) - { - Console.WriteLine($"Cleanup: {fileId} -> {fileClient.DeleteFile(fileId, requestOptions)?.GetRawResponse().Status}"); - } - _threadIdsWithMessageIdsToDelete.Clear(); - _assistantIdsToDelete.Clear(); - _threadIdsToDelete.Clear(); - _vectorStoreFileAssociationsToRemove.Clear(); - _vectorStoreIdsToDelete.Clear(); - _fileIdsToDelete.Clear(); - } - - private readonly List _assistantIdsToDelete = []; - private readonly List _threadIdsToDelete = []; - private readonly List<(string, string)> _threadIdsWithMessageIdsToDelete = []; - private readonly List _fileIdsToDelete = []; - private readonly List<(string, string)> _vectorStoreFileAssociationsToRemove = []; - private readonly List _vectorStoreIdsToDelete = []; -} - -internal class TestClientOptions : AzureOpenAIClientOptions -{ - public bool ShouldOutputRequests { get; init; } = true; - public bool ShouldOutputResponses { get; init; } = true; - public object ParentClientObject { get; init; } -} \ No newline at end of file diff --git a/.dotnet.azure/tests/TestConfig.cs b/.dotnet.azure/tests/TestConfig.cs deleted file mode 100644 index 93ad4d979..000000000 --- a/.dotnet.azure/tests/TestConfig.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using OpenAI.Assistants; -using OpenAI.Audio; -using OpenAI.Batch; -using OpenAI.Chat; -using OpenAI.Embeddings; -using OpenAI.Files; -using OpenAI.FineTuning; -using OpenAI.Images; -using OpenAI.VectorStores; -using System.ClientModel; - -namespace Azure.AI.OpenAI.Tests; - -internal class TestConfig -{ - private readonly dynamic _dynamicConfig; - - public TestConfig() - { - string configPath = Path.Combine(AssetFolderName, AssetFilename); - if (File.Exists(configPath)) - { - using Stream configStream = File.OpenRead(configPath); - BinaryData configData = BinaryData.FromStream(configStream); - _dynamicConfig = configData.ToDynamicFromJson(); - } - } - - public Uri GetEndpointFor() - { - dynamic configNode = GetConfigNode(); - string endpointFromVariable = configNode?.endpoint_name is not null - ? Environment.GetEnvironmentVariable(configNode.endpoint_name) : null; - string rawEndpoint = configNode?.endpoint - ?? endpointFromVariable - ?? throw new KeyNotFoundException($"{typeof(TClient)}: endpoint"); - - return new Uri(rawEndpoint); - } - - public ApiKeyCredential GetApiKeyFromEnvironmentFor() - { - string environmentVariableName = GetConfigNode()?.api_key_name - ?? throw new KeyNotFoundException($"{typeof(TClient)}: api_key_name"); - string rawKeyFromEnvironment = Environment.GetEnvironmentVariable(environmentVariableName) - ?? throw new KeyNotFoundException(environmentVariableName); - return new(rawKeyFromEnvironment); - } - - public string GetDeploymentNameFor() - => GetConfigNode()?.deployment - ?? throw new NotImplementedException(); - - private dynamic GetConfigNode() - { - switch (typeof(TClient).Name) - { -#pragma warning disable OPENAI001 - case nameof(AssistantClient): return _dynamicConfig?.assistants; -#pragma warning restore - case nameof(AudioClient): return _dynamicConfig?.audio; - case nameof(BatchClient): return _dynamicConfig?.batch; - case nameof(ChatClient): return _dynamicConfig?.chat; - case nameof(EmbeddingClient): return _dynamicConfig?.embeddings; - case nameof(FileClient): return _dynamicConfig?.files; - case nameof(FineTuningClient): return _dynamicConfig?.fine_tuning; - case nameof(ImageClient): return _dynamicConfig?.images; -#pragma warning disable OPENAI001 - case nameof(VectorStoreClient): return _dynamicConfig?.vector_stores; -#pragma warning restore - default: throw new NotImplementedException(typeof(TClient).Name); - } - } - - private const string AssetFolderName = "Assets"; - private const string AssetFilename = "test_config.json"; -} \ No newline at end of file diff --git a/.dotnet.azure/tests/TestPipelinePolicy.cs b/.dotnet.azure/tests/TestPipelinePolicy.cs deleted file mode 100644 index cc5ccc395..000000000 --- a/.dotnet.azure/tests/TestPipelinePolicy.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace OpenAI.Tests; - -internal partial class TestPipelinePolicy : PipelinePolicy -{ - private readonly Action _processRequestAction; - private readonly Action _processResponseAction; - - public TestPipelinePolicy(Action requestAction, Action responseAction) - { - _processRequestAction = requestAction; - _processResponseAction = responseAction; - } - - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - InvokeActions(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - InvokeActions(message); - return ProcessNextAsync(message, pipeline, currentIndex); - } - - private void InvokeActions(PipelineMessage message) - { - if (message?.Request is not null) - { - _processRequestAction?.Invoke(message.Request); - } - if (message?.Response is not null) - { - _processResponseAction?.Invoke(message.Response); - } - } -} \ No newline at end of file diff --git a/.dotnet.azure/tests/VectorStoreTests.cs b/.dotnet.azure/tests/VectorStoreTests.cs deleted file mode 100644 index e0a6cfca0..000000000 --- a/.dotnet.azure/tests/VectorStoreTests.cs +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -#nullable disable - -using OpenAI; -using OpenAI.Files; -using OpenAI.VectorStores; -using System.ClientModel; -using System.ClientModel.Primitives; - -namespace Azure.AI.OpenAI.Tests; - -#pragma warning disable OPENAI001 - -public class VectorStoreTests : TestBase -{ - [Test] - [Category("Smoke")] - public void CanCreateClient() - { - AzureOpenAIClient client = new(); - VectorStoreClient vectorStoreClient = client.GetVectorStoreClient(); - Assert.That(vectorStoreClient, Is.Not.Null); - } - - [Test] - public void CanCreateGetAndDeleteVectorStores() - { - VectorStoreClient client = GetTestClient(); - - VectorStore vectorStore = client.CreateVectorStore(); - Validate(vectorStore); - bool deleted = client.DeleteVectorStore(vectorStore); - Assert.That(deleted, Is.True); - - IReadOnlyList testFiles = GetNewTestFiles(5); - - vectorStore = client.CreateVectorStore(new() - { - FileIds = { testFiles[0].Id }, - Name = "test vector store", - ExpirationPolicy = new VectorStoreExpirationPolicy() - { - Anchor = VectorStoreExpirationAnchor.LastActiveAt, - Days = 3, - }, - Metadata = - { - ["test-key"] = "test-value", - }, - }); - Validate(vectorStore); - Assert.Multiple(() => - { - Assert.That(vectorStore.Name, Is.EqualTo("test vector store")); - Assert.That(vectorStore.ExpirationPolicy?.Anchor, Is.EqualTo(VectorStoreExpirationAnchor.LastActiveAt)); - Assert.That(vectorStore.ExpirationPolicy?.Days, Is.EqualTo(3)); - Assert.That(vectorStore.FileCounts.Total, Is.EqualTo(1)); - Assert.That(vectorStore.CreatedAt, Is.GreaterThan(s_2024)); - Assert.That(vectorStore.ExpiresAt, Is.GreaterThan(s_2024)); - Assert.That(vectorStore.Status, Is.EqualTo(VectorStoreStatus.InProgress)); - Assert.That(vectorStore.Metadata?.TryGetValue("test-key", out string metadataValue) == true && metadataValue == "test-value"); - }); - vectorStore = client.GetVectorStore(vectorStore); - Assert.Multiple(() => - { - Assert.That(vectorStore.Name, Is.EqualTo("test vector store")); - Assert.That(vectorStore.ExpirationPolicy?.Anchor, Is.EqualTo(VectorStoreExpirationAnchor.LastActiveAt)); - Assert.That(vectorStore.ExpirationPolicy?.Days, Is.EqualTo(3)); - Assert.That(vectorStore.FileCounts.Total, Is.EqualTo(1)); - Assert.That(vectorStore.CreatedAt, Is.GreaterThan(s_2024)); - Assert.That(vectorStore.ExpiresAt, Is.GreaterThan(s_2024)); - Assert.That(vectorStore.Metadata?.TryGetValue("test-key", out string metadataValue) == true && metadataValue == "test-value"); - }); - - deleted = client.DeleteVectorStore(vectorStore.Id); - Assert.That(deleted, Is.True); - - vectorStore = client.CreateVectorStore(new() - { - FileIds = testFiles.Select(file => file.Id).ToList() - }); - Validate(vectorStore); - Assert.Multiple(() => - { - Assert.That(vectorStore.Name, Is.Null.Or.Empty); - Assert.That(vectorStore.FileCounts.Total, Is.EqualTo(5)); - }); - } - - [Test] - public void CanEnumerateVectorStores() - { - VectorStoreClient client = GetTestClient(); - for (int i = 0; i < 10; i++) - { - VectorStore vectorStore = client.CreateVectorStore(new VectorStoreCreationOptions() - { - Name = $"Test Vector Store {i}", - }); - Validate(vectorStore); - Assert.That(vectorStore.Name, Is.EqualTo($"Test Vector Store {i}")); - } - - int lastIdSeen = int.MaxValue; - int count = 0; - - foreach (VectorStore vectorStore in client.GetVectorStores(ListOrder.NewestFirst)) - { - Assert.That(vectorStore.Id, Is.Not.Null); - if (vectorStore.Name?.StartsWith("Test Vector Store ") == true) - { - string idString = vectorStore.Name["Test Vector Store ".Length..]; - - Assert.That(int.TryParse(idString, out int seenId), Is.True); - Assert.That(seenId, Is.LessThan(lastIdSeen)); - lastIdSeen = seenId; - } - if (lastIdSeen == 0 || ++count >= 100) - { - break; - } - } - - Assert.That(lastIdSeen, Is.EqualTo(0)); - } - - [Test] - public async Task CanEnumerateVectorStoresAsync() - { - VectorStoreClient client = GetTestClient(); - for (int i = 0; i < 10; i++) - { - VectorStore vectorStore = await client.CreateVectorStoreAsync(new VectorStoreCreationOptions() - { - Name = $"Test Vector Store {i}", - }); - Validate(vectorStore); - Assert.That(vectorStore.Name, Is.EqualTo($"Test Vector Store {i}")); - } - - int lastIdSeen = int.MaxValue; - int count = 0; - - await foreach (VectorStore vectorStore in client.GetVectorStoresAsync(ListOrder.NewestFirst)) - { - Assert.That(vectorStore.Id, Is.Not.Null); - if (vectorStore.Name?.StartsWith("Test Vector Store ") == true) - { - string idString = vectorStore.Name["Test Vector Store ".Length..]; - - Assert.That(int.TryParse(idString, out int seenId), Is.True); - Assert.That(seenId, Is.LessThan(lastIdSeen)); - lastIdSeen = seenId; - } - if (lastIdSeen == 0 || ++count >= 100) - { - break; - } - } - - Assert.That(lastIdSeen, Is.EqualTo(0)); - } - - [Test] - public void CanAssociateFiles() - { - VectorStoreClient client = GetTestClient(); - VectorStore vectorStore = client.CreateVectorStore(); - Validate(vectorStore); - - IReadOnlyList files = GetNewTestFiles(3); - - foreach (OpenAIFileInfo file in files) - { - VectorStoreFileAssociation association = client.AddFileToVectorStore(vectorStore, file); - Validate(association); - Assert.Multiple(() => - { - Assert.That(association.FileId, Is.EqualTo(file.Id)); - Assert.That(association.VectorStoreId, Is.EqualTo(vectorStore.Id)); - Assert.That(association.LastError, Is.Null); - Assert.That(association.CreatedAt, Is.GreaterThan(s_2024)); - Assert.That(association.Status, Is.EqualTo(VectorStoreFileAssociationStatus.InProgress)); - }); - } - - bool removed = client.RemoveFileFromStore(vectorStore, files[0]); - Assert.True(removed); - - // Errata: removals aren't immediately reflected when requesting the list - Thread.Sleep(1000); - - int count = 0; - foreach (VectorStoreFileAssociation association in client.GetFileAssociations(vectorStore)) - { - count++; - Assert.That(association.FileId, Is.Not.EqualTo(files[0].Id)); - Assert.That(association.VectorStoreId, Is.EqualTo(vectorStore.Id)); - } - Assert.That(count, Is.EqualTo(2)); - } - - [Test] - public void CanUseBatchIngestion() - { - VectorStoreClient client = GetTestClient(); - VectorStore vectorStore = client.CreateVectorStore(); - Validate(vectorStore); - - IReadOnlyList testFiles = GetNewTestFiles(5); - - VectorStoreBatchFileJob batchJob = client.CreateBatchFileJob(vectorStore, testFiles); - Validate(batchJob); - - Assert.Multiple(() => - { - Assert.That(batchJob.BatchId, Is.Not.Null); - Assert.That(batchJob.VectorStoreId, Is.EqualTo(vectorStore.Id)); - Assert.That(batchJob.Status, Is.EqualTo(VectorStoreBatchFileJobStatus.InProgress)); - }); - - for (int i = 0; i < 10 && client.GetBatchFileJob(batchJob).Value.Status != VectorStoreBatchFileJobStatus.Completed; i++) - { - Thread.Sleep(500); - } - - foreach (VectorStoreFileAssociation association in client.GetFileAssociations(batchJob)) - { - Assert.Multiple(() => - { - Assert.That(association.FileId, Is.Not.Null); - Assert.That(association.VectorStoreId, Is.EqualTo(vectorStore.Id)); - Assert.That(association.Status, Is.EqualTo(VectorStoreFileAssociationStatus.Completed)); - // Assert.That(association.Size, Is.GreaterThan(0)); - Assert.That(association.CreatedAt, Is.GreaterThan(s_2024)); - Assert.That(association.LastError, Is.Null); - }); - } - } - - private IReadOnlyList GetNewTestFiles(int count) - { - AzureOpenAIClient azureClient = GetTestTopLevelClient(new() - { - ShouldOutputRequests = false, - ShouldOutputResponses = false, - }); - FileClient client = azureClient.GetFileClient(); - - List files = []; - for (int i = 0; i < count; i++) - { - OpenAIFileInfo file = client.UploadFile( - BinaryData.FromString("This is a test file").ToStream(), - $"test_file_{i.ToString().PadLeft(3, '0')}.txt", - FileUploadPurpose.Assistants); - Validate(file); - files.Add(file); - } - - return files; - } - - private static readonly DateTimeOffset s_2024 = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); -} \ No newline at end of file diff --git a/.dotnet/CHANGELOG.md b/.dotnet/CHANGELOG.md index 4d7218707..d8777eb6a 100644 --- a/.dotnet/CHANGELOG.md +++ b/.dotnet/CHANGELOG.md @@ -1,37 +1,37 @@ # Release History -## 2.0.0-beta.9 (Unreleased) +## 2.0.0-beta.9 (2024-08-23) ### Features Added -- Added support for the new [structured outputs](https://platform.openai.com/docs/guides/structured-outputs/introduction) response format feature, which enables chat completions, assistants, and tools on each of those clients to provide a specific JSON Schema that generated content should adhere to. +- Added support for the new [structured outputs](https://platform.openai.com/docs/guides/structured-outputs/introduction) response format feature, which enables chat completions, assistants, and tools on each of those clients to provide a specific JSON Schema that generated content should adhere to. ([3467b53](https://github.com/openai/openai-dotnet/commit/3467b535c918e72237a4c0dc36d4bda5548edb7a)) - To enable top-level structured outputs for response content, use `ChatResponseFormat.CreateJsonSchemaFormat()` and `AssistantResponseFormat.CreateJsonSchemaFormat()` as the `ResponseFormat` in method options like `ChatCompletionOptions` - To enable structured outputs for function tools, set `StrictParameterSchemaEnabled` to `true` on the tool definition - For more information, please see [the new section in readme.md](readme.md#how-to-use-structured-outputs) -- Chat completions: the request message types of `AssistantChatMessage`, `SystemChatMessage`, and `ToolChatMessage` now support array-based content part collections in addition to simple string input. +- Chat completions: the request message types of `AssistantChatMessage`, `SystemChatMessage`, and `ToolChatMessage` now support array-based content part collections in addition to simple string input. ([3467b53](https://github.com/openai/openai-dotnet/commit/3467b535c918e72237a4c0dc36d4bda5548edb7a)) - Added the following model factories (static classes that can be used to instantiate OpenAI models for mocking in non-live test scenarios): - - `OpenAIAudioModelFactory` in the `OpenAI.Audio` namespace (commit_hash) - - `OpenAIEmbeddingsModelFactory` in the `OpenAI.Embeddings` namespace (commit_hash) - - `OpenAIFilesModelFactory` in the `OpenAI.Files` namespace (commit_hash) - - `OpenAIImagesModelFactory` in the `OpenAI.Images` namespace (commit_hash) - - `OpenAIModelsModelFactory` in the `OpenAI.Models` namespace (commit_hash) - - `OpenAIModerationsModelFactory` in the `OpenAI.Moderations` namespace (commit_hash) + - `OpenAIAudioModelFactory` in the `OpenAI.Audio` namespace ([3284295](https://github.com/openai/openai-dotnet/commit/3284295e7fd9922a3395d921513473bcb483655e)) + - `OpenAIEmbeddingsModelFactory` in the `OpenAI.Embeddings` namespace ([3284295](https://github.com/openai/openai-dotnet/commit/3284295e7fd9922a3395d921513473bcb483655e)) + - `OpenAIFilesModelFactory` in the `OpenAI.Files` namespace ([b1ce397](https://github.com/openai/openai-dotnet/commit/b1ce397ff4f9a55db797167be9e86e138ed5d403)) + - `OpenAIImagesModelFactory` in the `OpenAI.Images` namespace ([3284295](https://github.com/openai/openai-dotnet/commit/3284295e7fd9922a3395d921513473bcb483655e)) + - `OpenAIModelsModelFactory` in the `OpenAI.Models` namespace ([b1ce397](https://github.com/openai/openai-dotnet/commit/b1ce397ff4f9a55db797167be9e86e138ed5d403)) + - `OpenAIModerationsModelFactory` in the `OpenAI.Moderations` namespace ([b1ce397](https://github.com/openai/openai-dotnet/commit/b1ce397ff4f9a55db797167be9e86e138ed5d403)) ### Breaking Changes -- Removed client constructors that do not explicitly take an API key parameter or an endpoint via an `OpenAIClientOptions` parameter, making it clearer how to appropriately instantiate a client. (commit_hash) -- Removed the endpoint parameter from all client constructors, making it clearer that an alternative endpoint must be specified via the `OpenAIClientOptions` parameter. (commit_hash) -- Removed `OpenAIClient`'s `Endpoint` `protected` property. (commit_hash) -- Made `OpenAIClient`'s constructor that takes a `ClientPipeline` parameter `protected internal` instead of just `protected`. (commit_hash) -- Renamed the `User` property in applicable Options classes to `EndUserId`, making its purpose clearer. (commit_hash) +- Removed client constructors that do not explicitly take an API key parameter or an endpoint via an `OpenAIClientOptions` parameter, making it clearer how to appropriately instantiate a client. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Removed the endpoint parameter from all client constructors, making it clearer that an alternative endpoint must be specified via the `OpenAIClientOptions` parameter. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Removed `OpenAIClient`'s `Endpoint` `protected` property. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Made `OpenAIClient`'s constructor that takes a `ClientPipeline` parameter `protected internal` instead of just `protected`. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) +- Renamed the `User` property in applicable Options classes to `EndUserId`, making its purpose clearer. ([13a9c68](https://github.com/openai/openai-dotnet/commit/13a9c68647c8d54475f1529a63b13ad711bd4ba6)) ### Bugs Fixed -- The `Assistants` namespace `VectorStoreCreationHelper` type now properly includes a `ChunkingStrategy` property. +- The `Assistants` namespace `VectorStoreCreationHelper` type now properly includes a `ChunkingStrategy` property. ([3467b53](https://github.com/openai/openai-dotnet/commit/3467b535c918e72237a4c0dc36d4bda5548edb7a)) ### Other Changes -- `ChatCompletion.ToString()` will no longer throw an exception when no content is present, as is the case for tool calls. Additionally, if a tool call is present with no content, `ToString()` will return the serialized form of the first available tool call. +- `ChatCompletion.ToString()` will no longer throw an exception when no content is present, as is the case for tool calls. Additionally, if a tool call is present with no content, `ToString()` will return the serialized form of the first available tool call. ([3467b53](https://github.com/openai/openai-dotnet/commit/3467b535c918e72237a4c0dc36d4bda5548edb7a)) ## 2.0.0-beta.8 (2024-07-31) diff --git a/.dotnet/src/OpenAI.csproj b/.dotnet/src/OpenAI.csproj index 811fd69a0..ccedc487e 100644 --- a/.dotnet/src/OpenAI.csproj +++ b/.dotnet/src/OpenAI.csproj @@ -5,7 +5,7 @@ OpenAI 2.0.0 - beta.8 + beta.9 netstandard2.0;net6.0 latest