Skip to content

Commit 028d663

Browse files
sethjuarezCopilot
andcommitted
merge: resolve conflict in bindings.test.ts from main
Keep consolidated index.js import (F branch has the correct fix). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents 583ef39 + a70bf7b commit 028d663

4 files changed

Lines changed: 448 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.ClientModel;
4+
using System.ClientModel.Primitives;
5+
using Azure.AI.OpenAI;
6+
using Azure.Identity;
7+
using OpenAI;
8+
using Prompty.Core;
9+
10+
namespace Prompty.Foundry;
11+
12+
/// <summary>
13+
/// Model discovery for Azure OpenAI / Microsoft Foundry endpoints.
14+
/// Creates the appropriate client (API key or Entra ID) and delegates
15+
/// listing and enrichment to <see cref="OpenAI.OpenAIModels"/>.
16+
/// </summary>
17+
public static class FoundryModels
18+
{
19+
private const string DefaultApiVersion = "2024-12-01-preview";
20+
21+
/// <summary>
22+
/// List models available from an Azure OpenAI / Foundry endpoint.
23+
/// </summary>
24+
public static async Task<IReadOnlyList<ModelInfo>> ListModelsAsync(
25+
Connection connection, CancellationToken cancellationToken = default)
26+
{
27+
var client = CreateClient(connection);
28+
return await OpenAI.OpenAIModels.ListModelsAsync(client, cancellationToken);
29+
}
30+
31+
private static OpenAIClient CreateClient(Connection connection)
32+
{
33+
if (connection is ReferenceConnection refConn)
34+
{
35+
var client = ConnectionRegistry.Get(refConn.Name!)
36+
?? throw new InvalidOperationException(
37+
$"Connection '{refConn.Name}' not found in ConnectionRegistry. " +
38+
$"Call ConnectionRegistry.Register(\"{refConn.Name}\", client) first.");
39+
return (OpenAIClient)client;
40+
}
41+
42+
string? endpoint = null;
43+
string? apiKey = null;
44+
45+
if (connection is ApiKeyConnection keyConn)
46+
{
47+
endpoint = keyConn.Endpoint;
48+
apiKey = keyConn.ApiKey;
49+
}
50+
else if (connection is FoundryConnection foundryConn)
51+
{
52+
endpoint = foundryConn.Endpoint;
53+
}
54+
else
55+
{
56+
throw new InvalidOperationException(
57+
$"Connection kind '{connection.Kind}' is not supported for Foundry model listing. " +
58+
"Use 'foundry', 'key', or 'reference'.");
59+
}
60+
61+
if (string.IsNullOrEmpty(endpoint))
62+
throw new InvalidOperationException(
63+
"Azure/Foundry endpoint is required. Set connection.endpoint or ${env:AZURE_OPENAI_ENDPOINT}.");
64+
65+
if (!string.IsNullOrEmpty(apiKey))
66+
return CreateApiKeyClient(endpoint, apiKey);
67+
68+
return CreateEntraIdClient(endpoint);
69+
}
70+
71+
private static OpenAIClient CreateApiKeyClient(string endpoint, string apiKey)
72+
{
73+
// Base endpoint for model listing — no deployment scoping
74+
var baseEndpoint = new Uri($"{endpoint.TrimEnd('/')}/openai");
75+
76+
var clientOptions = new OpenAIClientOptions
77+
{
78+
Endpoint = baseEndpoint,
79+
};
80+
81+
clientOptions.AddPolicy(
82+
new AzureApiVersionPolicy(DefaultApiVersion),
83+
PipelinePosition.BeforeTransport);
84+
85+
clientOptions.AddPolicy(
86+
new AzureApiKeyAuthPolicy(apiKey),
87+
PipelinePosition.BeforeTransport);
88+
89+
return new OpenAIClient(new ApiKeyCredential("azure-api-key"), clientOptions);
90+
}
91+
92+
private static AzureOpenAIClient CreateEntraIdClient(string endpoint)
93+
{
94+
return new AzureOpenAIClient(
95+
new Uri(endpoint.TrimEnd('/')),
96+
new DefaultAzureCredential());
97+
}
98+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.ClientModel;
4+
using OpenAI;
5+
using OpenAI.Models;
6+
using Prompty.Core;
7+
8+
namespace Prompty.OpenAI;
9+
10+
/// <summary>
11+
/// Model discovery for OpenAI — lists available models and enriches
12+
/// sparse API responses with known context window and modality metadata.
13+
/// </summary>
14+
public static class OpenAIModels
15+
{
16+
/// <summary>
17+
/// Known model metadata used to enrich the sparse data returned by GET /v1/models.
18+
/// Keys are sorted by descending length so prefix matching finds the most specific
19+
/// match first (e.g. "gpt-4o-mini" before "gpt-4o" before "gpt-4").
20+
/// </summary>
21+
private static readonly (string Key, int? ContextWindow, string[] Inputs, string[] Outputs)[] KnownModels =
22+
[
23+
("text-embedding-3-large", 8191, ["text"], []),
24+
("text-embedding-3-small", 8191, ["text"], []),
25+
("gpt-3.5-turbo", 16385, ["text"], ["text"]),
26+
("gpt-4o-mini", 128000, ["text", "image"], ["text"]),
27+
("gpt-4-turbo", 128000, ["text", "image"], ["text"]),
28+
("dall-e-3", null, ["text"], ["image"]),
29+
("gpt-4o", 128000, ["text", "image"], ["text"]),
30+
("gpt-4", 8192, ["text"], ["text"]),
31+
];
32+
33+
/// <summary>
34+
/// List models available from an OpenAI endpoint using connection credentials.
35+
/// </summary>
36+
public static async Task<IReadOnlyList<ModelInfo>> ListModelsAsync(
37+
Connection connection, CancellationToken cancellationToken = default)
38+
{
39+
var client = CreateClient(connection);
40+
return await ListModelsAsync(client, cancellationToken);
41+
}
42+
43+
/// <summary>
44+
/// List models using a pre-configured <see cref="OpenAIClient"/>.
45+
/// Useful when the caller already has a client from <see cref="ConnectionRegistry"/>
46+
/// or manual construction.
47+
/// </summary>
48+
public static async Task<IReadOnlyList<ModelInfo>> ListModelsAsync(
49+
OpenAIClient client, CancellationToken cancellationToken = default)
50+
{
51+
var modelClient = client.GetOpenAIModelClient();
52+
var result = await modelClient.GetModelsAsync(cancellationToken);
53+
54+
var models = new List<ModelInfo>();
55+
foreach (var m in result.Value)
56+
{
57+
var info = new ModelInfo
58+
{
59+
Id = m.Id,
60+
OwnedBy = m.OwnedBy,
61+
};
62+
Enrich(info);
63+
models.Add(info);
64+
}
65+
66+
return models.AsReadOnly();
67+
}
68+
69+
/// <summary>
70+
/// Enrich a <see cref="ModelInfo"/> with known context window and modality data.
71+
/// Matches exact IDs first, then falls back to prefix matching for versioned
72+
/// model names (e.g. "gpt-4o-2024-08-06" → "gpt-4o").
73+
/// </summary>
74+
internal static void Enrich(ModelInfo info)
75+
{
76+
foreach (var (key, contextWindow, inputs, outputs) in KnownModels)
77+
{
78+
if (string.Equals(info.Id, key, StringComparison.OrdinalIgnoreCase)
79+
|| info.Id.StartsWith(key + "-", StringComparison.OrdinalIgnoreCase))
80+
{
81+
info.ContextWindow = contextWindow;
82+
info.InputModalities = inputs;
83+
info.OutputModalities = outputs;
84+
return;
85+
}
86+
}
87+
}
88+
89+
private static OpenAIClient CreateClient(Connection connection)
90+
{
91+
if (connection is ReferenceConnection refConn)
92+
{
93+
var client = ConnectionRegistry.Get(refConn.Name!)
94+
?? throw new InvalidOperationException(
95+
$"Connection '{refConn.Name}' not found in ConnectionRegistry. " +
96+
$"Call ConnectionRegistry.Register(\"{refConn.Name}\", client) first.");
97+
return (OpenAIClient)client;
98+
}
99+
100+
if (connection is ApiKeyConnection keyConn)
101+
{
102+
if (string.IsNullOrEmpty(keyConn.ApiKey))
103+
throw new InvalidOperationException(
104+
"OpenAI API key is required. Set connection.apiKey or ${env:OPENAI_API_KEY}.");
105+
106+
if (!string.IsNullOrEmpty(keyConn.Endpoint))
107+
{
108+
var options = new OpenAIClientOptions { Endpoint = new Uri(keyConn.Endpoint) };
109+
return new OpenAIClient(new ApiKeyCredential(keyConn.ApiKey), options);
110+
}
111+
112+
return new OpenAIClient(keyConn.ApiKey);
113+
}
114+
115+
throw new InvalidOperationException(
116+
$"Connection kind '{connection.Kind}' is not supported for model listing. " +
117+
"Use 'key' (with apiKey) or 'reference' (with ConnectionRegistry).");
118+
}
119+
}

runtime/csharp/Prompty.OpenAI/Prompty.OpenAI.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
<PackageIcon>icon.png</PackageIcon>
1919
</PropertyGroup>
2020

21+
<ItemGroup>
22+
<InternalsVisibleTo Include="Prompty.Providers.Tests" />
23+
</ItemGroup>
24+
2125
<ItemGroup>
2226
<None Include="README.md" Pack="true" PackagePath="\" />
2327
<None Include="..\icon.png" Pack="true" PackagePath="\" />

0 commit comments

Comments
 (0)