What: - disable tool-call id sanitization for OpenAI/OpenAI Codex transcript policy - gate id sanitization in image sanitizer to full mode only - keep orphan reasoning downgrade scoped to OpenAI model-switch replay path - update transcript policy, session-history, sanitizer, and reasoning replay tests - document OpenAI model-switch orphan-reasoning cleanup behavior in transcript hygiene reference Why: - OpenAI Responses replay depends on canonical call_id|fc_id pairings for reasoning followers - strict id rewriting in OpenAI path breaks follower matching and triggers rs_* orphan 400s - limiting scope avoids behavior expansion while fixing the identified regression Tests: - pnpm vitest run src/agents/transcript-policy.test.ts src/agents/pi-embedded-runner.sanitize-session-history.test.ts src/agents/openai-responses.reasoning-replay.test.ts - pnpm vitest run --config vitest.e2e.config.ts src/agents/transcript-policy.e2e.test.ts src/agents/pi-embedded-runner.sanitize-session-history.e2e.test.ts src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.e2e.test.ts src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts - pnpm lint - pnpm format:check - pnpm check:docs - pnpm test (fails in current macOS bash 3.2 env at test/git-hooks-pre-commit.integration.test.ts: mapfile not found)
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
import type { ToolCallIdMode } from "./tool-call-id.js";
|
|
import { normalizeProviderId } from "./model-selection.js";
|
|
import { isAntigravityClaude, isGoogleModelApi } from "./pi-embedded-helpers/google.js";
|
|
|
|
export type TranscriptSanitizeMode = "full" | "images-only";
|
|
|
|
export type TranscriptPolicy = {
|
|
sanitizeMode: TranscriptSanitizeMode;
|
|
sanitizeToolCallIds: boolean;
|
|
toolCallIdMode?: ToolCallIdMode;
|
|
repairToolUseResultPairing: boolean;
|
|
preserveSignatures: boolean;
|
|
sanitizeThoughtSignatures?: {
|
|
allowBase64Only?: boolean;
|
|
includeCamelCase?: boolean;
|
|
};
|
|
normalizeAntigravityThinkingBlocks: boolean;
|
|
applyGoogleTurnOrdering: boolean;
|
|
validateGeminiTurns: boolean;
|
|
validateAnthropicTurns: boolean;
|
|
allowSyntheticToolResults: boolean;
|
|
};
|
|
|
|
const MISTRAL_MODEL_HINTS = [
|
|
"mistral",
|
|
"mixtral",
|
|
"codestral",
|
|
"pixtral",
|
|
"devstral",
|
|
"ministral",
|
|
"mistralai",
|
|
];
|
|
const OPENAI_MODEL_APIS = new Set([
|
|
"openai",
|
|
"openai-completions",
|
|
"openai-responses",
|
|
"openai-codex-responses",
|
|
]);
|
|
const OPENAI_PROVIDERS = new Set(["openai", "openai-codex"]);
|
|
|
|
function isOpenAiApi(modelApi?: string | null): boolean {
|
|
if (!modelApi) {
|
|
return false;
|
|
}
|
|
return OPENAI_MODEL_APIS.has(modelApi);
|
|
}
|
|
|
|
function isOpenAiProvider(provider?: string | null): boolean {
|
|
if (!provider) {
|
|
return false;
|
|
}
|
|
return OPENAI_PROVIDERS.has(normalizeProviderId(provider));
|
|
}
|
|
|
|
function isAnthropicApi(modelApi?: string | null, provider?: string | null): boolean {
|
|
if (modelApi === "anthropic-messages") {
|
|
return true;
|
|
}
|
|
const normalized = normalizeProviderId(provider ?? "");
|
|
// MiniMax now uses openai-completions API, not anthropic-messages
|
|
return normalized === "anthropic";
|
|
}
|
|
|
|
function isMistralModel(params: { provider?: string | null; modelId?: string | null }): boolean {
|
|
const provider = normalizeProviderId(params.provider ?? "");
|
|
if (provider === "mistral") {
|
|
return true;
|
|
}
|
|
const modelId = (params.modelId ?? "").toLowerCase();
|
|
if (!modelId) {
|
|
return false;
|
|
}
|
|
return MISTRAL_MODEL_HINTS.some((hint) => modelId.includes(hint));
|
|
}
|
|
|
|
export function resolveTranscriptPolicy(params: {
|
|
modelApi?: string | null;
|
|
provider?: string | null;
|
|
modelId?: string | null;
|
|
}): TranscriptPolicy {
|
|
const provider = normalizeProviderId(params.provider ?? "");
|
|
const modelId = params.modelId ?? "";
|
|
const isGoogle = isGoogleModelApi(params.modelApi);
|
|
const isAnthropic = isAnthropicApi(params.modelApi, provider);
|
|
const isOpenAi = isOpenAiProvider(provider) || (!provider && isOpenAiApi(params.modelApi));
|
|
const isMistral = isMistralModel({ provider, modelId });
|
|
const isOpenRouterGemini =
|
|
(provider === "openrouter" || provider === "opencode") &&
|
|
modelId.toLowerCase().includes("gemini");
|
|
const isAntigravityClaudeModel = isAntigravityClaude({
|
|
api: params.modelApi,
|
|
provider,
|
|
modelId,
|
|
});
|
|
|
|
const needsNonImageSanitize = isGoogle || isAnthropic || isMistral || isOpenRouterGemini;
|
|
|
|
const sanitizeToolCallIds = isGoogle || isMistral || isAnthropic;
|
|
const toolCallIdMode: ToolCallIdMode | undefined = isMistral
|
|
? "strict9"
|
|
: sanitizeToolCallIds
|
|
? "strict"
|
|
: undefined;
|
|
const repairToolUseResultPairing = isGoogle || isAnthropic;
|
|
const sanitizeThoughtSignatures = isOpenRouterGemini
|
|
? { allowBase64Only: true, includeCamelCase: true }
|
|
: undefined;
|
|
const normalizeAntigravityThinkingBlocks = isAntigravityClaudeModel;
|
|
|
|
return {
|
|
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
|
|
sanitizeToolCallIds: !isOpenAi && sanitizeToolCallIds,
|
|
toolCallIdMode,
|
|
repairToolUseResultPairing: !isOpenAi && repairToolUseResultPairing,
|
|
preserveSignatures: isAntigravityClaudeModel,
|
|
sanitizeThoughtSignatures: isOpenAi ? undefined : sanitizeThoughtSignatures,
|
|
normalizeAntigravityThinkingBlocks,
|
|
applyGoogleTurnOrdering: !isOpenAi && isGoogle,
|
|
validateGeminiTurns: !isOpenAi && isGoogle,
|
|
validateAnthropicTurns: !isOpenAi && isAnthropic,
|
|
allowSyntheticToolResults: !isOpenAi && (isGoogle || isAnthropic),
|
|
};
|
|
}
|