Public Access
Archive summary refinements and profile switching
PR and Push Build/Test / build-and-test (push) Successful in 6m37s
PR and Push Build/Test / build-and-test (push) Successful in 6m37s
This commit is contained in:
@@ -21,6 +21,7 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
private readonly TimeSpan reconnectionDelay;
|
||||
private readonly LiteLlmResponsesCompactionOptions? compactionOptions;
|
||||
private readonly ILogger? logger;
|
||||
private int responsesRequestCount;
|
||||
|
||||
public LiteLlmResponsesChatClient(
|
||||
Uri endpoint,
|
||||
@@ -31,7 +32,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
int reconnectionAttempts,
|
||||
TimeSpan reconnectionDelay,
|
||||
LiteLlmResponsesCompactionOptions? compactionOptions = null,
|
||||
ILogger? logger = null)
|
||||
ILogger? logger = null,
|
||||
bool firstRequestIsUser = true)
|
||||
: this(
|
||||
new HttpClient { BaseAddress = NormalizeEndpoint(endpoint) },
|
||||
apiKey,
|
||||
@@ -41,7 +43,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
reconnectionAttempts,
|
||||
reconnectionDelay,
|
||||
compactionOptions,
|
||||
logger)
|
||||
logger,
|
||||
firstRequestIsUser)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -54,7 +57,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
int reconnectionAttempts,
|
||||
TimeSpan reconnectionDelay,
|
||||
LiteLlmResponsesCompactionOptions? compactionOptions = null,
|
||||
ILogger? logger = null)
|
||||
ILogger? logger = null,
|
||||
bool firstRequestIsUser = true)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
@@ -65,6 +69,7 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
this.reconnectionDelay = reconnectionDelay > TimeSpan.Zero ? reconnectionDelay : TimeSpan.Zero;
|
||||
this.compactionOptions = compactionOptions;
|
||||
this.logger = logger;
|
||||
responsesRequestCount = firstRequestIsUser ? 0 : 1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -81,6 +86,8 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
var responseJson = await PostWithRetryAsync(payload, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = ParseResponseJson(responseJson);
|
||||
LogResponseDiagnostics(responseJson, response);
|
||||
ThrowIfResponseHasNoContent(responseJson, response);
|
||||
LogResponseUsage(response.Usage);
|
||||
return response;
|
||||
}
|
||||
@@ -225,12 +232,11 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
}
|
||||
};
|
||||
|
||||
using var content = new StringContent(request.ToJsonString(JsonOptions), Encoding.UTF8, "application/json");
|
||||
using var response = await httpClient.PostAsync(
|
||||
NormalizeRelativePath(compactionOptions.CompactPath),
|
||||
content,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
using var requestMessage = CreateJsonRequest(
|
||||
NormalizeRelativePath(compactionOptions.CompactPath),
|
||||
request.ToJsonString(JsonOptions),
|
||||
"agent");
|
||||
using var response = await httpClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
||||
var responseJson = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -319,6 +325,11 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.MaxOutputTokens is > 0)
|
||||
{
|
||||
payload["max_output_tokens"] = options.MaxOutputTokens.Value;
|
||||
}
|
||||
|
||||
var tools = CreateTools(options?.Tools);
|
||||
if (tools.Count > 0)
|
||||
{
|
||||
@@ -338,8 +349,13 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
{
|
||||
try
|
||||
{
|
||||
using var content = new StringContent(payloadJson, Encoding.UTF8, "application/json");
|
||||
using var response = await httpClient.PostAsync("responses", content, cancellationToken).ConfigureAwait(false);
|
||||
var initiator = NextInitiator();
|
||||
LogRequestDiagnostics(payload, initiator, attempt);
|
||||
using var request = CreateJsonRequest(
|
||||
"responses",
|
||||
payloadJson,
|
||||
initiator);
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
var responseJson = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
@@ -367,6 +383,28 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
throw lastException ?? new InvalidOperationException("LiteLLM Responses request failed.");
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateJsonRequest(
|
||||
string requestUri,
|
||||
string payloadJson,
|
||||
string? initiator = null)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, requestUri)
|
||||
{
|
||||
Content = new StringContent(payloadJson, Encoding.UTF8, "application/json")
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(initiator))
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation("X-Initiator", initiator);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private string NextInitiator()
|
||||
{
|
||||
return Interlocked.Increment(ref responsesRequestCount) == 1 ? "user" : "agent";
|
||||
}
|
||||
|
||||
private async Task DelayBeforeRetryAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (reconnectionDelay > TimeSpan.Zero)
|
||||
@@ -423,6 +461,110 @@ public sealed class LiteLlmResponsesChatClient : IChatClient
|
||||
contextWindow);
|
||||
}
|
||||
|
||||
private void LogRequestDiagnostics(JsonObject payload, string initiator, int attempt)
|
||||
{
|
||||
if (logger is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var inputCount = payload.TryGetPropertyValue("input", out var input) && input is JsonArray inputArray
|
||||
? inputArray.Count
|
||||
: 0;
|
||||
var tools = payload.TryGetPropertyValue("tools", out var toolsNode) && toolsNode is JsonArray toolsArray
|
||||
? toolsArray
|
||||
: [];
|
||||
var toolNames = tools
|
||||
.OfType<JsonObject>()
|
||||
.Select(tool => tool.TryGetPropertyValue("name", out var name) ? name?.GetValue<string>() : null)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.ToArray();
|
||||
var instructionsPreview = payload.TryGetPropertyValue("instructions", out var instructionsNode)
|
||||
? Truncate(instructionsNode?.GetValue<string>() ?? "", maxLength: 500)
|
||||
: "";
|
||||
|
||||
logger.LogInformation(
|
||||
"LiteLLM Responses request: initiator={Initiator}, attempt={Attempt}, inputItems={InputItems}, tools={ToolCount}, toolNames={ToolNames}, instructionsPreview={InstructionsPreview}",
|
||||
initiator,
|
||||
attempt + 1,
|
||||
inputCount,
|
||||
toolNames.Length,
|
||||
string.Join(", ", toolNames),
|
||||
instructionsPreview);
|
||||
}
|
||||
|
||||
private void LogResponseDiagnostics(string responseJson, ChatResponse response)
|
||||
{
|
||||
if (logger is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var outputTypes = GetOutputItemTypes(responseJson);
|
||||
var parsedTypes = response.Messages
|
||||
.SelectMany(message => message.Contents)
|
||||
.Select(content => content.GetType().Name)
|
||||
.ToArray();
|
||||
var functionCalls = response.Messages
|
||||
.SelectMany(message => message.Contents)
|
||||
.OfType<FunctionCallContent>()
|
||||
.Select(call => call.Name)
|
||||
.ToArray();
|
||||
|
||||
logger.LogInformation(
|
||||
"LiteLLM Responses response: responseId={ResponseId}, outputTypes={OutputTypes}, parsedContents={ParsedContents}, functionCalls={FunctionCalls}, textLength={TextLength}",
|
||||
response.ResponseId,
|
||||
string.Join(", ", outputTypes),
|
||||
string.Join(", ", parsedTypes),
|
||||
string.Join(", ", functionCalls),
|
||||
response.Text?.Length ?? 0);
|
||||
|
||||
if (parsedTypes.Length == 0)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"LiteLLM Responses response contained no parseable message text or function calls. Raw response preview: {ResponsePreview}",
|
||||
Truncate(responseJson, maxLength: 6000));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowIfResponseHasNoContent(string responseJson, ChatResponse response)
|
||||
{
|
||||
if (response.Messages.SelectMany(message => message.Contents).Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var outputTypes = GetOutputItemTypes(responseJson);
|
||||
throw new InvalidOperationException(
|
||||
"LiteLLM Responses returned no parseable message text or function calls. " +
|
||||
$"ResponseId={response.ResponseId ?? "<none>"}, outputTypes=[{string.Join(", ", outputTypes)}].");
|
||||
}
|
||||
|
||||
private static string[] GetOutputItemTypes(string responseJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(responseJson);
|
||||
return document.RootElement.TryGetProperty("output", out var output) && output.ValueKind == JsonValueKind.Array
|
||||
? output
|
||||
.EnumerateArray()
|
||||
.Select(item => GetString(item, "type") ?? item.ValueKind.ToString())
|
||||
.ToArray()
|
||||
: [];
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return ["<invalid-json>"];
|
||||
}
|
||||
}
|
||||
|
||||
private static string Truncate(string value, int maxLength)
|
||||
{
|
||||
return value.Length <= maxLength
|
||||
? value
|
||||
: value[..maxLength] + "...";
|
||||
}
|
||||
|
||||
private static void AddInputItem(JsonArray input, StringBuilder instructions, ChatMessage message)
|
||||
{
|
||||
var role = message.Role.Value;
|
||||
|
||||
Reference in New Issue
Block a user