{"_id":"@abdoseadaa/convai","_rev":"5-57b30fd8bc09915998ef89a1933b4e5e","name":"@abdoseadaa/convai","dist-tags":{"latest":"0.1.4"},"versions":{"0.1.0":{"name":"@abdoseadaa/convai","version":"0.1.0","keywords":["openai","conversations","chat","ai","sdk","gpt","stateful","context","responses-api","chat-completions","typescript"],"author":{"name":"abdoseadaa"},"license":"MIT","_id":"@abdoseadaa/convai@0.1.0","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"dist":{"shasum":"728c95161b5b00c0727486f11589eda1feed1029","tarball":"https://registry.npmjs.org/@abdoseadaa/convai/-/convai-0.1.0.tgz","fileCount":115,"integrity":"sha512-zCNLGNhUUMZs1XS0JDBskQKQ/89FoqmeUSlfzF1Deh5PtiBLctv9LxL6MjFDvQo0t9SMGCSOV25Inqa11tAM0Q==","signatures":[{"sig":"MEUCIQDkUriUPnyRKiLs6/C/UIJ8318mm1WtHwhQfkMHKCoFewIgUHNNY4WqjlDMA3wgLfxAxpuT21d/tI6P6bqKADcmLJs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":290778},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"scripts":{"dev":"tsc --watch","build":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"repository":{"url":"","type":"git"},"_npmVersion":"11.13.0","description":"Typed shorthand SDK for OpenAI stateful conversations and stateless chat completions","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"openai":"^4.98.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"peerDependencies":{"openai":">=4.98.0"},"_npmOperationalInternal":{"tmp":"tmp/convai_0.1.0_1782337947961_0.757903227026467","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@abdoseadaa/convai","version":"0.1.1","keywords":["openai","conversations","chat","ai","sdk","gpt","stateful","context","responses-api","chat-completions","typescript"],"author":{"name":"abdoseadaa"},"license":"MIT","_id":"@abdoseadaa/convai@0.1.1","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"dist":{"shasum":"2962573d8f3715d394d42537c768ff4e1731a16b","tarball":"https://registry.npmjs.org/@abdoseadaa/convai/-/convai-0.1.1.tgz","fileCount":115,"integrity":"sha512-VIcczQ50AShiLHtfalH3FNFFetnwGaNkdp0a1ILrLBv6hIZIThKKpl3zwkMk+Fi6Fuqt6SFykcC9U5oA1nomyA==","signatures":[{"sig":"MEQCIEvMx1UE2ShGvAu19bSd38aJfj8JB91XtzBWc1DMdNboAiA3AEM0G2VdoNYWC2zFd8Ug8q26FZpb29SezCKMWkF1pg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":292045},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"scripts":{"dev":"tsc --watch","build":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"repository":{"url":"","type":"git"},"_npmVersion":"11.13.0","description":"Typed shorthand SDK for OpenAI stateful conversations and stateless chat completions","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"openai":"^4.98.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"peerDependencies":{"openai":">=4.98.0"},"_npmOperationalInternal":{"tmp":"tmp/convai_0.1.1_1782338515956_0.5670728933613436","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@abdoseadaa/convai","version":"0.1.3","keywords":["openai","conversations","chat","ai","sdk","gpt","stateful","context","responses-api","chat-completions","typescript"],"author":{"name":"abdoseadaa"},"license":"MIT","_id":"@abdoseadaa/convai@0.1.3","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"dist":{"shasum":"65d256d0f6bf04df284d4f237056afa2e52ac5e3","tarball":"https://registry.npmjs.org/@abdoseadaa/convai/-/convai-0.1.3.tgz","fileCount":119,"integrity":"sha512-nPeR5qC5lc9F+Qr7puDsyRX7z+J0wyhbMXRWHyl1MYJWKMxrobC+PnlnXbHg2jJg80WMIWryKMzwGttYt8HP8A==","signatures":[{"sig":"MEYCIQCRHvPZ6z8QSuCWxKQvslKaunOg2QV9aWoWQTHRsNjtpAIhAIaMotH11XoWfRALoj/Pem55kE6wC1MT4cp2B4xcAhJw","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":293717},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"scripts":{"dev":"tsc --watch","build":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"repository":{"url":"","type":"git"},"_npmVersion":"11.13.0","description":"Typed shorthand SDK for OpenAI stateful conversations and stateless chat completions","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"openai":"^4.98.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"peerDependencies":{"openai":">=4.98.0"},"_npmOperationalInternal":{"tmp":"tmp/convai_0.1.3_1782340067159_0.1824566146064599","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@abdoseadaa/convai","version":"0.1.4","keywords":["openai","conversations","chat","ai","sdk","gpt","stateful","context","responses-api","chat-completions","typescript"],"author":{"name":"abdoseadaa"},"license":"MIT","_id":"@abdoseadaa/convai@0.1.4","maintainers":[{"name":"abdoseadaa","email":"abdom.seada@gmail.com"}],"homepage":"https://gitlab.com/abdom.seada/convai","bugs":{"url":"https://gitlab.com/abdom.seada/convai/-/issues"},"dist":{"shasum":"e31d4954a1904d09516a55f36e7f66b73a8e216e","tarball":"https://registry.npmjs.org/@abdoseadaa/convai/-/convai-0.1.4.tgz","fileCount":119,"integrity":"sha512-0531aEabB9rCPNOXcZD0f3r6SoPhsjC83Wo3ITiNn82U6jQNl77MTdGFvaL1B7fJiu82ng4tcDv1lBxuQM42YA==","signatures":[{"sig":"MEUCIARnsCbsz0CpseS3f9eZAnz4lIY+S9k5wNiXJaoO4bi3AiEAoIij+YTkD0BCH6vPNqSGtvWIOWaFtcLUYj3QdmjF1Uo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":293890},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"gitHead":"d41e992881d0694af22dbbb889670677db815453","scripts":{"dev":"tsc --watch","build":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"abdoseadaa","email":"abdom.seada@gmail.com"},"repository":{"url":"git+https://gitlab.com/abdom.seada/convai.git","type":"git"},"_npmVersion":"11.13.0","description":"Typed shorthand SDK for OpenAI stateful conversations and stateless chat completions","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"openai":"^4.98.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"peerDependencies":{"openai":">=4.98.0"},"_npmOperationalInternal":{"tmp":"tmp/convai_0.1.4_1782341536962_0.5692046813513982","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-06-24T21:52:27.772Z","modified":"2026-06-29T19:01:54.722Z","0.1.0":"2026-06-24T21:52:28.079Z","0.1.1":"2026-06-24T22:01:56.113Z","0.1.3":"2026-06-24T22:27:47.306Z","0.1.4":"2026-06-24T22:52:17.117Z"},"bugs":{"url":"https://gitlab.com/abdom.seada/convai/-/issues"},"author":{"name":"abdoseadaa"},"license":"MIT","homepage":"https://gitlab.com/abdom.seada/convai","keywords":["openai","conversations","chat","ai","sdk","gpt","stateful","context","responses-api","chat-completions","typescript"],"repository":{"url":"git+https://gitlab.com/abdom.seada/convai.git","type":"git"},"description":"Typed shorthand SDK for OpenAI stateful conversations and stateless chat completions","maintainers":[{"email":"abdom.seada@gmail.com","name":"abdoseadaa"},{"email":"altlbany2014@gmail.com","name":"mohamedtebo"}],"readme":"# convai\n\n> Typed shorthand SDK for the OpenAI Responses API and Chat Completions API.\n\n`convai` wraps the official `openai` npm package with two things developers always end up writing themselves: **persistent conversation context** and **clean shorthand functions** that skip the boilerplate.\n\nEvery function exists in two forms — one that throws a typed `SdkError`, and one that returns `{ success, data, error }` — so you pick the calling style that fits each use case.\n\n---\n\n## Why convai?\n\nThe official `openai` package is complete but low-level. Using it for real products means writing the same wrappers repeatedly:\n\n- Chaining `previous_response_id` for multi-turn context\n- Parsing `choices[0].message.content` on every call\n- Building pagination loops for history\n- Handling `rate_limit_exceeded` vs `insufficient_quota` differently\n- Parsing tool call arguments from JSON strings\n- Accumulating streaming deltas into a full string\n\n`convai` does all of that once, correctly, and exposes it as clean named functions with full TypeScript types.\n\n---\n\n## Install\n\n```bash\nnpm install @abdoseadaa/convai openai\n```\n\n`openai >= 4.98.0` is a peer dependency — install it alongside `convai`.\n\n**Requirements:** Node.js >= 18.0.0, TypeScript >= 5.0 (optional but recommended)\n\n---\n\n## Quick Start\n\n```ts\nimport { createClient } from '@abdoseadaa/convai'\n\nconst ai = createClient({\n  apiKey: process.env.OPENAI_API_KEY!,\n  model:  'gpt-4o',\n})\n\n// ── Stateful: context is managed by OpenAI ────────────────────────────────────\nconst { convId } = await ai.session.startSession('You are a senior MERN developer.')\n\nconst r1 = await ai.shorthand.ask(convId, 'What DB handles high write throughput best?')\nconst r2 = await ai.shorthand.ask(convId, 'How does that compare to PostgreSQL?')\n// r2 has full context — no messages[] array to manage\n\n// ── Stateless: one-off calls with no context ──────────────────────────────────\nconst label   = await ai.chat_shorthand.classify('I love this product!', ['positive', 'negative', 'neutral'])\nconst summary = await ai.chat_shorthand.summarize(longText, 'bullet')\nconst french  = await ai.chat_shorthand.translate('Hello world', 'French')\n```\n\n---\n\n## Two APIs, One Client\n\n`convai` covers both OpenAI conversation patterns:\n\n| | **Stateful** | **Stateless** |\n|---|---|---|\n| **Context** | OpenAI manages it server-side | You own `messages[]` |\n| **API used** | Responses API (`previous_response_id`) | Chat Completions API |\n| **Namespace** | `ai.conversation`, `ai.chat`, `ai.session`, `ai.shorthand`, etc. | `ai.completion`, `ai.chat_shorthand` |\n| **Best for** | Chatbots, assistants, multi-turn agents | Classification, transforms, one-shot prompts |\n\n---\n\n## Two Layers, Every Function\n\nEvery function in `convai` exists in two forms. Pick by context:\n\n### Layer 1 — throws `SdkError` (use in services and composed logic)\n\n```ts\ntry {\n  const { convId } = await ai.session.startSession('You are a helpful assistant.')\n  const reply = await ai.shorthand.ask(convId, 'Explain closures in JavaScript.')\n  console.log(reply)\n} catch (err) {\n  const e = err as SdkError\n  console.log(e.formatted.code)        // 'RATE_LIMITED'\n  console.log(e.formatted.message)     // 'Rate limit exceeded — too many requests...'\n  console.log(e.formatted.hint)        // 'Back off and retry. Check retryAfterMs...'\n  console.log(e.formatted.retryable)   // true\n  console.log(e.formatted.retryAfterMs) // 12000\n  console.log(e.formatted.requestId)   // 'req_abc123' — for OpenAI support tickets\n  console.log(e.raw)                   // original OpenAI APIError, untouched\n}\n```\n\n### Layer 2 — returns `SdkResult<T>` (use in route controllers, never throws)\n\n```ts\nconst result = await ai.safe.shorthand.ask(convId, 'Explain closures in JavaScript.')\n\nif (result.success) {\n  console.log(result.data)   // string — TypeScript narrows this, no cast needed\n} else {\n  console.log(result.error.code)       // ErrorCode enum value\n  console.log(result.error.hint)       // actionable next step\n  console.log(result.error.retryable)  // boolean\n}\n```\n\nThe `SdkResult<T>` type is a **discriminated union** — TypeScript automatically narrows `result.data` to `T` when `result.success` is `true`, and `result.error` to `FormattedError` when `false`. No null checks on both sides.\n\n```ts\n// TypeScript enforces the check — this won't compile:\nresult.data.someField    // ❌ Error: data may be null\n\n// This works:\nif (result.success) {\n  result.data.someField  // ✅ TypeScript knows data is T here\n}\n```\n\n**Access the safe layer** via `ai.safe.*` — it mirrors every namespace:\n\n```\nai.safe.conversation.*\nai.safe.chat.*\nai.safe.session.*\nai.safe.shorthand.*\nai.safe.historyOps.*\nai.safe.tokens.*\nai.safe.multi.*\nai.safe.completion.*\nai.safe.chat_shorthand.*\n```\n\n---\n\n## createClient(config)\n\n```ts\nimport { createClient } from '@abdoseadaa/convai'\n\nconst ai = createClient({\n  apiKey:        string    // Required. Your OpenAI API key.\n  model?:        string    // Default model for all calls. Default: 'gpt-4o'\n  defaultSystem?: string   // Default system prompt for session.startSession()\n  store?:        boolean   // Store responses server-side. Default: true\n  maxRetries?:   number    // Retries on 429/500/503. Default: 2\n  timeoutMs?:    number    // Request timeout in ms. Default: 30000\n})\n```\n\n---\n\n## Stateful API Reference\n\n### `ai.conversation.*` — conversation container CRUD\n\nManages the conversation object — the root of every multi-turn chain.\n\nCreating a conversation is a **local, zero-cost operation** — it mints a stable\nsynthetic id (`conv_<uuid>`) and registers in-process state (chain head, system\nprompt, transcript). No model call is made until the first `chat.send()`.\n\n```ts\n// Create a new conversation. Returns { id, created_at, metadata? }. No API call.\nconst conv = await ai.conversation.create()\nconst conv = await ai.conversation.create({ metadata: { userId: 'u_123' } })\n\n// Retrieve conversation metadata by ID (local read)\nconst conv = await ai.conversation.get(convId)\n\n// Update metadata on a conversation (local)\nconst conv = await ai.conversation.update(convId, { topic: 'typescript', status: 'active' })\n\n// Delete a conversation — clears local state and best-effort deletes the\n// latest stored response from OpenAI. Returns { id, deleted }.\nawait ai.conversation.remove(convId)\n```\n\n---\n\n### `ai.chat.*` — raw AI response calls\n\nLow-level calls — return the full OpenAI Response object. Use `ai.shorthand.*` for extracted text.\n\n```ts\n// Send a message — context is maintained automatically via previous_response_id\nconst response = await ai.chat.send(convId, 'What is a closure?')\nconst response = await ai.chat.send(convId, 'What is a closure?', {\n  model:        'gpt-4o-mini',  // override model for this call\n  instructions: 'Be concise.',  // one-off system override\n  maxTokens:    500,\n  temperature:  0.3,\n})\n\n// Send with a temporary system instructions override for this turn only\nconst response = await ai.chat.sendWithSystem(convId, 'Reply only in bullet points.', 'List 5 JS tips.')\n\n// Get the raw async stream iterable — use ai.shorthand.streamAsk() for a simpler API\nconst stream = await ai.chat.stream(convId, 'Write a sorting algorithm.')\n```\n\n---\n\n### `ai.history.*` — conversation item management\n\nRead and write individual items (messages) inside a conversation. History is\nbacked by the conversation's local transcript — the Responses API can't return a\nfull transcript in a single call, so `convai` records each turn as it happens.\n\n```ts\n// List items in a conversation — returns one page: { data, has_more, last_id }\nconst page = await ai.history.list(convId)\nconst page = await ai.history.list(convId, { limit: 50, order: 'asc', after: cursorId })\n\n// Retrieve a single transcript item by ID\nconst item = await ai.history.getItem(convId, itemId)\n\n// addItem annotates the LOCAL transcript only — no model call.\n// A 'system' item also updates the conversation's persistent system prompt,\n// so it is applied on every subsequent turn.\nawait ai.history.addItem(convId, { role: 'user',      content: 'A note for the record.' })\nawait ai.history.addItem(convId, { role: 'assistant', content: 'Understood.' })\nawait ai.history.addItem(convId, { role: 'system',    content: 'New persistent instructions.' })\n\n// To inject messages the MODEL should treat as prior context, use\n// historyOps.injectContext (it chains them into the response chain).\n\n// Delete a specific item from the local transcript\nawait ai.history.removeItem(convId, itemId)\n```\n\n---\n\n### `ai.response.*` — response object management\n\nManage the individual stored response objects that form a conversation chain.\n\n```ts\n// Retrieve a stored response by its ID\nconst res = await ai.response.get(responseId)\n\n// Delete a stored response\nawait ai.response.remove(responseId)\n\n// Cancel an in-flight background response\nawait ai.response.cancel(responseId)\n\n// Compact the context of a response — reduces token cost on subsequent turns\nawait ai.response.compact(responseId)\n\n// List input items that were sent to a specific response (useful for debugging)\nconst items = await ai.response.getItems(responseId)\n\n// Get the input token count for a response\nconst { input_tokens } = await ai.response.getTokenCount(responseId)\n```\n\n---\n\n### `ai.session.*` — session lifecycle shortcuts\n\nHigh-level composed functions. Each replaces 2–4 raw API calls.\n\n```ts\n// quickChat — one-off question: creates conv → sends → returns text → deletes conv\n// Creating the conv is local, so this costs a single model call.\nconst reply = await ai.session.quickChat('What is the capital of France?')\nconst reply = await ai.session.quickChat('Explain async/await.', { model: 'gpt-4o-mini' })\n\n// startSession — create a conversation and set a PERSISTENT system prompt.\n// The prompt is re-applied as instructions on every turn (no extra model call).\n// Returns { convId, createdAt }. Store convId for all subsequent calls.\nconst { convId } = await ai.session.startSession()\nconst { convId } = await ai.session.startSession('You are a code reviewer. Be strict and concise.')\nconst { convId } = await ai.session.startSession('You are a helpful assistant.', {\n  metadata: { userId: 'u_123', topic: 'code-review' }\n})\n\n// resetSession — delete a conversation and create a fresh one\n// Useful for \"clear chat\" features. Returns the new convId.\nconst newConvId = await ai.session.resetSession(oldConvId)\nconst newConvId = await ai.session.resetSession(oldConvId, 'New system prompt for the fresh session.')\n\n// cloneSession — branch a conversation into a new one.\n// The clone shares the source's server-side chain head, so it continues with\n// full prior context but diverges independently from the next turn onward.\nconst branchedConvId = await ai.session.cloneSession(sourceConvId)\n```\n\n---\n\n### `ai.shorthand.*` — stateful chat shortcuts\n\nThe most common operations — each returns a clean value with no parsing needed.\n\n```ts\n// ask — send a message and get the reply as a plain string\nconst reply = await ai.shorthand.ask(convId, 'What is TypeScript?')\nconst reply = await ai.shorthand.ask(convId, 'What is TypeScript?', { temperature: 0 })\n\n// askWithSystem — one-off instructions override for a single turn\nconst reply = await ai.shorthand.askWithSystem(\n  convId,\n  'Reply only in Spanish.',\n  'What is the weather like today?'\n)\n\n// streamAsk — stream the response, call onChunk for each delta, return full text when done\nconst fullText = await ai.shorthand.streamAsk(\n  convId,\n  'Write a merge sort implementation in TypeScript.',\n  (chunk) => process.stdout.write(chunk)  // called for each text delta\n)\n\n// retry — cancel (if in-flight) + delete a response and resend\n// Covers the \"regenerate response\" UX pattern.\nconst newResponse = await ai.shorthand.retry(convId, lastResponseId, 'Try again with more detail.')\n```\n\n---\n\n### `ai.historyOps.*` — history operations\n\nComposed functions for reading, writing, and managing conversation history at scale.\n\n```ts\n// getFullHistory — fetch ALL items in a conversation with automatic pagination\n// Returns a flat array — cursor pagination is handled transparently.\nconst items = await ai.historyOps.getFullHistory(convId)\n\n// getLastReply — get the most recent assistant message as a plain string\nconst lastReply = await ai.historyOps.getLastReply(convId)  // string | null\n\n// injectContext — seed real model context with multiple messages in ONE call\n// Chains the messages into the response chain so the model treats them as prior\n// context on subsequent turns (unlike history.addItem, which is local-only).\nawait ai.historyOps.injectContext(convId, [\n  { role: 'user',      content: 'My name is Alice.' },\n  { role: 'assistant', content: 'Got it, Alice.' },\n  { role: 'user',      content: 'I am a TypeScript developer.' },\n])\n\n// pruneHistory — delete the oldest items, keeping only the last N\n// Fetch history first, pass it in to avoid double-fetching.\nconst history = await ai.historyOps.getFullHistory(convId)\nconst result  = await ai.historyOps.pruneHistory(convId, 20, history)\n// result: { deleted: 45, remaining: 20 }\n\n// exportTranscript — format the conversation as text, JSON, or Markdown\nconst markdown = await ai.historyOps.exportTranscript(convId, 'markdown')\nconst jsonStr  = await ai.historyOps.exportTranscript(convId, 'json')\nconst plainTxt = await ai.historyOps.exportTranscript(convId, 'text')\n\n// Optionally pass pre-fetched history to avoid a second round-trip\nconst history  = await ai.historyOps.getFullHistory(convId)\nconst markdown = await ai.historyOps.exportTranscript(convId, 'markdown', history)\n\n// summarizeAndCompress — LLM-summarize the conversation, then replace the local\n// transcript with a single summary item. Shrinks what history reads return.\nconst { summary, itemsRemoved } = await ai.historyOps.summarizeAndCompress(convId)\n// summary: \"The conversation covered X, Y, and Z...\"\n// itemsRemoved: 87\n```\n\n---\n\n### `ai.tokens.*` — token and cost tracking\n\n```ts\n// getTokenUsage — aggregate input/output tokens across all responses in a conversation\nconst usage = await ai.tokens.getTokenUsage(convId)\n// {\n//   inputTokens:      12400,\n//   outputTokens:     3200,\n//   totalTokens:      15600,\n//   estimatedCostUsd: 0.063   ← based on gpt-4o pricing\n// }\n\n// compactIfNeeded — compact only if the token count exceeds a threshold\n// Pass the latest responseId and a token threshold. Default threshold: 50,000.\nconst result = await ai.tokens.compactIfNeeded(convId, latestResponseId)\nconst result = await ai.tokens.compactIfNeeded(convId, latestResponseId, 100_000)\n// {\n//   compacted:    true,\n//   reason:       'Token count (62000) exceeded threshold (50000). Compaction applied.',\n//   tokensBefore: 62000\n// }\n```\n\n---\n\n### `ai.multi.*` — multi-conversation operations\n\n```ts\n// broadcast — send the same message to multiple conversations in parallel\n// Errors per conversation are caught individually — one failure won't abort the rest.\nconst results = await ai.multi.broadcast([convId1, convId2, convId3], 'Summarize your topic.')\n// [\n//   { convId: '...', reply: 'This conversation covered...', error: null },\n//   { convId: '...', reply: null, error: 'Rate limit exceeded...' },\n//   { convId: '...', reply: 'Topics included...', error: null },\n// ]\n\n// deleteAll — delete multiple conversations in parallel\nconst results = await ai.multi.deleteAll([convId1, convId2, convId3])\n// [\n//   { convId: '...', deleted: true,  error: null },\n//   { convId: '...', deleted: false, error: 'Not found' },\n// ]\n```\n\n---\n\n## Stateless API Reference\n\n### `ai.completion.*` — raw chat completion calls\n\nLow-level stateless calls — return the full `ChatCompletion` object.\n\n```ts\n// complete — raw call with full messages array\nconst res = await ai.completion.complete([\n  { role: 'system', content: 'You are a helpful assistant.' },\n  { role: 'user',   content: 'What is a closure?' },\n])\nconst text = res.choices[0].message.content\n\n// structured — enforce a JSON schema on the output\nconst res = await ai.completion.structured(\n  [{ role: 'user', content: 'Extract the person info from: John Doe, age 32.' }],\n  {\n    name:   'person',\n    schema: {\n      type:       'object',\n      properties: { name: { type: 'string' }, age: { type: 'number' } },\n      required:   ['name', 'age'],\n    },\n  }\n)\n\n// withTools — function/tool calling, returns full ChatCompletion\nconst res = await ai.completion.withTools(messages, tools)\n\n// vision — image analysis\nconst res = await ai.completion.vision('https://example.com/image.jpg', 'What is in this image?')\n\n// stream — raw async stream iterable\nconst stream = await ai.completion.stream(messages)\n\n// jsonMode — less strict JSON output without schema enforcement\nconst res = await ai.completion.jsonMode(messages)\n```\n\n---\n\n### `ai.chat_shorthand.*` — stateless shorthand functions\n\nThe main consumer-facing stateless API. Returns clean values — no parsing needed.\n\n```ts\n// ask — messages array → reply string\nconst reply = await ai.chat_shorthand.ask([\n  { role: 'system', content: 'You are a helpful assistant.' },\n  { role: 'user',   content: 'What is a closure?' },\n])\n\n// askOnce — single user message → reply string (simplest possible call)\nconst reply = await ai.chat_shorthand.askOnce('What is a closure?')\nconst reply = await ai.chat_shorthand.askOnce('What is a closure?', {\n  system:      'Be concise.',\n  temperature: 0,\n  model:       'gpt-4o-mini',\n})\n\n// structured<T> — enforces JSON schema, returns parsed typed object\ntype Person = { name: string; age: number }\nconst person = await ai.chat_shorthand.structured<Person>(\n  [{ role: 'user', content: 'Extract: John Doe, age 32.' }],\n  {\n    name:   'person',\n    schema: {\n      type:       'object',\n      properties: { name: { type: 'string' }, age: { type: 'number' } },\n      required:   ['name', 'age'],\n    },\n  }\n)\n// person.name === 'John Doe'\n// person.age  === 32\n\n// extractJSON<T> — JSON mode without schema, returns parsed object\ntype Tags = { tags: string[] }\nconst result = await ai.chat_shorthand.extractJSON<Tags>([\n  { role: 'user', content: 'Return a JSON object with a tags array for: TypeScript, Node, MongoDB.' }\n])\n// result.tags === ['TypeScript', 'Node', 'MongoDB']\n\n// withTools — function calling with pre-parsed args\nconst toolResult = await ai.chat_shorthand.withTools(messages, tools)\n// {\n//   calledTools: true,\n//   toolCalls: [\n//     { id: 'call_abc', name: 'get_weather', args: { city: 'Cairo' } }\n//   ],\n//   text: null,\n//   raw: ChatCompletion\n// }\n\n// If the model didn't call a tool:\n// { calledTools: false, toolCalls: [], text: 'Here is my answer...', raw: ... }\n\n// vision — image URL or base64 → analysis string\nconst analysis = await ai.chat_shorthand.vision(\n  'https://example.com/chart.png',\n  'Describe the trend shown in this chart.',\n  { detail: 'high' }  // 'auto' | 'low' | 'high'\n)\n\n// Base64 is also supported:\nconst analysis = await ai.chat_shorthand.vision(\n  'data:image/jpeg;base64,/9j/4AAQSkZJR...',\n  'What does this image show?'\n)\n\n// stream — messages → streaming with callback → full string returned\nconst fullText = await ai.chat_shorthand.stream(\n  [{ role: 'user', content: 'Write a quicksort in TypeScript.' }],\n  (chunk) => process.stdout.write(chunk)\n)\n\n// countTokens — estimate tokens before sending (sends 1-token request)\nconst { promptTokens, estimatedCostUsd } = await ai.chat_shorthand.countTokens([\n  { role: 'system', content: 'You are a helpful assistant.' },\n  { role: 'user',   content: longDocumentText },\n])\n// { promptTokens: 8420, estimatedCostUsd: 0.00002105 }\n\n// classify — classify input into one of a set of labels\nconst label = await ai.chat_shorthand.classify(\n  'I love this product, it works perfectly!',\n  ['positive', 'negative', 'neutral']\n)\n// 'positive'\n\nconst label = await ai.chat_shorthand.classify(\n  'The response time is very slow.',\n  ['bug', 'feature-request', 'performance', 'docs'],\n  'Classify this customer support ticket.',  // optional context\n  { model: 'gpt-4o-mini' }\n)\n\n// summarize — summarize text in different styles\nconst brief   = await ai.chat_shorthand.summarize(longText)                     // 1-2 sentences\nconst bullets = await ai.chat_shorthand.summarize(longText, 'bullet')           // 3-5 bullet points\nconst detail  = await ai.chat_shorthand.summarize(longText, 'detailed')         // 2-3 paragraphs\n\n// translate — translate text to any language\nconst spanish = await ai.chat_shorthand.translate('Hello, how are you?', 'Spanish')\nconst arabic  = await ai.chat_shorthand.translate('Good morning', 'Arabic')\nconst french  = await ai.chat_shorthand.translate(text, 'French', { model: 'gpt-4o-mini' })\n```\n\n---\n\n## Error Handling\n\n### The `SdkError` shape\n\nEvery error thrown by a Layer 1 function contains two things:\n\n```ts\ninterface SdkError {\n  formatted: FormattedError  // clean, typed, actionable\n  raw:       unknown         // original OpenAI APIError — never swallowed\n}\n\ninterface FormattedError {\n  code:          ErrorCode        // SDK-level named code — switch on this\n  status:        number           // HTTP status (0 for network errors)\n  type:          string           // OpenAI's raw error.type string\n  openaiCode:    string | null    // OpenAI's raw error.code string\n  message:       string           // human-readable description\n  hint:          string           // what to do about it\n  retryable:     boolean          // safe to retry?\n  retryAfterMs?: number           // from x-ratelimit-reset header — ms to wait\n  requestId?:    string           // x-request-id — for OpenAI support tickets\n  timestamp:     string           // ISO 8601 when the error occurred\n}\n```\n\n### `ErrorCode` enum — all 16 codes\n\n```ts\nimport { ErrorCode } from '@abdoseadaa/convai'\n\n// Auth (401) — never retry\nErrorCode.AUTH_INVALID_KEY      // bad, expired, or missing API key\nErrorCode.AUTH_NO_ORG           // account not part of an organization\n\n// Access (403) — never retry\nErrorCode.ACCESS_DENIED         // no permission for the resource\nErrorCode.ACCESS_REGION_BLOCKED // region or country restriction\nErrorCode.ACCESS_IP_BLOCKED     // IP not on project allowlist\n\n// Request (400 / 404 / 422) — never retry, fix the request\nErrorCode.NOT_FOUND             // conversation, item, or response not found\nErrorCode.BAD_REQUEST           // malformed parameters\nErrorCode.VALIDATION_FAILED     // schema or type mismatch (422)\nErrorCode.CONTEXT_TOO_LONG      // exceeds model's token limit\nErrorCode.INVALID_MODEL         // model doesn't exist or account lacks access\n\n// Capacity (429) — RATE_LIMITED is retryable, QUOTA_EXCEEDED is not\nErrorCode.RATE_LIMITED          // RPM/TPM exceeded — retry with backoff\nErrorCode.QUOTA_EXCEEDED        // billing quota exhausted — fix billing, don't retry\n\n// Server (500 / 502 / 503) — retry with backoff\nErrorCode.SERVER_ERROR          // internal server error (500/502)\nErrorCode.SERVICE_UNAVAILABLE   // overloaded (503)\n\n// Network — retry\nErrorCode.CONNECTION_FAILED     // cannot reach the API\nErrorCode.TIMEOUT               // request timed out\n\n// Fallback\nErrorCode.UNKNOWN               // catch-all for unexpected errors\n```\n\n### Type guard helpers\n\n```ts\nimport {\n  isRetryable,\n  isAuthError,\n  isRateLimitError,\n  isQuotaExceededError,\n  isAccessError,\n  isNotFoundError,\n  isNetworkError,\n  isServerError,\n  toLogObject,\n} from '@abdoseadaa/convai'\n\ntry {\n  const reply = await ai.shorthand.ask(convId, message)\n} catch (err) {\n  const e = err as SdkError\n\n  if (isRateLimitError(e)) {\n    const wait = e.formatted.retryAfterMs ?? 5000\n    await sleep(wait)\n    // retry...\n  }\n\n  if (isQuotaExceededError(e)) {\n    // Don't retry — this is a billing issue\n    notifyOpsTeam('OpenAI quota exhausted')\n    return\n  }\n\n  if (isAuthError(e)) {\n    // Surface to user — won't resolve with retries\n    throw new Error('AI service authentication failed. Contact support.')\n  }\n\n  if (isNetworkError(e) || isServerError(e)) {\n    // Safe to retry with exponential backoff\n    scheduleRetry(e.formatted.retryAfterMs)\n  }\n\n  // Log safely — strips raw to avoid logging API keys or sensitive headers\n  logger.error('AI call failed', toLogObject(e))\n}\n```\n\n### Retry pattern example\n\n```ts\nconst sleep = (ms: number) => new Promise(r => setTimeout(r, ms))\n\nasync function askWithRetry(convId: string, message: string, maxAttempts = 3) {\n  for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n    const result = await ai.safe.shorthand.ask(convId, message)\n\n    if (result.success) return result.data\n\n    const { error } = result\n\n    if (!error.retryable || attempt === maxAttempts) {\n      throw new Error(`${error.code}: ${error.message}`)\n    }\n\n    const wait = error.retryAfterMs ?? Math.pow(2, attempt) * 1000\n    await sleep(wait)\n  }\n}\n```\n\n---\n\n## TypeScript\n\n`convai` is written in TypeScript with strict mode enabled. Full type definitions are included — no separate `@types` package needed.\n\n### Exported types\n\n```ts\nimport type {\n  // Client\n  ConvSdkClient,       // return type of createClient()\n\n  // Config and options\n  ClientConfig,        // createClient() config\n  ChatOptions,         // per-call options for stateful chat\n  ConversationOptions, // conversation.create() options\n  ListOptions,         // history.list() pagination options\n  CompletionOptions,   // base options for all completion calls\n  StructuredOptions,   // extends CompletionOptions — adds strict\n  ToolOptions,         // extends CompletionOptions — adds toolChoice\n  VisionOptions,       // extends CompletionOptions — adds detail\n  ToolDefinition,      // OpenAI.ChatCompletionTool re-export\n  CompletionMessage,   // OpenAI.ChatCompletionMessageParam re-export\n\n  // Results\n  SdkResult,           // discriminated union: { success, data, error }\n  SdkError,            // { formatted: FormattedError, raw: unknown }\n  FormattedError,      // the clean error shape\n  WithToolsResult,     // return type of chat_shorthand.withTools()\n  TokenUsage,          // return type of tokens.getTokenUsage()\n  CompactionResult,    // return type of tokens.compactIfNeeded()\n  TranscriptMessage,   // { role, text, itemId }\n  RawItem,             // raw conversation item from OpenAI\n\n  // Errors\n  ErrorCode,           // enum of all 16 SDK error codes\n} from '@abdoseadaa/convai'\n```\n\n---\n\n## Common Patterns\n\n### Chatbot with session persistence\n\n```ts\nimport { createClient } from '@abdoseadaa/convai'\n\nconst ai = createClient({ apiKey: process.env.OPENAI_API_KEY! })\n\n// On first message — store convId in your DB per user\nconst { convId } = await ai.session.startSession('You are a helpful assistant.')\nawait db.users.update({ id: userId }, { aiConvId: convId })\n\n// On follow-up messages — load convId from DB\nconst user   = await db.users.findById(userId)\nconst reply  = await ai.shorthand.ask(user.aiConvId, userMessage)\n\n// On \"clear chat\"\nconst newConvId = await ai.session.resetSession(user.aiConvId)\nawait db.users.update({ id: userId }, { aiConvId: newConvId })\n```\n\n### Express route controller (safe layer)\n\n```ts\nimport { createClient, isRateLimitError } from '@abdoseadaa/convai'\nimport type { SdkError } from '@abdoseadaa/convai'\n\nconst ai = createClient({ apiKey: process.env.OPENAI_API_KEY! })\n\napp.post('/chat', async (req, res) => {\n  const { convId, message } = req.body\n\n  const result = await ai.safe.shorthand.ask(convId, message)\n\n  if (!result.success) {\n    const status = result.error.status || 500\n    return res.status(status).json({\n      error:     result.error.code,\n      message:   result.error.message,\n      retryable: result.error.retryable,\n    })\n  }\n\n  res.json({ reply: result.data })\n})\n```\n\n### Function calling loop\n\n```ts\nconst tools: ToolDefinition[] = [\n  {\n    type:     'function',\n    function: {\n      name:        'get_weather',\n      description: 'Get current weather for a city',\n      parameters: {\n        type:       'object',\n        properties: { city: { type: 'string' } },\n        required:   ['city'],\n      },\n    },\n  },\n]\n\nlet messages: CompletionMessage[] = [\n  { role: 'user', content: 'What is the weather in Cairo?' }\n]\n\nconst result = await ai.chat_shorthand.withTools(messages, tools)\n\nif (result.calledTools) {\n  const call   = result.toolCalls[0]\n  const weather = await getWeather(call.args.city as string)  // your function\n\n  // Continue the loop with the tool result\n  messages = [\n    ...messages,\n    { role: 'assistant', content: '', tool_calls: [{ id: call.id, type: 'function', function: { name: call.name, arguments: JSON.stringify(call.args) } }] },\n    { role: 'tool', content: JSON.stringify(weather), tool_call_id: call.id },\n  ]\n\n  const finalReply = await ai.chat_shorthand.ask(messages)\n  console.log(finalReply)\n}\n```\n\n### Auto-manage context length\n\n```ts\n// After every N turns, check token usage and compress if needed\nconst usage = await ai.tokens.getTokenUsage(convId)\n\nif (usage.totalTokens > 80_000) {\n  const { summary, itemsRemoved } = await ai.historyOps.summarizeAndCompress(convId)\n  console.log(`Compressed: removed ${itemsRemoved} items. Summary: ${summary}`)\n}\n\n// Or use the automatic threshold helper\nconst { compacted } = await ai.tokens.compactIfNeeded(convId, latestResponseId, 80_000)\n```\n\n### Broadcast to multiple conversations\n\n```ts\n// Run the same prompt across multiple user conversations in parallel\nconst userConvIds = await db.sessions.getActiveConvIds()\n\nconst results = await ai.multi.broadcast(userConvIds, 'System update: new features are available.')\n\nconst failed = results.filter(r => r.error !== null)\nif (failed.length > 0) {\n  logger.warn('Broadcast partial failure', { failed })\n}\n```\n\n---\n\n## File Structure\n\n```\nconvai/\n├── dist/                             ← compiled output (what npm ships)\n│   ├── index.js / index.d.ts        ← package entry\n│   ├── createClient.js              ← factory function\n│   ├── types/                       ← all interfaces and option types\n│   ├── errors/                      ← error codes, handler, guards\n│   ├── core/                        ← Layer 1: raw API calls\n│   ├── composed/                    ← Layer 1: composed shorthand functions\n│   ├── safe/                        ← Layer 2: SdkResult wrappers\n│   └── utils/                       ← wrapSafe utility\n├── README.md\n├── LICENSE\n└── package.json\n```\n\nSource code is excluded from the npm package — only `dist/` is shipped.\n\n---\n\n## Notes\n\n**Conversations API compatibility** — the `client.conversations.*` API is not yet available in the current `openai` npm SDK (v4.x). `convai` emulates it using `responses.create()` with `previous_response_id` chaining and `store: true` — which is exactly what the Conversations API wraps internally. Each turn chains off the conversation's current head (the latest response id), so multi-turn context accumulates correctly. When `client.conversations` becomes available in a future SDK release, `convai` will upgrade internally with no breaking changes to the consumer API.\n\n**Conversation state & persistence** — per-conversation state (the chain head, the persistent system prompt, and the local message transcript) is kept **in-memory for the lifetime of the `createClient()` instance**. It is not shared across processes or restarts. For durable chatbots, persist the `convId` plus your own message log in your database; on a new process, re-seed context with `historyOps.injectContext()` if you need the model to remember prior turns. The system prompt set by `startSession()` is re-applied as `instructions` on every turn, because the Responses API does not carry instructions across a `previous_response_id` chain.\n\n**Token cost estimates** — `getTokenUsage()` and `countTokens()` use gpt-4o pricing ($2.50/1M input, $10.00/1M output) as defaults. Actual costs vary by model.\n\n**`summarizeAndCompress()`** — uses the conversation's own context to summarize itself. For very long conversations, consider pruning first to avoid hitting the model's context limit before the summary call.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}