mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-26 01:21:30 +02:00
[proxy,management] Conform the Agent Network endpoint to the LLM gateway protocol Reviewed the proxy against Claude Code's published gateway contract. The transport layer already held up; fourteen gaps sat one layer up, in the model catalog and in the non-inference endpoints clients call. Two of them cost money. The catalog carried no claude-opus-5 or claude-sonnet-5, so an operator could not authorise the models coding agents default to — those requests denied as not-routable, or priced at zero where a catch-all carried them. And gateway records pin ParserID "openai" while the same record serves /v1/messages, so Anthropic responses were read with the OpenAI parser, which never looks at message_start where input tokens live: input metered as roughly zero on every stream and cost was skipped entirely. The rest fix requests refused for structural rather than policy reasons: model discovery denied for every account with a model allowlist, token counting denied on Bedrock and mis-parsed on Vertex, startup probes refused and written into the access log at every session start, and denials rendered in a shape no LLM client parses. Two changes are additive by design — the deny body keeps every field it had and adds the vendor's error object alongside, and body-level identity injection is now gated on the request's dialect so it stops sending OpenAI-shape fields into Anthropic bodies that reject them. The end-to-end work turned up one more: the discovery filter treated any slash in a model id as a gateway prefix, which would have dropped every self-hosted "Qwen/..." model from the picker.
73 lines
2.2 KiB
Go
73 lines
2.2 KiB
Go
//go:build e2e
|
|
|
|
// Package agentnetwork holds the container-based agent-network e2e suite. A
|
|
// single combined server is built and bootstrapped once per package run
|
|
// (TestMain) and shared across tests via srv; each test creates and cleans up
|
|
// its own resources so order doesn't matter.
|
|
package agentnetwork
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/netbirdio/netbird/e2e/harness"
|
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
)
|
|
|
|
// srv is the shared combined server for the package, ready (PAT-authenticated)
|
|
// by the time any Test runs.
|
|
var srv *harness.Combined
|
|
|
|
func TestMain(m *testing.M) {
|
|
os.Exit(run(m))
|
|
}
|
|
|
|
func run(m *testing.M) int {
|
|
// Generous timeout to cover a cold image build on first run.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
|
defer cancel()
|
|
|
|
var err error
|
|
srv, err = harness.StartCombined(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "e2e: start combined server: %v\n", err)
|
|
return 1
|
|
}
|
|
defer func() { _ = srv.Terminate(context.Background()) }()
|
|
|
|
if _, err := srv.Bootstrap(ctx); err != nil {
|
|
fmt.Fprintf(os.Stderr, "e2e: bootstrap admin PAT: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
// Bootstrap the account's agent-network endpoint once for the package:
|
|
// providers no longer have settings side effects, and every data-plane
|
|
// test expects the shared account pinned to the combined proxy cluster.
|
|
cluster := harness.AgentNetworkCluster
|
|
if _, err := srv.CreateSettings(ctx, api.AgentNetworkSettingsCreateRequest{ProxyAddress: &cluster}); err != nil {
|
|
fmt.Fprintf(os.Stderr, "e2e: bootstrap agent-network endpoint: %v\n", err)
|
|
return 1
|
|
}
|
|
|
|
return m.Run()
|
|
}
|
|
|
|
// waitBeforeRetry pauses between attempts of a polling loop and reports
|
|
// whether the caller should keep going. A cancelled context ends the loop
|
|
// where a plain sleep would keep retrying against it: every call fails
|
|
// instantly once ctx is done, so the loop would spend its whole remaining
|
|
// window sleeping between failures nobody is waiting for any more.
|
|
func waitBeforeRetry(ctx context.Context, d time.Duration) bool {
|
|
timer := time.NewTimer(d)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-timer.C:
|
|
return true
|
|
}
|
|
}
|