使用 Yarp.ReverseProxy 动态转发 ai 调用请求至三方服务

对上个方案进行改进,通过提交参数指定不同的服务商 使用 Yarp.ReverseProxy 转发 ai 调用请求至硅基流动

// // iox

namespace GeneralAPI.ApiService.Components.AiServices;

using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authorization;
using Yarp.ReverseProxy.Configuration;
using Yarp.ReverseProxy.Transforms;

public static class AiYarpReverseProxyExtention
{
    private const string DefaultProvider = "silicon";

    // 默认带的 keyvalue 
    private static readonly Dictionary<string, (string Host, string Key)> Providers = new()
    {
        { DefaultProvider, ("https://api.siliconflow.cn", "sk-xxxxx") },
        { "qwen", ("https://dashscope.aliyuncs.com/compatible-mode", "sk-xxxx") }
    };

    /// <summary>
    ///     添加 ai 反射代理服务
    /// </summary>
    public static IServiceCollection AddAiYarpReverseProxy(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddReverseProxy()
            .LoadFromMemory(
                [
                    new RouteConfig
                    {
                        RouteId = "ai_route", // 路由ID
                        ClusterId = "ai_cluster", // 集群ID
                        AuthorizationPolicy = "AiAccessPolicy", // 指定访问策略
                        Match = new RouteMatch { Path = "/api/ai/v1/{**remainder}", }, // 匹配路径, 将链接参数配置到变量 remainder 中
                    },
                ],
                [
                    new ClusterConfig
                    {
                        ClusterId = "ai_cluster", // 集群ID
                        Destinations = new Dictionary<string, DestinationConfig>
                        {
                            { "default", new DestinationConfig { Address = Providers[DefaultProvider].Host, } }, // 目的地
                        },
                    },
                ])
            .AddTransforms(builderContext =>
            {
                // builderContext.AddPathRouteValues("/v1/{remainder}"); // 路径重写 /api/ai/v1/chat -> /v1/chat, 如果有多级目录, / 会被转义

                // 核心:替换 API Key
                builderContext.AddRequestTransform(async transformContext =>
                {
                    // 1. 获取客户端传来的参数 (例如:X-AI-Provider: kimi)
                    var providerKey = transformContext.HttpContext.Request.Headers["X-AI-Provider"].ToString().ToLower();
                    if (!Providers.TryGetValue(providerKey, out var config))
                    {
                        config = Providers[DefaultProvider]; // 默认硅基流动
                    }

                    var remainder = transformContext.HttpContext.GetRouteValue("remainder")?.ToString();

                    if (!string.IsNullOrEmpty(remainder))
                    {
                        // 重新手动构建 RequestUri,避免 YARP 的默认转义逻辑
                        // 这样得到的路径就是原始的 "audio/transcriptions"
                        var targetUrl = $"{config.Host.TrimEnd('/')}/v1/{remainder}";
                        transformContext.ProxyRequest.RequestUri = new Uri(targetUrl);
                    }

                    // 替换认证信息
                    transformContext.ProxyRequest.Headers.Remove("Authorization");
                    transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", config.Key);

                    // 保持 Host 为目标地址,否则某些 CDN 会拒绝请求
                    transformContext.ProxyRequest.Headers.Host = new Uri(config.Host).Host;

                    await ValueTask.CompletedTask;
                });
            });

        return services;
    }

    /// <summary>
    ///     加入 ai 的反射代理认证
    /// </summary>
    /// <param name="options"></param>
    /// <param name="scheme"></param>
    public static void AddAiYarpReverseProxyAuthorizationOptions(this AuthorizationOptions options, string scheme)
    {
        options.AddPolicy("AiAccessPolicy", policy =>
        {
            // # "JWT_Selector"
            policy.AddAuthenticationSchemes(scheme); // 关键:指定使用你配置的动态选择方案 
            policy.RequireAuthenticatedUser(); // 要求必须通过 Authelia 或 Introspection 认证
            // 如果有组权限要求,可以加上:
            // policy.RequireRole("ai-users"); 
        });
    }
}
上一篇