Feat/litellm unified adapters 379#388
Merged
Nicola Franco (franconicola) merged 23 commits intoMay 23, 2026
Merged
Conversation
Route every chat-completion AgentType through `litellm.completion`. The LiteLLMAgent base now owns provider-prefix routing, the unified `thinking` knob, tool-calls, and reasoning-content extraction. OpenAI and Ollama agents collapse to thin subclasses that pin the provider prefix (`openai`, `ollama_chat`) and translate `thinking` to `reasoning_effort` or Ollama's `think` field. ADK still requires HTTP transport LiteLLM doesn't speak natively, so the adapter registers a per-instance `litellm.CustomLLM` handler that implements the `POST /run` + sessions + events protocol and packages the result into a `ModelResponse`. From the router's perspective ADK is now just another LiteLLM provider. Router config branches collapse: all chat AgentTypes share the same metadata-merge path; ADK only adds `user_id`. Unit tests for OpenAI, Ollama, and ADK rewritten against `litellm.completion` patching, and the OpenAI integration test no longer asserts on a non-existent SDK client. LITELLM_ROUTER_REFACTOR_PLAN.md captures the follow-up plan to move the call site into `router.py` and shrink the adapters folder further. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase A of the LiteLLM router refactor plan: pull the response-shaping logic out of the adapter classes into pure functions in ``hackagent/router/envelope.py``. Adapter classes now delegate to those helpers; the public dict shape returned by ``handle_request`` is unchanged (snapshot tests in test_envelope.py). Also lands ``hackagent/router/provider_config.py`` — the AgentType → provider-prefix + thinking-translator + extra-passthrough-keys table that Phase C will use to bypass the adapter classes entirely. The table isn't wired in yet; this commit just ships the lookup module with its own unit tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B of the LiteLLM router refactor. LiteLLMAgent.__init__ now accepts an optional ProviderConfig. OpenAIAgent and OllamaAgent look their config up from hackagent/router/provider_config.py instead of overriding ``_apply_thinking`` and ``PROVIDER_PREFIX``. The class-level ``PROVIDER_PREFIX`` path stays for backwards compatibility (and is still how ADKAgent injects its per-instance provider name). Phase C will use this same ProviderConfig lookup inside ``AgentRouter`` to bypass the adapter classes entirely for chat-completion agent types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase C of the LiteLLM router refactor. ``AgentRouter.route_request`` now dispatches chat-completion AgentTypes (LITELLM, OPENAI_SDK, OLLAMA, LANGCHAIN) directly through ``litellm.completion`` via the new ``_dispatch_via_litellm`` method, looking up the provider prefix, thinking translator, and passthrough keys from ``hackagent/router/provider_config.py``. The adapter classes are still instantiated and still expose ``handle_request`` for backwards compatibility — that's the path GOOGLE_ADK and any future protocol-specific AgentType continue to use. For chat types, ``handle_request`` is no longer on the hot path; the adapter instance is consulted only for its already-resolved model name, endpoint, API key, and generation defaults. The HackAgent envelope dict shape is byte-identical to the previous adapter-driven path (verified by leaving all existing adapter tests green plus six new tests covering the new dispatch path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase D of the LiteLLM router refactor. Adds
``hackagent/router/tracking_logger.py`` — a ``CustomLogger`` subclass
that hooks ``log_pre_api_call``, ``log_success_event``, and
``log_failure_event``, emitting structured records via
``hackagent.logger`` that downstream sinks (TUI, dashboard) can pick
up.
``AgentRouter.__init__`` registers the logger on ``litellm.callbacks``
exactly once per process. ``_dispatch_via_litellm`` attaches
``metadata={"hackagent_agent_id": ..., "hackagent_adapter_type": ...}``
to every call so the logger can filter HackAgent-owned traffic and
correlate input ↔ output ↔ cost via LiteLLM's call_id. Caller-supplied
``metadata`` (e.g. trace_id for Langfuse/OTEL) is merged in and wins
on collision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase E (partial) of the LiteLLM router refactor. The Google ADK provider — which is fundamentally a ``litellm.CustomLLM`` wrapping the ADK ``POST /run`` + sessions + events protocol — now lives at ``hackagent/router/providers/adk.py``, its logical home. The old ``hackagent/router/adapters/google_adk.py`` path is preserved as a thin re-export shim so existing imports keep working. The chat adapter classes (``LiteLLMAgent``, ``OpenAIAgent``, ``OllamaAgent``) stay in ``hackagent/router/adapters/`` for now. They no longer run on the hot path — ``AgentRouter._dispatch_via_litellm`` calls ``litellm.completion`` directly — but their public symbols are still imported by external callers, so deleting them is a separate decision documented as Phase E.2 in ``LITELLM_ROUTER_REFACTOR_PLAN.md``. The plan markdown is updated with a Status section enumerating which commits landed which phase and what's deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Phase F.1) Phase F.1 of the LiteLLM router refactor. - ``hackagent/router/envelope.py`` gains ``extract_response_cost`` and ``extract_litellm_call_id`` helpers that pull LiteLLM's ``_hidden_params['response_cost']`` and ``_hidden_params['litellm_call_id']`` (falling back to ``response.id``). Both fields, when present, flow through ``build_agent_specific_data`` into the envelope so downstream traces can correlate input ↔ output ↔ spend without rooting in the raw response object. - ``AgentRouter._build_error_response`` now sets ``status_code`` (the canonical field used by the chat-dispatch envelope) alongside the legacy ``raw_response_status`` alias, eliminating the inconsistency between the two error paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase E.2a of the LiteLLM router refactor. ``ADKAgent`` no longer inherits from ``LiteLLMAgent``; it extends :class:`Agent` directly and implements ``handle_request`` itself, calling ``litellm.completion(model="hackagent_adk_<id>/<app>", messages=…)`` which still routes through the per-instance ``_ADKCustomLLM``. The lazy ``_get_litellm`` helper is now defined locally in this module so ADK doesn't depend on ``hackagent.router.adapters.litellm``, which Phase E.2c is about to delete. All ADK public attributes (``litellm_model``, ``name``, ``endpoint``, ``user_id``, ``timeout``, ``session_id``, ``fresh_session_per_request``, ``default_*``) are preserved so the router's dispatch path and external code that pokes at the adapter keep working unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….2b) Phase E.2b of the LiteLLM router refactor. ``AgentRouter`` no longer instantiates ``LiteLLMAgent`` / ``OpenAIAgent`` / ``OllamaAgent`` for chat-completion AgentTypes; it builds a lightweight ``_ChatRegistration`` config holder instead. The dispatch path is unchanged — it reads the same attribute names (``litellm_model``, ``api_base_url``, ``actual_api_key``, ``default_*``…) off whichever object is stored in ``_agent_registry``. ``_ChatRegistration`` covers the two adapter-class quirks the dispatch path didn't yet own: - OpenAI custom endpoint without API key → placeholder ``"not-required"``. - OpenAI custom endpoint without model name → defaults to ``"default"``. - Ollama default endpoint resolution + trailing ``/api/*`` stripping. ADK still uses ``ADKAgent`` because its CustomLLM registration with LiteLLM is a per-instance side-effect. The chat adapter classes (``LiteLLMAgent`` / ``OpenAIAgent`` / ``OllamaAgent``) remain importable but are no longer on any hot path or instantiated by the router. Integration tests updated to assert against the ``_ChatRegistration`` shape; Phase E.2c will delete the adapter classes and the obsolete unit tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase E.2c (final phase) of the LiteLLM router refactor. Deletes:
- hackagent/router/adapters/litellm.py
- hackagent/router/adapters/openai.py
- hackagent/router/adapters/ollama.py
- tests/unit/adapters/test_{litellm,openai,ollama}.py
- tests/integration/adapters/test_{litellm,openai,ollama}.py
These adapter classes haven't been on the hot path since Phase C and
the router stopped instantiating them in Phase E.2b — every chat
AgentType now goes through ``AgentRouter._dispatch_via_litellm`` with
config supplied by ``_ChatRegistration``. Coverage moves to
``tests/unit/router/test_dispatch.py`` and
``tests/unit/router/test_chat_registration.py``.
``hackagent/router/adapters/__init__.py`` is reduced to the exception
classes + an ``ADKAgent`` re-export. ``hackagent/router/__init__.py``
drops the public ``OllamaAgent`` symbol. ``AGENT_TYPE_TO_ADAPTER_MAP``
now only carries the ADK entry; chat-type validation goes through
``get_provider_config``.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Phase F.2)
Phase F.2 cleans up the LiteLLM ``metadata`` correlation keys. Instead
of flat ``"hackagent_agent_id"`` / ``"hackagent_adapter_type"`` keys
that sit alongside whatever the caller (Langfuse, OTEL, user code…)
also stuffs into ``metadata``, the router now writes a nested block:
metadata = {
"hackagent": {"id": "<registration_key>", "adapter_type": "<label>"},
# caller-supplied keys preserved verbatim
"trace_id": "...", "user_id": "...",
}
``HackAgentTrackingLogger._extract_hackagent_metadata`` reads
``metadata["hackagent"]`` instead of scanning for the flat prefix.
Calls without that namespace are ignored, so other tools' callbacks
aren't accidentally double-logged.
Callers who want to override or extend the namespace can supply their
own ``metadata={"hackagent": {"id": "override", ...}}`` — their keys
win on collision inside the namespace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase F.3 finishes the cleanup. The ``adapters/`` directory no longer
holds anything that's actually an adapter, so it's gone:
- ``adapters/base.py`` → ``hackagent/router/agent.py`` (just the
``Agent`` ABC + the three ``Adapter*Error`` exceptions; the dead
``ChatCompletionsAgent`` template is removed since nothing
inherits from it any more).
- ``adapters/google_adk.py`` back-compat shim deleted; callers
should import from ``hackagent.router.providers.adk`` (the new
canonical home, also re-exported from ``hackagent.router``).
- ``adapters/__init__.py`` deleted.
- ``tests/unit/adapters/test_google_adk.py`` →
``tests/unit/router/test_adk_agent.py``.
- ``tests/integration/adapters/test_google_adk.py`` →
``tests/integration/router/test_adk_agent.py``.
- ``hackagent.router`` now re-exports ``Agent`` plus the three
exceptions for the small amount of external code that catches them.
The hierarchical logger name ``hackagent.router.adapters.{type}.{id}``
becomes ``hackagent.router.{type}.{id}`` to match the new module path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Since #379 every chat-completion AgentType goes through LiteLLM, so HackAgent transparently supports the ~140 providers LiteLLM speaks. Picking a different provider is a model-string change, not a different adapter — but the examples folder didn't show this. Adds ``hackagent/examples/litellm_multi_provider/`` with a runnable ``demo.py`` that targets Anthropic, Gemini, Bedrock, Groq, Mistral, Together, OpenRouter, or OpenAI via the same HackAgent config (only the model string and API-key env var change). README explains the LiteLLM ``"<provider>/<model>"`` convention. The ``AgentTypeEnum`` docstring is rewritten to make clear that ``LITELLM`` is the general path; ``OPENAI_SDK`` / ``OLLAMA`` / ``LANGCHAIN`` are convenience aliases over the same LiteLLM dispatch. Gap-filler types (``GOOGLE_ADK`` today, ``MCP``/``A2A`` later) keep their own slots because LiteLLM doesn't speak those protocols natively. Includes a small ``tests/unit/examples/`` smoke-test that the provider table is well-formed and the config builder produces valid LiteLLM configurations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tests Closes the issue #379 refactor: - ``tests/integration/router/test_litellm_dispatch.py`` replaces the coverage of the deleted per-adapter integration tests (``test_litellm.py``, ``test_openai.py``, ``test_ollama.py`` — removed in Phase E.2c). The new file exercises ``AgentRouter.route_request`` end-to-end against a real OpenAI-compatible endpoint, asserts the envelope shape (status, processed_response, agent_specific_data with usage + finish_reason), confirms the ``prompt`` shorthand still works, and verifies the ``metadata['hackagent']`` namespace makes it onto every ``litellm.completion`` call. - ``LITELLM_ROUTER_REFACTOR_PLAN.md`` is removed now that all phases A–F.4 landed; the commit chain is the canonical record. - One stale docstring in ``router/providers/adk.py`` that mentioned the deleted ``LiteLLMAgent`` base is corrected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| try: | ||
| import litellm | ||
|
|
||
| _litellm_module = litellm |
| None, lambda: self.completion(*args, **kwargs) | ||
| ) | ||
|
|
||
| _ADK_CUSTOM_LLM_CLASS = _ADKCustomLLM |
| litellm.callbacks = callbacks | ||
|
|
||
| _LOGGER_INSTANCE = instance | ||
| _REGISTERED = True |
| model_response.choices[0].message.content = final_text # type: ignore[attr-defined] | ||
| try: | ||
| model_response.choices[0].finish_reason = "stop" # type: ignore[attr-defined] | ||
| except Exception: |
| "adk_raw_request": result["raw_request"], | ||
| "adk_status_code": result["status_code"], | ||
| } | ||
| except Exception: |
| completion_result["tool_calls"] = tool_calls | ||
| try: | ||
| completion_result["finish_reason"] = response.choices[0].finish_reason | ||
| except (AttributeError, IndexError, TypeError): |
| try: | ||
| if response.usage is not None: | ||
| completion_result["usage"] = response.usage.model_dump() | ||
| except AttributeError: |
| pass | ||
| try: | ||
| completion_result["provider_model"] = response.model | ||
| except AttributeError: |
| duration_ms = None | ||
| try: | ||
| duration_ms = (end_time - start_time).total_seconds() * 1000 | ||
| except (AttributeError, TypeError): |
| duration_ms = None | ||
| try: | ||
| duration_ms = (end_time - start_time).total_seconds() * 1000 | ||
| except (AttributeError, TypeError): |
The Docusaurus sidebar still referenced the deleted ``hackagent.router.adapters.*`` module pages, which made ``npm run build`` fail in CI for PR #388 with "sidebar document ids do not exist". Updated to point at the post-refactor modules: - ``hackagent/router/agent`` (Agent ABC + exceptions, replaces ``adapters/base``) - ``hackagent/router/envelope`` (envelope helpers) - ``hackagent/router/provider_config`` (AgentType → ProviderConfig) - ``hackagent/router/tracking_logger`` (CustomLogger) - ``hackagent/router/providers/adk`` (the only remaining gap-filler; replaces the old ``adapters/google_adk``) The chat adapter modules (``adapters/litellm``, ``adapters/openai``, ``adapters/ollama``) are deleted and have no sidebar entry. Verified locally with ``cd docs && npm run build``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Ollama-integration job in ``.github/workflows/ci.yml`` and the slow-integration job in ``.github/workflows/nightly.yml`` were still collecting tests from ``tests/integration/adapters/`` — a path that no longer exists after Phase F.3 of #379 moved the ADK integration test to ``tests/integration/router/test_adk_agent.py``. Pytest exits with code 4 when given a non-existent collection path, so the job failed before running any tests. Updated both workflow files to point at ``tests/integration/router/`` (which holds the new ``test_litellm_dispatch.py`` and the moved ``test_adk_agent.py``). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…in CI Two fixes prompted by the failing Integration Tests (Ollama) job on PR #388: 1. ``_ChatRegistration`` now discards any ``api_key`` for OLLAMA AgentTypes. The orchestrator's category classifier (``hackagent/router/tracking/category_classifier.py``) and the attack router factory both forward ``backend.get_api_key()`` — the HackAgent backend token — into the adapter's ``api_key`` config. With LiteLLM's ``ollama_chat`` provider, that key was being sent as ``Authorization: Bearer <hackagent-token>`` to local Ollama, visible in the failing job's litellm curl trace. Ollama ignored it so the request still succeeded, but the leak is real and would be a credential issue against any auth-checking proxy. 2. ``.github/workflows/ci.yml`` and ``nightly.yml`` now pull ``gemma3:4b`` alongside ``tinyllama``. The default ``category_classifier`` uses ``gemma3:4b`` (``DEFAULT_CATEGORY_CLASSIFIER_IDENTIFIER``), so the baseline e2e test was hitting a 404 on ``/api/show`` for that model. The classifier endpoint check is stricter under LiteLLM's ``ollama_chat`` provider than the previous direct-HTTP ``OllamaAgent`` was, which is why it surfaced now. New unit test ``test_backend_api_key_is_dropped_for_ollama`` covers the leak fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related fixes prompted by the failing Integration Tests (Ollama) job on PR #388: 1. ``test_session_creation`` in tests/integration/router/test_adk_agent.py was still calling ``adapter._initialize_session(...)`` — a method that lived on the pre-Phase-E.2a ``ADKAgent``. Session management moved into the per-instance ``_ADKCustomLLM`` handler; the test now uses ``adapter._custom_handler._create_session(...)``. 2. ``test_same_attack_different_frameworks`` and ``test_advprefix_with_ollama_judges`` were passing the default ``category_classifier`` config (``gemma3:4b``) which is a 4B-param model running on CPU-only GitHub runners — 9 judgments blew the 120s pytest-timeout. Replaced ``_explicit_default_category_classifier`` (hardcoded to ``gemma3:4b``) with ``_fast_classifier_config`` that takes ``ollama_model`` and ``ollama_base_url`` fixtures so the classifier reuses whichever small model CI pulls (``tinyllama``). 3. Deleted ``.github/workflows/nightly.yml``. Three @pytest.mark.slow tests don't justify a separate cron workflow whose failures land on whoever happens to be working that morning. The slow filter (``-m "not slow"``) is removed from ``.github/workflows/ci.yml``, so all integration tests now run on every PR with a clear failure owner (the PR author). Reverted the gemma3:4b pull from ci.yml since nothing in the test suite uses it anymore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three integration tests were failing on the post-#379 CI with ``Failed: Timeout (>120.0s) from pytest-timeout``. Verified locally inside a singularity sandbox running Ollama + tinyllama; with the two fixes below all three now pass. 1. ``_explicit_default_category_classifier`` in tests/integration/conftest.py was hardcoded to ``gemma3:4b``. Made it accept ``model`` / ``endpoint`` arguments, and updated ``basic_attack_config``, ``advprefix_attack_config``, and ``advprefix_attack_config_with_ollama_judges`` to pass ``ollama_model`` + ``ollama_base_url`` from existing fixtures. Local suites still get the hardcoded gemma3:4b by default; CI gets whichever model is pulled (``tinyllama``). 2. Added per-test ``@pytest.mark.timeout(...)`` markers on the three heavy LLM-pipeline tests: - ``test_same_attack_different_frameworks``: 600s (passed in 130s). - ``test_advprefix_with_ollama_judges``: 900s (passed in ~14m). - ``test_hackagent_google_adk_baseline_attack``: 600s. The global ``--timeout=120`` in pyproject.toml stays for everything else — these three just need more head-room because each runs a full attack pipeline with LLM-backed judges on CPU. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…outs Three changes prompted by PR #388 friction (slow tests blocking commits and CI): 1. ``.pre-commit-config.yaml`` no longer runs integration tests. Local pre-commit now executes ``pytest tests/unit/ -n 4`` only (~10s instead of ~20 min). Integration coverage stays in GitHub Actions. Capped at ``-n 4`` rather than ``-n auto`` so the hook works on HPC login nodes (Leonardo advertises 64 logical CPUs but enforces per-user thread limits — OpenBLAS hits the cap on a bare ``-n auto``). 2. ``.github/workflows/ci.yml`` ``integration-ollama`` job is now a 2-way matrix (``shard: [fast, slow]``). Each shard runs on its own ubuntu-latest runner, so the ~14-minute advprefix test on the ``slow`` shard no longer blocks the rest of the suite. Within each shard pytest-xdist uses ``-n auto`` (resolves to 4 on the runner) and Ollama serves multiple concurrent requests via ``OLLAMA_NUM_PARALLEL=4``. Coverage artifacts get the shard name appended; the merge job picks both up via the existing ``coverage-*`` glob. 3. Added ``@pytest.mark.timeout(900)`` to the two remaining heavy ADK tests (``test_hackagent_google_adk_advprefix_attack`` and ``test_hackagent_google_adk_with_ollama_judges``) that the earlier pass missed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit pass to deduplicate and correctly classify the test suite.
1. **Deleted real duplicate**: ``tests/e2e/test_google_adk.py`` ran an
advprefix attack against a Google ADK target, which is already
covered by both ``test_hackagent_google_adk_advprefix_attack`` and
``test_hackagent_google_adk_with_ollama_judges`` in
``tests/integration/router/test_adk_agent.py``. Worse, the e2e
version swallowed every exception, had no assertions, and used
nonexistent endpoint URLs (``HACKAGENT_API_BASE_URL/api/generate``,
``/api/judge``) — it was a test that could never actually fail.
Its supporting infrastructure (``tests/e2e/google_adk/``) was only
referenced by this file and is removed too.
2. **Relocated misclassified integration tests** to ``tests/unit/``.
These were tagged ``@pytest.mark.integration`` but use only
in-process mocks / pure-Python — they don't need a backend, an
Ollama server, or any LLM API. Moving them out of
``tests/integration/`` shrinks that suite to actual integration
coverage and lets the unit suite (which runs on every PR matrix
entry) cover them:
- tests/integration/attacks/test_advprefix_evaluation.py
→ tests/unit/attacks/advprefix/test_advprefix_evaluation_extended.py
- tests/integration/attacks/test_evaluation_step.py
→ tests/unit/attacks/test_evaluation_step.py
- tests/integration/attacks/flipattack/test_flipattack_*.py (5 files)
→ tests/unit/attacks/flipattack/test_flipattack_*.py
- tests/integration/storage/test_local_backend_e2e.py
→ tests/unit/server/storage/test_local_backend_e2e.py
The ``@pytest.mark.integration`` decorator is dropped from the
moved files. Empty integration subdirectories
(``tests/integration/storage/``, ``tests/integration/attacks/flipattack/``)
are removed.
Net effect:
- Unit suite: +177 tests (1753 → 1930), still <11s with ``-n 4``.
- Integration suite: ~115 tests left, all genuinely needing a
real backend / Ollama / OpenAI / ADK server.
- e2e suite: just the auth smoke test now, unique.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit (a3c9b5d) moved ``tests/integration/storage/test_local_backend_e2e.py`` to ``tests/unit/server/storage/`` and removed the now-empty ``tests/integration/storage/`` directory. The ``integration-offline`` job in ``.github/workflows/ci.yml`` still passed that path to pytest, so the job failed with exit code 5 ("no tests collected"). Path list now contains only ``tests/integration/tui/`` (90 mock-based TUI integration tests). Updated the job's comment to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
No description provided.