G3, the part that needs no model: a ComfyUI model's comfyui.output (or external.output) says pictures or meshes. A request for a 3D model — a job at /v1/3d/generations — carries the picture as base64; at the replica it is uploaded to ComfyUI's input directory (the gate lets /upload/image through), the workflow is filled in with its name, the seed and the resolution, and the mesh the workflow saves comes back with its type, which is the file the job keeps. comfy.Client reads every list of files an output names, not "images" only, and types a file by its extension where ComfyUI sends octet-stream. A model's license (name, url, notice) is said in /v1/models and in the job, and written into a GLB's asset.extras, since the file travels without the API; a mesh it cannot be written into is refused. per_mesh in auth.pricing, usage.meshes in the rows, the sums and the export (migration 0045). A picture asked of a mesh model, or a mesh of a picture model, is refused saying which it makes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
163 lines
5.8 KiB
Go
163 lines
5.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
func newRequestCmd(client func() *ctlClient, asJSON *bool) *cobra.Command {
|
|
var (
|
|
body bool
|
|
replay string
|
|
)
|
|
c := &cobra.Command{
|
|
Use: "request ID",
|
|
Short: "What one request went through: replica, queue, first token, tokens, guardrails",
|
|
Long: "Every response carries its id in X-Request-Id, and a client may send its\n" +
|
|
"own. Given one, this says which replica answered, how long admission held\n" +
|
|
"it, how long the first token took and what it cost — the answer to \"why\n" +
|
|
"was this slow\". The access log has the same fields under \"request\".\n\n" +
|
|
"For a project that keeps its requests (`llmgw keys requests`), --body\n" +
|
|
"shows what the model was sent and what came back, and --replay MODEL\n" +
|
|
"sends the same request to another model, past the limits and the\n" +
|
|
"guardrails, and shows its answer beside the first.",
|
|
Example: " llmgw ctl request req_3f2a…\n llmgw ctl request req_3f2a… --body\n llmgw ctl request req_3f2a… --replay qwen2.5-14b-instruct",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if replay != "" {
|
|
return replayRequest(cmd, client, args[0], replay, *asJSON)
|
|
}
|
|
if body {
|
|
return showRequestBody(cmd, client, args[0], *asJSON)
|
|
}
|
|
var v adminapi.RequestView
|
|
if err := client().get(cmd.Context(), "/api/v1/requests/"+url.PathEscape(args[0]), &v); err != nil {
|
|
return err
|
|
}
|
|
if *asJSON {
|
|
return json.NewEncoder(cmd.OutOrStdout()).Encode(v)
|
|
}
|
|
model := v.Model
|
|
if v.ServedModel != "" && v.ServedModel != v.Model {
|
|
model += " (served by " + v.ServedModel + ")"
|
|
}
|
|
out := cmd.OutOrStdout()
|
|
fmt.Fprintf(out, "request %s at %s\n", v.RequestID, v.At.UTC().Format("2006-01-02 15:04:05Z"))
|
|
fmt.Fprintf(out, "model %s\n", model)
|
|
fmt.Fprintf(out, "replica %s\n", or(v.Replica, "- (none answered)"))
|
|
fmt.Fprintf(out, "status %d\n", v.Status)
|
|
fmt.Fprintf(out, "time %d ms total: %d queued, %d to the first token\n", v.LatencyMs, v.QueueMs, v.TTFTMs)
|
|
fmt.Fprintf(out, "tokens %d in, %d out\n", v.PromptTokens, v.CompletionTokens)
|
|
if v.Images > 0 {
|
|
fmt.Fprintf(out, "images %d\n", v.Images)
|
|
}
|
|
if v.Meshes > 0 {
|
|
fmt.Fprintf(out, "meshes %d\n", v.Meshes)
|
|
}
|
|
if v.CacheHit {
|
|
fmt.Fprintf(out, "cache answered from the project's answer cache: nothing generated, nothing charged\n")
|
|
}
|
|
fmt.Fprintf(out, "who key %s, project %s%s\n", v.KeyID, v.ProjectID, prefixed(", user ", v.UserID))
|
|
if v.EndUser != "" {
|
|
fmt.Fprintf(out, "end user %s (the client's own name for them)\n", v.EndUser)
|
|
}
|
|
if len(v.Tags) > 0 {
|
|
keys := make([]string, 0, len(v.Tags))
|
|
for k := range v.Tags {
|
|
keys = append(keys, k)
|
|
}
|
|
slices.Sort(keys)
|
|
var kv []string
|
|
for _, k := range keys {
|
|
kv = append(kv, k+"="+v.Tags[k])
|
|
}
|
|
fmt.Fprintf(out, "tags %s\n", strings.Join(kv, ", "))
|
|
}
|
|
if v.GuardAction != "" {
|
|
what := or(v.GuardCategory, "-")
|
|
if v.GuardRule != "" {
|
|
what += ": " + v.GuardRule
|
|
}
|
|
fmt.Fprintf(out, "guard %s by policy %s (%s), %d ms\n", v.GuardAction, v.GuardPolicy, what, v.GuardMs)
|
|
} else if v.GuardMs > 0 {
|
|
fmt.Fprintf(out, "guard allowed, %d ms\n", v.GuardMs)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
c.Flags().BoolVar(&body, "body", false, "show the request and its answer, for a project that keeps them")
|
|
c.Flags().StringVar(&replay, "replay", "", "send the kept request to this model and show both answers")
|
|
return c
|
|
}
|
|
|
|
func showRequestBody(cmd *cobra.Command, client func() *ctlClient, id string, asJSON bool) error {
|
|
var b store.RequestBody
|
|
if err := client().get(cmd.Context(), "/api/v1/requests/"+url.PathEscape(id)+"/body", &b); err != nil {
|
|
return err
|
|
}
|
|
if asJSON {
|
|
return json.NewEncoder(cmd.OutOrStdout()).Encode(b)
|
|
}
|
|
out := cmd.OutOrStdout()
|
|
fmt.Fprintf(out, "request %s at %s, %s to %s (served by %s), status %d\n", b.RequestID,
|
|
b.At.UTC().Format("2006-01-02 15:04:05Z"), b.Path, b.Model, b.ServedModel, b.Status)
|
|
fmt.Fprintf(out, "kept until %s\n\n", b.ExpiresAt.UTC().Format("2006-01-02 15:04Z"))
|
|
fmt.Fprintln(out, "--- sent")
|
|
fmt.Fprintln(out, indentJSON(b.Request))
|
|
fmt.Fprintln(out, "--- answered")
|
|
fmt.Fprintln(out, or(b.Response, "(no text: an error, or not a completion)"))
|
|
if b.Truncated {
|
|
fmt.Fprintln(out, "(longer than what is kept; cut here)")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func replayRequest(cmd *cobra.Command, client func() *ctlClient, id, model string, asJSON bool) error {
|
|
var v adminapi.ReplayView
|
|
body, _ := json.Marshal(map[string]string{"model": model})
|
|
if err := client().post(cmd.Context(), "/api/v1/requests/"+url.PathEscape(id)+"/replay", body, &v); err != nil {
|
|
return err
|
|
}
|
|
if asJSON {
|
|
return json.NewEncoder(cmd.OutOrStdout()).Encode(v)
|
|
}
|
|
out := cmd.OutOrStdout()
|
|
fmt.Fprintf(out, "--- %s answered (status %d)\n%s\n\n", v.Original.ServedModel, v.Original.Status,
|
|
or(v.Original.Response, "(no text)"))
|
|
fmt.Fprintf(out, "--- %s answers (status %d, %d ms)\n%s\n", v.Model, v.Status, v.TookMs, or(replyText(v.Body), string(v.Body)))
|
|
return nil
|
|
}
|
|
|
|
// replyText is a completion's text, or "" when it is not one.
|
|
func replyText(body json.RawMessage) string {
|
|
var c struct {
|
|
Choices []struct {
|
|
Message struct {
|
|
Content string `json:"content"`
|
|
} `json:"message"`
|
|
Text string `json:"text"`
|
|
} `json:"choices"`
|
|
}
|
|
if json.Unmarshal(body, &c) != nil || len(c.Choices) == 0 {
|
|
return ""
|
|
}
|
|
return c.Choices[0].Message.Content + c.Choices[0].Text
|
|
}
|
|
|
|
func indentJSON(s string) string {
|
|
var v any
|
|
if json.Unmarshal([]byte(s), &v) != nil {
|
|
return s
|
|
}
|
|
b, _ := json.MarshalIndent(v, "", " ")
|
|
return string(b)
|
|
}
|