openai-http-proxy 3.0.2__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/PKG-INFO +5 -2
  2. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/README.md +3 -0
  3. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/bootstrap.py +11 -6
  4. openai_http_proxy-3.2.0/lm_proxy/strategies/__init__.py +4 -0
  5. openai_http_proxy-3.2.0/lm_proxy/strategies/fallback.py +69 -0
  6. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/utils.py +32 -6
  7. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/pyproject.toml +2 -2
  8. openai_http_proxy-3.0.2/lm_proxy/_app.py +0 -82
  9. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/LICENSE +0 -0
  10. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/__init__.py +0 -0
  11. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/__main__.py +0 -0
  12. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/api_key_check/__init__.py +0 -0
  13. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/api_key_check/allow_all.py +0 -0
  14. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/api_key_check/in_config.py +0 -0
  15. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/api_key_check/with_request.py +0 -0
  16. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/app.py +0 -0
  17. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/base_types.py +0 -0
  18. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/config.py +0 -0
  19. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/config_loaders/__init__.py +0 -0
  20. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/config_loaders/json.py +0 -0
  21. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/config_loaders/python.py +0 -0
  22. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/config_loaders/toml.py +0 -0
  23. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/config_loaders/yaml.py +0 -0
  24. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/core.py +0 -0
  25. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/errors.py +0 -0
  26. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/handlers/__init__.py +0 -0
  27. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/handlers/forward_http_headers.py +0 -0
  28. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/handlers/rate_limiter.py +0 -0
  29. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/loggers.py +0 -0
  30. {openai_http_proxy-3.0.2 → openai_http_proxy-3.2.0}/lm_proxy/models_endpoint.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openai-http-proxy
3
- Version: 3.0.2
3
+ Version: 3.2.0
4
4
  Summary: OpenAI HTTP Proxy is an OpenAI-compatible http proxy server for inferencing various LLMs capable of working with Google, Anthropic, OpenAI APIs, local PyTorch inference, etc.
5
5
  License: MIT License
6
6
 
@@ -45,7 +45,7 @@ Provides-Extra: all
45
45
  Provides-Extra: anthropic
46
46
  Provides-Extra: google
47
47
  Provides-Extra: test
48
- Requires-Dist: ai-microcore (>=5.1.2,<6)
48
+ Requires-Dist: ai-microcore (>=5.1.2,<7)
49
49
  Requires-Dist: anthropic (>=0.77,<1) ; extra == "all"
50
50
  Requires-Dist: anthropic (>=0.77,<1) ; extra == "anthropic"
51
51
  Requires-Dist: fastapi (>=0.121.3,<1)
@@ -73,6 +73,8 @@ Description-Content-Type: text/markdown
73
73
  <a href="https://github.com/Nayjest/lm-proxy/actions/workflows/code-style.yml"><img src="https://github.com/Nayjest/lm-proxy/actions/workflows/code-style.yml/badge.svg" alt="Code Style"></a>
74
74
  <img src="https://raw.githubusercontent.com/Nayjest/lm-proxy/main/coverage.svg" alt="Code Coverage">
75
75
  <a href="https://www.bestpractices.dev/projects/11364"><img src="https://www.bestpractices.dev/projects/11364/badge"></a>
76
+ <br>
77
+ <a href="https://github.com/vshymanskyy/StandWithUkraine/blob/main/README.md"><img src="https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/refs/heads/main/badges/StandWithUkraine.svg" alt="Stand With Ukraine"></a>
76
78
  <a href="https://github.com/Nayjest/lm-proxy/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Nayjest/lm-proxy?color=d08aff" alt="License"></a>
77
79
  </p>
78
80
 
@@ -711,6 +713,7 @@ prefix = "SECURITY_AUDIT"
711
713
 
712
714
  For more detailed information, check out these articles:
713
715
  - [HTTP Header Management](https://github.com/Nayjest/lm-proxy/blob/main/doc/http_headers.md)
716
+ - [Configuring fallbacks](https://github.com/Nayjest/lm-proxy/blob/main/doc/fallback.md)
714
717
 
715
718
 
716
719
  ## 🚧 Known Limitations<a id="-known-limitations"></a>
@@ -8,6 +8,8 @@
8
8
  <a href="https://github.com/Nayjest/lm-proxy/actions/workflows/code-style.yml"><img src="https://github.com/Nayjest/lm-proxy/actions/workflows/code-style.yml/badge.svg" alt="Code Style"></a>
9
9
  <img src="https://raw.githubusercontent.com/Nayjest/lm-proxy/main/coverage.svg" alt="Code Coverage">
10
10
  <a href="https://www.bestpractices.dev/projects/11364"><img src="https://www.bestpractices.dev/projects/11364/badge"></a>
11
+ <br>
12
+ <a href="https://github.com/vshymanskyy/StandWithUkraine/blob/main/README.md"><img src="https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/refs/heads/main/badges/StandWithUkraine.svg" alt="Stand With Ukraine"></a>
11
13
  <a href="https://github.com/Nayjest/lm-proxy/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Nayjest/lm-proxy?color=d08aff" alt="License"></a>
12
14
  </p>
13
15
 
@@ -646,6 +648,7 @@ prefix = "SECURITY_AUDIT"
646
648
 
647
649
  For more detailed information, check out these articles:
648
650
  - [HTTP Header Management](https://github.com/Nayjest/lm-proxy/blob/main/doc/http_headers.md)
651
+ - [Configuring fallbacks](https://github.com/Nayjest/lm-proxy/blob/main/doc/fallback.md)
649
652
 
650
653
 
651
654
  ## 🚧 Known Limitations<a id="-known-limitations"></a>
@@ -61,6 +61,12 @@ class Env:
61
61
  @staticmethod
62
62
  def init(config: Config | str | PathLike, debug: bool = False):
63
63
  """Initializes the LM-Proxy runtime environment singleton."""
64
+
65
+ def _is_async_callable(obj) -> bool:
66
+ return inspect.iscoroutinefunction(obj) or inspect.iscoroutinefunction(
67
+ getattr(obj, "__call__", None)
68
+ )
69
+
64
70
  env.debug = debug
65
71
 
66
72
  if not isinstance(config, Config):
@@ -78,14 +84,13 @@ class Env:
78
84
  # initialize connections
79
85
  env.connections = {}
80
86
  for conn_name, conn_config in env.config.connections.items():
81
- logging.info("Initializing '%s' LLM proxy connection...", conn_name)
87
+ logging.info("Initializing \"%s\" LLM proxy connection...", ui.green(conn_name))
82
88
  try:
83
- if inspect.iscoroutinefunction(conn_config):
84
- env.connections[conn_name] = conn_config
85
- elif isinstance(conn_config, str):
86
- env.connections[conn_name] = resolve_instance_or_callable(conn_config)
89
+ fn_or_config = resolve_instance_or_callable(conn_config, allow_types=[dict])
90
+ if _is_async_callable(fn_or_config):
91
+ env.connections[conn_name] = fn_or_config
87
92
  else:
88
- mc.configure(**conn_config, EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE)
93
+ mc.configure(**fn_or_config, EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE)
89
94
  env.connections[conn_name] = mc.env().llm_async_function
90
95
  except mc.LLMConfigError as e:
91
96
  raise ValueError(f"Error in configuration for connection '{conn_name}': {e}") from e
@@ -0,0 +1,4 @@
1
+ from .fallback import Fallback
2
+
3
+
4
+ __all__ = ['Fallback']
@@ -0,0 +1,69 @@
1
+ """Fallback strategy: tries connections in order until one succeeds."""
2
+
3
+ import logging
4
+
5
+ import microcore as mc
6
+ from microcore import ui
7
+
8
+ from pydantic import BaseModel, field_validator
9
+
10
+ from ..bootstrap import env
11
+
12
+
13
+ class Fallback(BaseModel):
14
+ """
15
+ Tries each connection in sequence, returning the first successful response.
16
+
17
+ If a connection fails, the error is logged and the next one is attempted.
18
+ If all connections fail, the last exception is re-raised.
19
+ """
20
+
21
+ connections: dict[str, dict] | list[str]
22
+
23
+ @field_validator("connections")
24
+ @classmethod
25
+ def validate_connections(cls, v: list[str] | dict[str]) -> dict[str, dict]:
26
+ if len(v) < 2:
27
+ raise ValueError("Fallback requires at least 2 connections")
28
+ if isinstance(v, list):
29
+ v_dict = {}
30
+ for conn_name_and_model in v:
31
+ if "." in conn_name_and_model:
32
+ conn_name, model = conn_name_and_model.split(".", 1)
33
+ v_dict[conn_name] = {"model": model}
34
+ else:
35
+ v_dict[conn_name_and_model] = {}
36
+ return v_dict
37
+ return v
38
+
39
+ async def __call__(self, *args, **kwargs):
40
+ for conn_name, override_params in self.connections.items():
41
+ logging.info(
42
+ f'Fallback strategy: using "{ui.green(conn_name)}" connection'
43
+ + (
44
+ (", overridden params: " + ui.yellow(override_params))
45
+ if override_params
46
+ else ""
47
+ )
48
+ )
49
+ if conn_name not in env.connections:
50
+ raise ValueError(
51
+ f"Fallback connection '{conn_name}' not found. "
52
+ f"Available: {list(env.connections.keys())}"
53
+ )
54
+ kw_args = dict(kwargs)
55
+ kw_args.update(override_params or {})
56
+ fn: mc.types.LLMAsyncFunctionType = env.connections[conn_name]
57
+ try:
58
+ return await fn(*args, **kw_args)
59
+ except Exception as e:
60
+ is_last = conn_name == list(self.connections)[-1]
61
+ if is_last:
62
+ logging.error("All fallback connections failed, last error: %s", e)
63
+ raise
64
+ logging.warning(
65
+ "Connection '%s' failed (%s: %s), trying next one...",
66
+ conn_name,
67
+ type(e).__name__,
68
+ e,
69
+ )
@@ -37,19 +37,45 @@ def resolve_instance_or_callable(
37
37
  allow_types: list[type] = None,
38
38
  ) -> Callable | object | None:
39
39
  """
40
- Resolves a class instance or callable from various configuration formats.
40
+ Resolves a configuration value into a callable or an object instance.
41
+
42
+ Supports multiple input formats commonly used in configuration files:
43
+
44
+ - ``None`` or ``""``: Returns ``None``.
45
+ - ``dict`` with a class key: Instantiates the class with remaining dict entries as kwargs.
46
+ Example: ``{"class": "my_module.MyClass", "param": 42}`` → ``MyClass(param=42)``
47
+ - ``str``: Imports the dotted path. Classes are instantiated; functions are returned as-is.
48
+ Example: ``"my_module.my_func"`` → ``my_func``
49
+ Example: ``"my_module.MyClass"`` → ``MyClass()``
50
+ - ``class``: Instantiated with no arguments.
51
+ - ``callable``: Returned as-is (lambdas, function objects, callable instances).
52
+ - Other types: Accepted only if their type is listed in ``allow_types``.
53
+
54
+ Args:
55
+ item: The configuration value to resolve.
56
+ class_key: Key used to identify the class in dict configs (default: ``"class"``).
57
+ debug_name: Human-readable label for error messages
58
+ (e.g., ``"logger"``, ``"api_key_check"``).
59
+ allow_types: Additional types to accept as valid values without transformation.
60
+
61
+ Returns:
62
+ A callable, an object instance, or ``None``.
63
+
64
+ Raises:
65
+ ValueError: If the input cannot be resolved to a valid callable or instance.
41
66
  """
42
67
  if item is None or item == "":
43
68
  return None
44
69
  if isinstance(item, dict):
45
- if class_key not in item:
70
+ if class_key in item:
71
+ args = dict(item)
72
+ class_name = args.pop(class_key)
73
+ constructor = resolve_callable(class_name)
74
+ return constructor(**args)
75
+ elif dict not in (allow_types or []):
46
76
  raise ValueError(
47
77
  f"'{class_key}' key is missing in {debug_name or 'item'} config: {item}"
48
78
  )
49
- args = dict(item)
50
- class_name = args.pop(class_key)
51
- constructor = resolve_callable(class_name)
52
- return constructor(**args)
53
79
  if isinstance(item, str):
54
80
  fn = resolve_callable(item)
55
81
  return fn() if inspect.isclass(fn) else fn
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openai-http-proxy"
3
- version = "3.0.2"
3
+ version = "3.2.0"
4
4
  description = "OpenAI HTTP Proxy is an OpenAI-compatible http proxy server for inferencing various LLMs capable of working with Google, Anthropic, OpenAI APIs, local PyTorch inference, etc."
5
5
  readme = "README.md"
6
6
  keywords = ["llm", "large language models", "ai", "gpt", "openai", "proxy", "http", "proxy-server", "llm gateway", "openai", "anthropic", "google genai"]
@@ -35,7 +35,7 @@ documentation = "https://github.com/Nayjest/lm-proxy#readme"
35
35
  license = { file = "LICENSE" }
36
36
 
37
37
  dependencies = [
38
- "ai-microcore>=5.1.2,<6",
38
+ "ai-microcore>=5.1.2,<7",
39
39
  "fastapi>=0.121.3,<1",
40
40
  "uvicorn>=0.41.0",
41
41
  "typer>=0.24.0",
@@ -1,82 +0,0 @@
1
- """
2
- LM-Proxy Application Entrypoint
3
- """
4
-
5
- import logging
6
- from typing import Optional
7
- from fastapi import FastAPI
8
- import typer
9
- import uvicorn
10
-
11
- from .bootstrap import env, bootstrap
12
- from .core import chat_completions
13
- from .models_endpoint import models
14
-
15
- cli_app = typer.Typer()
16
-
17
-
18
- @cli_app.callback(invoke_without_command=True)
19
- def run_server(
20
- config: Optional[str] = typer.Option(None, help="Path to the configuration file"),
21
- debug: Optional[bool] = typer.Option(None, help="Enable debug mode (more verbose logging)"),
22
- env_file: Optional[str] = typer.Option(
23
- ".env",
24
- "--env",
25
- "--env-file",
26
- "--env_file",
27
- help="Set the .env file to load ENV vars from",
28
- ),
29
- ):
30
- """
31
- Default command for CLI application: Run LM-Proxy web server
32
- """
33
- try:
34
- bootstrap(config=config or "config.toml", env_file=env_file, debug=debug)
35
- uvicorn.run(
36
- "lm_proxy.app:web_app",
37
- host=env.config.host,
38
- port=env.config.port,
39
- ssl_keyfile=getattr(env.config, "ssl_keyfile", None),
40
- ssl_certfile=getattr(env.config, "ssl_certfile", None),
41
- reload=env.config.dev_autoreload,
42
- factory=True,
43
- )
44
- except Exception as e:
45
- if env.debug:
46
- raise
47
- logging.error(e)
48
- raise typer.Exit(code=1)
49
-
50
-
51
- def web_app():
52
- """
53
- Entrypoint for ASGI server
54
- """
55
- app = FastAPI(title="LM-Proxy", description="OpenAI-compatible proxy server for LLM inference")
56
- app.add_api_route(
57
- path=f"{env.config.api_prefix}/chat/completions",
58
- endpoint=chat_completions,
59
- methods=["POST"],
60
- )
61
- app.add_api_route(
62
- path=f"{env.config.api_prefix}/models",
63
- endpoint=models,
64
- methods=["GET"],
65
- )
66
- # app.add_api_route(path="", endpoint=lambda: {"status": "ok"}, methods=["GET"])
67
-
68
- # @app.middleware("http")
69
- # async def log_requests(request, call_next):
70
- # body = await request.body()
71
- # logging.info(f"Request URL: {request.url}")
72
- # logging.info(f"Request Headers: {dict(request.headers)}")
73
- # logging.info(f"Request Body: {body.decode()}")
74
- #
75
- # response = await call_next(request)
76
- # return response
77
-
78
- return app
79
-
80
-
81
- if __name__ == "__main__":
82
- cli_app()