-
-
Notifications
You must be signed in to change notification settings - Fork 106
Add Kilocode provider support #115
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
Changes from 1 commit
e11ea15
ece8d3a
ec7d93a
52ebc4d
4fbe556
46e99ae
31c884d
dbec473
b458835
07bf628
5d1922e
d550021
36aa94c
c9043e0
cdc2486
3317fcb
33e06f4
1f8d665
e907994
b7a5346
a5789c7
0920df9
6e34ed8
f9cb1fa
c58d07c
9401d51
4934876
09ff3a5
82bb900
decad17
1da0792
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |||||||||||
| "cohere": "https://api.cohere.ai/v1", | ||||||||||||
| "bedrock": "https://bedrock-runtime.us-east-1.amazonaws.com", | ||||||||||||
| "openrouter": "https://openrouter.ai/api/v1", | ||||||||||||
| "kilocode": "https://kilocode.ai/api/openrouter", | ||||||||||||
|
greptile-apps[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect URL will be constructed for Kilocode requests The return f"{base_url}/v1/{action}"This produces To align with how LiteLLM constructs the URL, add a Kilocode-specific case before the fallback, or structure the entry to end with
Suggested change
|
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| def get_provider_endpoint(provider: str, model_name: str, incoming_path: str) -> Optional[str]: | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -443,9 +443,9 @@ def __init__( | |
| custom_caps[provider][tier_key][model_key] = {} | ||
|
|
||
| # Store max_requests value | ||
| custom_caps[provider][tier_key][model_key]["max_requests"] = ( | ||
| env_value | ||
| ) | ||
| custom_caps[provider][tier_key][model_key][ | ||
| "max_requests" | ||
| ] = env_value | ||
|
|
||
| elif env_key.startswith(cooldown_prefix): | ||
| # Parse cooldown config | ||
|
|
@@ -1476,9 +1476,9 @@ async def _execute_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
|
|
||
| # Record in accumulator for client reporting | ||
|
|
@@ -1519,9 +1519,9 @@ async def _execute_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
| classified_error = classify_error(e, provider=provider) | ||
| error_message = str(e).split("\n")[0] | ||
|
|
@@ -1569,9 +1569,9 @@ async def _execute_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
| classified_error = classify_error(e, provider=provider) | ||
| error_message = str(e).split("\n")[0] | ||
|
|
@@ -1654,14 +1654,22 @@ async def _execute_with_retry( | |
|
|
||
| if "gemma-3" in model and "messages" in litellm_kwargs: | ||
| litellm_kwargs["messages"] = [ | ||
| {"role": "user", "content": m["content"]} | ||
| if m.get("role") == "system" | ||
| else m | ||
| ( | ||
| {"role": "user", "content": m["content"]} | ||
| if m.get("role") == "system" | ||
| else m | ||
| ) | ||
| for m in litellm_kwargs["messages"] | ||
| ] | ||
|
|
||
| litellm_kwargs = sanitize_request_payload(litellm_kwargs, model) | ||
|
|
||
| # If the provider is 'nvidia', set the custom provider to 'nvidia_nim' | ||
| # and strip the prefix from the model name for LiteLLM. | ||
| if provider == "nvidia": | ||
| litellm_kwargs["custom_llm_provider"] = "nvidia_nim" | ||
| litellm_kwargs["model"] = model.split("/", 1)[1] | ||
|
Comment on lines
+2361
to
+2365
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
|
|
||
| for attempt in range(self.max_retries): | ||
| try: | ||
| lib_logger.info( | ||
|
|
@@ -1716,9 +1724,9 @@ async def _execute_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
| classified_error = classify_error(e, provider=provider) | ||
|
|
||
|
|
@@ -1760,9 +1768,9 @@ async def _execute_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
| classified_error = classify_error(e, provider=provider) | ||
| error_message = str(e).split("\n")[0] | ||
|
|
@@ -1815,9 +1823,9 @@ async def _execute_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
|
|
||
| classified_error = classify_error(e, provider=provider) | ||
|
|
@@ -1878,9 +1886,9 @@ async def _execute_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
|
|
||
| if request and await request.is_disconnected(): | ||
|
|
@@ -2257,9 +2265,9 @@ async def _streaming_acompletion_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
|
|
||
| # Record in accumulator for client reporting | ||
|
|
@@ -2302,9 +2310,9 @@ async def _streaming_acompletion_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
| classified_error = classify_error(e, provider=provider) | ||
| error_message = str(e).split("\n")[0] | ||
|
|
@@ -2352,9 +2360,9 @@ async def _streaming_acompletion_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
| classified_error = classify_error(e, provider=provider) | ||
| error_message = str(e).split("\n")[0] | ||
|
|
@@ -2426,9 +2434,11 @@ async def _streaming_acompletion_with_retry( | |
|
|
||
| if "gemma-3" in model and "messages" in litellm_kwargs: | ||
| litellm_kwargs["messages"] = [ | ||
| {"role": "user", "content": m["content"]} | ||
| if m.get("role") == "system" | ||
| else m | ||
| ( | ||
| {"role": "user", "content": m["content"]} | ||
| if m.get("role") == "system" | ||
| else m | ||
| ) | ||
| for m in litellm_kwargs["messages"] | ||
| ] | ||
|
|
||
|
|
@@ -2533,9 +2543,9 @@ async def _streaming_acompletion_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| raw_response_text=cleaned_str, | ||
| ) | ||
|
|
||
|
|
@@ -2629,9 +2639,9 @@ async def _streaming_acompletion_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
| classified_error = classify_error(e, provider=provider) | ||
| error_message_text = str(e).split("\n")[0] | ||
|
|
@@ -2680,9 +2690,9 @@ async def _streaming_acompletion_with_retry( | |
| model=model, | ||
| attempt=attempt + 1, | ||
| error=e, | ||
| request_headers=dict(request.headers) | ||
| if request | ||
| else {}, | ||
| request_headers=( | ||
| dict(request.headers) if request else {} | ||
| ), | ||
| ) | ||
| classified_error = classify_error(e, provider=provider) | ||
| error_message_text = str(e).split("\n")[0] | ||
|
|
@@ -3127,7 +3137,9 @@ async def get_quota_stats( | |
| group_stats["total_requests_remaining"] = 0 | ||
| # Fallback to avg_remaining_pct when max_requests unavailable | ||
| # This handles providers like Firmware that only provide percentage | ||
| group_stats["total_remaining_pct"] = group_stats.get("avg_remaining_pct") | ||
| group_stats["total_remaining_pct"] = group_stats.get( | ||
| "avg_remaining_pct" | ||
| ) | ||
|
|
||
| prov_stats["quota_groups"][group_name] = group_stats | ||
|
|
||
|
|
@@ -3334,9 +3346,9 @@ async def force_refresh_quota( | |
| """ | ||
| result = { | ||
| "action": "force_refresh", | ||
| "scope": "credential" | ||
| if credential | ||
| else ("provider" if provider else "all"), | ||
| "scope": ( | ||
| "credential" if credential else ("provider" if provider else "all") | ||
| ), | ||
| "provider": provider, | ||
| "credential": credential, | ||
| "credentials_refreshed": 0, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # SPDX-License-Identifier: LGPL-3.0-only | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Copyright (c) 2026 Mirrowel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import httpx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import List | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .provider_interface import ProviderInterface | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lib_logger = logging.getLogger('rotator_library') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lib_logger.propagate = False # Ensure this logger doesn't propagate to root | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not lib_logger.handlers: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lib_logger.addHandler(logging.NullHandler()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class KilocodeProvider(ProviderInterface): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Provider implementation for the Kilocode API. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Kilocode routes requests to various providers through model prefixes: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - minimax/minimax-m2.1:free | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - moonshotai/kimi-k2.5:free | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - z-ai/glm-4.7:free | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - And other provider/model combinations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def get_models(self, api_key: str, client: httpx.AsyncClient) -> List[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistency: Unlike api_base = os.getenv("KILOCODE_API_BASE", "https://kilocode.ai/api/openrouter").rstrip("/")
response = await client.get(
f"{api_base}/models",
...
) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Fetches the list of available models from the Kilocode API. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = await client.get( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "https://kilocode.ai/api/openrouter/models", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This URL is hardcoded and ignores the |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers={"Authorization": f"Bearer {api_key}"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded URL ignores The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.raise_for_status() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [f"kilocode/{model['id']}" for model in response.json().get("data", [])] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except httpx.RequestError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uncaught
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uncaught
Add
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lib_logger.error(f"Failed to fetch Kilocode models: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent with import os
api_base = os.getenv("KILOCODE_API_BASE", "https://kilocode.ai/api/openrouter").rstrip("/")
response = await client.get(
f"{api_base}/models",
headers={"Authorization": f"Bearer {api_key}"},
)Prompt To Fix With AIThis is a comment left during a code review.
Path: src/rotator_library/providers/kilocode_provider.py
Line: 28-43
Comment:
Inconsistent with `OpencodeProvider` which uses configurable `OPENCODE_API_BASE`. Consider using `KILOCODE_API_BASE` env var here for consistency:
```python
import os
api_base = os.getenv("KILOCODE_API_BASE", "https://kilocode.ai/api/openrouter").rstrip("/")
response = await client.get(
f"{api_base}/models",
headers={"Authorization": f"Bearer {api_key}"},
)
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Comment on lines
+29
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uncaught
Suggested change
Comment on lines
+25
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong domain in Line 31 uses This breaks model discovery and ignores user overrides. For consistency with import os
api_base = os.getenv("KILOCODE_API_BASE", "https://kilocode.ai/api/openrouter").rstrip("/")
response = await client.get(
f"{api_base}/models",
headers={"Authorization": f"Bearer {api_key}"},
)
Comment on lines
+29
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uncaught
Add except (httpx.RequestError, httpx.HTTPError) as e:
Comment on lines
+34
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes.
Source(s): Citations: 🏁 Script executed: head -50 src/rotator_library/providers/kilocode_provider.py | cat -nRepository: Mirrowel/LLM-API-Key-Proxy Length of output: 1964 Catch
Change the exception handler to catch Suggested fix- except httpx.RequestError as e:
- lib_logger.error(f"Failed to fetch Kilocode models: {e}")
+ except httpx.HTTPError as e:
+ lib_logger.error("Failed to fetch Kilocode models: %s", e)
return []📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.4)[warning] 39-39: Use Replace with (TRY400) [warning] 39-39: Logging statement uses f-string (G004) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
The PR description mentions
https://kilocode.ai/api/openrouter/v1/chat/completionsas the endpoint, but here/v1is missing. Please verify if Kilocode requires/v1in the path for OpenRouter compatibility.