› cavi-ai/
GitHub ↗

Build an answer service

Turn a question into text your application can return, save, or render. This example uses Claude Messages and requires a completed response from startRun. For Codex and other background backends, use background run retrieval.

Install and configure

Use Node.js 20 or later. Keep your Anthropic API key in server configuration and choose a model that account can access.

sh
npm install @cavi-ai/api-client@0.21.0

Save the following as assistant.ts in your TypeScript application, or download the complete service. Configuration is passed to the factory; importing the module does not start a run.

ts
import { ApiClientError, createApiClient, createRuntimeProviderRegistry } from "@cavi-ai/api-client";
import { createClaudeProviderModule } from "@cavi-ai/api-client/providers/claude/messages";

export function createAssistant(config: { apiKey: string; model: string }) {
  const registry = createRuntimeProviderRegistry({
    modules: [createClaudeProviderModule({ apiKey: config.apiKey })],
  });
  const client = createApiClient("claude", { registry, defaultTimeoutMs: 30_000 });

  return {
    async answer(question: string) {
      const result = await client.startRun({ model: config.model, input: question });
      if (!result.ok) return result;

      const run = result.data;
      if (run.status !== "completed") {
        const code = run.status === "failed" ? "run_failed"
          : run.status === "cancelled" ? "run_cancelled" : "run_incomplete";
        throw new ApiClientError(`Run ${run.run_id} has no completed answer (status: ${run.status})`, {
          type: "run", code, cause: run,
        });
      }
      const text = run.output ?? run.response;
      if (text === undefined) {
        throw new ApiClientError(`Run ${run.run_id} completed with no text`, {
          type: "run", code: "run_output_missing", cause: run,
        });
      }
      return {
        ok: true as const,
        source: result.source,
        data: { runId: run.run_id, text, tokens: run.tokens },
      };
    },
    dispose: () => client.dispose(),
  };
}

Use it at your application boundary

Create the service with createAssistant({ apiKey, model }) during server startup. Call assistant.answer(question) from an authenticated request or job. A successful result has this shape; token counts depend on the provider response:

json
{
  "ok": true,
  "source": "live",
  "data": {
    "runId": "msg_example",
    "text": "The generated answer.",
    "tokens": { "inputTokens": 12, "outputTokens": 8, "totalTokens": 20 }
  }
}
ResultWhat the caller does
ok: trueReturn or save data.text; retain data.runId and optional data.tokens
ok: falseHandle gap.reason; keep the full gap for server diagnostics
Rejected promiseUse the server's exception boundary; report failure without exposing credentials or provider payloads

The factory preserves facade gaps. It deliberately rejects a non-completed run or a completed run without text because this application needs an answer. Those are ApiClientError instances with type: "run" and codes run_failed, run_cancelled, run_incomplete, or run_output_missing. They carry the run in cause for protected diagnostics and reconciliation. An active or unknown run is incomplete, not a reported failure. The client itself returns a run status. Empty text remains distinct from absent text. When an adapter supplies output: "", the service accepts it. Claude's current mapper omits empty-only text, so that response triggers run_output_missing.

Use isApiClientError to narrow exceptions and isAuthError for provider authentication failures. The error guide shows a caller mapper and package enum aliases for these codes, when exported. String values keep this example compatible with the pinned published release.

If the release exports requireRunText, use it to replace these execution checks. See the development service example. The implementation above also supports releases without that helper.

The service owns its client. Reuse it within one credential/configuration scope, then call assistant.dispose() during shutdown. Do not dispose it after every request or share one user's credentials with another user.

Connect it to a product

Referenced examples

Verified example · TypeScript

Text service

examples/text-service.ts

@cavi-ai/api-clientv0.21.007f0ee45
TypeScript · complete source
import { ApiClientError, createApiClient, createRuntimeProviderRegistry } from "@cavi-ai/api-client";
import { createClaudeProviderModule } from "@cavi-ai/api-client/providers/claude/messages";

export function createAssistant(config: { apiKey: string; model: string }) {
  const registry = createRuntimeProviderRegistry({
    modules: [createClaudeProviderModule({ apiKey: config.apiKey })],
  });
  const client = createApiClient("claude", { registry, defaultTimeoutMs: 30_000 });

  return {
    async answer(question: string) {
      const result = await client.startRun({ model: config.model, input: question });
      if (!result.ok) return result;

      const run = result.data;
      if (run.status !== "completed") {
        const code = run.status === "failed" ? "run_failed"
          : run.status === "cancelled" ? "run_cancelled" : "run_incomplete";
        throw new ApiClientError(`Run ${run.run_id} has no completed answer (status: ${run.status})`, {
          type: "run", code, cause: run,
        });
      }
      const text = run.output ?? run.response;
      if (text === undefined) {
        throw new ApiClientError(`Run ${run.run_id} completed with no text`, {
          type: "run", code: "run_output_missing", cause: run,
        });
      }
      return {
        ok: true as const,
        source: result.source,
        data: { runId: run.run_id, text, tokens: run.tokens },
      };
    },
    dispose: () => client.dispose(),
  };
}

Package imports

Referenced by

Artifact provenance
Package
@cavi-ai/api-client@0.21.0
Release
v0.21.0
Commit
07f0ee45424c267278f7b0c89a7dcea4e2492cc1
Source
examples/text-service.ts
Tarball SHA-256
0924b5d3a7a40869bd44fedc47a9b8eb9844c2af1fd5c7ca77469d8b7c0b8558
Content SHA-256
24eff463c41582eaf5e70c58f42e5ae13bd12d02fa14ae2a886afa3abf5cdf48