diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index fd8770a03e..d1d93adb9f 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -443,7 +443,7 @@ public class ApplicationProperties { /** * Auto Form Detection settings. The model itself is downloaded on demand by an admin (see - * {@code /api/v1/ai/form-detection-model/*}); only lightweight pointers are persisted here. + * {@code /api/v1/form/form-detection-model/*}); only lightweight pointers are persisted here. */ @Data public static class FormDetection { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java index fcc38f89a6..b3722facd6 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java @@ -41,10 +41,14 @@ import stirling.software.proprietary.formdetection.service.FormDetectionModelMan * Server-side detection endpoint. Gated behind the {@code form-detection} endpoint key, which is * disabled until a model is installed (so the tool tile is greyed in the UI). Returns the shared * detection schema, or - when {@code applyToPdf=true} - the AcroForm-applied PDF. + * + *

Deliberately under {@code /api/v1/form}, not {@code /api/v1/ai}: anything on the AI prefix is + * classified billable by {@code BillableOperationClassifier} and blocked with 402 when the instance + * is unlinked - which would gate on-device detection that never touches the server. */ @Slf4j @RestController -@RequestMapping("/api/v1/ai/form-detection") +@RequestMapping("/api/v1/form/form-detection") @ConditionalOnClass(name = "ai.onnxruntime.OrtEnvironment") @RequiredArgsConstructor @Tag(name = "Auto Form Detection") diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelController.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelController.java index 6c8b3f55aa..9b8b34cca6 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelController.java @@ -26,10 +26,14 @@ import stirling.software.proprietary.formdetection.service.FormDetectionModelMan * Admin-managed lifecycle for the Auto Form Detection model. Lives under the never-gated {@code * form-detection-model} endpoint key so install/status stay reachable while the feature itself (the * {@code form-detection} detect endpoint) is disabled until a model is ready. + * + *

Deliberately under {@code /api/v1/form}, not {@code /api/v1/ai}: anything on the AI prefix is + * classified billable by {@code BillableOperationClassifier} and blocked with 402 when the instance + * is unlinked - which would gate on-device detection that never touches the server. */ @Slf4j @RestController -@RequestMapping("/api/v1/ai/form-detection-model") +@RequestMapping("/api/v1/form/form-detection-model") @RequiredArgsConstructor @Tag(name = "Auto Form Detection") public class FormDetectionModelController { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelServeController.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelServeController.java index 7cc59368ce..a7bddf1ed2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelServeController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelServeController.java @@ -36,7 +36,7 @@ import stirling.software.proprietary.formdetection.service.FormDetectionModelMan */ @Slf4j @RestController -@RequestMapping("/api/v1/ai/form-detection-model") +@RequestMapping("/api/v1/form/form-detection-model") @RequiredArgsConstructor @Tag(name = "Auto Form Detection") public class FormDetectionModelServeController { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/model/ModelStatusResponse.java b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/model/ModelStatusResponse.java index 3b7f19f052..f6b87f74d6 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/model/ModelStatusResponse.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/formdetection/model/ModelStatusResponse.java @@ -7,7 +7,7 @@ import lombok.Data; import lombok.NoArgsConstructor; /** - * Snapshot returned by {@code GET /api/v1/ai/form-detection-model/status}. Includes the full + * Snapshot returned by {@code GET /api/v1/form/form-detection-model/status}. Includes the full * catalog so the browser can read the active model's parity-critical pipeline spec. */ @Data diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/controller/FormDetectionControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/controller/FormDetectionControllerTest.java index beaee0e287..e9ac398b1d 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/controller/FormDetectionControllerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/controller/FormDetectionControllerTest.java @@ -46,7 +46,7 @@ class FormDetectionControllerTest { Mockito.when(manager.isReady()).thenReturn(false); mvc(manager, Mockito.mock(OnnxFormDetector.class), Mockito.mock(PageRasterizer.class)) - .perform(multipart("/api/v1/ai/form-detection/detect").file(pdf())) + .perform(multipart("/api/v1/form/form-detection/detect").file(pdf())) .andExpect(status().isServiceUnavailable()) .andExpect(jsonPath("$.reason").value("DEPENDENCY")); } @@ -62,7 +62,7 @@ class FormDetectionControllerTest { .thenReturn(List.of()); // no pages -> no detections, detector never called mvc(manager, Mockito.mock(OnnxFormDetector.class), rasterizer) - .perform(multipart("/api/v1/ai/form-detection/detect").file(pdf())) + .perform(multipart("/api/v1/form/form-detection/detect").file(pdf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.detections").isArray()) .andExpect(jsonPath("$.detections").isEmpty()); @@ -83,7 +83,7 @@ class FormDetectionControllerTest { Mockito.when(rasterizer.rasterize(Mockito.any(), Mockito.anyInt())).thenReturn(tooMany); mvc(manager, Mockito.mock(OnnxFormDetector.class), rasterizer) - .perform(multipart("/api/v1/ai/form-detection/detect").file(pdf())) + .perform(multipart("/api/v1/form/form-detection/detect").file(pdf())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.reason").value("LIMIT")); } @@ -99,7 +99,7 @@ class FormDetectionControllerTest { mvc(manager, Mockito.mock(OnnxFormDetector.class), rasterizer) .perform( - multipart("/api/v1/ai/form-detection/detect") + multipart("/api/v1/form/form-detection/detect") .file(pdf()) .param("confThreshold", "-42")) .andExpect(status().isOk()) diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelControllerTest.java index 2ef8d0edfe..4b507f5ef0 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelControllerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/formdetection/controller/FormDetectionModelControllerTest.java @@ -34,7 +34,7 @@ class FormDetectionModelControllerTest { Mockito.when(manager.status()).thenReturn(notInstalled()); mvc(manager) - .perform(get("/api/v1/ai/form-detection-model/status")) + .perform(get("/api/v1/form/form-detection-model/status")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("not_installed")) .andExpect(jsonPath("$.writable").value(true)); @@ -47,7 +47,7 @@ class FormDetectionModelControllerTest { mvc(manager) .perform( - post("/api/v1/ai/form-detection-model/install") + post("/api/v1/form/form-detection-model/install") .contentType(MediaType.APPLICATION_JSON) .content("{\"modelId\":\"\"}")) .andExpect(status().isBadRequest()); @@ -60,7 +60,7 @@ class FormDetectionModelControllerTest { mvc(manager) .perform( - post("/api/v1/ai/form-detection-model/install") + post("/api/v1/form/form-detection-model/install") .contentType(MediaType.APPLICATION_JSON) .content("{\"modelId\":\"ffdnet-s\"}")) .andExpect(status().isAccepted()); @@ -76,7 +76,7 @@ class FormDetectionModelControllerTest { mvc(manager) .perform( - post("/api/v1/ai/form-detection-model/install") + post("/api/v1/form/form-detection-model/install") .contentType(MediaType.APPLICATION_JSON) .content("{\"modelId\":\"ffdnet-s\"}")) .andExpect(status().isConflict()); @@ -88,7 +88,7 @@ class FormDetectionModelControllerTest { Mockito.when(manager.status()).thenReturn(notInstalled()); mvc(manager) - .perform(delete("/api/v1/ai/form-detection-model")) + .perform(delete("/api/v1/form/form-detection-model")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("not_installed")); } diff --git a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx index 9804ea8078..5dc8e32357 100644 --- a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx @@ -486,7 +486,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { "Automatically detect form fields with AI and make your PDF fillable.", ), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.GENERAL, + subcategoryId: SubcategoryId.AUTOMATION, maxFiles: 1, endpoints: ["form-detection"], unavailableMessage: t( diff --git a/frontend/editor/src/core/hooks/tools/autoFormDetection/useAutoFormDetectionOperation.ts b/frontend/editor/src/core/hooks/tools/autoFormDetection/useAutoFormDetectionOperation.ts index edfe4a6b39..4643aee7e0 100644 --- a/frontend/editor/src/core/hooks/tools/autoFormDetection/useAutoFormDetectionOperation.ts +++ b/frontend/editor/src/core/hooks/tools/autoFormDetection/useAutoFormDetectionOperation.ts @@ -22,8 +22,8 @@ import { resolveConfidence, } from "@app/hooks/tools/autoFormDetection/useAutoFormDetectionParameters"; -const DETECT_ENDPOINT = "/api/v1/ai/form-detection/detect"; -const STATUS_URL = "/api/v1/ai/form-detection-model/status"; +const DETECT_ENDPOINT = "/api/v1/form/form-detection/detect"; +const STATUS_URL = "/api/v1/form/form-detection-model/status"; export const buildAutoFormDetectionFormData = ( parameters: AutoFormDetectionParameters, diff --git a/frontend/editor/src/core/hooks/useFormDetectionModelStatus.ts b/frontend/editor/src/core/hooks/useFormDetectionModelStatus.ts index 05b5fee30d..63a5fb5303 100644 --- a/frontend/editor/src/core/hooks/useFormDetectionModelStatus.ts +++ b/frontend/editor/src/core/hooks/useFormDetectionModelStatus.ts @@ -51,10 +51,10 @@ export interface FormDetectionModelStatus { downloadingModelId?: string | null; } -const STATUS_URL = "/api/v1/ai/form-detection-model/status"; -const INSTALL_URL = "/api/v1/ai/form-detection-model/install"; -const CONFIG_URL = "/api/v1/ai/form-detection-model/config"; -const MODEL_URL = "/api/v1/ai/form-detection-model"; +const STATUS_URL = "/api/v1/form/form-detection-model/status"; +const INSTALL_URL = "/api/v1/form/form-detection-model/install"; +const CONFIG_URL = "/api/v1/form/form-detection-model/config"; +const MODEL_URL = "/api/v1/form/form-detection-model"; /** * Polls the Auto Form Detection model status and exposes admin install/uninstall actions. diff --git a/frontend/editor/src/core/services/formDetection/modelCache.ts b/frontend/editor/src/core/services/formDetection/modelCache.ts index 199170b1ae..c171a3de5c 100644 --- a/frontend/editor/src/core/services/formDetection/modelCache.ts +++ b/frontend/editor/src/core/services/formDetection/modelCache.ts @@ -1,7 +1,7 @@ // Fetch the active .onnx from the backend serve endpoint, verify its SHA-256, and keep it in the // Cache API keyed by checksum so it is downloaded only once per device (then reused across reloads). -const MODEL_FILE_URL = "/api/v1/ai/form-detection-model/file"; +const MODEL_FILE_URL = "/api/v1/form/form-detection-model/file"; const CACHE_NAME = "stirling-form-detection-models"; function toHex(buf: ArrayBuffer): string { diff --git a/frontend/editor/src/core/tests/stubbed/auto-form-detection-walkthrough.spec.ts b/frontend/editor/src/core/tests/stubbed/auto-form-detection-walkthrough.spec.ts index aecdf53067..18c0ca1c1f 100644 --- a/frontend/editor/src/core/tests/stubbed/auto-form-detection-walkthrough.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/auto-form-detection-walkthrough.spec.ts @@ -73,7 +73,7 @@ function modelStatus(overrides: Record = {}) { } async function stubStatus(page: Page, overrides: Record = {}) { - await page.route("**/api/v1/ai/form-detection-model/status", (r: Route) => + await page.route("**/api/v1/form/form-detection-model/status", (r: Route) => r.fulfill({ json: modelStatus(overrides) }), ); } @@ -111,7 +111,7 @@ async function stubDetect( ) { const pdf = fs.readFileSync(SAMPLE_PDF); const detections = opts.detections ?? DETECTIONS; - await page.route("**/api/v1/ai/form-detection/detect", async (r: Route) => { + await page.route("**/api/v1/form/form-detection/detect", async (r: Route) => { if (opts.delayMs) await new Promise((res) => setTimeout(res, opts.delayMs)); const body = r.request().postData() ?? ""; if (body.includes("applyToPdf") && body.includes("true")) { diff --git a/frontend/editor/src/core/tests/stubbed/auto-form-detection.spec.ts b/frontend/editor/src/core/tests/stubbed/auto-form-detection.spec.ts index ea483e2e7f..4e9f9c2492 100644 --- a/frontend/editor/src/core/tests/stubbed/auto-form-detection.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/auto-form-detection.spec.ts @@ -35,7 +35,7 @@ const MODEL_STATUS_NOT_INSTALLED = { }; async function stubModelStatus(page: Page) { - await page.route("**/api/v1/ai/form-detection-model/status", (route) => + await page.route("**/api/v1/form/form-detection-model/status", (route) => route.fulfill({ json: MODEL_STATUS_NOT_INSTALLED }), ); } diff --git a/frontend/editor/src/core/tests/stubbed/ui-review-capture.spec.ts b/frontend/editor/src/core/tests/stubbed/ui-review-capture.spec.ts index bbb25e35a1..3c45ae6ea2 100644 --- a/frontend/editor/src/core/tests/stubbed/ui-review-capture.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/ui-review-capture.spec.ts @@ -77,7 +77,7 @@ function modelStatus(overrides: StatusOverrides = {}) { } async function stubStatus(page: Page, overrides: StatusOverrides = {}) { - await page.route("**/api/v1/ai/form-detection-model/status", (r: Route) => + await page.route("**/api/v1/form/form-detection-model/status", (r: Route) => r.fulfill({ json: modelStatus(overrides) }), ); } @@ -117,7 +117,7 @@ async function stubDetect(page: Page, delayMs = 0) { confidence: 0.74, }, ]; - await page.route("**/api/v1/ai/form-detection/detect", async (r: Route) => { + await page.route("**/api/v1/form/form-detection/detect", async (r: Route) => { if (delayMs) await new Promise((res) => setTimeout(res, delayMs)); const body = r.request().postData() ?? ""; if (body.includes("applyToPdf") && body.includes("true")) {