> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reducto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# OCR provider configuration

> Configure cloud OCR providers for on-premise Reducto installations

export const PasswordProtect = ({children}) => {
  const [password, setPassword] = useState("");
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [error, setError] = useState("");
  const correctPasswordHash = "9daff39ca2584edc54444193f62e5e54dce0bcd5e5d604b1748c79bfb3d7d1fd";
  const hashPassword = async inputPassword => {
    const encoder = new TextEncoder();
    const data = encoder.encode(inputPassword);
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  };
  useEffect(() => {
    const storedPassword = localStorage.getItem("reducto-onprem-password");
    if (storedPassword) {
      checkStoredPassword(storedPassword);
    }
  }, []);
  const checkStoredPassword = async storedPassword => {
    const hashedStored = await hashPassword(storedPassword);
    if (hashedStored === correctPasswordHash) {
      setIsAuthenticated(true);
      setError("");
    }
  };
  const handleSubmit = async e => {
    e.preventDefault();
    const hashedInput = await hashPassword(password);
    if (hashedInput === correctPasswordHash) {
      setIsAuthenticated(true);
      setError("");
      localStorage.setItem("reducto-onprem-password", password);
    } else {
      setError("Incorrect password. Please try again.");
      setPassword("");
    }
  };
  if (isAuthenticated) {
    return <>{children}</>;
  }
  return <div style={{
    padding: "2rem",
    border: "2px solid #e2e8f0",
    borderRadius: "8px",
    textAlign: "center",
    margin: "2rem 0"
  }}>
      <div style={{
    fontSize: "2rem",
    marginBottom: "1rem"
  }}>🔒</div>
      <h2>Protected Content</h2>
      <p>This content requires a password to access.</p>
      <form onSubmit={handleSubmit} style={{
    marginTop: "1rem"
  }}>
        <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Enter password" style={{
    padding: "0.5rem",
    border: "1px solid #cbd5e0",
    borderRadius: "4px",
    marginRight: "0.5rem",
    fontSize: "1rem"
  }} />
        <button type="submit" style={{
    padding: "0.5rem 1rem",
    backgroundColor: "#5c0c5c",
    color: "white",
    border: "none",
    borderRadius: "4px",
    cursor: "pointer",
    fontSize: "1rem"
  }}>
          Unlock
        </button>
      </form>
      {error && <p style={{
    color: "red",
    marginTop: "1rem"
  }}>{error}</p>}
    </div>;
};

<PasswordProtect>
  Reducto supports multiple cloud OCR providers. By default, Reducto uses its own local OCR models (which require GPU), but you can configure cloud OCR providers for broader language support or to avoid provisioning GPU hardware for OCR.

  ## Provider overview

  | Provider              | Credentials needed                                                                                         | Best for                                                                              |
  | --------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
  | **Reducto local OCR** | None (default)                                                                                             | General-purpose, privacy-sensitive, GPU-equipped deployments                          |
  | **AWS Textract**      | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`                                                               | AWS-native deployments                                                                |
  | **Azure Vision Read** | `AZURE_VISION_ENDPOINT` + `AZURE_VISION_KEY` (or `AZURE_VISION_ARRAY`)                                     | Azure-native deployments                                                              |
  | **GCP Vision API**    | `GOOGLE_APPLICATION_CREDENTIALS` + `GCP_PROJECT_ID` (or `GCP_SERVICE_ACCOUNT_EMAIL` for workload identity) | GCP-native deployments, cross-cloud deployments wanting GCP OCR, 60+ language support |

  ## How OCR provider is selected

  The `ocr_system` parameter in API requests controls provider routing. Only two values are available to API callers: `standard` (default) and `legacy`.

  **`standard` (default) routing priority:**

  1. GCP Vision API, if GCP credentials are configured
  2. Azure Vision or AWS Textract, if configured (Azure is preferred over Textract)

  **`legacy` routing priority:**

  1. Azure Vision or AWS Textract, if configured (Azure is preferred over Textract)
  2. GCP Vision API, as fallback if only GCP credentials are available

  In practice: if you configure GCP credentials, `standard` requests use GCP Vision. If you only have Azure/AWS credentials, both `standard` and `legacy` use those.

  ### Auto-detection: GCP-only environments

  When **only** GCP credentials are configured (no AWS or Azure), Reducto auto-detects this and routes all OCR through GCP Vision regardless of the `ocr_system` value. This requires:

  * No Azure credentials (`AZURE_VISION_ENDPOINT` and `AZURE_VISION_ARRAY` both unset)
  * No AWS credentials (`AWS_ACCESS_KEY_ID` unset)
  * GCP credentials available (`GOOGLE_APPLICATION_CREDENTIALS` or `GCP_SERVICE_ACCOUNT_EMAIL`)
  * `GCP_PROJECT_ID` is set

  ***

  ## AWS Textract

  ### Environment variables

  | Variable                | Description                                             | Required |
  | ----------------------- | ------------------------------------------------------- | -------- |
  | `AWS_ACCESS_KEY_ID`     | AWS access key ID                                       | Yes      |
  | `AWS_SECRET_ACCESS_KEY` | AWS secret access key                                   | Yes      |
  | `TEXTRACT_REGIONS`      | Comma-separated `region:quota` pairs for load balancing | No       |

  Default regions: `us-east-2:100,us-east-1:100,us-west-2:100,ap-south-1:5,eu-west-1:5`

  ```bash theme={null}
  # Format: region:quota pairs (quota defaults to 1 if omitted)
  TEXTRACT_REGIONS=us-east-1:50,us-west-2:25,eu-west-1:10

  # Government Cloud
  TEXTRACT_REGIONS=us-gov-west-1:10,us-gov-east-1:10
  ```

  ### Required IAM permissions

  ```json theme={null}
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": "textract:DetectDocumentText",
        "Resource": "*"
      }
    ]
  }
  ```

  ***

  ## Azure Vision Read

  ### Environment variables

  **Single endpoint:**

  | Variable                | Description                        | Required                                |
  | ----------------------- | ---------------------------------- | --------------------------------------- |
  | `AZURE_VISION_ENDPOINT` | Azure Computer Vision endpoint URL | Yes (unless using `AZURE_VISION_ARRAY`) |
  | `AZURE_VISION_KEY`      | Azure Computer Vision API key      | Yes (unless using `AZURE_VISION_ARRAY`) |

  **Multiple endpoints (load balancing and failover):**

  | Variable                      | Description                                               | Required |
  | ----------------------------- | --------------------------------------------------------- | -------- |
  | `AZURE_VISION_ARRAY`          | JSON array of `{"endpoint": "...", "key": "..."}` objects | No       |
  | `AZURE_VISION_ARRAY_STRATEGY` | `load_balance` (default) or `priority`                    | No       |

  ```bash theme={null}
  AZURE_VISION_ARRAY='[
    {"endpoint": "https://vision-east.cognitiveservices.azure.com/", "key": "<key-1>"},
    {"endpoint": "https://vision-west.cognitiveservices.azure.com/", "key": "<key-2>"}
  ]'
  ```

  | Strategy                 | Behavior                                                                                       |
  | ------------------------ | ---------------------------------------------------------------------------------------------- |
  | `load_balance` (default) | Randomly selects an endpoint per request. On transient error, retries then fails over to next. |
  | `priority`               | Tries endpoints in order. On transient error, retries then fails over to next.                 |

  **Timeout and retry tuning:**

  | Variable                          | Default         | Description                                                      |
  | --------------------------------- | --------------- | ---------------------------------------------------------------- |
  | `AZURE_VISION_TOTAL_CALL_TIMEOUT` | `45` (seconds)  | Hard cancellation cap per `analyze` call, including all retries. |
  | `AZURE_VISION_WALL_TIMEOUT`       | `120` (seconds) | SDK-level wall-clock timeout (checked between retries).          |
  | `AZURE_VISION_CONNECTION_TIMEOUT` | `10` (seconds)  | Per-attempt TCP connect timeout.                                 |
  | `AZURE_VISION_READ_TIMEOUT`       | `30` (seconds)  | Per-attempt socket read timeout.                                 |
  | `AZURE_VISION_MAX_RETRIES`        | `2`             | Retries per endpoint. Total attempts = `1 + MAX_RETRIES`.        |

  Transient errors (408, 429, 5xx, network errors) trigger retries and failover. Non-retryable 4xx errors surface immediately.

  <Note>
    When `AZURE_VISION_ARRAY` is set, `AZURE_VISION_ENDPOINT` and `AZURE_VISION_KEY` are ignored.
  </Note>

  ***

  ## GCP Vision API

  ### Environment variables

  | Variable                         | Description                                                                                                                                                           | Required                                       |
  | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
  | `GOOGLE_APPLICATION_CREDENTIALS` | Path to service account key JSON file, or the raw JSON content. Handles both OCR routing detection and Vision API authentication via Application Default Credentials. | Yes (unless using `GCP_SERVICE_ACCOUNT_EMAIL`) |
  | `GCP_PROJECT_ID`                 | GCP project ID for quota attribution                                                                                                                                  | Yes                                            |
  | `GCP_SERVICE_ACCOUNT_EMAIL`      | Service account email for workload identity auth (alternative to `GOOGLE_APPLICATION_CREDENTIALS`).                                                                   | No                                             |
  | `GCP_API_KEY`                    | [API key](https://console.cloud.google.com/apis/credentials) with Cloud Vision API enabled. Optional; if set, the Vision API client uses this instead of ADC.         | No                                             |
  | `GCP_REGION`                     | Region for Vertex AI                                                                                                                                                  | No (default: `us-central1`)                    |

  ### Authentication methods

  **Option 1: Service account key (recommended)**

  `GOOGLE_APPLICATION_CREDENTIALS` handles everything: Reducto uses it to detect GCP availability for routing, and the Vision API client picks it up via Application Default Credentials (ADC).

  ```bash theme={null}
  GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json
  GCP_PROJECT_ID=your-project-id
  ```

  `GOOGLE_APPLICATION_CREDENTIALS` can be a file path or the raw JSON content of the service account key.

  **Option 2: Workload identity (GKE)**

  Set `GCP_SERVICE_ACCOUNT_EMAIL` to use GKE workload identity. No key file needed.

  ```bash theme={null}
  GCP_SERVICE_ACCOUNT_EMAIL=ocr-sa@your-project.iam.gserviceaccount.com
  GCP_PROJECT_ID=your-project-id
  ```

  ### Required GCP API and roles

  * **Cloud Vision API** must be enabled on the project (`GCP_PROJECT_ID`).
  * If using a service account, it needs at minimum the `roles/cloudvision.user` role.
  * If using `GOOGLE_APPLICATION_CREDENTIALS` for broader GCP features (storage, Vertex AI), the service account also needs:
    * `roles/aiplatform.user` (for Vertex AI / Gemini LLM calls)
    * `roles/storage.objectAdmin` (if using GCS for file storage)

  ***

  ## Cross-cloud OCR: using GCP Vision on non-GCP infrastructure

  If you run on Azure or AWS but want GCP Vision for OCR, set `GCP_OCR_ONLY=true` so Reducto uses GCP only for Vision API calls and does not initialize GCS storage.

  | Variable       | Description                                       | Default |
  | -------------- | ------------------------------------------------- | ------- |
  | `GCP_OCR_ONLY` | Use GCP for Vision API OCR only, not for storage. | `false` |

  ### Example: Azure infrastructure with GCP Vision OCR

  ```bash theme={null}
  # --- Azure storage (unchanged) ---
  AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...

  # --- Azure Vision (optional, keep as fallback or remove) ---
  # If you want to remove Azure Vision entirely, delete these.
  # If you keep them, 'legacy' OCR requests will still use Azure Vision,
  # while 'standard' (default) requests will use GCP Vision.
  AZURE_VISION_ENDPOINT=https://your-vision.cognitiveservices.azure.com/
  AZURE_VISION_KEY=your-azure-vision-key

  # --- GCP Vision OCR ---
  GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json
  GCP_PROJECT_ID=your-gcp-project-id
  GCP_OCR_ONLY=true
  ```

  With this configuration:

  * **Default API requests** (`ocr_system=standard` or unset) route to **GCP Vision API**
  * **`ocr_system=legacy` requests** route to **Azure Vision** (if Azure Vision credentials are still set) or **GCP Vision** (if Azure Vision credentials are removed)
  * **File storage** remains on **Azure Blob Storage**

  ### Example: AWS infrastructure with GCP Vision OCR

  ```bash theme={null}
  # --- AWS storage ---
  AWS_ACCESS_KEY_ID=your-aws-key
  AWS_SECRET_ACCESS_KEY=your-aws-secret
  BUCKET=your-s3-bucket

  # --- GCP Vision OCR ---
  GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json
  GCP_PROJECT_ID=your-gcp-project-id
  GCP_OCR_ONLY=true
  ```

  ### Migration path: switching from Azure Vision to GCP Vision

  If you are currently using Azure Vision and want to switch to GCP Vision as your primary OCR:

  1. **Add GCP credentials** to your deployment:
     ```bash theme={null}
     GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json
     GCP_PROJECT_ID=your-gcp-project-id
     GCP_OCR_ONLY=true
     ```

  2. **Test with a few requests.** The default `ocr_system=standard` will now route to GCP Vision. Verify OCR quality meets your expectations.

  3. **Optionally remove Azure Vision credentials.** If you no longer need Azure Vision as a fallback for `legacy` requests, remove `AZURE_VISION_ENDPOINT` and `AZURE_VISION_KEY` (or `AZURE_VISION_ARRAY`). This simplifies the configuration.

  4. **`GCP_OCR_ONLY` should remain set** as long as you are using non-GCP file storage.

  ***

  ## GPU OCR deployment (`OCR_ONLY` mode)

  For deployments that want to use Reducto's own OCR models (which run on GPU) as a dedicated service, set `OCR_ONLY=true`. This starts a lightweight service exposing only the `/ocr` endpoint.

  | Variable                  | Description                                                                                                                                  | Default              |
  | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
  | `OCR_ONLY`                | Enable OCR-only mode                                                                                                                         | `false`              |
  | `OFFLINE`                 | Must be `1` for on-prem deployments                                                                                                          | -                    |
  | `HTTP_WORKERS`            | HTTP worker processes                                                                                                                        | `8`                  |
  | `MAX_PROCESSING_REQUESTS` | Max concurrent OCR requests per worker                                                                                                       | `1`                  |
  | `MAX_QUEUED_REQUESTS`     | Max queued requests per worker                                                                                                               | `0`                  |
  | `OCR_THREADS`             | Thread pool size for OCR                                                                                                                     | `5`                  |
  | `NUM_GPUS`                | GPUs available (for CUDA device assignment)                                                                                                  | `0`                  |
  | `REQUIRE_GPU`             | Fail startup if ONNX Runtime cannot reach the GPU. Defaults to on for GPU images. Set `false` to intentionally run a GPU image on CPU nodes. | (GPU images: `true`) |

  <Warning>
    GPU images refuse to start when ONNX Runtime cannot actually reach the GPU, rather
    than silently falling back to CPU at a fraction of the throughput. The startup log
    names the cause and the commands to confirm it.

    The most common cause on NVSwitch hardware (HGX/SXM H100, H200, A100) is
    `nvidia-fabricmanager` not running, or running at a version that does not exactly
    match the installed driver. Note that `nvidia-smi` still reports a perfectly healthy
    GPU in this state, because NVML does not touch fabric state. To confirm on the node:

    ```sh theme={null}
    nvidia-smi -q | grep -i -A3 fabric      # expect "State: Completed"
    systemctl status nvidia-fabricmanager
    cat /proc/driver/nvidia/version         # must match the fabricmanager package version
    ```

    Repair the node before upgrading the image: a deployment that was previously serving
    degraded CPU-only OCR will now fail its readiness check instead. If you need to run a
    GPU image on CPU nodes deliberately, set `REQUIRE_GPU=false`.
  </Warning>

  ```yaml theme={null}
  # Helm values
  gpuOcr:
    enabled: true
    replicaCount: 1
    resources:
      requests:
        nvidia.com/gpu: 1
      limits:
        nvidia.com/gpu: 1
  ```

  The Helm chart sets `OCR_ONLY=true` and `OFFLINE=1` automatically.

  The readiness probes require a GPU OCR image that includes the `/ready` endpoint;
  use the image built from this change or a later release. Because the chart and
  image are independently upgradeable, both probes are disabled by default for
  compatibility with existing pinned images. Enable both together after upgrading
  the image. If `gpuOcrImage.tag` is pinned to an older image, upgrade that image
  first; an older image with the probes enabled returns `404` and never becomes
  Ready.

  ### Startup behavior and readiness

  The pod loads its OCR models **before** accepting traffic, rather than on the first
  request. When the probes are enabled, a pod on a node whose GPU is unusable never
  becomes Ready, so Kubernetes routes no traffic to it and the problem is visible
  from `kubectl get pods` at deploy time instead of surfacing later as failed jobs.

  Every worker process loads its own copy, one at a time. With the probes enabled,
  the pod reports Ready only after all `HTTP_WORKERS` finish. Reporting Ready earlier
  would advertise full capacity while most workers were still loading, and
  `MAX_QUEUED_REQUESTS` defaults to `0`, so the surplus comes back as `429`. After the pod has been fully warm once
  it stays in service while any worker can serve, so losing one worker later reduces
  capacity instead of removing the pod.

  Size `HTTP_WORKERS` against GPU and host memory, not just latency: every worker
  holds its own copy of the models, and the pod now needs all of them resident before
  it serves. Set it under `gpuOcr.extraEnv`, so changing the OCR worker count does
  not also resize the main HTTP tier:

  ```yaml theme={null}
  gpuOcr:
    extraEnv:
      - name: HTTP_WORKERS
        value: "4"
  ```

  The shared environment remains the fallback for existing installations; when a
  name appears in both places, `gpuOcr.extraEnv` wins for the GPU OCR pod only.

  Read the real per-worker load time off the startup logs to check it against the
  readiness budget:

  ```sh theme={null}
  kubectl logs deploy/<release>-reducto-gpu-ocr | grep "OCR model load complete"
  ```

  Warm-up is roughly that duration times `HTTP_WORKERS`, because the loads are
  serialized. If that exceeds the probe budget below, lower `HTTP_WORKERS` or raise
  `failureThreshold`. The rollout deadline also includes image pull and GPU-node
  scheduling time.

  When the cause is the GPU check, the pod stays running (not `CrashLoopBackOff`) so
  you can inspect it for as long as the `startupProbe` budget lasts, about 500s with
  the shipped values. After that the kubelet kills the container and Kubernetes
  restarts it, so a persistently unusable GPU does end in a restart loop; use
  `kubectl logs --previous` once that starts. Any other model-load failure, such as
  missing weights or an out-of-memory kill, ends the container immediately.

  `/ready` returns `503` until every worker is warm and reports how many are;
  `/health` remains a plain liveness signal. Once the pod has been fully warm it
  stays in service while any worker can serve, so losing a worker later reduces
  capacity instead of removing the pod.

  ```sh theme={null}
  kubectl get pods -l app=reducto-gpu-ocr           # 0/1 means the models did not load
  kubectl exec deploy/<release>-reducto-gpu-ocr -- curl -s localhost:80/ready  # replace 80 if gpuOcr.port is overridden
  kubectl logs deploy/<release>-reducto-gpu-ocr | grep -i "OCR model init failed"
  ```

  Three consequences worth planning for:

  * **Startup is slower**, by roughly `HTTP_WORKERS` model loads. Lower `HTTP_WORKERS`
    to shorten it. There is no `livenessProbe`, so a slow load never restarts a pod
    that is making progress; the `startupProbe` does restart one that never warms up.
  * **A deployment previously serving degraded CPU-only OCR will stop serving.** Repair
    the node first, or set `REQUIRE_GPU=false` to deliberately run on CPU.
  * **Running degraded rejects the surplus.** Once the pod has been fully warm it
    stays in service while any worker can serve, but the OCR client does not retry,
    so requests beyond the remaining capacity fail rather than queue. Prefer more
    replicas over a high `HTTP_WORKERS` if losing a worker must not fail jobs.
  * **A worker that cannot reach the GPU exits and is respawned.** It never serves,
    so requests are not routed to a handler that would fail them. A transient cause,
    such as a pod scheduled while `nvidia-fabricmanager` is still starting on a fresh
    node, therefore recovers without operator action. A persistent one logs the reason
    on every attempt and eventually restarts the container.
  * **One bad GPU on a multi-GPU node stops the whole pod.** Workers are pinned per
    device (`CUDA_VISIBLE_DEVICES = pid % NUM_GPUS`), so only the workers landing on
    the bad device fail, but the initial gate needs every worker warm, so the pod
    never becomes Ready and is restarted rather than serving at reduced capacity. The
    symptom is a never-Ready pod, not an obvious GPU error, so grep the logs for
    `OCR model init failed` to identify it.

  Two probes, because the two windows differ. `startupProbe` absorbs the warm-up;
  Kubernetes suppresses readiness until it passes, which lets `readinessProbe` stay
  tight and pull a pod that stops serving in about 30s instead of waiting out the
  whole warm-up budget.

  ```yaml theme={null}
  gpuOcr:
    progressDeadlineSeconds: 1800
    startupProbe:
      enabled: true
      periodSeconds: 10
      timeoutSeconds: 5
      failureThreshold: 50   # 500s of warm-up, with rollout margin
    readinessProbe:
      enabled: true
      periodSeconds: 10
      timeoutSeconds: 5
      failureThreshold: 3    # ~30s to stop routing to a pod that goes bad
  ```

  <Note>
    `gpuOcr.port` sets the container port, the Service target, the readiness probe and
    `HTTP_PORT` together, so the port the app binds always matches the one Kubernetes
    probes. It defaults to `80` to preserve rolling upgrades of existing standard GPU
    images. Chainguard images run as a non-root user and require `gpuOcr.port: 8888`.
    The Service still publishes `80`, so callers are unaffected. Set the port before
    switching image families; changing it and the image in one upgrade is a port
    migration, not a transparent rolling update.
  </Note>
</PasswordProtect>
