From 2fe0efc9e14db1c519962059fb472e8f983313e7 Mon Sep 17 00:00:00 2001 From: Pandadadadazxf <2273529713@qq.com> Date: Sun, 22 Mar 2026 00:27:51 +0800 Subject: [PATCH] fix: compaction safeguard summary budget (#27727) Merged via squash. Prepared head SHA: a7ab64e3940f89ceaa4d2ab7b8dbbdec8d05ff90 Co-authored-by: Pandadadadazxf <200469161+Pandadadadazxf@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman --- CHANGELOG.md | 1 + .../openai/media-understanding-provider.ts | 2 +- extensions/openai/openai-codex-provider.ts | 2 +- extensions/telegram/src/setup-surface.test.ts | 58 +++++--- .../compaction-safeguard.test.ts | 136 +++++++++++++++++- .../pi-extensions/compaction-safeguard.ts | 132 +++++++++++++++-- src/memory/embeddings-openai.ts | 2 + src/plugin-sdk/provider-models.ts | 7 +- src/plugins/provider-model-defaults.ts | 12 +- src/tts/tts.ts | 8 +- 10 files changed, 311 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af78f36eb25..11f0949823f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -210,6 +210,7 @@ Docs: https://docs.openclaw.ai - Config/startup: keep bundled web-search allowlist compatibility on a lightweight manifest path so config validation no longer pulls bundled web-search registry imports into startup, while still avoiding accidental auto-allow of config-loaded override plugins. (#51574) Thanks @RichardCao. - Gateway/chat.send: persist uploaded image references across reloads and compaction without delaying first-turn dispatch or double-submitting the same image to vision models. (#51324) Thanks @fuller-stack-dev. - Plugins/runtime state: share plugin-facing infra singleton state across duplicate module graphs and keep session-binding adapter ownership stable until the active owner unregisters. (#50725) thanks @huntharo. +- Agents/compaction safeguard: preserve split-turn context and preserved recent turns when capped retry fallback reuses the last successful summary. (#27727) thanks @Pandadadadazxf. ### Breaking diff --git a/extensions/openai/media-understanding-provider.ts b/extensions/openai/media-understanding-provider.ts index ecd2c5d8deb..9f7c7001935 100644 --- a/extensions/openai/media-understanding-provider.ts +++ b/extensions/openai/media-understanding-provider.ts @@ -5,7 +5,7 @@ import { type AudioTranscriptionRequest, type MediaUnderstandingProvider, } from "openclaw/plugin-sdk/media-understanding"; -import { OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL } from "../../src/providers/openai-defaults.js"; +import { OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL } from "openclaw/plugin-sdk/provider-models"; export const DEFAULT_OPENAI_AUDIO_BASE_URL = "https://api.openai.com/v1"; diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index d63eb07c4a0..4b085104aea 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -15,11 +15,11 @@ import { DEFAULT_CONTEXT_TOKENS, normalizeModelCompat, normalizeProviderId, + OPENAI_CODEX_DEFAULT_MODEL, type ProviderPlugin, } from "openclaw/plugin-sdk/provider-models"; import { createOpenAIAttributionHeadersWrapper } from "openclaw/plugin-sdk/provider-stream"; import { fetchCodexUsage } from "openclaw/plugin-sdk/provider-usage"; -import { OPENAI_CODEX_DEFAULT_MODEL } from "../../src/providers/openai-defaults.js"; import { buildOpenAICodexProvider } from "./openai-codex-catalog.js"; import { cloneFirstTemplateModel, diff --git a/extensions/telegram/src/setup-surface.test.ts b/extensions/telegram/src/setup-surface.test.ts index 83e696bea52..66de1d61bf0 100644 --- a/extensions/telegram/src/setup-surface.test.ts +++ b/extensions/telegram/src/setup-surface.test.ts @@ -31,40 +31,62 @@ async function runFinalize(cfg: OpenClawConfig, accountId: string) { return prompter.note; } +function expectPreparedResult( + prepared: Awaited>, +): { cfg: OpenClawConfig } & Exclude>, void | undefined> { + expect(prepared).toBeDefined(); + if ( + !prepared || + typeof prepared !== "object" || + !("cfg" in prepared) || + prepared.cfg === undefined + ) { + throw new Error("Expected prepare result with cfg"); + } + return prepared as { cfg: OpenClawConfig } & Exclude< + Awaited>, + void | undefined + >; +} + describe("telegramSetupWizard.prepare", () => { it('adds groups["*"].requireMention=true for fresh setups', async () => { - const prepared = await runPrepare( - { - channels: { - telegram: { - botToken: "tok", + const prepared = expectPreparedResult( + await runPrepare( + { + channels: { + telegram: { + botToken: "tok", + }, }, }, - }, - DEFAULT_ACCOUNT_ID, + DEFAULT_ACCOUNT_ID, + ), ); - expect(prepared?.cfg.channels?.telegram?.groups).toEqual({ + expect(prepared.cfg.channels?.telegram?.groups).toEqual({ "*": { requireMention: true }, }); }); it("preserves an explicit wildcard group mention setting", async () => { - const prepared = await runPrepare( - { - channels: { - telegram: { - botToken: "tok", - groups: { - "*": { requireMention: false }, + const prepared = expectPreparedResult( + await runPrepare( + { + channels: { + telegram: { + botToken: "tok", + groups: { + "*": { requireMention: false }, + }, }, }, }, - }, - DEFAULT_ACCOUNT_ID, + DEFAULT_ACCOUNT_ID, + ), ); - expect(prepared?.cfg.channels?.telegram?.groups).toEqual({ + expect(prepared.cfg.channels?.telegram?.groups).toEqual({ "*": { requireMention: false }, }); }); diff --git a/src/agents/pi-extensions/compaction-safeguard.test.ts b/src/agents/pi-extensions/compaction-safeguard.test.ts index 509bbdd25b2..897e49cb655 100644 --- a/src/agents/pi-extensions/compaction-safeguard.test.ts +++ b/src/agents/pi-extensions/compaction-safeguard.test.ts @@ -37,12 +37,18 @@ const { resolveQualityGuardMaxRetries, extractOpaqueIdentifiers, auditSummaryQuality, + capCompactionSummary, + capCompactionSummaryPreservingSuffix, + formatFileOperations, computeAdaptiveChunkRatio, isOversizedForSummary, readWorkspaceContextForSummary, BASE_CHUNK_RATIO, MIN_CHUNK_RATIO, SAFETY_MARGIN, + MAX_COMPACTION_SUMMARY_CHARS, + MAX_FILE_OPS_SECTION_CHARS, + SUMMARY_TRUNCATED_MARKER, } = __testing; function stubSessionManager(): ExtensionContext["sessionManager"] { @@ -255,6 +261,104 @@ describe("compaction-safeguard tool failures", () => { }); }); +describe("compaction-safeguard summary budgets", () => { + it("caps file operations summary and reports omitted entries", () => { + const readFiles = Array.from( + { length: 200 }, + (_, i) => `docs/very/long/path/${i}-read-file.md`, + ); + const modifiedFiles = Array.from( + { length: 200 }, + (_, i) => `src/features/${i}/nested/component/file-${i}.ts`, + ); + + const section = formatFileOperations(readFiles, modifiedFiles); + + expect(section).toContain(""); + expect(section).toContain(""); + expect(section).toContain("...and "); + expect(section.length).toBeLessThanOrEqual(MAX_FILE_OPS_SECTION_CHARS); + }); + + it("caps final compaction summary with a truncation marker", () => { + const oversized = "x".repeat(MAX_COMPACTION_SUMMARY_CHARS + 500); + const capped = capCompactionSummary(oversized); + + expect(capped.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS); + expect(capped).toContain(SUMMARY_TRUNCATED_MARKER.trim()); + expect(capped.endsWith(SUMMARY_TRUNCATED_MARKER)).toBe(true); + }); + + it("preserves workspace critical rules suffix when capping", () => { + const suffix = + "\n\n\n## Session Startup\nRead AGENTS.md\n"; + const body = "x".repeat(MAX_COMPACTION_SUMMARY_CHARS); + + const capped = capCompactionSummaryPreservingSuffix(body, suffix); + + expect(capped.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS); + expect(capped).toContain(""); + expect(capped).toContain("## Session Startup"); + expect(capped.endsWith(suffix)).toBe(true); + }); + + it("preserves diagnostic sections (tool failures, file ops) when capping oversized body", () => { + const diagnosticSuffix = + "\n\n## Tool Failures\n- exec: failed\n\n\nfoo.ts\n\n\n" + + "\n## Session Startup\nRead AGENTS.md\n"; + const body = "x".repeat(MAX_COMPACTION_SUMMARY_CHARS); + + const capped = capCompactionSummaryPreservingSuffix(body, diagnosticSuffix); + + expect(capped.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS); + expect(capped).toContain("## Tool Failures"); + expect(capped).toContain(""); + expect(capped).toContain(""); + expect(capped.endsWith(diagnosticSuffix)).toBe(true); + }); + + it("keeps section separator when body ends without newline (e.g. buildStructuredFallbackSummary)", () => { + const bodyNoNewline = "## Exact identifiers\nNone."; + const suffixNoLeadingNewline = "## Tool Failures\n- exec: failed"; + + const capped = capCompactionSummaryPreservingSuffix( + bodyNoNewline, + `\n\n${suffixNoLeadingNewline}`, + ); + + expect(capped).toContain("None.\n\n## Tool Failures"); + expect(capped).not.toMatch(/None\.## Tool Failures/); + }); + + it("keeps body prefix when truncation marker cannot fit (tiny budget)", () => { + const body = "## Decisions\nKeep flow.\n## Constraints\nFollow rules."; + const tinyBudget = 10; // Smaller than SUMMARY_TRUNCATED_MARKER.length + const capped = capCompactionSummary(body, tinyBudget); + + expect(capped.length).toBeLessThanOrEqual(tinyBudget); + expect(capped).toContain("## Decis"); + expect(capped).not.toContain("[Compaction summary truncated"); + }); + + it("preserves tail sections when suffix exceeds cap (workspace rules, diagnostics over preserved turns)", () => { + const criticalTail = + "\n\n## Tool Failures\n- exec: failed\n\n\nfoo.ts\n\n\n" + + "\n## Session Startup\nRead AGENTS.md\n"; + const preservedTurns = + "## Recent turns preserved verbatim\n- User: x\n- Assistant: y\n" + + "x".repeat(MAX_COMPACTION_SUMMARY_CHARS); + const oversizedSuffix = preservedTurns + criticalTail; + + const capped = capCompactionSummaryPreservingSuffix("short body", oversizedSuffix); + + expect(capped.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS); + expect(capped).toContain(""); + expect(capped).toContain("## Tool Failures"); + expect(capped).toContain(""); + expect(capped).toContain("## Session Startup"); + }); +}); + describe("computeAdaptiveChunkRatio", () => { const CONTEXT_WINDOW = 200_000; @@ -1358,17 +1462,20 @@ describe("compaction-safeguard recent-turn preservation", () => { expect(secondCall?.customInstructions).toContain("latest_user_ask_not_reflected"); }); - it("keeps last successful summary when a quality retry call fails", async () => { + it("preserves split-turn and recent-turn suffixes when retry fallback is capped", async () => { mockSummarizeInStages.mockReset(); + const oversizedHistorySummary = "history detail ".repeat(MAX_COMPACTION_SUMMARY_CHARS); + const splitTurnPrefixSummary = "split-turn prefix context that must survive capping"; mockSummarizeInStages - .mockResolvedValueOnce("short summary missing headings") + .mockResolvedValueOnce(oversizedHistorySummary) + .mockResolvedValueOnce(splitTurnPrefixSummary) .mockRejectedValueOnce(new Error("retry transient failure")); const sessionManager = stubSessionManager(); const model = createAnthropicModelFixture(); setCompactionSafeguardRuntime(sessionManager, { model, - recentTurnsPreserve: 0, + recentTurnsPreserve: 1, qualityGuardEnabled: true, qualityGuardMaxRetries: 1, }); @@ -1384,8 +1491,16 @@ describe("compaction-safeguard recent-turn preservation", () => { messagesToSummarize: [ { role: "user", content: "older context", timestamp: 1 }, { role: "assistant", content: "older reply", timestamp: 2 } as unknown as AgentMessage, + { role: "user", content: "latest ask status", timestamp: 3 }, + { + role: "assistant", + content: [{ type: "text", text: "latest assistant reply" }], + timestamp: 4, + } as unknown as AgentMessage, + ], + turnPrefixMessages: [ + { role: "user", content: "prefix request that was split out", timestamp: 0 }, ], - turnPrefixMessages: [], firstKeptEntryId: "entry-1", tokensBefore: 1_500, fileOps: { @@ -1395,7 +1510,7 @@ describe("compaction-safeguard recent-turn preservation", () => { }, settings: { reserveTokens: 4_000 }, previousSummary: undefined, - isSplitTurn: false, + isSplitTurn: true, }, customInstructions: "", signal: new AbortController().signal, @@ -1407,8 +1522,15 @@ describe("compaction-safeguard recent-turn preservation", () => { }; expect(result.cancel).not.toBe(true); - expect(result.compaction?.summary).toContain("short summary missing headings"); - expect(mockSummarizeInStages).toHaveBeenCalledTimes(2); + const summary = result.compaction?.summary ?? ""; + expect(summary.length).toBeLessThanOrEqual(MAX_COMPACTION_SUMMARY_CHARS); + expect(summary).toContain(SUMMARY_TRUNCATED_MARKER); + expect(summary).toContain("**Turn Context (split turn):**"); + expect(summary).toContain(splitTurnPrefixSummary); + expect(summary).toContain("## Recent turns preserved verbatim"); + expect(summary).toContain("latest ask status"); + expect(summary).toContain("latest assistant reply"); + expect(mockSummarizeInStages).toHaveBeenCalledTimes(3); }); it("keeps required headings when all turns are preserved and history is carried forward", async () => { diff --git a/src/agents/pi-extensions/compaction-safeguard.ts b/src/agents/pi-extensions/compaction-safeguard.ts index 92332140656..d96fa4412ff 100644 --- a/src/agents/pi-extensions/compaction-safeguard.ts +++ b/src/agents/pi-extensions/compaction-safeguard.ts @@ -38,6 +38,10 @@ const TURN_PREFIX_INSTRUCTIONS = " early progress, and any details needed to understand the retained suffix."; const MAX_TOOL_FAILURES = 8; const MAX_TOOL_FAILURE_CHARS = 240; +const MAX_COMPACTION_SUMMARY_CHARS = 16_000; +const MAX_FILE_OPS_SECTION_CHARS = 2_000; +const MAX_FILE_OPS_LIST_CHARS = 900; +const SUMMARY_TRUNCATED_MARKER = "\n\n[Compaction summary truncated to fit budget]"; const DEFAULT_RECENT_TURNS_PRESERVE = 3; const DEFAULT_QUALITY_GUARD_MAX_RETRIES = 1; const MAX_RECENT_TURNS_PRESERVE = 12; @@ -194,17 +198,85 @@ function computeFileLists(fileOps: FileOperations): { } function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string { - const sections: string[] = []; - if (readFiles.length > 0) { - sections.push(`\n${readFiles.join("\n")}\n`); + function formatBoundedFileList(tag: string, files: string[], maxChars: number): string { + if (files.length === 0 || maxChars <= 0) { + return ""; + } + const openTag = `<${tag}>\n`; + const closeTag = `\n`; + const lines: string[] = []; + let usedChars = openTag.length + closeTag.length; + + for (let i = 0; i < files.length; i++) { + const line = `${files[i]}\n`; + const remaining = files.length - i - 1; + const overflowLine = remaining > 0 ? `...and ${remaining} more\n` : ""; + const projected = usedChars + line.length + overflowLine.length; + if (projected > maxChars) { + const overflow = `...and ${files.length - i} more\n`; + if (usedChars + overflow.length <= maxChars) { + lines.push(overflow); + } + break; + } + lines.push(line); + usedChars += line.length; + } + + return lines.length > 0 ? `${openTag}${lines.join("")}${closeTag}` : ""; } - if (modifiedFiles.length > 0) { - sections.push(`\n${modifiedFiles.join("\n")}\n`); + + const sections: string[] = []; + const readSection = formatBoundedFileList("read-files", readFiles, MAX_FILE_OPS_LIST_CHARS); + const modifiedSection = formatBoundedFileList( + "modified-files", + modifiedFiles, + MAX_FILE_OPS_LIST_CHARS, + ); + if (readSection) { + sections.push(readSection); + } + if (modifiedSection) { + sections.push(modifiedSection); } if (sections.length === 0) { return ""; } - return `\n\n${sections.join("\n\n")}`; + const combined = `\n\n${sections.join("\n\n")}`; + return capCompactionSummary(combined, MAX_FILE_OPS_SECTION_CHARS); +} + +function capCompactionSummary(summary: string, maxChars = MAX_COMPACTION_SUMMARY_CHARS): string { + if (maxChars <= 0 || summary.length <= maxChars) { + return summary; + } + const marker = SUMMARY_TRUNCATED_MARKER; + const budget = Math.max(0, maxChars - marker.length); + if (budget <= 0) { + // Marker cannot fit; keep body prefix instead of a partial marker fragment. + return summary.slice(0, maxChars); + } + return `${summary.slice(0, budget)}${marker}`; +} + +function capCompactionSummaryPreservingSuffix( + summaryBody: string, + suffix: string, + maxChars = MAX_COMPACTION_SUMMARY_CHARS, +): string { + if (!suffix) { + return capCompactionSummary(summaryBody, maxChars); + } + if (maxChars <= 0) { + return capCompactionSummary(`${summaryBody}${suffix}`, maxChars); + } + if (suffix.length >= maxChars) { + // Preserve tail (workspace rules, diagnostics) over head (preserved turns). + return suffix.slice(-maxChars); + } + const bodyBudget = Math.max(0, maxChars - suffix.length); + const cappedBody = capCompactionSummary(summaryBody, bodyBudget); + return `${cappedBody}${suffix}`; } function extractMessageText(message: AgentMessage): string { @@ -887,6 +959,8 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { const effectivePreviousSummary = droppedSummary ?? preparation.previousSummary; let summary = ""; + let lastHistorySummary = ""; + let lastSplitTurnSection = ""; let currentInstructions = structuredInstructions; const totalAttempts = qualityGuardEnabled ? qualityGuardMaxRetries + 1 : 1; let lastSuccessfulSummary: string | null = null; @@ -894,8 +968,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { for (let attempt = 0; attempt < totalAttempts; attempt += 1) { let summaryWithoutPreservedTurns = ""; let summaryWithPreservedTurns = ""; + let splitTurnSection = ""; + let historySummary = ""; try { - const historySummary = + historySummary = messagesToSummarize.length > 0 ? await summarizeInStages({ messages: messagesToSummarize, @@ -928,7 +1004,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { summarizationInstructions, previousSummary: undefined, }); - const splitTurnSection = `**Turn Context (split turn):**\n\n${prefixSummary}`; + splitTurnSection = `**Turn Context (split turn):**\n\n${prefixSummary}`; summaryWithoutPreservedTurns = historySummary.trim() ? `${historySummary}\n\n---\n\n${splitTurnSection}` : splitTurnSection; @@ -951,6 +1027,8 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { throw attemptError; } lastSuccessfulSummary = summaryWithPreservedTurns; + lastHistorySummary = historySummary; + lastSplitTurnSection = splitTurnSection; const canRegenerate = messagesToSummarize.length > 0 || @@ -983,14 +1061,31 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { : `${structuredInstructions}\n\n${qualityFeedbackInstruction}`; } - summary = appendSummarySection(summary, toolFailureSection); - summary = appendSummarySection(summary, fileOpsSummary); - - // Append workspace critical context (Session Startup + Red Lines from AGENTS.md) + // Cap the main history body first, then append split-turn context, preserved + // turns, diagnostics, and workspace rules so they survive truncation. + // Truncation keeps the prefix (slice(0, budget)), so sections at the end + // of the body would be dropped first—split-turn, preserved turns, tool + // failures, and file ops must be in the suffix. + const reservedSuffix = appendSummarySection( + appendSummarySection( + appendSummarySection( + appendSummarySection("", lastSplitTurnSection), + preservedTurnsSection, + ), + toolFailureSection, + ), + fileOpsSummary, + ); const workspaceContext = await readWorkspaceContextForSummary(); - if (workspaceContext) { - summary = appendSummarySection(summary, workspaceContext); - } + const fullReservedSuffix = appendSummarySection(reservedSuffix, workspaceContext); + // Ensure leading separator so suffix does not merge with body (e.g. when body + // ends without newline from buildStructuredFallbackSummary: "...## Exact identifiers## Tool Failures"). + const normalizedSuffix = + fullReservedSuffix && !/^\s/.test(fullReservedSuffix) + ? `\n\n${fullReservedSuffix}` + : fullReservedSuffix; + const bodyToCap = lastHistorySummary || summary; + summary = capCompactionSummaryPreservingSuffix(bodyToCap, normalizedSuffix ?? ""); return { compaction: { @@ -1023,10 +1118,17 @@ export const __testing = { resolveQualityGuardMaxRetries, extractOpaqueIdentifiers, auditSummaryQuality, + capCompactionSummary, + capCompactionSummaryPreservingSuffix, + formatFileOperations, computeAdaptiveChunkRatio, isOversizedForSummary, readWorkspaceContextForSummary, BASE_CHUNK_RATIO, MIN_CHUNK_RATIO, SAFETY_MARGIN, + MAX_COMPACTION_SUMMARY_CHARS, + MAX_FILE_OPS_SECTION_CHARS, + MAX_FILE_OPS_LIST_CHARS, + SUMMARY_TRUNCATED_MARKER, } as const; diff --git a/src/memory/embeddings-openai.ts b/src/memory/embeddings-openai.ts index da7ab3f8d51..dd92d770f2f 100644 --- a/src/memory/embeddings-openai.ts +++ b/src/memory/embeddings-openai.ts @@ -21,6 +21,8 @@ const OPENAI_MAX_INPUT_TOKENS: Record = { "text-embedding-ada-002": 8191, }; +export const DEFAULT_OPENAI_EMBEDDING_MODEL = OPENAI_DEFAULT_EMBEDDING_MODEL; + export function normalizeOpenAiModel(model: string): string { return normalizeEmbeddingModelWithPrefixes({ model, diff --git a/src/plugin-sdk/provider-models.ts b/src/plugin-sdk/provider-models.ts index e38c02138bb..1717bc3c612 100644 --- a/src/plugin-sdk/provider-models.ts +++ b/src/plugin-sdk/provider-models.ts @@ -31,7 +31,12 @@ export { applyGoogleGeminiModelDefault, GOOGLE_GEMINI_DEFAULT_MODEL, } from "../plugins/provider-model-defaults.js"; -export { applyOpenAIConfig, OPENAI_DEFAULT_MODEL } from "../plugins/provider-model-defaults.js"; +export { + applyOpenAIConfig, + OPENAI_CODEX_DEFAULT_MODEL, + OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL, + OPENAI_DEFAULT_MODEL, +} from "../plugins/provider-model-defaults.js"; export { OPENCODE_GO_DEFAULT_MODEL_REF } from "../plugins/provider-model-defaults.js"; export { OPENCODE_ZEN_DEFAULT_MODEL } from "../plugins/provider-model-defaults.js"; export { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js"; diff --git a/src/plugins/provider-model-defaults.ts b/src/plugins/provider-model-defaults.ts index 079ce97b70d..9c53df8ff08 100644 --- a/src/plugins/provider-model-defaults.ts +++ b/src/plugins/provider-model-defaults.ts @@ -1,10 +1,18 @@ import type { OpenClawConfig } from "../config/config.js"; -import { OPENAI_DEFAULT_MODEL } from "../providers/openai-defaults.js"; +import { + OPENAI_CODEX_DEFAULT_MODEL, + OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL, + OPENAI_DEFAULT_MODEL, +} from "../providers/openai-defaults.js"; import { ensureModelAllowlistEntry } from "./provider-model-allowlist.js"; import { applyAgentDefaultPrimaryModel } from "./provider-model-primary.js"; export const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview"; -export { OPENAI_DEFAULT_MODEL } from "../providers/openai-defaults.js"; +export { + OPENAI_CODEX_DEFAULT_MODEL, + OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL, + OPENAI_DEFAULT_MODEL, +} from "../providers/openai-defaults.js"; export const OPENCODE_GO_DEFAULT_MODEL_REF = "opencode-go/kimi-k2.5"; export const OPENCODE_ZEN_DEFAULT_MODEL = "opencode/claude-opus-4-6"; diff --git a/src/tts/tts.ts b/src/tts/tts.ts index 5d52ddae43b..be00ae6c1d5 100644 --- a/src/tts/tts.ts +++ b/src/tts/tts.ts @@ -25,6 +25,10 @@ import type { import { logVerbose } from "../globals.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { stripMarkdown } from "../line/markdown-to-line.js"; +import { + OPENAI_DEFAULT_TTS_MODEL as DEFAULT_OPENAI_MODEL, + OPENAI_DEFAULT_TTS_VOICE as DEFAULT_OPENAI_VOICE, +} from "../providers/openai-defaults.js"; import { CONFIG_DIR, resolveUserPath } from "../utils.js"; import { getSpeechProvider, @@ -32,10 +36,6 @@ import { normalizeSpeechProviderId, } from "./provider-registry.js"; import type { SpeechVoiceOption } from "./provider-types.js"; -import { - OPENAI_DEFAULT_TTS_MODEL as DEFAULT_OPENAI_MODEL, - OPENAI_DEFAULT_TTS_VOICE as DEFAULT_OPENAI_VOICE, -} from "../providers/openai-defaults.js"; import { DEFAULT_OPENAI_BASE_URL, isValidOpenAIModel,