-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathProviderPluginBase.cs
More file actions
111 lines (97 loc) · 4.63 KB
/
ProviderPluginBase.cs
File metadata and controls
111 lines (97 loc) · 4.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// -----------------------------------------------------------------------
// <copyright file="ProviderPluginBase.cs" company="Petabridge, LLC">
// Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com>
// </copyright>
// -----------------------------------------------------------------------
using System.Net.Sockets;
using Microsoft.Extensions.AI;
using Netclaw.Configuration;
using Netclaw.Configuration.Providers;
namespace Netclaw.Providers;
/// <summary>
/// Base class for provider plugins that eliminates IProviderDescriptor
/// delegation boilerplate. Each plugin only needs to implement
/// <see cref="CreateChatClient"/>.
/// </summary>
public abstract class ProviderPluginBase<TDescriptor> : ILlmProviderPlugin
where TDescriptor : IProviderDescriptor
{
protected TDescriptor Descriptor { get; }
protected ProviderPluginBase(TDescriptor descriptor)
{
Descriptor = descriptor;
}
public string TypeKey => Descriptor.TypeKey;
public string DisplayName => Descriptor.DisplayName;
public string DefaultEndpoint => Descriptor.DefaultEndpoint;
public string ModelListingPath => Descriptor.ModelListingPath;
public IProviderAuth Auth => Descriptor.Auth;
public Task<ProviderProbeResult> ProbeAsync(ProviderEntry entry, CancellationToken ct = default)
=> Descriptor.ProbeAsync(entry, ct);
public abstract IChatClient CreateChatClient(ProviderEntry entry, ModelReference model);
public virtual IVendorOptionsSource? CreateVendorOptionsSource(ProviderEntry entry) => null;
/// <summary>
/// Creates an <see cref="HttpClient"/> with a generous timeout suitable for LLM calls.
/// The default <see cref="HttpClient.Timeout"/> of 100 seconds is far too short for
/// large-context models — prefill alone can exceed 100 seconds on self-hosted hardware.
/// Session-level timeouts (FirstTokenTimeout via ProcessingWatchdog)
/// are the authoritative timeout layer; the HttpClient timeout is a last-resort safety
/// net that should never fire before the watchdog.
/// </summary>
protected static HttpClient CreateLlmHttpClient(Uri? baseAddress = null)
{
var socketHandler = new SocketsHttpHandler
{
ConnectTimeout = TimeSpan.FromSeconds(10),
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(60),
// HTTP/2 PING for cloud providers that negotiate h2 over TLS.
// No-op for HTTP/1.1 connections (self-hosted backends).
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
KeepAlivePingDelay = TimeSpan.FromSeconds(15),
KeepAlivePingTimeout = TimeSpan.FromSeconds(5),
// TCP keepalive detects dead/half-open peers at the OS level.
// Probes peer liveness independently of data flow — a slow-but-alive
// prefill answers probes; a dead/restarted backend does not.
ConnectCallback = async (context, ct) =>
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
{
NoDelay = true
};
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 10);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 5);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 3);
try
{
await socket.ConnectAsync(context.DnsEndPoint, ct);
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
};
return new HttpClient(new SessionAffinityHandler(socketHandler))
{
BaseAddress = baseAddress,
Timeout = TimeSpan.FromHours(1)
};
}
/// <summary>
/// Resolves the API key or OAuth token from a provider entry.
/// Throws if neither is available.
/// </summary>
protected static string GetRequiredApiKey(ProviderEntry provider, string providerType)
{
if (!provider.ApiKey.IsNullOrEmpty())
return provider.ApiKey.Value;
if (!provider.OAuthAccessToken.IsNullOrEmpty())
return provider.OAuthAccessToken.Value;
throw new InvalidOperationException(
$"Provider type '{providerType}' requires authentication. "
+ "Configure ApiKey or OAuthAccessToken in secrets.json.");
}
}