Files
antigravity-claude-proxy/src/format/content-converter.js
Badri Narayanan S 1d91bc0d30 Improve logging, rate limiting, and error handling (#29)
* feat: apply local user changes and fixes

* ;D

* Clean up PR #28: Remove duplicate code lines and unnecessary file

- Remove pullrequest.md (PR notes file not needed in repo)
- Fix duplicate lines in account-manager.js:
  - rateLimitResetTime assignment
  - saveToDisk() calls in markRateLimited and markInvalid
  - invalidReason/invalidAt assignments
  - double return statement in discoverProject

Original PR by M2noa: fix sticky accs, 500s, logging updates, and rate limit handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: M2noa <226494568+M2noa@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>

* chore: replace console.log with logger methods for consistency

- Replace all console.log calls with logger.warn/debug in:
  - src/cloudcode-client.js (4 places)
  - src/format/thinking-utils.js (7 places)

This ensures consistent logging behavior with the new logger utility,
respecting --debug mode for verbose output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: M1noa <minoa@minoa.cat>
Co-authored-by: M2noa <226494568+M2noa@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-01 14:35:06 +05:30

171 lines
6.3 KiB
JavaScript

/**
* Content Converter
* Converts Anthropic message content to Google Generative AI parts format
*/
import { MIN_SIGNATURE_LENGTH, GEMINI_SKIP_SIGNATURE } from '../constants.js';
import { getCachedSignature } from './signature-cache.js';
import { logger } from '../utils/logger.js';
/**
* Convert Anthropic role to Google role
* @param {string} role - Anthropic role ('user', 'assistant')
* @returns {string} Google role ('user', 'model')
*/
export function convertRole(role) {
if (role === 'assistant') return 'model';
if (role === 'user') return 'user';
return 'user'; // Default to user
}
/**
* Convert Anthropic message content to Google Generative AI parts
* @param {string|Array} content - Anthropic message content
* @param {boolean} isClaudeModel - Whether the model is a Claude model
* @param {boolean} isGeminiModel - Whether the model is a Gemini model
* @returns {Array} Google Generative AI parts array
*/
export function convertContentToParts(content, isClaudeModel = false, isGeminiModel = false) {
if (typeof content === 'string') {
return [{ text: content }];
}
if (!Array.isArray(content)) {
return [{ text: String(content) }];
}
const parts = [];
for (const block of content) {
if (block.type === 'text') {
// Skip empty text blocks - they cause API errors
if (block.text && block.text.trim()) {
parts.push({ text: block.text });
}
} else if (block.type === 'image') {
// Handle image content
if (block.source?.type === 'base64') {
// Base64-encoded image
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data
}
});
} else if (block.source?.type === 'url') {
// URL-referenced image
parts.push({
fileData: {
mimeType: block.source.media_type || 'image/jpeg',
fileUri: block.source.url
}
});
}
} else if (block.type === 'document') {
// Handle document content (e.g. PDF)
if (block.source?.type === 'base64') {
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data
}
});
} else if (block.source?.type === 'url') {
parts.push({
fileData: {
mimeType: block.source.media_type || 'application/pdf',
fileUri: block.source.url
}
});
}
} else if (block.type === 'tool_use') {
// Convert tool_use to functionCall (Google format)
// For Claude models, include the id field
const functionCall = {
name: block.name,
args: block.input || {}
};
if (isClaudeModel && block.id) {
functionCall.id = block.id;
}
// Build the part with functionCall
const part = { functionCall };
// For Gemini models, include thoughtSignature at the part level
// This is required by Gemini 3+ for tool calls to work correctly
if (isGeminiModel) {
// Priority: block.thoughtSignature > cache > GEMINI_SKIP_SIGNATURE
let signature = block.thoughtSignature;
if (!signature && block.id) {
signature = getCachedSignature(block.id);
if (signature) {
logger.debug(`[ContentConverter] Restored signature from cache for: ${block.id}`);
}
}
part.thoughtSignature = signature || GEMINI_SKIP_SIGNATURE;
}
parts.push(part);
} else if (block.type === 'tool_result') {
// Convert tool_result to functionResponse (Google format)
let responseContent = block.content;
let imageParts = [];
if (typeof responseContent === 'string') {
responseContent = { result: responseContent };
} else if (Array.isArray(responseContent)) {
// Extract images from tool results first (e.g., from Read tool reading image files)
for (const item of responseContent) {
if (item.type === 'image' && item.source?.type === 'base64') {
imageParts.push({
inlineData: {
mimeType: item.source.media_type,
data: item.source.data
}
});
}
}
// Extract text content
const texts = responseContent
.filter(c => c.type === 'text')
.map(c => c.text)
.join('\n');
responseContent = { result: texts || (imageParts.length > 0 ? 'Image attached' : '') };
}
const functionResponse = {
name: block.tool_use_id || 'unknown',
response: responseContent
};
// For Claude models, the id field must match the tool_use_id
if (isClaudeModel && block.tool_use_id) {
functionResponse.id = block.tool_use_id;
}
parts.push({ functionResponse });
// Add any images from the tool result as separate parts
parts.push(...imageParts);
} else if (block.type === 'thinking') {
// Handle thinking blocks - only those with valid signatures
if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
// Convert to Gemini format with signature
parts.push({
text: block.thinking,
thought: true,
thoughtSignature: block.signature
});
}
// Unsigned thinking blocks are dropped upstream
}
}
return parts;
}