From a781abb86238ea0d91e163f550bacbf02cf5f21e Mon Sep 17 00:00:00 2001 From: Florian Krebs Date: Fri, 22 May 2026 16:56:27 +0200 Subject: [PATCH] add --append-system-prompt, --max-turns, --max-budget-usd, --json-schema, --exclude-dynamic-system-prompt-sections mappings --- server.py | 136 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 26 deletions(-) diff --git a/server.py b/server.py index 37bf7f6..8b359ef 100644 --- a/server.py +++ b/server.py @@ -141,11 +141,17 @@ def _temperature_to_effort(temperature: float) -> str: def _build_command( model: str, - system_prompt: Optional[str], - effort: Optional[str] = None, - temperature: Optional[float] = None, - fallback_model: Optional[str] = None, -) -> list[str]: + system_prompt, + effort=None, + temperature=None, + fallback_model=None, + system_prompt_mode="replace", + max_turns=None, + max_budget_usd=None, + json_schema=None, + exclude_dynamic_sections=False, +): + import json as _json cmd = [ CLAUDE_BIN, "--print", @@ -160,13 +166,11 @@ def _build_command( "--model", model, ] - # Determine effort level to pass to CLI - resolved_effort: Optional[str] = None + resolved_effort = None if effort is not None and effort in _VALID_EFFORT_LEVELS: resolved_effort = effort elif temperature is not None: resolved_effort = _temperature_to_effort(temperature) - # If both are None, don't add --effort (let CLI use its default) if resolved_effort is not None: cmd += ["--effort", resolved_effort] @@ -175,29 +179,54 @@ def _build_command( cmd += ["--fallback-model", fallback_model] if system_prompt: - cmd += ["--system-prompt", system_prompt] + if system_prompt_mode == "append": + cmd += ["--append-system-prompt", system_prompt] + else: + cmd += ["--system-prompt", system_prompt] + + if max_turns is not None: + cmd += ["--max-turns", str(max_turns)] + + if max_budget_usd is not None: + cmd += ["--max-budget-usd", str(max_budget_usd)] + + if json_schema is not None: + cmd += ["--json-schema", _json.dumps(json_schema)] + + if exclude_dynamic_sections: + cmd += ["--exclude-dynamic-system-prompt-sections"] + return cmd - - # --------------------------------------------------------------------------- # Core: run subprocess and stream parsed events # --------------------------------------------------------------------------- async def _run_claude( model: str, - system_prompt: Optional[str], - turns: list[dict], - effort: Optional[str] = None, - temperature: Optional[float] = None, - fallback_model: Optional[str] = None, -) -> AsyncGenerator[dict, None]: + system_prompt, + turns: list, + effort=None, + temperature=None, + fallback_model=None, + system_prompt_mode="replace", + max_turns=None, + max_budget_usd=None, + json_schema=None, + exclude_dynamic_sections=False, +): """ Yields dicts with keys: {"event": "delta", "text": "..."} {"event": "done", "usage": {...}, "cost_usd": float | None, "rate_limit": dict | None} {"event": "error", "status": int, "message": "..."} """ - cmd = _build_command(model, system_prompt, effort=effort, temperature=temperature, fallback_model=fallback_model) + cmd = _build_command( + model, system_prompt, + effort=effort, temperature=temperature, fallback_model=fallback_model, + system_prompt_mode=system_prompt_mode, max_turns=max_turns, + max_budget_usd=max_budget_usd, json_schema=json_schema, + exclude_dynamic_sections=exclude_dynamic_sections, + ) log.debug("Spawning: %s", " ".join(cmd[:6]) + " ...") env = _subprocess_env() @@ -340,12 +369,17 @@ async def _run_claude( async def _stream_response( request: Request, model: str, - system_prompt: Optional[str], - turns: list[dict], - effort: Optional[str] = None, - temperature: Optional[float] = None, - fallback_model: Optional[str] = None, -) -> AsyncGenerator[str, None]: + system_prompt, + turns: list, + effort=None, + temperature=None, + fallback_model=None, + system_prompt_mode="replace", + max_turns=None, + max_budget_usd=None, + json_schema=None, + exclude_dynamic_sections=False, +): completion_id = f"chatcmpl-{uuid.uuid4().hex}" created = int(time.time()) @@ -354,6 +388,9 @@ async def _stream_response( async for ev in _run_claude( model, system_prompt, turns, effort=effort, temperature=temperature, fallback_model=fallback_model, + system_prompt_mode=system_prompt_mode, max_turns=max_turns, + max_budget_usd=max_budget_usd, json_schema=json_schema, + exclude_dynamic_sections=exclude_dynamic_sections, ): # Check if client disconnected if await request.is_disconnected(): @@ -501,10 +538,51 @@ async def chat_completions(request: Request): if fallback_model is not None: fallback_model = str(fallback_model) - # Warn about unsupported / silently-ignored params + # Extract new CLI-mapped parameters from extra_body + # system_prompt_mode: "replace" (default, --system-prompt) or "append" (--append-system-prompt) + system_prompt_mode: str = str(extra_body.get("system_prompt_mode", "replace")) + if system_prompt_mode not in ("replace", "append"): + system_prompt_mode = "replace" + + max_turns: Optional[int] = None + raw_max_turns = extra_body.get("max_turns") + if raw_max_turns is not None: + try: + max_turns = int(raw_max_turns) + except (TypeError, ValueError): + pass + + max_budget_usd: Optional[float] = None + raw_budget = extra_body.get("max_budget_usd") + if raw_budget is not None: + try: + max_budget_usd = float(raw_budget) + except (TypeError, ValueError): + pass + + # response_format.json_schema → --json-schema + json_schema: Optional[dict] = None + response_format = body.get("response_format") or {} + if isinstance(response_format, dict): + if response_format.get("type") == "json_schema": + js = response_format.get("json_schema") + if isinstance(js, dict): + # OpenAI wraps it: {name, strict, schema} — pass the schema sub-dict to CLI + json_schema = js.get("schema") or js + elif response_format.get("type") == "json_object": + # Instruct via system prompt append instead of --json-schema + json_schema = {"type": "object"} + # Also allow direct extra_body.json_schema override + if extra_body.get("json_schema"): + json_schema = extra_body["json_schema"] + + # exclude_dynamic_system_prompt_sections improves cache reuse in multi-user deployments + exclude_dynamic_sections: bool = bool(extra_body.get("exclude_dynamic_system_prompt_sections", False)) + + # Warn about other unsupported / silently-ignored params _IGNORED = {"top_p", "frequency_penalty", "presence_penalty", "logit_bias", "n", "best_of", "logprobs", "top_logprobs", - "response_format", "seed", "stop", "user"} + "seed", "stop", "user"} for param in _IGNORED: if param in body: log.debug("Ignoring unsupported parameter: %s", param) @@ -519,6 +597,9 @@ async def chat_completions(request: Request): gen = await _stream_response( request, model, system_prompt, turns, effort=effort, temperature=temperature, fallback_model=fallback_model, + system_prompt_mode=system_prompt_mode, max_turns=max_turns, + max_budget_usd=max_budget_usd, json_schema=json_schema, + exclude_dynamic_sections=exclude_dynamic_sections, ) return StreamingResponse( gen, @@ -539,6 +620,9 @@ async def chat_completions(request: Request): async for ev in _run_claude( model, system_prompt, turns, effort=effort, temperature=temperature, fallback_model=fallback_model, + system_prompt_mode=system_prompt_mode, max_turns=max_turns, + max_budget_usd=max_budget_usd, json_schema=json_schema, + exclude_dynamic_sections=exclude_dynamic_sections, ): if ev["event"] == "delta": full_text += ev["text"]