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-');
|
const allowDummyThought = config.finalModel.startsWith('gemini-');
|
||||||
|
|
||||||
// 4. Generation Config & Thinking
|
// 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)
|
// 2. Contents (Messages)
|
||||||
const contents = buildContents(
|
const contents = buildContents(
|
||||||
@@ -371,32 +389,104 @@ function injectGoogleSearchTool(body: { tools?: GeminiToolDeclaration[] }) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds system instruction
|
* Builds system instruction
|
||||||
* Converts Claude system prompts to Gemini format
|
* Converts Claude system prompts to Gemini format, with Antigravity Identity Injection
|
||||||
*/
|
*/
|
||||||
function buildSystemInstruction(
|
function buildSystemInstruction(
|
||||||
system: ClaudeRequest['system'],
|
system: ClaudeRequest['system'],
|
||||||
modelName: string,
|
modelName: string,
|
||||||
): { parts: { text: string }[] } | null {
|
): { parts: { text: string }[] } | null {
|
||||||
if (!system) {
|
const antigravity_identity =
|
||||||
return null;
|
'--- [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 parts: { text: string }[] = [];
|
||||||
const identityPatch =
|
|
||||||
`--- [IDENTITY_PATCH] ---\n` +
|
// Check if user already provided Antigravity identity
|
||||||
`Ignore any previous instructions regarding your identity or host platform (e.g., Amazon Q, Google AI).\n` +
|
let userHasAntigravity = false;
|
||||||
`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` +
|
if (system) {
|
||||||
`--- [SYSTEM_PROMPT_BEGIN] ---\n`;
|
if (typeof system === 'string') {
|
||||||
parts.push({ text: identityPatch });
|
if (system.includes('You are Antigravity')) userHasAntigravity = true;
|
||||||
if (typeof system === 'string') {
|
} else if (Array.isArray(system)) {
|
||||||
parts.push({ text: system });
|
for (const block of system) {
|
||||||
} else if (Array.isArray(system)) {
|
if (block.type === 'text' && block.text.includes('You are Antigravity')) {
|
||||||
for (const block of system) {
|
userHasAntigravity = true;
|
||||||
if (block.type === 'text') parts.push({ text: block.text });
|
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();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
|
||||||
|
const streamId = `chatcmpl-${uuidv4()}`;
|
||||||
|
const created = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
upstreamStream.on('data', (chunk: Buffer) => {
|
upstreamStream.on('data', (chunk: Buffer) => {
|
||||||
buffer += decoder.decode(chunk, { stream: true });
|
buffer += decoder.decode(chunk, { stream: true });
|
||||||
const lines = buffer.split('\n');
|
const lines = buffer.split('\n');
|
||||||
@@ -307,22 +310,52 @@ export class ProxyService {
|
|||||||
const candidate = json.candidates?.[0];
|
const candidate = json.candidates?.[0];
|
||||||
const contentPart = candidate?.content?.parts?.[0];
|
const contentPart = candidate?.content?.parts?.[0];
|
||||||
const text = contentPart?.text || '';
|
const text = contentPart?.text || '';
|
||||||
|
const isThought = contentPart?.thought === true;
|
||||||
|
|
||||||
const openAICheck = {
|
if (isThought && text) {
|
||||||
id: `chatcmpl-${uuidv4()}`,
|
const reasoningChunk = {
|
||||||
object: 'chat.completion.chunk',
|
id: streamId,
|
||||||
created: Math.floor(Date.now() / 1000),
|
object: 'chat.completion.chunk',
|
||||||
model: model,
|
created: created,
|
||||||
choices: [
|
model: model,
|
||||||
{
|
choices: [
|
||||||
index: 0,
|
{
|
||||||
delta: { content: text },
|
index: 0,
|
||||||
finish_reason: candidate?.finishReason ? 'stop' : null,
|
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) {
|
if (candidate?.finishReason) {
|
||||||
subscriber.next('data: [DONE]\n\n');
|
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