{"version":3,"file":"openai.mjs","names":["apiMutate","getAccessToken","getMergedConfig","OpenAiMessage","role","content","tool_calls","OpenAiToolCall","tool_call_id","key","id","type","function","name","arguments","OpenAiChoice","message","finish_reason","OpenAiChunk","choices","Array","delta","index","OpenAiToolDefinition","description","parameters","Record","GatewayConfig","baseUrl","headers","gatewayHeaders","gateway","Promise","httpError","res","Response","Error","text","data","JSON","parse","Object","assign","status","response","RequestError","describeRequestError","err","body","stringify","readCompletionStream","stream","ReadableStream","Uint8Array","onDelta","choice","toolCalls","applyChunk","chunk","c","length","part","call","fn","dispatch","payload","join","handleLine","line","startsWith","colon","indexOf","field","slice","value","push","reader","getReader","decoder","TextDecoder","buffer","finished","done","read","decode","held","endsWith","lines","split","pop","cancel","catch","undefined","filter","Boolean","chatCompletion","fetch","method","ok","contentType","get","includes","json","listModels","map","m"],"sources":["../../src/aiAssistant/openai.ts"],"sourcesContent":["import { apiMutate } from '../utilities/useAxiosMutate';\nimport { getAccessToken, getMergedConfig } from '../utilities/apiHelpers';\n\n// ── OpenAI wire format (pass-through) ──\n//\n// These types deliberately mirror the OpenAI Chat Completions schema and stay\n// open ([key: string]: unknown) so new fields LiteLLM/OpenAI add flow through\n// without a library change. Do NOT wrap or rename fields here — the whole\n// point of the gateway (LiteLLM) is that we speak the standard format.\n\nexport interface OpenAiMessage {\n  /** 'system' | 'user' | 'assistant' | 'tool' — open so provider-specific roles pass through. */\n  role: string;\n  content: string | null;\n  tool_calls?: OpenAiToolCall[];\n  tool_call_id?: string;\n  [key: string]: unknown;\n}\n\nexport interface OpenAiToolCall {\n  id: string;\n  type: string;\n  function?: { name: string; arguments: string };\n  [key: string]: unknown;\n}\n\n/**\n * One entry of `choices[]`. `finish_reason` is 'stop', 'length' (hit the\n * completion-token limit), 'tool_calls', or 'content_filter'.\n */\nexport interface OpenAiChoice {\n  message: OpenAiMessage;\n  finish_reason?: string | null;\n  [key: string]: unknown;\n}\n\n/** One `chat.completion.chunk` of a streamed response: `choices[].delta` fragments. */\nexport interface OpenAiChunk {\n  choices?: Array<{\n    delta?: {\n      role?: string;\n      content?: string | null;\n      tool_calls?: Array<{\n        index?: number;\n        id?: string;\n        type?: string;\n        function?: { name?: string; arguments?: string };\n        [key: string]: unknown;\n      }>;\n      [key: string]: unknown;\n    };\n    finish_reason?: string | null;\n    [key: string]: unknown;\n  }>;\n  [key: string]: unknown;\n}\n\nexport interface OpenAiToolDefinition {\n  type: 'function';\n  function: {\n    name: string;\n    description?: string;\n    parameters?: Record<string, unknown>;\n    [key: string]: unknown;\n  };\n  [key: string]: unknown;\n}\n\n/** OpenAI-compatible gateway (udp-ai-gateway / LiteLLM). */\nexport interface GatewayConfig {\n  /** e.g. https://<gateway-host> — endpoints live under /v1 */\n  baseUrl: string;\n  /**\n   * Extra headers, e.g. { 'udp-product-id': '83' }. Authorization and\n   * currentTenantId are added by the transport (same helper apiMutate uses).\n   */\n  headers?: Record<string, string>;\n}\n\n// ── Transport ──\n//\n// The only file that talks HTTP. Completions stream over Server-Sent Events;\n// the chunk format is OpenAI's `chat.completion.chunk` and LiteLLM's proxy\n// emits it unchanged for every provider. Model discovery stays on apiMutate.\n\n/** UDP auth (JWT + tenant) plus the gateway's own headers. */\nconst gatewayHeaders = async (gateway: GatewayConfig): Promise<Record<string, string>> => {\n  const { headers } = getMergedConfig(await getAccessToken(), {\n    headers: { 'Content-Type': 'application/json', ...gateway.headers },\n  }) as { headers: Record<string, string> };\n  return headers;\n};\n\n/** Axios-shaped error (`response.status` / `response.data`) so callers' error handling is unchanged. */\nconst httpError = async (res: Response): Promise<Error> => {\n  const text = await res.text();\n  let data: unknown = text;\n  try {\n    data = JSON.parse(text);\n  } catch {\n    // plain-text body\n  }\n  return Object.assign(new Error(`HTTP ${res.status}`), { response: { status: res.status, data } });\n};\n\n/** Axios-shaped error as thrown by `httpError` and by apiMutate. */\ninterface RequestError {\n  message?: string;\n  response?: { status?: number; data?: unknown };\n}\n\n/** One-line description of a failed gateway call: `HTTP <status> — <body>`, else the error message. */\nexport const describeRequestError = (err: unknown): string => {\n  const { message, response } = (err ?? {}) as RequestError;\n  if (response) {\n    const { status, data } = response;\n    const body = typeof data === 'string' ? data : data ? JSON.stringify(data) : '';\n    return `HTTP ${status}${body ? ` — ${body}` : ''}`;\n  }\n  if (message === 'Network Error') {\n    return 'Network error — the gateway was unreachable or blocked the request.';\n  }\n  return message || 'Unknown error';\n};\n\n/**\n * Accumulates a `chat.completion.chunk` stream into one choice.\n *\n * Per the Chat Completions streaming reference: `delta.content` arrives in\n * fragments; `delta.tool_calls[]` entries carry an `index` into the final\n * `tool_calls` array, with `id`/`type`/`function.name` on the first fragment\n * and `function.arguments` split across fragments to be concatenated;\n * `finish_reason` is non-null only on the last content chunk; the stream ends\n * with `data: [DONE]`. Event-stream framing follows the WHATWG spec: lines end\n * with CRLF, LF or CR, a blank line dispatches the event, `data:` values are\n * joined with LF (one leading space stripped), `:` lines are comments, other\n * fields are ignored.\n */\nconst readCompletionStream = async (\n  stream: ReadableStream<Uint8Array>,\n  onDelta?: (text: string) => void,\n): Promise<OpenAiChoice> => {\n  const choice: OpenAiChoice = { message: { role: 'assistant', content: null }, finish_reason: null };\n  const toolCalls: OpenAiToolCall[] = [];\n\n  const applyChunk = (chunk: OpenAiChunk) => {\n    const c = chunk.choices?.[0];\n    if (!c) return;\n    const delta = c.delta ?? {};\n    if (typeof delta.content === 'string' && delta.content.length > 0) {\n      choice.message.content = (choice.message.content ?? '') + delta.content;\n      onDelta?.(delta.content);\n    }\n    for (const part of delta.tool_calls ?? []) {\n      const index = typeof part.index === 'number' ? part.index : 0;\n      const call =\n        toolCalls[index] ?? (toolCalls[index] = { id: '', type: 'function', function: { name: '', arguments: '' } });\n      const fn = call.function ?? (call.function = { name: '', arguments: '' });\n      if (part.id) call.id = part.id;\n      if (part.type) call.type = part.type;\n      if (part.function?.name) fn.name += part.function.name;\n      if (part.function?.arguments) fn.arguments += part.function.arguments;\n    }\n    if (c.finish_reason) choice.finish_reason = c.finish_reason;\n  };\n\n  let data: string[] = [];\n  /** Dispatches the buffered event. Returns true on the `[DONE]` sentinel. */\n  const dispatch = (): boolean => {\n    if (data.length === 0) return false;\n    const payload = data.join('\\n');\n    data = [];\n    if (payload === '[DONE]') return true;\n    try {\n      applyChunk(JSON.parse(payload) as OpenAiChunk);\n    } catch {\n      // not JSON — ignore, per \"unknown data is ignored\"\n    }\n    return false;\n  };\n  const handleLine = (line: string): boolean => {\n    if (line === '') return dispatch();\n    if (line.startsWith(':')) return false;\n    const colon = line.indexOf(':');\n    const field = colon === -1 ? line : line.slice(0, colon);\n    let value = colon === -1 ? '' : line.slice(colon + 1);\n    if (value.startsWith(' ')) value = value.slice(1);\n    if (field === 'data') data.push(value);\n    return false;\n  };\n\n  const reader = stream.getReader();\n  const decoder = new TextDecoder();\n  let buffer = '';\n  let finished = false;\n  while (!finished) {\n    const { value, done } = await reader.read();\n    buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });\n    // A trailing CR may be the first half of a CRLF still in flight.\n    let text = buffer;\n    let held = '';\n    if (!done && text.endsWith('\\r')) {\n      text = text.slice(0, -1);\n      held = '\\r';\n    }\n    const lines = text.split(/\\r\\n|\\n|\\r/);\n    buffer = (done ? '' : (lines.pop() ?? '')) + held;\n    for (const line of lines) {\n      if (handleLine(line)) {\n        finished = true;\n        break;\n      }\n    }\n    if (done) break;\n  }\n  if (finished) await reader.cancel().catch(() => undefined);\n  else dispatch(); // stream closed without a trailing blank line\n\n  if (toolCalls.length > 0) choice.message.tool_calls = toolCalls.filter(Boolean);\n  return choice;\n};\n\n/**\n * POST /v1/chat/completions, streamed. `body` is the raw OpenAI request body —\n * model, messages, tools, and any additional params the caller passes through\n * (the gateway runs LiteLLM with drop_params, so unsupported params are\n * silently dropped per provider). `onDelta` receives assistant text as it\n * arrives; the resolved choice is the fully accumulated message, identical in\n * shape to a non-streamed response. A gateway that ignores `stream` and\n * answers with one JSON completion is handled too.\n */\nexport const chatCompletion = async (\n  gateway: GatewayConfig,\n  body: Record<string, unknown>,\n  onDelta?: (text: string) => void,\n): Promise<OpenAiChoice | undefined> => {\n  const res = await fetch(`${gateway.baseUrl}/v1/chat/completions`, {\n    method: 'POST',\n    headers: await gatewayHeaders(gateway),\n    body: JSON.stringify({ ...body, stream: true }),\n  });\n  if (!res.ok) throw await httpError(res);\n  const contentType = res.headers.get('content-type') ?? '';\n  if (!res.body || !contentType.includes('text/event-stream')) {\n    const json = (await res.json()) as { choices?: OpenAiChoice[] };\n    return json.choices?.[0];\n  }\n  return readCompletionStream(res.body, onDelta);\n};\n\n/** GET /v1/models — returns the model ids the (tenant, product) cell is entitled to. */\nexport const listModels = async (gateway: GatewayConfig): Promise<string[]> => {\n  const res = (await apiMutate(\n    gateway.baseUrl,\n    'v1/models',\n    { method: 'get', headers: { ...gateway.headers } },\n    {},\n  )) as { data?: { data?: Array<{ id?: string }> } } | undefined;\n  return (res?.data?.data ?? []).map(m => m?.id).filter((id): id is string => !!id);\n};\n"],"mappings":";;;;AAsFA,MAAM8B,iBAAiB,OAAOC,YAA4D;CACxF,MAAM,EAAEF,YAAY3B,gBAAgB,MAAMD,gBAAgB,EAAE,EAC1D4B,SAAS;EAAE,gBAAgB;EAAoB,GAAGE,QAAQF;EAAQ,EACnE,CAAC;AACF,QAAOA;;;AAIT,MAAMI,YAAY,OAAOC,QAAkC;CACzD,MAAMG,OAAO,MAAMH,IAAIG,MAAM;CAC7B,IAAIC,OAAgBD;AACpB,KAAI;AACFC,SAAOC,KAAKC,MAAMH,KAAK;SACjB;AAGR,QAAOI,OAAOC,uBAAO,IAAIN,MAAM,QAAQF,IAAIS,SAAS,EAAE,EAAEC,UAAU;EAAED,QAAQT,IAAIS;EAAQL;EAAK,EAAG,CAAC;;;AAUnG,MAAaQ,wBAAwBC,QAAyB;CAC5D,MAAM,EAAE/B,SAAS4B,aAAcG,OAAO,EAAE;AACxC,KAAIH,UAAU;EACZ,MAAM,EAAED,QAAQL,SAASM;EACzB,MAAMI,OAAO,OAAOV,SAAS,WAAWA,OAAOA,OAAOC,KAAKU,UAAUX,KAAK,GAAG;AAC7E,SAAO,QAAQK,SAASK,OAAO,MAAMA,SAAS;;AAEhD,KAAIhC,YAAY,gBACd,QAAO;AAET,QAAOA,WAAW;;;;;;;;;;;;;;;AAgBpB,MAAMkC,uBAAuB,OAC3BC,QACAG,YAC0B;CAC1B,MAAMC,SAAuB;EAAEvC,SAAS;GAAEZ,MAAM;GAAaC,SAAS;GAAM;EAAEY,eAAe;EAAM;CACnG,MAAMuC,YAA8B,EAAE;CAEtC,MAAMC,cAAcC,UAAuB;EACzC,MAAMC,IAAID,MAAMvC,UAAU;AAC1B,MAAI,CAACwC,EAAG;EACR,MAAMtC,QAAQsC,EAAEtC,SAAS,EAAE;AAC3B,MAAI,OAAOA,MAAMhB,YAAY,YAAYgB,MAAMhB,QAAQuD,SAAS,GAAG;AACjEL,UAAOvC,QAAQX,WAAWkD,OAAOvC,QAAQX,WAAW,MAAMgB,MAAMhB;AAChEiD,aAAUjC,MAAMhB,QAAQ;;AAE1B,OAAK,MAAMwD,QAAQxC,MAAMf,cAAc,EAAE,EAAE;GACzC,MAAMgB,QAAQ,OAAOuC,KAAKvC,UAAU,WAAWuC,KAAKvC,QAAQ;GAC5D,MAAMwC,OACJN,UAAUlC,WAAWkC,UAAUlC,SAAS;IAAEZ,IAAI;IAAIC,MAAM;IAAYC,UAAU;KAAEC,MAAM;KAAIC,WAAW;KAAG;IAAG;GAC7G,MAAMiD,KAAKD,KAAKlD,aAAakD,KAAKlD,WAAW;IAAEC,MAAM;IAAIC,WAAW;IAAI;AACxE,OAAI+C,KAAKnD,GAAIoD,MAAKpD,KAAKmD,KAAKnD;AAC5B,OAAImD,KAAKlD,KAAMmD,MAAKnD,OAAOkD,KAAKlD;AAChC,OAAIkD,KAAKjD,UAAUC,KAAMkD,IAAGlD,QAAQgD,KAAKjD,SAASC;AAClD,OAAIgD,KAAKjD,UAAUE,UAAWiD,IAAGjD,aAAa+C,KAAKjD,SAASE;;AAE9D,MAAI6C,EAAE1C,cAAesC,QAAOtC,gBAAgB0C,EAAE1C;;CAGhD,IAAIqB,OAAiB,EAAE;;CAEvB,MAAM0B,iBAA0B;AAC9B,MAAI1B,KAAKsB,WAAW,EAAG,QAAO;EAC9B,MAAMK,UAAU3B,KAAK4B,KAAK,KAAK;AAC/B5B,SAAO,EAAE;AACT,MAAI2B,YAAY,SAAU,QAAO;AACjC,MAAI;AACFR,cAAWlB,KAAKC,MAAMyB,QAAQ,CAAgB;UACxC;AAGR,SAAO;;CAET,MAAME,cAAcC,SAA0B;AAC5C,MAAIA,SAAS,GAAI,QAAOJ,UAAU;AAClC,MAAII,KAAKC,WAAW,IAAI,CAAE,QAAO;EACjC,MAAMC,QAAQF,KAAKG,QAAQ,IAAI;EAC/B,MAAMC,QAAQF,UAAU,KAAKF,OAAOA,KAAKK,MAAM,GAAGH,MAAM;EACxD,IAAII,QAAQJ,UAAU,KAAK,KAAKF,KAAKK,MAAMH,QAAQ,EAAE;AACrD,MAAII,MAAML,WAAW,IAAI,CAAEK,SAAQA,MAAMD,MAAM,EAAE;AACjD,MAAID,UAAU,OAAQlC,MAAKqC,KAAKD,MAAM;AACtC,SAAO;;CAGT,MAAME,SAASzB,OAAO0B,WAAW;CACjC,MAAMC,UAAU,IAAIC,aAAa;CACjC,IAAIC,SAAS;CACb,IAAIC,WAAW;AACf,QAAO,CAACA,UAAU;EAChB,MAAM,EAAEP,OAAOQ,SAAS,MAAMN,OAAOO,MAAM;AAC3CH,YAAUE,OAAOJ,QAAQM,QAAQ,GAAGN,QAAQM,OAAOV,OAAO,EAAEvB,QAAQ,MAAM,CAAC;EAE3E,IAAId,OAAO2C;EACX,IAAIK,OAAO;AACX,MAAI,CAACH,QAAQ7C,KAAKiD,SAAS,KAAK,EAAE;AAChCjD,UAAOA,KAAKoC,MAAM,GAAG,GAAG;AACxBY,UAAO;;EAET,MAAME,QAAQlD,KAAKmD,MAAM,aAAa;AACtCR,YAAUE,OAAO,KAAMK,MAAME,KAAK,IAAI,MAAOJ;AAC7C,OAAK,MAAMjB,QAAQmB,MACjB,KAAIpB,WAAWC,KAAK,EAAE;AACpBa,cAAW;AACX;;AAGJ,MAAIC,KAAM;;AAEZ,KAAID,SAAU,OAAML,OAAOc,QAAQ,CAACC,YAAYC,KAAAA,EAAU;KACrD5B,WAAU;AAEf,KAAIR,UAAUI,SAAS,EAAGL,QAAOvC,QAAQV,aAAakD,UAAUqC,OAAOC,QAAQ;AAC/E,QAAOvC;;;;;;;;;;;AAYT,MAAawC,iBAAiB,OAC5BhE,SACAiB,MACAM,YACsC;CACtC,MAAMpB,MAAM,MAAM8D,MAAM,GAAGjE,QAAQH,QAAO,uBAAwB;EAChEqE,QAAQ;EACRpE,SAAS,MAAMC,eAAeC,QAAQ;EACtCiB,MAAMT,KAAKU,UAAU;GAAE,GAAGD;GAAMG,QAAQ;GAAM,CAAA;EAC/C,CAAC;AACF,KAAI,CAACjB,IAAIgE,GAAI,OAAM,MAAMjE,UAAUC,IAAI;CACvC,MAAMiE,cAAcjE,IAAIL,QAAQuE,IAAI,eAAe,IAAI;AACvD,KAAI,CAAClE,IAAIc,QAAQ,CAACmD,YAAYE,SAAS,oBAAoB,CAEzD,SADc,MAAMnE,IAAIoE,MAAM,EAClBnF,UAAU;AAExB,QAAO+B,qBAAqBhB,IAAIc,MAAMM,QAAQ;;;AAIhD,MAAaiD,aAAa,OAAOxE,YAA8C;AAO7E,UANa,MAAM/B,UACjB+B,QAAQH,SACR,aACA;EAAEqE,QAAQ;EAAOpE,SAAS,EAAE,GAAGE,QAAQF,SAAQ;EAAG,EAClD,EACF,CAAC,GACYS,MAAMA,QAAQ,EAAE,EAAEkE,KAAIC,MAAKA,GAAG/F,GAAG,CAACmF,QAAQnF,OAAqB,CAAC,CAACA,GAAG"}