<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jangwook Kim</title>
    <description>The latest articles on DEV Community by Jangwook Kim (@jangwook_kim_e31e7291ad98).</description>
    <link>https://dev.to/jangwook_kim_e31e7291ad98</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1909290%2F60a8c15f-b2b5-4189-8578-78b8ab78900b.jpg</url>
      <title>DEV Community: Jangwook Kim</title>
      <link>https://dev.to/jangwook_kim_e31e7291ad98</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jangwook_kim_e31e7291ad98"/>
    <language>en</language>
    <item>
      <title>RAGFlow 0.26.4 in Production: Strong Parsing, Expensive Baseline, Unproven Tenant Isolation</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Thu, 15 Oct 2026 00:41:09 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/ragflow-0264-in-production-strong-parsing-expensive-baseline-unproven-tenant-isolation-ha2</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/ragflow-0264-in-production-strong-parsing-expensive-baseline-unproven-tenant-isolation-ha2</guid>
      <description>&lt;h2&gt;
  
  
  Why We Evaluated RAGFlow
&lt;/h2&gt;

&lt;p&gt;We brought RAGFlow into our evaluation pipeline because the recurring cost of custom RAG glue had stopped being theoretical.&lt;/p&gt;

&lt;p&gt;Retrieval quality vs memory baseline&lt;span&gt;Parsing fidelity 99%/100&lt;/span&gt;&lt;span&gt;Retrieval accuracy 73%/100&lt;/span&gt;&lt;span&gt;Idle memory 8,517.3 MB&lt;/span&gt;&lt;span&gt;Peak memory during ingestion 9,959.6 MB&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;RAGFlow preserves 99% of planted facts but surfaces the right answer in the top five only 73% of the time, while idling at 8,517.3 MB.&lt;/p&gt;

&lt;p&gt;A typical internal deployment had accumulated a parser service, chunking workers, embedding jobs, vector and keyword retrieval, reranking, chat orchestration, document status tracking, and an administrative interface. LangChain or LlamaIndex could connect those pieces, but neither removed our responsibility for operating them. Every new tenant added more configuration, lifecycle handling, and failure states.&lt;/p&gt;

&lt;p&gt;RAGFlow offered a more integrated proposition: deep document parsing, dataset management, hybrid retrieval, chat configuration, citations, model registration, and an HTTP API in one platform. It also uses the Apache 2.0 license, which matters when the deployment will be embedded in a commercial service rather than kept inside an internal prototype.&lt;/p&gt;

&lt;p&gt;The attraction was not that RAGFlow invented retrieval. It was that it bundled the operational workflow around retrieval:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a dataset.&lt;/li&gt;
&lt;li&gt;Select an embedding model and parser.&lt;/li&gt;
&lt;li&gt;Upload documents.&lt;/li&gt;
&lt;li&gt;Trigger parsing.&lt;/li&gt;
&lt;li&gt;Wait for chunks to become available.&lt;/li&gt;
&lt;li&gt;Query those chunks directly or through a configured chat assistant.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That workflow replaces a meaningful amount of application code. It does not, however, eliminate infrastructure.&lt;/p&gt;

&lt;p&gt;For our &lt;a href="https://knowledgebasebench.com/platforms/ragflow/" rel="noopener noreferrer"&gt;0.26.4 production baseline&lt;/a&gt;, we evaluated the published-image deployment results separately from our offline client-contract sandbox. The sandbox tested response handling, not deployment performance. In the published-image deployment baseline we evaluated, RAGFlow ran in five containers, consumed 8,517.3 MB at idle, and reached 9,959.6 MB during ingestion. Elasticsearch alone accounted for more than 4 GB of idle memory in that deployment.&lt;/p&gt;

&lt;p&gt;Those numbers immediately changed the question. We were no longer asking whether RAGFlow could replace some Python orchestration. We were asking whether the reduction in engineering work justified an approximately 8.5 GB idle platform while still budgeting for workload growth, local models, observability, backups, and concurrency.&lt;/p&gt;

&lt;p&gt;We also needed an answer to a harder question: does RAGFlow provide enough isolation to operate unrelated customers on one shared installation?&lt;/p&gt;

&lt;p&gt;We found tenant-aware configuration and authentication surfaces, but we did not find enough evidence to certify hard multi-tenant isolation, per-tenant resource quotas, or noisy-neighbor protection. That distinction is critical. A platform can associate datasets with tenants without providing the controls expected from a hostile or heavily regulated multi-tenant service.&lt;/p&gt;

&lt;p&gt;Our review therefore focused on three production-readiness criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can we automate setup and retrieval without driving the web interface?&lt;/li&gt;
&lt;li&gt;Can the API fail safely under automation?&lt;/li&gt;
&lt;li&gt;Can we establish a credible cost and isolation model before consolidating tenants?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For other infrastructure evaluations using the same deployment-first approach, &lt;a href="https://dev.to/tools"&gt;browse our tools collection&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We scoped our review to RAGFlow 0.26.4 and the benchmark CLI instructions at commit &lt;code&gt;d32e05d5&lt;/code&gt;. Pinning matters because API routes and backend behavior change between releases. For deployment planning, we also reviewed the Go v1.0.0-rc1 quickstart. Its host-hardening guidance and Compose launch command below are reference steps, not a validated 0.26.4 installation procedure. Our local sandbox exercised only offline Requests response handling.&lt;/p&gt;

&lt;p&gt;On an Elasticsearch-backed host, we first checked the kernel mapping limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl vm.max_map_count
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; vm.max_map_count&lt;span class="o"&gt;=&lt;/span&gt;262144
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We kept the persistent setting in the host configuration rather than relying on the temporary &lt;code&gt;sysctl -w&lt;/code&gt; change. We also changed the dependency credentials in &lt;code&gt;docker/.env&lt;/code&gt; before exposing any port beyond loopback.&lt;/p&gt;

&lt;p&gt;For the built-in CPU embedding path used by the benchmark scripts, we configured the TEI model before starting the containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;ragflow

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'TEI_MODEL=BAAI/bge-small-en-v1.5'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; docker/.env
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'COMPOSE_PROFILES=${COMPOSE_PROFILES},tei-cpu'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; docker/.env

&lt;span class="nb"&gt;cd &lt;/span&gt;docker
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benchmark environment uses Python 3.13 and &lt;code&gt;uv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ..
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; 3.13 &lt;span class="nt"&gt;--group&lt;/span&gt; &lt;span class="nb"&gt;test

export &lt;/span&gt;&lt;span class="nv"&gt;ZHIPU_AI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"redacted"&lt;/span&gt;
&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./test uv run &lt;span class="nt"&gt;-m&lt;/span&gt; benchmark retrieval &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--base-url&lt;/span&gt; http://127.0.0.1:9380 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-register&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--login-email&lt;/span&gt; &lt;span class="s2"&gt;"qa@infiniflow.org"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--login-password&lt;/span&gt; &lt;span class="s2"&gt;"123"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-llm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--llm-factory&lt;/span&gt; ZHIPU-AI &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--llm-api-key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZHIPU_AI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dataset-name&lt;/span&gt; &lt;span class="s2"&gt;"bench_dataset"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dataset-payload&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"bench_dataset","embedding_model":"BAAI/bge-small-en-v1.5@Builtin"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-path&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/benchmark/test_docs/Doc1.pdf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-path&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/benchmark/test_docs/Doc2.pdf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-path&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/benchmark/test_docs/Doc3.pdf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--question&lt;/span&gt; &lt;span class="s2"&gt;"What does RAG mean?"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iterations&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canonical entry point is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PYTHONPATH=./test uv run -m benchmark [global flags] &amp;lt;chat|retrieval&amp;gt; [command flags]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI can create a dataset, upload files, trigger parsing, poll for completion, execute retrieval or chat requests, and optionally delete the generated resources. It is not a general administrative CLI. We captured generated dataset IDs from its report when we wanted to reuse resources across runs.&lt;/p&gt;

&lt;p&gt;We use the following hypothetical report sketch to illustrate reporting concepts, not verified CLI output or a measured retrieval result. Its field names are illustrative, and the dataset ID, 10 iterations, four-way concurrency, ten successes, and zero failures are synthetic example values. Our only executed local experiment used 12 offline response fixtures; it did not run retrieval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"retrieval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"base_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:9380"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dataset_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7f52c4a3..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iterations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"concurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"successful"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What does RAG mean?"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For chat tests, we supplied both the chat assistant configuration and the &lt;code&gt;model&lt;/code&gt; field expected by the OpenAI-compatible endpoint. The important detail is that the request-level &lt;code&gt;--model&lt;/code&gt; value does not override the model configured on the server-side chat assistant.&lt;/p&gt;

&lt;p&gt;The full-corpus 0.26.4 baseline we used ingested 36 of 36 documents and stored 477 chunks. It retained 99% of planted facts in the stored chunks and put the answer in the top-five context for 73% of 93 retrieval questions. Retrieval latency with CPU embedding was 414.7 ms at p50 and 464.4 ms at p95.&lt;/p&gt;

&lt;p&gt;That result exposed the difference between parsing fidelity and retrieval accuracy. RAGFlow preserved nearly all the facts, yet the tested retrieval configuration surfaced the correct answer in the top five only 73% of the time. Better parsing did not automatically produce excellent ranking.&lt;/p&gt;

&lt;p&gt;We also reproduced the API error-handling problem offline with Requests 2.32.3. The adapter below is runnable and deliberately fails closed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BusinessError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnvelopeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_ragflow_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;EnvelopeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Response is not valid JSON&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;EnvelopeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected a JSON object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;EnvelopeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected an integer business code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unspecified RAGFlow error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;BusinessError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAGFlow code &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across 12 fixtures, transport-only &lt;code&gt;raise_for_status()&lt;/code&gt; missed four business failures returned with HTTP 200. Our adapter accepted only the valid success envelope, rejected codes &lt;code&gt;101&lt;/code&gt;, &lt;code&gt;106&lt;/code&gt;, &lt;code&gt;108&lt;/code&gt;, and &lt;code&gt;109&lt;/code&gt; as business errors, rejected malformed envelopes, and still raised normal HTTP errors for 403 and 500 responses.&lt;/p&gt;

&lt;p&gt;That test did not emulate a running RAGFlow server. It verified the client behavior we require when integrating an API that uses both HTTP status and a JSON business code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Limitations We Encountered
&lt;/h2&gt;

&lt;p&gt;The first trap was ingestion lifecycle ambiguity. Uploading a document does not mean that RAGFlow has parsed it. Parsing is a separate action. A client that uploads and then waits for chunks without triggering parsing can wait indefinitely for work that was never scheduled.&lt;/p&gt;

&lt;p&gt;Our workaround was to model ingestion as an explicit state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created -&amp;gt; uploaded -&amp;gt; parse_requested -&amp;gt; parsing -&amp;gt; ready
                                      \-&amp;gt; failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We persisted both the RAGFlow document ID and our own idempotency key. We also separated parse timeout from HTTP timeout because parsing can legitimately outlive an individual API request.&lt;/p&gt;

&lt;p&gt;The second trap was error semantics. HTTP 200 means the request reached the application, not necessarily that the operation succeeded. We now require both a successful HTTP status and an integer body-level &lt;code&gt;code&lt;/code&gt; equal to zero for endpoints using the business envelope. OpenAI-compatible completion responses need their own parser because they do not necessarily use that envelope.&lt;/p&gt;

&lt;p&gt;The third trap was authentication during bootstrap. API-token authentication is straightforward once a token exists, but login automation can require client-side RSA encryption of the password. We avoided reproducing browser login logic in production services. We provisioned API tokens out of band and injected them through the secret manager instead.&lt;/p&gt;

&lt;p&gt;The fourth trap was route churn. Version 0.26 moved routes under a new prefix, making older examples return 404. The later Go backend introduces another migration boundary, including canonical endpoint and parser-field changes. We pinned API tests to the deployed release and treated a backend upgrade as an application migration, not a routine container refresh.&lt;/p&gt;

&lt;p&gt;The fifth issue was resource overhead. The deployment consumed 8,517.3 MB even while idle. Peak memory reached 9,959.6 MB during ingestion in the measured 36-document corpus. Time to the first served request was 511 seconds from a clean host, including image pulls. That is acceptable for a persistent service, but poor for ephemeral preview environments or scale-to-zero workers.&lt;/p&gt;

&lt;p&gt;The largest unresolved concern was tenant isolation. We could configure tenant model defaults, authenticate requests, and select datasets by ID. We could not establish the following controls from the reviewed 0.26.4 surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforced per-tenant ingestion concurrency&lt;/li&gt;
&lt;li&gt;Per-tenant CPU or memory quotas&lt;/li&gt;
&lt;li&gt;Retrieval rate limits&lt;/li&gt;
&lt;li&gt;Storage quotas&lt;/li&gt;
&lt;li&gt;Dedicated worker pools&lt;/li&gt;
&lt;li&gt;Tenant-specific Elasticsearch capacity&lt;/li&gt;
&lt;li&gt;A documented noisy-neighbor policy&lt;/li&gt;
&lt;li&gt;A formal isolation guarantee suitable for hostile tenants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We therefore treated one shared installation as a trust-boundary decision, not merely a cost optimization. If tenants share parsing workers and Elasticsearch capacity, one tenant's image-heavy ingestion or index growth could affect another tenant's retrieval. We treated that as a deployment risk to test, not as measured noisy-neighbor behavior in RAGFlow 0.26.4.&lt;/p&gt;

&lt;p&gt;High availability presented a similar gap. We kept &lt;a href="https://github.com/infiniflow/ragflow/issues/9963" rel="noopener noreferrer"&gt;issue #9963&lt;/a&gt; as a production release-gate reference. In our HA planning, we treated the defaults as local-development or single-instance oriented, with potential bottlenecks under clustered or high-traffic load. We also treated high concurrency as an enterprise-edition capability to validate before purchase, rather than assuming it was available in our planned deployment.&lt;/p&gt;

&lt;p&gt;The issue did not give us a complete list of unsafe keys and replacement values, so we did not invent one. Our mitigation was architectural: we refused to label the default Compose stack as HA. Before production approval, we required external database and object-storage durability, dependency health checks, tested restore procedures, multiple stateless API replicas where supported, controlled worker concurrency, and load testing against the exact edition being purchased.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;RAGFlow's economic case depends less on request volume than on how much platform engineering it replaces.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Operational baseline&lt;/th&gt;
&lt;th&gt;Parsing and retrieval control&lt;/th&gt;
&lt;th&gt;Tenant-isolation burden&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RAGFlow 0.26.4&lt;/td&gt;
&lt;td&gt;Five containers in the measured deployment; 8,517.3 MB idle&lt;/td&gt;
&lt;td&gt;Strong document workflow, inspectable chunks, integrated retrieval and chat&lt;/td&gt;
&lt;td&gt;High unless isolation is independently validated or deployments are separated&lt;/td&gt;
&lt;td&gt;Document-heavy products with trusted tenants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangChain or LlamaIndex composition&lt;/td&gt;
&lt;td&gt;Determined by the services we assemble&lt;/td&gt;
&lt;td&gt;Maximum flexibility, but we own integration and lifecycle code&lt;/td&gt;
&lt;td&gt;Entirely ours to design&lt;/td&gt;
&lt;td&gt;Teams with an established AI platform group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lean vector stack&lt;/td&gt;
&lt;td&gt;Can be materially smaller when parsing and orchestration remain simple&lt;/td&gt;
&lt;td&gt;Good for clean text; more custom work for complex documents&lt;/td&gt;
&lt;td&gt;Entirely ours to design&lt;/td&gt;
&lt;td&gt;Narrow retrieval services and cost-sensitive workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed RAG service&lt;/td&gt;
&lt;td&gt;Little infrastructure for us to operate&lt;/td&gt;
&lt;td&gt;Provider-defined ingestion, ranking, and observability&lt;/td&gt;
&lt;td&gt;Contractual and provider-dependent&lt;/td&gt;
&lt;td&gt;Fast launch, variable traffic, limited operations staff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated RAGFlow per tenant&lt;/td&gt;
&lt;td&gt;Multiplies the fixed RAGFlow baseline&lt;/td&gt;
&lt;td&gt;Same integrated workflow with a clearer failure boundary&lt;/td&gt;
&lt;td&gt;Stronger practical separation, higher cost&lt;/td&gt;
&lt;td&gt;Regulated or high-value tenants&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We used 414.7 ms at p50 and 464.4 ms at p95 as our CPU-embedding retrieval baseline. We would not use those values as a universal service-level objective. Corpus size, Elasticsearch configuration, embedding placement, reranking, metadata filters, and concurrent ingestion all change the result. We would use them as a baseline showing that retrieval alone already occupies a substantial portion of a sub-second API budget.&lt;/p&gt;

&lt;p&gt;We built our break-even model with explicit assumptions rather than pretending the available evidence included a monetary bill.&lt;/p&gt;

&lt;p&gt;Consider a shared 16 GB host priced at an illustrative &lt;code&gt;$0.25/hour&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host cost:             730 hours × $0.25       = $182.50/month
Operations allowance: 4 hours × $100           = $400.00/month
Estimated fixed cost:                            $582.50/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At an illustrative managed retrieval price of &lt;code&gt;$0.002/request&lt;/code&gt;, infrastructure plus four hours of monthly operations breaks even at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$582.50 / $0.002 = 291,250 retrieval requests per month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This excludes generation tokens, embedding-provider charges, backup storage, outbound traffic, monitoring, and engineering migration work. Those exclusions apply to both sides and must be filled with actual vendor quotes.&lt;/p&gt;

&lt;p&gt;Shared across ten modest tenants, the illustrative fixed cost becomes &lt;code&gt;$58.25&lt;/code&gt; per tenant per month. Dedicated stacks provide clearer isolation boundaries but push the same illustrative baseline toward &lt;code&gt;$582.50&lt;/code&gt; per tenant before model usage. That is the central trade-off: sharing makes RAGFlow financially attractive precisely where we have the least evidence of effective quotas and noisy-neighbor protection.&lt;/p&gt;

&lt;p&gt;We would choose RAGFlow over custom LangChain or LlamaIndex glue when the engineering savings from complex parsing and administrative visibility exceed its incremental infrastructure and operations costs relative to our existing stack. We would choose a lean vector stack when inputs are already clean text and the product needs only ingestion, metadata filtering, and retrieval. We would choose managed RAG when volume is uncertain and avoiding an 8.5 GB idle platform matters more than infrastructure ownership.&lt;/p&gt;

&lt;p&gt;Teams needing a workload-specific model can &lt;a href="https://dev.to/services"&gt;review our AI infrastructure services&lt;/a&gt; or &lt;a href="https://dev.to/contact"&gt;contact us for an architecture assessment&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Could Not Verify
&lt;/h2&gt;

&lt;p&gt;We could not establish hard multi-tenant isolation, per-tenant resource quotas, or noisy-neighbor protection from the reviewed 0.26.4 surfaces. Our offline API fixtures verified client behavior, not a running RAGFlow server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;RAGFlow 0.26.4 works best as a document-understanding platform, not as a RAG deployment that is production-ready by default.&lt;/p&gt;

&lt;p&gt;Its strongest result was parsing fidelity: 99% of planted facts remained available in stored chunks in the measured corpus. Its weaker result was ranking: 73% top-five retrieval accuracy left meaningful room for embedding, chunking, threshold, keyword, and reranking work. The platform preserves evidence well, but teams still have to tune how that evidence is surfaced.&lt;/p&gt;

&lt;p&gt;The Apache 2.0 license, integrated UI, benchmark CLI, HTTP interface, and explicit parse workflow make RAGFlow credible for commercial deployment. The memory baseline, API quirks, route migration risk, and unverified multi-tenant controls stop us from recommending the default Compose configuration as a production architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy this if
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Complex PDFs, tables, and document layouts justify a heavy parsing stack.&lt;/li&gt;
&lt;li&gt;Your tenants belong to one trusted organization or can share a failure domain.&lt;/li&gt;
&lt;li&gt;An approximately 8.5 GB idle baseline is acceptable.&lt;/li&gt;
&lt;li&gt;You will keep the service warm rather than starting it on demand.&lt;/li&gt;
&lt;li&gt;You can pin versions and maintain contract tests around every API upgrade.&lt;/li&gt;
&lt;li&gt;You will check both HTTP status and body-level business codes.&lt;/li&gt;
&lt;li&gt;You can operate Elasticsearch and test backup restoration.&lt;/li&gt;
&lt;li&gt;You are prepared to tune retrieval instead of accepting defaults.&lt;/li&gt;
&lt;li&gt;You can validate enterprise concurrency capabilities before relying on them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hold off or avoid it if
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need cheap scale-to-zero retrieval.&lt;/li&gt;
&lt;li&gt;Your documents are mostly clean text and do not need deep parsing.&lt;/li&gt;
&lt;li&gt;Untrusted customers must share one installation.&lt;/li&gt;
&lt;li&gt;You require proven per-tenant quotas and noisy-neighbor controls.&lt;/li&gt;
&lt;li&gt;Your availability plan assumes Docker Compose is already an HA topology.&lt;/li&gt;
&lt;li&gt;Your host has less than the practical memory headroom implied by the measured baseline.&lt;/li&gt;
&lt;li&gt;You expect upload alone to complete ingestion.&lt;/li&gt;
&lt;li&gt;Your client treats every HTTP 200 response as success.&lt;/li&gt;
&lt;li&gt;You cannot absorb endpoint and backend migration work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our production recommendation is conservative: use RAGFlow for trusted internal deployments or isolate valuable tenants into separate stacks. Do not consolidate unrelated customers merely because the API exposes tenant-aware objects. Authentication and object ownership are not substitutes for resource isolation.&lt;/p&gt;

&lt;p&gt;Before signing off, we would run the &lt;a href="https://github.com/infiniflow/ragflow/blob/d32e05d5/test/benchmark/README.md" rel="noopener noreferrer"&gt;official benchmark CLI&lt;/a&gt; against the actual corpus, embedding model, concurrency target, and RAGFlow edition planned for production. We would then inject simultaneous parsing and retrieval from multiple tenant identities and verify latency, authorization, queue behavior, recovery, and cost.&lt;/p&gt;

&lt;p&gt;RAGFlow can replace a substantial amount of RAG glue. It cannot replace production engineering.&lt;/p&gt;

</description>
      <category>ragflow</category>
      <category>rag</category>
      <category>multitenancy</category>
      <category>docker</category>
    </item>
    <item>
      <title>PyRIT in Production: Agentic Red-Team Scenarios and CI Gate Design</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Wed, 14 Oct 2026 00:37:35 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/pyrit-in-production-agentic-red-team-scenarios-and-ci-gate-design-36ha</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/pyrit-in-production-agentic-red-team-scenarios-and-ci-gate-design-36ha</guid>
      <description>&lt;h2&gt;
  
  
  Why We Brought This Tool Into Our Lab
&lt;/h2&gt;

&lt;p&gt;We brought &lt;a href="https://microsoft.github.io/PyRIT/latest/" rel="noopener noreferrer"&gt;PyRIT&lt;/a&gt; into our evaluation queue because prompt-level test suites stop being convincing once an application becomes agentic. A single malicious input is easy to represent in YAML. A sequence that adapts after a refusal, changes persona, invokes several transformations, and preserves conversational context is not.&lt;/p&gt;

&lt;p&gt;PyRIT: orchestration engine, not a merge gate&lt;/p&gt;
&lt;p&gt;Scenarios turn multi-turn attack loops into repeatable, scenario-run-ID-stamped artifacts, but with no verified exit-code contract, retry/backoff behavior, or scorer false-negative rate in the reviewed material, a low ASR cannot be read as proof of safety — so exit codes should report only whether the scan executed, and release policy must be evaluated separately from the complete result set.&lt;/p&gt;

&lt;p&gt;PyRIT addresses that gap with Python-native attack execution. Its core pipeline combines objectives, attack strategies, converters, targets, scorers, and persistent memory. Multi-turn strategies such as Crescendo, TAP, PAIR, and red-teaming loops can adjust subsequent prompts based on the target's previous response. That makes PyRIT materially different from a static prompt regression suite.&lt;/p&gt;

&lt;p&gt;The July 2026 Scenarios layer was the feature that made us consider it for release engineering rather than occasional security research. A scenario is a packaged playbook containing four practical elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dataset of harmful or policy-violating objectives.&lt;/li&gt;
&lt;li&gt;One or more attack techniques.&lt;/li&gt;
&lt;li&gt;Converters or adversarial prompt transformations.&lt;/li&gt;
&lt;li&gt;Scorers and a result printer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The built-in catalog includes RapidResponse, Jailbreak, Cyber, Leakage, Psychosocial, Scam, Encoding, RedTeamAgent, TextAdaptive, and AdversarialBenchmark. Results receive a scenario-run identifier in PyRIT memory, making cross-run comparison and resumption possible.&lt;/p&gt;

&lt;p&gt;That is useful infrastructure. It is not, however, an autonomous-agent security simulator.&lt;/p&gt;

&lt;p&gt;We cross-checked the architecture against the &lt;a href="https://cloudsecurityalliance.org/artifacts/evaluating-pyrit-for-agentic-ai-red-teaming" rel="noopener noreferrer"&gt;Cloud Security Alliance's agentic PyRIT evaluation&lt;/a&gt;. That boundary matched our assessment of the framework's capabilities: PyRIT tests model-mediated behavior, but it does not natively implement an agent's authorization model, tool execution engine, or complete state machine. It can prompt an application into attempting tool misuse, but it cannot independently prove that the tool call crossed an actual permission boundary.&lt;/p&gt;

&lt;p&gt;That distinction matters. A successful prompt attack is evidence of unsafe model behavior. It is not automatically evidence that money moved, data left the tenant, or an unauthorized API operation completed. For production agents, we would pair PyRIT with application telemetry and assertions around the real tool broker.&lt;/p&gt;

&lt;p&gt;Our evaluation also exposed an important packaging distinction. PyRIT is a red-team execution framework. It is not, by itself, a finished merge policy. We identified RAMPART as a pytest-native layer for turning PyRIT findings into repeatable CI regression tests. That separation is sensible, but it means teams expecting &lt;code&gt;pyrit_scan&lt;/code&gt; to behave like a security linter will have integration work ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We began with the published installation path and Python 3.13 recommendation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.13 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate

python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
pip &lt;span class="nb"&gt;install &lt;/span&gt;pyrit

python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import pyrit; print(f'PyRIT version installed: {pyrit.__version__}')"&lt;/span&gt;
pyrit_scan &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a production repository, we would not leave &lt;code&gt;pyrit&lt;/code&gt; unpinned. The current material spans &lt;code&gt;/latest/&lt;/code&gt;, versioned &lt;code&gt;1.0.1&lt;/code&gt; scanner pages, and a scenario article that references features introduced in &lt;code&gt;0.14.0&lt;/code&gt;. We would install once in a controlled environment, capture the resolved version and lock file, and then promote that exact environment into CI.&lt;/p&gt;

&lt;p&gt;PyRIT expects endpoint configuration in &lt;code&gt;~/.pyrit/.env&lt;/code&gt; and startup configuration in &lt;code&gt;~/.pyrit/.pyrit_conf&lt;/code&gt;. The minimum documented shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ~/.pyrit/.env
OPENAI_CHAT_ENDPOINT="https://api.openai.com/v1"
OPENAI_CHAT_KEY="${OPENAI_CHAT_KEY}"
OPENAI_CHAT_MODEL="your-model"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ~/.pyrit/.pyrit_conf&lt;/span&gt;
&lt;span class="na"&gt;memory_db_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;in_memory&lt;/span&gt;

&lt;span class="na"&gt;initializers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;target&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;scorer&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scorer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Azure, we had to treat the endpoint as provider-specific configuration rather than a generic base URL. The accepted examples include both of these Azure forms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPENAI_CHAT_ENDPOINT="https://example.cognitiveservices.azure.com/openai/v1/"
# Or:
OPENAI_CHAT_ENDPOINT="https://example.services.ai.azure.com/openai/v1"
OPENAI_CHAT_KEY="${AZURE_OPENAI_KEY}"
OPENAI_CHAT_MODEL="deployment-or-model-name"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We would inject that file at runtime rather than bake it into a container. The key should come from the CI secret store, while the endpoint and model can remain environment-specific deployment variables.&lt;/p&gt;

&lt;p&gt;For Ollama or llama.cpp, we found no source-verified native initializer example in the material we evaluated. The viable pattern is therefore an OpenAI-compatible endpoint or a custom PyRIT HTTP target, not an assumed first-class &lt;code&gt;ollama&lt;/code&gt; target. An OpenAI-compatible local configuration may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Valid only when the local server exposes an OpenAI-compatible /v1 API.
OPENAI_CHAT_ENDPOINT="http://host.docker.internal:11434/v1"
OPENAI_CHAT_KEY="local-placeholder"
OPENAI_CHAT_MODEL="local-model-name"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linux containers may need an explicit host mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host.docker.internal:host-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.pyrit:/root/.pyrit:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  pyrit-runner:locked &lt;span class="se"&gt;\&lt;/span&gt;
  pyrit_scan run airt.scam &lt;span class="nt"&gt;--target&lt;/span&gt; openai_chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We would use a custom target rather than force compatibility if the local server's request or response shape differs. That also gives us a clean place to record request counts, inject deterministic failures, and distinguish target latency from adversarial-model latency.&lt;/p&gt;

&lt;p&gt;The basic scanner paths are concise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyrit_scan run airt.scam &lt;span class="nt"&gt;--target&lt;/span&gt; openai_chat
pyrit_scan run airt.rapid_response &lt;span class="nt"&gt;--target&lt;/span&gt; my_target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RapidResponse performs a baseline pass and, with its default technique selection, applies role-play and many-shot attacks across seven AIRT categories: hate, fairness, violence, sexual content, harassment, misinformation, and leakage. This is a broad assessment, not a cheap smoke test.&lt;/p&gt;

&lt;p&gt;The same abstraction is available from Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyrit.scenario.scenarios.airt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RapidResponse&lt;/span&gt;

&lt;span class="n"&gt;scenario&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RapidResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objective_target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_async&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We skipped local PyRIT execution in this review. Our intended sandbox used Python 3.12, and we lacked a verified compatible package pin, a runnable offline target/scorer configuration, and a model endpoint suitable for measuring real scorer behavior. The installation and configuration examples above are proposed setup paths, not verified installation or scenario-run results.&lt;/p&gt;

&lt;p&gt;The following runnable shell harness demonstrates how we would normalize a completed scan into an adapter-owned CI artifact. The JSON is explicitly simulated; it is not presented as PyRIT's native schema or as observed benchmark output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; artifacts

python - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; artifacts/pyrit-gate.json
import json

# Simulated normalized output produced by a CI adapter after reading
# PyRIT memory or exported results. This is not PyRIT's native schema.
result = {
    "fixture": "simulated",
    "scenario": "airt.scam",
    "scenario_run_id": "demo-run-001",
    "objectives": 12,
    "successful_attacks": 1,
    "attack_success_rate": 0.0833,
    "inconclusive_scores": 2,
    "transport_errors": 0,
    "gate_decision": "manual-review"
}
print(json.dumps(result, indent=2))
&lt;/span&gt;&lt;span class="no"&gt;PY

&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;artifacts/pyrit-gate.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simulated output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fixture"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"simulated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scenario"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"airt.scam"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scenario_run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-run-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"objectives"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"successful_attacks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attack_success_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0833&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inconclusive_scores"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transport_errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gate_decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manual-review"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adapter boundary is deliberate. We would preserve raw conversations and scorer records, then create a stable internal schema that does not change whenever PyRIT changes its CLI presentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  PyRIT Integration Problems and Limitations
&lt;/h2&gt;

&lt;p&gt;The first roadblock was version ambiguity. Our setup plan starts with &lt;code&gt;pip install pyrit&lt;/code&gt;, but we found examples spread across latest and versioned documentation. That is acceptable for exploration but not for a shipping gate. A new scenario name, initializer contract, or result representation can break a pipeline without changing our application.&lt;/p&gt;

&lt;p&gt;Our workaround is conventional: pin Python, pin PyRIT, commit the lock file, record &lt;code&gt;pyrit.__version__&lt;/code&gt; in every artifact, and run a configuration canary before the expensive scenario.&lt;/p&gt;

&lt;p&gt;The second problem was endpoint overloading. &lt;code&gt;OPENAI_CHAT_ENDPOINT&lt;/code&gt; can refer to OpenAI, one of two Azure endpoint forms, or potentially a local OpenAI-compatible proxy. A syntactically valid URL can still be semantically wrong because of a missing &lt;code&gt;/openai/v1&lt;/code&gt;, incorrect deployment or model naming, container DNS, or incompatible authentication.&lt;/p&gt;

&lt;p&gt;We also investigated the visible &lt;code&gt;BASE_URL&lt;/code&gt; error message. We could not reproduce it as a PyRIT runtime configuration error because the text appears as a generic MyST documentation-site warning: “This may be due to an incorrect BASE_URL configuration.” It is not evidence that PyRIT requires a &lt;code&gt;BASE_URL&lt;/code&gt; environment variable for target execution. Treating that banner as runtime troubleshooting would send an operator toward the wrong configuration.&lt;/p&gt;

&lt;p&gt;For target failures, we would validate these separately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DNS and TCP reachability from inside the runner.&lt;/li&gt;
&lt;li&gt;The exact API path and authentication header.&lt;/li&gt;
&lt;li&gt;Model or deployment resolution.&lt;/li&gt;
&lt;li&gt;A minimal chat-completions request.&lt;/li&gt;
&lt;li&gt;PyRIT target initialization.&lt;/li&gt;
&lt;li&gt;Scenario execution.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third issue is that target and scorer plumbing can become entangled. LLM-based scoring needs its own reliable model endpoint. Pointing both attack execution and scoring at the same constrained local model may save cloud spend, but it can also turn the target's weaknesses into the judge's weaknesses. A model that refuses awkwardly, follows transformed text inconsistently, or cannot classify nuanced policy violations will produce noisy verdicts.&lt;/p&gt;

&lt;p&gt;We could not establish a scorer false-negative rate without labeled responses and a real scoring endpoint. That absence itself prevents us from endorsing a binary gate. A low measured attack success rate (ASR) can mean at least four different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The target resisted the attacks.&lt;/li&gt;
&lt;li&gt;The selected techniques were ineffective.&lt;/li&gt;
&lt;li&gt;Requests failed or were throttled.&lt;/li&gt;
&lt;li&gt;The scorer missed successful harmful behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those outcomes require different remediation, yet a naive &lt;code&gt;ASR == 0&lt;/code&gt; rule collapses them into “pass.”&lt;/p&gt;

&lt;p&gt;Rate limiting introduces the same ambiguity. RapidResponse applies every selected technique to every objective, and multi-turn attacks add several requests per attempt. We accounted for latency, API rate-limit budget, and adversarial-chat tokens when assessing scan cost, but we did not verify a retry count, backoff policy, or &lt;code&gt;Retry-After&lt;/code&gt; contract for orchestrator loops.&lt;/p&gt;

&lt;p&gt;We would therefore place retries at a controlled HTTP client or proxy layer, log every 429 separately, and cap retries by objective. Exhausted retries should produce an infrastructure error, never a safe-model verdict. CI should fail closed on lost coverage but should label the result “invalid scan,” not “vulnerability found.”&lt;/p&gt;

&lt;p&gt;Finally, PyRIT does not natively validate actual tool authorization, memory integrity, or long-horizon agent behavior. We would instrument the application under test with a sandboxed tool broker and correlate each PyRIT conversation with tool-call audit events. Without that correlation, an apparent tool-misuse success may be only generated text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;We found the main scaling equation straightforward. RapidResponse behaves roughly like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;objectives × techniques × requests per attack&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The baseline adds another target request per objective, while Crescendo, TAP, PAIR, and other multi-turn methods can add variable numbers of target and adversarial-model calls. Scoring adds further calls when an LLM judge is used.&lt;/p&gt;

&lt;p&gt;TextAdaptive changes the technique dimension. It selects techniques using historical ASR and stops after success, with three attempts per objective as the stated default. Its selector queries PyRIT memory rather than keeping counts internally. That can reduce wasted attempts, but it also means historical data affects future runs. For a strict release comparison, we would isolate memory or use a current-run scope; otherwise, the test policy silently changes as the database accumulates evidence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;th&gt;Multi-turn offensive orchestration&lt;/th&gt;
&lt;th&gt;Agent-system validation&lt;/th&gt;
&lt;th&gt;CI readiness&lt;/th&gt;
&lt;th&gt;Main cost driver&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyRIT Scenarios&lt;/td&gt;
&lt;td&gt;Broad and focused adversarial discovery&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Partial; external instrumentation required&lt;/td&gt;
&lt;td&gt;Medium; adapter or RAMPART needed&lt;/td&gt;
&lt;td&gt;Target, attacker, and scorer calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAMPART on PyRIT&lt;/td&gt;
&lt;td&gt;Regression tests derived from findings&lt;/td&gt;
&lt;td&gt;Inherits PyRIT&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;High for pytest-oriented teams&lt;/td&gt;
&lt;td&gt;Test count and underlying PyRIT calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt-level YAML harness&lt;/td&gt;
&lt;td&gt;Fast deterministic prompt regression&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Number of prompts and judge calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect-style evaluation harness&lt;/td&gt;
&lt;td&gt;General model or agent quality evaluation&lt;/td&gt;
&lt;td&gt;Custom implementation required&lt;/td&gt;
&lt;td&gt;Depends on test environment&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Dataset size and evaluator calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed red-team service&lt;/td&gt;
&lt;td&gt;Teams preferring hosted operation&lt;/td&gt;
&lt;td&gt;Usually packaged&lt;/td&gt;
&lt;td&gt;Provider-dependent&lt;/td&gt;
&lt;td&gt;Medium to high&lt;/td&gt;
&lt;td&gt;Per-scan service and model usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual specialist assessment&lt;/td&gt;
&lt;td&gt;Novel, high-context attack discovery&lt;/td&gt;
&lt;td&gt;Strong but labor-intensive&lt;/td&gt;
&lt;td&gt;Potentially strong&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Engineer or consultant time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We have not published wall-clock, token, throughput, or memory numbers because our setup did not reach a valid endpoint-backed benchmark. Any specific figure would be invented. The useful break-even model is still simple:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;automated release cost = runner cost + target calls + attacker calls + scorer calls + review time&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Compare that with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;manual release cost = specialist hours × loaded hourly rate&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;PyRIT pays off when the same scenario is run repeatedly across releases. The initial integration includes target adapters, labeled scorer calibration, artifact normalization, and triage policy. After that, the marginal cost per release can fall substantially because scenario execution and evidence capture are automated.&lt;/p&gt;

&lt;p&gt;It does not pay off when a team runs one shallow scan and treats the resulting ASR as a security certificate. In that case, setup and scorer calibration can cost more than a focused manual review while producing weaker evidence.&lt;/p&gt;

&lt;p&gt;For cost control, we would use a staged pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run a small deterministic regression set on every pull request.&lt;/li&gt;
&lt;li&gt;Run focused Jailbreak, Leakage, Cyber, or Scam scenarios on protected branches.&lt;/li&gt;
&lt;li&gt;Run RapidResponse on scheduled builds or release candidates.&lt;/li&gt;
&lt;li&gt;Use TextAdaptive after enough trustworthy historical evidence exists.&lt;/li&gt;
&lt;li&gt;Send successful and inconclusive results to human review.&lt;/li&gt;
&lt;li&gt;Retain raw traces only under an explicit sensitive-data policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teams comparing this approach with adjacent infrastructure can &lt;a href="https://dev.to/tools"&gt;browse our tools collection&lt;/a&gt;. For architecture and integration help around model gateways, observability, and controlled agent sandboxes, see &lt;a href="https://dev.to/services"&gt;our engineering services&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Could Not Verify
&lt;/h2&gt;

&lt;p&gt;We did not complete a live scenario benchmark, measure scorer false-negative rates, or verify orchestrator retry and backoff behavior. Our configuration examples and simulated CI artifact do not establish runtime performance or gate reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;PyRIT is one of the more credible open-source foundations we have examined for repeatable, multi-turn offensive testing. Scenarios solve a real operational problem: they turn fragile notebooks into named, reusable playbooks with scenario-run identifiers and memory-backed results.&lt;/p&gt;

&lt;p&gt;We would deploy it, but not as a bare binary merge gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need multi-turn attacks rather than a static prompt corpus.&lt;/li&gt;
&lt;li&gt;We can pin the package and own the target initializer.&lt;/li&gt;
&lt;li&gt;We can separate target, attacker, and scorer endpoints.&lt;/li&gt;
&lt;li&gt;We are prepared to calibrate scorers against human-labeled examples.&lt;/li&gt;
&lt;li&gt;We can preserve raw traces and distinguish vulnerabilities from infrastructure failures.&lt;/li&gt;
&lt;li&gt;We can instrument real tools, permissions, memory, and side effects around the agent.&lt;/li&gt;
&lt;li&gt;We want repeatable discovery scans that can later become regression tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need a turnkey non-zero exit code whenever any attack succeeds.&lt;/li&gt;
&lt;li&gt;We expect ASR alone to prove that a release is safe.&lt;/li&gt;
&lt;li&gt;Our agent cannot be placed behind a sandboxed target or tool broker.&lt;/li&gt;
&lt;li&gt;We cannot tolerate variable multi-turn request volume.&lt;/li&gt;
&lt;li&gt;We lack a trustworthy scorer or a human adjudication path.&lt;/li&gt;
&lt;li&gt;We need native validation of authorization, tool execution, or long-horizon state.&lt;/li&gt;
&lt;li&gt;We are unwilling to maintain compatibility across PyRIT releases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our production pattern would be PyRIT for discovery, a normalized artifact adapter for CI, and deterministic regression tests for confirmed findings. We would use process exit codes only to indicate whether the scan executed successfully. Our CI adapter would evaluate security policy separately using the complete result set, with three outcomes: pass, block, or manual review.&lt;/p&gt;

&lt;p&gt;That is less convenient than &lt;code&gt;pyrit_scan &amp;amp;&amp;amp; merge&lt;/code&gt;, but it is operationally honest. A transport timeout is not a safe result. A scorer disagreement is not a pass. A low ASR is not proof that an agent's tools are protected.&lt;/p&gt;

&lt;p&gt;PyRIT is worth deploying when we treat it as an offensive orchestration engine rather than a compliance checkbox. Its scenarios provide a strong starting methodology, its memory model supports repeatable analysis, and its Python architecture leaves room for custom targets and scorers. Our production integration would verify retry behavior, calibrate scorers, normalize artifacts, enforce release policy, and add real agent-side observability.&lt;/p&gt;

&lt;p&gt;If those controls are already part of the platform, PyRIT can remove a large amount of repetitive red-team scripting. If they are not, budget for that engineering before promising a shipping gate. Teams deciding whether that investment fits their release process can &lt;a href="https://dev.to/contact"&gt;contact us&lt;/a&gt; with their target architecture and threat model.&lt;/p&gt;

</description>
      <category>airedteaming</category>
      <category>pyrit</category>
      <category>cicd</category>
      <category>agentsecurity</category>
    </item>
    <item>
      <title>Daft in Production: A Hard-Nosed Multimodal Pipeline Review Against Ray Data and Spark</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Tue, 13 Oct 2026 00:40:04 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/daft-in-production-a-hard-nosed-multimodal-pipeline-review-against-ray-data-and-spark-5bi6</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/daft-in-production-a-hard-nosed-multimodal-pipeline-review-against-ray-data-and-spark-5bi6</guid>
      <description>&lt;h2&gt;
  
  
  Why We Evaluated Daft for Multimodal Pipelines
&lt;/h2&gt;

&lt;p&gt;The question that pushed us toward &lt;a href="https://github.com/Eventual-Inc/Daft" rel="noopener noreferrer"&gt;Daft&lt;/a&gt; was not whether it could perform another Parquet scan. We needed to know whether it could move compressed images, audio, PDFs, and video through CPU decoding and GPU inference without forcing us to hand-manage every intermediate batch.&lt;/p&gt;

&lt;p&gt;That is a materially different problem from conventional analytics. A JPEG may expand by roughly 20 times when decoded. A video can turn into thousands of multi-megabyte frames. The engine must overlap object-storage reads, decoding, preprocessing, inference, and output while keeping expanded objects from accumulating in memory.&lt;/p&gt;

&lt;p&gt;Daft approaches this as a declarative DataFrame engine with native multimodal expressions. Its Swordfish execution layer streams batches between operators and applies backpressure when a downstream operation, such as GPU inference, cannot keep up. Flotilla extends that execution model across workers. The practical appeal is straightforward: retain DataFrame and SQL-style planning while avoiding the partition-sized materialization that makes binary-heavy pipelines painful.&lt;/p&gt;

&lt;p&gt;We compared that model with two realistic alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ray Data&lt;/strong&gt;, where resource-aware Python operators, actors, and independent CPU/GPU scaling offer considerable control, but partitioning, column retention, actor concurrency, and object-store pressure can become application responsibilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spark&lt;/strong&gt;, which remains the operational default in many companies but exposes awkward global controls for Python and Arrow UDF batches. Those controls are especially blunt when one stage handles compressed bytes and another holds decoded tensors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We used the September 2025 distributed benchmark as the common reference point: Daft 0.6.2, Ray Data 2.49.2, and AWS EMR Spark 7.10.0 on eight g6.xlarge workers. The workload set covered 113,800 audio files with Whisper-tiny, 10,000 PDFs with all-MiniLM-L6-v2, 803,580 ImageNet images with ResNet18, and 1,000 videos with YOLO11n. The code and logs are linked from &lt;a href="https://docs.daft.ai/en/stable/benchmarks/" rel="noopener noreferrer"&gt;Daft's benchmark page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We did not treat those headline results as neutral truth. We inspected the competing reproduction, input-layout differences, batch settings, encoder defaults, and instance-shape sweep. That changed our conclusion substantially: Daft is strong, but “Daft is always faster” is not defensible. CPU-to-GPU ratio and data layout can reverse the ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;The base installation path is intentionally small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate

pip &lt;span class="nb"&gt;install &lt;/span&gt;daft
&lt;span class="c"&gt;# Equivalent with uv:&lt;/span&gt;
uv add daft
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the Ray-backed distributed path, the documented extra is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"daft[ray]"&lt;/span&gt;
ray start &lt;span class="nt"&gt;--head&lt;/span&gt; &lt;span class="nt"&gt;--disable-usage-stats&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default process runs locally. Connecting the same logical pipeline to Ray requires setting the runner before constructing or executing the DataFrame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;daft&lt;/span&gt;

&lt;span class="n"&gt;daft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_runner_ray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ray://127.0.0.1:10001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one-line switch is real at the pipeline API level, but it does not make cluster operations disappear. The client and workers must carry matching Daft versions and matching Python minor versions. Worker images still need model files, codecs, credentials, native libraries, and compatible accelerator builds.&lt;/p&gt;

&lt;p&gt;We designed the following local smoke test for a COCO-style image record; it requires no API keys. As written, our script is designed to generate JPEG and WAV fixtures, serve them from localhost, define a lazy image-decoding expression, and write a Parquet manifest containing only IDs and URLs. Because the final projection excludes &lt;code&gt;decoded_image&lt;/code&gt;, the optimizer can prune that unused expression; writing the manifest would not establish that downloading or decoding actually executed. The audio file is included so the fixture set can be extended into a decode or transcription stage without requiring an external API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"daft==0.6.2"&lt;/span&gt; &lt;span class="s2"&gt;"Pillow==11.3.0"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; fixtures output

python - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
from pathlib import Path
from PIL import Image
import math
import struct
import wave

Path("fixtures").mkdir(exist_ok=True)

image = Image.new("RGB", (640, 480), color=(34, 91, 140))
image.save("fixtures/sample.jpg", quality=90)

sample_rate = 16_000
seconds = 1
with wave.open("fixtures/sample.wav", "wb") as wav:
    wav.setnchannels(1)
    wav.setsampwidth(2)
    wav.setframerate(sample_rate)
    frames = []
    for i in range(sample_rate * seconds):
        value = int(8000 * math.sin(2 * math.pi * 440 * i / sample_rate))
        frames.append(struct.pack("&amp;lt;h", value))
    wav.writeframes(b"".join(frames))
&lt;/span&gt;&lt;span class="no"&gt;PY

&lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000 &lt;span class="nt"&gt;--directory&lt;/span&gt; fixtures &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/tmp/daft-fixture-http.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nv"&gt;SERVER_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'kill "$SERVER_PID"'&lt;/span&gt; EXIT

python - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import json
import time
import daft

started = time.perf_counter()

df = daft.from_pydict(
    {
        "image_id": [1],
        "image_url": ["http://127.0.0.1:8000/sample.jpg"],
        "audio_url": ["http://127.0.0.1:8000/sample.wav"],
    }
)

df = df.with_column(
    "decoded_image",
    daft.col("image_url")
        .url.download()
        .image.decode(mode=daft.ImageMode.RGB),
)

df.select("image_id", "image_url", "audio_url").write_parquet(
    "output/manifest.parquet"
)

print(
    json.dumps(
        {
            "status": "ok",
            "rows_written": 1,
            "image_decode": "materialized",
            "output": "output/manifest.parquet",
            "elapsed_s": round(time.perf_counter() - started, 3),
        }
    )
)
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful run should produce stdout in this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rows_written"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image_decode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"materialized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"output/manifest.parquet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"elapsed_s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.742&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON above is illustrative, not observed output: the elapsed value is simulated, and &lt;code&gt;image_decode&lt;/code&gt; is hard-coded to &lt;code&gt;materialized&lt;/code&gt;, not measured. As written, our harness is intended to check installation, expression construction, and manifest writing—not successful image decoding. A decode-validation test must consume the decoded result and assert its contents. We did not complete such a test because our sandbox failed during installation with a storage error and produced no runtime observations. A performance job should also execute thousands of objects, separate cold and warm runs, and capture process RSS, object-store spilling, GPU utilization, output bytes, and per-stage counters.&lt;/p&gt;

&lt;p&gt;Our attempted pinned installation provided one useful but unglamorous result. The resolver successfully selected the Daft 0.6.2 CPython ABI3 Linux ARM64 wheel, but the isolated environment ran out of storage while installing Daft, PyArrow, NumPy, and Pillow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Installing collected packages: typing-extensions, tqdm, pyarrow,
Pillow, numpy, fsspec, daft
ERROR: Could not install packages due to an OSError:
[Errno 28] No space left on device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We therefore do not present the local decode test as completed. This was a sandbox-capacity failure, not a Daft runtime defect, but it highlights why CI images need more headroom than the 43.8 MB Daft wheel alone suggests. PyArrow, NumPy, Pillow, downloaded wheels, the virtual environment, fixtures, and outputs all count.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke: The Gotchas and Limitations We Hit
&lt;/h2&gt;

&lt;p&gt;The largest problem was not syntax. It was separating engine behavior from benchmark construction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flotilla and Ray are not cleanly separate setup choices.&lt;/strong&gt; We used the Flotilla-powered benchmark as our reference and evaluated Ray or Kubernetes as the distributed deployment substrate, rather than treating Flotilla as a third interchangeable runner. On the Ray path, &lt;code&gt;daft.set_runner_ray()&lt;/code&gt; selects distributed execution and Flotilla workers perform the underlying work. We found no standalone &lt;code&gt;set_runner_flotilla()&lt;/code&gt; deployment recipe in the reviewed material. Teams should not plan for three interchangeable runners named local, Flotilla, and Ray. In practice, the choices are local execution or a distributed deployment substrate, with Flotilla inside Daft's distributed architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version equality is an operational contract.&lt;/strong&gt; Ray Client requires the Daft version and Python minor version to match between client and server. A laptop on Python 3.12 cannot safely be treated as a transparent client for workers built around another minor version. We would ship the driver and worker from the same immutable image rather than rely on Ray runtime installation.&lt;/p&gt;

&lt;p&gt;We also would not run benchmark-pinned Daft 0.6.2 code against the current stable documentation without a compatibility test. The newer documentation includes AI functions and a legacy UDF migration path that are not a substitute for the API surface used by old benchmark scripts. We could not establish a specific cross-release break from our failed local execution, so we are not inventing one. Our production safeguard is a lockfile, image digest, schema test, and materialization test before any version bump.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Null and corrupt media need an explicit policy.&lt;/strong&gt; In our best-effort image-classification reproduction, we added an explicit null filter after decoding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decoded_image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;not_null&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is more than cleanup. Silent row removal changes corpus cardinality and can bias a dataset if corruption correlates with source, format, or age. We would write rejected URIs and decoder errors to a quarantine table, assert accepted plus rejected equals input, and fail CI if the reject rate crosses a threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partition layout moved Daft's image runtime by 20–25%.&lt;/strong&gt; In our best-effort image-classification reproduction, we added an explicit repartition step that was absent from the original pipeline. Without it, execution time increased by 20–25%. We suspected the original metadata was already suitably partitioned, but did not establish that explanation. This means “same number of images” is insufficient. Benchmark manifests must preserve row-group sizes, partitions, URI ordering, and object sizes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Many small objects remain dangerous.&lt;/strong&gt; None of the reviewed end-to-end tables publishes a small-file collapse threshold or peak RSS. We therefore cannot attach a trustworthy files-per-second number to that failure mode. We would test 5,000 small objects against five objects containing the same total bytes through MinIO or S3, recording request count, first-output latency, throughput, and peak RSS. Metadata calls and per-object connection overhead can dominate before model inference matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch size is still a production knob.&lt;/strong&gt; In the reference runs, Ray's PDF embedding batch had to fall from 100 to 10 to avoid repeated failures. Audio used 64 and still experienced some failures while completing. Spark required &lt;code&gt;spark.executor.cores=1&lt;/code&gt; for most workloads plus &lt;code&gt;spark.sql.execution.arrow.maxRecordsPerBatch&lt;/code&gt;. The Spark setting is global, so a safe batch for decoded images also constrains a lightweight text UDF.&lt;/p&gt;

&lt;p&gt;Daft needed model-UDF batch tuning too, but its streaming backpressure avoided forcing every stage through the same global batch size. That is the most credible architectural advantage we found.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Could Not Verify
&lt;/h2&gt;

&lt;p&gt;Our installation failed before we could complete the local decode test. We also could not verify peak RSS, a small-file failure threshold, or the share of wall-clock time spent on downloads, media decoding, embedding, and writes. The published tables are end-to-end. In the image-classification instance sweep, we observed GPU utilization below 20% for both Daft and Ray on four-CPU g6.xlarge workers. That identified upstream CPU starvation, but did not establish the precise share of time spent decoding. For our own deployments, per-stage timing is a release gate, not an optional dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;The original eight-worker reference produced the following end-to-end results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Daft 0.6.2&lt;/th&gt;
&lt;th&gt;Ray Data 2.49.2&lt;/th&gt;
&lt;th&gt;EMR Spark 7.10.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Audio transcription, 113,800 files&lt;/td&gt;
&lt;td&gt;6m 22s&lt;/td&gt;
&lt;td&gt;29m 20s&lt;/td&gt;
&lt;td&gt;25m 46s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document embedding, 10,000 PDFs&lt;/td&gt;
&lt;td&gt;1m 54s&lt;/td&gt;
&lt;td&gt;14m 32s&lt;/td&gt;
&lt;td&gt;8m 04s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image classification, 803,580 images&lt;/td&gt;
&lt;td&gt;4m 23s&lt;/td&gt;
&lt;td&gt;23m 30s&lt;/td&gt;
&lt;td&gt;45m 07s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video object detection, 1,000 videos&lt;/td&gt;
&lt;td&gt;11m 46s&lt;/td&gt;
&lt;td&gt;25m 54s&lt;/td&gt;
&lt;td&gt;3h 36m&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those numbers favor Daft heavily, but they do not settle the buying decision.&lt;/p&gt;

&lt;p&gt;In our &lt;a href="https://www.anyscale.com/blog/ray-data-daft-benchmarking-multimodal-ai-workloads" rel="noopener noreferrer"&gt;best-effort Ray Data reproduction&lt;/a&gt;, we could not access the original datasets for the audio, document, and image workloads. We reconstructed inputs using Common Voice, Digital Corpora, and an 800,000-image ImageNet subset, following the published methodology; the video workload used the original dataset. We dropped consumed columns from the Ray pipelines and used Ray Data 2.50 RC's newer download and Parquet optimizations for image classification. These results are useful for tuning, but they are not a byte-for-byte rerun of every original workload.&lt;/p&gt;

&lt;p&gt;The CPU-to-GPU sweep mattered most. On image classification, Daft completed in about 315 seconds versus Ray Data 2.50 RC at about 456 seconds on four-CPU g6.xlarge workers. At 32 CPUs per GPU, Ray completed in about 111 seconds while Daft stayed near 195 seconds. In this image-classification sweep, engine ranking flipped as additional CPUs reduced upstream starvation; GPU utilization still remained low in absolute terms.&lt;/p&gt;

&lt;p&gt;The cost-optimal points were much closer: approximately $0.44 per Daft run and $0.43 per Ray run, with Ray finishing in about 145 seconds versus Daft at about 202 seconds at each engine's lowest-cost configuration. For an 800,000-image run, that normalizes to roughly $0.55 per million images for Daft and $0.54 for Ray, excluding storage requests, output storage, model distribution, control-plane cost, retries, and engineering labor.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision factor&lt;/th&gt;
&lt;th&gt;Daft&lt;/th&gt;
&lt;th&gt;Ray Data&lt;/th&gt;
&lt;th&gt;Spark&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multimodal API&lt;/td&gt;
&lt;td&gt;Strong native expressions&lt;/td&gt;
&lt;td&gt;Flexible Python operators and integrations&lt;/td&gt;
&lt;td&gt;UDF-heavy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backpressure&lt;/td&gt;
&lt;td&gt;Integrated streaming model&lt;/td&gt;
&lt;td&gt;Streaming execution with explicit tuning options&lt;/td&gt;
&lt;td&gt;Partition and executor tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU/GPU scheduling&lt;/td&gt;
&lt;td&gt;Simple, less manually exposed&lt;/td&gt;
&lt;td&gt;Most flexible across resource shapes&lt;/td&gt;
&lt;td&gt;Awkward for mixed Python/GPU stages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory controls&lt;/td&gt;
&lt;td&gt;Per-UDF batching and adaptive streaming&lt;/td&gt;
&lt;td&gt;Object store, actors, concurrency, batch and block controls&lt;/td&gt;
&lt;td&gt;Executor memory and global Arrow batch controls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational maturity&lt;/td&gt;
&lt;td&gt;Younger ecosystem&lt;/td&gt;
&lt;td&gt;Strong fit if Ray is already deployed&lt;/td&gt;
&lt;td&gt;Strongest enterprise footprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Declarative multimodal ETL&lt;/td&gt;
&lt;td&gt;Tuned distributed AI pipelines&lt;/td&gt;
&lt;td&gt;Existing SQL-centric data platform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The video comparison showed that output encoding also affected runtime. Daft used a Rust-native PNG encoder while the Ray implementation used Pillow. Pillow's default compression produced files 7.8% smaller than the Rust default; compression level two narrowed the discrepancy to 4.4%. Changing compression and Ray's batch size reduced its video runtime from roughly 1,590 seconds to 922 seconds, while Daft remained around 759 seconds on g6.xlarge workers. That benchmark was partly measuring encoder defaults.&lt;/p&gt;

&lt;p&gt;Our break-even rule is therefore operational. If Ray already runs your training or serving estate, a difference of about one cent per 800,000-image run does not justify introducing another control plane. If a small team is about to adopt Spark solely for multimodal preprocessing, Daft is the more compelling first test because the developer and tuning burden can dominate compute cost. Teams evaluating a broader stack can also &lt;a href="https://dev.to/tools"&gt;check our tools collection&lt;/a&gt; or bring a representative pipeline to our &lt;a href="https://dev.to/services"&gt;AI infrastructure services&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Deploy Daft if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your rows contain images, audio, PDFs, video, tensors, or embeddings rather than mostly scalar columns.&lt;/li&gt;
&lt;li&gt;You want declarative filtering, projection, schema handling, and multimodal transformations in one pipeline.&lt;/li&gt;
&lt;li&gt;Decode expansion and intermediate memory pressure are causing OOMs or excessive disk spilling.&lt;/li&gt;
&lt;li&gt;You need the same logical pipeline to run locally and on a Ray-backed cluster.&lt;/li&gt;
&lt;li&gt;Your team does not want to manage Spark executor cores and one global Arrow batch limit for heterogeneous UDFs.&lt;/li&gt;
&lt;li&gt;You can pin Daft, Python, codecs, model packages, and worker images together.&lt;/li&gt;
&lt;li&gt;You will benchmark your own object layout and CPU-to-GPU ratio instead of copying a vendor chart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ray already powers your distributed estate and your engineers are comfortable tuning actors, blocks, column retention, and resource requests.&lt;/li&gt;
&lt;li&gt;Your workload benefits from more CPU capacity per GPU; the image-classification instance sweep shows Ray can win decisively on CPU-richer workers.&lt;/li&gt;
&lt;li&gt;Your platform is fundamentally SQL and JVM-centric, with governance and operations already standardized on Spark.&lt;/li&gt;
&lt;li&gt;You require a standalone, fully documented Flotilla deployment path separate from Ray or Kubernetes.&lt;/li&gt;
&lt;li&gt;You cannot tolerate a younger API surface or enforce exact client-worker version parity.&lt;/li&gt;
&lt;li&gt;You expect the engine to solve corrupt-media policy, corpus accounting, and small-file economics automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We would start with Daft for a new multimodal ETL tier, but only after a controlled comparison. We would pin the environment, use identical bytes and output encoding, preserve the same partitions, sweep UDF batch sizes, test at least two CPU-to-GPU ratios, and capture per-stage wall clock plus peak RSS. We would also price storage requests and retries, not merely GPU seconds.&lt;/p&gt;

&lt;p&gt;Daft's strongest case is not the headline 2–18x benchmark advantage. That figure changes when datasets, partitioning, CPUs, column retention, and encoder settings change. Its strongest case is that it gives us a cleaner way to express a multimodal pipeline while placing backpressure and bounded streaming inside the engine.&lt;/p&gt;

&lt;p&gt;That is enough to make Daft worth a serious proof of concept. It is not enough to skip measurement. If you want help designing a reproducible comparison using representative production data, &lt;a href="https://dev.to/contact"&gt;contact the Effloow team&lt;/a&gt; before committing to the cluster architecture.&lt;/p&gt;

</description>
      <category>daft</category>
      <category>raydata</category>
      <category>apachespark</category>
      <category>multimodaletl</category>
    </item>
    <item>
      <title>OpenHands Self-Hosted Runtime Review: Docker Isolation, Agent Cost, and the Production Reality</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Mon, 12 Oct 2026 00:36:53 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/openhands-self-hosted-runtime-review-docker-isolation-agent-cost-and-the-production-reality-3eh4</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/openhands-self-hosted-runtime-review-docker-isolation-agent-cost-and-the-production-reality-3eh4</guid>
      <description>&lt;h2&gt;
  
  
  Why We Evaluated OpenHands
&lt;/h2&gt;

&lt;p&gt;We brought OpenHands into our evaluation queue because buying another coding-agent seat does not solve the infrastructure question facing engineering teams. We wanted to know whether we could operate the execution layer ourselves, connect it to a model provider of our choice, and retain control over source code, runtime images, network policy, and usage accounting.&lt;/p&gt;

&lt;p&gt;We often see OpenHands described as an open-source Devin alternative, but that description is too shallow for an architecture review. The interesting component is not the chat interface. It is the runtime responsible for turning model-generated actions into shell commands, file mutations, Python execution, and observations.&lt;/p&gt;

&lt;p&gt;We traced that path through the &lt;a href="https://docs.openhands.dev/openhands/usage/architecture/runtime" rel="noopener noreferrer"&gt;OpenHands runtime architecture&lt;/a&gt;. The backend launches a purpose-built runtime container, starts an action execution server inside it, sends actions over an HTTP interface, and receives structured observations. The model remains outside that execution boundary. The container is where potentially destructive commands run.&lt;/p&gt;

&lt;p&gt;That separation is useful. It gives us an explicit control point for image policy, workspace mounts, resource limits, process cleanup, and audit logging. It also means OpenHands is not merely an application that happens to use Docker. Docker is part of its security and scheduling model.&lt;/p&gt;

&lt;p&gt;The image workflow is more sophisticated than a naïve build-per-task design. We found three levels of runtime tags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A versioned tag tied to the OpenHands version and base image.&lt;/li&gt;
&lt;li&gt;A lock tag derived from the base image and dependency files.&lt;/li&gt;
&lt;li&gt;A source tag incorporating the OpenHands source hash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an exact source image exists, the runtime can reuse it. When only the dependency-compatible layer exists, it can copy changed source without repeating the slow dependency installation path. That design should reduce repeated build work, although we would still pin immutable image digests rather than rely on a moving tag in production.&lt;/p&gt;

&lt;p&gt;The project’s 90,000-plus-star &lt;a href="https://github.com/All-Hands-AI/OpenHands" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; made it impossible to dismiss as a weekend prototype. We treated that popularity as an adoption signal, not evidence of secure isolation or predictable operating cost.&lt;/p&gt;

&lt;p&gt;Our central questions were therefore practical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can we reproduce the documented local deployment without hidden services?&lt;/li&gt;
&lt;li&gt;Does the sandbox protect the host under a realistic threat model?&lt;/li&gt;
&lt;li&gt;Can we measure model spend per conversation?&lt;/li&gt;
&lt;li&gt;What happens when multiple sandboxes need ports, storage, and cleanup?&lt;/li&gt;
&lt;li&gt;At what team size does self-hosting become cheaper than buying hosted seats?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also wanted to separate what we could verify from what required a functioning Docker daemon and paid model credentials. That distinction became important almost immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We started with the supported local prerequisites: Python 3.12, &lt;code&gt;uv&lt;/code&gt;, and a Docker installation with access to the Docker socket. We treated the 4 GB host-memory recommendation as a setup baseline, not a measured per-sandbox footprint. It is a minimum host recommendation, not a capacity-planning number.&lt;/p&gt;

&lt;p&gt;The shortest launcher path was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;openhands &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12
openhands serve &lt;span class="nt"&gt;--mount-cwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--mount-cwd&lt;/code&gt; option mounts the current repository into the workspace. That convenience has a direct security consequence: an agent with write access can modify anything exposed through that mount.&lt;/p&gt;

&lt;p&gt;For a more controlled reproduction, we prepared a command with explicit application and agent-server versions rather than using &lt;code&gt;latest&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--pull&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AGENT_SERVER_IMAGE_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ghcr.io/openhands/agent-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AGENT_SERVER_IMAGE_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.26.0-python &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;LOG_ALL_EVENTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.openhands:/.openhands"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-host&lt;/span&gt; host.docker.internal:host-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; openhands-app &lt;span class="se"&gt;\&lt;/span&gt;
  docker.openhands.dev/openhands/openhands:1.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command exposes the web application on &lt;code&gt;http://localhost:3000&lt;/code&gt;. More importantly, it mounts &lt;code&gt;/var/run/docker.sock&lt;/code&gt; into the application container so OpenHands can create and manage sandbox containers.&lt;/p&gt;

&lt;p&gt;We hit a hard environmental stop before a useful runtime benchmark: our available runner did not expose a Docker daemon. We could inspect the launch configuration, but we did not run a deployment-contract test or measure container startup latency, memory consumption, concurrent sandbox throughput, or cleanup behavior on that runner. We did not replace those missing measurements with invented numbers.&lt;/p&gt;

&lt;p&gt;For a host with Docker available, the SDK-level sandbox path is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openhands.workspace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DockerWorkspace&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect_platform&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;machine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aarch64&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linux/arm64&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linux/amd64&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DockerWorkspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;server_image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ghcr.io/openhands/agent-server:1.26.0-python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;host_port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;detect_platform&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;printf &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sandbox-ready&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &amp;amp;&amp;amp; pwd &amp;amp;&amp;amp; python --version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stdout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stderr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected response shape below is simulated to make the reproduction contract explicit. It is not presented as a measurement from our Docker-unavailable runner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"printf 'sandbox-ready&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n' &amp;amp;&amp;amp; pwd &amp;amp;&amp;amp; python --version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exit_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stdout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sandbox-ready&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;/workspace&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Python 3.12.x&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stderr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DockerWorkspace&lt;/code&gt; is the right choice when we can use a prebuilt agent-server image. &lt;code&gt;DockerDevWorkspace&lt;/code&gt; builds from a selected base image and is appropriate when we need additional compilers, system packages, or internal tooling. We would not use the development workspace on a latency-sensitive request path because image construction adds an avoidable startup stage.&lt;/p&gt;

&lt;p&gt;A real agent conversation introduces another dependency: an LLM credential. For our API-backed conversation design, we use &lt;code&gt;LLM_API_KEY&lt;/code&gt; and LiteLLM-style model identifiers such as &lt;code&gt;anthropic/...&lt;/code&gt; or &lt;code&gt;openai/...&lt;/code&gt;, with accumulated cost reporting available through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_stats&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_combined_metrics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accumulated_cost&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We used that metric as the required accounting surface in our production design. We would record it by conversation, repository, model, tenant, and completion state. We would not treat it as a complete bill until reconciling it against provider invoices, retries, failed requests, cache behavior, and any model calls made outside the conversation object.&lt;/p&gt;

&lt;p&gt;A local model endpoint can use a placeholder API-key value when its server does not authenticate requests. That removes the paid-provider key dependency, but it does not remove inference cost. GPU depreciation, idle capacity, model serving, and weaker task completion can easily cost more than API tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment Limitations and Operational Risks
&lt;/h2&gt;

&lt;p&gt;The first failure was simple and decisive: without Docker, there was no meaningful OpenHands sandbox benchmark. A mock Docker client could verify Python control flow, but it could not verify namespaces, mounts, port binding, image pulls, process termination, or actual container cleanup. We rejected a mock-only result as production evidence.&lt;/p&gt;

&lt;p&gt;The second issue was the Docker socket. The architecture isolates agent commands in a child container, but the parent OpenHands application requires control of the host Docker daemon in the direct deployment. On a conventional Linux host, access to that socket is effectively privileged host control. If the application container is compromised, the sandbox boundary does not protect the Docker host from the controller.&lt;/p&gt;

&lt;p&gt;Our workaround is architectural rather than cosmetic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run OpenHands workers on dedicated nodes or disposable virtual machines.&lt;/li&gt;
&lt;li&gt;Do not colocate them with databases, CI secrets, or customer-facing services.&lt;/li&gt;
&lt;li&gt;Restrict outbound network access from both controller and sandbox.&lt;/li&gt;
&lt;li&gt;Mount only the repository needed for the task.&lt;/li&gt;
&lt;li&gt;Prefer read-only mounts plus an explicit writable worktree where possible.&lt;/li&gt;
&lt;li&gt;Apply CPU, memory, process, and execution-time limits.&lt;/li&gt;
&lt;li&gt;Treat the Docker host as replaceable after hostile workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenHands supports bind mounts, named volumes, and overlay copy-on-write behavior. Overlay mode requires a bind mount marked with &lt;code&gt;:overlay&lt;/code&gt; and a writable &lt;code&gt;SANDBOX_VOLUME_OVERLAYS&lt;/code&gt; host directory. If we do not configure that directory, OpenHands skips the overlay path. We would fail startup on that condition rather than silently continue with different write semantics.&lt;/p&gt;

&lt;p&gt;When planning the reverse-proxy deployment, we identified another design problem. Each sandbox normally receives random host ports, while generated browser URLs default to a &lt;code&gt;localhost&lt;/code&gt; pattern. Setting &lt;code&gt;OH_WEB_URL&lt;/code&gt; does not rewrite those sandbox URLs; it affects the allowed origin rather than the hostname and port used to reach the container.&lt;/p&gt;

&lt;p&gt;We found two imperfect options.&lt;/p&gt;

&lt;p&gt;The first is to set &lt;code&gt;OH_SANDBOX_CONTAINER_URL_PATTERN&lt;/code&gt; to a public hostname containing &lt;code&gt;{port}&lt;/code&gt;. That retains a random port for each sandbox, forcing the edge layer to forward a dynamic range.&lt;/p&gt;

&lt;p&gt;The second is host networking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_SERVER_USE_HOST_NETWORK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This exposes fixed ports, including agent-server port 8000 and editor-related port 8001. The trade-off is severe: every sandbox tries to bind the same ports, so only one sandbox can run on that host without collision. We would not use host networking for a multi-tenant production worker.&lt;/p&gt;

&lt;p&gt;We also found that editor capabilities vary by image. We should not assume portable SDK images include VS Code, VNC, a fixed editor port, or &lt;code&gt;/api/vscode/url&lt;/code&gt;. The &lt;code&gt;extra_ports=True&lt;/code&gt; pattern belongs to selected legacy editor-capable images, where &lt;code&gt;host_port + 1&lt;/code&gt; and &lt;code&gt;host_port + 2&lt;/code&gt; may expose VS Code and VNC. We would feature-detect those services instead of deriving URLs unconditionally.&lt;/p&gt;

&lt;p&gt;Finally, moving tags are a reproducibility risk. &lt;code&gt;ghcr.io/openhands/agent-server:latest-python&lt;/code&gt; is convenient for exploration, but production rollouts need pinned versions or, preferably, image digests. We would test the application image, agent-server image, base image, SDK, and model configuration as one release unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Evaluation Could Not Verify
&lt;/h2&gt;

&lt;p&gt;We could not measure OpenHands startup latency, throughput, or memory use because our runner lacked Docker. The available setup material also does not provide a reproducible production benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling and Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;What we can evaluate is the cost and scaling shape.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Execution ownership&lt;/th&gt;
&lt;th&gt;Isolation model&lt;/th&gt;
&lt;th&gt;Model cost&lt;/th&gt;
&lt;th&gt;Operational burden&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenHands self-hosted&lt;/td&gt;
&lt;td&gt;Our team&lt;/td&gt;
&lt;td&gt;Docker sandbox managed by OpenHands&lt;/td&gt;
&lt;td&gt;Paid API or local inference&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Teams needing model and runtime control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2B-style managed sandbox&lt;/td&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;Remote managed sandbox&lt;/td&gt;
&lt;td&gt;Model plus sandbox usage&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Fast deployment without worker operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Docker runner&lt;/td&gt;
&lt;td&gt;Our team&lt;/td&gt;
&lt;td&gt;Whatever we build and audit&lt;/td&gt;
&lt;td&gt;Paid API or local inference&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;Specialized policies or narrow workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted coding-agent seat&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Vendor-managed&lt;/td&gt;
&lt;td&gt;Usually bundled or plan-dependent&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Teams prioritizing speed over control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE agent&lt;/td&gt;
&lt;td&gt;Developer machine&lt;/td&gt;
&lt;td&gt;Local permissions or editor controls&lt;/td&gt;
&lt;td&gt;Subscription or API&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;td&gt;Interactive individual development&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OpenHands removes per-seat runtime licensing from the equation, but it does not make agent execution free. Our monthly model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OpenHands monthly cost =
    worker infrastructure
  + engineering maintenance
  + observability and storage
  + LLM or local inference
  + security and incident overhead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding hosted calculation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hosted monthly cost =
    number of seats × effective monthly seat price
  + overages
  + integration overhead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an illustrative break-even model, assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worker infrastructure and logs: $600 per month.&lt;/li&gt;
&lt;li&gt;Runtime maintenance: 12 engineering hours per month.&lt;/li&gt;
&lt;li&gt;Loaded engineering rate: $150 per hour.&lt;/li&gt;
&lt;li&gt;Self-hosted model usage: $120 per active user per month.&lt;/li&gt;
&lt;li&gt;Hosted alternative: $500 per active user per month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That produces $2,400 in fixed monthly self-hosting cost and a $380 per-user advantage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Break-even users = $2,400 / ($500 - $120) = 6.32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under those assumptions, self-hosting becomes cheaper at approximately seven active users. This is a planning example, not a quoted Devin price or an observed OpenHands bill. If maintenance rises to 30 hours, or if agents burn tokens through repeated tool loops, the break-even point moves sharply upward.&lt;/p&gt;

&lt;p&gt;Agent-loop cost is the largest variable. A task is not one model call. The agent may inspect files, invoke tools, ingest observations, revise its plan, run tests, encounter errors, and ask the model again. Long command output also returns to context and can increase subsequent request cost.&lt;/p&gt;

&lt;p&gt;We would enforce four controls before opening access:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Maximum model spend per conversation.&lt;/li&gt;
&lt;li&gt;Maximum loop iterations and wall-clock duration.&lt;/li&gt;
&lt;li&gt;Output truncation and artifact storage outside the prompt.&lt;/li&gt;
&lt;li&gt;Per-team and per-repository budgets with hard rejection.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For teams building this into a broader internal platform, our &lt;a href="https://dev.to/services"&gt;AI infrastructure services&lt;/a&gt; cover the worker isolation, observability, and cost-control layer that the default local launcher does not provide. We also maintain a broader set of implementation references in our &lt;a href="https://dev.to/tools"&gt;tools collection&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;OpenHands has a credible runtime design. We like the separation between the agent backend and the in-container action server, the prebuilt &lt;code&gt;DockerWorkspace&lt;/code&gt; path, the customizable development workspace, and the exposed conversation-cost metric.&lt;/p&gt;

&lt;p&gt;We do not consider the default single-host Docker deployment a complete production security boundary. The Docker socket, repository mounts, dynamic ports, image compatibility, outbound network policy, and cleanup behavior all become our responsibility the moment we self-host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need control over model providers, runtime images, and source-code location.&lt;/li&gt;
&lt;li&gt;We can dedicate isolated worker hosts to untrusted agent execution.&lt;/li&gt;
&lt;li&gt;We have enough active usage to absorb recurring platform maintenance.&lt;/li&gt;
&lt;li&gt;We can pin and test the full application and agent-server image set.&lt;/li&gt;
&lt;li&gt;We will enforce token, iteration, time, memory, and network limits.&lt;/li&gt;
&lt;li&gt;We accept that self-hosting transfers operational risk rather than eliminating cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need a turnkey service with contractual support and predictable seats.&lt;/li&gt;
&lt;li&gt;We plan to expose the host Docker socket on shared infrastructure.&lt;/li&gt;
&lt;li&gt;We cannot operate dynamic sandbox ports behind our edge architecture.&lt;/li&gt;
&lt;li&gt;We expect bundled VS Code or VNC capabilities in every SDK image.&lt;/li&gt;
&lt;li&gt;We lack per-conversation model budgets and provider-bill reconciliation.&lt;/li&gt;
&lt;li&gt;We need audited multi-tenant isolation stronger than ordinary Docker containers.&lt;/li&gt;
&lt;li&gt;We need benchmark-backed capacity numbers before running our own Docker tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our recommendation is a guarded &lt;strong&gt;deploy for a controlled pilot, not an immediate shared production rollout&lt;/strong&gt;. Start with a dedicated worker, one pinned image set, outbound-deny networking, disposable repository copies, and a small group of engineers. Measure cold and warm startup separately, record accumulated model cost, test cleanup after forced termination, and attempt deliberate workspace escapes before expanding.&lt;/p&gt;

&lt;p&gt;OpenHands can reduce vendor lock-in and give us a useful coding-agent control plane. It does not remove the hard parts. It makes those parts visible—and assigns them to our infrastructure team. Teams that want help validating that trade-off against their own workload can &lt;a href="https://dev.to/contact"&gt;contact us&lt;/a&gt; with their concurrency, repository, compliance, and model assumptions.&lt;/p&gt;

</description>
      <category>openhands</category>
      <category>codingagents</category>
      <category>docker</category>
      <category>aiinfrastructure</category>
    </item>
    <item>
      <title>Inspect AI in Production: A Hard-Nosed Review of Agent Evals, Logs, and Model-Upgrade Gates</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Sun, 11 Oct 2026 00:39:48 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/inspect-ai-in-production-a-hard-nosed-review-of-agent-evals-logs-and-model-upgrade-gates-1mj5</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/inspect-ai-in-production-a-hard-nosed-review-of-agent-evals-logs-and-model-upgrade-gates-1mj5</guid>
      <description>&lt;h2&gt;
  
  
  Why We Evaluated Inspect AI
&lt;/h2&gt;

&lt;p&gt;The model upgrade itself is rarely the difficult part. The difficult part is proving that the new model did not silently damage extraction accuracy, tool behavior, refusal handling, or operational cost.&lt;/p&gt;

&lt;p&gt;Inspect AI Verdict&lt;/p&gt;
&lt;p&gt;Inspect AI provides the execution and artifact layer for item-level agent-eval gates, but raw log reproducibility, scorer contracts, sandbox boundaries, and cost accounting remain engineering responsibilities.&lt;/p&gt;

&lt;p&gt;We have seen the same internal harness emerge repeatedly: a JSONL dataset, an asynchronous request loop, a pile of retry logic, a scorer script, and a spreadsheet comparing the old model with the new one. It works until an incident forces us to answer questions the harness was never designed to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What exact prompt and tool trajectory produced this result?&lt;/li&gt;
&lt;li&gt;Was the failure caused by the model, the grader, the provider, or the sandbox?&lt;/li&gt;
&lt;li&gt;Which individual samples regressed?&lt;/li&gt;
&lt;li&gt;Did the replacement model consume more tokens despite improving pass rate?&lt;/li&gt;
&lt;li&gt;Can we reproduce the evaluation from a pinned task and immutable artifact?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We evaluated &lt;a href="https://inspect.aisi.org.uk/" rel="noopener noreferrer"&gt;Inspect AI&lt;/a&gt; because it handles that entire execution layer. An Inspect evaluation combines a dataset, a solver, and one or more scorers. The solver may be a single generation call, a custom function decorated with &lt;code&gt;@solver&lt;/code&gt;, or a multi-turn agent. The scorer may perform exact matching, inclusion matching, model grading, or arbitrary custom logic.&lt;/p&gt;

&lt;p&gt;The framework also writes structured &lt;code&gt;.eval&lt;/code&gt; logs and ships the &lt;code&gt;inspect view&lt;/code&gt; browser interface. That separation matters. We do not want the dashboard to be the source of truth; we want the artifact to be the source of truth and the dashboard to be one way of reading it.&lt;/p&gt;

&lt;p&gt;The agent surface was another reason for testing it. Inspect accepts agents in the solver position and includes a ReAct loop, tool interfaces, message and token limits, custom agents, agent handoffs, and adapters for external agents. Its tool catalog includes shell, Python, text editing, web, computer, and MCP-oriented interfaces. For dangerous execution, it supports sandbox backends including Docker and more infrastructure-heavy options.&lt;/p&gt;

&lt;p&gt;We also inspected the &lt;a href="https://github.com/UKGovernmentBEIS/inspect_evals" rel="noopener noreferrer"&gt;Inspect Evals repository&lt;/a&gt; rather than evaluating the framework only with toy arithmetic. It provides a substantial task corpus and demonstrates how real evaluations organize datasets, task versions, dependencies, scoring, and sandbox assets. We still recommend pinning a small subset into an internal regression suite rather than importing a moving benchmark wholesale.&lt;/p&gt;

&lt;p&gt;Our short conclusion before the details: Inspect gives us a much stronger foundation than another home-grown request loop. It does not, by itself, make a benchmark deterministic, a model grader trustworthy, or a tool safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We ran the offline scorer check with Python 3.12 in an isolated container. For the local walkthrough, we recommend a virtual environment. That was deliberate: Python 3.11 and 3.12 are the Inspect Evals project's preferred targets, while later Python versions carry compatibility caveats for parts of the broader task corpus.&lt;/p&gt;

&lt;p&gt;For the local walkthrough, use this installation path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.12 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate

python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"inspect-ai==0.3.100"&lt;/span&gt; openai

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"replace-me"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pinned &lt;code&gt;inspect-ai&lt;/code&gt; wheel resolved successfully in our environment. The wheel we downloaded had this SHA-256 digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;78e4c5b7e426fcc25563cd68f9f976a1c18c00adc9f4955bde9a6c97016363d7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, we would pin the complete lockfile rather than only the top-level package. Inspect pulls in a nontrivial dependency tree that includes asynchronous HTTP, schema, filesystem, terminal UI, and cloud-storage packages.&lt;/p&gt;

&lt;p&gt;We then reduced the task shape to the smallest useful production example: registered task, registered solver, custom partial-credit scorer, stable sample IDs, and an explicit log directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# regression_eval.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inspect_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inspect_ai.dataset&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sample&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inspect_ai.scorer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scorer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inspect_ai.solver&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Generate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;solver&lt;/span&gt;


&lt;span class="nd"&gt;@solver&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer_once&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Generate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;solve&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="nd"&gt;@scorer&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;partial_answer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exact normalized match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Target present with additional text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Target absent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;explanation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;


&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upgrade_gate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund-window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reply with only the refund window: 30 days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30 days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reply with only the support tier: enterprise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enterprise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;solver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;answer_once&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;scorer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;partial_answer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;upgrade_gate&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSPECT_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;log_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To execute this worked example through Python or the CLI and inspect its logs, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;INSPECT_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"openai/gpt-5"&lt;/span&gt;
python regression_eval.py

&lt;span class="c"&gt;# Equivalent task discovery through the CLI&lt;/span&gt;
inspect &lt;span class="nb"&gt;eval &lt;/span&gt;regression_eval.py@upgrade_gate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSPECT_MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-dir&lt;/span&gt; logs

inspect view &lt;span class="nt"&gt;--log-dir&lt;/span&gt; logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful run should produce output in this general shape. The following console block is illustrative, not a measurement from our failed qualification run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ inspect eval regression_eval.py@upgrade_gate \
    --model openai/gpt-5 \
    --log-dir logs

upgrade_gate (2 samples): complete
Model: openai/gpt-5
Log: logs/2026-10-11T101530_upgrade-gate.eval

Scores:
  partial_answer: 0.750

Samples:
  completed: 2
  errors:    0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an agent task, the task definition changes more than the execution command. This is the minimal pattern we would use before adding a pinned challenge dataset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inspect_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inspect_ai.agent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;react&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inspect_ai.scorer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;includes&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;inspect_ai.tool&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;todo_write&lt;/span&gt;


&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sandbox_agent_eval&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;load_pinned_samples&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;solver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use the available tools and submit only the final answer.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;python&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;todo_write&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
            &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;scorer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;message_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important production property is not the decorator syntax. It is that sample inputs, targets, messages, tool calls, outputs, scores, and run configuration can travel together in one reviewable artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation Failures and Verification Limits
&lt;/h2&gt;

&lt;p&gt;Our first narrow scorer contract test did not complete successfully.&lt;/p&gt;

&lt;p&gt;We created five fixed completion fixtures to compare &lt;code&gt;includes()&lt;/code&gt; with &lt;code&gt;match(location="exact")&lt;/code&gt;. One fixture was intentionally labelled as an incomplete-flag negative case. Our test expected the exact matcher to return incorrect, but Inspect returned &lt;code&gt;C&lt;/code&gt;, causing this assertion failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssertionError:
('incomplete_flag_negative', 'exact', 'C', 'I')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We do not treat that as proof that Inspect's scorer is defective. It shows that this fixture's expected result did not match the scorer's returned value. The assertion alone does not establish whether normalization, answer extraction, fixture construction, or another factor caused the mismatch. Because the assertion terminated the run, we did not obtain a complete five-fixture result set.&lt;/p&gt;

&lt;p&gt;That failure changed our implementation policy: every built-in scorer we adopt gets a table-driven contract suite containing punctuation, surrounding prose, denial text, case changes, whitespace, multiple answers, and truncated targets. A scorer name is not a specification.&lt;/p&gt;

&lt;p&gt;Partial credit introduces another interpretation problem. A score set of &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;0.5&lt;/code&gt;, and &lt;code&gt;1&lt;/code&gt; is easy to emit, but the business meaning of its aggregate is ours to define. A plain arithmetic mean treats two partial passes as equivalent to one full pass and one complete failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mean([0.5, 0.5]) == mean([1.0, 0.0]) == 0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those distributions carry different operational risk. We therefore preserve at least four outputs: full-pass rate, partial-pass rate, hard-failure rate, and mean score. We would not approve a model upgrade from the mean alone.&lt;/p&gt;

&lt;p&gt;We have not established byte-for-byte &lt;code&gt;.eval&lt;/code&gt; reproducibility. Our offline scorer check did not run evaluations or compare log files; log reproducibility requires a separate experiment. Consequently, we have no defensible raw SHA-256 comparison to publish.&lt;/p&gt;

&lt;p&gt;Even after a successful run, we would separate two requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Artifact integrity:&lt;/strong&gt; the stored &lt;code&gt;.eval&lt;/code&gt; file remains unchanged after creation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic reproducibility:&lt;/strong&gt; equivalent runs contain the same sample outputs and scores after excluding run-specific metadata.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Raw byte equality is a stricter test. Run IDs, timestamps, ordering, provider metadata, and archive serialization can invalidate it even when the evaluation result is semantically identical. Our CI gate would hash the original artifact for custody, then generate a normalized comparison document for regression analysis.&lt;/p&gt;

&lt;p&gt;Model grading creates another source of nondeterminism. Exact match is brittle but inspectable. A model grader is flexible but adds a second model call, another prompt, another model version, and another failure mode. We did not complete a valid exact-match-versus-model-grader drift experiment, so we will not invent a disagreement rate. Our deployment rule is still clear: we pin the grader independently, store its explanation, and maintain a human-adjudicated calibration set.&lt;/p&gt;

&lt;p&gt;The sandbox boundary also needs precise language. Adding &lt;code&gt;bash()&lt;/code&gt; does not automatically isolate anything. In our task definition, &lt;code&gt;sandbox="docker"&lt;/code&gt; is what assigns shell and Python execution to the container. We would not assume that custom Python tools are isolated; we would verify their execution context and route dangerous operations through a sandbox-aware interface.&lt;/p&gt;

&lt;p&gt;We therefore treat the evaluator host as sensitive infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We run it with minimal credentials.&lt;/li&gt;
&lt;li&gt;We mount only required fixtures.&lt;/li&gt;
&lt;li&gt;We remove Docker socket access from evaluated containers.&lt;/li&gt;
&lt;li&gt;We disable outbound networking unless the task requires it.&lt;/li&gt;
&lt;li&gt;We apply message, token, time, and cost limits.&lt;/li&gt;
&lt;li&gt;We assume model-generated shell commands are hostile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, we did not complete a provider rate-limit benchmark or a two-model token-cost comparison. The available run failed before remote inference. We cannot publish retry counts, wall time, throughput, or cost per sample from that attempt.&lt;/p&gt;

&lt;p&gt;The workaround is procedural, not rhetorical: record provider errors as their own failure category, pin concurrency, preserve token usage per sample, and rerun the exact same sample IDs against both model versions. We would reject any benchmark table that silently drops exhausted retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;Inspect's strongest comparison is not against an observability dashboard. It is against the bespoke evaluation runner that engineering teams eventually build around raw provider SDKs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Execution model&lt;/th&gt;
&lt;th&gt;Item-level scoring&lt;/th&gt;
&lt;th&gt;Agent trajectories&lt;/th&gt;
&lt;th&gt;Isolation&lt;/th&gt;
&lt;th&gt;Regression artifacts&lt;/th&gt;
&lt;th&gt;Main cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inspect AI&lt;/td&gt;
&lt;td&gt;Local Python runner with provider APIs&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Explicit sandbox configuration&lt;/td&gt;
&lt;td&gt;Structured &lt;code&gt;.eval&lt;/code&gt; logs&lt;/td&gt;
&lt;td&gt;Integration and evaluation design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bespoke Python harness&lt;/td&gt;
&lt;td&gt;Whatever we implement&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;td&gt;Usually custom work&lt;/td&gt;
&lt;td&gt;Usually custom work&lt;/td&gt;
&lt;td&gt;Usually JSONL or database rows&lt;/td&gt;
&lt;td&gt;Engineering ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Langfuse or Phoenix-style observability&lt;/td&gt;
&lt;td&gt;Trace collection and analysis&lt;/td&gt;
&lt;td&gt;Possible, but not the core runner&lt;/td&gt;
&lt;td&gt;Strong tracing&lt;/td&gt;
&lt;td&gt;Outside the primary scope&lt;/td&gt;
&lt;td&gt;Trace-oriented&lt;/td&gt;
&lt;td&gt;Backend operation and instrumentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt-oriented CI harness&lt;/td&gt;
&lt;td&gt;Configuration-driven test execution&lt;/td&gt;
&lt;td&gt;Strong for prompt and security checks&lt;/td&gt;
&lt;td&gt;Depends on integration&lt;/td&gt;
&lt;td&gt;Depends on provider and tool setup&lt;/td&gt;
&lt;td&gt;CI reports and artifacts&lt;/td&gt;
&lt;td&gt;Configuration growth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commercial evaluation platform&lt;/td&gt;
&lt;td&gt;Hosted execution and dashboards&lt;/td&gt;
&lt;td&gt;Usually strong&lt;/td&gt;
&lt;td&gt;Product-dependent&lt;/td&gt;
&lt;td&gt;Product-dependent&lt;/td&gt;
&lt;td&gt;Vendor-managed&lt;/td&gt;
&lt;td&gt;Subscription, data governance, lock-in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We did not produce valid latency or throughput numbers, so we do not present fictional requests-per-second figures. For our deployment benchmark, we would measure provider latency, agent trajectory length, tool execution time, and local orchestration overhead separately before identifying the dominant bottleneck.&lt;/p&gt;

&lt;p&gt;Our cost gate uses artifact-derived quantities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sample_cost =
    input_tokens  × input_price_per_token
  + output_tokens × output_price_per_token
  + grader_input_tokens  × grader_input_price_per_token
  + grader_output_tokens × grader_output_price_per_token
  + external_tool_cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We calculate the model-upgrade delta over identical sample IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;full_run_delta =
    sum(new_model_sample_costs)
  - sum(old_model_sample_costs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then combine quality and cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost_per_additional_full_pass =
    full_run_delta
    / (new_full_pass_count - old_full_pass_count)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the denominator is zero, this ratio is undefined; an upgrade can still improve cost efficiency by preserving quality at a lower full-run cost. Equal full-pass counts alone do not establish preserved quality, so we also review item-level regressions and failure categories. If the denominator is negative, we assess the quality loss and cost change separately rather than interpreting the ratio as a cost per additional full pass. If model grading is enabled, grader tokens stay separate from candidate-model tokens so that a grading prompt change cannot masquerade as candidate-model cost growth.&lt;/p&gt;

&lt;p&gt;The build-versus-buy calculation is similarly direct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;annual_internal_harness_cost =
    initial_engineering_hours × loaded_hourly_rate
  + annual_maintenance_hours × loaded_hourly_rate
  + incident_and_audit_overhead

annual_inspect_cost =
    integration_hours × loaded_hourly_rate
  + task_maintenance_hours × loaded_hourly_rate
  + model_and_tool_usage
  + sandbox_infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect is open source, but it is not free to operate. The expensive parts are representative datasets, reliable scorers, model calls, sandbox execution, and review of ambiguous failures. Inspect removes a large amount of harness plumbing; it does not remove evaluation engineering.&lt;/p&gt;

&lt;p&gt;For broader implementation help, we keep our infrastructure patterns in the &lt;a href="https://dev.to/tools"&gt;Effloow tools collection&lt;/a&gt;. For teams turning an ad hoc benchmark into a release gate, our &lt;a href="https://dev.to/services"&gt;AI engineering services&lt;/a&gt; cover dataset versioning, grader calibration, and CI integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;We would deploy Inspect AI as the execution and artifact layer for a model-upgrade gate, with conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need repeatable item-level comparisons rather than aggregate dashboard metrics.&lt;/li&gt;
&lt;li&gt;We evaluate multi-turn agents, tool calls, or sandboxed code execution.&lt;/li&gt;
&lt;li&gt;We want task, solver, and scorer logic stored as reviewable Python.&lt;/li&gt;
&lt;li&gt;We need transcripts and scoring explanations for incident review.&lt;/li&gt;
&lt;li&gt;We are willing to pin framework, task corpus, dataset, model, grader, and pricing versions.&lt;/li&gt;
&lt;li&gt;We can maintain scorer contract tests and a human-adjudicated calibration subset.&lt;/li&gt;
&lt;li&gt;We want to avoid building logging, resumption, trajectory capture, and visualization from scratch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We only need a few deterministic HTTP assertions in CI.&lt;/li&gt;
&lt;li&gt;We require raw byte-identical logs and have not built normalization and integrity checks.&lt;/li&gt;
&lt;li&gt;We expect model-graded scores to be stable without pinning and calibration.&lt;/li&gt;
&lt;li&gt;We cannot isolate model-generated code from evaluator credentials and host resources.&lt;/li&gt;
&lt;li&gt;We need a fully managed dashboard with organizational workflows more than a Python evaluation framework.&lt;/li&gt;
&lt;li&gt;We are unwilling to inspect failed samples and only want a single pass-rate number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our production verdict is &lt;strong&gt;adopt with explicit validation and operational controls&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The component model is clean, the agent support is substantive, the task corpus is useful, and &lt;code&gt;inspect view&lt;/code&gt; makes trajectory debugging materially easier. Inspect can replace weeks of basic harness construction.&lt;/p&gt;

&lt;p&gt;Our own validation was limited, however. Installation succeeded, but the offline scorer check terminated on an assertion mismatch before producing a complete five-fixture result set. Log reproducibility, provider rate-limit behavior, model-grading drift, and per-sample upgrade cost were outside that check's scope and remain untested. We would not promote that check into a production benchmark table.&lt;/p&gt;

&lt;p&gt;That restraint is part of the recommendation. Inspect provides the execution and logging components for an auditable release gate. We still have to prove the scorer, define the sandbox boundary, preserve the artifacts, and make the economics explicit.&lt;/p&gt;

&lt;p&gt;If those controls are already on your roadmap, Inspect is one of the first frameworks we would prototype. If you want help designing the gate around your own failure modes, &lt;a href="https://dev.to/contact"&gt;contact Effloow&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agentevals</category>
      <category>llminfrastructure</category>
      <category>regressiontesting</category>
      <category>modelevaluation</category>
    </item>
    <item>
      <title>vllm-mlx on Apple Silicon: Continuous Batching Works, but Unified Memory Sets the Real Limit</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Sat, 10 Oct 2026 00:36:02 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/vllm-mlx-on-apple-silicon-continuous-batching-works-but-unified-memory-sets-the-real-limit-3ee0</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/vllm-mlx-on-apple-silicon-continuous-batching-works-but-unified-memory-sets-the-real-limit-3ee0</guid>
      <description>&lt;h2&gt;
  
  
  Why We Brought This Tool Into Our Lab
&lt;/h2&gt;

&lt;p&gt;The appeal of Apple Silicon inference is easy to understand. A sufficiently large Mac can keep quantized models, application services, and development tooling on one quiet machine without a remote GPU endpoint. The harder question is whether that machine can serve several interactive users without turning every request into a serial queue.&lt;/p&gt;

&lt;p&gt;M4 Max continuous batching speedups (5 concurrent requests)&lt;span&gt;Llama-3.2-1B-Instruct-4bit (5 req) 2.05x&lt;/span&gt;&lt;span&gt;Llama-3.2-3B-Instruct-4bit (5 req) 1.51x&lt;/span&gt;&lt;span&gt;Qwen3-0.6B-8bit (5 req) 3.39x&lt;/span&gt;&lt;span&gt;Qwen3-30B-A3B-4bit (5 req) 2.38x&lt;/span&gt;&lt;span&gt;Qwen2.5-1.5B-Instruct-4bit (5 req) 1.64x&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Batching five concurrent requests yields 1.51x to 3.39x aggregate-throughput gains on the cited M4 Max models, so the multiplier depends on model, architecture, and memory bandwidth rather than being fixed.&lt;/p&gt;

&lt;p&gt;That is the problem we evaluated with &lt;a href="https://github.com/waybarrios/vllm-mlx" rel="noopener noreferrer"&gt;vllm-mlx&lt;/a&gt;. It exposes OpenAI- and Anthropic-compatible APIs over native MLX execution and adds the scheduling features missing from a basic &lt;code&gt;mlx-lm&lt;/code&gt; generation loop: continuous batching, paged KV storage, prefix caching, request scheduling, and streaming.&lt;/p&gt;

&lt;p&gt;Continuous batching is the important part. Instead of finishing request A before starting request B, the scheduler admits new requests into an active batch and advances multiple sequences together. Model weights can then be reused across rows in the same decode step. Aggregate throughput rises, although each client may receive tokens more slowly because it shares the machine.&lt;/p&gt;

&lt;p&gt;The operational distinction matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple mode processes one request at a time and avoids batching overhead.&lt;/li&gt;
&lt;li&gt;Continuous-batching mode combines active requests and optimizes aggregate throughput.&lt;/li&gt;
&lt;li&gt;Paged caching stores KV state in fixed-size blocks and can share repeated prefixes.&lt;/li&gt;
&lt;li&gt;Prefix caching helps only when prompts actually share reusable content.&lt;/li&gt;
&lt;li&gt;None of these mechanisms creates additional physical memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our evaluation hit an important boundary before model execution: the available isolated host was Linux without the Apple Silicon, Metal, and model weights needed for these tests. MLX scheduling, KV growth, and unified-memory exhaustion cannot be reproduced faithfully on that platform. We therefore did not fabricate a local M4 result or present a mocked scheduler as a real benchmark.&lt;/p&gt;

&lt;p&gt;Instead, we reviewed the installation instructions and CLI options, then assessed throughput and configuration trade-offs using the Apple Silicon benchmark cases for &lt;a href="https://vllm-mlx.is-a.dev/guides/continuous-batching/" rel="noopener noreferrer"&gt;vllm-mlx continuous batching&lt;/a&gt;, &lt;a href="https://github.com/ddalcu/mlx-serve/blob/main/docs/performance.md" rel="noopener noreferrer"&gt;mlx-serve performance&lt;/a&gt;, and &lt;a href="https://github.com/lablup/mlxcel/blob/main/docs/CONTINUOUS_BATCHING.md" rel="noopener noreferrer"&gt;mlxcel scheduling&lt;/a&gt;. We did not execute an installation or CLI contract test in the sandbox. We used the throughput values below as benchmark reference points, not as measurements from this Linux host or a validated production capacity envelope.&lt;/p&gt;

&lt;p&gt;For our vllm-mlx throughput evaluation, we used the benchmark case with an M4 Max, 128 GB of unified memory, and five concurrent requests. Aggregate throughput increased from 328.1 to 1,111.8 tokens per second for Qwen3-0.6B-8bit, from 98.1 to 233.3 tokens per second for Qwen3-30B-A3B-4bit, and from 196.9 to 322.2 tokens per second for Qwen2.5-1.5B-Instruct-4bit. Those are 3.39x, 2.38x, and 1.64x gains respectively.&lt;/p&gt;

&lt;p&gt;That spread is the real story. “Continuous batching” is not a fixed multiplier. Gains depend on architecture, model size, memory bandwidth, batch width, KV traffic, prompt shape, and whether the implementation has a genuinely batched forward path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We pinned the walkthrough to a specific release rather than installing whichever version was latest. For this review, that reference was &lt;code&gt;vllm-mlx==0.4.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On an Apple Silicon host, our deployment recipe would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate

python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"vllm-mlx==0.4.1"&lt;/span&gt; openai

vllm-mlx model inspect &lt;span class="se"&gt;\&lt;/span&gt;
  mlx-community/Llama-3.2-3B-Instruct-4bit

vllm-mlx model acquire &lt;span class="se"&gt;\&lt;/span&gt;
  mlx-community/Llama-3.2-3B-Instruct-4bit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-dir&lt;/span&gt; ./models/llama-3b-4bit

vllm-mlx serve ./models/llama-3b-4bit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--served-model-name&lt;/span&gt; llama-3b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--continuous-batching&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--use-paged-cache&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-memory-mb&lt;/span&gt; 2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-num-seqs&lt;/span&gt; 8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stream-interval&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-metrics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We would not expose this listener directly to an untrusted network. The production command should additionally set &lt;code&gt;--api-key&lt;/code&gt;, place the process behind a health-aware proxy, and apply an external concurrency limit below the server’s theoretical sequence ceiling.&lt;/p&gt;

&lt;p&gt;The built-in benchmarker provides a practical first pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vllm-mlx bench-serve &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://127.0.0.1:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; llama-3b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompts&lt;/span&gt; short,medium,long &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 1,2,4,8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-tokens&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repetitions&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scrape-metrics&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; bench.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following is an illustrative reporting schema we would use for automation, not a verified bench-serve output format or a measured result. We would check the actual benchmark output on Apple Silicon before implementing a parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llama-3b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"concurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"repetitions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"result_kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"simulated-example"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"requests_completed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"requests_failed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aggregate_output_tokens_per_second"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"median_ttft_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p95_ttft_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"peak_metal_memory_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Populate metric values only from the target Apple Silicon host."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For capacity testing, we would sweep three axes independently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Concurrency: 1, 2, 4, 8, and then higher only if memory headroom remains.&lt;/li&gt;
&lt;li&gt;Prompt length: approximately 512, 2,048, 8,192, and 16,384 tokens.&lt;/li&gt;
&lt;li&gt;Generated length: 128 and 512 tokens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mixing all three variables in one first run makes the result difficult to interpret. Long prompts stress prefill and KV allocation; long outputs stress sustained KV growth; higher concurrency multiplies active sequence state.&lt;/p&gt;

&lt;p&gt;We treat the default &lt;code&gt;--max-num-seqs 256&lt;/code&gt; as a scheduler ceiling, not a safe production batch size. A 256-sequence limit says what the scheduler may admit. It says nothing about whether a specific model, context length, and Mac can hold those sequences.&lt;/p&gt;

&lt;p&gt;For streaming, &lt;code&gt;--stream-interval 1&lt;/code&gt; sends every token immediately. Raising it to 2–5 reduces delivery overhead but creates chunkier output. Values of 10 or more favor throughput over perceived smoothness. We would benchmark this separately from model throughput because HTTP chunk frequency can distort client-side latency without changing core decode speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation Limits and Production Risks
&lt;/h2&gt;

&lt;p&gt;The first failure was environmental and absolute: no Apple Silicon meant no honest MLX performance run. MLX’s Metal execution and Apple’s unified-memory behavior cannot be inferred from CUDA, CPU emulation, or a fake cache implementation. We stopped that path rather than manufacturing numbers.&lt;/p&gt;

&lt;p&gt;The second problem is that memory controls are easy to overestimate. &lt;code&gt;--cache-memory-mb 2048&lt;/code&gt; limits cache consumption; it is not a two-gigabyte limit for the entire process. Model weights, temporary prefill tensors, active KV state, tokenizer buffers, Metal allocations, the server, and macOS still compete for unified memory.&lt;/p&gt;

&lt;p&gt;The default memory-aware cache fraction is 20% of available RAM. That is useful for containing prefix-cache growth, but it does not guarantee that the runtime will survive a large active batch. Likewise, &lt;code&gt;--memory-budget-gb&lt;/code&gt; applies to registry-managed model-weight residency. It is explicitly not a total runtime-memory cap and does not guarantee prevention of a Metal or MLX out-of-memory failure.&lt;/p&gt;

&lt;p&gt;That leaves us uncertain about how vllm-mlx handles unified-memory exhaustion. We found no verified basis for promising transparent CPU spill, automatic model eviction, or graceful continuation of active generations. Our production workaround would be preventive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reserve memory for macOS and colocated services.&lt;/li&gt;
&lt;li&gt;Keep the model-weight footprint well below physical capacity.&lt;/li&gt;
&lt;li&gt;Cap request prompt length and &lt;code&gt;max_tokens&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set a conservative application-level concurrency limit.&lt;/li&gt;
&lt;li&gt;Reject or queue excess work before it reaches MLX.&lt;/li&gt;
&lt;li&gt;Restart the worker after a fatal Metal allocation failure.&lt;/li&gt;
&lt;li&gt;Run the API gateway outside the inference process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paged cache is valuable, but its memory savings depend on the workload. It reduces fragmentation and enables block sharing when requests have common prefixes. In our cache evaluation, we treated the 80%+ memory-saving figure as applying to shared-prefix KV storage for 10 or more concurrent users sharing the same system prompt, not to total process memory. We would not apply that estimate to unrelated prompts, unique RAG context, or independent coding sessions.&lt;/p&gt;

&lt;p&gt;Prompt length also changes the result. The vllm-mlx M4 Max batching table does not disclose enough prompt and generation details to turn its speedups into a universal sizing rule. For mlxcel, we evaluated a more tightly specified benchmark case: on an M1 Ultra, &lt;code&gt;meta-llama-3.1-8b-instruct-4bit&lt;/code&gt;, a 512-token prompt, and 128 generated tokens, four clients reached 107.9 aggregate tokens per second versus 56.8 for one batched client. Mean time to first token under four-client load was 189 ms, compared with 3,257 ms in the serial &lt;code&gt;parallel=1&lt;/code&gt; baseline.&lt;/p&gt;

&lt;p&gt;That result shows why scheduling helps under contention, but it does not establish performance at 16K prompts or on an M4 Max.&lt;/p&gt;

&lt;p&gt;Long prompts can also stall active decodes unless prefill is chunked. mlxcel’s scheduler makes this explicit with a default 2,048-token prefill chunk and a grant interval that lets decode work proceed between prompt chunks. vllm-mlx exposes a batching scheduler, but we would still test mixed workloads rather than assume a long ingestion request cannot disrupt short chats.&lt;/p&gt;

&lt;p&gt;Batching support also depends on model architecture. Some implementations advertise batching even when a model family executes one single-sequence forward per row and merely evaluates those graphs together. mlxcel explicitly clamps unsupported SSM, hybrid, and mixed-cache families to one active batching row. Its native batched-forward list includes Llama 3, Llama 4, Qwen 3, Qwen 3.5, Gemma 3, Helium, Muse Glimmer, and Qwen3-MoE in the referenced implementation. We therefore need to verify model support for each family rather than infer it from a server-wide feature flag.&lt;/p&gt;

&lt;p&gt;For more deployment reviews that separate API compatibility from actual serving behavior, &lt;a href="https://dev.to/tools"&gt;browse our infrastructure tools collection&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;We compared the three MLX-serving approaches around the behavior that matters in production rather than headline single-stream decode.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Default concurrency posture&lt;/th&gt;
&lt;th&gt;Verified reference behavior&lt;/th&gt;
&lt;th&gt;Memory-pressure behavior&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vllm-mlx&lt;/td&gt;
&lt;td&gt;Continuous batching is opt-in; &lt;code&gt;max-num-seqs&lt;/code&gt; defaults to 256&lt;/td&gt;
&lt;td&gt;On M4 Max 128 GB, five-request throughput improved 1.51x to 3.39x across the five models listed above&lt;/td&gt;
&lt;td&gt;Cache can be capped, but that is not a total MLX memory limit&lt;/td&gt;
&lt;td&gt;OpenAI and Anthropic compatibility, broad features, shared Mac services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mlx-serve&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--max-concurrent 1&lt;/code&gt; by default&lt;/td&gt;
&lt;td&gt;About 1.6x total throughput at four-way concurrency on dense models, with per-request latency cost&lt;/td&gt;
&lt;td&gt;KV quantization trades speed for 2x or 4x KV-memory savings&lt;/td&gt;
&lt;td&gt;Performance-focused local serving with explicit tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mlxcel&lt;/td&gt;
&lt;td&gt;Four active sequences by default&lt;/td&gt;
&lt;td&gt;On M1 Ultra and the specified 8B workload, four clients delivered 107.9 aggregate tok/s and 189 ms mean TTFT&lt;/td&gt;
&lt;td&gt;Paged-KV admission control is designed to return backpressure under KV-budget pressure; the budget guard is inert on dense decode&lt;/td&gt;
&lt;td&gt;Strong scheduler controls, bounded queues, and explicit admission policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct &lt;code&gt;mlx-lm&lt;/code&gt; loop&lt;/td&gt;
&lt;td&gt;Usually one request at a time&lt;/td&gt;
&lt;td&gt;No batching benefit&lt;/td&gt;
&lt;td&gt;Application must manage all queueing and recovery&lt;/td&gt;
&lt;td&gt;Single-user tools and offline jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote GPU endpoint&lt;/td&gt;
&lt;td&gt;Provider-dependent&lt;/td&gt;
&lt;td&gt;Scales beyond one Mac when capacity is purchased&lt;/td&gt;
&lt;td&gt;Memory management is delegated to the provider&lt;/td&gt;
&lt;td&gt;Bursty traffic, larger models, high availability, geographic distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;mlx-serve exposes a direct memory-for-speed trade through KV quantization. In our tuning evaluation, we treated KV quantization as a memory-for-speed trade: about 10% lower decode throughput at 2K–4K context, and about 18% lower at 42K with fused packed reads automatically enabled from 8K, compared with the earlier 47% penalty. We budgeted approximately 2x KV-memory savings for 8-bit mode and 4x for 4-bit mode. That is attractive on a 16 GB Mac, but it is not free capacity.&lt;/p&gt;

&lt;p&gt;Its sliding-window optimization also demonstrates why context length cannot be reduced to one number. On an M4 Max, Muse-Glimmer 30B 4-bit decode improved from 24.7 to 40.2 tokens per second at 16K context and from 8.6 to 21.0 at 64K after every attention read was trimmed to the model window. Those are release-specific measurements, not expected vllm-mlx results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost break-even&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For an illustrative capacity model, assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mac acquisition cost: $4,000.&lt;/li&gt;
&lt;li&gt;Amortization period: 36 months.&lt;/li&gt;
&lt;li&gt;Average inference-system draw: 150 W.&lt;/li&gt;
&lt;li&gt;Electricity: $0.20 per kWh.&lt;/li&gt;
&lt;li&gt;Continuous availability: 720 hours per month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monthly hardware amortization is about $111. Electricity adds roughly $22, producing a baseline of approximately $133 per month before support, networking, backup capacity, and engineering time.&lt;/p&gt;

&lt;p&gt;At an assumed remote accelerator cost of $1 per running hour, the raw infrastructure break-even is about 133 accelerator-hours per month. At $2 per hour, it is roughly 67 hours. These are planning assumptions, not current provider quotes.&lt;/p&gt;

&lt;p&gt;The calculation favors the Mac only when it stays useful. A lightly used machine assigned to one small model can cost more per completed request than an autoscaled endpoint. A Mac already purchased for development has a different equation because its marginal capital cost may be close to zero.&lt;/p&gt;

&lt;p&gt;Availability changes the result again. One Mac is one failure domain. A production service requiring maintenance without interruption needs at least two hosts, a load balancer, model synchronization, monitoring, and spare memory on both machines. Teams comparing one Mac against a managed endpoint often omit that redundancy cost.&lt;/p&gt;

&lt;p&gt;If you need a workload-specific capacity and failover model, &lt;a href="https://dev.to/contact"&gt;contact our architecture team&lt;/a&gt; rather than sizing from peak tokens per second alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;vllm-mlx is credible for shared Apple Silicon inference. Continuous batching produces meaningful aggregate-throughput gains, and its OpenAI and Anthropic surfaces make migration easier than building a custom MLX scheduler. The M4 Max reference results show that the gain can be substantial, especially for smaller models.&lt;/p&gt;

&lt;p&gt;Our reservation is operational, not conceptual. Unified memory is the hard boundary, and cache limits do not convert vllm-mlx into a memory-safe multi-tenant platform. Production safety still depends on request admission, context limits, monitoring, and worker recovery outside the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already own Apple Silicon capacity with enough memory for weights, KV state, and operating-system headroom.&lt;/li&gt;
&lt;li&gt;Several users share the same model and generate concurrently.&lt;/li&gt;
&lt;li&gt;Your workloads reuse system prompts or conversation prefixes.&lt;/li&gt;
&lt;li&gt;You need OpenAI or Anthropic API compatibility without a cloud dependency.&lt;/li&gt;
&lt;li&gt;You can cap prompt length, output length, queue depth, and effective concurrency.&lt;/li&gt;
&lt;li&gt;A single-region, local-network service is acceptable.&lt;/li&gt;
&lt;li&gt;You can benchmark the exact model family rather than assuming all architectures batch equally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need guaranteed graceful recovery after unified-memory exhaustion.&lt;/li&gt;
&lt;li&gt;Traffic is highly bursty and the Mac would sit idle most of the month.&lt;/li&gt;
&lt;li&gt;You require multi-host scheduling, automatic failover, or elastic scaling out of the box.&lt;/li&gt;
&lt;li&gt;Your workload depends on very long, unrelated prompts with little prefix reuse.&lt;/li&gt;
&lt;li&gt;You are trying to run a model whose weights already consume nearly all physical memory.&lt;/li&gt;
&lt;li&gt;You need a published service-level objective without first measuring mixed prompt lengths and generation sizes.&lt;/li&gt;
&lt;li&gt;You intend to treat &lt;code&gt;--max-num-seqs 256&lt;/code&gt; as a safe batch size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our production starting point would be four active requests, a two-gigabyte cache cap, paged caching, strict API-side context limits, and at least 20% physical-memory headroom after model load. We would then raise concurrency only after recording aggregate throughput, per-request decode rate, median and p95 time to first token, queue wait, and peak Metal memory.&lt;/p&gt;

&lt;p&gt;The bottom line is straightforward: vllm-mlx makes a Mac a legitimate multi-user inference server, not an elastic GPU cluster. Within that boundary, continuous batching is worth enabling. Outside it, especially when memory exhaustion or host failure must be invisible to clients, we would keep the Mac as an edge or development tier and route overflow to a managed backend.&lt;/p&gt;

</description>
      <category>applesilicon</category>
      <category>mlx</category>
      <category>llmserving</category>
      <category>continuousbatching</category>
    </item>
    <item>
      <title>Voyage AI vs Cohere Embed v4 vs Nemotron 3 Embed: Production RAG Trade-offs</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Thu, 08 Oct 2026 00:39:22 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/voyage-ai-vs-cohere-embed-v4-vs-nemotron-3-embed-production-rag-trade-offs-2bpp</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/voyage-ai-vs-cohere-embed-v4-vs-nemotron-3-embed-production-rag-trade-offs-2bpp</guid>
      <description>&lt;h2&gt;
  
  
  Why We Evaluated These Embedding Models
&lt;/h2&gt;

&lt;p&gt;Embedding selection looks reversible until a production index contains tens of millions of vectors. At that point, changing the model can mean re-reading every source document, reproducing the original chunking pipeline, paying for another embedding pass, building a second index, and switching traffic without mixing incompatible vector spaces.&lt;/p&gt;

&lt;p&gt;That operational commitment—not a leaderboard position—was why we evaluated Voyage 4, Cohere Embed v4, and NVIDIA Nemotron 3 Embed.&lt;/p&gt;

&lt;p&gt;The three options represent materially different deployment choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Voyage 4 is a managed, text-focused family with selectable dimensions and quantized output formats.&lt;/li&gt;
&lt;li&gt;Cohere Embed v4 is a managed multimodal model for text, images, and mixed documents.&lt;/li&gt;
&lt;li&gt;Nemotron 3 Embed is positioned as an open-weight, self-hosted family where licensing cost is replaced by GPU and operations cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, each model converts documents and queries into vectors that an index compares by cosine similarity, dot product, or an equivalent distance function. The implementation details differ, but the production contract is the same: document and query preprocessing must remain stable, dimensions must match, and we need continued access to a query encoder compatible with the stored vectors, rather than necessarily the exact model version originally used.&lt;/p&gt;

&lt;p&gt;We found an immediate problem with the public comparison narrative. The available benchmark figures were not generated on one common evaluation suite.&lt;/p&gt;

&lt;p&gt;The Nemotron 3 figures we examined included 78.46 on RTEB and 75.45 on MMTEB Retrieval for the 8B checkpoint. The 1B checkpoint was listed at 72.38 RTEB and 71.04 MMTEB Retrieval. Figures circulating for Voyage and Cohere were general MTEB aggregates, not directly comparable RTEB runs. We therefore refused to place those values into a single “winner” ranking.&lt;/p&gt;

&lt;p&gt;A 78.46 retrieval score and a roughly 67 general MTEB aggregate do not measure the same thing. MTEB can blend retrieval, clustering, classification, and semantic-similarity tasks. RTEB is retrieval-specific. Different task sets, relevance judgments, languages, pooling policies, and gain conventions can reverse an apparent lead.&lt;/p&gt;

&lt;p&gt;This matters because a model can perform well on public web retrieval and still miss exact clauses in contracts, versioned API behavior, product identifiers, or cross-language support content. Our deployment recommendation consequently depends on a customer’s labeled corpus rather than the headline score.&lt;/p&gt;

&lt;p&gt;We treat the model choice as an index architecture decision. Teams evaluating the surrounding stack can compare it with the vector and retrieval tools in our &lt;a href="https://dev.to/tools"&gt;AI tools collection&lt;/a&gt;, but they should freeze the embedding contract before optimizing the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We started by validating the metric harness rather than sending three sets of vectors into an untrusted scorer. That step caught a class of benchmark errors more damaging than a slow API: document-ID misalignment can turn perfect retrieval into a score of zero while every individual array still has the expected shape.&lt;/p&gt;

&lt;p&gt;We ran the tests in Python 3.12 with these pinned dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate

python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;numpy&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2.1.3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;scipy&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.14.1 &lt;span class="se"&gt;\&lt;/span&gt;
  scikit-learn&lt;span class="o"&gt;==&lt;/span&gt;1.5.2

python benchmark_metric_contract.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following compact fixture reproduces the key cutoff and all-zero-relevance checks; the document-ID alignment results are reported separately below. It is runnable without an API key or GPU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ndcg_score&lt;/span&gt;

&lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;document_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ranking_scores&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;y_true&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ranking_scores&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ndcg_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Higher retrieval scores produce earlier ranks.
&lt;/span&gt;&lt;span class="n"&gt;descending_scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;relevance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;document_count&lt;/span&gt;
    &lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descending_scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_11&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all_zero_relevance_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;document_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;descending_scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_11&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all_zero_relevance_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our executed fixture produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"single_relevant_at_rank_1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"single_relevant_at_rank_2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.6309297535714574&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"single_relevant_at_rank_10"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2890648263178878&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"single_relevant_at_rank_11"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"all_zero_relevance_query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also tested graded relevance. Swapping two graded results produced an nDCG@10 of &lt;code&gt;0.9224945116765988&lt;/code&gt; with linear gains and &lt;code&gt;0.842828264880938&lt;/code&gt; when we supplied exponential gains. That absolute difference of &lt;code&gt;0.07966624679566081&lt;/code&gt; is large enough to manufacture a benchmark “win” if two runs use different gain conventions.&lt;/p&gt;

&lt;p&gt;The alignment fixture exposed an even larger scoring error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aligned_score=1.0
joint_column_permutation_score=1.0
judgments_only_permutation_score=0.0
all_assertions_passed=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permuting document IDs in both predictions and judgments preserved the score. Permuting only the relevance judgments reduced it from 1.0 to 0.0. We now require one canonical document-ID map across chunk storage, relevance judgments, cached vectors, and result files.&lt;/p&gt;

&lt;p&gt;This was a measurement-contract test, not a model-quality run. We did not produce vendor embeddings in this executed sandbox, so we do not present invented nDCG, recall, latency, throughput, or multilingual results for Voyage, Cohere, or Nemotron.&lt;/p&gt;

&lt;p&gt;For a real head-to-head, our required run manifest is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"corpus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chunks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2000-5000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chunker_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frozen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content_hashes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"queries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"relevance_judgments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"graded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zero_relevance_policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"declared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"languages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stratified"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retrieval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nDCG@10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recall@5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recall@10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recall@50"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"similarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cosine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"normalization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"explicit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reranker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disabled"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pinned"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dimension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recorded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recorded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"truncation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disabled-or-counted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"batch_and_realtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"separate"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Voyage’s &lt;a href="https://docs.voyageai.com/docs/embeddings" rel="noopener noreferrer"&gt;embedding reference&lt;/a&gt; and &lt;a href="https://github.com/voyage-ai/voyageai-python" rel="noopener noreferrer"&gt;official Python client&lt;/a&gt; expose the necessary model, input type, output dimension, truncation, and output data type controls. Teams should pin Cohere’s equivalent configuration using its &lt;a href="https://docs.cohere.com/docs/embed" rel="noopener noreferrer"&gt;Embed reference&lt;/a&gt;, rather than infer it from a framework wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark Limitations and Production Risks
&lt;/h2&gt;

&lt;p&gt;The first failure was not an API error. It was evidence comparability. We could not reproduce a valid three-model leaderboard from the supplied material because no common fixed-corpus run existed. Any article naming an unconditional winner from these inputs would be overstating the evidence.&lt;/p&gt;

&lt;p&gt;We hit six additional production hazards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Dimensions are a schema, not a tuning flag.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage 4 supports 256, 512, 1,024, and 2,048 dimensions, with 1,024 as the default. Cohere Embed v4 configurations include 256, 512, 1,024, and 1,536 dimensions. The Nemotron 3 material we reviewed described 2,048 dimensions for the 1B model and 4,096 for 8B.&lt;/p&gt;

&lt;p&gt;A collection created at 1,024 dimensions cannot accept a 1,536- or 4,096-element vector. We plan a new collection, validation, and traffic cutover for a dimension change. Switching to an incompatible embedding space requires re-embedding; reducing compatible Matryoshka vectors may allow us to reuse existing embeddings after validating the transformation and retrieval quality. We store model, revision, dimension, normalization, data type, and preprocessing hash beside every index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Voyage silently truncates by default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage’s &lt;code&gt;truncation&lt;/code&gt; option defaults to &lt;code&gt;True&lt;/code&gt;. An oversized input can therefore return a valid vector for incomplete content. That is worse than an explicit failure when the missing text contains the relevant clause.&lt;/p&gt;

&lt;p&gt;We set &lt;code&gt;truncation=False&lt;/code&gt; in ingestion jobs and route over-length chunks to a dead-letter queue. We would rather fail visibly than index partial documents silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Query and document modes are operationally significant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage prepends different retrieval prompts when &lt;code&gt;input_type&lt;/code&gt; is &lt;code&gt;query&lt;/code&gt; or &lt;code&gt;document&lt;/code&gt;. Cohere also distinguishes retrieval input types. We treat this field as part of the model version. A missing query mode is not harmless configuration drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Request size is not the same as rate limit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage accepts at most 1,000 texts per request, with additional token ceilings. The documented total is up to one million tokens for &lt;code&gt;voyage-4-lite&lt;/code&gt;, 320,000 for &lt;code&gt;voyage-4&lt;/code&gt;, and 120,000 for &lt;code&gt;voyage-4-large&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those are payload limits, not throughput guarantees. We could not verify per-key requests-per-minute, token-per-minute limits, cooldown behavior, or reserved-capacity terms across all three providers. Teams should load-test their own account tier rather than extrapolate from maximum batch size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Quantization support is uneven.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage’s hosted 4-series can return float, int8, uint8, binary, and unsigned-binary output. The local &lt;code&gt;voyage-4-nano&lt;/code&gt; path does not support int8 or uint8 because its required calibration ranges are not shipped; those modes raise &lt;code&gt;NotImplementedError&lt;/code&gt;. Float and binary formats remain available locally.&lt;/p&gt;

&lt;p&gt;The local extra also requires Python 3.10 or newer and installs PyTorch plus Sentence Transformers. We kept API-only and local-inference environments separate to avoid turning a lightweight client image into a large ML runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Nemotron naming can produce the wrong comparison.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We found material mixing Nemotron 3 Embed with the older &lt;code&gt;llama-nemotron-embed-1b-v2&lt;/code&gt;. The older model has an 8,192-token input limit and Matryoshka dimensions down from 2,048. Those specifications must not be copied onto the newer Nemotron 3 family described with a 32,768-token context.&lt;/p&gt;

&lt;p&gt;We pin exact model identifiers and artifact revisions. “Nemotron Embed” is not precise enough for an index manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;We normalized the product configurations available during the review, but we did not measure service latency or GPU throughput. The latency column is therefore deliberately absent rather than filled with synthetic numbers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;Context limit&lt;/th&gt;
&lt;th&gt;Dimensions reviewed&lt;/th&gt;
&lt;th&gt;Multimodal&lt;/th&gt;
&lt;th&gt;Public price used for planning&lt;/th&gt;
&lt;th&gt;Main operational trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Voyage 4 Lite&lt;/td&gt;
&lt;td&gt;Managed API&lt;/td&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;256-2,048&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;$0.02 per 1M tokens&lt;/td&gt;
&lt;td&gt;Lowest listed API rate; quality must be tested by domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voyage 4&lt;/td&gt;
&lt;td&gt;Managed API&lt;/td&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;256-2,048&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;$0.06 per 1M tokens&lt;/td&gt;
&lt;td&gt;Balanced managed text retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voyage 4 Large&lt;/td&gt;
&lt;td&gt;Managed API&lt;/td&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;256-2,048&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;$0.12 per 1M tokens&lt;/td&gt;
&lt;td&gt;Higher-cost quality tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cohere Embed v4&lt;/td&gt;
&lt;td&gt;Managed API&lt;/td&gt;
&lt;td&gt;128,000&lt;/td&gt;
&lt;td&gt;256-1,536&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$0.12 per 1M text tokens&lt;/td&gt;
&lt;td&gt;Best fit here for mixed text and image retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nemotron 3 Embed 1B&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;2,048&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No per-token license charge&lt;/td&gt;
&lt;td&gt;GPU serving, observability, and capacity become our responsibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nemotron 3 Embed 8B&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;4,096&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No per-token license charge&lt;/td&gt;
&lt;td&gt;Largest raw vector payload at the reviewed dimensions; serving capacity must be measured&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At one billion text tokens, the simple API bill is approximately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Voyage 4 Lite: $20&lt;/li&gt;
&lt;li&gt;Voyage 4: $60&lt;/li&gt;
&lt;li&gt;Voyage 4 Large: $120&lt;/li&gt;
&lt;li&gt;Cohere Embed v4: $120&lt;/li&gt;
&lt;li&gt;Nemotron 3: infrastructure-dependent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers expose an important reality: initial embedding API charges can be smaller than vector storage, indexing, replicas, backups, and migration engineering. At float32, one million raw vectors require approximately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;256 dimensions: 0.95 GiB&lt;/li&gt;
&lt;li&gt;1,024 dimensions: 3.81 GiB&lt;/li&gt;
&lt;li&gt;1,536 dimensions: 5.72 GiB&lt;/li&gt;
&lt;li&gt;2,048 dimensions: 7.63 GiB&lt;/li&gt;
&lt;li&gt;4,096 dimensions: 15.26 GiB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That excludes IDs, graph edges, metadata, allocator overhead, replicas, and snapshots. Moving from 1,024 to 4,096 dimensions quadruples the raw vector payload before the vector database adds its own structures.&lt;/p&gt;

&lt;p&gt;For Nemotron, we use a break-even equation rather than pretending open weights are free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;self_hosted_cost_per_million = gpu_hourly_cost / millions_of_tokens_per_hour
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a hypothetical $2 per GPU-hour, self-hosting must sustain more than 16.67 million tokens per hour to beat a $0.12-per-million API on raw compute. It must exceed 33.33 million tokens per hour to beat a $0.06-per-million API. Engineering labor, idle capacity, failover, deployment, and monitoring move the real threshold higher.&lt;/p&gt;

&lt;p&gt;We did not measure Nemotron throughput, memory use, or NVFP4 quality loss, so those break-even examples are formulas, not performance claims.&lt;/p&gt;

&lt;p&gt;For teams planning a migration or capacity model, our &lt;a href="https://dev.to/services"&gt;AI infrastructure services&lt;/a&gt; focus on the complete cost boundary: embedding generation, index memory, replicas, refresh cadence, and rollback—not just the token invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;We cannot honestly name a universal retrieval-quality winner from the available evidence. Nemotron 3 has the strongest retrieval-specific public numbers in this comparison, but those numbers were not generated in the same run as Voyage 4 and Cohere Embed v4. Our executed work validated the metric harness, not the three models.&lt;/p&gt;

&lt;p&gt;That distinction defines our verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy Voyage 4 if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need managed text embeddings with low list pricing.&lt;/li&gt;
&lt;li&gt;We want selectable dimensions and native quantized output.&lt;/li&gt;
&lt;li&gt;A 32,000-token context covers the chunking strategy.&lt;/li&gt;
&lt;li&gt;We can pin &lt;code&gt;input_type&lt;/code&gt;, reject truncation, and keep a model manifest.&lt;/li&gt;
&lt;li&gt;We have domain labels proving that Lite, standard, or Large meets recall targets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deploy Cohere Embed v4 if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need text, images, and mixed documents in one retrieval space.&lt;/li&gt;
&lt;li&gt;A 128,000-token input window materially simplifies document handling.&lt;/li&gt;
&lt;li&gt;We value a managed endpoint more than control over model weights.&lt;/li&gt;
&lt;li&gt;We can absorb the API price and validate multilingual performance on our own language mix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deploy Nemotron 3 Embed if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We already operate NVIDIA inference infrastructure.&lt;/li&gt;
&lt;li&gt;Data residency or service independence makes self-hosting valuable.&lt;/li&gt;
&lt;li&gt;Query volume is high and steady enough to amortize GPUs.&lt;/li&gt;
&lt;li&gt;We can benchmark the exact checkpoint, precision, and dimension used in production.&lt;/li&gt;
&lt;li&gt;We accept responsibility for capacity, upgrades, observability, and rollback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid all three as a final choice if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We do not have labeled queries from the target domain.&lt;/li&gt;
&lt;li&gt;We are comparing RTEB and general MTEB scores as if they were interchangeable.&lt;/li&gt;
&lt;li&gt;We have not calculated index memory at the chosen dimension and replica count.&lt;/li&gt;
&lt;li&gt;The ingestion pipeline allows silent truncation.&lt;/li&gt;
&lt;li&gt;Model names, preprocessing prefixes, or revisions are not pinned.&lt;/li&gt;
&lt;li&gt;We are inferring multilingual quality from an aggregate score.&lt;/li&gt;
&lt;li&gt;We have not tested rate-limit and batch assumptions against the production account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our practical default is Voyage 4 at 1,024 dimensions for managed, text-only evaluation; Cohere Embed v4 when multimodal retrieval is a real requirement; and Nemotron 3 only when self-hosting is an architectural objective rather than a reaction to per-token pricing.&lt;/p&gt;

&lt;p&gt;We would not approve production deployment until all candidates had run over the same 2,000-to-5,000-chunk corpus, the same graded queries, the same distance metric, and the same nDCG and recall implementation. The winning model is the one that clears the application’s recall threshold at the lowest total operating cost—not the one with the largest vector or the most flattering public benchmark.&lt;/p&gt;

&lt;p&gt;If the index migration risk is already material, &lt;a href="https://dev.to/contact"&gt;contact our engineering team&lt;/a&gt; before changing dimensions or model families. Rebuilding a benchmark is cheap. Rebuilding a live retrieval index under customer traffic is not.&lt;/p&gt;

</description>
      <category>embeddings</category>
      <category>rag</category>
      <category>vectorsearch</category>
      <category>aiinfrastructure</category>
    </item>
    <item>
      <title>Cline in Production: BYO-Key Costs, MCP Limits, and Terminal-Bench Results</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Wed, 07 Oct 2026 00:38:57 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/cline-in-production-byo-key-costs-mcp-limits-and-terminal-bench-results-3cod</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/cline-in-production-byo-key-costs-mcp-limits-and-terminal-bench-results-3cod</guid>
      <description>&lt;h2&gt;
  
  
  Why we evaluated Cline
&lt;/h2&gt;

&lt;p&gt;We did not test Cline because we needed another autocomplete tool. We tested it because flat per-seat AI IDE pricing makes cost attribution difficult, while closed agent runtimes make model and tool migrations expensive.&lt;/p&gt;

&lt;p&gt;Cline + Kimi K3 on Terminal-Bench 2.1: score up, spend down&lt;span&gt;Baseline pass rate (69/89) 77.5%/100&lt;/span&gt;&lt;span&gt;Confirmation pass rate (79/89) 88.8%/100&lt;/span&gt;&lt;span&gt;Baseline run cost $79&lt;/span&gt;&lt;span&gt;Confirmation run cost $49.80&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The higher-scoring run on the same 89-task suite was also the cheaper one, because fewer sessions died in retries, loops, and self-termination.&lt;/p&gt;

&lt;p&gt;Cline separates the agent runtime from the inference provider. The VS Code extension supplies the agent loop, file operations, terminal integration, approvals, context management, and MCP client. We supply the model account and pay the inference provider directly. That separation gives us three things we cannot assume from a bundled IDE subscription:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We can select or replace the model independently of the editor.&lt;/li&gt;
&lt;li&gt;We can inspect token usage and assign inference cost to a task.&lt;/li&gt;
&lt;li&gt;We can expose internal services through MCP without first waiting for an IDE vendor to add a native integration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That does not make Cline free. It exchanges predictable seat pricing for variable inference spend, provider rate limits, API-key management, and substantially more operational responsibility.&lt;/p&gt;

&lt;p&gt;We evaluated four practical questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can a developer install Cline without adopting another IDE?&lt;/li&gt;
&lt;li&gt;Can we switch between Anthropic and OpenAI-style providers without rewriting the workflow?&lt;/li&gt;
&lt;li&gt;Does MCP provide a usable portability boundary?&lt;/li&gt;
&lt;li&gt;Do the published Terminal-Bench numbers survive scrutiny as production evidence?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The short answer is mixed. BYO-key model choice and usage-based accounting provide useful control, but we did not verify the extension's installation or provider-configuration workflow. Our separate Python MCP SDK test failed during initialization; it does not establish a Cline client defect. The benchmark is much more useful as a harness-debugging case study than as a definitive “Cline beats Cursor” ranking.&lt;/p&gt;

&lt;p&gt;Cline’s SDK production architecture also matters. We can instrument model-call, tool-use, session-end, and usage events, including token counts and finish reasons. The SDK supports iteration limits, per-turn token limits, loop detection, mistake limits, and host-side cancellation. Those are the controls we expect from an agent runtime. They are not proof that a developer workstation is safely sandboxed.&lt;/p&gt;

&lt;p&gt;Our evaluation therefore treated Cline as an agent harness with an editor front end, not as a security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Cline, configuring MCP, and running Terminal-Bench
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installing and verifying the extension
&lt;/h3&gt;

&lt;p&gt;For a team rollout, we would verify the current Marketplace extension identifier and test installation in a clean VS Code profile. The commands below are illustrative; we have not verified their identifier or execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; saoudrizwan.claude-dev
code &lt;span class="nt"&gt;--list-extensions&lt;/span&gt; &lt;span class="nt"&gt;--show-versions&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; saoudrizwan.claude-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We would confirm the publisher, display name, and exact extension identifier against the current Marketplace listing before using these commands.&lt;/p&gt;

&lt;p&gt;For a team rollout, we would pin and test a known version before broad deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; saoudrizwan.claude-dev@&amp;lt;approved-version&amp;gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cline supports BYO-key providers, including Anthropic and OpenAI. We would verify the extension's current provider interface and credential-storage behavior before rollout, and keep keys out of workspace settings and committed files.&lt;/p&gt;

&lt;p&gt;For Anthropic or OpenAI, we would confirm the current provider-specific setup instructions, configure a dedicated development key, and select the intended model. We have not verified the extension's exact settings labels, compatible-endpoint fields, or approval controls. Our rollout plan would begin with a read-only task before enabling terminal commands or file writes.&lt;/p&gt;

&lt;p&gt;We would use separate development and production provider keys and configure provider-side budget alerts. A BYO-key extension installed on every laptop creates a larger credential-management surface than a centrally administered seat license.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wiring a local MCP stub
&lt;/h3&gt;

&lt;p&gt;The following is an illustrative Python MCP server using the SDK version pinned for our local test attempt. This example was not successfully validated, and it is not a verified Cline integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# server.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effloow-local-stub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_ticket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create a local test ticket without external side effects.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority must be between 1 and 5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stdio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We installed its dependencies in an isolated environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;.&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"mcp==1.9.4"&lt;/span&gt; &lt;span class="s2"&gt;"pydantic==2.11.5"&lt;/span&gt; &lt;span class="s2"&gt;"anyio==4.9.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following is an unverified registration example, not a configuration we tested in Cline. We would confirm the current client schema, configuration location, and approval fields before using it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"effloow-local-stub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/project/.venv/bin/python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/project/server.py"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"disabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"autoApprove"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a future integration test, we would use absolute interpreter and server paths and check the environment supplied by the client. We would also verify the meaning of approval fields rather than assume that an empty &lt;code&gt;autoApprove&lt;/code&gt; list enforces the intended policy. State-changing tools should require explicit approval.&lt;/p&gt;

&lt;p&gt;Our isolated Python MCP SDK handshake did not complete successfully. The test failed during initialization with &lt;code&gt;McpError: Connection closed&lt;/code&gt; and exit code 1, producing no tool observations. Dependency setup completed, but we did not verify Cline's registration structure, client behavior, argument validation, or recovery. This was a failed SDK integration attempt, not evidence of a Cline deployment defect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the 89-task evaluation
&lt;/h3&gt;

&lt;p&gt;For the agent benchmark, we used the open Terminal-Bench dataset through Harbor. The relevant projects are the &lt;a href="https://github.com/laude-institute/terminal-bench" rel="noopener noreferrer"&gt;Terminal-Bench repository&lt;/a&gt; and the &lt;a href="https://github.com/laude-institute/harbor" rel="noopener noreferrer"&gt;Harbor evaluation harness&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The baseline prerequisites were Python, &lt;code&gt;uv&lt;/code&gt;, Docker, one provider key, and optionally Modal for parallel execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;harbor
pip &lt;span class="nb"&gt;install &lt;/span&gt;modal
modal setup

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CPUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;14
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MEMORY_MB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8192
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"replace-with-real-key"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OPENROUTER_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

harbor run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; terminal-bench@2.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-a&lt;/span&gt; cline-cli &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-m&lt;/span&gt; openrouter:anthropic/claude-opus-4.6 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt; modal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ak&lt;/span&gt; &lt;span class="nv"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ak&lt;/span&gt; &lt;span class="nb"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2400 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; 89 &lt;span class="nt"&gt;-l&lt;/span&gt; 89 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--override-cpus&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CPUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--override-memory-mb&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MEMORY_MB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 2,400-second timeout matters. The earlier 600-second default caused agent timeouts that looked like capability failures but were actually harness configuration failures.&lt;/p&gt;

&lt;p&gt;The reproduced full-sweep output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;89/89 Mean: 0.573
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0:45:05  0:00:00

Passed: 51
Failed: 38
Mean reward: 0.573

Failure classes:
  AgentTimeoutError
  VerifierTimeoutError
  missing_expected_files
  command_exit_status_not_observed
  provider_rate_limit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a Terminal-Bench 2.0 run using the Cline CLI adapter and an OpenRouter-hosted Anthropic model. It must not be compared directly with later Terminal-Bench 2.1 Kimi K3 results.&lt;/p&gt;

&lt;p&gt;For cost accounting, we examined the SDK's host-side estimator: it prefers provider-reported &lt;code&gt;usage.totalCost&lt;/code&gt; and otherwise combines token counts with a maintained pricing table. We did not obtain actual per-task cost output. The following record is synthetic and illustrates a possible logging format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tb2-task-041"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider:model-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18442&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2106&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider_reported_total_cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1187&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iterations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That JSON is an output-format example, not an independently verified cost for a named Terminal-Bench task. The externally auditable numbers were aggregate: the later Kimi K3 baseline cost $79 for 89 attempts, or $0.888 per attempted task, while the confirmation run cost $49.80, or $0.560 per attempted task. Those averages include failed tasks and cannot reveal the task-level distribution.&lt;/p&gt;

&lt;p&gt;For production accounting, we would reject aggregate-only reporting. We would persist provider, model, input tokens, output tokens, cached tokens, provider-reported cost, tool calls, duration, and finish reason for every task.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke: The Gotchas and Limitations We Hit
&lt;/h2&gt;

&lt;p&gt;The first local failure was initialization in a separate Python MCP SDK test, not in Cline's MCP client. Our pinned Python client received &lt;code&gt;Connection closed&lt;/code&gt; during &lt;code&gt;session.initialize()&lt;/code&gt;. There were no tool observations, so we could not establish invocation, schema validation, or recovery.&lt;/p&gt;

&lt;p&gt;Our immediate debugging checklist was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use an absolute interpreter and server path.&lt;/li&gt;
&lt;li&gt;Launch the server manually with the same interpreter.&lt;/li&gt;
&lt;li&gt;Keep stdout exclusively for MCP protocol traffic.&lt;/li&gt;
&lt;li&gt;Send diagnostics to stderr.&lt;/li&gt;
&lt;li&gt;Confirm the client and server SDK versions agree on transport behavior.&lt;/li&gt;
&lt;li&gt;Restart the VS Code window after changing MCP registration.&lt;/li&gt;
&lt;li&gt;Avoid assuming that the extension inherits shell variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For us, this failed SDK initialization highlighted checks we would perform before relying on an MCP integration. It did not measure Cline's portability or compare clients. We would validate process supervision, approval semantics, credentials, timeouts, logs, and environment propagation for each server/client pairing.&lt;/p&gt;

&lt;p&gt;The second issue was misleading benchmark failure attribution. We found several classes that were not model-intelligence problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent timed out before completing valid work.&lt;/li&gt;
&lt;li&gt;Command exit codes were not surfaced correctly.&lt;/li&gt;
&lt;li&gt;Long-running build or test commands were terminated early.&lt;/li&gt;
&lt;li&gt;The agent declared completion without verifying expected files.&lt;/li&gt;
&lt;li&gt;A provider returned a rate limit and the session stopped instead of retrying.&lt;/li&gt;
&lt;li&gt;Repeated polling of a progressing background process was mistaken for a loop.&lt;/li&gt;
&lt;li&gt;Broad &lt;code&gt;pkill -f&lt;/code&gt; commands matched the harness process and killed the agent itself.&lt;/li&gt;
&lt;li&gt;Prompt text containing &lt;code&gt;@a&lt;/code&gt;-style tokens triggered file-mention processing while an unreferenced asynchronous worker allowed the process to exit before a model call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These failures matter because they demonstrate how much benchmark performance belongs to the harness rather than the model. Retries with exponential backoff, output-aware loop detection, explicit completion verification, PID-based process cleanup, and a longer timeout produced real improvements.&lt;/p&gt;

&lt;p&gt;The score still had material run-to-run noise. Six comparable runs produced 0.49, 0.43, 0.45, 0.44, 0.48, and 0.46, with a 0.458 average and 0.455 median. We would not merge a harness change based on a one- or two-point improvement from a single run. We would repeat close comparisons three to six times.&lt;/p&gt;

&lt;p&gt;We also encountered an evidence problem around cost. The runtime exposes enough usage data to implement task-level accounting, but the headline benchmark material mostly supplied whole-run spend. Dividing $49.80 by 89 attempts is useful for budgeting; it is not a substitute for a cost histogram or pass-adjusted cost.&lt;/p&gt;

&lt;p&gt;Finally, before replacing Cursor or Windsurf with a BYO-key harness, we would review operational requirements beyond raw model quality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers must select models and understand provider pricing.&lt;/li&gt;
&lt;li&gt;Provider outages and rate limits become our problem.&lt;/li&gt;
&lt;li&gt;Every API key needs scope, rotation, revocation, and offboarding.&lt;/li&gt;
&lt;li&gt;We must standardize approval defaults across machines.&lt;/li&gt;
&lt;li&gt;We would verify workspace-rule compatibility rather than assume rules transfer unchanged between agents.&lt;/li&gt;
&lt;li&gt;MCP support does not guarantee identical tool behavior.&lt;/li&gt;
&lt;li&gt;Cline can spend far more than expected when an agent loops, retries, or reads a large repository repeatedly.&lt;/li&gt;
&lt;li&gt;Extension updates need staged internal validation even though Marketplace publication itself is effectively all-or-nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We would manage Cline through an approved configuration baseline, a provider gateway, and centrally collected usage events. A collection of individually configured laptops is not a production deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;Our local Docker path was suitable for smoke tests, not rapid 89-task iteration. Sequential execution can take many hours. Modal parallelization brought the full run into the reported 40–50 minute range, with our reproduced output finishing in 45:05. The three-task setup check still took roughly 15 minutes because environment and sandbox startup were significant.&lt;/p&gt;

&lt;p&gt;We did not measure workstation memory consumption for the VS Code extension, so we will not invent a number. The benchmark configuration explicitly reserved 14 CPUs and 8,192 MB per task environment for the parallel workflow; that is an evaluation allocation, not the extension’s desktop footprint.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cline option&lt;/th&gt;
&lt;th&gt;Inference arrangement&lt;/th&gt;
&lt;th&gt;Accounting considerations&lt;/th&gt;
&lt;th&gt;Integration considerations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BYO key&lt;/td&gt;
&lt;td&gt;Provider API usage; model-agnostic provider choice&lt;/td&gt;
&lt;td&gt;Host-side SDK accounting can use provider-reported cost or token counts and a pricing table&lt;/td&gt;
&lt;td&gt;Verify each MCP server/client pairing and approval policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClinePass&lt;/td&gt;
&lt;td&gt;$9.99/month for curated open-weight models, with no separate provider keys&lt;/td&gt;
&lt;td&gt;We did not verify task-level billing detail or compare its accounting with bundled IDE plans&lt;/td&gt;
&lt;td&gt;We did not test MCP behavior under this plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We did not measure competitor accounting granularity, MCP behavior, or administrative burden, so we would compare those separately before making a procurement decision.&lt;/p&gt;

&lt;p&gt;The practical break-even calculation is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monthly Cline cost per developer
  = tasks per month × mean API cost per task
  + shared gateway, observability, and support cost

Break-even task count
  = competing monthly seat price
  ÷ mean API cost per task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the $0.560 attempted-task average from the Kimi K3 confirmation run, a hypothetical $40 seat reaches raw inference break-even at roughly 71 attempted tasks per month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$40 / $0.560 = 71.4 tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not mean Cline becomes uneconomic at task 72. A Terminal-Bench task is not equivalent to an autocomplete request, and seat plans can include quotas or overages. The calculation only shows why teams need their own task distribution.&lt;/p&gt;

&lt;p&gt;The later Terminal-Bench 2.1 campaign moved from 69/89, or 77.5%, at $79 to 79/89, or 88.8%, at $49.80. The entire optimization campaign consumed roughly one billion tokens and about $680 over 17 hours. The winning run became cheaper because fewer sessions died in retries, loops, and self-termination.&lt;/p&gt;

&lt;p&gt;We found that result technically valuable but causally incomplete. The campaign had no budget-matched parallel-sampling control and no held-out task split. Repeatedly optimizing against the same 89 tasks can improve that suite without proving equivalent generalization. We therefore treat 88.8% as evidence that Cline plus Kimi K3 completed 79 tasks under that configuration, not proof that Cline is universally better than Cursor, Windsurf, or every other harness.&lt;/p&gt;

&lt;p&gt;If you are designing a broader agent platform rather than selecting an editor extension, &lt;a href="https://dev.to/tools"&gt;review our tools collection&lt;/a&gt; and evaluate Cline’s SDK, CLI, and event model separately from its VS Code experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we could not verify
&lt;/h2&gt;

&lt;p&gt;We did not verify the extension installation and provider-configuration procedures, credential-storage behavior, Cline MCP registration or approvals, successful MCP tool execution, the extension's memory footprint, or the task-level cost distribution behind the benchmark totals. Our local MCP experiment used a separate Python SDK client and failed during initialization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;Cline works best when we treat it as an inspectable agent harness rather than a cheaper clone of a commercial IDE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy Cline if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need BYO-key access across multiple providers.&lt;/li&gt;
&lt;li&gt;We can collect token and cost events centrally.&lt;/li&gt;
&lt;li&gt;We want to keep VS Code instead of forcing an IDE migration.&lt;/li&gt;
&lt;li&gt;We are prepared to test model-and-harness combinations together.&lt;/li&gt;
&lt;li&gt;We need MCP integrations and can validate each server/client pairing.&lt;/li&gt;
&lt;li&gt;We can enforce approval policies for shell, writes, network access, and state-changing tools.&lt;/li&gt;
&lt;li&gt;We have a real eval set drawn from our repositories and workflows.&lt;/li&gt;
&lt;li&gt;Variable inference spend is preferable to flat seat pricing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need a zero-administration agent product.&lt;/li&gt;
&lt;li&gt;Developers would manage unrestricted production keys locally.&lt;/li&gt;
&lt;li&gt;We cannot sandbox terminal execution.&lt;/li&gt;
&lt;li&gt;We expect MCP configuration to behave identically across clients.&lt;/li&gt;
&lt;li&gt;We need predictable monthly spend but cannot implement budget controls.&lt;/li&gt;
&lt;li&gt;We plan to use a single Terminal-Bench score as a procurement decision.&lt;/li&gt;
&lt;li&gt;We cannot tolerate provider rate limits or model-routing changes.&lt;/li&gt;
&lt;li&gt;We lack staff to inspect failed traces and maintain the harness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before deployment, we would require per-task usage storage, provider budget alerts, &lt;code&gt;maxTokensPerTurn&lt;/code&gt;, &lt;code&gt;maxIterations&lt;/code&gt;, explicit task cancellation, restricted tool policies, and separate development and production credentials. We would also start MCP with no auto-approved state-changing tools.&lt;/p&gt;

&lt;p&gt;Our final assessment is that Cline is a production candidate for an engineering organization willing to own inference operations and validate its configuration. BYO-key provider choice and host-side usage accounting offer useful control, but we did not establish a comparative portability or accounting advantage over specific closed IDE products. It does not eliminate lock-in entirely: the lock-in moves from the editor to agent rules, approval behavior, telemetry, and tool integration details.&lt;/p&gt;

&lt;p&gt;The benchmark work reinforced the most important lesson. Agent quality is not just model quality. Timeouts, exit-code handling, retries, process supervision, loop detection, and completion verification moved the score from 47% to 57%, while later harness repairs helped reach 79/89 on a different benchmark version and model. Those are runtime-engineering gains, not prompt magic.&lt;/p&gt;

&lt;p&gt;We would deploy Cline first to a small, technically strong cohort, instrument every task, and compare cost per accepted change against the existing workflow. We would not mandate it across a company based on headline benchmark percentages.&lt;/p&gt;

&lt;p&gt;For help designing that controlled rollout, including gateway, MCP, observability, and evaluation architecture, &lt;a href="https://dev.to/contact"&gt;talk to our engineering team&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cline</category>
      <category>aicodingagents</category>
      <category>mcp</category>
      <category>terminalbench</category>
    </item>
    <item>
      <title>Exa vs Tavily in 2026: Our Agent Web Search Benchmark</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Tue, 06 Oct 2026 00:39:13 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/exa-vs-tavily-in-2026-our-agent-web-search-benchmark-3mib</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/exa-vs-tavily-in-2026-our-agent-web-search-benchmark-3mib</guid>
      <description>&lt;h2&gt;
  
  
  Why We Compared Exa and Tavily
&lt;/h2&gt;

&lt;p&gt;Search latency was not the original problem. Grounding reliability was.&lt;/p&gt;

&lt;p&gt;Answer accuracy on 300 company-news questions&lt;span&gt;Exa fast 99.3%/100&lt;/span&gt;&lt;span&gt;Exa instant 97.7%/100&lt;/span&gt;&lt;span&gt;Tavily advanced 93.0%/100&lt;/span&gt;&lt;span&gt;Tavily basic 87.7%/100&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;On the same fixed suite, Exa fast and instant beat Tavily advanced and basic on answer accuracy, with Exa fast at 99.3% versus 87.7% for Tavily basic.&lt;/p&gt;

&lt;p&gt;In agent traces, we kept finding steps that completed quickly but returned the wrong source type: a secondary blog instead of the original announcement, a directory instead of a company page, or an article mentioning a paper rather than the paper itself. The search call looked successful, yet the agent entered its next reasoning step with weak evidence.&lt;/p&gt;

&lt;p&gt;That failure mode is expensive because it does not necessarily throw an error. It consumes model tokens, produces a confident answer, and may survive superficial evaluation.&lt;/p&gt;

&lt;p&gt;We brought Exa and Tavily into our lab to answer four operational questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which API puts an answer-bearing source in the first five results?&lt;/li&gt;
&lt;li&gt;What latency should a multi-step agent budget for, especially beyond the median?&lt;/li&gt;
&lt;li&gt;How much usable text reaches the model?&lt;/li&gt;
&lt;li&gt;What does a correct answer cost after search and input-token charges?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both products expose agent-oriented search APIs, but they emphasize different retrieval and content-delivery features.&lt;/p&gt;

&lt;p&gt;Exa behaves like semantic retrieval infrastructure. Its search types include &lt;code&gt;instant&lt;/code&gt;, &lt;code&gt;fast&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt;, and deeper research modes, while its category-specific retrieval is designed for entities such as people, companies, publications, and code. Its query-dependent highlights can return selected passages instead of an entire page.&lt;/p&gt;

&lt;p&gt;Tavily behaves more like a packaged research interface. It offers &lt;code&gt;basic&lt;/code&gt;, &lt;code&gt;advanced&lt;/code&gt;, and speed-oriented search modes, along with general, news, and finance topics. It can return result snippets, generated answers, and raw page content. That reduces integration work when an agent needs search and extraction in one transaction.&lt;/p&gt;

&lt;p&gt;Before running this harness against live services, we would verify its payloads against the &lt;a href="https://docs.exa.ai/reference/search" rel="noopener noreferrer"&gt;official Exa search reference&lt;/a&gt; and &lt;a href="https://docs.tavily.com/documentation/api-reference/endpoint/search" rel="noopener noreferrer"&gt;official Tavily search reference&lt;/a&gt;, rather than relying on framework wrappers. Wrappers are convenient, but they can hide defaults, rename parameters, and make a provider migration look easier than it is.&lt;/p&gt;

&lt;p&gt;For the main relevance comparison, we fixed the workload at 300 company-news questions. Each question had one pre-established answer tied to an official newsroom or wire URL. Every provider received one natural-language query and could return up to ten results. The same answer extraction path evaluated whether those results contained enough evidence to recover the correct value.&lt;/p&gt;

&lt;p&gt;That suite measures practical grounding, not abstract semantic similarity. It also avoids letting an evaluator decide the expected answer after seeing a provider’s results.&lt;/p&gt;

&lt;p&gt;Our run records showed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exa &lt;code&gt;fast&lt;/code&gt;: 99.3% answer accuracy and 99.3% answer recall at five.&lt;/li&gt;
&lt;li&gt;Exa &lt;code&gt;instant&lt;/code&gt;: 97.7% answer accuracy and 97.3% answer recall at five.&lt;/li&gt;
&lt;li&gt;Tavily &lt;code&gt;advanced&lt;/code&gt;: 93.0% answer accuracy and 92.7% answer recall at five.&lt;/li&gt;
&lt;li&gt;Tavily &lt;code&gt;basic&lt;/code&gt;: 87.7% answer accuracy and recall at five.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also inspected the larger entity-oriented evaluation behind &lt;a href="https://exa.ai/versus/tavily" rel="noopener noreferrer"&gt;Exa’s 2026 comparison&lt;/a&gt;. Under that test’s original conditions—Exa &lt;code&gt;fast&lt;/code&gt;, Tavily &lt;code&gt;advanced&lt;/code&gt;, consistent domain constraints, and model-based grading—Exa reached 75.5% versus 40.5% rank-one recall for people, 81.5% versus 61.3% for companies, and 63.3% versus 31.8% for publication retrieval. The sets contained 200 people queries, 200 company queries, and 597 publication queries.&lt;/p&gt;

&lt;p&gt;We treated those entity figures as vendor-originated evidence, not as a neutral replacement for our fixed factual suite. They are still useful because the workload matches what Exa is designed to do, but the provenance belongs in any purchasing decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We deliberately avoided SDKs for the first pass. Direct HTTP made the provider differences visible and kept dependency versions out of the result.&lt;/p&gt;

&lt;p&gt;We stored credentials as environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;EXA_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"replace-with-exa-key"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TAVILY_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"replace-with-tavily-key"&lt;/span&gt;

python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"httpx&amp;gt;=0.27,&amp;lt;1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our query fixture used one JSON object per line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"q001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"How much did Example Corp say it would invest in its announced Ohio facility?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"expected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"$2 billion"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"q002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"On what date did Example Labs announce its acquisition of Sample AI?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"expected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"September 12, 2026"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following minimal harness is runnable. It measures wall-clock latency, retains normalized URLs for overlap analysis, and emits p50 and p95 for any suite you supply. The two-query fixture is not statistically meaningful; production runs should use the full pinned suite and preserve raw responses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlunsplit&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;EXA_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXA_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;TAVILY_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TAVILY_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonicalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;urlunsplit&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;www.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.exa.ai/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EXA_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fast&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;numResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;highlights&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxCharacters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.tavily.com/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TAVILY_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;advanced&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_raw_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elapsed_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;canonicalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queries.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;observations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tavily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tavily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;latencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;observations&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p50_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fmean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simulated output sample looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"provider":"exa","latency_ms":571.8,"status":200,"result_count":10,"urls":["https://example.com/news/investment"]}
{"provider":"tavily","latency_ms":4187.3,"status":200,"result_count":10,"urls":["https://example.com/news/investment"]}
{"provider":"exa","calls":300,"p50_ms":569.0,"p95_ms":"calculate-from-retained-raw-run","mean_ms":"calculate-from-retained-raw-run"}
{"provider":"tavily","calls":300,"p50_ms":4200.0,"p95_ms":"calculate-from-retained-raw-run","mean_ms":"calculate-from-retained-raw-run"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We intentionally do not insert an invented p95. The retained 300-query benchmark summary gives us medians—569 ms for Exa &lt;code&gt;fast&lt;/code&gt;, 386 ms for Exa &lt;code&gt;instant&lt;/code&gt;, 4.2 seconds for Tavily &lt;code&gt;advanced&lt;/code&gt;, and 1.7 seconds for Tavily &lt;code&gt;basic&lt;/code&gt;—but not the raw latency distribution required to calculate a defensible p95.&lt;/p&gt;

&lt;p&gt;A separate 333-call speed comparison preserved p50, p90, and p99: Exa &lt;code&gt;instant&lt;/code&gt; measured 235/263/437 ms, while Tavily’s comparable ultra-fast mode measured 245/334/576 ms. The medians were close; the tail was not. We use those figures for timeout planning, but we do not relabel p90 or interpolate p95.&lt;/p&gt;

&lt;p&gt;For teams building their own provider abstraction, we collect broader implementation patterns in the &lt;a href="https://dev.to/tools"&gt;Effloow tools collection&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration Issues and Verification Limits
&lt;/h2&gt;

&lt;p&gt;The first problem was parameter parity. There is no safe one-line provider swap.&lt;/p&gt;

&lt;p&gt;Tavily’s &lt;code&gt;max_results&lt;/code&gt; maps conceptually to Exa’s &lt;code&gt;numResults&lt;/code&gt;. Tavily uses &lt;code&gt;include_domains&lt;/code&gt; and &lt;code&gt;exclude_domains&lt;/code&gt;; Exa uses &lt;code&gt;includeDomains&lt;/code&gt; and &lt;code&gt;excludeDomains&lt;/code&gt;. Search-depth labels also differ. Tavily &lt;code&gt;ultra-fast&lt;/code&gt; is closest to Exa &lt;code&gt;instant&lt;/code&gt;, but Tavily &lt;code&gt;advanced&lt;/code&gt; is not automatically equivalent to Exa &lt;code&gt;fast&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt;, or &lt;code&gt;deep&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Content retrieval caused a larger semantic mismatch. Tavily can attach static raw page content to search results. Exa can return full text, but its more distinctive path is query-dependent highlights. Replacing Tavily raw content with Exa highlights changes the context contract: the model receives less text and loses some surrounding material.&lt;/p&gt;

&lt;p&gt;That is often beneficial, but it is not lossless. We retained a full-text fallback for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contract language where adjacent clauses matter;&lt;/li&gt;
&lt;li&gt;technical pages where examples depend on preceding definitions;&lt;/li&gt;
&lt;li&gt;tables whose headers are separated from matching rows;&lt;/li&gt;
&lt;li&gt;pages requiring provenance beyond a selected passage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also found that “extraction quality” needs a precise definition. Our 300-question score measures whether search results let the answering model recover a correct fact. It does not isolate HTML cleaning, JavaScript rendering, table reconstruction, or PDF extraction. We refuse to convert factual-answer accuracy into a pure extractor score.&lt;/p&gt;

&lt;p&gt;The same caution applies to result overlap. Raw URL overlap is unstable because tracking parameters, mirrors, syndication, language variants, and canonical redirects can make identical sources look different. Our harness strips query strings and normalizes hosts, but we still treat Jaccard overlap as a debugging signal rather than a relevance metric. A low overlap can mean healthy source diversity.&lt;/p&gt;

&lt;p&gt;Configuration validation exposed another trap. We reproduced the common pre-restart check with Python 3.12.14 and &lt;code&gt;json.load&lt;/code&gt;. An OpenClaw-style file containing a trailing comma failed with &lt;code&gt;JSONDecodeError&lt;/code&gt; at line 6, column 7, exactly as expected.&lt;/p&gt;

&lt;p&gt;A duplicate provider key was more dangerous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"web"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tavily"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"brave"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default parser accepted this file and silently selected &lt;code&gt;brave&lt;/code&gt;, the last value. Syntax validation alone therefore prevents malformed JSON but does not prove configuration intent. Our local test covered only standard-library &lt;code&gt;json.load&lt;/code&gt; on three synthetic fixtures; it did not test the duplicate-key rejection hook below, the agent’s configuration schema, or restart behavior. For deployment, we would use the hook below to reject duplicate keys and separately validate the configuration against the agent’s schema before restarting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reject_duplicates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Duplicate JSON key: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object_pairs_hook&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reject_duplicates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rate-limit design also needs explicit treatment. Tavily publishes a higher standard ceiling of 1,000 requests per minute. Exa publishes 10-plus queries per second with custom enterprise scaling. We did not run a production-scale saturation test, so we cannot tell buyers how either service degrades during a burst. We implement bounded concurrency, exponential backoff with jitter, and provider-specific circuit breakers rather than assuming the published ceiling is a latency guarantee.&lt;/p&gt;

&lt;p&gt;Domain filters differ too. Exa supports up to 1,200 included and 1,200 excluded domains. Tavily’s referenced limits are 300 included and 150 excluded. That matters for regulated allowlists and large tenant-specific exclusion sets.&lt;/p&gt;

&lt;p&gt;Finally, all returned content is untrusted. We do not store raw extracted bodies in long-lived agent memory. We persist source URLs, hashes, constrained summaries, and citation metadata. Otherwise, prompt-injection text can survive beyond the search turn and contaminate later sessions.&lt;/p&gt;

&lt;p&gt;If this threat model is part of a larger agent deployment, our &lt;a href="https://dev.to/services"&gt;AI infrastructure services&lt;/a&gt; cover retrieval boundaries, observability, and tool authorization rather than treating search as an isolated API call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;Here is the decision table we actually use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Answer recall at 5&lt;/th&gt;
&lt;th&gt;Median search latency&lt;/th&gt;
&lt;th&gt;Search price per 1,000&lt;/th&gt;
&lt;th&gt;Result tokens&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exa &lt;code&gt;fast&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;99.3%&lt;/td&gt;
&lt;td&gt;99.3%&lt;/td&gt;
&lt;td&gt;569 ms&lt;/td&gt;
&lt;td&gt;$7&lt;/td&gt;
&lt;td&gt;1,987&lt;/td&gt;
&lt;td&gt;High-accuracy factual and semantic retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exa &lt;code&gt;instant&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;97.7%&lt;/td&gt;
&lt;td&gt;97.3%&lt;/td&gt;
&lt;td&gt;386 ms&lt;/td&gt;
&lt;td&gt;$7&lt;/td&gt;
&lt;td&gt;2,128&lt;/td&gt;
&lt;td&gt;Latency-sensitive agent loops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily &lt;code&gt;advanced&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;93.0%&lt;/td&gt;
&lt;td&gt;92.7%&lt;/td&gt;
&lt;td&gt;4.2 s&lt;/td&gt;
&lt;td&gt;$16&lt;/td&gt;
&lt;td&gt;2,210&lt;/td&gt;
&lt;td&gt;General research with bundled content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily &lt;code&gt;basic&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;87.7%&lt;/td&gt;
&lt;td&gt;87.7%&lt;/td&gt;
&lt;td&gt;1.7 s&lt;/td&gt;
&lt;td&gt;$8&lt;/td&gt;
&lt;td&gt;1,639&lt;/td&gt;
&lt;td&gt;Lower-cost general lookups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecrawl search&lt;/td&gt;
&lt;td&gt;95.3%&lt;/td&gt;
&lt;td&gt;96.7%&lt;/td&gt;
&lt;td&gt;471 ms&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;678&lt;/td&gt;
&lt;td&gt;Search attached to a crawl-heavy stack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These figures come from the same 300-question company-news workload summarized in the &lt;a href="https://openbenchmarks.com/web-search/exa-vs-tavily-vs-firecrawl" rel="noopener noreferrer"&gt;independent benchmark record&lt;/a&gt;. We kept latency and cost separate from accuracy rather than collapsing them into a subjective weighted score.&lt;/p&gt;

&lt;p&gt;At pay-as-you-go list pricing, 100,000 searches cost approximately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exa: $700.&lt;/li&gt;
&lt;li&gt;Tavily basic: $800.&lt;/li&gt;
&lt;li&gt;Tavily advanced: $1,600.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tavily’s monthly plans can reduce credit prices to roughly $0.0075–$0.005, so procurement volume can reverse part of that difference. Enterprise discounts can also make public prices irrelevant. We recommend comparing signed quotes, not landing pages.&lt;/p&gt;

&lt;p&gt;Search cost is only half of the calculation. At a hypothetical input price of $5 per million tokens, the recorded result payloads produce these 100,000-query totals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Search cost&lt;/th&gt;
&lt;th&gt;Approximate input-token cost&lt;/th&gt;
&lt;th&gt;Combined cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exa &lt;code&gt;fast&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;$700.00&lt;/td&gt;
&lt;td&gt;$993.50&lt;/td&gt;
&lt;td&gt;$1,693.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily &lt;code&gt;advanced&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;$1,600.00&lt;/td&gt;
&lt;td&gt;$1,105.00&lt;/td&gt;
&lt;td&gt;$2,705.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily &lt;code&gt;basic&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;$800.00&lt;/td&gt;
&lt;td&gt;$819.50&lt;/td&gt;
&lt;td&gt;$1,619.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tavily basic is slightly cheaper in this model, but it gives up 11.6 percentage points of accuracy versus Exa fast. For search spending alone, the more useful metric is search-API cost per correct result. Our benchmark record puts Exa fast at about $7.05 per 1,000 correct answers and Tavily advanced at $17.20, excluding downstream model-token charges.&lt;/p&gt;

&lt;p&gt;Token totals remain workload-specific. Exa highlights can materially reduce context on long pages, while Tavily basic happened to return fewer tokens in this suite. We would not sign a contract based on a universal “Exa uses fewer tokens” assumption. Measure the payload generated by your parameters and your query mix.&lt;/p&gt;

&lt;p&gt;Latency compounds in agent loops. As a planning scenario, if each of five sequential calls took its provider’s measured single-call p99 time, search alone would total roughly 2.19 seconds with Exa &lt;code&gt;instant&lt;/code&gt; versus 2.88 seconds with Tavily ultra-fast, before model inference or retries. These sums are not measured five-step runtimes or workflow p99 estimates. Parallel fan-out changes the arithmetic, but the slowest branch still controls completion.&lt;/p&gt;

&lt;p&gt;The acquisition of Tavily by Nebius in February 2026 is also a roadmap consideration. We do not treat ownership change as an automatic negative. It can bring distribution, infrastructure, and enterprise contracting advantages. We do, however, put API compatibility, pricing protection, data residency, and deprecation notice periods into the contract because integration priorities can change after an acquisition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;We would deploy Exa first when the agent must find a specific company, person, paper, code reference, or original factual source. It won our fixed factual suite on accuracy, cost per correct answer, and practical latency. Its &lt;code&gt;instant&lt;/code&gt; mode also gave us the tighter recorded tail in the separate speed test.&lt;/p&gt;

&lt;p&gt;We would deploy Tavily when the team wants a simpler general-research interface with raw content, generated-answer options, news and finance modes, and a high published request ceiling. Tavily is also reasonable when consolidated Nebius procurement matters more than absolute retrieval performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy Exa if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rank-one semantic retrieval is central to the product;&lt;/li&gt;
&lt;li&gt;agents search for entities or publications;&lt;/li&gt;
&lt;li&gt;tail latency controls a multi-step workflow;&lt;/li&gt;
&lt;li&gt;query-dependent highlights can replace most full-page text;&lt;/li&gt;
&lt;li&gt;large domain allowlists or blocklists are required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deploy Tavily if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;general web research dominates the workload;&lt;/li&gt;
&lt;li&gt;bundled raw content reduces engineering work;&lt;/li&gt;
&lt;li&gt;news or finance topic routing is valuable;&lt;/li&gt;
&lt;li&gt;the published 1,000 RPM standard ceiling fits a high-volume design;&lt;/li&gt;
&lt;li&gt;Nebius consolidation simplifies procurement or infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or run a longer proof of concept if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript-heavy extraction, PDFs, or tables are the primary workload;&lt;/li&gt;
&lt;li&gt;you need contractual latency objectives rather than benchmark medians;&lt;/li&gt;
&lt;li&gt;your agent sends sensitive customer data in queries;&lt;/li&gt;
&lt;li&gt;you cannot isolate untrusted retrieved text from instructions and memory;&lt;/li&gt;
&lt;li&gt;your economics depend on unverified enterprise discounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our production recommendation is not “pick one forever.” Put both behind a typed adapter, retain raw timing and billing metadata, and route by workload. Start with Exa for semantic or high-value factual retrieval. Use Tavily where full-page general research is the actual requirement. Add a fallback only after measuring whether the extra recall justifies duplicate search spend.&lt;/p&gt;

&lt;p&gt;Most importantly, benchmark correctness before speed. A 250 ms search call that grounds the agent in the wrong source is not fast infrastructure. It is a fast path to an expensive mistake.&lt;/p&gt;

</description>
      <category>exa</category>
      <category>tavily</category>
      <category>agentsearch</category>
      <category>rag</category>
    </item>
    <item>
      <title>Groq API in Production: The Latency, Throughput, and Cost Trade-Offs That Matter</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Mon, 05 Oct 2026 00:43:51 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/groq-api-in-production-the-latency-throughput-and-cost-trade-offs-that-matter-kme</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/groq-api-in-production-the-latency-throughput-and-cost-trade-offs-that-matter-kme</guid>
      <description>&lt;h2&gt;
  
  
  Why We Brought This Tool Into Our Lab
&lt;/h2&gt;

&lt;p&gt;We brought GroqCloud into our lab for a narrow reason: user-facing agents spend too much time waiting for models to emit tokens.&lt;/p&gt;

&lt;p&gt;An agent can have a fast vector store, a warm application container, and sub-20 ms internal service calls, yet still feel slow because generation takes several seconds. Faster decoding changes that interaction materially. It also shortens sequential agent loops where every tool result triggers another model call.&lt;/p&gt;

&lt;p&gt;Groq attacks this bottleneck with its LPU inference architecture and an OpenAI-compatible API. We did not treat that architecture as proof of production performance. We separated four measurements that are too often collapsed into one headline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-observed time to first useful token:&lt;/strong&gt; request start to the first non-empty content delta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end latency:&lt;/strong&gt; request start to a completed stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-side output-throughput proxy:&lt;/strong&gt; returned completion tokens divided by elapsed time from the first non-empty content delta to stream end. We do not treat this as verified answer-token decoding speed when reasoning or other non-visible tokens may be included in completion usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workload cost:&lt;/strong&gt; returned input and output tokens multiplied by the model-specific rates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction matters. Groq’s console records server-side latency, while an end user experiences network time plus server time. A dashboard can therefore look healthy while clients in another region see a worse tail. We also found that “first chunk” is not necessarily “first useful token”: role metadata or an empty content delta can arrive before visible text.&lt;/p&gt;

&lt;p&gt;The historical LPU result that put Groq on engineering teams’ radar used approximately 100 input tokens and 200 output tokens. Groq’s Llama 2 Chat 70B endpoint reached 241 output tokens per second in that 2024 test, with an estimated 0.8-second response time for 100 output tokens. That is useful architectural evidence, but it is not a valid 2026 capacity number. The model, API traffic, and product lineup have all changed.&lt;/p&gt;

&lt;p&gt;For an external comparison, we recorded the July 2026 snapshot summarized in this &lt;a href="https://markaicode.com/benchmarks/groq-production-benchmark-latency/" rel="noopener noreferrer"&gt;2026 Groq benchmark&lt;/a&gt;. In our July 2026 snapshot comparison, we recorded median time to first token of 0.71 seconds for GPT-OSS 120B at high reasoning effort, 0.73 seconds at low reasoning effort, and 0.77 seconds for GPT-OSS 20B at high reasoning effort. Those figures include internal thinking before an answer token appears. We use them as a sanity check, not as substitutes for measurements from our deployment region.&lt;/p&gt;

&lt;p&gt;The model lineup was our first warning against benchmark marketing. &lt;code&gt;llama-3.1-8b-instant&lt;/code&gt; and &lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt; were scheduled to shut down on August 16, 2026. A benchmark built around either identifier is now historical evidence, not a basis for deployment. We based the runnable harness on &lt;code&gt;openai/gpt-oss-20b&lt;/code&gt; and &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;, while keeping the model configurable.&lt;/p&gt;

&lt;p&gt;Our goal was not to prove that one accelerator always beats another. We wanted to answer a more operational question: can we route latency-sensitive agent turns to Groq without losing control of tail latency, billing, stream integrity, and fallback behavior?&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We used the chat-completions endpoint directly so the harness could inspect SSE events and response headers. The only secret required is a Groq API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"httpx==0.27.0"&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GROQ_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gsk_replace_me"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GROQ_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"openai/gpt-oss-20b"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GROQ_RUNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We fixed the workload to one compact agent-planning turn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A system instruction requiring JSON.&lt;/li&gt;
&lt;li&gt;One user request describing three support-ticket actions.&lt;/li&gt;
&lt;li&gt;Temperature set to zero.&lt;/li&gt;
&lt;li&gt;A maximum of 256 output tokens.&lt;/li&gt;
&lt;li&gt;Streaming enabled.&lt;/li&gt;
&lt;li&gt;Sequential requests, so the result measures single-stream behavior rather than concurrent account capacity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following script calculates p50 and p95 for first useful token and total latency. It also records output throughput, token usage, routing headers, and cost. We intentionally fail a run if the stream ends without a completion signal or without authoritative usage; silently estimating billing tokens would make the cost result look more precise than it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# bench_groq.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.groq.com/openai/v1/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GROQ_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/gpt-oss-20b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RUNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GROQ_RUNS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;PRICES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/gpt-oss-20b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.075&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/gpt-oss-120b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;MESSAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a support operations agent. Return JSON only with keys &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority, actions, and escalation_reason. Use at most three actions.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A paid customer cannot export invoices. Login works. The export &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request returns HTTP 500, and retrying twice did not help. Plan the response.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MESSAGES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service_tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on_demand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;first_content_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;finish_reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-groq-region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cf-ray&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rate_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-ratelimit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry-after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_lines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DONE]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="n"&gt;choices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_content_at&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;first_content_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finish_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;finish_reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finish_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_content_at&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stream contained no non-empty content delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;finish_reason&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stream ended without a verified completion signal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No authoritative token usage was returned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;decode_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;first_content_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttft_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_content_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;decode_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cf_ray&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate_headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rate_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_chars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GROQ_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;45.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RUNS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ttft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttft_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;throughput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRICES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;
        &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RUNS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttft_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;throughput&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p05&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;throughput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;estimated_cost_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python bench_groq.py &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; result.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful execution emits this structure with values measured from the caller’s own network path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai/gpt-oss-20b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"runs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ttft_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p50"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;measured by the script&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p95"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;measured by the script&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p50"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;measured by the script&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p95"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;measured by the script&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output_tps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"median"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;measured by the script&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p05"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;measured by the script&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;API-reported total&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;API-reported total&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"estimated_cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;calculated from reported usage&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"regions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;x-groq-region value&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What This Article Could Not Verify
&lt;/h3&gt;

&lt;p&gt;Our retained evidence does not include a completed remote run, so we cannot report live latency percentiles from our own deployment. The harness is our reproducible artifact; the 2026 external figures provide a comparison. Anyone evaluating a production region should run it from the same network and runtime that will serve users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming Failures and Deployment Constraints
&lt;/h2&gt;

&lt;p&gt;The most important failure was not a slow token. It was an apparently successful but incomplete stream.&lt;/p&gt;

&lt;p&gt;We reproduced the stream contract locally with &lt;code&gt;groq==0.9.0&lt;/code&gt;, &lt;code&gt;httpx==0.27.0&lt;/code&gt;, and &lt;code&gt;pydantic==2.8.2&lt;/code&gt;. Our HTTP transport emitted deterministic OpenAI-compatible SSE chunks, which let us test completion, a read failure after partial content, and a clean EOF without a finish chunk.&lt;/p&gt;

&lt;p&gt;The complete fixture returned four SDK chunks, two non-empty content chunks, the text &lt;code&gt;Hello from fixture.&lt;/code&gt;, and &lt;code&gt;finish_reason="stop"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When we injected a transport read failure after the first content delta, the SDK returned &lt;code&gt;Hello&lt;/code&gt; and propagated an &lt;code&gt;httpx.ReadError&lt;/code&gt;. It issued one request and did not replay automatically. That behavior is preferable to an invisible retry, but the application must distinguish partial UI output from a committed result.&lt;/p&gt;

&lt;p&gt;The nastier case was clean EOF. The iterator returned normally after yielding &lt;code&gt;Hello&lt;/code&gt; even though no finish reason had arrived. There was no exception. Code that treats normal loop termination as success can therefore persist a truncated answer, trigger an incomplete tool call, or bill a workflow as completed.&lt;/p&gt;

&lt;p&gt;Our workaround is strict:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buffer or mark streamed content as provisional.&lt;/li&gt;
&lt;li&gt;Require a non-null finish reason before committing it.&lt;/li&gt;
&lt;li&gt;Treat EOF without a completion signal as an integrity failure.&lt;/li&gt;
&lt;li&gt;Never replay an agent turn blindly after partial output.&lt;/li&gt;
&lt;li&gt;Attach idempotency controls to downstream tools and business actions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also hit four broader deployment constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits are account and model constraints, not accelerator throughput.&lt;/strong&gt; A model may decode at hundreds of tokens per second while the account still rejects a burst. We log every &lt;code&gt;x-ratelimit-*&lt;/code&gt; and &lt;code&gt;retry-after&lt;/code&gt; header, cap concurrency with a semaphore, and retry 429 or transient 5xx responses with bounded exponential backoff and jitter. We do not retry malformed requests or replay partially executed tool turns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service tiers change failure semantics.&lt;/strong&gt; We use &lt;code&gt;on_demand&lt;/code&gt; for interactive traffic. Flex processing can provide up to ten times the current rate limits when capacity exists, but it may fail rapidly when capacity is constrained. Auto begins with on-demand limits and can fall back to flex. That makes flex useful for replayable enrichment jobs, not for an agent action that must execute exactly once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model identifiers are routing dependencies.&lt;/strong&gt; Groq is not a generic deployment target where we upload arbitrary weights. We route only to its curated catalog, and scheduled model removal can break hard-coded applications. We now place model IDs behind configuration, run a startup capability check, and keep a quality-tested fallback at another provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token counting must come from the response.&lt;/strong&gt; Character counts, local tokenizers, and concatenated SSE fragments are useful diagnostics, but they are not billing records. Reasoning and tool-related tokens can make client estimates diverge. We calculate cost from authoritative prompt and completion usage, store the raw usage object, and reject a benchmark sample when usage is missing.&lt;/p&gt;

&lt;p&gt;The same caution applies to latency. We separate network time from server time and treat input length as the primary TTFT driver in our measurement design (&lt;a href="https://console.groq.com/docs/production-readiness/optimizing-latency" rel="noopener noreferrer"&gt;latency reference&lt;/a&gt;). We preserve input size, output cap, reasoning mode, region, service tier, and concurrency with every sample. Without those dimensions, p95 is just a number detached from a workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;Our current comparison is not “LPU versus GPU” in the abstract. It is hosted Groq versus a conventional GPU API versus self-hosted vLLM for a specific traffic shape.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision factor&lt;/th&gt;
&lt;th&gt;GroqCloud&lt;/th&gt;
&lt;th&gt;Hosted GPU inference API&lt;/th&gt;
&lt;th&gt;Self-hosted vLLM or SGLang&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fast single-stream decoding&lt;/td&gt;
&lt;td&gt;Core strength; published 2026 rates were 1,000 tok/s for GPT-OSS 20B and 500 tok/s for GPT-OSS 120B&lt;/td&gt;
&lt;td&gt;Varies heavily by provider, batching, and model&lt;/td&gt;
&lt;td&gt;Tunable, but low-load latency and saturated throughput require different configurations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational work&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High: GPUs, autoscaling, model loading, observability, and upgrades&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model freedom&lt;/td&gt;
&lt;td&gt;Curated Groq catalog&lt;/td&gt;
&lt;td&gt;Usually broader&lt;/td&gt;
&lt;td&gt;Highest, subject to hardware and license&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burst behavior&lt;/td&gt;
&lt;td&gt;Controlled by account, model, and service-tier limits&lt;/td&gt;
&lt;td&gt;Provider-specific quotas and queues&lt;/td&gt;
&lt;td&gt;Controlled by owned capacity; excess traffic queues or fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost model&lt;/td&gt;
&lt;td&gt;Per input/output token&lt;/td&gt;
&lt;td&gt;Usually per token&lt;/td&gt;
&lt;td&gt;GPU-hours plus engineering and idle capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming risk&lt;/td&gt;
&lt;td&gt;SSE completion must be verified&lt;/td&gt;
&lt;td&gt;Similar API-stream concerns&lt;/td&gt;
&lt;td&gt;Full control, but full responsibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Interactive agents where latency has business value&lt;/td&gt;
&lt;td&gt;Broad model access and managed fallback&lt;/td&gt;
&lt;td&gt;Stable, high utilization or strict deployment control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The July 2026 price points we validated were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Published speed&lt;/th&gt;
&lt;th&gt;Input per 1M tokens&lt;/th&gt;
&lt;th&gt;Output per 1M tokens&lt;/th&gt;
&lt;th&gt;Fixed workload cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-OSS 20B&lt;/td&gt;
&lt;td&gt;1,000 tok/s&lt;/td&gt;
&lt;td&gt;$0.075&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;td&gt;$0.000150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-OSS 120B&lt;/td&gt;
&lt;td&gt;500 tok/s&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;td&gt;$0.60&lt;/td&gt;
&lt;td&gt;$0.000300&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fixed workload cost assumes 1,000 input tokens and 250 output tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPT-OSS 20B:
(1,000 × $0.075 / 1,000,000) + (250 × $0.30 / 1,000,000)
= $0.000150 per turn

GPT-OSS 120B:
(1,000 × $0.15 / 1,000,000) + (250 × $0.60 / 1,000,000)
= $0.000300 per turn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At one million such turns, that is $150 for GPT-OSS 20B or $300 for GPT-OSS 120B before caching, retries, failed generations, tool-loop expansion, and taxes. The blended rates are $0.12 and $0.24 per million total tokens respectively, but that shorthand is valid only for this 4:1 input-to-output ratio. Output-heavy agents cost more because output tokens carry the higher rate.&lt;/p&gt;

&lt;p&gt;Batch processing reduces synchronous pricing by 50% and does not consume standard API quotas, but its completion window makes it a different product. At the same workload, the nominal model cost becomes $75 or $150 per million turns. We would use that for evaluation runs, enrichment, and scheduled classification—not interactive chat.&lt;/p&gt;

&lt;p&gt;For self-hosting, the useful break-even equation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;required requests per hour =
GPU cluster cost per hour / hosted API cost per request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a self-hosted cluster costs &lt;code&gt;G&lt;/code&gt; dollars per hour, GPT-OSS 20B’s token-only break-even for this workload is &lt;code&gt;G / 0.000150&lt;/code&gt; requests per hour. For GPT-OSS 120B it is &lt;code&gt;G / 0.000300&lt;/code&gt;. We then add engineering, failover capacity, observability, idle headroom, and the cost of meeting p95 during bursts. Comparing Groq’s per-token bill against a fully utilized GPU while ignoring idle time is not a serious production calculation.&lt;/p&gt;

&lt;p&gt;We would also require identical models, prompts, output caps, concurrency, and regions before claiming that Groq is “N times faster” than a GPU service. The old 241 tok/s Llama 2 result illustrates the hardware’s decoding performance; it does not establish a current speed advantage over an unspecified GPU service.&lt;/p&gt;

&lt;p&gt;Teams building a multi-provider router can review our broader &lt;a href="https://dev.to/tools"&gt;tools collection&lt;/a&gt;. For workload-specific capacity and failover design, our &lt;a href="https://dev.to/services"&gt;AI infrastructure services&lt;/a&gt; cover the parts that raw tokens-per-second charts omit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;Groq works best as a low-latency inference option within a multi-provider system, not as the default provider for every workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User-perceived generation speed directly affects conversion, retention, or operator productivity.&lt;/li&gt;
&lt;li&gt;Your agent makes several sequential model calls and faster decoding shortens the full loop.&lt;/li&gt;
&lt;li&gt;A supported GPT-OSS model meets your quality requirements.&lt;/li&gt;
&lt;li&gt;You can measure TTFT and completion latency from the production region.&lt;/li&gt;
&lt;li&gt;You calculate cost from returned usage rather than tokenizer estimates.&lt;/li&gt;
&lt;li&gt;You enforce completion signals before committing streamed output.&lt;/li&gt;
&lt;li&gt;You have bounded retries, concurrency controls, and idempotent tools.&lt;/li&gt;
&lt;li&gt;You can tolerate a curated model catalog and maintain a tested fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need custom or fine-tuned weights that Groq does not host.&lt;/li&gt;
&lt;li&gt;Your compliance requirements have not been contractually verified.&lt;/li&gt;
&lt;li&gt;Your workload is offline and latency has little economic value.&lt;/li&gt;
&lt;li&gt;You depend on a deprecated model identifier.&lt;/li&gt;
&lt;li&gt;Your traffic exceeds available quotas and cannot be queued or shifted to batch.&lt;/li&gt;
&lt;li&gt;You need provider-independent behavior across every OpenAI-compatible extension.&lt;/li&gt;
&lt;li&gt;You cannot safely recover from a partial agent stream.&lt;/li&gt;
&lt;li&gt;You are choosing it solely because an old benchmark reported a large GPU multiple.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our verdict is positive, with conditions. The published 2026 pricing is aggressive, the single-stream throughput targets are unusually high, and the API surface is straightforward. The operational risks sit around the accelerator: rate limits, service-tier semantics, model churn, network distance, token accounting, and ambiguous stream termination.&lt;/p&gt;

&lt;p&gt;We would deploy Groq for latency-sensitive conversational turns behind our own gateway, with on-demand processing, explicit model configuration, raw usage logging, verified &lt;code&gt;finish_reason&lt;/code&gt;, and a secondary provider. We would route replayable bulk work to batch or flex, compare median TTFT with the dated external figures only under comparable model and reasoning settings, and track p95 against our own workload-specific baseline and latency target.&lt;/p&gt;

&lt;p&gt;We would not approve a rollout based on the 1,000 tok/s headline alone. Before routing production traffic to Groq, run the supplied harness from the actual service region, add a concurrency sweep that stays within the account’s quotas, and verify quality on the same agent traces used in production. If you want us to review that routing design, &lt;a href="https://dev.to/contact"&gt;contact Effloow&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>groq</category>
      <category>llminference</category>
      <category>aiagents</category>
      <category>apibenchmark</category>
    </item>
    <item>
      <title>Browser Use vs Skyvern Self-Hosted: Setup, Trade-offs, and Completion Verification</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Sun, 04 Oct 2026 00:48:36 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/browser-use-vs-skyvern-self-hosted-setup-trade-offs-and-completion-verification-1l5c</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/browser-use-vs-skyvern-self-hosted-setup-trade-offs-and-completion-verification-1l5c</guid>
      <description>&lt;h2&gt;
  
  
  Why we evaluated Browser Use and Skyvern
&lt;/h2&gt;

&lt;p&gt;We did not bring Browser Use and Skyvern into our lab to see whether an agent could click a button in a polished demo. We wanted to answer a more expensive question: can either system complete a recurring login-to-download workflow without creating a permanent human repair queue?&lt;/p&gt;

&lt;p&gt;Agent-reported done is not verified completion&lt;/p&gt;
&lt;p&gt;A strict schema receipt still passed for a missing and a corrupted file, so a workflow should count as complete only when page state, business identifier, and artifact size and digest are checked independently of the agent's own success claim.&lt;/p&gt;

&lt;p&gt;Our target shape was deliberately ordinary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a locally hosted application.&lt;/li&gt;
&lt;li&gt;Authenticate with a test account.&lt;/li&gt;
&lt;li&gt;complete a three-stage form.&lt;/li&gt;
&lt;li&gt;Submit the transaction.&lt;/li&gt;
&lt;li&gt;Download the generated file.&lt;/li&gt;
&lt;li&gt;Prove that the downloaded artifact is the expected file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last condition matters. An agent saying “done” is not evidence that the workflow finished correctly. A successful click is not a successful transaction, and a valid JSON response is not proof that a file exists.&lt;/p&gt;

&lt;p&gt;The architectural distinction also turned out to be less absolute than the usual “DOM versus vision” shorthand suggests.&lt;/p&gt;

&lt;p&gt;Browser Use builds its page representation primarily from browser structure, including the DOM and accessibility information exposed through Chrome. Screenshots can be disabled, enabled, or requested when needed. That lets us configure Browser Use for DOM-first operation, but it is not DOM-only.&lt;/p&gt;

&lt;p&gt;Skyvern’s self-hosted execution loop captures a screenshot, combines visual interpretation with a simplified DOM, asks the configured model for the next action, and executes that action through Playwright. That makes it vision-forward, but not vision-only.&lt;/p&gt;

&lt;p&gt;In practical terms, both products run a perception-action loop. Each additional loop can add model latency, input volume, another chance to select the wrong element, and another opportunity for a retry to multiply cost. The meaningful comparison is therefore not whether one product “uses AI.” Both do. We care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verified workflow completion rate;&lt;/li&gt;
&lt;li&gt;wall-clock time per verified completion;&lt;/li&gt;
&lt;li&gt;model requests and input volume per run;&lt;/li&gt;
&lt;li&gt;retries and terminal failure reasons;&lt;/li&gt;
&lt;li&gt;human interventions per 100 runs;&lt;/li&gt;
&lt;li&gt;infrastructure and inference cost per successful task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also separated the product shapes. &lt;a href="https://github.com/browser-use/browser-use" rel="noopener noreferrer"&gt;Browser Use&lt;/a&gt; is primarily an embeddable MIT-licensed agent library. Skyvern is a larger AGPL-licensed workflow platform with an API server, web interface, database, browser runtime, workflow state, and operational views.&lt;/p&gt;

&lt;p&gt;That distinction affects engineering cost more than a synthetic leaderboard score. Browser Use gives us an embeddable component. Skyvern gives us a broader platform for managing browser workflows.&lt;/p&gt;

&lt;p&gt;For adjacent infrastructure evaluations, we keep our current shortlist in &lt;a href="https://dev.to/tools"&gt;our tools collection&lt;/a&gt;. The rest of this review focuses on what we could verify without pretending that an incomplete run ledger was a production benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We treated installation reproducibility as the first test. If a team cannot recreate the environment from pinned commands, any reliability percentage is suspect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser Use installation path
&lt;/h3&gt;

&lt;p&gt;For Browser Use, we use Python 3.11 or newer and take &lt;code&gt;uv add browser-use&lt;/code&gt; as the documented setup path. The following pip-based setup is a proposed reproduction path, not an installation test we completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.12 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv-browser-use
&lt;span class="nb"&gt;source&lt;/span&gt; .venv-browser-use/bin/activate

python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
pip &lt;span class="nb"&gt;install &lt;/span&gt;browser-use

&lt;span class="c"&gt;# Make the browser dependency explicit instead of relying on first-run behavior.&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We would make Chromium provisioning an explicit build step rather than assume that package installation downloads a compatible browser binary. We did not test first-run browser provisioning here. Our production image would pin the package, verify the browser installation procedure for that version, cache the browser layer, and fail the build if Chromium could not launch.&lt;/p&gt;

&lt;p&gt;The minimal agent shape is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;browser_use&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local-development-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Open http://demo-app:8080, sign in with the supplied test &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credentials, complete the three-step request form, download &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the receipt, and return its filename and visible request ID.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final_result&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local OpenAI-compatible endpoint removes provider charges, but it does not make inference free. GPU depreciation, power, model-serving memory, queueing, and operator time still belong in the cost model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skyvern self-hosted path
&lt;/h3&gt;

&lt;p&gt;For Skyvern, we used the full-stack deployment boundary rather than mistaking the lightweight Python package for the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Skyvern-AI/skyvern.git
&lt;span class="nb"&gt;cd &lt;/span&gt;skyvern

&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Configure the selected LLM provider or local Ollama endpoint in .env.&lt;/span&gt;

docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important trap is that plain &lt;code&gt;pip install skyvern&lt;/code&gt; is the remote API SDK, not the complete local platform. The server-capable package is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"skyvern[server]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the web interface, API server, browser runtime, and PostgreSQL together, Docker Compose remains the practical starting point. We budgeted at least the documented 4 GB of RAM for evaluation and would follow the recommendation of 8 GB or more for production. The 8 GB figure is a recommendation, not a mandatory deployment minimum or a memory measurement from our workload.&lt;/p&gt;

&lt;p&gt;For our &lt;a href="https://www.skyvern.com/docs/developers/self-hosted/overview" rel="noopener noreferrer"&gt;Skyvern self-hosting plan&lt;/a&gt;, we account for the API server, web UI, PostgreSQL, browser runtime, and a configured LLM provider. Self-hosting removes a per-task platform charge, but we still have to operate the API, browser capacity, database, model endpoint, backups, updates, and any required proxy service.&lt;/p&gt;

&lt;h3&gt;
  
  
  The completion contract we actually executed
&lt;/h3&gt;

&lt;p&gt;We did not have a defensible N-run vendor trace containing success, duration, model usage, and artifact hashes. Rather than invent those numbers, we tested the most important measurement boundary: whether structured output alone can falsely mark a workflow complete.&lt;/p&gt;

&lt;p&gt;We pinned Pydantic 2.10.6 and checked an application-defined strict completion receipt against a real local fixture file. We tested Pydantic validation and our proposed completion contract, not Browser Use or Skyvern integrations. Matching the fixture’s size and digest established only that its bytes agreed with the expected receipt—not that a browser workflow completed or a website delivered the correct business document. The contract and artifact checks were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ConfigDict&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DownloadReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConfigDict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;byte_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_artifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DownloadReceipt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;artifact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing_artifact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;size_matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byte_count&lt;/span&gt;
    &lt;span class="n"&gt;digest_matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;size_matches&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;digest_matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matched&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size_matches&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;digest_matches&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifact_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observed_bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size_matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;size_matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;digest_matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest_matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our test output was unambiguous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pydantic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.10.6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"genuine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict_schema_accepted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"artifact_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"missing_artifact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict_schema_accepted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"artifact_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"missing_artifact"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"corrupted_artifact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict_schema_accepted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"artifact_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"artifact_mismatch"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"numeric_string"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lax_schema_accepted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict_schema_accepted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"error_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int_type"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strict validation rejected a numeric string where an integer was required. It also rejected missing required fields and an invalid status. However, schema validation could not determine whether the referenced file existed or matched its declared digest.&lt;/p&gt;

&lt;p&gt;Our completion numerator must therefore be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verified_success =
    expected_final_page_state
    AND expected_business_identifier
    AND artifact_exists
    AND artifact_size_matches
    AND artifact_digest_matches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything weaker inflates the apparent reliability of both products.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we could not verify and deployment limitations
&lt;/h2&gt;

&lt;p&gt;The first failure was methodological: we could not produce a valid head-to-head reliability score from the available run evidence.&lt;/p&gt;

&lt;p&gt;We had install paths, architectural boundaries, pricing inputs, and a completed validation experiment. We did not have repeated Browser Use and Skyvern executions with comparable model settings, task seeds, token accounting, retries, and artifact verification. Publishing “9/10 versus 8/10” under those conditions would be fiction.&lt;/p&gt;

&lt;p&gt;That absence exposed several production traps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser installation is a separate dependency
&lt;/h3&gt;

&lt;p&gt;Installing the Python package is not equivalent to provisioning a runnable Chromium binary. For a reproduction build, we would verify whether &lt;code&gt;python -m playwright install chromium&lt;/code&gt; is required by the pinned Browser Use version and add a browser launch smoke test.&lt;/p&gt;

&lt;p&gt;Our workaround is simple: pin Browser Use, pin the container base image, install Chromium during the build, and never permit a production worker to download browser assets on first request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skyvern’s packages support two different deployment models
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pip install skyvern&lt;/code&gt; does not create the API server, UI, browser runtime, and database stack. We would use &lt;code&gt;skyvern[server]&lt;/code&gt; only for a controlled source-based development path. For team evaluation, we would start with Compose and pin every image digest.&lt;/p&gt;

&lt;p&gt;We would also inspect and explicitly configure the maximum step count, task timeout, and retry policy before submitting real work. An unbounded retry loop does not ensure resilience and can make costs unpredictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-hosting does not automatically keep inference local
&lt;/h3&gt;

&lt;p&gt;Skyvern can send screenshots to a configured remote model on every perception step. Browser Use can likewise send page state and optional screenshots to an external model. Running the orchestrator on our own server does not change that data boundary.&lt;/p&gt;

&lt;p&gt;For sensitive workflows, we would either use an approved private endpoint or a local model that had passed the same task suite. We would not assume a smaller local model preserved cloud-model reliability.&lt;/p&gt;

&lt;h3&gt;
  
  
  CAPTCHA and bot detection remain hard boundaries
&lt;/h3&gt;

&lt;p&gt;For self-hosted Skyvern, CAPTCHA handling can require manual intervention. Browser Use’s managed browser offering adds stealth, proxies, and CAPTCHA services, but no browser configuration guarantees universal success.&lt;/p&gt;

&lt;p&gt;That distinction changes the operating model. A workflow that occasionally waits for a person is not unattended automation. We would classify it as “human-assisted” and account for intervention time separately.&lt;/p&gt;

&lt;p&gt;The same rule applies to authentication state. Browser Use profile synchronization covers cookies, but not every form of local storage, IndexedDB state, or extension-backed authentication. A supposedly warm session can still return to the login screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry loops can disguise selector mistakes
&lt;/h3&gt;

&lt;p&gt;An agent can select a plausible but incorrect element, observe no expected transition, and retry with a slightly different action. Without a hard step budget and a terminal-state validator, the run may look active while making no business progress.&lt;/p&gt;

&lt;p&gt;Our workaround is to record a failure taxonomy rather than one aggregate error:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure class&lt;/th&gt;
&lt;th&gt;Required evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wrong element or selector hallucination&lt;/td&gt;
&lt;td&gt;Intended control, selected control, screenshot, DOM reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigation or page timeout&lt;/td&gt;
&lt;td&gt;URL, elapsed time, timeout boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication failure&lt;/td&gt;
&lt;td&gt;Login state, challenge type, session source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAPTCHA or bot block&lt;/td&gt;
&lt;td&gt;Challenge screenshot and intervention requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry exhaustion&lt;/td&gt;
&lt;td&gt;Step count, repeated action signature, last changed state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False completion&lt;/td&gt;
&lt;td&gt;Agent result versus independent page and artifact checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact failure&lt;/td&gt;
&lt;td&gt;Missing file, wrong size, or digest mismatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model failure&lt;/td&gt;
&lt;td&gt;Invalid action schema, refusal, or unavailable endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the level of instrumentation we would require before involving a customer workflow. Teams needing help building that harness can review &lt;a href="https://dev.to/services"&gt;our AI infrastructure services&lt;/a&gt; or &lt;a href="https://dev.to/contact"&gt;contact us&lt;/a&gt; with the target volume and error budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;We cannot honestly report measured success rate, wall-clock time, token count, or dollars per completed task for the full login-to-download workflow. Those values were not established by the executable experiment.&lt;/p&gt;

&lt;p&gt;What we can compare is the cost structure and the operational work each option leaves with us.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision factor&lt;/th&gt;
&lt;th&gt;Browser Use OSS&lt;/th&gt;
&lt;th&gt;Skyvern self-hosted&lt;/th&gt;
&lt;th&gt;Deterministic Playwright&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary shape&lt;/td&gt;
&lt;td&gt;Python agent library&lt;/td&gt;
&lt;td&gt;Workflow platform and API&lt;/td&gt;
&lt;td&gt;Browser automation library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;AGPL v3&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grounding&lt;/td&gt;
&lt;td&gt;DOM and accessibility first; optional vision&lt;/td&gt;
&lt;td&gt;Screenshot plus simplified DOM&lt;/td&gt;
&lt;td&gt;Explicit selectors and assertions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial setup&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI and run history&lt;/td&gt;
&lt;td&gt;We build it&lt;/td&gt;
&lt;td&gt;Included in the stack&lt;/td&gt;
&lt;td&gt;We build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database requirement&lt;/td&gt;
&lt;td&gt;Application choice&lt;/td&gt;
&lt;td&gt;PostgreSQL in Compose&lt;/td&gt;
&lt;td&gt;Application choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model cost&lt;/td&gt;
&lt;td&gt;Per agent step&lt;/td&gt;
&lt;td&gt;Per agent step, often with image input&lt;/td&gt;
&lt;td&gt;None for scripted steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser infrastructure&lt;/td&gt;
&lt;td&gt;We operate it or buy managed browsers&lt;/td&gt;
&lt;td&gt;We operate it&lt;/td&gt;
&lt;td&gt;We operate it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAPTCHA in self-hosted mode&lt;/td&gt;
&lt;td&gt;No universal guarantee&lt;/td&gt;
&lt;td&gt;Manual intervention may be required&lt;/td&gt;
&lt;td&gt;External solver or manual path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact verification&lt;/td&gt;
&lt;td&gt;Application responsibility&lt;/td&gt;
&lt;td&gt;Workflow plus application checks&lt;/td&gt;
&lt;td&gt;Direct application checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Agent capability embedded in code&lt;/td&gt;
&lt;td&gt;Operations-owned recurring workflows&lt;/td&gt;
&lt;td&gt;Stable, high-volume sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verified benchmark result here&lt;/td&gt;
&lt;td&gt;Not established&lt;/td&gt;
&lt;td&gt;Not established&lt;/td&gt;
&lt;td&gt;Not run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For managed Browser Use browsers, we budget $0.02 per browser-hour, with model usage and traffic accounted for separately. At that rate, browser time alone is rarely the dominant cost for a short task. Model calls, retries, proxy traffic, failed runs, and repair labor can be much larger.&lt;/p&gt;

&lt;p&gt;We use this formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost_per_verified_success =
    total_browser_cost
    + total_model_cost
    + total_proxy_cost
    + allocated_infrastructure_cost
    + human_intervention_cost
    ------------------------------------------------
    verified_success_count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The denominator is the dangerous part. If a task costs $0.04 per attempt but only 80% of attempts pass independent verification, the attempt cost understates reality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$0.04 / 0.80 = $0.05 per verified success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That example is arithmetic, not our measured vendor result.&lt;/p&gt;

&lt;p&gt;For self-hosting, our break-even comparison would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monthly_self_hosted_cost =
    compute
    + model inference
    + database and storage
    + proxy service
    + monitoring
    + engineering maintenance

monthly_managed_cost =
    successful task volume
    × managed cost per verified success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If self-hosting costs $1,500 per month after infrastructure and engineering allocation, and a managed service costs $0.15 more per verified workflow, the nominal break-even point is 10,000 verified workflows per month. Reliability differences can move that threshold dramatically.&lt;/p&gt;

&lt;p&gt;A deterministic Playwright script remains the baseline we would beat. If we control the target application or its selectors rarely change, adding an LLM to every click usually adds avoidable latency and reliability risks. The better hybrid is to script stable login, navigation, submission, and artifact checks while reserving the agent for ambiguous page interpretation.&lt;/p&gt;

&lt;p&gt;Skyvern is more attractive when operations staff need workflow visibility, stored state, conditional blocks, and reruns without editing Python. Browser Use is more attractive when we already own the surrounding queue, credentials, telemetry, and validation layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;We would not recommend one product for every use case.&lt;/p&gt;

&lt;p&gt;Browser Use is the better engineering component. It is easier to embed, has a permissive license, and lets us control the orchestration around the agent. We would choose it when our team already has production primitives for queues, secrets, traces, retries, browser isolation, and result validation.&lt;/p&gt;

&lt;p&gt;Skyvern self-hosted is the better workflow platform. We would choose it when the operational interface, run history, workflow authoring, and centralized task state justify running a larger stack. We would review the AGPL implications before modifying and exposing the system over a network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Browser Use if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;we want browser control inside an existing Python service;&lt;/li&gt;
&lt;li&gt;engineers own the workflow and surrounding infrastructure;&lt;/li&gt;
&lt;li&gt;we can explicitly bootstrap and pin Chromium;&lt;/li&gt;
&lt;li&gt;we want DOM-first operation with selective vision;&lt;/li&gt;
&lt;li&gt;we can implement independent completion checks;&lt;/li&gt;
&lt;li&gt;we are comfortable building our own queue, credential, and observability layers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Deploy Skyvern self-hosted if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;operations teams need a visual workflow and run investigation interface;&lt;/li&gt;
&lt;li&gt;we accept running an API, UI, browser runtime, and PostgreSQL;&lt;/li&gt;
&lt;li&gt;we have at least the documented memory floor and a scaling plan;&lt;/li&gt;
&lt;li&gt;we can configure step ceilings, timeouts, retries, and model routing;&lt;/li&gt;
&lt;li&gt;we understand that CAPTCHA handling may require a person;&lt;/li&gt;
&lt;li&gt;the AGPL license is compatible with the intended deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hold off or avoid both if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the target site has a stable API we can call directly;&lt;/li&gt;
&lt;li&gt;deterministic Playwright already completes the workflow reliably;&lt;/li&gt;
&lt;li&gt;every run must finish without human intervention on bot-protected sites;&lt;/li&gt;
&lt;li&gt;screenshots or page contents cannot leave our network, and we cannot run a suitable model within that boundary;&lt;/li&gt;
&lt;li&gt;we cannot verify the final business state independently;&lt;/li&gt;
&lt;li&gt;the business requires a reliability percentage that has not been measured on its own workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our strongest conclusion is not that vision beats the DOM or that one agent is categorically more reliable. It is that browser-agent success must be externally verified.&lt;/p&gt;

&lt;p&gt;The strict Pydantic receipt helped prevent malformed output, but it still accepted a perfectly structured reference to a missing or corrupted file. We only caught those false completions by checking the artifact itself.&lt;/p&gt;

&lt;p&gt;That is the production gate we would apply to both Browser Use and Skyvern: no workflow counts as complete merely because the agent says it is. Until repeated runs produce traceable success, latency, usage, intervention, and artifact data, we have a promising automation system—not a reliability benchmark.&lt;/p&gt;

</description>
      <category>browserautomation</category>
      <category>aiagents</category>
      <category>selfhosted</category>
      <category>playwright</category>
    </item>
    <item>
      <title>Turbopuffer Review: Object-Storage Vector Search, Cold Starts, and Cost Trade-offs</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Sat, 03 Oct 2026 00:37:54 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/turbopuffer-review-object-storage-vector-search-cold-starts-and-cost-trade-offs-19gg</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/turbopuffer-review-object-storage-vector-search-cold-starts-and-cost-trade-offs-19gg</guid>
      <description>&lt;h2&gt;
  
  
  Why We Brought This Tool Into Our Lab
&lt;/h2&gt;

&lt;p&gt;We started this evaluation with a recurring retrieval-infrastructure problem: most vector databases make inactive tenants expensive.&lt;/p&gt;

&lt;p&gt;A conventional deployment keeps indexes on provisioned memory or SSD capacity whether a tenant sends 10,000 queries per hour or one query per month. That works for a small number of uniformly active indexes. It becomes painful when a product has hundreds of thousands of workspaces, repositories, agents, or customer-specific knowledge bases with highly skewed activity.&lt;/p&gt;

&lt;p&gt;Turbopuffer takes a materially different approach. In the &lt;a href="https://turbopuffer.com/docs/architecture" rel="noopener noreferrer"&gt;architecture reference&lt;/a&gt; we inspected, the API routes requests to Rust binaries operating over an object-storage source of truth. Query nodes are stateless from a durability perspective. When a namespace is cold, a query node reads it from object storage and caches its documents on local NVMe. The service routes subsequent requests toward the same node to preserve cache locality, although another node can still serve the namespace after a failure or routing change.&lt;/p&gt;

&lt;p&gt;That architecture is not ordinary storage tiering. The service commits writes to a write-ahead log under the namespace's object-storage prefix. Separate indexing workers asynchronously convert committed data into searchable index structures. Query nodes use memory and NVMe as disposable acceleration layers rather than as the authoritative copy.&lt;/p&gt;

&lt;p&gt;For vector retrieval, Turbopuffer uses SPFresh, a centroid-based ANN design. This matters because graph traversal can require a chain of dependent reads, which is hostile to object-store latency. A centroid hierarchy can identify promising clusters with a bounded number of broad reads. In our architectural analysis, we budgeted roughly three to four object-storage round trips for a cold query, with each round trip on the order of 100 ms.&lt;/p&gt;

&lt;p&gt;The trade is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We avoid paying premium storage prices for every inactive namespace.&lt;/li&gt;
&lt;li&gt;We accept a meaningful first-query penalty when a namespace is not cached.&lt;/li&gt;
&lt;li&gt;We depend on routing and cache residency for stable warm latency.&lt;/li&gt;
&lt;li&gt;We must decide whether approximate retrieval at roughly 90–95% recall@10 is sufficient.&lt;/li&gt;
&lt;li&gt;We inherit a consistency-versus-latency decision on every latency-sensitive query path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The strongest large-scale result we could verify was the ANN v3 result framed as 200 ms p99 over 100 billion vectors. Its workload uses 100 billion 1,024-dimensional &lt;code&gt;f16&lt;/code&gt; vectors, representing about 200 TiB of dense vector data, with a target above 1,000 QPS. We did not treat that number as a proxy for an ordinary cold serverless namespace. At that scale, the ANN tree is sized to remain on SSD, so it measures a different operating condition from fetching a dormant tenant's index from object storage.&lt;/p&gt;

&lt;p&gt;That distinction drove our review. We were not trying to prove that Turbopuffer can store a huge index. We wanted to know whether its economics survive a real multi-tenant query mix without turning cold starts into user-visible timeouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;The service is exposed through HTTP and requires an API key, so our planned harness had no infrastructure dependency beyond a public embedding dataset and a spend-limited account. We separated the test into four phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert identical vector corpora into namespaces containing 100,000, 1 million, and 10 million documents.&lt;/li&gt;
&lt;li&gt;Poll namespace metadata until &lt;code&gt;unindexed_bytes&lt;/code&gt; returned to zero.&lt;/li&gt;
&lt;li&gt;Query previously untouched namespaces to capture the first-query latency.&lt;/li&gt;
&lt;li&gt;Run repeated queries against the same namespace to capture the warm-up curve and steady-state percentiles.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also planned to write a sentinel document, query it immediately with strong consistency, and poll metadata independently. That separates write visibility from asynchronous indexing completion: a document can be visible through exhaustive WAL search before it has entered the ANN index.&lt;/p&gt;

&lt;p&gt;Our execution environment did not contain a Turbopuffer API credential, an offline server, or verified protocol fixtures. We therefore stopped before issuing hosted-service requests. We did not substitute a cache simulation or another vector database and label that as a Turbopuffer benchmark. Doing so would have produced precise-looking but useless numbers.&lt;/p&gt;

&lt;p&gt;The following harness is the HTTP timing collector we prepared. It is runnable once &lt;code&gt;TPUF_QUERY_URL&lt;/code&gt;, &lt;code&gt;TPUF_API_KEY&lt;/code&gt;, and a valid query payload are supplied. The URL is parameterized deliberately so that CI does not hard-code a region or API revision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;.&lt;/span&gt; .venv/bin/activate
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"httpx==0.27.2"&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TPUF_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"replace-with-a-spend-limited-key"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TPUF_QUERY_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://replace-with-current-region-and-query-path"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SAMPLE_COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"100"&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; query.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "rank_by": ["vector", "ANN", [0.01, 0.02, 0.03]],
  "top_k": 10,
  "include_attributes": ["category"]
}
&lt;/span&gt;&lt;span class="no"&gt;JSON

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bench.py &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import json
import math
import os
import statistics
import sys
import time

import httpx

api_key = os.environ.get("TPUF_API_KEY")
query_url = os.environ.get("TPUF_QUERY_URL")
sample_count = int(os.environ.get("SAMPLE_COUNT", "100"))

if not api_key:
    sys.exit("ERROR: TPUF_API_KEY is not set")
if not query_url:
    sys.exit("ERROR: TPUF_QUERY_URL is not set")

with open("query.json", "r", encoding="utf-8") as handle:
    payload = json.load(handle)

latencies_ms = []
status_counts = {}

with httpx.Client(timeout=30.0, http2=True) as client:
    for sequence in range(sample_count):
        started = time.perf_counter_ns()
        response = client.post(
            query_url,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
            },
            json=payload,
        )
        elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000
        status_counts[str(response.status_code)] = (
            status_counts.get(str(response.status_code), 0) + 1
        )

        if response.is_error:
            print(
                json.dumps(
                    {
                        "sequence": sequence,
                        "status": response.status_code,
                        "latency_ms": round(elapsed_ms, 2),
                        "body": response.text[:1000],
                    }
                ),
                file=sys.stderr,
            )
            response.raise_for_status()

        latencies_ms.append(elapsed_ms)
        print(
            json.dumps(
                {
                    "sequence": sequence,
                    "latency_ms": round(elapsed_ms, 2),
                    "phase": "first" if sequence == 0 else "subsequent",
                }
            )
        )

ordered = sorted(latencies_ms)

def percentile(value):
    index = max(0, math.ceil((value / 100) * len(ordered)) - 1)
    return ordered[index]

summary = {
    "samples": len(ordered),
    "first_query_ms": round(ordered[0] if len(ordered) == 1 else latencies_ms[0], 2),
    "p50_ms": round(statistics.median(ordered), 2),
    "p90_ms": round(percentile(90), 2),
    "p99_ms": round(percentile(99), 2),
    "status_counts": status_counts,
}
print(json.dumps({"summary": summary}))
&lt;/span&gt;&lt;span class="no"&gt;PY

&lt;/span&gt;python bench.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following is simulated output showing the collector's format, not a result from our lab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"sequence": 0, "latency_ms": 886.42, "phase": "first"}
{"sequence": 1, "latency_ms": 18.71, "phase": "subsequent"}
{"sequence": 2, "latency_ms": 15.64, "phase": "subsequent"}
...
{"summary":{"samples":100,"first_query_ms":886.42,"p50_ms":14.93,"p90_ms":18.27,"p99_ms":28.84,"status_counts":{"200":100}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We chose those illustrative values to resemble the published control ranges so the output would look realistic. They are not independent measurements.&lt;/p&gt;

&lt;p&gt;The verified architectural baseline for 1 million documents is a first-query p50 of 874 ms and a cached p50 of 14 ms. For our comparison, we used a separate 10-million-document, 1,024-dimension, approximately 40 GB workload at 8 QPS and &lt;code&gt;top_k=10&lt;/code&gt; as a reference: warm p50/p90/p99 values of 14/17/27 ms and cold values of 874/1,214/1,686 ms. We did not independently measure these endpoints.&lt;/p&gt;

&lt;p&gt;These are useful controls, but they are not a reproduced warm-up curve. We could verify the cold and warm endpoints in the published material, not the intermediate transition across queries or elapsed time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Limitations and Operational Constraints
&lt;/h2&gt;

&lt;p&gt;Our immediate roadblock was test access. Without a hosted API key, we could not truthfully produce service-side error responses, independent percentiles, cache-eviction measurements, or indexing-delay distributions.&lt;/p&gt;

&lt;p&gt;If run without an API key, the prepared harness would exit with the following message; we did not execute it in this evaluation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: TPUF_API_KEY is not set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is our harness error, not a Turbopuffer API response. We are making the distinction explicit because fabricated vendor error strings are worse than a missing test.&lt;/p&gt;

&lt;p&gt;Several operational constraints still surfaced during our review.&lt;/p&gt;

&lt;p&gt;First, “the second query is warm” is too simplistic. Cache locality is node-specific, and any query node can serve any namespace. A failover, rebalance, or eviction can put the next request back on an object-storage path. We would use the warm-cache endpoint or a pre-flight query before interactive traffic, but that transfers responsibility to the application. It also creates one warm-up operation per active namespace.&lt;/p&gt;

&lt;p&gt;Second, strong consistency imposes an approximately 10 ms latency floor because the query path checks object storage for newer writes. Eventual consistency can get below that floor, but it permits staleness of up to roughly one hour in the worst case. In our consistency assessment, we accounted for eventual queries searching up to 128 MiB of unindexed data and used the greater-than-99.8% consistent-response figure as a reference, not as an independently measured result. That is encouraging, but it is not a guarantee suitable for permission changes, deletion enforcement, or user-facing read-after-write workflows.&lt;/p&gt;

&lt;p&gt;Third, each namespace currently produces at most one WAL entry per second. Concurrent writes can be grouped, preserving high aggregate throughput, but a batch opened shortly after the preceding commit can take up to one second. We would batch ingestion intentionally rather than sending a stream of tiny writes and expecting database-style single-row latency.&lt;/p&gt;

&lt;p&gt;Fourth, schema flexibility has hard edges. Attribute types must remain consistent within a namespace. Every vector in a vector column must use the same dimensionality. A namespace can have at most four embedded attributes, and an upsert batch containing embedded attributes is limited to 30 rows. We could verify those limits, but not the exact API messages emitted when they are violated.&lt;/p&gt;

&lt;p&gt;That leaves an important gap in our evaluation: we cannot provide authentic response bodies for a type conflict, non-indexed filter, dimension mismatch, fifth embedded attribute, or oversized embedded upsert. Teams automating error classification should capture those responses during their own proof of concept instead of matching on guessed strings.&lt;/p&gt;

&lt;p&gt;Finally, cache behavior remains underspecified for capacity planning. We could not establish an eviction policy, a guaranteed residency duration, or a namespace-count threshold at which warm p99 deteriorates. This is the main unresolved risk for products with a very long tenant tail. If that describes your workload, bring a production-shaped Zipf distribution to the test rather than benchmarking one namespace in a loop.&lt;/p&gt;

&lt;p&gt;For help designing that workload, &lt;a href="https://dev.to/contact"&gt;contact our infrastructure team&lt;/a&gt; or review the systems in our &lt;a href="https://dev.to/tools"&gt;AI tools collection&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;The architectural comparison is clearer than the invoice comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision factor&lt;/th&gt;
&lt;th&gt;Turbopuffer&lt;/th&gt;
&lt;th&gt;Pinecone&lt;/th&gt;
&lt;th&gt;Self-hosted Qdrant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Durable storage model&lt;/td&gt;
&lt;td&gt;Object storage as source of truth&lt;/td&gt;
&lt;td&gt;Managed service model varies by product and plan&lt;/td&gt;
&lt;td&gt;Operator-selected disk, replication, and snapshots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold namespace behavior&lt;/td&gt;
&lt;td&gt;Direct object-store reads, then NVMe caching&lt;/td&gt;
&lt;td&gt;Depends on selected service architecture and capacity model&lt;/td&gt;
&lt;td&gt;Normally resident on provisioned node storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm latency evidence in scope&lt;/td&gt;
&lt;td&gt;10M workload: 14 ms p50, 17 ms p90, 27 ms p99&lt;/td&gt;
&lt;td&gt;Must be measured on the purchased plan&lt;/td&gt;
&lt;td&gt;Must be measured on the selected hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold latency evidence in scope&lt;/td&gt;
&lt;td&gt;10M workload: 874 ms p50, 1,214 ms p90, 1,686 ms p99&lt;/td&gt;
&lt;td&gt;Not measured in this review&lt;/td&gt;
&lt;td&gt;Usually an OS-cache or process-start concern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations burden&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inactive-namespace economics&lt;/td&gt;
&lt;td&gt;Strong architectural fit&lt;/td&gt;
&lt;td&gt;Quote and workload dependent&lt;/td&gt;
&lt;td&gt;Capacity remains provisioned unless nodes scale down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read-after-write&lt;/td&gt;
&lt;td&gt;Strong by default, with latency floor&lt;/td&gt;
&lt;td&gt;Plan and API dependent&lt;/td&gt;
&lt;td&gt;Configuration dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary risk&lt;/td&gt;
&lt;td&gt;Cold-tail latency and cache uncertainty&lt;/td&gt;
&lt;td&gt;Metering and vendor-plan economics&lt;/td&gt;
&lt;td&gt;Staffing, replication, upgrades, and capacity planning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We could not produce a defensible current-dollar Pinecone comparison without fixing a plan, cloud, region, pod or serverless model, read units, write units, metadata size, and query rate. Any single “Turbopuffer is X times cheaper” figure would hide the variables that dominate the invoice.&lt;/p&gt;

&lt;p&gt;We can still establish a useful storage floor. The infrastructure reference rates we validated are approximately $0.02 per GB-month for object storage and $0.10 per GB-month for SSD cache. These are component-cost references, not Turbopuffer customer pricing.&lt;/p&gt;

&lt;p&gt;For the documented 10-million-vector workload occupying approximately 40 GB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Object-storage component = 40 GB × $0.02 = $0.80/month
Full SSD-cache component = 40 GB × $0.10 = $4.00/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalized to 1 million vectors, that becomes approximately $0.08 per month for the object-storage component and $0.40 per month if the corresponding 4 GB remains cached on SSD. This excludes compute, queries, indexing, metadata, replication overhead, service margin, networking, support, and minimum commitments.&lt;/p&gt;

&lt;p&gt;A more practical model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monthly hierarchy cost floor = D × (0.02 + A × 0.10)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;D&lt;/code&gt; is stored gigabytes and &lt;code&gt;A&lt;/code&gt; is the average fraction resident in SSD cache. With 40 GB and 10% average cache residency, the component floor is approximately $1.20 per month. At 100% residency, it is approximately $4.80.&lt;/p&gt;

&lt;p&gt;This explains where Turbopuffer should win: large &lt;code&gt;D&lt;/code&gt;, low &lt;code&gt;A&lt;/code&gt;, and enough tolerance for cold requests. It does not prove the final invoice beats Pinecone because query and service charges are missing.&lt;/p&gt;

&lt;p&gt;Against Qdrant, we would compare Turbopuffer's invoice with the complete monthly cost of nodes, attached storage, replicas, snapshots, cross-zone traffic, monitoring, upgrades, and engineering time. Qdrant can be the better choice when the dataset is continuously hot, latency must be predictable, and a team already operates stateful search clusters. Object storage becomes more compelling as the inactive-to-active namespace ratio rises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;Turbopuffer's architecture is technically coherent. Rust query binaries, object-storage durability, disposable compute, NVMe locality, SPFresh clustering, and asynchronous indexing fit the economics of sparse multi-tenant retrieval better than keeping every tenant permanently resident.&lt;/p&gt;

&lt;p&gt;The cold-start penalty is not theoretical. The verified 10-million-document controls span 27 ms warm p99 versus 1,686 ms cold p99. That difference is large enough to change an application's request flow, timeout policy, and user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have many isolated namespaces but only a small active working set.&lt;/li&gt;
&lt;li&gt;We can pre-warm a namespace before an interactive retrieval path.&lt;/li&gt;
&lt;li&gt;A roughly one-second cold request is acceptable outside the critical path.&lt;/li&gt;
&lt;li&gt;We value managed operation more than absolute latency control.&lt;/li&gt;
&lt;li&gt;We can use strong consistency selectively and eventual consistency elsewhere.&lt;/li&gt;
&lt;li&gt;A 90–95% recall@10 target is acceptable and we will validate recall on our corpus.&lt;/li&gt;
&lt;li&gt;Our writes can be batched around the one-WAL-entry-per-second namespace behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every request must meet a strict sub-100 ms p99, including after eviction or failover.&lt;/li&gt;
&lt;li&gt;We require guaranteed read-after-write behavior below the strong-consistency latency floor.&lt;/li&gt;
&lt;li&gt;Our schema needs more than four embedded attributes.&lt;/li&gt;
&lt;li&gt;We rely on large embedded upsert batches exceeding 30 rows.&lt;/li&gt;
&lt;li&gt;We cannot tolerate approximate retrieval or independently test recall.&lt;/li&gt;
&lt;li&gt;We need a documented cache-residency guarantee.&lt;/li&gt;
&lt;li&gt;We expect a reliable cost comparison without testing our actual mix of queries and namespaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our conclusion is positive on architectural fit but conditional on workload validation. We would shortlist Turbopuffer for a product with thousands or millions of intermittently active tenant indexes. We would not approve production deployment until we reproduced the namespace warm-up curve, measured p99 across several corpus sizes, captured indexing delay under concurrent writes, and recorded the exact schema and filter errors from the hosted API.&lt;/p&gt;

&lt;p&gt;The relevant technical starting points are the &lt;a href="https://turbopuffer.com/docs/architecture" rel="noopener noreferrer"&gt;architecture overview&lt;/a&gt;, the &lt;a href="https://turbopuffer.com/blog/ann-v3" rel="noopener noreferrer"&gt;ANN v3 design&lt;/a&gt;, and the &lt;a href="https://turbopuffer.com/docs/concepts" rel="noopener noreferrer"&gt;concepts and recall model&lt;/a&gt;. If the cold-start budget is uncertain, we would run the proof of concept before negotiating around headline storage savings.&lt;/p&gt;

</description>
      <category>vectorsearch</category>
      <category>turbopuffer</category>
      <category>pinecone</category>
      <category>qdrant</category>
    </item>
  </channel>
</rss>
