From 18b1077244d54ea200797f567f264c1d72eccd3f Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 2 Sep 2026 22:00:00 -0400 Subject: [PATCH 1/3] refactor: adopt drafter terminology --- PROVIDER_CONTRACT.md | 8 +- .../litellm/with_directive_drafter.py | 51 +++++---- .../litellm_proxy/README.md | 12 +- ...ler_precall_hook_with_directive_drafter.py | 23 ++-- .../openwebui_pipe/README.md | 22 ++-- .../open_webui_pipe_with_directive_drafter.py | 103 ++++++++++-------- ...st_litellm_proxy_with_directive_drafter.py | 53 ++++++--- .../test_litellm_with_directive_drafter.py | 27 ++++- ...t_openwebui_pipe_with_directive_drafter.py | 94 ++++++++++------ 9 files changed, 249 insertions(+), 144 deletions(-) diff --git a/PROVIDER_CONTRACT.md b/PROVIDER_CONTRACT.md index 70e2304..2ff230f 100644 --- a/PROVIDER_CONTRACT.md +++ b/PROVIDER_CONTRACT.md @@ -14,7 +14,7 @@ Optional: ```shell export PROVIDER=openai export MODEL=openai/gpt-4o-mini -export PREPROCESSOR_MODEL=openai/gpt-4o-mini +export DRAFTER_MODEL=openai/gpt-4o-mini export OPENAI_BASE_URL=... ``` @@ -48,8 +48,10 @@ Startup emits one concise config line showing resolved `mode`, `base_url`, `model`, and resolution `source` (`default`, `PROVIDER`, or `OPENAI_BASE_URL override`). -`MODEL` and `PREPROCESSOR_MODEL` use LiteLLM format: `/`. -`PREPROCESSOR_MODEL` is optional and defaults to `MODEL`. +`MODEL` and `DRAFTER_MODEL` use LiteLLM format: `/`. +`DRAFTER_MODEL` is optional and defaults to `MODEL`. `PREPROCESSOR_MODEL` is +deprecated but remains supported as a compatibility alias; `DRAFTER_MODEL` +wins when both are set. The directive-drafter integration always uses heuristic-first processing with the configured fallback model when needed. diff --git a/python/examples/prompt_construction/litellm/with_directive_drafter.py b/python/examples/prompt_construction/litellm/with_directive_drafter.py index b648a72..428f895 100644 --- a/python/examples/prompt_construction/litellm/with_directive_drafter.py +++ b/python/examples/prompt_construction/litellm/with_directive_drafter.py @@ -111,7 +111,7 @@ def _build_trace_text( *, original_input: str, compiler_input: str, - preprocessor_output: str | None, + drafter_output: str | None, decision: Decision | DecisionKind, premise_before: str | None, policies_before: Mapping[str, PolicyValue], @@ -124,7 +124,7 @@ def _build_trace_text( "Context Compiler trace", f"- original_input: {original_input}", f"- compiler_input: {compiler_input}", - f"- preprocessor_output: {preprocessor_output if preprocessor_output is not None else '(none)'}", + f"- drafter_output: {drafter_output if drafter_output is not None else '(none)'}", f"- decision: {kind}", f"- llm_called: {'yes' if llm_called else 'no'}", ] @@ -224,20 +224,25 @@ def _create_directive_drafter( def _get_directive_drafter() -> DirectiveDrafter: config = resolve_provider_config(default_model="openai/gpt-4o-mini") - preprocessor_model = os.getenv("PREPROCESSOR_MODEL", "").strip() or config.model - return _create_directive_drafter( - preprocessor_model, config.api_key, config.base_url - ) - - -def _preprocess_user_input(message: str) -> str | None: + drafter_model = os.getenv("DRAFTER_MODEL", "").strip() + if not drafter_model: + drafter_model = os.getenv("PREPROCESSOR_MODEL", "").strip() + if drafter_model: + logger.warning( + "PREPROCESSOR_MODEL is deprecated; use DRAFTER_MODEL instead" + ) + drafter_model = drafter_model or config.model + return _create_directive_drafter(drafter_model, config.api_key, config.base_url) + + +def _draft_user_input(message: str) -> str | None: try: drafted_result = _get_directive_drafter().draft_directive(message) - logger.debug("preprocessor: drafted_result=%r", drafted_result) + logger.debug("drafter: drafted_result=%r", drafted_result) return _extract_drafted_text(drafted_result) except Exception: # Safe no-op fallback: if drafter path fails, preserve basic behavior. - logger.debug("preprocessor: drafter_exception", exc_info=True) + logger.debug("drafter: exception", exc_info=True) return None return None @@ -258,7 +263,7 @@ def _append_trace( *, original_input: str, compiler_input: str, - preprocessor_output: str | None, + drafter_output: str | None, decision: Decision | DecisionKind, state_before: tuple[str | None, dict[str, PolicyValue]], state_after: tuple[str | None, dict[str, PolicyValue]], @@ -269,7 +274,7 @@ def _append_trace( trace_text = _build_trace_text( original_input=original_input, compiler_input=compiler_input, - preprocessor_output=preprocessor_output, + drafter_output=drafter_output, decision=decision, premise_before=state_before[0], policies_before=state_before[1], @@ -293,30 +298,30 @@ def handle_turn( approval_handler: ApprovalHandler = _default_approval_handler, ) -> str: state_before = (engine.premise, dict(engine.policies)) - preprocessd = _preprocess_user_input(user_input) - if preprocessd is None: + drafted_input = _draft_user_input(user_input) + if drafted_input is None: messages = _build_messages(user_input, engine) response_text = _call_litellm(messages) return _append_trace( response_text, original_input=user_input, compiler_input=user_input, - preprocessor_output=None, + drafter_output=None, decision=DecisionKind.NO_DIRECTIVE, state_before=state_before, state_after=(engine.premise, dict(engine.policies)), llm_called=True, ) - compile_input = preprocessd - logger.debug("preprocessor: engine_input=directive") + compile_input = drafted_input + logger.debug("drafter: engine_input=directive") approved = approval_handler(compile_input) if not approved: return _append_trace( "Directive rejected. No state change applied.", original_input=user_input, compiler_input=compile_input, - preprocessor_output=preprocessd, + drafter_output=drafted_input, decision=DecisionKind.NO_DIRECTIVE, state_before=state_before, state_after=(engine.premise, dict(engine.policies)), @@ -330,7 +335,7 @@ def handle_turn( kind = DECISION_UPDATE else: kind = DecisionKind.NO_DIRECTIVE.value - logger.debug("preprocessor: decision=%s", kind) + logger.debug("drafter: decision=%s", kind) if decision.kind == DecisionKind.ERROR: response_text = ( @@ -340,7 +345,7 @@ def handle_turn( response_text, original_input=user_input, compiler_input=compile_input, - preprocessor_output=preprocessd, + drafter_output=drafted_input, decision=decision, state_before=state_before, state_after=(engine.premise, dict(engine.policies)), @@ -352,7 +357,7 @@ def handle_turn( response_text, original_input=user_input, compiler_input=compile_input, - preprocessor_output=preprocessd, + drafter_output=drafted_input, decision=decision, state_before=state_before, state_after=(engine.premise, dict(engine.policies)), @@ -364,7 +369,7 @@ def handle_turn( response_text, original_input=user_input, compiler_input=compile_input, - preprocessor_output=preprocessd, + drafter_output=drafted_input, decision=decision, state_before=state_before, state_after=(engine.premise, dict(engine.policies)), diff --git a/python/reference_integrations/litellm_proxy/README.md b/python/reference_integrations/litellm_proxy/README.md index 1461198..bca1e90 100644 --- a/python/reference_integrations/litellm_proxy/README.md +++ b/python/reference_integrations/litellm_proxy/README.md @@ -201,10 +201,12 @@ python.reference_integrations.litellm_proxy.context_compiler_precall_hook.proxy_ Optional env vars for directive-drafter fallback: ```shell -export PREPROCESSOR_MODEL=openai/gpt-4o-mini +export DRAFTER_MODEL=openai/gpt-4o-mini ``` -`PREPROCESSOR_MODEL` is optional and defaults to `MODEL`. +`DRAFTER_MODEL` is optional and defaults to `MODEL`. `PREPROCESSOR_MODEL` is +deprecated but remains supported as a compatibility alias; `DRAFTER_MODEL` +wins when both are set. The directive-drafter integration always uses heuristic-first processing with the configured fallback model when needed. @@ -213,7 +215,7 @@ the configured fallback model when needed. - Mixed-content user messages compile only text segments from the latest user turn. -- `MODEL` and `PREPROCESSOR_MODEL` use LiteLLM format: `/`. +- `MODEL` and `DRAFTER_MODEL` use LiteLLM format: `/`. - Corrupt or incompatible checkpoints fail clearly in persistent mode and do not silently reset state. - In the directive-drafter hook, drafter state context now comes from restored @@ -230,8 +232,8 @@ the configured fallback model when needed. explicit `stateless` mode - proxy starts but upstream calls fail: check `OPENAI_API_KEY` and upstream model/provider config in `config.example.yaml` -- directive-drafter fallback issues: `PREPROCESSOR_MODEL` defaults to `MODEL`; - set it explicitly only when using a separate fallback model +- directive-drafter fallback issues: `DRAFTER_MODEL` defaults to `MODEL`; + `PREPROCESSOR_MODEL` remains available as a deprecated compatibility alias ## Opt-in Runtime Smoke Test diff --git a/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py b/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py index 2a46670..ff46f69 100644 --- a/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py +++ b/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py @@ -76,17 +76,23 @@ def _create_directive_drafter( def _get_directive_drafter() -> DirectiveDrafter: - preprocessor_model = os.getenv("PREPROCESSOR_MODEL", "").strip() - if not preprocessor_model: - preprocessor_model = os.getenv("MODEL", "").strip() - if not preprocessor_model: + drafter_model = os.getenv("DRAFTER_MODEL", "").strip() + if not drafter_model: + drafter_model = os.getenv("PREPROCESSOR_MODEL", "").strip() + if drafter_model: + logger.warning( + "PREPROCESSOR_MODEL is deprecated; use DRAFTER_MODEL instead" + ) + if not drafter_model: + drafter_model = os.getenv("MODEL", "").strip() + if not drafter_model: return DirectiveDrafter() api_key = os.getenv("OPENAI_API_KEY") if not api_key: return DirectiveDrafter() return _create_directive_drafter( - preprocessor_model, api_key, os.getenv("OPENAI_BASE_URL") or None + drafter_model, api_key, os.getenv("OPENAI_BASE_URL") or None ) @@ -98,7 +104,7 @@ def _draft_last_user_message(message: str) -> DraftResult: return DirectiveDrafter().draft_directive(message) -class ContextCompilerPreCallHookWithPreprocessor(CustomLogger): +class ContextCompilerPreCallHookWithDrafter(CustomLogger): async def async_pre_call_hook( self, user_api_key_dict: Any, @@ -181,4 +187,7 @@ async def async_pre_call_hook( return data -proxy_handler_instance = ContextCompilerPreCallHookWithPreprocessor() +# Import-compatible alias for existing LiteLLM proxy configurations. +ContextCompilerPreCallHookWithPreprocessor = ContextCompilerPreCallHookWithDrafter + +proxy_handler_instance = ContextCompilerPreCallHookWithDrafter() diff --git a/python/reference_integrations/openwebui_pipe/README.md b/python/reference_integrations/openwebui_pipe/README.md index f155371..6ba7f52 100644 --- a/python/reference_integrations/openwebui_pipe/README.md +++ b/python/reference_integrations/openwebui_pipe/README.md @@ -2,7 +2,7 @@ Saved compiler state changes which turns the pipe handles locally and what it forwards downstream. These examples show Open WebUI pipe behavior with and -without Directive Drafter preprocessing. +without Directive Drafter drafting. ## Core behavior @@ -69,8 +69,10 @@ If using `open_webui_pipe_with_directive_drafter.py`: - Install directive-drafter support if needed: `pip install "context-compiler>=0.9.0dev13" "context-compiler-directive-drafter>=0.2.0dev5"` -- Optionally set `PREPROCESSOR_MODEL_ID` to use a separate fallback drafting model -- If `PREPROCESSOR_MODEL_ID` is unset, fallback uses `BASE_MODEL_ID` +- Optionally set `DRAFTER_MODEL_ID` to use a separate fallback drafting model +- `PREPROCESSOR_MODEL_ID` remains supported as a deprecated compatibility alias; + `DRAFTER_MODEL_ID` wins when both are set +- If neither model id is set, fallback uses `BASE_MODEL_ID` Model fallback output is structurally validated before handoff. This does not prove that the model interpreted the user correctly. The automated fallback path is experimental pending a separate source-aware acceptance policy and reviewed drafting workflow. @@ -91,7 +93,7 @@ If frontmatter dependency installs are disabled, offline, or unavailable: ### Finding valid model ids Use the Open WebUI model picker/list to copy exact model ids for `BASE_MODEL_ID` -(and optional `PREPROCESSOR_MODEL_ID` for the directive-drafter pipe). +(and optional `DRAFTER_MODEL_ID` for the directive-drafter pipe). ## Verify behavior @@ -129,7 +131,7 @@ Advanced check: ### Directive-drafter pipe -Use this pipe when you want the same runtime behavior plus Directive Drafter preprocessing. +Use this pipe when you want the same runtime behavior plus Directive Drafter drafting. When the drafter produces a `CanonicalDirective`, the pipe uses Open WebUI's native `__event_call__` confirmation dialog for HITL approval. The lifecycle is: @@ -240,13 +242,13 @@ rejection flows. - `BASE_MODEL_ID is required`: set a valid Open WebUI model id in the function valves, or enable `ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true` only for local testing. - `BASE_MODEL_ID was not found in Open WebUI models`: copy the exact id from `Admin Panel → Settings → Models`. -- `PREPROCESSOR_MODEL_ID was not found in Open WebUI models`: set a valid fallback model id or leave it unset to default to `BASE_MODEL_ID`. -- `PREPROCESSOR_MODEL_ID must not match the selected pipe model id`: choose a real backend model id, not the pipe model id itself. -- `PREPROCESSOR_MODEL_ID is invalid or not configured in Open WebUI`: the fallback route hit a missing model; fix the configured fallback model or unset it to reuse `BASE_MODEL_ID`. +- `DRAFTER_MODEL_ID was not found in Open WebUI models`: set a valid fallback model id or leave it unset to default to `BASE_MODEL_ID`. +- `DRAFTER_MODEL_ID must not match the selected pipe model id`: choose a real backend model id, not the pipe model id itself. +- `DRAFTER_MODEL_ID is invalid or not configured in Open WebUI`: the fallback route hit a missing model; fix the configured fallback model or unset it to reuse `BASE_MODEL_ID`. - `ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true`: directive-only updates still run locally, but passthrough returns a deterministic debug message instead of calling a downstream model. - imports fail after function upload: install `context-compiler>=0.9.0dev13` in the Open WebUI runtime, and add `context-compiler-directive-drafter>=0.2.0dev5` only for the Directive Drafter pipe, because the copied function runs from a temp/cached location. ## Fallback notes -- Fallback drafting uses `PREPROCESSOR_MODEL_ID` first, while the main passthrough path still forwards with `BASE_MODEL_ID`. -- If the fallback model returns `model not found`, the pipe normalizes that into the deterministic `PREPROCESSOR_MODEL_ID` misconfiguration message above. +- Fallback drafting uses `DRAFTER_MODEL_ID` first, then the deprecated `PREPROCESSOR_MODEL_ID` alias, while the main passthrough path still forwards with `BASE_MODEL_ID`. +- If the fallback model returns `model not found`, the pipe normalizes that into the deterministic `DRAFTER_MODEL_ID` misconfiguration message above. diff --git a/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py b/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py index 6ad328d..09b559a 100644 --- a/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +++ b/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py @@ -377,12 +377,16 @@ class Valves(BaseModel): "llama3.1:8b." ), ) - PREPROCESSOR_MODEL_ID: str | None = Field( + DRAFTER_MODEL_ID: str | None = Field( default=None, description=( "Optional model id for fallback drafting (defaults to BASE_MODEL_ID)." ), ) + PREPROCESSOR_MODEL_ID: str | None = Field( + default=None, + description=("Deprecated compatibility alias for DRAFTER_MODEL_ID."), + ) ALLOW_MISSING_BASE_MODEL_FOR_DEBUG: bool = Field( default=False, description="Allow missing BASE_MODEL_ID for debug/testing only.", @@ -394,7 +398,7 @@ class Valves(BaseModel): def __init__(self) -> None: self.valves = self.Valves() - self._last_preprocessor_error: str | None = None + self._last_drafter_error: str | None = None def _allow_missing_base_model_for_debug(self) -> bool: return _is_truthy_bool( @@ -498,12 +502,12 @@ def _with_trace( state_before: object, state_after: object, llm_called: bool, - preprocessor_output: str | None = None, + drafter_output: str | None = None, state_injected: str = "no", ) -> Any: if not self._trace_enabled(): return response - del original_input, compiler_input, preprocessor_output + del original_input, compiler_input, drafter_output trace_text = _build_compact_trace_text( decision=decision, state_before=state_before, @@ -548,30 +552,42 @@ def _normalize_forward_exception(self, exc: Exception) -> str | None: ) return None - def _normalize_preprocessor_error(self, response: Any) -> str | None: + def _normalize_drafter_error(self, response: Any) -> str | None: if self._contains_model_not_found(response): return ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or " + "Context Compiler pipe misconfigured: DRAFTER_MODEL_ID is invalid or " "not configured in Open WebUI. Configure a valid model id in " "Admin Panel → Settings → Models." ) return None - def _normalize_preprocessor_exception(self, exc: Exception) -> str | None: + def _normalize_drafter_exception(self, exc: Exception) -> str | None: detail = getattr(exc, "detail", None) if self._contains_model_not_found(detail) or self._contains_model_not_found( str(exc) ): return ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or " + "Context Compiler pipe misconfigured: DRAFTER_MODEL_ID is invalid or " "not configured in Open WebUI. Configure a valid model id in " "Admin Panel → Settings → Models." ) return None - def _resolve_preprocessor_model_id(self, base_model_id: str | None) -> str | None: - preprocessor_model_id = _normalize_model_id(self.valves.PREPROCESSOR_MODEL_ID) - return preprocessor_model_id or base_model_id + def _resolve_drafter_model_id(self, base_model_id: str | None) -> str | None: + drafter_model_id = _normalize_model_id( + getattr(self.valves, "DRAFTER_MODEL_ID", None) + ) + if drafter_model_id: + return drafter_model_id + legacy_model_id = _normalize_model_id( + getattr(self.valves, "PREPROCESSOR_MODEL_ID", None) + ) + if legacy_model_id: + logger.warning( + "PREPROCESSOR_MODEL_ID is deprecated; use DRAFTER_MODEL_ID instead" + ) + return legacy_model_id + return base_model_id async def _validate_configured_model_ids( self, @@ -579,10 +595,10 @@ async def _validate_configured_model_ids( user_payload: dict[str, Any], *, base_model_id: str | None, - preprocessor_model_id: str | None, + drafter_model_id: str | None, ) -> str | None: base_model_id = _normalize_model_id(base_model_id) - preprocessor_model_id = _normalize_model_id(preprocessor_model_id) + drafter_model_id = _normalize_model_id(drafter_model_id) # Best-effort preflight: fail closed only for clear missing-model mismatches. # If model discovery fails, preserve runtime behavior and rely on call-path # normalization below. @@ -608,9 +624,9 @@ async def _validate_configured_model_ids( "Context Compiler pipe misconfigured: BASE_MODEL_ID was not found " "in Open WebUI models." ) - if preprocessor_model_id and preprocessor_model_id not in known_model_ids: + if drafter_model_id and drafter_model_id not in known_model_ids: return ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID was not found " + "Context Compiler pipe misconfigured: DRAFTER_MODEL_ID was not found " "in Open WebUI models." ) return None @@ -623,7 +639,7 @@ async def _llm_fallback_candidate( user_payload: dict[str, Any], model_id: str | None, ) -> str | None: - self._last_preprocessor_error = None + self._last_drafter_error = None model_id = _normalize_model_id(model_id) if model_id is None: return None @@ -642,16 +658,16 @@ async def _llm_fallback_candidate( try: response = await generate_chat_completion(request, payload, user) except Exception as exc: - normalized_exception = self._normalize_preprocessor_exception(exc) + normalized_exception = self._normalize_drafter_exception(exc) if normalized_exception is not None: - self._last_preprocessor_error = normalized_exception - logger.warning("preprocessor: %s", normalized_exception) + self._last_drafter_error = normalized_exception + logger.warning("drafter: %s", normalized_exception) return None - normalized_error = self._normalize_preprocessor_error(response) + normalized_error = self._normalize_drafter_error(response) if normalized_error is not None: - self._last_preprocessor_error = normalized_error - logger.warning("preprocessor: %s", normalized_error) + self._last_drafter_error = normalized_error + logger.warning("drafter: %s", normalized_error) return None return _extract_completion_content(response) @@ -687,7 +703,7 @@ def _extract_drafted_text(self, drafted_result: DraftResult) -> str | None: return None return None - async def _preprocess_user_input( + async def _run_drafter( self, message: str, *, @@ -695,14 +711,14 @@ async def _preprocess_user_input( user_payload: dict[str, Any], model_id: str | None, ) -> tuple[DraftResult, str | None]: - self._last_preprocessor_error = None + self._last_drafter_error = None drafted_result = await self._draft_user_input( message, request=request, user_payload=user_payload, model_id=model_id, ) - return drafted_result, self._last_preprocessor_error + return drafted_result, self._last_drafter_error async def _forward_passthrough( self, @@ -758,7 +774,7 @@ async def _apply_approved_directive( state_before = _snapshot_engine_state(engine) engine_snapshot_json = engine.export_json() compile_input = directive.text - logger.debug("preprocessor: approved_input=%r", compile_input) + logger.debug("drafter: approved_input=%r", compile_input) decision = engine.apply_directive(directive) state_after = _snapshot_engine_state(engine) @@ -773,7 +789,7 @@ async def _apply_approved_directive( decision=decision, state_before=state_before, state_after=state_after, - preprocessor_output=compile_input, + drafter_output=compile_input, llm_called=False, ) if decision.kind == DecisionKind.UPDATE: @@ -784,7 +800,7 @@ async def _apply_approved_directive( decision=decision, state_before=state_before, state_after=state_after, - preprocessor_output=compile_input, + drafter_output=compile_input, llm_called=False, ) @@ -803,7 +819,7 @@ async def _apply_approved_directive( decision=decision, state_before=state_before, state_after=state_after, - preprocessor_output=compile_input, + drafter_output=compile_input, llm_called=base_model_id is not None, state_injected=state_injected, ) @@ -819,7 +835,7 @@ async def pipe( ) -> Any: # Open WebUI integration entrypoint: # 1) extract latest user input - # 2) run preprocess (heuristic -> LLM fallback) + # 2) run drafter (heuristic -> LLM fallback) # 3) pass directive or original input to engine.step(...) # 4) map decision back to Open WebUI response behavior raw_messages = body.get("messages") @@ -829,8 +845,7 @@ async def pipe( else [] ) base_model_id = _normalize_model_id(self.valves.BASE_MODEL_ID) - preprocessor_model_id = _normalize_model_id(self.valves.PREPROCESSOR_MODEL_ID) - effective_preprocessor_model = preprocessor_model_id or base_model_id + effective_drafter_model = self._resolve_drafter_model_id(base_model_id) current_model_id = str(body.get("model", "")).strip() if not base_model_id and not self._allow_missing_base_model_for_debug(): @@ -844,12 +859,12 @@ async def pipe( "the selected pipe model id to avoid recursive routing." ) if ( - effective_preprocessor_model + effective_drafter_model and current_model_id - and effective_preprocessor_model == current_model_id + and effective_drafter_model == current_model_id ): return ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID must not " + "Context Compiler pipe misconfigured: DRAFTER_MODEL_ID must not " "match the selected pipe model id to avoid recursive routing." ) @@ -857,13 +872,13 @@ async def pipe( __request__, __user__, base_model_id=base_model_id, - preprocessor_model_id=effective_preprocessor_model, + drafter_model_id=effective_drafter_model, ) if preflight_error is not None: return preflight_error latest_user_text = _extract_latest_user_text(messages) - logger.debug("preprocessor: user_input_found=%s", latest_user_text is not None) + logger.debug("drafter: user_input_found=%s", latest_user_text is not None) if latest_user_text is None: return await self._forward_passthrough( @@ -883,17 +898,17 @@ async def pipe( return _render_show_state_summary(engine) state_before = _snapshot_engine_state(engine) - preprocess_error: str | None = None - drafted_result, preprocess_error = await self._preprocess_user_input( + drafter_error: str | None = None + drafted_result, drafter_error = await self._run_drafter( latest_user_text, request=__request__, user_payload=__user__, - model_id=effective_preprocessor_model, + model_id=effective_drafter_model, ) - if preprocess_error is not None: - return preprocess_error + if drafter_error is not None: + return drafter_error - logger.debug("preprocessor: drafted_result=%r", drafted_result) + logger.debug("drafter: drafted_result=%r", drafted_result) if not isinstance(drafted_result.result, CanonicalDirective): state_injected = ( "yes" if _has_non_empty_authoritative_state(engine) else "no" @@ -912,7 +927,7 @@ async def pipe( decision=DecisionKind.NO_DIRECTIVE, state_before=state_before, state_after=state_before, - preprocessor_output=None, + drafter_output=None, llm_called=base_model_id is not None, state_injected=state_injected, ) diff --git a/python/tests/test_litellm_proxy_with_directive_drafter.py b/python/tests/test_litellm_proxy_with_directive_drafter.py index d85acae..9cfb210 100644 --- a/python/tests/test_litellm_proxy_with_directive_drafter.py +++ b/python/tests/test_litellm_proxy_with_directive_drafter.py @@ -48,7 +48,7 @@ class _CustomLogger: def test_drafter_runs_only_for_current_turn(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_current_only") - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() drafted_calls: list[tuple[str, dict[str, object]]] = [] def fake_draft(message: str) -> object: @@ -77,7 +77,7 @@ def fake_draft(message: str) -> object: def test_drafter_output_applies_to_current_turn_only(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_applies") - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() directive = decompose_directive("prohibit docker") assert directive is not None monkeypatch.setattr( @@ -109,7 +109,7 @@ def test_persistent_mode_with_drafter_rejects_failed_application_without_persist ) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_failed_apply") module.CHECKPOINT_STORE.clear() - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() drafted_inputs: list[str] = [] def fake_draft(message: str) -> object: @@ -139,7 +139,7 @@ def fake_draft(message: str) -> object: def test_missing_session_key_fails_clearly_in_persistent_mode(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_missing_session") - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() data = { "model": "demo", "context_compiler_mode": "persistent", @@ -154,7 +154,7 @@ def test_missing_session_key_fails_clearly_in_persistent_mode(monkeypatch) -> No def test_default_mode_is_stateless_and_requires_no_session_key(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_default_stateless") - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() monkeypatch.setattr( module, "_draft_last_user_message", @@ -175,7 +175,7 @@ def test_default_mode_is_stateless_and_requires_no_session_key(monkeypatch) -> N def test_stateless_mode_has_no_cross_call_continuity(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_stateless") - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() monkeypatch.setattr( module, "_draft_last_user_message", @@ -209,7 +209,7 @@ def test_persistent_mode_with_drafter_preserves_existing_checkpoint_on_failure( ) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_preserve_checkpoint") module.CHECKPOINT_STORE.clear() - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() def seed_draft(message: str) -> object: return module.DraftResult( @@ -271,7 +271,7 @@ def reject_draft(message: str) -> object: def test_normal_update_explicitly_saves_checkpoint(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_save_after_update") module.CHECKPOINT_STORE.clear() - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() directive = decompose_directive("prohibit peanuts") assert directive is not None monkeypatch.setattr( @@ -300,7 +300,7 @@ def test_normal_update_explicitly_saves_checkpoint(monkeypatch) -> None: def test_restore_happens_before_drafting(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_restore_first") module.CHECKPOINT_STORE.clear() - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() module.CHECKPOINT_STORE.save( "chat-restore-first", {"premise": None, "policies": {"peanuts": "prohibit"}, "version": 2}, @@ -331,7 +331,7 @@ def test_corrupt_checkpoint_fails_clearly(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_corrupt") module.CHECKPOINT_STORE.clear() module.CHECKPOINT_STORE.save("broken", {"checkpoint_version": 99}) - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() data = { "model": "demo", "context_compiler_mode": "persistent", @@ -347,7 +347,7 @@ def test_corrupt_checkpoint_fails_clearly(monkeypatch) -> None: def test_forwarded_messages_keep_original_user_prompt_text(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_forwarded_text") - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() directive = decompose_directive("use docker") assert directive is not None original_messages = [ @@ -379,7 +379,7 @@ def test_compound_directives_fall_through_to_normal_forwarding_when_not_applied( ) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_compound") module.CHECKPOINT_STORE.clear() - hook = module.ContextCompilerPreCallHookWithPreprocessor() + hook = module.ContextCompilerPreCallHookWithDrafter() monkeypatch.setattr( module, "_draft_last_user_message", @@ -405,7 +405,7 @@ def test_compound_directives_fall_through_to_normal_forwarding_when_not_applied( assert checkpoint["policies"] == {} -def test_fallback_adapter_receives_preprocessor_model(monkeypatch) -> None: +def test_fallback_adapter_receives_drafter_model(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_shared_prompt") monkeypatch.setenv("OPENAI_API_KEY", "dummy") monkeypatch.setenv("MODEL", "openai/demo-model") @@ -423,8 +423,35 @@ def fallback_factory(**kwargs): assert seen["model"] == "openai/demo-model" +def test_drafter_model_legacy_alias_and_precedence(monkeypatch, caplog) -> None: + module = _load_module(monkeypatch, "litellm_proxy_with_drafter_model_alias") + monkeypatch.setenv("OPENAI_API_KEY", "dummy") + monkeypatch.setenv("MODEL", "openai/main-model") + monkeypatch.setenv("PREPROCESSOR_MODEL", "openai/legacy-model") + seen: dict[str, Any] = {} + + def fallback_factory(**kwargs): + seen.update(kwargs) + return lambda _message: "use docker" + + monkeypatch.setattr(module, "create_litellm_fallback", fallback_factory) + module._create_directive_drafter.cache_clear() + module._get_directive_drafter() + assert seen["model"] == "openai/legacy-model" + assert "PREPROCESSOR_MODEL is deprecated" in caplog.text + + monkeypatch.setenv("DRAFTER_MODEL", "openai/drafter-model") + module._create_directive_drafter.cache_clear() + module._get_directive_drafter() + assert seen["model"] == "openai/drafter-model" + + def test_no_removed_replay_api_remains(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_no_replay") + assert ( + module.ContextCompilerPreCallHookWithPreprocessor + is module.ContextCompilerPreCallHookWithDrafter + ) assert not hasattr(module, "compile_transcript") assert "_state_before_last_message" not in MODULE_PATH.read_text(encoding="utf-8") diff --git a/python/tests/test_litellm_with_directive_drafter.py b/python/tests/test_litellm_with_directive_drafter.py index 7ed0fdd..47a9977 100644 --- a/python/tests/test_litellm_with_directive_drafter.py +++ b/python/tests/test_litellm_with_directive_drafter.py @@ -317,7 +317,7 @@ def test_call_litellm_logs_startup_config_once( assert "source=OPENAI_BASE_URL override" in message -def test_preprocessor_model_defaults_to_model(monkeypatch) -> None: +def test_drafter_model_defaults_to_model(monkeypatch) -> None: seen: dict[str, object] = {} def fallback_factory(**kwargs: Any): @@ -326,6 +326,7 @@ def fallback_factory(**kwargs: Any): monkeypatch.setenv("OPENAI_API_KEY", "dummy") monkeypatch.setenv("MODEL", "openai/main-model") + monkeypatch.delenv("DRAFTER_MODEL", raising=False) monkeypatch.delenv("PREPROCESSOR_MODEL", raising=False) monkeypatch.setattr(module, "create_litellm_fallback", fallback_factory) module._create_directive_drafter.cache_clear() @@ -334,7 +335,7 @@ def fallback_factory(**kwargs: Any): assert seen["model"] == "openai/main-model" -def test_preprocessor_model_override_wins(monkeypatch) -> None: +def test_drafter_model_legacy_alias_is_supported(monkeypatch, caplog) -> None: seen: dict[str, object] = {} def fallback_factory(**kwargs: Any): @@ -343,12 +344,32 @@ def fallback_factory(**kwargs: Any): monkeypatch.setenv("OPENAI_API_KEY", "dummy") monkeypatch.setenv("MODEL", "openai/main-model") + monkeypatch.delenv("DRAFTER_MODEL", raising=False) monkeypatch.setenv("PREPROCESSOR_MODEL", "openai/preprocessor-model") monkeypatch.setattr(module, "create_litellm_fallback", fallback_factory) module._create_directive_drafter.cache_clear() module._get_directive_drafter() assert seen["model"] == "openai/preprocessor-model" + assert "PREPROCESSOR_MODEL is deprecated" in caplog.text + + +def test_drafter_model_wins_over_legacy_alias(monkeypatch) -> None: + seen: dict[str, object] = {} + + def fallback_factory(**kwargs: Any): + seen.update(kwargs) + return lambda _message: "use docker" + + monkeypatch.setenv("OPENAI_API_KEY", "dummy") + monkeypatch.setenv("MODEL", "openai/main-model") + monkeypatch.setenv("DRAFTER_MODEL", "openai/drafter-model") + monkeypatch.setenv("PREPROCESSOR_MODEL", "openai/preprocessor-model") + monkeypatch.setattr(module, "create_litellm_fallback", fallback_factory) + module._create_directive_drafter.cache_clear() + + module._get_directive_drafter() + assert seen["model"] == "openai/drafter-model" def test_directive_shaped_malformed_inputs_can_fall_through_to_normal_turn_flow( @@ -400,7 +421,7 @@ def downstream(_messages: list[dict[str, str]]) -> str: def test_handle_turn_has_no_session_or_resume_behavior(monkeypatch) -> None: monkeypatch.setattr(module, "_call_litellm", lambda _messages: "ok") - monkeypatch.setattr(module, "_preprocess_user_input", lambda _text: None) + monkeypatch.setattr(module, "_draft_user_input", lambda _text: None) engine = Engine() diff --git a/python/tests/test_openwebui_pipe_with_directive_drafter.py b/python/tests/test_openwebui_pipe_with_directive_drafter.py index 97b5232..ea78d76 100644 --- a/python/tests/test_openwebui_pipe_with_directive_drafter.py +++ b/python/tests/test_openwebui_pipe_with_directive_drafter.py @@ -115,7 +115,7 @@ async def confirm(event: dict[str, object]) -> bool: monkeypatch.setattr(module.Pipe, "_draft_user_input", fake_draft) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" chat_id = "chat-native-confirmation" result = asyncio.run( @@ -169,7 +169,7 @@ async def fake_draft(*args, **kwargs): pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" chat_id = "chat-before-step" async def confirm(event: dict[str, object]) -> bool: @@ -223,7 +223,7 @@ def tracked_apply_directive(directive): monkeypatch.setattr(module, "Engine", Engine_with_tracking) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def update_draft(*args, **kwargs): return DraftResult( @@ -289,7 +289,7 @@ async def confirm(event: dict[str, object]) -> bool: first_pipe = module.Pipe() first_pipe.valves.BASE_MODEL_ID = "base-model" - first_pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + first_pipe.valves.DRAFTER_MODEL_ID = "prep-model" chat_id = "chat-pending-lifecycle" first_result = asyncio.run( first_pipe.pipe( @@ -308,7 +308,7 @@ async def confirm(event: dict[str, object]) -> bool: module._ENGINES_BY_CHAT_KEY.clear() second_pipe = module.Pipe() second_pipe.valves.BASE_MODEL_ID = "base-model" - second_pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + second_pipe.valves.DRAFTER_MODEL_ID = "prep-model" second_result = asyncio.run( second_pipe.pipe( { @@ -344,7 +344,7 @@ def test_rejection_does_not_mutate_state(monkeypatch) -> None: ) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def update_draft(*args, **kwargs): return DraftResult( @@ -404,7 +404,7 @@ def test_rejected_confirmation_does_not_affect_show_state(monkeypatch) -> None: module = _load_module("owui_with_drafter_pending_show_state", monkeypatch) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def update_draft(*args, **kwargs): return DraftResult( @@ -477,7 +477,7 @@ async def forward( module.generate_chat_completion = forward pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def update_draft(*args, **kwargs): return DraftResult( @@ -563,7 +563,7 @@ async def forward( module.generate_chat_completion = forward pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def update_draft(*args, **kwargs): return DraftResult( @@ -660,7 +660,7 @@ async def no_draft(*args, **kwargs): pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" result = asyncio.run( pipe.pipe( @@ -693,7 +693,7 @@ async def forward( module.generate_chat_completion = forward pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def update_draft(*args, **kwargs): return DraftResult( @@ -763,7 +763,7 @@ async def forward( module.generate_chat_completion = forward pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def no_draft(*args, **kwargs): return DraftResult( @@ -826,7 +826,7 @@ async def forward( module.generate_chat_completion = forward pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def compound_draft(*args, **kwargs): return DraftResult( @@ -871,7 +871,7 @@ async def forward( module.generate_chat_completion = forward pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" chat_id = "chat-passthrough" async def update_draft(*args, **kwargs): @@ -935,26 +935,48 @@ async def no_draft(*args, **kwargs): assert len(cc_messages) == 1 -def test_preprocessor_model_defaults_to_base_model(monkeypatch) -> None: +def test_drafter_model_defaults_to_base_model(monkeypatch) -> None: module = _load_module("owui_with_drafter_model_default", monkeypatch) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = None + pipe.valves.DRAFTER_MODEL_ID = None - assert pipe._resolve_preprocessor_model_id("base-model") == "base-model" + assert pipe._resolve_drafter_model_id("base-model") == "base-model" -def test_preprocessor_model_override_wins(monkeypatch) -> None: +def test_drafter_model_override_wins(monkeypatch) -> None: module = _load_module("owui_with_drafter_model_override", monkeypatch) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" - assert pipe._resolve_preprocessor_model_id("base-model") == "prep-model" + assert pipe._resolve_drafter_model_id("base-model") == "prep-model" -def test_invalid_preprocessor_model_id_from_model_list(monkeypatch) -> None: - module = _load_module("owui_with_drafter_invalid_preprocessor_model", monkeypatch) +def test_drafter_model_legacy_alias_is_supported_and_warns(monkeypatch, caplog) -> None: + module = _load_module("owui_with_drafter_legacy_model_alias", monkeypatch) + pipe = module.Pipe() + pipe.valves.BASE_MODEL_ID = "base-model" + pipe.valves.DRAFTER_MODEL_ID = None + pipe.valves.PREPROCESSOR_MODEL_ID = "legacy-model" + + assert pipe._resolve_drafter_model_id("base-model") == "legacy-model" + assert "PREPROCESSOR_MODEL_ID is deprecated" in caplog.text + + +def test_drafter_model_wins_over_legacy_alias(monkeypatch, caplog) -> None: + module = _load_module("owui_with_drafter_model_precedence", monkeypatch) + pipe = module.Pipe() + pipe.valves.BASE_MODEL_ID = "base-model" + pipe.valves.DRAFTER_MODEL_ID = "drafter-model" + pipe.valves.PREPROCESSOR_MODEL_ID = "legacy-model" + + assert pipe._resolve_drafter_model_id("base-model") == "drafter-model" + assert "PREPROCESSOR_MODEL_ID is deprecated" not in caplog.text + + +def test_invalid_drafter_model_id_from_model_list(monkeypatch) -> None: + module = _load_module("owui_with_drafter_invalid_drafter_model", monkeypatch) pipe = module.Pipe() async def models(_: object, user: object = None) -> list[dict[str, str]]: @@ -968,21 +990,21 @@ async def models(_: object, user: object = None) -> list[dict[str, str]]: request=object(), user_payload={"id": "u1"}, base_model_id="base-model", - preprocessor_model_id="missing-prep-model", + drafter_model_id="missing-drafter-model", ) ) assert error == ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID was not found " + "Context Compiler pipe misconfigured: DRAFTER_MODEL_ID was not found " "in Open WebUI models." ) -def test_recursion_guard_for_preprocessor_model_id(monkeypatch) -> None: +def test_recursion_guard_for_drafter_model_id(monkeypatch) -> None: module = _load_module("owui_with_drafter_recursion_guard", monkeypatch) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "pipe-model" + pipe.valves.DRAFTER_MODEL_ID = "pipe-model" result = asyncio.run( pipe.pipe( @@ -993,7 +1015,7 @@ def test_recursion_guard_for_preprocessor_model_id(monkeypatch) -> None: ) assert result == ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID must not " + "Context Compiler pipe misconfigured: DRAFTER_MODEL_ID must not " "match the selected pipe model id to avoid recursive routing." ) @@ -1004,7 +1026,7 @@ def test_debug_mode_missing_base_model_returns_deterministic_message( module = _load_module("owui_with_drafter_debug_missing_base", monkeypatch) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = None - pipe.valves.PREPROCESSOR_MODEL_ID = None + pipe.valves.DRAFTER_MODEL_ID = None pipe.valves.ALLOW_MISSING_BASE_MODEL_FOR_DEBUG = True async def no_draft(*args, **kwargs): @@ -1033,11 +1055,11 @@ async def no_draft(*args, **kwargs): ) -def test_preprocessor_model_not_found_is_normalized(monkeypatch) -> None: - module = _load_module("owui_with_drafter_preprocessor_not_found", monkeypatch) +def test_drafter_model_not_found_is_normalized(monkeypatch) -> None: + module = _load_module("owui_with_drafter_drafter_not_found", monkeypatch) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def generate( _: object, payload: dict[str, object], __: object @@ -1060,19 +1082,19 @@ async def generate( ) assert result == ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or " + "Context Compiler pipe misconfigured: DRAFTER_MODEL_ID is invalid or " "not configured in Open WebUI. Configure a valid model id in " "Admin Panel → Settings → Models." ) -def test_fallback_uses_preprocessor_model_then_forward_uses_base_model( +def test_fallback_uses_drafter_model_then_forward_uses_base_model( monkeypatch, ) -> None: module = _load_module("owui_with_drafter_fallback_routing", monkeypatch) pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" calls: list[str] = [] async def generate( @@ -1138,7 +1160,7 @@ async def forward( module.generate_chat_completion = forward pipe = module.Pipe() pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" + pipe.valves.DRAFTER_MODEL_ID = "prep-model" async def unknown_draft(*args, **kwargs): return DraftResult( @@ -1175,7 +1197,7 @@ async def get_user_by_id(user_id: object) -> dict[str, object]: request=object(), user_payload={"id": "u1"}, base_model_id="base-model", - preprocessor_model_id="prep-model", + drafter_model_id="prep-model", ) ) From c95d0d9a6862dee65036932827c3ac2cff3c5519 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 2 Sep 2026 22:05:26 -0400 Subject: [PATCH 2/3] refactor: remove deprecated drafter aliases --- PROVIDER_CONTRACT.md | 4 +-- .../litellm/with_directive_drafter.py | 6 ----- .../litellm_proxy/README.md | 7 ++--- ...ler_precall_hook_with_directive_drafter.py | 9 ------- .../openwebui_pipe/README.md | 4 +-- .../open_webui_pipe_with_directive_drafter.py | 12 --------- ...st_litellm_proxy_with_directive_drafter.py | 27 ------------------- .../test_litellm_with_directive_drafter.py | 23 +--------------- ...t_openwebui_pipe_with_directive_drafter.py | 24 +---------------- 9 files changed, 6 insertions(+), 110 deletions(-) diff --git a/PROVIDER_CONTRACT.md b/PROVIDER_CONTRACT.md index 2ff230f..69385f3 100644 --- a/PROVIDER_CONTRACT.md +++ b/PROVIDER_CONTRACT.md @@ -49,9 +49,7 @@ Startup emits one concise config line showing resolved `mode`, `base_url`, `OPENAI_BASE_URL override`). `MODEL` and `DRAFTER_MODEL` use LiteLLM format: `/`. -`DRAFTER_MODEL` is optional and defaults to `MODEL`. `PREPROCESSOR_MODEL` is -deprecated but remains supported as a compatibility alias; `DRAFTER_MODEL` -wins when both are set. +`DRAFTER_MODEL` is optional and defaults to `MODEL`. The directive-drafter integration always uses heuristic-first processing with the configured fallback model when needed. diff --git a/python/examples/prompt_construction/litellm/with_directive_drafter.py b/python/examples/prompt_construction/litellm/with_directive_drafter.py index 428f895..432a1d9 100644 --- a/python/examples/prompt_construction/litellm/with_directive_drafter.py +++ b/python/examples/prompt_construction/litellm/with_directive_drafter.py @@ -225,12 +225,6 @@ def _create_directive_drafter( def _get_directive_drafter() -> DirectiveDrafter: config = resolve_provider_config(default_model="openai/gpt-4o-mini") drafter_model = os.getenv("DRAFTER_MODEL", "").strip() - if not drafter_model: - drafter_model = os.getenv("PREPROCESSOR_MODEL", "").strip() - if drafter_model: - logger.warning( - "PREPROCESSOR_MODEL is deprecated; use DRAFTER_MODEL instead" - ) drafter_model = drafter_model or config.model return _create_directive_drafter(drafter_model, config.api_key, config.base_url) diff --git a/python/reference_integrations/litellm_proxy/README.md b/python/reference_integrations/litellm_proxy/README.md index bca1e90..f871e5e 100644 --- a/python/reference_integrations/litellm_proxy/README.md +++ b/python/reference_integrations/litellm_proxy/README.md @@ -204,9 +204,7 @@ Optional env vars for directive-drafter fallback: export DRAFTER_MODEL=openai/gpt-4o-mini ``` -`DRAFTER_MODEL` is optional and defaults to `MODEL`. `PREPROCESSOR_MODEL` is -deprecated but remains supported as a compatibility alias; `DRAFTER_MODEL` -wins when both are set. +`DRAFTER_MODEL` is optional and defaults to `MODEL`. The directive-drafter integration always uses heuristic-first processing with the configured fallback model when needed. @@ -232,8 +230,7 @@ the configured fallback model when needed. explicit `stateless` mode - proxy starts but upstream calls fail: check `OPENAI_API_KEY` and upstream model/provider config in `config.example.yaml` -- directive-drafter fallback issues: `DRAFTER_MODEL` defaults to `MODEL`; - `PREPROCESSOR_MODEL` remains available as a deprecated compatibility alias +- directive-drafter fallback issues: `DRAFTER_MODEL` defaults to `MODEL` ## Opt-in Runtime Smoke Test diff --git a/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py b/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py index ff46f69..a9842df 100644 --- a/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py +++ b/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py @@ -77,12 +77,6 @@ def _create_directive_drafter( def _get_directive_drafter() -> DirectiveDrafter: drafter_model = os.getenv("DRAFTER_MODEL", "").strip() - if not drafter_model: - drafter_model = os.getenv("PREPROCESSOR_MODEL", "").strip() - if drafter_model: - logger.warning( - "PREPROCESSOR_MODEL is deprecated; use DRAFTER_MODEL instead" - ) if not drafter_model: drafter_model = os.getenv("MODEL", "").strip() if not drafter_model: @@ -187,7 +181,4 @@ async def async_pre_call_hook( return data -# Import-compatible alias for existing LiteLLM proxy configurations. -ContextCompilerPreCallHookWithPreprocessor = ContextCompilerPreCallHookWithDrafter - proxy_handler_instance = ContextCompilerPreCallHookWithDrafter() diff --git a/python/reference_integrations/openwebui_pipe/README.md b/python/reference_integrations/openwebui_pipe/README.md index 6ba7f52..13d728f 100644 --- a/python/reference_integrations/openwebui_pipe/README.md +++ b/python/reference_integrations/openwebui_pipe/README.md @@ -70,8 +70,6 @@ If using `open_webui_pipe_with_directive_drafter.py`: - Install directive-drafter support if needed: `pip install "context-compiler>=0.9.0dev13" "context-compiler-directive-drafter>=0.2.0dev5"` - Optionally set `DRAFTER_MODEL_ID` to use a separate fallback drafting model -- `PREPROCESSOR_MODEL_ID` remains supported as a deprecated compatibility alias; - `DRAFTER_MODEL_ID` wins when both are set - If neither model id is set, fallback uses `BASE_MODEL_ID` Model fallback output is structurally validated before handoff. This does not prove that the model interpreted the user correctly. The automated fallback path is experimental pending a separate source-aware acceptance policy and reviewed drafting workflow. @@ -250,5 +248,5 @@ rejection flows. ## Fallback notes -- Fallback drafting uses `DRAFTER_MODEL_ID` first, then the deprecated `PREPROCESSOR_MODEL_ID` alias, while the main passthrough path still forwards with `BASE_MODEL_ID`. +- Fallback drafting uses `DRAFTER_MODEL_ID`, while the main passthrough path still forwards with `BASE_MODEL_ID`. - If the fallback model returns `model not found`, the pipe normalizes that into the deterministic `DRAFTER_MODEL_ID` misconfiguration message above. diff --git a/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py b/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py index 09b559a..186c146 100644 --- a/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +++ b/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py @@ -383,10 +383,6 @@ class Valves(BaseModel): "Optional model id for fallback drafting (defaults to BASE_MODEL_ID)." ), ) - PREPROCESSOR_MODEL_ID: str | None = Field( - default=None, - description=("Deprecated compatibility alias for DRAFTER_MODEL_ID."), - ) ALLOW_MISSING_BASE_MODEL_FOR_DEBUG: bool = Field( default=False, description="Allow missing BASE_MODEL_ID for debug/testing only.", @@ -579,14 +575,6 @@ def _resolve_drafter_model_id(self, base_model_id: str | None) -> str | None: ) if drafter_model_id: return drafter_model_id - legacy_model_id = _normalize_model_id( - getattr(self.valves, "PREPROCESSOR_MODEL_ID", None) - ) - if legacy_model_id: - logger.warning( - "PREPROCESSOR_MODEL_ID is deprecated; use DRAFTER_MODEL_ID instead" - ) - return legacy_model_id return base_model_id async def _validate_configured_model_ids( diff --git a/python/tests/test_litellm_proxy_with_directive_drafter.py b/python/tests/test_litellm_proxy_with_directive_drafter.py index 9cfb210..beb08e0 100644 --- a/python/tests/test_litellm_proxy_with_directive_drafter.py +++ b/python/tests/test_litellm_proxy_with_directive_drafter.py @@ -423,35 +423,8 @@ def fallback_factory(**kwargs): assert seen["model"] == "openai/demo-model" -def test_drafter_model_legacy_alias_and_precedence(monkeypatch, caplog) -> None: - module = _load_module(monkeypatch, "litellm_proxy_with_drafter_model_alias") - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setenv("MODEL", "openai/main-model") - monkeypatch.setenv("PREPROCESSOR_MODEL", "openai/legacy-model") - seen: dict[str, Any] = {} - - def fallback_factory(**kwargs): - seen.update(kwargs) - return lambda _message: "use docker" - - monkeypatch.setattr(module, "create_litellm_fallback", fallback_factory) - module._create_directive_drafter.cache_clear() - module._get_directive_drafter() - assert seen["model"] == "openai/legacy-model" - assert "PREPROCESSOR_MODEL is deprecated" in caplog.text - - monkeypatch.setenv("DRAFTER_MODEL", "openai/drafter-model") - module._create_directive_drafter.cache_clear() - module._get_directive_drafter() - assert seen["model"] == "openai/drafter-model" - - def test_no_removed_replay_api_remains(monkeypatch) -> None: module = _load_module(monkeypatch, "litellm_proxy_with_drafter_no_replay") - assert ( - module.ContextCompilerPreCallHookWithPreprocessor - is module.ContextCompilerPreCallHookWithDrafter - ) assert not hasattr(module, "compile_transcript") assert "_state_before_last_message" not in MODULE_PATH.read_text(encoding="utf-8") diff --git a/python/tests/test_litellm_with_directive_drafter.py b/python/tests/test_litellm_with_directive_drafter.py index 47a9977..613d067 100644 --- a/python/tests/test_litellm_with_directive_drafter.py +++ b/python/tests/test_litellm_with_directive_drafter.py @@ -327,7 +327,6 @@ def fallback_factory(**kwargs: Any): monkeypatch.setenv("OPENAI_API_KEY", "dummy") monkeypatch.setenv("MODEL", "openai/main-model") monkeypatch.delenv("DRAFTER_MODEL", raising=False) - monkeypatch.delenv("PREPROCESSOR_MODEL", raising=False) monkeypatch.setattr(module, "create_litellm_fallback", fallback_factory) module._create_directive_drafter.cache_clear() @@ -335,26 +334,7 @@ def fallback_factory(**kwargs: Any): assert seen["model"] == "openai/main-model" -def test_drafter_model_legacy_alias_is_supported(monkeypatch, caplog) -> None: - seen: dict[str, object] = {} - - def fallback_factory(**kwargs: Any): - seen.update(kwargs) - return lambda _message: "use docker" - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setenv("MODEL", "openai/main-model") - monkeypatch.delenv("DRAFTER_MODEL", raising=False) - monkeypatch.setenv("PREPROCESSOR_MODEL", "openai/preprocessor-model") - monkeypatch.setattr(module, "create_litellm_fallback", fallback_factory) - module._create_directive_drafter.cache_clear() - - module._get_directive_drafter() - assert seen["model"] == "openai/preprocessor-model" - assert "PREPROCESSOR_MODEL is deprecated" in caplog.text - - -def test_drafter_model_wins_over_legacy_alias(monkeypatch) -> None: +def test_drafter_model_override_wins(monkeypatch) -> None: seen: dict[str, object] = {} def fallback_factory(**kwargs: Any): @@ -364,7 +344,6 @@ def fallback_factory(**kwargs: Any): monkeypatch.setenv("OPENAI_API_KEY", "dummy") monkeypatch.setenv("MODEL", "openai/main-model") monkeypatch.setenv("DRAFTER_MODEL", "openai/drafter-model") - monkeypatch.setenv("PREPROCESSOR_MODEL", "openai/preprocessor-model") monkeypatch.setattr(module, "create_litellm_fallback", fallback_factory) module._create_directive_drafter.cache_clear() diff --git a/python/tests/test_openwebui_pipe_with_directive_drafter.py b/python/tests/test_openwebui_pipe_with_directive_drafter.py index ea78d76..161456a 100644 --- a/python/tests/test_openwebui_pipe_with_directive_drafter.py +++ b/python/tests/test_openwebui_pipe_with_directive_drafter.py @@ -953,28 +953,6 @@ def test_drafter_model_override_wins(monkeypatch) -> None: assert pipe._resolve_drafter_model_id("base-model") == "prep-model" -def test_drafter_model_legacy_alias_is_supported_and_warns(monkeypatch, caplog) -> None: - module = _load_module("owui_with_drafter_legacy_model_alias", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.DRAFTER_MODEL_ID = None - pipe.valves.PREPROCESSOR_MODEL_ID = "legacy-model" - - assert pipe._resolve_drafter_model_id("base-model") == "legacy-model" - assert "PREPROCESSOR_MODEL_ID is deprecated" in caplog.text - - -def test_drafter_model_wins_over_legacy_alias(monkeypatch, caplog) -> None: - module = _load_module("owui_with_drafter_model_precedence", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.DRAFTER_MODEL_ID = "drafter-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "legacy-model" - - assert pipe._resolve_drafter_model_id("base-model") == "drafter-model" - assert "PREPROCESSOR_MODEL_ID is deprecated" not in caplog.text - - def test_invalid_drafter_model_id_from_model_list(monkeypatch) -> None: module = _load_module("owui_with_drafter_invalid_drafter_model", monkeypatch) pipe = module.Pipe() @@ -1077,7 +1055,7 @@ async def generate( }, __user__={"id": "u1"}, __request__=object(), - __chat_id__="chat-preprocessor-not-found", + __chat_id__="chat-drafter-not-found", ) ) From 2c3cf6ddd9b31e48b0bc5b1201da41955ba771de Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 3 Sep 2026 00:32:57 -0400 Subject: [PATCH 3/3] chore: bump openwebui drafter pipe version --- .../openwebui_pipe/open_webui_pipe_with_directive_drafter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py b/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py index 186c146..cbee6c3 100644 --- a/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +++ b/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py @@ -2,7 +2,7 @@ title: Context Compiler Open WebUI Pipe (Directive Drafter) author: rlippmann author_url: https://github.com/rlippmann/context-compiler-example-integrations -version: 0.10.1 +version: 0.10.2 requirements: context-compiler>=0.9.0dev13, context-compiler-directive-drafter>=0.2.0dev5 Open WebUI integration with Context Compiler directive drafter.