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
29 changes: 29 additions & 0 deletions packages/app/src/components/GlobalFilterContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
Sequence,
SEQUENCE_OPTIONS,
} from '@/lib/data-mappings';
import { computeAutoSwitchDecision } from '@/lib/unofficial-run-auto-switch';
import type { AvailabilityRow, WorkflowInfoResponse } from '@/lib/api';

interface RunInfo {
Expand Down Expand Up @@ -172,6 +173,34 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) {
});
}, [availabilityRows, unofficialAvailable]);

// Auto-switch the selected model when an unofficial run is loaded that
// doesn't include the currently selected model. Without this, navigating
// to `?unofficialrun=<id>` while the default `g_model=DeepSeek-R1` sticks
// leaves the user staring at a chart with no overlay points — they'd have
// to know to open the dropdown and pick the run's model themselves.
//
// Precedence on first load: the `if (urlModel)` early-bail in
// `computeAutoSwitchDecision` is the primary guard for explicit `g_model`
// intent. The dedupe ref is a secondary guard for the narrow window after
// an auto-switch fires but before the URL-sync effect (below) writes
// `g_model` back to the URL — once that runs, `urlModel` is set on every
// subsequent render and the ref check is effectively redundant. The ref
// still matters across navigations between unofficial runs because it is
// reset whenever the overlay set goes empty.
const lastAutoSwitchKeyRef = useRef<string>('');
useEffect(() => {
const decision = computeAutoSwitchDecision(
unofficialAvailable,
getUrlParam('g_model'),
selectedModel,
lastAutoSwitchKeyRef.current,
);
lastAutoSwitchKeyRef.current = decision.nextKey;
if (decision.modelToSet !== null) {
setSelectedModel(decision.modelToSet);
}
}, [unofficialAvailable, selectedModel]);

// Sequences available for the selected model (DB ∪ unofficial run for this model)
const availableSequences = useMemo(() => {
const unofficialSeqs = unofficialAvailable
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/unofficial-run-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type UnofficialChartData = Record<

const UNOFFICIAL_RUN_PARAM_RE = /^unofficialruns?$/i;

interface AvailableModelSequence {
export interface AvailableModelSequence {
model: Model;
sequence: Sequence;
precisions: string[];
Expand Down
114 changes: 114 additions & 0 deletions packages/app/src/lib/unofficial-run-auto-switch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { describe, expect, it } from 'vitest';

import type { AvailableModelSequence } from '@/components/unofficial-run-provider';
import { Model, Sequence } from '@/lib/data-mappings';

import { computeAutoSwitchDecision } from './unofficial-run-auto-switch';

function entry(model: Model, sequence: Sequence): AvailableModelSequence {
return { model, sequence, precisions: [] };
}

describe('computeAutoSwitchDecision', () => {
it('returns no-op and resets the key when no unofficial run is loaded', () => {
expect(computeAutoSwitchDecision([], undefined, Model.DeepSeek_R1, 'stale-key')).toEqual({
nextKey: '',
modelToSet: null,
});
});

it('switches to the run model when g_model is not pinned and current model is not covered', () => {
const run = [entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK)];
const decision = computeAutoSwitchDecision(run, undefined, Model.DeepSeek_R1, '');
expect(decision.modelToSet).toBe(Model.DeepSeek_V4_Pro);
expect(decision.nextKey).toBe(Model.DeepSeek_V4_Pro);
});

it('respects an explicit g_model URL param even when the run lacks that model', () => {
const run = [entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK)];
const decision = computeAutoSwitchDecision(run, Model.DeepSeek_R1, Model.DeepSeek_R1, '');
expect(decision.modelToSet).toBeNull();
// Ref must not be advanced — if the URL is later cleared we still want
// a fresh load of the same run to be able to fire the switch.
expect(decision.nextKey).toBe('');
});

it('does not switch when the current model is already covered by the overlay', () => {
const run = [
entry(Model.DeepSeek_R1, Sequence.OneK_OneK),
entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK),
];
const decision = computeAutoSwitchDecision(run, undefined, Model.DeepSeek_R1, '');
expect(decision.modelToSet).toBeNull();
// Key still advances so we don't keep re-evaluating on every render.
expect(decision.nextKey).toBe([Model.DeepSeek_R1, Model.DeepSeek_V4_Pro].toSorted().join(','));
});

it('does not re-fire after a manual model change against the same run set', () => {
// Simulate the post-auto-switch state: ref already holds the run's key,
// user manually switched back to a model the run does not cover.
const run = [entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK)];
const lastKey = Model.DeepSeek_V4_Pro;
const decision = computeAutoSwitchDecision(run, undefined, Model.DeepSeek_R1, lastKey);
expect(decision.modelToSet).toBeNull();
expect(decision.nextKey).toBe(lastKey);
});

it('re-arms after the overlay set is cleared so a subsequent load can switch again', () => {
// Step 1: a run is loaded, switch fires.
const run = [entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK)];
const first = computeAutoSwitchDecision(run, undefined, Model.DeepSeek_R1, '');
expect(first.modelToSet).toBe(Model.DeepSeek_V4_Pro);

// Step 2: user dismisses the run, overlay set goes empty — ref resets.
const cleared = computeAutoSwitchDecision([], undefined, Model.DeepSeek_V4_Pro, first.nextKey);
expect(cleared).toEqual({ nextKey: '', modelToSet: null });

// Step 3: a *different* run is loaded with a different model. The cleared
// ref allows the switch to fire again.
const run2 = [entry(Model.Kimi_K2_5, Sequence.OneK_OneK)];
const second = computeAutoSwitchDecision(
run2,
undefined,
Model.DeepSeek_V4_Pro,
cleared.nextKey,
);
expect(second.modelToSet).toBe(Model.Kimi_K2_5);
});

it('ignores sequence-only changes in the dedupe key', () => {
// Same model, two sequences appearing across renders. The decision logic
// only branches on model, so the key should not change when a new
// sequence arrives for an already-covered model — otherwise the effect
// would re-evaluate (and bail) on every sequence delta.
const oneK = [entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK)];
const both = [
entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK),
entry(Model.DeepSeek_V4_Pro, Sequence.EightK_OneK),
];
const first = computeAutoSwitchDecision(oneK, undefined, Model.DeepSeek_R1, '');
const second = computeAutoSwitchDecision(both, undefined, Model.DeepSeek_V4_Pro, first.nextKey);
expect(first.nextKey).toBe(second.nextKey);
expect(second.modelToSet).toBeNull();
});

it('picks the first model deterministically across insertion orders', () => {
// Same set of models in two different orders should produce the same
// auto-picked target — protecting against `Object.keys`-driven nondeterminism
// in `parseAvailableModelsAndSequences`.
const orderA = [
entry(Model.MiniMax_M2_5, Sequence.OneK_OneK),
entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK),
entry(Model.Kimi_K2_5, Sequence.OneK_OneK),
];
const orderB = [
entry(Model.Kimi_K2_5, Sequence.OneK_OneK),
entry(Model.DeepSeek_V4_Pro, Sequence.OneK_OneK),
entry(Model.MiniMax_M2_5, Sequence.OneK_OneK),
];
const a = computeAutoSwitchDecision(orderA, undefined, Model.DeepSeek_R1, '');
const b = computeAutoSwitchDecision(orderB, undefined, Model.DeepSeek_R1, '');
expect(a.modelToSet).toBe(b.modelToSet);
expect(a.nextKey).toBe(b.nextKey);
});
});
48 changes: 48 additions & 0 deletions packages/app/src/lib/unofficial-run-auto-switch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { AvailableModelSequence } from '@/components/unofficial-run-provider';
import type { Model } from '@/lib/data-mappings';

export interface AutoSwitchDecision {
/** New value the caller should write into the dedupe ref. */
nextKey: string;
/** Model to switch to, or null when no switch is needed. */
modelToSet: Model | null;
}

/**
* Pure decision helper for the unofficial-run auto-switch effect in
* `GlobalFilterContext`. Given the unofficial run's available models, the URL
* `g_model` param, the currently selected model, and the previous dedupe key,
* returns whether to swap `selectedModel` and what the new dedupe key should be.
*
* - When the overlay set is empty, the dedupe key is reset so the next load
* re-arms the effect.
* - When the URL pinned `g_model` explicitly, no switch fires (respect intent).
* - Otherwise the dedupe key is the sorted unique list of overlay models — the
* sequence dimension is intentionally excluded so a sequence-only delta does
* not invalidate a manual model pick the user made earlier.
* - The first model is taken from a sorted unique list to keep the choice
* deterministic across renders (insertion order from `Object.keys` is not
* guaranteed for multi-model runs).
*/
export function computeAutoSwitchDecision(
unofficialAvailable: AvailableModelSequence[],
urlModel: string | undefined,
selectedModel: Model,
lastKey: string,
): AutoSwitchDecision {
if (unofficialAvailable.length === 0) {
return { nextKey: '', modelToSet: null };
}
if (urlModel) {
return { nextKey: lastKey, modelToSet: null };
}
const sortedModels = [...new Set(unofficialAvailable.map((a) => a.model))].toSorted();
const key = sortedModels.join(',');
if (lastKey === key) {
return { nextKey: lastKey, modelToSet: null };
}
if (sortedModels.includes(selectedModel)) {
return { nextKey: key, modelToSet: null };
}
return { nextKey: key, modelToSet: sortedModels[0] };
}
Loading