Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions internal/providers/configs/aimlapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
{
"name": "aimlapi.com",
"id": "aimlapi",
"api_key": "$AIMLAPI_API_KEY",
"api_endpoint": "https://api.aimlapi.com/v1",
"type": "openai-compat",
"default_large_model_id": "openai/gpt-5.6-terra",
"default_small_model_id": "openai/gpt-5.6-luna",
"models": [
{
"id": "openai/gpt-5.6-terra",
"name": "GPT-5.6 Terra",
"cost_per_1m_in": 2.6,
"cost_per_1m_out": 15.6,
"cost_per_1m_in_cached": 3.25,
"cost_per_1m_out_cached": 0.26,
"context_window": 1050000,
"default_max_tokens": 128000,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": true
},
{
"id": "openai/gpt-5.6-luna",
"name": "GPT-5.6 Luna",
"cost_per_1m_in": 0.26,
"cost_per_1m_out": 1.56,
"cost_per_1m_in_cached": 0.325,
"cost_per_1m_out_cached": 0.026,
"context_window": 1050000,
"default_max_tokens": 128000,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": true
},
{
"id": "openai/gpt-5.3-codex",
"name": "GPT-5.3 Codex",
"cost_per_1m_in": 4.8139,
"cost_per_1m_out": 38.5112,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0.48139,
"context_window": 400000,
"default_max_tokens": 128000,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": true
},
{
"id": "anthropic/claude-opus-5",
"name": "Claude Opus 5",
"cost_per_1m_in": 6.5,
"cost_per_1m_out": 32.5,
"cost_per_1m_in_cached": 8.125,
"cost_per_1m_out_cached": 0.65,
"context_window": 1000000,
"default_max_tokens": 128000,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": true
},
{
"id": "anthropic/claude-sonnet-5",
"name": "Claude Sonnet 5",
"cost_per_1m_in": 2.6,
"cost_per_1m_out": 13,
"cost_per_1m_in_cached": 3.25,
"cost_per_1m_out_cached": 0.26,
"context_window": 1000000,
"default_max_tokens": 128000,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": true
},
{
"id": "anthropic/claude-haiku-4.5",
"name": "Claude 4.5 Haiku",
"cost_per_1m_in": 1.3754,
"cost_per_1m_out": 6.877,
"cost_per_1m_in_cached": 1.71925,
"cost_per_1m_out_cached": 0.13754,
"context_window": 200000,
"default_max_tokens": 64000,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": true
},
{
"id": "google/gemini-3.8-flash",
"name": "Gemini 3.8 Flash",
"cost_per_1m_in": 0.975,
"cost_per_1m_out": 4.875,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0.0975,
"context_window": 1048576,
"default_max_tokens": 65536,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": true
},
{
"id": "deepseek/deepseek-v4-pro",
"name": "DeepSeek V4 Pro",
"cost_per_1m_in": 0.5655,
"cost_per_1m_out": 1.131,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0.0047125,
"context_window": 1000000,
"default_max_tokens": 128000,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": false
},
{
"id": "z-ai/glm-5.3-flash",
"name": "GLM 5.3 Flash",
"cost_per_1m_in": 0.0975,
"cost_per_1m_out": 0.325,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0.0195,
"context_window": 1048576,
"default_max_tokens": 128000,
"can_reason": true,
"reasoning_levels": [
"low",
"medium",
"high"
],
"default_reasoning_effort": "medium",
"supports_attachments": true
}
],
"default_headers": {
"HTTP-Referer": "https://charm.land",
"X-Title": "Crush",
"X-AIMLAPI-Partner-ID": "part_crush",
"X-AIMLAPI-Source": "agent/crush"
}
}
9 changes: 9 additions & 0 deletions internal/providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
//go:embed configs/aihubmix.json
var aiHubMixConfig []byte

//go:embed configs/aimlapi.json
var aimlAPIConfig []byte

//go:embed configs/alibaba-singapore.json
var alibabaSingaporeConfig []byte

Expand Down Expand Up @@ -136,6 +139,8 @@ var zhipuCodingConfig []byte
type ProviderFunc func() catwalk.Provider

var providerRegistry = []ProviderFunc{
aimlAPIProvider,

// Let's keep the main providers at the top.
anthropicProvider,
openAIProvider,
Expand Down Expand Up @@ -204,6 +209,10 @@ func aiHubMixProvider() catwalk.Provider {
return loadProviderFromConfig(aiHubMixConfig)
}

func aimlAPIProvider() catwalk.Provider {
return loadProviderFromConfig(aimlAPIConfig)
}

func alibabaSingaporeProvider() catwalk.Provider {
return loadProviderFromConfig(alibabaSingaporeConfig)
}
Expand Down
34 changes: 34 additions & 0 deletions internal/providers/providers_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package providers

import (
"regexp"
"slices"
"strings"
"testing"

"charm.land/catwalk/pkg/catwalk"
)

func TestValidDefaultModels(t *testing.T) {
Expand All @@ -21,3 +25,33 @@ func TestValidDefaultModels(t *testing.T) {
})
}
}

// aimlapiPartnerIDPattern is the shape the aimlapi.com gateway accepts for
// X-AIMLAPI-Partner-ID. A value that does not match is silently ignored
// server-side, so a typo here would never surface at runtime.
var aimlapiPartnerIDPattern = regexp.MustCompile(`^part_[A-Za-z0-9]{1,64}$`)

func TestAIMLAPIDefaultHeaders(t *testing.T) {
var found bool
for _, p := range GetAll() {
if p.ID != catwalk.InferenceProviderAIMLAPI {
// Attribution headers must never ride along to another provider.
for k := range p.DefaultHeaders {
if strings.HasPrefix(strings.ToLower(k), "x-aimlapi-") {
t.Errorf("provider %q must not send %q", p.ID, k)
}
}
continue
}
found = true
if got := p.DefaultHeaders["X-AIMLAPI-Partner-ID"]; !aimlapiPartnerIDPattern.MatchString(got) {
t.Errorf("X-AIMLAPI-Partner-ID %q does not match %s", got, aimlapiPartnerIDPattern)
}
if got, want := p.DefaultHeaders["X-AIMLAPI-Source"], "agent/crush"; got != want {
t.Errorf("X-AIMLAPI-Source = %q, want %q", got, want)
}
}
if !found {
t.Fatalf("provider %q not registered", catwalk.InferenceProviderAIMLAPI)
}
}
2 changes: 2 additions & 0 deletions pkg/catwalk/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const (
InferenceProviderBaseten InferenceProvider = "baseten"
InferenceProviderMoonshot InferenceProvider = "moonshot"
InferenceProviderAtlasCloud InferenceProvider = "atlascloud"
InferenceProviderAIMLAPI InferenceProvider = "aimlapi"
)

// Provider represents an AI provider configuration.
Expand Down Expand Up @@ -140,6 +141,7 @@ func KnownProviders() []InferenceProvider {
InferenceProviderBaseten,
InferenceProviderMoonshot,
InferenceProviderAtlasCloud,
InferenceProviderAIMLAPI,
}
}

Expand Down