Read the context window from a provider's model list

This commit is contained in:
Thorsten Sommer committed 2026-09-13 10:28:10 +02:00
1 parent d8a30c4e60
commit 1c03a46e2f
8 files changed
+467 -15

No files matched your search

@@ -1,3 +1,23 @@
using System.Text.Json.Serialization;
namespace AIStudio.Provider.SelfHosted;
public readonly record struct Model(string Id, string? Object, string? OwnedBy, ModelArchitecture? Architecture);
/// <summary>
/// One model as an OpenAI-compatible engine lists it.
/// </summary>
/// <remarks>
/// The context window is vLLM's addition to that route: it reports the window the operator started
/// the engine with, which is the one number no rule about the weights could ever know. Ollama,
/// LM Studio, and llama.cpp answer the same route without it, so it stays unknown there instead of
/// being guessed.
///
/// vLLM calls that field max_model_len, which reads like a limit on the model rather than on a
/// conversation. The wire keeps their spelling, and this record says what the number means, so that
/// nobody has to remember the translation while reading the code that uses it.
/// </remarks>
/// <param name="Id">The model's ID.</param>
/// <param name="Object">What kind of thing the entry is. Known value: "model".</param>
/// <param name="OwnedBy">Who the engine names as the owner of the model.</param>
/// <param name="Architecture">Which kinds of input and output the model takes, where the engine says.</param>
/// <param name="ContextWindowTokens">The context window the engine was started with, in tokens, where it says.</param>
public readonly record struct Model(string Id, string? Object, string? OwnedBy, ModelArchitecture? Architecture, [property: JsonPropertyName("max_model_len")] int? ContextWindowTokens);
@@ -3,6 +3,8 @@ using System.Runtime.CompilerServices;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Models;
using AIStudio.Models.Live;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
@@ -188,8 +190,23 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
return FailedModelLoadResult(this.GetModelLoadFailureReason(lmStudioResponse, responseBody), $"Status={(int)lmStudioResponse.StatusCode} {lmStudioResponse.ReasonPhrase}; Body='{responseBody}'");
}
var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token);
//
// Read with the shared options, the way every other model list of this app is read.
// This one route did without them, which quietly cost it every field an engine spells
// in snake case: owned_by has been arriving as nothing all along, and the next field
// somebody adds here would have gone the same way without anything failing.
//
var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(JSON_SERIALIZER_OPTIONS, token);
var models = lmStudioModelResponse.Data ?? [];
//
// What the engine said about its own models, taken from the whole list rather than
// from what is offered below: a model filtered out here as an embedding model is still
// a model somebody may have configured this instance with, and this list is the only
// place its window is ever stated.
//
ListedModels.Shared.Report(this.ConfiguredProviderId, ListingsOf(models));
return SuccessfulModelLoadResult(models.
Where(model => !string.IsNullOrWhiteSpace(model.Id) &&
!ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) &&
@@ -302,6 +319,27 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
}
}
/// <summary>
/// What an engine stated about the models it serves.
/// </summary>
/// <remarks>
/// A window of zero or less is dropped rather than repaired. An engine answering that way is
/// telling us something we cannot interpret, and falling back to what the rules say about the
/// weights is the one answer nobody has to invent.
/// </remarks>
/// <param name="models">The models exactly as the engine listed them.</param>
/// <returns>One listing per model the engine said something usable about.</returns>
private static IEnumerable<ModelListing> ListingsOf(IEnumerable<Model> models)
{
foreach (var model in models)
{
if (string.IsNullOrWhiteSpace(model.Id) || model.ContextWindowTokens is not > 0)
continue;
yield return new(model.Id, ContextWindow.Of(model.ContextWindowTokens.Value));
}
}
private static bool IsMatchingLlamaCppTextModel(Model model, string[] ignorePhrases, string[] filterPhrases)
{
if (string.IsNullOrWhiteSpace(model.Id))