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/messages.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MessageRole(enum.Enum):
|
|
8
|
+
SYSTEM = "system"
|
|
9
|
+
USER = "user"
|
|
10
|
+
ASSISTANT = "assistant"
|
|
11
|
+
TOOL = "tool"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
15
|
+
class ToolCall:
|
|
16
|
+
id: str
|
|
17
|
+
name: str
|
|
18
|
+
arguments: dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
22
|
+
class Message:
|
|
23
|
+
role: MessageRole
|
|
24
|
+
content: str
|
|
25
|
+
tool_calls: tuple[ToolCall, ...] = ()
|
|
26
|
+
tool_call_id: str | None = None
|
|
27
|
+
name: str | None = None
|
|
28
|
+
reasoning_content: str | None = None
|
|
29
|
+
images: tuple[str, ...] = ()
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
if not isinstance(self.tool_calls, tuple):
|
|
33
|
+
object.__setattr__(self, "tool_calls", tuple(self.tool_calls))
|
|
34
|
+
if self.role is MessageRole.TOOL:
|
|
35
|
+
if self.tool_call_id is None or self.name is None:
|
|
36
|
+
raise ValueError("tool messages require tool_call_id and name")
|
|
37
|
+
elif self.tool_call_id is not None or self.name is not None:
|
|
38
|
+
raise ValueError("tool_call_id and name are only allowed on tool messages")
|
|
39
|
+
if self.tool_calls and self.role is not MessageRole.ASSISTANT:
|
|
40
|
+
raise ValueError("tool_calls are only allowed on assistant messages")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
44
|
+
class Usage:
|
|
45
|
+
prompt_tokens: int
|
|
46
|
+
completion_tokens: int
|
|
47
|
+
total_tokens: int | None = None
|
|
48
|
+
cached_tokens: int = 0
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
if self.total_tokens is None:
|
|
52
|
+
object.__setattr__(
|
|
53
|
+
self,
|
|
54
|
+
"total_tokens",
|
|
55
|
+
self.prompt_tokens + self.completion_tokens,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def cache_hit_rate(self) -> float:
|
|
60
|
+
if self.prompt_tokens <= 0:
|
|
61
|
+
return 0.0
|
|
62
|
+
return self.cached_tokens / self.prompt_tokens
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
66
|
+
class AssistantResponse:
|
|
67
|
+
content: str
|
|
68
|
+
tool_calls: tuple[ToolCall, ...] = ()
|
|
69
|
+
finish_reason: str = "stop"
|
|
70
|
+
usage: Usage | None = None
|
|
71
|
+
reasoning_content: str | None = None
|
|
72
|
+
raw: dict | None = None
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
if not isinstance(self.tool_calls, tuple):
|
|
76
|
+
object.__setattr__(self, "tool_calls", tuple(self.tool_calls))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class InterruptIdleSignal(Exception):
|
|
80
|
+
"""Raised when an interrupted agent turn should return to idle."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
84
|
+
class TerminatorCall:
|
|
85
|
+
name: str
|
|
86
|
+
arguments: dict
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
if not self.name:
|
|
90
|
+
raise ValueError("TerminatorCall.name must be non-empty")
|
|
91
|
+
if not isinstance(self.arguments, dict):
|
|
92
|
+
raise TypeError("TerminatorCall.arguments must be a dict")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
96
|
+
class AgentResponse:
|
|
97
|
+
text: str
|
|
98
|
+
terminator_call: TerminatorCall | None = None
|
|
99
|
+
finish_reason: str = "stop"
|
|
100
|
+
usage: Usage | None = None
|
|
101
|
+
reasoning_content: str | None = None
|
|
102
|
+
raw: dict | None = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
106
|
+
class ToolResult:
|
|
107
|
+
ok: bool
|
|
108
|
+
content: str
|
|
109
|
+
payload: dict | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
113
|
+
class ToolSchema:
|
|
114
|
+
name: str
|
|
115
|
+
description: str
|
|
116
|
+
parameters: dict
|
codex_as_api/protocol.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Iterable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_value(value: Any, key: str, default: Any = None) -> Any:
|
|
8
|
+
if isinstance(value, dict):
|
|
9
|
+
return value.get(key, default)
|
|
10
|
+
return getattr(value, key, default)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize_stream_content(content: Any) -> str:
|
|
14
|
+
if content is None:
|
|
15
|
+
return ""
|
|
16
|
+
if isinstance(content, str):
|
|
17
|
+
return content
|
|
18
|
+
if isinstance(content, list):
|
|
19
|
+
parts: list[str] = []
|
|
20
|
+
for item in content:
|
|
21
|
+
text = get_value(item, "text")
|
|
22
|
+
if text:
|
|
23
|
+
parts.append(str(text))
|
|
24
|
+
return "".join(parts)
|
|
25
|
+
return str(content)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize_openai_chat_completion_chunk(chunk: Any) -> list[dict[str, Any]]:
|
|
29
|
+
"""Convert OpenAI-compatible chat-completion stream chunks into internal stream events."""
|
|
30
|
+
events: list[dict[str, Any]] = []
|
|
31
|
+
choices = get_value(chunk, "choices") or ()
|
|
32
|
+
if not choices:
|
|
33
|
+
return events
|
|
34
|
+
choice = choices[0]
|
|
35
|
+
delta = get_value(choice, "delta")
|
|
36
|
+
content = normalize_stream_content(get_value(delta, "content"))
|
|
37
|
+
if content:
|
|
38
|
+
events.append({"type": "content", "text": content})
|
|
39
|
+
for key in ("reasoning_content", "reasoning", "reasoning_summary"):
|
|
40
|
+
reasoning = normalize_stream_content(get_value(delta, key))
|
|
41
|
+
if reasoning:
|
|
42
|
+
events.append({"type": "reasoning_delta", "text": reasoning, "source_key": key})
|
|
43
|
+
raw_reasoning = normalize_stream_content(get_value(delta, "reasoning_text"))
|
|
44
|
+
if raw_reasoning:
|
|
45
|
+
events.append({"type": "reasoning_raw_delta", "text": raw_reasoning, "source_key": "reasoning_text"})
|
|
46
|
+
tool_calls = get_value(delta, "tool_calls")
|
|
47
|
+
if tool_calls:
|
|
48
|
+
events.append({"type": "tool_call_delta", "delta": tool_calls})
|
|
49
|
+
finish_reason = get_value(choice, "finish_reason")
|
|
50
|
+
if finish_reason:
|
|
51
|
+
events.append({"type": "finish", "finish_reason": finish_reason})
|
|
52
|
+
return events
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def response_failure_message(event: dict[str, Any], status: str) -> str:
|
|
56
|
+
response = event.get("response")
|
|
57
|
+
error: Any = event.get("error")
|
|
58
|
+
incomplete_details: Any = event.get("incomplete_details")
|
|
59
|
+
if isinstance(response, dict):
|
|
60
|
+
error = response.get("error", error)
|
|
61
|
+
incomplete_details = response.get("incomplete_details", incomplete_details)
|
|
62
|
+
detail_parts: list[str] = []
|
|
63
|
+
if isinstance(error, dict):
|
|
64
|
+
message = error.get("message") or error.get("code") or error.get("type")
|
|
65
|
+
if isinstance(message, str) and message:
|
|
66
|
+
detail_parts.append(message)
|
|
67
|
+
elif isinstance(error, str) and error:
|
|
68
|
+
detail_parts.append(error)
|
|
69
|
+
if isinstance(incomplete_details, dict):
|
|
70
|
+
reason = incomplete_details.get("reason") or incomplete_details.get("message")
|
|
71
|
+
if isinstance(reason, str) and reason:
|
|
72
|
+
detail_parts.append(reason)
|
|
73
|
+
elif isinstance(incomplete_details, str) and incomplete_details:
|
|
74
|
+
detail_parts.append(incomplete_details)
|
|
75
|
+
detail = "; ".join(detail_parts) if detail_parts else json.dumps(event, ensure_ascii=False, default=str)[:500]
|
|
76
|
+
return f"OpenAI protocol response {status}: {detail}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def reasoning_from_response_items(items: Iterable[dict[str, Any]]) -> str:
|
|
80
|
+
parts: list[str] = []
|
|
81
|
+
for item in items:
|
|
82
|
+
if item.get("type") != "reasoning":
|
|
83
|
+
continue
|
|
84
|
+
for field in ("summary", "content"):
|
|
85
|
+
value = item.get(field)
|
|
86
|
+
if isinstance(value, str) and value:
|
|
87
|
+
parts.append(value)
|
|
88
|
+
continue
|
|
89
|
+
if not isinstance(value, list):
|
|
90
|
+
continue
|
|
91
|
+
for part in value:
|
|
92
|
+
if isinstance(part, str) and part:
|
|
93
|
+
parts.append(part)
|
|
94
|
+
elif isinstance(part, dict):
|
|
95
|
+
text = part.get("text")
|
|
96
|
+
if isinstance(text, str) and text:
|
|
97
|
+
parts.append(text)
|
|
98
|
+
return "".join(parts)
|