From ae1d600c2c4b7ecb3ec470fd940a7087d97242be Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sun, 26 Jul 2026 00:08:13 +0700 Subject: [PATCH] fix(providers): stop the GraphQL round trip from dropping four agent options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The converter maps 13 keys of pconfig.AgentConfig; min_p, n, json and response_mime_type were never among them, and CreateProvider/UpdateProvider replace the whole row, so saving a provider through the UI persisted a config with those keys gone. The one that bites out of the box is `json`: openai's shipped simple_json default carries it, pconfig turns key-presence into llms.WithJSONMode(), and a user-defined provider built from that same default therefore called the LLM without JSON mode while the built-in one did not. Expose the four on AgentConfig/AgentConfigInput and map them both ways. From GraphQL a zero must be written as an absent key, not as a zero value — BuildOptions gates on presence, so `json: false` would otherwise switch JSON mode ON and `n: 0` would emit an invalid request parameter. The form carries them through untouched; none is user-editable yet. The behavioural test takes openai's shipped default through the round trip and asserts JSON mode still reaches the call — it fails on the old converter. Note: TestConvertModels_OffCapability fails in this working tree both before and after this change; it depends on the local langchaingo checkout, not on this. Co-Authored-By: Claude Opus 4.8 --- backend/pkg/database/converter/converter.go | 27 ++ .../pkg/database/converter/converter_test.go | 75 ++++ backend/pkg/graph/generated.go | 338 +++++++++++++++++- backend/pkg/graph/model/models_gen.go | 4 + backend/pkg/graph/schema.graphqls | 8 + .../e2e/mocks/cassettes/settings-providers.ts | 4 + frontend/graphql-schema.graphql | 4 + frontend/src/graphql/types.ts | 44 +++ .../src/pages/settings/settings-provider.tsx | 10 + 9 files changed, 513 insertions(+), 1 deletion(-) diff --git a/backend/pkg/database/converter/converter.go b/backend/pkg/database/converter/converter.go index e24a5167..ee9b0d51 100644 --- a/backend/pkg/database/converter/converter.go +++ b/backend/pkg/database/converter/converter.go @@ -695,6 +695,18 @@ func ConvertAgentConfigToGqlModel(ac *pconfig.AgentConfig) *model.AgentConfig { if ac.PresencePenalty != 0 { result.PresencePenalty = &ac.PresencePenalty } + if ac.MinP != 0 { + result.MinP = &ac.MinP + } + if ac.N != 0 { + result.N = &ac.N + } + if ac.JSON { + result.JSON = &ac.JSON + } + if ac.ResponseMIMEType != "" { + result.ResponseMimeType = &ac.ResponseMIMEType + } if !ac.Reasoning.IsZero() { reasoning := &model.ReasoningConfig{} @@ -796,6 +808,21 @@ func ConvertAgentConfigFromGqlModel(ac *model.AgentConfig) *pconfig.AgentConfig if ac.PresencePenalty != nil { rawConfig["presence_penalty"] = *ac.PresencePenalty } + // BuildOptions gates these on the key being present, not on its value, so a zero has to be + // written as an absent key — otherwise `json: false` would switch JSON mode on and `n: 0` + // would emit an invalid request parameter. + if ac.MinP != nil && *ac.MinP != 0 { + rawConfig["min_p"] = *ac.MinP + } + if ac.N != nil && *ac.N != 0 { + rawConfig["n"] = *ac.N + } + if ac.JSON != nil && *ac.JSON { + rawConfig["json"] = *ac.JSON + } + if ac.ResponseMimeType != nil && *ac.ResponseMimeType != "" { + rawConfig["response_mime_type"] = *ac.ResponseMimeType + } if ac.Reasoning != nil { reasoning := map[string]any{} diff --git a/backend/pkg/database/converter/converter_test.go b/backend/pkg/database/converter/converter_test.go index 4837231c..976b2515 100644 --- a/backend/pkg/database/converter/converter_test.go +++ b/backend/pkg/database/converter/converter_test.go @@ -4,6 +4,7 @@ import ( "testing" "pentagi/pkg/graph/model" + "pentagi/pkg/providers/openai" "pentagi/pkg/providers/pconfig" "github.com/stretchr/testify/assert" @@ -196,3 +197,77 @@ func TestConvertAgentConfigExtraBodyNilStaysNil(t *testing.T) { assert.Nil(t, ConvertAgentConfigToGqlModel(from).ExtraBody) } + +func ptr[T any](value T) *T { return &value } + +func TestConvertAgentConfigCallOptionFieldsRoundTrip(t *testing.T) { + from := ConvertAgentConfigFromGqlModel(&model.AgentConfig{ + Model: "m", + MinP: ptr(0.05), + N: ptr(3), + JSON: ptr(true), + ResponseMimeType: ptr("application/json"), + }) + require.NotNil(t, from) + assert.Equal(t, 0.05, from.MinP) + assert.Equal(t, 3, from.N) + assert.True(t, from.JSON) + assert.Equal(t, "application/json", from.ResponseMIMEType) + + back := ConvertAgentConfigToGqlModel(from) + require.NotNil(t, back) + require.NotNil(t, back.MinP) + require.NotNil(t, back.N) + require.NotNil(t, back.JSON) + require.NotNil(t, back.ResponseMimeType) + assert.Equal(t, 0.05, *back.MinP) + assert.Equal(t, 3, *back.N) + assert.True(t, *back.JSON) + assert.Equal(t, "application/json", *back.ResponseMimeType) +} + +// pconfig gates each emitter on the raw key being PRESENT, not on its value, so a zero written +// through would switch the option on: `json: false` must not reach the LLM as WithJSONMode(). +func TestConvertAgentConfigZeroCallOptionFieldsStayAbsent(t *testing.T) { + from := ConvertAgentConfigFromGqlModel(&model.AgentConfig{ + Model: "m", + MinP: ptr(0.0), + N: ptr(0), + JSON: ptr(false), + ResponseMimeType: ptr(""), + }) + require.NotNil(t, from) + + options := from.BuildOptions() + call := llms.CallOptions{} + for _, option := range options { + option(&call) + } + + assert.False(t, call.JSONMode, "json:false must not enable JSON mode") + assert.Nil(t, call.N, "n:0 must not reach the request as an explicit parameter") + assert.Empty(t, call.ResponseMIMEType) +} + +// The shipped simple_json default carries `json: true`; saving a provider through the UI used to +// strip it, so the reloaded config called the LLM without JSON mode while the built-in one did not. +func TestShippedSimpleJSONKeepsJSONModeThroughTheGraphQLRoundTrip(t *testing.T) { + pc, err := openai.DefaultProviderConfig() + require.NoError(t, err) + require.NotNil(t, pc.SimpleJSON) + require.True(t, pc.SimpleJSON.JSON, "the shipped default is what makes this test meaningful") + + gql := ConvertProviderConfigToGqlModel(pc) + require.NotNil(t, gql) + + restored := ConvertAgentsConfigFromGqlModel(gql) + require.NotNil(t, restored) + require.NotNil(t, restored.SimpleJSON) + + call := llms.CallOptions{} + for _, option := range restored.SimpleJSON.BuildOptions() { + option(&call) + } + + assert.True(t, call.JSONMode, "the simple_json agent must still ask for JSON mode after a save") +} diff --git a/backend/pkg/graph/generated.go b/backend/pkg/graph/generated.go index e6c1ac11..aac0cce2 100644 --- a/backend/pkg/graph/generated.go +++ b/backend/pkg/graph/generated.go @@ -78,14 +78,18 @@ type ComplexityRoot struct { AgentConfig struct { ExtraBody func(childComplexity int) int FrequencyPenalty func(childComplexity int) int + JSON func(childComplexity int) int MaxLength func(childComplexity int) int MaxTokens func(childComplexity int) int MinLength func(childComplexity int) int + MinP func(childComplexity int) int Model func(childComplexity int) int + N func(childComplexity int) int PresencePenalty func(childComplexity int) int Price func(childComplexity int) int Reasoning func(childComplexity int) int RepetitionPenalty func(childComplexity int) int + ResponseMimeType func(childComplexity int) int Temperature func(childComplexity int) int TopK func(childComplexity int) int TopP func(childComplexity int) int @@ -1035,6 +1039,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AgentConfig.FrequencyPenalty(childComplexity), true + case "AgentConfig.json": + if e.complexity.AgentConfig.JSON == nil { + break + } + + return e.complexity.AgentConfig.JSON(childComplexity), true + case "AgentConfig.maxLength": if e.complexity.AgentConfig.MaxLength == nil { break @@ -1056,6 +1067,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AgentConfig.MinLength(childComplexity), true + case "AgentConfig.minP": + if e.complexity.AgentConfig.MinP == nil { + break + } + + return e.complexity.AgentConfig.MinP(childComplexity), true + case "AgentConfig.model": if e.complexity.AgentConfig.Model == nil { break @@ -1063,6 +1081,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AgentConfig.Model(childComplexity), true + case "AgentConfig.n": + if e.complexity.AgentConfig.N == nil { + break + } + + return e.complexity.AgentConfig.N(childComplexity), true + case "AgentConfig.presencePenalty": if e.complexity.AgentConfig.PresencePenalty == nil { break @@ -1091,6 +1116,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AgentConfig.RepetitionPenalty(childComplexity), true + case "AgentConfig.responseMimeType": + if e.complexity.AgentConfig.ResponseMimeType == nil { + break + } + + return e.complexity.AgentConfig.ResponseMimeType(childComplexity), true + case "AgentConfig.temperature": if e.complexity.AgentConfig.Temperature == nil { break @@ -9776,6 +9808,170 @@ func (ec *executionContext) fieldContext_AgentConfig_presencePenalty(_ context.C return fc, nil } +func (ec *executionContext) _AgentConfig_minP(ctx context.Context, field graphql.CollectedField, obj *model.AgentConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AgentConfig_minP(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.MinP, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*float64) + fc.Result = res + return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AgentConfig_minP(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AgentConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Float does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AgentConfig_n(ctx context.Context, field graphql.CollectedField, obj *model.AgentConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AgentConfig_n(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.N, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AgentConfig_n(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AgentConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AgentConfig_json(ctx context.Context, field graphql.CollectedField, obj *model.AgentConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AgentConfig_json(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.JSON, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + fc.Result = res + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AgentConfig_json(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AgentConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AgentConfig_responseMimeType(ctx context.Context, field graphql.CollectedField, obj *model.AgentConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AgentConfig_responseMimeType(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ResponseMimeType, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AgentConfig_responseMimeType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AgentConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _AgentConfig_reasoning(ctx context.Context, field graphql.CollectedField, obj *model.AgentConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AgentConfig_reasoning(ctx, field) if err != nil { @@ -10684,6 +10880,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_simple(_ context.Context, return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -10756,6 +10960,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_simpleJson(_ context.Conte return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -10828,6 +11040,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_primaryAgent(_ context.Con return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -10900,6 +11120,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_assistant(_ context.Contex return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -10972,6 +11200,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_generator(_ context.Contex return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -11044,6 +11280,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_refiner(_ context.Context, return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -11116,6 +11360,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_adviser(_ context.Context, return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -11188,6 +11440,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_reflector(_ context.Contex return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -11260,6 +11520,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_searcher(_ context.Context return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -11332,6 +11600,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_enricher(_ context.Context return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -11404,6 +11680,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_coder(_ context.Context, f return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -11476,6 +11760,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_installer(_ context.Contex return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -11548,6 +11840,14 @@ func (ec *executionContext) fieldContext_AgentsConfig_pentester(_ context.Contex return ec.fieldContext_AgentConfig_frequencyPenalty(ctx, field) case "presencePenalty": return ec.fieldContext_AgentConfig_presencePenalty(ctx, field) + case "minP": + return ec.fieldContext_AgentConfig_minP(ctx, field) + case "n": + return ec.fieldContext_AgentConfig_n(ctx, field) + case "json": + return ec.fieldContext_AgentConfig_json(ctx, field) + case "responseMimeType": + return ec.fieldContext_AgentConfig_responseMimeType(ctx, field) case "reasoning": return ec.fieldContext_AgentConfig_reasoning(ctx, field) case "price": @@ -36725,7 +37025,7 @@ func (ec *executionContext) unmarshalInputAgentConfigInput(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"model", "maxTokens", "temperature", "topK", "topP", "minLength", "maxLength", "repetitionPenalty", "frequencyPenalty", "presencePenalty", "reasoning", "price", "extraBody"} + fieldsInOrder := [...]string{"model", "maxTokens", "temperature", "topK", "topP", "minLength", "maxLength", "repetitionPenalty", "frequencyPenalty", "presencePenalty", "minP", "n", "json", "responseMimeType", "reasoning", "price", "extraBody"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -36802,6 +37102,34 @@ func (ec *executionContext) unmarshalInputAgentConfigInput(ctx context.Context, return it, err } it.PresencePenalty = data + case "minP": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("minP")) + data, err := ec.unmarshalOFloat2ᚖfloat64(ctx, v) + if err != nil { + return it, err + } + it.MinP = data + case "n": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("n")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.N = data + case "json": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("json")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.JSON = data + case "responseMimeType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("responseMimeType")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.ResponseMimeType = data case "reasoning": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("reasoning")) data, err := ec.unmarshalOReasoningConfigInput2ᚖpentagiᚋpkgᚋgraphᚋmodelᚐReasoningConfig(ctx, v) @@ -37564,6 +37892,14 @@ func (ec *executionContext) _AgentConfig(ctx context.Context, sel ast.SelectionS out.Values[i] = ec._AgentConfig_frequencyPenalty(ctx, field, obj) case "presencePenalty": out.Values[i] = ec._AgentConfig_presencePenalty(ctx, field, obj) + case "minP": + out.Values[i] = ec._AgentConfig_minP(ctx, field, obj) + case "n": + out.Values[i] = ec._AgentConfig_n(ctx, field, obj) + case "json": + out.Values[i] = ec._AgentConfig_json(ctx, field, obj) + case "responseMimeType": + out.Values[i] = ec._AgentConfig_responseMimeType(ctx, field, obj) case "reasoning": out.Values[i] = ec._AgentConfig_reasoning(ctx, field, obj) case "price": diff --git a/backend/pkg/graph/model/models_gen.go b/backend/pkg/graph/model/models_gen.go index ebfb1370..37872d2b 100644 --- a/backend/pkg/graph/model/models_gen.go +++ b/backend/pkg/graph/model/models_gen.go @@ -45,6 +45,10 @@ type AgentConfig struct { RepetitionPenalty *float64 `json:"repetitionPenalty,omitempty"` FrequencyPenalty *float64 `json:"frequencyPenalty,omitempty"` PresencePenalty *float64 `json:"presencePenalty,omitempty"` + MinP *float64 `json:"minP,omitempty"` + N *int `json:"n,omitempty"` + JSON *bool `json:"json,omitempty"` + ResponseMimeType *string `json:"responseMimeType,omitempty"` Reasoning *ReasoningConfig `json:"reasoning,omitempty"` Price *ModelPrice `json:"price,omitempty"` ExtraBody map[string]interface{} `json:"extraBody,omitempty"` diff --git a/backend/pkg/graph/schema.graphqls b/backend/pkg/graph/schema.graphqls index 7fb3f410..863d7011 100644 --- a/backend/pkg/graph/schema.graphqls +++ b/backend/pkg/graph/schema.graphqls @@ -815,6 +815,10 @@ type AgentConfig { repetitionPenalty: Float frequencyPenalty: Float presencePenalty: Float + minP: Float + n: Int + json: Boolean + responseMimeType: String reasoning: ReasoningConfig price: ModelPrice extraBody: Map @@ -866,6 +870,10 @@ input AgentConfigInput { repetitionPenalty: Float frequencyPenalty: Float presencePenalty: Float + minP: Float + n: Int + json: Boolean + responseMimeType: String reasoning: ReasoningConfigInput price: ModelPriceInput extraBody: Map diff --git a/frontend/e2e/mocks/cassettes/settings-providers.ts b/frontend/e2e/mocks/cassettes/settings-providers.ts index 34aea2c7..c8f7119e 100644 --- a/frontend/e2e/mocks/cassettes/settings-providers.ts +++ b/frontend/e2e/mocks/cassettes/settings-providers.ts @@ -21,14 +21,18 @@ const agentConfig = (model = 'e2e-model'): AgentConfigFragmentFragment => entity('AgentConfig', { extraBody: null, frequencyPenalty: null, + json: null, maxLength: null, maxTokens: null, minLength: null, + minP: null, model, + n: null, presencePenalty: null, price: null, reasoning: null, repetitionPenalty: null, + responseMimeType: null, temperature: null, topK: null, topP: null, diff --git a/frontend/graphql-schema.graphql b/frontend/graphql-schema.graphql index 115c39e1..0547b3d0 100644 --- a/frontend/graphql-schema.graphql +++ b/frontend/graphql-schema.graphql @@ -321,6 +321,10 @@ fragment agentConfigFragment on AgentConfig { repetitionPenalty frequencyPenalty presencePenalty + minP + n + json + responseMimeType reasoning { mode effort diff --git a/frontend/src/graphql/types.ts b/frontend/src/graphql/types.ts index 62ad1541..a1a2533e 100644 --- a/frontend/src/graphql/types.ts +++ b/frontend/src/graphql/types.ts @@ -6,14 +6,18 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document- export type AgentConfigInput = { extraBody?: Record | null | undefined; frequencyPenalty?: number | null | undefined; + json?: boolean | null | undefined; maxLength?: number | null | undefined; maxTokens?: number | null | undefined; minLength?: number | null | undefined; + minP?: number | null | undefined; model: string; + n?: number | null | undefined; presencePenalty?: number | null | undefined; price?: ModelPriceInput | null | undefined; reasoning?: ReasoningConfigInput | null | undefined; repetitionPenalty?: number | null | undefined; + responseMimeType?: string | null | undefined; temperature?: number | null | undefined; topK?: number | null | undefined; topP?: number | null | undefined; @@ -547,6 +551,10 @@ export type AgentConfigFragmentFragment = { repetitionPenalty: number | null; frequencyPenalty: number | null; presencePenalty: number | null; + minP: number | null; + n: number | null; + json: boolean | null; + responseMimeType: string | null; extraBody: Record | null; reasoning: { mode: ReasoningMode | null; effort: ReasoningEffort | null; maxTokens: number | null } | null; price: { input: number; output: number; cacheRead: number; cacheWrite: number } | null; @@ -2166,6 +2174,10 @@ export const AgentConfigFragmentFragmentDoc = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, @@ -2357,6 +2369,10 @@ export const AgentsConfigFragmentFragmentDoc = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, @@ -2433,6 +2449,10 @@ export const ProviderConfigFragmentFragmentDoc = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, @@ -3884,6 +3904,10 @@ export const SettingsProvidersDocument = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, @@ -8789,6 +8813,10 @@ export const CreateProviderDocument = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, @@ -9062,6 +9090,10 @@ export const UpdateProviderDocument = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, @@ -11291,6 +11323,10 @@ export const ProviderCreatedDocument = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, @@ -11527,6 +11563,10 @@ export const ProviderUpdatedDocument = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, @@ -11763,6 +11803,10 @@ export const ProviderDeletedDocument = { { kind: 'Field', name: { kind: 'Name', value: 'repetitionPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'frequencyPenalty' } }, { kind: 'Field', name: { kind: 'Name', value: 'presencePenalty' } }, + { kind: 'Field', name: { kind: 'Name', value: 'minP' } }, + { kind: 'Field', name: { kind: 'Name', value: 'n' } }, + { kind: 'Field', name: { kind: 'Name', value: 'json' } }, + { kind: 'Field', name: { kind: 'Name', value: 'responseMimeType' } }, { kind: 'Field', name: { kind: 'Name', value: 'reasoning' }, diff --git a/frontend/src/pages/settings/settings-provider.tsx b/frontend/src/pages/settings/settings-provider.tsx index 856949ef..84287d1a 100644 --- a/frontend/src/pages/settings/settings-provider.tsx +++ b/frontend/src/pages/settings/settings-provider.tsx @@ -611,10 +611,13 @@ const agentConfigSchema = z .object({ extraBody: optionalJsonObject, frequencyPenalty: optionalNumber, + json: z.boolean().nullable().optional(), maxLength: optionalNumber, maxTokens: optionalNumber, minLength: optionalNumber, + minP: optionalNumber, model: requiredString('Model is required'), + n: optionalNumber, presencePenalty: optionalNumber, price: z .object({ @@ -634,6 +637,7 @@ const agentConfigSchema = z .nullable() .optional(), repetitionPenalty: optionalNumber, + responseMimeType: z.string().nullable().optional(), temperature: optionalNumber, topK: optionalNumber, topP: optionalNumber, @@ -886,10 +890,15 @@ export const transformFormToGraphQL = ( const config: AgentConfigInput = { extraBody: data?.extraBody?.trim() ? (JSON.parse(data.extraBody) as Record) : null, frequencyPenalty: data?.frequencyPenalty ?? null, + // Not user-editable: carried through so saving a provider does not strip what the + // shipped defaults set (json drives WithJSONMode on the simple_json agent). + json: data?.json ?? null, maxLength: data?.maxLength ?? null, maxTokens: data?.maxTokens ?? null, minLength: data?.minLength ?? null, + minP: data?.minP ?? null, model: data?.model ?? '', + n: data?.n ?? null, presencePenalty: data?.presencePenalty ?? null, price: data?.price && @@ -912,6 +921,7 @@ export const transformFormToGraphQL = ( } : null, repetitionPenalty: data?.repetitionPenalty ?? null, + responseMimeType: data?.responseMimeType ?? null, temperature: data?.temperature ?? null, topK: data?.topK ?? null, topP: data?.topP ?? null,