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.
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/PKG-INFO +27 -1
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/README.md +26 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/pyproject.toml +1 -1
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/__init__.py +1 -1
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/chat_completions.py +64 -1
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/PKG-INFO +27 -1
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_api.py +52 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_chat_completions.py +78 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/setup.cfg +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/auth.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/cli.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/config.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/main.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/models.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy/sse_utils.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/SOURCES.txt +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/dependency_links.txt +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/entry_points.txt +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/requires.txt +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/top_level.txt +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_auth.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_cli.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_config.py +0 -0
- {codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/tests/test_models.py +0 -0
- {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.
|
|
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
|
|
@@ -46,7 +46,7 @@ def chat_to_responses_request(
|
|
|
46
46
|
{
|
|
47
47
|
"type": "message",
|
|
48
48
|
"role": "user",
|
|
49
|
-
"content":
|
|
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.
|
|
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
|
{
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{codex_api_proxy-0.1.2 → codex_api_proxy-0.1.3}/src/codex_api_proxy.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|