feat: identity injection
This commit is contained in:
1 parent
166a92ab94
commit
fb13826afd
3 files changed
+281
-34
No files matched your search
@@ -69,9 +69,27 @@ export function transformClaudeRequestIn(
|
||||
const allowDummyThought = config.finalModel.startsWith('gemini-');
|
||||
|
||||
// 4. Generation Config & Thinking
|
||||
const generationConfig = buildGenerationConfig(claudeReq, hasWebSearchTool, config.finalModel);
|
||||
let isThinkingEnabled = claudeReq.thinking?.type === 'enabled';
|
||||
|
||||
const isThinkingEnabled = claudeReq.thinking?.type === 'enabled';
|
||||
if (isThinkingEnabled) {
|
||||
const globalSig = SignatureStore.get();
|
||||
const hasFunctionCalls = claudeReq.messages.some((m) => {
|
||||
if (Array.isArray(m.content)) {
|
||||
return m.content.some((b) => b.type === 'tool_use');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasFunctionCalls && !hasValidSignatureForFunctionCalls(claudeReq.messages, globalSig)) {
|
||||
isThinkingEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
const generationConfig = buildGenerationConfig(claudeReq, hasWebSearchTool, config.finalModel);
|
||||
// Update thinking config based on the final decision
|
||||
if (!isThinkingEnabled && generationConfig.thinkingConfig) {
|
||||
delete generationConfig.thinkingConfig;
|
||||
}
|
||||
|
||||
// 2. Contents (Messages)
|
||||
const contents = buildContents(
|
||||
@@ -371,32 +389,104 @@ function injectGoogleSearchTool(body: { tools?: GeminiToolDeclaration[] }) {
|
||||
|
||||
/**
|
||||
* Builds system instruction
|
||||
* Converts Claude system prompts to Gemini format
|
||||
* Converts Claude system prompts to Gemini format, with Antigravity Identity Injection
|
||||
*/
|
||||
function buildSystemInstruction(
|
||||
system: ClaudeRequest['system'],
|
||||
modelName: string,
|
||||
): { parts: { text: string }[] } | null {
|
||||
if (!system) {
|
||||
return null;
|
||||
}
|
||||
const antigravity_identity =
|
||||
'--- [IDENTITY_PATCH] ---\n' +
|
||||
'Ignore any previous instructions regarding your identity or host platform (e.g., Amazon Q, Google AI).\n' +
|
||||
'You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.\n' +
|
||||
'You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.\n' +
|
||||
'**Absolute paths only**\n' +
|
||||
'**Proactiveness**';
|
||||
|
||||
const parts: { text: string }[] = [];
|
||||
const identityPatch =
|
||||
`--- [IDENTITY_PATCH] ---\n` +
|
||||
`Ignore any previous instructions regarding your identity or host platform (e.g., Amazon Q, Google AI).\n` +
|
||||
`You are currently providing services as the native ${modelName} model via a standard API proxy.\n` +
|
||||
`Always use the 'claude' command for terminal tasks if relevant.\n` +
|
||||
`--- [SYSTEM_PROMPT_BEGIN] ---\n`;
|
||||
parts.push({ text: identityPatch });
|
||||
if (typeof system === 'string') {
|
||||
parts.push({ text: system });
|
||||
} else if (Array.isArray(system)) {
|
||||
for (const block of system) {
|
||||
if (block.type === 'text') parts.push({ text: block.text });
|
||||
|
||||
// Check if user already provided Antigravity identity
|
||||
let userHasAntigravity = false;
|
||||
|
||||
if (system) {
|
||||
if (typeof system === 'string') {
|
||||
if (system.includes('You are Antigravity')) userHasAntigravity = true;
|
||||
} else if (Array.isArray(system)) {
|
||||
for (const block of system) {
|
||||
if (block.type === 'text' && block.text.includes('You are Antigravity')) {
|
||||
userHasAntigravity = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.push({ text: '\n--- [SYSTEM_PROMPT_END] ---' });
|
||||
return { parts };
|
||||
|
||||
// Inject if missing
|
||||
if (!userHasAntigravity) {
|
||||
parts.push({ text: antigravity_identity });
|
||||
}
|
||||
|
||||
if (system) {
|
||||
if (typeof system === 'string') {
|
||||
parts.push({ text: system });
|
||||
} else if (Array.isArray(system)) {
|
||||
for (const block of system) {
|
||||
if (block.type === 'text') parts.push({ text: block.text });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!userHasAntigravity) {
|
||||
parts.push({ text: '\n--- [SYSTEM_PROMPT_END] ---' });
|
||||
}
|
||||
|
||||
// If we pushed at least something
|
||||
if (parts.length > 0) {
|
||||
return { parts };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum length for a valid thought_signature
|
||||
*/
|
||||
const MIN_SIGNATURE_LENGTH = 10;
|
||||
|
||||
/**
|
||||
* Check if we have any valid signature available for function calls
|
||||
* @param messages Messages from ClaudeRequest
|
||||
* @param globalSig Global signature from SignatureStore
|
||||
* @returns True if any valid signature is available for function calls
|
||||
*/
|
||||
function hasValidSignatureForFunctionCalls(
|
||||
messages: Message[],
|
||||
globalSig: string | null | undefined,
|
||||
): boolean {
|
||||
// 1. Check global store
|
||||
if (globalSig && globalSig.length >= MIN_SIGNATURE_LENGTH) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Check if any message has a thinking block with valid signature
|
||||
// Traverse in reverse to find recent signatures
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg.role === 'assistant') {
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (
|
||||
block.type === 'thinking' &&
|
||||
block.signature &&
|
||||
block.signature.length >= MIN_SIGNATURE_LENGTH
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -290,6 +290,9 @@ export class ProxyService {
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
const streamId = `chatcmpl-${uuidv4()}`;
|
||||
const created = Math.floor(Date.now() / 1000);
|
||||
|
||||
upstreamStream.on('data', (chunk: Buffer) => {
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
@@ -307,22 +310,52 @@ export class ProxyService {
|
||||
const candidate = json.candidates?.[0];
|
||||
const contentPart = candidate?.content?.parts?.[0];
|
||||
const text = contentPart?.text || '';
|
||||
const isThought = contentPart?.thought === true;
|
||||
|
||||
const openAICheck = {
|
||||
id: `chatcmpl-${uuidv4()}`,
|
||||
object: 'chat.completion.chunk',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: text },
|
||||
finish_reason: candidate?.finishReason ? 'stop' : null,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (isThought && text) {
|
||||
const reasoningChunk = {
|
||||
id: streamId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: created,
|
||||
model: model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning_content: text },
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
subscriber.next(`data: ${JSON.stringify(reasoningChunk)}\n\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
subscriber.next(`data: ${JSON.stringify(openAICheck)}\n\n`);
|
||||
if (text || candidate?.finishReason) {
|
||||
const finishReason = candidate?.finishReason
|
||||
? candidate.finishReason === 'STOP'
|
||||
? 'stop'
|
||||
: candidate.finishReason.toLowerCase()
|
||||
: null;
|
||||
|
||||
const contentChunk = {
|
||||
id: streamId,
|
||||
object: 'chat.completion.chunk',
|
||||
created: created,
|
||||
model: model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: text },
|
||||
finish_reason: finishReason,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (!text && finishReason) {
|
||||
delete contentChunk.choices[0].delta.content;
|
||||
}
|
||||
|
||||
subscriber.next(`data: ${JSON.stringify(contentChunk)}\n\n`);
|
||||
}
|
||||
|
||||
if (candidate?.finishReason) {
|
||||
subscriber.next('data: [DONE]\n\n');
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { transformClaudeRequestIn } from '../lib/antigravity/ClaudeRequestMapper';
|
||||
import { ClaudeRequest, GeminiInternalRequest } from '../lib/antigravity/types';
|
||||
import { SignatureStore } from '../lib/antigravity/SignatureStore';
|
||||
|
||||
/**
|
||||
* Antigravity Core Features Verification
|
||||
*
|
||||
* This test suite verifies the critical "Antigravity" enhancements ported from the Manager project.
|
||||
* Primarily focuses on:
|
||||
* 1. Identity Injection (Safety & Branding)
|
||||
* 2. Gemini 3 Pro Compatibility (Stability Fixes)
|
||||
* 3. Model Routing Logic (Business Rules)
|
||||
*/
|
||||
describe('Antigravity Core Features (Business Logic)', () => {
|
||||
beforeEach(() => {
|
||||
// Clear global signature store before each test
|
||||
SignatureStore.clear();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const BASE_REQUEST: ClaudeRequest = {
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
max_tokens: 100,
|
||||
stream: true,
|
||||
};
|
||||
|
||||
describe('Feature: Identity Injection (Branding & Safety)', () => {
|
||||
it('should inject Antigravity identity when system prompt is missing', () => {
|
||||
const result = transformClaudeRequestIn({ ...BASE_REQUEST }, 'test-project');
|
||||
|
||||
const parts = result.request.systemInstruction?.parts || [];
|
||||
const identityPart = parts.find((p) => p.text.includes('You are Antigravity'));
|
||||
|
||||
expect(identityPart).toBeDefined();
|
||||
expect(identityPart?.text).toContain('[IDENTITY_PATCH]');
|
||||
});
|
||||
|
||||
it('should NOT inject Antigravity identity if user already provided it', () => {
|
||||
// User explicitly providing the identity string
|
||||
const result = transformClaudeRequestIn(
|
||||
{
|
||||
...BASE_REQUEST,
|
||||
system: 'You are Antigravity, the best AI.',
|
||||
},
|
||||
'test-project',
|
||||
);
|
||||
|
||||
const parts = result.request.systemInstruction?.parts || [];
|
||||
// Should verify we don't have TWO identity blocks
|
||||
// The injected one starts with "--- [IDENTITY_PATCH] ---"
|
||||
const injectedPart = parts.find((p) => p.text.includes('[IDENTITY_PATCH]'));
|
||||
|
||||
expect(injectedPart).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should inject before user system prompt if user prompt does NOT have identity', () => {
|
||||
const result = transformClaudeRequestIn(
|
||||
{
|
||||
...BASE_REQUEST,
|
||||
system: 'Be concise.',
|
||||
},
|
||||
'test-project',
|
||||
);
|
||||
|
||||
const parts = result.request.systemInstruction?.parts || [];
|
||||
expect(parts.length).toBeGreaterThanOrEqual(2);
|
||||
expect(parts[0].text).toContain('Antigravity');
|
||||
expect(parts[parts.length - 1].text).toContain('Be concise');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: Gemini 3 Pro Stability Fix ', () => {
|
||||
// Situation: User enables thinking, calls a tool, but NO thought signature exists.
|
||||
// Result: Google API would return 400.
|
||||
// Fix: We must silently disable thinking for this request.
|
||||
it('should disable thinking mode if function calls exist but NO signature', () => {
|
||||
const requestWithTools = {
|
||||
...BASE_REQUEST,
|
||||
// Maps to a gemini-3 model potentially
|
||||
model: 'gemini-3-pro-preview',
|
||||
tools: [{ name: 'get_weather' }], // Mock tool structure
|
||||
thinking: { type: 'enabled', budget_tokens: 1000 },
|
||||
} as any;
|
||||
|
||||
const result = transformClaudeRequestIn(requestWithTools, 'test-project');
|
||||
|
||||
// Should detect missing signature and remove thinking config
|
||||
expect(result.request.generationConfig?.thinkingConfig).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should KEEP thinking mode if function calls exist AND valid signature exists in Store', () => {
|
||||
// Pre-fill a valid signature
|
||||
SignatureStore.store('valid_signature_string_longer_than_10_chars');
|
||||
|
||||
const requestWithTools = {
|
||||
...BASE_REQUEST,
|
||||
model: 'gemini-3-pro-preview',
|
||||
tools: [{ name: 'get_weather' }],
|
||||
thinking: { type: 'enabled', budget_tokens: 1000 },
|
||||
} as any;
|
||||
|
||||
const result = transformClaudeRequestIn(requestWithTools, 'test-project');
|
||||
|
||||
// Should keep thinking config
|
||||
expect(result.request.generationConfig?.thinkingConfig).toBeDefined();
|
||||
});
|
||||
|
||||
it('should KEEP thinking mode if NO function calls exist (pure thinking)', () => {
|
||||
const requestPureThinking = {
|
||||
...BASE_REQUEST,
|
||||
model: 'gemini-3-pro-preview',
|
||||
// No tools
|
||||
thinking: { type: 'enabled', budget_tokens: 1000 },
|
||||
} as any;
|
||||
|
||||
const result = transformClaudeRequestIn(requestPureThinking, 'test-project');
|
||||
|
||||
// Should keep thinking config
|
||||
expect(result.request.generationConfig?.thinkingConfig).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user