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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### Fixed

- Browser: distinguish requested CLI model keys from verified ChatGPT picker labels without inferring a server-side GPT version from a generic label. Fixes #317. Thanks @DragonFSKY!
- Browser: recognize GPT-5.6 Sol as the selected model when ChatGPT exposes Pro in its independent effort pill. Thanks @jung0han!
- Browser: treat WSL's systemd-resolved loopback DNS stub as localhost when connecting to a freshly launched Chrome DevTools endpoint. Thanks @Rokurolize!
- CLI: reject junk between duration tokens and warn when malformed browser duration flags fall back to defaults. Thanks @devYRPauli!
Expand Down
48 changes: 22 additions & 26 deletions src/browser/actions/modelSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,18 @@ export async function ensureModelSelection(
case "already-selected":
case "switched": {
const observedLabel = result.label?.trim() || null;
const label = observedLabel || (strategy === "current" ? null : desiredModel);
if (strategy !== "current") {
assertResolvedModelSelection(desiredModel, observedLabel ?? "");
if (strategy !== "current" && observedLabel !== null) {
assertResolvedModelSelection(desiredModel, observedLabel);
}
logger(`Model picker: ${label ?? "current model (label unavailable)"}`);
logger(`Model picker: ${observedLabel ?? "current model (label unavailable)"}`);
return {
requestedModel: desiredModel,
resolvedLabel: label,
// A picker target is intent, not observed UI evidence. Keep it separate from the
// resolved label so display code cannot turn a fallback into a claimed selection.
resolvedLabel: observedLabel,
strategy,
status: result.status,
verified: strategy !== "current",
verified: strategy !== "current" && observedLabel !== null,
source: "chatgpt-model-picker",
capturedAt: new Date().toISOString(),
};
Expand Down Expand Up @@ -354,12 +355,10 @@ function buildModelSelectionExpression(
const getComposerModelLabel = () =>
(document.querySelector(COMPOSER_MODEL_SIGNAL_SELECTOR)?.textContent ?? '').trim();
const readComposerModelSignal = () => normalizeText(getComposerModelLabel());
const isIntelligenceEffortLabel = (label) =>
label === 'instant' ||
const isEffortOnlyLabel = (label) =>
label === 'medium' ||
label === 'high' ||
label === 'extra high' ||
label === 'pro' ||
label === 'extended' ||
label === 'standard' ||
label === 'heavy' ||
Expand Down Expand Up @@ -447,7 +446,7 @@ function buildModelSelectionExpression(
}
return true;
};
const getResolvedLabel = (fallback) => {
const getResolvedLabel = (observedOptionLabel = '') => {
if (configuredSelectionMatchesTarget()) {
const variant = getConfiguredVariantLabel();
const version = formatModelOptionLabel(getConfiguredVersionLabel());
Expand Down Expand Up @@ -476,21 +475,18 @@ function buildModelSelectionExpression(
if (desiredModelVariant === 'sol' && hasProComposerPill()) return PRIMARY_LABEL;
return withProPillSignal(buttonLabel);
}
const fallbackLabel = formatModelOptionLabel(fallback);
const normalizedFallback = normalizeText(fallbackLabel);
if (
desiredVersion &&
desiredModelVariant &&
versionFromLabel(normalizedFallback) === desiredVersion &&
normalizedFallback.split(' ').includes(desiredModelVariant)
) {
return fallbackLabel;
}
if (composerLabel) return withProPillSignal(composerLabel);
if (fallbackLabel && !wantsPro && isIntelligenceEffortLabel(normalizedButton)) {
return fallbackLabel;
}
return withProPillSignal(buttonLabel || fallbackLabel || fallback);
const observedLabel = (label) => {
const formatted = formatModelOptionLabel(label);
return formatted && !isEffortOnlyLabel(normalizeText(formatted))
? withProPillSignal(formatted)
: '';
};
return (
observedLabel(observedOptionLabel) ||
observedLabel(composerLabel) ||
observedLabel(buttonLabel) ||
(wantsPro && hasProComposerPill() ? 'Pro' : '')
);
};
if (MODEL_STRATEGY === 'current') {
const currentLabel = getResolvedLabel('') || null;
Expand Down Expand Up @@ -610,7 +606,7 @@ function buildModelSelectionExpression(
};

if (activeSelectionMatchesTarget()) {
return { status: 'already-selected', label: getResolvedLabel(PRIMARY_LABEL) };
return { status: 'already-selected', label: getResolvedLabel() };
}

let lastPointerClick = 0;
Expand Down
102 changes: 102 additions & 0 deletions src/browser/modelDisplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { BrowserModelSelectionEvidence, SessionMetadata } from "../sessionStore.js";
import type { BrowserModelStrategy } from "./types.js";

interface BrowserModelDisplayInput {
model?: string | null;
desiredModel?: string | null;
modelStrategy?: BrowserModelStrategy;
evidence?: BrowserModelSelectionEvidence;
}

function cleanLabel(value?: string | null): string | null {
const label = value?.trim();
return label ? label : null;
}

function sameLabel(left: string, right: string): boolean {
return left.localeCompare(right, undefined, { sensitivity: "accent" }) === 0;
}

/**
* Describe what a browser run will try to select without presenting the target as observed fact.
*/
export function formatBrowserModelTarget({
model,
desiredModel,
modelStrategy,
}: BrowserModelDisplayInput): string {
const requested = cleanLabel(model) ?? "n/a";
if (modelStrategy === "current" || modelStrategy === "ignore") {
return `picker=${modelStrategy}; requested=${requested}`;
}
const target = cleanLabel(desiredModel);
if (!target) {
return requested;
}
return `target=${target}; requested=${requested}`;
}

/**
* Prefer picker evidence only when Oracle verified it. Otherwise retain the requested CLI key.
* In particular, a bare `Pro` picker label must not be expanded to a server-side model version.
*/
export function resolveBrowserModelDisplayName({
model,
evidence,
}: BrowserModelDisplayInput): string {
const verifiedLabel = evidence?.verified ? cleanLabel(evidence.resolvedLabel) : null;
return verifiedLabel ?? cleanLabel(model) ?? "n/a";
}

export function formatBrowserModelWithRequestedKey(input: BrowserModelDisplayInput): string {
const displayName = resolveBrowserModelDisplayName(input);
const requested = cleanLabel(input.model);
if (!requested || sameLabel(displayName, requested)) {
return displayName;
}
return `${displayName} (requested ${requested})`;
}

export function resolveSessionBrowserModelDisplayName(
metadata: SessionMetadata,
model = metadata.model,
): string {
const sessionModel = cleanLabel(metadata.model);
const requestedModel = cleanLabel(model);
const evidenceApplies =
requestedModel === null
? sessionModel === null
: sessionModel !== null && sameLabel(requestedModel, sessionModel);
return resolveBrowserModelDisplayName({
model,
evidence: evidenceApplies ? metadata.browser?.modelSelection : undefined,
});
}

export function formatSessionBrowserModelWithRequestedKey(
metadata: SessionMetadata,
model = metadata.model,
): string {
const sessionModel = cleanLabel(metadata.model);
const requestedModel = cleanLabel(model);
const evidenceApplies =
requestedModel === null
? sessionModel === null
: sessionModel !== null && sameLabel(requestedModel, sessionModel);
return formatBrowserModelWithRequestedKey({
model,
evidence: evidenceApplies ? metadata.browser?.modelSelection : undefined,
});
}

export function formatBrowserModelSelectionEvidence(
evidence: BrowserModelSelectionEvidence,
model?: string | null,
): string {
const requestedKey = cleanLabel(model) ?? "(none)";
const target = cleanLabel(evidence.requestedModel) ?? "(none)";
const resolvedLabel = cleanLabel(evidence.resolvedLabel) ?? "(unavailable)";
const strategy = evidence.strategy ?? "(default)";
const verified = evidence.verified ? "yes" : "no";
return `requestedKey=${requestedKey}; target=${target}; resolvedLabel=${resolvedLabel}; status=${evidence.status}; strategy=${strategy}; verified=${verified}; source=${evidence.source}; capturedAt=${evidence.capturedAt}`;
}
26 changes: 15 additions & 11 deletions src/browser/sessionRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import {
saveBrowserTranscriptArtifact,
saveDeepResearchReportArtifact,
} from "./artifacts.js";
import {
formatBrowserModelSelectionEvidence,
formatBrowserModelTarget,
resolveBrowserModelDisplayName,
} from "./modelDisplay.js";

export interface BrowserExecutionResult {
usage: {
Expand Down Expand Up @@ -72,14 +77,6 @@ function buildUnavailableModelSelectionEvidence(
};
}

function formatModelSelectionEvidence(evidence: BrowserModelSelectionEvidence): string {
const requested = evidence.requestedModel ?? "(none)";
const resolved = evidence.resolvedLabel ?? "(unavailable)";
const strategy = evidence.strategy ?? "(default)";
const verified = evidence.verified ? "yes" : "no";
return `[browser] Model selection evidence: requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}.`;
}

function isRequestedProBrowserRun(
runOptions: RunOracleOptions,
browserConfig: BrowserSessionConfig,
Expand Down Expand Up @@ -176,7 +173,12 @@ export async function runBrowserSessionExecution(
),
);
}
const headerLine = `Launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
const launchModel = formatBrowserModelTarget({
model: runOptions.model,
desiredModel: browserConfig.desiredModel,
modelStrategy: browserConfig.modelStrategy,
});
const headerLine = `Launching browser mode (${launchModel}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
const automationLogger: BrowserLogger = ((message?: string) => {
if (typeof message !== "string") return;
const shouldAlwaysPrint =
Expand Down Expand Up @@ -240,7 +242,9 @@ export async function runBrowserSessionExecution(
const modelSelection =
browserResult.modelSelection ?? buildUnavailableModelSelectionEvidence(browserConfig);
if (modelSelection) {
log(formatModelSelectionEvidence(modelSelection));
log(
`[browser] Model selection evidence: ${formatBrowserModelSelectionEvidence(modelSelection, runOptions.model)}`,
);
}
const warnings = buildBrowserRunWarnings({
runOptions,
Expand Down Expand Up @@ -288,7 +292,7 @@ export async function runBrowserSessionExecution(
})();
const { line1, line2 } = formatFinishLine({
elapsedMs: browserResult.tookMs,
model: `${runOptions.model}[browser]`,
model: `${resolveBrowserModelDisplayName({ model: runOptions.model, evidence: modelSelection })}[browser]`,
tokensPart,
detailParts: [
runOptions.file && runOptions.file.length > 0 ? `files=${runOptions.file.length}` : null,
Expand Down
15 changes: 13 additions & 2 deletions src/cli/dryRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { BrowserSessionConfig } from "../sessionStore.js";
import { buildTokenEstimateSuffix, formatAttachmentLabel } from "../browser/promptSummary.js";
import { buildCookiePlan } from "../browser/policies.js";
import { describeBrowserControlPlan, formatBrowserControlPlan } from "../browser/controlPlan.js";
import { formatBrowserModelTarget } from "../browser/modelDisplay.js";

interface DryRunDeps {
readFilesImpl?: typeof readFiles;
Expand Down Expand Up @@ -113,7 +114,12 @@ async function runBrowserDryRun(
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
const artifacts = await assemblePromptImpl(runOptions, { cwd });
const suffix = buildTokenEstimateSuffix(artifacts);
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
const displayModel = formatBrowserModelTarget({
model: runOptions.model,
desiredModel: browserConfig?.desiredModel,
modelStrategy: browserConfig?.modelStrategy,
});
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
log(chalk.cyan(headerLine));
logBrowserControlPlan(browserConfig, log, "dry-run");
logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "dry-run");
Expand Down Expand Up @@ -206,7 +212,12 @@ export async function runBrowserPreview(
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
const artifacts = await assemblePromptImpl(runOptions, { cwd });
const suffix = buildTokenEstimateSuffix(artifacts);
const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
const displayModel = formatBrowserModelTarget({
model: runOptions.model,
desiredModel: browserConfig?.desiredModel,
modelStrategy: browserConfig?.modelStrategy,
});
const headerLine = `[preview] Oracle (${version}) browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
log(chalk.cyan(headerLine));
logBrowserControlPlan(browserConfig, log, "preview");
logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
Expand Down
29 changes: 19 additions & 10 deletions src/cli/sessionDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ import {
resolveSessionLineage,
} from "./sessionLineage.js";
import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
import {
formatBrowserModelSelectionEvidence,
formatSessionBrowserModelWithRequestedKey,
resolveSessionBrowserModelDisplayName,
} from "../browser/modelDisplay.js";

const isTty = (): boolean => Boolean(process.stdout.isTTY);
const dim = (text: string): string => (isTty() ? kleur.dim(text) : text);
Expand Down Expand Up @@ -426,10 +431,18 @@ export async function attachSession(
const usage = run.usage
? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
: "";
console.log(`- ${chalk.cyan(run.model)} — ${run.status}${usage}`);
const modelLabel =
(metadata.mode ?? metadata.options?.mode) === "browser"
? formatSessionBrowserModelWithRequestedKey(metadata, run.model)
: run.model;
console.log(`- ${chalk.cyan(modelLabel)} — ${run.status}${usage}`);
}
} else if (metadata.model) {
console.log(`Model: ${metadata.model}`);
const modelLabel =
(metadata.mode ?? metadata.options?.mode) === "browser"
? formatSessionBrowserModelWithRequestedKey(metadata)
: metadata.model;
console.log(`Model: ${modelLabel}`);
}
const browserEvidence = formatBrowserEvidence(metadata);
if (browserEvidence) {
Expand Down Expand Up @@ -736,13 +749,7 @@ export function formatBrowserEvidence(metadata: SessionMetadata): string[] | nul
const lines: string[] = [];
const evidence = browser.modelSelection;
if (evidence) {
const requested = evidence.requestedModel ?? "(none)";
const resolved = evidence.resolvedLabel ?? "(unavailable)";
const strategy = evidence.strategy ?? "(default)";
const verified = evidence.verified ? "yes" : "no";
lines.push(
`model requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}`,
);
lines.push(`model ${formatBrowserModelSelectionEvidence(evidence, metadata.model)}`);
}
for (const warning of browser.warnings ?? []) {
lines.push(`warning ${warning.code}: ${warning.message}`);
Expand Down Expand Up @@ -1048,7 +1055,9 @@ export function formatCompletionSummary(
return null;
}
const modeLabel =
metadata.mode === "browser" ? `${metadata.model ?? "n/a"}[browser]` : (metadata.model ?? "n/a");
(metadata.mode ?? metadata.options?.mode) === "browser"
? `${resolveSessionBrowserModelDisplayName(metadata)}[browser]`
: (metadata.model ?? "n/a");
const usage = metadata.usage;
const cost = resolveSessionCost(metadata);
const tokensDisplay = [
Expand Down
7 changes: 6 additions & 1 deletion src/cli/sessionTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MODEL_CONFIGS } from "../oracle.js";
import type { SessionMetadata } from "../sessionStore.js";
import { estimateUsdCost } from "tokentally";
import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
import { resolveSessionBrowserModelDisplayName } from "../browser/modelDisplay.js";

const isRich = (rich?: boolean): boolean =>
rich ?? Boolean(process.stdout.isTTY && chalk.level > 0);
Expand Down Expand Up @@ -34,7 +35,11 @@ export function formatSessionTableRow(
): string {
const rich = isRich(options?.rich);
const status = colorStatus(meta.status ?? "unknown", rich);
const modelLabel = (meta.model ?? "n/a").padEnd(MODEL_PAD);
const displayModel =
(meta.mode ?? meta.options?.mode) === "browser"
? resolveSessionBrowserModelDisplayName(meta)
: (meta.model ?? "n/a");
const modelLabel = displayModel.padEnd(MODEL_PAD);
const model = rich ? chalk.white(modelLabel) : modelLabel;
const modeLabel = formatSessionExecutionLabel(meta).padEnd(MODE_PAD);
const mode = rich ? chalk.gray(modeLabel) : modeLabel;
Expand Down
Loading