free-antigravity-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +142 -0
  3. package/dist/chat.d.ts +2 -0
  4. package/dist/chat.d.ts.map +1 -0
  5. package/dist/chat.js +212 -0
  6. package/dist/chat.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +216 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +29 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +125 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/crypto.d.ts +5 -0
  16. package/dist/crypto.d.ts.map +1 -0
  17. package/dist/crypto.js +93 -0
  18. package/dist/crypto.js.map +1 -0
  19. package/dist/logger.d.ts +9 -0
  20. package/dist/logger.d.ts.map +1 -0
  21. package/dist/logger.js +10 -0
  22. package/dist/logger.js.map +1 -0
  23. package/dist/proxy/modelUtils.d.ts +31 -0
  24. package/dist/proxy/modelUtils.d.ts.map +1 -0
  25. package/dist/proxy/modelUtils.js +55 -0
  26. package/dist/proxy/modelUtils.js.map +1 -0
  27. package/dist/proxy/registry.d.ts +40 -0
  28. package/dist/proxy/registry.d.ts.map +1 -0
  29. package/dist/proxy/registry.js +176 -0
  30. package/dist/proxy/registry.js.map +1 -0
  31. package/dist/proxy/shared.d.ts +39 -0
  32. package/dist/proxy/shared.d.ts.map +1 -0
  33. package/dist/proxy/shared.js +74 -0
  34. package/dist/proxy/shared.js.map +1 -0
  35. package/dist/proxy/translators/anthropic.d.ts +119 -0
  36. package/dist/proxy/translators/anthropic.d.ts.map +1 -0
  37. package/dist/proxy/translators/anthropic.js +273 -0
  38. package/dist/proxy/translators/anthropic.js.map +1 -0
  39. package/dist/proxy/translators/google.d.ts +86 -0
  40. package/dist/proxy/translators/google.d.ts.map +1 -0
  41. package/dist/proxy/translators/google.js +111 -0
  42. package/dist/proxy/translators/google.js.map +1 -0
  43. package/dist/proxy/translators/ollama.d.ts +27 -0
  44. package/dist/proxy/translators/ollama.d.ts.map +1 -0
  45. package/dist/proxy/translators/ollama.js +82 -0
  46. package/dist/proxy/translators/ollama.js.map +1 -0
  47. package/dist/proxy/translators/openai.d.ts +132 -0
  48. package/dist/proxy/translators/openai.d.ts.map +1 -0
  49. package/dist/proxy/translators/openai.js +396 -0
  50. package/dist/proxy/translators/openai.js.map +1 -0
  51. package/dist/proxy/translators/utils.d.ts +60 -0
  52. package/dist/proxy/translators/utils.d.ts.map +1 -0
  53. package/dist/proxy/translators/utils.js +504 -0
  54. package/dist/proxy/translators/utils.js.map +1 -0
  55. package/dist/proxy.d.ts +22 -0
  56. package/dist/proxy.d.ts.map +1 -0
  57. package/dist/proxy.js +576 -0
  58. package/dist/proxy.js.map +1 -0
  59. package/dist/schemaValidator.d.ts +50 -0
  60. package/dist/schemaValidator.d.ts.map +1 -0
  61. package/dist/schemaValidator.js +208 -0
  62. package/dist/schemaValidator.js.map +1 -0
  63. package/install.cmd +33 -0
  64. package/package.json +46 -0
  65. package/src/chat.ts +184 -0
  66. package/src/cli.ts +184 -0
  67. package/src/config.ts +99 -0
  68. package/src/crypto.ts +49 -0
  69. package/src/logger.ts +8 -0
  70. package/src/proxy/modelUtils.ts +86 -0
  71. package/src/proxy/registry.ts +196 -0
  72. package/src/proxy/shared.ts +102 -0
  73. package/src/proxy/translators/anthropic.ts +420 -0
  74. package/src/proxy/translators/google.ts +162 -0
  75. package/src/proxy/translators/ollama.ts +88 -0
  76. package/src/proxy/translators/openai.ts +556 -0
  77. package/src/proxy/translators/utils.ts +552 -0
  78. package/src/proxy.ts +573 -0
  79. package/src/schemaValidator.ts +215 -0
  80. package/tsconfig.json +19 -0
package/src/cli.ts ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Free Antigravity CLI - Open Source Community Edition
4
+ * Supports custom AI models alongside Gemini.
5
+ */
6
+ import { Command } from 'commander';
7
+ import * as path from 'path';
8
+ import * as fs from 'fs';
9
+ import { loadModels, addModel, removeModel, listModels, getModel, ensureConfigDir, CustomModelEntry } from './config';
10
+ import { startProxy, getProxyPort, loadCustomModels, stopProxy } from './proxy';
11
+ import { startChat } from './chat';
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('antigravity')
17
+ .description('Free Antigravity CLI - Open Source Community Edition\nSupports OpenAI, Anthropic, Ollama, OpenRouter, Google AI Studio, and custom providers.')
18
+ .version('1.0.0');
19
+
20
+ // --- Chat command ---
21
+
22
+ program
23
+ .command('chat')
24
+ .description('Start interactive chat (default command)')
25
+ .argument('[prompt]', 'One-shot prompt (non-interactive)')
26
+ .option('-m, --model <name>', 'Model to use')
27
+ .action(async (prompt?: string, options?: { model?: string }) => {
28
+ if (prompt) {
29
+ // One-shot mode
30
+ console.log(`Sending to ${options?.model || 'default model'}...`);
31
+ await startChat(options?.model);
32
+ // In a full implementation, would send the prompt and exit
33
+ console.log('One-shot mode: Use interactive chat for now.');
34
+ } else {
35
+ await startChat(options?.model);
36
+ }
37
+ });
38
+
39
+ // --- Models command ---
40
+
41
+ const modelsCmd = program.command('models').description('Manage custom AI models');
42
+
43
+ modelsCmd
44
+ .command('list')
45
+ .description('List all configured models')
46
+ .action(() => {
47
+ const models = listModels();
48
+ if (models.length === 0) {
49
+ console.log('No models configured. Use "antigravity models add" to add one.');
50
+ return;
51
+ }
52
+ console.log('\nConfigured Models:');
53
+ console.log('─'.repeat(60));
54
+ for (const m of models) {
55
+ const keyStatus = m.apiKey && m.apiKey !== 'none' ? '🔑' : '🔓';
56
+ console.log(` ${keyStatus} ${m.displayName || m.name}`);
57
+ console.log(` Provider: ${m.provider} | Model: ${m.externalModelName}`);
58
+ console.log(` URL: ${m.apiUrl}`);
59
+ console.log();
60
+ }
61
+ console.log(`${models.length} model(s) configured.`);
62
+ });
63
+
64
+ modelsCmd
65
+ .command('add')
66
+ .description('Add a new custom model (interactive wizard)')
67
+ .action(async () => {
68
+ const inquirer = require('inquirer');
69
+
70
+ console.log('\n Add Custom AI Model\n' + '─'.repeat(40));
71
+
72
+ const answers = await inquirer.prompt([
73
+ { type: 'list', name: 'provider', message: 'Provider:', choices: ['openai', 'anthropic', 'google', 'ollama', 'openrouter', 'custom'] },
74
+ { type: 'input', name: 'modelId', message: 'Model ID (e.g. gpt-4o):', validate: (v: string) => v.length > 0 },
75
+ { type: 'input', name: 'displayName', message: 'Display name (optional):' },
76
+ { type: 'password', name: 'apiKey', message: 'API Key:', mask: '*' },
77
+ { type: 'input', name: 'apiUrl', message: 'API URL:', default: (ans: any) => {
78
+ const defaults: Record<string, string> = { openai: 'https://api.openai.com/v1/chat/completions', anthropic: 'https://api.anthropic.com/v1/messages', ollama: 'http://localhost:11434/v1/chat/completions', openrouter: 'https://openrouter.ai/api/v1/chat/completions', custom: 'https://api.together.xyz/v1' };
79
+ return ans.provider === 'google' ? `https://generativelanguage.googleapis.com/v1beta/models/${ans.modelId}:generateContent` : (defaults[ans.provider] || '');
80
+ }},
81
+ ]);
82
+
83
+ const entry: CustomModelEntry = {
84
+ name: 'models/' + answers.modelId,
85
+ displayName: answers.displayName || answers.modelId,
86
+ description: `${answers.displayName || answers.modelId} custom model via Free Antigravity CLI`,
87
+ provider: answers.provider,
88
+ apiKey: answers.apiKey || 'none',
89
+ apiUrl: answers.apiUrl,
90
+ externalModelName: answers.modelId,
91
+ };
92
+
93
+ const result = addModel(entry);
94
+ if (result.success) {
95
+ console.log(`\n Model "${entry.displayName}" added successfully!`);
96
+ console.log(' Restart the proxy or chat to use it.\n');
97
+ } else {
98
+ console.error(`\n Failed: ${result.error}\n`);
99
+ }
100
+ });
101
+
102
+ modelsCmd
103
+ .command('remove')
104
+ .description('Remove a model')
105
+ .argument('<name>', 'Model name or display name')
106
+ .action((name: string) => {
107
+ const result = removeModel(name);
108
+ if (result.success) console.log(`Model "${name}" removed.`);
109
+ else console.error(`Failed: ${result.error}`);
110
+ });
111
+
112
+ modelsCmd
113
+ .command('import')
114
+ .description('Import models from Antigravity desktop custom_models.json')
115
+ .action(() => {
116
+ const desktopPath = path.join(require('os').homedir(), '.gemini', 'antigravity', 'custom_models.json');
117
+ if (!fs.existsSync(desktopPath)) {
118
+ console.log('Desktop custom_models.json not found at:', desktopPath);
119
+ return;
120
+ }
121
+ try {
122
+ const content = fs.readFileSync(desktopPath, 'utf-8');
123
+ const parsed = JSON.parse(content);
124
+ const models = parsed.models || [];
125
+ if (models.length === 0) { console.log('No models in desktop config.'); return; }
126
+
127
+ ensureConfigDir();
128
+ // Decrypt desktop keys using the crypto module
129
+ const { decryptString } = require('./crypto');
130
+ const imported: CustomModelEntry[] = [];
131
+ for (const m of models) {
132
+ let apiKey = m.apiKey || 'none';
133
+ if (m.encrypted && apiKey !== 'none') {
134
+ try { apiKey = decryptString(apiKey); } catch { /* keep as-is */ }
135
+ }
136
+ imported.push({
137
+ name: m.name, displayName: m.displayName, description: m.description,
138
+ provider: m.provider, apiKey, apiUrl: m.apiUrl,
139
+ externalModelName: m.externalModelName, allowUnauthorized: m.allowUnauthorized,
140
+ });
141
+ }
142
+ // Save all
143
+ const { saveModels } = require('./config');
144
+ saveModels(imported);
145
+ console.log(`Imported ${imported.length} model(s) from desktop Antigravity.`);
146
+ for (const m of imported) console.log(` - ${m.displayName} (${m.provider})`);
147
+ } catch (e) { console.error('Import failed:', e); }
148
+ });
149
+
150
+ // --- Proxy command ---
151
+
152
+ program
153
+ .command('proxy')
154
+ .description('Start the proxy server (for IDE integration)')
155
+ .action(async () => {
156
+ try {
157
+ const port = await startProxy();
158
+ console.log(`Proxy running on http://127.0.0.1:${port}`);
159
+ console.log('Press Ctrl+C to stop.');
160
+ // Keep alive
161
+ process.on('SIGINT', async () => { await stopProxy(); process.exit(0); });
162
+ } catch (e) { console.error('Failed to start proxy:', e); }
163
+ });
164
+
165
+ // --- Configure command ---
166
+
167
+ program
168
+ .command('configure')
169
+ .description('Show configuration info')
170
+ .action(() => {
171
+ const configPath = (require('./config') as typeof import('./config')).getModelsPath();
172
+ console.log('Free Antigravity CLI Configuration');
173
+ console.log('─'.repeat(40));
174
+ console.log(`Models file: ${configPath}`);
175
+ console.log(`Models: ${listModels().length} configured`);
176
+ console.log(`Proxy port: ${getProxyPort() || 'not running'}`);
177
+ });
178
+
179
+ // Default: chat if no command
180
+ program.action(() => {
181
+ startChat();
182
+ });
183
+
184
+ program.parse(process.argv);
package/src/config.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Configuration and model storage management.
3
+ * Stores custom models in ~/.free-antigravity/models.json
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import { encryptString, decryptString } from './crypto';
9
+
10
+ export interface CustomModelEntry {
11
+ name: string;
12
+ displayName?: string;
13
+ description?: string;
14
+ provider: string;
15
+ apiKey: string;
16
+ apiUrl: string;
17
+ externalModelName: string;
18
+ allowUnauthorized?: boolean;
19
+ encrypted?: boolean;
20
+ timeout?: number;
21
+ maxRetries?: number;
22
+ }
23
+
24
+ export function getConfigDir(): string {
25
+ return path.join(os.homedir(), '.free-antigravity');
26
+ }
27
+
28
+ export function getModelsPath(): string {
29
+ return path.join(getConfigDir(), 'models.json');
30
+ }
31
+
32
+ export function ensureConfigDir(): void {
33
+ const dir = getConfigDir();
34
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
35
+ }
36
+
37
+ export function loadModels(): CustomModelEntry[] {
38
+ const filePath = getModelsPath();
39
+ if (!fs.existsSync(filePath)) return [];
40
+
41
+ try {
42
+ const content = fs.readFileSync(filePath, 'utf-8');
43
+ const parsed = JSON.parse(content) as { models?: CustomModelEntry[] };
44
+ const models = parsed.models || [];
45
+
46
+ return models.map((m) => {
47
+ if (m.encrypted && m.apiKey && m.apiKey !== 'none') {
48
+ try { return { ...m, apiKey: decryptString(m.apiKey), encrypted: false }; }
49
+ catch { return m; }
50
+ }
51
+ return m;
52
+ });
53
+ } catch (e) {
54
+ console.error('Failed to load models:', e);
55
+ return [];
56
+ }
57
+ }
58
+
59
+ export function saveModels(models: CustomModelEntry[]): void {
60
+ ensureConfigDir();
61
+ const encrypted = models.map((m) => {
62
+ if (m.apiKey && m.apiKey !== 'none' && !m.encrypted) {
63
+ return { ...m, apiKey: encryptString(m.apiKey), encrypted: true };
64
+ }
65
+ return m;
66
+ });
67
+ fs.writeFileSync(getModelsPath(), JSON.stringify({ models: encrypted }, null, 2), 'utf-8');
68
+ }
69
+
70
+ export function addModel(model: CustomModelEntry): { success: boolean; error?: string } {
71
+ const models = loadModels();
72
+ const existingIdx = models.findIndex((m) => m.name === model.name);
73
+
74
+ if (existingIdx !== -1) {
75
+ if (model.apiKey && (model.apiKey.includes('...') || model.apiKey.startsWith('***'))) {
76
+ model.apiKey = models[existingIdx].apiKey;
77
+ }
78
+ models[existingIdx] = model;
79
+ } else {
80
+ models.push(model);
81
+ }
82
+
83
+ saveModels(models);
84
+ return { success: true };
85
+ }
86
+
87
+ export function removeModel(modelName: string): { success: boolean; error?: string } {
88
+ const models = loadModels().filter((m) => m.name !== modelName);
89
+ saveModels(models);
90
+ return { success: true };
91
+ }
92
+
93
+ export function getModel(modelName: string): CustomModelEntry | undefined {
94
+ return loadModels().find((m) => m.name === modelName || m.displayName === modelName);
95
+ }
96
+
97
+ export function listModels(): CustomModelEntry[] {
98
+ return loadModels();
99
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * API key encryption for CLI.
3
+ * Uses base64 encoding as a basic obfuscation layer.
4
+ * For production use, consider integrating with OS keychain via keytar.
5
+ */
6
+ import * as crypto from 'crypto';
7
+
8
+ const ALGORITHM = 'aes-256-cbc';
9
+ // Derive a machine-specific key - in production, use keytar or OS keychain
10
+ const MACHINE_KEY = crypto.createHash('sha256').update(process.env.USERNAME || process.env.USER || 'antigravity-cli').digest();
11
+
12
+ export function encryptString(plainText: string): string {
13
+ if (!plainText || plainText === 'none') return plainText;
14
+ try {
15
+ const iv = crypto.randomBytes(16);
16
+ const cipher = crypto.createCipheriv(ALGORITHM, MACHINE_KEY, iv);
17
+ const encrypted = Buffer.concat([cipher.update(plainText, 'utf-8'), cipher.final()]);
18
+ return 'enc:' + Buffer.concat([iv, encrypted]).toString('base64');
19
+ } catch {
20
+ return 'fallback:' + Buffer.from(plainText, 'utf-8').toString('base64');
21
+ }
22
+ }
23
+
24
+ export function decryptString(encryptedText: string): string {
25
+ if (!encryptedText || encryptedText === 'none') return encryptedText;
26
+
27
+ if (encryptedText.startsWith('enc:')) {
28
+ try {
29
+ const data = Buffer.from(encryptedText.substring(4), 'base64');
30
+ const iv = data.subarray(0, 16);
31
+ const encrypted = data.subarray(16);
32
+ const decipher = crypto.createDecipheriv(ALGORITHM, MACHINE_KEY, iv);
33
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf-8');
34
+ } catch { return 'DECRYPTION_FAILED'; }
35
+ }
36
+
37
+ if (encryptedText.startsWith('fallback:')) {
38
+ try { return Buffer.from(encryptedText.substring(9), 'base64').toString('utf-8'); }
39
+ catch { return 'DECRYPTION_FAILED'; }
40
+ }
41
+
42
+ return encryptedText; // plaintext
43
+ }
44
+
45
+ export function isEncryptionAvailable(): boolean { return true; }
46
+ export function backupFile(filePath: string): void {
47
+ const fs = require('fs');
48
+ if (fs.existsSync(filePath)) fs.copyFileSync(filePath, filePath + '.bak');
49
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,8 @@
1
+ type LogFn = (message: string, ...args: unknown[]) => void;
2
+
3
+ export const log = {
4
+ info: console.log as LogFn,
5
+ warn: console.warn as LogFn,
6
+ error: console.error as LogFn,
7
+ debug: console.debug as LogFn,
8
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Centralized model capability detection.
3
+ * Replaces ~9 duplicate regex blocks across proxy.js.
4
+ */
5
+
6
+ // ─── Types ────────────────────────────────────────────────────────────────
7
+
8
+ export interface CustomModelConfig {
9
+ name: string;
10
+ provider: string;
11
+ externalModelName?: string;
12
+ displayName?: string;
13
+ }
14
+
15
+ export interface ModelCapabilities {
16
+ isThinking: boolean;
17
+ isDeepSeek: boolean;
18
+ isClaude: boolean;
19
+ maxTokens: number;
20
+ maxOutputTokens: number;
21
+ supportsImages: boolean;
22
+ }
23
+
24
+ export interface ModelNameCapabilities {
25
+ isClaudeThinkingModel: boolean;
26
+ isThinkingModel: boolean;
27
+ }
28
+
29
+ // ─── Detection ────────────────────────────────────────────────────────────
30
+
31
+ const THINKING_PATTERN = /thinking|reasoning|reasoner|o1|o3|r1|opus-4|sonnet-4|claude-4|3-7|4-7|3\.7|4\.7/i;
32
+ const DEEPSEEK_PATTERN = /deepseek/i;
33
+ const CLAUDE_PATTERN = /claude|opus|sonnet/i;
34
+ const CLAUDE_THINKING_PATTERN = /opus-4|sonnet-4|claude-4|claude-3-5|claude-3-7/i;
35
+ const THINKING_MODEL_PATTERN = /opus-4|sonnet-4|claude-4/i;
36
+ const IMAGE_SUPPORT_PATTERN = /gpt-4o|gpt-4-turbo|claude|gemini|vision|llava|qwenvl|pixtral|yi-vision|cogvlm|kimi|moonshot/i;
37
+ const NO_IMAGE_PATTERN = /deepseek(?!.*vision)|llama(?!.*vision)|mixtral(?!.*vision)|mistral(?!.*pixtral)|codestral|qwen(?!.*vl)/i;
38
+
39
+ /**
40
+ * Detects model capabilities from a custom model config object.
41
+ */
42
+ export function detectModelCapabilities(m: CustomModelConfig, includeDisplayName = true): ModelCapabilities {
43
+ const nameLower = (m.name || '').toLowerCase();
44
+ const extLower = (m.externalModelName || '').toLowerCase();
45
+ const displayLower = includeDisplayName ? (m.displayName || '').toLowerCase() : '';
46
+
47
+ const isThinking =
48
+ m.provider === 'anthropic' ||
49
+ m.provider === 'openai' ||
50
+ m.provider === 'openrouter' ||
51
+ THINKING_PATTERN.test(nameLower) ||
52
+ THINKING_PATTERN.test(extLower) ||
53
+ (includeDisplayName && THINKING_PATTERN.test(displayLower));
54
+
55
+ const isDeepSeek =
56
+ DEEPSEEK_PATTERN.test(nameLower) ||
57
+ DEEPSEEK_PATTERN.test(extLower) ||
58
+ (includeDisplayName && DEEPSEEK_PATTERN.test(displayLower));
59
+
60
+ const isClaude = m.provider === 'anthropic' || CLAUDE_PATTERN.test(nameLower) || CLAUDE_PATTERN.test(extLower);
61
+
62
+ const maxTokens = isClaude ? 200_000 : 1_048_576;
63
+ const maxOutputTokens = isDeepSeek ? 32_768 : isThinking ? 32_768 : 16_384;
64
+
65
+ // Image support: Claude, GPT-4o, Gemini always support images. DeepSeek, Ollama text models don't.
66
+ const allNames = nameLower + ' ' + extLower + ' ' + displayLower;
67
+ const supportsImages =
68
+ m.provider === 'anthropic' ||
69
+ m.provider === 'google' ||
70
+ (m.provider === 'openai' && IMAGE_SUPPORT_PATTERN.test(allNames)) ||
71
+ (m.provider === 'openrouter' && IMAGE_SUPPORT_PATTERN.test(allNames)) ||
72
+ (IMAGE_SUPPORT_PATTERN.test(allNames) && !NO_IMAGE_PATTERN.test(allNames));
73
+
74
+ return { isThinking, isDeepSeek, isClaude, maxTokens, maxOutputTokens, supportsImages };
75
+ }
76
+
77
+ /**
78
+ * Simplified detection for Gemini↔Anthropic translation (checks modelName string only).
79
+ */
80
+ export function detectModelCapabilitiesByName(modelName: string): ModelNameCapabilities {
81
+ const lower = (modelName || '').toLowerCase();
82
+ return {
83
+ isClaudeThinkingModel: CLAUDE_THINKING_PATTERN.test(lower),
84
+ isThinkingModel: THINKING_MODEL_PATTERN.test(lower),
85
+ };
86
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Provider Translator Registry.
3
+ * Auto-discovers translator modules and provides a unified interface for request/response mapping.
4
+ *
5
+ * To add a new provider:
6
+ * 1. Create a file in ./translators/ named <provider>.ts
7
+ * 2. Export: mapGeminiTo<Provider>, map<Provider>ToGemini, map<Provider>ChunkToGemini
8
+ * 3. The registry detects it automatically — no config changes needed.
9
+ */
10
+
11
+ import { log } from '../logger';
12
+ import * as openaiModule from './translators/openai';
13
+ import * as anthropicModule from './translators/anthropic';
14
+ import * as googleModule from './translators/google';
15
+ import * as ollamaModule from './translators/ollama';
16
+
17
+ // ─── Types ────────────────────────────────────────────────────────────────
18
+
19
+ export interface TranslatorModule {
20
+ mapGeminiToOpenAI?: (body: unknown, modelName: string) => unknown;
21
+ mapOpenAIToGemini?: (res: unknown, modelName: string) => unknown;
22
+ mapOpenAIChunkToGemini?: (chunk: unknown, modelName: string) => unknown | null;
23
+ mapGeminiToAnthropic?: (body: unknown, modelName: string) => unknown;
24
+ mapAnthropicToGemini?: (res: unknown, modelName: string) => unknown;
25
+ mapAnthropicChunkToGemini?: (chunk: unknown, modelName: string) => unknown | null;
26
+ mapGeminiToGoogle?: (body: unknown, modelName: string) => unknown;
27
+ mapGoogleToGemini?: (res: unknown, modelName: string) => unknown;
28
+ mapGoogleChunkToGemini?: (chunk: unknown, modelName: string) => unknown | null;
29
+ getGoogleApiUrl?: (baseUrl: string, modelName: string, isStream: boolean) => string;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ export interface ProviderHeaders {
34
+ 'Content-Type': string;
35
+ Authorization?: string;
36
+ 'x-api-key'?: string;
37
+ 'anthropic-version'?: string;
38
+ 'x-goog-api-key'?: string;
39
+ 'HTTP-Referer'?: string;
40
+ 'X-Title'?: string;
41
+ [key: string]: string | undefined;
42
+ }
43
+
44
+ // ─── Registry State ───────────────────────────────────────────────────────
45
+
46
+ const translators = new Map<string, TranslatorModule>();
47
+
48
+ // ─── Auto-Discovery ───────────────────────────────────────────────────────
49
+
50
+ function loadTranslators(): void {
51
+ translators.set('openai', openaiModule as unknown as TranslatorModule);
52
+ translators.set('anthropic', anthropicModule as unknown as TranslatorModule);
53
+ translators.set('google', googleModule as unknown as TranslatorModule);
54
+ translators.set('ollama', ollamaModule as unknown as TranslatorModule);
55
+ log.info(`[TranslatorRegistry] 4 provider translator(s) loaded: openai, anthropic, google, ollama`);
56
+ }
57
+
58
+ // ─── Public API ───────────────────────────────────────────────────────────
59
+
60
+ export function getTranslator(provider: string): TranslatorModule | null {
61
+ // openrouter uses OpenAI-compatible API — reuse OpenAI translator
62
+ if (provider === 'openrouter') return translators.get('openai') || null;
63
+ const key = provider === 'custom' ? 'openai' : provider;
64
+ return translators.get(key) || translators.get('openai') || null;
65
+ }
66
+
67
+ export function translateRequest(provider: string, geminiBody: unknown, modelName: string): unknown {
68
+ const t = getTranslator(provider);
69
+
70
+ if (provider === 'google') {
71
+ return geminiBody; // passthrough
72
+ }
73
+
74
+ if (provider === 'openai' || provider === 'ollama' || provider === 'custom' || provider === 'openrouter') {
75
+ return t?.mapGeminiToOpenAI ? t.mapGeminiToOpenAI(geminiBody, modelName) : geminiBody;
76
+ }
77
+
78
+ if (provider === 'anthropic') {
79
+ return t?.mapGeminiToAnthropic ? t.mapGeminiToAnthropic(geminiBody, modelName) : geminiBody;
80
+ }
81
+
82
+ // Generic: try mapGeminiTo<Provider> convention
83
+ const fnName = `mapGeminiTo${provider.charAt(0).toUpperCase() + provider.slice(1)}`;
84
+ if (t && typeof t[fnName] === 'function') {
85
+ return (t[fnName] as (...args: unknown[]) => unknown)(geminiBody, modelName);
86
+ }
87
+
88
+ log.warn(`[TranslatorRegistry] No request translator for provider "${provider}", passing through`);
89
+ return geminiBody;
90
+ }
91
+
92
+ export function translateResponse(provider: string, providerRes: unknown, modelName: string): unknown {
93
+ const t = getTranslator(provider);
94
+
95
+ if (provider === 'google') return providerRes;
96
+
97
+ if (provider === 'openai' || provider === 'ollama' || provider === 'custom' || provider === 'openrouter') {
98
+ return t?.mapOpenAIToGemini ? t.mapOpenAIToGemini(providerRes, modelName) : providerRes;
99
+ }
100
+
101
+ if (provider === 'anthropic') {
102
+ return t?.mapAnthropicToGemini ? t.mapAnthropicToGemini(providerRes, modelName) : providerRes;
103
+ }
104
+
105
+ const fnName = `map${provider.charAt(0).toUpperCase() + provider.slice(1)}ToGemini`;
106
+ if (t && typeof t[fnName] === 'function') {
107
+ return (t[fnName] as (...args: unknown[]) => unknown)(providerRes, modelName);
108
+ }
109
+
110
+ log.warn(`[TranslatorRegistry] No response translator for provider "${provider}", passing through`);
111
+ return providerRes;
112
+ }
113
+
114
+ export function translateStreamChunk(provider: string, chunk: unknown, modelName: string): unknown {
115
+ const t = getTranslator(provider);
116
+
117
+ if (provider === 'google') {
118
+ return t?.mapGoogleChunkToGemini ? t.mapGoogleChunkToGemini(chunk, modelName) : null;
119
+ }
120
+
121
+ if (provider === 'openai' || provider === 'ollama' || provider === 'custom' || provider === 'openrouter') {
122
+ return t?.mapOpenAIChunkToGemini ? t.mapOpenAIChunkToGemini(chunk, modelName) : null;
123
+ }
124
+
125
+ if (provider === 'anthropic') {
126
+ return t?.mapAnthropicChunkToGemini ? t.mapAnthropicChunkToGemini(chunk, modelName) : null;
127
+ }
128
+
129
+ const fnName = `map${provider.charAt(0).toUpperCase() + provider.slice(1)}ChunkToGemini`;
130
+ if (t && typeof t[fnName] === 'function') {
131
+ return (t[fnName] as (...args: unknown[]) => unknown)(chunk, modelName);
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ export function getProviderHeaders(provider: string, apiKey: string): ProviderHeaders {
138
+ const headers: ProviderHeaders = { 'Content-Type': 'application/json' };
139
+
140
+ switch (provider) {
141
+ case 'openai':
142
+ case 'custom':
143
+ headers['Authorization'] = `Bearer ${apiKey}`;
144
+ break;
145
+ case 'openrouter':
146
+ headers['Authorization'] = `Bearer ${apiKey}`;
147
+ // OpenRouter optional headers for leaderboard attribution
148
+ headers['HTTP-Referer'] = 'https://antigravity.google';
149
+ headers['X-Title'] = 'Antigravity';
150
+ break;
151
+ case 'anthropic':
152
+ headers['x-api-key'] = apiKey;
153
+ headers['anthropic-version'] = '2025-04-01';
154
+ break;
155
+ case 'google':
156
+ headers['x-goog-api-key'] = apiKey;
157
+ break;
158
+ case 'ollama':
159
+ // Ollama typically doesn't need auth headers
160
+ break;
161
+ default:
162
+ if (apiKey && apiKey !== 'none') {
163
+ headers['Authorization'] = `Bearer ${apiKey}`;
164
+ }
165
+ break;
166
+ }
167
+
168
+ return headers;
169
+ }
170
+
171
+ export function supportsStreaming(provider: string): boolean {
172
+ return ['openai', 'ollama', 'custom', 'anthropic', 'google', 'openrouter'].includes(provider);
173
+ }
174
+
175
+ // ─── URL Helpers ──────────────────────────────────────────────────────────
176
+
177
+ export function getProviderUrl(
178
+ baseUrl: string,
179
+ modelName: string,
180
+ isStream: boolean,
181
+ translator: TranslatorModule | null,
182
+ ): string {
183
+ // Google AI Studio: dynamic streaming vs non-streaming URL
184
+ if (translator && typeof translator['getGoogleApiUrl'] === 'function') {
185
+ return (translator['getGoogleApiUrl'] as (...args: unknown[]) => string)(baseUrl, modelName, isStream);
186
+ }
187
+ // Ollama: normalize to standard /v1/chat/completions endpoint
188
+ if (translator && typeof translator['getOllamaApiUrl'] === 'function') {
189
+ return (translator['getOllamaApiUrl'] as (...args: unknown[]) => string)(baseUrl);
190
+ }
191
+ return baseUrl;
192
+ }
193
+
194
+ // ─── Boot ─────────────────────────────────────────────────────────────────
195
+
196
+ loadTranslators();