Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions MultiImageClient/GrokWebClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -72,6 +73,42 @@ public sealed class GrokWebClient : IDisposable
/// "Prompt is too long (invalid_parameter)". Note this differs from the
/// official api.x.ai limit of 4096 — that does not apply here.
public const int MaxPromptChars = 8192;

/// Served-model surfacing (2026-08-08): this transport never pins a
/// model — it sends only enable_pro and grok.com picks the model
/// server-side, so a provider release (e.g. Imagine Image 2.0, shipped
/// 2026-08-07 as the new consumer Quality Mode) changes what serves us
/// with no code change here. Every response reports the serving model
/// as model_name; "imagine-x-1" is the only name this transport had
/// returned before that release. Any other reported name is announced
/// loudly once per process per distinct name so runs visibly say when
/// a new model has arrived. Display/telemetry only — never a gate,
/// never a fallback; generation results are used exactly as returned.
public static readonly IReadOnlyList<string> BaselineServedModelNames = new[] { "imagine-x-1" };

public static bool IsBaselineServedModel(string? modelName)
=> !string.IsNullOrWhiteSpace(modelName)
&& BaselineServedModelNames.Contains(modelName.Trim(), StringComparer.OrdinalIgnoreCase);

private static readonly ConcurrentDictionary<string, byte> AnnouncedServedModels =
new(StringComparer.OrdinalIgnoreCase);

private static void AnnounceServedModel(string? modelName, string? mode)
{
if (string.IsNullOrWhiteSpace(modelName) || IsBaselineServedModel(modelName))
{
return;
}
if (!AnnouncedServedModels.TryAdd(modelName.Trim(), 0))
{
return;
}
Logger.Log(
$"==== GROK-WEB MODEL CHANGE: this generation was served by model '{modelName.Trim()}' (mode '{mode ?? "?"}'), "
+ $"not the previously observed '{string.Join("', '", BaselineServedModelNames)}'. "
+ "grok.com switched what serves this transport server-side "
+ "(xAI released Imagine Image 2.0 as the consumer Quality Mode on 2026-08-07). ====");
}
private static readonly TimeSpan FirstImageEventTimeout = TimeSpan.FromSeconds(60);
private static readonly TimeSpan ImageEventInactivityTimeout = TimeSpan.FromSeconds(60);

Expand Down Expand Up @@ -523,6 +560,7 @@ public async Task<GrokWebImageGenerationResult> GenerateImageAsync(
}).ToList(),
},
metadata: new { requestId, operation = "image-generation" });
AnnounceServedModel(modelName, mode);
return new GrokWebImageGenerationResult
{
Images = images,
Expand Down
8 changes: 7 additions & 1 deletion MultiImageClient/ImageGenerators/GrokWebImagineGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ public async Task<TaskProcessResult> ProcessPromptAsync(IImageGenerator generato
}

_stats.GrokImageGenerationSuccessCount++;
Logger.Log($"\t<- Grok Web Imagine OK in {sw.ElapsedMilliseconds} ms; {result.Images.Count} image(s) model={result.ModelName ?? "?"} mode={result.Mode ?? "?"}");
var newModelFlag = !string.IsNullOrWhiteSpace(result.ModelName)
&& !GrokWebClient.IsBaselineServedModel(result.ModelName)
? " ** NEW MODEL **"
: "";
Logger.Log($"\t<- Grok Web Imagine OK in {sw.ElapsedMilliseconds} ms; {result.Images.Count} image(s) model={result.ModelName ?? "?"} mode={result.Mode ?? "?"}{newModelFlag}");
if (!string.IsNullOrWhiteSpace(result.CaptureDirectory))
{
Logger.Log($"\t capture: {result.CaptureDirectory}");
Expand All @@ -134,6 +138,8 @@ public async Task<TaskProcessResult> ProcessPromptAsync(IImageGenerator generato
ImageGenerator = _apiType,
ImageGeneratorDescription = generator.GetGeneratorSpecPart(),
CreateTotalMs = sw.ElapsedMilliseconds,
ServedModelName = result.ModelName,
ServedModelMode = result.Mode,
};
}
catch (Exception ex)
Expand Down
8 changes: 8 additions & 0 deletions MultiImageClient/Implementation/TaskProcessResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public class TaskProcessResult
public TextGeneratorApiType TextGenerator { get; set; }
public long CreateTotalMs { get; set; } = 0;
public long DownloadTotalMs { get; set; } = 0;

// The model identity the provider REPORTED serving this result with,
// when the transport exposes one (currently grok-web's imagine
// WebSocket model_name/mode). Null elsewhere. Informational only:
// lets runs say at runtime which server-side model produced the
// images (e.g. after xAI's 2026-08-07 Imagine Image 2.0 rollout).
public string ServedModelName { get; set; }
public string ServedModelMode { get; set; }
public string GeneratedMediaPath { get; set; }
public string GeneratedMediaContentType { get; set; }
public string GenerationAttemptId { get; set; } = "";
Expand Down
13 changes: 13 additions & 0 deletions MultiImageClient/Implementation/UiJobs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2801,6 +2801,8 @@ or KeyRecraftV41Vector or KeyRecraftV3 or KeyRecraftV4
long createMs = 0, downloadMs = 0;
string label = null;
string firstError = null;
string servedModel = null;
string servedModelMode = null;

for (var attempt = 0; attempt < want; attempt++)
{
Expand Down Expand Up @@ -2958,6 +2960,8 @@ or KeyRecraftV41Vector or KeyRecraftV3 or KeyRecraftV4
totalCost += costEstimate;
createMs += result.CreateTotalMs;
downloadMs += result.DownloadTotalMs;
servedModel ??= result.ServedModelName;
servedModelMode ??= result.ServedModelMode;
if (label == null)
{
label = copy.RuntimeMeta.TryGetValue("label", out var l) && !string.IsNullOrEmpty(l)
Expand Down Expand Up @@ -3045,6 +3049,15 @@ or KeyRecraftV41Vector or KeyRecraftV3 or KeyRecraftV4
mediaType,
label = label ?? key,
size = actualSize,
// Provider-REPORTED serving model, when the transport exposes
// one (grok-web model_name/mode). servedModelIsNew flags names
// outside the transport's known baseline so the card can say
// "you're on the new model" at runtime (e.g. after xAI's
// 2026-08-07 Imagine Image 2.0 rollout). Display only.
servedModel,
servedModelMode,
servedModelIsNew = servedModel != null
&& !GrokWebClient.IsBaselineServedModel(servedModel),
videoMode = key == KeyGrokWebVideo ? spec.VideoMode : null,
videoDurationSeconds = key == KeyGrokWebVideo
? spec.VideoDurationSeconds
Expand Down
22 changes: 22 additions & 0 deletions MultiImageClient/Ui/wwwroot/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5224,6 +5224,7 @@ function addJobCard(id, prompt, gens, hasImage, createdAtUnixMs, inputCount, opt
<div class="cell-head">
<span class="cell-name"></span>
<span class="cell-size"></span>
<span class="cell-served-model"></span>
<span class="cell-cost"></span>
<span class="cell-time"></span>
</div>
Expand Down Expand Up @@ -5445,6 +5446,27 @@ function applyJobEvent(id, card, evt) {
|| (evt.label && (/ \u00b7 (\d+x\d+)$/.exec(evt.label) || [])[1])
|| "";
cell.querySelector(".cell-size").textContent = sizeText;
// Provider-REPORTED serving model, when the transport exposes one
// (grok-web model_name). Highlighted when the server-side model isn't
// the known baseline — that's how "you're on the new model" surfaces
// at runtime without any client-side pinning.
const servedEl = cell.querySelector(".cell-served-model");
if (servedEl) {
if (evt.servedModel) {
servedEl.textContent = evt.servedModelIsNew
? `${evt.servedModel} — new model!`
: evt.servedModel;
servedEl.classList.toggle("new-model", !!evt.servedModelIsNew);
const modePart = evt.servedModelMode ? ` (mode ${evt.servedModelMode})` : "";
servedEl.title = evt.servedModelIsNew
? `The provider reported serving model "${evt.servedModel}"${modePart} — not the previously observed model on this transport.`
: `The provider reported serving model "${evt.servedModel}"${modePart}.`;
} else {
servedEl.textContent = "";
servedEl.classList.remove("new-model");
servedEl.removeAttribute("title");
}
}
if (evt.label) cell.querySelector(".cell-head").title = evt.label;
cell.dataset.cost = String(evt.cost || 0);
cell.dataset.imgCount = String(evt.images.filter(Boolean).length);
Expand Down
10 changes: 10 additions & 0 deletions MultiImageClient/Ui/wwwroot/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,16 @@ body:not(.show-costs) #cost-summary { display: none !important; }
}
/* Actual returned pixel size; secondary via smaller size, not desaturation. */
.cell-size { font-size: 11px; font-weight: 600; color: var(--accent-dark); white-space: nowrap; }
/* Provider-reported serving model (currently grok-web model_name). The
new-model variant flags a name outside the transport's known baseline:
the server-side model changed under us (e.g. Imagine Image 2.0). */
.cell-served-model { font-size: 10px; font-weight: 700; color: var(--accent-dark); white-space: nowrap; }
.cell-served-model.new-model {
padding: 1px 7px;
color: #fff;
background: var(--accent);
border-radius: 8px;
}
.cell-cost { margin-left: auto; font-size: 12px; color: var(--accent-dark); font-weight: 700; white-space: nowrap; }
.cell-time { font-size: 12px; color: var(--ok); font-weight: 600; }
.cell-time.err { color: var(--err); }
Expand Down