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.
@@ -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
@@ -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)