a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
227 lines
7.6 KiB
Go
227 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/zzet/gortex/internal/llm"
|
|
"github.com/zzet/gortex/internal/llm/registry"
|
|
)
|
|
|
|
// provider.go is the `gortex provider` command tree — a CLI front end
|
|
// for the custom OpenAI-compatible provider registry (providers.json).
|
|
// It lets users register arbitrary endpoints (OpenRouter, Groq,
|
|
// Together, a self-hosted gateway, …) by name, then select one via
|
|
// `llm.provider` / GORTEX_LLM_PROVIDER like any built-in provider.
|
|
|
|
var providerCmd = &cobra.Command{
|
|
Use: "provider",
|
|
Short: "Manage custom OpenAI-compatible LLM providers",
|
|
Long: "Manage the registry of custom OpenAI-compatible LLM providers " +
|
|
"(providers.json). A registered provider can be selected with " +
|
|
"`llm.provider: <name>` in config or GORTEX_LLM_PROVIDER=<name>, " +
|
|
"exactly like a built-in provider.",
|
|
}
|
|
|
|
var providerListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List registered custom providers",
|
|
Args: cobra.NoArgs,
|
|
RunE: runProviderList,
|
|
}
|
|
|
|
var providerShowCmd = &cobra.Command{
|
|
Use: "show <name>",
|
|
Short: "Show one custom provider's full definition",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runProviderShow,
|
|
}
|
|
|
|
var providerRemoveCmd = &cobra.Command{
|
|
Use: "remove <name>",
|
|
Aliases: []string{"rm", "delete"},
|
|
Short: "Remove a custom provider",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runProviderRemove,
|
|
}
|
|
|
|
var providerAddCmd = &cobra.Command{
|
|
Use: "add <name>",
|
|
Short: "Register (or replace) a custom OpenAI-compatible provider",
|
|
Long: "Register a custom OpenAI-compatible provider. --base-url and " +
|
|
"--model are required; the base URL should include any version " +
|
|
"segment (e.g. https://api.groq.com/openai/v1) — gortex appends " +
|
|
"/chat/completions.",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runProviderAdd,
|
|
}
|
|
|
|
// flags for `provider add`.
|
|
var (
|
|
providerBaseURL string
|
|
providerModel string
|
|
providerAPIKeyEnv string
|
|
providerSchemaMode string
|
|
providerMaxTokensF string
|
|
providerReasoning string
|
|
providerTemperature float64
|
|
providerHasTemp bool
|
|
providerHeaders []string
|
|
providerPriceInput float64
|
|
providerPriceOutput float64
|
|
)
|
|
|
|
func init() {
|
|
providerAddCmd.Flags().StringVar(&providerBaseURL, "base-url", "", "OpenAI-compatible API base, incl. version segment (required)")
|
|
providerAddCmd.Flags().StringVar(&providerModel, "model", "", "default model identifier (required)")
|
|
providerAddCmd.Flags().StringVar(&providerAPIKeyEnv, "api-key-env", "", "env var holding the bearer key (omit for keyless local endpoints)")
|
|
providerAddCmd.Flags().StringVar(&providerSchemaMode, "schema-mode", "", "structured-output mode: json_schema (default) | json_object | prompt")
|
|
providerAddCmd.Flags().StringVar(&providerMaxTokensF, "max-tokens-field", "", "override the output-token body key (default max_completion_tokens)")
|
|
providerAddCmd.Flags().StringVar(&providerReasoning, "reasoning-effort", "", "value sent as reasoning_effort (e.g. low|medium|high)")
|
|
providerAddCmd.Flags().Float64Var(&providerTemperature, "temperature", 0, "temperature to send (only sent when --temperature is set)")
|
|
providerAddCmd.Flags().StringArrayVar(&providerHeaders, "header", nil, "extra request header as key=value (repeatable)")
|
|
providerAddCmd.Flags().Float64Var(&providerPriceInput, "price-input", 0, "informational USD per 1M input tokens")
|
|
providerAddCmd.Flags().Float64Var(&providerPriceOutput, "price-output", 0, "informational USD per 1M output tokens")
|
|
|
|
providerCmd.AddCommand(providerListCmd)
|
|
providerCmd.AddCommand(providerShowCmd)
|
|
providerCmd.AddCommand(providerAddCmd)
|
|
providerCmd.AddCommand(providerRemoveCmd)
|
|
rootCmd.AddCommand(providerCmd)
|
|
}
|
|
|
|
func runProviderAdd(cmd *cobra.Command, args []string) error {
|
|
name := strings.TrimSpace(args[0])
|
|
providerHasTemp = cmd.Flags().Changed("temperature")
|
|
|
|
headers, err := parseHeaderFlags(providerHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cp := llm.CustomProvider{
|
|
BaseURL: strings.TrimSpace(providerBaseURL),
|
|
Model: strings.TrimSpace(providerModel),
|
|
APIKeyEnv: strings.TrimSpace(providerAPIKeyEnv),
|
|
SchemaMode: strings.TrimSpace(providerSchemaMode),
|
|
MaxTokensField: strings.TrimSpace(providerMaxTokensF),
|
|
ReasoningEffort: strings.TrimSpace(providerReasoning),
|
|
Headers: headers,
|
|
Pricing: llm.ProviderPricing{Input: providerPriceInput, Output: providerPriceOutput},
|
|
}
|
|
if providerHasTemp {
|
|
t := providerTemperature
|
|
cp.Temperature = &t
|
|
}
|
|
|
|
if err := registry.Add(name, cp); err != nil {
|
|
return fmt.Errorf("provider add %q: %w", name, err)
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Registered custom provider %q -> %s (model %s)\n", name, cp.BaseURL, cp.Model)
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Select it with: llm.provider: %s (or GORTEX_LLM_PROVIDER=%s)\n", name, name)
|
|
if cp.APIKeyEnv != "" {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Set the key in $%s before use.\n", cp.APIKeyEnv)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runProviderRemove(cmd *cobra.Command, args []string) error {
|
|
name := strings.TrimSpace(args[0])
|
|
removed, err := registry.Remove(name)
|
|
if err != nil {
|
|
return fmt.Errorf("provider remove %q: %w", name, err)
|
|
}
|
|
if !removed {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "No custom provider named %q\n", name)
|
|
return nil
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Removed custom provider %q\n", name)
|
|
return nil
|
|
}
|
|
|
|
func runProviderList(cmd *cobra.Command, _ []string) error {
|
|
entries, err := registry.List()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(entries) == 0 {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "No custom providers registered.\nAdd one with: gortex provider add <name> --base-url <url> --model <model>\n")
|
|
return nil
|
|
}
|
|
w := cmd.OutOrStdout()
|
|
fmt.Fprintf(w, "%-16s %-40s %-24s %s\n", "NAME", "BASE_URL", "MODEL", "SCHEMA")
|
|
for _, e := range entries {
|
|
mode := e.Provider.SchemaMode
|
|
if mode == "" {
|
|
mode = "json_schema"
|
|
}
|
|
fmt.Fprintf(w, "%-16s %-40s %-24s %s\n", e.Name, e.Provider.BaseURL, e.Provider.Model, mode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runProviderShow(cmd *cobra.Command, args []string) error {
|
|
name := strings.TrimSpace(args[0])
|
|
cp, ok, err := registry.Get(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return fmt.Errorf("no custom provider named %q", name)
|
|
}
|
|
w := cmd.OutOrStdout()
|
|
fmt.Fprintf(w, "name: %s\n", name)
|
|
fmt.Fprintf(w, "base_url: %s\n", cp.BaseURL)
|
|
fmt.Fprintf(w, "model: %s\n", cp.Model)
|
|
if cp.APIKeyEnv != "" {
|
|
fmt.Fprintf(w, "api_key_env: %s\n", cp.APIKeyEnv)
|
|
}
|
|
mode := cp.SchemaMode
|
|
if mode == "" {
|
|
mode = "json_schema (default)"
|
|
}
|
|
fmt.Fprintf(w, "schema_mode: %s\n", mode)
|
|
if cp.MaxTokensField != "" {
|
|
fmt.Fprintf(w, "max_tokens_field: %s\n", cp.MaxTokensField)
|
|
}
|
|
if cp.Temperature != nil {
|
|
fmt.Fprintf(w, "temperature: %g\n", *cp.Temperature)
|
|
}
|
|
if cp.ReasoningEffort != "" {
|
|
fmt.Fprintf(w, "reasoning_effort: %s\n", cp.ReasoningEffort)
|
|
}
|
|
if len(cp.Headers) > 0 {
|
|
keys := make([]string, 0, len(cp.Headers))
|
|
for k := range cp.Headers {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
fmt.Fprintf(w, "header: %s=%s\n", k, cp.Headers[k])
|
|
}
|
|
}
|
|
if cp.Pricing.Input != 0 || cp.Pricing.Output != 0 {
|
|
fmt.Fprintf(w, "pricing: $%.4f in / $%.4f out per 1M tokens\n", cp.Pricing.Input, cp.Pricing.Output)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseHeaderFlags turns repeated --header key=value flags into a map.
|
|
func parseHeaderFlags(flags []string) (map[string]string, error) {
|
|
if len(flags) == 0 {
|
|
return nil, nil
|
|
}
|
|
out := make(map[string]string, len(flags))
|
|
for _, f := range flags {
|
|
k, v, ok := strings.Cut(f, "=")
|
|
k = strings.TrimSpace(k)
|
|
if !ok || k == "" {
|
|
return nil, fmt.Errorf("invalid --header %q (want key=value)", f)
|
|
}
|
|
out[k] = strings.TrimSpace(v)
|
|
}
|
|
return out, nil
|
|
}
|