Shared callodnoga() helper
If you have more than one place that calls odnoga (you do, eventually), put one helper in _shared/airouter.ts and stop duplicating headers, error handling, and base URLs.
The pattern
// supabase/functions/_shared/airouter.ts
const BASE = Deno.env.get('AIROUTER_BASE_URL')!;
const KEY = Deno.env.get('AIROUTER_API_KEY')!;
export type AirouterMeta = {
requestId: string | null;
vendor: string | null;
model: string | null;
costUsd: number;
latencyMs: number;
cache: 'hit' | 'miss' | 'off' | null;
fallback: boolean;
remainingUsd: number | null;
};
export class AirouterError extends Error {
constructor(
public status: number,
public code: string,
message: string,
public requestId: string | null,
) { super(message); }
}
export async function callodnoga<T = unknown>(
path: string, // e.g. '/v1/chat/completions'
body: unknown,
opts: { endUser: string; signal?: AbortSignal } = { endUser: 'anonymous' },
): Promise<{ data: T; meta: AirouterMeta }> {
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
signal: opts.signal,
headers: {
'authorization': `Bearer ${KEY}`,
'content-type': 'application/json',
'x-airouter-end-user': opts.endUser,
},
body: JSON.stringify(body),
});
const requestId = res.headers.get('x-airouter-request-id');
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new AirouterError(
res.status,
j?.error?.code ?? 'unknown',
j?.error?.message ?? res.statusText,
requestId,
);
}
const data = await res.json() as T;
return {
data,
meta: {
requestId,
vendor: res.headers.get('x-airouter-vendor'),
model: res.headers.get('x-airouter-model'),
costUsd: Number(res.headers.get('x-airouter-cost-usd') ?? 0),
latencyMs: Number(res.headers.get('x-airouter-latency-ms') ?? 0),
cache: (res.headers.get('x-airouter-cache') as AirouterMeta['cache']) ?? null,
fallback: res.headers.get('x-airouter-fallback') === '1',
remainingUsd: res.headers.has('x-ratelimit-remaining-usd')
? Number(res.headers.get('x-ratelimit-remaining-usd')) : null,
},
};
}
Use it
const { data, meta } = await callodnoga<{ choices: Array<{ message: { content: string } }> }>(
'/v1/chat/completions',
{ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }] },
{ endUser: userId },
);
console.log('odnoga', meta.requestId, '#x27;, meta.costUsd, 'left', meta.remainingUsd);
return data.choices[0].message.content;
Why this is non-negotiable
- One file holds the base URL, headers, error mapping, and metering header. No drift across functions.
x-airouter-end-useris set every time — no exceptions.- Errors are typed (
AirouterError) withcode+status+requestId, so callers can branch oncodeand customer-support can find the row. - Cost + cache + fallback are surfaced in
meta— you can log them, attribute them, alert on them.
Add retry separately
Keep callodnoga simple; wrap it with a retry helper. See Retries and timeouts.