One command, four wire protocols, no credentials. The sandbox binds to loopback by default because it is not a security boundary.
Reference#
go run ./cmd/skyl-sandbox [-addr host:port] [-api-key KEY]| Flag | Default | Purpose |
|---|---|---|
-addr | 127.0.0.1:8099 | Listen address. Loopback by default, deliberately. |
-api-key | sandbox-key | The credential every mount accepts. |
Pass -addr 127.0.0.1:0 to let the OS choose a free port — which is what you
want when starting it from tests, so parallel packages cannot collide.
The mounts#
| Mount | Base URL | Auth header | Models |
|---|---|---|---|
anthropic | http://127.0.0.1:8099/anthropic | x-api-key | claude-opus-5, claude-sonnet-5, claude-haiku-4-5 |
openai | http://127.0.0.1:8099/openai/v1 | Authorization: Bearer | gpt-5.6, gpt-5.4-nano |
gemini | http://127.0.0.1:8099/gemini/v1beta | x-goog-api-key | gemini-3.6-flash, gemini-3.6-pro |
openaicompat | http://127.0.0.1:8099/compat/v1 | Authorization: Bearer (or none) | gpt-5.6, gpt-5.4-nano |
Each mount checks the header its real counterpart uses, so the adapters' credential handling is genuinely exercised rather than bypassed. The OpenAI-compatible mount also accepts no credential at all, because that is how Ollama, LM Studio and llama.cpp behave.
Wiring each adapter#
p := anthropic.New("sandbox-key",
anthropic.WithBaseURL("http://127.0.0.1:8099/anthropic"))
// models: claude-opus-5, claude-sonnet-5, claude-haiku-4-5p := anthropic.New("sandbox-key",
anthropic.WithBaseURL("http://127.0.0.1:8099/anthropic"))
// models: claude-opus-5, claude-sonnet-5, claude-haiku-4-5p := openai.New("sandbox-key",
openai.WithBaseURL("http://127.0.0.1:8099/openai/v1"))
// models: gpt-5.6, gpt-5.4-nanop := openai.New("sandbox-key",
openai.WithBaseURL("http://127.0.0.1:8099/openai/v1"))
// models: gpt-5.6, gpt-5.4-nanop := gemini.New("sandbox-key",
gemini.WithBaseURL("http://127.0.0.1:8099/gemini/v1beta"))
// models: gemini-3.6-flash, gemini-3.6-prop := gemini.New("sandbox-key",
gemini.WithBaseURL("http://127.0.0.1:8099/gemini/v1beta"))
// models: gemini-3.6-flash, gemini-3.6-prop := openaicompat.New(
openaicompat.WithBaseURL("http://127.0.0.1:8099/compat/v1"),
openaicompat.WithName("sandbox"),
)
// models: gpt-5.6, gpt-5.4-nanop := openaicompat.New(
openaicompat.WithBaseURL("http://127.0.0.1:8099/compat/v1"),
openaicompat.WithName("sandbox"),
)
// models: gpt-5.6, gpt-5.4-nanoThe model catalogue#
Each mount serves a small fixed list and rejects anything else with that provider's own 404.
Starting it from tests#
func startSandbox(t *testing.T) string {
t.Helper()
// :0 asks the OS for a free port, so parallel packages cannot collide.
cmd := exec.Command("go", "run",
"github.com/BAGOMBEKA-JOB-DEV/skyl/cmd/skyl-sandbox", "-addr", "127.0.0.1:0")
stderr, err := cmd.StderrPipe()
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = cmd.Process.Kill() })
sc := bufio.NewScanner(stderr)
for sc.Scan() {
if addr := parseAddr(sc.Text()); addr != "" {
return addr
}
}
t.Fatal("sandbox did not report a listen address")
return ""
}func startSandbox(t *testing.T) string {
t.Helper()
// :0 asks the OS for a free port, so parallel packages cannot collide.
cmd := exec.Command("go", "run",
"github.com/BAGOMBEKA-JOB-DEV/skyl/cmd/skyl-sandbox", "-addr", "127.0.0.1:0")
stderr, err := cmd.StderrPipe()
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = cmd.Process.Kill() })
sc := bufio.NewScanner(stderr)
for sc.Scan() {
if addr := parseAddr(sc.Text()); addr != "" {
return addr
}
}
t.Fatal("sandbox did not report a listen address")
return ""
}Troubleshooting#
404 on a model I expected
The catalogue is deliberately small. Use one of the IDs listed above, or a
sandbox-* fault model.
401 from a mount
Each mount checks the header its real counterpart uses — x-api-key,
Authorization: Bearer, x-goog-api-key. If you built the provider by hand,
check you are using the matching adapter for the mount.
Can I run it on 0.0.0.0?
You can, with -addr. Do not. It authenticates nothing meaningfully, and
loopback is the default for that reason.