diff --git a/README.md b/README.md index 757f6be..7ba70a6 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,12 @@ npm start "order": ["OpenAI", "Anthropic"], "allow_fallbacks": true } + }, + "modelProviders": { + "deepseek/deepseek-v4-pro": { + "only": ["DeepSeek"], + "allow_fallbacks": false + } } } ``` @@ -91,6 +97,7 @@ npm start | `host` | string | `"127.0.0.1"` | Интерфейс. Поставь `"0.0.0.0"` чтобы слушать все. | | `polzaApiKey` | string | `""` | Fallback API-ключ. Используется, если клиент не прислал `Authorization`. | | `inject` | object | `{}` | Поля, дописываемые в JSON-тело на `INJECT_PATHS`. Клиентское значение никогда не перетирается. Подробнее → [Инъекции](#инъекции). | +| `modelProviders` | object | `{}` | Per-model provider selection. Ключ — имя модели, значение — объект `provider` (те же поля: `only`, `order`, `allow_fallbacks`). Если модель запроса найдена здесь, её `provider` **полностью заменяет** глобальный `inject.provider`. Остальные ключи из `inject` работают как обычно. Подробнее → [Model-specific providers](#model-specific-providers). | Захардкожено в `src/config.js` и **не меняется** через конфиг: `UPSTREAM_BASE_URL` и `INJECT_PATHS`. Чтобы поменять — правь исходник. @@ -117,6 +124,31 @@ npm start Варианты: `order`, `only`, `allow_fallbacks`, и т. д. — как в доке Polza. +### Model-specific providers + +`modelProviders` позволяет привязать конкретную модель к конкретному провайдеру, не затрагивая глобальные настройки для остальных моделей. + +```json +{ + "modelProviders": { + "deepseek/deepseek-v4-pro": { + "only": ["DeepSeek"], + "allow_fallbacks": false + }, + "xiaomi/mimo-v2.5-pro": { + "only": ["Xiaomi", "DeepInfra"], + "allow_fallbacks": true + } + } +} +``` + +**Как работает:** +1. Если в теле запроса есть поле `model` и оно найдено в `modelProviders` — для этой модели инжектится **свой** `provider`, полностью заменяя глобальный `inject.provider`. +2. Если модели нет в `modelProviders` — работает глобальный `inject.provider` (текущее поведение). +3. Если клиент **уже прислал** `provider` в теле запроса — ни глобальная, ни per-model настройка не применяется (клиентское значение всегда в приоритете). +4. Все остальные поля из `inject` (кроме `provider`) инжектятся одинаково для всех моделей. + ## Переменные окружения | Переменная | Назначение | diff --git a/config.example.json b/config.example.json index 4c91e50..2366100 100644 --- a/config.example.json +++ b/config.example.json @@ -7,5 +7,15 @@ "only": ["OpenAI"], "allow_fallbacks": true } + }, + "modelProviders": { + "deepseek/deepseek-v4-pro": { + "only": ["DeepSeek"], + "allow_fallbacks": false + }, + "xiaomi/mimo-v2.5-pro": { + "only": ["Xiaomi", "DeepInfra"], + "allow_fallbacks": true + } } -} +} \ No newline at end of file diff --git a/src/banner.js b/src/banner.js index 2f8f6ec..3ff0f13 100644 --- a/src/banner.js +++ b/src/banner.js @@ -5,8 +5,10 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const SVG_PATH = resolve(__dirname, 'logo.svg'); -const GREEN = '#B2F72A'; -const GREEN_RGB = { r: 178, g: 247, b: 42 }; +const COLORTERM = (process.env.COLORTERM ?? '').toLowerCase(); +const USE_TRUECOLOR = COLORTERM === 'truecolor' || COLORTERM === '24bit'; +const GREEN = USE_TRUECOLOR ? '#39FF14' : '#B2F72A'; +const GREEN_RGB = USE_TRUECOLOR ? { r: 57, g: 255, b: 20 } : { r: 178, g: 247, b: 42 }; const WHITE_RGB = { r: 255, g: 255, b: 255 }; const BANNER_LINES = [ @@ -112,7 +114,9 @@ export async function printBanner({ version, host, port, upstream }) { for (const line of lines) process.stdout.write(line + '\n'); process.stdout.write('\n' + chk.hex(GREEN)(infoLine) + '\n\n'); } catch { - const open = process.stdout.isTTY && !process.env.NO_COLOR ? '\x1b[38;2;178;247;42m' : ''; + const open = process.stdout.isTTY && !process.env.NO_COLOR + ? `\x1b[38;2;${GREEN_RGB.r};${GREEN_RGB.g};${GREEN_RGB.b}m` + : ''; const close = open ? '\x1b[0m' : ''; for (const line of BANNER_LINES) process.stdout.write(open + line + close + '\n'); process.stdout.write(open + infoLine + close + '\n\n'); diff --git a/src/config.js b/src/config.js index 1f4cb8a..3b40f4a 100644 --- a/src/config.js +++ b/src/config.js @@ -37,6 +37,7 @@ export async function loadConfig() { upstreamBaseUrl: UPSTREAM_BASE_URL, polzaApiKey: fileKey || envKey || '', inject: raw.inject ?? {}, + modelProviders: raw.modelProviders ?? {}, injectPaths: INJECT_PATHS, sourcePath: path, }; diff --git a/src/server.js b/src/server.js index 620ffb4..c188719 100644 --- a/src/server.js +++ b/src/server.js @@ -89,18 +89,28 @@ async function main() { if (!body || typeof body !== 'object') return body; if (!shouldInject(subPath)) return body; + const modelProvider = body.model && config.modelProviders[body.model]; + const injected = []; const skipped = []; for (const [key, value] of Object.entries(config.inject ?? {})) { + const effectiveValue = key === 'provider' && modelProvider ? modelProvider : value; if (body[key] === undefined) { - body[key] = value; + body[key] = effectiveValue; injected.push(key); } else { skipped.push(key); } } + if (modelProvider && body.provider === undefined && !(config.inject && 'provider' in config.inject)) { + body.provider = modelProvider; + injected.push('provider'); + } if (injected.length || skipped.length) { - log.info({ injected, skippedByClient: skipped, path: subPath }, 'inject'); + log.info( + { injected, skippedByClient: skipped, path: subPath, modelProvider: !!modelProvider }, + 'inject', + ); } return body;