mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-10 14:27:00 -04:00
fix(proxy): route Codex OAuth image requests (#1215)
## Description Closes #1189. After a recent Codex Desktop update, its built-in image generation started going through Codex's image client, which POSTs to `images/generations` and `images/edits` relative to the configured provider base URL. In Headroom Proxy mode Codex is pointed at Headroom's `/v1` surface, so those land as `/v1/images/generations` and `/v1/images/edits`. Headroom already had `/v1/images/generations`, but it only ever hit the OpenAI API-key passthrough, and there was no `/v1/images/edits` route at all. So under ChatGPT/Codex OAuth the image calls had nowhere correct to go. This change routes OAuth image requests to `https://chatgpt.com/backend-api/codex/images/{generations,edits}` and leaves the API-key passthrough untouched. Latest upstream re-check: current `openai/codex` main is now `aaf737f`, and the relevant `ImagesClient`/provider-base source still resolves image generation and edit requests to `https://chatgpt.com/backend-api/codex/images/{generations,edits}` under ChatGPT-family auth. One issue-thread datapoint reports Codex Desktop `0.142.0-alpha.6` on macOS generating images successfully via the `/v1/responses` WebSocket path. The requester has now checked this against the latest timestamped Codex update, so this is ready for maintainer review with the remaining full-suite caveat documented below. **Reproduction / test contract** - Reporter's setup: Codex Desktop 0.142.0-alpha.1 on Windows 10, Headroom v0.26.0 Proxy mode, OAuth auth. `/v1/models` and `/v1/responses` work; built-in image generation fails. - Why the route was confirmed from source: the reporter's sanitized logs only show `/v1/models` and `/v1/responses`, so I traced the rest in current Codex source — image generation/edit go through `ImagesClient` as `images/generations` and `images/edits` against the provider base URL. - Regression test: `test_openai_image_routes_use_codex_backend_under_chatgpt_auth` asserts both OAuth image routes now resolve to the ChatGPT Codex image backend. Before this patch, `/v1/images/generations` used the OpenAI API-key target under OAuth and `/v1/images/edits` didn't exist. - Hardening tests: additional regressions cover stale upstream compression headers, OpenAI API-key fall-through for edits, and multipart edit body byte-preservation. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Route ChatGPT/Codex OAuth `/v1/images/generations` and `/v1/images/edits` to the ChatGPT Codex image backend. - Strip internal `x-headroom-*`, `Host`, and `Accept-Encoding` headers before forwarding Codex OAuth image requests upstream. - Strip stale `Content-Encoding` and `Content-Length` headers from image responses because httpx has already decoded the body. - Keep API-key image requests on the existing OpenAI passthrough. - Add regression coverage for both OAuth image routes, OpenAI image-edit passthrough, compressed-response header handling, and multipart edit bodies. - Add a `CHANGELOG.md` entry. ## Testing - [ ] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ uv run pytest tests/test_provider_proxy_routes.py tests/test_proxy_codex_route_aliases.py tests/test_openai_codex_routing.py -q 42 passed, 1 warning in 7.63s $ UV_PROJECT_ENVIRONMENT=.venv-py312 uv run --python 3.12 --extra dev --extra proxy pytest tests/test_provider_proxy_routes.py tests/test_proxy_codex_route_aliases.py tests/test_openai_codex_routing.py -q 42 passed, 1 warning in 8.54s $ uv run ruff check . All checks passed! $ uv run ruff format --check . 895 files already formatted $ uv run mypy headroom headroom/proxy/server.py:1152: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] headroom/proxy/server.py:1222: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] headroom/proxy/server.py:1226: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] Success: no issues found in 380 source files ``` Earlier full-suite attempt in this branch/environment, before the F1-F8 hardening pass (not rerun after hardening because the failures were unrelated to this route and expensive): ```text $ UV_PROJECT_ENVIRONMENT=.venv-py312 uv run --python 3.12 --extra dev --extra proxy pytest 6 failed, 6499 passed, 486 skipped, 5807 warnings in 219.53s ``` All 6 failures are outside the touched routes and unrelated to this change: - `tests/test_corrupt_golden_bytes_recovery.py` — 3 log-capture assertions - `tests/test_forwarded_headers.py::test_non_allowlisted_peer_ignores_forwarded_and_logs` — 1 log-capture assertion - `tests/test_image_compression.py::TestOnnxRouter::test_full_classify_with_image` — `ModuleNotFoundError: No module named 'PIL'` (only `dev,proxy` extras installed) - `tests/test_transforms/test_kompress_compressor.py::TestKompressBackendSelection::test_unrecognized_backend_warns_and_falls_back_to_auto` — 1 warning-capture assertion On Python 3.14.4, plain `uv run pytest` can't even collect: the project's dependency marker intentionally excludes `litellm` on 3.14, while `tests/test_memory_eval.py` imports the eval runner at collection time. ## Real Behavior Proof - **Environment:** macOS (Darwin arm64). Python 3.14.4 via uv for the default project env; Python 3.12.13 via `UV_PROJECT_ENVIRONMENT=.venv-py312` for the broader suite. Headroom FastAPI proxy route test harness. - **Exact command / steps:** read the reporter's sanitized issue logs; traced current Codex image-generation source; ran the focused Codex/proxy route tests on 3.14 and 3.12; ran lint, format check, and mypy; attempted the full 3.12 suite (output above). - **After-fix evidence + observed result:** the regression test captures the OAuth image requests and confirms they forward to `https://chatgpt.com/backend-api/codex/images/generations` and `.../images/edits` — auth and account headers preserved, internal/host/accept encoding headers stripped, query string carried through, JSON and multipart request bodies forwarded byte-for-byte, and stale upstream response compression headers removed. API-key image generation still uses `images/generations`, and image edits now have the matching `images/edits` passthrough. - **Source evidence:** Re-verified against current `openai/codex` HEAD `aaf737f`. `ImagesClient` still sends relative paths `images/generations` and `images/edits`; `Provider::url_for_path()` appends those to the active provider base; ChatGPT-family auth modes default that base to `CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"`. Therefore the source-resolved upstream paths are `/backend-api/codex/images/generations` and `/backend-api/codex/images/edits`, not `/backend-api/images/...`. - **Latest-build caveat:** an issue-thread report says Codex Desktop `0.142.0-alpha.6` on macOS uses `/v1/responses` WebSocket image generation and works through the proxy. That may mean the original Windows `0.142.0-alpha.1` regression is fixed client-side in newer desktop builds, even though the source image endpoint route remains valid and now covered here. The requester has checked this against the latest timestamped Codex update before moving the PR out of draft. - **Not fully tested:** a fully green `uv run pytest` remains unavailable in this local environment for the unrelated failures listed above. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes No dependency or version changes. The remaining caveat is that the full local suite isn't green in this environment for the unrelated failures listed above. Happy to follow up with additional runtime logs or to re-run the suite in a maintainer's preferred dev container if that's the cleaner path. --------- Co-authored-by: Johnson <johnsond@brightops.com>
This commit is contained in:
parent
08fb845fe3
commit
381d771e46
3 changed files with 322 additions and 0 deletions
|
|
@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* **proxy:** route Codex OAuth image generation and edit requests through the ChatGPT Codex image backend, while preserving OpenAI API-key image passthrough ([#1215](https://github.com/chopratejas/headroom/pull/1215)).
|
||||
* **proxy:** enable SSO credential resolution in the native Bedrock route via the `aws-config` `sso` feature flag, making the credential chain match what `docs/bedrock.md` already documented ([#999](https://github.com/chopratejas/headroom/pull/999)).
|
||||
* **proxy:** route native Bedrock `/model/{id}/converse` requests to the upstream Converse endpoint instead of the hard-coded `/invoke` action — the non-streaming handler now resolves the action from the inbound path, matching the streaming handler ([#999](https://github.com/chopratejas/headroom/pull/999)).
|
||||
* **ccr:** make retrieval store TTL configurable with `HEADROOM_CCR_TTL_SECONDS`, expose the effective TTL in `/v1/retrieve/stats`, and distinguish expired retrievals from missing hashes.
|
||||
|
|
|
|||
|
|
@ -411,6 +411,63 @@ async def _handle_chatgpt_model_metadata(
|
|||
return Response(content=str(exc), status_code=502)
|
||||
|
||||
|
||||
async def _handle_chatgpt_codex_images(
|
||||
proxy: Any,
|
||||
request: Request,
|
||||
sub_path: str,
|
||||
) -> Response | None:
|
||||
"""Forward Codex OAuth image requests to ChatGPT's Codex image backend."""
|
||||
from headroom.proxy.helpers import _strip_internal_headers
|
||||
|
||||
headers = dict(request.headers.items())
|
||||
headers.pop("host", None)
|
||||
headers.pop("accept-encoding", None)
|
||||
headers = _strip_internal_headers(headers)
|
||||
headers, is_chatgpt_auth = _resolve_codex_routing_headers(headers)
|
||||
if not is_chatgpt_auth:
|
||||
return None
|
||||
|
||||
url = f"https://chatgpt.com/backend-api/codex/images/{sub_path}"
|
||||
if request.url.query:
|
||||
url = f"{url}?{request.url.query}"
|
||||
|
||||
body = await request.body()
|
||||
try:
|
||||
client = getattr(proxy, "http_client_h1", None) or getattr(proxy, "http_client", None)
|
||||
if client is None:
|
||||
raise RuntimeError("No HTTP client configured for Codex image forwarding")
|
||||
# OAuth image traffic intentionally skips request-outcome telemetry; no token usage is available here.
|
||||
resp = await client.request(
|
||||
request.method,
|
||||
url,
|
||||
headers=headers,
|
||||
content=body,
|
||||
timeout=120.0,
|
||||
)
|
||||
response_headers = dict(resp.headers)
|
||||
response_headers.pop("content-encoding", None)
|
||||
response_headers.pop("content-length", None)
|
||||
return Response(
|
||||
content=resp.content,
|
||||
status_code=resp.status_code,
|
||||
headers=response_headers,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error("Passthrough /v1/images/%s failed: %s", sub_path, exc)
|
||||
return Response(
|
||||
content=json.dumps(
|
||||
{
|
||||
"error": {
|
||||
"type": "upstream_error",
|
||||
"message": "Failed to forward Codex image request",
|
||||
}
|
||||
}
|
||||
),
|
||||
status_code=502,
|
||||
media_type="application/json",
|
||||
)
|
||||
|
||||
|
||||
def register_provider_routes(app: FastAPI, proxy: Any) -> None:
|
||||
"""Register provider-specific proxy endpoints."""
|
||||
|
||||
|
|
@ -753,6 +810,14 @@ def register_provider_routes(app: FastAPI, proxy: Any) -> None:
|
|||
|
||||
@app.post("/v1/images/generations")
|
||||
async def openai_images_generations(request: Request):
|
||||
chatgpt_response = await _handle_chatgpt_codex_images(
|
||||
proxy,
|
||||
request,
|
||||
"generations",
|
||||
)
|
||||
if chatgpt_response is not None:
|
||||
return chatgpt_response
|
||||
|
||||
return await proxy.handle_passthrough(
|
||||
request,
|
||||
_api_target(proxy, "openai"),
|
||||
|
|
@ -760,6 +825,23 @@ def register_provider_routes(app: FastAPI, proxy: Any) -> None:
|
|||
"openai",
|
||||
)
|
||||
|
||||
@app.post("/v1/images/edits")
|
||||
async def openai_images_edits(request: Request):
|
||||
chatgpt_response = await _handle_chatgpt_codex_images(
|
||||
proxy,
|
||||
request,
|
||||
"edits",
|
||||
)
|
||||
if chatgpt_response is not None:
|
||||
return chatgpt_response
|
||||
|
||||
return await proxy.handle_passthrough(
|
||||
request,
|
||||
_api_target(proxy, "openai"),
|
||||
"images/edits",
|
||||
"openai",
|
||||
)
|
||||
|
||||
@app.post("/v1/audio/transcriptions")
|
||||
async def openai_audio_transcriptions(request: Request):
|
||||
return await proxy.handle_passthrough(
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ def test_provider_passthrough_routes_forward_expected_targets(monkeypatch) -> No
|
|||
assert client.post("/v1/embeddings").json()["provider"] == "openai"
|
||||
assert client.post("/v1/moderations").json()["sub_path"] == "moderations"
|
||||
assert client.post("/v1/images/generations").json()["sub_path"] == "images/generations"
|
||||
assert client.post("/v1/images/edits").json()["sub_path"] == "images/edits"
|
||||
assert client.post("/v1/audio/transcriptions").json()["sub_path"] == "audio/transcriptions"
|
||||
assert client.post("/v1/audio/speech").json()["sub_path"] == "audio/speech"
|
||||
assert client.get("/v1beta/models").json()["provider"] == "gemini"
|
||||
|
|
@ -464,6 +465,244 @@ def test_openai_response_subpath_aliases_and_chatgpt_auth_use_expected_targets(m
|
|||
]
|
||||
|
||||
|
||||
def test_openai_image_routes_use_codex_backend_under_chatgpt_auth(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"headroom.providers.proxy_routes._resolve_codex_routing_headers",
|
||||
lambda headers: ({**headers, "ChatGPT-Account-ID": "acct_123"}, True),
|
||||
)
|
||||
|
||||
class FakeAsyncClient:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, str, dict[str, str], bytes]] = []
|
||||
|
||||
async def request(self, method, url, **kwargs): # type: ignore[no-untyped-def]
|
||||
self.calls.append(
|
||||
(
|
||||
method,
|
||||
url,
|
||||
dict(kwargs.get("headers", {})),
|
||||
kwargs.get("content", b""),
|
||||
)
|
||||
)
|
||||
return httpx.Response(200, json={"url": url})
|
||||
|
||||
async def aclose(self) -> None:
|
||||
return None
|
||||
|
||||
with TestClient(_app()) as client:
|
||||
fake = FakeAsyncClient()
|
||||
client.app.state.proxy.http_client = fake
|
||||
client.app.state.proxy.http_client_h1 = fake
|
||||
|
||||
generate_response = client.post(
|
||||
"/v1/images/generations?client_version=0.142.0",
|
||||
headers={
|
||||
"Authorization": "Bearer oauth-token",
|
||||
"Accept-Encoding": "gzip",
|
||||
"X-Headroom-Bypass": "1",
|
||||
},
|
||||
json={"model": "gpt-image-2", "prompt": "a route probe"},
|
||||
)
|
||||
edit_response = client.post(
|
||||
"/v1/images/edits",
|
||||
headers={"Authorization": "Bearer oauth-token"},
|
||||
json={"model": "gpt-image-2", "prompt": "edit route probe", "images": []},
|
||||
)
|
||||
|
||||
assert generate_response.status_code == 200
|
||||
assert edit_response.status_code == 200
|
||||
assert len(fake.calls) == 2
|
||||
|
||||
generate_method, generate_url, generate_headers, generate_body = fake.calls[0]
|
||||
assert generate_method == "POST"
|
||||
assert (
|
||||
generate_url
|
||||
== "https://chatgpt.com/backend-api/codex/images/generations?client_version=0.142.0"
|
||||
)
|
||||
assert generate_headers["authorization"] == "Bearer oauth-token"
|
||||
assert generate_headers["ChatGPT-Account-ID"] == "acct_123"
|
||||
assert "host" not in generate_headers
|
||||
assert "accept-encoding" not in generate_headers
|
||||
assert "x-headroom-bypass" not in generate_headers
|
||||
assert generate_body == b'{"model":"gpt-image-2","prompt":"a route probe"}'
|
||||
|
||||
edit_method, edit_url, edit_headers, edit_body = fake.calls[1]
|
||||
assert edit_method == "POST"
|
||||
assert edit_url == "https://chatgpt.com/backend-api/codex/images/edits"
|
||||
assert edit_headers["authorization"] == "Bearer oauth-token"
|
||||
assert edit_headers["ChatGPT-Account-ID"] == "acct_123"
|
||||
assert "host" not in edit_headers
|
||||
assert edit_body == b'{"model":"gpt-image-2","prompt":"edit route probe","images":[]}'
|
||||
|
||||
|
||||
def test_openai_image_codex_response_strips_stale_compression_headers(monkeypatch) -> None:
|
||||
upstream_body = b'{"ok":true}'
|
||||
stale_content_length = "9999"
|
||||
monkeypatch.setattr(
|
||||
"headroom.providers.proxy_routes._resolve_codex_routing_headers",
|
||||
lambda headers: ({**headers, "ChatGPT-Account-ID": "acct_123"}, True),
|
||||
)
|
||||
|
||||
class FakeAsyncClient:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, str, bytes]] = []
|
||||
|
||||
async def request(self, method, url, **kwargs): # type: ignore[no-untyped-def]
|
||||
self.calls.append((method, url, kwargs.get("content", b"")))
|
||||
return FakeUpstreamResponse(
|
||||
content=upstream_body,
|
||||
status_code=200,
|
||||
headers={
|
||||
"content-encoding": "gzip",
|
||||
"content-length": stale_content_length,
|
||||
"content-type": "application/json",
|
||||
"x-upstream": "kept",
|
||||
},
|
||||
)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
return None
|
||||
|
||||
class FakeUpstreamResponse:
|
||||
def __init__(self, content: bytes, status_code: int, headers: dict[str, str]) -> None:
|
||||
self.content = content
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
|
||||
with TestClient(_app()) as client:
|
||||
fake = FakeAsyncClient()
|
||||
client.app.state.proxy.http_client = fake
|
||||
client.app.state.proxy.http_client_h1 = fake
|
||||
|
||||
response = client.post(
|
||||
"/v1/images/generations",
|
||||
headers={"Authorization": "Bearer oauth-token"},
|
||||
json={"model": "gpt-image-2", "prompt": "compressed response"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.content == upstream_body
|
||||
assert response.headers["x-upstream"] == "kept"
|
||||
assert response.headers.get("content-encoding") is None
|
||||
assert response.headers.get("content-length") == str(len(upstream_body))
|
||||
|
||||
assert fake.calls == [
|
||||
(
|
||||
"POST",
|
||||
"https://chatgpt.com/backend-api/codex/images/generations",
|
||||
b'{"model":"gpt-image-2","prompt":"compressed response"}',
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_openai_image_edits_api_key_auth_falls_through_to_openai_passthrough(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
calls: list[tuple[str, str, str, str, str]] = []
|
||||
|
||||
async def fake_passthrough(self, request, base_url, sub_path="", provider_name=""): # type: ignore[no-untyped-def]
|
||||
calls.append((request.method, request.url.path, base_url, sub_path, provider_name))
|
||||
return JSONResponse(
|
||||
{
|
||||
"base_url": base_url,
|
||||
"sub_path": sub_path,
|
||||
"provider": provider_name,
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(HeadroomProxy, "handle_passthrough", fake_passthrough)
|
||||
|
||||
with TestClient(_app()) as client:
|
||||
response = client.post(
|
||||
"/v1/images/edits",
|
||||
headers={"Authorization": "Bearer sk-proj-openai-test"},
|
||||
json={"model": "gpt-image-1", "prompt": "fall through", "image": "file-1"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"base_url": "https://api.openai.test",
|
||||
"sub_path": "images/edits",
|
||||
"provider": "openai",
|
||||
}
|
||||
assert calls == [
|
||||
(
|
||||
"POST",
|
||||
"/v1/images/edits",
|
||||
"https://api.openai.test",
|
||||
"images/edits",
|
||||
"openai",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_openai_image_edits_preserves_multipart_body_under_chatgpt_auth(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"headroom.providers.proxy_routes._resolve_codex_routing_headers",
|
||||
lambda headers: ({**headers, "ChatGPT-Account-ID": "acct_123"}, True),
|
||||
)
|
||||
boundary = "----headroom-boundary"
|
||||
body = (
|
||||
(
|
||||
f"--{boundary}\r\n"
|
||||
'Content-Disposition: form-data; name="model"\r\n\r\n'
|
||||
"gpt-image-2\r\n"
|
||||
f"--{boundary}\r\n"
|
||||
'Content-Disposition: form-data; name="prompt"\r\n\r\n'
|
||||
"preserve these bytes\r\n"
|
||||
f"--{boundary}\r\n"
|
||||
'Content-Disposition: form-data; name="image"; filename="input.png"\r\n'
|
||||
"Content-Type: image/png\r\n\r\n"
|
||||
).encode()
|
||||
+ b"\x89PNG\r\n\x1a\nraw-bytes\r\n"
|
||||
+ f"--{boundary}--\r\n".encode()
|
||||
)
|
||||
content_type = f"multipart/form-data; boundary={boundary}"
|
||||
|
||||
class FakeAsyncClient:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, str, dict[str, str], bytes]] = []
|
||||
|
||||
async def request(self, method, url, **kwargs): # type: ignore[no-untyped-def]
|
||||
self.calls.append(
|
||||
(
|
||||
method,
|
||||
url,
|
||||
dict(kwargs.get("headers", {})),
|
||||
kwargs.get("content", b""),
|
||||
)
|
||||
)
|
||||
return httpx.Response(200, json={"ok": True})
|
||||
|
||||
async def aclose(self) -> None:
|
||||
return None
|
||||
|
||||
with TestClient(_app()) as client:
|
||||
fake = FakeAsyncClient()
|
||||
client.app.state.proxy.http_client = fake
|
||||
client.app.state.proxy.http_client_h1 = fake
|
||||
|
||||
response = client.post(
|
||||
"/v1/images/edits",
|
||||
headers={
|
||||
"Authorization": "Bearer oauth-token",
|
||||
"Content-Type": content_type,
|
||||
},
|
||||
content=body,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(fake.calls) == 1
|
||||
method, url, headers, forwarded_body = fake.calls[0]
|
||||
assert method == "POST"
|
||||
assert url == "https://chatgpt.com/backend-api/codex/images/edits"
|
||||
assert headers["authorization"] == "Bearer oauth-token"
|
||||
assert headers["ChatGPT-Account-ID"] == "acct_123"
|
||||
assert headers["content-type"] == content_type
|
||||
assert "host" not in headers
|
||||
assert forwarded_body == body
|
||||
|
||||
|
||||
def test_gemini_batch_embed_contents_passthrough_uses_gemini_target(monkeypatch) -> None:
|
||||
calls: list[tuple[str, str, str]] = []
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue