{"record":{"id":"72c3bbe037588767","repo":"siyuan-note/siyuan","slug":"ai-editor-model-returned-nil-stream","errorCode":null,"errorMessage":"AI editor model returned nil stream","messagePattern":"AI editor model returned nil stream","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/ai.go","lineNumber":201,"sourceCode":"\t\tMessages:            messages,\n\t\tStream:              true,\n\t}\n\tstreamCtx, cancel := context.WithCancel(ctx)\n\trequestTimeout := time.Duration(prov.RequestTimeout) * time.Second\n\trequestTimer, requestTimerDone := startAIEditorCancelTimer(requestTimeout, cancel)\n\tclient := util.NewOpenAIClientWithModel(prov.APIKey, prov.BaseURL, m.Name)\n\tcompletionStream, err := util.CreateOpenAICompletionStream(streamCtx, client, prov.Protocol, req, nil)\n\trequestTimedOut := stopAIEditorCancelTimer(requestTimer, requestTimerDone)\n\tif requestTimedOut {\n\t\terr = errors.New(\"AI editor request timeout\")\n\t}\n\tif nil != err {\n\t\tcancel()\n\t\treturn nil, err\n\t}\n\tif nil == completionStream {\n\t\tcancel()\n\t\treturn nil, errors.New(\"AI editor model returned nil stream\")\n\t}\n\treturn &AIEditorChatStream{\n\t\tstream:      completionStream,\n\t\tcancel:      cancel,\n\t\tidleTimeout: 120 * time.Second,\n\t}, nil\n}\n\nfunc startAIEditorCancelTimer(timeout time.Duration, cancel context.CancelFunc) (*time.Timer, <-chan struct{}) {\n\tif 0 >= timeout {\n\t\treturn nil, nil\n\t}\n\tdone := make(chan struct{})\n\ttimer := time.AfterFunc(timeout, func() {\n\t\tcancel()\n\t\tclose(done)\n\t})\n\treturn timer, done","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/model/ai.go#L183-L219","documentation":"After util.CreateOpenAICompletionStream returns without error, NewAIEditorChatStream (kernel/model/ai.go:201) defensively checks that the stream pointer is non-nil. Reading kernel/util/openai_completion.go:51 shows both protocol branches (chat completions and responses) always return a non-nil wrapper or a non-nil error, so a nil stream with a nil error cannot occur in the current code — this is an invariant guard against future regressions or internal state corruption.","triggerScenarios":"Not reachable through normal configuration: would require CreateOpenAICompletionStream to return (nil, nil), which no current code path does. If it ever appears, it indicates a kernel bug (e.g. a new protocol branch forgetting to construct the wrapper) rather than a user mistake.","commonSituations":"Running a modified or nightly kernel build where a protocol branch was changed; essentially never seen on release builds.","solutions":["Retry the AI editor request once to rule out a transient internal state","Try switching the provider protocol setting (openai vs openai-responses) to exercise the other code path","Report it upstream as a kernel bug, attaching the kernel log, provider protocol, and SiYuan version"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err != nil && err.Error() == \"AI editor model returned nil stream\" {\n    // internal invariant break: retry once, then collect diagnostics\n    logKernelVersionAndProviderProtocol()\n    return retryOrFail(err)\n}","preventionTips":["Run release kernel builds; invariant guards like this target regressions, not user input","Keep provider protocol settings on supported values (openai, openai-responses)","Report recurrences upstream with the kernel log"],"tags":["ai","invariant","internal-error","openai"],"backgroundTag":"nil-return-without-error","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}