diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index dcbd68a54..d85974302 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `provider.appendOnlyContext: "auto"` staying inactive for Xiaomi Token Plan/SGLang endpoints, preserving prefix-cache hits without forcing append-only mode globally ([#1851](https://github.com/can1357/oh-my-pi/issues/1851)). + ## [15.9.0] - 2026-06-04 ### Breaking Changes diff --git a/packages/coding-agent/src/config/append-only-context-mode.ts b/packages/coding-agent/src/config/append-only-context-mode.ts new file mode 100644 index 000000000..0efb1a8dd --- /dev/null +++ b/packages/coding-agent/src/config/append-only-context-mode.ts @@ -0,0 +1,37 @@ +/** Provider metadata needed to resolve append-only context mode. */ +export interface AppendOnlyContextModel { + provider: string; + baseUrl: string; + compat?: object; +} + +function isXiaomiHost(baseUrl: string): boolean { + try { + const host = new URL(baseUrl).hostname; + return host === "xiaomimimo.com" || host.endsWith(".xiaomimimo.com"); + } catch { + return false; + } +} + +function shouldAutoEnableAppendOnlyContext(model: AppendOnlyContextModel | null | undefined): boolean { + if (!model) return false; + if (model.provider === "deepseek") return true; + if (isXiaomiHost(model.baseUrl)) return true; + return !!model.compat && "supportsStore" in model.compat && model.compat.supportsStore === true; +} + +/** Resolves whether append-only context should be active for a model and setting. */ +export function shouldEnableAppendOnlyContext( + setting: "auto" | "on" | "off" | undefined, + model: AppendOnlyContextModel | null | undefined, +): boolean { + switch (setting ?? "auto") { + case "on": + return true; + case "off": + return false; + default: + return shouldAutoEnableAppendOnlyContext(model); + } +} diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index d6661967e..277d70eba 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -3057,9 +3057,9 @@ export const SETTINGS_SCHEMA = { tab: "providers", label: "Append-Only Context", description: - "Cache system prompt + tool specs and keep an append-only message log so provider prefix caches (DeepSeek, Anthropic) hit at maximum rate. Auto enables for DeepSeek.", + "Cache system prompt + tool specs and keep an append-only message log so provider prefix caches (DeepSeek, Xiaomi/SGLang, Anthropic) hit at maximum rate. Auto enables for known prefix-cache providers.", options: [ - { value: "auto", label: "Auto", description: "Enable for DeepSeek (recommended)" }, + { value: "auto", label: "Auto", description: "Enable for known prefix-cache providers (recommended)" }, { value: "on", label: "On", description: "Always enable append-only context" }, { value: "off", label: "Off", description: "Disable append-only context" }, ], diff --git a/packages/coding-agent/src/modes/controllers/command-controller.ts b/packages/coding-agent/src/modes/controllers/command-controller.ts index 60552ecb5..6cfce5c9e 100644 --- a/packages/coding-agent/src/modes/controllers/command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/command-controller.ts @@ -13,6 +13,7 @@ import { import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui"; import { formatDuration, Snowflake } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; +import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode"; import { loadCustomShare } from "../../export/custom-share"; import type { CompactOptions } from "../../extensibility/extensions/types"; import { @@ -397,10 +398,10 @@ export class CommandController { // Append-only context { const setting = this.ctx.settings.get("provider.appendOnlyContext") ?? "auto"; - const provider = this.ctx.session.model?.provider; - const mode = setting === "on" ? true : setting === "off" ? false : provider === "deepseek"; + const model = this.ctx.session.model; + const mode = shouldEnableAppendOnlyContext(setting, model); const activeLabel = mode ? theme.fg("success", "active") : theme.fg("dim", "inactive"); - const settingLabel = setting === "auto" ? `${setting} (${provider ?? "?"})` : setting; + const settingLabel = setting === "auto" ? `${setting} (${model?.provider ?? "?"})` : setting; info += `${theme.fg("dim", "Append-Only:")} ${activeLabel} (setting: ${settingLabel})\n`; } info += `${theme.bold("Tokens")}\n`; diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 53473ab9f..dbcbf078d 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -39,6 +39,7 @@ import { createAutoresearchExtension } from "./autoresearch"; import { loadCapability } from "./capability"; import { type Rule, ruleCapability, setActiveRules } from "./capability/rule"; import { bucketRules } from "./capability/rule-buckets"; +import { shouldEnableAppendOnlyContext } from "./config/append-only-context-mode"; import { ModelRegistry } from "./config/model-registry"; import { formatModelString, @@ -646,24 +647,6 @@ function registerPythonCleanup(): void { postmortem.register("python-cleanup", disposeAllKernelSessions); } -/** - * Resolve whether to enable append-only context mode based on the setting and provider. - * - * - `"on"` → always enable - * - `"off"` → never enable - * - `"auto"` → enable for DeepSeek (prefix-caching provider) - */ -function resolveAppendOnlyMode(setting: "auto" | "on" | "off" | undefined, provider: string): boolean { - switch (setting ?? "auto") { - case "on": - return true; - case "off": - return false; - default: - return provider === "deepseek"; - } -} - function customToolToDefinition(tool: CustomTool): ToolDefinition { const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = { name: tool.name, @@ -2028,7 +2011,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} getToolChoice: () => session?.nextToolChoice(), telemetry: options.telemetry, appendOnlyContext: model - ? resolveAppendOnlyMode(settings.get("provider.appendOnlyContext"), model.provider) + ? shouldEnableAppendOnlyContext(settings.get("provider.appendOnlyContext"), model) ? new AppendOnlyContextManager() : undefined : undefined, diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 2691b2167..c1891819d 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -100,6 +100,7 @@ import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../a import { classifyDifficulty } from "../auto-thinking/classifier"; import { reset as resetCapabilities } from "../capability"; import type { Rule } from "../capability/rule"; +import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode"; import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry"; import { extractExplicitThinkingSelector, @@ -6735,8 +6736,8 @@ export class AgentSession { */ #syncAppendOnlyContext(model: Model | null | undefined): void { const setting = this.settings.get("provider.appendOnlyContext") ?? "auto"; + const enable = shouldEnableAppendOnlyContext(setting, model); const providerId = model?.provider; - const enable = setting === "on" || (setting === "auto" && providerId === "deepseek"); const prev = this.#lastAppendOnlyResolution; if (prev && prev.enable === enable && prev.providerId === providerId) return; this.#lastAppendOnlyResolution = { enable, providerId }; diff --git a/packages/coding-agent/test/append-only-context-mode.test.ts b/packages/coding-agent/test/append-only-context-mode.test.ts new file mode 100644 index 000000000..381c3215c --- /dev/null +++ b/packages/coding-agent/test/append-only-context-mode.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test"; +import { shouldEnableAppendOnlyContext } from "@oh-my-pi/pi-coding-agent/config/append-only-context-mode"; + +const XIAOMI_TOKEN_PLAN_ANTHROPIC = { + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic", +}; + +const GENERIC_PROXY = { + provider: "generic-proxy", + baseUrl: "https://llm.example.com/v1", +}; + +describe("shouldEnableAppendOnlyContext", () => { + test("honors explicit on and off settings", () => { + expect(shouldEnableAppendOnlyContext("on", GENERIC_PROXY)).toBe(true); + expect(shouldEnableAppendOnlyContext("off", { provider: "deepseek", baseUrl: "https://api.deepseek.com" })).toBe( + false, + ); + }); + + test("auto enables for DeepSeek", () => { + expect(shouldEnableAppendOnlyContext("auto", { provider: "deepseek", baseUrl: "https://api.deepseek.com" })).toBe( + true, + ); + }); + + test("auto enables for Xiaomi Token Plan SGLang HiCache endpoints", () => { + expect(shouldEnableAppendOnlyContext("auto", XIAOMI_TOKEN_PLAN_ANTHROPIC)).toBe(true); + }); + + test("auto enables when model compat explicitly supports stored requests", () => { + expect( + shouldEnableAppendOnlyContext("auto", { + ...GENERIC_PROXY, + compat: { supportsStore: true }, + }), + ).toBe(true); + }); + + test("auto remains off for unknown providers without prefix-cache signals", () => { + expect(shouldEnableAppendOnlyContext("auto", GENERIC_PROXY)).toBe(false); + }); +});