Close Menu
ToolTechBlogToolTechBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    An honest comparison for recruiting teams

    September 12, 2026

    OpenAI’s Sam Altman says it would be ‘ill-advised’ to go public in 2026

    September 12, 2026

    Is a 256GB SSD better than a 1TB hard drive? It depends how you’re using it

    September 12, 2026
    Facebook X (Twitter) Instagram
    ToolTechBlogToolTechBlog
    • Home
    • AI Tools
    • Web Hosting
    • Tech
    • Digital Marketing
    • Business Software
    • VPN & Cybersecurity
    ToolTechBlogToolTechBlog
    Home»Web Hosting»Emulating OpenAI Locally for Reliable AI Development
    Web Hosting

    Emulating OpenAI Locally for Reliable AI Development

    Tool Tech TeamBy Tool Tech TeamAugust 5, 2026No Comments12 Mins Read
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr WhatsApp Email
    Emulating OpenAI Locally for Reliable AI Development
    Share
    Facebook Twitter LinkedIn Pinterest Telegram Email

    Rubberduck: Emulating OpenAI Locally for Reliable AI Development

    SitePoint Team

    SitePoint TeamPublished inAI·JavaScript·APIs·
    July 24, 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.

    Developers building AI-powered features face a persistent set of friction points: unpredictable API costs, rate limits that interrupt flow, non-deterministic responses that break tests, and network dependencies that make CI/CD pipelines fragile. Rubberduck addresses these problems directly by providing a local OpenAI API emulator that returns deterministic, configurable responses, enabling teams to build and test AI integrations without external dependencies.

    This article walks through setting up Rubberduck in a JavaScript project, configuring response fixtures, integrating it into development and testing workflows, and handling advanced scenarios like error simulation and streaming.

    Table of Contents

    Why Local AI Emulation Matters

    The Problem with Developing Against Live APIs

    Every call to the OpenAI API during development costs money. For teams iterating rapidly on prompts, refining UI components that consume completions, or running test suites that exercise AI-dependent code paths, a 50-developer team can burn through a $500 monthly budget in days. Rate limits compound the problem. OpenAI enforces per-minute and per-day request caps that can stall a developer mid-session, particularly on shared API keys across a team.

    Beyond cost, the non-deterministic nature of language model responses makes automated testing unreliable. The same prompt can return different wording, different formatting, or different token counts across calls. Tests that assert on response content become flaky by design. Network dependency adds another layer of fragility: CI/CD pipelines that depend on an external service introduce a failure mode entirely outside the team’s control. For organizations handling sensitive data, sending prompts to an external API during development risks exposing PII or proprietary content to OpenAI’s servers. Note that Rubberduck mitigates this for the development phase only; staging and production environments that call the real API remain subject to OpenAI’s data use policies.

    The non-deterministic nature of language model responses makes automated testing unreliable. The same prompt can return different wording, different formatting, or different token counts across calls.

    What Rubberduck Does Differently

    Rubberduck is a lightweight, local HTTP server that emulates OpenAI’s REST API surface. Unlike generic mocking libraries that require developers to manually stub HTTP responses, or broad-purpose stub servers that need extensive configuration to match a specific API’s contract, Rubberduck is purpose-built for OpenAI API compatibility. It speaks the same request and response format, supports the same endpoint paths, and can be dropped into any project that already uses the OpenAI API with minimal wiring.

    • API-compatible: no changes to application code required.
    • Minimal-config for basic usage: a single configuration file is all you need; start it and point your client at localhost.
    • Deterministic: you configure responses, not generate them, so they return identically every time.

    Core Concepts of Rubberduck

    How the Emulation Layer Works

    Rubberduck runs as a local HTTP server, typically on a configurable port, that exposes endpoints mirroring OpenAI’s REST API. Its primary endpoint is /v1/chat/completions, but Rubberduck also supports /v1/embeddings and other endpoints in the OpenAI surface area. When your application sends a request to Rubberduck, the emulator inspects the request body, matches it against configured response fixtures, and returns a response formatted identically to what the real OpenAI API would return, including the expected JSON structure with id, object, choices, usage, and other fields.

    This format fidelity means that any client code using the OpenAI SDK or raw HTTP requests works without modification. Your application cannot distinguish between a Rubberduck response and a real one at the schema level.

    Deterministic vs. Dynamic Response Modes

    Rubberduck supports two primary response modes. In fixed response mode, every request to a matched endpoint returns the exact same configured response. This is ideal for unit tests where assertions depend on specific output. In template-based response mode, responses can include variable substitution, pulling values from the incoming request (such as echoing parts of the prompt or varying based on the specified model). This mode suits development scenarios where some variability helps exercise UI rendering or downstream processing without introducing true non-determinism.

    You configure responses in a project-level config file, giving teams version-controlled, reviewable response definitions.

    const{ spawn }=require("child_process");asyncfunctionmain(){const server =spawn("npx",["rubberduck","--port","4010"]);server.on("error",(err)=>{thrownewError(`Failed to start Rubberduck:${err.message}`);});server.stderr.on("data",(data)=>{console.error("[rubberduck stderr]", data.toString());});awaitnewPromise((resolve)=>setTimeout(resolve,1000));const controller =newAbortController();const timeoutId =setTimeout(()=> controller.abort(),15_000);try{const response =awaitfetch("http://localhost:4010/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer${process.env.OPENAI_API_KEY||"dev-placeholder-key"}`,},body:JSON.stringify({model:"gpt-4",messages:[{role:"user",content:"Summarize this document."}],temperature:0.7,}),signal: controller.signal,});if(!response.ok){const body =await response.text();thrownewError(`OpenAI API error${response.status}:${body}`);}const data =await response.json();console.log(data.choices[0].message.content);console.log(data.usage);}finally{clearTimeout(timeoutId);server.kill();}}main();

    Notice that the response shape matches the real OpenAI API exactly: id, object, created, model, choices array with message objects, and usage statistics. Any code parsing real API responses works against this output without changes.

    Setting Up Rubberduck in a JavaScript Project

    Prerequisites

    • Node.js 18 or later (node --version to check). Native fetch and modern ES module features require Node 18+.
    • npm 8 or later.
    • Verify the package name on npmjs.com/package/rubberduck-openai before installing. If the package is not available or has been renamed, check the project’s official repository for current installation instructions.

    Installation and Initial Configuration

    Rubberduck installs as an npm package, typically as a dev dependency since it is only needed during development and testing. You will also need concurrently and start-server-and-test for the development and test scripts shown below.

    npminstall --save-dev rubberduck-openai concurrently start-server-and-testtouch rubberduck.config.js

    Your configuration file defines the port, default response behavior, and any response fixtures. A minimal setup looks like this:

    module.exports={port:4010,defaultModel:"gpt-4",defaultResponse:{content:"This is a default emulated response from Rubberduck.",role:"assistant",},usage:{prompt_tokens:10,completion_tokens:15,total_tokens:25,},};

    Note: Verify field names against the official Rubberduck configuration reference before use; the schema shown here reflects the version current at the time of writing and may change in future releases. The defaultModel field controls which model identifier appears in emulated responses. Use a model string matching whatever your application uses in production.

    Add convenience scripts to package.json so that starting the emulator is a single command:

    {"scripts":{"rubberduck":"rubberduck --config rubberduck.config.js","dev":"concurrently "npm run rubberduck" "npm run start"","test:integration":"start-server-and-test "rubberduck --config rubberduck.config.js" http://localhost:4010/v1/chat/completions "vitest run""}}

    Running dev starts Rubberduck alongside your application using concurrently, while test:integration uses start-server-and-test to start the emulator, wait until it is listening, run the test suite, and then shut it down. This approach works cross-platform (Linux, macOS, and Windows). Note that start-server-and-test polls the given URL with GET; ensure the URL you provide returns a 2xx response to GET requests. If /v1/chat/completions does not respond to GET, use a known health endpoint (e.g., http://localhost:4010/healthz) or the server root, depending on what your Rubberduck version supports.

    Configuring Custom Responses

    For projects with multiple AI features, each calling OpenAI with different prompts and expecting different response shapes, Rubberduck supports named response fixtures. Rubberduck matches these against incoming requests based on prompt content, model, or other request properties. Requests matching no fixture return the defaultResponse defined at the top level of the configuration.

    module.exports={port:4010,latency:200,defaultResponse:{content:"Default response when no fixture matches.",role:"assistant",},fixtures:[{match:{messageContains:"summarize"},response:{content:"The document discusses three main points: infrastructure scaling, cost optimization, and team velocity.",role:"assistant",},usage:{prompt_tokens:45,completion_tokens:22,total_tokens:67},latency:350,},{match:{messageContains:"classify"},response:{content:JSON.stringify({category:"technical",confidence:0.92,subcategories:["infrastructure","devops"],}),role:"assistant",},usage:{prompt_tokens:38,completion_tokens:18,total_tokens:56},},],};

    Request code triggers specific fixtures based on prompt content:

    const summaryResponse =awaitfetch("http://localhost:4010/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer${process.env.OPENAI_API_KEY||"dev-placeholder-key"}`,},body:JSON.stringify({model:"gpt-4",messages:[{role:"user",content:"Please summarize the following report..."},],}),});const classifyResponse =awaitfetch("http://localhost:4010/v1/chat/completions",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer${process.env.OPENAI_API_KEY||"dev-placeholder-key"}`,},body:JSON.stringify({model:"gpt-4",messages:[{role:"user",content:"Classify this support ticket into a category...",},],}),});

    Latency simulation adds realism. A global 200ms delay applies to all responses unless overridden at the fixture level, letting developers test loading states and timeout handling under conditions that approximate real network behavior without the unpredictability.

    Integrating Rubberduck into Your Development Workflow

    Swapping Between Local and Live APIs

    Use an environment variable to control the API base URL. Application code reads this variable and defaults to the local Rubberduck URL in development, switching to the real OpenAI endpoint in production.

    constOPENAI_API_BASE=process.env.OPENAI_API_BASE||"http://localhost:4010/v1";constOPENAI_API_KEY= process.env.OPENAI_API_KEY||"dev-placeholder-key";asyncfunctioncreateChatCompletion(messages, model ="gpt-4", timeoutMs =15_000){const controller =newAbortController();const timeoutId =setTimeout(()=> controller.abort(), timeoutMs);try{const response =awaitfetch(`${OPENAI_API_BASE}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer${OPENAI_API_KEY}`,},body:JSON.stringify({ model, messages }),signal: controller.signal,});if(!response.ok){const body =await response.text();thrownewError(`OpenAI API error${response.status}:${body}`);}return response.json();}finally{clearTimeout(timeoutId);}}module.exports={ createChatCompletion };

    Environment files make the toggle explicit:

    OPENAI_API_BASE=http://localhost:4010/v1OPENAI_API_KEY=dev-placeholder-keyOPENAI_API_BASE=https://api.openai.com/v1OPENAI_API_KEY=sk-your-real-key-here

    Security: Add .env.development and .env.production to .gitignore immediately. Never commit files containing real API keys to version control.

    Important: Node.js does not read .env files automatically. Load environment variables using dotenv (npm install dotenv, then add require('dotenv').config() at your application entry point) or the --env-file flag available in Node 20+ (e.g., node --env-file .env.development app.js).

    Zero code changes between environments. Your application code is identical regardless of whether it hits Rubberduck or OpenAI. This eliminates bugs from mock-specific code paths that diverge from production behavior.

    Writing Reliable Tests with Deterministic Responses

    Because Rubberduck returns the same response for the same matched request every time, tests can make exact assertions on AI-dependent function output without mocking.

    import{ describe, it, expect, beforeAll, afterAll }from"vitest";import{ createChatCompletion }from"../lib/openai-client.js";import{ spawn }from"child_process";let rubberduckProcess;asyncfunctionwaitForServer(baseUrl,{ timeout =10000, interval =250}={}){const healthUrl = baseUrl.replace(//v1/.*$/,"")+"/healthz";const start =Date.now();while(Date.now()- start < timeout){try{const res =awaitfetch(healthUrl,{method:"GET"});await res.text();if(res.ok)return;}catch{}awaitnewPromise((r)=>setTimeout(r, interval));}thrownewError(`Server at${healthUrl}did not become ready within${timeout}ms`);}beforeAll(async()=>{rubberduckProcess =spawn("npx",["rubberduck","--config","rubberduck.config.js",]);rubberduckProcess.on("error",(err)=>{thrownewError(`Failed to start Rubberduck:${err.message}`);});rubberduckProcess.stderr.on("data",(data)=>{console.error("[rubberduck stderr]", data.toString());});awaitwaitForServer("http://localhost:4010/v1/chat/completions",{timeout:10000,interval:250,});});afterAll(()=>{if(rubberduckProcess){rubberduckProcess.kill();}});describe("summarizer",()=>{it("returns a structured summary from the AI",async()=>{const result =awaitcreateChatCompletion([{role:"user",content:"Please summarize the following report..."},]);expect(result.choices[0].message.content).toContain("infrastructure scaling");expect(result.choices[0].message.content).toContain("cost optimization");expect(result.choices[0].message.role).toBe("assistant");expect(result.usage.total_tokens).toBe(67);});it("returns classification results as structured JSON",async()=>{const result =awaitcreateChatCompletion([{role:"user",content:"Classify this support ticket into a category"},]);const parsed =JSON.parse(result.choices[0].message.content);expect(parsed.category).toBe("technical");expect(parsed.confidence).toBeGreaterThan(0.9);expect(parsed.subcategories).toContain("infrastructure");});});

    These tests are fully repeatable, run offline, and execute in milliseconds. Snapshot testing also becomes

    Using Rubberduck in CI/CD Pipelines

    Running Rubberduck in CI requires starting the server before the test suite and tearing it down afterward. Most CI systems support background processes or service containers. The start-server-and-test-based test:integration script shown above works cross-platform and handles startup waiting and teardown automatically. For Docker-based pipelines, Rubberduck can run as a sidecar container exposing the configured port. The same configuration file committed to the repository ensures consistent behavior across every developer’s machine, CI runner, and staging environment. No external network access is required during the test phase.

    Advanced Configuration and Patterns

    Simulating Error Responses and Edge Cases

    AI integrations need to handle failures gracefully. Rubberduck supports error simulation through fixture configuration, allowing developers to test retry logic, fallback behavior, and error UI states.

    module.exports={port:4010,fixtures:[{match:{messageContains:"trigger-rate-limit"},error:{status:429,body:{error:{message:"Rate limit exceeded. Please retry after 20s.",type:"rate_limit_error",code:"rate_limit_exceeded",},},},},{match:{messageContains:"trigger-server-error"},error:{status:500,body:{error:{message:"Internal server error",type:"server_error",code:null,},},},},],};

    Application-side retry logic can then be tested deterministically:

    constOPENAI_API_BASE=process.env.OPENAI_API_BASE||"http://localhost:4010/v1";constOPENAI_API_KEY=process.env.OPENAI_API_KEY||"dev-placeholder-key";asyncfunctioncreateCompletionWithRetry(messages, retries =3){let lastError;for(let attempt =1; attempt <= retries; attempt++){const controller =newAbortController();const timeoutId =setTimeout(()=> controller.abort(),10_000);try{const response =awaitfetch(`${OPENAI_API_BASE}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer${OPENAI_API_KEY}`,},body:JSON.stringify({model:"gpt-4", messages }),signal: controller.signal,});if(response.status===429&& attempt < retries){await response.text();awaitnewPromise((r)=>setTimeout(r,1000* attempt));lastError =newError(`Rate limited on attempt${attempt}`);continue;}if(!response.ok){const body =await response.text();thrownewError(`API error${response.status}:${body}`);}returnawait response.json();}catch(err){lastError = err;if(attempt === retries)break;}finally{clearTimeout(timeoutId);}}throw lastError ??newError("All retry attempts exhausted");}module.exports={ createCompletionWithRetry };

    Tests can verify that the retry handler makes the expected number of attempts, respects backoff timing, and eventually throws the correct error.

    Streaming Response Emulation

    For applications consuming OpenAI’s streaming responses, Rubberduck supports chunked streaming emulation. The emulator sends response tokens incrementally, mimicking the data: {“choices”: [{“delta”: {“content”: “…”}}]} format that the real API uses with stream: true. This enables local testing of streaming UI components, token-by-token rendering, and stream parsing logic without any external dependency

    Note: Verify streaming support in the Rubberduck changelog or README before relying on this feature. The SSE format described above follows the OpenAI streaming specification, but confirm that your installed version of Rubberduck implements it.

    Multi-Model and Multi-Endpoint Scenarios

    Fixtures can be scoped by model name, allowing different response profiles for gpt-4, gpt-3.5-turbo (or its successor), or custom model identifiers. This is useful when an application uses different models for different tasks, such as a smaller, cheaper model for classification and a more capable model for generation. You can similarly configure the embeddings endpoint (/v1/embeddings) with fixed vector responses, supporting local testing of similarity search, RAG pipelines, and other embedding-dependent features. Organizing fixtures by feature or module keeps the configuration file manageable as the project grows.

    Implementation Checklist

    • Confirm Node.js 18+ and npm 8+ are installed
    • Verify rubberduck-openai package availability on npm
    • Install Rubberduck, concurrently, and start-server-and-test as dev dependencies
    • Create configuration file with base response fixtures
    • Set up environment variable switching (OPENAI_API_BASE)
    • Install and configure dotenv (or use Node 20+ --env-file) for .env loading
    • Add .env.development and .env.production to .gitignore
    • Abstract API base URL in your client module
    • Define response fixtures for each AI feature/prompt
    • Configure error simulation for retry/error handling tests
    • If your app uses streaming, add streaming fixtures and verify feature support in the Rubberduck docs
    • Write deterministic unit tests against emulated responses
    • Add Rubberduck startup to CI/CD pipeline scripts (using start-server-and-test for cross-platform support)
    • Configure latency simulation for performance testing
    • Document team setup instructions in project README
    • Periodically diff your fixtures against actual OpenAI response schemas to catch drift

    Limitations and When to Use the Real API

    What Rubberduck Doesn’t Cover

    Rubberduck does not perform language model inference. You configure responses; Rubberduck does not generate them. That means it cannot help with prompt engineering, evaluating output quality, or testing how different prompts affect model behavior. That work still requires the real OpenAI API. There is also a schema drift risk: if OpenAI updates their API response format, you must update fixtures manually to stay in sync. Teams should periodically validate that their fixtures match the current production API schema.

    A Balanced Testing Strategy

    Layer Rubberduck into a broader strategy. Use it for local development, unit tests, and CI pipelines where speed, cost, and determinism matter most. Use the real OpenAI API for integration validation, prompt tuning, and staging environments where you need to observe actual model behavior. This layered approach cuts feedback loops from seconds (live API round-trips) to milliseconds (local fixture returns) during daily development, while preserving confidence in real-world behavior through periodic live validation.

    This layered approach cuts feedback loops from seconds (live API round-trips) to milliseconds (local fixture returns) during daily development, while preserving confidence in real-world behavior through periodic live validation.

    Summary

    Rubberduck lets teams develop and test against the OpenAI API locally, with deterministic responses, zero cost, and no network dependency. For teams adopting it, the implementation checklist above provides a concrete starting point for integrating Rubberduck into both development environments and CI/CD pipelines.

    Sharing our passion for building incredible internet things.

    Development Emulating Locally OpenAI Reliable
    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Tool Tech Team
    • Website

    Related Posts

    An honest comparison for recruiting teams

    September 12, 2026

    Power Up Your AI Agent With Live Web Search, for Fewer Tokens

    September 12, 2026

    8 competitor analysis tools, mapped to the workflow that actually uses them (2026)

    September 11, 2026

    A Developer’s Look at Integrating AI Speech Into Applications

    September 11, 2026

    Build a Rust AI Agent Gateway with Tokio and Axum

    September 10, 2026

    Which AI recruiting tool fits your team in 2026?

    September 10, 2026
    Leave A Reply Cancel Reply

    Top posts
    Web Hosting

    An honest comparison for recruiting teams

    By Tool Tech Team
    AI Tools

    OpenAI’s Sam Altman says it would be ‘ill-advised’ to go public in 2026

    By Tool Tech Team
    Tech

    Is a 256GB SSD better than a 1TB hard drive? It depends how you’re using it

    By Tool Tech Team
    Editors Picks

    An honest comparison for recruiting teams

    September 12, 2026

    OpenAI’s Sam Altman says it would be ‘ill-advised’ to go public in 2026

    September 12, 2026

    Is a 256GB SSD better than a 1TB hard drive? It depends how you’re using it

    September 12, 2026

    Y Combinator’s Garry Tan wants US open-weight AI labs to ‘distill’ frontier models, too

    September 12, 2026
    About Us

    Welcome to ToolTechBlog, your trusted source for the latest insights, reviews, and practical guides on AI tools, business software, cybersecurity, web hosting, and consumer technology.
    Our mission is simple: to help individuals, entrepreneurs, freelancers, students, and businesses discover the right digital tools to improve productivity, streamline workflows, and make informed technology decisions.

    Our Picks

    An honest comparison for recruiting teams

    September 12, 2026

    OpenAI’s Sam Altman says it would be ‘ill-advised’ to go public in 2026

    September 12, 2026

    Is a 256GB SSD better than a 1TB hard drive? It depends how you’re using it

    September 12, 2026
    Top Reviews

    The AI Hype Index: Unsexy AI

    July 29, 2026

    What it is and How to Fix it

    July 29, 2026

    LG to Ban Residential Proxies from Smart TV Apps

    July 29, 2026

    © 2026 tooltechblog.com. All rights reserved. Designed by DD.

    • About Us
    • Contact Us
    • Terms and Conditions
    • Privacy Policy
    • Disclaimer

    Type above and press Enter to search. Press Esc to cancel.