mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 16:01:05 +00:00
feat(ai): add headers option to StreamOptions for custom HTTP headers
- Added headers field to base StreamOptions interface - Updated all providers to merge options.headers with defaults - Forward headers and onPayload through streamSimple/completeSimple - Bedrock not supported (uses AWS SDK auth)
This commit is contained in:
parent
20c7b5fed4
commit
d2be6486a4
10 changed files with 96 additions and 28 deletions
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `headers` option to `StreamOptions` for custom HTTP headers in API requests. Supported by all providers except Amazon Bedrock (which uses AWS SDK auth). Headers are merged with provider defaults and `model.headers`, with `options.headers` taking precedence.
|
||||||
|
|
||||||
## [0.49.2] - 2026-01-19
|
## [0.49.2] - 2026-01-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,16 @@ export interface AnthropicOptions extends StreamOptions {
|
||||||
toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
|
toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string> {
|
||||||
|
const merged: Record<string, string> = {};
|
||||||
|
for (const headers of headerSources) {
|
||||||
|
if (headers) {
|
||||||
|
Object.assign(merged, headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
||||||
model: Model<"anthropic-messages">,
|
model: Model<"anthropic-messages">,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -154,7 +164,12 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
||||||
const { client, isOAuthToken } = createClient(model, apiKey, options?.interleavedThinking ?? true);
|
const { client, isOAuthToken } = createClient(
|
||||||
|
model,
|
||||||
|
apiKey,
|
||||||
|
options?.interleavedThinking ?? true,
|
||||||
|
options?.headers,
|
||||||
|
);
|
||||||
const params = buildParams(model, context, isOAuthToken, options);
|
const params = buildParams(model, context, isOAuthToken, options);
|
||||||
options?.onPayload?.(params);
|
options?.onPayload?.(params);
|
||||||
const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal });
|
const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal });
|
||||||
|
|
@ -328,6 +343,7 @@ function createClient(
|
||||||
model: Model<"anthropic-messages">,
|
model: Model<"anthropic-messages">,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
interleavedThinking: boolean,
|
interleavedThinking: boolean,
|
||||||
|
optionsHeaders?: Record<string, string>,
|
||||||
): { client: Anthropic; isOAuthToken: boolean } {
|
): { client: Anthropic; isOAuthToken: boolean } {
|
||||||
const betaFeatures = ["fine-grained-tool-streaming-2025-05-14"];
|
const betaFeatures = ["fine-grained-tool-streaming-2025-05-14"];
|
||||||
if (interleavedThinking) {
|
if (interleavedThinking) {
|
||||||
|
|
@ -337,14 +353,17 @@ function createClient(
|
||||||
const oauthToken = isOAuthToken(apiKey);
|
const oauthToken = isOAuthToken(apiKey);
|
||||||
if (oauthToken) {
|
if (oauthToken) {
|
||||||
// Stealth mode: Mimic Claude Code's headers exactly
|
// Stealth mode: Mimic Claude Code's headers exactly
|
||||||
const defaultHeaders = {
|
const defaultHeaders = mergeHeaders(
|
||||||
accept: "application/json",
|
{
|
||||||
"anthropic-dangerous-direct-browser-access": "true",
|
accept: "application/json",
|
||||||
"anthropic-beta": `claude-code-20250219,oauth-2025-04-20,${betaFeatures.join(",")}`,
|
"anthropic-dangerous-direct-browser-access": "true",
|
||||||
"user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
"anthropic-beta": `claude-code-20250219,oauth-2025-04-20,${betaFeatures.join(",")}`,
|
||||||
"x-app": "cli",
|
"user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
||||||
...(model.headers || {}),
|
"x-app": "cli",
|
||||||
};
|
},
|
||||||
|
model.headers,
|
||||||
|
optionsHeaders,
|
||||||
|
);
|
||||||
|
|
||||||
const client = new Anthropic({
|
const client = new Anthropic({
|
||||||
apiKey: null,
|
apiKey: null,
|
||||||
|
|
@ -357,12 +376,15 @@ function createClient(
|
||||||
return { client, isOAuthToken: true };
|
return { client, isOAuthToken: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultHeaders = {
|
const defaultHeaders = mergeHeaders(
|
||||||
accept: "application/json",
|
{
|
||||||
"anthropic-dangerous-direct-browser-access": "true",
|
accept: "application/json",
|
||||||
"anthropic-beta": betaFeatures.join(","),
|
"anthropic-dangerous-direct-browser-access": "true",
|
||||||
...(model.headers || {}),
|
"anthropic-beta": betaFeatures.join(","),
|
||||||
};
|
},
|
||||||
|
model.headers,
|
||||||
|
optionsHeaders,
|
||||||
|
);
|
||||||
|
|
||||||
const client = new Anthropic({
|
const client = new Anthropic({
|
||||||
apiKey,
|
apiKey,
|
||||||
|
|
|
||||||
|
|
@ -434,6 +434,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
||||||
Accept: "text/event-stream",
|
Accept: "text/event-stream",
|
||||||
...headers,
|
...headers,
|
||||||
...(isClaudeThinkingModel(model.id) ? { "anthropic-beta": CLAUDE_THINKING_BETA_HEADER } : {}),
|
...(isClaudeThinkingModel(model.id) ? { "anthropic-beta": CLAUDE_THINKING_BETA_HEADER } : {}),
|
||||||
|
...options?.headers,
|
||||||
};
|
};
|
||||||
const requestBodyJson = JSON.stringify(requestBody);
|
const requestBodyJson = JSON.stringify(requestBody);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
||||||
try {
|
try {
|
||||||
const project = resolveProject(options);
|
const project = resolveProject(options);
|
||||||
const location = resolveLocation(options);
|
const location = resolveLocation(options);
|
||||||
const client = createClient(model, project, location);
|
const client = createClient(model, project, location, options?.headers);
|
||||||
const params = buildParams(model, context, options);
|
const params = buildParams(model, context, options);
|
||||||
options?.onPayload?.(params);
|
options?.onPayload?.(params);
|
||||||
const googleStream = await client.models.generateContentStream(params);
|
const googleStream = await client.models.generateContentStream(params);
|
||||||
|
|
@ -276,11 +276,16 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createClient(model: Model<"google-vertex">, project: string, location: string): GoogleGenAI {
|
function createClient(
|
||||||
|
model: Model<"google-vertex">,
|
||||||
|
project: string,
|
||||||
|
location: string,
|
||||||
|
optionsHeaders?: Record<string, string>,
|
||||||
|
): GoogleGenAI {
|
||||||
const httpOptions: { headers?: Record<string, string> } = {};
|
const httpOptions: { headers?: Record<string, string> } = {};
|
||||||
|
|
||||||
if (model.headers) {
|
if (model.headers || optionsHeaders) {
|
||||||
httpOptions.headers = { ...model.headers };
|
httpOptions.headers = { ...model.headers, ...optionsHeaders };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasHttpOptions = Object.values(httpOptions).some(Boolean);
|
const hasHttpOptions = Object.values(httpOptions).some(Boolean);
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
||||||
const client = createClient(model, apiKey);
|
const client = createClient(model, apiKey, options?.headers);
|
||||||
const params = buildParams(model, context, options);
|
const params = buildParams(model, context, options);
|
||||||
options?.onPayload?.(params);
|
options?.onPayload?.(params);
|
||||||
const googleStream = await client.models.generateContentStream(params);
|
const googleStream = await client.models.generateContentStream(params);
|
||||||
|
|
@ -264,14 +264,18 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createClient(model: Model<"google-generative-ai">, apiKey?: string): GoogleGenAI {
|
function createClient(
|
||||||
|
model: Model<"google-generative-ai">,
|
||||||
|
apiKey?: string,
|
||||||
|
optionsHeaders?: Record<string, string>,
|
||||||
|
): GoogleGenAI {
|
||||||
const httpOptions: { baseUrl?: string; apiVersion?: string; headers?: Record<string, string> } = {};
|
const httpOptions: { baseUrl?: string; apiVersion?: string; headers?: Record<string, string> } = {};
|
||||||
if (model.baseUrl) {
|
if (model.baseUrl) {
|
||||||
httpOptions.baseUrl = model.baseUrl;
|
httpOptions.baseUrl = model.baseUrl;
|
||||||
httpOptions.apiVersion = ""; // baseUrl already includes version path, don't append
|
httpOptions.apiVersion = ""; // baseUrl already includes version path, don't append
|
||||||
}
|
}
|
||||||
if (model.headers) {
|
if (model.headers || optionsHeaders) {
|
||||||
httpOptions.headers = model.headers;
|
httpOptions.headers = { ...model.headers, ...optionsHeaders };
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GoogleGenAI({
|
return new GoogleGenAI({
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
||||||
const accountId = extractAccountId(apiKey);
|
const accountId = extractAccountId(apiKey);
|
||||||
const body = buildRequestBody(model, context, options);
|
const body = buildRequestBody(model, context, options);
|
||||||
options?.onPayload?.(body);
|
options?.onPayload?.(body);
|
||||||
const headers = buildHeaders(model.headers, accountId, apiKey, options?.sessionId);
|
const headers = buildHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
|
||||||
const bodyJson = JSON.stringify(body);
|
const bodyJson = JSON.stringify(body);
|
||||||
|
|
||||||
// Fetch with retry logic for rate limits and transient errors
|
// Fetch with retry logic for rate limits and transient errors
|
||||||
|
|
@ -697,6 +697,7 @@ function extractAccountId(token: string): string {
|
||||||
|
|
||||||
function buildHeaders(
|
function buildHeaders(
|
||||||
initHeaders: Record<string, string> | undefined,
|
initHeaders: Record<string, string> | undefined,
|
||||||
|
additionalHeaders: Record<string, string> | undefined,
|
||||||
accountId: string,
|
accountId: string,
|
||||||
token: string,
|
token: string,
|
||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
|
|
@ -709,6 +710,9 @@ function buildHeaders(
|
||||||
headers.set("User-Agent", `pi (${os.platform()} ${os.release()}; ${os.arch()})`);
|
headers.set("User-Agent", `pi (${os.platform()} ${os.release()}; ${os.arch()})`);
|
||||||
headers.set("accept", "text/event-stream");
|
headers.set("accept", "text/event-stream");
|
||||||
headers.set("content-type", "application/json");
|
headers.set("content-type", "application/json");
|
||||||
|
for (const [key, value] of Object.entries(additionalHeaders || {})) {
|
||||||
|
headers.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
headers.set("session_id", sessionId);
|
headers.set("session_id", sessionId);
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
||||||
const client = createClient(model, context, apiKey);
|
const client = createClient(model, context, apiKey, options?.headers);
|
||||||
const params = buildParams(model, context, options);
|
const params = buildParams(model, context, options);
|
||||||
options?.onPayload?.(params);
|
options?.onPayload?.(params);
|
||||||
const openaiStream = await client.chat.completions.create(params, { signal: options?.signal });
|
const openaiStream = await client.chat.completions.create(params, { signal: options?.signal });
|
||||||
|
|
@ -318,7 +318,12 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createClient(model: Model<"openai-completions">, context: Context, apiKey?: string) {
|
function createClient(
|
||||||
|
model: Model<"openai-completions">,
|
||||||
|
context: Context,
|
||||||
|
apiKey?: string,
|
||||||
|
optionsHeaders?: Record<string, string>,
|
||||||
|
) {
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
if (!process.env.OPENAI_API_KEY) {
|
if (!process.env.OPENAI_API_KEY) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -354,6 +359,11 @@ function createClient(model: Model<"openai-completions">, context: Context, apiK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge options headers last so they can override defaults
|
||||||
|
if (optionsHeaders) {
|
||||||
|
Object.assign(headers, optionsHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
return new OpenAI({
|
return new OpenAI({
|
||||||
apiKey,
|
apiKey,
|
||||||
baseURL: model.baseUrl,
|
baseURL: model.baseUrl,
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
||||||
try {
|
try {
|
||||||
// Create OpenAI client
|
// Create OpenAI client
|
||||||
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
||||||
const client = createClient(model, context, apiKey);
|
const client = createClient(model, context, apiKey, options?.headers);
|
||||||
const params = buildParams(model, context, options);
|
const params = buildParams(model, context, options);
|
||||||
options?.onPayload?.(params);
|
options?.onPayload?.(params);
|
||||||
const openaiStream = await client.responses.create(
|
const openaiStream = await client.responses.create(
|
||||||
|
|
@ -319,7 +319,12 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createClient(model: Model<"openai-responses">, context: Context, apiKey?: string) {
|
function createClient(
|
||||||
|
model: Model<"openai-responses">,
|
||||||
|
context: Context,
|
||||||
|
apiKey?: string,
|
||||||
|
optionsHeaders?: Record<string, string>,
|
||||||
|
) {
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
if (!process.env.OPENAI_API_KEY) {
|
if (!process.env.OPENAI_API_KEY) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -355,6 +360,11 @@ function createClient(model: Model<"openai-responses">, context: Context, apiKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge options headers last so they can override defaults
|
||||||
|
if (optionsHeaders) {
|
||||||
|
Object.assign(headers, optionsHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
return new OpenAI({
|
return new OpenAI({
|
||||||
apiKey,
|
apiKey,
|
||||||
baseURL: model.baseUrl,
|
baseURL: model.baseUrl,
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,8 @@ function mapOptionsForApi<TApi extends Api>(
|
||||||
signal: options?.signal,
|
signal: options?.signal,
|
||||||
apiKey: apiKey || options?.apiKey,
|
apiKey: apiKey || options?.apiKey,
|
||||||
sessionId: options?.sessionId,
|
sessionId: options?.sessionId,
|
||||||
|
headers: options?.headers,
|
||||||
|
onPayload: options?.onPayload,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to clamp xhigh to high for providers that don't support it
|
// Helper to clamp xhigh to high for providers that don't support it
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,12 @@ export interface StreamOptions {
|
||||||
* Optional callback for inspecting provider payloads before sending.
|
* Optional callback for inspecting provider payloads before sending.
|
||||||
*/
|
*/
|
||||||
onPayload?: (payload: unknown) => void;
|
onPayload?: (payload: unknown) => void;
|
||||||
|
/**
|
||||||
|
* Optional custom HTTP headers to include in API requests.
|
||||||
|
* Merged with provider defaults; can override default headers.
|
||||||
|
* Not supported by all providers (e.g., AWS Bedrock uses SDK auth).
|
||||||
|
*/
|
||||||
|
headers?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified options with reasoning passed to streamSimple() and completeSimple()
|
// Unified options with reasoning passed to streamSimple() and completeSimple()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue