{"record":{"id":"bb9cba44f304cdb9","repo":"siyuan-note/siyuan","slug":"invalid-provider-config","errorCode":null,"errorMessage":"invalid provider config","messagePattern":"invalid provider config","errorType":"validation","errorClass":null,"httpStatus":200,"severity":"error","filePath":"kernel/api/ai.go","lineNumber":57,"sourceCode":"}\n\nfunc resolveAIProvider(arg map[string]any) (*conf.Provider, error) {\n\tif providerConfig, ok := arg[\"providerConfig\"]; ok && providerConfig != nil {\n\t\tdata, err := gulu.JSON.MarshalJSON(providerConfig)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tprovider := &conf.Provider{}\n\t\tif err = gulu.JSON.UnmarshalJSON(data, provider); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif strings.TrimSpace(provider.BaseURL) == \"\" {\n\t\t\treturn nil, errors.New(\"provider base URL is required\")\n\t\t}\n\t\tai := &conf.AI{Providers: []*conf.Provider{provider}}\n\t\tai.Normalize()\n\t\tif len(ai.Providers) != 1 {\n\t\t\treturn nil, errors.New(\"invalid provider config\")\n\t\t}\n\t\treturn ai.Providers[0], nil\n\t}\n\n\tproviderID, _ := arg[\"provider\"].(string)\n\tfor _, provider := range model.Conf.AI.Providers {\n\t\tif provider != nil && provider.ID == providerID {\n\t\t\treturn provider, nil\n\t\t}\n\t}\n\treturn nil, errors.New(\"provider not found\")\n}\n\nfunc chatGPT(c *gin.Context) {\n\tret := gulu.Ret.NewResult()\n\tdefer c.JSON(http.StatusOK, ret)\n\n\targ, ok := util.JsonArg(c, ret)","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/api/ai.go#L39-L75","documentation":"After accepting an inline providerConfig, resolveAIProvider runs it through conf.AI.Normalize() and then asserts the provider list still contains exactly one entry (kernel/api/ai.go:54-58). Normalize pruns nil entries and repairs malformed fields (empty baseURL is defaulted, bad IDs regenerated — conf/ai.go:588-631); a count change therefore means the submitted providerConfig was structurally invalid in a way normalization rejects, and the request fails with 'invalid provider config' rather than proceeding with a mutated provider.","triggerScenarios":"providerConfig marshals to something that unmarshals into conf.Provider yet normalizes away — in practice a nil/empty provider sneaking through (e.g. providerConfig passed as a JSON null-ish value that survives the earlier nil check, or a provider entry Normalize drops as nil in a future rule). Normal single-object payloads with a baseURL never hit it; they pass or fail earlier checks.","commonSituations":"Defensive canary firing after a conf.AI.Normalize() rule change adds new pruning (version upgrade between frontend and kernel); clients sending providerConfig: {} variants; plugin code constructing Provider structs programmatically with zero values.","solutions":["Send a fully-formed provider object: non-empty baseURL plus name/models; avoid empty shells","If it appears right after a SiYuan version change, re-save the provider via the AI settings UI so the kernel re-normalizes persisted config","For plugin authors calling resolveAIProvider-shaped flows, construct conf.Provider with all required fields rather than partial structs","Compare the payload against conf.Provider JSON tags before submitting"],"exampleFix":"// before\n{\"providerConfig\": {}}\n\n// after\n{\"providerConfig\": {\"baseURL\": \"https://api.openai.com/v1\", \"apiKey\": \"sk-x\", \"models\": [{\"name\": \"gpt-4o\"}]}}","handlingStrategy":"validation","validationCode":"const okProviderConfig = (p: any) =>\n  !!p && typeof p === 'object' && !Array.isArray(p) &&\n  typeof p.baseURL === 'string' && p.baseURL.trim() !== '' &&\n  (!p.models || Array.isArray(p.models));\nif (!okProviderConfig(req.providerConfig)) throw new Error('invalid provider config');","typeGuard":"const isProviderConfig = (p: unknown): p is Record<string, any> =>\n  !!p && typeof p === 'object' && !Array.isArray(p) &&\n  typeof (p as any).baseURL === 'string' && (p as any).baseURL.trim() !== '';","tryCatchPattern":"null","preventionTips":["Send fully-formed provider objects; never empty shells or null-ish providerConfig values","After upgrading SiYuan, re-save providers in Settings - AI so persisted config re-normalizes","For plugin authors, populate every required conf.Provider field programmatically"],"tags":["ai","provider","config","normalization"],"backgroundTag":"config-normalization-rejected","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}