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