Skip to content

fix: surface BYOK stream response failures#345

Merged
JeffOtano merged 1 commit intomainfrom
jeffOtano/fix-openai-byok-response-failed
May 6, 2026
Merged

fix: surface BYOK stream response failures#345
JeffOtano merged 1 commit intomainfrom
jeffOtano/fix-openai-byok-response-failed

Conversation

@JeffOtano
Copy link
Copy Markdown
Owner

@JeffOtano JeffOtano commented May 6, 2026

Summary

  • Surface OpenAI Responses stream failures that finish with error but do not throw provider exceptions.
  • Route those non-thrown provider failures through existing BYOK handling so users see the OpenAI-specific fallback message instead of the generic empty failed-message banner.
  • Add regression coverage for the djwhelan failure shape.

Changes

  • convex/ai/resilience.ts: throws a safe provider_response_failed sentinel when the stream accumulator records finishReason: "error" after result.text resolves.
  • convex/ai/byokErrors.ts: classifies that sentinel as byok_unknown_error so the existing BYOK reporting path finalizes the pending message safely.
  • convex/ai/resilienceStreamFailure.test.ts: exercises the non-thrown provider failure path through streamWithRetry and asserts the BYOK message path is used.

Checklist

  • npx tsc --noEmit passes
  • npm run lint passes
  • npm test passes (existing + new tests)
  • New logic has tests (happy path + at least one error/edge case)
  • No new or materially expanded file exceeds the 300-line soft cap
  • No file exceeds the 400-line hard cap (enforced by CI)
  • Functions stay near the 60-line convention
  • No new any types (enforced by ESLint); as casts only at deserialization boundaries
  • Tested in browser (if UI changes)
  • Commits follow conventional format (type: description)
  • No unused exports or dead code introduced

Test Plan

  • npm run typecheck
  • npm run lint
  • npx vitest run convex/ai/resilience.test.ts convex/ai/resilienceFinalize.test.ts convex/ai/resilienceStreamFailure.test.ts
  • npm test

Summary by CodeRabbit

  • Bug Fixes
    • Improved error detection during streaming operations to catch and properly report provider response failures that were previously undetected
    • Enhanced error classification system to better identify and handle provider-related failures

Treat provider streams that finish with an error reason as provider failures so BYOK handling can finalize the pending message with a safe user-facing error instead of leaving the generic empty-message fallback.

Add regression coverage for OpenAI Responses stream failures that do not throw provider exceptions.

Co-authored-by: Codex <[email protected]>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

This PR adds provider failure detection to the streaming path in convex/ai/resilience.ts by checking the accumulator's finish reason after streaming completes, and extends error classification in byokErrors.ts to map provider_response_failed errors to byok_unknown_error. A comprehensive test validates the end-to-end flow.

Changes

Provider Failure Detection and Classification

Layer / File(s) Summary
Core Detection Logic
convex/ai/resilience.ts
After streaming result completes, a guard checks accumulator.toRow().finishReason and throws provider_response_failed error if the finish reason is "error".
Error Classification
convex/ai/byokErrors.ts
New conditional in classifyByokError maps error text containing "provider_response_failed" to "byok_unknown_error", placed before existing status-based logic.
Tests
convex/ai/resilienceStreamFailure.test.ts
New test file validates that streamWithRetry surfaces non-thrown provider finish errors as BYOK messages, updates pending mutations, and records errors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • JeffOtano/roni#289: Both PRs modify streaming error handling to surface provider failures and BYOK-related classification.
  • JeffOtano/roni#338: Both PRs modify convex/ai/resilience.ts error-finalization logic to detect and report provider failures.
  • JeffOtano/roni#254: Both PRs modify convex/ai/resilience.ts to surface provider failures and map them through BYOK classification.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: surface BYOK stream response failures' accurately and clearly describes the main change: surfacing OpenAI stream response failures through BYOK error handling.
Description check ✅ Passed The description covers all required sections: a clear summary of the changes, detailed list of modifications across three files, and a comprehensive checklist with nearly all items marked complete (only browser testing unchecked due to no UI changes).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jeffOtano/fix-openai-byok-response-failed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@JeffOtano JeffOtano marked this pull request as ready for review May 6, 2026 19:16
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
convex/ai/byokErrors.ts (1)

53-53: ⚡ Quick win

Extract "provider_response_failed" to a shared named constant.

The sentinel "provider_response_failed" is a raw string literal here and again in resilience.ts line 269. A future typo in either location would silently break BYOK detection with no compiler or runtime warning.

Export a constant from byokErrors.ts (or a thin shared module) and import it in resilience.ts:

♻️ Proposed refactor
+// Sentinel thrown by resilience.ts when the stream accumulator records
+// finishReason "error" without a provider exception.
+export const PROVIDER_RESPONSE_FAILED = "provider_response_failed";
+
 export function classifyByokError(error: unknown): ByokErrorCode | null {
   ...
-  if (lower.includes("provider_response_failed")) return "byok_unknown_error";
+  if (lower.includes(PROVIDER_RESPONSE_FAILED)) return "byok_unknown_error";

Then in resilience.ts:

+import { ..., PROVIDER_RESPONSE_FAILED } from "./byokErrors";
 ...
-    if (accumulator.toRow().finishReason === "error") throw new Error("provider_response_failed");
+    if (accumulator.toRow().finishReason === "error") throw new Error(PROVIDER_RESPONSE_FAILED);

As per coding guidelines: "Extract magic values to named constants instead of using literals in code."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/ai/byokErrors.ts` at line 53, Extract the literal
"provider_response_failed" into a named exported constant (e.g.
PROVIDER_RESPONSE_FAILED) from byokErrors.ts, replace the raw string in the
predicate (the line with lower.includes("provider_response_failed") in
byokErrors) with that constant, and update resilience.ts to import and use that
same constant instead of the literal; ensure the constant is a named export and
referenced by its symbol in both files to avoid duplicated magic strings.
convex/ai/resilienceStreamFailure.test.ts (1)

60-114: ⚡ Quick win

Consider adding a non-BYOK coverage case.

The single test verifies isByok: true thoroughly, but isByok: false takes a different code path in runAttempt: tryReportByok returns early, and the error falls through to reportError (with AI_ERROR_MESSAGE). That branch is currently untested.

A minimal addition:

it("surfaces provider finish errors as generic AI error for non-BYOK users", async () => {
  const streamText = vi.fn(
    async (options: { onStepFinish: (step: StepResult<ToolSet>) => void }) => {
      options.onStepFinish(responseFailedStep());
      return { text: Promise.resolve("") };
    },
  );
  const agent = {
    continueThread: vi.fn(async () => ({ thread: { streamText } })),
  } as unknown as Agent;
  const runQuery = vi.fn(async () => ({
    page: [{ _id: "pending-message", status: "pending" }],
  }));
  const runMutation = vi.fn(async () => undefined);
  const runAction = vi.fn(async () => undefined);

  await streamWithRetry(
    { runQuery, runMutation, runAction } as unknown as ActionCtx,
    {
      primaryAgent: agent,
      fallbackAgent: agent,
      primaryModelName: "gpt-5.4",
      threadId: "thread-1",
      userId: "user-1",
      prompt: "hello",
      isByok: false,           // <-- key difference
      provider: "openai",
      source: "chat",
      environment: "prod",
    },
  );

  expect(saveMessage).toHaveBeenCalledWith(
    expect.anything(),
    expect.anything(),
    expect.objectContaining({
      message: expect.objectContaining({ content: expect.stringContaining("trouble") }),
    }),
  );
});

As per coding guidelines: "Every function must have at least one error/edge case test in addition to happy path tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/ai/resilienceStreamFailure.test.ts` around lines 60 - 114, Add a
second test in resilienceStreamFailure.test.ts that mirrors the existing
"surfaces non-thrown provider finish errors as BYOK messages" case but sets
isByok: false to exercise the non-BYOK path in streamWithRetry/runAttempt;
simulate the provider finish error via the same streamText +
responseFailedStep(), call streamWithRetry, and assert that saveMessage was
called with a message content containing the generic AI error wording (the
branch handled by reportError/AI_ERROR_MESSAGE) and that the
runMutation/runAction expectations align with the non-BYOK flow. Ensure the test
references the same agent/runQuery/runMutation/runAction mocks used by the BYOK
test so it exercises tryReportByok returning early and reportError being
invoked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@convex/ai/byokErrors.ts`:
- Line 53: Extract the literal "provider_response_failed" into a named exported
constant (e.g. PROVIDER_RESPONSE_FAILED) from byokErrors.ts, replace the raw
string in the predicate (the line with
lower.includes("provider_response_failed") in byokErrors) with that constant,
and update resilience.ts to import and use that same constant instead of the
literal; ensure the constant is a named export and referenced by its symbol in
both files to avoid duplicated magic strings.

In `@convex/ai/resilienceStreamFailure.test.ts`:
- Around line 60-114: Add a second test in resilienceStreamFailure.test.ts that
mirrors the existing "surfaces non-thrown provider finish errors as BYOK
messages" case but sets isByok: false to exercise the non-BYOK path in
streamWithRetry/runAttempt; simulate the provider finish error via the same
streamText + responseFailedStep(), call streamWithRetry, and assert that
saveMessage was called with a message content containing the generic AI error
wording (the branch handled by reportError/AI_ERROR_MESSAGE) and that the
runMutation/runAction expectations align with the non-BYOK flow. Ensure the test
references the same agent/runQuery/runMutation/runAction mocks used by the BYOK
test so it exercises tryReportByok returning early and reportError being
invoked.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 128e7fa2-6333-4467-a88e-26af6ab787b2

📥 Commits

Reviewing files that changed from the base of the PR and between eab9305 and 48a134c.

📒 Files selected for processing (3)
  • convex/ai/byokErrors.ts
  • convex/ai/resilience.ts
  • convex/ai/resilienceStreamFailure.test.ts

@JeffOtano JeffOtano merged commit 7c96055 into main May 6, 2026
13 checks passed
@JeffOtano JeffOtano deleted the jeffOtano/fix-openai-byok-response-failed branch May 6, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant