Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,11 @@ yarn-error.log*

# typescript
*.tsbuildinfo
next-env.d.ts
.venv/
venv/
__pycache__/
**/__pycache__/
*.py[cod]

# macOS
.DS_Store
51 changes: 51 additions & 0 deletions app/api/utilization-model/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { NextResponse } from "next/server";

// Avoid direct Node `process` typing to satisfy edge runtimes and linting
function getPythonBaseUrl(): string {
const env = (globalThis as any)?.process?.env as
| Record<string, string | undefined>
| undefined;
return env?.PY_UTILIZATION_BASE_URL || "http://127.0.0.1:8001";
}
Comment on lines +4 to +9
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Code duplication: Extract shared environment access logic

This getPythonBaseUrl function is duplicated from the predict route. Consider extracting it to a shared utility.

Create a shared utility file app/api/utilization-model/utils.ts:

// app/api/utilization-model/utils.ts
export function getPythonBaseUrl(): string {
  return process.env.PY_UTILIZATION_BASE_URL || "http://127.0.0.1:8001";
}

Then update both routes:

-// Avoid direct Node `process` typing to satisfy edge runtimes and linting
-function getPythonBaseUrl(): string {
-  const env = (globalThis as any)?.process?.env as
-    | Record<string, string | undefined>
-    | undefined;
-  return env?.PY_UTILIZATION_BASE_URL || "http://127.0.0.1:8001";
-}
+import { getPythonBaseUrl } from "./utils";
🤖 Prompt for AI Agents
In app/api/utilization-model/health/route.ts around lines 4 to 9, the
getPythonBaseUrl implementation is duplicated from the predict route; extract
the shared logic into a new module app/api/utilization-model/utils.ts that
exports getPythonBaseUrl (returning process.env.PY_UTILIZATION_BASE_URL ||
"http://127.0.0.1:8001"), then replace the local function in this file (and the
predict route) with an import from that utils file and remove the duplicated
code.


export async function GET(): Promise<Response> {
const baseUrl = getPythonBaseUrl();

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch(`${baseUrl}/health`, {
method: "GET",
signal: controller.signal,
headers: {
accept: "application/json",
},
// ensure server-side only
cache: "no-store",
});
clearTimeout(timeout);

if (!res.ok) {
const text = await res.text().catch(() => "");
return NextResponse.json(
{
status: "error",
code: res.status,
detail: text || "Python service health check failed",
},
{ status: 502 }
);
}

const data = await res.json().catch(() => ({}));
return NextResponse.json({ status: "ok", upstream: data }, { status: 200 });
} catch (error: unknown) {
clearTimeout(timeout);
const message = error instanceof Error ? error.message : "Unknown error";
const status = message.includes("The user aborted a request") ? 504 : 500;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve timeout detection consistency

Similar to the predict route, the timeout detection logic should be more robust.

-    const status = message.includes("The user aborted a request") ? 504 : 500;
+    const isTimeout = error instanceof Error && 
+      (error.name === 'AbortError' || message.includes("abort"));
+    const status = isTimeout ? 504 : 500;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const status = message.includes("The user aborted a request") ? 504 : 500;
const isTimeout = error instanceof Error &&
(error.name === 'AbortError' || message.includes("abort"));
const status = isTimeout ? 504 : 500;
🤖 Prompt for AI Agents
In app/api/utilization-model/health/route.ts around line 45, the timeout
detection currently only checks message.includes("The user aborted a request");
update it to mirror the predict route's more robust logic: safely extract the
error message (handle null/undefined), check for AbortError (error.name ===
'AbortError'), and look for multiple timeout indicators (e.g., "The user aborted
a request", "timed out", or other provider-specific timeout phrases) and set
status = 504 when any match; otherwise leave status = 500. Ensure you handle
non-string messages without throwing.

return NextResponse.json(
{ status: "error", code: status, detail: message },
{ status }
);
}
}
113 changes: 113 additions & 0 deletions app/api/utilization-model/predict/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Utilization Prediction API Route
*
* Bridges the Next.js app to the Python FastAPI utilization service. Accepts a
* JSON body of optional numeric features and forwards them to the upstream
* `/predict` endpoint. Validates the upstream response to ensure it includes
* all expected utilization count fields before returning to the client.
*
* Key behavior:
* - Reads `PY_UTILIZATION_BASE_URL` env var (defaults to http://127.0.0.1:8001)
* - 15s timeout with abort controller
* - Returns 502 if upstream fails or response shape is invalid
* - Returns 504 on client-aborted timeout, otherwise 500 on unknown errors
*/
import { NextResponse } from "next/server";

// Minimal schema validation without bringing in zod here
type PredictRequest = Record<string, unknown>;

interface PredictResponse {
pcp_visits: number;
outpatient_visits: number;
er_visits: number;
inpatient_admits: number;
home_health_visits: number;
rx_fills: number;
dental_visits: number;
equipment_purchases: number;
}

// Avoid direct Node `process` typing to satisfy edge runtimes and linting
function getPythonBaseUrl(): string {
const env = (globalThis as any)?.process?.env as
| Record<string, string | undefined>
| undefined;
return env?.PY_UTILIZATION_BASE_URL || "http://127.0.0.1:8001";
}

function isValidResponse(json: any): json is PredictResponse {
if (!json || typeof json !== "object") return false;
const keys = [
"pcp_visits",
"outpatient_visits",
"er_visits",
"inpatient_admits",
"home_health_visits",
"rx_fills",
"dental_visits",
"equipment_purchases",
] as const;
return keys.every(
(k) => typeof json[k] === "number" && Number.isFinite(json[k])
);
}

export async function POST(req: Request): Promise<Response> {
const baseUrl = getPythonBaseUrl();
let body: PredictRequest;
try {
body = await req.json();
} catch {
return NextResponse.json(
{ status: "error", detail: "Invalid JSON body" },
{ status: 400 }
);
}

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
try {
const res = await fetch(`${baseUrl}/predict`, {
method: "POST",
signal: controller.signal,
headers: {
"content-type": "application/json",
accept: "application/json",
},
body: JSON.stringify(body),
cache: "no-store",
});
clearTimeout(timeout);

const text = await res.text().catch(() => "");
let json: unknown;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}

if (!res.ok || !isValidResponse(json)) {
return NextResponse.json(
{
status: "error",
code: res.status,
detail: "Upstream prediction failed or returned invalid response",
upstream: text?.slice(0, 500),
},
{ status: 502 }
);
}

return NextResponse.json(json as PredictResponse, { status: 200 });
} catch (error: unknown) {
clearTimeout(timeout);
const message = error instanceof Error ? error.message : "Unknown error";
const status = message.includes("The user aborted a request") ? 504 : 500;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve timeout detection logic

The current string matching approach for detecting timeout errors is brittle and may not work across different JavaScript environments or future API changes.

-    const status = message.includes("The user aborted a request") ? 504 : 500;
+    const isTimeout = error instanceof Error && 
+      (error.name === 'AbortError' || message.includes("abort"));
+    const status = isTimeout ? 504 : 500;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const status = message.includes("The user aborted a request") ? 504 : 500;
const isTimeout = error instanceof Error &&
(error.name === 'AbortError' || message.includes("abort"));
const status = isTimeout ? 504 : 500;
🤖 Prompt for AI Agents
In app/api/utilization-model/predict/route.ts around line 107, the code
currently detects timeouts by string-matching the message "The user aborted a
request"; replace this brittle check with robust error-type checks: inspect the
thrown error's properties (e.g. error.name === 'AbortError' || error.code ===
'ETIMEDOUT' || error.type === 'aborted') and fallback to message substring only
if those properties are absent, then set status = 504 when any of those timeout
indicators are present and 500 otherwise; ensure the implementation safely
handles undefined error properties to avoid runtime exceptions.

return NextResponse.json(
{ status: "error", code: status, detail: message },
{ status }
);
}
}
Loading