mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 20:32:20 +03:00
* fix(antigravity): surface aborted Gemini tool calls off end_turn Gemini/Antigravity aborts a turn with finishReason MALFORMED_FUNCTION_CALL (or a sibling like UNEXPECTED_TOOL_CALL) instead of completing cleanly. Both Claude-facing translators collapsed these to a clean end_turn, hiding the aborted tool call as a successful completion: - the OpenAI hub path (openai-to-claude.ts convertFinishReason default), and - the DIRECT Gemini->Claude path (gemini-to-claude.ts), which is the one Claude Code actually hits through an antigravity/Gemini-routed model. Add isAbortFinishReason() to finishReason.ts and map these reasons to tool_use on both paths; genuinely unknown reasons still fall back to end_turn. Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com> Inspired-by: https://github.com/decolua/9router/pull/2462 * chore(6713): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>
62 lines
2.0 KiB
TypeScript
62 lines
2.0 KiB
TypeScript
const OPENAI_FINISH_REASONS = new Set([
|
|
"stop",
|
|
"length",
|
|
"tool_calls",
|
|
"content_filter",
|
|
"function_call",
|
|
]);
|
|
|
|
const SAFETY_FINISH_REASONS = new Set([
|
|
"safety",
|
|
"recitation",
|
|
"blocklist",
|
|
"prohibited_content",
|
|
"content_filtered",
|
|
"policy_violation",
|
|
"malformed_response",
|
|
]);
|
|
|
|
// Gemini/Antigravity finish reasons that mean the model ABORTED the turn before
|
|
// completing it — most commonly a tool call the model started narrating but
|
|
// Gemini could not parse/execute (MALFORMED_FUNCTION_CALL, UNEXPECTED_TOOL_CALL).
|
|
// Distinct from SAFETY_FINISH_REASONS: those are deliberate, deterministic
|
|
// content blocks; these are execution failures mid tool-call. Left un-mapped
|
|
// here (still passed through raw, e.g. "malformed_function_call") so an
|
|
// OpenAI-format client at least sees a non-standard-but-honest value instead of
|
|
// a misleading "stop" — downstream Claude translation classifies them via
|
|
// isAbortFinishReason() so it does not collapse them to a clean "end_turn"
|
|
// (9router#2462 sub-bug #2: an aborted tool call must not present to the client
|
|
// as a successful completion).
|
|
const ABORT_FINISH_REASONS = new Set([
|
|
"malformed_function_call",
|
|
"unexpected_tool_call",
|
|
"finish_reason_unspecified",
|
|
"other",
|
|
"language",
|
|
"no_image",
|
|
]);
|
|
|
|
export function isAbortFinishReason(value: unknown): boolean {
|
|
if (typeof value !== "string") return false;
|
|
return ABORT_FINISH_REASONS.has(value.toLowerCase());
|
|
}
|
|
|
|
export function normalizeOpenAICompatibleFinishReason(value: unknown): unknown {
|
|
if (typeof value !== "string") return value;
|
|
|
|
const normalized = value.toLowerCase();
|
|
if (OPENAI_FINISH_REASONS.has(normalized)) return normalized;
|
|
if (normalized === "max_tokens") return "length";
|
|
if (SAFETY_FINISH_REASONS.has(normalized)) return "content_filter";
|
|
|
|
return normalized;
|
|
}
|
|
|
|
export function normalizeOpenAICompatibleFinishReasonString(
|
|
value: unknown,
|
|
fallback = "stop"
|
|
): string {
|
|
const normalized = normalizeOpenAICompatibleFinishReason(value);
|
|
return typeof normalized === "string" && normalized ? normalized : fallback;
|
|
}
|