diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 756a588183..1c8c80d583 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -16,6 +16,8 @@ export interface RegistryModel { toolCalling?: boolean; targetFormat?: string; unsupportedParams?: readonly string[]; + /** Maximum context window in tokens */ + contextLength?: number; } // Reasoning models reject temperature, top_p, penalties, logprobs, n. @@ -65,6 +67,8 @@ export interface RegistryEntry { chatPath?: string; clientVersion?: string; passthroughModels?: boolean; + /** Default context window for all models in this provider (can be overridden per-model) */ + defaultContextLength?: number; } interface LegacyProvider { @@ -137,6 +141,7 @@ export const REGISTRY: Record = { urlSuffix: "?beta=true", authType: "oauth", authHeader: "x-api-key", + defaultContextLength: 200000, headers: { "Anthropic-Version": "2023-06-01", "Anthropic-Beta": @@ -180,6 +185,7 @@ export const REGISTRY: Record = { }, authType: "apikey", authHeader: "x-goog-api-key", + defaultContextLength: 1000000, oauth: { clientIdEnv: "GEMINI_OAUTH_CLIENT_ID", clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com", @@ -216,6 +222,7 @@ export const REGISTRY: Record = { }, authType: "oauth", authHeader: "bearer", + defaultContextLength: 1000000, oauth: { clientIdEnv: "GEMINI_CLI_OAUTH_CLIENT_ID", clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com", @@ -247,6 +254,7 @@ export const REGISTRY: Record = { baseUrl: "https://chatgpt.com/backend-api/codex/responses", authType: "oauth", authHeader: "bearer", + defaultContextLength: 400000, headers: { Version: "0.92.0", "Openai-Beta": "responses=experimental", @@ -399,6 +407,7 @@ export const REGISTRY: Record = { responsesBaseUrl: "https://api.githubcopilot.com/responses", authType: "oauth", authHeader: "bearer", + defaultContextLength: 128000, headers: { "copilot-integration-id": "vscode-chat", "editor-version": "vscode/1.110.0", @@ -447,6 +456,7 @@ export const REGISTRY: Record = { baseUrl: "https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse", authType: "oauth", authHeader: "bearer", + defaultContextLength: 200000, headers: { "Content-Type": "application/json", Accept: "application/vnd.amazon.eventstream", @@ -473,6 +483,7 @@ export const REGISTRY: Record = { chatPath: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools", authType: "oauth", authHeader: "bearer", + defaultContextLength: 200000, headers: { "connect-accept-encoding": "gzip", "connect-protocol-version": "1", @@ -501,6 +512,7 @@ export const REGISTRY: Record = { baseUrl: "https://api.openai.com/v1/chat/completions", authType: "apikey", authHeader: "bearer", + defaultContextLength: 128000, models: [ { id: "gpt-4o", name: "GPT-4o" }, { id: "gpt-4o-mini", name: "GPT-4o Mini" }, @@ -522,6 +534,7 @@ export const REGISTRY: Record = { urlSuffix: "?beta=true", authType: "apikey", authHeader: "x-api-key", + defaultContextLength: 200000, headers: { "Anthropic-Version": "2023-06-01", }, @@ -548,6 +561,7 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "Authorization", authPrefix: "Bearer", + defaultContextLength: 200000, models: [ { id: "glm-5", name: "GLM-5" }, { id: "kimi-k2.5", name: "Kimi K2.5" }, @@ -565,6 +579,7 @@ export const REGISTRY: Record = { authType: "apikey", authHeader: "Authorization", authPrefix: "Bearer", + defaultContextLength: 200000, models: [ { id: "minimax-m2.5-free", name: "MiniMax M2.5 Free" }, { id: "big-pickle", name: "Big Pickle" }, @@ -580,6 +595,7 @@ export const REGISTRY: Record = { baseUrl: "https://openrouter.ai/api/v1/chat/completions", authType: "apikey", authHeader: "bearer", + defaultContextLength: 128000, headers: { "HTTP-Referer": "https://endpoint-proxy.local", "X-Title": "Endpoint Proxy", @@ -593,6 +609,7 @@ export const REGISTRY: Record = { format: "claude", executor: "default", baseUrl: "https://api.z.ai/api/anthropic/v1/messages", + defaultContextLength: 200000, urlSuffix: "?beta=true", authType: "apikey", authHeader: "x-api-key", diff --git a/open-sse/services/contextManager.ts b/open-sse/services/contextManager.ts index 3c93028ccb..194a252091 100644 --- a/open-sse/services/contextManager.ts +++ b/open-sse/services/contextManager.ts @@ -5,14 +5,34 @@ * 3 layers: trim tool messages, compress thinking, aggressive purification. */ -// Default token limits per provider (rough estimates based on model context windows) -const DEFAULT_LIMITS = { +import { REGISTRY } from "../config/providerRegistry.ts"; + +// Default token limits per provider (fallbacks when not in registry) +const DEFAULT_LIMITS: Record = { claude: 200000, openai: 128000, gemini: 1000000, + codex: 400000, default: 128000, }; +// Environment variable overrides (highest priority) +function getEnvOverride(provider: string): number | null { + const envKey = `CONTEXT_LENGTH_${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`; + const envValue = process.env[envKey]; + if (envValue) { + const parsed = parseInt(envValue, 10); + if (!isNaN(parsed) && parsed > 0) return parsed; + } + // Global override + const globalValue = process.env.CONTEXT_LENGTH_DEFAULT; + if (globalValue) { + const parsed = parseInt(globalValue, 10); + if (!isNaN(parsed) && parsed > 0) return parsed; + } + return null; +} + // Rough chars-per-token ratio for quick estimation const CHARS_PER_TOKEN = 4; @@ -27,9 +47,20 @@ export function estimateTokens(text) { /** * Get token limit for a provider/model combination + * Priority: Env override > Registry defaultContextLength > DEFAULT_LIMITS */ export function getTokenLimit(provider, model = null) { - // Check if model has a known limit + // 1. Check environment variable override first + const envOverride = getEnvOverride(provider); + if (envOverride) return envOverride; + + // 2. Check registry for provider default + const registryEntry = REGISTRY[provider]; + if (registryEntry?.defaultContextLength) { + return registryEntry.defaultContextLength; + } + + // 3. Check if model name hints at a known limit if (model) { const lower = model.toLowerCase(); if (lower.includes("claude")) return DEFAULT_LIMITS.claude; @@ -38,10 +69,13 @@ export function getTokenLimit(provider, model = null) { lower.includes("gpt") || lower.includes("o1") || lower.includes("o3") || - lower.includes("o4") + lower.includes("o4") || + lower.includes("codex") ) - return DEFAULT_LIMITS.openai; + return DEFAULT_LIMITS.codex; } + + // 4. Fallback to DEFAULT_LIMITS or default return DEFAULT_LIMITS[provider] || DEFAULT_LIMITS.default; } diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index ae202e0bdc..2798c2c954 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -16,6 +16,7 @@ import { getAllAudioModels } from "@omniroute/open-sse/config/audioRegistry.ts"; import { getAllModerationModels } from "@omniroute/open-sse/config/moderationRegistry.ts"; import { getAllVideoModels, getVideoProvider } from "@omniroute/open-sse/config/videoRegistry.ts"; import { getAllMusicModels, getMusicProvider } from "@omniroute/open-sse/config/musicRegistry.ts"; +import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts"; const FALLBACK_ALIAS_TO_PROVIDER = { ag: "antigravity", @@ -190,6 +191,7 @@ export async function getUnifiedModelsResponse( permission: [], root: combo.name, parent: null, + ...(combo.context_length ? { context_length: combo.context_length } : {}), }); } @@ -206,8 +208,15 @@ export async function getUnifiedModelsResponse( continue; } + // Get default context length from registry (provider-level default) + const registryEntry = REGISTRY[alias] || REGISTRY[canonicalProviderId]; + const defaultContextLength = registryEntry?.defaultContextLength; + for (const model of providerModels) { const aliasId = `${alias}/${model.id}`; + // Model-level context length overrides provider default + const contextLength = model.contextLength || defaultContextLength; + models.push({ id: aliasId, object: "model", @@ -216,6 +225,7 @@ export async function getUnifiedModelsResponse( permission: [], root: model.id, parent: null, + ...(contextLength ? { context_length: contextLength } : {}), }); // Add provider-id prefix in addition to short alias (ex: kiro/model + kr/model). @@ -229,6 +239,7 @@ export async function getUnifiedModelsResponse( permission: [], root: model.id, parent: aliasId, + ...(contextLength ? { context_length: contextLength } : {}), }); } } diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index a6a6d3a1ce..079f45052f 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -107,6 +107,7 @@ export const createComboSchema = z.object({ system_message: z.string().max(50000).optional(), tool_filter_regex: z.string().max(1000).optional(), context_cache_protection: z.boolean().optional(), + context_length: z.number().int().min(1000).max(2000000).optional(), }); // ──── Auto-Combo Schemas ────