flexgate 0.6.0__tar.gz → 0.7.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.
- {flexgate-0.6.0 → flexgate-0.7.0}/CLAUDE.md +7 -2
- {flexgate-0.6.0 → flexgate-0.7.0}/PKG-INFO +1 -1
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/__init__.py +1 -1
- flexgate-0.7.0/flexgate/fallback.py +99 -0
- flexgate-0.7.0/flexgate/images.py +192 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/server.py +13 -1
- {flexgate-0.6.0 → flexgate-0.7.0}/.github/workflows/docs.yml +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/.github/workflows/release.yml +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/.gitignore +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/README.md +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/config.yaml.template +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/Makefile +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/_static/.gitkeep +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/cli.md +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/conf.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/configuration.md +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/development.md +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/index.md +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/quickstart.md +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/requirements.txt +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/docs/usage.md +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/__main__.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/cli.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/config.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/doctor.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/healthcheck.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/main.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/migrate.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/proxy.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/router.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/service.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/settings.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/sync.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/ui.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/update.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/flexgate/usage.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/pyproject.toml +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/test.png +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/test_multimodal.py +0 -0
- {flexgate-0.6.0 → flexgate-0.7.0}/test_multimodal_gateway.py +0 -0
|
@@ -73,10 +73,13 @@ without a systemd user instance.
|
|
|
73
73
|
|
|
74
74
|
```
|
|
75
75
|
Claude Code → POST /v1/messages (model="claude-sonnet-4-6")
|
|
76
|
-
→ server.py (Starlette app,
|
|
76
|
+
→ server.py (Starlette app, /v1/messages + /v1/images/* routes)
|
|
77
77
|
→ router.py (resolve(): regex match model, check schedule windows first, then default routes)
|
|
78
78
|
→ proxy.py (rewrite model field if override, swap x-api-key, SSE streaming pass-through)
|
|
79
79
|
→ Upstream provider (z.ai, minimax, xiaomi, etc.)
|
|
80
|
+
|
|
81
|
+
OpenAI-style client → POST /v1/images/generations
|
|
82
|
+
→ images.py (translate to MiniMax /v1/image_generation; provider auto-detected by base_url)
|
|
80
83
|
```
|
|
81
84
|
|
|
82
85
|
### Source files (`flexgate/`)
|
|
@@ -89,7 +92,9 @@ Claude Code → POST /v1/messages (model="claude-sonnet-4-6")
|
|
|
89
92
|
| `router.py` | `resolve(config, model)` — schedule-first then default routes, first regex match wins; model aliases normalized before matching |
|
|
90
93
|
| `proxy.py` | `handle_request()` — httpx async proxy, per-key fallback retry loop, SSE streaming + JSON pass-through |
|
|
91
94
|
| `usage.py` | `flexgate status` usage queries — per-platform adapters (MiniMax coding_plan API, Kimi Code usages API, z.ai quota API, LiteLLM `/key/info`) plus a minimal chat probe fallback |
|
|
92
|
-
| `server.py` | Starlette app creation, `POST /v1/messages`
|
|
95
|
+
| `server.py` | Starlette app creation, `POST /v1/messages` + `POST /v1/images/{generations,edits}` endpoints, `SIGUSR1` lifespan reload |
|
|
96
|
+
| `images.py` | OpenAI Images API → MiniMax `/v1/image_generation` translation (`image-01`, size→width/height clamped to 512–2048 mult of 8, `data.image_base64[]`→`data[].b64_json`); edits endpoint returns 501; unknown image model names get 501 `model_not_supported` |
|
|
97
|
+
| `fallback.py` | Catch-all route: merged Anthropic/OpenAI-shaped JSON errors for every unserved path — 404 `route_not_found` (lists valid routes), 405 `method_not_allowed`, 501 `endpoint_not_supported` per recognized API family with an actionable reason |
|
|
93
98
|
| `main.py` | Thin bootstrap: load config → create app → run uvicorn |
|
|
94
99
|
| `service.py` | Authoritative systemd user-service install/start/stop/restart/reload/status and legacy PID migration |
|
|
95
100
|
| `healthcheck.py` | Upstream connectivity probing (`POST /v1/messages`, max_tokens=1) for each referenced (provider, model) pair; called by `doctor` (`flexgate check` is a deprecated alias of doctor) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flexgate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Flexible local API gateway for Claude Code — route models to different providers
|
|
5
5
|
Project-URL: Homepage, https://github.com/Agony5757/flexible-gateway
|
|
6
6
|
Project-URL: Repository, https://github.com/Agony5757/flexible-gateway
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Structured errors for every route and capability flexgate does not serve.
|
|
2
|
+
|
|
3
|
+
A bare Starlette 404 tells an API client nothing: it cannot tell "wrong port"
|
|
4
|
+
apart from "right gateway, unsupported endpoint". Every unmatched request
|
|
5
|
+
instead gets a JSON error explaining what was asked for and what flexgate
|
|
6
|
+
does support, so client-side fallback logic (and humans reading logs) can
|
|
7
|
+
react without guessing.
|
|
8
|
+
|
|
9
|
+
Error payload shape is a merge of the Anthropic and OpenAI conventions so
|
|
10
|
+
both client families parse it:
|
|
11
|
+
|
|
12
|
+
{"type": "error", "error": {"type": ..., "message": ..., "code": ...}}
|
|
13
|
+
|
|
14
|
+
Status code policy:
|
|
15
|
+
- 404 route_not_found unknown path entirely; message lists valid routes
|
|
16
|
+
- 405 method_not_allowed known path, wrong HTTP method
|
|
17
|
+
- 501 endpoint_not_supported recognized API family flexgate does not serve
|
|
18
|
+
- 501 capability_not_supported / model_not_supported for valid routes with
|
|
19
|
+
unsupported operations or model names (raised by the owning module)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
from starlette.requests import Request
|
|
27
|
+
from starlette.responses import JSONResponse
|
|
28
|
+
|
|
29
|
+
SUPPORTED_ROUTES = ("POST /v1/messages", "POST /v1/images/generations")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def error_json(message: str, status: int, error_type: str, code: str) -> JSONResponse:
|
|
33
|
+
return JSONResponse(
|
|
34
|
+
{"type": "error", "error": {"type": error_type, "message": message, "code": code}},
|
|
35
|
+
status_code=status,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Recognized-but-unsupported API families: (path regex, human reason).
|
|
40
|
+
# Order matters; first match wins. Keep reasons actionable: say what the
|
|
41
|
+
# caller probably wanted and where that capability lives, if anywhere.
|
|
42
|
+
_UNSUPPORTED_FAMILIES: list[tuple[re.Pattern[str], str]] = [
|
|
43
|
+
(
|
|
44
|
+
re.compile(r"^/v1/messages/count_tokens$"),
|
|
45
|
+
"Token counting is not proxied; flexgate only forwards POST /v1/messages. "
|
|
46
|
+
"Count tokens client-side (e.g. with a tokenizer) instead.",
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
re.compile(r"^/v1/(chat/completions|responses)(/.*)?$"),
|
|
50
|
+
"OpenAI chat is not served: flexgate proxies Anthropic-style POST /v1/messages only. "
|
|
51
|
+
"Point an Anthropic-compatible client at this gateway, or call the upstream provider directly.",
|
|
52
|
+
),
|
|
53
|
+
(
|
|
54
|
+
re.compile(r"^/v1/images/edits$"),
|
|
55
|
+
# Owning module (images.py) normally answers this first; kept as a safety net.
|
|
56
|
+
"Image editing/inpainting is not available: the MiniMax image-01 backend is text-to-image only. "
|
|
57
|
+
"Use POST /v1/images/generations, or edit the image with another tool and upload the result.",
|
|
58
|
+
),
|
|
59
|
+
(
|
|
60
|
+
re.compile(r"^/v1/embeddings"),
|
|
61
|
+
"Embeddings are not proxied: no configured provider exposes an embedding endpoint. "
|
|
62
|
+
"Call an embedding-capable provider directly.",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
re.compile(r"^/v1/(audio|videos?)(/.*)?$"),
|
|
66
|
+
"Audio/video endpoints are not proxied by flexgate. "
|
|
67
|
+
"Call the upstream provider's native API directly.",
|
|
68
|
+
),
|
|
69
|
+
(
|
|
70
|
+
re.compile(r"^/v1/(models|files|fine_tuning|batches|moderations|vector_stores|assistants|threads|uploads)(/.*)?$"),
|
|
71
|
+
"This OpenAI platform endpoint is not proxied: flexgate is a routing gateway, not an API platform. "
|
|
72
|
+
"It only serves POST /v1/messages and POST /v1/images/generations.",
|
|
73
|
+
),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def fallback(request: Request) -> JSONResponse:
|
|
78
|
+
"""Catch-all handler for every path/method flexgate does not serve."""
|
|
79
|
+
path = request.url.path.rstrip("/") or "/"
|
|
80
|
+
|
|
81
|
+
if path in ("/v1/messages", "/v1/images/generations"):
|
|
82
|
+
return error_json(
|
|
83
|
+
f"{request.method} is not allowed on {path}; use POST.",
|
|
84
|
+
405,
|
|
85
|
+
"invalid_request_error",
|
|
86
|
+
"method_not_allowed",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for pattern, reason in _UNSUPPORTED_FAMILIES:
|
|
90
|
+
if pattern.match(path):
|
|
91
|
+
return error_json(reason, 501, "not_implemented", "endpoint_not_supported")
|
|
92
|
+
|
|
93
|
+
return error_json(
|
|
94
|
+
f"Unknown route: {request.method} {path}. "
|
|
95
|
+
f"flexgate serves exactly: {', '.join(SUPPORTED_ROUTES)}.",
|
|
96
|
+
404,
|
|
97
|
+
"not_found_error",
|
|
98
|
+
"route_not_found",
|
|
99
|
+
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""OpenAI-compatible Images API endpoint, backed by MiniMax image generation.
|
|
2
|
+
|
|
3
|
+
Exposes `POST /v1/images/generations` so OpenAI-style clients (e.g. editppt's
|
|
4
|
+
openai-compatible-api image backend) can generate images through the gateway
|
|
5
|
+
without any per-provider configuration: the upstream is derived from whichever
|
|
6
|
+
configured provider points at MiniMax (`api.minimaxi.com` / `api.minimax.io`).
|
|
7
|
+
|
|
8
|
+
Translation notes:
|
|
9
|
+
- MiniMax only has text-to-image (`POST /v1/image_generation`, model
|
|
10
|
+
`image-01`). `size` ("WxH") is mapped to width/height clamped to 512-2048
|
|
11
|
+
and rounded to multiples of 8; unrecognized sizes are omitted.
|
|
12
|
+
- MiniMax replies with `data.image_base64[]` (JPEG bytes); it is forwarded
|
|
13
|
+
verbatim as OpenAI `data[].b64_json` — downstream tools sniff the real
|
|
14
|
+
content type, so no transcoding dependency is introduced here.
|
|
15
|
+
- `/v1/images/edits` is answered 501: MiniMax has no general image-editing or
|
|
16
|
+
inpainting capability (its subject_reference is character-only).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
import time
|
|
25
|
+
from urllib.parse import urlsplit
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
from starlette.requests import Request
|
|
29
|
+
from starlette.responses import JSONResponse
|
|
30
|
+
|
|
31
|
+
from flexgate.config import GatewayConfig, ProviderConfig
|
|
32
|
+
from flexgate.fallback import error_json
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("flexgate.images")
|
|
35
|
+
|
|
36
|
+
MINIMAX_MODEL = "image-01"
|
|
37
|
+
MINIMAX_MIN_EDGE = 512
|
|
38
|
+
MINIMAX_MAX_EDGE = 2048
|
|
39
|
+
MINIMAX_MAX_PROMPT_CHARS = 1500
|
|
40
|
+
|
|
41
|
+
# Requested model names containing any of these hints are accepted and mapped
|
|
42
|
+
# to MiniMax image-01 (the only image backend behind this gateway).
|
|
43
|
+
_IMAGE_MODEL_HINTS = (
|
|
44
|
+
"gpt-image",
|
|
45
|
+
"dall-e",
|
|
46
|
+
"image-01",
|
|
47
|
+
"seedream",
|
|
48
|
+
"flux",
|
|
49
|
+
"sdxl",
|
|
50
|
+
"stable-diffusion",
|
|
51
|
+
"minimax",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _error_json(message: str, status: int, code: str = "minimax_error") -> JSONResponse:
|
|
56
|
+
error_type = {
|
|
57
|
+
400: "invalid_request_error",
|
|
58
|
+
501: "not_implemented",
|
|
59
|
+
503: "not_implemented",
|
|
60
|
+
}.get(status, "api_error")
|
|
61
|
+
return error_json(message, status, error_type, code)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _find_minimax_provider(config: GatewayConfig) -> ProviderConfig | None:
|
|
65
|
+
candidates = [p for p in config.providers.values() if "minimax" in p.base_url]
|
|
66
|
+
for provider in candidates:
|
|
67
|
+
if provider.name == "minimax":
|
|
68
|
+
return provider
|
|
69
|
+
return candidates[0] if candidates else None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _image_api_root(base_url: str) -> str:
|
|
73
|
+
parts = urlsplit(base_url)
|
|
74
|
+
return f"{parts.scheme}://{parts.netloc}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _clamp_edge(value: int) -> int:
|
|
78
|
+
return max(MINIMAX_MIN_EDGE, min(MINIMAX_MAX_EDGE, value))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _round8(value: float) -> int:
|
|
82
|
+
return max(8, int(round(value / 8)) * 8)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _map_size(size: object) -> dict[str, int]:
|
|
86
|
+
if not isinstance(size, str):
|
|
87
|
+
return {}
|
|
88
|
+
match = re.fullmatch(r"(\d+)x(\d+)", size)
|
|
89
|
+
if not match:
|
|
90
|
+
return {}
|
|
91
|
+
w, h = int(match.group(1)), int(match.group(2))
|
|
92
|
+
if w <= 0 or h <= 0:
|
|
93
|
+
return {}
|
|
94
|
+
scale = min(1.0, MINIMAX_MAX_EDGE / max(w, h))
|
|
95
|
+
w, h = w * scale, h * scale
|
|
96
|
+
if min(w, h) < MINIMAX_MIN_EDGE:
|
|
97
|
+
boost = MINIMAX_MIN_EDGE / min(w, h)
|
|
98
|
+
w, h = w * boost, h * boost
|
|
99
|
+
if max(w, h) > MINIMAX_MAX_EDGE:
|
|
100
|
+
shrink = MINIMAX_MAX_EDGE / max(w, h)
|
|
101
|
+
w, h = w * shrink, h * shrink
|
|
102
|
+
return {"width": _clamp_edge(_round8(w)), "height": _clamp_edge(_round8(h))}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def images_generate(request: Request) -> JSONResponse:
|
|
106
|
+
t0 = time.monotonic()
|
|
107
|
+
config: GatewayConfig = request.app.state.config
|
|
108
|
+
client: httpx.AsyncClient = request.app.state.client
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
body = json.loads(await request.body())
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
return _error_json("Invalid JSON in request body", 400, "bad_request")
|
|
114
|
+
|
|
115
|
+
provider = _find_minimax_provider(config)
|
|
116
|
+
if provider is None:
|
|
117
|
+
return _error_json(
|
|
118
|
+
"No MiniMax provider configured (need a provider whose base_url points at MiniMax)",
|
|
119
|
+
503,
|
|
120
|
+
"provider_not_found",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
prompt = body.get("prompt")
|
|
124
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
125
|
+
return _error_json("Missing required field: prompt", 400, "bad_request")
|
|
126
|
+
|
|
127
|
+
requested_model = str(body.get("model") or "")
|
|
128
|
+
if requested_model and not any(h in requested_model.lower() for h in _IMAGE_MODEL_HINTS):
|
|
129
|
+
return _error_json(
|
|
130
|
+
f"Model '{requested_model}' is not a supported image model. flexgate maps image "
|
|
131
|
+
f"generation to MiniMax {MINIMAX_MODEL} (text-to-image only); request a gpt-image-* "
|
|
132
|
+
f"alias or '{MINIMAX_MODEL}'.",
|
|
133
|
+
501,
|
|
134
|
+
"model_not_supported",
|
|
135
|
+
)
|
|
136
|
+
logger.info(
|
|
137
|
+
"images/generations model=%s -> %s via %s",
|
|
138
|
+
requested_model or "(default)", MINIMAX_MODEL, provider.name,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
n = body.get("n", 1)
|
|
142
|
+
payload: dict = {
|
|
143
|
+
"model": MINIMAX_MODEL,
|
|
144
|
+
"prompt": prompt[:MINIMAX_MAX_PROMPT_CHARS],
|
|
145
|
+
"response_format": "base64",
|
|
146
|
+
"n": max(1, min(9, int(n))) if isinstance(n, (int, float)) else 1,
|
|
147
|
+
}
|
|
148
|
+
payload.update(_map_size(body.get("size")))
|
|
149
|
+
|
|
150
|
+
url = f"{_image_api_root(provider.base_url)}/v1/image_generation"
|
|
151
|
+
headers = {
|
|
152
|
+
"Authorization": f"Bearer {provider.api_key}",
|
|
153
|
+
"Content-Type": "application/json",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
resp = await client.post(url, headers=headers, json=payload)
|
|
158
|
+
except httpx.ConnectError as exc:
|
|
159
|
+
logger.error("Provider %s unreachable: %s", provider.name, exc)
|
|
160
|
+
return _error_json(f"Provider unreachable: {provider.name}", 502)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
data = resp.json()
|
|
164
|
+
except Exception:
|
|
165
|
+
return _error_json(resp.text[:500], 502)
|
|
166
|
+
|
|
167
|
+
base_resp = data.get("base_resp") or {}
|
|
168
|
+
status_code = base_resp.get("status_code", 0)
|
|
169
|
+
if resp.status_code != 200 or status_code != 0:
|
|
170
|
+
message = base_resp.get("status_msg") or f"HTTP {resp.status_code}"
|
|
171
|
+
logger.error("MiniMax image generation failed: %s", message)
|
|
172
|
+
return _error_json(f"MiniMax upstream: {message}", 502)
|
|
173
|
+
|
|
174
|
+
images = (data.get("data") or {}).get("image_base64") or []
|
|
175
|
+
if not images:
|
|
176
|
+
return _error_json("MiniMax returned no image_base64 payload", 502)
|
|
177
|
+
|
|
178
|
+
elapsed_ms = int((time.monotonic() - t0) * 1000)
|
|
179
|
+
logger.info("images/generations <- %s | %d image(s) | %dms", provider.name, len(images), elapsed_ms)
|
|
180
|
+
return JSONResponse({
|
|
181
|
+
"created": int(time.time()),
|
|
182
|
+
"data": [{"b64_json": img, "revised_prompt": None} for img in images],
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def images_edits(request: Request) -> JSONResponse:
|
|
187
|
+
return _error_json(
|
|
188
|
+
"MiniMax image-01 does not support image editing/inpainting; "
|
|
189
|
+
"only /v1/images/generations (text-to-image) is available through this gateway.",
|
|
190
|
+
501,
|
|
191
|
+
"edits_not_supported",
|
|
192
|
+
)
|
|
@@ -13,6 +13,8 @@ from starlette.responses import JSONResponse
|
|
|
13
13
|
from starlette.routing import Route
|
|
14
14
|
|
|
15
15
|
from flexgate.config import GatewayConfig, load_config
|
|
16
|
+
from flexgate.fallback import fallback
|
|
17
|
+
from flexgate.images import images_edits, images_generate
|
|
16
18
|
from flexgate.proxy import handle_request
|
|
17
19
|
from flexgate.router import NoRouteMatchError, resolve
|
|
18
20
|
|
|
@@ -65,7 +67,17 @@ def create_app(config: GatewayConfig, config_path: str | None = None) -> Starlet
|
|
|
65
67
|
timeout=httpx.Timeout(connect=10, read=3600, write=10, pool=10),
|
|
66
68
|
)
|
|
67
69
|
|
|
68
|
-
routes = [
|
|
70
|
+
routes = [
|
|
71
|
+
Route("/v1/messages", messages, methods=["POST"]),
|
|
72
|
+
Route("/v1/images/generations", images_generate, methods=["POST"]),
|
|
73
|
+
Route("/v1/images/edits", images_edits, methods=["POST"]),
|
|
74
|
+
# Catch-all last: structured, reasoned errors for anything unsupported.
|
|
75
|
+
Route(
|
|
76
|
+
"/{path:path}",
|
|
77
|
+
fallback,
|
|
78
|
+
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
79
|
+
),
|
|
80
|
+
]
|
|
69
81
|
|
|
70
82
|
async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:
|
|
71
83
|
app.state.config = config
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|