diff --git a/engine_test.go b/engine_test.go index 2a9beb8..19e1cb9 100644 --- a/engine_test.go +++ b/engine_test.go @@ -1154,6 +1154,73 @@ func TestOpenAICompatibleProfileRunsThroughNormalProfilePath(t *testing.T) { } } +func TestOpenAICompatibleProfileDefersExtraParamsValidation(t *testing.T) { + cyclic := map[string]any{} + cyclic["self"] = cyclic + + prof := scriptorium.OpenAICompatibleProfile(scriptorium.OpenAICompatibleProfileConfig{ + ID: "cyclic-template-profile", + Endpoint: "http://cyclic-template/v1", + Model: "cyclic-template-model", + ExtraParams: cyclic, + }) + + _, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"}, + scriptorium.WithProfiles(prof), + ) + if !errors.Is(err, scriptorium.ErrInvalidConfig) { + t.Fatalf("expected ErrInvalidConfig, got %v", err) + } +} + +func TestOpenAICompatibleProfileNestedExtraParamsRunThroughWithProfiles(t *testing.T) { + fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}} + nested := map[string]any{ + "labels": map[string]string{"route": "primary"}, + "ids": []int{1, 2, 3}, + } + extraParams := map[string]any{ + "nested": nested, + } + prof := scriptorium.OpenAICompatibleProfile(scriptorium.OpenAICompatibleProfileConfig{ + ID: "nested-template-profile", + Endpoint: "http://nested-template/v1", + Model: "nested-template-model", + ExtraParams: extraParams, + }) + extraParams["added"] = "mutated-after-construction" + + engine, err := scriptorium.NewEngine(scriptorium.Config{ + PromptDir: "./examples/prompts", + SchemaDir: "./examples/schemas", + }, scriptorium.WithProfiles(prof), scriptorium.WithLLMClient(fake)) + if err != nil { + t.Fatalf("expected engine construction to succeed, got %v", err) + } + nested["added"] = "mutated-after-construction" + + _, err = engine.Run(context.Background(), scriptorium.RunRequest{ + PromptID: "generic.markdown_summary", + ProfileID: "nested-template-profile", + Inputs: map[string]scriptorium.ArtifactRef{ + "transcript": scriptorium.Inline("Rin opens the gate."), + "glossary": scriptorium.Inline("gate: A guarded passage."), + }, + }) + if err != nil { + t.Fatalf("expected run to succeed, got %v", err) + } + want := map[string]any{ + "nested": map[string]any{ + "labels": map[string]string{"route": "primary"}, + "ids": []int{1, 2, 3}, + }, + } + if !reflect.DeepEqual(fake.requests[0].Target.ExtraParams, want) { + t.Fatalf("unexpected extra params:\ngot=%#v\nwant=%#v", fake.requests[0].Target.ExtraParams, want) + } +} + func TestInMemoryProfileAPIKeyRequiredBehavior(t *testing.T) { engine, err := scriptorium.NewEngine(scriptorium.Config{ PromptDir: "./examples/prompts", diff --git a/profiles.go b/profiles.go index 8f60c43..334638e 100644 --- a/profiles.go +++ b/profiles.go @@ -28,10 +28,21 @@ func OpenAICompatibleProfile(cfg OpenAICompatibleProfileConfig) Profile { ServiceTier: cfg.ServiceTier, ReasoningEffort: cfg.ReasoningEffort, APIKeyRequired: cfg.APIKeyRequired, - ExtraParams: copyAnyMap(cfg.ExtraParams), + ExtraParams: copyShallowAnyMap(cfg.ExtraParams), } } +func copyShallowAnyMap(src map[string]any) map[string]any { + if src == nil { + return nil + } + out := make(map[string]any, len(src)) + for k, v := range src { + out[k] = v + } + return out +} + type memoryProfileRepository struct { profiles map[string]domain.ExecutionProfile }