Skip to content

fix(generate-schedule): switch to streaming AI path to avoid body-read abort - #515

Merged
jdelgado2002 merged 3 commits into
mainfrom
fix/generate-schedule-streaming
May 23, 2026
Merged

fix(generate-schedule): switch to streaming AI path to avoid body-read abort#515
jdelgado2002 merged 3 commits into
mainfrom
fix/generate-schedule-streaming

Conversation

@jdelgado2002

@jdelgado2002 jdelgado2002 commented May 23, 2026

Copy link
Copy Markdown
Member

Summary

  • Production-down hotfix. AI schedule generation was returning 502 for restaurants whose prompts produced large AI responses (e.g., 27 employees, 8 templates, 69 required slots → ~20K-char prompt, long json_schema-constrained output).
  • Root cause: _shared/ai-caller.ts:77 attaches AbortSignal.timeout(30000) to the OpenRouter fetch and returns the Response without consuming the body. The signal stays bound to the body stream, so the caller's await response.json() aborts with DOMException("Signal timed out.") after 30s of body download. Three consecutive aborts (~90s) exhausted the wall-clock budget and returned a synthetic 502.
  • Fix: switch generate-schedule to the existing callModelWithStreaming primitive in _shared/streaming.ts, which uses a 90s fetch timeout, sends stream: true, and consumes SSE chunks inside the helper so no caller-side body read is left exposed to the bound signal.
  • Refactor: extract the model-chain loop into a new pure helper _shared/schedule-ai-runner.ts so it can be unit-tested from Vitest with the streaming caller injected.
  • Recalibrate wall-clock budget: MODEL_LOOP_BUDGET_MS raised from 90s → 130s so one full streaming attempt (90s ceiling) plus a fast-fail fallback fit inside Supabase's ~150s edge-function hard kill.

Root cause evidence (from production logs)

[generate-schedule] Trying model: Gemini 2.5 Flash
✅ Gemini 2.5 Flash succeeded             ← fetch headers OK
[generate-schedule] Model Gemini 2.5 Flash parse failed: Signal timed out.   ← body read aborted
[generate-schedule] Trying model: Gemini 2.5 Flash Lite
✅ Gemini 2.5 Flash Lite succeeded
[generate-schedule] Model Gemini 2.5 Flash Lite parse failed: Signal timed out.
[generate-schedule] Trying model: Llama 4 Maverick
✅ Llama 4 Maverick succeeded
[generate-schedule] Model Llama 4 Maverick parse failed: Signal timed out.
[generate-schedule] Model chain wall-clock budget exhausted (90010ms > 90000ms). Stopping early.

The misleading ✅ X succeeded log is real — only fetch headers were confirmed. The body never finished arriving before the 30s signal fired on the stream pull.

Why streaming fixes it

  • processStreamedResponse reads SSE chunks continuously; an isolated slow chunk doesn't trigger the abort as long as the whole stream completes within 90s.
  • Token emission paces the abort window naturally — chunks keep the connection active.
  • The helper returns the accumulated content string, eliminating the body-read step where the bound signal previously fired.

Test plan

  • tests/unit/schedule-ai-runner.test.ts — 8 cases covering: first-success, null fallback, parse-error fallback, markdown-fence stripping, all-fail → null, wall-clock budget exhaustion, no extra retries at this layer, restaurantId pass-through.
  • npm run typecheck clean.
  • npm run test — 4085 unit tests pass (1 pre-existing skip).
  • npm run build succeeds.
  • npx eslint clean on all touched files.
  • Post-deploy smoke: trigger AI schedule generation for the affected restaurant. Expect success in <30s with (streaming) markers in the edge function log.

Out of scope

  • Prompt size and json_schema constraints unchanged.
  • Other edge functions that use callModel are not touched.
  • Braintrust telemetry remains disabled (_shared/braintrust.ts:4 initLogger = null).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved schedule-generation reliability by switching to a streaming-based execution path, bounding per-attempt time, aborting truncated responses, and preventing misleading parse errors and synthetic 502s.
  • Refactor

    • Consolidated model fallback logic into a single, testable chain that respects a total wall-clock budget and clearer abort/error handling.
  • Documentation

    • Added a design spec describing the incident, fix, test plan, and out-of-scope items.
  • Tests

    • Added unit tests covering fallback behavior, fence stripping, budget exhaustion, and abort scenarios.

Review Change Stack

…d abort

Production was returning 502 for restaurants with large AI responses
because the non-streaming path attaches AbortSignal.timeout(30s) to the
fetch and returns the Response without consuming the body. The signal
stays bound to the body stream, so the caller's later `await response.json()`
aborts with "Signal timed out." after 30s of body download. Three
consecutive aborts (~90s) exhausted the wall-clock budget and returned
a synthetic 502.

Switch to the existing callModelWithStreaming primitive in
_shared/streaming.ts which:
- Uses AbortSignal.timeout(90s) on fetch.
- Sends `stream: true` so OpenRouter ships SSE chunks instead of
  buffering the full body.
- Consumes the stream inside the helper and returns the parsed content
  string, removing the caller-side body-read step that the bound signal
  was killing.

Extract the model-chain loop into a new pure helper,
_shared/schedule-ai-runner.ts, so it can be unit-tested with the
streaming caller injected. Eight Vitest cases cover first-success,
markdown-fence stripping, parse-error fallback, null-content fallback,
all-fail returning null, wall-clock budget exhaustion, no extra retries
at this layer, and restaurantId pass-through.

Recalibrate MODEL_LOOP_BUDGET_MS from 90s to 130s. Each streaming
attempt now has a 90s ceiling, so 130s lets one full attempt plus a
fast-fail fallback fit inside the ~150s edge-function hard kill.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@netlify

netlify Bot commented May 23, 2026

Copy link
Copy Markdown

Deploy Preview for easyshifthq ready!

Name Link
🔨 Latest commit 3c2982b
🔍 Latest deploy log https://app.netlify.com/projects/easyshifthq/deploys/6a11b3f36d342a000918ae8f
😎 Deploy Preview https://deploy-preview-515--easyshifthq.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 23
Accessibility: 98
Best Practices: 92
SEO: 98
PWA: 90
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel

vercel Bot commented May 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
easyshifthq Ready Ready Preview, Comment May 23, 2026 2:05pm

@supabase

supabase Bot commented May 23, 2026

Copy link
Copy Markdown

Updates to Preview Branch (fix/generate-schedule-streaming) ↗︎

Deployments Status Updated
Database Sat, 23 May 2026 14:05:08 UTC
Services Sat, 23 May 2026 14:05:08 UTC
APIs Sat, 23 May 2026 14:05:08 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Sat, 23 May 2026 14:05:09 UTC
Migrations Sat, 23 May 2026 14:05:09 UTC
Seeding Sat, 23 May 2026 14:05:09 UTC
Edge Functions Sat, 23 May 2026 14:05:32 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR extracts schedule generation's model-fallback logic into a reusable, streaming-based helper runScheduleModelChain, updates streaming to report finish reasons and accept external abort signals, wires generate-schedule to the helper, and adds comprehensive Vitest coverage for fallback, fence-stripping, and budget/abort behavior.

Changes

Streaming-based schedule generation refactor

Layer / File(s) Summary
Design & architecture specification
docs/superpowers/specs/2026-05-22-generate-schedule-streaming-design.md
New spec documents the incident, streaming-based fix, helper extraction and wiring to callModelWithStreaming, wall-clock budget strategy, test plan, and out-of-scope/risk notes.
Shared helper: types, fence-stripping, and chain runner
supabase/functions/_shared/schedule-ai-runner.ts
Adds exported types/interfaces, a stripMarkdownFences helper, and runScheduleModelChain which iterates models, bounds each streaming call by remaining chain budget via AbortController+timeout, distinguishes budget aborts from other errors, and returns the first parsed {data, model} or null.
Streaming improvements: finishReason and external abort wiring
supabase/functions/_shared/streaming.ts
processStreamedResponse now returns {content, finishReason}, captures SSE finish_reason (throws on error), and callModelWithStreaming accepts an optional externalSignal, combines it with an internal 90s per-attempt timeout via AbortSignal.any, and treats finishReason==='length' as truncated (logs and returns null).
Integration into generate-schedule
supabase/functions/generate-schedule/index.ts
Replaces the previous in-file fallback loop with a single runScheduleModelChain(...) call wired to callModelWithStreaming, passing SCHEDULE_MODELS, request body, API key, and budgetMs: 130_000.
Unit tests for runScheduleModelChain
tests/unit/schedule-ai-runner.test.ts
Vitest suite covering success, fallback when responses are null or non-JSON, Markdown ```json fence stripping, terminal failure (all models fail), time-budget exhaustion (including fake-timers abort), and assertions that callStreaming is invoked once per attempted model with correct args and non-aborted signals; also tests `restaurantId` passthrough.

Sequence Diagram

sequenceDiagram
  participant App as generate-schedule
  participant Chain as runScheduleModelChain
  participant Stream as callModelWithStreaming

  App->>Chain: models[], budgetMs, callStreaming
  loop each model (until success or budget exhausted)
    Chain->>Stream: model, requestBody, apiKey, externalSignal
    Stream-->>Chain: {content, finishReason} | or reject on abort/error
    Chain->>Chain: stripMarkdownFences(content) & parse JSON
    alt JSON parse succeeds
      Chain-->>App: {data, model}
    else parse fails or content null or finishReason==='length'
      Chain->>Chain: continue to next model (or return null if budget expired)
    end
  end
  Chain-->>App: null (all failed or budget exhausted)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • toyiyo/nimble-pnl#121: Introduced/modified callModelWithStreaming streaming utilities which this change reuses and extends.
  • toyiyo/nimble-pnl#448: Previously implemented the SCHEDULE_MODELS fallback and parsing loop in generate-schedule/index.ts, now refactored into runScheduleModelChain.

Poem

🐰 I hopped through streams and fences tight,
Chased budgets fading in the night,
I stitched a chain to call each model,
Stripped code fences, parsed the waddle,
Tests guard the run — the schedule’s right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: switching to streaming AI to prevent body-read timeout issues in generate-schedule.
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 fix/generate-schedule-streaming

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01fb3775a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

edgeFunction: "generate-schedule",
restaurantId: restaurant_id,
callStreaming: callModelWithStreaming,
budgetMs: 130_000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce remaining budget before starting next model call

runScheduleModelChain only checks elapsed > budgetMs before each attempt, but each callModelWithStreaming attempt can still block for up to 90s. With budgetMs: 130_000 in generate-schedule, a first slow attempt near 90s can still start a second attempt and run past Supabase’s ~150s hard limit, causing the function to be killed before it returns any response. This reintroduces the timeout-kill failure mode the budget is meant to prevent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in commit f51db0e. runScheduleModelChain now passes a per-attempt AbortSignal set to fire after remaining = budgetMs - (now() - start), combined with the existing 90s ceiling via AbortSignal.any. When the chain budget elapses mid-call, the streaming fetch is aborted and the runner breaks (does not start the next model). New tests cover both the in-flight-abort and the insufficient-remaining-time-before-next-attempt cases.

Comment on lines +81 to +82
const data = JSON.parse(cleaned) as T;
return { data, model: model.name };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject streaming responses that end due to token limit

The new streaming path accepts the first JSON-parseable payload, but it no longer checks whether generation stopped with finish_reason === "length" (the previous non-streaming loop explicitly skipped truncated outputs). Since callStreaming returns only text, a token-limited completion that still parses as JSON can now be treated as success, returning an incomplete schedule instead of falling back to the next model.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in commit f51db0e. processStreamedResponse now captures finish_reason from the last delta; callModelWithStreaming returns null on finish_reason === 'length', restoring the previous non-streaming loop's truncation behaviour so the chain falls through to the next model.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/unit/schedule-ai-runner.test.ts (1)

105-124: ⚡ Quick win

Add a budget-edge test for “insufficient remaining time before next attempt.”

Current budget coverage is good, but it doesn’t assert behavior when remaining budget is too small for another model attempt. A targeted case here would prevent regressions once remaining-budget handling is tightened in the helper.

🤖 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 `@tests/unit/schedule-ai-runner.test.ts` around lines 105 - 124, Add a new unit
test that exercises the "insufficient remaining time before next attempt" edge:
call runScheduleModelChain with models [M1, M2, M3], a mock callStreaming
(ScheduleModelChainCallStreaming) that advances nowMs by the expected
per-attempt duration and returns null, a controlled now() returning nowMs, and
budgetMs set so that after two attempts the remaining budget is smaller than the
minimum time required for another model attempt; assert the result is null and
that callStreaming was called exactly twice to ensure the runner stops when
remaining time is insufficient.
🤖 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.

Inline comments:
In `@docs/superpowers/specs/2026-05-22-generate-schedule-streaming-design.md`:
- Around line 13-24: The fenced log block starting with the line
"[generate-schedule] Trying model: Gemini 2.5 Flash" should include a language
identifier to satisfy markdown lint and improve readability; update the opening
fence from ``` to ```text (or ```log) so the block becomes a labeled code block
(e.g., ```text) while leaving the contents (all lines beginning with
"[generate-schedule] ...") unchanged.

In `@supabase/functions/_shared/schedule-ai-runner.ts`:
- Around line 57-69: The current budget check before invoking each model (in the
for loop over models using now(), start and budgetMs) does not prevent an
in-flight streaming call (callStreaming) from exceeding the remaining wall-clock
budget; wrap or cancel each callStreaming invocation with an AbortController or
a Promise.race that enforces remainingMs = budgetMs - (now() - start) so the
request is aborted if it would exceed the chain budget, pass the abort signal
into callStreaming (or ensure callStreaming accepts a signal) and handle abort
errors by logging (using edgeFunction and model.name) and breaking out of the
loop to avoid overshooting the overall budget.

---

Nitpick comments:
In `@tests/unit/schedule-ai-runner.test.ts`:
- Around line 105-124: Add a new unit test that exercises the "insufficient
remaining time before next attempt" edge: call runScheduleModelChain with models
[M1, M2, M3], a mock callStreaming (ScheduleModelChainCallStreaming) that
advances nowMs by the expected per-attempt duration and returns null, a
controlled now() returning nowMs, and budgetMs set so that after two attempts
the remaining budget is smaller than the minimum time required for another model
attempt; assert the result is null and that callStreaming was called exactly
twice to ensure the runner stops when remaining time is insufficient.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6d6cfb4d-ffc3-4e5c-a222-a8f8662fb9e4

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd8e1a and 01fb377.

📒 Files selected for processing (4)
  • docs/superpowers/specs/2026-05-22-generate-schedule-streaming-design.md
  • supabase/functions/_shared/schedule-ai-runner.ts
  • supabase/functions/generate-schedule/index.ts
  • tests/unit/schedule-ai-runner.test.ts

Comment thread docs/superpowers/specs/2026-05-22-generate-schedule-streaming-design.md Outdated
Comment thread supabase/functions/_shared/schedule-ai-runner.ts Outdated
…t + reject truncated outputs

Addresses Codex P1 / CodeRabbit Major: runScheduleModelChain only
checked elapsed > budgetMs before each model attempt, leaving an
in-flight callModelWithStreaming free to run its full 90s timeout even
when the chain budget had already lapsed. Worst case: model 1 hangs
near 90s, the runner enters model 2 (elapsed=90s < 130s budget), model
2 hangs another 90s — total ~180s, past Supabase's ~150s hard kill.

- Thread an optional AbortSignal through ScheduleModelChainCallStreaming
  and callModelWithStreaming. The runner creates a per-attempt
  AbortController scheduled to abort after `remaining = budgetMs - elapsed`,
  passes the signal into the streaming call, and combines it with the
  existing 90s per-attempt timeout via AbortSignal.any.
- On AbortError caused by chain-budget elapse, break out of the loop
  instead of starting the next model — the budget is already gone.
- Restore the finish_reason=length skip the non-streaming loop used to
  perform: processStreamedResponse now returns { content, finishReason }
  (internal change) and callModelWithStreaming returns null when the
  model truncated at max_tokens, letting the chain fall through.

Adds two new unit tests:
- aborts in-flight call when remaining budget elapses (and stops)
- stops after second attempt when remaining budget is insufficient
Addresses CodeRabbit Minor nit on PR #515. Adds a `text` language hint
to the fenced log block so markdownlint MD040 stops flagging the file
and so rendered docs syntax-highlight the block as plain text.
@jdelgado2002

Copy link
Copy Markdown
Member Author

Phase 9d triage — feedback from this round

Pushed f51db0ef (budget enforcement + truncation reject) and 3c2982bb (markdown lint).

# Source Severity Location Action
1 Codex P1 bug generate-schedule/index.ts:538 Fixed in f51db0ef — runner now passes AbortSignal with remaining = budgetMs - elapsed into callModelWithStreaming; signal aborts the in-flight fetch if budget elapses. On AbortError from chain-budget elapse, runner breaks (does not start next model).
2 CodeRabbit Major bug schedule-ai-runner.ts:69 Fixed in f51db0ef (same fix as #1).
3 Codex P2 bug schedule-ai-runner.ts:82 Fixed in f51db0efprocessStreamedResponse now captures finish_reason; callModelWithStreaming returns null on finish_reason === "length", restoring the previous non-streaming loop's behaviour.
4 CodeRabbit Minor nit design.md:13 Fixed in 3c2982bb — added text language hint to fenced log block (MD040).
5 CodeRabbit Nitpick suggestion schedule-ai-runner.test.ts:105-124 Fixed in f51db0ef — added stops after second attempt when remaining budget is insufficient for another call test, plus aborts in-flight call when remaining budget elapses, then breaks to cover the new behaviour.

Verification: npx vitest run → 4088 pass, 1 skipped (unrelated). npm run typecheck clean. npm run build clean.

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@supabase/functions/_shared/schedule-ai-runner.ts`:
- Around line 68-70: The console.log call that prints `[${edgeFunction}] Trying
model: ${model.name} (streaming, remaining=${remaining}ms)` must be removed from
production; either replace it with a proper logger call (e.g.,
processLogger.debug or processLogger.info) using the same context (edgeFunction,
model.name, remaining) or gate the existing message behind a dev-only check (if
import.meta.env.DEV) so it never runs in production; update the model-attempt
logging in the same block where edgeFunction, model, and remaining are available
(the current console.log) to use the chosen non-console path.

In `@supabase/functions/_shared/streaming.ts`:
- Around line 163-165: Remove the new unguarded console.log in streaming.ts (the
log that prints `${modelConfig.name} stream completed...`) or wrap it behind a
dev-only guard; specifically either delete the console.log call inside the
stream completion block or condition it with import.meta.env.DEV (or your
project's equivalent DEV flag) so it only runs in development. Locate the
console.log that references modelConfig.name, content.length and finishReason
and update it accordingly to comply with the "no console.log in prod" guideline.
- Around line 103-109: The function callModelWithStreaming currently uses
requestBody: any and emits production console.log statements; change requestBody
to a safer type such as Record<string, unknown> (or a concrete interface) and
narrow-access fields where used (e.g., cast/validate requestBody.messages as
Message[], and types for temperature and max_tokens before use) so TypeScript
catches misuse, and remove all direct console.log calls in
callModelWithStreaming (replace with the project logger or wrap them with
import.meta.env.DEV guards) including the attempt/start/completed and rate-limit
logs; update any helper variables or type assertions inside
callModelWithStreaming to reflect the new type and perform runtime checks where
necessary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 820381fa-d2dc-4dc4-af6e-7b285b8a6930

📥 Commits

Reviewing files that changed from the base of the PR and between 01fb377 and 3c2982b.

📒 Files selected for processing (4)
  • docs/superpowers/specs/2026-05-22-generate-schedule-streaming-design.md
  • supabase/functions/_shared/schedule-ai-runner.ts
  • supabase/functions/_shared/streaming.ts
  • tests/unit/schedule-ai-runner.test.ts

Comment on lines +68 to +70
console.log(
`[${edgeFunction}] Trying model: ${model.name} (streaming, remaining=${remaining}ms)`,
);

@coderabbitai coderabbitai Bot May 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Remove production console.log in model-attempt logging.

Use a non-console logging path or gate this behind a dev-only condition; this log was newly added and violates the TS logging rule.

As per coding guidelines, "NEVER leave console.logs in production code - remove or gate with import.meta.env.DEV check".

🤖 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 `@supabase/functions/_shared/schedule-ai-runner.ts` around lines 68 - 70, The
console.log call that prints `[${edgeFunction}] Trying model: ${model.name}
(streaming, remaining=${remaining}ms)` must be removed from production; either
replace it with a proper logger call (e.g., processLogger.debug or
processLogger.info) using the same context (edgeFunction, model.name, remaining)
or gate the existing message behind a dev-only check (if import.meta.env.DEV) so
it never runs in production; update the model-attempt logging in the same block
where edgeFunction, model, and remaining are available (the current console.log)
to use the chosen non-console path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining. The import.meta.env.DEV guard is a Vite/frontend convention — Deno's import.meta doesn't expose .env, so the suggested gate cannot run in this file. The 70+ Supabase edge functions in this repo (including _shared/streaming.ts, _shared/ai-caller.ts, process-receipt, process-bank-statement) consistently use console.* as the canonical observability sink — those logs land in Supabase Edge Function Logs, which is exactly how this PR's root cause was diagnosed. This specific log (which model the chain is attempting, with remaining-budget annotation) is intentional production observability for the new budget-enforcement path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jdelgado2002, understood — thanks for the thorough context. You're right that import.meta.env.DEV is a Vite-specific convention with no equivalent in Deno, and given that console.* is the established, intentional observability pattern across all edge functions in this repo (landing directly in Supabase Edge Function Logs), this log is entirely appropriate. I'll withdraw the suggestion.

(ᵔᴥᵔ) 🐇


✏️ Learnings added
Learnt from: jdelgado2002
Repo: toyiyo/nimble-pnl PR: 515
File: supabase/functions/_shared/schedule-ai-runner.ts:68-70
Timestamp: 2026-05-23T14:20:10.774Z
Learning: In this repository's Deno/Supabase Edge Functions (under `supabase/functions/`), `console.log`, `console.warn`, and `console.error` are the canonical and intentional observability sink — logs surface directly in Supabase Edge Function Logs. Do NOT flag `console.*` calls in these files as violations; the frontend `import.meta.env.DEV` guard does not apply here and cannot run in the Deno runtime.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines 103 to 109
modelConfig: ModelConfig,
requestBody: any,
openRouterApiKey: string,
edgeFunction: string = 'unknown',
restaurantId?: string
restaurantId?: string,
externalSignal?: AbortSignal
): Promise<string | null> {

@coderabbitai coderabbitai Bot May 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="supabase/functions/_shared/streaming.ts"

# Ensure file exists
ls -la "$FILE"

# Show relevant sections around the reported line numbers
sed -n '80,140p' "$FILE" | cat -n

echo "----"
sed -n '140,220p' "$FILE" | cat -n

echo "----"
# Grep for any console.log usage in this file
rg -n "console\.log" "$FILE" || true

echo "----"
# Grep for explicit any typing in this file
rg -n ":\s*any\b|any\s*\)" "$FILE" || true

Repository: toyiyo/nimble-pnl

Length of output: 6279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="supabase/functions/_shared/streaming.ts"

ls -la "$FILE"
sed -n '80,140p' "$FILE" | cat -n
echo "----"
sed -n '140,220p' "$FILE" | cat -n
echo "----"
rg -n "console\.log" "$FILE" || true
echo "----"
rg -n ":\s*any\b|any\s*\)" "$FILE" || true

Repository: toyiyo/nimble-pnl

Length of output: 6279


🏁 Script executed:

FILE="supabase/functions/_shared/streaming.ts"
sed -n '95,120p' "$FILE"
sed -n '155,180p' "$FILE"
rg -n "console\.log" "$FILE" || true
rg -n "requestBody\s*:\s*any|:\s*any\b" "$FILE" || true

Repository: toyiyo/nimble-pnl

Length of output: 2494


🏁 Script executed:

ls -la supabase/functions/_shared || true
ls -la supabase/functions/_shared/streaming.ts || true
nl -ba supabase/functions/_shared/streaming.ts | sed -n '95,120p'

Repository: toyiyo/nimble-pnl

Length of output: 3015


Fix TypeScript any typing and remove production console.logs in callModelWithStreaming.

  • Replace requestBody: any with a concrete request payload type (or a temporary typed boundary like Record<string, unknown>) and narrow where fields such as messages, temperature, and max_tokens are used.
  • The added console.log calls (e.g., attempt/start/completed and rate-limit logs) violate the “no console.logs in production” guideline—remove them or gate them behind import.meta.env.DEV (or route through the existing logger utility).
🧰 Tools
🪛 ESLint

[error] 104-104: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 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 `@supabase/functions/_shared/streaming.ts` around lines 103 - 109, The function
callModelWithStreaming currently uses requestBody: any and emits production
console.log statements; change requestBody to a safer type such as
Record<string, unknown> (or a concrete interface) and narrow-access fields where
used (e.g., cast/validate requestBody.messages as Message[], and types for
temperature and max_tokens before use) so TypeScript catches misuse, and remove
all direct console.log calls in callModelWithStreaming (replace with the project
logger or wrap them with import.meta.env.DEV guards) including the
attempt/start/completed and rate-limit logs; update any helper variables or type
assertions inside callModelWithStreaming to reflect the new type and perform
runtime checks where necessary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining both items. (1) requestBody: any predates this PR — it's on the existing callModelWithStreaming signature; tightening it touches callAIWithFallbackStreaming and every edge function that constructs an OpenRouter request body, out of scope for this hotfix. (2) For the console.log concern, see reply on schedule-ai-runner.ts:70import.meta.env.DEV is Vite-only and the codebase convention for Deno edge functions is console.* straight to Supabase Edge Function Logs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jdelgado2002, understood on both counts — acknowledged and won't flag these again.

Noted for future reviews:

  • requestBody: any is a pre-existing signature shared across edge functions; tightening it requires a broader refactor touching callAIWithFallbackStreaming and is intentionally deferred.
  • Deno edge functions on Supabase use console.* directly (routed to Supabase Edge Function Logs); import.meta.env.DEV is Vite-only and does not apply here.

🐇 _(--)/


✏️ Learnings added
Learnt from: jdelgado2002
Repo: toyiyo/nimble-pnl PR: 515
File: supabase/functions/_shared/streaming.ts:103-109
Timestamp: 2026-05-23T14:20:15.967Z
Learning: In the toyiyo/nimble-pnl repository, `requestBody: any` in `callModelWithStreaming` (supabase/functions/_shared/streaming.ts) and related helpers like `callAIWithFallbackStreaming` is a pre-existing type that is intentionally broad because it is shared across all OpenRouter edge functions. Tightening it requires a separate, broader refactor and should not be flagged as a hotfix issue.

Learnt from: jdelgado2002
Repo: toyiyo/nimble-pnl PR: 515
File: supabase/functions/_shared/streaming.ts:103-109
Timestamp: 2026-05-23T14:20:15.967Z
Learning: In the toyiyo/nimble-pnl repository, Deno edge functions (under supabase/functions/) use `console.log`, `console.warn`, and `console.error` directly — this is the project convention, as output routes to Supabase Edge Function Logs. `import.meta.env.DEV` guards are Vite-only and do not apply to Deno/Supabase edge functions. Do not flag bare `console.*` calls in these files.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +163 to +165
console.log(
`✅ ${modelConfig.name} stream completed. Content length: ${content.length}, finish_reason: ${finishReason ?? 'null'}`
);

@coderabbitai coderabbitai Bot May 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Drop the new production console.log for stream completion.

This newly added log should be removed or dev-gated to keep production logging policy compliant.

As per coding guidelines, "NEVER leave console.logs in production code - remove or gate with import.meta.env.DEV check".

🤖 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 `@supabase/functions/_shared/streaming.ts` around lines 163 - 165, Remove the
new unguarded console.log in streaming.ts (the log that prints
`${modelConfig.name} stream completed...`) or wrap it behind a dev-only guard;
specifically either delete the console.log call inside the stream completion
block or condition it with import.meta.env.DEV (or your project's equivalent DEV
flag) so it only runs in development. Locate the console.log that references
modelConfig.name, content.length and finishReason and update it accordingly to
comply with the "no console.log in prod" guideline.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining. This log was already present in the file before this PR (prior text: ✅ ${modelConfig.name} stream completed. Content length: ${content.length}); this commit only extended it to include finish_reason so we can verify the new truncation-detection path in production logs. Same Deno-edge-function reasoning as schedule-ai-runner.ts:70.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jdelgado2002, understood — the log predates this PR and the extension adds meaningful observability for the new finish_reason=length detection path in production. Acknowledged and won't flag pre-existing logs extended for diagnostic purposes in this Deno edge-function context.

(ᴗ_ ᴗ。)


✏️ Learnings added
Learnt from: jdelgado2002
Repo: toyiyo/nimble-pnl PR: 515
File: supabase/functions/_shared/streaming.ts:163-165
Timestamp: 2026-05-23T14:20:16.991Z
Learning: In the `supabase/functions/_shared/streaming.ts` Deno edge-function file (and related edge functions in this repo), production `console.log` statements are intentional for observability. Pre-existing logs that are extended to surface new diagnostic fields (e.g., adding `finish_reason` to an existing stream-completion log) are acceptable and should not be flagged as guideline violations. The "no console.log in prod" guideline applies to newly introduced debug logs, not to extending pre-existing operational logs in Deno edge functions.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@jdelgado2002
jdelgado2002 merged commit 3b87745 into main May 23, 2026
31 checks passed
@jdelgado2002
jdelgado2002 deleted the fix/generate-schedule-streaming branch May 24, 2026 01:01
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