Gemini 3.8 Flash Reasoning Effort: Balancing Latency and Costs in TypeScript

SitePoint TeamPublished inAI·Programming·
September 25, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
Most engineering teams treat large language model API calls as fixed-cost black boxes. A request goes out, tokens come back, and the bill arrives at the end of the month showing aggregate token counts with no per-request breakdown. Gemini 3.8 Flash changes this dynamic by exposing a thinkingLevel setting that gives developers programmatic control over reasoning effort on a per-request basis. This is the single most effective cost lever available in the Gemini 3.8 Flash API, yet few production systems take advantage of it.
What follows is a hands-on tutorial for building a TypeScript controller that dynamically allocates thinking levels, tracks token spend in real time, and cascades through fallback strategies when latency constraints are breached. The controller handles timeout-based cascade fallback, structured logging for cost attribution, and per-request spend tracking. The deliverable is a complete, shareable gemini-reasoning-controller.ts harness that readers can drop into existing projects and adapt to their own latency SLAs and cost targets.
Table of Contents
Prerequisites and Project Setup
Environment and Dependencies
The project requires Node.js 20 or later (for native ESM support, the performance global, and built-in fetch) and TypeScript 5.x. The primary SDK is @google/genai, Google’s official generative AI client library for Node.js. Manage environment variables with dotenv. You need a valid Google AI Studio API key, obtainable at no cost from the Google AI Studio console.
Scaffolding the Project
The project uses ECMAScript modules with strict TypeScript compilation. Below is the baseline scaffold.
{"name":"gemini-reasoning-controller","type":"module","scripts":{"build":"tsc","demo":"tsc && node dist/demo.js"},"dependencies":{"@google/genai":"^1.0.0","dotenv":"^16.4.0","zod":"^3.23.0"},"devDependencies":{"typescript":"^5.5.0","@types/node":"^20.0.0"}}// tsconfig.json{"compilerOptions": {"target": "ES2022","module": "NodeNext","moduleResolution": "NodeNext","outDir": "dist","rootDir": "src","strict": true,"esModuleInterop": true,"skipLibCheck": true},"include": ["src"]}Run npm install to pull dependencies. Create a .env file at the project root containing GEMINI_API_KEY=your_key_here. Immediately add .env to your .gitignore to prevent accidental key exposure. Never commit API keys to version control. The
Understanding Gemini 3.8 Flash Reasoning Effort Parameters
What thinkingLevel Controls
The thinkingLevel field lives inside the thinkingConfig object passed to a generateContent call. On Gemini 3.8 Flash it accepts one of three values: low, medium (the default), and high (verify the current level set against the Gemini API documentation, as supported levels vary across models). Earlier Flash generations exposed a numeric thinkingBudget instead; 3.8 Flash replaces that dial with discrete levels, and it does not accept the minimal level that some other Gemini 3 models support. At low, the model produces output with minimal internal chain-of-thought deliberation. At high, the model gets extensive room to reason through complex, multi-step problems before producing a final answer.
thinkingLevel is a guidance signal: the model calibrates its deliberation depth to the level, but consult the API documentation for current enforcement guarantees. Thinking tokens are billed as output tokens, so deeper reasoning directly raises the per-request price. Check the current Gemini pricing page for rates – at the time of writing, Gemini 3.8 Flash costs $0.75 per million input tokens and $3.75 per million output tokens (thinking included) through December 31, 2026.
The Three-Way Trade-Off: Quality vs. Latency vs. Cost
Higher thinking levels produce deeper chains of thought. For tasks that require multi-step reasoning, mathematical derivation, or complex code generation, this translates directly into higher accuracy. You pay more: additional thinking tokens increase both latency per request and your bill per million tokens.
Lower budgets work well for tasks where the model’s “fast path” is sufficient. Simple classification, entity extraction, and template-based generation rarely benefit from extended deliberation. Choosing the low thinking level for a sentiment classification request instead of high can cut both latency and cost without degrading classification accuracy.
Choosing the low thinking level for a sentiment classification request instead of high can cut both latency and cost without degrading classification accuracy.
The right budget depends entirely on task complexity. Multi-hop question answering and agentic tool selection show improved tool-selection accuracy and answer completeness at medium-to-high budgets. Straightforward extraction and routing decisions do well at the low end.
How Reasoning Effort Differs from Temperature and Top-K
Reasoning effort is orthogonal to sampling parameters like temperature and top-K. Temperature controls the randomness of token selection from the probability distribution. Top-K limits how many candidate tokens the model considers at each step. Neither affects how deeply the model reasons before it starts generating output tokens. The thinkingLevel controls the depth of internal deliberation, a separate axis entirely. A request can have a high thinking level with low temperature (deep reasoning, deterministic output) or a low thinking level with high temperature (shallow reasoning, creative output).
Building the Reasoning Controller Core
Defining the Configuration Interface
The controller needs a type-safe way to express reasoning tiers, each mapping to a specific thinking level and latency SLA.
import{ ThinkingLevel }from"@google/genai";exporttypeReasoningTier="low"|"medium"|"high";exportinterfaceTierConfig{thinkingLevel: ThinkingLevel;latencyLimitMs:number;}exportconstTIER_CONFIGS: Record<ReasoningTier, TierConfig>={low:{ thinkingLevel: ThinkingLevel.LOW, latencyLimitMs:3_000},medium:{ thinkingLevel: ThinkingLevel.MEDIUM, latencyLimitMs:8_000},high:{ thinkingLevel: ThinkingLevel.HIGH, latencyLimitMs:15_000},};exportinterfaceControllerConfig{apiKey:string;model:string;maxCascadeDepth:number;}exportinterfaceRequestContext{prompt:string;initialTier: ReasoningTier;systemInstruction?:string;}exportinterfaceSpendRecord{tier: ReasoningTier;promptTokens:number;candidateTokens:number;thinkingTokens:number;totalTokens:number;latencyMs:number;estimatedCostUsd:number;}The tier definitions map each tier to a ThinkingLevel enum value from the SDK: low to ThinkingLevel.LOW, medium to ThinkingLevel.MEDIUM, and high to ThinkingLevel.HIGH. Each tier also carries a latency limit that drives the fallback cascade logic.
Initializing the Gemini Client with Dynamic Thinking Levels
The client factory creates a GoogleGenAI instance and exposes a function that injects the thinking level dynamically at call time.
import{ GoogleGenAI }from"@google/genai";import{ ReasoningTier,TIER_CONFIGS}from"./types.js";exportfunctioncreateGeminiClient(apiKey:string){returnnewGoogleGenAI({ apiKey });}exportasyncfunctiongenerateWithBudget(client: GoogleGenAI,model:string,prompt:string,tier: ReasoningTier,systemInstruction?:string,signal?: AbortSignal){const{ thinkingLevel }=TIER_CONFIGS[tier];const response =await client.models.generateContent({model,contents: prompt,config:{systemInstruction,thinkingConfig:{ thinkingLevel },},...(signal ?{ signal }:{}),});const text =typeof response.text ==="function"? response.text():(response.text ??"");return{...response, resolvedText: text };}Every call to generateWithBudget reads the level from the tier configuration and passes it through thinkingConfig.thinkingLevel. The signal parameter threads into the generateContent call so that AbortController-based timeouts can cancel in-flight requests. The response text resolves defensively, handling both method and property forms of response.text across SDK versions. Pass a tier name, get a response calibrated to that depth of reasoning.
Note: Aborting the request prevents your code from receiving the response but does not guarantee the API stops processing server-side. You may still be billed for tokens generated before the connection closes. Verify abort behavior with your SDK version.
Token Spend Tracking
The TokenLedger class extracts usageMetadata from each response and accumulates spend across the controller’s lifetime.
import{ SpendRecord, ReasoningTier }from"./types.js";interfaceUsageMetadata{promptTokenCount?:number;candidatesTokenCount?:number;thoughtsTokenCount?:number;totalTokenCount?:number;}exportclassTokenLedger{private records: SpendRecord[]=[];constructor(privatereadonly pricing ={inputPerM:0.75,outputPerM:3.75,thinkingPerM:3.75,}){}record(tier: ReasoningTier,usageMetadata: UsageMetadata |undefined,latencyMs:number): SpendRecord {const promptTokens = usageMetadata?.promptTokenCount ??0;const candidateTokens = usageMetadata?.candidatesTokenCount ??0;const thinkingTokens = usageMetadata?.thoughtsTokenCount ??0;const totalTokens = usageMetadata?.totalTokenCount ??0;if(promptTokens ===0&& candidateTokens ===0&& thinkingTokens ===0){console.warn(JSON.stringify({event:"ledger_zero_tokens",tier,warning:"All token counts are zero — verify usageMetadata field names",receivedKeys: usageMetadata ? Object.keys(usageMetadata):null,}));}const estimatedCostUsd =(promptTokens /1_000_000)*this.pricing.inputPerM +(candidateTokens /1_000_000)*this.pricing.outputPerM +(thinkingTokens /1_000_000)*this.pricing.thinkingPerM;const entry: SpendRecord ={tier, promptTokens, candidateTokens,thinkingTokens, totalTokens, latencyMs, estimatedCostUsd,};this.records.push(entry);return entry;}getReport(){const totalCost =this.records.reduce((s, r)=> s + r.estimatedCostUsd,0);const totalTokens =this.records.reduce((s, r)=> s + r.totalTokens,0);return{ totalCost, totalTokens, callCount:this.records.length, records:this.records };}}The ledger parses promptTokenCount, candidatesTokenCount, and thoughtsTokenCount from the response’s usageMetadata object. A typed UsageMetadata interface replaces the previous untyped any parameter, catching incorrect field names at compile time. When all token counts are zero, the ledger emits a warning with the actual keys received, making silent data-loss conditions immediately visible. Cost estimation uses per-token pricing constants computed from the disaggregated fields only, to avoid double-counting thinking tokens that totalTokenCount may already include. Update these pricing constants to match current published rates, as Google adjusts them periodically.
Implementing Latency-Aware Fallback Cascades
The Cascade Strategy
The cascade pattern starts at the highest appropriate reasoning tier for a given request. If the response exceeds the tier’s latency SLA, the controller retries with the next lower tier. With the default maxDowngrades of 2, a request starting at high can fall through medium to low before returning whatever result is available. This prevents runaway retry loops while still giving the system room to find a workable trade-off.
Note on rate limits: Each cascade attempt is a separate billable API call. If your workload triggers cascades frequently, account for the multiplied request volume against your API quota. Implement backoff or circuit-breaking if you observe 429 Too Many Requests responses.
Timeout Mechanics with AbortController
Native AbortController combined with setTimeout enforces per-call latency budgets. When a timeout fires, the abort signal cancels the in-flight request.
import{ GoogleGenAI }from"@google/genai";import{ generateWithBudget }from"./client.js";import{ TokenLedger }from"./ledger.js";import{ ReasoningTier,TIER_CONFIGS}from"./types.js";constTIER_ORDER: ReasoningTier[]=["high","medium","low"];functionnextLowerTier(current: ReasoningTier): ReasoningTier |null{const idx =TIER_ORDER.indexOf(current);return idx <TIER_ORDER.length -1?TIER_ORDER[idx +1]:null;}exportasyncfunctionexecuteWithCascade(client: GoogleGenAI,model:string,prompt:string,initialTier: ReasoningTier,ledger: TokenLedger,maxDowngrades:number=2,systemInstruction?:string):Promise<{ text:string; record:import("./types.js").SpendRecord; cascadePath: ReasoningTier[]}>{let tier: ReasoningTier |null= initialTier;const cascadePath: ReasoningTier[]=[];let downgrades =0;while(tier !==null&& downgrades <= maxDowngrades){cascadePath.push(tier);const{ latencyLimitMs }=TIER_CONFIGS[tier];const controller =newAbortController();const timeout: ReturnType<typeof setTimeout>=setTimeout(()=> controller.abort(), latencyLimitMs);const start = performance.now();try{const response =awaitgenerateWithBudget(client, model, prompt, tier, systemInstruction, controller.signal);clearTimeout(timeout);const latencyMs = Math.round(performance.now()- start);const record = ledger.record(tier, response.usageMetadata, latencyMs);try{console.log(JSON.stringify({event:"cascade_attempt", tier,thinkingLevel:TIER_CONFIGS[tier].thinkingLevel,latencyMs, tokens: record.totalTokens, status:"success",}));}catch(_){}return{ text: response.resolvedText, record, cascadePath };}catch(err:unknown){clearTimeout(timeout);const latencyMs = Math.round(performance.now()- start);const reason =err instanceofError&& err.name ==="AbortError"?"timeout":"error";try{console.log(JSON.stringify({event:"cascade_attempt", tier,thinkingLevel:TIER_CONFIGS[tier].thinkingLevel,latencyMs, status:"failed", reason,}));}catch(_){}tier =nextLowerTier(tier);downgrades++;}}thrownewError(`Cascade exhausted after${downgrades}downgrades from${initialTier}`);}Logging and Observability
Each attempt emits a structured JSON log line containing the tier, thinking level, measured latency in milliseconds, token counts, and fallback reason. Structured logs integrate directly with aggregation systems like Datadog, Grafana Loki, or CloudWatch. Without per-request cost attribution, teams cannot answer the question “which reasoning tier is responsible for this month’s bill?”
Structured Tool Call Verification
Why Reasoning Effort Affects Tool Use Reliability
Higher thinking levels tend to improve the model’s ability to correctly select tools from a provided schema and populate their parameters accurately. At the low level, the model may partially populate tool call schemas, assign wrong parameter types, or hallucinate tool names that do not exist in the declared set. Reduced deliberation means fewer internal tokens available for the model to match the user’s intent to the right tool and construct valid arguments.
Validating Tool Call Output with Zod
Zod provides runtime schema validation that catches malformed tool calls before they reach downstream systems. If validation fails, the controller escalates to a higher reasoning tier, implementing an inverse cascade (the opposite of the latency-driven downgrade cascade; here the system moves to higher tiers on failure).
import{ z }from"zod";import{ GoogleGenAI }from"@google/genai";import{ generateWithBudget }from"./client.js";import{ TokenLedger }from"./ledger.js";import{ ReasoningTier,TIER_CONFIGS}from"./types.js";constTIER_ORDER: ReasoningTier[]=["low","medium","high"];functionnextHigherTier(current: ReasoningTier): ReasoningTier |null{const idx =TIER_ORDER.indexOf(current);return idx <TIER_ORDER.length -1?TIER_ORDER[idx +1]:null;}const WeatherToolSchema = z.object({name: z.literal("getWeather"),args: z.object({location: z.string().min(1),unit: z.enum(["celsius","fahrenheit"]),}),});exportasyncfunctionverifyToolCall(client: GoogleGenAI,model:string,prompt:string,startTier: ReasoningTier,ledger: TokenLedger,maxEscalations:number=2){let tier: ReasoningTier |null= startTier;let attempts =0;while(tier !==null&& attempts < maxEscalations){const{ latencyLimitMs }=TIER_CONFIGS[tier];const abortController =newAbortController();const timeout: ReturnType<typeof setTimeout>=setTimeout(()=> abortController.abort(), latencyLimitMs);const start = performance.now();try{const response =awaitgenerateWithBudget(client, model, prompt, tier,undefined, abortController.signal);clearTimeout(timeout);const latencyMs = Math.round(performance.now()- start);ledger.record(tier, response.usageMetadata, latencyMs);const rawFunctionCall =response.functionCalls?.[0]??(response asany).candidates?.[0]?.content?.parts?.find((p:any)=> p.functionCall !=null)?.functionCall;if(rawFunctionCall){const parsed = WeatherToolSchema.safeParse({name: rawFunctionCall.name,args: rawFunctionCall.args,});if(parsed.success){return{ toolCall: parsed.data, tier, attempts };}console.warn(JSON.stringify({event:"tool_validation_failed",tier,attempt: attempts,errors: parsed.error.issues,}));}else{console.warn(JSON.stringify({event:"no_function_call_returned",tier,attempt: attempts,}));}}catch(err:unknown){clearTimeout(timeout);const reason =err instanceofError&& err.name ==="AbortError"?"timeout":"error";console.warn(JSON.stringify({event:"tool_attempt_failed", tier, attempt: attempts, reason,}));}tier =nextHigherTier(tier);attempts++;}thrownewError(`Tool call validation failed after${attempts}escalations`);}The reasoning level acts as a quality gate here. Instead of always paying for maximum reasoning, the system starts cheap and escalates only when the output fails validation. The function call extraction attempts both the top-level response.functionCalls path and the nested response.candidates[0].content.parts path as a fallback, since the exact field location may vary across SDK versions. Note that the WeatherToolSchema is hardcoded here for demonstration; replace it with your own z.ZodSchema for other tool types.
Instead of always paying for maximum reasoning, the system starts cheap and escalates only when the output fails validation.
Putting It All Together: The Complete Harness
The gemini-reasoning-controller.ts Module
The full assembly integrates dynamic thinking level allocation, cascade fallbacks, tool call verification, and token spend tracking into a single exported class.
import{ GoogleGenAI }from"@google/genai";import{ createGeminiClient }from"./client.js";import{ TokenLedger }from"./ledger.js";import{ executeWithCascade }from"./cascade.js";import{ verifyToolCall }from"./tool-verify.js";import{ ControllerConfig, ReasoningTier, RequestContext }from"./types.js";exportclassReasoningController{private client: GoogleGenAI;private ledger =newTokenLedger();private config: ControllerConfig;constructor(config: ControllerConfig){this.config = config;this.client =createGeminiClient(config.apiKey);}asyncrun(ctx: RequestContext){returnexecuteWithCascade(this.client,this.config.model, ctx.prompt,ctx.initialTier,this.ledger,this.config.maxCascadeDepth,ctx.systemInstruction);}asyncrunWithTools(prompt:string, startTier: ReasoningTier ="low"){returnverifyToolCall(this.client,this.config.model, prompt,startTier,this.ledger);}getSpendReport(){returnthis.ledger.getReport();}}export{ ReasoningTier, RequestContext, ControllerConfig }from"./types.js";The exported API surface is intentionally small: run() for standard text generation with cascade fallback, runWithTools() for tool-calling tasks with quality-gated escalation, and getSpendReport() for cumulative cost tracking. Callers are responsible for loading environment variables (e.g., in their own entrypoint before instantiating the controller
Running the Demo
import"dotenv/config";import{ ReasoningController }from"./gemini-reasoning-controller.js";const apiKey = process.env.GEMINI_API_KEY;if(!apiKey || apiKey.trim()===""){console.error("[fatal] GEMINI_API_KEY is not set. "+"Create a .env file with GEMINI_API_KEY=your_key_here and retry.");process.exit(1);}const controller =newReasoningController({apiKey,model:"gemini-3.8-flash",maxCascadeDepth:2,});asyncfunctionmain(){const r1 =await controller.run({prompt:"Classify this review as positive or negative: 'The battery life is incredible.'",initialTier:"low",});console.log(`[Classification] Tier path:${r1.cascadePath}| Result:${r1.text.trim()}`);const r2 =await controller.run({prompt:"A train leaves city A at 60 km/h. Another leaves city B, 300 km away, at 90 km/h toward A. When and where do they meet?",initialTier:"high",});console.log(`[Reasoning] Tier path:${r2.cascadePath}| Result:${r2.text.trim()}`);const r3 =await controller.runWithTools("What is the weather in Tokyo in celsius?","low");console.log(`[Tool Call] Resolved at tier:${r3.tier}after${r3.attempts}attempts`);const report = controller.getSpendReport();console.log(`[Spend Report] Calls:${report.callCount}| Total tokens:${report.totalTokens}| Est. cost: $${report.totalCost.toFixed(6)}`);}main().catch(console.error);Running npm run demo produces structured log output for each cascade attempt followed by the results. The classification task should resolve immediately at low. The reasoning task exercises the high tier. The tool call demonstrates the inverse cascade, starting at low and escalating if Zod validation rejects the output.
Before first run: Log the raw response.usageMetadata object from a test call to confirm the exact field names (thoughtsTokenCount vs. thinkingTokenCount, candidatesTokenCount vs. candidateTokenCount, etc.). Incorrect field names silently return undefined, causing all token tracking to report zeros.
Benchmarking Results and Cost Analysis
Latency by Reasoning Tier
The following table shows hypothetical example values for orientation only. These are not reproducible benchmarks. Run your own measurements against representative workloads before making production decisions.
| Tier | Thinking Level | Median Latency (ms) | P95 Latency (ms) | Avg Thinking Tokens | Avg Total Tokens |
|---|---|---|---|---|---|
| low | ThinkingLevel.LOW | 620 | 1,100 | 45 | 180 |
| medium | ThinkingLevel.MEDIUM | 1,800 | 3,400 | 850 | 1,200 |
| high | ThinkingLevel.HIGH | 4,200 | 7,500 | 3,100 | 3,800 |
Cost Implications at Scale
All cost figures in this section use illustrative pricing. Verify current rates at ai.google.dev/gemini-api/docs/pricing before using these calculations in production.
Consider a production scenario processing 100,000 requests per day with the following tier distribution: 70% low, 20% medium, 10% high. Using the token averages from the table above, the daily output token consumption is approximately (70,000 x 180) + (20,000 x 1,200) + (10,000 x 3,800) = 74,600,000 tokens. At $3.75/M output tokens, output costs total ~$279.75/day. Input token costs must be added separately; for example, at $0.75/M input tokens, 100,000 requests with a 200-token prompt add approximately $15.00/day, for a revised daily total of ~$294.75.
A flat approach processing all 100,000 requests at the high level would consume approximately 100,000 x 3,800 = 380,000,000 output tokens, costing roughly $1,425/day in output tokens alone (plus input costs). The tiered approach costs roughly 5x less than the flat high-level strategy ($294.75 vs. ~$1,440 with input). The exact ratio depends on your actual token volumes and current pricing.
The tiered approach costs roughly 5x less than running every request at the high level. The exact ratio depends on your actual token volumes and current pricing.
When to Pin a Tier vs. Use Dynamic Cascades
Deterministic, well-scoped workloads like classification, entity extraction, and format conversion should be pinned to low. Output quality for these tasks does not improve meaningfully with additional reasoning tokens. Variable-complexity workloads tell a different story. User-facing chat, agentic loops, and tasks where prompt complexity is unpredictable all benefit from letting the cascade find the right tier dynamically.
Common Mistakes
Earlier Flash generations allowed disabling thinking entirely with a zero numeric budget. Gemini 3.8 Flash does not support that: the lowest level available on this model is low, and the minimal level exists only on certain other Gemini 3 models. For the simplest tasks, low is fine. For anything requiring multi-step reasoning, expect noticeably worse results. Teams that pin every request to low and still expect chain-of-thought quality will be disappointed.
Budget overruns frequently trace back to ignoring thinking tokens in cost projections. Thinking tokens are billed at the output token rate, and at higher levels they can dominate the total token count. Always check the current pricing page before projecting spend.
A subtler failure mode: overly aggressive timeouts. If your latency limits cut off medium and high tiers before they can complete, the cascade degrades to low on every request, effectively negating the entire tiered strategy. Base your latency limits on measured P95 values, not optimistic guesses.
Where to Go from Here
Tune reasoning effort per request, not globally. The gemini-reasoning-controller.ts harness gives teams a working cascade loop and spend ledger to extend. Natural next steps include combining reasoning effort control with streaming responses for time-to-first-token optimization and integrating Gemini’s grounding capabilities for retrieval-augmented workloads.
Sharing our passion for building incredible internet things.


