Skip to content
Merged
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ FROM openclaw:latest
CLAW_TYPE openclaw
AGENT AGENTS.md # behavioral contract — bind-mounted read-only

MODEL primary openrouter/anthropic/claude-sonnet-4
MODEL fallback anthropic/claude-haiku-3-5
MODEL primary openrouter/anthropic/claude-sonnet-5
MODEL fallback anthropic/claude-haiku-4-5

CLLAMA passthrough # governance proxy — credential starvation + cost tracking

Expand Down Expand Up @@ -574,8 +574,8 @@ $ claw audit --since 24h
Pod: research-pod
Events: 460
CLAW REQ RESP ERR INT TOOLS TOOL_ERR TOK_IN TOK_OUT COST_USD MODELS
analyst 142 142 0 3 18 0 284011 39402 1.8742 anthropic/claude-sonnet-4
researcher 88 87 1 0 5 0 151233 20118 0.9931 anthropic/claude-sonnet-4
analyst 142 142 0 3 18 0 284011 39402 1.8742 anthropic/claude-sonnet-5
researcher 88 87 1 0 5 0 151233 20118 0.9931 anthropic/claude-sonnet-5

Totals: req=230 resp=229 err=1 int=3 tools=23/0 tokens=435244/59520 cost=$2.8673
```
Expand Down
24 changes: 22 additions & 2 deletions cmd/claw/compose_up.go
Original file line number Diff line number Diff line change
Expand Up @@ -3316,8 +3316,11 @@ func mergeResolvedSkills(imageSkills, podSkills []driver.ResolvedSkill) []driver
}

// mergeModelSlots overlays pod-declared model slots onto image-declared slots.
// Image-only slots are preserved; pod entries replace image entries by key.
// Empty or nil pod maps suppress pod defaults only; image labels still apply.
// Image-only slots are preserved; pod entries replace image entries by key,
// except the fallback family (fallback, fallback-2, ...), which replaces
// atomically: any pod-declared fallback chain drops the image's entire chain
// so the two can never interleave. Empty or nil pod maps suppress pod defaults
// only; image labels still apply.
func mergeModelSlots(image, pod map[string]string) map[string]string {
out := cloneStringMap(image)
if len(pod) == 0 {
Expand All @@ -3326,7 +3329,24 @@ func mergeModelSlots(image, pod map[string]string) map[string]string {
if out == nil {
out = make(map[string]string, len(pod))
}
podDeclaresFallback := false
for key := range pod {
if cllama.FallbackSlotOrdinal(key) > 0 {
podDeclaresFallback = true
break
}
}
if podDeclaresFallback {
for key := range out {
if cllama.FallbackSlotOrdinal(key) > 0 {
delete(out, key)
}
}
}
for key, value := range pod {
if cllama.FallbackSlotOrdinal(key) > 0 && strings.TrimSpace(value) == "" {
continue
}
out[key] = value
}
return out
Expand Down
59 changes: 58 additions & 1 deletion cmd/claw/compose_up_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ func TestMergeModelSlots(t *testing.T) {
pod: nil,
want: nil,
},
{
name: "pod fallback replaces entire image fallback family",
image: map[string]string{"primary": "image-primary", "fallback": "image-fb", "fallback-2": "image-fb2"},
pod: map[string]string{"fallback": "pod-fb"},
want: map[string]string{"primary": "image-primary", "fallback": "pod-fb"},
},
{
name: "pod fallback chain replaces image scalar fallback",
image: map[string]string{"primary": "image-primary", "fallback": "image-fb"},
pod: map[string]string{"fallback": "pod-fb", "fallback-2": "pod-fb2"},
want: map[string]string{"primary": "image-primary", "fallback": "pod-fb", "fallback-2": "pod-fb2"},
},
{
name: "pod empty fallback list clears image fallback family",
image: map[string]string{"primary": "image-primary", "fallback": "image-fb", "fallback-2": "image-fb2"},
pod: map[string]string{"fallback": ""},
want: map[string]string{"primary": "image-primary"},
},
{
name: "image fallback family preserved when pod declares none",
image: map[string]string{"primary": "image-primary", "fallback": "image-fb", "fallback-2": "image-fb2"},
pod: map[string]string{"primary": "pod-primary"},
want: map[string]string{"primary": "pod-primary", "fallback": "image-fb", "fallback-2": "image-fb2"},
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -153,6 +177,39 @@ services:
t.Fatalf("%s: expected image-declared slots to remain after pod-default suppression, got %v", serviceName, got)
}
}

const clearDefaultsYAML = `
x-claw:
pod: clear-model-defaults
models-defaults:
primary: pod-default-primary
fallback:
- pod-default-fallback
- pod-default-fallback-2

services:
clear_fallbacks:
image: clear-fallbacks:latest
x-claw:
agent: ./AGENTS.md
models:
fallback: []
`

parsed, err = pod.Parse(strings.NewReader(clearDefaultsYAML))
if err != nil {
t.Fatalf("Parse clear defaults: %v", err)
}
service := parsed.Services["clear_fallbacks"]
got := mergeModelSlots(map[string]string{
"primary": "image-primary",
"fallback": "image-fallback",
"fallback-2": "image-fallback-2",
}, service.Claw.Models)
want := map[string]string{"primary": "pod-default-primary"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("explicit empty fallback list must clear inherited and image fallback chains: got %v, want %v", got, want)
}
}

func TestBuiltinClawAPIDescriptorUsesShortFleetAlertsFeedWindow(t *testing.T) {
Expand Down Expand Up @@ -2901,7 +2958,7 @@ func TestMergeProviderSeedsNoKeylessProviderWithoutModelRef(t *testing.T) {
claws := map[string]*driver.ResolvedClaw{
"analyst": {
Cllama: []string{"passthrough"},
Models: map[string]string{"primary": "openrouter/google/gemini-2.5-flash"},
Models: map[string]string{"primary": "openrouter/google/gemini-3.6-flash"},
},
}
if err := mergeProviderSeeds(dir, p, claws); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/claw/scaffold_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
const (
defaultAgentName = "assistant"
defaultClawType = "openclaw"
defaultModel = "openrouter/anthropic/claude-sonnet-4"
defaultModel = "openrouter/anthropic/claude-sonnet-5"
defaultPlatform = "discord"
defaultCllamaType = "passthrough"
)
Expand Down
10 changes: 5 additions & 5 deletions cmd/claw/skill_data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ FROM openclaw:latest
CLAW_TYPE openclaw # REQUIRED: selects runtime driver
AGENT AGENTS.md # behavioral contract — must exist on host

MODEL primary openrouter/anthropic/claude-sonnet-4
MODEL fallback anthropic/claude-haiku-3-5
MODEL primary openrouter/anthropic/claude-sonnet-5
MODEL fallback anthropic/claude-haiku-4-5

CLLAMA passthrough # governance proxy type
PERSONA ./personas/trader # identity materialization (local or OCI)
Expand Down Expand Up @@ -391,9 +391,9 @@ The proxy sits between agents and LLM providers. Agents get bearer tokens, proxy

| Provider | Auth | Model format |
|----------|------|-------------|
| OpenAI | Bearer | `openai/gpt-4o` |
| Anthropic | X-Api-Key | `anthropic/claude-sonnet-4` |
| OpenRouter | Bearer | `openrouter/anthropic/claude-sonnet-4` |
| OpenAI | Bearer | `openai/gpt-5.6` |
| Anthropic | X-Api-Key | `anthropic/claude-sonnet-5` |
| OpenRouter | Bearer | `openrouter/anthropic/claude-sonnet-5` |
| xAI | Bearer | `xai/grok-3` |
| Ollama | None | `ollama/llama3` |

Expand Down
Loading
Loading