86 lines
3.6 KiB
C#
86 lines
3.6 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
using MyAgent.Storage;
|
|
|
|
namespace MyAgent.Agents;
|
|
|
|
public sealed class OpenAiModelService(LocalConfigStore localConfig, ILogger<OpenAiModelService> logger)
|
|
{
|
|
private readonly LocalConfigStore _localConfig = localConfig;
|
|
private readonly ILogger<OpenAiModelService> _logger = logger;
|
|
private readonly HttpClient _http = new();
|
|
|
|
public async Task<OpenAiModelsResponse> ListAsync(string? baseUrl = null, string? apiKey = null, CancellationToken ct = default)
|
|
{
|
|
var cfg = await _localConfig.ReadAsync(ct);
|
|
baseUrl = string.IsNullOrWhiteSpace(baseUrl) ? cfg.OpenAi.BaseUrl : baseUrl;
|
|
apiKey = string.IsNullOrWhiteSpace(apiKey) ? cfg.OpenAi.ApiKey : apiKey;
|
|
|
|
if (string.IsNullOrWhiteSpace(baseUrl))
|
|
return new OpenAiModelsResponse { Error = "Base URL is empty." };
|
|
|
|
baseUrl = baseUrl.TrimEnd('/');
|
|
var url = baseUrl + "/models";
|
|
|
|
try
|
|
{
|
|
using var resp = await SendModelsAsync(url, apiKey, ct);
|
|
var body = await resp.Content.ReadAsStringAsync(ct);
|
|
|
|
// 兼容 CLI Proxy 根地址:根返回 { endpoints: ["GET /v1/models"] },实际 models 在 /v1/models。
|
|
if ((resp.StatusCode == System.Net.HttpStatusCode.NotFound || resp.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed)
|
|
&& !baseUrl.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
using var resp2 = await SendModelsAsync(baseUrl + "/v1/models", apiKey, ct);
|
|
body = await resp2.Content.ReadAsStringAsync(ct);
|
|
if (!resp2.IsSuccessStatusCode)
|
|
return new OpenAiModelsResponse { BaseUrl = baseUrl, Error = $"HTTP {(int)resp2.StatusCode}: {body}" };
|
|
}
|
|
else if (!resp.IsSuccessStatusCode)
|
|
{
|
|
return new OpenAiModelsResponse { BaseUrl = baseUrl, Error = $"HTTP {(int)resp.StatusCode}: {body}" };
|
|
}
|
|
|
|
using var doc = JsonDocument.Parse(body);
|
|
var models = new List<string>();
|
|
if (doc.RootElement.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var item in data.EnumerateArray())
|
|
{
|
|
if (item.TryGetProperty("id", out var id) && id.ValueKind == JsonValueKind.String)
|
|
{
|
|
var value = id.GetString();
|
|
if (!string.IsNullOrWhiteSpace(value)) models.Add(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new OpenAiModelsResponse
|
|
{
|
|
BaseUrl = baseUrl,
|
|
Models = models.Distinct().OrderBy(x => x).ToList()
|
|
};
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to list OpenAI-compatible models");
|
|
return new OpenAiModelsResponse { BaseUrl = baseUrl, Error = ex.Message };
|
|
}
|
|
}
|
|
|
|
private async Task<HttpResponseMessage> SendModelsAsync(string url, string? apiKey, CancellationToken ct)
|
|
{
|
|
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
|
if (!string.IsNullOrWhiteSpace(apiKey))
|
|
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
|
return await _http.SendAsync(req, ct);
|
|
}
|
|
}
|
|
|
|
public sealed class OpenAiModelsResponse
|
|
{
|
|
public string BaseUrl { get; set; } = "";
|
|
public List<string> Models { get; set; } = [];
|
|
public string? Error { get; set; }
|
|
}
|