mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Fix fast service tier state detection
This commit is contained in:
parent
4c0b8d30c3
commit
d80d9789e8
8 changed files with 218 additions and 16 deletions
|
|
@ -2,13 +2,21 @@ export type ServiceTier = string;
|
|||
export type RequestedServiceTier = "fast" | "off";
|
||||
export type ServiceTierRequest = string | null | undefined;
|
||||
|
||||
export interface ServiceTierMetadata {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function parseServiceTier(value: unknown): ServiceTier | null {
|
||||
if (typeof value === "string" && value.length > 0) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function requestedServiceTierRequestValue(value: RequestedServiceTier | null): "fast" | null | undefined {
|
||||
if (value === "fast") return "fast";
|
||||
export function requestedServiceTierRequestValue(
|
||||
value: RequestedServiceTier | null,
|
||||
fastServiceTierId = "fast",
|
||||
): string | null | undefined {
|
||||
if (value === "fast") return fastServiceTierId;
|
||||
if (value === "off") return null;
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -16,3 +24,10 @@ export function requestedServiceTierRequestValue(value: RequestedServiceTier | n
|
|||
export function configuredServiceTierRequestValue(value: ServiceTier | null): string | undefined {
|
||||
return value ?? undefined;
|
||||
}
|
||||
|
||||
export function isFastServiceTier(value: ServiceTier | null | undefined, serviceTiers: readonly ServiceTierMetadata[] = []): boolean {
|
||||
if (!value) return false;
|
||||
if (value === "fast") return true;
|
||||
if (serviceTiers.length === 0) return value === "priority";
|
||||
return serviceTiers.some((tier) => tier.id === value && tier.name.trim().toLowerCase() === "fast");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { collaborationModeToggleMessage, nextCollaborationMode } from "../../run
|
|||
import { readRuntimeConfig } from "../../runtime/config";
|
||||
import {
|
||||
autoReviewActive,
|
||||
currentServiceTier,
|
||||
fastModeActive,
|
||||
fastServiceTierRequestValue,
|
||||
requestedTurnRuntimeSettings,
|
||||
runtimeOverridePayload,
|
||||
type RuntimeSnapshot,
|
||||
|
|
@ -78,8 +79,9 @@ export class ChatRuntimeSettingsController {
|
|||
}
|
||||
|
||||
async toggleFastMode(): Promise<void> {
|
||||
const current = currentServiceTier(this.host.runtimeSnapshot(), readRuntimeConfig(this.state.effectiveConfig));
|
||||
const next: RequestedServiceTier = current === "fast" ? "off" : "fast";
|
||||
const snapshot = this.host.runtimeSnapshot();
|
||||
const config = readRuntimeConfig(this.state.effectiveConfig);
|
||||
const next: RequestedServiceTier = fastModeActive(snapshot, config) ? "off" : "fast";
|
||||
this.dispatch({ type: "runtime/requested-service-tier-set", serviceTier: next });
|
||||
this.dispatch({ type: "ui/panel-set", panel: null });
|
||||
if (!(await this.applyPendingThreadSettings())) return;
|
||||
|
|
@ -112,7 +114,8 @@ export class ChatRuntimeSettingsController {
|
|||
private pendingThreadSettingsUpdate(): ThreadSettingsUpdate {
|
||||
const update: ThreadSettingsUpdate = {};
|
||||
const state = this.state;
|
||||
const turnSettings = requestedTurnRuntimeSettings(this.host.runtimeSnapshot());
|
||||
const snapshot = this.host.runtimeSnapshot();
|
||||
const turnSettings = requestedTurnRuntimeSettings(snapshot);
|
||||
|
||||
if (state.requestedModel.kind !== "default") {
|
||||
const model = runtimeOverridePayload(state.requestedModel);
|
||||
|
|
@ -123,7 +126,10 @@ export class ChatRuntimeSettingsController {
|
|||
if (effort !== undefined) update.effort = effort;
|
||||
}
|
||||
if (state.requestedServiceTier !== null) {
|
||||
const serviceTier = requestedServiceTierRequestValue(state.requestedServiceTier);
|
||||
const serviceTier = requestedServiceTierRequestValue(
|
||||
state.requestedServiceTier,
|
||||
fastServiceTierRequestValue(snapshot, readRuntimeConfig(state.effectiveConfig)),
|
||||
);
|
||||
if (serviceTier !== undefined) update.serviceTier = serviceTier;
|
||||
}
|
||||
if (state.requestedApprovalsReviewer !== null) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
autoReviewActive,
|
||||
currentModel,
|
||||
currentReasoningEffort,
|
||||
currentServiceTier,
|
||||
fastModeActive,
|
||||
runtimeOverrideLabel,
|
||||
runtimeSummaryLabel,
|
||||
serviceTierLabel,
|
||||
|
|
@ -156,7 +156,7 @@ export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel
|
|||
runtimeOpen,
|
||||
planActive: state.requestedCollaborationMode === "plan",
|
||||
autoReviewActive: autoReviewActive(snapshot, config),
|
||||
fastActive: currentServiceTier(snapshot, config) === "fast",
|
||||
fastActive: fastModeActive(snapshot, config),
|
||||
runtimeSummary: runtimeSummaryLabel(model, effort),
|
||||
runtimeTitle: `Model: ${model ?? "(Codex default)"}; Effort: ${effort ?? "(Codex default)"}`,
|
||||
runtimeEmphasized: false,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { RateLimitSnapshot } from "../generated/app-server/v2/RateLimitSnap
|
|||
import type { ThreadTokenUsage } from "../generated/app-server/v2/ThreadTokenUsage";
|
||||
import {
|
||||
configuredServiceTierRequestValue,
|
||||
isFastServiceTier,
|
||||
requestedServiceTierRequestValue,
|
||||
type RequestedServiceTier,
|
||||
type ServiceTier,
|
||||
|
|
@ -61,7 +62,9 @@ export function requestedOrConfiguredServiceTier(
|
|||
snapshot: RuntimeSnapshot,
|
||||
config: RuntimeConfigProjection = readRuntimeConfig(snapshot.effectiveConfig),
|
||||
): ServiceTierRequest {
|
||||
if (snapshot.requestedServiceTier !== null) return requestedServiceTierRequestValue(snapshot.requestedServiceTier);
|
||||
if (snapshot.requestedServiceTier !== null) {
|
||||
return requestedServiceTierRequestValue(snapshot.requestedServiceTier, fastServiceTierRequestValue(snapshot, config));
|
||||
}
|
||||
return configuredServiceTierRequestValue(config.serviceTier);
|
||||
}
|
||||
|
||||
|
|
@ -138,16 +141,30 @@ export function serviceTierLabel(
|
|||
return currentServiceTier(snapshot, config) ?? "(Codex default)";
|
||||
}
|
||||
|
||||
export function fastModeActive(
|
||||
snapshot: RuntimeSnapshot,
|
||||
config: RuntimeConfigProjection = readRuntimeConfig(snapshot.effectiveConfig),
|
||||
): boolean {
|
||||
return isFastServiceTier(currentServiceTier(snapshot, config), currentModelServiceTiers(snapshot, config));
|
||||
}
|
||||
|
||||
export function fastModeLabel(
|
||||
snapshot: RuntimeSnapshot,
|
||||
config: RuntimeConfigProjection = readRuntimeConfig(snapshot.effectiveConfig),
|
||||
): string {
|
||||
if (snapshot.requestedServiceTier === "off") return "off";
|
||||
if (fastModeActive(snapshot, config)) return "on";
|
||||
const serviceTier = currentServiceTier(snapshot, config);
|
||||
if (serviceTier === "fast") return "on";
|
||||
return serviceTier ? "off" : "Codex default";
|
||||
}
|
||||
|
||||
export function fastServiceTierRequestValue(
|
||||
snapshot: RuntimeSnapshot,
|
||||
config: RuntimeConfigProjection = readRuntimeConfig(snapshot.effectiveConfig),
|
||||
): string {
|
||||
return currentModelServiceTiers(snapshot, config).find((tier) => tier.name.trim().toLowerCase() === "fast")?.id ?? "fast";
|
||||
}
|
||||
|
||||
export function defaultRuntimeOverride<T>(): RuntimeOverride<T> {
|
||||
return { kind: "default" };
|
||||
}
|
||||
|
|
@ -175,3 +192,10 @@ export function runtimeOverrideLabel<T>(override: RuntimeOverride<T>): string {
|
|||
function isAutoReviewReviewer(value: ApprovalsReviewer | null): boolean {
|
||||
return value === "auto_review" || value === "guardian_subagent";
|
||||
}
|
||||
|
||||
function currentModelServiceTiers(
|
||||
snapshot: RuntimeSnapshot,
|
||||
config: RuntimeConfigProjection = readRuntimeConfig(snapshot.effectiveConfig),
|
||||
): Model["serviceTiers"] {
|
||||
return findModelByIdOrName(snapshot.availableModels, currentModel(snapshot, config))?.serviceTiers ?? [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { configuredServiceTierRequestValue, parseServiceTier, requestedServiceTierRequestValue } from "../../src/app-server/service-tier";
|
||||
import {
|
||||
configuredServiceTierRequestValue,
|
||||
isFastServiceTier,
|
||||
parseServiceTier,
|
||||
requestedServiceTierRequestValue,
|
||||
} from "../../src/app-server/service-tier";
|
||||
|
||||
describe("service tier", () => {
|
||||
it("accepts non-empty service tier ids from config and app-server reports", () => {
|
||||
|
|
@ -20,6 +25,7 @@ describe("service tier", () => {
|
|||
|
||||
it("serializes panel fast mode off as an explicit null request", () => {
|
||||
expect(requestedServiceTierRequestValue("fast")).toBe("fast");
|
||||
expect(requestedServiceTierRequestValue("fast", "priority")).toBe("priority");
|
||||
expect(requestedServiceTierRequestValue("off")).toBeNull();
|
||||
expect(requestedServiceTierRequestValue(null)).toBeUndefined();
|
||||
});
|
||||
|
|
@ -29,4 +35,23 @@ describe("service tier", () => {
|
|||
expect(configuredServiceTierRequestValue("catalog-tier")).toBe("catalog-tier");
|
||||
expect(configuredServiceTierRequestValue(null)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("recognizes Codex fast tier aliases without rejecting other tier ids", () => {
|
||||
expect(isFastServiceTier("fast")).toBe(true);
|
||||
expect(isFastServiceTier("priority")).toBe(true);
|
||||
expect(isFastServiceTier("catalog-fast", [{ id: "catalog-fast", name: "Fast" }])).toBe(true);
|
||||
expect(isFastServiceTier("priority", [{ id: "priority", name: "Priority" }])).toBe(false);
|
||||
expect(isFastServiceTier("flex", [{ id: "flex", name: "Flex" }])).toBe(false);
|
||||
});
|
||||
|
||||
it("locks the Codex 0.134.0 Fast catalog semantics observed from app-server", () => {
|
||||
// Codex app-server 0.134.0 reports Fast as serviceTiers[{ id: "priority", name: "Fast" }].
|
||||
const codex01340FastTier = { id: "priority", name: "Fast" };
|
||||
|
||||
expect(requestedServiceTierRequestValue("fast", codex01340FastTier.id)).toBe("priority");
|
||||
expect(isFastServiceTier("priority", [codex01340FastTier])).toBe(true);
|
||||
|
||||
// Clearing Fast with serviceTier: null is reported back by 0.134.0 as "default", not null.
|
||||
expect(isFastServiceTier("default", [codex01340FastTier])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ChatRuntimeSettingsController } from "../../../src/features/chat/runtime-settings-controller";
|
||||
import { createChatState, createChatStateStore } from "../../../src/features/chat/chat-state";
|
||||
import { createChatState, createChatStateStore, type ChatAction } from "../../../src/features/chat/chat-state";
|
||||
import type { AppServerClient } from "../../../src/app-server/client";
|
||||
import type { Model } from "../../../src/generated/app-server/v2/Model";
|
||||
|
||||
describe("ChatRuntimeSettingsController", () => {
|
||||
it("applies pending runtime overrides through thread settings and commits them", async () => {
|
||||
|
|
@ -62,6 +63,30 @@ describe("ChatRuntimeSettingsController", () => {
|
|||
expect(messages).toEqual(["Fast mode on for subsequent turns."]);
|
||||
});
|
||||
|
||||
it("requests the catalog Fast tier id and toggles it off from the reported effective id", async () => {
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread";
|
||||
state.activeModel = "gpt-5.5";
|
||||
// Codex app-server 0.134.0 advertises Fast as id "priority" and reports that id as the effective service tier.
|
||||
state.availableModels = [modelFixture("gpt-5.5", "priority")];
|
||||
const store = createChatStateStore(state);
|
||||
const client = clientFixture();
|
||||
const messages: string[] = [];
|
||||
const controller = runtimeControllerFixture(store, client, messages);
|
||||
|
||||
await controller.toggleFastMode();
|
||||
|
||||
expect(client.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: "priority" });
|
||||
expect(store.getState().activeServiceTier).toBe("priority");
|
||||
|
||||
store.dispatch({ type: "thread/settings-applied", ...threadSettings("priority") });
|
||||
await controller.toggleFastMode();
|
||||
|
||||
expect(client.updateThreadSettings).toHaveBeenLastCalledWith("thread", { serviceTier: null });
|
||||
expect(store.getState().activeServiceTier).toBeNull();
|
||||
expect(messages).toEqual(["Fast mode on for subsequent turns.", "Fast mode off for subsequent turns."]);
|
||||
});
|
||||
|
||||
it("leaves pending override in place when the app-server update fails", async () => {
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread";
|
||||
|
|
@ -122,3 +147,37 @@ function clientFixture(
|
|||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function modelFixture(model: string, fastTierId: string): Model {
|
||||
return {
|
||||
id: model,
|
||||
model,
|
||||
upgrade: null,
|
||||
upgradeInfo: null,
|
||||
availabilityNux: null,
|
||||
displayName: model,
|
||||
description: "",
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: "medium",
|
||||
inputModalities: [],
|
||||
supportsPersonality: true,
|
||||
additionalSpeedTiers: ["fast"],
|
||||
serviceTiers: [{ id: fastTierId, name: "Fast", description: "" }],
|
||||
defaultServiceTier: null,
|
||||
isDefault: true,
|
||||
};
|
||||
}
|
||||
|
||||
function threadSettings(serviceTier: string | null): Omit<Extract<ChatAction, { type: "thread/settings-applied" }>, "type"> {
|
||||
return {
|
||||
cwd: "/vault",
|
||||
model: "gpt-5.5",
|
||||
reasoningEffort: "high",
|
||||
collaborationMode: "default",
|
||||
serviceTier,
|
||||
approvalPolicy: "on-request",
|
||||
approvalsReviewer: "user",
|
||||
activePermissionProfile: null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,30 @@ describe("chat view model", () => {
|
|||
expect(model.modelChoices).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("marks fast active for the catalog Fast service tier id", () => {
|
||||
const state = createChatState();
|
||||
state.activeModel = "gpt-5.5";
|
||||
state.activeServiceTier = "priority";
|
||||
state.effectiveConfig = effectiveConfigFixture({ model: "gpt-5.5", model_reasoning_effort: "high" });
|
||||
state.availableModels = [modelFixture("gpt-5.5", "priority")];
|
||||
|
||||
const model = toolbarViewModel({
|
||||
state,
|
||||
snapshot: runtimeSnapshotForChatState({ state }),
|
||||
connected: true,
|
||||
turnBusy: false,
|
||||
vaultPath: "/vault",
|
||||
configuredCommand: "codex",
|
||||
archiveConfirmThreadId: null,
|
||||
archiveExportEnabled: true,
|
||||
modelChoices: [],
|
||||
effortChoices: [],
|
||||
renameState: () => null,
|
||||
});
|
||||
|
||||
expect(model.fastActive).toBe(true);
|
||||
});
|
||||
|
||||
it("builds slash-command status lines from chat state", () => {
|
||||
const state = createChatState();
|
||||
state.activeThreadId = "thread-1";
|
||||
|
|
@ -147,7 +171,7 @@ function threadFixture(id: string, name: string | null): Thread {
|
|||
};
|
||||
}
|
||||
|
||||
function modelFixture(model: string): Model {
|
||||
function modelFixture(model: string, fastTierId?: string): Model {
|
||||
return {
|
||||
id: model,
|
||||
model,
|
||||
|
|
@ -161,8 +185,8 @@ function modelFixture(model: string): Model {
|
|||
defaultReasoningEffort: "high",
|
||||
inputModalities: [],
|
||||
supportsPersonality: false,
|
||||
additionalSpeedTiers: [],
|
||||
serviceTiers: [],
|
||||
additionalSpeedTiers: fastTierId ? ["fast"] : [],
|
||||
serviceTiers: fastTierId ? [{ id: fastTierId, name: "Fast", description: "" }] : [],
|
||||
defaultServiceTier: null,
|
||||
isDefault: true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
currentModel,
|
||||
currentReasoningEffort,
|
||||
currentServiceTier,
|
||||
fastModeActive,
|
||||
fastServiceTierRequestValue,
|
||||
fastModeLabel,
|
||||
requestedOrConfiguredServiceTier,
|
||||
requestedTurnRuntimeSettings,
|
||||
|
|
@ -241,6 +243,40 @@ describe("runtime settings", () => {
|
|||
expect(fastModeLabel(snapshot)).toBe("off");
|
||||
});
|
||||
|
||||
it("treats the catalog Fast service tier id as fast mode while preserving the id", () => {
|
||||
const model = modelFixture("gpt-5.5");
|
||||
// This mirrors Codex app-server 0.134.0 model/list: Fast is named "Fast" but its id is "priority".
|
||||
model.serviceTiers = [{ id: "priority", name: "Fast", description: "1.5x speed, increased usage" }];
|
||||
const snapshot = runtimeSnapshot({
|
||||
activeModel: "gpt-5.5",
|
||||
activeServiceTier: "priority",
|
||||
effectiveConfig: effectiveConfigFixture({ model: "gpt-5.5" }),
|
||||
availableModels: [model],
|
||||
});
|
||||
|
||||
expect(currentServiceTier(snapshot)).toBe("priority");
|
||||
expect(serviceTierLabel(snapshot)).toBe("priority");
|
||||
expect(fastModeActive(snapshot)).toBe(true);
|
||||
expect(fastModeLabel(snapshot)).toBe("on");
|
||||
expect(fastServiceTierRequestValue(snapshot)).toBe("priority");
|
||||
});
|
||||
|
||||
it("treats the Codex 0.134.0 reported default tier after clearing Fast as fast mode off", () => {
|
||||
const model = modelFixture("gpt-5.5");
|
||||
model.serviceTiers = [{ id: "priority", name: "Fast", description: "1.5x speed, increased usage" }];
|
||||
const snapshot = runtimeSnapshot({
|
||||
activeModel: "gpt-5.5",
|
||||
activeServiceTier: "default",
|
||||
effectiveConfig: effectiveConfigFixture({ model: "gpt-5.5" }),
|
||||
availableModels: [model],
|
||||
});
|
||||
|
||||
expect(currentServiceTier(snapshot)).toBe("default");
|
||||
expect(serviceTierLabel(snapshot)).toBe("default");
|
||||
expect(fastModeActive(snapshot)).toBe(false);
|
||||
expect(fastModeLabel(snapshot)).toBe("off");
|
||||
});
|
||||
|
||||
it("uses requested service tier above active and configured service tiers", () => {
|
||||
const snapshot = runtimeSnapshot({
|
||||
requestedServiceTier: "off",
|
||||
|
|
@ -494,6 +530,19 @@ describe("runtime settings", () => {
|
|||
expect(requestedOrConfiguredServiceTier(snapshot)).toBeNull();
|
||||
});
|
||||
|
||||
it("serializes requested fast mode using the catalog Fast service tier id", () => {
|
||||
const model = modelFixture("gpt-5.5");
|
||||
model.serviceTiers = [{ id: "priority", name: "Fast", description: "1.5x speed, increased usage" }];
|
||||
const snapshot = runtimeSnapshot({
|
||||
requestedServiceTier: "fast",
|
||||
effectiveConfig: effectiveConfigFixture({ model: "gpt-5.5" }),
|
||||
availableModels: [model],
|
||||
});
|
||||
|
||||
expect(fastModeLabel(snapshot)).toBe("on");
|
||||
expect(requestedOrConfiguredServiceTier(snapshot)).toBe("priority");
|
||||
});
|
||||
|
||||
it("omits service tier when neither config nor override selects one", () => {
|
||||
expect(requestedOrConfiguredServiceTier(runtimeSnapshot({ effectiveConfig: effectiveConfigFixture({}) }))).toBeUndefined();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue