Fixed GPT-6 Sol and Luna, and the system prompt role of newer OpenAI models (#1010)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-09-27 19:13:54 +02:00
1 parent 1eaca9b12f
commit 8dbe459a53
27 files changed
+739 -355

No files matched your search

@@ -92,6 +92,15 @@ public readonly record struct ModelProfile
/// </summary>
public ImageLimits Images { get; init; }
/// <summary>
/// Which role the system prompt is sent in.
/// </summary>
/// <remarks>
/// Stated by the rules of OpenAI's own models alone, and unknown for everything else: the other
/// providers have one way to send a system prompt, and the provider knows it without asking.
/// </remarks>
public SystemPromptRole SystemPromptRole { get; init; }
/// <summary>
/// Whether the model has every one of the given capabilities.
/// </summary>
@@ -55,6 +55,11 @@ public sealed record ModelProfileChange
/// </summary>
public ImageLimits? Images { get; init; }
/// <summary>
/// The role the system prompt is sent in, or null to leave it as it was.
/// </summary>
public SystemPromptRole? SystemPromptRole { get; init; }
/// <summary>
/// Applies this change to a profile.
/// </summary>
@@ -80,5 +85,6 @@ public sealed record ModelProfileChange
Context = this.Context ?? profile.Context,
Tokenizer = this.Tokenizer ?? profile.Tokenizer,
Images = this.Images ?? profile.Images,
SystemPromptRole = this.SystemPromptRole ?? profile.SystemPromptRole,
};
}
@@ -33,6 +33,7 @@ public sealed class ModelRuleBuilder(string patternText, ModelRuleKind ruleKind,
private ContextWindow? context;
private TokenizerRef? tokenizer;
private ImageLimits? images;
private SystemPromptRole? systemPromptRole;
/// <summary>
/// The text this rule answers for, before anything was stated about it.
@@ -226,6 +227,17 @@ public sealed class ModelRuleBuilder(string patternText, ModelRuleKind ruleKind,
return this;
}
/// <summary>
/// Which role the model takes its system prompt in.
/// </summary>
/// <param name="role">The role.</param>
/// <returns>The rule, to go on stating.</returns>
public ModelRuleBuilder SystemPromptRole(SystemPromptRole role)
{
this.systemPromptRole = role;
return this;
}
/// <summary>
/// Takes everything the rule stated before this one and goes on from there.
/// </summary>
@@ -342,6 +354,7 @@ public sealed class ModelRuleBuilder(string patternText, ModelRuleKind ruleKind,
Context = this.context ?? basis?.Context,
Tokenizer = this.tokenizer ?? basis?.Tokenizer,
Images = this.images ?? basis?.Images,
SystemPromptRole = this.systemPromptRole ?? basis?.SystemPromptRole,
};
private ModelRuleBuilder MatchingAs(MatchKind kind)
@@ -25,7 +25,8 @@ public sealed class Gpt35Family : ModelFamily
builder.Rule("gpt-3.5").AsPrefix()
.Capabilities(TEXT_INPUT | TEXT_OUTPUT)
.Apis(CHAT_COMPLETION_API)
.Tokenizer(TokenizerKind.TIKTOKEN, "cl100k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "cl100k_base")
.SystemPromptRole(SystemPromptRole.SYSTEM);
//
// The odd one out, and kept odd on purpose: the previous rules put this one model on the
@@ -36,6 +37,7 @@ public sealed class Gpt35Family : ModelFamily
builder.Rule("gpt-3.5-turbo").AsExact()
.Capabilities(TEXT_INPUT | TEXT_OUTPUT)
.Apis(RESPONSES_API)
.Tokenizer(TokenizerKind.TIKTOKEN, "cl100k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "cl100k_base")
.SystemPromptRole(SystemPromptRole.SYSTEM);
}
}
@@ -31,7 +31,8 @@ public sealed class Gpt4Family : ModelFamily
.Capabilities(TEXT_INPUT | TEXT_OUTPUT)
.Apis(RESPONSES_API)
.ContextWindow(8_192)
.Tokenizer(TokenizerKind.TIKTOKEN, "cl100k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "cl100k_base")
.SystemPromptRole(SystemPromptRole.SYSTEM);
builder.Rule("gpt-4-turbo").AsPrefix().Inherits()
.Capabilities(MULTIPLE_IMAGE_INPUT | FUNCTION_CALLING)
@@ -33,7 +33,8 @@ public sealed class Gpt4oFamily : ModelFamily
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING | WEB_SEARCH)
.Apis(RESPONSES_API)
.ContextWindow(128_000)
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base")
.SystemPromptRole(SystemPromptRole.DEVELOPER);
//
// The search previews are the same generation and almost nothing like it: they search the
@@ -45,12 +46,14 @@ public sealed class Gpt4oFamily : ModelFamily
.Capabilities(TEXT_INPUT | TEXT_OUTPUT | WEB_SEARCH)
.Apis(CHAT_COMPLETION_API)
.ContextWindow(128_000)
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base")
.SystemPromptRole(SystemPromptRole.DEVELOPER);
builder.Rule("gpt-4o-mini-search-preview").AsExact()
.Capabilities(TEXT_INPUT | TEXT_OUTPUT | WEB_SEARCH)
.Apis(CHAT_COMPLETION_API)
.ContextWindow(128_000)
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base")
.SystemPromptRole(SystemPromptRole.DEVELOPER);
}
}
@@ -45,7 +45,8 @@ public sealed class Gpt5Family : ModelFamily
.Apis(RESPONSES_API)
.Reasoning(ReasoningSupport.ALWAYS)
.ContextWindow(400_000)
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base")
.SystemPromptRole(SystemPromptRole.DEVELOPER);
//
// The alias for the model of this generation which does not reason. The previous rules had
@@ -1,27 +0,0 @@
using static AIStudio.Provider.Capability;
namespace AIStudio.Models.OpenAI;
/// <summary>
/// GPT-6 Astra.
/// </summary>
/// <remarks>
/// Unlike the 5.5 and 5.6 models it reasons on every request: the effort reaches from low to max,
/// and there is no setting which switches thinking off.
/// </remarks>
public sealed class Gpt6AstraFamily : ModelFamily
{
/// <inheritdoc />
public override ModelVendor Vendor => ModelVendor.OPEN_AI;
/// <inheritdoc />
public override ModelSource Source => new("https://developers.openai.com/api/docs/models", new DateOnly(2026, 9, 12), "Capabilities ported unchanged from ProviderExtensions.OpenAI.cs: reasons on every request, both APIs. The models page states the window as 1.05M tokens.");
/// <inheritdoc />
protected override void Declare(ModelFamilyBuilder builder) =>
builder.Rule("gpt-6-astra").AsPrefix()
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING | WEB_SEARCH)
.Apis(RESPONSES_API | CHAT_COMPLETION_API)
.Reasoning(ReasoningSupport.ALWAYS)
.ContextWindow(1_050_000);
}
@@ -0,0 +1,52 @@
using static AIStudio.Provider.Capability;
namespace AIStudio.Models.OpenAI;
/// <summary>
/// The whole GPT-6 line: Astra, Sol, and Luna.
/// </summary>
/// <remarks>
/// One family for the generation rather than one per model, because the three differ in a single
/// sentence: Astra reasons on every request -- its effort reaches from low to max and nothing
/// switches thinking off -- while Sol and Luna reason unless the effort is set to none. Everything
/// else, the window included, they share.
///
/// The rule for the generation is also what answers for a GPT-6 model OpenAI releases after this
/// was written. That is on purpose: this family used to know Astra alone, and Sol and Luna went to
/// the global assumption, which sent them to the chat completion API without images, web search,
/// or reasoning.
///
/// Sol's page adds that calling functions through the chat completion API needs the effort set to
/// none. That concerns only the gateways which reach these models through that API; OpenAI itself
/// is asked through the Responses API.
/// </remarks>
public sealed class Gpt6Family : ModelFamily
{
/// <inheritdoc />
public override ModelVendor Vendor => ModelVendor.OPEN_AI;
/// <inheritdoc />
public override ModelSource Source => new("https://developers.openai.com/api/docs/models", new DateOnly(2026, 9, 27), "The models page lists gpt-6-astra, gpt-6-sol, and gpt-6-luna as the GPT-6 line.");
/// <inheritdoc />
public override IReadOnlyList<ModelSource> FurtherSources =>
[
new("https://developers.openai.com/api/docs/models/gpt-6-astra", new DateOnly(2026, 9, 27), "Text and image input, text output, both APIs, function calling, and web search. A window of 1,050,000 tokens. The effort reaches from low to max, with no way to turn reasoning off."),
new("https://developers.openai.com/api/docs/models/gpt-6-sol", new DateOnly(2026, 9, 27), "The same abilities and window as Astra. The effort reaches from none to max and defaults to medium."),
new("https://developers.openai.com/api/docs/models/gpt-6-luna", new DateOnly(2026, 9, 27), "The same abilities and window as Astra. The effort reaches from none to max and defaults to medium.")
];
/// <inheritdoc />
protected override void Declare(ModelFamilyBuilder builder)
{
builder.Rule("gpt-6").AsPrefix()
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING | WEB_SEARCH)
.Apis(RESPONSES_API | CHAT_COMPLETION_API)
.Reasoning(ReasoningSupport.ON_BY_DEFAULT)
.ContextWindow(1_050_000)
.SystemPromptRole(SystemPromptRole.DEVELOPER);
builder.Rule("gpt-6-astra").AsPrefix().InheritsFrom("gpt-6")
.Reasoning(ReasoningSupport.ALWAYS);
}
}
@@ -0,0 +1,45 @@
using AIStudio.Provider;
using static AIStudio.Provider.Capability;
namespace AIStudio.Models.OpenAI;
/// <summary>
/// Every GPT model at OpenAI which no rule of its generation knows yet.
/// </summary>
/// <remarks>
/// OpenAI releases faster than anybody writes rules, and somebody always reaches a new model on the
/// day it appears. Unlike the global assumption, which has to stand for a hundred thousand models
/// of every kind, this one stands for a single line in its vendor's own cloud, and that line is
/// uniform: every GPT chat model OpenAI lists calls tools, looks at images, searches the web,
/// reasons, and answers through the Responses API. Left to the global assumption instead, a new
/// model went to the chat completion API without any of that, and a Responses API parameter the
/// person had set was turned down -- which is what happened to GPT-6 Sol and Luna.
///
/// How it reasons is the one guess in here, and on by default is the guess which can be taken back:
/// the person switches it off in the expert settings, whereas a model stated to always reason
/// could not be told otherwise.
///
/// The rule is written as a name part on purpose. Specificity weighs how a pattern matches before
/// it weighs how long it is, so a prefix rule for "gpt" would outrank the name part "gpt-oss" and
/// answer for the open weights wherever they are served. As a name part, every longer rule beats
/// it. Binding it to OpenAI keeps it away from the other models named that way, such as a
/// self-hosted gpt-j. What the model is made for stays with the modifiers: an image, realtime, or
/// transcription model is recognized by its name as before.
/// </remarks>
public sealed class GptBaselineFamily : ModelFamily
{
/// <inheritdoc />
public override ModelVendor Vendor => ModelVendor.OPEN_AI;
/// <inheritdoc />
public override ModelSource Source => new("https://developers.openai.com/api/docs/models", new DateOnly(2026, 9, 27), "Every GPT chat model the page lists supports function calling, image input, web search, reasoning, and the Responses API. The models which are no chat models carry image, realtime, live, transcribe, or tts in their names.");
/// <inheritdoc />
protected override void Declare(ModelFamilyBuilder builder) =>
builder.Rule("gpt").AsSegment().OnlyOn(LLMProviders.OPEN_AI)
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING | WEB_SEARCH)
.Apis(RESPONSES_API)
.Reasoning(ReasoningSupport.ON_BY_DEFAULT)
.SystemPromptRole(SystemPromptRole.DEVELOPER);
}
@@ -36,26 +36,38 @@ public sealed class OSeriesFamily : ModelFamily
.Apis(RESPONSES_API)
.Reasoning(ReasoningSupport.ALWAYS)
.ContextWindow(200_000)
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base")
.SystemPromptRole(SystemPromptRole.DEVELOPER);
//
// The first mini and the preview came out before OpenAI let a reasoning model take
// instructions of its own, so whatever the system prompt says has to reach them as a
// message of the person.
//
builder.Rule("o1-mini").AsPrefix()
.Capabilities(TEXT_INPUT | TEXT_OUTPUT)
.Apis(CHAT_COMPLETION_API)
.Reasoning(ReasoningSupport.ALWAYS)
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base")
.SystemPromptRole(SystemPromptRole.USER);
builder.Rule("o1-preview").AsPrefix().InheritsFrom("o1")
.SystemPromptRole(SystemPromptRole.USER);
builder.Rule("o3").AsPrefix()
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING | WEB_SEARCH)
.Apis(RESPONSES_API)
.Reasoning(ReasoningSupport.ALWAYS)
.ContextWindow(200_000)
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base")
.SystemPromptRole(SystemPromptRole.DEVELOPER);
builder.Rule("o3-mini").AsPrefix()
.Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING)
.Apis(RESPONSES_API)
.Reasoning(ReasoningSupport.ALWAYS)
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base");
.Tokenizer(TokenizerKind.TIKTOKEN, "o200k_base")
.SystemPromptRole(SystemPromptRole.DEVELOPER);
// The one mini which is not cut down: it is the o3 generation under another number.
builder.Rule("o4-mini").AsPrefix().InheritsFrom("o3");
@@ -0,0 +1,36 @@
namespace AIStudio.Models;
/// <summary>
/// Which role the system prompt is sent in.
/// </summary>
/// <remarks>
/// A question only OpenAI's own cloud asks. Its models took their instructions as "system" first,
/// then as "developer", and the early reasoning models took none at all, so that the instructions
/// had to travel as a message of the person instead. Every other provider has one way to send a
/// system prompt, and for their models the rules say nothing about this.
///
/// That the vendor's name is not in the members is on purpose: they say what the model accepts,
/// and the provider decides how to spell that in its request.
/// </remarks>
public enum SystemPromptRole
{
/// <summary>
/// No rule says anything about it, so the provider decides.
/// </summary>
UNKNOWN,
/// <summary>
/// The model takes its instructions in the system role.
/// </summary>
SYSTEM,
/// <summary>
/// The model takes its instructions in the developer role.
/// </summary>
DEVELOPER,
/// <summary>
/// The model takes no instructions of its own, so they go as a message of the person.
/// </summary>
USER,
}