sigoden/aichat · error

{err}

Error message

{err}

What it means

During streaming chat completions, the upstream provider/channel pushes an error as the first stream event (ResEvent::First(Some(err))). The server aborts the request with bail!("{err}"), surfacing the underlying provider error to the client before any content is streamed.

Solutions

  1. Read the interpolated message for the underlying provider error and fix that root cause first
  2. Verify the provider API key/credentials configured for the model's channel
  3. Confirm the requested model name exists on the provider
  4. Retry with a non-streaming request to get a clearer error payload
  5. Switch to a different provider/channel to isolate whether it is provider-side

Example fix

// before: err message obscures root cause
bail!("{err}");
// after: log full context and re-raise
error!("upstream first-event error: {err}");
bail!("{err}");
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling: verify provider config
if (!apiKey || !model) throw new Error('missing model or apiKey');

Try / catch

try {
  const stream = await client.chat.completions.create({ model, messages, stream: true });
} catch (e) {
  // message is the upstream provider error; inspect and map
  console.error('upstream stream error:', e.message);
}

Prevention

When it happens

Trigger: Calling POST /v1/chat/completions with stream=true when the first event received on the channel is an error (e.g. the provider rejected the request, auth failed, or model unavailable).

Common situations: Upstream API key invalid or expired; selected model not available on the provider; provider returns an immediate error for the request body; proxy/channel misconfiguration.

Related errors


AI-assisted analysis of sigoden/aichat@82976d349a (2026-09-09). Data as JSON: /api/errors/9f896bc4acc5a2e4. Report an issue: GitHub.

Appendix: source

Thrown at src/serve.rs:413

                    handler.done();
                }
                tokio::join!(
                    map_event(sse_rx, &tx, is_first.clone()),
                    chat_completions(
                        client.as_ref(),
                        &http_client,
                        &mut handler,
                        data,
                        &tx,
                        is_first
                    ),
                );
            });

            let first_event = rx.recv().await;

            if let Some(ResEvent::First(Some(err))) = first_event {
                bail!("{err}");
            }

            let shared: Arc<(String, String, i64, AtomicBool)> =
                Arc::new((completion_id, model_name, created, AtomicBool::new(false)));
            let stream = UnboundedReceiverStream::new(rx);
            let stream = stream.filter_map(move |res_event| {
                let shared = shared.clone();
                async move {
                    let (completion_id, model, created, has_tool_calls) = shared.as_ref();
                    match res_event {
                        ResEvent::Text(text) => {
                            Some(Ok(create_text_frame(completion_id, model, *created, &text)))
                        }
                        ResEvent::ToolCalls(tool_calls) => {
                            has_tool_calls.store(true, Ordering::SeqCst);
                            Some(Ok(create_tool_calls_frame(
                                completion_id,
                                model,

View on GitHub (pinned to 82976d349a)