This commit is contained in:
Daniel Dafoe (He/Him) 2026-08-31 09:36:50 -07:00 committed by GitHub
commit ae86fe75d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 484 additions and 0 deletions

View file

@ -279,6 +279,7 @@ curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
| [Anthropic (`anthropic`)](https://docs.litellm.ai/docs/providers/anthropic) | ✅ | ✅ | ✅ | | | | | | ✅ | |
| [Anthropic Text (`anthropic_text`)](https://docs.litellm.ai/docs/providers/anthropic) | ✅ | ✅ | ✅ | | | | | | ✅ | |
| [Anyscale](https://docs.litellm.ai/docs/providers/anyscale) | ✅ | ✅ | ✅ | | | | | | | |
| [Aquaduck (`aquaduck`)](https://docs.litellm.ai/docs/providers/aquaduck) | ✅ | | | | | | | | | |
| [AssemblyAI (`assemblyai`)](https://docs.litellm.ai/docs/pass_through/assembly_ai) | ✅ | ✅ | ✅ | | | ✅ | | | | |
| [Auto Router (`auto_router`)](https://docs.litellm.ai/docs/proxy/auto_routing) | ✅ | ✅ | ✅ | | | | | | | |
| [AWS - Bedrock (`bedrock`)](https://docs.litellm.ai/docs/providers/bedrock) | ✅ | ✅ | ✅ | ✅ | | | | | | ✅ |

View file

@ -806,6 +806,7 @@ openai_compatible_endpoints: Final[list] = [
"https://api.meta.ai/v1",
"https://api.cognition.ai/v1",
"https://api.scx.ai/v1",
"https://aqi.aquaduck.ai/v1",
]
@ -875,6 +876,7 @@ openai_compatible_providers: Final[list] = [
"meta", # Meta Model API (Muse Spark) - JSON-configured provider
"cognition",
"scx-ai",
"aquaduck",
]
openai_text_completion_compatible_providers: Final[list] = [ # providers that support `/v1/completions`
"together_ai",

View file

@ -200,5 +200,14 @@
"temperature_max": 1.99
},
"supported_endpoints": ["/v1/chat/completions"]
},
"aquaduck": {
"base_url": "https://aqi.aquaduck.ai/v1",
"api_key_env": "AQUADUCK_API_KEY",
"api_base_env": "AQUADUCK_API_BASE",
"param_mappings": {
"max_completion_tokens": "max_tokens"
},
"supported_endpoints": ["/v1/chat/completions"]
}
}

View file

@ -2816,6 +2816,66 @@
"bedrock_converse_supports_strict_tools": false,
"prompt_cache_min_tokens": 1024
},
"aquaduck/zai-org/glm-4.7-flash": {
"max_tokens": 202752,
"max_input_tokens": 202752,
"max_output_tokens": 202752,
"input_cost_per_token": 5e-08,
"output_cost_per_token": 2e-07,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": false,
"source": "https://aquaduck.ai"
},
"aquaduck/qwen/qwen3-14b": {
"max_tokens": 32768,
"max_input_tokens": 32768,
"max_output_tokens": 32768,
"input_cost_per_token": 5e-08,
"output_cost_per_token": 1.2e-07,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": false,
"source": "https://aquaduck.ai"
},
"aquaduck/qwen/qwen3.8-27b": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 2e-07,
"output_cost_per_token": 1.5e-06,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": true,
"source": "https://aquaduck.ai"
},
"aquaduck/google/gemma-4-26b-a4b-it": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 5e-08,
"output_cost_per_token": 2e-07,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": true,
"source": "https://aquaduck.ai"
},
"aquaduck/ornith-ai/ornith-1.5-9b": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 5e-08,
"output_cost_per_token": 1e-07,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": false,
"source": "https://aquaduck.ai"
},
"assemblyai/best": {
"input_cost_per_second": 3.333e-05,
"litellm_provider": "assemblyai",

View file

@ -193,6 +193,23 @@
"a2a": false
}
},
"aquaduck": {
"display_name": "Aquaduck (`aquaduck`)",
"url": "https://docs.litellm.ai/docs/providers/aquaduck",
"endpoints": {
"chat_completions": true,
"messages": false,
"responses": false,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false,
"a2a": false
}
},
"assemblyai": {
"display_name": "AssemblyAI (`assemblyai`)",
"url": "https://docs.litellm.ai/docs/pass_through/assembly_ai",

View file

@ -101,6 +101,34 @@
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "Aquaduck",
"provider_display_name": "Aquaduck AI",
"litellm_provider": "aquaduck",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://aqi.aquaduck.ai/v1",
"tooltip": null,
"required": false,
"field_type": "text",
"options": null,
"default_value": null
},
{
"key": "api_key",
"label": "API Key",
"placeholder": null,
"tooltip": null,
"required": true,
"field_type": "password",
"options": null,
"default_value": null
}
],
"default_model_placeholder": "aquaduck/zai-org/glm-4.7-flash"
},
{
"provider": "Bedrock",
"provider_display_name": "Amazon Bedrock",

View file

@ -3848,6 +3848,7 @@ class LlmProviders(str, Enum):
COGNITION = "cognition"
SCX_AI = "scx-ai"
DARKBLOOM = "darkbloom"
AQUADUCK = "aquaduck"
META = "meta"
LITELLM_AGENT = "litellm_agent"
CURSOR = "cursor"

View file

@ -2816,6 +2816,66 @@
"bedrock_converse_supports_strict_tools": false,
"prompt_cache_min_tokens": 1024
},
"aquaduck/zai-org/glm-4.7-flash": {
"max_tokens": 202752,
"max_input_tokens": 202752,
"max_output_tokens": 202752,
"input_cost_per_token": 5e-08,
"output_cost_per_token": 2e-07,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": false,
"source": "https://aquaduck.ai"
},
"aquaduck/qwen/qwen3-14b": {
"max_tokens": 32768,
"max_input_tokens": 32768,
"max_output_tokens": 32768,
"input_cost_per_token": 5e-08,
"output_cost_per_token": 1.2e-07,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": false,
"source": "https://aquaduck.ai"
},
"aquaduck/qwen/qwen3.8-27b": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 2e-07,
"output_cost_per_token": 1.5e-06,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": true,
"source": "https://aquaduck.ai"
},
"aquaduck/google/gemma-4-26b-a4b-it": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 5e-08,
"output_cost_per_token": 2e-07,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": true,
"source": "https://aquaduck.ai"
},
"aquaduck/ornith-ai/ornith-1.5-9b": {
"max_tokens": 262144,
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"input_cost_per_token": 5e-08,
"output_cost_per_token": 1e-07,
"litellm_provider": "aquaduck",
"mode": "chat",
"supports_function_calling": true,
"supports_vision": false,
"source": "https://aquaduck.ai"
},
"assemblyai/best": {
"input_cost_per_second": 3.333e-05,
"litellm_provider": "assemblyai",

View file

@ -210,6 +210,23 @@
"a2a": false
}
},
"aquaduck": {
"display_name": "Aquaduck (`aquaduck`)",
"url": "https://docs.litellm.ai/docs/providers/aquaduck",
"endpoints": {
"chat_completions": true,
"messages": false,
"responses": false,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false,
"a2a": false
}
},
"assemblyai": {
"display_name": "AssemblyAI (`assemblyai`)",
"url": "https://docs.litellm.ai/docs/pass_through/assembly_ai",

View file

@ -0,0 +1,273 @@
"""
Tests for the Aquaduck provider identity.
Aquaduck serves an OpenAI-compatible /v1/chat/completions surface at
https://aqi.aquaduck.ai/v1. It must resolve to its own `aquaduck` provider so
OpenAI-specific pricing and provider-level reporting never apply to its traffic.
"""
import json
from pathlib import Path
import pytest
import litellm
class TestAquaduckProviderIdentity:
def test_aquaduck_is_a_registered_provider(self):
from litellm import LlmProviders
assert LlmProviders.AQUADUCK.value == "aquaduck"
assert "aquaduck" in litellm.provider_list
def test_aquaduck_json_config(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
aquaduck = JSONProviderRegistry.get("aquaduck")
assert aquaduck is not None
assert aquaduck.base_url == "https://aqi.aquaduck.ai/v1"
assert aquaduck.api_key_env == "AQUADUCK_API_KEY"
assert aquaduck.api_base_env == "AQUADUCK_API_BASE"
assert aquaduck.param_mappings.get("max_completion_tokens") == "max_tokens"
assert aquaduck.supported_endpoints == ["/v1/chat/completions"]
def test_aquaduck_in_openai_compatible_providers(self):
from litellm.constants import openai_compatible_endpoints, openai_compatible_providers
assert "aquaduck" in openai_compatible_providers
assert "https://aqi.aquaduck.ai/v1" in openai_compatible_endpoints
def test_prefixed_model_resolves_to_aquaduck_not_openai(self):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
model, provider, _, api_base = get_llm_provider(
model="aquaduck/zai-org/glm-4.7-flash",
custom_llm_provider=None,
api_base=None,
api_key=None,
)
assert model == "zai-org/glm-4.7-flash"
assert provider == "aquaduck"
assert api_base == "https://aqi.aquaduck.ai/v1"
def test_explicit_api_base_and_key_win(self):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
_, provider, api_key, api_base = get_llm_provider(
model="aquaduck/zai-org/glm-4.7-flash",
custom_llm_provider=None,
api_base="https://aquaduck.internal.example/v1",
api_key="sk-test",
)
assert provider == "aquaduck"
assert api_base == "https://aquaduck.internal.example/v1"
assert api_key == "sk-test"
def test_api_base_autodetects_aquaduck(self, monkeypatch: pytest.MonkeyPatch):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
monkeypatch.setenv("AQUADUCK_API_KEY", "sk-aquaduck-env")
_, provider, api_key, api_base = get_llm_provider(
model="zai-org/glm-4.7-flash",
custom_llm_provider=None,
api_base="https://aqi.aquaduck.ai/v1",
api_key=None,
)
assert provider == "aquaduck"
assert api_base == "https://aqi.aquaduck.ai/v1"
assert api_key == "sk-aquaduck-env"
def test_autodetected_api_base_keeps_the_caller_api_key(self, monkeypatch: pytest.MonkeyPatch):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
monkeypatch.setenv("AQUADUCK_API_KEY", "sk-aquaduck-env")
_, provider, api_key, _ = get_llm_provider(
model="zai-org/glm-4.7-flash",
custom_llm_provider=None,
api_base="https://aqi.aquaduck.ai/v1",
api_key="sk-aquaduck-caller",
)
assert provider == "aquaduck"
assert api_key == "sk-aquaduck-caller"
def test_env_api_key_is_read_from_aquaduck_variable(self, monkeypatch: pytest.MonkeyPatch):
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("AQUADUCK_API_KEY", "sk-aquaduck-env")
provider = JSONProviderRegistry.get("aquaduck")
assert provider is not None
api_base, api_key = create_config_class(provider)()._get_openai_compatible_provider_info(None, None)
assert api_base == "https://aqi.aquaduck.ai/v1"
assert api_key == "sk-aquaduck-env"
def test_max_completion_tokens_mapped(self):
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
provider = JSONProviderRegistry.get("aquaduck")
assert provider is not None
config = create_config_class(provider)()
optional_params = config.map_openai_params(
non_default_params={"max_completion_tokens": 256},
optional_params={},
model="zai-org/glm-4.7-flash",
drop_params=False,
)
assert optional_params["max_tokens"] == 256
assert "max_completion_tokens" not in optional_params
class TestAquaduckCostTracking:
AQUADUCK_MODELS = (
"aquaduck/zai-org/glm-4.7-flash",
"aquaduck/qwen/qwen3-14b",
"aquaduck/qwen/qwen3.8-27b",
"aquaduck/google/gemma-4-26b-a4b-it",
"aquaduck/ornith-ai/ornith-1.5-9b",
)
VISION_MODELS = (
"aquaduck/qwen/qwen3.8-27b",
"aquaduck/google/gemma-4-26b-a4b-it",
)
@pytest.fixture(autouse=True)
def _use_local_model_cost_map(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
litellm.get_model_info.cache_clear()
yield
litellm.get_model_info.cache_clear()
@staticmethod
def _load(path_parts):
json_path = Path(__file__).parents[4].joinpath(*path_parts)
with open(json_path) as f:
return json.load(f)
@pytest.mark.parametrize(
"model, input_cost, output_cost",
[
("aquaduck/zai-org/glm-4.7-flash", 5e-08, 2e-07),
("aquaduck/qwen/qwen3-14b", 5e-08, 1.2e-07),
("aquaduck/qwen/qwen3.8-27b", 2e-07, 1.5e-06),
("aquaduck/google/gemma-4-26b-a4b-it", 5e-08, 2e-07),
("aquaduck/ornith-ai/ornith-1.5-9b", 5e-08, 1e-07),
],
)
def test_cost_map_entries(self, model: str, input_cost: float, output_cost: float):
info = litellm.get_model_info(model=model)
assert info["litellm_provider"] == "aquaduck"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == input_cost
assert info["output_cost_per_token"] == output_cost
assert info.get("supports_vision", False) is (model in self.VISION_MODELS)
def test_aquaduck_models_synced_to_backup(self):
model_cost = self._load(("model_prices_and_context_window.json",))
backup = self._load(("litellm", "model_prices_and_context_window_backup.json"))
for model in self.AQUADUCK_MODELS:
assert model in backup, f"{model} missing from backup json"
assert backup[model] == model_cost[model], f"{model} differs between root and backup json"
@pytest.mark.parametrize(
"model, expected_prompt_cost, expected_completion_cost",
[
("aquaduck/zai-org/glm-4.7-flash", 0.05, 0.2),
("aquaduck/qwen/qwen3.8-27b", 0.2, 1.5),
],
)
def test_cost_per_million_tokens(
self, model: str, expected_prompt_cost: float, expected_completion_cost: float
):
from litellm.cost_calculator import cost_per_token
prompt_cost, completion_cost = cost_per_token(
model=model,
prompt_tokens=1_000_000,
completion_tokens=1_000_000,
custom_llm_provider="aquaduck",
)
assert prompt_cost == pytest.approx(expected_prompt_cost)
assert completion_cost == pytest.approx(expected_completion_cost)
def test_supported_endpoints_matrix(self):
matrix = json.loads((Path(litellm.__file__).parent / "provider_endpoints_support_backup.json").read_text())
endpoints = matrix["providers"]["aquaduck"]["endpoints"]
assert endpoints["chat_completions"] is True
assert endpoints["messages"] is False
assert endpoints["responses"] is False
assert endpoints["embeddings"] is False
class TestAquaduckRouting:
@pytest.fixture(autouse=True)
def _use_local_model_cost_map(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
litellm.get_model_info.cache_clear()
yield
litellm.get_model_info.cache_clear()
@pytest.mark.asyncio
async def test_router_spend_is_attributed_to_aquaduck_pricing(self):
from litellm import Router
router = Router(
model_list=[
{
"model_name": "glm-flash",
"litellm_params": {
"model": "aquaduck/zai-org/glm-4.7-flash",
"api_key": "sk-test",
},
}
]
)
response = await router.acompletion(
model="glm-flash",
messages=[{"role": "user", "content": "hi"}],
mock_response="hello from aquaduck",
)
usage = response.usage
expected = usage.prompt_tokens * 5e-08 + usage.completion_tokens * 2e-07
assert response._hidden_params["response_cost"] == pytest.approx(expected)
class TestAquaduckDashboardRegistration:
@staticmethod
def _provider_create_fields():
path = Path(litellm.__file__).parent / "proxy" / "public_endpoints" / "provider_create_fields.json"
with open(path) as f:
return json.load(f)
def test_aquaduck_is_selectable_in_the_add_model_form(self):
entries = [e for e in self._provider_create_fields() if e["litellm_provider"] == "aquaduck"]
assert len(entries) == 1, "aquaduck must appear exactly once in provider_create_fields.json"
entry = entries[0]
assert entry["provider"] == "Aquaduck"
assert entry["provider_display_name"] == "Aquaduck AI"
assert entry["default_model_placeholder"].startswith("aquaduck/")
fields = {f["key"]: f for f in entry["credential_fields"]}
assert fields["api_key"]["required"] is True
assert fields["api_key"]["field_type"] == "password"
assert fields["api_base"]["required"] is False
assert fields["api_base"]["placeholder"] == "https://aqi.aquaduck.ai/v1"

View file

@ -73,6 +73,14 @@ describe("provider_info_helpers", () => {
expect(fromEnumKey.logo).toBe(providerLogoMap[Providers.SCX_AI]);
});
it("should map aquaduck slug and Aquaduck enum key to the Aquaduck display name", () => {
const fromSlug = getProviderLogoAndName("aquaduck");
expect(fromSlug.displayName).toBe(Providers.Aquaduck);
const fromEnumKey = getProviderLogoAndName("Aquaduck");
expect(fromEnumKey.displayName).toBe(Providers.Aquaduck);
});
it("should map bedrock_mantle slug to Bedrock Mantle display name and logo", () => {
const result = getProviderLogoAndName("bedrock_mantle");
expect(result.displayName).toBe(Providers.BedrockMantle);
@ -146,6 +154,7 @@ describe("provider_info_helpers", () => {
it("should map every provider to a bundled logo except the known logoless set, never a raw /ui/assets path", () => {
const knownLogolessProviders = [
Providers.AUTO_ROUTER,
Providers.Aquaduck,
Providers.BYTEZ,
Providers.CLARIFAI,
Providers.Cognition,
@ -195,6 +204,10 @@ describe("provider_info_helpers", () => {
expect(getPlaceholder(Providers.SCX_AI)).toBe("scx-ai/GLM-5.2");
});
it("should return an aquaduck model placeholder for Aquaduck provider", () => {
expect(getPlaceholder(Providers.Aquaduck)).toBe("aquaduck/zai-org/glm-4.7-flash");
});
it("should return claude-3-opus placeholder for Anthropic provider", () => {
expect(getPlaceholder(Providers.Anthropic)).toBe("claude-3-opus");
});

View file

@ -72,6 +72,7 @@ export enum Providers {
AIOHTTP_OPENAI = "Aiohttp Openai",
Anthropic = "Anthropic",
ANTHROPIC_TEXT = "Anthropic Text",
Aquaduck = "Aquaduck AI",
AssemblyAI = "AssemblyAI",
AUTO_ROUTER = "Auto Router",
Bedrock = "Amazon Bedrock",
@ -184,6 +185,7 @@ export const provider_map: Record<string, string> = {
AIOHTTP_OPENAI: "aiohttp_openai",
Anthropic: "anthropic",
ANTHROPIC_TEXT: "anthropic_text",
Aquaduck: "aquaduck",
AssemblyAI: "assemblyai",
AUTO_ROUTER: "auto_router",
Azure: "azure",
@ -412,6 +414,7 @@ export const getProviderLogoAndName = (providerValue: string): { logo: string; d
const providerPlaceholderMap: Partial<Record<Providers, string>> = {
[Providers.AIML]: "aiml/flux-pro/v1.1",
[Providers.Anthropic]: "claude-3-opus",
[Providers.Aquaduck]: "aquaduck/zai-org/glm-4.7-flash",
[Providers.Azure]: "my-deployment",
[Providers.Azure_AI_Studio]: "azure_ai/command-r-plus",
[Providers.Bedrock]: "claude-3-opus",