refactor: consolidate single-source routing and protocol helpers

This commit is contained in:
Cita
2026-03-22 21:27:09 +08:00
parent 96e7614835
commit 9290e51ecc
64 changed files with 3571 additions and 2681 deletions
@@ -1,97 +1,5 @@
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': 'gzip, deflate, br, zstd',
};
if (input.isClaudeOauthUpstream) {
headers.Authorization = `Bearer ${input.tokenValue}`;
} else {
headers['x-api-key'] = input.tokenValue;
}
return headers;
}
import { buildClaudeRuntimeHeaders } from './headerUtils.js';
export const claudeProviderProfile: ProviderProfile = {
id: 'claude',
@@ -1,130 +1,34 @@
import { createHash, randomUUID } from 'node:crypto';
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderProfile } from './types.js';
import { config } from '../../config.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';
import { buildCodexRuntimeHeaders, getInputHeader } from './headerUtils.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 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>;
oauthProvider?: 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 isCodexOauth = asTrimmedString(input.oauthProvider).toLowerCase() === 'codex';
const configuredUserAgent = isCodexOauth ? asTrimmedString(config.codexHeaderDefaults.userAgent) : '';
const websocketTransport = getInputHeader(input.baseHeaders, 'x-metapi-responses-websocket-transport') === '1';
const configuredBetaFeatures = (
isCodexOauth && websocketTransport
? asTrimmedString(config.codexHeaderDefaults.betaFeatures)
: ''
);
const userAgent = configuredUserAgent || getInputHeader(input.baseHeaders, 'user-agent') || CODEX_DEFAULT_USER_AGENT;
const codexBetaFeatures = getInputHeader(input.baseHeaders, 'x-codex-beta-features') || configuredBetaFeatures;
const codexTurnState = getInputHeader(input.baseHeaders, 'x-codex-turn-state');
const codexTurnMetadata = getInputHeader(input.baseHeaders, 'x-codex-turn-metadata');
const timingMetrics = getInputHeader(input.baseHeaders, 'x-responsesapi-include-timing-metrics');
const openAiBeta = getInputHeader(input.baseHeaders, 'openai-beta')
|| (websocketTransport ? asTrimmedString(config.codexResponsesWebsocketBeta) : null);
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,
...(codexBetaFeatures ? { 'x-codex-beta-features': codexBetaFeatures } : {}),
...(codexTurnState ? { 'x-codex-turn-state': codexTurnState } : {}),
...(codexTurnMetadata ? { 'x-codex-turn-metadata': codexTurnMetadata } : {}),
...(timingMetrics ? { 'x-responsesapi-include-timing-metrics': timingMetrics } : {}),
...(openAiBeta ? { 'OpenAI-Beta': openAiBeta } : {}),
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 isCodexOauth = asTrimmedString(input.oauthProvider).toLowerCase() === 'codex';
const websocketTransport = getInputHeader(input.baseHeaders, 'x-metapi-responses-websocket-transport') === '1';
const configuredUserAgent = isCodexOauth ? asTrimmedString(config.codexHeaderDefaults.userAgent) : '';
const configuredBetaFeatures = (
isCodexOauth && websocketTransport
? asTrimmedString(config.codexHeaderDefaults.betaFeatures)
: ''
);
const headers = buildCodexRuntimeHeaders({
baseHeaders: input.baseHeaders,
providerHeaders: input.providerHeaders,
oauthProvider: input.oauthProvider,
explicitSessionId: asTrimmedString(input.codexExplicitSessionId) || null,
continuityKey: asTrimmedString(input.codexSessionCacheKey) || null,
userAgentOverride: configuredUserAgent || null,
codexBetaFeatures: getInputHeader(input.baseHeaders, 'x-codex-beta-features') || configuredBetaFeatures,
codexTurnState: getInputHeader(input.baseHeaders, 'x-codex-turn-state'),
codexTurnMetadata: getInputHeader(input.baseHeaders, 'x-codex-turn-metadata'),
timingMetrics: getInputHeader(input.baseHeaders, 'x-responsesapi-include-timing-metrics'),
openAiBeta: getInputHeader(input.baseHeaders, 'openai-beta')
|| (websocketTransport ? asTrimmedString(config.codexResponsesWebsocketBeta) : null),
});
const codexSessionId = getInputHeader(headers, 'session_id') || getInputHeader(headers, 'session-id');
const shouldInjectDerivedPromptCacheKey = !!codexSessionId
@@ -1,60 +1,10 @@
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderAction, ProviderProfile } from './types.js';
import { buildGeminiCliRuntimeHeaders } from './headerUtils.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';
@@ -74,25 +24,12 @@ export const geminiCliProviderProfile: ProviderProfile = {
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';
}
const headers = buildGeminiCliRuntimeHeaders({
baseHeaders: input.baseHeaders,
providerHeaders: input.providerHeaders,
modelName: input.modelName,
stream: action === 'streamGenerateContent',
});
return {
path: resolvePath(action),
headers,
@@ -0,0 +1,119 @@
import { describe, expect, it } from 'vitest';
describe('provider header utils', () => {
it('coerces header values and performs case-insensitive lookups', async () => {
const { headerValueToString, getInputHeader } = await import('./headerUtils.js');
expect(headerValueToString(' value ')).toBe('value');
expect(headerValueToString(['', ' first ', 'second'])).toBe('first');
expect(getInputHeader({ Authorization: 'Bearer test' }, 'authorization')).toBe('Bearer test');
});
it('derives stable uuids from seeds', async () => {
const { uuidFromSeed } = await import('./headerUtils.js');
expect(uuidFromSeed('seed-1')).toBe(uuidFromSeed('seed-1'));
expect(uuidFromSeed('seed-1')).not.toBe(uuidFromSeed('seed-2'));
expect(uuidFromSeed('seed-1')).toMatch(/^[0-9a-f-]{36}$/);
});
it('merges claude beta headers without duplicating entries', async () => {
const { mergeClaudeBetaHeader } = await import('./headerUtils.js');
expect(
mergeClaudeBetaHeader(null, 'beta-a,beta-b', ['beta-b', 'beta-c']),
).toBe('beta-a,beta-b,beta-c');
expect(
mergeClaudeBetaHeader('custom-a,custom-b', 'beta-a,beta-b', ['beta-c']),
).toBe('custom-a,custom-b');
});
it('preserves gemini cli runtime metadata when rebuilding user agents', async () => {
const {
buildGeminiCliUserAgent,
parseGeminiCliUserAgentRuntime,
} = await import('./headerUtils.js');
expect(
parseGeminiCliUserAgentRuntime('GeminiCLI/0.55.0/gemini-2.5-pro (darwin; arm64)'),
).toEqual({
version: '0.55.0',
platform: 'darwin',
arch: 'arm64',
});
expect(
buildGeminiCliUserAgent(
'gemini-2.5-flash',
'GeminiCLI/0.55.0/gemini-2.5-pro (darwin; arm64)',
),
).toBe('GeminiCLI/0.55.0/gemini-2.5-flash (darwin; arm64)');
});
it('builds codex runtime headers with continuity-derived session identifiers', async () => {
const { buildCodexRuntimeHeaders } = await import('./headerUtils.js');
const headers = buildCodexRuntimeHeaders({
baseHeaders: {
authorization: 'Bearer test',
version: '0.101.0',
},
providerHeaders: {
originator: 'codex_cli_rs',
'chatgpt-account-id': 'acct-1',
},
continuityKey: 'cache-key-1',
explicitSessionId: null,
});
expect(headers.Authorization).toBe('Bearer test');
expect(headers.Originator).toBe('codex_cli_rs');
expect(headers['Chatgpt-Account-Id']).toBe('acct-1');
expect(headers.Session_id).toMatch(/^[0-9a-f-]{36}$/);
expect(headers.Conversation_id).toBe(headers.Session_id);
expect(headers.Accept).toBe('text/event-stream');
});
it('builds claude runtime headers with merged betas and oauth bearer auth', async () => {
const { buildClaudeRuntimeHeaders } = await import('./headerUtils.js');
const headers = buildClaudeRuntimeHeaders({
baseHeaders: {
'Content-Type': 'application/json',
},
claudeHeaders: {
'anthropic-beta': 'custom-beta',
},
anthropicVersion: '2023-06-01',
stream: true,
isClaudeOauthUpstream: true,
tokenValue: 'oauth-token',
});
expect(headers['anthropic-version']).toBe('2023-06-01');
expect(headers['anthropic-beta']).toBe('custom-beta');
expect(headers.Authorization).toBe('Bearer oauth-token');
expect(headers['x-api-key']).toBeUndefined();
expect(headers.Accept).toBe('text/event-stream');
});
it('builds gemini cli runtime headers with preserved api client metadata', async () => {
const { buildGeminiCliRuntimeHeaders } = await import('./headerUtils.js');
const headers = buildGeminiCliRuntimeHeaders({
baseHeaders: {
Authorization: 'Bearer test',
},
providerHeaders: {
'x-goog-api-client': 'gl-node/22 gccl/0.31.0',
'user-agent': 'GeminiCLI/0.55.0/gemini-2.5-pro (darwin; arm64)',
},
modelName: 'gemini-2.5-flash',
stream: true,
});
expect(headers.Authorization).toBe('Bearer test');
expect(headers['X-Goog-Api-Client']).toBe('gl-node/22 gccl/0.31.0');
expect(headers['User-Agent']).toBe('GeminiCLI/0.55.0/gemini-2.5-flash (darwin; arm64)');
expect(headers.Accept).toBe('text/event-stream');
});
});
@@ -0,0 +1,245 @@
import { createHash, randomUUID } from 'node:crypto';
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';
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';
export 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;
}
export 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;
}
export 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('-');
}
export function mergeClaudeBetaHeader(
explicitValue: string | null,
defaultValue: string,
extraBetas: string[] = [],
): string {
const source = explicitValue || defaultValue;
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(',');
}
export 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',
};
}
export 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 = typeof modelName === 'string' ? modelName.trim() : '';
return `GeminiCLI/${version}/${effectiveModel || 'unknown'} (${platform}; ${arch})`;
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
export function buildCodexRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
providerHeaders?: Record<string, string>;
explicitSessionId?: string | null;
continuityKey?: string | null;
versionDefault?: string;
userAgentDefault?: string;
userAgentOverride?: string | null;
originatorDefault?: string;
codexBetaFeatures?: string | null;
codexTurnState?: string | null;
codexTurnMetadata?: string | null;
timingMetrics?: string | null;
openAiBeta?: string | null;
}): Record<string, string> {
const authorization = (
getInputHeader(input.baseHeaders, 'authorization')
|| getInputHeader(input.baseHeaders, 'Authorization')
|| ''
);
const originator = getInputHeader(input.providerHeaders, 'originator')
|| input.originatorDefault
|| 'codex_cli_rs';
const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id');
const version = getInputHeader(input.baseHeaders, 'version')
|| input.versionDefault
|| CODEX_CLIENT_VERSION;
const userAgent = input.userAgentOverride
|| getInputHeader(input.baseHeaders, 'user-agent')
|| input.userAgentDefault
|| CODEX_DEFAULT_USER_AGENT;
const codexBetaFeatures = input.codexBetaFeatures || null;
const codexTurnState = input.codexTurnState || null;
const codexTurnMetadata = input.codexTurnMetadata || null;
const timingMetrics = input.timingMetrics || null;
const openAiBeta = input.openAiBeta || null;
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,
...(codexBetaFeatures ? { 'x-codex-beta-features': codexBetaFeatures } : {}),
...(codexTurnState ? { 'x-codex-turn-state': codexTurnState } : {}),
...(codexTurnMetadata ? { 'x-codex-turn-metadata': codexTurnMetadata } : {}),
...(timingMetrics ? { 'x-responsesapi-include-timing-metrics': timingMetrics } : {}),
...(openAiBeta ? { 'OpenAI-Beta': openAiBeta } : {}),
Session_id: sessionId,
...(conversationId ? { Conversation_id: conversationId } : {}),
'User-Agent': userAgent,
Accept: 'text/event-stream',
Connection: 'Keep-Alive',
};
}
export function buildClaudeRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
claudeHeaders: Record<string, string>;
anthropicVersion: string;
stream: boolean;
isClaudeOauthUpstream: boolean;
tokenValue: string;
extraBetas?: string[];
defaultBetaHeader?: string;
defaultUserAgent?: string;
}): Record<string, string> {
const anthropicBeta = mergeClaudeBetaHeader(
getInputHeader(input.claudeHeaders, 'anthropic-beta'),
input.defaultBetaHeader || CLAUDE_DEFAULT_BETA_HEADER,
input.extraBetas,
);
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') || input.defaultUserAgent || CLAUDE_DEFAULT_USER_AGENT,
Connection: 'keep-alive',
Accept: input.stream ? 'text/event-stream' : 'application/json',
'Accept-Encoding': 'gzip, deflate, br, zstd',
};
if (input.isClaudeOauthUpstream) {
headers.Authorization = `Bearer ${input.tokenValue}`;
} else {
headers['x-api-key'] = input.tokenValue;
}
return headers;
}
export function buildGeminiCliRuntimeHeaders(input: {
baseHeaders: Record<string, string>;
providerHeaders?: Record<string, string>;
modelName: string;
stream: boolean;
}): Record<string, string> {
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 (input.stream) {
headers.Accept = 'text/event-stream';
}
return headers;
}