diff --git a/README.md b/README.md index 92b5532..7c9d56f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ $ export PATH=$GOPATH/bin:$PATH ## Quick-start ```bash -# initialise metadata at the repository root +# initialize repository configuration. +# This will analyze the project files, and summarize them using your LLM provider of choice. $ vyb init # ask the LLM to implement a TODO in the current module @@ -86,6 +87,41 @@ A hierarchical representation of the workspace: The metadata is fully derived from the file system; you should never edit it manually. + +### Project Configuration (`.vyb/config.yaml`) + +`vyb init` creates a **config file** alongside the project metadata so the +CLI knows which LLM backend to call: + +```yaml +provider: openai +``` + +Only one key is defined for now but the document might grow in the future +(temperature defaults, retries, …). The provider string is case-insensitive +and must match one of the options returned by `vyb llm.SupportedProviders()`. + +### Model abstraction – family & size + +Instead of hard-coding provider-specific model identifiers in every template +we use a two-part specification: + +* **Family** – logical grouping (`gpt`, `reasoning`, …) +* **Size** – `large` or `small` + +The active provider maps the tuple to its concrete model name. For example +the OpenAI implementation currently resolves to: + +| Family / Size | Resolved model | +|---------------|----------------| +| gpt / large | GPT-4.1 | +| gpt / small | GPT-4.1-mini | +| reasoning / large | o3 | +| reasoning / small | o4-mini | + +This indirection keeps templates provider-agnostic and allows you to switch +backends without touching prompt definitions. + ### Annotations `vyb` records three complementary summaries for every module: diff --git a/cmd/init.go b/cmd/init.go index 68fb91c..203436e 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -4,7 +4,10 @@ import ( "fmt" "os" + "github.com/AlecAivazis/survey/v2" "github.com/spf13/cobra" + "github.com/vybdev/vyb/config" + "github.com/vybdev/vyb/llm" "github.com/vybdev/vyb/workspace/project" ) @@ -14,11 +17,39 @@ var initCmd = &cobra.Command{ Run: Init, } +// Init is the cobra handler for `vyb init`. func Init(_ *cobra.Command, _ []string) { - err := project.Create(".") - if err != nil { - fmt.Printf("Error creating metadata: %v\n", err) + // --------------------------------------------------------------------- + // 1. Ask the user which provider should be configured. + // --------------------------------------------------------------------- + provider := chooseProvider() + + // --------------------------------------------------------------------- + // 2. Generate project configuration and update annotations + // --------------------------------------------------------------------- + if err := project.Create(".", provider); err != nil { + fmt.Printf("Error initializing project: %v\n", err) os.Exit(1) } - fmt.Println("Project metadata created successfully.") + + fmt.Println("Project initialized successfully.") +} + +// chooseProvider interacts with the user to pick a provider. When the +// session is not interactive or the prompt fails, it returns the default +// provider. +func chooseProvider() string { + providers := llm.SupportedProviders() + + var selection string + prompt := &survey.Select{ + Message: "Select LLM provider:", + Options: providers, + Default: config.Default().Provider, + } + // Ignore prompt errors (non-tty, etc.) and fall back to default. + if err := survey.AskOne(prompt, &selection); err != nil || selection == "" { + return config.Default().Provider + } + return selection } diff --git a/cmd/template/README.md b/cmd/template/README.md index 617d9a8..23fde21 100644 --- a/cmd/template/README.md +++ b/cmd/template/README.md @@ -15,6 +15,7 @@ fields: | `requestExclusionPatterns` | Files to never embed | | `modificationInclusionPatterns` | Files the LLM is allowed to touch | | `modificationExclusionPatterns` | Guard-rails against accidental edits | +| `model` *(opt)* | Tuple `{family, size}` selecting the LLM | At runtime the loader merges three sources (by precedence): @@ -24,3 +25,18 @@ At runtime the loader merges three sources (by precedence): Templates use Mustache placeholders to inject dynamic data (e.g. the command-specific prompt gets embedded into a global *system* prompt). + +### `model` field + +Every template can optionally override the default model by specifying the +following YAML fragment: + +```yaml +model: + family: reasoning # one of: gpt, reasoning + size: small # large or small +``` + +When absent the loader falls back to `{family: reasoning, size: large}`. +The exact resolution to a concrete model string is handled by the active +provider (see `.vyb/config.yaml`). diff --git a/cmd/template/loader.go b/cmd/template/loader.go index f2f21fa..707cf8a 100644 --- a/cmd/template/loader.go +++ b/cmd/template/loader.go @@ -2,6 +2,7 @@ package template import ( "embed" + "github.com/vybdev/vyb/config" "gopkg.in/yaml.v3" "io/fs" "os" @@ -37,13 +38,18 @@ func loadConfigs(rootFS fs.FS) []*Definition { continue } - var cmdDef Definition - if err := yaml.Unmarshal(data, &cmdDef); err != nil { + cmdDef := &Definition{ + Model: Model{ + Family: config.ModelFamilyReasoning, + Size: config.ModelSizeLarge, + }, + } + if err := yaml.Unmarshal(data, cmdDef); err != nil { // Handle or log error as needed continue } - cmdDefinitions = append(cmdDefinitions, &cmdDef) + cmdDefinitions = append(cmdDefinitions, cmdDef) } } diff --git a/cmd/template/template.go b/cmd/template/template.go index 2f78f08..c5aa710 100644 --- a/cmd/template/template.go +++ b/cmd/template/template.go @@ -2,13 +2,14 @@ package template import ( "fmt" + "github.com/vybdev/vyb/config" "os" "path/filepath" "strings" "github.com/cbroglie/mustache" "github.com/spf13/cobra" - "github.com/vybdev/vyb/llm/openai" + "github.com/vybdev/vyb/llm" "github.com/vybdev/vyb/llm/payload" "github.com/vybdev/vyb/workspace/context" "github.com/vybdev/vyb/workspace/matcher" @@ -25,9 +26,14 @@ var systemExclusionPatterns = []string{ "go.sum", } +type Model struct { + Family config.ModelFamily `yaml:"family"` + Size config.ModelSize `yaml:"size"` +} + type Definition struct { Name string `yaml:"name"` - Model string `yaml:"model"` + Model Model `yaml:"model"` // ArgExclusionPatterns specifies patterns for files that should be excluded as command arguments. ArgExclusionPatterns []string `yaml:"argExclusionPatterns"` @@ -124,6 +130,11 @@ func execute(cmd *cobra.Command, args []string, def *Definition) error { rootFS := os.DirFS(absRoot) + cfg, err := config.Load(absRoot) + if err != nil { + return err + } + if relTarget != nil { if !matcher.IsIncluded(rootFS, *relTarget, append(systemExclusionPatterns, def.ArgExclusionPatterns...), def.ArgInclusionPatterns) { return fmt.Errorf("command \"%s\" does not support given target %s", cmd.Use, *relTarget) @@ -206,7 +217,7 @@ func execute(cmd *cobra.Command, args []string, def *Definition) error { systemMessage := rendered - proposal, err := openai.GetWorkspaceChangeProposals(systemMessage, userMsg) + proposal, err := llm.GetWorkspaceChangeProposals(cfg, def.Model.Family, def.Model.Size, systemMessage, userMsg) if err != nil { return err } diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a89d9a9 --- /dev/null +++ b/config/config.go @@ -0,0 +1,80 @@ +package config + +import ( + "fmt" + "io/fs" + "os" + + "gopkg.in/yaml.v3" +) + +// Config captures user-level settings stored in .vyb/config.yaml. +// +// The initial schema is intentionally minimal; new fields can be added +// without breaking forwards-compatibility as callers should always +// access configuration via the exported struct rather than raw maps. +// +// Example YAML: +// +// provider: openai +// +// Zero-value Config is invalid – use Default() when no config file is +// found. +// +// NOTE: keep field tags in sync with YAML when extending this struct. +// +// Use explicit field names so unknown keys are rejected when the +// file *is* present (surfacing typo errors early). +// +//nolint:revive // field name is intentionally simple +type Config struct { + Provider string `yaml:"provider"` +} + +// defaultProvider is used when no configuration file exists or it cannot +// be parsed. The value must always map to a known provider in the llm +// dispatcher. +const defaultProvider = "openai" + +// Default returns a Config populated with hard-coded defaults. It should +// be used whenever .vyb/config.yaml is missing. +func Default() *Config { + return &Config{Provider: defaultProvider} +} + +// Load reads .vyb/config.yaml located under projectRoot. When the file +// does not exist the function returns Default() with a nil error so the +// caller can proceed transparently. Any other I/O or unmarshalling error +// is propagated. +func Load(projectRoot string) (*Config, error) { + if projectRoot == "" { + return nil, fmt.Errorf("projectRoot must not be empty") + } + return LoadFS(os.DirFS(projectRoot)) +} + +// LoadFS performs the same operation as Load but works directly on an +// fs.FS. This facilitates unit-testing with fstest.MapFS. +func LoadFS(fsys fs.FS) (*Config, error) { + const relPath = ".vyb/config.yaml" + + data, err := fs.ReadFile(fsys, relPath) + if err != nil { + if os.IsNotExist(err) { + // No config file – fall back to defaults. + return Default(), nil + } + return nil, fmt.Errorf("failed to read %s: %w", relPath, err) + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal %s: %w", relPath, err) + } + + // Basic sanity check – default when Provider is empty. + if cfg.Provider == "" { + cfg.Provider = defaultProvider + } + return &cfg, nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..5f92b26 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,33 @@ +package config + +import ( + "path/filepath" + "testing" + "testing/fstest" +) + +func TestLoadFS_Default(t *testing.T) { + fsys := fstest.MapFS{} + + cfg, err := LoadFS(fsys) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Provider != "openai" { + t.Fatalf("expected default provider 'openai', got %s", cfg.Provider) + } +} + +func TestLoadFS_FromFile(t *testing.T) { + fsys := fstest.MapFS{ + filepath.ToSlash(".vyb/config.yaml"): &fstest.MapFile{Data: []byte("provider: fooai\n")}, + } + + cfg, err := LoadFS(fsys) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Provider != "fooai" { + t.Fatalf("expected provider 'fooai', got %s", cfg.Provider) + } +} diff --git a/config/types.go b/config/types.go new file mode 100644 index 0000000..6fb9998 --- /dev/null +++ b/config/types.go @@ -0,0 +1,50 @@ +package config + +// ModelFamily represents the generic family of a language model. +// +// The enumeration is intentionally small for now – new families can be +// added without touching the public API surface that depends on the +// underlying string values. +// +// NOTE: keep the string literals all-lowercase as they are used for +// YAML/JSON marshaling and command-line flags. +// +// Example usage: +// +// var f ModelFamily = ModelFamilyGPT +// fmt.Println(f) // -> "gpt" +// +// The zero value is an empty string and therefore invalid. Always +// initialise the variable with one of the provided constants. +// +// New families MUST be handled in every provider implementation – use +// exhaustive switch checks to ensure compile-time safety. +type ModelFamily string + +const ( + // ModelFamilyGPT groups together GPT-style chat models optimised for + // general purpose coding / conversation (e.g. GPT-4). + ModelFamilyGPT ModelFamily = "gpt" + + // ModelFamilyReasoning corresponds to models targeted at long + // reasoning or planning tasks. + ModelFamilyReasoning ModelFamily = "reasoning" +) + +func (m ModelFamily) String() string { return string(m) } + +// ModelSize captures the coarse size tier of a model within the same +// family. Providers translate these buckets to concrete model names +// (e.g. "large" → "gpt-4o", "small" → "gpt-3.5-turbo-0125"). +type ModelSize string + +const ( + // ModelSizeLarge is the higher-capability (and more expensive) + // variant within a given family. + ModelSizeLarge ModelSize = "large" + + // ModelSizeSmall is the cheaper and faster sibling of Large. + ModelSizeSmall ModelSize = "small" +) + +func (m ModelSize) String() string { return string(m) } diff --git a/config/types_test.go b/config/types_test.go new file mode 100644 index 0000000..bce8745 --- /dev/null +++ b/config/types_test.go @@ -0,0 +1,54 @@ +package config + +import "testing" + +func TestModelFamilyString(t *testing.T) { + cases := []struct { + in ModelFamily + want string + }{ + {ModelFamilyGPT, "gpt"}, + {ModelFamilyReasoning, "reasoning"}, + } + + for _, c := range cases { + if got := c.in.String(); got != c.want { + t.Fatalf("ModelFamily.String() = %s, want %s", got, c.want) + } + } + + // Compile-time exhaustiveness – if a new constant is added the + // switch below must be updated or the build will fail. + var fam ModelFamily = ModelFamilyGPT + switch fam { + case ModelFamilyGPT, ModelFamilyReasoning: + // ok + default: + t.Fatalf("unhandled ModelFamily constant %q", fam) + } +} + +func TestModelSizeString(t *testing.T) { + cases := []struct { + in ModelSize + want string + }{ + {ModelSizeLarge, "large"}, + {ModelSizeSmall, "small"}, + } + + for _, c := range cases { + if got := c.in.String(); got != c.want { + t.Fatalf("ModelSize.String() = %s, want %s", got, c.want) + } + } + + // Compile-time exhaustiveness guard. + var sz ModelSize = ModelSizeLarge + switch sz { + case ModelSizeLarge, ModelSizeSmall: + // ok + default: + t.Fatalf("unhandled ModelSize constant %q", sz) + } +} diff --git a/go.mod b/go.mod index a1c487a..efaba86 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/vybdev/vyb go 1.24 require ( + github.com/AlecAivazis/survey/v2 v2.3.7 github.com/cbroglie/mustache v1.2.0 github.com/google/go-cmp v0.7.0 github.com/spf13/cobra v1.6.1 @@ -53,6 +54,7 @@ require ( github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a // indirect github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kisielk/gotool v1.0.0 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect github.com/kyoh86/exportloopref v0.1.4 // indirect @@ -61,6 +63,7 @@ require ( github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb // indirect github.com/mattn/go-colorable v0.1.7 // indirect github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/nakabonne/nestif v0.3.0 // indirect @@ -93,11 +96,11 @@ require ( github.com/ultraware/funlen v0.0.2 // indirect github.com/ultraware/whitespace v0.0.4 // indirect github.com/uudashr/gocognit v1.0.1 // indirect - golang.org/x/mod v0.3.0 // indirect - golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect - golang.org/x/text v0.3.2 // indirect - golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347 // indirect - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/text v0.4.0 // indirect + golang.org/x/tools v0.1.12 // indirect gopkg.in/ini.v1 v1.51.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect honnef.co/go/tools v0.0.1-2020.1.4 // indirect diff --git a/go.sum b/go.sum index 3cde69a..de7bbd4 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -18,6 +20,8 @@ github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 h1:XTrzB+F8+SpRm github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us= github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= @@ -48,6 +52,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -188,6 +194,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -205,6 +213,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -231,6 +241,7 @@ github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpAp github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb h1:RHba4YImhrUVQDHUCe2BNSOz4tVy2yGyXhvYDvxGgeE= github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -242,6 +253,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -385,6 +398,7 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -397,6 +411,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -417,8 +432,9 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -436,8 +452,10 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -447,6 +465,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -467,12 +486,22 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -513,8 +542,9 @@ golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200428185508-e9a00ec82136/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347 h1:/e4fNMHdLn7SQSxTrRZTma2xjQW6ELdxcnpqMhpo9X4= golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/llm/README.md b/llm/README.md index 873b074..6a0104e 100644 --- a/llm/README.md +++ b/llm/README.md @@ -1,8 +1,18 @@ # llm Package -`llm` wraps all interaction with OpenAI and exposes strongly-typed data +`llm` wraps all interaction with OpenAI and exposes strongly typed data structures so the rest of the codebase never has to deal with raw JSON. +## Model abstractions ⚙️ + +| Type | Constants | Purpose | +|----------------|--------------------------|--------------------------------------| +| `ModelFamily` | `gpt`, `reasoning` | High-level family/category of models | +| `ModelSize` | `large`, `small` | Coarse size tier inside a family | + +The `(family, size)` tuple is later resolved by the active provider into a +concrete model string (e.g. `GPT+Large → "GPT-4.1"`). + ## Sub-packages ### `llm/openai` diff --git a/llm/dispatcher.go b/llm/dispatcher.go new file mode 100644 index 0000000..5d66a9d --- /dev/null +++ b/llm/dispatcher.go @@ -0,0 +1,67 @@ +package llm + +import ( + "fmt" + "github.com/vybdev/vyb/config" + "github.com/vybdev/vyb/llm/internal/openai" + "github.com/vybdev/vyb/llm/payload" +) + +// provider captures the common operations expected from any LLM backend. +// It is intentionally unexported so that the public surface of the llm +// package stays minimal while allowing internal dispatch based on user +// configuration. +// +// Additional methods should be appended here whenever new high-level +// helpers are added to the llm façade. +type provider interface { + GetWorkspaceChangeProposals(fam config.ModelFamily, sz config.ModelSize, systemMessage, userMessage string) (*payload.WorkspaceChangeProposal, error) + GetModuleContext(systemMessage, userMessage string) (*payload.ModuleSelfContainedContext, error) + GetModuleExternalContexts(systemMessage, userMessage string) (*payload.ModuleExternalContextResponse, error) +} + +type openAIProvider struct{} + +func (*openAIProvider) GetWorkspaceChangeProposals(fam config.ModelFamily, sz config.ModelSize, sysMsg, userMsg string) (*payload.WorkspaceChangeProposal, error) { + return openai.GetWorkspaceChangeProposals(fam, sz, sysMsg, userMsg) +} + +func (*openAIProvider) GetModuleContext(sysMsg, userMsg string) (*payload.ModuleSelfContainedContext, error) { + return openai.GetModuleContext(sysMsg, userMsg) +} + +func (*openAIProvider) GetModuleExternalContexts(sysMsg, userMsg string) (*payload.ModuleExternalContextResponse, error) { + return openai.GetModuleExternalContexts(sysMsg, userMsg) +} + +func GetModuleExternalContexts(cfg *config.Config, sysMsg, userMsg string) (*payload.ModuleExternalContextResponse, error) { + if provider, err := resolveProvider(cfg); err != nil { + return nil, err + } else { + return provider.GetModuleExternalContexts(sysMsg, userMsg) + } +} + +func GetModuleContext(cfg *config.Config, sysMsg, userMsg string) (*payload.ModuleSelfContainedContext, error) { + if provider, err := resolveProvider(cfg); err != nil { + return nil, err + } else { + return provider.GetModuleContext(sysMsg, userMsg) + } +} +func GetWorkspaceChangeProposals(cfg *config.Config, fam config.ModelFamily, sz config.ModelSize, sysMsg, userMsg string) (*payload.WorkspaceChangeProposal, error) { + if provider, err := resolveProvider(cfg); err != nil { + return nil, err + } else { + return provider.GetWorkspaceChangeProposals(fam, sz, sysMsg, userMsg) + } +} + +func resolveProvider(cfg *config.Config) (provider, error) { + switch cfg.Provider { + case "openai": + return &openAIProvider{}, nil + default: + return nil, fmt.Errorf("unknown provider: %s", cfg.Provider) + } +} diff --git a/llm/dispatcher_test.go b/llm/dispatcher_test.go new file mode 100644 index 0000000..25a5243 --- /dev/null +++ b/llm/dispatcher_test.go @@ -0,0 +1,28 @@ +package llm + +import ( + "testing" + + "github.com/vybdev/vyb/config" +) + +// TestResolveProvider verifies that the dispatcher returns the expected +// concrete implementation for known providers and fails for unknown ones. +func TestResolveProvider(t *testing.T) { + // 1. Happy-path – "openai" should map to *openAIProvider. + cfg := &config.Config{Provider: "openai"} + + p, err := resolveProvider(cfg) + if err != nil { + t.Fatalf("unexpected error resolving provider: %v", err) + } + if _, ok := p.(*openAIProvider); !ok { + t.Fatalf("resolveProvider returned %T, want *openAIProvider", p) + } + + // 2. Unknown provider should surface an error. + cfg.Provider = "doesnotexist" + if _, err := resolveProvider(cfg); err == nil { + t.Fatalf("expected error for unknown provider, got nil") + } +} diff --git a/llm/openai/internal/schema/schema.go b/llm/internal/openai/internal/schema/schema.go similarity index 100% rename from llm/openai/internal/schema/schema.go rename to llm/internal/openai/internal/schema/schema.go diff --git a/llm/openai/internal/schema/schema_test.go b/llm/internal/openai/internal/schema/schema_test.go similarity index 100% rename from llm/openai/internal/schema/schema_test.go rename to llm/internal/openai/internal/schema/schema_test.go diff --git a/llm/openai/internal/schema/schemas/module_external_context_schema.json b/llm/internal/openai/internal/schema/schemas/module_external_context_schema.json similarity index 100% rename from llm/openai/internal/schema/schemas/module_external_context_schema.json rename to llm/internal/openai/internal/schema/schemas/module_external_context_schema.json diff --git a/llm/openai/internal/schema/schemas/module_selfcontained_context_schema.json b/llm/internal/openai/internal/schema/schemas/module_selfcontained_context_schema.json similarity index 100% rename from llm/openai/internal/schema/schemas/module_selfcontained_context_schema.json rename to llm/internal/openai/internal/schema/schemas/module_selfcontained_context_schema.json diff --git a/llm/openai/internal/schema/schemas/workspace_change_proposal_schema.json b/llm/internal/openai/internal/schema/schemas/workspace_change_proposal_schema.json similarity index 100% rename from llm/openai/internal/schema/schemas/workspace_change_proposal_schema.json rename to llm/internal/openai/internal/schema/schemas/workspace_change_proposal_schema.json diff --git a/llm/openai/openai.go b/llm/internal/openai/openai.go similarity index 81% rename from llm/openai/openai.go rename to llm/internal/openai/openai.go index 0adb270..a9d5219 100644 --- a/llm/openai/openai.go +++ b/llm/internal/openai/openai.go @@ -5,11 +5,13 @@ import ( "encoding/json" "errors" "fmt" - "github.com/vybdev/vyb/llm/openai/internal/schema" - "github.com/vybdev/vyb/llm/payload" + "github.com/vybdev/vyb/config" + "github.com/vybdev/vyb/llm/internal/openai/internal/schema" "io" "net/http" "os" + + "github.com/vybdev/vyb/llm/payload" "time" ) @@ -51,9 +53,39 @@ func (o openaiErrorResponse) Error() string { return fmt.Sprintf("OpenAI API error: %s", o.OpenAIError.Message) } -// GetModuleContext calls the LLM and returns a parsed ModuleSelfContainedContext value. +// ----------------------------------------------------------------------------- +// +// Model resolver +// +// ----------------------------------------------------------------------------- +// mapModel converts a generic (family,size) pair into a concrete OpenAI model +// string. The mapping is local to this provider so business-level code never +// depends on provider-specific identifiers. +func mapModel(fam config.ModelFamily, sz config.ModelSize) (string, error) { + switch fam { + case config.ModelFamilyGPT: + switch sz { + case config.ModelSizeLarge: + return "GPT-4.1", nil + case config.ModelSizeSmall: + return "GPT-4.1-mini", nil + } + case config.ModelFamilyReasoning: + switch sz { + case config.ModelSizeLarge: + return "o3", nil + case config.ModelSizeSmall: + return "o4-mini", nil + } + } + return "", fmt.Errorf("openai: unsupported model mapping for family=%s size=%s", fam, sz) +} + +// GetModuleContext calls the LLM and returns a parsed ModuleSelfContainedContext +// value using the model derived from family/size. func GetModuleContext(systemMessage, userMessage string) (*payload.ModuleSelfContainedContext, error) { - openaiResp, err := callOpenAI(systemMessage, userMessage, schema.GetModuleContextSchema(), "o4-mini") + model := "o4-mini" + openaiResp, err := callOpenAI(systemMessage, userMessage, schema.GetModuleContextSchema(), model) if err != nil { var openAIErrResp openaiErrorResponse if errors.As(err, &openAIErrResp) { @@ -74,8 +106,11 @@ func GetModuleContext(systemMessage, userMessage string) (*payload.ModuleSelfCon // GetWorkspaceChangeProposals sends the given messages to the OpenAI API and // returns the structured workspace change proposal. -func GetWorkspaceChangeProposals(systemMessage, userMessage string) (*payload.WorkspaceChangeProposal, error) { - model := "o3" +func GetWorkspaceChangeProposals(fam config.ModelFamily, sz config.ModelSize, systemMessage, userMessage string) (*payload.WorkspaceChangeProposal, error) { + model, err := mapModel(fam, sz) + if err != nil { + return nil, err + } openaiResp, err := callOpenAI(systemMessage, userMessage, schema.GetWorkspaceChangeProposalSchema(), model) if err != nil { diff --git a/llm/providers.go b/llm/providers.go new file mode 100644 index 0000000..0ed5e6b --- /dev/null +++ b/llm/providers.go @@ -0,0 +1,13 @@ +package llm + +// SupportedProviders returns the list of LLM providers that can be chosen +// when initialising a new vyb project. The slice is a copy – callers may +// modify it without affecting the package-level data. +func SupportedProviders() []string { + return append([]string(nil), supportedProviders...) // defensive copy +} + +// supportedProviders holds the hard-coded list of providers until dynamic +// registration lands. Keep the strings in lowercase as they are written +// verbatim to .vyb/config.yaml. +var supportedProviders = []string{"openai"} diff --git a/workspace/project/annotation.go b/workspace/project/annotation.go index d9bae9a..3ec86da 100644 --- a/workspace/project/annotation.go +++ b/workspace/project/annotation.go @@ -2,7 +2,8 @@ package project import ( "fmt" - "github.com/vybdev/vyb/llm/openai" + "github.com/vybdev/vyb/config" + "github.com/vybdev/vyb/llm" "github.com/vybdev/vyb/llm/payload" "io/fs" "strings" @@ -22,7 +23,7 @@ type Annotation struct { // modules back to the root. For each module that has no Annotation, it calls // addOrUpdateSelfContainedContext for it after all its submodules are annotated. The creation of // annotations is performed in parallel using goroutines. -func annotate(metadata *Metadata, sysfs fs.FS) error { +func annotate(cfg *config.Config, metadata *Metadata, sysfs fs.FS) error { if metadata == nil || metadata.Modules == nil { return nil } @@ -56,7 +57,7 @@ func annotate(metadata *Metadata, sysfs fs.FS) error { for _, sub := range mod.Modules { <-dones[sub] } - err := addOrUpdateSelfContainedContext(mod, sysfs) + err := addOrUpdateSelfContainedContext(cfg, mod, sysfs) if err != nil { errCh <- fmt.Errorf("failed to create annotation for module %q: %w", mod.Name, err) // Signal done to avoid blocking parents. @@ -82,7 +83,7 @@ func annotate(metadata *Metadata, sysfs fs.FS) error { // Add all external context annotations in a single shot // In the future, we should make this take into consideration // the token count of the annotations and possibly split the calls. - return addOrUpdateExternalContext(root) + return addOrUpdateExternalContext(cfg, root) } // collectModulesInPostOrder gathers modules in a post-order traversal (children first). @@ -134,7 +135,7 @@ func buildModuleContextRequest(m *Module) *payload.ModuleSelfContainedContextReq } // addOrUpdateSelfContainedContext calls OpenAI to construct the internal and public context of a given module. -func addOrUpdateSelfContainedContext(m *Module, sysfs fs.FS) error { +func addOrUpdateSelfContainedContext(cfg *config.Config, m *Module, sysfs fs.FS) error { // Build the ModuleSelfContainedContextRequest tree starting from this module. req := buildModuleContextRequest(m) @@ -171,7 +172,7 @@ you included in the Internal Context, but also all the Public Context informatio Each type of context should be as descriptive as possible, using around one thousand LLM tokens, each.` - context, err := openai.GetModuleContext(systemMessage, userMsg) + context, err := llm.GetModuleContext(cfg, systemMessage, userMsg) fmt.Printf(" Got response for module %q\n", m.Name) @@ -216,7 +217,7 @@ Each type of context should be as descriptive as possible, using around one thou // corresponding module, creating annotation objects when necessary. // // If the LLM call fails the error is propagated to the caller. -func addOrUpdateExternalContext(m *Module) error { +func addOrUpdateExternalContext(cfg *config.Config, m *Module) error { if m == nil { return nil } @@ -285,7 +286,7 @@ concise explanation of where the module lives in the hierarchy and what lives Return your answer as JSON following the schema you have been provided.` - resp, err := openai.GetModuleExternalContexts(sysPrompt, userMsg) + resp, err := llm.GetModuleExternalContexts(cfg, sysPrompt, userMsg) if err != nil { return err } diff --git a/workspace/project/metadata.go b/workspace/project/metadata.go index 10ccebf..bc638ca 100644 --- a/workspace/project/metadata.go +++ b/workspace/project/metadata.go @@ -4,6 +4,7 @@ import ( "crypto/md5" "encoding/hex" "fmt" + "github.com/vybdev/vyb/config" "github.com/vybdev/vyb/workspace/context" "io/fs" "os" @@ -124,10 +125,17 @@ var systemExclusionPatterns = []string{ } // Create creates the project metadata configuration at the project root. -// Returns an error if the metadata cannot be created, or if it already exists. -// If a ".vyb" folder exists in the root directory or any of its subdirectories, -// this function returns an error. -func Create(projectRoot string) error { +// The function now also persists .vyb/config.yaml with the chosen LLM +// provider so callers do not have to duplicate that logic. +// +// Returns an error if the metadata cannot be created, or if it already +// exists. If a ".vyb" folder exists in the root directory or any of its +// subdirectories, this function returns an error. +func Create(projectRoot string, provider string) error { + + if provider == "" { + provider = config.Default().Provider + } rootFS := os.DirFS(projectRoot) existingFolders, err := findAllConfigWithinRoot(rootFS) @@ -143,12 +151,32 @@ func Create(projectRoot string) error { return fmt.Errorf("failed to create .vyb directory: %w", err) } + // ------------------------------------------------------------------ + // 1. Persist configuration – this must happen before metadata so that + // later code relying on config.Load() works even during init. + // ------------------------------------------------------------------ + cfg := &config.Config{Provider: provider} + cfgBytes, err := yaml.Marshal(cfg) + if err != nil { + return fmt.Errorf("failed to marshal config.yaml: %w", err) + } + if len(cfgBytes) == 0 || cfgBytes[len(cfgBytes)-1] != '\n' { + cfgBytes = append(cfgBytes, '\n') + } + cfgPath := filepath.Join(configDir, "config.yaml") + if err := os.WriteFile(cfgPath, cfgBytes, 0644); err != nil { + return fmt.Errorf("failed to write config.yaml: %w", err) + } + + // ------------------------------------------------------------------ + // 2. Build and annotate metadata as before. + // ------------------------------------------------------------------ metadata, err := buildMetadata(rootFS) if err != nil { return fmt.Errorf("failed to build metadata: %w", err) } - err = annotate(metadata, rootFS) + err = annotate(cfg, metadata, rootFS) if err != nil { return fmt.Errorf("failed to annotate metadata: %w", err) } diff --git a/workspace/project/update.go b/workspace/project/update.go index 0f00bb6..f4374db 100644 --- a/workspace/project/update.go +++ b/workspace/project/update.go @@ -2,6 +2,7 @@ package project import ( "fmt" + "github.com/vybdev/vyb/config" "os" "path/filepath" @@ -87,27 +88,31 @@ func Update(projectRoot string) error { rootFS := os.DirFS(absRoot) - // Step 1 – load existing metadata (with annotations). + // load existing metadata (with annotations). stored, err := loadStoredMetadata(rootFS) if err != nil { return err } - // Step 2 – build a fresh snapshot. + // build a fresh snapshot. fresh, err := buildMetadata(rootFS) if err != nil { return err } - // Step 3 – patch stored metadata with the fresh structure. + // patch stored metadata with the fresh structure. stored.Patch(fresh) - // Step 4 – (re)annotate modules missing or with invalid annotations. - if err := annotate(stored, rootFS); err != nil { + cfg, err := config.Load(absRoot) + if err != nil { + return err + } + // (re)annotate modules missing or with invalid annotations. + if err := annotate(cfg, stored, rootFS); err != nil { return err } - // Step 5 – persist back to .vyb/metadata.yaml. + // persist back to .vyb/metadata.yaml. data, err := yaml.Marshal(stored) if err != nil { return fmt.Errorf("failed to marshal updated metadata: %w", err)