<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Ankush Singh on Medium]]></title>
        <description><![CDATA[Stories by Ankush Singh on Medium]]></description>
        <link>https://medium.com/@ankushkun?source=rss-2e453df3b6b5------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*VHAgI2qCv7leu0Q8IH3qSA.png</url>
            <title>Stories by Ankush Singh on Medium</title>
            <link>https://medium.com/@ankushkun?source=rss-2e453df3b6b5------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 11:06:23 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@ankushkun/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Cloudflares Clef beat Jev on its own turf]]></title>
            <link>https://ankushkun.medium.com/cloudflares-clef-beat-jev-on-its-own-turf-d230b3acc633?source=rss-2e453df3b6b5------2</link>
            <guid isPermaLink="false">https://medium.com/p/d230b3acc633</guid>
            <category><![CDATA[cloudflare]]></category>
            <category><![CDATA[clef]]></category>
            <category><![CDATA[decision-model]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[jevs]]></category>
            <dc:creator><![CDATA[Ankush Singh]]></dc:creator>
            <pubDate>Sat, 10 Oct 2026 14:25:45 GMT</pubDate>
            <atom:updated>2026-10-10T14:25:45.941Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mqc2nF0-s05NZCuilWgClA.png" /></figure><p>When Cloudflare released Clef on October 1, its launch post carried a table of ten public benchmarks, seven of them led by a Clef model. The Hacker News thread passed 600 points in a day, and one of the first replies was six words long: “Public benchmarks are easy to cheat” (<a href="https://traictory.com/news/2026-10-03-cloudflare-clef-decision-models">Traictory</a>, <a href="https://news.ycombinator.com/item?id=49923692">Hacker News</a>).</p><p>Fair. So we ran the test nobody could have trained for. Construct’s agent has about 40 small decisions written for a decision model, and every one of them was written around Jev: the prompts, the questions, and the thresholds, all built against Jev over the last two weeks. That is Jev’s home turf. We pointed the same requests at Clef and Clef-flash without changing a line, and scored all three against labelled cases.</p><p><strong>Clef is Cloudflare’s open-weight decision model, a drop-in alternative to TypeSafe’s Jev. On 117 labelled cases across five of our production decisions, Clef reached the right outcome on 92.3% of calls using thresholds set for Jev, against 88.6% for Jev and 82.1% for Clef-flash. Both Clef models returned the identical probability on every repeated call, and Jev did not.</strong></p><p>We build <a href="https://construct.computer">Construct</a>, an AI employee with its own computer. Every number below comes from our own runs on <strong>October 4, 2026</strong> (UTC): 117 cases, three calls each, three models, 1,053 calls in the main run, plus about 800 more in follow-up controls. Facts about the models themselves come from Cloudflare’s and TypeSafe’s documentation, checked the same day. The full method, confidence intervals and per-case results are in the paper: <a href="https://construct.computer/blog/clef-vs-jev-benchmark/compatible-apis-incompatible-thresholds.pdf">Compatible APIs, Incompatible Thresholds: A Drop-In Replacement Study of Clef and Jev on Decision Specifications from a Production Agent</a> (PDF, 13 pages).</p><ul><li><strong>Accuracy, drop-in</strong>: Clef 92.3%, Jev 88.6%, Clef-flash 82.1%, all on thresholds set for Jev. The gap between Clef and Jev is not statistically significant.</li><li><strong>Accuracy, own thresholds</strong>: Clef 93.2%, Jev 90.9%, Clef-flash 90.6%, with each threshold chosen on held-out cases.</li><li><strong>Repeatability</strong>: Clef and Clef-flash gave the same probability on all 351 calls each. The probability Jev’s decisions act on moved by up to 0.10, and 2 cases changed verdict.</li><li><strong>Code changed to swap models</strong>: one string.</li><li><strong>Failed calls</strong>: 0 of 1,053.</li><li><strong>Cost</strong>: $23 per million decisions on Jev, $35 on Clef-flash, $93 on Clef. The main run cost about five cents.</li></ul><h4>What are Clef and Clef-flash?</h4><p>Clef and Clef-flash are decision models from Cloudflare: you send a state and a set of typed questions, and they return a probability for each allowed answer instead of generating text. Clef is a 27 billion parameter model built on Qwen3.8, and Clef-flash is a 9 billion parameter model built on Qwen3.5. Both are released under Apache 2.0 with weights on Hugging Face, both run on Workers AI, and both accept the same request format as Jev (<a href="https://blog.cloudflare.com/clef-decision-models/">Cloudflare</a>).</p><p>If decision models are new to you: they are the fast, cheap judgment calls around a language model. Is this the same person? Does this email need a reply? Should this tool call wait for a human? TypeSafe calls the category System One models, after Kahneman’s fast, intuitive System 1. Our <a href="https://construct.computer/blog/jev-ai-agents/?utm_source=syndication&amp;utm_medium=syndication&amp;utm_campaign=clef-vs-jev-benchmark">first post on Jev</a> explains the idea and how we put one inside an AI agent’s memory.</p><p><strong>Jev, Clef and Clef-flash compared, October 2026</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S3TiuDoXZ6lZXJFPA3T3Wg.png" /><figcaption>A side-by-side comparison of the three classifier models available on Workers AI: TypeSafe AI’s Jev 1.13 and Cloudflare’s Clef and Clef-flash. All three handle the same question types (Choice, Score and Noul) and share a 64k token context window, and none of them charge for output tokens. The differences show up in openness and input support. Jev is closed and doesn’t publish its size, while both Clef models ship open weights under Apache 2.0, at 27B and 9B parameters respectively, and can also take images alongside text and JSON. On price, Jev is the cheapest at $0.042 per million input tokens, with Clef-flash at $0.09 and the full Clef model at $0.24. The last row lists the model ID you’d use to call each one from Workers AI.</figcaption></figure><p>Two details in that table matter later. Clef reads images, which Jev does not today. And the prices are per input token, so the model that counts fewer tokens for the same request closes some of the gap.</p><h4>How we benchmarked Clef against Jev</h4><p>We did not write a new test for Clef. We took five decisions that already exist in Construct’s code and ran each one’s real request builder and real interpreter, the same functions production calls.</p><p><strong>The five production decisions in the benchmark</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q6f9xJG2e3Juu_OOgTEhmQ.png" /><figcaption>The five decisions the agent currently hands off to a classifier, along with the question each one answers, how many test cases we’ve built for it, and the confidence threshold it runs at today. Entity match checks whether a newly mentioned person, project or company is someone the agent already knows, and has the largest test set at 30 cases. Email triage sorts incoming mail into newsletters, receipts, spam, requests or things to keep. The memory extract gate decides whether a conversation contains anything worth saving, and it runs at a deliberately low threshold of 0.05 so that very little slips past it. Notification priority judges whether an automation notice is worth interrupting the user with a toast, and tool-call risk flags tool calls that should pause for the user’s approval before running.</figcaption></figure><p>Entity match is live in production. The other four are built and switched off, waiting on shadow data. Their thresholds were set by judgment with Jev in mind and have never been fitted to data, for Jev or anything else.</p><p>Swapping the model was the least interesting part of the work. This is the whole change:</p><pre>// Before<br>await env.AI.run(&quot;typesafe/jev&quot;, { state, questions }, options);<br><br>// After<br>await env.AI.run(&quot;@cf/cloudflare/clef&quot;, { state, questions }, options);</pre><p>Cloudflare says Clef is “fully Jev-API compatible”. On our requests that held: Choice, Score and Noul questions, JSON state, and the response shape all worked unchanged, and all 702 Clef calls returned valid answers.</p><p>The rest of the method, briefly:</p><ul><li><strong>Cases</strong>: 117, all synthetic, weighted toward the edges of each decision. We drafted and labelled them with an AI assistant (Claude) before any of the three models saw them. No label came from Jev or Clef, and the labels have not yet been independently reviewed by a second person.</li><li><strong>Runs</strong>: each case three times per model, one call at a time, models interleaved so they shared the same network conditions, with the gateway cache skipped.</li><li><strong>Scoring</strong>: a call is correct when the production interpreter reaches the labelled outcome at the production threshold. We also report each model’s best threshold, found on the same cases, which flatters all three equally.</li><li><strong>Route</strong>: Cloudflare’s REST API through our AI Gateway, from a laptop. Fine for accuracy. Wrong for latency, which gets its own section.</li></ul><h4>Clef vs Jev accuracy: the results</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qp4rppiDupYaoi5KP4lKxA.jpeg" /><figcaption>Accuracy for each of the three models across all five production decisions, measured as the share of 351 calls that reached the correct outcome (117 synthetic cases run three times per model). The left group shows what happens if you swap a model straight into the current setup without retuning, keeping the thresholds that were originally set for Jev. In that drop-in case Clef scores 92.3% and Jev scores 88.6%, although Clef’s 3.7-point lead is small enough to fall within the noise of a 117-case test set. Clef-flash struggles most here at 82.1%, which suggests its confidence scores sit on a different scale from Jev’s rather than that it judges the cases worse. The right group lets each model use whichever threshold works best for it, and every model improves: Jev rises to 93.7%, Clef to 96.6% and Clef-flash to 94.9%. Clef-flash gains the most from retuning, close to 13 points, and ends up ahead of Jev. These right-hand numbers are an in-sample upper bound, since the thresholds were picked on the same cases they’re scored against, so real-world accuracy after retuning will likely land a little lower. Note that the vertical axis starts at 50%, which makes the gaps look larger than they are. Data comes from Construct’s bake-off on 4 October 2026.</figcaption></figure><p><strong>Overall accuracy across five production decisions, 351 calls per model. Left: the thresholds configured today, set for Jev. Right: each model’s own best threshold, found in-sample.</strong></p><p>With no retuning at all, Clef had the highest score of the three, and the highest or equal-highest on four of the five decisions, on thresholds that were set for a different model.</p><p><strong>Accuracy by decision at the production threshold, with accuracy at each model’s own best threshold in brackets</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aEE1aH7H_jkaD2NohYgPqA.png" /><figcaption>The same bake-off broken down by decision, showing where each model wins and loses. The main figure in each cell is accuracy at the thresholds originally set for Jev, and the figure in parentheses is accuracy once that model uses its own best threshold. Bold marks the top score in each row at Jev’s thresholds. Clef’s overall lead comes mostly from entity match, where it scores 86.7% against Jev’s 72.2%, and from notification priority, where it gets every case right. Jev holds its own on email triage at 93.3% and ties Clef on tool-call risk and the memory extract gate. Clef-flash trails on almost every decision at Jev’s thresholds, with its weakest showing on notification priority at 75.0%, but retuning lifts it to 100% on three of the five decisions, including tool-call risk, where it ends up ahead of both other models. Entity match is the hardest decision for every model even after tuning, which makes it the most likely place to look for further gains. As with the overall chart, the parenthesised figures are in-sample, and each decision has only 15 to 30 cases, so a single case can move a score by three to seven points.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Y1RgD0ietpebxW7vyKJ1TQ.png" /><figcaption>Drop-in accuracy for each model on each of the five decisions, using the thresholds originally configured for Jev, which are listed under each decision alongside its case count. This is the chart that shows where the overall numbers come from. Entity match is the clearest win for Clef, which reaches 87% while Jev manages 72%, and it’s also the one decision where Clef-flash beats Jev at 80%. Email triage tips the other way, with Jev on top at 93% and Clef-flash falling to 73%. The memory extract gate is close to solved for everyone, with Jev and Clef both at 100% and Clef-flash at 96%. Notification priority shows the biggest spread on the chart, since Clef gets every case right, Jev reaches 96%, and Clef-flash drops to 75%. Tool-call risk is a tie between Jev and Clef at 88%, with Clef-flash a few points behind at 83%. Read together, the bars show that no single model wins every decision, and that Clef-flash’s weak overall score comes mainly from two decisions where Jev’s thresholds suit it badly. Each decision has between 15 and 30 cases, so one case can shift a bar by roughly three to seven points, and the vertical axis starts at 50%, which makes the gaps look wider than they are. Data comes from Construct’s bake-off on 4 October 2026.</figcaption></figure><p><strong>Accuracy per decision at today’s thresholds. Jev keeps email triage. Clef takes entity matching and notification priority.</strong></p><p>The honest size of this result: Clef leads Jev by 3.7 points on 117 cases, with a 95% interval of -1.1 to +8.8 points. That is not statistically significant, and we would not stretch it into “Clef beats Jev”. Three of the entity cases are ones our product resolves in code before any model is asked (identical names, and names that differ only in case). Jev did worst on exactly those, and without them the lead shrinks to 2.0 points (92.1% against 90.1%). What we can say is narrower and more useful. Cloudflare’s public numbers predicted Clef would be at least as good as Jev on decision tasks, and on private prompts it had never seen, written for its competitor, it was not measurably worse.</p><p>Jev deserves its due here too. It won email triage outright, it never made a wrong merge, and it was perfect on the extract gate. This is a close race between two good models.</p><h4>Is Clef deterministic?</h4><p>Yes, on everything we sent it. We called each model three times with the same request for all 117 cases. Clef returned the same probability, to four decimal places, on all 351 calls. So did Clef-flash. To rule out a cache, we sent the same requests again with changes that do not touch the model’s input (a unique query string, reformatted JSON, reordered keys): same answers, a cache miss on every response, and a new inference ID each time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Nhb9DSWAEsobsJ0X3b7JVw.png" /><figcaption>How consistent each model was when given exactly the same input three times in a row. Each bar shows the largest change, across all cases in a decision, in the probability that the decision acts on. Clef and Clef-flash returned identical probabilities on every repeat call for every decision, so their bars sit flat at zero. Jev’s scores moved between runs on all five decisions, by as little as 0.03 on the memory extract gate and as much as 0.10 on notification priority. Most of that drift didn’t matter in practice, since the score stayed on the same side of the threshold, but in two cases it was enough to flip Jev’s verdict from one run to the next. A swing of 0.10 is large next to thresholds like 0.85 for notifications or 0.80 for tool-call risk, which means a case sitting close to the line could get a toast or a confirmation prompt on one call and not on the next. For an agent that makes these calls quietly in the background, getting the same answer to the same question is a big part of why its behaviour feels predictable, and on this test the Clef models were fully repeatable while Jev was not. Data comes from Construct’s bake-off on 4 October 2026, with 117 synthetic cases run three times per model.</figcaption></figure><p><strong>Largest change in probability between three identical calls. Clef and Clef-flash sit at zero on every decision</strong>.</p><p>The probability each Jev decision acts on moved between identical calls by up to 0.10, and two cases changed verdict from one call to the next. Across every probability Jev returned, the largest move was 0.16, and 100 of the 117 cases came back with at least one number different. TypeSafe describes Jev as returning “similar answers for similar inputs”, which is a promise of consistency, not determinism, and a wobble of this size is consistent. But a decision layer acts when a probability crosses a line, so any wobble means a case near the line is decided by which call you happened to make.</p><p>Cloudflare’s post explains why Clef behaves differently: the decision step is non-autoregressive, with choices derived directly from the model’s internal representations and no text sampled along the way. We can confirm the practical result. For anyone who has debugged an agent that did something different the second time, this is the finding that matters most, more than a few points of accuracy.</p><h4>Jev’s scores moved under the same version name</h4><p>This one surprised us. On September 20 we calibrated our live entity matcher on Jev. Real matches scored 0.71 to 0.82, everything else scored 0.34 or less, and we put the merge threshold at 0.70, in the gap.</p><p>Two weeks later we asked the same three calibration probes again.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PC0Nj_OlTXkd43s70AsKew.png" /><figcaption>Jev’s probability for the correct match on the three probes our live entity matcher was calibrated on, measured two weeks apart while the model was served under the same version identifier, jev-1.13.0. The hollow circles are single calls made on 20 September, when all three real matches scored above the 0.70 merge bar, shown as the dashed line: 0.71 for “Dr. Priya Shah” against “Priya Shah”, 0.76 for “Priya Shah” against itself, and 0.82 for “atlas migration” against “Atlas Migration”. The filled dots and diamonds are means of 20 calls each on 4 October, one set through Workers AI and one set on TypeSafe’s own API with the version pinned, and the shaded band behind them covers the full range of those 40 calls. Every probe had moved well below the bar. The Dr. Priya Shah probe fell to 0.47 and 0.46, the identical-name probe fell to 0.42 and 0.43, and the Atlas probe fell to 0.60 and 0.61, and even the highest single October call, at 0.66, stayed under 0.70. The two routes land almost on top of each other, which points to a change in the model and rules out a quirk of how Workers AI serves it. Since the version string never changed, nothing would have warned us, and in production these matches would simply have stopped merging. This doesn’t mean Jev got worse across the board, only that a threshold calibrated on one day can quietly stop holding on another. The September baseline is one call per probe, so it’s less certain than the October means.</figcaption></figure><p><strong>Jev’s probability for the correct match on our three calibration probes: one call on September 20, and the mean of 20 calls on October 4 through Workers AI and on TypeSafe’s own API with the version pinned. All were served as jev-1.13.0.</strong></p><p>All three now fall under the bar. To be sure this was not a noisy afternoon, we asked each probe 20 more times: none of the 60 calls reached 0.70, and the highest was 0.66. Clef, asked the same 20 times, returned the identical number every time. The served version was `jev-1.13.0` both times, so the alarm we built to catch version changes stayed quiet. On this benchmark Jev still never merged two different people, which is the expensive mistake, but it made only 44% of the merges it should have. In production, a missed merge becomes a duplicate.</p><p>We do not know why the scores moved. Our first suspect was our own route: we call Jev through Cloudflare’s Workers AI, which cannot pin a version. So we repeated everything against TypeSafe’s own API with the version pinned to `jev-1.13.0`. Same result: none of 60 calls reached 0.70, and Jev’s answers still varied between identical calls on 104 of 117 cases. The route is not the cause. Three probes are still three probes, and our September baseline is one call each. We are not claiming Jev got worse in general. The lesson is about our own setup: a threshold is only as stable as the model under it, and a version string is not a guarantee. Open weights can change that, if you use them: a model you pin or host yourself cannot move under its thresholds. The hosted Clef alias we tested carries no version either, so we will re-ask these same probes on Clef in two weeks.</p><h4>Entity matching: where the three models differ most</h4><p>Entity matching is the decision where a mistake is permanent. A wrong merge fuses two real people in the agent’s memory, so precision matters more than recall. <a href="https://construct.computer/blog/ai-agent-memory/?utm_source=syndication&amp;utm_medium=syndication&amp;utm_campaign=clef-vs-jev-benchmark">AI agent memory</a> covers what that memory holds and how you can correct it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AW6i4Ckc1H2DPu4yxAu8KQ.png" /><figcaption>A closer look at entity matching, the decision where the models differed most, scored at the production merge bar of 0.70 on 15 pairs that should merge and 15 that should not. The left group is recall, the share of true matches each model actually merged, and the right group is precision, the share of merges that turned out to be correct. Precision matters most here, because a wrong merge folds two different people or projects into one record and can’t be undone. Jev never merged anything it shouldn’t have, scoring 100% on precision, but it got there by being far too cautious and merged only 44% of the real matches, which fits with the drift chart showing its scores for true matches falling below 0.70. Clef and Clef-flash both caught 80% of the real matches, but they paid for it differently. Clef’s merges were right 92% of the time, while Clef-flash’s were right only 80% of the time, so roughly one in five of its merges joined two things that should have stayed apart. The footnote points to an easy fix for Clef: raising its bar to 0.80 still gave it 10 of the 15 correct merges, well ahead of Jev, with no wrong merges at all on this set. With only 15 pairs on each side, a single case moves these numbers by several points, so the 0.80 result is a promising direction to test further and not yet a settled setting.</figcaption></figure><p><strong>Entity matching at the production threshold of 0.70: 15 pairs that should merge and 15 that should not.</strong></p><ul><li><strong>Clef</strong> caught the aliases this decision exists for: “Jose Garcia Marquez” against the accented spelling at 0.98, “Stripe, Inc.” against “Stripe” at 0.98, “I.B.M.” against “IBM” at 0.99. Its one wrong merge was “Acme” into a lone “Acme Corporation” at 0.78, a case reasonable people would argue about.</li><li><strong>At a 0.80 threshold, Clef made 10 of 15 correct merges and no wrong ones</strong>. That is our starting point for shadow mode, not a recommendation: it was picked on these same 30 cases, and “Acme” against “Acme Corporation” sits at 0.78, just under it.</li><li><strong>Jev is not worse at telling people apart</strong>. Drop its threshold to 0.50 and it makes 64% of the correct merges, still with no wrong ones, close to Clef’s 67% at 0.80. Its problem at 0.70 is that its scores moved, not that it lost the ability to rank the pairs.</li><li><strong>Clef-flash</strong> is quick to say yes. It matched “Acme Labs Europe” to “Acme Labs” at 0.76 and the bare concept “pricing” to “pricing model” at 0.89. For a merge, that is too eager. For a gate that only skips work, it is fine.</li><li><strong>All three</strong> refused a one-letter surname typo, “Mendelsohn” against “Mendelson”. Good. Those can be two people.</li></ul><h4>Thresholds do not transfer between decision models</h4><p>If you take one thing from this post into your own migration, take this. A threshold is a property of a model and a prompt together. Move the model and the threshold is wrong, even when the new model is better.</p><p>The three models are not far apart in ability. Asked to rank our cases from “should act” to “should not”, all three do it well: on the four decisions we could measure this way, the ranking score (AUROC) is 0.93 or higher for every model and a perfect 1.00 on two decisions. What differs is the number each model attaches to the same judgment.</p><p><strong>The most accurate threshold per model on two decisions, against the configured threshold</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*83ZazPqS-_LO_fzR8YoG-Q.png" /><figcaption>The two decisions where the configured threshold sat furthest from where the models actually work best, comparing the production bar with the threshold that gave each model its highest accuracy on the test set. On tool-call risk, all three models do best with a much lower bar than the 0.80 in production, ranging from 0.49 for Jev down to 0.22 for Clef. In practice that means the models were giving genuinely risky tool calls scores well below 0.80, so the current setting let some of them run without stopping to ask the user first, and lowering the bar would make the agent pause on those calls as it should. Notification priority tells a different story. Clef’s best threshold is exactly the configured 0.85, which is why it already scored 100% on that decision without any retuning, while Jev and Clef-flash both work best much lower, at 0.60 and 0.61. It’s notable that Jev’s own best sits so far from a threshold that was originally set for Jev, which fits the drift shown earlier, where Jev’s scores moved down under the same version name. These best values are picked in-sample on 24 cases per decision, so each one is better read as a point inside a range of thresholds that perform about equally well, not a precise setting to copy straight into production.</figcaption></figure><p>Clef-flash shows the effect best. On Jev’s thresholds it scored 82.1%. With thresholds chosen for it on held-out cases it scored 90.6%, level with Jev’s 90.9% on the same footing, and Clef stayed ahead at 93.2%. The 9B model was never bad at these decisions. It was being read with another model’s ruler.</p><p>The thresholds in the table were found on the same cases they are scored on, which is why the bracketed figures in the results table (96.6%, 94.9%, 93.7%) run higher than the held-out ones. Treat them as a direction and not as settings. The practical rule is simple: swap the model in shadow, log probabilities for a week, and set the threshold from that.</p><h4>How fast is Clef? What we could and could not measure</h4><p>Cloudflare reports a median of 209 ms for Clef and 38.8 ms for Clef-flash, against 524 ms for Jev. We are not going to confirm or dispute those numbers, because our harness was the wrong instrument.</p><p>We called all three models through Cloudflare’s REST API from a laptop. The fastest call we saw for any model was 340 ms, and for every model the floor sat between 340 and 384 ms. That floor is the path: authentication, the public API, the gateway, and the round trip. You cannot see a 38 ms model through a 340 ms window.</p><p><strong>Latency per call through the REST API from a laptop, 351 calls per model. Not a production measurement.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GBogGYJMDTerKbfEH0fQ9w.png" /><figcaption>How long each model took to answer, from the fastest call through the median to the 95th percentile, which is the time that all but the slowest one in twenty calls came in under. At their best the three models are close, with the Clef models edging ahead at around 340 to 350 ms against Jev’s 384 ms. The differences show up as you move toward the slow end. Jev is the most consistent of the three, since its typical call takes 454 ms and even its slow calls finish within 591 ms. Clef-flash matches Jev on the median at 449 ms but has a much longer tail, with its slow calls stretching past a second. Clef is the slowest overall, about 200 ms behind the others on a typical call and taking more than twice as long as Jev on its slow ones, at 1,262 ms. For most of these decisions that hardly matters, because email triage, memory extraction and entity matching all run in the background where nobody is waiting. It matters more for tool-call risk, which sits directly between the agent and the action it’s about to take, so an extra half-second on a slow call is something a user could actually feel. Clef’s accuracy and consistency gains come with a real but modest cost in speed, mostly at the tail.</figcaption></figure><p>For transparency, those are the raw numbers. On this path Clef-flash matched Jev at the median, Clef was slower, and both Clef models had longer tails, three days after launch. None of that tells you what a Worker sees when it calls the model through the AI binding, next to the GPU, which is how we call it in production. That measurement is next, and we will publish it here.</p><h4>What does Clef cost compared with Jev?</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-07UWPartjjhOFC_6PLtrw.png" /><figcaption>What each model would cost to make a million decisions, worked out by multiplying the average number of input tokens per call by each model’s list price per million input tokens. Output tokens are free on all three, so input is the whole bill. Jev is the cheapest at about $23 per million decisions, Clef-flash comes next at about $35, and Clef is the most expensive at about $93. The gap is smaller than the list prices alone would suggest. Clef costs 5.7 times as much as Jev per token, but its tokenizer counts the same request as 387 tokens where Jev counts 541, which is 28% fewer, so per call Clef works out to roughly four times Jev’s cost. Clef-flash shares Clef’s tokenizer and ends up only about one and a half times Jev’s cost. In absolute terms all three are cheap, since even Clef comes to less than a hundredth of a cent per decision, and moving from Jev to Clef adds about $70 for every million calls. Whether that’s worth paying depends on what it buys, which the earlier charts answer with higher accuracy, fewer wrong entity merges and identical answers on repeat calls, set against a slower tail on latency.</figcaption></figure><p><strong>Cost per million decisions at list price, using the mean input tokens each model counted for our requests.</strong></p><p>Clef’s list price is 5.7 times Jev’s, which is the first thing the Hacker News thread did arithmetic on. Two things soften that.</p><p>First, Clef counted 28% fewer tokens than Jev for the same request: 387 per call against 541. Per decision, the gap is about 4 times, not 5.7.</p><p>Second, look at the unit. A million Clef decisions cost $93. All 1,053 calls in this benchmark cost about five cents. An agent that makes a thousand decisions a day spends about nine cents a day on Clef and two on Jev, next to a language model bill many times that. If you run billions of decisions a day, price it carefully, and look at Clef-flash at $35 per million. For everyone else, accuracy and repeatability are worth more than the difference.</p><h4>Clef vs Jev: which decision model should you use?</h4><p><strong>Which decision model fits which job, based on our benchmark and each vendor’s documentation</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fHv8lxI5GIq6684SnblymA.png" /></figure><p>The broader field is moving fast. Besides Jev and Clef there are open decision models such as Laya, Kev and OpenDecider, and Cloudflare’s own table includes several of them. We have only tested the three in this post.</p><h4>What we are switching, and why Cloudflare makes it easy</h4><p>Construct already runs on Cloudflare: Workers, Durable Objects, AI Gateway, and Workers AI for the small models. <a href="https://construct.computer/blog/running-ai-agents-on-cloudflare-not-vms/?utm_source=syndication&amp;utm_medium=syndication&amp;utm_campaign=clef-vs-jev-benchmark">How our agents get computers we mostly do not pay for</a> describes that stack. So Clef is not a new vendor for us. It is a new model ID on a binding we already call.</p><ul><li><strong>Entity matching goes to Clef in shadow first, starting at 0.80.</strong> Jev keeps deciding while Clef’s answers are recorded beside it. The threshold we ship will come from that data, and we switch only if real traffic agrees with this benchmark.</li><li><strong>Background decisions default to Clef</strong>, each with its own threshold measured in shadow.</li><li><strong>Interactive decisions wait for the latency test</strong>. If Clef-flash is as fast from a Worker as Cloudflare reports, it is the natural fit for anything a person is waiting on.</li><li><strong>Clef-flash stays away from anything that merges or deletes</strong> until it has its own thresholds.</li></ul><p>There is a quieter benefit. In our Jev post we said that decisions which read your conversations or email “wait for the paperwork”, because sending that text to another provider is a privacy decision. A first-party Cloudflare model keeps that text on the platform that already runs the rest of Construct, which removes the main reason most of our decisions are still switched off.</p><p>We are also watching Cloudflare’s new fine-tuning work, which it calls Reinforcement Learning for Calibrated Decisions. Our decision layer already records the label, the probability and what the product did for every decision, with no user text. That is close to the dataset such a system wants.</p><p><strong>Read next</strong>: <a href="https://construct.computer/blog/jev-ai-agents/?utm_source=syndication&amp;utm_medium=syndication&amp;utm_campaign=clef-vs-jev-benchmark">We put Jev inside our AI employee</a></p><p><strong>How to try Clef</strong></p><ul><li><strong>On Workers AI</strong>: call `@cf/cloudflare/clef` or `@cf/cloudflare/clef-flash` through the AI binding or the REST API. The request is a `state` plus a map of `questions`, the same as Jev.</li><li><strong>Coming from Jev</strong>: change the model ID and keep everything else. Then re-measure every threshold.</li><li><strong>Self-hosted</strong>: the weights are at `Cloudflare/clef` and `Cloudflare/clef-flash` on Hugging Face under Apache 2.0.</li><li><strong>Read the probabilities, not the `confidence` field</strong>. It means different things on Clef and Jev, and on Jev it changed meaning between our September and October runs. Read `probabilities` and both models behave the same.</li><li><strong>Pace a REST benchmark</strong>. Cloudflare’s REST API throttled our token at roughly 90 calls a minute. The Workers binding is the route for real traffic.</li></ul><h3>Where Construct fits</h3><p>Construct is an AI employee with its own computer: a workspace filesystem, long-term memory, a schedule, email, and workflows you can read before you run them. Decision models make the small calls around the edges, such as whether a person your agent just read about is one it already knows. Planning and writing run on general-purpose language models.</p><p>Two boundaries, stated plainly. As of this post, Jev still makes the live entity-matching decision and Clef has only run in this benchmark; this page will say when that changes. And the other four decisions in this benchmark are built but switched off in production until shadow data supports them.</p><p>If decision models are new to you, start with <a href="https://construct.computer/blog/jev-ai-agents/?utm_source=syndication&amp;utm_medium=syndication&amp;utm_campaign=clef-vs-jev-benchmark">we put Jev inside our AI employee</a>. For why small, repeatable judgments matter so much in long tasks, read <a href="https://construct.computer/blog/agent-task-half-life/?utm_source=syndication&amp;utm_medium=syndication&amp;utm_campaign=clef-vs-jev-benchmark">your agent has a half-life</a>. And if the category itself is new, begin with <a href="https://construct.computer/blog/what-is-an-ai-employee/?utm_source=syndication&amp;utm_medium=syndication&amp;utm_campaign=clef-vs-jev-benchmark">what is an AI employee</a>.</p><p>Originally published at <a href="https://construct.computer/blog/clef-vs-jev-benchmark/?utm_source=syndication&amp;utm_medium=syndication&amp;utm_campaign=clef-vs-jev-benchmark">construct.computer</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d230b3acc633" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[All our Agents get computers, yet we pay for almost none]]></title>
            <link>https://ankushkun.medium.com/we-dont-pay-our-ai-agents-computer-bill-3203b54b478a?source=rss-2e453df3b6b5------2</link>
            <guid isPermaLink="false">https://medium.com/p/3203b54b478a</guid>
            <category><![CDATA[ai-workflow]]></category>
            <category><![CDATA[ai-agents-in-action]]></category>
            <category><![CDATA[ai-employee]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[cloudflare]]></category>
            <dc:creator><![CDATA[Ankush Singh]]></dc:creator>
            <pubDate>Mon, 03 Aug 2026 07:22:32 GMT</pubDate>
            <atom:updated>2026-08-03T07:22:32.352Z</atom:updated>
            <content:encoded><![CDATA[<p>Construct is a coworker agent with a real Linux computer per user. hol’ up we are not even paying for all of them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vuvIXGhVg__2hBj8dWSotQ.jpeg" /></figure><p>Construct is an AI employee. You hand it a job. It goes and does the job, without you having to babysit it.<br>To do that with agents like Hermes or OpenClaw, you spin up a VPS. Real CPU. Real disk. A full Linux machine that stays on between turns. That costs real money, and the moment more people sign up to poke around and never come back, <strong>it murders your infrastructure bill</strong>.</p><p><a href="https://construct.computer">&lt; Meet Construct &gt;</a></p><p>We give every Agent that same kind of computer.<br>Don’t get excited, we are not paying for all of them.</p><p>The computers work. When someone asks Construct to do something, the machine wakes up and does it. When the tab goes quiet, nothing stays on just to cosplay as useful. Idle boxes are a kink we refuse to subsidize.</p><p><em>We like edging</em>. So we put the whole stack on the <em>edge</em>. The agent loop in a Durable Object. Linux summoned for one tool call, then gone. Disk that is not a disk. The bill only finishes when somebody actually works.</p><p>This post is how we built that. Behaviour, constants, and tradeoffs are production.</p><h3>The bill that scaled with people who never came back</h3><p>The default way to run an AI agent is to hand it a Linux box and walk away.<br>Not stupid. Agents are trained on bash. Bash wants a filesystem and a process table. The lazy move is a machine that stays up between turns. Lazy is usually correct.</p><p><strong>Until the invoice arrives.</strong></p><p>We know that invoice because we signed it first. Construct’s original backend was a Bun and Elysia monolith on a VPS. SQLite on local disk. nginx out front. Roughly 1,300 lines of container management whose only job was handing every user their own box. It worked. Growing the product meant growing a fleet of real machines with real disks, billed through the silent hours where nobody typed a character.</p><p>That is the expensive version of “every user gets a computer”. Technically true. Financially a treadmill. Someone who signed up once and ghosted cost the same as a power user. The product could not scale if the bill scaled with accounts instead of work.</p><p>We killed it on 30 March 2026 with the commit title: “migrate backend from VPS monolith to Cloudflare Workers.” Everything in this post starts there.</p><p>Hono on Workers. Per-user WebSocket hub in a Durable Object with hibernation. Container object that archived to R2 on sleep and restored on wake. D1 for relational state. R2 for files. Frontend on Workers assets. Two days later we dragged the leftover container infrastructure into a legacy folder and wrote the plan we still run: the agent runs headlessly inside Durable Objects.</p><p>Containers and Sandboxes went GA on 13 April 2026. We moved onto the Sandbox SDK once it had the shape we wanted.</p><p>We did not see the future. We stared at a cost curve that scaled with signups instead of usage, bet on Durable Objects before there was a tidy product name for the thing, and watched the platform walk to the same answer. <a href="https://camelai.com/blog/our-coding-agent-runs-in-a-cloudflare-durable-object-not-a-vm">camelAI</a> hit the same wall and reached nearly the same conclusion independently. Validation, or a warning about people who write posts like this. Maybe both.</p><h3>Only half a computer deserves to be awake</h3><p>“The agent needs a computer” is two unrelated things taped together.</p><p><strong>Agent loop</strong>: transcript, tool routing, model calls, memory recall. This is how Construct thinks across a turn.</p><p><strong>Machine</strong>: the thing that runs <em>pdftotext</em>, converts a spreadsheet, compiles a project, unzips the archive somebody should not have uploaded. This is how Construct touches a real filesystem.</p><p>Only the second needs Linux.</p><p>The first needs durable state and a socket. Cloudflare sells exactly that. Split the two and the expensive half stops needing to be alive at all.</p><p>Everybody argues about which model to use. Almost nobody argues about which half of their stack is <em>getting paid to nap</em>. That is the whole post, in one sentence.</p><h3><em>Linux shows up, edges the job, and leaves 💦</em></h3><p>This is the section the title is about.<br>Each Construct agent gets one Durable Object, resolved by name. Sessions are rows inside it, not objects of their own. Temporary subagents run as delegated sessions on the same parent. Fan out a job , buy concurrency, not instances. That is how Construct can split a big ask without spinning up a fleet.</p><p>The transcript lives in that object’s SQLite. Two things, kept apart on purpose. One table is the append-only message log the user scrolls through. A separate record holds the compaction summary plus a watermark of how far it has already consumed. That second one is what reaches the model. Conflate them and you pay, per turn, to re-send an afternoon of conversation to a model that did not ask for it. People do this. At scale.</p><p>Hibernation is where the bill actually moves. We use the WebSocket hibernation API, stash per-connection state on the socket itself, and let the runtime answer keepalives without ever waking the object.</p><p>A tab open all afternoon is not a running process. It is a socket the runtime holds and a handful of SQLite rows. The object wakes when something happens. Goes back down when it does not. Held, not running. Your Construct session feels continuous. Our invoice does not.</p><p>Eviction can happen any moment, so every transcript row also archives to D1. Empty on start? Restore from the archive. Make death cheap and constant and it stops being an incident.</p><p>One annoying edge case: replayed reconnect events can include a turn-started with no matching completion, because the object got evicted mid-turn. Client reconnects, nothing is running , we send terminal idle status so nobody sits watching a spinner think about a thought that died twenty minutes ago.</p><p>When the agent calls the terminal tool, we resolve a sandbox:</p><pre>sandbox = getSandbox(SANDBOX_BINDING, instanceKeyFor(user, workspace), {<br> transport: rpc,<br> enableDefaultSession: false, # every exec is a fresh shell<br> sleepAfter: idleWindowFor(plan),<br>})</pre><p>Instance key scoped to user, or workspace when we know it. Not per agent. Not per session. Live containers track active humans, not agent count. Matters the moment somebody spawns twelve agents and goes to lunch.</p><p><em>sleepAfter</em> is the pivot. Ten minutes idle and the box is gone. We call that a feature. You can call it <em>edging</em>. Same config value. The idle window is the plan’s command ceiling in minutes, clamped by a global 300-second cap, plus a five minute buffer. That resolves to a ten minute sleep on every plan today. Pair with active CPU billing on Cloudflare Containers , charged for CPU consumed, not wall-clock uptime , and an idle sandbox stops being something you manage.</p><p>Difference between “we should build a reaper for idle machines” and “there is nothing to reap.” One is a roadmap item forever. The other is a config value.</p><p>Unglamorous consequence: every exec is a fresh shell. Working directory does not persist. Agents hate this personally. We wrote a hint into the failure path explaining that each terminal call starts clean, relative paths resolve against the scratch tree, and any directory change or exported variable from an earlier call is gone forever. Grief counselling in a string in an error branch.</p><p>Before each exec: kill leftover processes, then mount, then run. Kill after mount and you drop the local bucket’s inotify watch and can leave a stale read-only workspace behind. Learned that the hard way.</p><h3>She wrangler on my D1 till I R2 😳</h3><p>Construct’s durable workspace is an R2 bucket mounted into the container over s3fs. Explicit uid and gid so the non-root user can actually write.<br><em>She wrangler on my D1 till I R2,</em> The files <em>finish</em>. The machine does not have to.</p><p>This is the second half of the cost story, and the half people skip. Quotas of 100 MB, 1 GB, and 3 GB are quotas on bytes stored, not volumes kept spun up. When Construct saves a report, a spreadsheet, or a research brief, those files outlive the machine by construction. They were never on the machine. Nobody’s work sits on provisioned block storage waiting for their owner to remember the product exists.</p><p>Contrast again: provisioned volume waiting patiently versus bytes in R2 that do not care if the box is awake.</p><p>How we verify the mount matters more than the mount itself. Checking the directory exists is useless. Passes on a stale read-only mount every time. So we make the non-root user touch a probe file and delete it, with a ten second timeout. Exit code zero means the mount is actually writable.</p><p>Probe fails: unmount, remount, fix ownership, probe again, only then give up. Debounced to once a minute. Isolate-level mount cache key carries a version string we bump when mount policy changes, so a warm isolate can never silently skip a policy change.</p><p>Honest footnote: local development uses plain directory sync, not FUSE. Files land root-owned. We chown on mount. That local/prod divergence cost us an afternoon. It is why we trust a write probe over any status output that claims everything is fine.</p><h4>What living on the edge actually costs</h4><p>No sales pitch from here down. Living on the edge has a price and we pay it.</p><p>You cannot set `USER` in a Cloudflare Sandbox image. Control plane intercepts HTTPS and needs root. Privilege drop moves to the point of execution. Dom energy stays with the control plane. The agent bottoms out as `<em>runuser</em>`. Elevated commands stay root through a wrapper. Everything else is `<em>runuser -u &lt;agent user&gt;</em>`.</p><p>No sudo. No passwordless sudoers. Process and FD limits by the wrapper. Elevated access per command or until the container sleeps. Grants live inside the container, not Worker memory. Once the machine is disposable, the container is the correct place for short-lived trust. Permission that dies with the box is permission you never have to remember to revoke.</p><p>Memory and a registry catalog used to be separate Workers behind service bindings. We folded them into one. Never a latency optimisation. Service bindings are already efficient in-account transport. The cost was operational: two more deployments, more local processes, more domains, more generated binding contracts, releases that had to be choreographed. Microservice theatre for 41 KiB. We stopped. Nothing came.</p><p>Memory was about 41 KiB compressed. Registry under 1 KiB. Combined Worker about 1,002 KiB against a 2 MiB gate. 500 ms startup stays a cutover check because Wrangler dry runs do not report startup. We wrote down when to undo it: compressed bundle past 2 MiB, startup past 500 ms, own release cadence needed, or failures widen the API blast radius. Four triggers. Before anyone got attached.</p><p>Web app served by a Worker with static assets rather than Pages, because we needed custom domains to manage their own DNS and Pages OAuth could not.</p><p>Living on the edge means never assuming you have time. Construct’s memory recall runs on a budget. Whole recall: 800 ms. Inside that, remote embedding plus Vectorize: 250 ms. Those are configured budgets, not measured percentiles. We do not have deployed latency dashboards yet. Docs say so.</p><p>SQLite full-text search and graph traversal are the floor and always return, local to the Durable Object. Vectorize is best-effort on top. Vector path drags: slightly worse answer, not a hang. On one big warm box you would simply wait. Waiting is exactly what you cannot afford when nothing is warm by default. Warmth is a luxury. Edging is the default.</p><p>Embedding model was the one place with real measurements. 18-query set with close distractors, on Workers AI:</p><pre>|      Model           | recall@1  |  mrr  |<br>| - - - - - - - - - - -| - - - - - | - - - |<br>| qwen3-embedding-0.6b | 15/18     | 0.892 |<br>| bge-m3               | 14/18     | 0.889 |<br>| bge-large-en-v1.5    | 13/18     | 0.852 |<br>| embeddinggemma-300m  | 12/18     | 0.824 |<br>[man, medium should have more tabular data support]</pre><p>Qwen3 is trained for asymmetric retrieval. Tell it whether it holds a question or a stored record and ranking tightens. Query-side instruction moved mrr from 0.892 to 0.924 on the same set. Free accuracy for one string. Take it.</p><p>A 0.6b model also keeps this inside budget. Memory pipeline gate: USD 1 per 1,000 turns, excluding the primary chat model. Do not swap embedding models casually. Vectorize indexes are built at those exact dimensions. Different model means recreate and re-embed everything.</p><p>Three scars from the platform:</p><p><em>setTimeout</em> versus <em>setAlarm</em> has a boundary around ten seconds. Discord gateway reconnects with backoff. Anything beyond ten seconds goes on an alarm. Timers do not survive eviction. That object holds one WebSocket for every guild we serve. Split it when Discord reports more than one shard, somewhere around 2,500 guilds. Better to schedule that than discover it at 3am.</p><p>A fallback on the same backend as its primary is not a fallback. Same outage, different name. Compaction runs the same Gemma build on Workers AI and AI Studio. Failover costs the route. Every other slot crosses providers.</p><p>Alarms hand you a bare Durable Object id. `ctx.id.name` only exists when you reached the object through a named stub. Alarm-triggered instantiation does not. Ours addressed outbound messages. Queue rejected them. Repair alarm re-queued forever, at cost. Infinite unfinished business. Fix: persist the name on first use. Nothing warns you.</p><p>The container is real. It is not free. Image lands between 1.5 and 1.9 GB, which forces `standard-1`, because disk allocation is effectively the image size limit. LibreOffice needs 4 GiB or it gets OOM killed. Production configured for up to 100 concurrent instances. Command execution capped at 300 seconds on every plan today. That is a real product limit this architecture caused, not a feature. s3fs is not real POSIX. It will surprise you on a weekend. Fresh shells irritate agents trained on bash. We pay for that in prompt tokens.</p><p>Flat version: if your workload is one long-lived process per user that truly never idles, none of this helps you. Buy a VM. Simpler. Happier. Nobody will make edging jokes at you.</p><h4>What we run today</h4><p>Workers. Durable Objects with SQLite storage. Containers through the Sandbox SDK. D1. R2. Queues with dead letter queues. Vectorize. Workers AI. AI Gateway. Email Routing inbound with the send binding outbound. Rate limiting bindings. Cache API. Turnstile. Cron triggers. Static assets. Workers Logs. Agents SDK.</p><p>No KV. No Hyperdrive. No machines of our own. Just Construct users with computers that mostly sleep.</p><p><em>All our Agents get computers, we pay for almost none.<br>We like edging. So we put the whole stack on the edge.</em></p><p>That is how an AI employee can feel always-on without an always-on bill.</p><p>If you want the product side rather than the plumbing, <a href="https://construct.computer/blog/ai-employee/">what an AI employee actually does</a> covers the work it completes, <a href="https://construct.computer/blog/ai-agent-memory/">inspectable agent memory</a> covers what it remembers and how you correct it, and <a href="https://construct.computer/blog/construct-vs-diy/">building your own agent stack</a> is the honest version of what you would be operating instead.</p><p>Originally posted on <a href="https://construct.computer">construct.computer</a></p><p><a href="https://construct.computer/blog/running-ai-agents-on-cloudflare-not-vms/">All our Agents get computers, we pay for almost none</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3203b54b478a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Publish to NPM from Github actions using OIDC]]></title>
            <link>https://ankushkun.medium.com/publish-to-npm-from-github-actions-using-oidc-c7e4702f3b94?source=rss-2e453df3b6b5------2</link>
            <guid isPermaLink="false">https://medium.com/p/c7e4702f3b94</guid>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[npm]]></category>
            <category><![CDATA[oidc]]></category>
            <dc:creator><![CDATA[Ankush Singh]]></dc:creator>
            <pubDate>Sat, 20 Dec 2025 11:17:52 GMT</pubDate>
            <atom:updated>2025-12-20T11:17:52.042Z</atom:updated>
            <content:encoded><![CDATA[<p>I frequently create developer tools and publish node packages on npmjs, but recently classic tokens got deprecated, granular token are limited to 90 days, require 2FA and a bunch of changes were introduced to improve security around publishing packages. More about that on this <a href="https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/">github announcement</a>.</p><p>One of the improvements was the introduction of <a href="https://openid.net/developers/how-connect-works/">OpenID Connect</a> (OIDC), which allows developers to automate package publishing through github actions, without having to setup any kind of api key or access token, and today we are having a look at how you can implement it for your packages as well.</p><h3>Step 1: setup a node package (or skip if you already have one)</h3><p>As an example, we’ll create a fresh package which just logs hello world to console</p><p><strong>NOTE: this package is only meant as a demo for this blog. It is not recommended to create such packages for yourself and you should just setup OIDC on your existing packages :)</strong></p><pre>mkdir npm-oidc-demo &amp;&amp; cd npm-oidc-demo<br>npm init -y<br>touch index.js</pre><pre>export function helloWorld() {<br>    console.log(&quot;Hello World&quot;);<br>}</pre><p>Update your package.json to look like this:</p><pre>{<br>  &quot;name&quot;: &quot;npm-oidc-demo&quot;,<br>  &quot;version&quot;: &quot;1.0.1&quot;,<br>  &quot;description&quot;: &quot;A simple hello world package&quot;,<br>  &quot;main&quot;: &quot;index.js&quot;,<br>  &quot;type&quot;: &quot;module&quot;,<br>  &quot;exports&quot;: {<br>    &quot;.&quot;: &quot;./index.js&quot;<br>  },<br>  &quot;repository&quot;: {<br>    &quot;type&quot;: &quot;git&quot;,<br>    &quot;url&quot;: &quot;https://github.com/ankushKun/npm-oidc-demo&quot;<br>  },<br>  &quot;keywords&quot;: [<br>    &quot;npm&quot;,<br>    &quot;oidc&quot;<br>  ],<br>  &quot;author&quot;: &quot;&quot;,<br>  &quot;license&quot;: &quot;ISC&quot;<br>}</pre><p>an already published package is required to use OIDC<strong> </strong>(might change in the future, who knows…)</p><p>Run <strong><em>npm login</em></strong> and follow the steps to link the npm cli with your npmjs account, when successful, run <strong><em>npm whoami</em></strong><em> </em>and it should print your username.</p><p>and we are ready for publishing, just run <strong><em>npm publish</em></strong></p><p><strong>NOTE: If you ignored the previous note and still followed this guide to publish the demo package yourself, you might need to change your package name as I have already used “npm-oidc-demo”</strong></p><pre>$ npm whoami<br>ankushkun<br><br>$ npm publish<br>npm notice<br>npm notice 📦  npm-oidc-demo@1.0.0<br>npm notice Tarball Contents<br>npm notice 428B README.md<br>npm notice 64B index.js<br>npm notice 239B package.json<br>npm notice Tarball Details<br>npm notice name: npm-oidc-demo<br>npm notice version: 1.0.0<br>npm notice filename: npm-oidc-demo-1.0.0.tgz<br>npm notice package size: 602 B<br>npm notice unpacked size: 731 B<br>npm notice shasum: 338ccdfe6ad57442bef858098fc247680fb23c9e<br>npm notice integrity: sha512-XAKUT4avwrfcV[...]lEjW6hNUMJieQ==<br>npm notice total files: 3<br>npm notice<br>npm notice Publishing to https://registry.npmjs.org/ with tag latest and default access<br>Authenticate your account at:<br>https://www.npmjs.com/auth/cli/5f8fa35b-c07a-4af8-9851-831f191efc3e<br>Press ENTER to open in the browser...<br><br>+ npm-oidc-demo@1.0.0</pre><p>The package is now available at <a href="https://www.npmjs.com/package/npm-oidc-demo">https://www.npmjs.com/package/npm-oidc-demo</a></p><h3>Step 2: Update OIDC details on package settings</h3><p>Head over to the settings tab on our freshly published package and select GitHub Actions as the trusted publisher</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s0fSghe8DIxQPw6K-OhozA.png" /><figcaption>npmjs package settings tab</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4Fc2oJ3_Ijr8uLga0P4v_A.png" /><figcaption>GitHub actions trusted publisher settings</figcaption></figure><p>I have already created a github repo at <a href="https://github.com/ankushKun/npm-oidc-demo">ankushKun/npm-oidc-demo</a>, now we just need to create a workflow file. Put this inside <strong><em>.github/workflows/publish.yml</em></strong></p><pre>name: Publish to npm<br><br>on:<br>  release:<br>    types: [published]<br>  workflow_dispatch:<br><br>jobs:<br>  publish:<br>    runs-on: ubuntu-latest<br>    permissions:<br>      contents: read<br>      id-token: write<br>    steps:<br>      - uses: actions/checkout@v4<br><br>      - uses: actions/setup-node@v4<br>        with:<br>          node-version: &quot;20.x&quot;<br>          registry-url: &quot;https://registry.npmjs.org&quot;<br><br>      - name: Update npm<br>        run: npm install -g npm@latest<br><br>      - name: Publish to npm<br>        run: npm publish</pre><p>Push the yml file to the repo and update the OIDC details on the package settings. I have kept the environment name empty because I don’t have any explicit environment setup for actions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fjfYVS4FfV6Av8xqeaMNqw.png" /></figure><p>Now when you create a new release on the github repo, a workflow will start running which will publish the package to npm with provenance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5vdxP7PcaEI51ntX42K5_A.png" /><figcaption>successful workflow run</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CYJjIDfnPq3HfLTo8G37vA.png" /><figcaption>build and publishing verification on the package page</figcaption></figure><p>And that’s how one can setup cicd to publish npm packages from github actions without having to create any access tokens themselves!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c7e4702f3b94" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A noobs guide to web APIs]]></title>
            <link>https://ankushkun.medium.com/a-noobs-guide-to-web-apis-ac49b80e8b0b?source=rss-2e453df3b6b5------2</link>
            <guid isPermaLink="false">https://medium.com/p/ac49b80e8b0b</guid>
            <category><![CDATA[servers]]></category>
            <category><![CDATA[expressjs]]></category>
            <category><![CDATA[rest-api]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Ankush Singh]]></dc:creator>
            <pubDate>Wed, 19 Feb 2025 16:44:01 GMT</pubDate>
            <atom:updated>2025-02-19T16:44:01.612Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Alllll riiiight lets do this one more time</em>, my name is Ankush and I’m your friendly neighbourhood web developer, here to educate your <em>pathetic ass</em> a bit about web APIs, how they can be used to connect your frontend with your backend and how you can write an API using NodeJS 🫡</p><p>An API — Application Programming Interface, defines how different applications can talk to each other and exchange data through structures requests and responses</p><p>API Types?<br>1. <strong>Web APIs</strong> — the one we will be focusing on today<br>2. OS APIs — Windows APIs, Android APIs, etc<br>3. Library APIs — Like a python library<br>4. Hardware APIs — allow accessing hardware functions (embedded libraries?)</p><h3>RESTful Web APIs</h3><p>A RESTful Web API is one that follows the “REST” (Representational State Transfer) principles, a software architecture style that ensures scalability, simplicity, and stateless communication between servers and clients.</p><p><strong>Common REST HTTP Methods</strong><br>There are 4 common methods/requests to call REST APIs<br>1. GET — to <em>get</em> some data from the server<br>2. POST — to send/<em>create</em> some data from the server and get a response<br>3. PUT — to <em>update</em> existing data on a server<br>4. DELETE — to <em>remove</em> some data form a server</p><p>When browsing the web, GET request is used to fetch the client side content that gets rendered by the browser</p><p><strong>API response codes<br></strong>Whenever an API is called, it will always return a <em>status code</em> (a number) to signify the result of the request. Categorised into 1XX (informational response), 2XX (success codes), 3XX (redirects), 4XX (client errors), 5XX (server errors)</p><p>200 — OK, successful request<br>301 — Moved Permanently<br>400 — Bad Request, invalid inputs<br>401 — Unauthorised<br>404 — Not Found<br>500 — Internal Server Error</p><h3>Lets write an API endpoint ourselves</h3><p>Make sure you have NodeJS + NPM already installed and follow these steps</p><pre>mkdir api-test<br>cd api-test<br>npm init -y<br>npm i express<br>touch index.js</pre><p>This will create a folder for our express API backend and create a JS file to write code in</p><p>Edit the index.js file and add this code</p><pre>const express = require(&#39;express&#39;);<br>const app = express();<br><br>app.get(&#39;/&#39;, (req, res) =&gt; {<br>    res.send(&#39;Hello World&#39;);<br>});<br><br>app.listen(3000, () =&gt; {<br>    console.log(&#39;Server is running on port 3000&#39;);<br>});</pre><p>We have just initialised an express backend which is serving the “Hello World” string at root (/) endpoint. i.e. on sending a GET request to /, Hello World will be returned</p><p>Run this file with node index.js and open http://localhost:3000/ on any browser. It should show Hello World</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/662/1*fh2r1wAEpvcPK0bZpV9Y-Q.png" /><figcaption>Server running and serving Hello World at port 3000</figcaption></figure><p>Try changing the string in res.send(&#39;this one&#39;) to a simple HTML like &lt;h1&gt;Hi there&lt;/h1&gt; and restart the express server (ctrl+c and node index.js again), check if something changes on the served page</p><p>But this is just for GET requests, we cannot make POST requests to this endpoint</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/618/1*cJV5BQAHvocrDKE_LO1jMQ.png" /></figure><p>As you can see it gives an error if we try to make a POST request to the GET endpoint we created</p><p>Lets define a POST endpoint now</p><pre>// add this after defining defining the app<br>app.use(express.json());<br><br>// POST endpoint<br>app.post(&#39;/&#39;, (req, res) =&gt; {<br>    const name = req.body.name<br>    res.send(`Hello ${name}!`);<br>});</pre><p>restart the app and let’s try sending a post request to the server</p><p>You can use any HTTP client like Thunder Client on VSCode or Postman. I am going to use cURL coz simple</p><pre>curl http://localhost:3000/ -d &#39;{ &quot;name&quot;: &quot;Ankush&quot; }&#39;</pre><p>this should print the response as Hello Ankush!</p><p>BOOM! You just created a GET and POST request! A majority of web APIs are comprised of just GET and POST requests</p><h3>Time to write some frontend</h3><pre>&lt;html&gt;<br>  &lt;body&gt;<br>    &lt;input type=&quot;text&quot; id=&quot;name&quot; name=&quot;name&quot; placeholder=&quot;Enter your name&quot;&gt;<br>    &lt;button onclick=&quot;handleSubmit()&quot;&gt;Submit&lt;/button&gt;<br>    &lt;script&gt;<br>      async function handleSubmit() {<br>        const name = document.getElementById(&#39;name&#39;).value;<br>        const response = await fetch(&#39;/&#39;, {<br>          method: &#39;POST&#39;,<br>          headers: {&#39;Content-Type&#39;: &#39;application/json&#39;},<br>          body: JSON.stringify({ name })<br>        });<br>        const data = await response.text();<br>        document.body.innerHTML = data;<br>      }<br>    &lt;/script&gt;<br>  &lt;/body&gt;<br>&lt;/html&gt;</pre><p>We will serve this HTML through the GET endpoint we created earlier</p><pre>app.get(&#39;/&#39;, (req, res) =&gt; {<br>    res.send(`<br>        &lt;html&gt;<br>            &lt;body&gt;<br>                &lt;input type=&quot;text&quot; id=&quot;name&quot; name=&quot;name&quot; placeholder=&quot;Enter your name&quot;&gt;<br>                &lt;button onclick=&quot;handleSubmit()&quot;&gt;Submit&lt;/button&gt;<br>                &lt;script&gt;<br>                    async function handleSubmit() {<br>                        const name = document.getElementById(&#39;name&#39;).value;<br>                        const response = await fetch(&#39;/&#39;, {<br>                            method: &#39;POST&#39;,<br>                            headers: {<br>                                &#39;Content-Type&#39;: &#39;application/json&#39;<br>                            },<br>                            body: JSON.stringify({ name })<br>                        });<br>                        const data = await response.text();<br>                        document.body.innerHTML = data;<br>                    }<br>                &lt;/script&gt;<br>            &lt;/body&gt;<br>        &lt;/html&gt;<br>    `);<br>});</pre><p>Restart the express app and refresh localhost:3000, and try entering your name</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/836/1*KhXDm_zbhiECJnoVBZE-XA.png" /></figure><p>Now if you open developer tools and go into the network tab, you can see the POST request being sent and the different data associated with it</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wkbk2CrKOqYMwyQ11P9Eiw.png" /></figure><p>Congrats you just sent a POST request with some data, received some data back in response and showed it on the frontend!</p><p>Final Code:</p><pre>const express = require(&#39;express&#39;);<br>const app = express();<br><br>app.use(express.json());<br><br>// To test this endpoint using curl, you can use the following command:<br>// curl http://localhost:3000/ <br>app.get(&#39;/&#39;, (req, res) =&gt; {<br>    res.send(`<br>        &lt;html&gt;<br>            &lt;body&gt;<br>                &lt;input type=&quot;text&quot; id=&quot;name&quot; name=&quot;name&quot; placeholder=&quot;Enter your name&quot;&gt;<br>                &lt;button onclick=&quot;handleSubmit()&quot;&gt;Submit&lt;/button&gt;<br>                &lt;script&gt;<br>                    async function handleSubmit() {<br>                        const name = document.getElementById(&#39;name&#39;).value;<br>                        const response = await fetch(&#39;/&#39;, {<br>                            method: &#39;POST&#39;,<br>                            headers: {<br>                                &#39;Content-Type&#39;: &#39;application/json&#39;<br>                            },<br>                            body: JSON.stringify({ name })<br>                        });<br>                        const data = await response.text();<br>                        document.body.innerHTML = data;<br>                    }<br>                &lt;/script&gt;<br>            &lt;/body&gt;<br>        &lt;/html&gt;<br>    `);<br>});<br><br>// To test this endpoint using curl, you can use the following command:<br>// curl http://localhost:3000/ -d &#39;{ &quot;name&quot;: &quot;Ankush&quot; }&#39;<br>app.post(&#39;/&#39;, (req, res) =&gt; {<br>    const name = req.body.name;<br>    res.send(`Hello ${name}!`);<br>});<br><br>app.listen(3000, () =&gt; {<br>    console.log(&#39;Server is running on port http://localhost:3000&#39;);<br>});</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ac49b80e8b0b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automating Mandelbrot Fractal generation with SYCL Programming]]></title>
            <link>https://ankushkun.medium.com/automating-mandelbrot-fractal-generation-with-sycl-programming-22da4a89e012?source=rss-2e453df3b6b5------2</link>
            <guid isPermaLink="false">https://medium.com/p/22da4a89e012</guid>
            <category><![CDATA[hackathons]]></category>
            <category><![CDATA[iit-roorkee]]></category>
            <category><![CDATA[cognizance]]></category>
            <category><![CDATA[intel]]></category>
            <category><![CDATA[oneapi]]></category>
            <dc:creator><![CDATA[Ankush Singh]]></dc:creator>
            <pubDate>Thu, 30 Mar 2023 12:54:26 GMT</pubDate>
            <atom:updated>2023-03-30T12:54:26.976Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HiYIE_ehe_7jpMEYs9meew.jpeg" /><figcaption>High resolution Mandelbrot Fractal with SYCL</figcaption></figure><h3>What’s this about?</h3><p>I had recently attended a workshop by <strong>Intel</strong> on their <strong>oneAPI programming model</strong> which was conducted by <a href="https://medium.com/u/af7b551ddcdb">Abhishek Nandy</a> at IIT Roorkee during the Cognizance 2023. I attended this workshop with my friend, <a href="https://medium.com/u/81d41cb48956">Krish Agrawal</a> who is also the co-author of this blog. The workshop was held for 2 days in which Abhishek Sir on the first day told about this new model by Intel and how to get started with its implementation with the help of <strong>JupyterLab</strong>. Before ending the day, we were told that there is a hackathon where he will take ideas by all the students in the form of teams as to how we use Intel oneAPI in real-life projects.</p><p><strong>SYCL </strong>(/sɪkl/) is a programming model for high-performance computing that allows developers to write code for heterogeneous systems that use accelerators such as GPUs, FPGAs, and other specialised processing units. It is an open standard developed by the Khronos Group, an industry consortium that also develops other widely used graphics and compute APIs such as OpenGL and Vulkan. SYCL code can be executed on a variety of devices, including CPUs, GPUs, and FPGAs, without modification.</p><p><strong>Fractals</strong> are fascinating geometric shapes that repeat themselves infinitely on different scales. They are created through a process of repeating a simple mathematical equation or algorithm to form increasingly intricate patterns. These fractal shapes are naturally occurring and can be observed in many aspects of nature, such as the <strong>patterns of a fern leaf</strong>, the <strong>coastlines of continents</strong>, and even the <strong>formation of clouds</strong>. One of the most well-known and captivating fractals is the <strong>Mandelbrot fractal</strong>. This type of fractal is generated through an iterative process using complex numbers, resulting in a shape that is renowned for its intricate detail and infinite complexity. It’s a true marvel of mathematics!</p><p>Now that we know what our project revolves around, let’s see its implementation in JupyterLab.</p><h3>Getting started on DevCloud</h3><p><strong>DevCloud</strong> is sandbox that intel provides to learn and try out their oneAPI ecosystem. We will be using the devcloud jupyter lab to run our SYCL fractal generation project. It is way easier and quicker to run SYCL projects on the sandbox than on your local machine.</p><p>So head over to <a href="https://devcloud.intel.com/oneapi/">https://devcloud.intel.com/oneapi/</a> and create an account. Navigate to the ‘Getting Started’ tab, scroll down to ‘Connect with JupyterLab’ and Launch a JupyterLab instance. It might take some time or get loaded instantly depending on the server load so have some patience.</p><figure><img alt="Landing page for the JupyterLab sandbox" src="https://cdn-images-1.medium.com/max/1024/1*8JUHbOpbnWCiTXeLqpqEPQ.png" /><figcaption>Landing page for JupyterLab sandbox</figcaption></figure><p>Once launched, you will find a bunch of folders in the file explorer, including a ‘mandelbrot’ folder.</p><p>Open the Terminal from the landing page, cd into the mandelbrot directory and run the build script.</p><pre>cd mandelbrot<br>chmod +x build.sh<br>./build.sh</pre><figure><img alt="build script output" src="https://cdn-images-1.medium.com/max/954/1*62IvxnuK0M2Maf3cG43Cjw.png" /><figcaption>build script output</figcaption></figure><p>This will compile and run the project and create a ‘mandelbrot.png’ file inside the ‘build’ folder. As you can see the image is 1024x1024 pixels and it took just 190 milliseconds to generate the image (serial time + parallel time). Now if we were to do the same in python or regular C++ without SYCL, it would take seconds to generate the same result. So that’s the power of SYCL.</p><p>If you checkout the ‘src/mandel.hpp’ file, you can find 4 variables row and column size, max iterations and repetitions. We can modify these values to generate Mandelbrot sets of different resolutions and qualities.</p><p>Try changing these values and see how much time it takes for the images of different configurations to generate.</p><h3>Automate it</h3><p>Let’s now write a <strong>bash script</strong> that automatically changes the values in the C++ file and builds the project.</p><p>Create a file ‘autogen.sh’ and inside it write the following lines.</p><pre>#!/bin/bash<br><br>PASSES=(10 100 500 1000 5000)<br>SIZES=(1024 2048 4096 8192 16384)<br><br>EDIT_FILE=&quot;src/mandel.hpp&quot;<br>BUILD_FILE=&quot;build.sh&quot;<br><br>prev_size=100<br>prev_pass=100<br>let count=0</pre><p>The variable ‘PASSES’ contains a list of iterations that will be used in each image of the image generated and sizes contains the list of resolutions for the images in pixels. ‘prev_size’ and ‘prev_pass’ is a placeholder that we will use later. Count is the number of images generated.</p><pre>for SIZE in ${SIZES[@]}; do<br>    for PASS in ${PASSES[@]}; do<br>        echo &quot;&quot;<br>        echo &quot;Generating fractal for $SIZE x $SIZE with $PASS passes&quot;<br>        echo &quot;&quot;<br><br>        ...<br><br>    done<br>done</pre><p>We create a nested loop for the values of SIZES and PASSES, so we can have all the combinations of sizes and passes to generate the images with. In the body of the loop we print a message to display to the user.</p><p>In order to edit the values of the 3 variables in the .hpp file we will use the sed command.</p><pre>sed -ie &quot;s/row_size=$prev_size/row_size=$SIZE/g&quot; $EDIT_FILE<br>sed -ie &quot;s/col_size=$prev_size/col_size=$SIZE/g&quot; $EDIT_FILE<br>sed -ie &quot;s/max_iterations=$prev_pass/max_iterations=$PASS/g&quot; $EDIT_FILE<br>sed -ie &quot;s/repetitions=$prev_pass/repetitions=$PASS/g&quot; $EDIT_FILE<br><br>prev_size=$SIZE<br>prev_pass=$PASS<br>count=$((count+1))</pre><p>This uses find and replace to replace the previous values of the variables with new values and updates the count in each iteration of the bash loop.</p><p>To run the build command we simply do</p><pre>./$BUILD_FILE<br><br>echo &quot;&quot;<br>echo &quot;Generated for $SIZE x $SIZE with $PASS passes&quot;<br>echo &quot;&quot;</pre><p>The project is built and image is saved in the build folder, so we will have to copy it from there into a location of our choice. Create a folder images in the mandelbrot folder and add the following copy command to the autogen.sh script</p><pre>cp &quot;./build/mandelbrot.png&quot; &quot;./images/$count.mandelbrot_${SIZE}_${PASS}.png&quot;</pre><p>This copies the image from build into images folder and also renames it to contain useful information such as size and number of passes and the count.</p><p>Run the autogen script and let the fractals be created!</p><pre>chmod +x autogen.sh<br>./autogen.sh</pre><p>Here is the full autogen script</p><pre>#!/bin/bash<br><br>PASSES=(10 100 500 1000 5000)<br>SIZES=(1024 2048 4096 8192 16384)<br><br>EDIT_FILE=&quot;src/mandel.hpp&quot;<br>BUILD_FILE=&quot;build.sh&quot;<br><br>prev_size=100<br>prev_pass=100<br>let count=0<br><br>for SIZE in ${SIZES[@]}; do<br>    for PASS in ${PASSES[@]}; do<br>        echo &quot;&quot;<br>        echo &quot;Generating fractal for $SIZE x $SIZE with $PASS passes&quot;<br>        echo &quot;&quot;<br>        sed -ie &quot;s/row_size=$prev_size/row_size=$SIZE/g&quot; $EDIT_FILE<br>        sed -ie &quot;s/col_size=$prev_size/col_size=$SIZE/g&quot; $EDIT_FILE<br>        sed -ie &quot;s/max_iterations=$prev_pass/max_iterations=$PASS/g&quot; $EDIT_FILE<br>        sed -ie &quot;s/repetitions=$prev_pass/repetitions=$PASS/g&quot; $EDIT_FILE<br><br>        prev_size=$SIZE<br>        prev_pass=$PASS<br>        count=$((count+1))<br><br>        ./$BUILD_FILE<br><br>        echo &quot;&quot;<br>        echo &quot;Generated for $SIZE x $SIZE with $PASS passes&quot;<br>        echo &quot;&quot;<br><br>        cp &quot;./build/mandelbrot.png&quot; &quot;./images/$count.mandelbrot_${SIZE}_${PASS}.png&quot;<br>        <br>    done<br>done</pre><figure><img alt="Images folder showing a 4K resolution image with 100 iterations" src="https://cdn-images-1.medium.com/max/1024/1*Wu-I9mjifg7MeS8DIU_Q9g.png" /><figcaption>List of images generated</figcaption></figure><p>More iterations means better quality of the image, you can zoom in and see the small details and tiny fractal repetitions, but to see these clearly the resolution must be increased.</p><h4>The winning moment</h4><p>It was Day 2 and we both had absolutely nothing with us. We sat at the back thinking what we would present the crowd with. We had about 3 hours in our hands to build something. While brainstorming, we were randomly playing with the Mandelbrot directory already given by Intel in their Virtual Private Server. We ran the code with the proper command and was later told to rerun using qbus -I which then redirected usto another portal maybe through some kind of routing. We then decided to create a bash script to randomise four values in the original program i.e. row_size, column_size, max_iterations and repetitions. We were able to do this and now we had started generating images in a folder. Some images with higher resolution or more repetitions took more time than the rest. The longest time was taken by 16K resolution image with 1000 repetitions which took approximately 50 minutes (still wayy less than an average C++ or Python script generating the same iterations and resolution).</p><p>We presented our solution made on a presentation made on Keynote. We remarkably remember that we were the only team with the minimal interaction with Abhishek Sir and also the only ones to receive a round of applause from him. At the end of all prizes, 6 stood out of 13 and were congratulated on stage for the same. Then came the cash prizes for the Top 3. As soon as second was done, we thought the claps were consolatory of nature and we were already packing. But then we realised, we were indeed 1st. (Out of excitement, I slammed the top of Krish’s Macbook Pro 😂). In the end we got ₹12000 as a prize in the form of Amazon Vouchers.</p><p>If you scrolled till this point, you may as well checkout my <a href="https://ankushKun.github.io/links">social handles </a>and leave a follow, Thanks for reading ;)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=22da4a89e012" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>