diff --git a/audit-filter-real-preview-v3.gif b/audit-filter-real-preview-v3.gif new file mode 100644 index 000000000..e653075af Binary files /dev/null and b/audit-filter-real-preview-v3.gif differ diff --git a/cmd/gomodel/docs/docs.go b/cmd/gomodel/docs/docs.go index 19483583f..d886aaab9 100644 --- a/cmd/gomodel/docs/docs.go +++ b/cmd/gomodel/docs/docs.go @@ -185,6 +185,18 @@ const docTemplate = `{ "name": "user_path", "in": "query" }, + { + "type": "string", + "description": "Case-insensitive substring search on the tracked user path", + "name": "user_path_search", + "in": "query" + }, + { + "type": "string", + "description": "Filter by exact request id", + "name": "request_id", + "in": "query" + }, { "type": "string", "description": "Filter by exact session id", @@ -314,6 +326,18 @@ const docTemplate = `{ "name": "user_path", "in": "query" }, + { + "type": "string", + "description": "Case-insensitive substring search on the tracked user path", + "name": "user_path_search", + "in": "query" + }, + { + "type": "string", + "description": "Filter by exact request id", + "name": "request_id", + "in": "query" + }, { "type": "string", "description": "Filter by error type", @@ -885,6 +909,271 @@ const docTemplate = `{ ] } }, + "/admin/failover": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List failover mappings", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/failover.View" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create or update one failover mapping", + "parameters": [ + { + "description": "Failover mapping", + "name": "mapping", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.upsertFailoverRuleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/failover.View" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete one failover mapping", + "parameters": [ + { + "description": "Failover primary model to remove", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/admin.deleteFailoverRuleRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + } + }, + "/admin/failover/generate": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Generate failover mapping suggestions", + "parameters": [ + { + "description": "Optional source model filter", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/admin.generateFailoverRulesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/failover.View" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + } + }, + "/admin/failover/reset": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Reset dashboard-managed failover mappings", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/failover.View" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + } + }, "/admin/mcp-servers": { "get": { "produces": [ @@ -7120,25 +7409,6 @@ const docTemplate = `{ } ] } - }, - "/version": { - "get": { - "produces": [ - "application/json" - ], - "tags": [ - "health" - ], - "summary": "Report the running version and whether an update exists", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/versioncheck.Status" - } - } - } - } } }, "definitions": { @@ -7465,6 +7735,14 @@ const docTemplate = `{ } } }, + "admin.deleteFailoverRuleRequest": { + "type": "object", + "properties": { + "primary_model": { + "type": "string" + } + } + }, "admin.deleteModelPricingOverrideRequest": { "type": "object", "properties": { @@ -7501,6 +7779,17 @@ const docTemplate = `{ "source" ] }, + "admin.generateFailoverRulesRequest": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "primary_model": { + "type": "string" + } + } + }, "admin.mcpServerViewResponse": { "type": "object", "properties": { @@ -7992,6 +8281,23 @@ const docTemplate = `{ } } }, + "admin.upsertFailoverRuleRequest": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "fallback_models": { + "type": "array", + "items": { + "type": "string" + } + }, + "primary_model": { + "type": "string" + } + } + }, "admin.upsertMCPServerRequest": { "type": "object", "properties": { @@ -8150,10 +8456,6 @@ const docTemplate = `{ "enabled": { "type": "boolean" }, - "failover": { - "description": "Failover retries a failed request on the remaining targets. Omitted\nmeans enabled; false serves the chosen target only.", - "type": "boolean" - }, "old_source": { "type": "string" }, @@ -10600,6 +10902,35 @@ const docTemplate = `{ } } }, + "failover.View": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "fallback_models": { + "type": "array", + "items": { + "type": "string" + } + }, + "managed": { + "type": "boolean" + }, + "managed_source": { + "type": "string" + }, + "primary_model": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "mcpgateway.CatalogFeature": { "type": "object", "properties": { @@ -11587,29 +11918,6 @@ const docTemplate = `{ } } }, - "versioncheck.Status": { - "type": "object", - "properties": { - "app": { - "type": "string" - }, - "checked_at": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "latest": { - "type": "string" - }, - "update_available": { - "type": "boolean" - }, - "version": { - "type": "string" - } - } - }, "virtualmodels.Target": { "type": "object", "properties": { @@ -11636,9 +11944,6 @@ const docTemplate = `{ "enabled": { "type": "boolean" }, - "failover": { - "type": "boolean" - }, "kind": { "type": "string" }, @@ -11705,10 +12010,6 @@ const docTemplate = `{ "enabled": { "type": "boolean" }, - "failover": { - "description": "Failover retries a failed request against the redirect's remaining\ntargets. Tri-state: nil means enabled (the default); explicit false\nserves the chosen target only. The failover strategy always fails over.", - "type": "boolean" - }, "managed": { "description": "Managed marks a virtual model supplied declaratively through config.yaml or\nthe VIRTUAL_MODELS env var rather than the admin store. It is an in-memory\nflag only: stores never read or write it. Managed rows override store rows\nof the same Source and are read-only to the admin API.", "type": "boolean" diff --git a/docs/openapi.json b/docs/openapi.json index 5ae9a37b7..c683469a6 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -235,6 +235,22 @@ "type": "string" } }, + { + "description": "Case-insensitive substring search on the tracked user path", + "name": "user_path_search", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "description": "Filter by exact request id", + "name": "request_id", + "in": "query", + "schema": { + "type": "string" + } + }, { "description": "Filter by exact session id", "name": "session_id", @@ -410,6 +426,22 @@ "type": "string" } }, + { + "description": "Case-insensitive substring search on the tracked user path", + "name": "user_path_search", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "description": "Filter by exact request id", + "name": "request_id", + "in": "query", + "schema": { + "type": "string" + } + }, { "description": "Filter by error type", "name": "error_type", @@ -1191,6 +1223,366 @@ } } }, + "/admin/failover": { + "get": { + "tags": [ + "admin" + ], + "summary": "List failover mappings", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/failover.View" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "x-mint": { + "metadata": { + "sidebarTitle": "/admin/failover", + "title": "List failover mappings", + "description": "GoModel API reference for GET /admin/failover: List failover mappings." + } + } + }, + "put": { + "tags": [ + "admin" + ], + "summary": "Create or update one failover mapping", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/admin.upsertFailoverRuleRequest" + } + } + }, + "description": "Failover mapping", + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/failover.View" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "x-mint": { + "metadata": { + "sidebarTitle": "/admin/failover", + "title": "Create or update one failover mapping", + "description": "GoModel API reference for PUT /admin/failover: Create or update one failover mapping." + } + } + }, + "delete": { + "tags": [ + "admin" + ], + "summary": "Delete one failover mapping", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/admin.deleteFailoverRuleRequest" + } + } + }, + "description": "Failover primary model to remove", + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "x-mint": { + "metadata": { + "sidebarTitle": "/admin/failover", + "title": "Delete one failover mapping", + "description": "GoModel API reference for DELETE /admin/failover: Delete one failover mapping." + } + } + } + }, + "/admin/failover/generate": { + "post": { + "tags": [ + "admin" + ], + "summary": "Generate failover mapping suggestions", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/admin.generateFailoverRulesRequest" + } + } + }, + "description": "Optional source model filter" + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/failover.View" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "x-mint": { + "metadata": { + "sidebarTitle": "/admin/failover/generate", + "title": "Generate failover mapping suggestions", + "description": "GoModel API reference for POST /admin/failover/generate: Generate failover mapping suggestions." + } + } + } + }, + "/admin/failover/reset": { + "post": { + "tags": [ + "admin" + ], + "summary": "Reset dashboard-managed failover mappings", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/failover.View" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/core.GatewayError" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "x-mint": { + "metadata": { + "sidebarTitle": "/admin/failover/reset", + "title": "Reset dashboard-managed failover mappings", + "description": "GoModel API reference for POST /admin/failover/reset: Reset dashboard-managed failover mappings." + } + } + } + }, "/admin/mcp-servers": { "get": { "tags": [ @@ -10623,33 +11015,6 @@ } } } - }, - "/version": { - "get": { - "tags": [ - "health" - ], - "summary": "Report the running version and whether an update exists", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/versioncheck.Status" - } - } - } - } - }, - "x-mint": { - "metadata": { - "sidebarTitle": "/version", - "title": "Report the running version and whether an update exists", - "description": "GoModel API reference for GET /version: Report the running version and whether an update exists." - } - } - } } }, "servers": [ @@ -11040,6 +11405,14 @@ "budget_key" ] }, + "admin.deleteFailoverRuleRequest": { + "type": "object", + "properties": { + "primary_model": { + "type": "string" + } + } + }, "admin.deleteModelPricingOverrideRequest": { "type": "object", "properties": { @@ -11079,6 +11452,17 @@ "source" ] }, + "admin.generateFailoverRulesRequest": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "primary_model": { + "type": "string" + } + } + }, "admin.mcpServerViewResponse": { "type": "object", "properties": { @@ -11577,6 +11961,23 @@ "budget_key" ] }, + "admin.upsertFailoverRuleRequest": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "fallback_models": { + "type": "array", + "items": { + "type": "string" + } + }, + "primary_model": { + "type": "string" + } + } + }, "admin.upsertMCPServerRequest": { "type": "object", "properties": { @@ -11739,10 +12140,6 @@ "enabled": { "type": "boolean" }, - "failover": { - "description": "Failover retries a failed request on the remaining targets. Omitted\nmeans enabled; false serves the chosen target only.", - "type": "boolean" - }, "old_source": { "type": "string" }, @@ -14240,6 +14637,35 @@ } } }, + "failover.View": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "fallback_models": { + "type": "array", + "items": { + "type": "string" + } + }, + "managed": { + "type": "boolean" + }, + "managed_source": { + "type": "string" + }, + "primary_model": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "mcpgateway.CatalogFeature": { "type": "object", "properties": { @@ -15229,29 +15655,6 @@ } } }, - "versioncheck.Status": { - "type": "object", - "properties": { - "app": { - "type": "string" - }, - "checked_at": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "latest": { - "type": "string" - }, - "update_available": { - "type": "boolean" - }, - "version": { - "type": "string" - } - } - }, "virtualmodels.Target": { "type": "object", "properties": { @@ -15278,9 +15681,6 @@ "enabled": { "type": "boolean" }, - "failover": { - "type": "boolean" - }, "kind": { "type": "string" }, @@ -15359,10 +15759,6 @@ "enabled": { "type": "boolean" }, - "failover": { - "description": "Failover retries a failed request against the redirect's remaining\ntargets. Tri-state: nil means enabled (the default); explicit false\nserves the chosen target only. The failover strategy always fails over.", - "type": "boolean" - }, "managed": { "description": "Managed marks a virtual model supplied declaratively through config.yaml or\nthe VIRTUAL_MODELS env var rather than the admin store. It is an in-memory\nflag only: stores never read or write it. Managed rows override store rows\nof the same Source and are read-only to the admin API.", "type": "boolean" diff --git a/internal/admin/handler_audit.go b/internal/admin/handler_audit.go index 8efd04f42..8f0dfb98f 100644 --- a/internal/admin/handler_audit.go +++ b/internal/admin/handler_audit.go @@ -47,6 +47,8 @@ const conversationBuildTimeout = 10 * time.Second // @Param method query string false "Filter by HTTP method" // @Param path query string false "Filter by request path" // @Param user_path query string false "Filter by tracked user path subtree" +// @Param user_path_search query string false "Case-insensitive substring search on the tracked user path" +// @Param request_id query string false "Filter by exact request id" // @Param session_id query string false "Filter by exact session id" // @Param error_type query string false "Filter by error type" // @Param status_code query int false "Filter by status code" @@ -134,6 +136,7 @@ func parseAuditLogQueryParams(c *echo.Context) (auditlog.LogQueryParams, error) if err != nil { return params, err } + userPathSearch := strings.TrimSpace(c.QueryParam("user_path_search")) requestedModel := c.QueryParam("requested_model") if requestedModel == "" { @@ -147,6 +150,8 @@ func parseAuditLogQueryParams(c *echo.Context) (auditlog.LogQueryParams, error) Method: strings.ToUpper(c.QueryParam("method")), Path: c.QueryParam("path"), UserPath: userPath, + UserPathSearch: userPathSearch, + RequestID: strings.TrimSpace(c.QueryParam("request_id")), SessionID: sessionID, ErrorType: c.QueryParam("error_type"), Search: c.QueryParam("search"), @@ -207,6 +212,8 @@ func parseAuditLogQueryParams(c *echo.Context) (auditlog.LogQueryParams, error) // @Param method query string false "Filter by HTTP method" // @Param path query string false "Filter by request path" // @Param user_path query string false "Filter by tracked user path subtree" +// @Param user_path_search query string false "Case-insensitive substring search on the tracked user path" +// @Param request_id query string false "Filter by exact request id" // @Param error_type query string false "Filter by error type" // @Param status_code query int false "Filter by status code" // @Param stream query bool false "Filter by stream mode (true/false)" @@ -348,11 +355,13 @@ func (h *Handler) AuditStats(c *echo.Context) error { _, location := dashboardTimeZone(c) params := auditlog.RequestStatsParams{ - StartDate: dateRange.StartDate, - EndDate: dateRange.EndDate, - Interval: interval, - Location: location, - Now: timeNow(), + QueryParams: auditlog.QueryParams{ + StartDate: dateRange.StartDate, + EndDate: dateRange.EndDate, + }, + Interval: interval, + Location: location, + Now: timeNow(), } stats, err := h.auditReader.GetRequestStats(c.Request().Context(), params) diff --git a/internal/admin/handler_audit_sessions_test.go b/internal/admin/handler_audit_sessions_test.go index c7e5451a2..9fda00289 100644 --- a/internal/admin/handler_audit_sessions_test.go +++ b/internal/admin/handler_audit_sessions_test.go @@ -65,7 +65,7 @@ func TestAuditSessions_Success(t *testing.T) { } h := NewHandler(nil, nil, WithAuditReader(reader)) - c, rec := newHandlerContext("/admin/audit/sessions?days=7&user_path=/team") + c, rec := newHandlerContext("/admin/audit/sessions?days=7&user_path=/team&user_path_search=xinat") if err := h.AuditSessions(c); err != nil { t.Fatalf("unexpected error: %v", err) @@ -76,6 +76,9 @@ func TestAuditSessions_Success(t *testing.T) { if reader.lastQuery.UserPath != "/team" { t.Errorf("user_path filter not forwarded: %q", reader.lastQuery.UserPath) } + if reader.lastQuery.UserPathSearch != "xinat" { + t.Errorf("user_path_search filter not forwarded: %q", reader.lastQuery.UserPathSearch) + } var result struct { Sessions []struct { diff --git a/internal/admin/handler_test.go b/internal/admin/handler_test.go index 875fbb78a..adc9e3753 100644 --- a/internal/admin/handler_test.go +++ b/internal/admin/handler_test.go @@ -1430,7 +1430,7 @@ func TestAuditLog_WithFilters(t *testing.T) { } h := NewHandler(nil, nil, WithAuditReader(reader)) - c, rec := newHandlerContext("/admin/audit/log?model=gpt-4&provider=openai&method=post&path=/v1/chat/completions&user_path=/team&error_type=provider_error&status_code=502&stream=true&search=timeout&limit=10&offset=5") + c, rec := newHandlerContext("/admin/audit/log?model=gpt-4&provider=openai&method=post&path=/v1/chat/completions&user_path=/team&request_id=req-42&error_type=provider_error&status_code=502&stream=true&search=timeout&limit=10&offset=5") if err := h.AuditLog(c); err != nil { t.Fatalf("unexpected error: %v", err) @@ -1454,6 +1454,9 @@ func TestAuditLog_WithFilters(t *testing.T) { if reader.lastQuery.UserPath != "/team" { t.Errorf("expected user_path filter to match, got %q", reader.lastQuery.UserPath) } + if reader.lastQuery.RequestID != "req-42" { + t.Errorf("expected request_id filter req-42, got %q", reader.lastQuery.RequestID) + } if reader.lastQuery.ErrorType != "provider_error" { t.Errorf("expected error_type provider_error, got %q", reader.lastQuery.ErrorType) } diff --git a/internal/auditlog/reader.go b/internal/auditlog/reader.go index 1a0f65175..ca97666c0 100644 --- a/internal/auditlog/reader.go +++ b/internal/auditlog/reader.go @@ -19,6 +19,11 @@ type LogQueryParams struct { Method string Path string UserPath string + // UserPathSearch is a case-insensitive substring filter used by the + // dashboard's incremental user-path search. UserPath keeps its canonical + // exact/subtree semantics for API callers. + UserPathSearch string + RequestID string // exact-match request id filter SessionID string // exact-match session id filter ErrorType string Search string diff --git a/internal/auditlog/reader_mongodb.go b/internal/auditlog/reader_mongodb.go index 0069df70b..36889725f 100644 --- a/internal/auditlog/reader_mongodb.go +++ b/internal/auditlog/reader_mongodb.go @@ -265,6 +265,12 @@ func mongoLogMatchFilters(params LogQueryParams) (bson.D, error) { matchFilters = append(matchFilters, mongoUserPathMatchFilter(userPath)) } } + if userPathSearch := strings.TrimSpace(params.UserPathSearch); userPathSearch != "" { + matchFilters = append(matchFilters, bson.E{Key: "user_path", Value: bson.D{ + {Key: "$regex", Value: regexp.QuoteMeta(userPathSearch)}, + {Key: "$options", Value: "i"}, + }}) + } if params.ErrorType != "" { matchFilters = append(matchFilters, bson.E{ Key: "error_type", @@ -274,6 +280,9 @@ func mongoLogMatchFilters(params LogQueryParams) (bson.D, error) { }, }) } + if params.RequestID != "" { + matchFilters = append(matchFilters, bson.E{Key: "request_id", Value: params.RequestID}) + } if params.SessionID != "" { matchFilters = append(matchFilters, bson.E{Key: "session_id", Value: params.SessionID}) } diff --git a/internal/auditlog/reader_mongodb_test.go b/internal/auditlog/reader_mongodb_test.go index 7912e4ee4..c3bf6aeb6 100644 --- a/internal/auditlog/reader_mongodb_test.go +++ b/internal/auditlog/reader_mongodb_test.go @@ -145,3 +145,28 @@ func TestMongoExactUserPathMatchFilter(t *testing.T) { } }) } + +func TestMongoLogMatchFilters_RequestIDUsesExactFieldMatch(t *testing.T) { + got, err := mongoLogMatchFilters(LogQueryParams{RequestID: "req-42"}) + if err != nil { + t.Fatalf("mongoLogMatchFilters returned error: %v", err) + } + want := bson.D{{Key: "request_id", Value: "req-42"}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("mongoLogMatchFilters(request_id) = %#v, want %#v", got, want) + } +} + +func TestMongoLogMatchFilters_UserPathSearchUsesSubstringRegex(t *testing.T) { + got, err := mongoLogMatchFilters(LogQueryParams{UserPathSearch: "xinat"}) + if err != nil { + t.Fatalf("mongoLogMatchFilters returned error: %v", err) + } + want := bson.D{{Key: "user_path", Value: bson.D{ + {Key: "$regex", Value: "xinat"}, + {Key: "$options", Value: "i"}, + }}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("mongoLogMatchFilters(user_path_search) = %#v, want %#v", got, want) + } +} diff --git a/internal/auditlog/reader_sql.go b/internal/auditlog/reader_sql.go index 05ea05160..497c74137 100644 --- a/internal/auditlog/reader_sql.go +++ b/internal/auditlog/reader_sql.go @@ -231,9 +231,15 @@ func (r *SQLReader) logFilters(ctx context.Context, params LogQueryParams) ([]st add(auditUserPathSQLPredicate(userPath, r.dialect.userPath), userPath, lower, upper) } } + if userPathSearch := strings.TrimSpace(params.UserPathSearch); userPathSearch != "" { + add(r.likeClause("user_path"), contains(userPathSearch)) + } if params.ErrorType != "" { add(r.likeClause("error_type"), contains(params.ErrorType)) } + if params.RequestID != "" { + add("request_id = ?", params.RequestID) + } if params.SessionID != "" { add("session_id = ?", params.SessionID) } diff --git a/internal/auditlog/reader_sql_boundary_test.go b/internal/auditlog/reader_sql_boundary_test.go index 4383ead17..9d2f18b4d 100644 --- a/internal/auditlog/reader_sql_boundary_test.go +++ b/internal/auditlog/reader_sql_boundary_test.go @@ -54,10 +54,12 @@ func TestSQLReaderGetLogs_IncludesFractionalStartBoundaryAndExcludesFractionalEn } result, err := reader.GetLogs(ctx, LogQueryParams{ - StartDate: time.Date(2026, 1, 16, 0, 0, 0, 0, location), - EndDate: time.Date(2026, 1, 16, 0, 0, 0, 0, location), - Limit: 10, - Offset: 0, + QueryParams: QueryParams{ + StartDate: time.Date(2026, 1, 16, 0, 0, 0, 0, location), + EndDate: time.Date(2026, 1, 16, 0, 0, 0, 0, location), + }, + Limit: 10, + Offset: 0, }) if err != nil { t.Fatalf("GetLogs returned error: %v", err) @@ -131,6 +133,60 @@ func TestSQLReaderGetLogs_SearchMatchesUserPath(t *testing.T) { }) } +func TestSQLReaderGetLogs_UserPathFilterMatchesPartialPath(t *testing.T) { + sqlxtest.Run(t, func(t *testing.T, db sqlx.DB) { + store, err := newSQLStoreForTest(t, db, 0) + if err != nil { + t.Fatalf("failed to create store: %v", err) + } + if err := store.WriteBatch(context.Background(), []*LogEntry{ + { + ID: "partial-user-path", Timestamp: time.Date(2026, 1, 16, 12, 0, 0, 0, time.UTC), + RequestedModel: "gpt-5", Provider: "openai", UserPath: "/team/alpha", + }, + { + ID: "video-user-path", Timestamp: time.Date(2026, 1, 16, 11, 0, 0, 0, time.UTC), + RequestedModel: "gpt-5", Provider: "openai", UserPath: "/voxinate/events/97gn9a9e", + }, + }); err != nil { + t.Fatalf("failed to seed audit log: %v", err) + } + + reader, err := NewSQLReader(db) + if err != nil { + t.Fatalf("failed to create reader: %v", err) + } + + for _, tc := range []struct { + name, query, wantID string + }{ + {name: "complete segment", query: "alpha", wantID: "partial-user-path"}, + {name: "video prefix", query: "voxina", wantID: "video-user-path"}, + {name: "word fragment", query: "xinat", wantID: "video-user-path"}, + } { + t.Run(tc.name, func(t *testing.T) { + result, err := reader.GetLogs(context.Background(), LogQueryParams{UserPathSearch: tc.query, Limit: 10}) + if err != nil { + t.Fatalf("GetLogs returned error: %v", err) + } + if result.Total != 1 || len(result.Entries) != 1 || result.Entries[0].ID != tc.wantID { + t.Fatalf("partial user path %q returned %#v (total %d), want %s", tc.query, result.Entries, result.Total, tc.wantID) + } + }) + } + + sessions, err := reader.GetSessions(context.Background(), LogQueryParams{ + UserPathSearch: "voxina", Limit: 10, + }) + if err != nil { + t.Fatalf("GetSessions returned error: %v", err) + } + if sessions.Total != 1 || len(sessions.Sessions) != 1 || sessions.Sessions[0].Latest.ID != "video-user-path" { + t.Fatalf("partial user path sessions = %#v (total %d), want video-user-path", sessions.Sessions, sessions.Total) + } + }) +} + // A full canonical UUID takes the indexed-identifier fast path: equality on // id/request_id/auth_key_id/session_id, case-insensitively — and deliberately // no longer the LIKE sweep over the free-text columns. @@ -200,6 +256,36 @@ func TestSQLReaderGetLogs_SearchUUIDMatchesIdentifierColumns(t *testing.T) { }) } +func TestSQLReaderGetLogs_RequestIDMatchesOnlyRequestID(t *testing.T) { + sqlxtest.Run(t, func(t *testing.T, db sqlx.DB) { + store, err := newSQLStoreForTest(t, db, 0) + if err != nil { + t.Fatalf("failed to create store: %v", err) + } + + ctx := context.Background() + if err := store.WriteBatch(ctx, []*LogEntry{ + {ID: "request-match", Timestamp: time.Date(2026, 1, 16, 12, 0, 0, 0, time.UTC), Provider: "openai", RequestID: "req-42"}, + {ID: "session-only", Timestamp: time.Date(2026, 1, 16, 11, 0, 0, 0, time.UTC), Provider: "openai", SessionID: "req-42"}, + {ID: "model-only", Timestamp: time.Date(2026, 1, 16, 10, 0, 0, 0, time.UTC), Provider: "openai", RequestedModel: "req-42"}, + }); err != nil { + t.Fatalf("failed to seed audit logs: %v", err) + } + + reader, err := NewSQLReader(db) + if err != nil { + t.Fatalf("failed to create reader: %v", err) + } + result, err := reader.GetLogs(ctx, LogQueryParams{RequestID: "req-42", Limit: 10}) + if err != nil { + t.Fatalf("GetLogs returned error: %v", err) + } + if result.Total != 1 || len(result.Entries) != 1 || result.Entries[0].ID != "request-match" { + t.Fatalf("request_id filter returned total=%d entries=%v, want only request-match", result.Total, result.Entries) + } + }) +} + func TestSQLReaderGetLogs_SearchMatchesErrorMessage(t *testing.T) { sqlxtest.Run(t, func(t *testing.T, db sqlx.DB) { diff --git a/internal/auditlog/search_index.go b/internal/auditlog/search_index.go index 021c91c00..4c7d2432c 100644 --- a/internal/auditlog/search_index.go +++ b/internal/auditlog/search_index.go @@ -18,6 +18,7 @@ var searchColumns = []string{ } const trigramSearchIndex = "idx_audit_search_trgm" +const userPathTrigramSearchIndex = "idx_audit_user_path_trgm" // minTrigramSearchLength is the shortest term, in characters, that yields a // trigram. @@ -70,6 +71,7 @@ func ensureTrigramSearchIndex(ctx context.Context, db sqlx.DB, errorMessage stri } } if hasTrigramSearchIndex(ctx, db) { + ensureUserPathTrigramSearchIndex(ctx, db, schema) return } statement := fmt.Sprintf(`CREATE INDEX CONCURRENTLY IF NOT EXISTS %s ON audit_logs USING GIN (%s %s.gin_trgm_ops)`, @@ -80,6 +82,25 @@ func ensureTrigramSearchIndex(ctx context.Context, db sqlx.DB, errorMessage stri return } slog.Info("auditlog: built trigram search index", "duration", time.Since(started).Round(time.Millisecond)) + ensureUserPathTrigramSearchIndex(ctx, db, schema) +} + +func ensureUserPathTrigramSearchIndex(ctx context.Context, db sqlx.DB, schema string) { + var valid *bool + if err := db.QueryRow(ctx, `SELECT i.indisvalid FROM pg_index i WHERE i.indexrelid = to_regclass(?)`, userPathTrigramSearchIndex).Scan(&valid); err == nil && valid != nil && !*valid { + slog.Warn("auditlog: rebuilding interrupted user path trigram index") + if _, err := db.Exec(ctx, "DROP INDEX "+userPathTrigramSearchIndex); err != nil { + slog.Warn("auditlog: failed to drop invalid user path trigram index", "error", err) + return + } + } + if hasUserPathTrigramSearchIndex(ctx, db) { + return + } + statement := fmt.Sprintf("CREATE INDEX CONCURRENTLY IF NOT EXISTS %s ON audit_logs USING GIN (user_path %s.gin_trgm_ops)", userPathTrigramSearchIndex, `"`+strings.ReplaceAll(schema, `"`, `""`)+`"`) + if _, err := db.Exec(ctx, statement); err != nil { + slog.Warn("auditlog: failed to create user path trigram index", "error", err) + } } // hasTrigramSearchIndex reports whether the trigram index exists and is @@ -92,3 +113,12 @@ func hasTrigramSearchIndex(ctx context.Context, db sqlx.DB) bool { err := db.QueryRow(ctx, `SELECT COALESCE(i.indisvalid, FALSE) FROM pg_index i WHERE i.indexrelid = to_regclass(?)`, trigramSearchIndex).Scan(&valid) return err == nil && valid } + +func hasUserPathTrigramSearchIndex(ctx context.Context, db sqlx.DB) bool { + if db.Dialect() != sqlx.PostgreSQL { + return false + } + var valid bool + err := db.QueryRow(ctx, `SELECT COALESCE(i.indisvalid, FALSE) FROM pg_index i WHERE i.indexrelid = to_regclass(?)`, userPathTrigramSearchIndex).Scan(&valid) + return err == nil && valid +} diff --git a/internal/auditlog/search_index_test.go b/internal/auditlog/search_index_test.go index c5bfd5509..cb205eed5 100644 --- a/internal/auditlog/search_index_test.go +++ b/internal/auditlog/search_index_test.go @@ -26,6 +26,9 @@ func requireTrigramIndex(t *testing.T, db sqlx.DB) (*SQLStore, *SQLReader) { if !hasTrigramSearchIndex(ctx, db) { t.Skip("pg_trgm could not be installed on the test server") } + if !hasUserPathTrigramSearchIndex(ctx, db) { + t.Fatal("user path trigram index was not created") + } reader, err := NewSQLReader(db) if err != nil { t.Fatalf("failed to create reader: %v", err) diff --git a/tests/contract/testdata/gemini/native_chat_completion_stream.txt b/tests/contract/testdata/gemini/native_chat_completion_stream.txt index 3eccc3c9e..5e81bdfca 100644 --- a/tests/contract/testdata/gemini/native_chat_completion_stream.txt +++ b/tests/contract/testdata/gemini/native_chat_completion_stream.txt @@ -1,3 +1,3 @@ -data: {"candidates": [{"content": {"parts": [{"text": "Hello"}],"role": "model"},"index": 0}],"usageMetadata": {"promptTokenCount": 11,"candidatesTokenCount": 1,"totalTokenCount": 30,"promptTokensDetails": [{"modality": "TEXT","tokenCount": 11}],"thoughtsTokenCount": 18,"serviceTier": "standard"},"modelVersion": "gemini-2.5-flash","responseId": "cWmNaqOEH9CRkdUP1bWcsQU"} - -data: {"candidates": [{"content": {"parts": [{"text": ", World!"}],"role": "model"},"finishReason": "STOP","index": 0}],"usageMetadata": {"promptTokenCount": 11,"candidatesTokenCount": 4,"totalTokenCount": 33,"promptTokensDetails": [{"modality": "TEXT","tokenCount": 11}],"thoughtsTokenCount": 18,"serviceTier": "standard"},"modelVersion": "gemini-2.5-flash","responseId": "cWmNaqOEH9CRkdUP1bWcsQU"} +data: {"candidates": [{"content": {"parts": [{"text": "Hello"}],"role": "model"},"index": 0}],"usageMetadata": {"promptTokenCount": 11,"candidatesTokenCount": 1,"totalTokenCount": 30,"promptTokensDetails": [{"modality": "TEXT","tokenCount": 11}],"thoughtsTokenCount": 18,"serviceTier": "standard"},"modelVersion": "gemini-2.5-flash","responseId": "cWmNaqOEH9CRkdUP1bWcsQU"} + +data: {"candidates": [{"content": {"parts": [{"text": ", World!"}],"role": "model"},"finishReason": "STOP","index": 0}],"usageMetadata": {"promptTokenCount": 11,"candidatesTokenCount": 4,"totalTokenCount": 33,"promptTokensDetails": [{"modality": "TEXT","tokenCount": 11}],"thoughtsTokenCount": 18,"serviceTier": "standard"},"modelVersion": "gemini-2.5-flash","responseId": "cWmNaqOEH9CRkdUP1bWcsQU"} diff --git a/tests/e2e/manage-release-e2e-stack.sh b/tests/e2e/manage-release-e2e-stack.sh old mode 100755 new mode 100644 diff --git a/tests/e2e/run-release-e2e.sh b/tests/e2e/run-release-e2e.sh old mode 100755 new mode 100644 diff --git a/tests/e2e/test-iac-virtualmodels.sh b/tests/e2e/test-iac-virtualmodels.sh old mode 100755 new mode 100644 diff --git a/tests/e2e/upgrade-compat.sh b/tests/e2e/upgrade-compat.sh old mode 100755 new mode 100644 diff --git a/tools/seed-demo-data.sh b/tools/seed-demo-data.sh old mode 100755 new mode 100644 diff --git a/web/dashboard/messages/en.json b/web/dashboard/messages/en.json index e2f852398..40bac5917 100644 --- a/web/dashboard/messages/en.json +++ b/web/dashboard/messages/en.json @@ -212,6 +212,16 @@ "audit_loading": "Loading audit logs", "audit_search_placeholder": "Search by request ID, model, provider, path, user path, or error…", "audit_search_label": "Search by request ID, model, provider, path, user path, or error", + "audit_field_value_placeholder": "Enter a value to filter", + "audit_field_value_label": "Audit field value", + "audit_filter_field_label": "Audit filter field", + "audit_filter_field_user_path": "User path", + "audit_filter_field_request_id": "Request ID", + "audit_filter_field_model": "Model", + "audit_filter_field_provider": "Provider", + "audit_filter_field_session_id": "Session ID", + "audit_filter_field_error_type": "Error type", + "audit_filter_field_search": "All fields", "audit_filter_method_label": "HTTP method filter", "audit_filter_all_methods": "All Methods", "audit_filter_status_label": "Status code filter", @@ -566,22 +576,11 @@ "models_all_scope": "All providers and models", "models_all_in_provider": "All models in {provider}", "models_virtual_model": "Virtual model", - "models_this_model": "This model", "models_redirect": "Redirect", "models_managed_config": "Managed by config.yaml / VIRTUAL_MODELS", "models_config": "Config", "models_targets": "Targets", "models_redirects_to": "Redirects to", - "models_falls_back_to": "Falls back to", - "models_balanced_with": "Balanced with", - "models_failover_title": "Failover: this model serves first; if it fails, the next target in line takes over", - "models_balanced_title": "Load balancing: requests are spread across this model and the other targets; a failed request is retried on another target", - "models_balanced_title_failover_off": "Load balancing without failover: requests are spread across this model and the other targets; a failed request is not retried", - "models_balanced_title_failover_global_off": "Load balancing: failover is switched off globally (FAILOVER_ENABLED), so a failed request is not retried", - "models_remove_failover": "Remove fallbacks for {model}", - "models_removing_failover": "Removing fallbacks for {model}", - "models_remove_balancing": "Remove load balancing for {model}", - "models_removing_balancing": "Removing load balancing for {model}", "models_remove": "remove", "models_remove_redirect": "Remove redirect for {model}", "models_removing_redirect": "Removing redirect for {model}", @@ -596,18 +595,7 @@ "models_source": "Source", "models_add_target": "Add target (load balancing)", "models_strategy": "Load-balancing strategy", - "models_strategy_pending_hint": "Applies once two or more targets are listed.", - "models_strategy_help_label": "Load-balancing strategy help", - "models_summary_failover": "Requests for {source} are served by {source}; if it fails, by {fallbacks}.", - "models_summary_balanced": "Requests for {source} are balanced across {source} and {peers} ({strategy}).", - "models_summary_replaced": "Requests are routed to {target}. Add {source} as a target if you don't want to shadow the source model.", - "models_summary_replaced_failover": "Requests are routed to {first}; if it fails, to {rest}. Add {source} as a target if you don't want to shadow the source model.", - "models_summary_replaced_balanced": "Requests are balanced across {targets} ({strategy}). Add {source} as a target if you don't want to shadow the source model.", - "models_summary_alias": "Requests for {source} go to {target}.", - "models_summary_alias_failover": "Requests for {source} go to {first}; if it fails, to {rest}.", - "models_summary_alias_balanced": "Requests for {source} are balanced across {targets} ({strategy}).", "models_session_keeping": "Session keeping (route a detected client session to the target that served it)", - "models_failover_option": "Failover (retry a failed request on the next target)", "models_user_paths": "User Paths", "models_user_paths_help_label": "User paths help", "models_user_paths_help": "Use / to allow every user path. Use a team path to restrict to that subtree, or an unused path to make the selector unavailable.", @@ -620,10 +608,8 @@ "models_yes": "yes", "models_no": "no", "models_target_model": "Target model", - "models_target_search_placeholder": "Search models or type a name…", "models_target_weight": "Target weight", "models_weight": "weight", - "models_weight_help": "Relative share of requests: 1 is a normal share, 2 gets twice as many, 3 three times (whole numbers, 1 or more).", "models_remove_target": "Remove target", "models_pricing_editor": "Model pricing editor", "models_pricing_save": "Save Pricing", @@ -799,7 +785,6 @@ "workflows_all_scope": "All providers and models", "workflows_model": "Model", "workflows_all_provider_models": "All models for provider", - "workflows_model_search_placeholder": "Search models…", "workflows_name": "Name", "workflows_name_placeholder": "Optional. Defaults to scope label.", "workflows_user_path": "User Path", @@ -1161,8 +1146,6 @@ "models_removing_alias": "Removing alias {name}", "models_edit_alias": "Edit alias {name}", "models_edit_redirect": "Edit redirect for {name}", - "models_edit_failover": "Edit failover for {name}", - "models_edit_balancing": "Edit load balancing for {name}", "models_virtual_model_toggle": "virtual model", "models_category_all": "Models", "models_category_text_generation": "Text generation", @@ -1334,6 +1317,33 @@ "models_strategy_round_robin": "Round-robin (rotate across targets; honors weights)", "models_strategy_cost": "Lowest cost (cheapest target per request)", "models_strategy_adaptive": "Adaptive (target chosen by the registered routing extension)", + "models_this_model": "This model", + "models_falls_back_to": "Falls back to", + "models_balanced_with": "Balanced with", + "models_failover_title": "Failover: this model serves first; if it fails, the next target in line takes over", + "models_balanced_title": "Load balancing: requests are spread across this model and the other targets; a failed request is retried on another target", + "models_balanced_title_failover_off": "Load balancing without failover: requests are spread across this model and the other targets; a failed request is not retried", + "models_balanced_title_failover_global_off": "Load balancing: failover is switched off globally (FAILOVER_ENABLED), so a failed request is not retried", + "models_remove_failover": "Remove fallbacks for {model}", + "models_removing_failover": "Removing fallbacks for {model}", + "models_remove_balancing": "Remove load balancing for {model}", + "models_removing_balancing": "Removing load balancing for {model}", + "models_strategy_pending_hint": "Applies once two or more targets are listed.", + "models_strategy_help_label": "Load-balancing strategy help", + "models_summary_failover": "Requests for {source} are served by {source}; if it fails, by {fallbacks}.", + "models_summary_balanced": "Requests for {source} are balanced across {source} and {peers} ({strategy}).", + "models_summary_replaced": "Requests are routed to {target}. Add {source} as a target if you don't want to shadow the source model.", + "models_summary_replaced_failover": "Requests are routed to {first}; if it fails, to {rest}. Add {source} as a target if you don't want to shadow the source model.", + "models_summary_replaced_balanced": "Requests are balanced across {targets} ({strategy}). Add {source} as a target if you don't want to shadow the source model.", + "models_summary_alias": "Requests for {source} go to {target}.", + "models_summary_alias_failover": "Requests for {source} go to {first}; if it fails, to {rest}.", + "models_summary_alias_balanced": "Requests for {source} are balanced across {targets} ({strategy}).", + "models_failover_option": "Failover (retry a failed request on the next target)", + "models_target_search_placeholder": "Search models or type a name…", + "models_weight_help": "Relative share of requests: 1 is a normal share, 2 gets twice as many, 3 three times (whole numbers, 1 or more).", + "workflows_model_search_placeholder": "Search models…", + "models_edit_failover": "Edit failover for {name}", + "models_edit_balancing": "Edit load balancing for {name}", "models_strategy_failover": "Failover (priority order: first available target, then the next in line)", "models_strategy_name_round_robin": "Round-robin", "models_strategy_name_cost": "Lowest cost", diff --git a/web/dashboard/messages/pl.json b/web/dashboard/messages/pl.json index bee60a0be..029a888af 100644 --- a/web/dashboard/messages/pl.json +++ b/web/dashboard/messages/pl.json @@ -214,6 +214,16 @@ "audit_loading": "Wczytywanie logów audytowych", "audit_search_placeholder": "Szukaj według ID Requestu, modelu, dostawcy, ścieżki, User Path lub błędu…", "audit_search_label": "Szukaj według ID Requestu, modelu, dostawcy, ścieżki, User Path lub błędu", + "audit_field_value_placeholder": "Wpisz wartość filtra", + "audit_field_value_label": "Wartość pola audytu", + "audit_filter_field_label": "Pole filtra audytu", + "audit_filter_field_user_path": "Ścieżka użytkownika", + "audit_filter_field_request_id": "ID żądania", + "audit_filter_field_model": "Model", + "audit_filter_field_provider": "Dostawca", + "audit_filter_field_session_id": "ID sesji", + "audit_filter_field_error_type": "Typ błędu", + "audit_filter_field_search": "Wszystkie pola", "audit_filter_method_label": "Filtr metody HTTP", "audit_filter_all_methods": "Wszystkie metody", "audit_filter_status_label": "Filtr kodu statusu", @@ -574,22 +584,11 @@ "models_all_scope": "Wszyscy dostawcy i modele", "models_all_in_provider": "Wszystkie modele dostawcy {provider}", "models_virtual_model": "Model wirtualny", - "models_this_model": "Ten model", "models_redirect": "Przekierowanie", "models_managed_config": "Zarządzane przez config.yaml / VIRTUAL_MODELS", "models_config": "Config", "models_targets": "Cele", "models_redirects_to": "Przekierowuje do", - "models_falls_back_to": "Awaryjnie przełącza na", - "models_balanced_with": "Równoważony z", - "models_failover_title": "Failover: ten model obsługuje żądania jako pierwszy; gdy zawiedzie, przejmuje kolejny cel", - "models_balanced_title": "Równoważenie obciążenia: żądania są rozkładane między ten model i pozostałe cele; nieudane żądanie jest ponawiane na innym celu", - "models_balanced_title_failover_off": "Równoważenie obciążenia bez failoveru: żądania są rozkładane między ten model i pozostałe cele; nieudane żądanie nie jest ponawiane", - "models_balanced_title_failover_global_off": "Równoważenie obciążenia: failover jest wyłączony globalnie (FAILOVER_ENABLED), więc nieudane żądanie nie jest ponawiane", - "models_remove_failover": "Usuń cele awaryjne dla {model}", - "models_removing_failover": "Usuwanie celów awaryjnych dla {model}", - "models_remove_balancing": "Usuń równoważenie obciążenia dla {model}", - "models_removing_balancing": "Usuwanie równoważenia obciążenia dla {model}", "models_remove": "usuń", "models_remove_redirect": "Usuń przekierowanie dla {model}", "models_removing_redirect": "Usuwanie przekierowania dla {model}", @@ -604,18 +603,7 @@ "models_source": "Źródło", "models_add_target": "Dodaj cel (równoważenie obciążenia)", "models_strategy": "Strategia równoważenia obciążenia", - "models_strategy_pending_hint": "Ma zastosowanie, gdy wskazane są co najmniej dwa cele.", - "models_strategy_help_label": "Pomoc: strategia równoważenia obciążenia", - "models_summary_failover": "Żądania do {source} obsługuje {source}; gdy zawiedzie — {fallbacks}.", - "models_summary_balanced": "Żądania do {source} są równoważone między {source} i {peers} ({strategy}).", - "models_summary_replaced": "Żądania trafiają do {target}. Dodaj {source} jako cel, jeśli nie chcesz przesłonić modelu źródłowego.", - "models_summary_replaced_failover": "Żądania trafiają do {first}; gdy zawiedzie — do {rest}. Dodaj {source} jako cel, jeśli nie chcesz przesłonić modelu źródłowego.", - "models_summary_replaced_balanced": "Żądania są równoważone między {targets} ({strategy}). Dodaj {source} jako cel, jeśli nie chcesz przesłonić modelu źródłowego.", - "models_summary_alias": "Żądania do {source} trafiają do {target}.", - "models_summary_alias_failover": "Żądania do {source} trafiają do {first}; gdy zawiedzie — do {rest}.", - "models_summary_alias_balanced": "Żądania do {source} są równoważone między {targets} ({strategy}).", "models_session_keeping": "Utrzymywanie sesji (kieruj wykrytą sesję klienta do celu, który ją obsłużył)", - "models_failover_option": "Failover (ponów nieudane żądanie na kolejnym celu)", "models_user_paths": "User Paths", "models_user_paths_help_label": "Pomoc dotycząca ścieżek użytkownika", "models_user_paths_help": "Użyj /, aby zezwolić na każdy User Path. Użyj User Path zespołu, aby ograniczyć dostęp do jego poddrzewa, albo nieużywanego User Path, aby selektor był niedostępny.", @@ -628,10 +616,8 @@ "models_yes": "tak", "models_no": "nie", "models_target_model": "Model docelowy", - "models_target_search_placeholder": "Szukaj modeli lub wpisz nazwę…", "models_target_weight": "Waga celu", "models_weight": "waga", - "models_weight_help": "Względny udział żądań: 1 to zwykły udział, 2 dostaje dwa razy więcej, 3 trzy razy (liczby całkowite, co najmniej 1).", "models_remove_target": "Usuń cel", "models_pricing_editor": "Edytor cen modelu", "models_pricing_save": "Zapisz ceny", @@ -807,7 +793,6 @@ "workflows_all_scope": "Wszyscy dostawcy i modele", "workflows_model": "Model", "workflows_all_provider_models": "Wszystkie modele dostawcy", - "workflows_model_search_placeholder": "Szukaj modeli…", "workflows_name": "Nazwa", "workflows_name_placeholder": "Opcjonalna. Domyślnie etykieta zakresu.", "workflows_user_path": "User Path", @@ -1185,8 +1170,6 @@ "models_removing_alias": "Usuwanie aliasu {name}", "models_edit_alias": "Edytuj alias {name}", "models_edit_redirect": "Edytuj przekierowanie dla {name}", - "models_edit_failover": "Edytuj failover dla {name}", - "models_edit_balancing": "Edytuj równoważenie obciążenia dla {name}", "models_virtual_model_toggle": "model wirtualny", "models_category_all": "Modele", "models_category_text_generation": "Generowanie tekstu", @@ -1362,6 +1345,33 @@ "models_strategy_round_robin": "Round-robin (rotacja między celami z uwzględnieniem wag)", "models_strategy_cost": "Najniższy koszt (najtańszy cel dla Requestu)", "models_strategy_adaptive": "Adaptive (cel wybierany przez zarejestrowane rozszerzenie routingu)", + "models_this_model": "Ten model", + "models_falls_back_to": "Awaryjnie przełącza na", + "models_balanced_with": "Równoważony z", + "models_failover_title": "Failover: ten model obsługuje żądania jako pierwszy; gdy zawiedzie, przejmuje kolejny cel", + "models_balanced_title": "Równoważenie obciążenia: żądania są rozkładane między ten model i pozostałe cele; nieudane żądanie jest ponawiane na innym celu", + "models_balanced_title_failover_off": "Równoważenie obciążenia bez failoveru: żądania są rozkładane między ten model i pozostałe cele; nieudane żądanie nie jest ponawiane", + "models_balanced_title_failover_global_off": "Równoważenie obciążenia: failover jest wyłączony globalnie (FAILOVER_ENABLED), więc nieudane żądanie nie jest ponawiane", + "models_remove_failover": "Usuń cele awaryjne dla {model}", + "models_removing_failover": "Usuwanie celów awaryjnych dla {model}", + "models_remove_balancing": "Usuń równoważenie obciążenia dla {model}", + "models_removing_balancing": "Usuwanie równoważenia obciążenia dla {model}", + "models_strategy_pending_hint": "Ma zastosowanie, gdy wskazane są co najmniej dwa cele.", + "models_strategy_help_label": "Pomoc: strategia równoważenia obciążenia", + "models_summary_failover": "Żądania do {source} obsługuje {source}; gdy zawiedzie — {fallbacks}.", + "models_summary_balanced": "Żądania do {source} są równoważone między {source} i {peers} ({strategy}).", + "models_summary_replaced": "Żądania trafiają do {target}. Dodaj {source} jako cel, jeśli nie chcesz przesłonić modelu źródłowego.", + "models_summary_replaced_failover": "Żądania trafiają do {first}; gdy zawiedzie — do {rest}. Dodaj {source} jako cel, jeśli nie chcesz przesłonić modelu źródłowego.", + "models_summary_replaced_balanced": "Żądania są równoważone między {targets} ({strategy}). Dodaj {source} jako cel, jeśli nie chcesz przesłonić modelu źródłowego.", + "models_summary_alias": "Żądania do {source} trafiają do {target}.", + "models_summary_alias_failover": "Żądania do {source} trafiają do {first}; gdy zawiedzie — do {rest}.", + "models_summary_alias_balanced": "Żądania do {source} są równoważone między {targets} ({strategy}).", + "models_failover_option": "Failover (ponów nieudane żądanie na kolejnym celu)", + "models_target_search_placeholder": "Szukaj modeli lub wpisz nazwę…", + "models_weight_help": "Względny udział żądań: 1 to zwykły udział, 2 dostaje dwa razy więcej, 3 trzy razy (liczby całkowite, co najmniej 1).", + "workflows_model_search_placeholder": "Szukaj modeli…", + "models_edit_failover": "Edytuj failover dla {name}", + "models_edit_balancing": "Edytuj równoważenie obciążenia dla {name}", "models_strategy_failover": "Failover (kolejność priorytetów: pierwszy dostępny cel, potem kolejne)", "models_strategy_name_round_robin": "Round-robin", "models_strategy_name_cost": "Najniższy koszt", diff --git a/web/dashboard/src/pages/audit-logs/AuditFilters.svelte b/web/dashboard/src/pages/audit-logs/AuditFilters.svelte index 3f7744b96..a56860f82 100644 --- a/web/dashboard/src/pages/audit-logs/AuditFilters.svelte +++ b/web/dashboard/src/pages/audit-logs/AuditFilters.svelte @@ -1,6 +1,5 @@