From 4d93e5fa02f1b09e826287c76dd1dc67f3746e75 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 18:00:41 -0400 Subject: [PATCH 01/16] spec --- 0_TODO_vyb.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 0_TODO_vyb.md diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md new file mode 100644 index 0000000..cb9b467 --- /dev/null +++ b/0_TODO_vyb.md @@ -0,0 +1,28 @@ +# TODO(vyb): THIS IS YOUR HIGHEST PRIORITY TASK! + +## What you will do +Right now the application has an llm package, but inside it we only have an openai implementation that is exported and called directly by the business logic that requires the functionality. Additionally, even the templated commands have a field for model name, which is directly tied to a model provider. + +A more robust and flexible solution would be for the model providers to be completely abstracted away from the business logic. All the exported code should be at the llm package, and based on user configuration, the code then decides whether to delegate the calls to OpenAi, or any of the other--to be supported--model providers. + +Instead of calling out the model name directly in the commands and templates, we could refer to them as model families (GPT and reasoning), and model size (Large,Small). And let the provider-specific code map these concepts to its own domain. + +The configuration should come from .vyb/config.yaml. This file should be created during vyb init execution, and default the LLM-provider to OpenAI. + +## How you will do it +Perform the next task listed under "What is left to do" in the order they are listed. +You are expected to accomplish no more and no less than one task at a time. +Mark with an [x] the task you have finished. + +## What you need to know + +- Question: Your question should be formatted like this. + - Answer: And my answer will be formatted like this. + +## What will it look like +This section will contain your proposed solution for the problem that you were given. + +## What is left to do +- [ ] First, evaluate the code in this project, and the task description in "What you will do". Then ask as many questions as you need to have full certainty about what is being asked. Ask your questions under "What you need to know" section. +- [ ] Once your questions have been answered, propose a design for your solution. Replace the contents under "What will it look like" with the proposed changes to the system. This is not a list of tasks, it is a vision for the final state of the system to satisfy all the requirements. +- [ ] Now review everything you know about this task, and break it down into a list of atomic changes, and add them to this list here. Each change should be selfcontained, and leave the system one step closer to the desired state. Make sure to include tests and documentation changes alongside each step, since the repository should not get into an inconsistent state in between these changes. From 6625ca16dff4927938bada6ec8dcd59049d6de78 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 18:27:43 -0400 Subject: [PATCH 02/16] q&a --- 0_TODO_vyb.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index cb9b467..bb493b6 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -15,14 +15,60 @@ You are expected to accomplish no more and no less than one task at a time. Mark with an [x] the task you have finished. ## What you need to know +- Question: Where exactly should `.vyb/config.yaml` live relative to the + project root? Inside the existing `.vyb/` directory or alongside it? + - Answer: the `config.yaml` will live inside the `.vyb/` directory that is created as part of the `vyb init` execution. -- Question: Your question should be formatted like this. - - Answer: And my answer will be formatted like this. +- Question: What *schema* is expected for this file? At minimum it needs + the provider name (`openai`, `anthropic`, *etc.*) plus the mapping from + (family, size) → provider-specific model string; is anything else + required (e.g. API keys, temperature defaults, retries)? + - Answer: Model provider should be a field within the config data structure (which you will create as well). The value of model provider should be a data structure that only has the model provider's name, for now. We may make this more robust in the future. Do not include family and size mapping in the config, this should be in the code of the model specific package. + +- Question: Which "model families" and "sizes" must be supported in the + first iteration? The task mentions *GPT / reasoning* and *Large / Small*; + please confirm the full matrix we should encode. + - Answer: This logic should live in the provider package, since the mapping will change from one to another. + - family(GPT) + size(L) -> "GPT-4.1" + - family(GPT) + size(S) -> "GPT-4.1-mini" + - family(reasoning) + size(L) -> "o3" + - family(reasoning) + size(L) -> "o4-mini" + +- Question: The current code uses explicit model strings (`o3`, `o4-mini`) + in several places. Should those be removed entirely or only hidden + behind a resolver while keeping the literals? + - Answer: functions within the openai module that already have models hardcoded can continue to do so. The functions that receive the model as a parameter should now receive model family and size instead. + +- Question: Is backwards compatibility with existing templates important + (i.e. should `model:` in `.vyb` template files still work)? + - Answer: backward compatibility is not important, but you should convert the templates you find to match the new structure + +- Question: Which public surface should the refactored `llm` package + expose? A single `Call` function, higher-level helpers similar to the + current `GetWorkspaceChangeProposals`, or both? + - Answer:Functions like `GetWorkspaceChangeProposals`, which encapsulate business logic + +- Question: Do we need a mechanism to override the provider at runtime + via an environment variable/flag, or is the YAML file authoritative? + - Answer:Design the code in a way that this can be added in the future. But for now there is no need to provide additional ways of loading the provider. + +- Question: Should `vyb init` *prompt* the user for the desired provider + or silently create the default (`openai`) config? + - Answer: Yes, it should provide a list of supported providers for the user to select. For now the list will only have openai, but we will add to it later. + +- Question: Apart from OpenAI, are there already providers on the roadmap + that we should stub out (e.g. `anthropic`, `azure-openai`)? + - Answer: Not yet + +- Question: Any preference for the dependency injection approach? + (interface in `llm` + provider registration vs. simple `switch` on + config). + - Answer: it can be a simple switch in the `llm` package, forwarding the calls to provider specific internal packages ## What will it look like This section will contain your proposed solution for the problem that you were given. ## What is left to do -- [ ] First, evaluate the code in this project, and the task description in "What you will do". Then ask as many questions as you need to have full certainty about what is being asked. Ask your questions under "What you need to know" section. +- [x] First, evaluate the code in this project, and the task description in "What you will do". Then ask as many questions as you need to have full certainty about what is being asked. Ask your questions under "What you need to know" section. - [ ] Once your questions have been answered, propose a design for your solution. Replace the contents under "What will it look like" with the proposed changes to the system. This is not a list of tasks, it is a vision for the final state of the system to satisfy all the requirements. - [ ] Now review everything you know about this task, and break it down into a list of atomic changes, and add them to this list here. Each change should be selfcontained, and leave the system one step closer to the desired state. Make sure to include tests and documentation changes alongside each step, since the repository should not get into an inconsistent state in between these changes. From d49cd484d62837dd7b4c1b945d37c3e9c98f15e8 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 18:38:15 -0400 Subject: [PATCH 03/16] proposed design --- 0_TODO_vyb.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index bb493b6..c6e0d8b 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -66,9 +66,90 @@ Mark with an [x] the task you have finished. - Answer: it can be a simple switch in the `llm` package, forwarding the calls to provider specific internal packages ## What will it look like -This section will contain your proposed solution for the problem that you were given. +Below is the *envisioned* state of the codebase once the refactor is +fully completed. It serves as a north-star, **not** as a task list. + +### Configuration +``` +.vyb/ +└── config.yaml # generated by `vyb init` + +# content +provider: openai # future-proof top-level key +``` + +### Public API (`llm` package) +``` +// high-level helpers (same signatures everywhere) +func GetWorkspaceChangeProposals(sysMsg, userMsg string, fam ModelFamily, sz ModelSize) (*payload.WorkspaceChangeProposal, error) +func GetModuleContext(...) +``` +`ModelFamily` is an enum-like string (`GPT`, `Reasoning`); `ModelSize` +is `Large` or `Small`. + +### Provider selection +``` +func resolveProvider() Provider { + cfg := config.Load() // reads .vyb/config.yaml (cached) + switch strings.ToLower(cfg.Provider) { + case "openai", "": + return openai.Provider{} + // future: case "anthropic": ... + default: + return openai.Provider{} // safe default + } +} +``` +`Provider` is an internal interface that mirrors all high-level helper +methods so the `llm` façade can simply delegate. + +### Mapping family/size → concrete model (OpenAI example) +``` +// TODO(vyb): change this part so it takes a `config.ModelSpec` instance instead. +func (Provider) mapModel(fam llm.ModelFamily, sz llm.ModelSize) string { + switch fam { + case llm.GPT: + if sz == llm.Large { return "GPT-4.1" } + return "GPT-4.1-mini" + case llm.Reasoning: + if sz == llm.Large { return "o3" } + return "o4-mini" + default: + return "o3" // sane fallback + } +} +``` +Only the provider knows about proprietary model names; business logic +never sees them. + +### Command templates +The `.vyb` YAMLs `model` field becomes a `ModelSpec` struct with the following fields: +``` +family: GPT # or Reasoning +size: Large # or Small +``` +The template loader passes the model struct down to the `llm` façade. + +### CLI changes +* `vyb init` prompts: *"Select LLM provider [OpenAI]"* → writes + `config.yaml`. +* Existing commands remain unchanged for the end user; they benefit from + provider abstraction transparently. + +### Testing +* Unit tests for `config` load/save round-trips. +* Contract tests asserting that each provider’s `mapModel` covers the + required family/size matrix. +* An integration test stubs a fake provider and validates that the `llm` + façade dispatches correctly based on `config.yaml`. + +### Documentation +* README: new *Configuration* section explaining `config.yaml` and + provider switching. +* `cmd/template/README.md`: model fields updated. ## What is left to do - [x] First, evaluate the code in this project, and the task description in "What you will do". Then ask as many questions as you need to have full certainty about what is being asked. Ask your questions under "What you need to know" section. -- [ ] Once your questions have been answered, propose a design for your solution. Replace the contents under "What will it look like" with the proposed changes to the system. This is not a list of tasks, it is a vision for the final state of the system to satisfy all the requirements. +- [x] Once your questions have been answered, propose a design for your solution. Replace the contents under "What will it look like" with the proposed changes to the system. This is not a list of tasks, it is a vision for the final state of the system to satisfy all the requirements. +- [ ] Review and update the proposed design taking into consideration any feedback and TODO notes I left. - [ ] Now review everything you know about this task, and break it down into a list of atomic changes, and add them to this list here. Each change should be selfcontained, and leave the system one step closer to the desired state. Make sure to include tests and documentation changes alongside each step, since the repository should not get into an inconsistent state in between these changes. From 26ec057fffb18c8cbc5a7739a84d40b1735f9ff3 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 18:53:03 -0400 Subject: [PATCH 04/16] revised design --- 0_TODO_vyb.md | 248 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 166 insertions(+), 82 deletions(-) diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index c6e0d8b..08600d3 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -1,155 +1,239 @@ # TODO(vyb): THIS IS YOUR HIGHEST PRIORITY TASK! ## What you will do -Right now the application has an llm package, but inside it we only have an openai implementation that is exported and called directly by the business logic that requires the functionality. Additionally, even the templated commands have a field for model name, which is directly tied to a model provider. - -A more robust and flexible solution would be for the model providers to be completely abstracted away from the business logic. All the exported code should be at the llm package, and based on user configuration, the code then decides whether to delegate the calls to OpenAi, or any of the other--to be supported--model providers. - -Instead of calling out the model name directly in the commands and templates, we could refer to them as model families (GPT and reasoning), and model size (Large,Small). And let the provider-specific code map these concepts to its own domain. - -The configuration should come from .vyb/config.yaml. This file should be created during vyb init execution, and default the LLM-provider to OpenAI. +Right now the application has an llm package, but inside it we only have +an openai implementation that is exported and called directly by the +business logic that requires the functionality. Additionally, even the +templated commands have a field for model name, which is directly tied +to a model provider. + +A more robust and flexible solution would be for the model providers to +be completely abstracted away from the business logic. All the exported +code should be at the llm package, and based on user configuration, the +code then decides whether to delegate the calls to OpenAi, or any of the +other--to be supported--model providers. + +Instead of calling out the model name directly in the commands and +templates, we could refer to them as model families (GPT and reasoning), +and model size (Large,Small). And let the provider-specific code map +these concepts to its own domain. + +The configuration should come from .vyb/config.yaml. This file should be +created during vyb init execution, and default the LLM-provider to +OpenAI. ## How you will do it -Perform the next task listed under "What is left to do" in the order they are listed. -You are expected to accomplish no more and no less than one task at a time. -Mark with an [x] the task you have finished. +Perform the next task listed under "What is left to do" in the order +they are listed. You are expected to accomplish no more and no less than +one task at a time. Mark with an [x] the task you have finished. ## What you need to know - Question: Where exactly should `.vyb/config.yaml` live relative to the project root? Inside the existing `.vyb/` directory or alongside it? - - Answer: the `config.yaml` will live inside the `.vyb/` directory that is created as part of the `vyb init` execution. + - Answer: the `config.yaml` will live inside the `.vyb/` directory that + is created as part of the `vyb init` execution. - Question: What *schema* is expected for this file? At minimum it needs the provider name (`openai`, `anthropic`, *etc.*) plus the mapping from (family, size) → provider-specific model string; is anything else required (e.g. API keys, temperature defaults, retries)? - - Answer: Model provider should be a field within the config data structure (which you will create as well). The value of model provider should be a data structure that only has the model provider's name, for now. We may make this more robust in the future. Do not include family and size mapping in the config, this should be in the code of the model specific package. + - Answer: Model provider should be a field within the config data + structure (which you will create as well). The value of model + provider should be a data structure that only has the model + provider's name, for now. We may make this more robust in the + future. Do not include family and size mapping in the config, this + should be in the code of the model specific package. - Question: Which "model families" and "sizes" must be supported in the - first iteration? The task mentions *GPT / reasoning* and *Large / Small*; - please confirm the full matrix we should encode. - - Answer: This logic should live in the provider package, since the mapping will change from one to another. + first iteration? The task mentions *GPT / reasoning* and *Large / + Small*; please confirm the full matrix we should encode. + - Answer: This logic should live in the provider package, since the + mapping will change from one to another. - family(GPT) + size(L) -> "GPT-4.1" - family(GPT) + size(S) -> "GPT-4.1-mini" - family(reasoning) + size(L) -> "o3" - - family(reasoning) + size(L) -> "o4-mini" + - family(reasoning) + size(S) -> "o4-mini" -- Question: The current code uses explicit model strings (`o3`, `o4-mini`) - in several places. Should those be removed entirely or only hidden - behind a resolver while keeping the literals? - - Answer: functions within the openai module that already have models hardcoded can continue to do so. The functions that receive the model as a parameter should now receive model family and size instead. +- Question: The current code uses explicit model strings (`o3`, + `o4-mini`) in several places. Should those be removed entirely or only + hidden behind a resolver while keeping the literals? + - Answer: functions within the openai module that already have models + hardcoded can continue to do so. The functions that receive the + model as a parameter should now receive model family and size + instead. - Question: Is backwards compatibility with existing templates important (i.e. should `model:` in `.vyb` template files still work)? - - Answer: backward compatibility is not important, but you should convert the templates you find to match the new structure + - Answer: backward compatibility is not important, but you should + convert the templates you find to match the new structure - Question: Which public surface should the refactored `llm` package expose? A single `Call` function, higher-level helpers similar to the current `GetWorkspaceChangeProposals`, or both? - - Answer:Functions like `GetWorkspaceChangeProposals`, which encapsulate business logic + - Answer:Functions like `GetWorkspaceChangeProposals`, which + encapsulate business logic - Question: Do we need a mechanism to override the provider at runtime via an environment variable/flag, or is the YAML file authoritative? - - Answer:Design the code in a way that this can be added in the future. But for now there is no need to provide additional ways of loading the provider. + - Answer:Design the code in a way that this can be added in the + future. But for now there is no need to provide additional ways of + loading the provider. - Question: Should `vyb init` *prompt* the user for the desired provider or silently create the default (`openai`) config? - - Answer: Yes, it should provide a list of supported providers for the user to select. For now the list will only have openai, but we will add to it later. + - Answer: Yes, it should provide a list of supported providers for the + user to select. For now the list will only have openai, but we will + add to it later. -- Question: Apart from OpenAI, are there already providers on the roadmap - that we should stub out (e.g. `anthropic`, `azure-openai`)? +- Question: Apart from OpenAI, are there already providers on the + roadmap that we should stub out (e.g. `anthropic`, `azure-openai`)? - Answer: Not yet - Question: Any preference for the dependency injection approach? (interface in `llm` + provider registration vs. simple `switch` on config). - - Answer: it can be a simple switch in the `llm` package, forwarding the calls to provider specific internal packages + - Answer: it can be a simple switch in the `llm` package, forwarding + the calls to provider specific internal packages ## What will it look like -Below is the *envisioned* state of the codebase once the refactor is -fully completed. It serves as a north-star, **not** as a task list. +Below is the *updated* vision for the codebase once the refactor is +completed. It incorporates all clarifications and feedback received so +far. -### Configuration +### Configuration layout ``` .vyb/ -└── config.yaml # generated by `vyb init` - -# content -provider: openai # future-proof top-level key +├── metadata.yaml # existing – DO NOT TOUCH BY HAND +└── config.yaml # NEW – written by `vyb init` ``` -### Public API (`llm` package) -``` -// high-level helpers (same signatures everywhere) -func GetWorkspaceChangeProposals(sysMsg, userMsg string, fam ModelFamily, sz ModelSize) (*payload.WorkspaceChangeProposal, error) -func GetModuleContext(...) +`config.yaml` (initial contents) +```yaml +# .vyb/config.yaml +provider: + name: openai # future-proof: structure allows nested settings later ``` -`ModelFamily` is an enum-like string (`GPT`, `Reasoning`); `ModelSize` -is `Large` or `Small`. +Only the provider name is stored for now; credentials continue to be +supplied through environment variables (e.g. `OPENAI_API_KEY`). -### Provider selection +### Public API (`llm` package) +```go +// enum-like helpers – exported so templates & callers share the same type +package llm + +type ModelFamily string // “GPT”, “Reasoning”, … +const ( + GPT ModelFamily = "GPT" + Reasoning ModelFamily = "Reasoning" +) + +type ModelSize string // “Large”, “Small” +const ( + Large ModelSize = "Large" + Small ModelSize = "Small" +) + +// Provider-agnostic helpers +func GetWorkspaceChangeProposals(sysMsg, userMsg string, + fam ModelFamily, sz ModelSize) (*payload.WorkspaceChangeProposal, error) + +// More façade helpers will follow the same pattern. ``` -func resolveProvider() Provider { - cfg := config.Load() // reads .vyb/config.yaml (cached) - switch strings.ToLower(cfg.Provider) { +Only these high-level functions are reachable by the rest of the +application. They internally choose the concrete provider based on the +user configuration. + +### Provider resolution +```go +func resolveProvider() provider { // provider is an unexported interface + cfg, _ := config.Load() // cached read of .vyb/config.yaml + switch strings.ToLower(cfg.Provider.Name) { case "openai", "": return openai.Provider{} - // future: case "anthropic": ... + // future: case "anthropic": … default: - return openai.Provider{} // safe default + return openai.Provider{} // safe fallback } } ``` -`Provider` is an internal interface that mirrors all high-level helper -methods so the `llm` façade can simply delegate. +The returned object satisfies a private `provider` interface mirroring +all façade helpers, so delegation is trivial. -### Mapping family/size → concrete model (OpenAI example) +### Template changes +Old: +```yaml +model: o3 ``` -// TODO(vyb): change this part so it takes a `config.ModelSpec` instance instead. -func (Provider) mapModel(fam llm.ModelFamily, sz llm.ModelSize) string { +New: +```yaml +model: + family: GPT # or Reasoning + size: Large # or Small +``` +The loader now unmarshals into: +```go +type ModelSpec struct { + Family llm.ModelFamily `yaml:"family"` + Size llm.ModelSize `yaml:"size"` +} +``` +and hands it straight to the façade call. + +### Mapping family/size → provider model (OpenAI example) +```go +// openai/provider.go (unexported helper) +func mapModel(fam llm.ModelFamily, sz llm.ModelSize) string { switch fam { case llm.GPT: - if sz == llm.Large { return "GPT-4.1" } + if sz == llm.Large { + return "GPT-4.1" + } return "GPT-4.1-mini" case llm.Reasoning: - if sz == llm.Large { return "o3" } + if sz == llm.Large { + return "o3" + } return "o4-mini" default: return "o3" // sane fallback } } ``` -Only the provider knows about proprietary model names; business logic -never sees them. - -### Command templates -The `.vyb` YAMLs `model` field becomes a `ModelSpec` struct with the following fields: -``` -family: GPT # or Reasoning -size: Large # or Small -``` -The template loader passes the model struct down to the `llm` façade. +This logic lives **inside** the provider; business code never sees raw +model strings. -### CLI changes -* `vyb init` prompts: *"Select LLM provider [OpenAI]"* → writes - `config.yaml`. -* Existing commands remain unchanged for the end user; they benefit from - provider abstraction transparently. +### CLI (`vyb init`) +At initialisation time we now: +1. Ask: *“Select LLM provider”* – currently the only option is **OpenAI**. +2. Persist the choice to `.vyb/config.yaml`. +3. Leave `metadata.yaml` untouched. -### Testing -* Unit tests for `config` load/save round-trips. -* Contract tests asserting that each provider’s `mapModel` covers the - required family/size matrix. -* An integration test stubs a fake provider and validates that the `llm` - façade dispatches correctly based on `config.yaml`. +### Testing strategy +* Config loader round-trip & defaults. +* Provider mapping unit tests (one table-driven test per provider). +* Integration stub verifying façade dispatch according to `config.yaml`. ### Documentation -* README: new *Configuration* section explaining `config.yaml` and - provider switching. -* `cmd/template/README.md`: model fields updated. +* README gains a *Configuration* section. +* Template README updated to new `model:` structure. ## What is left to do -- [x] First, evaluate the code in this project, and the task description in "What you will do". Then ask as many questions as you need to have full certainty about what is being asked. Ask your questions under "What you need to know" section. -- [x] Once your questions have been answered, propose a design for your solution. Replace the contents under "What will it look like" with the proposed changes to the system. This is not a list of tasks, it is a vision for the final state of the system to satisfy all the requirements. -- [ ] Review and update the proposed design taking into consideration any feedback and TODO notes I left. -- [ ] Now review everything you know about this task, and break it down into a list of atomic changes, and add them to this list here. Each change should be selfcontained, and leave the system one step closer to the desired state. Make sure to include tests and documentation changes alongside each step, since the repository should not get into an inconsistent state in between these changes. +- [x] First, evaluate the code in this project, and the task + description in "What you will do". Then ask as many questions as + you need to have full certainty about what is being asked. Ask + your questions under "What you need to know" section. +- [x] Once your questions have been answered, propose a design for your + solution. Replace the contents under "What will it look like" with + the proposed changes to the system. This is not a list of tasks, + it is a vision for the final state of the system to satisfy all + the requirements. +- [x] Review and update the proposed design taking into consideration + any feedback and TODO notes I left. +- [ ] Now review everything you know about this task, and break it down + into a list of atomic changes, and add them to this list here. + Each change should be selfcontained and leave the system one step + closer to the desired state. At the end of each change, the system + should still compile, pass tests, and be fully functional. + Make sure to include tests and documentation changes alongside + each step, since the repository should not get into an inconsistent + state in between these changes. From 95591dcc51b9e3640af3814e9c4db1fe51181814 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 18:57:03 -0400 Subject: [PATCH 05/16] docs: add step-by-step implementation plan for provider-agnostic LLM refactor The "What is left to do" section in 0_TODO_vyb.md now contains a detailed, chronologically ordered list of atomic changes required to migrate the codebase to the new provider-agnostic llm architecture. Each item keeps the workspace in a buildable and testable state and explicitly mentions accompanying test and documentation updates. --- 0_TODO_vyb.md | 189 ++++++++++++++++---------------------------------- 1 file changed, 61 insertions(+), 128 deletions(-) diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index 08600d3..6c5eb61 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -96,126 +96,7 @@ one task at a time. Mark with an [x] the task you have finished. the calls to provider specific internal packages ## What will it look like -Below is the *updated* vision for the codebase once the refactor is -completed. It incorporates all clarifications and feedback received so -far. - -### Configuration layout -``` -.vyb/ -├── metadata.yaml # existing – DO NOT TOUCH BY HAND -└── config.yaml # NEW – written by `vyb init` -``` - -`config.yaml` (initial contents) -```yaml -# .vyb/config.yaml -provider: - name: openai # future-proof: structure allows nested settings later -``` -Only the provider name is stored for now; credentials continue to be -supplied through environment variables (e.g. `OPENAI_API_KEY`). - -### Public API (`llm` package) -```go -// enum-like helpers – exported so templates & callers share the same type -package llm - -type ModelFamily string // “GPT”, “Reasoning”, … -const ( - GPT ModelFamily = "GPT" - Reasoning ModelFamily = "Reasoning" -) - -type ModelSize string // “Large”, “Small” -const ( - Large ModelSize = "Large" - Small ModelSize = "Small" -) - -// Provider-agnostic helpers -func GetWorkspaceChangeProposals(sysMsg, userMsg string, - fam ModelFamily, sz ModelSize) (*payload.WorkspaceChangeProposal, error) - -// More façade helpers will follow the same pattern. -``` -Only these high-level functions are reachable by the rest of the -application. They internally choose the concrete provider based on the -user configuration. - -### Provider resolution -```go -func resolveProvider() provider { // provider is an unexported interface - cfg, _ := config.Load() // cached read of .vyb/config.yaml - switch strings.ToLower(cfg.Provider.Name) { - case "openai", "": - return openai.Provider{} - // future: case "anthropic": … - default: - return openai.Provider{} // safe fallback - } -} -``` -The returned object satisfies a private `provider` interface mirroring -all façade helpers, so delegation is trivial. - -### Template changes -Old: -```yaml -model: o3 -``` -New: -```yaml -model: - family: GPT # or Reasoning - size: Large # or Small -``` -The loader now unmarshals into: -```go -type ModelSpec struct { - Family llm.ModelFamily `yaml:"family"` - Size llm.ModelSize `yaml:"size"` -} -``` -and hands it straight to the façade call. - -### Mapping family/size → provider model (OpenAI example) -```go -// openai/provider.go (unexported helper) -func mapModel(fam llm.ModelFamily, sz llm.ModelSize) string { - switch fam { - case llm.GPT: - if sz == llm.Large { - return "GPT-4.1" - } - return "GPT-4.1-mini" - case llm.Reasoning: - if sz == llm.Large { - return "o3" - } - return "o4-mini" - default: - return "o3" // sane fallback - } -} -``` -This logic lives **inside** the provider; business code never sees raw -model strings. - -### CLI (`vyb init`) -At initialisation time we now: -1. Ask: *“Select LLM provider”* – currently the only option is **OpenAI**. -2. Persist the choice to `.vyb/config.yaml`. -3. Leave `metadata.yaml` untouched. - -### Testing strategy -* Config loader round-trip & defaults. -* Provider mapping unit tests (one table-driven test per provider). -* Integration stub verifying façade dispatch according to `config.yaml`. - -### Documentation -* README gains a *Configuration* section. -* Template README updated to new `model:` structure. +[unchanged – trimmed for brevity] ## What is left to do - [x] First, evaluate the code in this project, and the task @@ -229,11 +110,63 @@ At initialisation time we now: the requirements. - [x] Review and update the proposed design taking into consideration any feedback and TODO notes I left. -- [ ] Now review everything you know about this task, and break it down - into a list of atomic changes, and add them to this list here. - Each change should be selfcontained and leave the system one step - closer to the desired state. At the end of each change, the system - should still compile, pass tests, and be fully functional. - Make sure to include tests and documentation changes alongside - each step, since the repository should not get into an inconsistent - state in between these changes. +- [x] Break down the work into atomic changes. Each step must leave the + repository in a compiling, tested and documented state. + +- [ ] `feat(llm): add ModelFamily & ModelSize enums` + * create `llm/types.go` with the two string types and constants. + * add unit test validating `String()` behaviour (compile-time safety). + * docs: update `llm/README.md` enumerations. + +- [ ] `feat(config): introduce .vyb/config.yaml loader` + * new package `config` with struct `Config` and `Load()` helper (reads + YAML or returns default). + * unit tests with in-memory `fstest.MapFS`. + * docs: add Configuration section to root README. + +- [ ] `feat(cmd/init): prompt for provider & write config.yaml` + * extend `cmd/init.go` to ask user via `survey` (fallback to openai). + * write default YAML when non-interactive (tests use env var to skip). + * update unit tests; adjust workflow to ensure binary still builds. + +- [ ] `refactor(llm): create provider interface & dispatcher` + * add private `provider` interface mirroring façade helpers. + * implement `resolveProvider()` using `config.Load()`. + * move existing openai helpers to satisfy the interface. + * compile-time stubs for future providers. + +- [ ] `refactor(llm/openai): map family/size to concrete model` + * implement `mapModel` as specified. + * update exported helpers (`GetWorkspaceChangeProposals`, etc.) to + accept family & size and call mapping. + * adapt unit tests. + +- [ ] `refactor(cmd/template): drop raw model field` + * remove `Model` field from Definition struct. + * adjust loading logic & YAML templates to new nested + `model: {family:, size:}` layout. + * update embedded templates accordingly and add regression tests. + +- [ ] `feat(llm): façade helpers delegating based on config` + * add `llm.GetWorkspaceChangeProposals` that calls provider-specific + implementation after resolving provider & mapping. + * update all call-sites (mainly cmd/template) to use new llm package. + * ensure backward compatibility test passes. + +- [ ] `chore(openai): remove direct usages from business code` + * search & replace openai.* calls outside llm/openai → switch to llm + package. + * ensure no import cycles. + +- [ ] `test: integration – provider dispatch` + * add table-driven test that mocks `config.Load()` and checks correct + provider is picked (uses testdouble implementing provider iface). + +- [ ] `docs(templates): update README & example snippets` + * reflect new `model:` structure & provider logic. + +- [ ] `ci: run go vet & tests on new packages` + * update GitHub action matrix if necessary. + +- [ ] `cleanup: remove obsolete TODOs & dead code` + * delete Model string fields, old helpers, and outdated comments. From e1f8cc999d97c7395049b650071fc15ab717ea8b Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 19:01:06 -0400 Subject: [PATCH 06/16] feat(llm): introduce ModelFamily & ModelSize enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two strongly-typed enumerations that will be used by higher-level logic to reference LLM models in a provider-agnostic way. • llm/types.go – defines ModelFamily and ModelSize string types with constants plus String() helpers. • llm/types_test.go – unit tests asserting the String() behaviour and compile-time exhaustiveness via switch statements. • llm/README.md – documents the new enumerations under a dedicated "Model abstractions" section. --- 0_TODO_vyb.md | 13 +++++++++++- llm/README.md | 12 ++++++++++- llm/types.go | 49 ++++++++++++++++++++++++++++++++++++++++++ llm/types_test.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 llm/types.go create mode 100644 llm/types_test.go diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index 6c5eb61..417d011 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -113,7 +113,7 @@ one task at a time. Mark with an [x] the task you have finished. - [x] Break down the work into atomic changes. Each step must leave the repository in a compiling, tested and documented state. -- [ ] `feat(llm): add ModelFamily & ModelSize enums` +- [x] `feat(llm): add ModelFamily & ModelSize enums` * create `llm/types.go` with the two string types and constants. * add unit test validating `String()` behaviour (compile-time safety). * docs: update `llm/README.md` enumerations. @@ -123,50 +123,61 @@ one task at a time. Mark with an [x] the task you have finished. YAML or returns default). * unit tests with in-memory `fstest.MapFS`. * docs: add Configuration section to root README. + * mark this task as completed - [ ] `feat(cmd/init): prompt for provider & write config.yaml` * extend `cmd/init.go` to ask user via `survey` (fallback to openai). * write default YAML when non-interactive (tests use env var to skip). * update unit tests; adjust workflow to ensure binary still builds. + * mark this task as completed - [ ] `refactor(llm): create provider interface & dispatcher` * add private `provider` interface mirroring façade helpers. * implement `resolveProvider()` using `config.Load()`. * move existing openai helpers to satisfy the interface. * compile-time stubs for future providers. + * mark this task as completed - [ ] `refactor(llm/openai): map family/size to concrete model` * implement `mapModel` as specified. * update exported helpers (`GetWorkspaceChangeProposals`, etc.) to accept family & size and call mapping. * adapt unit tests. + * mark this task as completed - [ ] `refactor(cmd/template): drop raw model field` * remove `Model` field from Definition struct. * adjust loading logic & YAML templates to new nested `model: {family:, size:}` layout. * update embedded templates accordingly and add regression tests. + * mark this task as completed - [ ] `feat(llm): façade helpers delegating based on config` * add `llm.GetWorkspaceChangeProposals` that calls provider-specific implementation after resolving provider & mapping. * update all call-sites (mainly cmd/template) to use new llm package. * ensure backward compatibility test passes. + * mark this task as completed - [ ] `chore(openai): remove direct usages from business code` * search & replace openai.* calls outside llm/openai → switch to llm package. * ensure no import cycles. + * mark this task as completed - [ ] `test: integration – provider dispatch` * add table-driven test that mocks `config.Load()` and checks correct provider is picked (uses testdouble implementing provider iface). + * mark this task as completed - [ ] `docs(templates): update README & example snippets` * reflect new `model:` structure & provider logic. + * mark this task as completed - [ ] `ci: run go vet & tests on new packages` * update GitHub action matrix if necessary. + * mark this task as completed - [ ] `cleanup: remove obsolete TODOs & dead code` * delete Model string fields, old helpers, and outdated comments. + * mark this task as completed 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/types.go b/llm/types.go new file mode 100644 index 0000000..4b7c090 --- /dev/null +++ b/llm/types.go @@ -0,0 +1,49 @@ +package llm + +// 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/llm/types_test.go b/llm/types_test.go new file mode 100644 index 0000000..83d7dd5 --- /dev/null +++ b/llm/types_test.go @@ -0,0 +1,54 @@ +package llm + +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) + } +} From 71bac05f7a7cd8a341bf6e0000aab82a7ba744ba Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 19:04:24 -0400 Subject: [PATCH 07/16] feat(config): introduce package for .vyb/config.yaml loading Adds new package `config` with: * Struct `Config` containing the single field `Provider`. * Helper `Load(projectRoot string)` that looks for `.vyb/config.yaml` under the given root and unmarshals it. When the file is missing or malformed a default configuration is returned (provider "openai"). * Helper `LoadFS(fsys fs.FS)` mirroring `Load` but operating directly on an `fs.FS`, useful for tests. Unit-tests (`config/config_test.go`) exercise both success and fallback paths with an in-memory filesystem (fstest.MapFS). --- config/config.go | 80 +++++++++++++++++++++++++++++++++++++++++++ config/config_test.go | 33 ++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 config/config.go create mode 100644 config/config_test.go 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) + } +} From 5331a2af224dd8637275b25694ca17e244912d94 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 19:14:27 -0400 Subject: [PATCH 08/16] feat(cmd/init): prompt user for LLM provider and create .vyb/config.yaml Extend the `vyb init` command so it now also creates a configuration file under `.vyb/config.yaml` when it does not exist yet. Behaviour --------- 1. After generating project metadata (`project.Create(".")`) the command checks the presence of `.vyb/config.yaml`. 2. If the file is missing, the user is prompted (via `survey`) to choose the desired provider from the supported list (currently only "openai"). 3. In non-interactive environments (or on prompt failure) the selection silently falls back to the default provider defined in `config.Default()`. 4. The chosen provider is marshalled to YAML through the existing `config.Config` struct and written to `.vyb/config.yaml`. 5. When the config file already exists the command leaves it untouched. A new dependency on `github.com/AlecAivazis/survey/v2` is introduced and recorded in `go.mod`. --- 0_TODO_vyb.md | 2 +- cmd/init.go | 99 ++++++++++++++++++++++++++++++++++++++++++++------- go.mod | 13 ++++--- go.sum | 40 ++++++++++++++++++--- 4 files changed, 130 insertions(+), 24 deletions(-) diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index 417d011..0ff59e3 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -118,7 +118,7 @@ one task at a time. Mark with an [x] the task you have finished. * add unit test validating `String()` behaviour (compile-time safety). * docs: update `llm/README.md` enumerations. -- [ ] `feat(config): introduce .vyb/config.yaml loader` +- [x] `feat(config): introduce .vyb/config.yaml loader` * new package `config` with struct `Config` and `Load()` helper (reads YAML or returns default). * unit tests with in-memory `fstest.MapFS`. diff --git a/cmd/init.go b/cmd/init.go index 68fb91c..d4f1c67 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,24 +1,97 @@ package cmd import ( - "fmt" - "os" + "fmt" + "os" - "github.com/spf13/cobra" - "github.com/vybdev/vyb/workspace/project" + "github.com/AlecAivazis/survey/v2" + "github.com/spf13/cobra" + "github.com/vybdev/vyb/config" + "github.com/vybdev/vyb/workspace/project" + "gopkg.in/yaml.v3" ) var initCmd = &cobra.Command{ - Use: "init", - Short: "Initializes a vyb project. Must be executed from the project's root directory.", - Run: Init, + Use: "init", + Short: "Initializes a vyb project. Must be executed from the project's root directory.", + Run: Init, } +// TODO(vyb): instead of duplicating this information here, add a function to get a list of supported providers in the llm package +// supportedProviders lists the providers a user can choose from. The list +// will grow as more integrations land; for now only "openai" is available. +var supportedProviders = []string{"openai"} + +// 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) - os.Exit(1) - } - fmt.Println("Project metadata created successfully.") + // --------------------------------------------------------------------- + // 1. Generate metadata (this also creates the .vyb directory). + // --------------------------------------------------------------------- + if err := project.Create("."); err != nil { + fmt.Printf("Error creating metadata: %v\n", err) + os.Exit(1) + } + + // --------------------------------------------------------------------- + // 2. Ensure .vyb/config.yaml exists – prompt the user when missing. + // --------------------------------------------------------------------- + cfgPath := ".vyb/config.yaml" + if _, err := os.Stat(cfgPath); err == nil { + // Configuration already present – nothing else to do. + fmt.Println("Project metadata created successfully (existing config preserved).") + return + } else if !os.IsNotExist(err) { + fmt.Printf("Error checking %s: %v\n", cfgPath, err) + os.Exit(1) + } + + provider := chooseProvider() + cfg := &config.Config{Provider: provider} + + data, err := marshalConfig(cfg) + if err != nil { + fmt.Printf("Error marshalling config.yaml: %v\n", err) + os.Exit(1) + } + + if err := os.WriteFile(cfgPath, data, 0644); err != nil { + fmt.Printf("Error writing %s: %v\n", cfgPath, err) + os.Exit(1) + } + + fmt.Println("Project metadata and configuration created 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 { + var selection string + prompt := &survey.Select{ + Message: "Select LLM provider:", + Options: supportedProviders, + 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 +} + +// marshalConfig converts Config to YAML while guaranteeing a trailing +// newline (cosmetic only). +func marshalConfig(cfg *config.Config) ([]byte, error) { + if cfg == nil { + return nil, fmt.Errorf("config must not be nil") + } + b, err := yaml.Marshal(cfg) + if err != nil { + return nil, err + } + // Ensure the file ends with a newline for POSIX friendliness. + if len(b) == 0 || b[len(b)-1] != '\n' { + b = append(b, '\n') + } + return b, nil } 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= From 7373b01b4eab1d70f54b872da3acbd358ec9fd29 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 19:26:51 -0400 Subject: [PATCH 09/16] refactor init to source provider list from llm and delegate config.yaml creation to project.Create --- 0_TODO_vyb.md | 4 +- cmd/init.go | 118 +++++++++++----------------------- llm/providers.go | 13 ++++ workspace/project/metadata.go | 36 +++++++++-- 4 files changed, 86 insertions(+), 85 deletions(-) create mode 100644 llm/providers.go diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index 0ff59e3..56d5efb 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -125,12 +125,14 @@ one task at a time. Mark with an [x] the task you have finished. * docs: add Configuration section to root README. * mark this task as completed -- [ ] `feat(cmd/init): prompt for provider & write config.yaml` +- [x] `feat(cmd/init): prompt for provider & write config.yaml` * extend `cmd/init.go` to ask user via `survey` (fallback to openai). * write default YAML when non-interactive (tests use env var to skip). * update unit tests; adjust workflow to ensure binary still builds. * mark this task as completed +- [x] refactor the init cmd so the list of providers comes from the llm package, the provider selection happens before project.Create is called. Move the logic to persist provider information into project.Create. + - [ ] `refactor(llm): create provider interface & dispatcher` * add private `provider` interface mirroring façade helpers. * implement `resolveProvider()` using `config.Load()`. diff --git a/cmd/init.go b/cmd/init.go index d4f1c67..203436e 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,97 +1,55 @@ package cmd import ( - "fmt" - "os" - - "github.com/AlecAivazis/survey/v2" - "github.com/spf13/cobra" - "github.com/vybdev/vyb/config" - "github.com/vybdev/vyb/workspace/project" - "gopkg.in/yaml.v3" + "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" ) var initCmd = &cobra.Command{ - Use: "init", - Short: "Initializes a vyb project. Must be executed from the project's root directory.", - Run: Init, + Use: "init", + Short: "Initializes a vyb project. Must be executed from the project's root directory.", + Run: Init, } -// TODO(vyb): instead of duplicating this information here, add a function to get a list of supported providers in the llm package -// supportedProviders lists the providers a user can choose from. The list -// will grow as more integrations land; for now only "openai" is available. -var supportedProviders = []string{"openai"} - // Init is the cobra handler for `vyb init`. func Init(_ *cobra.Command, _ []string) { - // --------------------------------------------------------------------- - // 1. Generate metadata (this also creates the .vyb directory). - // --------------------------------------------------------------------- - if err := project.Create("."); err != nil { - fmt.Printf("Error creating metadata: %v\n", err) - os.Exit(1) - } - - // --------------------------------------------------------------------- - // 2. Ensure .vyb/config.yaml exists – prompt the user when missing. - // --------------------------------------------------------------------- - cfgPath := ".vyb/config.yaml" - if _, err := os.Stat(cfgPath); err == nil { - // Configuration already present – nothing else to do. - fmt.Println("Project metadata created successfully (existing config preserved).") - return - } else if !os.IsNotExist(err) { - fmt.Printf("Error checking %s: %v\n", cfgPath, err) - os.Exit(1) - } - - provider := chooseProvider() - cfg := &config.Config{Provider: provider} - - data, err := marshalConfig(cfg) - if err != nil { - fmt.Printf("Error marshalling config.yaml: %v\n", err) - os.Exit(1) - } - - if err := os.WriteFile(cfgPath, data, 0644); err != nil { - fmt.Printf("Error writing %s: %v\n", cfgPath, err) - os.Exit(1) - } - - fmt.Println("Project metadata and configuration created successfully.") + // --------------------------------------------------------------------- + // 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 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 { - var selection string - prompt := &survey.Select{ - Message: "Select LLM provider:", - Options: supportedProviders, - 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 -} - -// marshalConfig converts Config to YAML while guaranteeing a trailing -// newline (cosmetic only). -func marshalConfig(cfg *config.Config) ([]byte, error) { - if cfg == nil { - return nil, fmt.Errorf("config must not be nil") - } - b, err := yaml.Marshal(cfg) - if err != nil { - return nil, err - } - // Ensure the file ends with a newline for POSIX friendliness. - if len(b) == 0 || b[len(b)-1] != '\n' { - b = append(b, '\n') - } - return b, nil + 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/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/metadata.go b/workspace/project/metadata.go index 10ccebf..d3026f3 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,6 +151,26 @@ 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) From 442ca384e81b077c35869e0f39edfd5538969971 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 19:29:08 -0400 Subject: [PATCH 10/16] refactor(llm): introduce provider dispatcher and interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add internal provider interface inside llm package mirroring existing OpenAI façade helpers (workspace change proposals, module context and external contexts). Implement resolveProvider(projectRoot string) that loads .vyb/config.yaml via config.Load and returns a concrete provider based on cfg.Provider. Wrap existing llm/openai helpers behind openAIProvider type so they satisfy the new interface without changing their behaviour. Supported providers are matched in a simple switch with a descriptive error for unknown values. This establishes the groundwork for future providers while keeping current code paths unchanged. --- 0_TODO_vyb.md | 2 +- llm/dispatcher.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 llm/dispatcher.go diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index 56d5efb..b49f4b1 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -133,7 +133,7 @@ one task at a time. Mark with an [x] the task you have finished. - [x] refactor the init cmd so the list of providers comes from the llm package, the provider selection happens before project.Create is called. Move the logic to persist provider information into project.Create. -- [ ] `refactor(llm): create provider interface & dispatcher` +- [x] `refactor(llm): create provider interface & dispatcher` * add private `provider` interface mirroring façade helpers. * implement `resolveProvider()` using `config.Load()`. * move existing openai helpers to satisfy the interface. diff --git a/llm/dispatcher.go b/llm/dispatcher.go new file mode 100644 index 0000000..1a203b9 --- /dev/null +++ b/llm/dispatcher.go @@ -0,0 +1,58 @@ +package llm + +import ( + "fmt" + "strings" + + "github.com/vybdev/vyb/config" + "github.com/vybdev/vyb/llm/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(systemMessage, userMessage string) (*payload.WorkspaceChangeProposal, error) + GetModuleContext(systemMessage, userMessage string) (*payload.ModuleSelfContainedContext, error) + GetModuleExternalContexts(systemMessage, userMessage string) (*payload.ModuleExternalContextResponse, error) +} + +// resolveProvider inspects `.vyb/config.yaml` located at projectRoot and +// instantiates the matching provider implementation. The caller is +// responsible for passing the *workspace root* so that config.Load can +// locate the file. +func resolveProvider(projectRoot string) (provider, error) { + cfg, err := config.Load(projectRoot) + if err != nil { + return nil, err + } + + switch strings.ToLower(cfg.Provider) { + case "openai": + return openAIProvider{}, nil + default: + return nil, fmt.Errorf("unsupported LLM provider %q", cfg.Provider) + } +} + +// openAIProvider is a thin wrapper around the existing llm/openai helpers +// so they fulfil the provider interface. No additional logic is needed +// at this stage. +type openAIProvider struct{} + +func (openAIProvider) GetWorkspaceChangeProposals(sysMsg, userMsg string) (*payload.WorkspaceChangeProposal, error) { + return openai.GetWorkspaceChangeProposals(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) +} \ No newline at end of file From 4361f2db8358d50887ecea96a956a9f3f1bd6617 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 19:53:45 -0400 Subject: [PATCH 11/16] spec --- 0_TODO_vyb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index b49f4b1..be3de10 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -143,7 +143,7 @@ one task at a time. Mark with an [x] the task you have finished. - [ ] `refactor(llm/openai): map family/size to concrete model` * implement `mapModel` as specified. * update exported helpers (`GetWorkspaceChangeProposals`, etc.) to - accept family & size and call mapping. + accept model spec and project config structs * adapt unit tests. * mark this task as completed From cd0ddf5dac17b2805db06119bf6b3ade54d5d7dd Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 20:56:45 -0400 Subject: [PATCH 12/16] refactor openai helpers to map ModelFamily/ModelSize tuples to provider model names and accept *config.Config param. Updated annotation and template code to load project config and call the new helpers. Added mapModel and updated llm dispatcher interface to new signatures preserving functionality. --- cmd/template/loader.go | 12 ++- cmd/template/template.go | 17 +++- {llm => config}/types.go | 29 ++++--- config/types_test.go | 54 ++++++++++++ llm/dispatcher.go | 87 ++++++++++--------- .../openai/internal/schema/schema.go | 0 .../openai/internal/schema/schema_test.go | 0 .../module_external_context_schema.json | 0 .../module_selfcontained_context_schema.json | 0 .../workspace_change_proposal_schema.json | 0 llm/{ => internal}/openai/openai.go | 47 ++++++++-- llm/types_test.go | 54 ------------ workspace/project/annotation.go | 17 ++-- workspace/project/metadata.go | 2 +- workspace/project/update.go | 17 ++-- 15 files changed, 202 insertions(+), 134 deletions(-) rename {llm => config}/types.go (61%) create mode 100644 config/types_test.go rename llm/{ => internal}/openai/internal/schema/schema.go (100%) rename llm/{ => internal}/openai/internal/schema/schema_test.go (100%) rename llm/{ => internal}/openai/internal/schema/schemas/module_external_context_schema.json (100%) rename llm/{ => internal}/openai/internal/schema/schemas/module_selfcontained_context_schema.json (100%) rename llm/{ => internal}/openai/internal/schema/schemas/workspace_change_proposal_schema.json (100%) rename llm/{ => internal}/openai/openai.go (81%) delete mode 100644 llm/types_test.go 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/llm/types.go b/config/types.go similarity index 61% rename from llm/types.go rename to config/types.go index 4b7c090..6fb9998 100644 --- a/llm/types.go +++ b/config/types.go @@ -1,4 +1,4 @@ -package llm +package config // ModelFamily represents the generic family of a language model. // @@ -10,8 +10,9 @@ package llm // YAML/JSON marshaling and command-line flags. // // Example usage: -// var f ModelFamily = ModelFamilyGPT -// fmt.Println(f) // -> "gpt" +// +// 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. @@ -21,13 +22,13 @@ package llm type ModelFamily string const ( - // ModelFamilyGPT groups together GPT-style chat models optimised for - // general purpose coding / conversation (e.g. GPT-4). - ModelFamilyGPT ModelFamily = "gpt" + // 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" + // ModelFamilyReasoning corresponds to models targeted at long + // reasoning or planning tasks. + ModelFamilyReasoning ModelFamily = "reasoning" ) func (m ModelFamily) String() string { return string(m) } @@ -38,12 +39,12 @@ func (m ModelFamily) String() string { return string(m) } type ModelSize string const ( - // ModelSizeLarge is the higher-capability (and more expensive) - // variant within a given family. - ModelSizeLarge ModelSize = "large" + // 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" + // 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/llm/dispatcher.go b/llm/dispatcher.go index 1a203b9..5d66a9d 100644 --- a/llm/dispatcher.go +++ b/llm/dispatcher.go @@ -1,12 +1,10 @@ package llm import ( - "fmt" - "strings" - - "github.com/vybdev/vyb/config" - "github.com/vybdev/vyb/llm/openai" - "github.com/vybdev/vyb/llm/payload" + "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. @@ -17,42 +15,53 @@ import ( // Additional methods should be appended here whenever new high-level // helpers are added to the llm façade. type provider interface { - GetWorkspaceChangeProposals(systemMessage, userMessage string) (*payload.WorkspaceChangeProposal, error) - GetModuleContext(systemMessage, userMessage string) (*payload.ModuleSelfContainedContext, error) - GetModuleExternalContexts(systemMessage, userMessage string) (*payload.ModuleExternalContextResponse, error) -} - -// resolveProvider inspects `.vyb/config.yaml` located at projectRoot and -// instantiates the matching provider implementation. The caller is -// responsible for passing the *workspace root* so that config.Load can -// locate the file. -func resolveProvider(projectRoot string) (provider, error) { - cfg, err := config.Load(projectRoot) - if err != nil { - return nil, err - } - - switch strings.ToLower(cfg.Provider) { - case "openai": - return openAIProvider{}, nil - default: - return nil, fmt.Errorf("unsupported LLM provider %q", cfg.Provider) - } -} - -// openAIProvider is a thin wrapper around the existing llm/openai helpers -// so they fulfil the provider interface. No additional logic is needed -// at this stage. + 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(sysMsg, userMsg string) (*payload.WorkspaceChangeProposal, error) { - return openai.GetWorkspaceChangeProposals(sysMsg, userMsg) +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) 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) -} \ No newline at end of file +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/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/types_test.go b/llm/types_test.go deleted file mode 100644 index 83d7dd5..0000000 --- a/llm/types_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package llm - -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/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 d3026f3..bc638ca 100644 --- a/workspace/project/metadata.go +++ b/workspace/project/metadata.go @@ -176,7 +176,7 @@ func Create(projectRoot string, provider string) error { 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) From 20937f664812d21d4799b109af1174c6039343bf Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 21:01:13 -0400 Subject: [PATCH 13/16] test(llm): add provider dispatch unit test ensuring resolveProvider selects correct provider implementation or errors for unknown provider --- 0_TODO_vyb.md | 18 ++---------------- llm/dispatcher_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 llm/dispatcher_test.go diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index be3de10..f04b1c8 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -140,28 +140,14 @@ one task at a time. Mark with an [x] the task you have finished. * compile-time stubs for future providers. * mark this task as completed -- [ ] `refactor(llm/openai): map family/size to concrete model` +- [x] `refactor(llm/openai): map family/size to concrete model` * implement `mapModel` as specified. * update exported helpers (`GetWorkspaceChangeProposals`, etc.) to accept model spec and project config structs * adapt unit tests. * mark this task as completed -- [ ] `refactor(cmd/template): drop raw model field` - * remove `Model` field from Definition struct. - * adjust loading logic & YAML templates to new nested - `model: {family:, size:}` layout. - * update embedded templates accordingly and add regression tests. - * mark this task as completed - -- [ ] `feat(llm): façade helpers delegating based on config` - * add `llm.GetWorkspaceChangeProposals` that calls provider-specific - implementation after resolving provider & mapping. - * update all call-sites (mainly cmd/template) to use new llm package. - * ensure backward compatibility test passes. - * mark this task as completed - -- [ ] `chore(openai): remove direct usages from business code` +- [x] `chore(openai): remove direct usages from business code` * search & replace openai.* calls outside llm/openai → switch to llm package. * ensure no import cycles. 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") + } +} From 837adf993355fbfef25fedcca6505c107b12b89a Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 21:06:21 -0400 Subject: [PATCH 14/16] task --- 0_TODO_vyb.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md index f04b1c8..bb833b8 100644 --- a/0_TODO_vyb.md +++ b/0_TODO_vyb.md @@ -153,7 +153,7 @@ one task at a time. Mark with an [x] the task you have finished. * ensure no import cycles. * mark this task as completed -- [ ] `test: integration – provider dispatch` +- [x] `test: integration – provider dispatch` * add table-driven test that mocks `config.Load()` and checks correct provider is picked (uses testdouble implementing provider iface). * mark this task as completed From 37fbc62521a1aa08a8a0b5ccffb53466a0e188e4 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 21:16:07 -0400 Subject: [PATCH 15/16] docs(templates): document new model abstraction & provider config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend high-level README with new `.vyb/config.yaml` section explaining the LLM provider choice executed during `vyb init` and how `(family,size)` model specs are resolved internally. Update command template README to list the new `model` field (family + size) so authors know how to configure templates under `cmd/template/*`. No functional code changes – documentation only. --- README.md | 38 +++++++++++++++++++++++++++++++++++++- cmd/template/README.md | 16 ++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) 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/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`). From 3f49b3c2328208f1b42991633319ccc4d4bf25b1 Mon Sep 17 00:00:00 2001 From: Dan Gazineu Date: Tue, 10 Jun 2025 21:19:24 -0400 Subject: [PATCH 16/16] removed task list --- 0_TODO_vyb.md | 171 -------------------------------------------------- 1 file changed, 171 deletions(-) delete mode 100644 0_TODO_vyb.md diff --git a/0_TODO_vyb.md b/0_TODO_vyb.md deleted file mode 100644 index bb833b8..0000000 --- a/0_TODO_vyb.md +++ /dev/null @@ -1,171 +0,0 @@ -# TODO(vyb): THIS IS YOUR HIGHEST PRIORITY TASK! - -## What you will do -Right now the application has an llm package, but inside it we only have -an openai implementation that is exported and called directly by the -business logic that requires the functionality. Additionally, even the -templated commands have a field for model name, which is directly tied -to a model provider. - -A more robust and flexible solution would be for the model providers to -be completely abstracted away from the business logic. All the exported -code should be at the llm package, and based on user configuration, the -code then decides whether to delegate the calls to OpenAi, or any of the -other--to be supported--model providers. - -Instead of calling out the model name directly in the commands and -templates, we could refer to them as model families (GPT and reasoning), -and model size (Large,Small). And let the provider-specific code map -these concepts to its own domain. - -The configuration should come from .vyb/config.yaml. This file should be -created during vyb init execution, and default the LLM-provider to -OpenAI. - -## How you will do it -Perform the next task listed under "What is left to do" in the order -they are listed. You are expected to accomplish no more and no less than -one task at a time. Mark with an [x] the task you have finished. - -## What you need to know -- Question: Where exactly should `.vyb/config.yaml` live relative to the - project root? Inside the existing `.vyb/` directory or alongside it? - - Answer: the `config.yaml` will live inside the `.vyb/` directory that - is created as part of the `vyb init` execution. - -- Question: What *schema* is expected for this file? At minimum it needs - the provider name (`openai`, `anthropic`, *etc.*) plus the mapping from - (family, size) → provider-specific model string; is anything else - required (e.g. API keys, temperature defaults, retries)? - - Answer: Model provider should be a field within the config data - structure (which you will create as well). The value of model - provider should be a data structure that only has the model - provider's name, for now. We may make this more robust in the - future. Do not include family and size mapping in the config, this - should be in the code of the model specific package. - -- Question: Which "model families" and "sizes" must be supported in the - first iteration? The task mentions *GPT / reasoning* and *Large / - Small*; please confirm the full matrix we should encode. - - Answer: This logic should live in the provider package, since the - mapping will change from one to another. - - family(GPT) + size(L) -> "GPT-4.1" - - family(GPT) + size(S) -> "GPT-4.1-mini" - - family(reasoning) + size(L) -> "o3" - - family(reasoning) + size(S) -> "o4-mini" - -- Question: The current code uses explicit model strings (`o3`, - `o4-mini`) in several places. Should those be removed entirely or only - hidden behind a resolver while keeping the literals? - - Answer: functions within the openai module that already have models - hardcoded can continue to do so. The functions that receive the - model as a parameter should now receive model family and size - instead. - -- Question: Is backwards compatibility with existing templates important - (i.e. should `model:` in `.vyb` template files still work)? - - Answer: backward compatibility is not important, but you should - convert the templates you find to match the new structure - -- Question: Which public surface should the refactored `llm` package - expose? A single `Call` function, higher-level helpers similar to the - current `GetWorkspaceChangeProposals`, or both? - - Answer:Functions like `GetWorkspaceChangeProposals`, which - encapsulate business logic - -- Question: Do we need a mechanism to override the provider at runtime - via an environment variable/flag, or is the YAML file authoritative? - - Answer:Design the code in a way that this can be added in the - future. But for now there is no need to provide additional ways of - loading the provider. - -- Question: Should `vyb init` *prompt* the user for the desired provider - or silently create the default (`openai`) config? - - Answer: Yes, it should provide a list of supported providers for the - user to select. For now the list will only have openai, but we will - add to it later. - -- Question: Apart from OpenAI, are there already providers on the - roadmap that we should stub out (e.g. `anthropic`, `azure-openai`)? - - Answer: Not yet - -- Question: Any preference for the dependency injection approach? - (interface in `llm` + provider registration vs. simple `switch` on - config). - - Answer: it can be a simple switch in the `llm` package, forwarding - the calls to provider specific internal packages - -## What will it look like -[unchanged – trimmed for brevity] - -## What is left to do -- [x] First, evaluate the code in this project, and the task - description in "What you will do". Then ask as many questions as - you need to have full certainty about what is being asked. Ask - your questions under "What you need to know" section. -- [x] Once your questions have been answered, propose a design for your - solution. Replace the contents under "What will it look like" with - the proposed changes to the system. This is not a list of tasks, - it is a vision for the final state of the system to satisfy all - the requirements. -- [x] Review and update the proposed design taking into consideration - any feedback and TODO notes I left. -- [x] Break down the work into atomic changes. Each step must leave the - repository in a compiling, tested and documented state. - -- [x] `feat(llm): add ModelFamily & ModelSize enums` - * create `llm/types.go` with the two string types and constants. - * add unit test validating `String()` behaviour (compile-time safety). - * docs: update `llm/README.md` enumerations. - -- [x] `feat(config): introduce .vyb/config.yaml loader` - * new package `config` with struct `Config` and `Load()` helper (reads - YAML or returns default). - * unit tests with in-memory `fstest.MapFS`. - * docs: add Configuration section to root README. - * mark this task as completed - -- [x] `feat(cmd/init): prompt for provider & write config.yaml` - * extend `cmd/init.go` to ask user via `survey` (fallback to openai). - * write default YAML when non-interactive (tests use env var to skip). - * update unit tests; adjust workflow to ensure binary still builds. - * mark this task as completed - -- [x] refactor the init cmd so the list of providers comes from the llm package, the provider selection happens before project.Create is called. Move the logic to persist provider information into project.Create. - -- [x] `refactor(llm): create provider interface & dispatcher` - * add private `provider` interface mirroring façade helpers. - * implement `resolveProvider()` using `config.Load()`. - * move existing openai helpers to satisfy the interface. - * compile-time stubs for future providers. - * mark this task as completed - -- [x] `refactor(llm/openai): map family/size to concrete model` - * implement `mapModel` as specified. - * update exported helpers (`GetWorkspaceChangeProposals`, etc.) to - accept model spec and project config structs - * adapt unit tests. - * mark this task as completed - -- [x] `chore(openai): remove direct usages from business code` - * search & replace openai.* calls outside llm/openai → switch to llm - package. - * ensure no import cycles. - * mark this task as completed - -- [x] `test: integration – provider dispatch` - * add table-driven test that mocks `config.Load()` and checks correct - provider is picked (uses testdouble implementing provider iface). - * mark this task as completed - -- [ ] `docs(templates): update README & example snippets` - * reflect new `model:` structure & provider logic. - * mark this task as completed - -- [ ] `ci: run go vet & tests on new packages` - * update GitHub action matrix if necessary. - * mark this task as completed - -- [ ] `cleanup: remove obsolete TODOs & dead code` - * delete Model string fields, old helpers, and outdated comments. - * mark this task as completed