package edgecmd import ( "bytes" "encoding/json" "fmt" "io" "net" "net/http" "strings" "time" "github.com/spf13/cobra" ) func smokeCmd() *cobra.Command { c := &cobra.Command{ Use: "smoke", Short: "Run verification and smoke tests", Long: `Run diagnostic smoke tests to verify the connectivity and functionality of IOP Edge components. Typical local flow: 1. iop-edge config init 2. iop-edge node register 3. iop-edge serve 4. iop-edge smoke openai --model `, Example: ` iop-edge smoke openai --model gemma4:26b iop-edge smoke openai --model gemma4:26b --base-url http://127.0.0.1:8080`, } c.AddCommand(smokeOpenAICmd()) return c } var ( smokeModel string smokeBaseURL string smokePrompt string smokeTimeout string ) type openAIModel struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` OwnedBy string `json:"owned_by"` } type openAIModelsResponse struct { Object string `json:"object"` Data []openAIModel `json:"data"` } type chatCompletionMessage struct { Role string `json:"role"` Content string `json:"content"` } type chatCompletionChoice struct { Index int `json:"index"` Message chatCompletionMessage `json:"message"` FinishReason string `json:"finish_reason"` } type chatCompletionUsage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } type chatCompletionResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []chatCompletionChoice `json:"choices"` Usage *chatCompletionUsage `json:"usage"` } func resolveSmokeBaseURL(cmd *cobra.Command) (string, error) { if smokeBaseURL != "" { return strings.TrimSuffix(smokeBaseURL, "/"), nil } cfg, _, err := LoadEdgeConfig(cmd) if err != nil { return "", fmt.Errorf("failed to load edge config to resolve base URL (pass --base-url to override): %w", err) } listen := cfg.OpenAI.Listen if listen == "" { listen = "127.0.0.1:8080" } host, port, err := net.SplitHostPort(listen) if err != nil { return "http://127.0.0.1:8080", nil } if host == "0.0.0.0" || host == "" { host = "127.0.0.1" } return "http://" + net.JoinHostPort(host, port), nil } func smokeOpenAICmd() *cobra.Command { c := &cobra.Command{ Use: "openai", Short: "Perform OpenAI-compatible endpoint smoke test", Long: `Validate the IOP Edge OpenAI-compatible HTTP API surface. It runs a three-step diagnostics check: 1. Checks GET /healthz (server status). 2. Checks GET /v1/models (advertised model compatibility). 3. Checks POST /v1/chat/completions (inference request processing). If --base-url is not provided, it auto-discovers the OpenAI URL from the effective config's listen address.`, Example: ` iop-edge smoke openai --model gemma4:26b iop-edge smoke openai --model gemma4:26b --base-url http://127.0.0.1:8080 --prompt "say hello" --timeout 15s`, RunE: func(cmd *cobra.Command, _ []string) error { baseURL, err := resolveSmokeBaseURL(cmd) if err != nil { return err } timeoutDur := 10 * time.Second if smokeTimeout != "" { d, err := time.ParseDuration(smokeTimeout) if err != nil { return fmt.Errorf("invalid timeout duration: %w", err) } timeoutDur = d } client := &http.Client{ Timeout: timeoutDur, } fmt.Fprintf(cmd.OutOrStdout(), "IOP Edge OpenAI Smoke Test\n") fmt.Fprintf(cmd.OutOrStdout(), "========================================\n") fmt.Fprintf(cmd.OutOrStdout(), "Target Endpoint: %s\n", baseURL) fmt.Fprintf(cmd.OutOrStdout(), "Target Model: %s\n", smokeModel) fmt.Fprintf(cmd.OutOrStdout(), "Timeout: %s\n\n", timeoutDur) fmt.Fprintf(cmd.OutOrStdout(), "Step 1: Checking /healthz ... ") healthURL := baseURL + "/healthz" resp, err := client.Get(healthURL) if err != nil { fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("GET %s failed: %w (make sure the edge server is running)", healthURL, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("GET %s returned non-200 status %d: %s", healthURL, resp.StatusCode, string(body)) } var healthData map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&healthData); err != nil { fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("failed to parse /healthz JSON response: %w", err) } fmt.Fprintln(cmd.OutOrStdout(), "[OK]") fmt.Fprintf(cmd.OutOrStdout(), "Step 2: Checking /v1/models ... ") modelsURL := baseURL + "/v1/models" resp, err = client.Get(modelsURL) if err != nil { fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("GET %s failed: %w", modelsURL, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("GET %s returned non-200 status %d: %s", modelsURL, resp.StatusCode, string(body)) } var modelsResp openAIModelsResponse if err := json.NewDecoder(resp.Body).Decode(&modelsResp); err != nil { fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("failed to parse /v1/models JSON response: %w", err) } modelIDs := make([]string, 0, len(modelsResp.Data)) modelMatched := false for _, m := range modelsResp.Data { modelIDs = append(modelIDs, m.ID) if m.ID == smokeModel { modelMatched = true } } fmt.Fprintf(cmd.OutOrStdout(), "[OK] (found models: %s)\n", strings.Join(modelIDs, ", ")) if !modelMatched { fmt.Fprintf(cmd.OutOrStdout(), "WARNING: Target model %q is not advertised by /v1/models (advertised: %s)\n", smokeModel, strings.Join(modelIDs, ", ")) } fmt.Fprintf(cmd.OutOrStdout(), "Step 3: Checking /v1/chat/completions ... ") prompt := smokePrompt if prompt == "" { prompt = "ping" } reqPayload := map[string]interface{}{ "model": smokeModel, "messages": []map[string]string{ {"role": "user", "content": prompt}, }, "stream": false, } payloadBytes, err := json.Marshal(reqPayload) if err != nil { return fmt.Errorf("failed to marshal request payload: %w", err) } chatURL := baseURL + "/v1/chat/completions" resp, err = client.Post(chatURL, "application/json", bytes.NewReader(payloadBytes)) if err != nil { fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("POST %s failed: %w (make sure active nodes are online)", chatURL, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("POST %s returned non-200 status %d: %s", chatURL, resp.StatusCode, string(body)) } var chatResp chatCompletionResponse if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil { fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("failed to parse /v1/chat/completions JSON response: %w", err) } if len(chatResp.Choices) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("POST %s returned empty choices", chatURL) } content := chatResp.Choices[0].Message.Content if strings.TrimSpace(content) == "" { fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") return fmt.Errorf("POST %s returned empty assistant message content", chatURL) } fmt.Fprintf(cmd.OutOrStdout(), "[OK]\n") fmt.Fprintf(cmd.OutOrStdout(), "Response Content: %q\n\n", content) fmt.Fprintf(cmd.OutOrStdout(), "IOP Edge OpenAI Smoke Test SUCCESS!\n") return nil }, } c.Flags().StringVar(&smokeModel, "model", "", "model identifier to check (required)") c.Flags().StringVar(&smokeBaseURL, "base-url", "", "override OpenAI base URL (e.g. http://127.0.0.1:8080)") c.Flags().StringVar(&smokePrompt, "prompt", "ping", "prompt text to send") c.Flags().StringVar(&smokeTimeout, "timeout", "10s", "client timeout duration") c.MarkFlagRequired("model") return c }