codex-api-proxy 0.1.2__tar.gz → 0.1.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 (25) hide show
  1. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/PKG-INFO +27 -1
  2. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/README.md +26 -0
  3. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/pyproject.toml +1 -1
  4. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/__init__.py +1 -1
  5. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/chat_completions.py +64 -1
  6. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/PKG-INFO +27 -1
  7. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_api.py +52 -0
  8. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_chat_completions.py +78 -0
  9. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/setup.cfg +0 -0
  10. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/auth.py +0 -0
  11. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/cli.py +0 -0
  12. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/config.py +0 -0
  13. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/main.py +0 -0
  14. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/models.py +0 -0
  15. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/sse_utils.py +0 -0
  16. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/SOURCES.txt +0 -0
  17. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/dependency_links.txt +0 -0
  18. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/entry_points.txt +0 -0
  19. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/requires.txt +0 -0
  20. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/top_level.txt +0 -0
  21. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_auth.py +0 -0
  22. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_cli.py +0 -0
  23. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_config.py +0 -0
  24. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_models.py +0 -0
  25. {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_release_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-api-proxy
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Local OpenAI-compatible HTTP proxy backed by Codex/OpenAI credentials
5
5
  Author: codex-api-proxy contributors
6
6
  License-Expression: MIT
@@ -209,6 +209,32 @@ curl -sS http://127.0.0.1:8765/v1/responses \
209
209
  -d '{"model":"gpt-5.5","input":"Reply with exactly: pong"}'
210
210
  ```
211
211
 
212
+ Image input through Chat Completions:
213
+
214
+ ```bash
215
+ BASE64_IMAGE=$(base64 < image.jpg)
216
+ curl -sS http://127.0.0.1:8765/v1/chat/completions \
217
+ -H 'Content-Type: application/json' \
218
+ -d '{
219
+ "model": "gpt-5.5",
220
+ "messages": [{
221
+ "role": "user",
222
+ "content": [
223
+ {"type": "text", "text": "What is in this image?"},
224
+ {
225
+ "type": "image_url",
226
+ "image_url": {
227
+ "url": "data:image/jpeg;base64,'"$BASE64_IMAGE"'",
228
+ "detail": "high"
229
+ }
230
+ }
231
+ ]
232
+ }]
233
+ }'
234
+ ```
235
+
236
+ When using ChatGPT Codex credentials, Chat Completions image parts are converted to Responses API `input_image` parts. `/v1/responses` requests are passed through unchanged.
237
+
212
238
  When `--api-key` is configured:
213
239
 
214
240
  ```bash
@@ -185,6 +185,32 @@ curl -sS http://127.0.0.1:8765/v1/responses \
185
185
  -d '{"model":"gpt-5.5","input":"Reply with exactly: pong"}'
186
186
  ```
187
187
 
188
+ Image input through Chat Completions:
189
+
190
+ ```bash
191
+ BASE64_IMAGE=$(base64 < image.jpg)
192
+ curl -sS http://127.0.0.1:8765/v1/chat/completions \
193
+ -H 'Content-Type: application/json' \
194
+ -d '{
195
+ "model": "gpt-5.5",
196
+ "messages": [{
197
+ "role": "user",
198
+ "content": [
199
+ {"type": "text", "text": "What is in this image?"},
200
+ {
201
+ "type": "image_url",
202
+ "image_url": {
203
+ "url": "data:image/jpeg;base64,'"$BASE64_IMAGE"'",
204
+ "detail": "high"
205
+ }
206
+ }
207
+ ]
208
+ }]
209
+ }'
210
+ ```
211
+
212
+ When using ChatGPT Codex credentials, Chat Completions image parts are converted to Responses API `input_image` parts. `/v1/responses` requests are passed through unchanged.
213
+
188
214
  When `--api-key` is configured:
189
215
 
190
216
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codex-api-proxy"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Local OpenAI-compatible HTTP proxy backed by Codex/OpenAI credentials"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """OpenAI-compatible HTTP proxy backed by Codex/OpenAI credentials."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
@@ -46,7 +46,7 @@ def chat_to_responses_request(
46
46
  {
47
47
  "type": "message",
48
48
  "role": "user",
49
- "content": [{"type": "input_text", "text": text}],
49
+ "content": message_content_to_response_content(message.get("content", "")),
50
50
  }
51
51
  )
52
52
  continue
@@ -147,6 +147,69 @@ def message_content_to_text(content: object) -> str:
147
147
  return "".join(parts)
148
148
 
149
149
 
150
+ def message_content_to_response_content(content: object) -> list[dict[str, str]]:
151
+ if isinstance(content, str):
152
+ return [{"type": "input_text", "text": content}]
153
+ if not isinstance(content, list):
154
+ return [{"type": "input_text", "text": ""}]
155
+
156
+ parts: list[dict[str, str]] = []
157
+ for item in content:
158
+ if isinstance(item, str):
159
+ parts.append({"type": "input_text", "text": item})
160
+ continue
161
+ if not isinstance(item, dict):
162
+ continue
163
+
164
+ part_type = item.get("type")
165
+ text = item.get("text")
166
+ if part_type in {"text", "input_text"} and isinstance(text, str):
167
+ parts.append({"type": "input_text", "text": text})
168
+ continue
169
+
170
+ image_part = _image_content_part(item)
171
+ if image_part:
172
+ parts.append(image_part)
173
+
174
+ return parts or [{"type": "input_text", "text": ""}]
175
+
176
+
177
+ def _image_content_part(item: dict) -> dict[str, str] | None:
178
+ part_type = item.get("type")
179
+ if part_type == "image_url":
180
+ image_url = item.get("image_url")
181
+ url: object
182
+ detail: object = item.get("detail")
183
+ if isinstance(image_url, dict):
184
+ url = image_url.get("url")
185
+ detail = image_url.get("detail", detail)
186
+ else:
187
+ url = image_url
188
+ if not isinstance(url, str):
189
+ return None
190
+ part = {"type": "input_image", "image_url": url}
191
+ if isinstance(detail, str):
192
+ part["detail"] = detail
193
+ return part
194
+
195
+ if part_type == "input_image":
196
+ part = {"type": "input_image"}
197
+ image_url = item.get("image_url")
198
+ file_id = item.get("file_id")
199
+ detail = item.get("detail")
200
+ if isinstance(image_url, str):
201
+ part["image_url"] = image_url
202
+ if isinstance(file_id, str):
203
+ part["file_id"] = file_id
204
+ if isinstance(detail, str):
205
+ part["detail"] = detail
206
+ if "image_url" not in part and "file_id" not in part:
207
+ return None
208
+ return part
209
+
210
+ return None
211
+
212
+
150
213
  def extract_output_text(response: dict) -> str:
151
214
  chunks: list[str] = []
152
215
  for item in response.get("output", []):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-api-proxy
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Local OpenAI-compatible HTTP proxy backed by Codex/OpenAI credentials
5
5
  Author: codex-api-proxy contributors
6
6
  License-Expression: MIT
@@ -209,6 +209,32 @@ curl -sS http://127.0.0.1:8765/v1/responses \
209
209
  -d '{"model":"gpt-5.5","input":"Reply with exactly: pong"}'
210
210
  ```
211
211
 
212
+ Image input through Chat Completions:
213
+
214
+ ```bash
215
+ BASE64_IMAGE=$(base64 < image.jpg)
216
+ curl -sS http://127.0.0.1:8765/v1/chat/completions \
217
+ -H 'Content-Type: application/json' \
218
+ -d '{
219
+ "model": "gpt-5.5",
220
+ "messages": [{
221
+ "role": "user",
222
+ "content": [
223
+ {"type": "text", "text": "What is in this image?"},
224
+ {
225
+ "type": "image_url",
226
+ "image_url": {
227
+ "url": "data:image/jpeg;base64,'"$BASE64_IMAGE"'",
228
+ "detail": "high"
229
+ }
230
+ }
231
+ ]
232
+ }]
233
+ }'
234
+ ```
235
+
236
+ When using ChatGPT Codex credentials, Chat Completions image parts are converted to Responses API `input_image` parts. `/v1/responses` requests are passed through unchanged.
237
+
212
238
  When `--api-key` is configured:
213
239
 
214
240
  ```bash
@@ -170,6 +170,58 @@ def test_chatgpt_mode_converts_chat_completion_to_responses(tmp_path: Path, monk
170
170
  assert seen["body"]["store"] is False
171
171
 
172
172
 
173
+ def test_chatgpt_mode_converts_chat_completion_images_to_responses(tmp_path: Path, monkeypatch) -> None:
174
+ seen = {}
175
+
176
+ def handler(request: httpx.Request) -> httpx.Response:
177
+ seen["body"] = json.loads(request.content)
178
+ body = "\n\n".join(
179
+ [
180
+ 'data: {"type":"response.output_text.delta","delta":"a cat"}',
181
+ 'data: {"type":"response.completed","response":{"output":[]}}',
182
+ "",
183
+ ]
184
+ )
185
+ return httpx.Response(200, content=body, headers={"content-type": "text/event-stream"})
186
+
187
+ mock_auto_auth(monkeypatch, chatgpt_auth(tmp_path))
188
+ mock_upstream(monkeypatch, handler)
189
+ app = create_app(Settings())
190
+ client = TestClient(app)
191
+
192
+ response = client.post(
193
+ "/v1/chat/completions",
194
+ json={
195
+ "model": "gpt-5.5",
196
+ "messages": [
197
+ {
198
+ "role": "user",
199
+ "content": [
200
+ {"type": "text", "text": "what is in this image?"},
201
+ {
202
+ "type": "image_url",
203
+ "image_url": {
204
+ "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ==",
205
+ "detail": "high",
206
+ },
207
+ },
208
+ ],
209
+ }
210
+ ],
211
+ },
212
+ )
213
+
214
+ assert response.status_code == 200
215
+ assert seen["body"]["input"][0]["content"] == [
216
+ {"type": "input_text", "text": "what is in this image?"},
217
+ {
218
+ "type": "input_image",
219
+ "image_url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ==",
220
+ "detail": "high",
221
+ },
222
+ ]
223
+
224
+
173
225
  def test_chatgpt_mode_applies_default_latency_controls(tmp_path: Path, monkeypatch) -> None:
174
226
  seen = {}
175
227
 
@@ -27,6 +27,84 @@ def test_chat_to_responses_request_maps_messages() -> None:
27
27
  ]
28
28
 
29
29
 
30
+ def test_chat_to_responses_request_maps_image_content_parts() -> None:
31
+ payload = chat_to_responses_request(
32
+ {
33
+ "model": "gpt-5.5",
34
+ "messages": [
35
+ {
36
+ "role": "user",
37
+ "content": [
38
+ {"type": "text", "text": "what is in this image?"},
39
+ {
40
+ "type": "image_url",
41
+ "image_url": {
42
+ "url": "https://example.com/cat.jpg",
43
+ "detail": "high",
44
+ },
45
+ },
46
+ {
47
+ "type": "image_url",
48
+ "image_url": {
49
+ "url": "data:image/png;base64,iVBORw0KGgo=",
50
+ },
51
+ },
52
+ ],
53
+ }
54
+ ],
55
+ }
56
+ )
57
+
58
+ assert payload["input"] == [
59
+ {
60
+ "type": "message",
61
+ "role": "user",
62
+ "content": [
63
+ {"type": "input_text", "text": "what is in this image?"},
64
+ {
65
+ "type": "input_image",
66
+ "image_url": "https://example.com/cat.jpg",
67
+ "detail": "high",
68
+ },
69
+ {
70
+ "type": "input_image",
71
+ "image_url": "data:image/png;base64,iVBORw0KGgo=",
72
+ },
73
+ ],
74
+ }
75
+ ]
76
+
77
+
78
+ def test_chat_to_responses_request_preserves_responses_image_content_parts() -> None:
79
+ payload = chat_to_responses_request(
80
+ {
81
+ "model": "gpt-5.5",
82
+ "messages": [
83
+ {
84
+ "role": "user",
85
+ "content": [
86
+ {"type": "input_text", "text": "compare these images"},
87
+ {
88
+ "type": "input_image",
89
+ "image_url": "https://example.com/a.jpg",
90
+ "detail": "low",
91
+ },
92
+ ],
93
+ }
94
+ ],
95
+ }
96
+ )
97
+
98
+ assert payload["input"][0]["content"] == [
99
+ {"type": "input_text", "text": "compare these images"},
100
+ {
101
+ "type": "input_image",
102
+ "image_url": "https://example.com/a.jpg",
103
+ "detail": "low",
104
+ },
105
+ ]
106
+
107
+
30
108
  def test_chat_to_responses_request_applies_default_latency_controls() -> None:
31
109
  payload = chat_to_responses_request(
32
110
  {