fix: comprehensive tool argument normalization hardening

- Remove all { raw: ... } returns that caused InputValidationError with
  z.strictObject schemas — return {} instead for clean Zod errors
- Extend normalizeAtStop buffering to all mapped tools (Read, Write,
  Edit, Glob, Grep) so streaming paths also get normalized
- Make repairPossiblyTruncatedObjectJson generic — repair any valid
  JSON object, not just ones with a command field
- Export hasToolFieldMapping for streaming normalizeAtStop decision
- Skip normalization on finish_reason: length to preserve raw truncated
  buffer
- Update all test expectations to match new behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gnanam1990
2026-04-06 18:38:33 +05:30
parent 50efbe5614
commit 3a25d71004
4 changed files with 35 additions and 44 deletions

View File

@@ -44,6 +44,7 @@ import { sanitizeSchemaForOpenAICompat } from '../../utils/schemaSanitizer.js'
import { redactSecretValueForDisplay } from '../../utils/providerProfile.js'
import {
normalizeToolArguments,
hasToolFieldMapping,
} from './toolArgumentNormalization.js'
type SecretValueSource = Partial<{
@@ -486,7 +487,7 @@ const JSON_REPAIR_SUFFIXES = [
function repairPossiblyTruncatedObjectJson(raw: string): string | null {
try {
const parsed = JSON.parse(raw)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) && typeof (parsed as Record<string, unknown>).command === 'string'
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? raw
: null
} catch {
@@ -494,12 +495,7 @@ function repairPossiblyTruncatedObjectJson(raw: string): string | null {
try {
const repaired = raw + combo
const parsed = JSON.parse(repaired)
if (
parsed &&
typeof parsed === 'object' &&
!Array.isArray(parsed) &&
typeof (parsed as Record<string, unknown>).command === 'string'
) {
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return repaired
}
} catch {}
@@ -619,7 +615,7 @@ async function* openaiStreamToAnthropic(
const toolBlockIndex = contentBlockIndex
const initialArguments = tc.function.arguments ?? ''
const normalizeAtStop = tc.function.name === 'Bash'
const normalizeAtStop = hasToolFieldMapping(tc.function.name)
activeToolCalls.set(tc.index, {
id: tc.id,
name: tc.function.name,