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-user is set every time — no exceptions.
  • Errors are typed (AirouterError) with code + status + requestId, so callers can branch on code and 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.