feat: complete rebranding to Gemini Nexus with improved UI, proxies and documentation
This commit is contained in:
1 parent
e524a340cb
commit
433dccbaa9
108 files changed
+1004
-1139
No files matched your search
@@ -0,0 +1,251 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
ClaudeResponse,
|
||||
GeminiResponse,
|
||||
GeminiPart,
|
||||
ContentBlock,
|
||||
Usage,
|
||||
GroundingMetadata,
|
||||
} from './types';
|
||||
import { decodeSignature } from './signature-utils';
|
||||
|
||||
/**
|
||||
* Non-streaming response processor (Gemini -> Claude)
|
||||
*
|
||||
*/
|
||||
class NonStreamingProcessor {
|
||||
private contentBlocks: ContentBlock[] = [];
|
||||
private textBuilder: string = '';
|
||||
private thinkingBuilder: string = '';
|
||||
private thinkingSignature: string | null = null;
|
||||
private trailingSignature: string | null = null;
|
||||
private hasToolCall: boolean = false;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public process(geminiResponse: GeminiResponse): ClaudeResponse {
|
||||
const candidate = geminiResponse.candidates?.[0];
|
||||
const parts = candidate?.content?.parts || [];
|
||||
|
||||
// 1. Process all parts
|
||||
for (const part of parts) {
|
||||
this.processPart(part);
|
||||
}
|
||||
|
||||
// 2. Process grounding (web search)
|
||||
if (candidate?.groundingMetadata) {
|
||||
this.processGrounding(candidate.groundingMetadata);
|
||||
}
|
||||
|
||||
// 3. Flush remaining content
|
||||
this.flushThinking();
|
||||
this.flushText();
|
||||
|
||||
// 4. Handle trailingSignature
|
||||
if (this.trailingSignature) {
|
||||
this.contentBlocks.push({
|
||||
type: 'thinking',
|
||||
thinking: '',
|
||||
signature: this.trailingSignature,
|
||||
});
|
||||
this.trailingSignature = null; // Consumed
|
||||
}
|
||||
|
||||
// 5. Build response
|
||||
return this.buildResponse(geminiResponse);
|
||||
}
|
||||
|
||||
private processPart(part: GeminiPart) {
|
||||
const signature = decodeSignature(part.thoughtSignature) || null;
|
||||
|
||||
// 1. Handle FunctionCall
|
||||
if (part.functionCall) {
|
||||
this.flushThinking();
|
||||
this.flushText();
|
||||
|
||||
// Handle trailing signature logic
|
||||
if (this.trailingSignature) {
|
||||
this.contentBlocks.push({
|
||||
type: 'thinking',
|
||||
thinking: '',
|
||||
signature: this.trailingSignature,
|
||||
});
|
||||
this.trailingSignature = null;
|
||||
}
|
||||
|
||||
this.hasToolCall = true;
|
||||
|
||||
const fc = part.functionCall;
|
||||
const toolId = fc.id || `${fc.name}-${uuidv4()}`;
|
||||
|
||||
const toolUse: ContentBlock = {
|
||||
type: 'tool_use',
|
||||
id: toolId,
|
||||
name: fc.name,
|
||||
input: fc.args || {},
|
||||
signature: signature || undefined,
|
||||
};
|
||||
|
||||
this.contentBlocks.push(toolUse);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Handle Text / Thinking
|
||||
if (part.text !== undefined) {
|
||||
const text = part.text;
|
||||
if (part.thought) {
|
||||
// Thinking Part
|
||||
this.flushText();
|
||||
|
||||
// Handle trailing signature before thinking
|
||||
if (this.trailingSignature) {
|
||||
this.flushThinking(); // Ensure previous thinking is flushed
|
||||
this.contentBlocks.push({
|
||||
type: 'thinking',
|
||||
thinking: '',
|
||||
signature: this.trailingSignature,
|
||||
});
|
||||
this.trailingSignature = null;
|
||||
}
|
||||
|
||||
this.thinkingBuilder += text;
|
||||
if (signature) {
|
||||
this.thinkingSignature = signature;
|
||||
}
|
||||
} else {
|
||||
// Normal Text
|
||||
if (text === '') {
|
||||
// Empty text with signature -> store as trailing
|
||||
if (signature) {
|
||||
this.trailingSignature = signature;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.flushThinking();
|
||||
|
||||
// Handle trailing signature
|
||||
if (this.trailingSignature) {
|
||||
this.flushText();
|
||||
this.contentBlocks.push({
|
||||
type: 'thinking',
|
||||
thinking: '',
|
||||
signature: this.trailingSignature,
|
||||
});
|
||||
this.trailingSignature = null;
|
||||
}
|
||||
|
||||
this.textBuilder += text;
|
||||
|
||||
// Non-empty text with signature -> flush immediately empty thinking block with sig
|
||||
if (signature) {
|
||||
this.flushText();
|
||||
this.contentBlocks.push({
|
||||
type: 'thinking',
|
||||
thinking: '',
|
||||
signature: signature,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Handle InlineData (Image)
|
||||
if (part.inlineData) {
|
||||
this.flushThinking();
|
||||
const { mimeType, data } = part.inlineData;
|
||||
if (data) {
|
||||
const markdownImg = ``;
|
||||
this.textBuilder += markdownImg;
|
||||
this.flushText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processGrounding(grounding: GroundingMetadata) {
|
||||
let groundingText = '';
|
||||
|
||||
if (grounding.webSearchQueries && grounding.webSearchQueries.length > 0) {
|
||||
groundingText += `\n\n---\n**🔍 Searched for you:** ${grounding.webSearchQueries.join(', ')}`;
|
||||
}
|
||||
|
||||
if (grounding.groundingChunks) {
|
||||
const links: string[] = [];
|
||||
grounding.groundingChunks.forEach((chunk, index) => {
|
||||
if (chunk.web) {
|
||||
const title = chunk.web.title || 'Web source';
|
||||
const uri = chunk.web.uri || '#';
|
||||
links.push(`[${index + 1}] [${title}](${uri})`);
|
||||
}
|
||||
});
|
||||
|
||||
if (links.length > 0) {
|
||||
groundingText += `\n\n**🌐 Citations:**\n` + links.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if (groundingText) {
|
||||
this.flushThinking();
|
||||
this.flushText();
|
||||
this.textBuilder += groundingText;
|
||||
this.flushText();
|
||||
}
|
||||
}
|
||||
|
||||
private flushText() {
|
||||
if (!this.textBuilder) return;
|
||||
this.contentBlocks.push({
|
||||
type: 'text',
|
||||
text: this.textBuilder,
|
||||
});
|
||||
this.textBuilder = '';
|
||||
}
|
||||
|
||||
private flushThinking() {
|
||||
if (!this.thinkingBuilder && !this.thinkingSignature) return;
|
||||
|
||||
this.contentBlocks.push({
|
||||
type: 'thinking',
|
||||
thinking: this.thinkingBuilder,
|
||||
signature: this.thinkingSignature || undefined,
|
||||
});
|
||||
|
||||
this.thinkingBuilder = '';
|
||||
this.thinkingSignature = null;
|
||||
}
|
||||
|
||||
private buildResponse(geminiResponse: GeminiResponse): ClaudeResponse {
|
||||
const finishReason = geminiResponse.candidates?.[0]?.finishReason;
|
||||
|
||||
let stopReason = 'end_turn';
|
||||
if (this.hasToolCall) {
|
||||
stopReason = 'tool_use';
|
||||
} else if (finishReason === 'MAX_TOKENS') {
|
||||
stopReason = 'max_tokens';
|
||||
}
|
||||
|
||||
const usage: Usage = {
|
||||
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
||||
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
id: geminiResponse.responseId || `msg_${uuidv4()}`,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
model: geminiResponse.modelVersion || '',
|
||||
content: this.contentBlocks,
|
||||
stop_reason: stopReason,
|
||||
usage: usage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API: Transform Gemini Response to Claude Response
|
||||
*/
|
||||
export function transformResponse(geminiResponse: GeminiResponse): ClaudeResponse {
|
||||
const processor = new NonStreamingProcessor();
|
||||
return processor.process(geminiResponse);
|
||||
}
|
||||
Reference in new issue
Block a user