{"record":{"id":"5b1d6608569497d4","repo":"siyuan-note/siyuan","slug":"provider-base-url-is-required","errorCode":null,"errorMessage":"provider base URL is required","messagePattern":"provider base URL is required","errorType":"validation","errorClass":null,"httpStatus":200,"severity":"error","filePath":"kernel/api/ai.go","lineNumber":52,"sourceCode":"\tTaskID  string                  `json:\"taskID\"`\n\tIDs     []string                `json:\"ids\"`\n\tInput   string                  `json:\"input\"`\n\tAction  string                  `json:\"action\"`\n\tHistory []model.AIEditorMessage `json:\"history\"`\n}\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","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/api/ai.go#L34-L70","documentation":"resolveAIProvider (kernel/api/ai.go:41-60) resolves the AI provider for a request either from saved config (by provider id) or from an inline providerConfig object. When an inline providerConfig is supplied, its baseURL must be a non-empty string after trimming; otherwise the request is rejected before any network call. This is deliberate: conf.AI.Normalize() would silently default an empty BaseURL to https://api.openai.com/v1 (conf/ai.go:593-596), so the API layer refuses blank URLs rather than let a misconfigured request hit OpenAI by accident.","triggerScenarios":"Any AI API taking a providerConfig object (e.g. /api/ai/ chat endpoints and agent provider selection) with baseURL omitted, empty, or whitespace-only — for example a custom provider meant for a local gateway (Ollama/vLLM) sent as {\"name\": \"local\", \"apiKey\": \"...\"} with no baseURL.","commonSituations":"Frontend forms adding a provider but not sending the URL field; JSON field-name mismatch (url vs baseUrl vs baseURL — the struct tag is baseURL); users assuming the saved default applies to inline configs; copy-pasted configs with the URL line deleted.","solutions":["Set baseURL explicitly, including scheme and version path, e.g. https://api.openai.com/v1 or http://127.0.0.1:11434/v1 for local gateways","Check the exact JSON key: the Provider struct binds baseURL (case-insensitive JSON match, but 'url' will not bind)","If you want to use an already-configured provider, send its provider id instead of an inline providerConfig object","Trim-test the value client-side before posting"],"exampleFix":"// before\n{\"providerConfig\": {\"name\": \"local-llm\", \"apiKey\": \"sk-x\"}}\n\n// after\n{\"providerConfig\": {\"name\": \"local-llm\", \"baseURL\": \"http://127.0.0.1:11434/v1\", \"apiKey\": \"sk-x\"}}","handlingStrategy":"validation","validationCode":"const okProvider = (p: any) =>\n  !!p && typeof p.baseURL === 'string' && p.baseURL.trim().length > 0;\nif (!okProvider(req.providerConfig)) throw new Error('provider base URL is required');","typeGuard":"const hasBaseURL = (p: unknown): p is {baseURL: string} =>\n  !!p && typeof (p as any).baseURL === 'string' && (p as any).baseURL.trim() !== '';","tryCatchPattern":"null","preventionTips":["Always send baseURL with scheme and version path (…/v1) for inline provider configs","Prefer referencing a saved provider by id instead of inline providerConfig when reusing the configured one","Validate the exact JSON key names against the Provider struct (baseURL, apiKey, models)"],"tags":["ai","provider","config","validation"],"backgroundTag":"missing-base-url","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}