diff --git a/.env.example b/.env.example index e22877e..8e20d1a 100644 --- a/.env.example +++ b/.env.example @@ -11,8 +11,8 @@ OLI_OPENAI_BASE_URL="https://api.openai.com/v1" OLI_OPENAI_API_KEY="" OLI_OPENAI_MODEL="" OLI_OPENAI_SMALL_MODEL="" -# Vision serialization: "openai" (default) or "bedrock" (for Kong/LiteLLM proxies fronting Bedrock) OLI_OPENAI_VISION_STYLE="openai" +OLI_OPENAI_OPTIONAL_HEADERS='{}' # HuggingFace configuration OLI_HUGGINGFACE_BASE_URL="https://api-inference.huggingface.co" diff --git a/oli_bot/backends/openai.py b/oli_bot/backends/openai.py index 58ca658..01a49b8 100644 --- a/oli_bot/backends/openai.py +++ b/oli_bot/backends/openai.py @@ -18,6 +18,8 @@ UsageChunk, ) +from ..config import configs + from .base import ( MAX_TOKENS, TEMPERATURE, @@ -30,6 +32,14 @@ logger = logging.getLogger(__name__) +_raw_headers = configs.openai_optional_headers +if isinstance(_raw_headers, str): + _OPTIONAL_HEADERS = json.loads(_raw_headers) +elif isinstance(_raw_headers, dict): + _OPTIONAL_HEADERS = _raw_headers +else: + _OPTIONAL_HEADERS = {} + class OpenAIBackend(ModelBackend): def __init__( @@ -42,14 +52,16 @@ def __init__( self.model = model self.api_key = api_key self.base_url = base_url - # "openai" keeps the standard image_url payload; "bedrock" emits - # Bedrock-native image blocks self.vision_style = vision_style - self.client = AsyncOpenAI(api_key=api_key, base_url=base_url) + self.client = AsyncOpenAI( + api_key=api_key, base_url=base_url, default_headers=_OPTIONAL_HEADERS + ) def set_base_url(self, url: str) -> None: self.base_url = url - self.client = AsyncOpenAI(api_key=self.api_key, base_url=url) + self.client = AsyncOpenAI( + api_key=self.api_key, base_url=url, default_headers=_OPTIONAL_HEADERS + ) async def generate( self, diff --git a/oli_bot/chat.py b/oli_bot/chat.py index f062698..824f314 100644 --- a/oli_bot/chat.py +++ b/oli_bot/chat.py @@ -3,13 +3,12 @@ oli — a Multi-Backend AI Chat Agent with a Textual-based TUI chat interface. """ +import re +import time import argparse import asyncio import logging import random -import re -import sys -import time from datetime import datetime, timezone from pathlib import Path from typing import Optional diff --git a/oli_bot/config.py b/oli_bot/config.py index c4123a5..59e8154 100644 --- a/oli_bot/config.py +++ b/oli_bot/config.py @@ -30,7 +30,7 @@ class AppConfig(BaseSettings): populate_by_name=True, ) - # Backend selection: "ollama", "openai", or "huggingface" + # Backend selection: "ollama", "openai", "huggingface", or "transformers" backend: str = Field(default="ollama") # OpenAI configuration @@ -42,6 +42,9 @@ class AppConfig(BaseSettings): # Bedrock-native {"image": {"format", "source": {"bytes"}}} blocks for # OpenAI-compatible proxies that pass content through to Bedrock unchanged. openai_vision_style: str = Field(default="openai") + # Optional headers to include in OpenAI API requests. This can be used to + # pass additional headers required by certain OpenAI-compatible endpoints. + openai_optional_headers: Optional[dict] = Field(default={}) # Ollama ollama_base_url: str = Field(default="http://localhost:11434") diff --git a/oli_bot/screens/config_screen.py b/oli_bot/screens/config_screen.py index 839ba03..60063fd 100644 --- a/oli_bot/screens/config_screen.py +++ b/oli_bot/screens/config_screen.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json + from textual.app import ComposeResult from textual.containers import Container, Horizontal, VerticalScroll from textual.screen import ModalScreen @@ -165,6 +167,12 @@ def compose(self) -> ComposeResult: RadioButton("bedrock", id="vs-bedrock"), id="cfg-openai-vision-style", ) + yield Input( + placeholder='Optional headers (JSON, e.g. {"X-Token": "..."})', + id="cfg-openai-optional-headers", + classes="config-input", + value=json.dumps(op.get("optional_headers", {})), + ) yield Label( "Ollama", id="ollama-section-title", classes="section-title" @@ -504,6 +512,9 @@ def _save(self) -> None: "large_model": self._val("#cfg-openai-large-model"), "small_model": self._val("#cfg-openai-small-model"), "vision_style": vision_style, + "optional_headers": self._json( + "#cfg-openai-optional-headers" + ), }, "ollama": { "base_url": self._val("#cfg-ollama-base-url"), @@ -585,6 +596,15 @@ def _float(self, selector: str, default: float) -> float: def _bool(self, selector: str) -> bool: return self.query_one(selector, Checkbox).value + def _json(self, selector: str) -> dict: + try: + value = json.loads(self._val(selector) or "{}") + if isinstance(value, dict): + return value + return {} + except (json.JSONDecodeError, TypeError): + return {} + @staticmethod def _radio_label(rs: RadioSet, default: str) -> str: buttons = list(rs.query(RadioButton)) diff --git a/oli_bot/settings.py b/oli_bot/settings.py index 986253e..07de6ba 100644 --- a/oli_bot/settings.py +++ b/oli_bot/settings.py @@ -18,6 +18,7 @@ "large_model": "gpt-5", "small_model": "gpt-5-mini", "vision_style": "openai", + "optional_headers": {}, }, "ollama": { "base_url": "http://localhost:11434", @@ -86,6 +87,7 @@ "OLI_OPENAI_MODEL": "openai.large_model", "OLI_OPENAI_SMALL_MODEL": "openai.small_model", "OLI_OPENAI_VISION_STYLE": "openai.vision_style", + "OLI_OPENAI_OPTIONAL_HEADERS": "openai.optional_headers", "OLI_OLLAMA_BASE_URL": "ollama.base_url", "OLI_OLLAMA_MODEL": "ollama.large_model", "OLI_OLLAMA_SMALL_MODEL": "ollama.small_model", @@ -289,6 +291,7 @@ def to_appconfig(self, settings: dict) -> AppConfig: openai_model=op.get("large_model", "gpt-5"), openai_small_model=op.get("small_model", "gpt-5-mini"), openai_vision_style=op.get("vision_style", "openai"), + openai_optional_headers=op.get("optional_headers", {}), ollama_base_url=ol.get("base_url", "http://localhost:11434"), ollama_model=ol.get("large_model", ""), ollama_small_model=ol.get("small_model", ""), @@ -338,6 +341,7 @@ def from_appconfig(self, config: AppConfig) -> dict: settings["openai"]["large_model"] = config.openai_model settings["openai"]["small_model"] = config.openai_small_model settings["openai"]["vision_style"] = config.openai_vision_style + settings["openai"]["optional_headers"] = config.openai_optional_headers settings["ollama"]["base_url"] = config.ollama_base_url settings["ollama"]["large_model"] = config.ollama_model settings["ollama"]["small_model"] = config.ollama_small_model