mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 00:51:28 +02:00
A budget stop, a blocked model or an unroutable model all rendered as the NetBird deny envelope alone. LLM clients only parse their own provider's error shape, so the reason never reached the user: Claude Code showed an unexplained API error where it could have shown the policy message. Carry the resolved surface on the deny reason and add the vendor's error object next to the existing fields. The body stays a superset of what it was, so anything reading code, message, details or middleware is unaffected. Status codes are unchanged here: mapping window caps to 429 needs the window reset plumbed through the limits response before a correct Retry-After can be sent.
93 lines
3.6 KiB
Go
93 lines
3.6 KiB
Go
package middleware
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// decodeDeny renders a denial and returns the parsed body plus the status.
|
|
func decodeDeny(t *testing.T, reason *DenyReason, status int) (map[string]any, int) {
|
|
t.Helper()
|
|
rec := httptest.NewRecorder()
|
|
RenderDenyResponse(rec, "llm_limit_check", reason, status)
|
|
|
|
var body map[string]any
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body), "deny body must be valid JSON")
|
|
return body, rec.Code
|
|
}
|
|
|
|
// TestRenderDeny_AnthropicSurfaceMirrorsVendorShape covers a budget stop
|
|
// reaching Claude Code. The client only parses the Anthropic envelope, so
|
|
// without the mirror the user sees an unexplained API error instead of the
|
|
// reason their request was refused.
|
|
func TestRenderDeny_AnthropicSurfaceMirrorsVendorShape(t *testing.T) {
|
|
body, status := decodeDeny(t, &DenyReason{
|
|
Code: "llm_policy.budget_cap_exceeded",
|
|
Message: "LLM policy limit exceeded",
|
|
Surface: "anthropic",
|
|
}, http.StatusForbidden)
|
|
|
|
assert.Equal(t, http.StatusForbidden, status)
|
|
assert.Equal(t, "error", body["type"], "Anthropic errors carry type=error at the top level")
|
|
|
|
errObj, ok := body["error"].(map[string]any)
|
|
require.True(t, ok, "error must be an object")
|
|
assert.Equal(t, "permission_error", errObj["type"], "403 maps to permission_error")
|
|
assert.Equal(t, "LLM policy limit exceeded", errObj["message"])
|
|
|
|
// The NetBird fields stay put so existing consumers keep working.
|
|
assert.Equal(t, "llm_policy.budget_cap_exceeded", body["code"])
|
|
assert.Equal(t, "LLM policy limit exceeded", body["message"])
|
|
assert.Equal(t, "llm_limit_check", body["middleware"])
|
|
}
|
|
|
|
// TestRenderDeny_OpenAISurfaceMirrorsVendorShape pins the OpenAI envelope,
|
|
// which nests the code and carries no top-level type.
|
|
func TestRenderDeny_OpenAISurfaceMirrorsVendorShape(t *testing.T) {
|
|
body, _ := decodeDeny(t, &DenyReason{
|
|
Code: "llm_policy.model_blocked",
|
|
Message: "model is not in the policy allowlist",
|
|
Surface: "openai",
|
|
}, http.StatusForbidden)
|
|
|
|
assert.NotContains(t, body, "type", "OpenAI errors have no top-level type")
|
|
|
|
errObj, ok := body["error"].(map[string]any)
|
|
require.True(t, ok, "error must be an object")
|
|
assert.Equal(t, "invalid_request_error", errObj["type"])
|
|
assert.Equal(t, "llm_policy.model_blocked", errObj["code"], "the NetBird code rides in the vendor code field")
|
|
assert.Equal(t, "model is not in the policy allowlist", errObj["message"])
|
|
}
|
|
|
|
// TestRenderDeny_RateLimitStatusMapsToVendorRateLimit pins the mapping a
|
|
// client's backoff keys on.
|
|
func TestRenderDeny_RateLimitStatusMapsToVendorRateLimit(t *testing.T) {
|
|
body, status := decodeDeny(t, &DenyReason{
|
|
Code: "llm_policy.token_cap_exceeded",
|
|
Message: "LLM policy limit exceeded",
|
|
Surface: "anthropic",
|
|
}, http.StatusTooManyRequests)
|
|
|
|
assert.Equal(t, http.StatusTooManyRequests, status, "429 must survive the status clamp")
|
|
errObj := body["error"].(map[string]any)
|
|
assert.Equal(t, "rate_limit_error", errObj["type"])
|
|
}
|
|
|
|
// TestRenderDeny_NoSurfaceKeepsLegacyShape guards non-LLM middlewares and
|
|
// denials raised before a surface is known.
|
|
func TestRenderDeny_NoSurfaceKeepsLegacyShape(t *testing.T) {
|
|
body, _ := decodeDeny(t, &DenyReason{
|
|
Code: "llm_policy.model_not_routable",
|
|
Message: "no provider configured for model x",
|
|
}, http.StatusForbidden)
|
|
|
|
assert.NotContains(t, body, "type", "no surface means no vendor mirror")
|
|
assert.NotContains(t, body, "error", "no surface means no vendor mirror")
|
|
assert.Equal(t, "llm_policy.model_not_routable", body["code"])
|
|
}
|