fix: preserve native compaction auth errors

(cherry picked from commit ab8a823e1de425735825392db7d98b33905f0dc3)
This commit is contained in:
usr-bin-roygbiv
2026-07-26 21:35:13 +00:00
committed by can1357
parent a8349d4dea
commit 2fe31c61b6
2 changed files with 40 additions and 20 deletions
+4 -2
View File
@@ -38,6 +38,7 @@ import {
getOpenAIResponsesHistoryPayload,
normalizeResponsesToolCallId,
} from "@oh-my-pi/pi-ai/utils";
import { captureOpenAIHttpError } from "@oh-my-pi/pi-ai/utils/openai-http";
import {
CODEX_BASE_URL,
getCodexAccountId,
@@ -840,18 +841,19 @@ export async function requestOpenAiRemoteCompaction(
});
if (!response.ok) {
const errorText = await response.text().catch(() => "");
const cause = await captureOpenAIHttpError(response);
logger.warn("OpenAI remote compaction failed", {
endpoint,
status: response.status,
statusText: response.statusText,
errorText,
errorText: cause.captured.bodyText ?? "",
});
throw new ProviderHttpError(
`Remote compaction failed (${response.status} ${response.statusText})`,
response.status,
{
headers: response.headers,
cause,
},
);
}
@@ -81,25 +81,42 @@ describe("issue #986 compaction auth fallback", () => {
return { currentModel, fallbackModel };
}
it("falls back across providers when native compaction returns auth_unavailable", async () => {
it("falls back across providers when native compaction receives 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) {
throw new Error(
"Turn prefix summarization failed: 503 auth_unavailable: no auth available (providers=codex, model=gpt-5.4-mini)",
);
}
if (model.provider !== fallbackModel.provider || model.id !== fallbackModel.id) {
throw new Error(`Unexpected compaction model ${model.provider}/${model.id}`);
}
return {
summary: "fallback summary",
shortSummary: "fallback short summary",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: 42,
details: { provider: model.provider },
};
});
const originalCompact = compactionModule.compact;
const fetchMock = vi.fn(async () =>
Response.json(
{ error: { type: "auth_unavailable", message: "no auth available for codex" } },
{ status: 503, statusText: "Service Unavailable" },
),
);
const compactSpy = vi
.spyOn(compactionModule, "compact")
.mockImplementation(async (preparation, model, apiKey, customInstructions, signal, options) => {
if (model.provider === currentModel.provider && model.id === currentModel.id) {
return originalCompact(
{
...preparation,
settings: { ...preparation.settings, remoteStreamingV2Enabled: false },
},
model,
apiKey,
customInstructions,
signal,
{ ...options, fetch: fetchMock },
);
}
if (model.provider !== fallbackModel.provider || model.id !== fallbackModel.id) {
throw new Error(`Unexpected compaction model ${model.provider}/${model.id}`);
}
return {
summary: "fallback summary",
shortSummary: "fallback short summary",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: 42,
details: { provider: model.provider },
};
});
vi.spyOn(modelRegistry, "getApiKey").mockImplementation(async model => {
if (model.provider === currentModel.provider && model.id === currentModel.id) return "codex-token";
if (model.provider === fallbackModel.provider && model.id === fallbackModel.id) return "anthropic-token";
@@ -109,6 +126,7 @@ describe("issue #986 compaction auth fallback", () => {
const result = await session.compact();
expect(result.summary).toBe("fallback summary");
expect(fetchMock).toHaveBeenCalled();
expect(compactSpy).toHaveBeenCalledTimes(2);
expect(compactSpy.mock.calls.map(([, model]) => `${model.provider}/${model.id}`)).toEqual([
`${currentModel.provider}/${currentModel.id}`,