superlocalmemory 3.6.2 → 3.6.3
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.
- package/CHANGELOG.md +159 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/optimize/adapters/wrap.py +12 -3
- package/src/superlocalmemory/optimize/cache/manager.py +63 -9
- package/src/superlocalmemory/optimize/compress/router.py +7 -9
- package/src/superlocalmemory/optimize/proxy/_helpers.py +236 -1
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +187 -5
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +373 -14
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +329 -7
- package/src/superlocalmemory/optimize/proxy/server.py +10 -3
- package/src/superlocalmemory.egg-info/PKG-INFO +1 -1
- package/src/superlocalmemory.egg-info/SOURCES.txt +1 -1
|
@@ -1,21 +1,54 @@
|
|
|
1
|
-
"""gemini_surface.py — Gemini native and OpenAI-compat surfaces.
|
|
1
|
+
"""gemini_surface.py — Gemini native and OpenAI-compat surfaces.
|
|
2
|
+
|
|
3
|
+
BUG-FIX (v3.6.4): Both surfaces were pure pass-throughs with zero cache or
|
|
4
|
+
compress operations. Fixed:
|
|
5
|
+
|
|
6
|
+
- handle_gemini_native(): full cache check → compress → stream-and-cache
|
|
7
|
+
pipeline, matching the pattern in anthropic_surface.py. Streaming uses
|
|
8
|
+
_parse_gemini_sse_to_json / _gemini_sse_from_cached_json helpers that
|
|
9
|
+
understand Gemini's SSE format (no 'event:' prefix; final chunk carries
|
|
10
|
+
'finishReason'). Non-streaming response is cached verbatim.
|
|
11
|
+
|
|
12
|
+
- handle_gemini_openai_compat(): cache check → compress → forward pipeline
|
|
13
|
+
for POST requests. The /v1beta/openai/chat/completions endpoint speaks
|
|
14
|
+
standard OpenAI JSON, so _parse_openai_sse_to_json and the OpenAI SSE
|
|
15
|
+
replay helper are reused after importing from openai_surface.
|
|
16
|
+
|
|
17
|
+
Gemini CLI and AGY/Antigravity (when using Google models) both hit the native
|
|
18
|
+
surface. Codex/Antigravity in OpenAI-compat mode hit the compat surface.
|
|
19
|
+
|
|
20
|
+
NOTE ON GOOGLE GENAI ENV VARS (v3.6.4):
|
|
21
|
+
Set GOOGLE_GENAI_BASE_URL=http://127.0.0.1:8765 for google-genai SDK >=0.6
|
|
22
|
+
Set GOOGLE_API_BASE=http://127.0.0.1:8765 for older google-generativeai SDK
|
|
23
|
+
Gemini CLI (≤2025-06-18): GEMINI_API_HOST=http://127.0.0.1:8765
|
|
24
|
+
AGY (Antigravity): GOOGLE_GENAI_BASE_URL=http://127.0.0.1:8765
|
|
25
|
+
"""
|
|
2
26
|
|
|
3
27
|
from __future__ import annotations
|
|
4
28
|
|
|
29
|
+
import json
|
|
5
30
|
import logging
|
|
6
31
|
import re
|
|
7
32
|
import urllib.parse
|
|
8
33
|
|
|
9
34
|
from fastapi.requests import Request
|
|
10
|
-
from fastapi.responses import Response
|
|
35
|
+
from fastapi.responses import Response, StreamingResponse
|
|
11
36
|
|
|
12
37
|
from superlocalmemory.optimize.proxy._helpers import (
|
|
13
38
|
_GEMINI_NATIVE_FORWARD_HEADERS,
|
|
14
39
|
_GEMINI_OPENAI_COMPAT_FORWARD_HEADERS,
|
|
40
|
+
_body_has_tools,
|
|
15
41
|
_fail_open_forward,
|
|
16
42
|
_filter_response_headers,
|
|
43
|
+
_redact_headers,
|
|
44
|
+
_safe_cache_check,
|
|
45
|
+
_safe_cache_hit_callbacks,
|
|
46
|
+
_safe_cache_store,
|
|
47
|
+
_safe_compress,
|
|
48
|
+
_stream_and_cache_forward,
|
|
17
49
|
_stream_forward,
|
|
18
50
|
)
|
|
51
|
+
from superlocalmemory.optimize.proxy.lifecycle import ProviderResponse, ProxyRequest
|
|
19
52
|
|
|
20
53
|
logger = logging.getLogger("slm.optimize.proxy.gemini")
|
|
21
54
|
|
|
@@ -35,11 +68,149 @@ def _validate_gemini_path(model_and_method: str) -> bool:
|
|
|
35
68
|
return bool(_GEMINI_PATH_RE.match(model_and_method))
|
|
36
69
|
|
|
37
70
|
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Gemini native SSE helpers
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def _parse_gemini_sse_to_json(sse_bytes: bytes) -> bytes | None:
|
|
76
|
+
"""Assemble a Gemini SSE stream into a single generateContent JSON response.
|
|
77
|
+
|
|
78
|
+
Gemini streaming format (no 'event:' prefix — just 'data:' lines):
|
|
79
|
+
data: {"candidates":[{"content":{"parts":[{"text":"Hello"}],"role":"model"},"index":0}],"usageMetadata":{...}}
|
|
80
|
+
data: {"candidates":[{"content":{"parts":[{"text":" world"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{...}}
|
|
81
|
+
|
|
82
|
+
Completeness gate: returns None if no candidate ever emits a non-null
|
|
83
|
+
finishReason — ensures incomplete streams are not cached. This is the
|
|
84
|
+
Gemini equivalent of checking 'message_stop' (Anthropic) or '[DONE]' (OpenAI).
|
|
85
|
+
"""
|
|
86
|
+
parts_by_idx: dict[int, list[str]] = {}
|
|
87
|
+
finish_reasons: dict[int, str] = {}
|
|
88
|
+
usage_metadata: dict = {}
|
|
89
|
+
model_version = ""
|
|
90
|
+
|
|
91
|
+
for raw_line in sse_bytes.decode("utf-8", errors="replace").split("\n"):
|
|
92
|
+
line = raw_line.rstrip("\r")
|
|
93
|
+
if not line.startswith("data: "):
|
|
94
|
+
continue
|
|
95
|
+
data_str = line[6:].strip()
|
|
96
|
+
if not data_str or data_str == "[DONE]":
|
|
97
|
+
continue
|
|
98
|
+
try:
|
|
99
|
+
chunk = json.loads(data_str)
|
|
100
|
+
except json.JSONDecodeError:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if "usageMetadata" in chunk:
|
|
104
|
+
usage_metadata = chunk["usageMetadata"]
|
|
105
|
+
if "modelVersion" in chunk:
|
|
106
|
+
model_version = chunk["modelVersion"]
|
|
107
|
+
|
|
108
|
+
for candidate in chunk.get("candidates", []):
|
|
109
|
+
idx = candidate.get("index", 0)
|
|
110
|
+
for part in candidate.get("content", {}).get("parts", []):
|
|
111
|
+
text = part.get("text")
|
|
112
|
+
if text is not None:
|
|
113
|
+
parts_by_idx.setdefault(idx, []).append(text)
|
|
114
|
+
fr = candidate.get("finishReason")
|
|
115
|
+
# Gemini emits finishReason as a string on the final chunk.
|
|
116
|
+
# Intermediate chunks omit it or include None / "null".
|
|
117
|
+
if fr and fr not in (None, "null", "FINISH_REASON_UNSPECIFIED"):
|
|
118
|
+
finish_reasons[idx] = fr
|
|
119
|
+
|
|
120
|
+
# Completeness gate
|
|
121
|
+
if not finish_reasons:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
candidates = []
|
|
125
|
+
for idx in sorted(parts_by_idx.keys()):
|
|
126
|
+
candidates.append({
|
|
127
|
+
"content": {
|
|
128
|
+
"parts": [{"text": "".join(parts_by_idx.get(idx, []))}],
|
|
129
|
+
"role": "model",
|
|
130
|
+
},
|
|
131
|
+
"finishReason": finish_reasons.get(idx, "STOP"),
|
|
132
|
+
"index": idx,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if not candidates:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
result: dict = {"candidates": candidates, "usageMetadata": usage_metadata}
|
|
139
|
+
if model_version:
|
|
140
|
+
result["modelVersion"] = model_version
|
|
141
|
+
|
|
142
|
+
return json.dumps(result, separators=(",", ":")).encode("utf-8")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _gemini_sse_from_cached_json(cached_bytes: bytes) -> "StreamingResponse | None":
|
|
146
|
+
"""Replay a stored generateContent JSON as a Gemini SSE stream.
|
|
147
|
+
|
|
148
|
+
Emits the text in 100-char chunks followed by a final chunk containing
|
|
149
|
+
finishReason + usageMetadata, matching what the real API would send.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
resp = json.loads(cached_bytes)
|
|
153
|
+
except (json.JSONDecodeError, ValueError):
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
if "candidates" not in resp:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
async def _generate():
|
|
160
|
+
for candidate in resp.get("candidates", []):
|
|
161
|
+
idx = candidate.get("index", 0)
|
|
162
|
+
text = "".join(
|
|
163
|
+
p.get("text", "")
|
|
164
|
+
for p in candidate.get("content", {}).get("parts", [])
|
|
165
|
+
)
|
|
166
|
+
finish_reason = candidate.get("finishReason", "STOP")
|
|
167
|
+
|
|
168
|
+
# Emit text in chunks
|
|
169
|
+
chunk_size = 100
|
|
170
|
+
for start in range(0, max(len(text), 1), chunk_size):
|
|
171
|
+
piece = text[start: start + chunk_size]
|
|
172
|
+
chunk = {
|
|
173
|
+
"candidates": [{
|
|
174
|
+
"content": {"parts": [{"text": piece}], "role": "model"},
|
|
175
|
+
"index": idx,
|
|
176
|
+
}]
|
|
177
|
+
}
|
|
178
|
+
yield f"data: {json.dumps(chunk)}\n\n".encode()
|
|
179
|
+
|
|
180
|
+
# Final chunk with finishReason and usageMetadata
|
|
181
|
+
final: dict = {
|
|
182
|
+
"candidates": [{
|
|
183
|
+
"content": {"parts": [{"text": ""}], "role": "model"},
|
|
184
|
+
"finishReason": finish_reason,
|
|
185
|
+
"index": idx,
|
|
186
|
+
}],
|
|
187
|
+
"usageMetadata": resp.get("usageMetadata", {}),
|
|
188
|
+
}
|
|
189
|
+
if "modelVersion" in resp:
|
|
190
|
+
final["modelVersion"] = resp["modelVersion"]
|
|
191
|
+
yield f"data: {json.dumps(final)}\n\n".encode()
|
|
192
|
+
|
|
193
|
+
return StreamingResponse(_generate(), media_type="text/event-stream")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Route handlers
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
38
200
|
async def handle_gemini_native(
|
|
39
201
|
proxy: object,
|
|
40
202
|
request: Request,
|
|
41
203
|
model_and_method: str,
|
|
42
204
|
) -> Response:
|
|
205
|
+
"""Handle /v1beta/models/{model}:{method} — native Gemini API.
|
|
206
|
+
|
|
207
|
+
BUG-FIX (v3.6.4): Was a pure pass-through. Now applies cache check,
|
|
208
|
+
optional compression, and post-stream cache store for streaming responses.
|
|
209
|
+
Non-streaming responses are cached verbatim.
|
|
210
|
+
|
|
211
|
+
Streaming: _parse_gemini_sse_to_json validates completeness via finishReason.
|
|
212
|
+
Non-streaming: response JSON stored directly.
|
|
213
|
+
"""
|
|
43
214
|
request_id = await proxy.next_request_id()
|
|
44
215
|
|
|
45
216
|
if not _validate_gemini_path(model_and_method):
|
|
@@ -58,62 +229,250 @@ async def handle_gemini_native(
|
|
|
58
229
|
|
|
59
230
|
try:
|
|
60
231
|
body_bytes = await request.body()
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
body = json.loads(body_bytes) if body_bytes else {}
|
|
235
|
+
except json.JSONDecodeError:
|
|
236
|
+
body = {}
|
|
237
|
+
|
|
238
|
+
stream = "streamGenerateContent" in model_and_method
|
|
239
|
+
has_tools = _body_has_tools(body)
|
|
240
|
+
|
|
241
|
+
ctx = ProxyRequest(
|
|
242
|
+
provider="gemini",
|
|
243
|
+
method="POST",
|
|
244
|
+
path=f"/v1beta/{model_and_method}",
|
|
245
|
+
headers=_redact_headers(dict(request.headers)),
|
|
246
|
+
body=body,
|
|
247
|
+
body_bytes=body_bytes,
|
|
248
|
+
request_id=request_id,
|
|
249
|
+
stream=stream,
|
|
250
|
+
has_tools=has_tools,
|
|
251
|
+
)
|
|
252
|
+
|
|
61
253
|
fwd_headers = {
|
|
62
254
|
k: v for k, v in request.headers.items()
|
|
63
255
|
if k.lower() in _GEMINI_NATIVE_FORWARD_HEADERS
|
|
64
256
|
}
|
|
65
|
-
fwd_headers["content-length"] = str(len(body_bytes))
|
|
66
257
|
|
|
67
|
-
stream = "streamGenerateContent" in model_and_method
|
|
68
258
|
if stream:
|
|
259
|
+
# ── 1. Cache check ──────────────────────────────────────────────
|
|
260
|
+
if proxy.hooks.cache:
|
|
261
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
262
|
+
if cache_result and cache_result.hit and cache_result.data:
|
|
263
|
+
logger.debug(
|
|
264
|
+
"[%s] Gemini native streaming cache HIT key=%s",
|
|
265
|
+
request_id, cache_result.cache_key,
|
|
266
|
+
)
|
|
267
|
+
await _safe_cache_hit_callbacks(
|
|
268
|
+
proxy.hooks, ctx, cache_result.data, tokens_saved=0
|
|
269
|
+
)
|
|
270
|
+
sse_resp = _gemini_sse_from_cached_json(cache_result.data)
|
|
271
|
+
if sse_resp is not None:
|
|
272
|
+
return sse_resp
|
|
273
|
+
|
|
274
|
+
# ── 2. Compression ──────────────────────────────────────────────
|
|
275
|
+
outbound_bytes = body_bytes
|
|
276
|
+
if proxy.hooks.compress:
|
|
277
|
+
compress_result = await _safe_compress(proxy.hooks, ctx)
|
|
278
|
+
if compress_result.body_bytes != body_bytes:
|
|
279
|
+
outbound_bytes = compress_result.body_bytes
|
|
280
|
+
|
|
281
|
+
fwd_headers["content-length"] = str(len(outbound_bytes))
|
|
282
|
+
|
|
283
|
+
# ── 3. Build upstream streaming URL ─────────────────────────────
|
|
69
284
|
allowed = {
|
|
70
285
|
k: v for k, v in request.query_params.items()
|
|
71
286
|
if k.lower() in _GEMINI_ALLOWED_QUERY_PARAMS
|
|
72
287
|
}
|
|
73
288
|
allowed["alt"] = "sse"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
289
|
+
upstream_stream_url = f"{upstream_url}?{urllib.parse.urlencode(allowed)}"
|
|
290
|
+
|
|
291
|
+
# ── 4. Stream + accumulate → cache store ─────────────────────────
|
|
292
|
+
store_callback = None
|
|
293
|
+
if proxy.hooks.cache:
|
|
294
|
+
_hooks = proxy.hooks
|
|
295
|
+
_ctx = ctx
|
|
296
|
+
|
|
297
|
+
async def _store_from_gemini_sse(sse_bytes: bytes) -> None:
|
|
298
|
+
parsed = _parse_gemini_sse_to_json(sse_bytes)
|
|
299
|
+
if parsed is None:
|
|
300
|
+
return # incomplete stream — skip
|
|
301
|
+
prov = ProviderResponse(
|
|
302
|
+
modified=False, body={}, body_bytes=parsed,
|
|
303
|
+
tokens_before=0, tokens_after=0, strategy="none",
|
|
304
|
+
)
|
|
305
|
+
await _safe_cache_store(_hooks, _ctx, prov)
|
|
306
|
+
|
|
307
|
+
store_callback = _store_from_gemini_sse
|
|
308
|
+
|
|
309
|
+
return await _stream_and_cache_forward(
|
|
310
|
+
proxy, request_id, fwd_headers, outbound_bytes, upstream_stream_url,
|
|
311
|
+
on_complete=store_callback,
|
|
77
312
|
)
|
|
78
313
|
|
|
314
|
+
# ── Non-streaming path ──────────────────────────────────────────────
|
|
315
|
+
cache_result = None
|
|
316
|
+
if proxy.hooks.cache:
|
|
317
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
318
|
+
if cache_result.hit and cache_result.data:
|
|
319
|
+
await _safe_cache_hit_callbacks(
|
|
320
|
+
proxy.hooks, ctx, cache_result.data, tokens_saved=0
|
|
321
|
+
)
|
|
322
|
+
return Response(
|
|
323
|
+
content=cache_result.data,
|
|
324
|
+
status_code=200,
|
|
325
|
+
media_type="application/json",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
outbound_bytes = body_bytes
|
|
329
|
+
if proxy.hooks.compress:
|
|
330
|
+
compress_result = await _safe_compress(proxy.hooks, ctx)
|
|
331
|
+
if compress_result.body_bytes != body_bytes:
|
|
332
|
+
outbound_bytes = compress_result.body_bytes
|
|
333
|
+
|
|
334
|
+
fwd_headers["content-length"] = str(len(outbound_bytes))
|
|
335
|
+
|
|
79
336
|
upstream_resp = await proxy.http_client.post(
|
|
80
|
-
upstream_url, content=
|
|
337
|
+
upstream_url, content=outbound_bytes, headers=fwd_headers,
|
|
81
338
|
)
|
|
339
|
+
resp_bytes = upstream_resp.content
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
upstream_resp.status_code == 200
|
|
343
|
+
and proxy.hooks.cache
|
|
344
|
+
and cache_result is not None
|
|
345
|
+
and cache_result.cache_key
|
|
346
|
+
):
|
|
347
|
+
prov_resp = ProviderResponse(
|
|
348
|
+
modified=False, body={}, body_bytes=resp_bytes,
|
|
349
|
+
tokens_before=0, tokens_after=0, strategy="none",
|
|
350
|
+
)
|
|
351
|
+
await _safe_cache_store(proxy.hooks, ctx, prov_resp)
|
|
352
|
+
|
|
82
353
|
return Response(
|
|
83
|
-
content=
|
|
354
|
+
content=resp_bytes,
|
|
84
355
|
status_code=upstream_resp.status_code,
|
|
85
356
|
media_type="application/json",
|
|
86
357
|
headers=_filter_response_headers(dict(upstream_resp.headers)),
|
|
87
358
|
)
|
|
359
|
+
|
|
88
360
|
except Exception as exc:
|
|
89
361
|
logger.error("[%s] handle_gemini_native exc=%r — fail-open", request_id, exc)
|
|
90
362
|
return await _fail_open_forward(proxy, request, upstream_url)
|
|
91
363
|
|
|
92
364
|
|
|
93
365
|
async def handle_gemini_openai_compat(proxy: object, request: Request) -> Response:
|
|
366
|
+
"""Handle /v1beta/openai/chat/completions and /v1beta/openai/models.
|
|
367
|
+
|
|
368
|
+
BUG-FIX (v3.6.4): POST requests (chat completions) now go through the
|
|
369
|
+
cache + compress pipeline. The /v1beta/openai surface accepts standard
|
|
370
|
+
OpenAI JSON, so we reuse OpenAI format parsing from openai_surface.py
|
|
371
|
+
for streaming SSE detection and replay.
|
|
372
|
+
|
|
373
|
+
GET requests (models list) are pass-through — no caching needed.
|
|
374
|
+
"""
|
|
94
375
|
local_path = request.url.path
|
|
95
376
|
upstream_url = f"{_GEMINI_UPSTREAM_BASE}{local_path}"
|
|
96
377
|
request_id = await proxy.next_request_id()
|
|
378
|
+
|
|
97
379
|
try:
|
|
98
380
|
body_bytes = await request.body()
|
|
381
|
+
|
|
382
|
+
# GET (models) — pass-through
|
|
383
|
+
if request.method == "GET" or not body_bytes:
|
|
384
|
+
fwd_headers = {
|
|
385
|
+
k: v for k, v in request.headers.items()
|
|
386
|
+
if k.lower() in _GEMINI_OPENAI_COMPAT_FORWARD_HEADERS
|
|
387
|
+
}
|
|
388
|
+
upstream_resp = await proxy.http_client.request(
|
|
389
|
+
method=request.method,
|
|
390
|
+
url=upstream_url,
|
|
391
|
+
content=body_bytes if body_bytes else None,
|
|
392
|
+
headers=fwd_headers,
|
|
393
|
+
)
|
|
394
|
+
return Response(
|
|
395
|
+
content=upstream_resp.content,
|
|
396
|
+
status_code=upstream_resp.status_code,
|
|
397
|
+
media_type="application/json",
|
|
398
|
+
headers=_filter_response_headers(dict(upstream_resp.headers)),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# POST (chat completions) — cache + compress
|
|
402
|
+
try:
|
|
403
|
+
body = json.loads(body_bytes)
|
|
404
|
+
except json.JSONDecodeError:
|
|
405
|
+
return await _fail_open_forward(proxy, request, upstream_url)
|
|
406
|
+
|
|
407
|
+
has_tools = _body_has_tools(body)
|
|
408
|
+
stream = bool(body.get("stream", False))
|
|
409
|
+
|
|
410
|
+
ctx = ProxyRequest(
|
|
411
|
+
provider="gemini-openai-compat",
|
|
412
|
+
method="POST",
|
|
413
|
+
path=local_path,
|
|
414
|
+
headers=_redact_headers(dict(request.headers)),
|
|
415
|
+
body=body,
|
|
416
|
+
body_bytes=body_bytes,
|
|
417
|
+
request_id=request_id,
|
|
418
|
+
stream=stream,
|
|
419
|
+
has_tools=has_tools,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Cache check
|
|
423
|
+
cache_result = None
|
|
424
|
+
if proxy.hooks.cache:
|
|
425
|
+
cache_result = await _safe_cache_check(proxy.hooks, ctx)
|
|
426
|
+
if cache_result.hit and cache_result.data:
|
|
427
|
+
await _safe_cache_hit_callbacks(
|
|
428
|
+
proxy.hooks, ctx, cache_result.data, tokens_saved=0
|
|
429
|
+
)
|
|
430
|
+
return Response(
|
|
431
|
+
content=cache_result.data,
|
|
432
|
+
status_code=200,
|
|
433
|
+
media_type="application/json",
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Compress
|
|
437
|
+
outbound_bytes = body_bytes
|
|
438
|
+
if proxy.hooks.compress:
|
|
439
|
+
compress_result = await _safe_compress(proxy.hooks, ctx)
|
|
440
|
+
if compress_result.body_bytes != body_bytes:
|
|
441
|
+
outbound_bytes = compress_result.body_bytes
|
|
442
|
+
|
|
99
443
|
fwd_headers = {
|
|
100
444
|
k: v for k, v in request.headers.items()
|
|
101
445
|
if k.lower() in _GEMINI_OPENAI_COMPAT_FORWARD_HEADERS
|
|
102
446
|
}
|
|
103
|
-
|
|
104
|
-
|
|
447
|
+
fwd_headers["content-length"] = str(len(outbound_bytes))
|
|
448
|
+
|
|
105
449
|
upstream_resp = await proxy.http_client.request(
|
|
106
|
-
method=
|
|
450
|
+
method="POST",
|
|
107
451
|
url=upstream_url,
|
|
108
|
-
content=
|
|
452
|
+
content=outbound_bytes,
|
|
109
453
|
headers=fwd_headers,
|
|
110
454
|
)
|
|
455
|
+
resp_bytes = upstream_resp.content
|
|
456
|
+
|
|
457
|
+
if (
|
|
458
|
+
upstream_resp.status_code == 200
|
|
459
|
+
and proxy.hooks.cache
|
|
460
|
+
and cache_result is not None
|
|
461
|
+
and cache_result.cache_key
|
|
462
|
+
):
|
|
463
|
+
prov_resp = ProviderResponse(
|
|
464
|
+
modified=False, body={}, body_bytes=resp_bytes,
|
|
465
|
+
tokens_before=0, tokens_after=0, strategy="none",
|
|
466
|
+
)
|
|
467
|
+
await _safe_cache_store(proxy.hooks, ctx, prov_resp)
|
|
468
|
+
|
|
111
469
|
return Response(
|
|
112
|
-
content=
|
|
470
|
+
content=resp_bytes,
|
|
113
471
|
status_code=upstream_resp.status_code,
|
|
114
472
|
media_type="application/json",
|
|
115
473
|
headers=_filter_response_headers(dict(upstream_resp.headers)),
|
|
116
474
|
)
|
|
475
|
+
|
|
117
476
|
except Exception as exc:
|
|
118
477
|
logger.error(
|
|
119
478
|
"[%s] handle_gemini_openai_compat exc=%r — fail-open", request_id, exc
|