argo-proxy 3.2.1__tar.gz → 3.2.3__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 (52) hide show
  1. {argo_proxy-3.2.1/src/argo_proxy.egg-info → argo_proxy-3.2.3}/PKG-INFO +2 -2
  2. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/pyproject.toml +1 -1
  3. {argo_proxy-3.2.1 → argo_proxy-3.2.3/src/argo_proxy.egg-info}/PKG-INFO +2 -2
  4. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argo_proxy.egg-info/SOURCES.txt +5 -2
  5. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argo_proxy.egg-info/requires.txt +1 -1
  6. argo_proxy-3.2.3/src/argoproxy/__init__.py +1 -0
  7. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/app.py +5 -1
  8. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/cli/handlers.py +3 -3
  9. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/endpoints/dispatch.py +2 -4
  10. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/endpoints/passthrough.py +4 -0
  11. argo_proxy-3.2.3/src/argoproxy/models/__init__.py +45 -0
  12. argo_proxy-3.2.3/src/argoproxy/models/constants.py +153 -0
  13. argo_proxy-3.2.3/src/argoproxy/models/registry.py +432 -0
  14. argo_proxy-3.2.3/src/argoproxy/models/upstream.py +343 -0
  15. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/utils/attack_logger.py +157 -2
  16. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/utils/misc.py +10 -0
  17. argo_proxy-3.2.3/tests/test_security_middleware.py +161 -0
  18. argo_proxy-3.2.1/src/argoproxy/__init__.py +0 -1
  19. argo_proxy-3.2.1/src/argoproxy/models.py +0 -1011
  20. argo_proxy-3.2.1/src/argoproxy/utils/models.py +0 -23
  21. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/LICENSE +0 -0
  22. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/README.md +0 -0
  23. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/setup.cfg +0 -0
  24. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argo_proxy.egg-info/dependency_links.txt +0 -0
  25. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argo_proxy.egg-info/entry_points.txt +0 -0
  26. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argo_proxy.egg-info/top_level.txt +0 -0
  27. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/_vendor/__init__.py +0 -0
  28. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/_vendor/semver.py +0 -0
  29. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/_vendor/yaml.py +0 -0
  30. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/cli/__init__.py +0 -0
  31. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/cli/display.py +0 -0
  32. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/cli/parser.py +0 -0
  33. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/config/__init__.py +0 -0
  34. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/config/interactive.py +0 -0
  35. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/config/io.py +0 -0
  36. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/config/model.py +0 -0
  37. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/config/validation.py +0 -0
  38. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/endpoints/dev_proxy.py +0 -0
  39. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/endpoints/extras.py +0 -0
  40. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/performance.py +0 -0
  41. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/py.typed +0 -0
  42. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/utils/image_processing.py +0 -0
  43. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/utils/logging.py +0 -0
  44. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/src/argoproxy/utils/transports.py +0 -0
  45. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/test/test_chat_completions.py +0 -0
  46. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/test/test_embeddings.py +0 -0
  47. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/test/test_function_calling_multiple.py +0 -0
  48. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/test/test_function_calling_single.py +0 -0
  49. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/test/test_leaked_tool_parser.py +0 -0
  50. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/test/test_model_resolution.py +0 -0
  51. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/tests/test_config_migrate.py +0 -0
  52. {argo_proxy-3.2.1 → argo_proxy-3.2.3}/tests/test_unix_socket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argo-proxy
3
- Version: 3.2.1
3
+ Version: 3.2.3
4
4
  Summary: Proxy server to Argo API, OpenAI format compatible
5
5
  Author-email: Peng Ding <oaklight@gmx.com>
6
6
  License-Expression: MIT
@@ -15,7 +15,7 @@ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: aiohttp>=3.12.2
18
- Requires-Dist: llm-rosetta>=0.7.0
18
+ Requires-Dist: llm-rosetta>=0.7.1
19
19
  Requires-Dist: pydantic>=2.11.7
20
20
  Requires-Dist: tiktoken>=0.9.0
21
21
  Requires-Dist: tqdm>=4.67.1
@@ -20,7 +20,7 @@ classifiers = [
20
20
 
21
21
  dependencies = [
22
22
  "aiohttp>=3.12.2",
23
- "llm-rosetta>=0.7.0",
23
+ "llm-rosetta>=0.7.1",
24
24
  "pydantic>=2.11.7",
25
25
  "tiktoken>=0.9.0",
26
26
  "tqdm>=4.67.1",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argo-proxy
3
- Version: 3.2.1
3
+ Version: 3.2.3
4
4
  Summary: Proxy server to Argo API, OpenAI format compatible
5
5
  Author-email: Peng Ding <oaklight@gmx.com>
6
6
  License-Expression: MIT
@@ -15,7 +15,7 @@ Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: aiohttp>=3.12.2
18
- Requires-Dist: llm-rosetta>=0.7.0
18
+ Requires-Dist: llm-rosetta>=0.7.1
19
19
  Requires-Dist: pydantic>=2.11.7
20
20
  Requires-Dist: tiktoken>=0.9.0
21
21
  Requires-Dist: tqdm>=4.67.1
@@ -9,7 +9,6 @@ src/argo_proxy.egg-info/requires.txt
9
9
  src/argo_proxy.egg-info/top_level.txt
10
10
  src/argoproxy/__init__.py
11
11
  src/argoproxy/app.py
12
- src/argoproxy/models.py
13
12
  src/argoproxy/performance.py
14
13
  src/argoproxy/py.typed
15
14
  src/argoproxy/_vendor/__init__.py
@@ -28,11 +27,14 @@ src/argoproxy/endpoints/dev_proxy.py
28
27
  src/argoproxy/endpoints/dispatch.py
29
28
  src/argoproxy/endpoints/extras.py
30
29
  src/argoproxy/endpoints/passthrough.py
30
+ src/argoproxy/models/__init__.py
31
+ src/argoproxy/models/constants.py
32
+ src/argoproxy/models/registry.py
33
+ src/argoproxy/models/upstream.py
31
34
  src/argoproxy/utils/attack_logger.py
32
35
  src/argoproxy/utils/image_processing.py
33
36
  src/argoproxy/utils/logging.py
34
37
  src/argoproxy/utils/misc.py
35
- src/argoproxy/utils/models.py
36
38
  src/argoproxy/utils/transports.py
37
39
  test/test_chat_completions.py
38
40
  test/test_embeddings.py
@@ -41,4 +43,5 @@ test/test_function_calling_single.py
41
43
  test/test_leaked_tool_parser.py
42
44
  test/test_model_resolution.py
43
45
  tests/test_config_migrate.py
46
+ tests/test_security_middleware.py
44
47
  tests/test_unix_socket.py
@@ -1,5 +1,5 @@
1
1
  aiohttp>=3.12.2
2
- llm-rosetta>=0.7.0
2
+ llm-rosetta>=0.7.1
3
3
  pydantic>=2.11.7
4
4
  tiktoken>=0.9.0
5
5
  tqdm>=4.67.1
@@ -0,0 +1 @@
1
+ __version__ = "3.2.3"
@@ -19,6 +19,7 @@ from .performance import (
19
19
  OptimizedHTTPSession,
20
20
  get_performance_config,
21
21
  )
22
+ from .utils.attack_logger import security_middleware
22
23
  from .utils.logging import log_debug, log_error, log_info, log_warning
23
24
 
24
25
 
@@ -320,7 +321,10 @@ def create_app():
320
321
  # Set client_max_size to 100MB to handle large image payloads from remote clients
321
322
  # Users may send images larger than the gateway's 20MB limit; argo-proxy will
322
323
  # compress them before forwarding. Default aiohttp limit is 1MB which is too small.
323
- app = web.Application(client_max_size=100 * 1024 * 1024)
324
+ app = web.Application(
325
+ client_max_size=100 * 1024 * 1024,
326
+ middlewares=[security_middleware],
327
+ )
324
328
  app.on_startup.append(prepare_app)
325
329
  app.on_shutdown.append(cleanup_app)
326
330
 
@@ -700,7 +700,7 @@ def handle_models(args: argparse.Namespace):
700
700
  from collections import defaultdict
701
701
 
702
702
  from ..config import load_config
703
- from ..models import ModelRegistry
703
+ from ..models import ModelRegistry, classify_model_family
704
704
 
705
705
  config_data, _ = load_config(args.config, verbose=False)
706
706
  if not config_data:
@@ -762,7 +762,7 @@ def handle_models(args: argparse.Namespace):
762
762
  if args.json:
763
763
  output = []
764
764
  for internal_id, aliases in sorted(chat_id_to_aliases.items()):
765
- family = registry._classify_model_by_family(internal_id)
765
+ family = classify_model_family(internal_id)
766
766
  output.append(
767
767
  {
768
768
  "upstream_id": internal_id,
@@ -807,7 +807,7 @@ def handle_models(args: argparse.Namespace):
807
807
  # Classify chat models by family
808
808
  chat_families: dict[str, list[tuple[str, list[str]]]] = defaultdict(list)
809
809
  for internal_id, aliases in sorted(chat_id_to_aliases.items()):
810
- family = registry._classify_model_by_family(internal_id)
810
+ family = classify_model_family(internal_id)
811
811
  chat_families[family].append((internal_id, aliases))
812
812
 
813
813
  for family in family_order:
@@ -27,6 +27,7 @@ from llm_rosetta.shims.providers import load_providers
27
27
  from ..config import ArgoConfig
28
28
  from ..models import ModelRegistry
29
29
  from ..utils.image_processing import process_anthropic_images, process_openai_images
30
+ from ..utils.misc import build_user_agent
30
31
  from ..utils.logging import (
31
32
  clear_request_user,
32
33
  log_converted_request,
@@ -288,10 +289,7 @@ def _build_upstream_headers(
288
289
  """Build HTTP headers for the upstream API request."""
289
290
  headers: dict[str, str] = {"Content-Type": "application/json"}
290
291
 
291
- # Pass through client's User-Agent to upstream (transparent proxy)
292
- client_ua = request.headers.get("User-Agent")
293
- if client_ua:
294
- headers["User-Agent"] = client_ua
292
+ headers["User-Agent"] = build_user_agent(request.headers.get("User-Agent"))
295
293
 
296
294
  if should_use_username_passthrough():
297
295
  api_key = _extract_client_credential(request, target_provider) or fallback_user
@@ -13,6 +13,7 @@ from aiohttp import web
13
13
 
14
14
  from ..config import ArgoConfig
15
15
  from ..models import ModelRegistry
16
+ from ..utils.misc import build_user_agent
16
17
  from ..utils.logging import (
17
18
  log_converted_request,
18
19
  log_debug,
@@ -67,6 +68,9 @@ async def proxy_embeddings_request(
67
68
  upstream_url = f"{config.native_openai_base_url}/embeddings"
68
69
 
69
70
  headers = {"Content-Type": "application/json"}
71
+
72
+ headers["User-Agent"] = build_user_agent(request.headers.get("User-Agent"))
73
+
70
74
  if "Authorization" in request.headers:
71
75
  headers["Authorization"] = request.headers["Authorization"]
72
76
 
@@ -0,0 +1,45 @@
1
+ """Model registry, classification, and upstream communication."""
2
+
3
+ from .constants import (
4
+ ANTHROPIC_PATTERNS,
5
+ GEMINI_PATTERN,
6
+ GPT_O_PATTERN,
7
+ NATIVE_TOOL_CALL_MODELS,
8
+ NATIVE_TOOL_CALL_PATTERNS,
9
+ NO_SYS_MSG_MODELS,
10
+ NO_SYS_MSG_PATTERNS,
11
+ OPTION_2_INPUT_MODELS,
12
+ OPTION_2_INPUT_PATTERNS,
13
+ TIKTOKEN_ENCODING_PREFIX_MAPPING,
14
+ _DEFAULT_CHAT_MODELS,
15
+ _EMBED_MODELS,
16
+ classify_model_family,
17
+ filter_model_by_patterns,
18
+ flatten_mapping,
19
+ is_anthropic_model,
20
+ )
21
+ from .registry import ModelRegistry, OpenAIModel
22
+ from .upstream import Model, produce_argo_model_list
23
+
24
+ __all__ = [
25
+ "ANTHROPIC_PATTERNS",
26
+ "GEMINI_PATTERN",
27
+ "GPT_O_PATTERN",
28
+ "NATIVE_TOOL_CALL_MODELS",
29
+ "NATIVE_TOOL_CALL_PATTERNS",
30
+ "NO_SYS_MSG_MODELS",
31
+ "NO_SYS_MSG_PATTERNS",
32
+ "OPTION_2_INPUT_MODELS",
33
+ "OPTION_2_INPUT_PATTERNS",
34
+ "TIKTOKEN_ENCODING_PREFIX_MAPPING",
35
+ "Model",
36
+ "ModelRegistry",
37
+ "OpenAIModel",
38
+ "_DEFAULT_CHAT_MODELS",
39
+ "_EMBED_MODELS",
40
+ "classify_model_family",
41
+ "filter_model_by_patterns",
42
+ "flatten_mapping",
43
+ "is_anthropic_model",
44
+ "produce_argo_model_list",
45
+ ]
@@ -0,0 +1,153 @@
1
+ """Model constants, default lists, patterns, and classification helpers."""
2
+
3
+ import fnmatch
4
+ from typing import Any
5
+
6
+
7
+ def flatten_mapping(mapping: dict[str, Any]) -> dict[str, str]:
8
+ flat = {}
9
+ for model, aliases in mapping.items():
10
+ if isinstance(aliases, str):
11
+ flat[aliases] = model
12
+ else:
13
+ for alias in aliases:
14
+ flat[alias] = model
15
+ return flat
16
+
17
+
18
+ # Default models fallback — kept in sync with the live /v1/models endpoint.
19
+ _DEFAULT_CHAT_MODELS = flatten_mapping(
20
+ {
21
+ # openai – gpt-4o (legacy, still served)
22
+ "gpt4o": "argo:gpt-4o",
23
+ # openai – o-series reasoning
24
+ "gpto1": ["argo:gpt-o1", "argo:o1"],
25
+ "gpto3mini": ["argo:gpt-o3-mini", "argo:o3-mini"],
26
+ "gpto3": ["argo:gpt-o3", "argo:o3"],
27
+ "gpto4mini": ["argo:gpt-o4-mini", "argo:o4-mini"],
28
+ # openai – gpt-4.1 family
29
+ "gpt41": "argo:gpt-4.1",
30
+ "gpt41mini": "argo:gpt-4.1-mini",
31
+ "gpt41nano": "argo:gpt-4.1-nano",
32
+ # openai – gpt-5 family
33
+ "gpt5": "argo:gpt-5",
34
+ "gpt5mini": "argo:gpt-5-mini",
35
+ "gpt5nano": "argo:gpt-5-nano",
36
+ "gpt51": "argo:gpt-5.1",
37
+ "gpt52": "argo:gpt-5.2",
38
+ "gpt54": "argo:gpt-5.4",
39
+ "gpt54mini": "argo:gpt-5.4-mini",
40
+ "gpt54nano": "argo:gpt-5.4-nano",
41
+ "gpt55": "argo:gpt-5.5",
42
+ # gemini
43
+ "gemini25pro": "argo:gemini-2.5-pro",
44
+ "gemini25flash": "argo:gemini-2.5-flash",
45
+ "gemini35flash": "argo:gemini-3.5-flash",
46
+ "gemini31flashlite": "argo:gemini-3.1-flash-lite",
47
+ # claude – opus
48
+ "claudeopus48": ["argo:claude-4.8-opus", "argo:claude-opus-4.8"],
49
+ "claudeopus47": ["argo:claude-4.7-opus", "argo:claude-opus-4.7"],
50
+ "claudeopus46": ["argo:claude-4.6-opus", "argo:claude-opus-4.6"],
51
+ "claudeopus45": ["argo:claude-4.5-opus", "argo:claude-opus-4.5"],
52
+ "claudeopus41": ["argo:claude-4.1-opus", "argo:claude-opus-4.1"],
53
+ # claude – sonnet
54
+ "claudesonnet5": ["argo:claude-5-sonnet", "argo:claude-sonnet-5"],
55
+ "claudesonnet46": ["argo:claude-4.6-sonnet", "argo:claude-sonnet-4.6"],
56
+ "claudesonnet45": ["argo:claude-4.5-sonnet", "argo:claude-sonnet-4.5"],
57
+ # claude – haiku
58
+ "claudehaiku45": ["argo:claude-4.5-haiku", "argo:claude-haiku-4.5"],
59
+ }
60
+ )
61
+
62
+ _EMBED_MODELS = flatten_mapping(
63
+ {
64
+ "ada002": "argo:text-embedding-ada-002",
65
+ "v3small": "argo:text-embedding-3-small",
66
+ "v3large": "argo:text-embedding-3-large",
67
+ }
68
+ )
69
+
70
+
71
+ def filter_model_by_patterns(
72
+ model_dict: dict[str, str], patterns: set[str]
73
+ ) -> list[str]:
74
+ """Filter model_dict values (model_id) by given fnmatch patterns,
75
+ returning both the model_name (key) and model_id (value) for matches."""
76
+ matching = set()
77
+ for model_name, model_id in model_dict.items():
78
+ if any(fnmatch.fnmatch(model_id, pattern) for pattern in patterns):
79
+ matching.add(model_name)
80
+ matching.add(model_id)
81
+ return sorted(matching)
82
+
83
+
84
+ # any models that unable to handle system prompt (o1-mini / o1-preview retired)
85
+ NO_SYS_MSG_PATTERNS: set[str] = set()
86
+
87
+ NO_SYS_MSG_MODELS = filter_model_by_patterns(
88
+ _DEFAULT_CHAT_MODELS,
89
+ NO_SYS_MSG_PATTERNS,
90
+ )
91
+
92
+
93
+ # any models that only able to handle single system prompt and no system prompt at all
94
+ OPTION_2_INPUT_PATTERNS: set[str] = set()
95
+
96
+ OPTION_2_INPUT_MODELS = filter_model_by_patterns(
97
+ _DEFAULT_CHAT_MODELS,
98
+ OPTION_2_INPUT_PATTERNS,
99
+ )
100
+
101
+ # any models that supports native tool call
102
+ NATIVE_TOOL_CALL_PATTERNS: set[str] = {
103
+ "*o1",
104
+ "*o3*",
105
+ "*o4*",
106
+ }
107
+
108
+ NATIVE_TOOL_CALL_MODELS = filter_model_by_patterns(
109
+ _DEFAULT_CHAT_MODELS,
110
+ NATIVE_TOOL_CALL_PATTERNS,
111
+ )
112
+
113
+ TIKTOKEN_ENCODING_PREFIX_MAPPING = {
114
+ "gpt5": "o200k_base", # gpt-5 family
115
+ "gpto": "o200k_base", # o-series
116
+ "gpt4o": "o200k_base", # gpt-4o
117
+ "gpt41": "o200k_base", # gpt-4.1 family
118
+ # this order need to be preserved to correctly parse mapping
119
+ "gpt4": "cl100k_base", # gpt-4 series (legacy fallback)
120
+ "ada002": "cl100k_base", # embedding
121
+ "v3": "cl100k_base", # embedding
122
+ }
123
+
124
+ # --- Provider detection patterns ---
125
+
126
+ GPT_O_PATTERN = "gpto*"
127
+ GEMINI_PATTERN = "gemini*"
128
+
129
+ ANTHROPIC_PATTERNS = ("claude*", "sonnet*", "opus*", "haiku*", "fable*")
130
+
131
+
132
+ def is_anthropic_model(model_id: str) -> bool:
133
+ return any(fnmatch.fnmatch(model_id, p) for p in ANTHROPIC_PATTERNS)
134
+
135
+
136
+ def classify_model_family(model_id: str) -> str:
137
+ """Classify a model by its provider family based on model ID patterns."""
138
+ if (
139
+ fnmatch.fnmatch(model_id, "gpt*")
140
+ or fnmatch.fnmatch(model_id, GPT_O_PATTERN)
141
+ or fnmatch.fnmatch(model_id, "ada*")
142
+ or fnmatch.fnmatch(model_id, "v3*")
143
+ or fnmatch.fnmatch(model_id, "*embedding*")
144
+ ):
145
+ return "openai"
146
+
147
+ if is_anthropic_model(model_id):
148
+ return "anthropic"
149
+
150
+ if fnmatch.fnmatch(model_id, GEMINI_PATTERN):
151
+ return "google"
152
+
153
+ return "unknown"