mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
feat: add fast mode toggle for OpenAI models
This commit is contained in:
50
src/agents/fast-mode.ts
Normal file
50
src/agents/fast-mode.ts
Normal 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" };
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -892,6 +892,7 @@ export async function runEmbeddedPiAgent(
|
||||
agentId: workspaceResolution.agentId,
|
||||
legacyBeforeAgentStartResult,
|
||||
thinkLevel,
|
||||
fastMode: params.fastMode,
|
||||
verboseLevel: params.verboseLevel,
|
||||
reasoningLevel: params.reasoningLevel,
|
||||
toolResultFormat: resolvedToolResultFormat,
|
||||
|
||||
@@ -1930,7 +1930,10 @@ export async function runEmbeddedAttempt(
|
||||
params.config,
|
||||
params.provider,
|
||||
params.modelId,
|
||||
params.streamParams,
|
||||
{
|
||||
...params.streamParams,
|
||||
fastMode: params.fastMode,
|
||||
},
|
||||
params.thinkLevel,
|
||||
sessionAgentId,
|
||||
);
|
||||
|
||||
@@ -79,6 +79,7 @@ export type RunEmbeddedPiAgentParams = {
|
||||
authProfileId?: string;
|
||||
authProfileIdSource?: "auto" | "user";
|
||||
thinkLevel?: ThinkLevel;
|
||||
fastMode?: boolean;
|
||||
verboseLevel?: VerboseLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
toolResultFormat?: ToolResultFormat;
|
||||
|
||||
Reference in New Issue
Block a user