Skip to content
Merged
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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# imagent-ui

Web console for imported `imagent-bench` reports plus a local Imagent generation
playground.
Product website for Imagent: home page, OpenRouter-backed generation
playground, benchmark leaderboard, whitepaper, and imported `imagent-bench`
reports.

The playground uses OpenRouter with the project-standard image model
`google/gemini-3.1-flash-image` (Gemini 3.1 Flash Image). The UI intentionally
shows only that model so contributors compare agent orchestration against a
fixed underlying image model.

## Gittensor Relationship

Expand Down
35 changes: 22 additions & 13 deletions app/api/openrouter/verify/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { NextResponse } from "next/server";
import { resolvePlaygroundEnvironment } from "@/lib/playground";
import {
IMAGENT_GENERATION_MODEL_ID,
IMAGENT_GENERATION_MODEL_OPTION
} from "@/lib/models";
import { resolvePublicSiteUrl } from "@/lib/site";

type VerifyRequest = {
Expand Down Expand Up @@ -28,7 +32,7 @@ type OpenRouterModel = {
output_modalities?: string[];
modality?: string;
};
pricing?: Record<string, string | number | null | undefined>;
pricing?: Record<string, string | number | null | undefined> | Array<Record<string, string | number | null | undefined>>;
};

type OpenRouterModelsResponse = {
Expand All @@ -40,7 +44,7 @@ type OpenRouterModelsResponse = {
};

const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
const OPENROUTER_IMAGE_MODELS_URL = "https://openrouter.ai/api/v1/models?output_modalities=image&sort=pricing-low-to-high";
const OPENROUTER_IMAGE_MODELS_URL = "https://openrouter.ai/api/v1/images/models";

export async function POST(request: Request) {
const body = await parseJson<VerifyRequest>(request);
Expand Down Expand Up @@ -90,23 +94,16 @@ export async function POST(request: Request) {
{
verified: true,
key: keyPayload.data || null,
models: [],
models: [fixedModelOption()],
usingServerKey,
warning: modelsPayload.error?.message || `OpenRouter model discovery failed with HTTP ${modelsResponse.status}`
},
{ status: 200 }
);
}

const models = (modelsPayload.data || [])
.map((model) => ({
id: String(model.id || ""),
name: String(model.name || model.id || ""),
description: String(model.description || ""),
pricing: pricingLabel(model.pricing || {})
}))
.filter((model) => model.id)
.sort((left, right) => left.name.localeCompare(right.name));
const fixedModel = (modelsPayload.data || []).find((model) => model.id === IMAGENT_GENERATION_MODEL_ID);
const models = [fixedModelOption(fixedModel)];

return NextResponse.json({
verified: true,
Expand All @@ -116,6 +113,15 @@ export async function POST(request: Request) {
});
}

function fixedModelOption(model?: OpenRouterModel) {
return {
id: IMAGENT_GENERATION_MODEL_ID,
name: String(model?.name || IMAGENT_GENERATION_MODEL_OPTION.name),
description: String(model?.description || IMAGENT_GENERATION_MODEL_OPTION.description),
pricing: pricingLabel(model?.pricing) || IMAGENT_GENERATION_MODEL_OPTION.pricing
};
}

async function parseJson<T>(response: Request | Response): Promise<T> {
try {
return (await response.json()) as T;
Expand All @@ -124,7 +130,10 @@ async function parseJson<T>(response: Request | Response): Promise<T> {
}
}

function pricingLabel(pricing: Record<string, string | number | null | undefined>) {
function pricingLabel(pricing: OpenRouterModel["pricing"]) {
if (!pricing || Array.isArray(pricing)) {
return "OpenRouter pricing";
}
const orderedKeys = ["image", "request", "prompt", "completion"];
const parts = orderedKeys
.map((key) => {
Expand Down
5 changes: 2 additions & 3 deletions app/api/playground/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { resolvePublicSiteUrl } from "@/lib/site";
type GenerateRequest = {
prompt?: string;
apiKey?: string;
model?: string;
quality?: string;
background?: string;
};
Expand Down Expand Up @@ -48,8 +47,8 @@ export async function POST(request: Request) {
const body = (await request.json()) as GenerateRequest;
const publicSiteUrl = resolvePublicSiteUrl();
const prompt = String(body.prompt || "").trim();
const model = String(body.model || DEFAULT_GENERATION_MODEL).trim();
const quality = String(body.quality || "low").trim();
const model = DEFAULT_GENERATION_MODEL;
const quality = String(body.quality || "auto").trim();
const background = String(body.background || "auto").trim();
const runtime = await getResolvedPlaygroundRuntime();
const apiKey = String(body.apiKey || (runtime.hasServerApiKey ? process.env.OPENROUTER_API_KEY : "") || "").trim();
Expand Down
6 changes: 4 additions & 2 deletions app/components/AppFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { RadioTower } from "lucide-react";
import type { Route } from "next";

const footerLinks: Array<{ href: Route; label: string }> = [
{ href: "/", label: "Home" },
{ href: "/generation", label: "Generation" },
{ href: "/leaderboard", label: "Leaderboard" }
{ href: "/leaderboard", label: "Leaderboard" },
{ href: "/whitepaper", label: "Whitepaper" }
];

export function AppFooter() {
Expand All @@ -21,7 +23,7 @@ export function AppFooter() {
<footer className="app-footer">
<div className="footer-inner">
<div className="footer-brand-panel">
<Link className="footer-brand" href="/generation">
<Link className="footer-brand" href="/">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/brand/imagent-ai-avatar.jpg" alt="" />
<strong>IMAGENT</strong>
Expand Down
8 changes: 5 additions & 3 deletions app/components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@

import Link from "next/link";
import { usePathname } from "next/navigation";
import { BarChart3, ImageIcon, RadioTower } from "lucide-react";
import { BarChart3, BookOpenText, Home, ImageIcon, RadioTower } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { Route } from "next";

const navItems: Array<{ href: Route; label: string; icon: LucideIcon }> = [
{ href: "/", label: "Home", icon: Home },
{ href: "/generation", label: "Generation", icon: ImageIcon },
{ href: "/leaderboard", label: "Leaderboard", icon: BarChart3 }
{ href: "/leaderboard", label: "Leaderboard", icon: BarChart3 },
{ href: "/whitepaper", label: "Whitepaper", icon: BookOpenText }
];

export function AppHeader() {
const pathname = usePathname();

return (
<header className="app-header">
<Link className="app-brand" href="/generation">
<Link className="app-brand" href="/">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/brand/imagent-ai-avatar.jpg" alt="" />
<strong>IMAGENT</strong>
Expand Down
64 changes: 36 additions & 28 deletions app/components/GenerationChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Check,
ChevronDown,
Download,
FileJson,
KeyRound,
Loader2,
MessageSquarePlus,
Expand All @@ -17,13 +18,18 @@ import {
UserRound,
X
} from "lucide-react";
import {
IMAGENT_GENERATION_MODEL_ID,
IMAGENT_GENERATION_MODEL_OPTION
} from "@/lib/models";

type ChatMessage = {
id: string;
role: "user" | "agent";
content: string;
imageUrl?: string;
imageFileName?: string;
traceUrl?: string;
provider?: string;
agentId?: string;
capability?: string;
Expand Down Expand Up @@ -117,8 +123,8 @@ const LEGACY_VERIFICATION_CACHE_KEY = "imagent.openrouterVerification";

const defaultSettings: PlaygroundSettings = {
apiKey: "",
model: "openai/gpt-image-1-mini",
quality: "low"
model: IMAGENT_GENERATION_MODEL_ID,
quality: "auto"
};

const defaultSavedSettings: SavedPlaygroundSettings = {
Expand All @@ -128,20 +134,12 @@ const defaultSavedSettings: SavedPlaygroundSettings = {

const fallbackModelOptions: OpenRouterModelOption[] = [
{
id: "openai/gpt-image-1-mini",
name: "OpenAI GPT Image 1 Mini",
description: "Default OpenRouter image model",
pricing: "pricing loads after verification"
},
{
id: "black-forest-labs/flux.2-klein-4b",
name: "Black Forest Labs FLUX 2 Klein",
description: "Fallback image model",
...IMAGENT_GENERATION_MODEL_OPTION,
pricing: "pricing loads after verification"
}
];

const qualityOptions = ["low", "medium", "high", "auto"];
const qualityOptions = ["auto", "low", "medium", "high"];

const emptyVerification: VerificationState = {
status: "idle",
Expand Down Expand Up @@ -188,7 +186,7 @@ export function GenerationChat() {
const savedSettings = readJson<Partial<SavedPlaygroundSettings>>(SETTINGS_KEY, defaultSavedSettings);
const initialSettings = {
...defaultSettings,
model: typeof savedSettings.model === "string" && savedSettings.model.trim() ? savedSettings.model : defaultSettings.model,
model: defaultSettings.model,
quality: isQualityOption(savedSettings.quality) ? savedSettings.quality : defaultSettings.quality
};
const initialSessions = savedSessions.length ? savedSessions : [newSession()];
Expand Down Expand Up @@ -261,7 +259,7 @@ export function GenerationChat() {
throw new Error(data.error || `Verification failed with HTTP ${response.status}`);
}

const models = data.models?.length ? data.models : fallbackModelOptions;
const models = fixedModelOptions(data.models);
const message = data.warning || (data.usingServerKey ? "Verified with the server key" : "Verified");
const nextCache = {
cacheKey,
Expand All @@ -281,10 +279,7 @@ export function GenerationChat() {
if ((apiKey && currentApiKey !== apiKey) || (!apiKey && currentApiKey)) {
return current;
}
if (models.some((model) => model.id === current.model)) {
return current;
}
return { ...current, model: models[0].id };
return { ...current, model: IMAGENT_GENERATION_MODEL_ID };
});
} catch (error) {
if (controller.signal.aborted) {
Expand Down Expand Up @@ -348,13 +343,9 @@ export function GenerationChat() {
}

function saveSettings() {
const selectableModels = availableModels.length ? availableModels : fallbackModelOptions;
const selectedModel = selectableModels.some((model) => model.id === draftSettings.model)
? draftSettings.model
: selectableModels[0]?.id || defaultSettings.model;
const nextSettings = {
apiKey: draftSettings.apiKey.trim(),
model: selectedModel,
model: IMAGENT_GENERATION_MODEL_ID,
quality: draftSettings.quality
};
if (!nextSettings.apiKey && !hasServerApiKey) {
Expand All @@ -380,7 +371,7 @@ export function GenerationChat() {
}

function updateComposerModel(model: string) {
setSettings((current) => ({...current, model}));
setSettings((current) => ({...current, model: model === IMAGENT_GENERATION_MODEL_ID ? model : IMAGENT_GENERATION_MODEL_ID}));
setOpenDropdown(null);
}

Expand Down Expand Up @@ -417,7 +408,6 @@ export function GenerationChat() {
body: JSON.stringify({
prompt: userPrompt,
apiKey: settings.apiKey.trim() || undefined,
model: settings.model,
quality: settings.quality
})
});
Expand All @@ -432,6 +422,7 @@ export function GenerationChat() {
content: "Generated with Imagent",
imageUrl: data.imageUrl,
imageFileName: data.imageFileName,
traceUrl: data.traceUrl,
provider: data.provider,
agentId: data.agentId,
capability: data.capability,
Expand Down Expand Up @@ -605,6 +596,12 @@ export function GenerationChat() {
) : null}
{typeof message.latencyMs === "number" ? <span>{message.latencyMs.toFixed(0)} ms</span> : null}
{typeof message.costUsd === "number" ? <span>${message.costUsd.toFixed(6)}</span> : null}
{message.traceUrl ? (
<a className="turn-trace-link" href={message.traceUrl} target="_blank" rel="noreferrer">
<FileJson size={12} />
View trace
</a>
) : null}
</div>
) : null}
</div>
Expand Down Expand Up @@ -745,11 +742,11 @@ export function GenerationChat() {
selectedModel={draftSettings.model}
selectedModelName={selectedDraftModel?.name || labelForModel(draftSettings.model, modelChoices)}
onOpenChange={(open) => setOpenDropdown(open ? "settings-model" : null)}
onSelect={(model) => setDraftSettings({...draftSettings, model})}
onSelect={() => setDraftSettings({...draftSettings, model: IMAGENT_GENERATION_MODEL_ID})}
/>
{canUseVerifiedModels ? (
<small className="field-note">
{selectedDraftModel?.pricing || "pricing unavailable"} · {modelChoices.length} image models loaded.
{selectedDraftModel?.pricing || "pricing unavailable"} · fixed project model.
</small>
) : null}
</div>
Expand Down Expand Up @@ -993,11 +990,22 @@ function titleFromPrompt(prompt: string) {
function labelForModel(model: string, models: OpenRouterModelOption[]) {
const knownModel = models.find((option) => option.id === model);
if (knownModel) {
return knownModel.name.replace("OpenAI ", "").replace("Black Forest Labs ", "");
return knownModel.name.replace("Google ", "");
}
return model.length > 30 ? `${model.slice(0, 30)}...` : model;
}

function fixedModelOptions(models?: OpenRouterModelOption[]) {
const discovered = models?.find((option) => option.id === IMAGENT_GENERATION_MODEL_ID);
return [
{
...fallbackModelOptions[0],
...(discovered || {}),
id: IMAGENT_GENERATION_MODEL_ID
}
];
}

function isUsableVerificationCache(cache: VerificationCache | null, cacheKey: string): cache is VerificationCache {
return Boolean(cache && cache.cacheKey === cacheKey && Array.isArray(cache.models) && cache.models.length > 0);
}
Expand Down
20 changes: 19 additions & 1 deletion app/components/LeaderboardBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function LeaderboardBoard({ entries }: { entries: LeaderboardEntry[] }) {
entry.pullRequest.number === null ? "" : String(entry.pullRequest.number),
entry.benchmarkVersion,
entry.improvement.label,
entry.generationModel,
entry.judgeModel,
entry.runId
]
Expand Down Expand Up @@ -109,6 +110,7 @@ export function LeaderboardBoard({ entries }: { entries: LeaderboardEntry[] }) {
<th>Pull Request</th>
<th>Score</th>
<th>Improvement</th>
<th>Generation Model</th>
<th>Result</th>
<th>Latency</th>
<th>Cost</th>
Expand Down Expand Up @@ -173,6 +175,12 @@ export function LeaderboardBoard({ entries }: { entries: LeaderboardEntry[] }) {
)}
</div>
</td>
<td>
<div className="model-cell">
<strong>{formatModelName(entry.generationModel)}</strong>
<small>{entry.generationModel || "model unavailable"}</small>
</div>
</td>
<td>
<span className={`result-badge ${entry.status}`}>
{entry.status === "pass" ? <CheckCircle2 size={13} /> : <XCircle size={13} />}
Expand All @@ -191,7 +199,7 @@ export function LeaderboardBoard({ entries }: { entries: LeaderboardEntry[] }) {
))}
{visibleEntries.length === 0 ? (
<tr>
<td className="empty-table-cell" colSpan={9}>No benchmark reports match this view.</td>
<td className="empty-table-cell" colSpan={10}>No benchmark reports match this view.</td>
</tr>
) : null}
</tbody>
Expand Down Expand Up @@ -230,3 +238,13 @@ function formatDimension(value: string) {
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, (character) => character.toUpperCase());
}

function formatModelName(value: string | null) {
if (!value) {
return "Unknown";
}
if (value === "google/gemini-3.1-flash-image") {
return "Gemini 3.1 Flash Image";
}
return value.split("/").pop()?.replace(/[-_]+/g, " ") || value;
}
Loading
Loading