fix: retain native compaction auth fallback
(cherry picked from commit 164d77ee90ed109b62ed7f1b6afcf644d26abe12)
This commit is contained in:
@@ -163,7 +163,7 @@
|
||||
- Added a selectable voice setting for `/live` realtime sessions ([#6566](https://github.com/can1357/oh-my-pi/issues/6566)).
|
||||
### Fixed
|
||||
|
||||
- Native compaction now keeps implicit role and largest-context fallbacks on the active provider, preventing a provider-native request from silently becoming another provider's generic summary. Explicit compaction models and soft compaction retain their existing fallback behavior.
|
||||
- Native compaction preserves provider-native success and non-authentication failure semantics while retaining authenticated cross-provider fallback when the native provider rejects credentials.
|
||||
|
||||
## [17.1.4] - 2026-07-26
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
type SummaryOptions,
|
||||
shouldCompact,
|
||||
shouldUseOpenAiRemoteCompaction,
|
||||
shouldUseCompactionV2Streaming,
|
||||
} from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import {
|
||||
DEFAULT_PRUNE_CONFIG,
|
||||
@@ -568,7 +567,6 @@ export class SessionMaintenance {
|
||||
let compactionCandidates = this.#getCompactionModelCandidates(
|
||||
availableModels,
|
||||
requireProviderRemote ? shouldUseOpenAiRemoteCompaction : undefined,
|
||||
effectiveSettings,
|
||||
);
|
||||
if (requireProviderRemote && compactionCandidates.length === 0) {
|
||||
this.#host.emitNotice(
|
||||
@@ -576,7 +574,7 @@ export class SessionMaintenance {
|
||||
`remote compaction is unavailable for ${this.#model.id} (no remote endpoint configured and no provider-native remote-capable model in the fallback chain) — using a local summary instead`,
|
||||
"compaction",
|
||||
);
|
||||
compactionCandidates = this.#getCompactionModelCandidates(availableModels, undefined, effectiveSettings);
|
||||
compactionCandidates = this.#getCompactionModelCandidates(availableModels);
|
||||
}
|
||||
const pathEntries = this.#host.sessionManager.getBranch();
|
||||
const preparation = prepareCompaction(pathEntries, effectiveSettings, this.#model);
|
||||
@@ -1392,73 +1390,46 @@ export class SessionMaintenance {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
#getCompactionModelCandidates(
|
||||
availableModels: Model[],
|
||||
filter: ((model: Model) => boolean) | undefined,
|
||||
settings: Pick<CompactionSettings, "remoteEnabled" | "remoteStreamingV2Enabled">,
|
||||
): Model[] {
|
||||
return this.resolveCompactionModelCandidates(
|
||||
this.#model,
|
||||
availableModels,
|
||||
filter,
|
||||
settings.remoteEnabled !== false,
|
||||
settings.remoteStreamingV2Enabled !== false,
|
||||
);
|
||||
#getCompactionModelCandidates(availableModels: Model[], filter?: (model: Model) => boolean): Model[] {
|
||||
return this.resolveCompactionModelCandidates(this.#model, availableModels, filter);
|
||||
}
|
||||
|
||||
resolveCompactionModelCandidates(
|
||||
preferredModel: Model | null | undefined,
|
||||
availableModels: Model[],
|
||||
filter?: (model: Model) => boolean,
|
||||
remoteEnabled = this.#host.settings.getGroup("compaction").remoteEnabled !== false,
|
||||
remoteStreamingV2Enabled =
|
||||
this.#host.settings.getGroup("compaction").remoteStreamingV2Enabled !== false,
|
||||
): Model[] {
|
||||
const candidates: Model[] = [];
|
||||
const seen = new Set<string>();
|
||||
const hasEffectiveNativeCompaction = (model: Model): boolean =>
|
||||
remoteEnabled &&
|
||||
(shouldUseOpenAiRemoteCompaction(model) ||
|
||||
(remoteStreamingV2Enabled && shouldUseCompactionV2Streaming(model)));
|
||||
const nativeProvider =
|
||||
preferredModel && hasEffectiveNativeCompaction(preferredModel) ? preferredModel.provider : undefined;
|
||||
|
||||
const addCandidate = (model: Model | undefined, source: "explicit" | "current" | "implicit"): void => {
|
||||
const addCandidate = (model: Model | undefined): void => {
|
||||
if (!model) return;
|
||||
const key = `${model.provider}/${model.id}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
// Explicit targets and the active model retain their established
|
||||
// semantics. Implicit role/context fallbacks must not turn a native
|
||||
// compaction request into a different provider's generic summary.
|
||||
if (
|
||||
source === "implicit" &&
|
||||
nativeProvider !== undefined &&
|
||||
(model.provider !== nativeProvider || !hasEffectiveNativeCompaction(model))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// `seen` still tracks rejected models so the largest-context fallback
|
||||
// scan below doesn't reintroduce them; the filter just suppresses
|
||||
// inclusion in this caller's candidate chain.
|
||||
if (filter && !filter(model)) return;
|
||||
candidates.push(model);
|
||||
};
|
||||
|
||||
if (preferredModel) {
|
||||
addCandidate(resolveCompactionConfiguredTarget(preferredModel, availableModels), "explicit");
|
||||
addCandidate(resolveCompactionConfiguredTarget(preferredModel, availableModels));
|
||||
}
|
||||
addCandidate(preferredModel ?? undefined, "current");
|
||||
addCandidate(preferredModel ?? undefined);
|
||||
for (const role of MODEL_ROLE_IDS) {
|
||||
addCandidate(
|
||||
resolveRoleModelFull(this.#host.settings, role, availableModels, preferredModel ?? undefined).model,
|
||||
"implicit",
|
||||
);
|
||||
}
|
||||
|
||||
const sortedByContext = [...availableModels].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
|
||||
for (const model of sortedByContext) {
|
||||
if (seen.has(`${model.provider}/${model.id}`)) continue;
|
||||
const candidateCount = candidates.length;
|
||||
addCandidate(model, "implicit");
|
||||
if (candidates.length > candidateCount) break;
|
||||
if (!seen.has(`${model.provider}/${model.id}`)) {
|
||||
addCandidate(model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
@@ -1485,8 +1456,7 @@ export class SessionMaintenance {
|
||||
precomputedCandidates?: Model[],
|
||||
): Promise<CompactionResult> {
|
||||
const candidates =
|
||||
precomputedCandidates ??
|
||||
this.#getCompactionModelCandidates(this.#host.modelRegistry.getAvailable(), undefined, preparation.settings);
|
||||
precomputedCandidates ?? this.#getCompactionModelCandidates(this.#host.modelRegistry.getAvailable());
|
||||
const telemetry = resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId());
|
||||
|
||||
for (const candidate of candidates) {
|
||||
@@ -2502,7 +2472,7 @@ export class SessionMaintenance {
|
||||
details = snapcompactResult.details;
|
||||
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
||||
} else {
|
||||
const candidates = this.#getCompactionModelCandidates(availableModels, undefined, compactionSettings);
|
||||
const candidates = this.#getCompactionModelCandidates(availableModels);
|
||||
const retrySettings = this.#host.settings.getGroup("retry");
|
||||
const telemetry = resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId());
|
||||
let compactResult: CompactionResult | undefined;
|
||||
|
||||
@@ -37,13 +37,7 @@ describe("issue #986 compaction auth fallback", () => {
|
||||
throw new Error("Expected bundled test models to exist");
|
||||
}
|
||||
|
||||
const settings = Settings.isolated({
|
||||
"compaction.keepRecentTokens": 1,
|
||||
"compaction.strategy": "context-full",
|
||||
// This suite covers the portable summarizer's auth fallback. Native
|
||||
// compaction keeps its implicit candidate chain provider-isolated.
|
||||
"compaction.remoteEnabled": false,
|
||||
});
|
||||
const settings = Settings.isolated({ "compaction.keepRecentTokens": 1, "compaction.strategy": "context-full" });
|
||||
if (options?.fallbackModelRole) {
|
||||
settings.setModelRole(options.fallbackModelRole, `${fallbackModel.provider}/${fallbackModel.id}`);
|
||||
}
|
||||
@@ -87,7 +81,7 @@ describe("issue #986 compaction auth fallback", () => {
|
||||
return { currentModel, fallbackModel };
|
||||
}
|
||||
|
||||
it("falls back to an authenticated role model when the current provider returns auth_unavailable", async () => {
|
||||
it("falls back across providers when native compaction returns auth_unavailable", async () => {
|
||||
const { currentModel, fallbackModel } = await createSession({ fallbackModelRole: "smol" });
|
||||
const compactSpy = vi.spyOn(compactionModule, "compact").mockImplementation(async (preparation, model) => {
|
||||
if (model.provider === currentModel.provider && model.id === currentModel.id) {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import {
|
||||
SessionMaintenance,
|
||||
type SessionMaintenanceHost,
|
||||
} from "@oh-my-pi/pi-coding-agent/session/session-maintenance";
|
||||
|
||||
function model(
|
||||
id: string,
|
||||
provider: string,
|
||||
contextWindow: number,
|
||||
remoteCompaction?: Model["remoteCompaction"],
|
||||
): Model {
|
||||
return buildModel({
|
||||
id,
|
||||
name: id,
|
||||
api: "openai-responses",
|
||||
provider,
|
||||
baseUrl: "https://example.test/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: 4096,
|
||||
remoteCompaction,
|
||||
});
|
||||
}
|
||||
|
||||
function maintenance(settings: Settings): SessionMaintenance {
|
||||
return new SessionMaintenance({ settings } as SessionMaintenanceHost);
|
||||
}
|
||||
|
||||
describe("native compaction provider isolation", () => {
|
||||
it("keeps implicit role and context fallbacks on the current native provider", () => {
|
||||
const settings = Settings.isolated();
|
||||
const current = model("native-current", "native-provider", 100_000, { enabled: true });
|
||||
const genericRole = model("generic-role", "generic-provider", 90_000);
|
||||
const nativeFallback = model("native-fallback", "native-provider", 80_000, { enabled: true });
|
||||
settings.setModelRole("smol", `${genericRole.provider}/${genericRole.id}`);
|
||||
|
||||
const candidates = maintenance(settings).resolveCompactionModelCandidates(
|
||||
current,
|
||||
[current, genericRole, nativeFallback],
|
||||
undefined,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(candidates.map(candidate => `${candidate.provider}/${candidate.id}`)).toEqual([
|
||||
"native-provider/native-current",
|
||||
"native-provider/native-fallback",
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains generic implicit fallbacks when provider-native compaction is disabled", () => {
|
||||
const settings = Settings.isolated();
|
||||
const current = model("native-current", "native-provider", 100_000, { enabled: true });
|
||||
const genericRole = model("generic-role", "generic-provider", 90_000);
|
||||
settings.setModelRole("smol", `${genericRole.provider}/${genericRole.id}`);
|
||||
|
||||
const candidates = maintenance(settings).resolveCompactionModelCandidates(
|
||||
current,
|
||||
[current, genericRole],
|
||||
undefined,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(candidates.map(candidate => `${candidate.provider}/${candidate.id}`)).toEqual([
|
||||
"native-provider/native-current",
|
||||
"generic-provider/generic-role",
|
||||
]);
|
||||
});
|
||||
|
||||
it("recognizes V2-only native capability under the effective streaming setting", () => {
|
||||
const settings = Settings.isolated();
|
||||
const current = model("v2-current", "v2-provider", 100_000, { v2StreamingEnabled: true });
|
||||
const genericRole = model("generic-role", "generic-provider", 90_000);
|
||||
const nativeFallback = model("v2-fallback", "v2-provider", 80_000, { v2StreamingEnabled: true });
|
||||
settings.setModelRole("smol", `${genericRole.provider}/${genericRole.id}`);
|
||||
|
||||
const candidates = maintenance(settings).resolveCompactionModelCandidates(
|
||||
current,
|
||||
[current, genericRole, nativeFallback],
|
||||
undefined,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(candidates.map(candidate => `${candidate.provider}/${candidate.id}`)).toEqual([
|
||||
"v2-provider/v2-current",
|
||||
"v2-provider/v2-fallback",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user