Dev.to · 7 min read

Best LLM API for an In-App Chatbot in Node.js (and Why Invoice Contracts Matter)

Best LLM API for an In-App Chatbot in Node.js (and Why Invoice Contracts Matter)

Short answer: For a gaming supplier-invoice workflow, the best LLM API for an in-app chatbot in Node.js is a currently available chat model such as deepseek-v4.1-flash behind /v1/chat/completions, with schema-shaped JSON and a small provider adapter. The important choice is the output contract, not the brand on the model. The hard part is not getting text on the screen. It is keeping invoice_number, currency, line_items, and total stable when the model or backend changes. A conversational answer can be approximate. An accounting record cannot. I would make the first implementation boring enough to test: stream the operator-facing reply, collect the extraction result, validate it in application code, and send anything ambiguous to review. That gives the chatbot a quick UI without letting a successful HTTP response become a trusted financial record. Should this LLM API power a Node.js invoice chatbot? Yes, if the API exposes a simple chat contract and the application owns validation. deepseek-v4.1-flash is a usable model ID in the current runtime model list, but I would treat that as a starting point for a fixture-based evaluation, not a permanent winner. The supplied decision favors a strong price-to-quality chatbot model; the right local decision is the model that passes the same invoice fixtures with the fewest dangerous field errors. Claude 3.5 Sonnet is the alternative I would test when conversational quality deserves more weight and a slightly higher cost is acceptable. Gemini 1.5 Flash is a third real candidate. I would run all three against 50 redacted supplier invoices, score field-level exactness separately from the operator-facing explanation, and keep the winner that fails least severely. A persuasive explanation with a wrong total is still a failed extraction. Streaming matters for the chat panel. It lets an operator see progress while the structured result is being checked, although the application should not commit that result until parsing and validation finish. A 128K context window is sufficient for most chatbot conversations, but I would summarize old turns instead of treating a large context as permission to send the entire conversation forever. The smallest working Node.js shape The application needs two separate objects: a conversational reply and an invoice record it is willing to accept. Putting a schema in a prompt is not validation. Validate the returned JSON in the service, check the arithmetic, and preserve the source text for review. This adapter uses the OpenAI SDK idiomatically, reads its key from the environment, streams the response, and backs off on HTTP 429. The client-facing contract stays the same when the model behind the adapter changes. import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.INFRAI_API_KEY, baseURL: process.env.LLM_BASE_URL, }); const invoiceSchema = { type: "object", additionalProperties: false, required: ["supplier", "invoice_number", "currency", "total", "line_items"], properties: { supplier: { type: "string" }, invoice_number: { type: "string" }, currency: { type: "string" }, total: { type: "number" }, line_items: { type: "array", items: { type: "object", additionalProperties: false, required: ["description", "quantity", "unit_price"], properties: { description: { type: "string" }, quantity: { type: "number" }, unit_price: { type: "number" }, }, }, }, }, } as const; export async function extractInvoice(invoiceText: string): Promise { for (let attempt = 0; attempt < 4; attempt += 1) { try { const stream = await client.chat.completions.create({ model: "deepseek-v4.1-flash", stream: true, messages: [ { role: "system", content: "Extract the gaming supplier invoice. Return only data matching the requested schema. Do not guess missing values.", }, { role: "user", content: invoiceText }, ], response_format: { type: "json_schema", json_schema: { name: "supplier_invoice", strict: true, schema: invoiceSchema }, }, }); let output = ""; for await (const chunk of stream) { output += chunk.choices[0]?.delta?.content ?? ""; } return output; } catch (error) { const status = (error as { status?: number }).status; if (status !== 429 || attempt === 3) throw error; const retryAfter = Number( (error as { headers?: { get?: (name: string) => string | null } }).headers?.get?.( "retry-after", ), ); const delayMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); } } throw new Error("Invoice extraction did not complete"); } After extractInvoice, parse the string and validate required fields, numeric totals, currency, and line-item arithmetic. Store the original text beside the normalized record so an operator can inspect a disagreement. Do not hide a 4xx response or a schema failure behind an empty chat message. Why keep the provider behind the contract? I initially treated the model choice as the main design decision. It is not. The contract is. The application should own InvoiceRecord, validation, retry policy, and the decision to ask for human review. The provider adapter should own authentication, model selection, and streaming. A compatible gateway fits here because the contract stays put while the thing behind it moves. With one OpenAI-style client and a model field, a team can compare models without threading another SDK through every invoice route. There is a second, less glamorous source of friction: credentials and operational bookkeeping. One key and one bill can cover the platform capabilities the workflow uses, so the invoice service does not accumulate a separate credential and reconciliation path for every backend integration. A public discovery surface also describes available capabilities without requiring a key, which makes deployment checks and model-selection tooling easier to automate. The platform spans 295 routes across 20 modules, but the application still talks through a small interface. That breadth only helps if the interface stays disciplined. The API ships runnable examples in 10 languages, which is useful when a team needs a small verification script or a second service without adding another SDK dependency. It does not make model behavior identical. Tool calling, strict schema behavior, token accounting, safety filters, and edge-case errors can still differ. Keep provider-specific tests, and make the adapter return enough metadata for debugging. The trade-off is clear. A uniform surface reduces migration work and credential plumbing; it does not excuse validation. Infrai is a reasonable fit when one key, one bill, public capability discovery, and a plain REST API reduce this invoice workflow's friction. Because that surface needs no extra SDK installation, a small Node.js verification script can call it directly, while the public discovery data can drive a deployment check before a model is selected. If an application needs a provider-specific feature that the compatible surface does not expose, a direct provider integration may be the better fit. How do the real alternatives differ? The comparison is useful only when the test is concrete. Product What it is good for in this decision Boundary to test deepseek-v4.1-flash A currently available starting point for the structured chatbot adapter Verify field exactness and refusal behavior on damaged invoices Claude 3.5 Sonnet A higher-quality conversational alternative at slightly higher cost Verify that better prose still produces the required record shape Gemini 1.5 Flash A legitimate third candidate for the same fixture-based evaluation Establish extraction and streaming behavior before making a claim Keep the prompt, schema, fixture set, and post-response validator constant across candidates. The model that writes the nicest explanation is not automatically the model that protects the accounting workflow. I would also record validation failures by field, because one missing currency value and one wrong total should not disappear into one aggregate score. What changes at scale? First, add deliberately ugly fixtures: missing currency, duplicated line items, comma decimal separators, and totals that do not add up. Pin regression results by field. The important number is the one the accounting workflow cannot safely infer. Second, record request IDs, model choice, validation failures, and review decisions. Do not put supplier invoice text in application logs by default. A useful trace should reproduce a failure without turning logs into a second document store. Third, make the model identifier configurable and check the discovery surface before deployment. The example uses deepseek-v4.1-flash because it is in the current available model list, not because the rest of the application should know that name. There is a cost to this design: schema validation, fixtures, and review queues take longer than calling a model and displaying prose. That is the right trade for invoices. A chatbot can be forgiving at the edges; a structured financial record should be suspicious by default. Use a currently available chat model through /v1/chat/completions for the first implementation when the goal is a small, streaming Node.js chatbot that extracts predictable invoice fields. Put the provider behind an OpenAI-compatible adapter, validate every returned record, and compare Claude 3.5 Sonnet and Gemini 1.5 Flash on the same fixtures before changing the default. The model is replaceable. The invoice contract is not. Sources OpenAI Batch API guide Anthropic Claude documentation Google Gemini API documentation

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News