mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 06:04:44 +00:00
Add custom headers support for models.json
Fixes #39 - Added headers field to Model type (provider and model level) - Model headers override provider headers when merged - Supported in all APIs: - Anthropic: defaultHeaders - OpenAI (completions/responses): defaultHeaders - Google: httpOptions.headers - Enables bypassing Cloudflare bot detection for proxied endpoints - Updated documentation with examples Also fixed: - Mistral/Chutes syntax error (iif -> if) - process.env.ANTHROPIC_API_KEY bug (use delete instead of = undefined)
This commit is contained in:
parent
425890e674
commit
de39f1f493
9 changed files with 95 additions and 7 deletions
|
|
@ -576,6 +576,24 @@ const ollamaModel: Model<'openai-completions'> = {
|
||||||
maxTokens: 32000
|
maxTokens: 32000
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Example: Custom endpoint with headers (bypassing Cloudflare bot detection)
|
||||||
|
const proxyModel: Model<'anthropic-messages'> = {
|
||||||
|
id: 'claude-sonnet-4',
|
||||||
|
name: 'Claude Sonnet 4 (Proxied)',
|
||||||
|
api: 'anthropic-messages',
|
||||||
|
provider: 'custom-proxy',
|
||||||
|
baseUrl: 'https://proxy.example.com/v1',
|
||||||
|
reasoning: true,
|
||||||
|
input: ['text', 'image'],
|
||||||
|
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||||
|
contextWindow: 200000,
|
||||||
|
maxTokens: 8192,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
||||||
|
'X-Custom-Auth': 'bearer-token-here'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Use the custom model
|
// Use the custom model
|
||||||
const response = await stream(ollamaModel, context, {
|
const response = await stream(ollamaModel, context, {
|
||||||
apiKey: 'dummy' // Ollama doesn't need a real key
|
apiKey: 'dummy' // Ollama doesn't need a real key
|
||||||
|
|
|
||||||
|
|
@ -288,11 +288,12 @@ function createClient(
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
"anthropic-dangerous-direct-browser-access": "true",
|
"anthropic-dangerous-direct-browser-access": "true",
|
||||||
"anthropic-beta": "oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14",
|
"anthropic-beta": "oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14",
|
||||||
|
...(model.headers || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear the env var if we're in Node.js to prevent SDK from using it
|
// Clear the env var if we're in Node.js to prevent SDK from using it
|
||||||
if (typeof process !== "undefined" && process.env) {
|
if (typeof process !== "undefined" && process.env) {
|
||||||
process.env.ANTHROPIC_API_KEY = undefined;
|
delete process.env.ANTHROPIC_API_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new Anthropic({
|
const client = new Anthropic({
|
||||||
|
|
@ -309,6 +310,7 @@ function createClient(
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
"anthropic-dangerous-direct-browser-access": "true",
|
"anthropic-dangerous-direct-browser-access": "true",
|
||||||
"anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
|
"anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
|
||||||
|
...(model.headers || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const client = new Anthropic({
|
const client = new Anthropic({
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = createClient(options?.apiKey);
|
const client = createClient(model, options?.apiKey);
|
||||||
const params = buildParams(model, context, options);
|
const params = buildParams(model, context, options);
|
||||||
const googleStream = await client.models.generateContentStream(params);
|
const googleStream = await client.models.generateContentStream(params);
|
||||||
|
|
||||||
|
|
@ -252,7 +252,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createClient(apiKey?: string): GoogleGenAI {
|
function createClient(model: Model<"google-generative-ai">, apiKey?: string): GoogleGenAI {
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
if (!process.env.GEMINI_API_KEY) {
|
if (!process.env.GEMINI_API_KEY) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -261,7 +261,10 @@ function createClient(apiKey?: string): GoogleGenAI {
|
||||||
}
|
}
|
||||||
apiKey = process.env.GEMINI_API_KEY;
|
apiKey = process.env.GEMINI_API_KEY;
|
||||||
}
|
}
|
||||||
return new GoogleGenAI({ apiKey });
|
return new GoogleGenAI({
|
||||||
|
apiKey,
|
||||||
|
httpOptions: model.headers ? { headers: model.headers } : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildParams(
|
function buildParams(
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,12 @@ function createClient(model: Model<"openai-completions">, apiKey?: string) {
|
||||||
}
|
}
|
||||||
apiKey = process.env.OPENAI_API_KEY;
|
apiKey = process.env.OPENAI_API_KEY;
|
||||||
}
|
}
|
||||||
return new OpenAI({ apiKey, baseURL: model.baseUrl, dangerouslyAllowBrowser: true });
|
return new OpenAI({
|
||||||
|
apiKey,
|
||||||
|
baseURL: model.baseUrl,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
defaultHeaders: model.headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildParams(model: Model<"openai-completions">, context: Context, options?: OpenAICompletionsOptions) {
|
function buildParams(model: Model<"openai-completions">, context: Context, options?: OpenAICompletionsOptions) {
|
||||||
|
|
@ -285,7 +290,7 @@ function buildParams(model: Model<"openai-completions">, context: Context, optio
|
||||||
|
|
||||||
if (options?.maxTokens) {
|
if (options?.maxTokens) {
|
||||||
// Mistral/Chutes uses max_tokens instead of max_completion_tokens
|
// Mistral/Chutes uses max_tokens instead of max_completion_tokens
|
||||||
iif (model.baseUrl.includes("mistral.ai") || model.baseUrl.includes("chutes.ai")) {
|
if (model.baseUrl.includes("mistral.ai") || model.baseUrl.includes("chutes.ai")) {
|
||||||
(params as any).max_tokens = options?.maxTokens;
|
(params as any).max_tokens = options?.maxTokens;
|
||||||
} else {
|
} else {
|
||||||
params.max_completion_tokens = options?.maxTokens;
|
params.max_completion_tokens = options?.maxTokens;
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,12 @@ function createClient(model: Model<"openai-responses">, apiKey?: string) {
|
||||||
}
|
}
|
||||||
apiKey = process.env.OPENAI_API_KEY;
|
apiKey = process.env.OPENAI_API_KEY;
|
||||||
}
|
}
|
||||||
return new OpenAI({ apiKey, baseURL: model.baseUrl, dangerouslyAllowBrowser: true });
|
return new OpenAI({
|
||||||
|
apiKey,
|
||||||
|
baseURL: model.baseUrl,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
defaultHeaders: model.headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildParams(model: Model<"openai-responses">, context: Context, options?: OpenAIResponsesOptions) {
|
function buildParams(model: Model<"openai-responses">, context: Context, options?: OpenAIResponsesOptions) {
|
||||||
|
|
|
||||||
|
|
@ -168,4 +168,5 @@ export interface Model<TApi extends Api> {
|
||||||
};
|
};
|
||||||
contextWindow: number;
|
contextWindow: number;
|
||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
|
headers?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Custom Headers**: Added support for custom HTTP headers in `models.json` configuration. Headers can be specified at both provider and model level, with model-level headers overriding provider-level ones. This enables bypassing Cloudflare bot detection and other proxy requirements. ([#39](https://github.com/badlogic/pi-mono/issues/39))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Chutes AI Provider**: Fixed 400 errors when using Chutes AI provider. Added compatibility fixes for `store` field exclusion, `max_tokens` parameter usage, and system prompt role handling. ([#42](https://github.com/badlogic/pi-mono/pull/42) by [@butelo](https://github.com/butelo))
|
||||||
|
- **Mistral/Chutes Syntax Error**: Fixed syntax error in merged PR that used `iif` instead of `if`.
|
||||||
|
|
||||||
## [0.7.25] - 2025-11-20
|
## [0.7.25] - 2025-11-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,44 @@ This allows both secure env var usage and literal keys for local servers.
|
||||||
|
|
||||||
This is useful when a provider supports multiple API standards through the same base URL.
|
This is useful when a provider supports multiple API standards through the same base URL.
|
||||||
|
|
||||||
|
### Custom Headers
|
||||||
|
|
||||||
|
You can add custom HTTP headers to bypass Cloudflare bot detection, add authentication tokens, or meet other proxy requirements:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"custom-proxy": {
|
||||||
|
"baseUrl": "https://proxy.example.com/v1",
|
||||||
|
"apiKey": "YOUR_API_KEY",
|
||||||
|
"api": "anthropic-messages",
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
||||||
|
"X-Custom-Auth": "bearer-token-here"
|
||||||
|
},
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "claude-sonnet-4",
|
||||||
|
"name": "Claude Sonnet 4 (Proxied)",
|
||||||
|
"reasoning": true,
|
||||||
|
"input": ["text", "image"],
|
||||||
|
"cost": {"input": 3, "output": 15, "cacheRead": 0.3, "cacheWrite": 3.75},
|
||||||
|
"contextWindow": 200000,
|
||||||
|
"maxTokens": 8192,
|
||||||
|
"headers": {
|
||||||
|
"X-Model-Specific-Header": "value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Provider-level `headers`**: Applied to all requests for models in that provider
|
||||||
|
- **Model-level `headers`**: Additional headers for specific models (merged with provider headers)
|
||||||
|
- Model headers override provider headers when keys conflict
|
||||||
|
|
||||||
### Model Selection Priority
|
### Model Selection Priority
|
||||||
|
|
||||||
When starting `pi`, models are selected in this order:
|
When starting `pi`, models are selected in this order:
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const ModelDefinitionSchema = Type.Object({
|
||||||
}),
|
}),
|
||||||
contextWindow: Type.Number(),
|
contextWindow: Type.Number(),
|
||||||
maxTokens: Type.Number(),
|
maxTokens: Type.Number(),
|
||||||
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ProviderConfigSchema = Type.Object({
|
const ProviderConfigSchema = Type.Object({
|
||||||
|
|
@ -44,6 +45,7 @@ const ProviderConfigSchema = Type.Object({
|
||||||
Type.Literal("google-generative-ai"),
|
Type.Literal("google-generative-ai"),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
models: Type.Array(ModelDefinitionSchema),
|
models: Type.Array(ModelDefinitionSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -174,6 +176,10 @@ function parseModels(config: ModelsConfig): Model<Api>[] {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge headers: provider headers are base, model headers override
|
||||||
|
const headers =
|
||||||
|
providerConfig.headers || modelDef.headers ? { ...providerConfig.headers, ...modelDef.headers } : undefined;
|
||||||
|
|
||||||
models.push({
|
models.push({
|
||||||
id: modelDef.id,
|
id: modelDef.id,
|
||||||
name: modelDef.name,
|
name: modelDef.name,
|
||||||
|
|
@ -185,6 +191,7 @@ function parseModels(config: ModelsConfig): Model<Api>[] {
|
||||||
cost: modelDef.cost,
|
cost: modelDef.cost,
|
||||||
contextWindow: modelDef.contextWindow,
|
contextWindow: modelDef.contextWindow,
|
||||||
maxTokens: modelDef.maxTokens,
|
maxTokens: modelDef.maxTokens,
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue