In-App Chatbot API: Nodejs Context Windows and JSON Portability A developer outlines an adapter-based architecture for in-app invoice extraction that places OpenAI, Anthropic Claude, Google Gemini, and OpenRouter behind a single application-owned interface, keeping the invoice schema and validation in the application rather than the model. The approach runs one recorded conversation unchanged against every candidate provider and selects on accepted JSON extractions rather than token pricing or advertised context windows, requiring two adapters to pass before the contract is considered portable and allowing exactly one structure-repair attempt per extraction. For supplier invoice extraction, an in-app chatbot should put OpenAI, Claude, and every other AI API behind one application-owned adapter. Run one recorded conversation unchanged against every candidate, then select on accepted JSON extractions, not token pricing or an advertised context window. | Architecture | Portability | Maintenance | Use it when | |---|---|---|---| | Thin application adapter | High | Moderate | One SaaS owns the workflow | | Self-hosted gateway behind an adapter | High | Higher | Several apps need shared policy | | Provider-specific integration | Low | Low at first | One verified capability is mandatory | Recommendation: keep the invoice schema and validation in the application. Put model IDs, limits, and request-envelope differences inside adapters. This spends a solo founder's scarce hours on review UX and extraction quality while leaving inference as a replaceable dependency. The model should not define the invoice record. The application should. A useful record might contain supplierName , invoiceNumber , currency , and totalMinor . A chat turn may clarify a missing currency, but it must not change field names because an API uses a different structured-output envelope. Keep the conversation, extraction request, and normalized result separate. Conversation messages are user-facing history. The request contains only evidence needed for this turn. The result is domain data validated before it reaches reconciliation or a database. Small wins here. type Message = { role: "system" | "user" | "assistant"; content: string; }; type InvoiceExtraction = { supplierName: string | null; invoiceNumber: string | null; currency: string | null; totalMinor: number | null; }; type ExtractResult = { data: InvoiceExtraction; usage: { inputTokens: number; outputTokens: number } | null; finishReason: "complete" | "length" | "blocked" | "unknown"; }; interface InvoiceModel { extract messages: Message : Promise