<?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: Tanbir Hossain Ramim</title>
    <description>The latest articles on DEV Community by Tanbir Hossain Ramim (@tanbirramim).</description>
    <link>https://dev.to/tanbirramim</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%2F4128103%2F0d6ecdf9-65aa-47cf-a298-8f2b30beaaa3.png</url>
      <title>DEV Community: Tanbir Hossain Ramim</title>
      <link>https://dev.to/tanbirramim</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tanbirramim"/>
    <language>en</language>
    <item>
      <title>Open Source Radar is looking for contributors: 11 small issues for your first PR</title>
      <dc:creator>Tanbir Hossain Ramim</dc:creator>
      <pubDate>Sun, 27 Sep 2026 15:42:56 +0000</pubDate>
      <link>https://dev.to/tanbirramim/open-source-radar-is-looking-for-contributors-11-small-issues-for-your-first-pr-2eo1</link>
      <guid>https://dev.to/tanbirramim/open-source-radar-is-looking-for-contributors-11-small-issues-for-your-first-pr-2eo1</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://github.com/TanbirRamim/open-source-radar" rel="noopener noreferrer"&gt;Open Source Radar&lt;/a&gt;, a free tool that lists open, unclaimed, beginner-friendly issues from about 1,200 active open source projects and flags each project's CLA, DCO and AI rules before you start. It's refreshed twice a day by a small Python pipeline on GitHub Actions.&lt;/p&gt;

&lt;p&gt;The radar itself is also a good place for a first pull request. This week the first outside feature landed: RSS feeds for every language, built by a contributor I'd never met before. So I've opened a batch of small, well-scoped issues for Hacktoberfest and beyond.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues you can pick up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Python (the pipeline, &lt;code&gt;scripts/radar.py&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/17" rel="noopener noreferrer"&gt;#17 Add an atom:link self reference to the RSS feeds&lt;/a&gt;: a few lines with ElementTree plus a test.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/19" rel="noopener noreferrer"&gt;#19 Add a smoke test that runs render() end to end&lt;/a&gt;: would have caught a real crash we hit last week.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/20" rel="noopener noreferrer"&gt;#20 A test fails on Windows&lt;/a&gt;: if you're on Windows, even just the traceback helps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Website (plain HTML, CSS and JavaScript)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/21" rel="noopener noreferrer"&gt;#21 Show the RSS feed for the selected language&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Writing, no coding needed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each quickstart is a short page on how projects in one language are usually built and tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/26" rel="noopener noreferrer"&gt;#26 Shell&lt;/a&gt;, &lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/27" rel="noopener noreferrer"&gt;#27 Julia&lt;/a&gt;, &lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/28" rel="noopener noreferrer"&gt;#28 OCaml&lt;/a&gt;, &lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/2" rel="noopener noreferrer"&gt;#2 Elixir&lt;/a&gt;, &lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/3" rel="noopener noreferrer"&gt;#3 Haskell&lt;/a&gt;, &lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/4" rel="noopener noreferrer"&gt;#4 Scala&lt;/a&gt;, &lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/5" rel="noopener noreferrer"&gt;#5 Lua&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/TanbirRamim/open-source-radar/issues/7" rel="noopener noreferrer"&gt;#7 Translate the guide&lt;/a&gt; into your language (Hindi is already in progress).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works here
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Comment on the issue you want, and I'll assign it to you.&lt;/li&gt;
&lt;li&gt;Read &lt;a href="https://github.com/TanbirRamim/open-source-radar/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;. The pipeline tests run with &lt;code&gt;python3 -m unittest discover scripts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open a pull request. Every PR gets a review, usually within a day, and a friendly one.&lt;/li&gt;
&lt;li&gt;Once it's merged, your name goes into the Contributors list in the README.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And if you're looking for issues in &lt;em&gt;other&lt;/em&gt; projects, that's what the radar is for: &lt;a href="https://tanbirramim.github.io/open-source-radar/" rel="noopener noreferrer"&gt;pick a language and start&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions or ideas? The &lt;a href="https://github.com/TanbirRamim/open-source-radar/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; are open, and I read everything there.&lt;/p&gt;

</description>
      <category>hacktoberfest</category>
      <category>opensource</category>
      <category>beginners</category>
      <category>python</category>
    </item>
    <item>
      <title>How I Built VoiceMax: Reading Emotion From a Voice Recording With Three Small AI Flows</title>
      <dc:creator>Tanbir Hossain Ramim</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:53:15 +0000</pubDate>
      <link>https://dev.to/tanbirramim/how-i-built-voicemax-reading-emotion-from-a-voice-recording-with-three-small-ai-flows-12ko</link>
      <guid>https://dev.to/tanbirramim/how-i-built-voicemax-reading-emotion-from-a-voice-recording-with-three-small-ai-flows-12ko</guid>
      <description>&lt;p&gt;&lt;em&gt;By Tanbir Hossain Ramim. Project page: &lt;a href="https://tanbirramim.com/projects/voicemax" rel="noopener noreferrer"&gt;VoiceMax&lt;/a&gt;. Source: &lt;a href="https://github.com/TanbirRamim/VoiceMax" rel="noopener noreferrer"&gt;github.com/TanbirRamim/VoiceMax&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;VoiceMax started at Hackaburg 2025, where I built it with my team (the footer still says "Team 2.1"). The idea was simple: your voice carries a lot of emotional information you do not consciously notice, so let people record a few seconds of speech and get a short, honest reading of how they sound, followed by something supportive.&lt;/p&gt;

&lt;p&gt;It is a Next.js app with TypeScript, shadcn/ui and Tailwind on the front, and Genkit with a Gemini model doing the analysis. This post walks through how it is put together and the decisions I would keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  One model, three narrow jobs
&lt;/h2&gt;

&lt;p&gt;The whole AI layer is configured in seven lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;genkit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;genkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;googleAI&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@genkit-ai/googleai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;genkit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;googleAI&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googleai/gemini-2.0-flash&lt;/span&gt;&lt;span class="dl"&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 temptation at a hackathon is to write one giant prompt that returns everything at once. I split it into three flows instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;analyzeAudioEmotion&lt;/code&gt; listens to the recording and describes it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;suggestAdditionalEmotions&lt;/code&gt; takes that description and proposes up to three secondary emotions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;providePersonalizedFeedback&lt;/code&gt; takes only the primary emotion and writes feedback, calling a tool when the emotion is negative.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each flow has one job, one input schema and one output schema. That made prompts much easier to iterate on, because when the feedback was off I knew exactly which prompt to change, and I could run each flow on its own in the Genkit developer UI (&lt;code&gt;npm run genkit:dev&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting structured answers out of audio
&lt;/h2&gt;

&lt;p&gt;The first flow is the only one that sees audio. The recording goes in as a base64 data URI, and the output is a Zod schema with five fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AnalyzeAudioEmotionOutputSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;primaryEmotion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The primary emotion expressed in the audio.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;perceivedStressLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A qualitative description of the perceived stress level in the voice (e.g., calm, moderate stress, high tension).&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;speechCharacteristics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Observations about speech patterns like pace, pauses, or fluency (e.g., fluid and confident, some hesitation, frequent pauses, rapid pace).&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;perceivedConfidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Description of the speaker's perceived confidence (e.g., confident and assertive, somewhat hesitant, appears unsure).&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;vocalEnergy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Qualitative assessment of the vocal energy or enthusiasm conveyed (e.g., high energy, moderate, low energy/flat).&lt;/span&gt;&lt;span class="dl"&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 &lt;code&gt;.describe()&lt;/code&gt; strings matter more than they look. Genkit passes the schema to the model, so the descriptions double as instructions, and giving example values ("calm, moderate stress, high tension") keeps the answers short and comparable instead of turning into paragraphs.&lt;/p&gt;

&lt;p&gt;All five fields are qualitative strings on purpose. A model listening to ten seconds of audio has no business producing "stress: 73%". A phrase like "some hesitation noted" is closer to what it can actually tell, and it reads better to the person on the other side.&lt;/p&gt;

&lt;p&gt;The prompt itself passes the audio through Handlebars media syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="nx"&gt;media&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;audioDataUri&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Chaining the flows without a second audio upload
&lt;/h2&gt;

&lt;p&gt;Only the first flow gets the audio. The second one gets a text summary built from the first flow's output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;suggestionsInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SuggestAdditionalEmotionsInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;primaryEmotion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audioAnalysisContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`The primary emotion detected is "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;primaryEmotion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". The voice also showed signs of "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;perceivedStressLevel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" stress, speech characteristics were noted as "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;speechCharacteristics&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;", perceived confidence as "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;perceivedConfidence&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;", and vocal energy as "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vocalEnergy&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". Consider nuances.`&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;This keeps the audio payload to a single request, and it means the secondary emotions are reasoned from the same observations the user sees on screen, so the two parts of the result cannot contradict each other in obvious ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting a tool, not the model, write the exercise
&lt;/h2&gt;

&lt;p&gt;The feedback flow was the one I cared most about getting right. If someone sounds anxious, I did not want the model improvising breathing instructions. So the exercise text comes from a Genkit tool with fixed answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anxious&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anxiety&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stressed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worried&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nervous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;emotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Try Box Breathing: Inhale for 4 seconds, hold your breath for 4 seconds, exhale for 4 seconds, and then hold your breath again for 4 seconds. Repeat this cycle several times to calm your nervous system.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... sad and angry branches, then a mindful-breathing default&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt then tells the model that for negative emotions it must call &lt;code&gt;breathingExerciseSuggestion&lt;/code&gt; and put the tool's output in the &lt;code&gt;suggestion&lt;/code&gt; field verbatim, with no "Here is the exercise:" wrapper. For positive emotions it writes a short tip itself and must not call the tool.&lt;/p&gt;

&lt;p&gt;The split is deliberate. The model is good at the empathetic sentence in &lt;code&gt;feedback&lt;/code&gt;. The deterministic part, the thing someone might actually follow, is plain code I can read and test. The matching uses &lt;code&gt;includes&lt;/code&gt;, so "slightly anxious" or "stressed out" still land in the right branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording audio in the browser
&lt;/h2&gt;

&lt;p&gt;The recorder is the most defensive code in the project, because microphone handling is where things break across browsers. Before creating a &lt;code&gt;MediaRecorder&lt;/code&gt; it negotiates a format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio/webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isTypeSupported&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isTypeSupported&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio/ogg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;MediaRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isTypeSupported&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&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="nx"&gt;options&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebM first, Ogg second, and otherwise an empty string so the browser picks its own default. When recording stops, the chunks become a &lt;code&gt;Blob&lt;/code&gt;, the file name takes its extension from the real MIME type, and a &lt;code&gt;FileReader&lt;/code&gt; turns it into the data URI the first flow expects.&lt;/p&gt;

&lt;p&gt;Permission errors get their own messages: &lt;code&gt;NotAllowedError&lt;/code&gt; explains how to re-enable the microphone, &lt;code&gt;NotFoundError&lt;/code&gt; says no microphone was found. Every path that acquires a stream also stops its tracks on reset, so the browser's recording indicator actually goes away when the user starts over. That cleanup is spread over four steps in a &lt;code&gt;useEffect&lt;/code&gt;, and every one of them was added after the indicator stayed on in testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors a person can act on
&lt;/h2&gt;

&lt;p&gt;The three flows run one after another inside a single &lt;code&gt;try&lt;/code&gt;. When something fails, the raw error is mapped to a sentence a user can do something with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;429 Too Many Requests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QuotaFailure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate limit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;userFriendlyError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Analysis failed due to API rate limits. ...&lt;/span&gt;&lt;span class="dl"&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;400 Bad Request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid argument&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;userFriendlyError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Analysis failed: The recorded audio might be too short, silent, corrupted, or in a format the AI could not process. ...&lt;/span&gt;&lt;span class="dl"&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;A 400 from the model almost always meant the recording was too short or silent, so the message says that instead of "Bad Request". Anything else is trimmed and capped at 200 characters so a stack trace never ends up on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would change
&lt;/h2&gt;

&lt;p&gt;Reading the code again, there is one thing I would fix first. After each flow the page calls &lt;code&gt;setAnalysisResult&lt;/code&gt; with a partial result, with a comment saying it is to "show partial results sooner". But the results section only renders when &lt;code&gt;!isLoading&lt;/code&gt;, and loading stays true until all three flows finish. So the progressive rendering is written but never visible. The fix is small: render each card as soon as its field exists and keep the spinner only for the parts still pending.&lt;/p&gt;

&lt;p&gt;The other change is running flows two and three in parallel. The feedback flow only needs &lt;code&gt;primaryEmotion&lt;/code&gt;, so it does not have to wait for the secondary emotions.&lt;/p&gt;

&lt;p&gt;The pattern I would reuse everywhere, though, is the shape of the AI layer: small flows with typed inputs and outputs, and plain code for anything that must be exactly right.&lt;/p&gt;

&lt;p&gt;If you want to read the whole thing, the source is on &lt;a href="https://github.com/TanbirRamim/VoiceMax" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and there is a short summary on the &lt;a href="https://tanbirramim.com/projects/voicemax" rel="noopener noreferrer"&gt;VoiceMax project page&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://tanbirramim.com/writing/how-i-built-voicemax" rel="noopener noreferrer"&gt;tanbirramim.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Find Good First Issues That Are Still Open in 2026</title>
      <dc:creator>Tanbir Hossain Ramim</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:50:25 +0000</pubDate>
      <link>https://dev.to/tanbirramim/how-to-find-good-first-issues-that-are-still-open-in-2026-4a1c</link>
      <guid>https://dev.to/tanbirramim/how-to-find-good-first-issues-that-are-still-open-in-2026-4a1c</guid>
      <description>&lt;p&gt;"Look for issues labeled good first issue" is the advice everyone gets when they want to start contributing to open source. It is also why so many first attempts stall.&lt;/p&gt;

&lt;p&gt;Here is what usually happens. You find an issue labeled good first issue. Someone claimed it three weeks ago. The next one already has two open pull requests. The third looks free, so you spend a weekend on it, and then a bot closes your pull request because the project requires a signed agreement, or does not accept AI-assisted code, or closes pull requests from accounts it does not recognise.&lt;/p&gt;

&lt;p&gt;None of that is about your code. It is about finding the right issue and knowing the rules before you start. This guide covers both, with the exact searches I use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why most good first issue lists do not work
&lt;/h2&gt;

&lt;p&gt;A label is a snapshot of a maintainer's opinion on the day they added it. It does not change when the issue gets claimed, when a pull request appears, or when the project stops merging outside contributions.&lt;/p&gt;

&lt;p&gt;Popular issues also go fast. When I built &lt;a href="https://tanbirramim.com/voluntary/open-source-radar" rel="noopener noreferrer"&gt;Open Source Radar&lt;/a&gt;, a tool that tracks newcomer-friendly issues across more than 1,200 active projects, the pattern was obvious: in well-known repositories, a clearly described beginner issue often has a pull request within days of being labeled. A list compiled last month is mostly a list of work other people have already done.&lt;/p&gt;

&lt;p&gt;So the goal is not to find issues with the right label. It is to find issues with the right label that nobody is working on, in a project that still reviews outside contributions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe72lt8lqdakcycazefgb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe72lt8lqdakcycazefgb.webp" alt="Five checks before picking an issue: a newcomer label, nobody assigned, no linked pull request, no recent claim in the comments, and a project that still merges outside pull requests." width="1600" height="640"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The label is only the first of five checks. The next two can be done in the search bar, the last two need a minute of reading.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Search GitHub for issues nobody has claimed
&lt;/h2&gt;

&lt;p&gt;GitHub's issue search can filter out most taken issues if you use the right qualifiers. Paste this into the search bar on github.com:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;is:issue is:open no:assignee -linked:pr label:"good first issue" language:python updated:&amp;gt;2026-07-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each part does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is:issue is:open&lt;/code&gt; limits results to open issues.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no:assignee&lt;/code&gt; hides issues that someone has been assigned to.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-linked:pr&lt;/code&gt; hides issues that already have a pull request connected to them. This single qualifier removes a large share of "available" issues.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;label:"good first issue"&lt;/code&gt; finds issues maintainers marked as suitable for newcomers. Try &lt;code&gt;label:"help wanted"&lt;/code&gt; too; those are often less contested.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;language:python&lt;/code&gt; narrows to one language. Replace it with the language you know best.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;updated:&amp;gt;2026-07-01&lt;/code&gt; skips issues nobody has touched in months. Use a date about two months ago.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynd67bl0fs4z1k4a6nk4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynd67bl0fs4z1k4a6nk4.webp" alt="GitHub issue search in the zitadel repository with the qualifiers is:issue state:open no:assignee -linked:pr label:" width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The same qualifiers inside one repository. When this was captured 29 issues matched, but look at the labels: several also carry To-be-closed, which is exactly the kind of detail a label-only list never shows you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then open each result and read the comments. A comment from last week saying "I'd like to work on this" means it is taken, even without an assignee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check that the project still merges outside contributions
&lt;/h2&gt;

&lt;p&gt;The project matters more than the issue. An active, welcoming project reviews your pull request in days. An overloaded one may never look at it.&lt;/p&gt;

&lt;p&gt;Before committing to an issue, spend two minutes on the project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recent commits.&lt;/strong&gt; Look at the commit history. Nothing in the last month is a warning sign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merged pull requests from outsiders.&lt;/strong&gt; Open the pull requests tab, filter by &lt;code&gt;is:pr is:merged&lt;/code&gt;, and look at the authors. If every merged pull request comes from the same two maintainers, outside contributions are not a priority right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response time.&lt;/strong&gt; Open a few recently merged pull requests from outside contributors and look at how long the first review took.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tone.&lt;/strong&gt; Read a couple of issue threads. Patient, specific maintainers are a good sign even when they say no.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Read the rules before you write code
&lt;/h2&gt;

&lt;p&gt;This is the step most guides skip, and the one that costs people the most time. Every project has its own rules for contributions, and some of them decide whether your pull request can be merged at all.&lt;/p&gt;

&lt;p&gt;Look for these files: &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, the pull request template in &lt;code&gt;.github/&lt;/code&gt;, &lt;code&gt;AI_POLICY.md&lt;/code&gt;, and &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rules that matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contributor License Agreements (CLA).&lt;/strong&gt; A legal agreement some projects require, usually signed through a bot on your first pull request. Of the 1,210 active projects Open Source Radar tracked in September 2026, 155 mentioned a CLA in their contribution files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DCO sign-off.&lt;/strong&gt; A lighter alternative where every commit needs a &lt;code&gt;Signed-off-by&lt;/code&gt; line. You add it with &lt;code&gt;git commit -s&lt;/code&gt;. 97 of those projects mentioned it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI contribution policies.&lt;/strong&gt; This is new and changing quickly. 127 projects asked contributors to disclose AI assistance, and 38 restricted or banned AI-generated contributions outright. If you use AI tools, follow the project's rule exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers come from automatically scanning contribution files, so treat them as a sense of scale rather than an exact count. The point is that a meaningful share of projects has a rule you need to know about before you start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9innegnzi8y6u6npj8kz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9innegnzi8y6u6npj8kz.webp" alt="Bar chart of rules found in the contribution files of 1,210 active projects: 155 require a Contributor License Agreement, 127 ask contributors to disclose AI assistance, 97 require DCO sign-off, and 38 restrict or ban AI-generated code." width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Rules found across 1,210 active projects tracked by Open Source Radar in September 2026.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch out for automation that closes pull requests
&lt;/h2&gt;

&lt;p&gt;Because of the growing volume of low-effort pull requests, some projects now run workflows that close pull requests automatically. Patterns I have seen include closing pull requests from accounts that opened pull requests to many unrelated repositories in the same week, from branches named after AI tools, or that do not reference an issue labeled for outside help.&lt;/p&gt;

&lt;p&gt;Two habits keep you clear of all of them: contribute steadily to a small number of projects instead of many at once, and read the files in &lt;code&gt;.github/workflows/&lt;/code&gt; if a project's contributing guide mentions automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim the issue the right way
&lt;/h2&gt;

&lt;p&gt;Some projects want a comment before you start; others say to just open a pull request. When a comment is expected, make it useful:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can reproduce this on version 4.2: running the command with an empty username loops forever instead of showing an error. The validation in the login prompt rejects the input without printing why. I plan to print the message and ask again, with a test. Does that approach sound right?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That comment shows you understood the problem, and it gives the maintainer a chance to correct your approach before you spend a weekend on it. Only claim an issue you will start this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the pull request easy to merge
&lt;/h2&gt;

&lt;p&gt;Maintainers review a lot of pull requests. The easier yours is to understand, the faster it moves.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce the bug first&lt;/strong&gt;, on your machine, before changing anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a test that fails&lt;/strong&gt; without your change and passes with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the diff small.&lt;/strong&gt; Change only what the issue needs. No drive-by refactoring or reformatting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the project's own checks&lt;/strong&gt; locally: tests, formatter, linter, type checker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow the commit message convention&lt;/strong&gt; you see in &lt;code&gt;git log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a short description&lt;/strong&gt; that says what was wrong, what you changed and how you tested it, and links the issue with &lt;code&gt;Fixes #123&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  If nothing happens
&lt;/h2&gt;

&lt;p&gt;Silence usually means busy, not no. After about a week, one friendly follow-up is fine: "This is ready for review whenever someone has time. Happy to make changes." Keep your branch free of merge conflicts in the meantime.&lt;/p&gt;

&lt;p&gt;If your pull request is closed, read the reason. Someone may have fixed it first, the maintainers may want a different approach, or the change may be out of scope. None of those are a verdict on you, and a polite reply leaves a good impression for your next contribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shortcut: Open Source Radar
&lt;/h2&gt;

&lt;p&gt;Doing all of the above by hand for every issue takes time, which is why I built &lt;a href="https://tanbirramim.github.io/open-source-radar/" rel="noopener noreferrer"&gt;Open Source Radar&lt;/a&gt;. It runs these checks automatically twice a day and lists only open issues with no assignee and no linked pull request, from projects with commits in the last 60 days, sorted by language and topic. Each project shows badges for CLA, DCO and AI policy requirements, linked to the file they came from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6le1neqy3y35gn8k23u.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6le1neqy3y35gn8k23u.webp" alt="The Open Source Radar website, showing the search qualifiers it applies and 3,533 matching issues in 1,207 projects." width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Open Source Radar website. Every listed issue already passed the search checks above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7my4td8gf7ri4bzctl85.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7my4td8gf7ri4bzctl85.webp" alt="Open Source Radar issue list with project rule badges such as CLA, Disclose AI use and Earlier PR closed shown next to each issue." width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Project rules appear next to each issue, so a CLA or an AI disclosure requirement is visible before you write any code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It is free and open source, and it includes a full &lt;a href="https://github.com/TanbirRamim/open-source-radar/tree/main/guide" rel="noopener noreferrer"&gt;contribution guide&lt;/a&gt; covering everything from setting up Git to handling review feedback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb770ho54ux94gh5sueaa.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb770ho54ux94gh5sueaa.webp" alt="The TanbirRamim/open-source-radar repository on GitHub, with folders for the guide, issues, language quickstarts, projects and the pipeline scripts." width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The repository behind it: the data pipeline, the guide and the language quickstarts are all public, so you can check how every issue was selected.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Pick one project you actually use, find one issue nobody has claimed, and start there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a good first issue on GitHub?
&lt;/h3&gt;

&lt;p&gt;It is an issue that maintainers have labeled as suitable for someone new to the project, usually with the label good first issue, beginner or first-timers-only. It is small, clearly described, and does not need deep knowledge of the codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know if someone is already working on an issue?
&lt;/h3&gt;

&lt;p&gt;Check four things: whether anyone is assigned, whether a pull request is linked in the Development sidebar or the timeline, whether a recent comment says someone is working on it, and whether an open pull request mentions the issue number. The GitHub search qualifiers &lt;code&gt;no:assignee&lt;/code&gt; and &lt;code&gt;-linked:pr&lt;/code&gt; filter out the first two automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to sign a CLA to contribute to open source?
&lt;/h3&gt;

&lt;p&gt;Only for projects that require one. Many company-backed projects ask for a Contributor License Agreement, usually through a bot that comments on your first pull request. Others ask for a DCO sign-off instead, which you add with &lt;code&gt;git commit -s&lt;/code&gt;. The contributing guide says which applies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use AI tools to contribute to open source?
&lt;/h3&gt;

&lt;p&gt;It depends on the project. Some allow AI assistance with the usual review standards, some require you to disclose it in the pull request, and some do not accept AI-generated contributions at all. Read the contributing guide, &lt;code&gt;AI_POLICY.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt; before you start, and always understand every line you submit.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take for a first pull request to be reviewed?
&lt;/h3&gt;

&lt;p&gt;Anywhere from hours to weeks. Active projects with several maintainers often respond within a few days. If there is no response after a week, one polite follow-up comment is fine.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://tanbirramim.com/writing/how-to-find-good-first-issues" rel="noopener noreferrer"&gt;tanbirramim.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>github</category>
      <category>beginners</category>
      <category>career</category>
    </item>
    <item>
      <title>I built a small Python library to add retries, caching, fallbacks, budgets, and guardrails around native LLM SDK calls</title>
      <dc:creator>Tanbir Hossain Ramim</dc:creator>
      <pubDate>Wed, 16 Sep 2026 12:54:33 +0000</pubDate>
      <link>https://dev.to/tanbirramim/i-built-a-small-python-library-to-add-retries-caching-fallbacks-budgets-and-guardrails-around-f75</link>
      <guid>https://dev.to/tanbirramim/i-built-a-small-python-library-to-add-retries-caching-fallbacks-budgets-and-guardrails-around-f75</guid>
      <description>&lt;p&gt;I got tired of writing the same LLM boilerplate in every project, so I made a library&lt;/p&gt;

&lt;p&gt;Three lines to call an LLM in a prototype. Then you go to production and suddenly you're writing the same 200 lines you wrote last time:&lt;/p&gt;

&lt;p&gt;Retry logic because OpenAI throws 429s at you. Cost tracking because someone left a loop running and burned $40 on a Saturday. Caching because your support bot answers "how do I reset my password?" eight hundred times a day and you're paying for each one. PII scrubbing because customer emails keep showing up in prompts. Output parsing because the model returns markdown when you asked for JSON, and now your frontend is on fire.&lt;/p&gt;

&lt;p&gt;I kept copy-pasting this stuff between projects until I finally pulled it into a library: callm.&lt;/p&gt;

&lt;p&gt;It's just a decorator&lt;/p&gt;

&lt;p&gt;I didn't want to learn a new client or replace the SDK. The whole idea is that you keep writing normal OpenAI/Anthropic code and slap a decorator on top:&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;python&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;openai&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;callm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;callm&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Summary&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;title&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;bullets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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="nd"&gt;@callm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cache&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;retry&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="n"&gt;fallback&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;anthropic/claude-sonnet-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;max_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;block_pii&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;detect_injection&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;output_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Summary&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;summarize&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="k"&gt;return&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;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;gpt-4o&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="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;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="n"&gt;text&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;You call summarize(), you get back a validated Summary. That's it. Outside the decorator, your SDK works exactly like before — nothing monkey-patched, nothing weird.&lt;/p&gt;

&lt;p&gt;What's actually going on under the hood&lt;/p&gt;

&lt;p&gt;When your function runs, the call passes through a stack of middleware:&lt;/p&gt;

&lt;p&gt;Security first — prompt injection scoring, then PII gets masked (&lt;a href="mailto:jane@acme.com"&gt;jane@acme.com&lt;/a&gt; → [EMAIL_1]). This happens before anything hits the cache or the network.&lt;br&gt;
Cache check — exact-match lookup based on the masked request, params, and schema.&lt;br&gt;
Validation — response gets parsed into your Pydantic model. If it fails, the model gets called again with the validation errors attached (usually fixes it on the second try).&lt;br&gt;
Fallback — if OpenAI is just not having it today, the request gets translated and sent to Claude instead. Your code still gets back an OpenAI ChatCompletion object, so nothing breaks downstream.&lt;br&gt;
Cost guard — estimated cost is checked against max_cost before the request actually goes out. No more surprise bills.&lt;br&gt;
Retry — exponential backoff with jitter, and it actually reads the provider's rate-limit headers instead of guessing.&lt;/p&gt;

&lt;p&gt;Everything gets logged locally (tokens, cost, cache hits, retries — never your actual prompts), and you can run callm stats to see what you're spending per provider, model, or function.&lt;/p&gt;

&lt;p&gt;A couple of design choices I want to explain&lt;/p&gt;

&lt;p&gt;The cache is exact-match on purpose. I know semantic caching sounds cool, but think about it: "Summarize &lt;a href="https://example.com/post-1" rel="noopener noreferrer"&gt;https://example.com/post-1&lt;/a&gt;" and "Summarize &lt;a href="https://example.com/post-2" rel="noopener noreferrer"&gt;https://example.com/post-2&lt;/a&gt;" look almost identical to an embedding model. A semantic cache would cheerfully hand you the wrong summary and you wouldn't notice for a while. callm does support semantic matching if you want it, but you have to opt in, and it'll never match across different system prompts or conversation histories.&lt;/p&gt;

&lt;p&gt;Fallback won't silently break your request. If you're using tools, images, or a structured response format, callm will only fall back to another model from the same provider. Translating tool schemas across providers is a can of worms, and I'd rather fail loudly than give you a subtly wrong result.&lt;/p&gt;

&lt;p&gt;"Cool, but does it add latency?"&lt;/p&gt;

&lt;p&gt;The repo has an offline benchmark that runs the real OpenAI SDK against a fake server:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What I measured
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Overhead per call (default settings)    +0.06 ms&lt;br&gt;
Cost savings with cache, FAQ-style traffic  91% lower&lt;br&gt;
Cost savings with cache, mostly unique prompts  2% lower&lt;br&gt;
Success rate with 20% random 503s: plain SDK → retry=2 → + fallback 79.9% → 99.4% → 100%&lt;/p&gt;

&lt;p&gt;The cache numbers are the obvious takeaway: caching is huge if your requests repeat, and basically irrelevant if they don't. Run callm stats on your own traffic before you count on those savings.&lt;/p&gt;

&lt;p&gt;Give it a spin&lt;br&gt;
bash&lt;br&gt;
pip install "callm-toolkit[openai,anthropic,validation]"&lt;/p&gt;

&lt;p&gt;There's an offline demo in the repo (examples/offline_demo.py) that walks through a retry, a cache hit, a fallback, a blocked expensive call, and a flagged injection — no API key needed.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/TanbirRamim/callm" rel="noopener noreferrer"&gt;https://github.com/TanbirRamim/callm&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://tanbirramim.github.io/callm/" rel="noopener noreferrer"&gt;https://tanbirramim.github.io/callm/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Author: Tanbir Hossain Ramim&lt;/p&gt;

&lt;p&gt;It's MIT-licensed and pretty new. If you try it on a real workload and something breaks or feels off, open an issue — that kind of feedback is exactly what I need right now.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdlvsv5imozfr2mlg6ve.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdlvsv5imozfr2mlg6ve.gif" alt=" " width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
