-
Notifications
You must be signed in to change notification settings - Fork 34
feat(aicore/fallback): add opt-in model fallback for Orchestration v2 #185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lenin-ribeiro
wants to merge
41
commits into
main
Choose a base branch
from
feat/aicore-model-fallback
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
ba5ed26
feat(orchestration): add content filtering and prompt shield module
lenin-ribeiro 7fa25a6
fix(orchestration): ruff formatting and ty type annotations
lenin-ribeiro 307a36f
fix(orchestration): use cast() to satisfy ty Literal return type
lenin-ribeiro 90410e5
test(telemetry): update module and operation count assertions for orc…
lenin-ribeiro 2502c58
feat(core): add typed env-var readers in core.env
lenin-ribeiro 3086021
refactor: move orchestration package to aicore/filtering
lenin-ribeiro efab15d
refactor(aicore/filtering): introduce Severity enum, AICORE_FILTER_* …
lenin-ribeiro 91c4ecb
refactor(aicore/filtering): update _litellm_patch test imports
lenin-ribeiro 7440272
refactor(telemetry): fold orchestration module into aicore
lenin-ribeiro 6c4c253
feat(aicore/filtering): add disable_filtering, retarget telemetry
lenin-ribeiro a90bf07
feat(aicore): flat re-export of filtering public API
lenin-ribeiro c61c719
docs(readme): update orchestration references to aicore filtering
lenin-ribeiro fcd5476
docs(aicore): merge orchestration user guide into aicore guide
lenin-ribeiro b4152c0
refactor(aicore/filtering): drop stale 'orchestration' wording
lenin-ribeiro 8a9babc
test(aicore/filtering): add integration tests for filtering and promp…
lenin-ribeiro 8b548dd
fix: satisfy CI ruff format + ty check; route self-harm prompt via env
lenin-ribeiro d0a3c8d
fix(aicore): satisfy CI ty check and match Azure's Prompt Shield wire…
lenin-ribeiro 38fac0b
feat(aicore/filtering): add ContentFilter, AzureContentFilter, LlamaG…
lenin-ribeiro cec3879
feat(aicore/filtering): add InputFiltering, OutputFiltering, ContentF…
lenin-ribeiro cc260b3
feat(aicore/filtering): replace kwarg set_filtering with class-based API
lenin-ribeiro a1780a9
test(aicore/filtering): update integration step defs for class API
lenin-ribeiro 2c0696f
docs(aicore): rewrite Content Filtering guide for class API
lenin-ribeiro cf4a364
fix(test): explicit None-narrowing for ty in from_env() tests
lenin-ribeiro 748e298
fix(aicore): address review comments + satisfy stricter CI ty
lenin-ribeiro 8bcd53b
refactor(aicore/filtering): address review comments #11-13
lenin-ribeiro b594bd6
refactor(aicore/filtering): collapse public API into filters.py
lenin-ribeiro 3e3edb7
fix(tests): remove stale ty: ignore directives that fail CI
lenin-ribeiro 5744e6d
fix(tests): remove stale ty: ignore directives that fail CI's uvx ty
lenin-ribeiro 286afb4
refactor(aicore/filtering): merge _litellm_patch + core/env into filt…
lenin-ribeiro ae34803
fix(aicore/filtering): address PR review fixes 21-23
lenin-ribeiro 4990896
feat(aicore/fallback): add opt-in model fallback for Orchestration v2
lenin-ribeiro 7e10fff
fix(aicore/fallback): broadcast primary prompt template to fallback m…
lenin-ribeiro 932957c
chore(aicore/fallback): add py.typed marker for PEP 561 parity
lenin-ribeiro c2b73a5
Merge branch 'main' into feat/orchestration-filtering
lenin-ribeiro 865c0aa
chore: bump version to 0.30.0
lenin-ribeiro 6010e6e
Merge branch 'main' into feat/orchestration-filtering
lenin-ribeiro e24fef8
refactor(aicore/filtering): split filters.py by concern
lenin-ribeiro 1302f54
style(aicore/filtering): ruff format config.py
lenin-ribeiro 81f9bdd
Merge feat/orchestration-filtering — port fallback hooks to refactore…
lenin-ribeiro 215f3bd
refactor(aicore): align fallback branch with merged filtering package…
2b74ed3
Merge remote-tracking branch 'origin/main' into feat/aicore-model-fal…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| """Model-fallback subpackage for SAP AI Core Orchestration v2. | ||
| Re-exports the public surface defined in :mod:`.fallback`. Users should import | ||
| flat from :mod:`sap_cloud_sdk.aicore`; this package is the source of truth. | ||
| """ | ||
|
|
||
| from .fallback import FallbackConfig, FallbackModel, set_fallbacks | ||
|
|
||
| __all__ = ["FallbackModel", "FallbackConfig", "set_fallbacks"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| """LiteLLM transport patch that adds model-fallback support on top of filtering. | ||
|
|
||
| Patches ``litellm.GenAIHubOrchestrationConfig`` with a subclass of the | ||
| filtering patch (:class:`sap_cloud_sdk.aicore.filtering._patch.FilteringOrchestrationConfig`) | ||
| that adds the fallback-side hooks: | ||
|
|
||
| - ``transform_request``: | ||
| 1. Injects ``fallback_sap_modules`` into ``optional_params`` before super | ||
| reads it. LiteLLM's ``GenAIHubOrchestrationConfig.transform_request`` | ||
| pops that key to build ``body["config"]["modules"]`` as a list. | ||
| 2. After super returns, copies the primary module's prompt template into | ||
| every fallback module entry — litellm builds the primary template from | ||
| ``messages`` but defaults each fallback's template to ``[]``, which the | ||
| orchestration server rejects with | ||
| ``"config.modules[N].prompt_templating.prompt.template should be non-empty"``. | ||
| 3. When filtering is active, broadcasts the filtering configuration across | ||
| every module entry (primary + every fallback). The filtering parent | ||
| class only injects on ``modules[0]``; the broadcast here keeps the | ||
| same filter set applied for every preference the server might pick. | ||
|
|
||
| - ``transform_response``: after super has handled filter-rejection detection, | ||
| attaches ``intermediate_failures`` (the per-preference failure list) from | ||
| the 200 response body onto the returned :class:`ModelResponse` so callers | ||
| can inspect which preferences were skipped. ``None`` when the primary | ||
| succeeded. Non-streaming only in v1. | ||
|
|
||
| The two patches share the monkeypatch slot. :func:`_install_fallback` | ||
| installs this subclass (which still does filtering thanks to inheritance); | ||
| clearing fallback restores the filtering-only class (or the original) by | ||
| calling :func:`sap_cloud_sdk.aicore.filtering._patch._install` with the | ||
| filtering side's current state. Filtering's ``_install`` cooperates by | ||
| deferring to fallback when :data:`_active_fallback_cfg` is non-``None``, | ||
| so ``set_filtering`` / ``disable_filtering`` calls while fallback is | ||
| active update filtering state without clobbering our class. Idempotent. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| import litellm | ||
| from litellm.types.utils import ModelResponse | ||
|
|
||
| from ..filtering import _patch as _filter_patch | ||
| from ..filtering._patch import FilteringOrchestrationConfig | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| # Module-level fallback state. ``None`` means fallback is inactive; the | ||
| # filtering module is the source of truth for the installed class in that | ||
| # case (see :func:`_install_fallback`). | ||
| _active_fallback_cfg: Any = None # FallbackConfig | None | ||
|
|
||
|
|
||
| class OrchestrationPatchConfig(FilteringOrchestrationConfig): | ||
| """Adds model-fallback request/response hooks to the filtering patch. | ||
|
|
||
| Inherits filtering injection + rejection handling from | ||
| :class:`FilteringOrchestrationConfig`. Adds, in order: | ||
|
|
||
| - ``fallback_sap_modules`` injection (so litellm builds ``modules`` as a | ||
| list of preference dicts). | ||
| - Prompt-template broadcast to every fallback module entry. | ||
| - Filtering broadcast across every module entry (overriding the parent's | ||
| primary-only injection). | ||
| - ``intermediate_failures`` attachment on the returned ``ModelResponse``. | ||
| """ | ||
|
|
||
| def transform_request( | ||
| self, | ||
| model: str, | ||
| messages: list, | ||
| optional_params: dict, | ||
| litellm_params: dict, | ||
| headers: dict, | ||
| ) -> dict: | ||
| # Inject fallback into optional_params BEFORE super reads it. | ||
| # LiteLLM's transform_request copies optional_params and pops | ||
| # ``"fallback_sap_modules"`` to build the modules list. | ||
| if _active_fallback_cfg is not None: | ||
| optional_params["fallback_sap_modules"] = ( | ||
| _active_fallback_cfg.to_litellm_kwarg() | ||
| ) | ||
|
|
||
| body = super().transform_request( | ||
| model=model, | ||
| messages=messages, | ||
| optional_params=optional_params, | ||
| litellm_params=litellm_params, | ||
| headers=headers, | ||
| ) | ||
|
|
||
| modules = body["config"]["modules"] | ||
| # No fallback => single dict, nothing else to do here. | ||
| if not isinstance(modules, list) or len(modules) <= 1: | ||
| return body | ||
|
|
||
| # Broadcast the primary's prompt template to every fallback entry. | ||
| # litellm only builds the primary's template from ``messages``; | ||
| # fallback entries get whatever was popped from their dict's | ||
| # ``"messages"`` key (litellm transformation.py L371), which is | ||
| # ``[]`` for ``FallbackModel.to_dict()``. Without this copy, the | ||
| # server rejects with | ||
| # "config.modules[N].prompt_templating.prompt.template should be | ||
| # non-empty". | ||
| primary_template = ( | ||
| modules[0].get("prompt_templating", {}).get("prompt", {}).get("template") | ||
| ) | ||
| if primary_template: | ||
| for entry in modules[1:]: | ||
| entry.setdefault("prompt_templating", {}).setdefault("prompt", {})[ | ||
| "template" | ||
| ] = primary_template | ||
|
|
||
| # Broadcast filtering across every module entry. The filtering parent | ||
| # installed it on ``modules[0]`` only; broadcasting keeps the same | ||
| # filter set applied for every preference the server might pick. | ||
| # To opt a fallback out of filtering, call ``disable_filtering()`` | ||
| # before the call. | ||
| if _filter_patch._active_cfg is not None: | ||
| filtering_dict = _filter_patch._active_cfg.to_dict() | ||
| if filtering_dict: | ||
| for entry in modules[1:]: | ||
| entry["filtering"] = filtering_dict | ||
|
|
||
| return body | ||
|
|
||
| def transform_response( | ||
| self, | ||
| model: str, | ||
| raw_response: Any, | ||
| model_response: ModelResponse, | ||
| logging_obj: Any, | ||
| request_data: dict, | ||
| messages: list, | ||
| optional_params: dict, | ||
| litellm_params: dict, | ||
| encoding: Any, | ||
| api_key: str | None = None, | ||
| json_mode: bool | None = None, | ||
| ) -> ModelResponse: | ||
| # Let the filtering parent handle filter-rejection detection first | ||
| # (it raises ``ContentFilteredError`` before falling through to | ||
| # super-super). If it raises, we never reach the attach below. | ||
| result = super().transform_response( | ||
| model=model, | ||
| raw_response=raw_response, | ||
| model_response=model_response, | ||
| logging_obj=logging_obj, | ||
| request_data=request_data, | ||
| messages=messages, | ||
| optional_params=optional_params, | ||
| litellm_params=litellm_params, | ||
| encoding=encoding, | ||
| api_key=api_key, | ||
| json_mode=json_mode, | ||
| ) | ||
|
|
||
| # Surface ``intermediate_failures`` on the returned ``ModelResponse`` | ||
| # so callers can see which preferences were skipped. Only present on | ||
| # non-streaming 200 responses — streaming surfacing is deferred. | ||
| if raw_response.status_code == 200: | ||
| try: | ||
| payload = raw_response.json() | ||
| except ValueError: | ||
| return result | ||
| failures = payload.get("intermediate_failures") | ||
| if failures is not None: | ||
| # ``ModelResponse`` uses pydantic ``extra="allow"`` so dynamic | ||
| # attribute assignment is supported at runtime. ``setattr`` | ||
| # keeps the static type checker happy. | ||
| setattr(result, "intermediate_failures", failures) | ||
|
|
||
| return result | ||
|
|
||
|
|
||
| def _install_fallback(cfg: Any) -> None: # cfg: FallbackConfig | None | ||
| """Set the active fallback config and refresh the installed patch class. | ||
|
|
||
| When ``cfg`` is non-``None``, installs :class:`OrchestrationPatchConfig` | ||
| (which inherits filtering, so filtering still works when active). | ||
|
|
||
| When ``cfg`` is ``None``, defers to the filtering module: re-runs its | ||
| ``_install`` with whatever filtering state is currently active, which | ||
| restores either ``FilteringOrchestrationConfig`` (filtering on) or | ||
| ``_ORIGINAL_CONFIG`` (both off). | ||
|
|
||
| Idempotent — repeated calls with the same value are safe. | ||
| """ | ||
| global _active_fallback_cfg | ||
| _active_fallback_cfg = cfg | ||
| if cfg is None: | ||
| # Hand back control to the filtering installer so it restores the | ||
| # correct class for the current filtering state. | ||
| _filter_patch._install(_filter_patch._active_cfg) | ||
| logger.debug("model fallback disabled") | ||
| else: | ||
| litellm.GenAIHubOrchestrationConfig = OrchestrationPatchConfig | ||
| logger.info("model fallback active (OrchestrationPatchConfig)") | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too many magic strings.