python-codex 0.1.1__py3-none-any.whl → 0.1.3__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.
- pycodex/__init__.py +5 -1
- pycodex/agent.py +39 -41
- pycodex/cli.py +51 -43
- pycodex/collaboration.py +6 -7
- pycodex/compat.py +99 -0
- pycodex/context.py +87 -87
- pycodex/doctor.py +40 -40
- pycodex/model.py +69 -69
- pycodex/portable.py +33 -33
- pycodex/portable_server.py +22 -21
- pycodex/protocol.py +84 -86
- pycodex/runtime.py +36 -35
- pycodex/runtime_services.py +72 -69
- pycodex/tools/agent_tool_schemas.py +0 -2
- pycodex/tools/apply_patch_tool.py +43 -44
- pycodex/tools/base_tool.py +35 -36
- pycodex/tools/close_agent_tool.py +2 -4
- pycodex/tools/code_mode_manager.py +61 -61
- pycodex/tools/exec_command_tool.py +5 -6
- pycodex/tools/exec_runtime.js +3 -3
- pycodex/tools/exec_tool.py +3 -5
- pycodex/tools/grep_files_tool.py +10 -11
- pycodex/tools/list_dir_tool.py +8 -9
- pycodex/tools/read_file_tool.py +13 -14
- pycodex/tools/request_permissions_tool.py +2 -4
- pycodex/tools/request_user_input_tool.py +13 -14
- pycodex/tools/resume_agent_tool.py +2 -4
- pycodex/tools/send_input_tool.py +8 -9
- pycodex/tools/shell_command_tool.py +5 -6
- pycodex/tools/shell_tool.py +5 -6
- pycodex/tools/spawn_agent_tool.py +4 -5
- pycodex/tools/unified_exec_manager.py +79 -61
- pycodex/tools/update_plan_tool.py +4 -5
- pycodex/tools/view_image_tool.py +4 -5
- pycodex/tools/wait_agent_tool.py +2 -4
- pycodex/tools/wait_tool.py +4 -5
- pycodex/tools/web_search_tool.py +1 -3
- pycodex/tools/write_stdin_tool.py +4 -5
- pycodex/utils/dotenv.py +6 -6
- pycodex/utils/get_env.py +57 -34
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/visualize.py +79 -79
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/METADATA +15 -9
- python_codex-0.1.3.dist-info/RECORD +74 -0
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/WHEEL +1 -1
- responses_server/__init__.py +17 -0
- responses_server/__main__.py +5 -0
- responses_server/app.py +227 -0
- responses_server/config.py +63 -0
- responses_server/payload_processors.py +86 -0
- responses_server/server.py +63 -0
- responses_server/session_store.py +37 -0
- responses_server/stream_router.py +784 -0
- responses_server/tools/__init__.py +4 -0
- responses_server/tools/custom_adapter.py +235 -0
- responses_server/tools/web_search.py +263 -0
- python_codex-0.1.1.dist-info/RECORD +0 -62
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/licenses/LICENSE +0 -0
responses_server/app.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
|
|
2
|
+
import argparse
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
import json
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import Iterator
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI, Request
|
|
12
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
13
|
+
import uvicorn
|
|
14
|
+
|
|
15
|
+
from .config import CompatServerConfig
|
|
16
|
+
from .server import ResponseServer
|
|
17
|
+
from .stream_router import OutcommingChatError, UnsupportedIncommingFeature
|
|
18
|
+
import typing
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run_uvicorn_server(server) -> 'None':
|
|
22
|
+
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
23
|
+
server.run()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _format_sse_event(event_name: 'str', payload: 'typing.Dict[str, object]') -> 'bytes':
|
|
27
|
+
data = json.dumps(payload, ensure_ascii=False)
|
|
28
|
+
return f"event: {event_name}\ndata: {data}\n\n".encode("utf-8")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _stream_events(response_server: 'ResponseServer', request_body: 'typing.Dict[str, object]', request_headers: 'typing.Dict[str, str]') -> 'Iterator[bytes]':
|
|
32
|
+
try:
|
|
33
|
+
event_iter = response_server.start_response_stream(request_body, request_headers)
|
|
34
|
+
for event_name, payload in event_iter:
|
|
35
|
+
yield _format_sse_event(event_name, payload)
|
|
36
|
+
except OutcommingChatError as exc:
|
|
37
|
+
yield _format_sse_event(
|
|
38
|
+
"response.failed",
|
|
39
|
+
{
|
|
40
|
+
"type": "response.failed",
|
|
41
|
+
"response": {
|
|
42
|
+
"error": {
|
|
43
|
+
"message": str(exc),
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_parser() -> 'argparse.ArgumentParser':
|
|
51
|
+
parser = argparse.ArgumentParser(
|
|
52
|
+
prog="python -m responses_server",
|
|
53
|
+
description=(
|
|
54
|
+
"Standalone localhost `/v1/responses` server that translates the "
|
|
55
|
+
"Codex/Responses subset onto an outcomming `/v1/chat/completions` backend."
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
59
|
+
parser.add_argument("--port", type=int, default=8001)
|
|
60
|
+
parser.add_argument("--outcomming-base-url", required=True)
|
|
61
|
+
parser.add_argument("--outcomming-api-key-env", default=None)
|
|
62
|
+
parser.add_argument("--model-provider", default=None)
|
|
63
|
+
parser.add_argument("--timeout-seconds", type=float, default=120.0)
|
|
64
|
+
return parser
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_server(config: 'CompatServerConfig') -> 'None':
|
|
68
|
+
uvicorn.run(
|
|
69
|
+
ManagedResponseServer.build_app(config),
|
|
70
|
+
host=config.host,
|
|
71
|
+
port=config.port,
|
|
72
|
+
log_level="info",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def launch_chat_completion_compat_server(
|
|
77
|
+
base_url: 'str',
|
|
78
|
+
api_key_env: 'typing.Union[str, None]' = None,
|
|
79
|
+
model_provider: 'typing.Union[str, None]' = None,
|
|
80
|
+
):
|
|
81
|
+
config = CompatServerConfig.from_base_url(
|
|
82
|
+
base_url,
|
|
83
|
+
api_key_env,
|
|
84
|
+
model_provider=model_provider,
|
|
85
|
+
)
|
|
86
|
+
server = ManagedResponseServer(config)
|
|
87
|
+
server.start()
|
|
88
|
+
return server
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ManagedResponseServer:
|
|
92
|
+
@staticmethod
|
|
93
|
+
def build_app(
|
|
94
|
+
config: 'CompatServerConfig',
|
|
95
|
+
session_store=None,
|
|
96
|
+
stream_router=None,
|
|
97
|
+
) -> 'FastAPI':
|
|
98
|
+
response_server = ResponseServer(
|
|
99
|
+
config,
|
|
100
|
+
session_store=session_store,
|
|
101
|
+
stream_router=stream_router,
|
|
102
|
+
)
|
|
103
|
+
app = FastAPI(title="ResponsesCompat", version="0.1.0")
|
|
104
|
+
app.state.response_server = response_server
|
|
105
|
+
|
|
106
|
+
@app.get("/health")
|
|
107
|
+
@app.get("/healthz")
|
|
108
|
+
async def health() -> 'typing.Dict[str, bool]':
|
|
109
|
+
return {"ok": True}
|
|
110
|
+
|
|
111
|
+
@app.get("/models")
|
|
112
|
+
@app.get("/v1/models")
|
|
113
|
+
async def list_models():
|
|
114
|
+
try:
|
|
115
|
+
return response_server.list_models()
|
|
116
|
+
except OutcommingChatError as exc:
|
|
117
|
+
return JSONResponse(
|
|
118
|
+
{"error": {"message": str(exc)}},
|
|
119
|
+
status_code=502,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@app.post("/responses")
|
|
123
|
+
@app.post("/v1/responses")
|
|
124
|
+
async def responses(request: 'Request'):
|
|
125
|
+
try:
|
|
126
|
+
request_body = await request.json()
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
return JSONResponse(
|
|
129
|
+
{"error": {"message": f"invalid JSON body: {exc}"}},
|
|
130
|
+
status_code=400,
|
|
131
|
+
)
|
|
132
|
+
if not isinstance(request_body, dict):
|
|
133
|
+
return JSONResponse(
|
|
134
|
+
{"error": {"message": "request body must be a JSON object"}},
|
|
135
|
+
status_code=400,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
request_headers = {
|
|
139
|
+
str(key).lower(): str(value)
|
|
140
|
+
for key, value in request.headers.items()
|
|
141
|
+
}
|
|
142
|
+
try:
|
|
143
|
+
response_server.stream_router.validate_incomming_request(request_body)
|
|
144
|
+
except UnsupportedIncommingFeature as exc:
|
|
145
|
+
return JSONResponse(
|
|
146
|
+
{"error": {"message": str(exc)}},
|
|
147
|
+
status_code=501,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return StreamingResponse(
|
|
151
|
+
_stream_events(response_server, request_body, request_headers),
|
|
152
|
+
media_type="text/event-stream",
|
|
153
|
+
headers={
|
|
154
|
+
"Cache-Control": "no-cache",
|
|
155
|
+
"Connection": "close",
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return app
|
|
160
|
+
|
|
161
|
+
def __init__(self, config: 'CompatServerConfig') -> 'None':
|
|
162
|
+
port = config.port or _reserve_free_port()
|
|
163
|
+
self._config = replace(config, port=port)
|
|
164
|
+
self._app = self.build_app(self._config)
|
|
165
|
+
self._uvicorn_config = uvicorn.Config(
|
|
166
|
+
self._app,
|
|
167
|
+
host=self._config.host,
|
|
168
|
+
port=self._config.port,
|
|
169
|
+
log_level="error",
|
|
170
|
+
access_log=False,
|
|
171
|
+
)
|
|
172
|
+
self._server = uvicorn.Server(self._uvicorn_config)
|
|
173
|
+
self._thread = threading.Thread(
|
|
174
|
+
target=_run_uvicorn_server,
|
|
175
|
+
args=(self._server,),
|
|
176
|
+
daemon=True,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def base_url(self) -> 'str':
|
|
181
|
+
return f"http://{self._config.host}:{self._config.port}/v1"
|
|
182
|
+
|
|
183
|
+
def start(self, timeout_seconds: 'float' = 10.0) -> 'None':
|
|
184
|
+
self._thread.start()
|
|
185
|
+
deadline = time.time() + timeout_seconds
|
|
186
|
+
while not self._server.started:
|
|
187
|
+
if time.time() >= deadline:
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"timed out waiting for managed responses server to start"
|
|
190
|
+
)
|
|
191
|
+
time.sleep(0.01)
|
|
192
|
+
|
|
193
|
+
def stop(self, timeout_seconds: 'float' = 5.0) -> 'None':
|
|
194
|
+
self._server.should_exit = True
|
|
195
|
+
self._thread.join(timeout=timeout_seconds)
|
|
196
|
+
if self._thread.is_alive():
|
|
197
|
+
raise RuntimeError(
|
|
198
|
+
"timed out waiting for managed responses server to stop"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main() -> 'None':
|
|
203
|
+
args = build_parser().parse_args()
|
|
204
|
+
run_server(
|
|
205
|
+
CompatServerConfig(
|
|
206
|
+
host=args.host,
|
|
207
|
+
port=args.port,
|
|
208
|
+
outcomming_base_url=args.outcomming_base_url,
|
|
209
|
+
outcomming_api_key_env=args.outcomming_api_key_env,
|
|
210
|
+
model_provider=args.model_provider,
|
|
211
|
+
timeout_seconds=args.timeout_seconds,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
main()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _reserve_free_port() -> 'int':
|
|
221
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
222
|
+
try:
|
|
223
|
+
sock.bind(("127.0.0.1", 0))
|
|
224
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
225
|
+
return int(sock.getsockname()[1])
|
|
226
|
+
finally:
|
|
227
|
+
sock.close()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import urllib.parse
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=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: 'typing.Union[str, None]' = None
|
|
14
|
+
model_provider: 'typing.Union[str, None]' = None
|
|
15
|
+
timeout_seconds: 'float' = 120.0
|
|
16
|
+
|
|
17
|
+
def outcomming_api_key(self) -> 'typing.Union[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: 'typing.Union[str, None]' = None,
|
|
46
|
+
model_provider: 'typing.Union[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
|
+
|
|
2
|
+
"""Provider-specific post-process hooks for canonical outgoing chat requests.
|
|
3
|
+
|
|
4
|
+
Each downstream chat-completions provider may have its own payload quirks:
|
|
5
|
+
extra fields, removed fields, role normalization, tool-shape tweaks, etc.
|
|
6
|
+
Keep all of those provider-specific rewrites here so `StreamRouter` can keep
|
|
7
|
+
building one canonical `outcomming_request`, while `server.py` selects the
|
|
8
|
+
appropriate hook from `CompatServerConfig.model_provider`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
from typing import Callable, Optional
|
|
13
|
+
import typing
|
|
14
|
+
from typing_extensions import TypedDict
|
|
15
|
+
|
|
16
|
+
ChatMessage = typing.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: 'typing.List[ChatMessage]'
|
|
30
|
+
stream: 'bool'
|
|
31
|
+
tools: 'Optional[typing.List[typing.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: 'typing.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: 'typing.Union[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
|
+
|
|
2
|
+
from .config import CompatServerConfig
|
|
3
|
+
from .payload_processors import post_process_outcomming_request
|
|
4
|
+
from .session_store import SessionStore
|
|
5
|
+
from .stream_router import StreamRouter
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResponseServer:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
config: 'CompatServerConfig',
|
|
13
|
+
session_store: 'typing.Union[SessionStore, None]' = None,
|
|
14
|
+
stream_router: 'typing.Union[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) -> 'typing.Dict[str, object]':
|
|
33
|
+
return self._stream_router.list_models()
|
|
34
|
+
|
|
35
|
+
def start_response_stream(
|
|
36
|
+
self,
|
|
37
|
+
request_body: 'typing.Dict[str, object]',
|
|
38
|
+
request_headers: 'typing.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
|
+
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, )
|
|
9
|
+
class StoredResponse:
|
|
10
|
+
response_id: 'str'
|
|
11
|
+
session_id: 'typing.Union[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: 'typing.Dict[str, StoredResponse]' = {}
|
|
21
|
+
|
|
22
|
+
def create_response(self, session_id: 'typing.Union[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') -> 'typing.Union[StoredResponse, None]':
|
|
36
|
+
with self._lock:
|
|
37
|
+
return self._responses.get(response_id)
|