codex-as-api 0.2.1__py3-none-any.whl
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_as_api/__init__.py +3 -0
- codex_as_api/anthropic_adapter.py +447 -0
- codex_as_api/auth.py +239 -0
- codex_as_api/messages.py +116 -0
- codex_as_api/protocol.py +98 -0
- codex_as_api/provider.py +701 -0
- codex_as_api/server.py +580 -0
- codex_as_api-0.2.1.dist-info/METADATA +543 -0
- codex_as_api-0.2.1.dist-info/RECORD +11 -0
- codex_as_api-0.2.1.dist-info/WHEEL +4 -0
- codex_as_api-0.2.1.dist-info/entry_points.txt +2 -0
codex_as_api/server.py
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .auth import ChatGPTOAuthError, ChatGPTOAuthMissingError, is_auth_locally_available
|
|
12
|
+
from .messages import Message, MessageRole, ToolSchema
|
|
13
|
+
from .provider import ChatGPTOAuthProvider
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _env_int(name: str, default: int) -> int:
|
|
17
|
+
value = os.getenv(name)
|
|
18
|
+
if value is not None and value.isdigit():
|
|
19
|
+
return int(value)
|
|
20
|
+
return default
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _env_str(name: str, default: str) -> str:
|
|
24
|
+
return os.getenv(name) or default
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
HOST = _env_str("CODEX_AS_API_HOST", "127.0.0.1")
|
|
28
|
+
PORT = _env_int("CODEX_AS_API_PORT", 18080)
|
|
29
|
+
MODEL = _env_str("CODEX_AS_API_MODEL", "gpt-5.5")
|
|
30
|
+
AUTH_PATH = os.getenv("CODEX_AS_API_AUTH_PATH")
|
|
31
|
+
|
|
32
|
+
_provider: ChatGPTOAuthProvider | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_provider() -> ChatGPTOAuthProvider:
|
|
36
|
+
global _provider
|
|
37
|
+
if _provider is None:
|
|
38
|
+
_provider = ChatGPTOAuthProvider(
|
|
39
|
+
model=MODEL,
|
|
40
|
+
auth_json_path=AUTH_PATH,
|
|
41
|
+
)
|
|
42
|
+
return _provider
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# FastAPI is an optional dependency; fail gracefully if missing.
|
|
46
|
+
try:
|
|
47
|
+
from fastapi import FastAPI, Request
|
|
48
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
49
|
+
from pydantic import BaseModel, Field
|
|
50
|
+
|
|
51
|
+
app = FastAPI(
|
|
52
|
+
title="codex-as-api",
|
|
53
|
+
description="Local OpenAI-compatible API server backed by ChatGPT/Codex OAuth.",
|
|
54
|
+
version="0.1.0",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@app.exception_handler(ChatGPTOAuthError)
|
|
58
|
+
async def _chatgpt_oauth_error_handler(_request: Request, exc: ChatGPTOAuthError) -> JSONResponse:
|
|
59
|
+
status = 401 if isinstance(exc, ChatGPTOAuthMissingError) else 500
|
|
60
|
+
return JSONResponse(status_code=status, content={"error": {"message": str(exc), "type": "chatgpt_oauth_error"}})
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------
|
|
63
|
+
# Request/response schemas
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
class ChatMessage(BaseModel):
|
|
67
|
+
role: str
|
|
68
|
+
content: str | list[dict[str, Any]] | None = None
|
|
69
|
+
name: str | None = None
|
|
70
|
+
tool_calls: list[dict[str, Any]] | None = None
|
|
71
|
+
tool_call_id: str | None = None
|
|
72
|
+
|
|
73
|
+
class ChatCompletionRequest(BaseModel):
|
|
74
|
+
model: str
|
|
75
|
+
messages: list[ChatMessage]
|
|
76
|
+
stream: bool = False
|
|
77
|
+
temperature: float | None = None
|
|
78
|
+
max_tokens: int | None = None
|
|
79
|
+
max_completion_tokens: int | None = None
|
|
80
|
+
stop: str | list[str] | None = None
|
|
81
|
+
tools: list[dict[str, Any]] | None = None
|
|
82
|
+
tool_choice: str | dict[str, Any] | None = None
|
|
83
|
+
reasoning_effort: str | None = None
|
|
84
|
+
prompt_cache_key: str | None = None
|
|
85
|
+
top_p: float | None = None
|
|
86
|
+
frequency_penalty: float | None = None
|
|
87
|
+
presence_penalty: float | None = None
|
|
88
|
+
user: str | None = None
|
|
89
|
+
subagent: str | None = None
|
|
90
|
+
memgen_request: bool | None = None
|
|
91
|
+
previous_response_id: str | None = None
|
|
92
|
+
service_tier: str | None = None
|
|
93
|
+
text: dict[str, Any] | None = None
|
|
94
|
+
client_metadata: dict[str, str] | None = None
|
|
95
|
+
|
|
96
|
+
class ImageGenerationRequest(BaseModel):
|
|
97
|
+
model: str
|
|
98
|
+
prompt: str
|
|
99
|
+
size: str | None = "auto"
|
|
100
|
+
reasoning_effort: str | None = None
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
# Helpers
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def _openai_model_id(request_model: str | None = None) -> str:
|
|
107
|
+
return f"codex-oauth:{request_model or MODEL}"
|
|
108
|
+
|
|
109
|
+
def _request_messages_to_internal(messages: list[ChatMessage]) -> list[Message]:
|
|
110
|
+
result: list[Message] = []
|
|
111
|
+
for msg in messages:
|
|
112
|
+
role = _map_role(msg.role)
|
|
113
|
+
content = _normalize_content(msg.content)
|
|
114
|
+
tool_calls = _parse_tool_calls(msg.tool_calls) if msg.tool_calls else ()
|
|
115
|
+
result.append(
|
|
116
|
+
Message(
|
|
117
|
+
role=role,
|
|
118
|
+
content=content,
|
|
119
|
+
tool_calls=tool_calls,
|
|
120
|
+
tool_call_id=msg.tool_call_id,
|
|
121
|
+
name=msg.name,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
def _map_role(role: str) -> MessageRole:
|
|
127
|
+
mapping = {
|
|
128
|
+
"system": MessageRole.SYSTEM,
|
|
129
|
+
"user": MessageRole.USER,
|
|
130
|
+
"assistant": MessageRole.ASSISTANT,
|
|
131
|
+
"tool": MessageRole.TOOL,
|
|
132
|
+
}
|
|
133
|
+
return mapping.get(role.lower(), MessageRole.USER)
|
|
134
|
+
|
|
135
|
+
def _normalize_content(content: str | list[dict[str, Any]] | None) -> str:
|
|
136
|
+
if content is None:
|
|
137
|
+
return ""
|
|
138
|
+
if isinstance(content, str):
|
|
139
|
+
return content
|
|
140
|
+
if isinstance(content, list):
|
|
141
|
+
parts: list[str] = []
|
|
142
|
+
for item in content:
|
|
143
|
+
if isinstance(item, dict):
|
|
144
|
+
text = item.get("text")
|
|
145
|
+
if isinstance(text, str):
|
|
146
|
+
parts.append(text)
|
|
147
|
+
return "".join(parts)
|
|
148
|
+
return str(content)
|
|
149
|
+
|
|
150
|
+
def _parse_tool_calls(raw: list[dict[str, Any]] | None) -> tuple[Any, ...]:
|
|
151
|
+
from .messages import ToolCall
|
|
152
|
+
if not raw:
|
|
153
|
+
return ()
|
|
154
|
+
calls = []
|
|
155
|
+
for item in raw:
|
|
156
|
+
if not isinstance(item, dict):
|
|
157
|
+
continue
|
|
158
|
+
call_id = item.get("id") or item.get("call_id") or str(uuid.uuid4().hex)
|
|
159
|
+
func = item.get("function") or {}
|
|
160
|
+
name = func.get("name") if isinstance(func, dict) else item.get("name")
|
|
161
|
+
args = func.get("arguments") if isinstance(func, dict) else item.get("arguments")
|
|
162
|
+
if isinstance(args, str):
|
|
163
|
+
try:
|
|
164
|
+
parsed = json.loads(args) if args else {}
|
|
165
|
+
except json.JSONDecodeError:
|
|
166
|
+
parsed = {"input": args}
|
|
167
|
+
elif isinstance(args, dict):
|
|
168
|
+
parsed = args
|
|
169
|
+
else:
|
|
170
|
+
parsed = {}
|
|
171
|
+
if name:
|
|
172
|
+
calls.append(ToolCall(id=str(call_id), name=str(name), arguments=parsed))
|
|
173
|
+
return tuple(calls)
|
|
174
|
+
|
|
175
|
+
def _parse_tools(raw: list[dict[str, Any]] | None) -> list[ToolSchema] | None:
|
|
176
|
+
if not raw:
|
|
177
|
+
return None
|
|
178
|
+
schemas = []
|
|
179
|
+
for item in raw:
|
|
180
|
+
if not isinstance(item, dict):
|
|
181
|
+
continue
|
|
182
|
+
func = item.get("function") or item
|
|
183
|
+
name = func.get("name")
|
|
184
|
+
desc = func.get("description") or ""
|
|
185
|
+
params = func.get("parameters") or {}
|
|
186
|
+
if name:
|
|
187
|
+
schemas.append(ToolSchema(name=str(name), description=str(desc), parameters=params if isinstance(params, dict) else {}))
|
|
188
|
+
return schemas if schemas else None
|
|
189
|
+
|
|
190
|
+
def _normalize_stop(stop: str | list[str] | None) -> list[str] | None:
|
|
191
|
+
if stop is None:
|
|
192
|
+
return None
|
|
193
|
+
if isinstance(stop, str):
|
|
194
|
+
return [stop]
|
|
195
|
+
return list(stop)
|
|
196
|
+
|
|
197
|
+
def _max_tokens_from_request(req: ChatCompletionRequest) -> int | None:
|
|
198
|
+
if req.max_completion_tokens is not None:
|
|
199
|
+
return req.max_completion_tokens
|
|
200
|
+
return req.max_tokens
|
|
201
|
+
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
# Endpoints
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
@app.get("/health")
|
|
207
|
+
async def health() -> dict[str, Any]:
|
|
208
|
+
return {
|
|
209
|
+
"status": "ok",
|
|
210
|
+
"auth_available": is_auth_locally_available(AUTH_PATH),
|
|
211
|
+
"model": MODEL,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@app.post("/v1/chat/completions", response_model=None)
|
|
215
|
+
async def chat_completions(request: ChatCompletionRequest, http_request: Request) -> JSONResponse | StreamingResponse:
|
|
216
|
+
provider = _get_provider()
|
|
217
|
+
messages = _request_messages_to_internal(request.messages)
|
|
218
|
+
tools = _parse_tools(request.tools)
|
|
219
|
+
stop = _normalize_stop(request.stop)
|
|
220
|
+
max_tokens = _max_tokens_from_request(request)
|
|
221
|
+
|
|
222
|
+
subagent = request.subagent or http_request.headers.get("x-openai-subagent")
|
|
223
|
+
memgen_request_header = http_request.headers.get("x-openai-memgen-request")
|
|
224
|
+
memgen_request: bool | None = request.memgen_request
|
|
225
|
+
if memgen_request is None and memgen_request_header is not None:
|
|
226
|
+
memgen_request = memgen_request_header.lower() not in ("false", "0", "")
|
|
227
|
+
previous_response_id = request.previous_response_id
|
|
228
|
+
|
|
229
|
+
if request.stream:
|
|
230
|
+
async def _stream() -> AsyncIterator[str]:
|
|
231
|
+
request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
|
|
232
|
+
created = int(time.time())
|
|
233
|
+
model = _openai_model_id(request.model)
|
|
234
|
+
|
|
235
|
+
# SSE preamble
|
|
236
|
+
preamble = {
|
|
237
|
+
"id": request_id,
|
|
238
|
+
"object": "chat.completion.chunk",
|
|
239
|
+
"created": created,
|
|
240
|
+
"model": model,
|
|
241
|
+
"choices": [{
|
|
242
|
+
"index": 0,
|
|
243
|
+
"delta": {"role": "assistant"},
|
|
244
|
+
"finish_reason": None,
|
|
245
|
+
}],
|
|
246
|
+
}
|
|
247
|
+
yield f"data: {json.dumps(preamble)}\n\n"
|
|
248
|
+
|
|
249
|
+
reasoning_parts: list[str] = []
|
|
250
|
+
content_parts: list[str] = []
|
|
251
|
+
tool_calls_buffer: list[dict[str, Any]] = []
|
|
252
|
+
usage_dict: dict[str, Any] | None = None
|
|
253
|
+
|
|
254
|
+
for event in provider.chat_stream(
|
|
255
|
+
messages,
|
|
256
|
+
model=request.model,
|
|
257
|
+
tools=tools,
|
|
258
|
+
tool_choice=request.tool_choice,
|
|
259
|
+
temperature=request.temperature,
|
|
260
|
+
reasoning_effort=request.reasoning_effort,
|
|
261
|
+
max_tokens=max_tokens,
|
|
262
|
+
stop=stop,
|
|
263
|
+
prompt_cache_key=request.prompt_cache_key,
|
|
264
|
+
subagent=subagent,
|
|
265
|
+
memgen_request=memgen_request,
|
|
266
|
+
previous_response_id=previous_response_id,
|
|
267
|
+
service_tier=request.service_tier,
|
|
268
|
+
text=request.text,
|
|
269
|
+
client_metadata=request.client_metadata,
|
|
270
|
+
):
|
|
271
|
+
typ = event.get("type")
|
|
272
|
+
if typ == "content":
|
|
273
|
+
text = str(event.get("text", ""))
|
|
274
|
+
content_parts.append(text)
|
|
275
|
+
chunk = {
|
|
276
|
+
"id": request_id,
|
|
277
|
+
"object": "chat.completion.chunk",
|
|
278
|
+
"created": created,
|
|
279
|
+
"model": model,
|
|
280
|
+
"choices": [{
|
|
281
|
+
"index": 0,
|
|
282
|
+
"delta": {"content": text},
|
|
283
|
+
"finish_reason": None,
|
|
284
|
+
}],
|
|
285
|
+
}
|
|
286
|
+
yield f"data: {json.dumps(chunk)}\n\n"
|
|
287
|
+
elif typ == "reasoning_delta":
|
|
288
|
+
text = str(event.get("text", ""))
|
|
289
|
+
reasoning_parts.append(text)
|
|
290
|
+
# OpenAI-compatible reasoning field
|
|
291
|
+
chunk = {
|
|
292
|
+
"id": request_id,
|
|
293
|
+
"object": "chat.completion.chunk",
|
|
294
|
+
"created": created,
|
|
295
|
+
"model": model,
|
|
296
|
+
"choices": [{
|
|
297
|
+
"index": 0,
|
|
298
|
+
"delta": {"reasoning_content": text},
|
|
299
|
+
"finish_reason": None,
|
|
300
|
+
}],
|
|
301
|
+
}
|
|
302
|
+
yield f"data: {json.dumps(chunk)}\n\n"
|
|
303
|
+
elif typ == "reasoning_raw_delta":
|
|
304
|
+
text = str(event.get("text", ""))
|
|
305
|
+
reasoning_parts.append(text)
|
|
306
|
+
chunk = {
|
|
307
|
+
"id": request_id,
|
|
308
|
+
"object": "chat.completion.chunk",
|
|
309
|
+
"created": created,
|
|
310
|
+
"model": model,
|
|
311
|
+
"choices": [{
|
|
312
|
+
"index": 0,
|
|
313
|
+
"delta": {"reasoning": text},
|
|
314
|
+
"finish_reason": None,
|
|
315
|
+
}],
|
|
316
|
+
}
|
|
317
|
+
yield f"data: {json.dumps(chunk)}\n\n"
|
|
318
|
+
elif typ == "tool_call":
|
|
319
|
+
tc = {
|
|
320
|
+
"id": event.get("id"),
|
|
321
|
+
"type": "function",
|
|
322
|
+
"function": {
|
|
323
|
+
"name": event.get("name"),
|
|
324
|
+
"arguments": json.dumps(event.get("arguments") or {}),
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
tool_calls_buffer.append(tc)
|
|
328
|
+
chunk = {
|
|
329
|
+
"id": request_id,
|
|
330
|
+
"object": "chat.completion.chunk",
|
|
331
|
+
"created": created,
|
|
332
|
+
"model": model,
|
|
333
|
+
"choices": [{
|
|
334
|
+
"index": 0,
|
|
335
|
+
"delta": {"tool_calls": [tc]},
|
|
336
|
+
"finish_reason": None,
|
|
337
|
+
}],
|
|
338
|
+
}
|
|
339
|
+
yield f"data: {json.dumps(chunk)}\n\n"
|
|
340
|
+
elif typ == "finish":
|
|
341
|
+
usage = event.get("usage")
|
|
342
|
+
if isinstance(usage, dict):
|
|
343
|
+
usage_dict = usage
|
|
344
|
+
chunk = {
|
|
345
|
+
"id": request_id,
|
|
346
|
+
"object": "chat.completion.chunk",
|
|
347
|
+
"created": created,
|
|
348
|
+
"model": model,
|
|
349
|
+
"choices": [{
|
|
350
|
+
"index": 0,
|
|
351
|
+
"delta": {},
|
|
352
|
+
"finish_reason": event.get("finish_reason") or "stop",
|
|
353
|
+
}],
|
|
354
|
+
}
|
|
355
|
+
yield f"data: {json.dumps(chunk)}\n\n"
|
|
356
|
+
|
|
357
|
+
# Usage summary chunk if available
|
|
358
|
+
if usage_dict:
|
|
359
|
+
u = usage_dict
|
|
360
|
+
finish_chunk = {
|
|
361
|
+
"id": request_id,
|
|
362
|
+
"object": "chat.completion.chunk",
|
|
363
|
+
"created": created,
|
|
364
|
+
"model": model,
|
|
365
|
+
"choices": [],
|
|
366
|
+
"usage": {
|
|
367
|
+
"prompt_tokens": u.get("prompt_tokens", 0),
|
|
368
|
+
"completion_tokens": u.get("completion_tokens", 0),
|
|
369
|
+
"total_tokens": u.get("total_tokens", 0),
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
yield f"data: {json.dumps(finish_chunk)}\n\n"
|
|
373
|
+
|
|
374
|
+
yield "data: [DONE]\n\n"
|
|
375
|
+
|
|
376
|
+
return StreamingResponse(_stream(), media_type="text/event-stream")
|
|
377
|
+
|
|
378
|
+
# Non-streaming
|
|
379
|
+
response = provider.chat(
|
|
380
|
+
messages,
|
|
381
|
+
model=request.model,
|
|
382
|
+
tools=tools,
|
|
383
|
+
tool_choice=request.tool_choice,
|
|
384
|
+
temperature=request.temperature,
|
|
385
|
+
reasoning_effort=request.reasoning_effort,
|
|
386
|
+
max_tokens=max_tokens,
|
|
387
|
+
stop=stop,
|
|
388
|
+
prompt_cache_key=request.prompt_cache_key,
|
|
389
|
+
subagent=subagent,
|
|
390
|
+
memgen_request=memgen_request,
|
|
391
|
+
previous_response_id=previous_response_id,
|
|
392
|
+
service_tier=request.service_tier,
|
|
393
|
+
text=request.text,
|
|
394
|
+
client_metadata=request.client_metadata,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
choices: list[dict[str, Any]] = [{
|
|
398
|
+
"index": 0,
|
|
399
|
+
"message": {
|
|
400
|
+
"role": "assistant",
|
|
401
|
+
"content": response.content,
|
|
402
|
+
},
|
|
403
|
+
"finish_reason": response.finish_reason,
|
|
404
|
+
}]
|
|
405
|
+
|
|
406
|
+
if response.tool_calls:
|
|
407
|
+
choices[0]["message"]["tool_calls"] = [
|
|
408
|
+
{
|
|
409
|
+
"id": tc.id,
|
|
410
|
+
"type": "function",
|
|
411
|
+
"function": {
|
|
412
|
+
"name": tc.name,
|
|
413
|
+
"arguments": json.dumps(tc.arguments, ensure_ascii=False),
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
for tc in response.tool_calls
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
if response.reasoning_content:
|
|
420
|
+
choices[0]["message"]["reasoning_content"] = response.reasoning_content
|
|
421
|
+
|
|
422
|
+
result: dict[str, Any] = {
|
|
423
|
+
"id": f"chatcmpl-{uuid.uuid4().hex[:24]}",
|
|
424
|
+
"object": "chat.completion",
|
|
425
|
+
"created": int(time.time()),
|
|
426
|
+
"model": _openai_model_id(request.model),
|
|
427
|
+
"choices": choices,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if response.usage:
|
|
431
|
+
result["usage"] = {
|
|
432
|
+
"prompt_tokens": response.usage.prompt_tokens,
|
|
433
|
+
"completion_tokens": response.usage.completion_tokens,
|
|
434
|
+
"total_tokens": response.usage.total_tokens or (response.usage.prompt_tokens + response.usage.completion_tokens),
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return JSONResponse(content=result)
|
|
438
|
+
|
|
439
|
+
@app.post("/v1/images/generations")
|
|
440
|
+
async def images_generations(request: ImageGenerationRequest) -> JSONResponse:
|
|
441
|
+
provider = _get_provider()
|
|
442
|
+
images = provider.generate_image(
|
|
443
|
+
request.prompt,
|
|
444
|
+
model=request.model,
|
|
445
|
+
size=request.size,
|
|
446
|
+
reasoning_effort=request.reasoning_effort,
|
|
447
|
+
)
|
|
448
|
+
data = [
|
|
449
|
+
{
|
|
450
|
+
"url": image.get("result"),
|
|
451
|
+
"revised_prompt": image.get("revised_prompt") or request.prompt,
|
|
452
|
+
}
|
|
453
|
+
for image in images
|
|
454
|
+
if image.get("result")
|
|
455
|
+
]
|
|
456
|
+
return JSONResponse(content={"created": int(time.time()), "data": data})
|
|
457
|
+
|
|
458
|
+
# ------------------------------------------------------------------
|
|
459
|
+
# Anthropic Messages API compatible endpoint
|
|
460
|
+
# ------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
from .anthropic_adapter import (
|
|
463
|
+
anthropic_request_to_internal,
|
|
464
|
+
internal_response_to_anthropic,
|
|
465
|
+
anthropic_stream_adapter,
|
|
466
|
+
format_anthropic_error,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@app.post("/v1/messages", response_model=None)
|
|
470
|
+
async def anthropic_messages(http_request: Request) -> JSONResponse | StreamingResponse:
|
|
471
|
+
provider = _get_provider()
|
|
472
|
+
body = await http_request.json()
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
messages, tools, tool_choice, stop, reasoning_effort = anthropic_request_to_internal(
|
|
476
|
+
model=body.get("model", MODEL),
|
|
477
|
+
messages=body.get("messages", []),
|
|
478
|
+
system=body.get("system"),
|
|
479
|
+
max_tokens=body.get("max_tokens", 4096),
|
|
480
|
+
tools=body.get("tools"),
|
|
481
|
+
tool_choice=body.get("tool_choice"),
|
|
482
|
+
stop_sequences=body.get("stop_sequences"),
|
|
483
|
+
thinking=body.get("thinking"),
|
|
484
|
+
)
|
|
485
|
+
except Exception as exc:
|
|
486
|
+
return JSONResponse(status_code=400, content=format_anthropic_error(400, str(exc)))
|
|
487
|
+
|
|
488
|
+
stream = body.get("stream", False)
|
|
489
|
+
request_model = body.get("model") or MODEL
|
|
490
|
+
|
|
491
|
+
if stream:
|
|
492
|
+
async def _stream() -> AsyncIterator[str]:
|
|
493
|
+
request_id = f"msg_{uuid.uuid4().hex[:24]}"
|
|
494
|
+
for sse_chunk in anthropic_stream_adapter(
|
|
495
|
+
provider.chat_stream(
|
|
496
|
+
messages,
|
|
497
|
+
model=request_model,
|
|
498
|
+
tools=tools,
|
|
499
|
+
tool_choice=tool_choice,
|
|
500
|
+
reasoning_effort=reasoning_effort,
|
|
501
|
+
stop=stop,
|
|
502
|
+
),
|
|
503
|
+
model=request_model,
|
|
504
|
+
request_id=request_id,
|
|
505
|
+
):
|
|
506
|
+
yield sse_chunk
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
return StreamingResponse(_stream(), media_type="text/event-stream")
|
|
510
|
+
except ChatGPTOAuthError as exc:
|
|
511
|
+
status = 401 if isinstance(exc, ChatGPTOAuthMissingError) else 500
|
|
512
|
+
return JSONResponse(status_code=status, content=format_anthropic_error(status, str(exc)))
|
|
513
|
+
|
|
514
|
+
# Non-streaming
|
|
515
|
+
try:
|
|
516
|
+
response = provider.chat(
|
|
517
|
+
messages,
|
|
518
|
+
model=request_model,
|
|
519
|
+
tools=tools,
|
|
520
|
+
tool_choice=tool_choice,
|
|
521
|
+
reasoning_effort=reasoning_effort,
|
|
522
|
+
stop=stop,
|
|
523
|
+
)
|
|
524
|
+
except ChatGPTOAuthError as exc:
|
|
525
|
+
status = 401 if isinstance(exc, ChatGPTOAuthMissingError) else 500
|
|
526
|
+
return JSONResponse(status_code=status, content=format_anthropic_error(status, str(exc)))
|
|
527
|
+
|
|
528
|
+
request_id = f"msg_{uuid.uuid4().hex[:24]}"
|
|
529
|
+
result = internal_response_to_anthropic(response, request_model, request_id)
|
|
530
|
+
return JSONResponse(content=result)
|
|
531
|
+
|
|
532
|
+
# ------------------------------------------------------------------
|
|
533
|
+
# Custom endpoints (not in standard OpenAI API, but exposed for full feature routing)
|
|
534
|
+
# ------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
@app.post("/v1/inspect")
|
|
537
|
+
async def inspect(request: Request) -> JSONResponse:
|
|
538
|
+
"""Inspect images with a text prompt.
|
|
539
|
+
|
|
540
|
+
Body: {"prompt": str, "images": [{"image_url": "data:image/..."}, ...], "reasoning_effort": str?}
|
|
541
|
+
"""
|
|
542
|
+
provider = _get_provider()
|
|
543
|
+
body = await request.json()
|
|
544
|
+
prompt = str(body.get("prompt", ""))
|
|
545
|
+
images = body.get("images") or []
|
|
546
|
+
reasoning_effort = body.get("reasoning_effort")
|
|
547
|
+
result = provider.inspect_images(prompt, images=images, reasoning_effort=reasoning_effort)
|
|
548
|
+
return JSONResponse(content={"content": result})
|
|
549
|
+
|
|
550
|
+
@app.post("/v1/compact")
|
|
551
|
+
async def compact(request: Request) -> JSONResponse:
|
|
552
|
+
"""Compact a conversation into a checkpoint for continuation.
|
|
553
|
+
|
|
554
|
+
Body: {"messages": [{"role": "system|user|assistant|tool", "content": str, ...}], "reasoning_effort": str?}
|
|
555
|
+
"""
|
|
556
|
+
provider = _get_provider()
|
|
557
|
+
body = await request.json()
|
|
558
|
+
raw_messages = body.get("messages") or []
|
|
559
|
+
messages = _request_messages_to_internal([ChatMessage.model_validate(m) for m in raw_messages])
|
|
560
|
+
reasoning_effort = body.get("reasoning_effort")
|
|
561
|
+
checkpoint = provider.compact_messages(messages, reasoning_effort=reasoning_effort)
|
|
562
|
+
return JSONResponse(content={"checkpoint": checkpoint})
|
|
563
|
+
|
|
564
|
+
# ------------------------------------------------------------------
|
|
565
|
+
# CLI entry point
|
|
566
|
+
# ------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
def main() -> None:
|
|
569
|
+
import uvicorn
|
|
570
|
+
uvicorn.run("codex_as_api.server:app", host=HOST, port=PORT, log_level="info")
|
|
571
|
+
|
|
572
|
+
except ImportError as _import_exc:
|
|
573
|
+
# FastAPI / uvicorn not installed
|
|
574
|
+
app = None # type: ignore[assignment]
|
|
575
|
+
|
|
576
|
+
def main() -> None: # type: ignore[misc]
|
|
577
|
+
raise ImportError(
|
|
578
|
+
"FastAPI and uvicorn are required to run the server. "
|
|
579
|
+
"Install with: pip install 'codex-as-api[server]'"
|
|
580
|
+
) from _import_exc
|