feat: add fast mode toggle for OpenAI models

This commit is contained in:
Peter Steinberger
2026-03-12 23:30:58 +00:00
parent ddcaec89e9
commit d5bffcdeab
66 changed files with 990 additions and 36 deletions

50
src/agents/fast-mode.ts Normal file
View File

@@ -0,0 +1,50 @@
import { normalizeFastMode } from "../auto-reply/thinking.js";
import type { OpenClawConfig } from "../config/config.js";
import type { SessionEntry } from "../config/sessions.js";
export type FastModeState = {
enabled: boolean;
source: "session" | "config" | "default";
};
function resolveConfiguredFastModeRaw(params: {
cfg: OpenClawConfig | undefined;
provider: string;
model: string;
}): unknown {
const modelKey = `${params.provider}/${params.model}`;
const modelConfig = params.cfg?.agents?.defaults?.models?.[modelKey];
return modelConfig?.params?.fastMode ?? modelConfig?.params?.fast_mode;
}
export function resolveConfiguredFastMode(params: {
cfg: OpenClawConfig | undefined;
provider: string;
model: string;
}): boolean {
return (
normalizeFastMode(
resolveConfiguredFastModeRaw(params) as string | boolean | null | undefined,
) ?? false
);
}
export function resolveFastModeState(params: {
cfg: OpenClawConfig | undefined;
provider: string;
model: string;
sessionEntry?: Pick<SessionEntry, "fastMode"> | undefined;
}): FastModeState {
const sessionOverride = normalizeFastMode(params.sessionEntry?.fastMode);
if (sessionOverride !== undefined) {
return { enabled: sessionOverride, source: "session" };
}
const configuredRaw = resolveConfiguredFastModeRaw(params);
const configured = normalizeFastMode(configuredRaw as string | boolean | null | undefined);
if (configured !== undefined) {
return { enabled: configured, source: "config" };
}
return { enabled: false, source: "default" };
}

View File

@@ -204,6 +204,7 @@ describe("applyExtraParamsToAgent", () => {
| Model<"openai-completions">;
options?: SimpleStreamOptions;
cfg?: Record<string, unknown>;
extraParamsOverride?: Record<string, unknown>;
payload?: Record<string, unknown>;
}) {
const payload = params.payload ?? { store: false };
@@ -217,6 +218,7 @@ describe("applyExtraParamsToAgent", () => {
params.cfg as Parameters<typeof applyExtraParamsToAgent>[1],
params.applyProvider,
params.applyModelId,
params.extraParamsOverride,
);
const context: Context = { messages: [] };
void agent.streamFn?.(params.model, context, params.options ?? {});
@@ -1627,6 +1629,80 @@ describe("applyExtraParamsToAgent", () => {
expect(payload.service_tier).toBe("default");
});
it("injects fast-mode payload defaults for direct OpenAI Responses", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "openai",
applyModelId: "gpt-5.4",
cfg: {
agents: {
defaults: {
models: {
"openai/gpt-5.4": {
params: {
fastMode: true,
},
},
},
},
},
},
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4",
baseUrl: "https://api.openai.com/v1",
} as unknown as Model<"openai-responses">,
payload: {
store: false,
},
});
expect(payload.reasoning).toEqual({ effort: "low" });
expect(payload.text).toEqual({ verbosity: "low" });
expect(payload.service_tier).toBe("priority");
});
it("preserves caller-provided OpenAI payload fields when fast mode is enabled", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "openai",
applyModelId: "gpt-5.4",
extraParamsOverride: { fastMode: true },
model: {
api: "openai-responses",
provider: "openai",
id: "gpt-5.4",
baseUrl: "https://api.openai.com/v1",
} as unknown as Model<"openai-responses">,
payload: {
reasoning: { effort: "medium" },
text: { verbosity: "high" },
service_tier: "default",
},
});
expect(payload.reasoning).toEqual({ effort: "medium" });
expect(payload.text).toEqual({ verbosity: "high" });
expect(payload.service_tier).toBe("default");
});
it("applies fast-mode defaults for openai-codex responses without service_tier", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "openai-codex",
applyModelId: "gpt-5.4",
extraParamsOverride: { fastMode: true },
model: {
api: "openai-codex-responses",
provider: "openai-codex",
id: "gpt-5.4",
baseUrl: "https://chatgpt.com/backend-api",
} as unknown as Model<"openai-codex-responses">,
payload: {
store: false,
},
});
expect(payload.reasoning).toEqual({ effort: "low" });
expect(payload.text).toEqual({ verbosity: "low" });
expect(payload).not.toHaveProperty("service_tier");
});
it("does not inject service_tier for non-openai providers", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "azure-openai-responses",

View File

@@ -22,8 +22,10 @@ import {
import {
createCodexDefaultTransportWrapper,
createOpenAIDefaultTransportWrapper,
createOpenAIFastModeWrapper,
createOpenAIResponsesContextManagementWrapper,
createOpenAIServiceTierWrapper,
resolveOpenAIFastMode,
resolveOpenAIServiceTier,
} from "./openai-stream-wrappers.js";
import {
@@ -437,6 +439,12 @@ export function applyExtraParamsToAgent(
// upstream model-ID heuristics for Gemini 3.1 variants.
agent.streamFn = createGoogleThinkingPayloadWrapper(agent.streamFn, thinkingLevel);
const openAIFastMode = resolveOpenAIFastMode(merged);
if (openAIFastMode) {
log.debug(`applying OpenAI fast mode for ${provider}/${modelId}`);
agent.streamFn = createOpenAIFastModeWrapper(agent.streamFn);
}
const openAIServiceTier = resolveOpenAIServiceTier(merged);
if (openAIServiceTier) {
log.debug(`applying OpenAI service_tier=${openAIServiceTier} for ${provider}/${modelId}`);

View File

@@ -4,6 +4,7 @@ import { streamSimple } from "@mariozechner/pi-ai";
import { log } from "./logger.js";
type OpenAIServiceTier = "auto" | "default" | "flex" | "priority";
type OpenAIReasoningEffort = "low" | "medium" | "high";
const OPENAI_RESPONSES_APIS = new Set(["openai-responses"]);
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
@@ -168,6 +169,89 @@ export function resolveOpenAIServiceTier(
return normalized;
}
function normalizeOpenAIFastMode(value: unknown): boolean | undefined {
if (typeof value === "boolean") {
return value;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim().toLowerCase();
if (
normalized === "on" ||
normalized === "true" ||
normalized === "yes" ||
normalized === "1" ||
normalized === "fast"
) {
return true;
}
if (
normalized === "off" ||
normalized === "false" ||
normalized === "no" ||
normalized === "0" ||
normalized === "normal"
) {
return false;
}
return undefined;
}
export function resolveOpenAIFastMode(
extraParams: Record<string, unknown> | undefined,
): boolean | undefined {
const raw = extraParams?.fastMode ?? extraParams?.fast_mode;
const normalized = normalizeOpenAIFastMode(raw);
if (raw !== undefined && normalized === undefined) {
const rawSummary = typeof raw === "string" ? raw : typeof raw;
log.warn(`ignoring invalid OpenAI fast mode param: ${rawSummary}`);
}
return normalized;
}
function resolveFastModeReasoningEffort(modelId: unknown): OpenAIReasoningEffort {
if (typeof modelId !== "string") {
return "low";
}
const normalized = modelId.trim().toLowerCase();
// Keep fast mode broadly compatible across GPT-5 family variants by using
// the lowest shared non-disabled effort that current transports accept.
if (normalized.startsWith("gpt-5")) {
return "low";
}
return "low";
}
function applyOpenAIFastModePayloadOverrides(params: {
payloadObj: Record<string, unknown>;
model: { provider?: unknown; id?: unknown; baseUrl?: unknown; api?: unknown };
}): void {
if (params.payloadObj.reasoning === undefined) {
params.payloadObj.reasoning = {
effort: resolveFastModeReasoningEffort(params.model.id),
};
}
const existingText = params.payloadObj.text;
if (existingText === undefined) {
params.payloadObj.text = { verbosity: "low" };
} else if (existingText && typeof existingText === "object" && !Array.isArray(existingText)) {
const textObj = existingText as Record<string, unknown>;
if (textObj.verbosity === undefined) {
textObj.verbosity = "low";
}
}
if (
params.model.provider === "openai" &&
params.payloadObj.service_tier === undefined &&
isOpenAIPublicApiBaseUrl(params.model.baseUrl)
) {
params.payloadObj.service_tier = "priority";
}
}
export function createOpenAIResponsesContextManagementWrapper(
baseStreamFn: StreamFn | undefined,
extraParams: Record<string, unknown> | undefined,
@@ -203,6 +287,31 @@ export function createOpenAIResponsesContextManagementWrapper(
};
}
export function createOpenAIFastModeWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
if (
(model.api !== "openai-responses" && model.api !== "openai-codex-responses") ||
(model.provider !== "openai" && model.provider !== "openai-codex")
) {
return underlying(model, context, options);
}
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
...options,
onPayload: (payload) => {
if (payload && typeof payload === "object") {
applyOpenAIFastModePayloadOverrides({
payloadObj: payload as Record<string, unknown>,
model,
});
}
return originalOnPayload?.(payload, model);
},
});
};
}
export function createOpenAIServiceTierWrapper(
baseStreamFn: StreamFn | undefined,
serviceTier: OpenAIServiceTier,

View File

@@ -892,6 +892,7 @@ export async function runEmbeddedPiAgent(
agentId: workspaceResolution.agentId,
legacyBeforeAgentStartResult,
thinkLevel,
fastMode: params.fastMode,
verboseLevel: params.verboseLevel,
reasoningLevel: params.reasoningLevel,
toolResultFormat: resolvedToolResultFormat,

View File

@@ -1930,7 +1930,10 @@ export async function runEmbeddedAttempt(
params.config,
params.provider,
params.modelId,
params.streamParams,
{
...params.streamParams,
fastMode: params.fastMode,
},
params.thinkLevel,
sessionAgentId,
);

View File

@@ -79,6 +79,7 @@ export type RunEmbeddedPiAgentParams = {
authProfileId?: string;
authProfileIdSource?: "auto" | "user";
thinkLevel?: ThinkLevel;
fastMode?: boolean;
verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
toolResultFormat?: ToolResultFormat;