code-puppy 0.0.348__py3-none-any.whl → 0.0.372__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.
- code_puppy/agents/__init__.py +8 -0
- code_puppy/agents/agent_manager.py +272 -1
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +11 -8
- code_puppy/agents/event_stream_handler.py +101 -8
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/chatgpt_codex_client.py +53 -0
- code_puppy/claude_cache_client.py +294 -41
- code_puppy/command_line/add_model_menu.py +13 -4
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/core_commands.py +89 -112
- code_puppy/command_line/model_picker_completion.py +3 -20
- code_puppy/command_line/model_settings_menu.py +21 -3
- code_puppy/config.py +145 -70
- code_puppy/gemini_model.py +706 -0
- code_puppy/http_utils.py +6 -3
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +50 -16
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +27 -24
- code_puppy/models.json +12 -12
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
- code_puppy/plugins/antigravity_oauth/transport.py +236 -45
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
- code_puppy/plugins/claude_code_oauth/utils.py +4 -1
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +52 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +83 -33
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
- code_puppy/prompts/codex_system_prompt.md +0 -310
- code_puppy/tools/browser/camoufox_manager.py +0 -235
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,6 +5,11 @@ ClaudeCacheAsyncClient: httpx client that tries to patch /v1/messages bodies.
|
|
|
5
5
|
We now also expose `patch_anthropic_client_messages` which monkey-patches
|
|
6
6
|
AsyncAnthropic.messages.create() so we can inject cache_control BEFORE
|
|
7
7
|
serialization, avoiding httpx/Pydantic internals.
|
|
8
|
+
|
|
9
|
+
This module also handles:
|
|
10
|
+
- Tool name prefixing/unprefixing for Claude Code OAuth compatibility
|
|
11
|
+
- Header transformations (anthropic-beta, user-agent)
|
|
12
|
+
- URL modifications (adding ?beta=true query param)
|
|
8
13
|
"""
|
|
9
14
|
|
|
10
15
|
from __future__ import annotations
|
|
@@ -12,8 +17,10 @@ from __future__ import annotations
|
|
|
12
17
|
import base64
|
|
13
18
|
import json
|
|
14
19
|
import logging
|
|
20
|
+
import re
|
|
15
21
|
import time
|
|
16
22
|
from typing import Any, Callable, MutableMapping
|
|
23
|
+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
|
17
24
|
|
|
18
25
|
import httpx
|
|
19
26
|
|
|
@@ -22,6 +29,13 @@ logger = logging.getLogger(__name__)
|
|
|
22
29
|
# Refresh token if it's older than 1 hour (3600 seconds)
|
|
23
30
|
TOKEN_MAX_AGE_SECONDS = 3600
|
|
24
31
|
|
|
32
|
+
# Tool name prefix for Claude Code OAuth compatibility
|
|
33
|
+
# Tools are prefixed on outgoing requests and unprefixed on incoming responses
|
|
34
|
+
TOOL_PREFIX = "cp_"
|
|
35
|
+
|
|
36
|
+
# User-Agent to send with Claude Code OAuth requests
|
|
37
|
+
CLAUDE_CLI_USER_AGENT = "claude-cli/2.1.2 (external, cli)"
|
|
38
|
+
|
|
25
39
|
try:
|
|
26
40
|
from anthropic import AsyncAnthropic
|
|
27
41
|
except ImportError: # pragma: no cover - optional dep
|
|
@@ -29,6 +43,22 @@ except ImportError: # pragma: no cover - optional dep
|
|
|
29
43
|
|
|
30
44
|
|
|
31
45
|
class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
46
|
+
"""Async HTTP client with Claude Code OAuth transformations.
|
|
47
|
+
|
|
48
|
+
Handles:
|
|
49
|
+
- Cache control injection for prompt caching
|
|
50
|
+
- Tool name prefixing on outgoing requests
|
|
51
|
+
- Tool name unprefixing on incoming streaming responses
|
|
52
|
+
- Header transformations (anthropic-beta, user-agent)
|
|
53
|
+
- URL modifications (adding ?beta=true)
|
|
54
|
+
- Proactive token refresh
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Regex pattern for unprefixing tool names in streaming responses
|
|
58
|
+
_TOOL_UNPREFIX_PATTERN = re.compile(
|
|
59
|
+
rf'"name"\s*:\s*"{re.escape(TOOL_PREFIX)}([^"]+)"'
|
|
60
|
+
)
|
|
61
|
+
|
|
32
62
|
def _get_jwt_age_seconds(self, token: str | None) -> float | None:
|
|
33
63
|
"""Decode a JWT and return its age in seconds.
|
|
34
64
|
|
|
@@ -89,27 +119,156 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
89
119
|
return None
|
|
90
120
|
|
|
91
121
|
def _should_refresh_token(self, request: httpx.Request) -> bool:
|
|
92
|
-
"""Check if the token
|
|
122
|
+
"""Check if the token should be refreshed (within 1 hour of expiry).
|
|
123
|
+
|
|
124
|
+
Uses two strategies:
|
|
125
|
+
1. Decode JWT to check token age (if possible)
|
|
126
|
+
2. Fall back to stored expires_at from token file
|
|
127
|
+
|
|
128
|
+
Returns True if token expires within TOKEN_MAX_AGE_SECONDS (1 hour).
|
|
129
|
+
"""
|
|
93
130
|
token = self._extract_bearer_token(request)
|
|
94
131
|
if not token:
|
|
95
132
|
return False
|
|
96
133
|
|
|
134
|
+
# Strategy 1: Try to decode JWT age
|
|
97
135
|
age = self._get_jwt_age_seconds(token)
|
|
98
|
-
if age is None:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
136
|
+
if age is not None:
|
|
137
|
+
should_refresh = age >= TOKEN_MAX_AGE_SECONDS
|
|
138
|
+
if should_refresh:
|
|
139
|
+
logger.info(
|
|
140
|
+
"JWT token is %.1f seconds old (>= %d), will refresh proactively",
|
|
141
|
+
age,
|
|
142
|
+
TOKEN_MAX_AGE_SECONDS,
|
|
143
|
+
)
|
|
144
|
+
return should_refresh
|
|
145
|
+
|
|
146
|
+
# Strategy 2: Fall back to stored expires_at from token file
|
|
147
|
+
should_refresh = self._check_stored_token_expiry()
|
|
102
148
|
if should_refresh:
|
|
103
149
|
logger.info(
|
|
104
|
-
"
|
|
105
|
-
age,
|
|
150
|
+
"Stored token expires within %d seconds, will refresh proactively",
|
|
106
151
|
TOKEN_MAX_AGE_SECONDS,
|
|
107
152
|
)
|
|
108
153
|
return should_refresh
|
|
109
154
|
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _check_stored_token_expiry() -> bool:
|
|
157
|
+
"""Check if the stored token expires within TOKEN_MAX_AGE_SECONDS.
|
|
158
|
+
|
|
159
|
+
This is a fallback for when JWT decoding fails or isn't available.
|
|
160
|
+
Uses the expires_at timestamp from the stored token file.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
from code_puppy.plugins.claude_code_oauth.utils import (
|
|
164
|
+
is_token_expired,
|
|
165
|
+
load_stored_tokens,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
tokens = load_stored_tokens()
|
|
169
|
+
if not tokens:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# is_token_expired already uses TOKEN_REFRESH_BUFFER_SECONDS (1 hour)
|
|
173
|
+
return is_token_expired(tokens)
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
logger.debug("Error checking stored token expiry: %s", exc)
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _prefix_tool_names(body: bytes) -> bytes | None:
|
|
180
|
+
"""Prefix all tool names in the request body with TOOL_PREFIX.
|
|
181
|
+
|
|
182
|
+
This is required for Claude Code OAuth compatibility - tools must be
|
|
183
|
+
prefixed on outgoing requests and unprefixed on incoming responses.
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(body.decode("utf-8"))
|
|
187
|
+
except Exception:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
if not isinstance(data, dict):
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
tools = data.get("tools")
|
|
194
|
+
if not isinstance(tools, list) or not tools:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
modified = False
|
|
198
|
+
for tool in tools:
|
|
199
|
+
if isinstance(tool, dict) and "name" in tool:
|
|
200
|
+
name = tool["name"]
|
|
201
|
+
if name and not name.startswith(TOOL_PREFIX):
|
|
202
|
+
tool["name"] = f"{TOOL_PREFIX}{name}"
|
|
203
|
+
modified = True
|
|
204
|
+
|
|
205
|
+
if not modified:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
return json.dumps(data).encode("utf-8")
|
|
209
|
+
|
|
210
|
+
def _unprefix_tool_names_in_text(self, text: str) -> str:
|
|
211
|
+
"""Remove TOOL_PREFIX from tool names in streaming response text."""
|
|
212
|
+
return self._TOOL_UNPREFIX_PATTERN.sub(r'"name": "\1"', text)
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _transform_headers_for_claude_code(
|
|
216
|
+
headers: MutableMapping[str, str],
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Transform headers for Claude Code OAuth compatibility.
|
|
219
|
+
|
|
220
|
+
- Sets user-agent to claude-cli
|
|
221
|
+
- Merges anthropic-beta headers appropriately
|
|
222
|
+
- Removes x-api-key (using Bearer auth instead)
|
|
223
|
+
"""
|
|
224
|
+
# Set user-agent
|
|
225
|
+
headers["user-agent"] = CLAUDE_CLI_USER_AGENT
|
|
226
|
+
|
|
227
|
+
# Handle anthropic-beta header
|
|
228
|
+
incoming_beta = headers.get("anthropic-beta", "")
|
|
229
|
+
incoming_betas = [b.strip() for b in incoming_beta.split(",") if b.strip()]
|
|
230
|
+
|
|
231
|
+
# Check if claude-code beta was explicitly requested
|
|
232
|
+
include_claude_code = "claude-code-20250219" in incoming_betas
|
|
233
|
+
|
|
234
|
+
# Build merged betas list
|
|
235
|
+
merged_betas = [
|
|
236
|
+
"oauth-2025-04-20",
|
|
237
|
+
"interleaved-thinking-2025-05-14",
|
|
238
|
+
]
|
|
239
|
+
if include_claude_code:
|
|
240
|
+
merged_betas.append("claude-code-20250219")
|
|
241
|
+
|
|
242
|
+
headers["anthropic-beta"] = ",".join(merged_betas)
|
|
243
|
+
|
|
244
|
+
# Remove x-api-key if present (we use Bearer auth)
|
|
245
|
+
for key in ["x-api-key", "X-API-Key", "X-Api-Key"]:
|
|
246
|
+
if key in headers:
|
|
247
|
+
del headers[key]
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def _add_beta_query_param(url: httpx.URL) -> httpx.URL:
|
|
251
|
+
"""Add ?beta=true query parameter to the URL if not already present."""
|
|
252
|
+
# Parse the URL
|
|
253
|
+
parsed = urlparse(str(url))
|
|
254
|
+
query_params = parse_qs(parsed.query)
|
|
255
|
+
|
|
256
|
+
# Only add if not already present
|
|
257
|
+
if "beta" not in query_params:
|
|
258
|
+
query_params["beta"] = ["true"]
|
|
259
|
+
# Rebuild query string
|
|
260
|
+
new_query = urlencode(query_params, doseq=True)
|
|
261
|
+
# Rebuild URL
|
|
262
|
+
new_parsed = parsed._replace(query=new_query)
|
|
263
|
+
return httpx.URL(urlunparse(new_parsed))
|
|
264
|
+
|
|
265
|
+
return url
|
|
266
|
+
|
|
110
267
|
async def send(
|
|
111
268
|
self, request: httpx.Request, *args: Any, **kwargs: Any
|
|
112
269
|
) -> httpx.Response: # type: ignore[override]
|
|
270
|
+
is_messages_endpoint = request.url.path.endswith("/v1/messages")
|
|
271
|
+
|
|
113
272
|
# Proactive token refresh: check JWT age before every request
|
|
114
273
|
if not request.extensions.get("claude_oauth_refresh_attempted"):
|
|
115
274
|
try:
|
|
@@ -131,50 +290,88 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
131
290
|
except Exception as exc:
|
|
132
291
|
logger.debug("Error during proactive token refresh check: %s", exc)
|
|
133
292
|
|
|
134
|
-
|
|
135
|
-
|
|
293
|
+
# Apply Claude Code OAuth transformations for /v1/messages
|
|
294
|
+
if is_messages_endpoint:
|
|
295
|
+
try:
|
|
136
296
|
body_bytes = self._extract_body_bytes(request)
|
|
297
|
+
headers = dict(request.headers)
|
|
298
|
+
url = request.url
|
|
299
|
+
body_modified = False
|
|
300
|
+
headers_modified = False
|
|
301
|
+
|
|
302
|
+
# 1. Transform headers for Claude Code OAuth
|
|
303
|
+
self._transform_headers_for_claude_code(headers)
|
|
304
|
+
headers_modified = True
|
|
305
|
+
|
|
306
|
+
# 2. Add ?beta=true query param
|
|
307
|
+
url = self._add_beta_query_param(url)
|
|
308
|
+
|
|
309
|
+
# 3. Prefix tool names in request body
|
|
137
310
|
if body_bytes:
|
|
138
|
-
|
|
139
|
-
if
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
311
|
+
prefixed_body = self._prefix_tool_names(body_bytes)
|
|
312
|
+
if prefixed_body is not None:
|
|
313
|
+
body_bytes = prefixed_body
|
|
314
|
+
body_modified = True
|
|
315
|
+
|
|
316
|
+
# 4. Inject cache_control
|
|
317
|
+
cached_body = self._inject_cache_control(body_bytes)
|
|
318
|
+
if cached_body is not None:
|
|
319
|
+
body_bytes = cached_body
|
|
320
|
+
body_modified = True
|
|
321
|
+
|
|
322
|
+
# Rebuild request if anything changed
|
|
323
|
+
if body_modified or headers_modified or url != request.url:
|
|
324
|
+
try:
|
|
325
|
+
rebuilt = self.build_request(
|
|
326
|
+
method=request.method,
|
|
327
|
+
url=url,
|
|
328
|
+
headers=headers,
|
|
329
|
+
content=body_bytes,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Copy core internals so httpx uses the modified body/stream
|
|
333
|
+
if hasattr(rebuilt, "_content"):
|
|
334
|
+
setattr(request, "_content", rebuilt._content) # type: ignore[attr-defined]
|
|
335
|
+
if hasattr(rebuilt, "stream"):
|
|
336
|
+
request.stream = rebuilt.stream
|
|
337
|
+
if hasattr(rebuilt, "extensions"):
|
|
338
|
+
request.extensions = rebuilt.extensions
|
|
339
|
+
|
|
340
|
+
# Update URL
|
|
341
|
+
request.url = url
|
|
342
|
+
|
|
343
|
+
# Update headers
|
|
344
|
+
for key, value in headers.items():
|
|
345
|
+
request.headers[key] = value
|
|
346
|
+
|
|
347
|
+
# Ensure Content-Length matches the new body
|
|
348
|
+
if body_bytes:
|
|
349
|
+
request.headers["Content-Length"] = str(len(body_bytes))
|
|
350
|
+
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
logger.debug("Error rebuilding request: %s", exc)
|
|
353
|
+
|
|
354
|
+
except Exception as exc:
|
|
355
|
+
logger.debug("Error in Claude Code transformations: %s", exc)
|
|
356
|
+
|
|
357
|
+
# Send the request
|
|
166
358
|
response = await super().send(request, *args, **kwargs)
|
|
359
|
+
|
|
360
|
+
# Transform streaming response to unprefix tool names
|
|
361
|
+
if is_messages_endpoint and response.status_code == 200:
|
|
362
|
+
try:
|
|
363
|
+
response = self._wrap_response_with_tool_unprefixing(response, request)
|
|
364
|
+
except Exception as exc:
|
|
365
|
+
logger.debug("Error wrapping response for tool unprefixing: %s", exc)
|
|
366
|
+
|
|
367
|
+
# Handle auth errors with token refresh
|
|
167
368
|
try:
|
|
168
|
-
# Check for both 401 and 400 - Anthropic/Cloudflare may return 400 for auth errors
|
|
169
|
-
# Also check if it's a Cloudflare HTML error response
|
|
170
369
|
if response.status_code in (400, 401) and not request.extensions.get(
|
|
171
370
|
"claude_oauth_refresh_attempted"
|
|
172
371
|
):
|
|
173
|
-
# Determine if this is an auth error (including Cloudflare HTML errors)
|
|
174
372
|
is_auth_error = response.status_code == 401
|
|
175
373
|
|
|
176
374
|
if response.status_code == 400:
|
|
177
|
-
# Check if this is a Cloudflare HTML error
|
|
178
375
|
is_auth_error = self._is_cloudflare_html_error(response)
|
|
179
376
|
if is_auth_error:
|
|
180
377
|
logger.info(
|
|
@@ -203,8 +400,64 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
|
|
|
203
400
|
logger.warning("Token refresh failed, returning original error")
|
|
204
401
|
except Exception as exc:
|
|
205
402
|
logger.debug("Error during token refresh attempt: %s", exc)
|
|
403
|
+
|
|
206
404
|
return response
|
|
207
405
|
|
|
406
|
+
def _wrap_response_with_tool_unprefixing(
|
|
407
|
+
self, response: httpx.Response, request: httpx.Request
|
|
408
|
+
) -> httpx.Response:
|
|
409
|
+
"""Wrap a streaming response to unprefix tool names.
|
|
410
|
+
|
|
411
|
+
Creates a new response with a transformed stream that removes the
|
|
412
|
+
TOOL_PREFIX from tool names in the response body.
|
|
413
|
+
"""
|
|
414
|
+
original_stream = response.stream
|
|
415
|
+
unprefix_fn = self._unprefix_tool_names_in_text
|
|
416
|
+
|
|
417
|
+
class UnprefixingStream(httpx.AsyncByteStream):
|
|
418
|
+
"""Async byte stream that unprefixes tool names.
|
|
419
|
+
|
|
420
|
+
Inherits from httpx.AsyncByteStream to ensure proper stream interface.
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
def __init__(self, inner_stream: Any) -> None:
|
|
424
|
+
self._inner = inner_stream
|
|
425
|
+
|
|
426
|
+
async def __aiter__(self):
|
|
427
|
+
async for chunk in self._inner:
|
|
428
|
+
if isinstance(chunk, bytes):
|
|
429
|
+
text = chunk.decode("utf-8", errors="replace")
|
|
430
|
+
text = unprefix_fn(text)
|
|
431
|
+
yield text.encode("utf-8")
|
|
432
|
+
else:
|
|
433
|
+
yield chunk
|
|
434
|
+
|
|
435
|
+
async def aclose(self) -> None:
|
|
436
|
+
if hasattr(self._inner, "aclose"):
|
|
437
|
+
try:
|
|
438
|
+
result = self._inner.aclose()
|
|
439
|
+
# Handle both sync and async aclose
|
|
440
|
+
if hasattr(result, "__await__"):
|
|
441
|
+
await result
|
|
442
|
+
except Exception:
|
|
443
|
+
pass # Ignore close errors
|
|
444
|
+
elif hasattr(self._inner, "close"):
|
|
445
|
+
try:
|
|
446
|
+
self._inner.close()
|
|
447
|
+
except Exception:
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
# Create a new response with the transformed stream
|
|
451
|
+
# Must include request for raise_for_status() to work
|
|
452
|
+
new_response = httpx.Response(
|
|
453
|
+
status_code=response.status_code,
|
|
454
|
+
headers=response.headers,
|
|
455
|
+
stream=UnprefixingStream(original_stream),
|
|
456
|
+
extensions=response.extensions,
|
|
457
|
+
request=request,
|
|
458
|
+
)
|
|
459
|
+
return new_response
|
|
460
|
+
|
|
208
461
|
@staticmethod
|
|
209
462
|
def _extract_body_bytes(request: httpx.Request) -> bytes | None:
|
|
210
463
|
# Try public content first
|
|
@@ -626,12 +626,21 @@ class AddModelMenu:
|
|
|
626
626
|
elif model_type == "openai" and "gpt-5" in model.model_id:
|
|
627
627
|
# GPT-5 models have special settings
|
|
628
628
|
if "codex" in model.model_id:
|
|
629
|
-
config["supported_settings"] = [
|
|
629
|
+
config["supported_settings"] = [
|
|
630
|
+
"temperature",
|
|
631
|
+
"top_p",
|
|
632
|
+
"reasoning_effort",
|
|
633
|
+
]
|
|
630
634
|
else:
|
|
631
|
-
config["supported_settings"] = [
|
|
635
|
+
config["supported_settings"] = [
|
|
636
|
+
"temperature",
|
|
637
|
+
"top_p",
|
|
638
|
+
"reasoning_effort",
|
|
639
|
+
"verbosity",
|
|
640
|
+
]
|
|
632
641
|
else:
|
|
633
|
-
# Default settings for most models
|
|
634
|
-
config["supported_settings"] = ["temperature", "seed"]
|
|
642
|
+
# Default settings for most models
|
|
643
|
+
config["supported_settings"] = ["temperature", "seed", "top_p"]
|
|
635
644
|
|
|
636
645
|
return config
|
|
637
646
|
|