python-codex 0.1.0__py3-none-any.whl → 0.1.2__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,63 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ import urllib.parse
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class CompatServerConfig:
10
+ host: str = "127.0.0.1"
11
+ port: int = 0
12
+ outcomming_base_url: str = "http://127.0.0.1:8000/v1"
13
+ outcomming_api_key_env: str | None = None
14
+ model_provider: str | None = None
15
+ timeout_seconds: float = 120.0
16
+
17
+ def outcomming_api_key(self) -> str | None:
18
+ if self.outcomming_api_key_env is None:
19
+ return None
20
+ value = os.environ.get(self.outcomming_api_key_env, "").strip()
21
+ return value or None
22
+
23
+ def outcomming_chat_completions_url(self) -> str:
24
+ base = self.outcomming_base_url.rstrip("/")
25
+ return f"{base}/chat/completions"
26
+
27
+ def outcomming_models_url(self) -> str:
28
+ base = self.outcomming_base_url.rstrip("/")
29
+ return f"{base}/models"
30
+
31
+ def with_ephemeral_port(self) -> CompatServerConfig:
32
+ return CompatServerConfig(
33
+ host=self.host,
34
+ port=0,
35
+ outcomming_base_url=self.outcomming_base_url,
36
+ outcomming_api_key_env=self.outcomming_api_key_env,
37
+ model_provider=self.model_provider,
38
+ timeout_seconds=self.timeout_seconds,
39
+ )
40
+
41
+ @classmethod
42
+ def from_base_url(
43
+ cls,
44
+ outcomming_base_url: str,
45
+ api_key_env: str | None = None,
46
+ model_provider: str | None = None,
47
+ ) -> CompatServerConfig:
48
+ parsed = urllib.parse.urlparse(outcomming_base_url)
49
+ if not parsed.scheme or not parsed.netloc:
50
+ raise ValueError(f"invalid outcomming base url: {outcomming_base_url}")
51
+ normalized_path = parsed.path.rstrip("/")
52
+ if normalized_path in {"", "/"}:
53
+ parsed = parsed._replace(path="/v1")
54
+ outcomming_base_url = urllib.parse.urlunparse(parsed)
55
+ else:
56
+ outcomming_base_url = urllib.parse.urlunparse(
57
+ parsed._replace(path=normalized_path)
58
+ )
59
+ return cls(
60
+ outcomming_base_url=outcomming_base_url,
61
+ outcomming_api_key_env=api_key_env,
62
+ model_provider=model_provider,
63
+ )
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ """Provider-specific post-process hooks for canonical outgoing chat requests.
4
+
5
+ Each downstream chat-completions provider may have its own payload quirks:
6
+ extra fields, removed fields, role normalization, tool-shape tweaks, etc.
7
+ Keep all of those provider-specific rewrites here so `StreamRouter` can keep
8
+ building one canonical `outcomming_request`, while `server.py` selects the
9
+ appropriate hook from `CompatServerConfig.model_provider`.
10
+ """
11
+
12
+ from collections.abc import Callable
13
+ from copy import deepcopy
14
+ from typing import Optional, TypedDict
15
+
16
+ ChatMessage = dict[str, object]
17
+
18
+
19
+ class OutgoingRequest(TypedDict):
20
+ """Canonical downstream `/v1/chat/completions` request shape.
21
+
22
+ `model`, `messages`, and `stream` are always populated by
23
+ `StreamRouter.build_outcomming_request(...)`. Provider-specific fields that
24
+ may be omitted use `Optional[...]` here so the schema stays simple and does
25
+ not rely on TypedDict inheritance.
26
+ """
27
+
28
+ model: str
29
+ messages: list[ChatMessage]
30
+ stream: bool
31
+ tools: Optional[list[dict[str, object]]]
32
+ tool_choice: Optional[object]
33
+ parallel_tool_calls: Optional[bool]
34
+
35
+
36
+ PayloadPostProcessor = Callable[[OutgoingRequest], OutgoingRequest]
37
+
38
+
39
+ def _identity(outcomming_request: OutgoingRequest) -> OutgoingRequest:
40
+ """Keep the canonical request unchanged."""
41
+
42
+ return outcomming_request
43
+
44
+
45
+ def _drop_developer_messages(outcomming_request: OutgoingRequest) -> OutgoingRequest:
46
+ """Remove all developer-role messages for providers that reject them."""
47
+
48
+ outcomming_request["messages"] = [
49
+ message
50
+ for message in outcomming_request["messages"]
51
+ if message.get("role") != "developer"
52
+ ]
53
+ return outcomming_request
54
+
55
+
56
+ PAYLOAD_POST_PROCESSORS: dict[str, PayloadPostProcessor] = {
57
+ "stepfun": _drop_developer_messages,
58
+ "vllm": _identity,
59
+ }
60
+ """Mapping from normalized `model_provider` name to payload rewrite hook."""
61
+
62
+
63
+ def post_process_outcomming_request(
64
+ outcomming_request: OutgoingRequest,
65
+ model_provider: str | None,
66
+ ) -> OutgoingRequest:
67
+ """Apply the provider-specific payload hook to one outgoing request.
68
+
69
+ This is the single wrapper around `PAYLOAD_POST_PROCESSORS`: it normalizes
70
+ the provider name, falls back to the default `vllm` behavior when the
71
+ provider is missing or unknown, deep-copies the canonical request, applies
72
+ the selected hook, and validates that the hook returns another request dict.
73
+ """
74
+
75
+ processed_request = deepcopy(outcomming_request)
76
+ provider_name = str(model_provider or "").strip().lower()
77
+ provider_processor = PAYLOAD_POST_PROCESSORS.get(
78
+ provider_name,
79
+ PAYLOAD_POST_PROCESSORS.get("vllm"),
80
+ )
81
+ if provider_processor is None:
82
+ return processed_request
83
+ processed_request = provider_processor(processed_request)
84
+ if not isinstance(processed_request, dict):
85
+ raise TypeError("payload processor must return a dict outcomming_request")
86
+ return processed_request
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from .config import CompatServerConfig
4
+ from .payload_processors import post_process_outcomming_request
5
+ from .session_store import SessionStore
6
+ from .stream_router import StreamRouter
7
+
8
+
9
+ class ResponseServer:
10
+ def __init__(
11
+ self,
12
+ config: CompatServerConfig,
13
+ session_store: SessionStore | None = None,
14
+ stream_router: StreamRouter | None = None,
15
+ ) -> None:
16
+ self._config = config
17
+ self._session_store = session_store or SessionStore()
18
+ self._stream_router = stream_router or StreamRouter(config)
19
+
20
+ @property
21
+ def config(self) -> CompatServerConfig:
22
+ return self._config
23
+
24
+ @property
25
+ def session_store(self) -> SessionStore:
26
+ return self._session_store
27
+
28
+ @property
29
+ def stream_router(self) -> StreamRouter:
30
+ return self._stream_router
31
+
32
+ def list_models(self) -> dict[str, object]:
33
+ return self._stream_router.list_models()
34
+
35
+ def start_response_stream(
36
+ self,
37
+ request_body: dict[str, object],
38
+ request_headers: dict[str, str],
39
+ ):
40
+ outcomming_request = self._stream_router.build_outcomming_request(request_body)
41
+ outcomming_request = post_process_outcomming_request(
42
+ outcomming_request,
43
+ self._config.model_provider,
44
+ )
45
+ custom_tool_names = self._stream_router.collect_custom_tool_names(request_body)
46
+ session_id = (
47
+ request_headers.get("x-client-request-id")
48
+ or str(request_body.get("prompt_cache_key", "")).strip()
49
+ or None
50
+ )
51
+ stored_response = self._session_store.create_response(
52
+ session_id=session_id,
53
+ model=str(outcomming_request["model"]),
54
+ )
55
+ incomming_stream = self._stream_router.open_outcomming_stream(
56
+ outcomming_request
57
+ )
58
+ return self._stream_router.route_stream(
59
+ incomming_stream,
60
+ stored_response,
61
+ outcomming_request,
62
+ custom_tool_names,
63
+ )
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import threading
5
+ import time
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class StoredResponse:
10
+ response_id: str
11
+ session_id: str | None
12
+ model: str
13
+ created_at: float
14
+
15
+
16
+ class SessionStore:
17
+ def __init__(self) -> None:
18
+ self._lock = threading.Lock()
19
+ self._next_response_number = 1
20
+ self._responses: dict[str, StoredResponse] = {}
21
+
22
+ def create_response(self, session_id: str | None, model: str) -> StoredResponse:
23
+ with self._lock:
24
+ response_id = f"resp_{self._next_response_number:08d}"
25
+ self._next_response_number += 1
26
+ stored = StoredResponse(
27
+ response_id=response_id,
28
+ session_id=session_id,
29
+ model=model,
30
+ created_at=time.time(),
31
+ )
32
+ self._responses[response_id] = stored
33
+ return stored
34
+
35
+ def get_response(self, response_id: str) -> StoredResponse | None:
36
+ with self._lock:
37
+ return self._responses.get(response_id)