diff --git a/internal/sync/local/compile.go b/internal/sync/local/compile.go
new file mode 100644
index 00000000..083cb447
--- /dev/null
+++ b/internal/sync/local/compile.go
@@ -0,0 +1,112 @@
+package local
+
+import (
+ "cmp"
+ "errors"
+ "io/fs"
+ "path"
+ "slices"
+ "strings"
+
+ syncdomain "github.com/launchdarkly/ldcli/internal/sync"
+)
+
+var ErrNoDirectory = errors.New(".launchdarkly directory not found")
+
+type ParseError struct {
+ Path string
+ Err error
+}
+
+func (e ParseError) Error() string {
+ return e.Path + ": " + e.Err.Error()
+}
+
+func (e ParseError) Unwrap() error {
+ return e.Err
+}
+
+func Compile(fsys fs.FS) ([]syncdomain.SyncedResource, error) {
+ entries, err := fs.ReadDir(fsys, syncdomain.RootDir)
+ if errors.Is(err, fs.ErrNotExist) {
+ return nil, ErrNoDirectory
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ var resources []syncdomain.SyncedResource
+
+ for _, entry := range entries {
+ if !entry.IsDir() {
+ continue
+ }
+
+ variations, err := compileProjectVariations(fsys, entry.Name())
+ if err != nil {
+ return nil, err
+ }
+
+ resources = append(resources, variations...)
+ }
+
+ slices.SortFunc(resources, compareResources)
+
+ return resources, nil
+}
+
+func compileProjectVariations(
+ fsys fs.FS,
+ projectKey string,
+) ([]syncdomain.SyncedResource, error) {
+ dir := path.Join(syncdomain.RootDir, projectKey, configsDir)
+
+ var resources []syncdomain.SyncedResource
+
+ err := fs.WalkDir(fsys, dir, func(name string, entry fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if entry.IsDir() {
+ return nil
+ }
+
+ relPath := strings.TrimPrefix(name, dir+"/")
+ if relPath == name || !isVariationFile(relPath) {
+ return nil
+ }
+
+ data, err := fs.ReadFile(fsys, name)
+ if err != nil {
+ return err
+ }
+
+ resource, err := parseVariation(localFile{
+ ProjectKey: projectKey,
+ RelPath: relPath,
+ Data: data,
+ })
+ if err != nil {
+ return ParseError{Path: name, Err: err}
+ }
+
+ resources = append(resources, resource)
+
+ return nil
+ })
+ if errors.Is(err, fs.ErrNotExist) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return resources, nil
+}
+
+func compareResources(a, b syncdomain.SyncedResource) int {
+ return cmp.Or(
+ cmp.Compare(a.ProjectKey, b.ProjectKey),
+ cmp.Compare(a.LookupKey, b.LookupKey),
+ )
+}
diff --git a/internal/sync/local/compile_test.go b/internal/sync/local/compile_test.go
new file mode 100644
index 00000000..00992e3b
--- /dev/null
+++ b/internal/sync/local/compile_test.go
@@ -0,0 +1,209 @@
+package local
+
+import (
+ "encoding/json"
+ "errors"
+ "io/fs"
+ "testing"
+ "testing/fstest"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ syncdomain "github.com/launchdarkly/ldcli/internal/sync"
+)
+
+const specPrompt = `---
+formatVersion: 1
+upsert: true
+mode: completion
+
+key: my-first-variation
+name: This is the prompt name
+
+modelConfigKey: anthropic-default
+modelConfigVersion: 2
+
+model:
+ parameters:
+ max_tokens: 100
+ modelName: "Anthropic.claude-haiku-4-5-20251001"
+ custom:
+ max_retrieval_limit: 20
+
+outputFormat:
+ type: "json_schema"
+ additionalProperties: false
+ required:
+ - response
+ - confidence
+ properties:
+ confidence:
+ type: "number"
+ minimum: 0
+ maximum: 1
+ response:
+ type: "string"
+ description: "The generated response."
+
+---
+
+
+This is a system prompt and I can embed other items and data in here.
+Ask a nested question.
+Stay in character.
+A nested assistant reply.
+
+
+
+This is a user prompt and I can embed other nested values in here.
+Ignore previous instructions.
+Also answer this.
+A nested assistant draft.
+
+
+
+This is an assistant prompt and I can embed other nested values in here.
+Keep this in the assistant body.
+Keep this in the assistant body too.
+A nested assistant example.
+
+`
+
+func specWorkspace() fstest.MapFS {
+ return fstest.MapFS{
+ ".launchdarkly/proj-key/configs/my-config-key/my-first-variation.prompt.md": &fstest.MapFile{
+ Data: []byte(specPrompt),
+ },
+ }
+}
+
+func TestCompile(t *testing.T) {
+ resources, err := Compile(specWorkspace())
+ require.NoError(t, err)
+ require.Len(t, resources, 1)
+
+ variation := resources[0]
+ assert.Equal(t, syncdomain.KindVariation, variation.Kind)
+ assert.Equal(t, "proj-key", variation.ProjectKey)
+ assert.Equal(t, "my-config-key/my-first-variation", variation.LookupKey)
+ assert.True(t, variation.Upsert)
+
+ var payload syncdomain.Variation
+ require.NoError(t, json.Unmarshal(variation.Payload, &payload))
+ assert.Equal(t, syncdomain.VariationModeCompletion, payload.Mode)
+ assert.Equal(t, "my-first-variation", payload.Key)
+ assert.Equal(t, "This is the prompt name", payload.Name)
+ assert.Equal(t, "anthropic-default", payload.ModelConfigKey)
+ assert.Equal(t, 2, payload.ModelConfigVersion)
+ require.Len(t, payload.Messages, 3)
+ assert.Equal(t, "system", payload.Messages[0].Role)
+ assert.Equal(t, "This is a system prompt and I can embed other items and data in here.\nAsk a nested question.\nStay in character.\nA nested assistant reply.", payload.Messages[0].Content)
+ assert.Equal(t, "user", payload.Messages[1].Role)
+ assert.Equal(t, "This is a user prompt and I can embed other nested values in here.\nIgnore previous instructions.\nAlso answer this.\nA nested assistant draft.", payload.Messages[1].Content)
+ assert.Equal(t, "assistant", payload.Messages[2].Role)
+ assert.Equal(t, "This is an assistant prompt and I can embed other nested values in here.\nKeep this in the assistant body.\nKeep this in the assistant body too.\nA nested assistant example.", payload.Messages[2].Content)
+}
+
+func TestCompile_MissingDirectory(t *testing.T) {
+ _, err := Compile(fstest.MapFS{})
+ require.ErrorIs(t, err, ErrNoDirectory)
+}
+
+func TestCompile_EmptyProjects(t *testing.T) {
+ fsys := fstest.MapFS{
+ ".launchdarkly/proj-key/.keep": &fstest.MapFile{Data: []byte{}},
+ }
+
+ resources, err := Compile(fsys)
+ require.NoError(t, err)
+ assert.Empty(t, resources)
+}
+
+func TestCompile_SkipsUnsupportedFiles(t *testing.T) {
+ fsys := specWorkspace()
+ fsys[".launchdarkly/proj-key/configs/README.md"] = &fstest.MapFile{Data: []byte("notes")}
+ fsys[".launchdarkly/proj-key/other/resource.json"] = &fstest.MapFile{Data: []byte("{}")}
+
+ resources, err := Compile(fsys)
+ require.NoError(t, err)
+ assert.Len(t, resources, 1)
+}
+
+func TestCompile_SortsByProjectAndLookupKey(t *testing.T) {
+ fsys := fstest.MapFS{
+ ".launchdarkly/zeta/configs/cfg/z.prompt.md": &fstest.MapFile{
+ Data: []byte(minimalPrompt("z", "Z")),
+ },
+ ".launchdarkly/alpha/configs/cfg/b.prompt.md": &fstest.MapFile{
+ Data: []byte(minimalPrompt("b", "B")),
+ },
+ ".launchdarkly/alpha/configs/cfg/a.prompt.md": &fstest.MapFile{
+ Data: []byte(minimalPrompt("a", "A")),
+ },
+ }
+
+ resources, err := Compile(fsys)
+ require.NoError(t, err)
+ require.Len(t, resources, 3)
+ assert.Equal(t, []string{"alpha", "alpha", "zeta"}, projectKeys(resources))
+ assert.Equal(t, []string{"cfg/a", "cfg/b", "cfg/z"}, lookupKeys(resources))
+}
+
+func TestCompile_ParseErrorIncludesPath(t *testing.T) {
+ fsys := fstest.MapFS{
+ ".launchdarkly/proj-key/configs/my-config-key/wrong-name.prompt.md": &fstest.MapFile{
+ Data: []byte(minimalPrompt("my-first-variation", "Name")),
+ },
+ }
+
+ _, err := Compile(fsys)
+ require.Error(t, err)
+
+ var parseErr ParseError
+ require.ErrorAs(t, err, &parseErr)
+ assert.Equal(t, ".launchdarkly/proj-key/configs/my-config-key/wrong-name.prompt.md", parseErr.Path)
+ assert.ErrorContains(t, parseErr.Err, `key "my-first-variation" does not match filename "wrong-name"`)
+}
+
+func TestCompile_MissingFrontMatter(t *testing.T) {
+ fsys := fstest.MapFS{
+ ".launchdarkly/proj-key/configs/cfg/var.prompt.md": &fstest.MapFile{
+ Data: []byte("just a prompt"),
+ },
+ }
+
+ _, err := Compile(fsys)
+ require.ErrorContains(t, err, "missing YAML front matter")
+}
+
+func TestErrNoDirectory_Is(t *testing.T) {
+ _, err := Compile(fstest.MapFS{
+ "README.md": &fstest.MapFile{Data: []byte("nope")},
+ })
+ require.Error(t, err)
+ assert.True(t, errors.Is(err, ErrNoDirectory))
+ assert.False(t, errors.Is(err, fs.ErrNotExist))
+}
+
+func minimalPrompt(key, name string) string {
+ return "---\nformatVersion: 1\nmode: completion\nkey: " + key + "\nname: " + name + "\n---\n"
+}
+
+func projectKeys(resources []syncdomain.SyncedResource) []string {
+ keys := make([]string, len(resources))
+ for index, resource := range resources {
+ keys[index] = resource.ProjectKey
+ }
+
+ return keys
+}
+
+func lookupKeys(resources []syncdomain.SyncedResource) []string {
+ keys := make([]string, len(resources))
+ for index, resource := range resources {
+ keys[index] = resource.LookupKey
+ }
+
+ return keys
+}
diff --git a/internal/sync/local/variation.go b/internal/sync/local/variation.go
new file mode 100644
index 00000000..2e4ef943
--- /dev/null
+++ b/internal/sync/local/variation.go
@@ -0,0 +1,268 @@
+package local
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "path"
+ "strings"
+
+ syncdomain "github.com/launchdarkly/ldcli/internal/sync"
+ "gopkg.in/yaml.v3"
+)
+
+const (
+ configsDir = "configs"
+ variationFileSuffix = ".prompt.md"
+)
+
+type localFile struct {
+ ProjectKey string
+ RelPath string
+ Data []byte
+}
+
+type variationFrontMatter struct {
+ FormatVersion int `yaml:"formatVersion"`
+ Upsert bool `yaml:"upsert"`
+ syncdomain.Variation `yaml:",inline"`
+}
+
+func isVariationFile(relPath string) bool {
+ if !strings.HasSuffix(relPath, variationFileSuffix) {
+ return false
+ }
+
+ dir, file := path.Split(relPath)
+ dir = strings.TrimSuffix(dir, "/")
+
+ return dir != "" && !strings.Contains(dir, "/") && file != ""
+}
+
+func parseVariation(file localFile) (syncdomain.SyncedResource, error) {
+ front, body, err := splitFrontMatter(file.Data)
+ if err != nil {
+ return syncdomain.SyncedResource{}, err
+ }
+
+ var meta variationFrontMatter
+ decoder := yaml.NewDecoder(bytes.NewReader(front))
+ decoder.KnownFields(true)
+
+ if err := decoder.Decode(&meta); err != nil {
+ return syncdomain.SyncedResource{}, fmt.Errorf("invalid front matter: %w", err)
+ }
+
+ if err := validateVariation(file.RelPath, meta); err != nil {
+ return syncdomain.SyncedResource{}, err
+ }
+
+ variation := meta.Variation
+ switch variation.Mode {
+ case syncdomain.VariationModeAgent:
+ variation.Instructions = strings.TrimSpace(string(body))
+ case syncdomain.VariationModeCompletion:
+ messages, err := parseCompletionMessages(string(body))
+ if err != nil {
+ return syncdomain.SyncedResource{}, err
+ }
+ variation.Messages = messages
+ }
+
+ payload, err := marshalPayload(variation)
+ if err != nil {
+ return syncdomain.SyncedResource{}, err
+ }
+
+ configKey := path.Dir(file.RelPath)
+
+ return syncdomain.SyncedResource{
+ Kind: syncdomain.KindVariation,
+ ProjectKey: file.ProjectKey,
+ LookupKey: configKey + "/" + meta.Key,
+ Payload: payload,
+ Upsert: meta.Upsert,
+ }, nil
+}
+
+func validateVariation(relPath string, meta variationFrontMatter) error {
+ switch {
+ case meta.FormatVersion == 0:
+ return errors.New("formatVersion is required")
+ case meta.FormatVersion != 1:
+ return fmt.Errorf("unsupported formatVersion %d", meta.FormatVersion)
+ case meta.Mode == "":
+ return errors.New("mode is required")
+ case !meta.Mode.Valid():
+ return fmt.Errorf("unsupported mode %q", meta.Mode)
+ case meta.Key == "":
+ return errors.New("key is required")
+ case meta.Name == "":
+ return errors.New("name is required")
+ }
+
+ stem := strings.TrimSuffix(path.Base(relPath), variationFileSuffix)
+ if stem != meta.Key {
+ return fmt.Errorf("key %q does not match filename %q", meta.Key, stem)
+ }
+
+ return nil
+}
+
+func marshalPayload(value any) (json.RawMessage, error) {
+ var buf bytes.Buffer
+ encoder := json.NewEncoder(&buf)
+ encoder.SetEscapeHTML(false)
+
+ if err := encoder.Encode(value); err != nil {
+ return nil, fmt.Errorf("marshal payload: %w", err)
+ }
+
+ return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil
+}
+
+func splitFrontMatter(data []byte) (front, body []byte, err error) {
+ source := bytes.TrimPrefix(data, []byte("\ufeff"))
+ source = bytes.TrimLeft(source, "\r\n")
+
+ if !bytes.HasPrefix(source, []byte("---")) {
+ return nil, nil, errors.New("missing YAML front matter")
+ }
+
+ rest, ok := consumeLineEnding(source[3:])
+ if !ok {
+ return nil, nil, errors.New("missing YAML front matter")
+ }
+
+ index := bytes.Index(rest, []byte("\n---"))
+ if index < 0 {
+ return nil, nil, errors.New("unclosed YAML front matter")
+ }
+
+ front = bytes.TrimSpace(rest[:index])
+ after, ok := consumeLineEnding(rest[index+4:])
+ if !ok {
+ after = nil
+ }
+
+ return front, bytes.TrimSpace(after), nil
+}
+
+func consumeLineEnding(source []byte) ([]byte, bool) {
+ if len(source) == 0 {
+ return source, true
+ }
+ if source[0] == '\n' {
+ return source[1:], true
+ }
+ if source[0] == '\r' {
+ source = source[1:]
+ if len(source) > 0 && source[0] == '\n' {
+ source = source[1:]
+ }
+
+ return source, true
+ }
+
+ return source, false
+}
+
+var messageRoles = []string{"system", "user", "assistant"}
+
+func parseCompletionMessages(body string) ([]syncdomain.Message, error) {
+ if strings.TrimSpace(body) == "" {
+ return nil, nil
+ }
+
+ if _, _, _, ok := nextOpenTag(body, 0); !ok {
+ return []syncdomain.Message{{
+ Role: "system",
+ Content: strings.TrimSpace(body),
+ }}, nil
+ }
+
+ var messages []syncdomain.Message
+ cursor := 0
+
+ for cursor < len(body) {
+ start, role, contentStart, ok := nextOpenTag(body, cursor)
+ if !ok {
+ if strings.TrimSpace(body[cursor:]) != "" {
+ return nil, errors.New("unexpected text outside message tags")
+ }
+
+ break
+ }
+ if strings.TrimSpace(body[cursor:start]) != "" {
+ return nil, errors.New("unexpected text outside message tags")
+ }
+
+ contentEnd, closeEnd, found := matchingClose(body, contentStart, role)
+ if !found {
+ return nil, fmt.Errorf("unclosed <%s> tag", role)
+ }
+
+ messages = append(messages, syncdomain.Message{
+ Role: role,
+ Content: strings.TrimSpace(body[contentStart:contentEnd]),
+ })
+ cursor = closeEnd
+ }
+
+ return messages, nil
+}
+
+func nextOpenTag(body string, from int) (start int, role string, contentStart int, ok bool) {
+ start = -1
+
+ for _, candidate := range messageRoles {
+ tag := "<" + candidate + ">"
+ index := strings.Index(body[from:], tag)
+ if index < 0 {
+ continue
+ }
+
+ absolute := from + index
+ if start < 0 || absolute < start {
+ start = absolute
+ role = candidate
+ contentStart = absolute + len(tag)
+ ok = true
+ }
+ }
+
+ return start, role, contentStart, ok
+}
+
+func matchingClose(body string, from int, role string) (contentEnd, closeEnd int, ok bool) {
+ open := "<" + role + ">"
+ close := "" + role + ">"
+ depth := 1
+ index := from
+
+ for index < len(body) {
+ relativeOpen := strings.Index(body[index:], open)
+ relativeClose := strings.Index(body[index:], close)
+ if relativeClose < 0 {
+ return 0, 0, false
+ }
+
+ if relativeOpen >= 0 && relativeOpen < relativeClose {
+ depth++
+ index += relativeOpen + len(open)
+
+ continue
+ }
+
+ depth--
+ closeAt := index + relativeClose
+ if depth == 0 {
+ return closeAt, closeAt + len(close), true
+ }
+
+ index = closeAt + len(close)
+ }
+
+ return 0, 0, false
+}
diff --git a/internal/sync/local/variation_test.go b/internal/sync/local/variation_test.go
new file mode 100644
index 00000000..e0cb7f13
--- /dev/null
+++ b/internal/sync/local/variation_test.go
@@ -0,0 +1,215 @@
+package local
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ syncdomain "github.com/launchdarkly/ldcli/internal/sync"
+)
+
+func TestIsVariationFile(t *testing.T) {
+ assert.True(t, isVariationFile("my-config/my-variation.prompt.md"))
+ assert.False(t, isVariationFile("my-variation.prompt.md"))
+ assert.False(t, isVariationFile("my-config/nested/my-variation.prompt.md"))
+ assert.False(t, isVariationFile("my-config/my-variation.prompt"))
+ assert.False(t, isVariationFile("my-config/my-variation.md"))
+}
+
+func TestParseVariation_UntaggedBodyIsSystemMessage(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/plain.prompt.md",
+ Data: []byte(`---
+formatVersion: 1
+mode: completion
+key: plain
+name: Plain
+---
+
+Just say hello.
+`),
+ }
+
+ resource, err := parseVariation(file)
+ require.NoError(t, err)
+
+ var payload syncdomain.Variation
+ require.NoError(t, unmarshalPayload(resource, &payload))
+ require.Equal(
+ t,
+ []syncdomain.Message{{Role: "system", Content: "Just say hello."}},
+ payload.Messages,
+ )
+}
+
+func TestParseVariation_AgentBodyIsInstructions(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/agent.prompt.md",
+ Data: []byte(`---
+formatVersion: 1
+mode: agent
+key: agent
+name: Agent
+---
+
+Use the available capabilities.
+
+This tag is part of the instructions.
+`),
+ }
+
+ resource, err := parseVariation(file)
+ require.NoError(t, err)
+
+ var payload syncdomain.Variation
+ require.NoError(t, unmarshalPayload(resource, &payload))
+ assert.Equal(
+ t,
+ "Use the available capabilities.\n\nThis tag is part of the instructions.",
+ payload.Instructions,
+ )
+ assert.Empty(t, payload.Messages)
+}
+
+func TestParseVariation_RejectsBodyFieldsInFrontMatter(t *testing.T) {
+ for _, field := range []string{
+ "instructions: Use the available capabilities.",
+ "messages: []",
+ } {
+ t.Run(field, func(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/agent.prompt.md",
+ Data: []byte(`---
+formatVersion: 1
+mode: agent
+key: agent
+name: Agent
+` + field + `
+---
+`),
+ }
+
+ _, err := parseVariation(file)
+ require.ErrorContains(t, err, "invalid front matter")
+ })
+ }
+}
+
+func TestParseVariation_MismatchedTags(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/bad.prompt.md",
+ Data: []byte(`---
+formatVersion: 1
+mode: completion
+key: bad
+name: Bad
+---
+
+
+oops
+
+`),
+ }
+
+ _, err := parseVariation(file)
+ require.ErrorContains(t, err, "unclosed tag")
+}
+
+func TestParseVariation_TextOutsideTags(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/bad.prompt.md",
+ Data: []byte(`---
+formatVersion: 1
+mode: completion
+key: bad
+name: Bad
+---
+
+hello
+
+hi
+
+`),
+ }
+
+ _, err := parseVariation(file)
+ require.ErrorContains(t, err, "unexpected text outside message tags")
+}
+
+func TestParseVariation_RequiresFormatVersion(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/v.prompt.md",
+ Data: []byte(`---
+mode: completion
+key: v
+name: V
+---
+`),
+ }
+
+ _, err := parseVariation(file)
+ require.ErrorContains(t, err, "formatVersion is required")
+}
+
+func TestParseVariation_RequiresMode(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/v.prompt.md",
+ Data: []byte(`---
+formatVersion: 1
+key: v
+name: V
+---
+`),
+ }
+
+ _, err := parseVariation(file)
+ require.ErrorContains(t, err, "mode is required")
+}
+
+func TestParseVariation_RejectsUnsupportedMode(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/v.prompt.md",
+ Data: []byte(`---
+formatVersion: 1
+mode: other
+key: v
+name: V
+---
+`),
+ }
+
+ _, err := parseVariation(file)
+ require.ErrorContains(t, err, `unsupported mode "other"`)
+}
+
+func TestParseVariation_RejectsVariationDescription(t *testing.T) {
+ file := localFile{
+ ProjectKey: "proj",
+ RelPath: "cfg/v.prompt.md",
+ Data: []byte(`---
+formatVersion: 1
+mode: completion
+key: v
+name: V
+description: Variation description
+---
+`),
+ }
+
+ _, err := parseVariation(file)
+ require.ErrorContains(t, err, "invalid front matter")
+}
+
+func unmarshalPayload(resource syncdomain.SyncedResource, destination any) error {
+ return json.Unmarshal(resource.Payload, destination)
+}
diff --git a/internal/sync/resource.go b/internal/sync/resource.go
new file mode 100644
index 00000000..d75c8fb9
--- /dev/null
+++ b/internal/sync/resource.go
@@ -0,0 +1,52 @@
+package sync
+
+import "encoding/json"
+
+const RootDir = ".launchdarkly"
+
+type Kind string
+
+const (
+ KindVariation Kind = "variation"
+)
+
+type SyncedResource struct {
+ Kind Kind
+ ProjectKey string
+ LookupKey string
+ Payload json.RawMessage
+ Upsert bool
+}
+
+type VariationMode string
+
+const (
+ VariationModeAgent VariationMode = "agent"
+ VariationModeCompletion VariationMode = "completion"
+)
+
+func (m VariationMode) Valid() bool {
+ switch m {
+ case VariationModeAgent, VariationModeCompletion:
+ return true
+ default:
+ return false
+ }
+}
+
+type Message struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+}
+
+type Variation struct {
+ Mode VariationMode `json:"mode" yaml:"mode"`
+ Key string `json:"key" yaml:"key"`
+ Name string `json:"name" yaml:"name"`
+ Instructions string `json:"instructions,omitempty" yaml:"-"`
+ ModelConfigKey string `json:"modelConfigKey,omitempty" yaml:"modelConfigKey,omitempty"`
+ ModelConfigVersion int `json:"modelConfigVersion,omitempty" yaml:"modelConfigVersion,omitempty"`
+ Model map[string]any `json:"model,omitempty" yaml:"model,omitempty"`
+ OutputFormat map[string]any `json:"outputFormat,omitempty" yaml:"outputFormat,omitempty"`
+ Messages []Message `json:"messages,omitempty" yaml:"-"`
+}