{"slug": "how-to-route-images-between-cheap-and-expensive-ai-models", "title": "How to Route Images Between Cheap and Expensive AI Models", "summary": "Roboflow published a guide showing how to cut vision inference costs by routing images through a cheap detector before sending only uncertain cases to a larger model. The workflow runs an RF-DETR Small detector first, then uses its detected classes and confidence scores in Roboflow Workflows conditional branches to send routine images to Gemini Flash-Lite, uncertain or priority cases to Gemini Flash or Pro, or flag them for human review. The guide also covers validating model responses, flagging disagreements, collecting selected images for retraining, and measuring whether routing lowers total cost while meeting inspection requirements.", "body_md": "To route images between a cheap model and an expensive model, run a fast object detector like RF-DETR first, then use its classes and confidence scores to decide where each image goes. In Roboflow Workflows, you set those routing rules with conditional branches: high-confidence, routine results go to a lightweight model, while uncertain or high-priority cases go to a larger VLM or to a person. That way you pay for the expensive model only on the images that need it.\n\nRouting images between a cheap model and an expensive model helps control inference costs by choosing which model analyzes each image. Instead of sending every image to your most expensive vision language model (VLM), you can use a lightweight detector’s predictions to route routine cases to a lower-cost model and reserve more expensive analysis for uncertain detections or priority defect classes. This approach is called model routing.\n\nConsider a furniture manufacturing line inspecting wooden panels before finishing. A familiar surface feature detected with high confidence may need only a lightweight check. A possible crack, missing knot, or low-confidence detection may warrant a different model. Routing rules make that choice explicit, using detected classes and confidence scores to select the next step.\n\nIn this guide, you’ll build an image-routing system in Roboflow Workflows using an RF-DETR Small detector and conditional logic. Each image will go to one of three Gemini models Flash-Lite, Flash, or Pro, or be flagged for human review. You’ll also validate model responses, flag disagreements, and collect selected images for retraining. Finally, you’ll learn what to measure to determine whether routing reduces total cost while meeting your inspection requirements.\n\nWhat Is Model Routing for Computer Vision?\n\nModel routing for computer vision is the process of selecting which model should analyze an image using signals such as detected objects, confidence scores, or the task being performed. The goal is to choose a model that meets the application’s accuracy and latency requirements at the lowest practical cost.\n\nFor example, a lightweight object detector can inspect an image first and return the objects it found with their confidence scores. Routing rules then use those predictions to choose the next model. A routine detection might send the image to a cheaper vision language model (VLM), while a possible crack or an uncertain detection sends it to a more expensive model.\n\nModel routing is related to image pre-filtering and sequential escalation. But each answers a different question:\n\nApproach\n\nQuestion it answers\n\nExample\n\nImage pre-filtering\n\nDoes this image need further analysis?\n\nSend an image to a VLM only when a detector finds a wooden panel.\n\nModel routing\n\nWhich model should analyze this image?\n\nSelect Flash-Lite, Flash, or Pro using detected classes and confidence scores.\n\nSequential escalation\n\nDoes the first model’s answer need another check?\n\nRun a cheaper VLM, evaluate its response, and call another model if needed.\n\nHow to Build an Image Model Router in Roboflow\n\nWe will build the inspection system, wood-defect-model-router workflow, in Roboflow Workflows by connecting the detector, routing logic, model branches, and review checks. First, RF-DETR will locate surface features in the panel image. Property Definition blocks will extract the class names and confidence scores, and a custom Python block (opens in a new tab) named Routing_Signals will validate the predictions. An Expression block will then choose a route, while Continue If gates will activate the selected branch.\n\nFor a Gemini route, the chosen model will analyze the original image. A JSON Parser will extract the response fields, and Inspection_Branch will group them with the model identifier and error flags. The human-review branch will use Human_Result to record that manual inspection is needed. First Non Empty Or Default will pass the result from the selected branch to Inspection_Decision, which will validate it, compare the detector and Gemini class sets when a Gemini response is available, and decide whether human review or image collection is needed.\n\nFinally, visualization blocks will draw the detector boxes, class labels, and confidence scores, then add the selected route, model, and review flag. We will also add an optional Dataset Upload path to collect selected images for retraining and a Webhook Sink to send flagged cases to a review application. Figure 1 shows how these components connect.\n\nStep 1: Define the Workflow inputs\n\nStart with the image and the parameters that the Workflow needs for each inspection. Configure these four inputs:\n\nInput\n\nPurpose\n\nDefault\n\nimage\n\nOriginal image of the wooden panel\n\nSupplied for each inspection\n\ninspection_id\n\nID used to link the result to an inspection\n\nSupplied by the calling application\n\ndetector_confidence\n\nMinimum confidence for detections returned by the detector\n\n0.20\n\nenable_collection\n\nAllows selected images to enter the collection path\n\nfalse\n\nUse an image input for image and parameter inputs for the other values. The calling application supplies inspection_id; later blocks include it in the results and collection metadata. Keep enable_collection=false while checking the routing behavior, then enable it when you are ready to collect images.\n\nCrack, Marrow, Resin, Dead_Knot, Knot_with_Crack, Quartzity, Live_Knot, and Knot_Missing.\n\nIts output contains bounding boxes, class names, and individual confidence scores. Object detection is useful here because a single panel can contain several features or defects, each with its own location and score.\n\nThe initial cutoff of 0.20 keeps lower-confidence detections available for routing. Predictions below this cutoff are still removed, so the router can inspect only the detections returned by the model.\n\nStep 3: Extract and validate the routing signals\n\nAdd two Property Definition blocks that read $steps.wood_detector.predictions. Name them detector_classes and detector_confidences. Use DetectionsPropertyExtract to extract class_name in the first block and confidence in the second.\n\nNext, add a custom Routing_Signals block and name it routing_signals. Connect the original detector predictions to its predictions input, the extracted class names to classes, and the extracted confidence scores to confidences.\n\nThe block checks that the numbers of class names, confidence scores, and bounding boxes match the number of detections. It also checks that each class is supported, each score is a finite number between 0 and 1, and each box has finite coordinates with positive width and height. A finite value excludes NaN and infinity. Configure the block to return these six Boolean outputs:\n\nFlag\n\nWhen it is true\n\ninvalid\n\nThe prediction data fails validation or an exception occurs while checking it.\n\ncritical\n\nAt least one Crack, Knot_with_Crack, or Knot_Missing is present.\n\nlow\n\nAt least one returned confidence is below 0.60.\n\nmedium\n\nAt least one returned confidence is below 0.90.\n\nempty\n\nA valid detector result contains zero detections.\n\nhigh\n\nAt least one detection exists and every returned confidence is at least 0.90.\n\nThe block starts with invalid=true and all other flags set to false. It sets the other flags only after the prediction data passes validation. Here, critical identifies the three classes given priority by our routing policy; it does not measure the panel’s physical strength or assign a wood grade.\n\nUse the following code in the routing_signals Custom Python block:\n\nPython\n\n``` python\ndef run(self, predictions, classes, confidences):\n    import math\n    import numpy as np\n\n    flags = {\n        \"invalid\": True,\n        \"critical\": False,\n        \"low\": False,\n        \"medium\": False,\n        \"empty\": False,\n        \"high\": False,\n    }\n\n    allowed = {\n        \"Crack\", \"Marrow\", \"Resin\", \"Dead_Knot\",\n        \"Knot_with_Crack\", \"Quartzity\", \"Live_Knot\", \"Knot_Missing\",\n    }\n    priority_classes = {\"Crack\", \"Knot_with_Crack\", \"Knot_Missing\"}\n\n    try:\n        names = list(classes) if classes is not None else []\n        scores = list(confidences) if confidences is not None else []\n        boxes = predictions.xyxy\n        n = len(predictions)\n\n        if (\n            len(names) != n\n            or len(scores) != n\n            or boxes is None\n            or len(boxes) != n\n        ):\n            return flags\n\n        for name, score, box in zip(names, scores, boxes):\n            if (\n                not isinstance(name, str)\n                or name not in allowed\n                or score is None\n                or not math.isfinite(float(score))\n                or not 0 <= float(score) <= 1\n                or not np.all(np.isfinite(box))\n                or float(box[2]) <= float(box[0])\n                or float(box[3]) <= float(box[1])\n            ):\n                return flags\n\n        flags.update(\n            invalid=False,\n            critical=any(name in priority_classes for name in names),\n            low=any(float(score) < 0.60 for score in scores),\n            medium=any(float(score) < 0.90 for score in scores),\n            empty=n == 0,\n            high=n > 0 and all(float(score) >= 0.90 for score in scores),\n        )\n\n    except Exception:\n        # Keep the invalid flag when prediction data cannot be checked.\n        pass\n\n    return flags\n```\n\nThese flags describe the detector’s results. In the next step, the select_route Expression block uses them to choose one route for the image.\n\nStep 4: Define which route each image takes\n\nAdd an Expression block (opens in a new tab) named select_route. Connect the six flags from routing_signals and configure the cases in the following order. The first matching case determines the route for the entire image.\n\nOrder\n\nCondition\n\nRoute value\n\n1\n\nInvalid prediction data or an unexpected class\n\nhuman\n\n2\n\nAny Crack, Knot_with_Crack, or Knot_Missing, regardless of its returned confidence\n\npro\n\n3\n\nOtherwise, any returned confidence below 0.60\n\npro\n\n4\n\nOtherwise, any returned confidence below 0.90\n\nflash\n\n5\n\nSuccessful detector result with zero detections\n\nflash_lite\n\n6\n\nOtherwise, every returned confidence is at least 0.90\n\nflash_lite\n\nDefault\n\nNo matching valid rule\n\nhuman\n\nThe order matters because some flags overlap. A confidence below 0.60 also satisfies the medium flag, but the Pro rule is checked first. A priority class also selects Pro even when its confidence is high.\n\nThe router checks every detection separately and does not average confidence scores. One low-confidence detection can therefore change the route for the whole image, even when the other detections have high scores.\n\nIf the detector successfully returns no detections, Flash-Lite checks the complete image. An empty detection result alone does not establish that the panel is defect-free.\n\nWe will use 0.60 and 0.90 as initial routing thresholds in the custom code. The 0.20 detector cutoff is a Workflow input. Adjust these values after evaluating the Workflow on your inspection images. If you change the routing thresholds, also update the matching routing-reason text in Inspection_Decision.\n\nWe will use per-detection confidence scores as the numeric routing signal. The Gemini response contains classes, an assessment, and a reason; it does not contain a likelihood score. If you add a score from another model, check what it means and test whether it helps with routing before choosing thresholds. A confidence value requested in a VLM prompt should not automatically use the detector's thresholds.\n\nA score returned by Gemini is available only after that model runs. It can support a later review decision, but it cannot decide whether to make the call that produced it.\n\nStep 5: Connect the Gemini and human-review branches\n\nAdd four Continue If gates (opens in a new tab). Each gate compares $steps.select_route.output with one route value. Set its next_steps connection to the first block in the matching branch:\n\nGate\n\nRoute value\n\nNext step\n\ngate_flash_lite\n\nflash_lite\n\ninspect_flash_lite\n\ngate_flash\n\nflash\n\ninspect_flash\n\ngate_pro\n\npro\n\ninspect_pro\n\ngate_human\n\nhuman\n\nhuman_result\n\nFor the first three branches, add a Google Gemini block (opens in a new tab) with the corresponding step name. Configure model_version for each branch as follows:\n\nBranch\n\nConfigured Gemini model\n\nFlash-Lite\n\ngemini-3.5-flash-lite\n\nFlash\n\ngemini-3.8-flash\n\nPro\n\ngemini-3.1-pro-preview\n\nCheck that all three models are available in the environment where you will run the Workflow.\n\nSet each Gemini block's image input to $inputs.image and its task to structured-answering. For a Gemini route, the selected model receives the original image. The visualization blocks prepare the annotated output separately.\n\nConfigure all three Gemini blocks to request the same response fields:\n\nField\n\nExpected content\n\nvisible_classes\n\nA list containing supported class names, or an empty list.\n\nassessment\n\nExactly visible_defect, no_visible_defect, or unclear.\n\nreason\n\nA short explanation based on visible evidence.\n\nAsk the model to identify the eight supported surface classes and return multiple classes when appropriate. It should return unclear when blur, glare, obstruction, or insufficient detail prevents a reliable assessment. Limit the model's answer to visible surface conditions and exclude judgments about internal strength or commercial wood grade.\n\nFor the human branch, use the custom Human_Result block named human_result. It creates a result with model=\"none\", an empty class list, assessment=\"unclear\", and the reason Manual review required. This records the need for a person to inspect the case; it does not perform that review.\n\nEach parser reads its Gemini block's output and extracts visible_classes, assessment, and reason. It also exposes an error flag. Each Inspection_Branch block packages these fields, the model identifier, and the model/parser error flags into a bundle: a dictionary that keeps the related values together.\n\nOnly the selected branch supplies a result. The join passes that bundle to the next stage, giving the review logic a common structure to read. It does not combine answers from multiple Gemini models or run an ensemble.\n\nConfigure its default bundle with model=\"none\", an empty class list, assessment=\"unclear\", the reason No branch completed, and both error flags set to true. This marks a missing branch result as unavailable when execution reaches the join.\n\nStep 7: Validate the answer and decide whether review is needed\n\nAdd the custom Inspection_Decision block and name it inspection_decision. Connect the selected route, the bundle from selected_result, the detector predictions, inspection_id, and the invalid and critical routing flags.\n\nThe block first checks whether the route is recognized and whether routing_signals has marked the predictions as invalid. It skips reading predictions already marked as invalid. Otherwise, it reads the class names and confidence scores, checks that their counts match the number of detections, and verifies that the classes are supported and the scores are finite numbers between 0 and 1. If reading or validating this data fails, the case is flagged for review.\n\nNext, the block checks Gemini’s response. The visible_classes field must be a list of supported class names, assessment must be visible_defect, no_visible_defect, or unclear, and reason must be a nonempty string. The fields must also agree: visible_defect requires at least one class, while no_visible_defect requires an empty class list.\n\nThe block sets needs_review=true when any of these conditions applies:\n\nThe route is human or unrecognized, or the detector predictions are invalid or cannot be read.\n\nThe model or JSON Parser reports an error, or the response values fail validation.\n\nGemini returns an unclear assessment.\n\nGemini and the detector report different class sets.\n\nFor a valid Gemini response with no reported errors and valid detector data, the block compares distinct class sets. A class set contains each class only once. For example, detector labels Crack, Crack, Live_Knot and Gemini labels Crack, Live_Knot produce the same set. Repeated boxes for one class do not create a disagreement.\n\nThe final parsed_result keeps Gemini’s response when the selected route is a recognized Gemini route, the detector data and response values are valid, and neither the model nor the parser reports an error. All other cases receive a fallback: an empty visible_classes list, assessment=\"unclear\", the reason Invalid or unavailable response, and parser_error=true. This includes the initial human-review route.\n\nHere, parser_error acts as a combined status flag for all these fallback conditions. The fallback prevents an unavailable or invalid automated result from being returned as no_visible_defect.\n\nA valid Gemini response that says unclear or disagrees with the detector is retained and flagged for review. The review decision and the assessment therefore serve different purposes. Likewise, needs_review=false means these checks did not flag the case; the detector and Gemini may still agree that a visible defect is present.\n\nThe block returns the inspection ID, routing reason, selected model, parsed result, review flag, and review reason. It also returns the detected classes and confidence scores as JSON strings, the final assessment, and the collect flag used by the collection path in Step 9.\n\nUse the following code in the inspection_decision Custom Python block:\n\nPython\n\n``` python\ndef run(self, route, bundle, predictions, inspection_id, invalid, critical):\n    import json\n    import math\n    import numpy as np\n\n    allowed = {\n        \"Crack\", \"Marrow\", \"Resin\", \"Dead_Knot\",\n        \"Knot_with_Crack\", \"Quartzity\", \"Live_Knot\", \"Knot_Missing\",\n    }\n    route = route if isinstance(route, str) else \"\"\n    known_route = route in {\"flash_lite\", \"flash\", \"pro\", \"human\"}\n\n    # Do not read predictions that the routing block has already rejected.\n    names = []\n    score_list = []\n    prediction_error = bool(invalid)\n\n    if not prediction_error:\n        try:\n            names = list(predictions.data.get(\"class_name\", []))\n            scores = predictions.confidence\n            score_list = [float(x) for x in scores] if scores is not None else []\n            count = len(predictions)\n\n            if len(names) != count or len(score_list) != count:\n                raise ValueError(\"Missing class names or confidence scores\")\n            if any(not isinstance(x, str) or x not in allowed for x in names):\n                raise ValueError(\"Unsupported detector class\")\n            if any(not math.isfinite(x) or not 0 <= x <= 1 for x in score_list):\n                raise ValueError(\"Invalid detector confidence\")\n        except Exception:\n            prediction_error = True\n            names = []\n            score_list = []\n\n    # Read the selected branch's response and error flags.\n    b = bundle if isinstance(bundle, dict) else {}\n    model = str(b.get(\"model\") or \"none\")\n    raw = b.get(\"visible_classes\")\n    assessment = b.get(\"assessment\")\n    reason = b.get(\"reason\")\n    failed = bool(b.get(\"parser_error\", True) or b.get(\"model_error\", True))\n\n    valid = (\n        isinstance(raw, list)\n        and all(isinstance(x, str) and x in allowed for x in raw)\n        and isinstance(assessment, str)\n        and assessment in (\"visible_defect\", \"no_visible_defect\", \"unclear\")\n        and isinstance(reason, str)\n        and bool(reason.strip())\n    )\n    if valid and assessment != \"unclear\":\n        if (assessment == \"visible_defect\") != (len(raw) > 0):\n            valid = False\n\n    # Describe the route chosen by select_route.\n    if route == \"human\":\n        route_reason = \"Invalid prediction data, unexpected class, or no matching rule\"\n    elif route == \"pro\":\n        route_reason = (\n            \"Critical class present\"\n            if critical else \"Detection confidence below 0.60\"\n        )\n    elif route == \"flash\":\n        route_reason = \"Detection confidence below 0.90\"\n    elif route == \"flash_lite\":\n        if prediction_error:\n            route_reason = \"Flash-Lite selected, but detector predictions are invalid\"\n        else:\n            route_reason = (\n                \"No detections; full-image verification required\"\n                if not names else \"All detector confidences at least 0.90\"\n            )\n    else:\n        route_reason = \"Unexpected route\"\n\n    # Request review for invalid routes, predictions, or model responses.\n    problems = []\n    if not known_route:\n        problems.append(\"Unexpected route\")\n    if prediction_error:\n        problems.append(\"Invalid or unreadable detector predictions\")\n\n    if route == \"human\":\n        if not problems:\n            problems.append(route_reason)\n    elif not problems:\n        if failed or not valid:\n            problems.append(\n                \"Gemini or JSON parsing failure, or invalid required response values\"\n            )\n        else:\n            if assessment == \"unclear\":\n                problems.append(\"Gemini assessment unclear: \" + reason.strip())\n            if set(raw) != set(names):\n                problems.append(\"Gemini classes disagree with detector classes\")\n\n    # Keep the response only when the automated result is usable.\n    usable = (\n        valid\n        and not failed\n        and not prediction_error\n        and known_route\n        and route != \"human\"\n    )\n    clean = {\n        \"visible_classes\": raw if usable else [],\n        \"assessment\": assessment if usable else \"unclear\",\n        \"reason\": reason if usable else \"Invalid or unavailable response\",\n        \"parser_error\": bool(\n            failed or not valid or prediction_error\n            or not known_route or route == \"human\"\n        ),\n    }\n    needs_review = bool(problems)\n\n    # Each remaining Flash-Lite case has a 5% chance of collection.\n    collect = (\n        route in (\"pro\", \"human\")\n        or needs_review\n        or (route == \"flash_lite\" and np.random.random() < 0.05)\n    )\n\n    return {\n        \"inspection_id\": str(inspection_id),\n        \"routing_reason\": route_reason,\n        \"selected_model\": model,\n        \"parsed_result\": clean,\n        \"needs_review\": needs_review,\n        \"review_reason\": \"; \".join(problems),\n        \"collect\": bool(collect),\n        \"detected_classes\": json.dumps(names),\n        \"detected_confidences\": json.dumps(score_list),\n        \"assessment\": clean[\"assessment\"],\n    }\n```\n\nStep 8: Create the annotated image and inspection outputs\n\nAdd bounding_box_visualization using the original image and wood_detector.predictions. Connect its image output to label_visualization, use the same predictions, and select Class and Confidence for the label text.\n\nThen add a Text Display block named route_banner. Use the labeled image as its input and show the selected route, Gemini model, and review flag across the top of the image. Use this template for the banner:\n\n```\nROUTE: {{ $parameters.route }} | {{ $parameters.model }} | REVIEW: {{ $parameters.review }}\n```\n\nMap the banner parameters as follows:\n\nroute → $steps.select_route.output\n\nmodel → $steps.inspection_decision.selected_model\n\nreview → $steps.inspection_decision.needs_review\n\nThe annotated image retains the original detector boxes, class names, and per-box confidence scores. The banner makes the route and review decision visible alongside those detections. Expose the following Workflow outputs:\n\nOutput\n\nWhat it contains\n\ninspection_id\n\nThe ID supplied by the calling application.\n\npredictions\n\nOriginal detector predictions.\n\noutput_image\n\nAnnotated image with the route banner.\n\nselected_route\n\nThe route chosen for the image.\n\nrouting_reason\n\nWhy the route was selected, or a problem identified with that route.\n\nselected_gemini_model\n\nModel identifier from the selected branch, or none for the human branch or an unavailable model identifier.\n\nparsed_gemini_result\n\nValidated Gemini response, or an unclear fallback when no usable automated response is available.\n\nneeds_review\n\nWhether the case needs human review.\n\nreview_reason\n\nWhy human review was requested.\n\ncollection_requested\n\nWhether the enabled collection gate selected the image.\n\ncollection_status\n\nMessage from the upload block when it runs.\n\ncollection_error\n\nUpload error status when the block runs.\n\nThe calling application can use inspection_id to link these outputs to the original inspection. Read parsed_gemini_result together with needs_review and review_reason to understand both the assessment and whether a person needs to check it. The next step configures the collection outputs.\n\nStep 9: Collect useful images for retraining\n\nThe Inspection_Decision block also decides whether an image is useful for collection. It selects Pro referrals, initial human cases, and any case flagged for review, including class disagreements. Each remaining Flash-Lite case has a 5% chance of being selected for collection.\n\nThis sampling happens in the custom block. The upload block is not configured to sample 5% of all incoming images.\n\nAdd an Expression block named collection_switch that returns true only when both inspection_decision.collect and enable_collection are true. Connect it to gate_collection, a Continue If block whose next step is collect_for_review.\n\nWith persist_predictions=false, uploaded images do not carry detector predictions as annotations. With fire_and_forget=false, the upload is synchronous: the Workflow waits for it to finish so the upload message and error status can be checked.\n\nAdd inspection_id and detector_model to the upload metadata so each collected image can be linked to its inspection and detector. To keep a fuller record for retraining analysis, have the calling application save the routing and review outputs alongside the inspection ID.\n\nMap collection_requested to collection_switch.output, collection_status to collect_for_review.message, and collection_error to collect_for_review.error_status. A request to collect an image does not prove that it was uploaded; check the status and error outputs when the upload block runs.\n\nCollection stays off while enable_collection=false. When enabled, the single gated upload path prevents multiple collection paths within one execution, even if a Pro referral also has a class disagreement. It does not remove duplicates across repeated inspections or neighboring video frames.\n\nStep 10: Prepare the optional human-review handoff\n\nAdd a Continue If block named gate_webhook that checks inspection_decision.needs_review. Connect it to a Webhook Sink (opens in a new tab) named review_webhook. Configure the sink to send a POST request containing the inspection ID, route, selected model, parsed result, review flag, and review reason.\n\nThe calling application must also handle detector or Gemini request failures and unhandled exceptions. These can stop execution before a review result is returned and must be treated as failed inspections requiring attention, never as clean panels.\n\nTesting the Workflow\n\nOpen the Workflow testing interface (opens in a new tab), supply a panel image and inspection_id, and run the Workflow. Check both output_image and the structured results so you can follow the route, model choice, and review decision.\n\nUse representative images to cover priority classes, non-priority detections in each confidence range, and successful results with no detections. Also check unreadable images, invalid prediction data where the routing check can run, and responses that are unclear, invalid, or disagree with the detector.\n\nWhen testing image collection or the webhook, check the test panel's Sinks setting as well as the Workflow's own enable/disable settings. The external write must be enabled at both levels for a browser test to perform it.\n\nIn an initial test with one wood image, a priority-class detection selected the Pro branch. Gemini and the detector returned different class sets, so the banner displayed REVIEW: True.\n\nFigure 7 shows the annotated image and structured results from this test. This confirms the Pro path and visual output for that image. The other routes and operating conditions still need representative testing before production reliance.\n\nMeasure the Results and Improve the Detector\n\nCompare the router with a baseline that sends every image to one selected Gemini model. Record how often each route runs, how many cases need human review, missed defects, false alarms, and total processing time.\n\nInclude the detector, the model calls actually made, and external review work when calculating costs. The outputs we defined track inspection decisions and collection status. Record processing time and billing details separately in the calling application or its monitoring system. Fewer Pro calls alone do not establish a lower total cost.\n\nUse the collected images to identify recurring problems, such as uncertain cracks, disagreements about knots, or wood appearances that the detector has not learned well. Annotate and review suitable images, then generate a new dataset version (opens in a new tab) and train an updated model. A dataset version is a fixed snapshot; adding collected images does not automatically update an existing training version.\n\nFinally, evaluate the updated detector (opens in a new tab) and test the routing policy again. Retraining can change the predictions and the distribution of confidence scores, which can change how often each branch is selected.\n\nConclusion\n\nModel routing uses the detector's results to choose the next step for each wood inspection. Routine images can receive a lightweight check, selected conditions can receive more detailed analysis, and uncertain results can be flagged for a person.\n\nBuild and test the policy in Roboflow Workflows (opens in a new tab), choose thresholds using reviewed inspection results, and use collected cases to improve the detector over time.", "url": "https://wpnews.pro/news/how-to-route-images-between-cheap-and-expensive-ai-models", "canonical_source": "https://roboflow.com/blog/how-to-route-images-between-cheap-and-expensive-ai-models", "published_at": "2026-10-26 18:49:00+00:00", "updated_at": "2026-10-26 19:12:00.271345+00:00", "lang": "en", "topics": ["computer-vision", "ai-tools", "ai-infrastructure", "ai-products"], "entities": ["Roboflow", "RF-DETR Small", "Roboflow Workflows", "Gemini Flash-Lite", "Gemini Flash", "Gemini Pro"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-route-images-between-cheap-and-expensive-ai-models", "markdown": "https://wpnews.pro/news/how-to-route-images-between-cheap-and-expensive-ai-models.md", "text": "https://wpnews.pro/news/how-to-route-images-between-cheap-and-expensive-ai-models.txt", "jsonld": "https://wpnews.pro/news/how-to-route-images-between-cheap-and-expensive-ai-models.jsonld"}}