The official openai package works unchanged; only the base URL and the model ids differ.
Setting up the client#
lib/upfyn.ts
import OpenAI from "openai";export const client = new OpenAI({baseURL: "https://ai.upfyn.com/v1",apiKey: process.env.UPFYN_API_KEY,timeout: _000,maxRetries: 0, // retry yourself: the SDK would also retry a 402, which cannot succeed});
Server-side only
Never construct this in the browser. A key in a client bundle is a key anyone can read and spend. Put it behind a route on your own server — the example further down does that.A basic call#
One turn
const response = await client.chat.completions.create({model: "upfyn-yuva",messages: [{ role: "system", content: "You answer in one short paragraph." },{ role: "user", content: "Why is my invoice higher this month?" },],max_tokens: 400,});console.log(response.choices[0].message.content);// Upfyn extras exist at runtime but are absent from the OpenAI types.const { credits_used } = response as unknown as { credits_used: number };
The OpenAI types do not know about credits_used, so TypeScript needs a cast. The field is present at runtime.
Streaming#
for await
const stream = await client.chat.completions.create({model: "upfyn-yuva",messages: [{ role: "user", content: "Draft a status update." }],stream: true,stream_options: { include_usage: true },});let text = "";for await (const chunk of stream) {const delta = chunk.choices?.[0]?.delta?.content;if (delta) {text += delta;process.stdout.write(delta);}}
- Use optional chaining — the final usage chunk has an empty
choicesarray. - The
[DONE]terminator is handled for you by the SDK.
Streaming to a browser#
Next.js route handler
// app/api/chat/route.ts — stream through to the browser without exposing the key.import { client } from "@/lib/upfyn";export async function POST(req: Request) {const { messages } = await req.json();const stream = await client.chat.completions.create({model: "upfyn-yuva",messages,stream: true,});const encoder = new TextEncoder();return new Response(new ReadableStream({async start(controller) {for await (const chunk of stream) {const delta = chunk.choices?.[0]?.delta?.content;if (delta) controller.enqueue(encoder.encode(delta));}controller.close();},}),{ headers: { "Content-Type": "text/plain; charset=utf-8" } },);}
Errors#
APIError
import { APIError } from "openai";try {await client.chat.completions.create({ /* ... */ });} catch (err) {if (err instanceof APIError) {if (err.status === 402) throw new OutOfCredits(err.message);if (err.status === 429) {const wait = Number(err.headers?.["retry-after"] ?? 5);// back off, then retry}}throw err;}
Which codes are worth retrying is covered in Errors.
Upfyn-only parameters
The SDK forwards unknown keys in the body; the cast is only to satisfy the types.
Extra fields
await client.chat.completions.create({model: "upfyn-yuva",messages,persist_session: true,upfyn_tag: "support",} as never);
