forked from boos/BoosAPI
Refactor proxy-core surfaces and transformer boundaries (#163)
* refactor: add canonical request contracts * test: harden transformer boundary contracts * refactor: adapt protocol transformers to canonical contract * refactor: add cli profile registry * refactor: extract provider profiles * refactor: add conductor and provider executors * refactor: add models surface * refactor: migrate responses surface * refactor: migrate chat surface * refactor: migrate gemini surface * refactor: migrate file surfaces * test: align runtime executor undici mock * fix: preserve compact SSE closeout payloads * feat: support all-api-hub v2 backup imports * fix: aggregate codex oauth SSE final payloads * fix: harden responses SSE failure handling
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderAction, ProviderProfile } from './types.js';
|
||||
|
||||
const ANTIGRAVITY_RUNTIME_USER_AGENT = 'antigravity/1.19.6 darwin/arm64';
|
||||
|
||||
function asTrimmedString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function resolveAction(action: ProviderAction | undefined, stream: boolean): ProviderAction {
|
||||
if (action) return action;
|
||||
return stream ? 'streamGenerateContent' : 'generateContent';
|
||||
}
|
||||
|
||||
function resolvePath(action: ProviderAction): string {
|
||||
if (action === 'countTokens') return '/v1internal:countTokens';
|
||||
if (action === 'streamGenerateContent') return '/v1internal:streamGenerateContent?alt=sse';
|
||||
return '/v1internal:generateContent';
|
||||
}
|
||||
|
||||
export const antigravityProviderProfile: ProviderProfile = {
|
||||
id: 'antigravity',
|
||||
prepareRequest(input: PrepareProviderRequestInput): PreparedProviderRequest {
|
||||
const action = resolveAction(input.action, input.stream);
|
||||
const projectId = asTrimmedString(input.oauthProjectId);
|
||||
return {
|
||||
path: resolvePath(action),
|
||||
headers: {
|
||||
Authorization: input.baseHeaders.Authorization,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: action === 'streamGenerateContent' ? 'text/event-stream' : 'application/json',
|
||||
'User-Agent': ANTIGRAVITY_RUNTIME_USER_AGENT,
|
||||
},
|
||||
body: {
|
||||
project: projectId,
|
||||
model: input.modelName,
|
||||
request: input.body,
|
||||
},
|
||||
runtime: {
|
||||
executor: 'antigravity',
|
||||
modelName: input.modelName,
|
||||
stream: action === 'streamGenerateContent',
|
||||
oauthProjectId: projectId,
|
||||
action,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderProfile } from './types.js';
|
||||
|
||||
const CLAUDE_DEFAULT_USER_AGENT = 'claude-cli/2.1.63 (external, cli)';
|
||||
const CLAUDE_DEFAULT_BETA_HEADER = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05';
|
||||
|
||||
function headerValueToString(value: unknown): string | null {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || null;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (typeof item !== 'string') continue;
|
||||
const trimmed = item.trim();
|
||||
if (trimmed) return trimmed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getInputHeader(
|
||||
headers: Record<string, unknown> | Record<string, string> | undefined,
|
||||
key: string,
|
||||
): string | null {
|
||||
if (!headers) return null;
|
||||
for (const [candidateKey, candidateValue] of Object.entries(headers)) {
|
||||
if (candidateKey.toLowerCase() !== key.toLowerCase()) continue;
|
||||
return headerValueToString(candidateValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergeClaudeBetaHeader(
|
||||
explicitValue: string | null,
|
||||
extraBetas: string[] = [],
|
||||
): string {
|
||||
const source = explicitValue || CLAUDE_DEFAULT_BETA_HEADER;
|
||||
const seen = new Set<string>();
|
||||
const merged: string[] = [];
|
||||
for (const entry of source.split(',')) {
|
||||
const normalized = entry.trim();
|
||||
if (!normalized || seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
merged.push(normalized);
|
||||
}
|
||||
if (!explicitValue) {
|
||||
for (const entry of extraBetas) {
|
||||
const normalized = entry.trim();
|
||||
if (!normalized || seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
merged.push(normalized);
|
||||
}
|
||||
}
|
||||
return merged.join(',');
|
||||
}
|
||||
|
||||
function buildClaudeRuntimeHeaders(input: {
|
||||
baseHeaders: Record<string, string>;
|
||||
claudeHeaders: Record<string, string>;
|
||||
anthropicVersion: string;
|
||||
stream: boolean;
|
||||
isClaudeOauthUpstream: boolean;
|
||||
tokenValue: string;
|
||||
}): Record<string, string> {
|
||||
const anthropicBeta = mergeClaudeBetaHeader(
|
||||
getInputHeader(input.claudeHeaders, 'anthropic-beta'),
|
||||
);
|
||||
const headers: Record<string, string> = {
|
||||
...input.baseHeaders,
|
||||
...input.claudeHeaders,
|
||||
'anthropic-version': input.anthropicVersion,
|
||||
...(anthropicBeta ? { 'anthropic-beta': anthropicBeta } : {}),
|
||||
'Anthropic-Dangerous-Direct-Browser-Access': 'true',
|
||||
'X-App': 'cli',
|
||||
'X-Stainless-Retry-Count': getInputHeader(input.claudeHeaders, 'x-stainless-retry-count') || '0',
|
||||
'X-Stainless-Runtime-Version': getInputHeader(input.claudeHeaders, 'x-stainless-runtime-version') || 'v24.3.0',
|
||||
'X-Stainless-Package-Version': getInputHeader(input.claudeHeaders, 'x-stainless-package-version') || '0.74.0',
|
||||
'X-Stainless-Runtime': getInputHeader(input.claudeHeaders, 'x-stainless-runtime') || 'node',
|
||||
'X-Stainless-Lang': getInputHeader(input.claudeHeaders, 'x-stainless-lang') || 'js',
|
||||
'X-Stainless-Arch': getInputHeader(input.claudeHeaders, 'x-stainless-arch') || 'x64',
|
||||
'X-Stainless-Os': getInputHeader(input.claudeHeaders, 'x-stainless-os') || 'Windows',
|
||||
'X-Stainless-Timeout': getInputHeader(input.claudeHeaders, 'x-stainless-timeout') || '600',
|
||||
'User-Agent': getInputHeader(input.claudeHeaders, 'user-agent') || CLAUDE_DEFAULT_USER_AGENT,
|
||||
Connection: 'keep-alive',
|
||||
Accept: input.stream ? 'text/event-stream' : 'application/json',
|
||||
'Accept-Encoding': input.stream ? 'identity' : 'gzip, deflate, br, zstd',
|
||||
};
|
||||
if (input.isClaudeOauthUpstream) {
|
||||
headers.Authorization = `Bearer ${input.tokenValue}`;
|
||||
} else {
|
||||
headers['x-api-key'] = input.tokenValue;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export const claudeProviderProfile: ProviderProfile = {
|
||||
id: 'claude',
|
||||
prepareRequest(input: PrepareProviderRequestInput): PreparedProviderRequest {
|
||||
const anthropicVersion = (
|
||||
input.claudeHeaders?.['anthropic-version']
|
||||
|| input.baseHeaders['anthropic-version']
|
||||
|| '2023-06-01'
|
||||
);
|
||||
const isClaudeOauthUpstream = input.sitePlatform?.trim().toLowerCase() === 'claude'
|
||||
&& input.oauthProvider === 'claude';
|
||||
const isCountTokens = input.action === 'countTokens';
|
||||
|
||||
return {
|
||||
path: isCountTokens ? '/v1/messages/count_tokens?beta=true' : '/v1/messages',
|
||||
headers: buildClaudeRuntimeHeaders({
|
||||
baseHeaders: input.baseHeaders,
|
||||
claudeHeaders: input.claudeHeaders ?? {},
|
||||
anthropicVersion,
|
||||
stream: isCountTokens ? false : input.stream,
|
||||
isClaudeOauthUpstream,
|
||||
tokenValue: input.tokenValue,
|
||||
}),
|
||||
body: input.body,
|
||||
runtime: {
|
||||
executor: 'claude',
|
||||
modelName: input.modelName,
|
||||
stream: isCountTokens ? false : input.stream,
|
||||
oauthProjectId: null,
|
||||
...(isCountTokens ? { action: 'countTokens' } : {}),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderProfile } from './types.js';
|
||||
|
||||
const CODEX_CLIENT_VERSION = '0.101.0';
|
||||
const CODEX_DEFAULT_USER_AGENT = 'codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464';
|
||||
|
||||
function asTrimmedString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function headerValueToString(value: unknown): string | null {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || null;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (typeof item !== 'string') continue;
|
||||
const trimmed = item.trim();
|
||||
if (trimmed) return trimmed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getInputHeader(
|
||||
headers: Record<string, unknown> | Record<string, string> | undefined,
|
||||
key: string,
|
||||
): string | null {
|
||||
if (!headers) return null;
|
||||
for (const [candidateKey, candidateValue] of Object.entries(headers)) {
|
||||
if (candidateKey.toLowerCase() !== key.toLowerCase()) continue;
|
||||
return headerValueToString(candidateValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function uuidFromSeed(seed: string): string {
|
||||
const hash = createHash('sha1').update(seed).digest();
|
||||
const bytes = new Uint8Array(hash.subarray(0, 16));
|
||||
bytes[6] = (bytes[6]! & 0x0f) | 0x50;
|
||||
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
||||
const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('');
|
||||
return [
|
||||
hex.slice(0, 8),
|
||||
hex.slice(8, 12),
|
||||
hex.slice(12, 16),
|
||||
hex.slice(16, 20),
|
||||
hex.slice(20, 32),
|
||||
].join('-');
|
||||
}
|
||||
|
||||
function buildCodexRuntimeHeaders(input: {
|
||||
baseHeaders: Record<string, string>;
|
||||
providerHeaders?: Record<string, string>;
|
||||
explicitSessionId?: string | null;
|
||||
continuityKey?: string | null;
|
||||
}): Record<string, string> {
|
||||
const authorization = (
|
||||
getInputHeader(input.baseHeaders, 'authorization')
|
||||
|| getInputHeader(input.baseHeaders, 'Authorization')
|
||||
|| ''
|
||||
);
|
||||
const originator = getInputHeader(input.providerHeaders, 'originator') || 'codex_cli_rs';
|
||||
const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id');
|
||||
const version = getInputHeader(input.baseHeaders, 'version') || CODEX_CLIENT_VERSION;
|
||||
const userAgent = getInputHeader(input.baseHeaders, 'user-agent') || CODEX_DEFAULT_USER_AGENT;
|
||||
const explicitSessionId = asTrimmedString(input.explicitSessionId);
|
||||
const continuityKey = asTrimmedString(input.continuityKey);
|
||||
const sessionId = (
|
||||
getInputHeader(input.baseHeaders, 'session_id')
|
||||
|| getInputHeader(input.baseHeaders, 'session-id')
|
||||
|| explicitSessionId
|
||||
|| (continuityKey ? uuidFromSeed(`metapi:codex:${continuityKey}`) : null)
|
||||
|| randomUUID()
|
||||
);
|
||||
const conversationId = (
|
||||
getInputHeader(input.baseHeaders, 'conversation_id')
|
||||
|| getInputHeader(input.baseHeaders, 'conversation-id')
|
||||
|| explicitSessionId
|
||||
|| (continuityKey ? sessionId : null)
|
||||
);
|
||||
|
||||
return {
|
||||
Authorization: authorization,
|
||||
'Content-Type': 'application/json',
|
||||
...(accountId ? { 'Chatgpt-Account-Id': accountId } : {}),
|
||||
Originator: originator,
|
||||
Version: version,
|
||||
Session_id: sessionId,
|
||||
...(conversationId ? { Conversation_id: conversationId } : {}),
|
||||
'User-Agent': userAgent,
|
||||
Accept: 'text/event-stream',
|
||||
Connection: 'Keep-Alive',
|
||||
};
|
||||
}
|
||||
|
||||
export const codexProviderProfile: ProviderProfile = {
|
||||
id: 'codex',
|
||||
prepareRequest(input: PrepareProviderRequestInput): PreparedProviderRequest {
|
||||
const headers = buildCodexRuntimeHeaders({
|
||||
baseHeaders: input.baseHeaders,
|
||||
providerHeaders: input.providerHeaders,
|
||||
explicitSessionId: asTrimmedString(input.codexExplicitSessionId) || null,
|
||||
continuityKey: asTrimmedString(input.codexSessionCacheKey) || null,
|
||||
});
|
||||
const codexSessionId = getInputHeader(headers, 'session_id') || getInputHeader(headers, 'session-id');
|
||||
const shouldInjectDerivedPromptCacheKey = !!codexSessionId
|
||||
&& !asTrimmedString(input.body.prompt_cache_key)
|
||||
&& !asTrimmedString(input.codexExplicitSessionId)
|
||||
&& !!asTrimmedString(input.codexSessionCacheKey);
|
||||
const body = shouldInjectDerivedPromptCacheKey
|
||||
? {
|
||||
...input.body,
|
||||
prompt_cache_key: codexSessionId,
|
||||
}
|
||||
: input.body;
|
||||
|
||||
return {
|
||||
path: '/responses',
|
||||
headers,
|
||||
body,
|
||||
runtime: {
|
||||
executor: 'codex',
|
||||
modelName: input.modelName,
|
||||
stream: input.stream,
|
||||
oauthProjectId: asTrimmedString(input.oauthProjectId) || null,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderAction, ProviderProfile } from './types.js';
|
||||
|
||||
function asTrimmedString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function headerValueToString(value: unknown): string | null {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || null;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
if (typeof item !== 'string') continue;
|
||||
const trimmed = item.trim();
|
||||
if (trimmed) return trimmed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getInputHeader(
|
||||
headers: Record<string, unknown> | Record<string, string> | undefined,
|
||||
key: string,
|
||||
): string | null {
|
||||
if (!headers) return null;
|
||||
for (const [candidateKey, candidateValue] of Object.entries(headers)) {
|
||||
if (candidateKey.toLowerCase() !== key.toLowerCase()) continue;
|
||||
return headerValueToString(candidateValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseGeminiCliUserAgentRuntime(userAgent: string | null): {
|
||||
version: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
} | null {
|
||||
if (!userAgent) return null;
|
||||
const match = /^GeminiCLI\/([^/]+)\/[^ ]+ \(([^;]+); ([^)]+)\)$/i.exec(userAgent.trim());
|
||||
if (!match) return null;
|
||||
return {
|
||||
version: match[1] || '0.31.0',
|
||||
platform: match[2] || 'win32',
|
||||
arch: match[3] || 'x64',
|
||||
};
|
||||
}
|
||||
|
||||
function buildGeminiCLIUserAgent(modelName: string, existingUserAgent?: string | null): string {
|
||||
const parsed = parseGeminiCliUserAgentRuntime(existingUserAgent ?? null);
|
||||
const version = parsed?.version || '0.31.0';
|
||||
const platform = parsed?.platform || 'win32';
|
||||
const arch = parsed?.arch || 'x64';
|
||||
const effectiveModel = asTrimmedString(modelName) || 'unknown';
|
||||
return `GeminiCLI/${version}/${effectiveModel} (${platform}; ${arch})`;
|
||||
}
|
||||
|
||||
function resolveAction(action: ProviderAction | undefined, stream: boolean): ProviderAction {
|
||||
if (action) return action;
|
||||
return stream ? 'streamGenerateContent' : 'generateContent';
|
||||
}
|
||||
|
||||
function resolvePath(action: ProviderAction): string {
|
||||
if (action === 'countTokens') return '/v1internal:countTokens';
|
||||
if (action === 'streamGenerateContent') return '/v1internal:streamGenerateContent?alt=sse';
|
||||
return '/v1internal:generateContent';
|
||||
}
|
||||
|
||||
export const geminiCliProviderProfile: ProviderProfile = {
|
||||
id: 'gemini-cli',
|
||||
prepareRequest(input: PrepareProviderRequestInput): PreparedProviderRequest {
|
||||
const projectId = asTrimmedString(input.oauthProjectId);
|
||||
if (!projectId) {
|
||||
throw new Error('gemini-cli oauth project id missing');
|
||||
}
|
||||
const action = resolveAction(input.action, input.stream);
|
||||
const apiClient = (
|
||||
getInputHeader(input.providerHeaders, 'x-goog-api-client')
|
||||
|| getInputHeader(input.baseHeaders, 'x-goog-api-client')
|
||||
);
|
||||
const userAgent = buildGeminiCLIUserAgent(
|
||||
input.modelName,
|
||||
getInputHeader(input.providerHeaders, 'user-agent') || getInputHeader(input.baseHeaders, 'user-agent'),
|
||||
);
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: input.baseHeaders.Authorization,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': userAgent,
|
||||
};
|
||||
if (apiClient) {
|
||||
headers['X-Goog-Api-Client'] = apiClient;
|
||||
}
|
||||
if (action === 'streamGenerateContent') {
|
||||
headers.Accept = 'text/event-stream';
|
||||
}
|
||||
return {
|
||||
path: resolvePath(action),
|
||||
headers,
|
||||
body: {
|
||||
project: projectId,
|
||||
model: input.modelName,
|
||||
request: input.body,
|
||||
},
|
||||
runtime: {
|
||||
executor: 'gemini-cli',
|
||||
modelName: input.modelName,
|
||||
stream: action === 'streamGenerateContent',
|
||||
oauthProjectId: projectId,
|
||||
action,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveProviderProfile } from './registry.js';
|
||||
|
||||
describe('resolveProviderProfile', () => {
|
||||
it('builds codex provider requests with codex-specific path, headers, and runtime metadata', () => {
|
||||
const profile = resolveProviderProfile('codex');
|
||||
expect(profile?.id).toBe('codex');
|
||||
|
||||
const result = profile!.prepareRequest({
|
||||
endpoint: 'responses',
|
||||
modelName: 'gpt-5.2-codex',
|
||||
stream: true,
|
||||
tokenValue: 'oauth-access-token',
|
||||
sitePlatform: 'codex',
|
||||
baseHeaders: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
},
|
||||
providerHeaders: {
|
||||
Originator: 'codex_cli_rs',
|
||||
'Chatgpt-Account-Id': 'chatgpt-account-123',
|
||||
},
|
||||
codexSessionCacheKey: 'gpt-5.2-codex:user-123',
|
||||
body: {
|
||||
model: 'gpt-5.2-codex',
|
||||
stream: true,
|
||||
store: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.path).toBe('/responses');
|
||||
expect(result.headers.Authorization).toBe('Bearer oauth-access-token');
|
||||
expect(result.headers.Originator).toBe('codex_cli_rs');
|
||||
expect(result.headers['Chatgpt-Account-Id']).toBe('chatgpt-account-123');
|
||||
expect(result.headers.Session_id).toMatch(/^[0-9a-f-]{36}$/i);
|
||||
expect(result.headers.Conversation_id).toBe(result.headers.Session_id);
|
||||
expect(result.runtime).toMatchObject({
|
||||
executor: 'codex',
|
||||
modelName: 'gpt-5.2-codex',
|
||||
stream: true,
|
||||
});
|
||||
expect(result.body.prompt_cache_key).toBe(result.headers.Session_id);
|
||||
});
|
||||
|
||||
it('builds claude provider requests without rebuilding protocol bodies', () => {
|
||||
const profile = resolveProviderProfile('claude');
|
||||
expect(profile?.id).toBe('claude');
|
||||
|
||||
const protocolBody = {
|
||||
model: 'claude-opus-4-6',
|
||||
max_tokens: 256,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
};
|
||||
|
||||
const result = profile!.prepareRequest({
|
||||
endpoint: 'messages',
|
||||
modelName: 'claude-opus-4-6',
|
||||
stream: false,
|
||||
tokenValue: 'oauth-access-token',
|
||||
oauthProvider: 'claude',
|
||||
sitePlatform: 'claude',
|
||||
baseHeaders: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
claudeHeaders: {},
|
||||
body: protocolBody,
|
||||
});
|
||||
|
||||
expect(result.path).toBe('/v1/messages');
|
||||
expect(result.headers.Authorization).toBe('Bearer oauth-access-token');
|
||||
expect(result.headers['x-api-key']).toBeUndefined();
|
||||
expect(result.headers['anthropic-version']).toBe('2023-06-01');
|
||||
expect(result.runtime).toMatchObject({
|
||||
executor: 'claude',
|
||||
modelName: 'claude-opus-4-6',
|
||||
stream: false,
|
||||
});
|
||||
expect(result.body).toBe(protocolBody);
|
||||
});
|
||||
|
||||
it('builds gemini-cli provider requests with wrapped runtime envelope and project validation', () => {
|
||||
const profile = resolveProviderProfile('gemini-cli');
|
||||
expect(profile?.id).toBe('gemini-cli');
|
||||
|
||||
const protocolBody = {
|
||||
contents: [{ role: 'user', parts: [{ text: 'hello gemini cli' }] }],
|
||||
};
|
||||
|
||||
const result = profile!.prepareRequest({
|
||||
endpoint: 'chat',
|
||||
modelName: 'gemini-2.5-pro',
|
||||
stream: false,
|
||||
tokenValue: 'oauth-access-token',
|
||||
oauthProvider: 'gemini-cli',
|
||||
oauthProjectId: 'project-demo',
|
||||
sitePlatform: 'gemini-cli',
|
||||
baseHeaders: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
},
|
||||
providerHeaders: {
|
||||
'User-Agent': 'GeminiCLI/0.31.0/unknown (win32; x64)',
|
||||
'X-Goog-Api-Client': 'google-genai-sdk/1.41.0 gl-node/v22.19.0',
|
||||
},
|
||||
body: protocolBody,
|
||||
});
|
||||
|
||||
expect(result.path).toBe('/v1internal:generateContent');
|
||||
expect(result.headers.Authorization).toBe('Bearer oauth-access-token');
|
||||
expect(result.headers['User-Agent']).toBe('GeminiCLI/0.31.0/gemini-2.5-pro (win32; x64)');
|
||||
expect(result.headers['X-Goog-Api-Client']).toContain('google-genai-sdk/');
|
||||
expect(result.runtime).toMatchObject({
|
||||
executor: 'gemini-cli',
|
||||
modelName: 'gemini-2.5-pro',
|
||||
oauthProjectId: 'project-demo',
|
||||
action: 'generateContent',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
project: 'project-demo',
|
||||
model: 'gemini-2.5-pro',
|
||||
request: protocolBody,
|
||||
});
|
||||
expect((result.body as Record<string, unknown>).request).toBe(protocolBody);
|
||||
|
||||
expect(() => profile!.prepareRequest({
|
||||
endpoint: 'chat',
|
||||
modelName: 'gemini-2.5-pro',
|
||||
stream: false,
|
||||
tokenValue: 'oauth-access-token',
|
||||
oauthProvider: 'gemini-cli',
|
||||
sitePlatform: 'gemini-cli',
|
||||
baseHeaders: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
},
|
||||
body: protocolBody,
|
||||
})).toThrow(/project id missing/i);
|
||||
});
|
||||
|
||||
it('builds antigravity provider requests without leaking gemini-cli client headers', () => {
|
||||
const profile = resolveProviderProfile('antigravity');
|
||||
expect(profile?.id).toBe('antigravity');
|
||||
|
||||
const protocolBody = {
|
||||
contents: [{ role: 'user', parts: [{ text: 'hello antigravity' }] }],
|
||||
};
|
||||
|
||||
const result = profile!.prepareRequest({
|
||||
endpoint: 'chat',
|
||||
modelName: 'gemini-3-pro-preview',
|
||||
stream: true,
|
||||
tokenValue: 'oauth-access-token',
|
||||
oauthProjectId: 'project-demo',
|
||||
sitePlatform: 'antigravity',
|
||||
baseHeaders: {
|
||||
Authorization: 'Bearer oauth-access-token',
|
||||
},
|
||||
providerHeaders: {
|
||||
'User-Agent': 'GeminiCLI/0.31.0/unknown (win32; x64)',
|
||||
'X-Goog-Api-Client': 'google-genai-sdk/1.41.0 gl-node/v22.19.0',
|
||||
},
|
||||
body: protocolBody,
|
||||
});
|
||||
|
||||
expect(result.path).toBe('/v1internal:streamGenerateContent?alt=sse');
|
||||
expect(result.headers.Authorization).toBe('Bearer oauth-access-token');
|
||||
expect(result.headers['User-Agent']).toBe('antigravity/1.19.6 darwin/arm64');
|
||||
expect(result.headers['X-Goog-Api-Client']).toBeUndefined();
|
||||
expect(result.headers.Accept).toBe('text/event-stream');
|
||||
expect(result.runtime).toMatchObject({
|
||||
executor: 'antigravity',
|
||||
modelName: 'gemini-3-pro-preview',
|
||||
oauthProjectId: 'project-demo',
|
||||
action: 'streamGenerateContent',
|
||||
});
|
||||
expect(result.body).toEqual({
|
||||
project: 'project-demo',
|
||||
model: 'gemini-3-pro-preview',
|
||||
request: protocolBody,
|
||||
});
|
||||
expect((result.body as Record<string, unknown>).request).toBe(protocolBody);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { antigravityProviderProfile } from './antigravityProviderProfile.js';
|
||||
import { claudeProviderProfile } from './claudeProviderProfile.js';
|
||||
import { codexProviderProfile } from './codexProviderProfile.js';
|
||||
import { geminiCliProviderProfile } from './geminiCliProviderProfile.js';
|
||||
import type { ProviderProfile } from './types.js';
|
||||
|
||||
const providerProfilesByPlatform: Record<string, ProviderProfile> = {
|
||||
codex: codexProviderProfile,
|
||||
claude: claudeProviderProfile,
|
||||
'gemini-cli': geminiCliProviderProfile,
|
||||
antigravity: antigravityProviderProfile,
|
||||
};
|
||||
|
||||
export function resolveProviderProfile(sitePlatform?: string | null): ProviderProfile | null {
|
||||
const normalized = typeof sitePlatform === 'string' ? sitePlatform.trim().toLowerCase() : '';
|
||||
return providerProfilesByPlatform[normalized] ?? null;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
export type ProviderProfileId =
|
||||
| 'codex'
|
||||
| 'claude'
|
||||
| 'gemini-cli'
|
||||
| 'antigravity';
|
||||
|
||||
export type ProviderEndpoint =
|
||||
| 'chat'
|
||||
| 'messages'
|
||||
| 'responses';
|
||||
|
||||
export type ProviderAction =
|
||||
| 'generateContent'
|
||||
| 'streamGenerateContent'
|
||||
| 'countTokens';
|
||||
|
||||
export type ProviderRuntimeDescriptor = {
|
||||
executor: 'default' | 'codex' | 'gemini-cli' | 'antigravity' | 'claude';
|
||||
modelName?: string;
|
||||
stream?: boolean;
|
||||
oauthProjectId?: string | null;
|
||||
action?: ProviderAction;
|
||||
};
|
||||
|
||||
export type PreparedProviderRequest = {
|
||||
path: string;
|
||||
headers: Record<string, string>;
|
||||
body: Record<string, unknown>;
|
||||
runtime: ProviderRuntimeDescriptor;
|
||||
};
|
||||
|
||||
export type PrepareProviderRequestInput = {
|
||||
endpoint: ProviderEndpoint;
|
||||
modelName: string;
|
||||
stream: boolean;
|
||||
tokenValue: string;
|
||||
oauthProvider?: string;
|
||||
oauthProjectId?: string;
|
||||
sitePlatform?: string;
|
||||
baseHeaders: Record<string, string>;
|
||||
providerHeaders?: Record<string, string>;
|
||||
claudeHeaders?: Record<string, string>;
|
||||
codexSessionCacheKey?: string | null;
|
||||
codexExplicitSessionId?: string | null;
|
||||
body: Record<string, unknown>;
|
||||
action?: ProviderAction;
|
||||
};
|
||||
|
||||
export type ProviderProfile = {
|
||||
id: ProviderProfileId;
|
||||
prepareRequest(input: PrepareProviderRequestInput): PreparedProviderRequest;
|
||||
};
|
||||
Reference in New Issue
Block a user