* 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>
171 lines
6.3 KiB
JavaScript
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;
|
|
}
|