# AI SDK advanced

Source: https://docs.orbytelabs.com/integrations/ai-sdk-advanced

Connect access checks and BillingTelemetry yourself.

Use this mode when you need to own the access checks and telemetry lifetime, or integrate with `generateText` and `streamText`. For automatic setup, use [TrackedToolLoopAgent](/integrations/ai-sdk).

The Credit integration connects AI SDK 6 agents to wallet balances. Your agent calls `check` before work starts. `BillingTelemetry` tracks completed model steps and successful priced tool calls.

This guide uses `@orbytelabs/credit/ai-sdk` with AI SDK 6. The integration uses `experimental_telemetry.integrations`. It requires Node.js 24+ and an AI Gateway model that returns `providerMetadata.gateway.cost`.

Complete the [SDK setup](/quickstart) first. Keep `ORBYTE_API_KEY` and `AI_GATEWAY_API_KEY` on the server. The Credit SDK uses `https://api.orbytelabs.com` automatically. Your billing key selects the Credit application; your Gateway key authenticates model requests. See [AI Gateway setup](https://ai-sdk.dev/providers/ai-sdk-providers/ai-gateway) for Gateway credentials.

Install the SDK and integration dependencies:

```sh
pnpm add @orbytelabs/credit ai@^6 zod
```

## Define your agent [#define-your-agent]

Create the agent for each request, using that request's wallet identity and billing telemetry. This keeps concurrent customers' usage separate.

```ts title="src/agent.ts"
import { ToolLoopAgent, tool } from "ai";
import { z } from "zod";
import { check } from "@orbytelabs/credit";
import { BillingTelemetry } from "@orbytelabs/credit/ai-sdk";

export function createAgent(identity: string, billing: BillingTelemetry) {
  return new ToolLoopAgent({
    model: "openai/gpt-5.4-mini",
    instructions:
      "Use the weather tool when asked about weather. Explain that its results are demo data.",
    experimental_telemetry: { integrations: billing },
    prepareStep: async () => {
      billing.assert();
      const access = await check("inference", identity, {
        quantity: 1_000_000,
      });
      if (!access.allowed) throw new Error("Inference usage is not allowed.");
      return {};
    },
    tools: {
      weather: tool({
        description: "Get demo weather conditions for a location",
        metadata: { orbyte: { cost: "0.02" } },
        inputSchema: z.object({ location: z.string() }),
        execute: async ({ location }) => {
          const access = await check("weather", identity);
          if (!access.allowed) throw new Error("Tool usage is not allowed.");
          return { location, temperature: 28, condition: "sunny" };
        },
      }),
    },
  });
}
```

The `weather` tool costs `0.02` credits per successful call. `metadata.orbyte.cost` accepts a decimal string or number. Tools without this metadata, tools priced at zero, and failed tool calls do not produce tool charges.

The inference check above tests a threshold of `$0.001`, expressed as `1_000_000` nanodollars. With the catalog below, that costs `0.001` credits. This is a starting-balance threshold, not an estimate or limit on the next model call. `check` does not reserve funds. A larger actual cost or concurrent usage can still cause the later charge to fail.

If you add [plan allowances](/subscriptions), `check` also evaluates their limits and subscription state. A denied check can mean a reached limit or inactive subscription, not just an insufficient credit balance.

## Deploy the agent's catalog [#deploy-the-agents-catalog]

`aiSdkBilling` reads the agent's tool metadata and creates the credit and feature declarations.

Use this agent catalog for the recipe below. It defines `inference` with fixed pricing in `agent-credit`, while the general quickstart defines a callback-priced `inference` feature in `credits`. Deploying both configs to the same application updates that same feature key. Choose this config instead of the quickstart config for this recipe. If the application already uses the quickstart catalog, update every inference caller to send nanodollar quantities before switching catalogs. The quickstart's callback data is not compatible with this agent catalog.

```ts title="orbyte.config.ts"
import { aiSdkBilling, BillingTelemetry } from "@orbytelabs/credit/ai-sdk";
import { createAgent } from "./src/agent.js";

const billing = new BillingTelemetry({ identity: "catalog" });

export default aiSdkBilling(createAgent("catalog", billing));
```

The factory only defines the agent. Loading this config does not run the model, execute tools, or create a wallet for `catalog`. Keep generation and top-ups outside modules that the config imports.

```sh
pnpm exec orbyte deploy --dry-run
pnpm exec orbyte deploy
```

Use a setup key with catalog read and write permissions for deployment. The [CLI reference](/cli) covers configuration options, generated keys, and Production deployment.

The helper deploys this catalog for the example:

| Entry               | Configuration                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------- |
| Credit type         | `agent-credit`, named Agent credit.                                                      |
| `inference` feature | One credit per `1_000_000_000` nanodollars of Gateway cost, equal to one credit per USD. |
| `weather` feature   | `0.02` credits per successful call.                                                      |

`inference` is reserved for model usage. Do not give a priced tool this name. The helper uses tool names as feature keys and includes only tools with a positive `metadata.orbyte.cost`. It sets no usage-period deduplication.

The helper fixes the credit key and the inference conversion above. To choose a different inference rate, define your own [pricing configuration](/pricing) with the same `inference` feature and nanodollar quantity unit. Telemetry sends quantities; the deployed catalog sets the charge.

## Fund the wallet [#fund-the-wallet]

Fund the `agent-credit` balance before running paid work.

```ts
import { credit, topup } from "@orbytelabs/credit";

await topup("customer_123", credit("agent-credit")(10), {
  idempotencyKey: "agent_credit_purchase_123",
});
```

Run this when granting or selling credits, using the same key when retrying that deposit. Do not top up on every agent request. See [wallets and top-ups](/wallets-and-topups).

## Generate and finish billing [#generate-and-finish-billing]

Create one `BillingTelemetry` instance for each run. Keep it alive until generation and its billing callbacks finish.

```ts title="src/reply.ts"
import { BillingTelemetry } from "@orbytelabs/credit/ai-sdk";
import { createAgent } from "./agent.js";

export async function reply(identity: string, prompt: string) {
  using billing = new BillingTelemetry({
    identity,
    runId: crypto.randomUUID(),
  });
  const agent = createAgent(identity, billing);
  const result = await agent.generate({ prompt });
  return result.text;
}
```

The `using` declaration calls `billing.assert()` when the function exits. A failed final charge therefore rejects `reply` before it returns the answer. The `prepareStep` callback also calls `assert()` before each new model step to stop after an earlier billing failure.

`BillingTelemetry` queues billing errors by default. AI SDK catches errors thrown by telemetry integrations, so `errorMode: "throw"` alone does not guarantee that your generation rejects. Always check errors after generation with `using` or an explicit `billing.assert()`. The optional `billing.prepareStep` callback performs this check only in `throw` mode; the example uses an explicit check in both modes.

`assert()` throws a single stored failure or an `AggregateError` for several failures, then clears the error queue. That queue stores errors, not pending usage for later replay. Disposal does not retry failed charges.

## Stream a response [#stream-a-response]

Keep the billing scope open until you consume the stream and await completion.

```ts title="src/stream-reply.ts"
import { BillingTelemetry } from "@orbytelabs/credit/ai-sdk";
import { createAgent } from "./agent.js";

export async function streamReply(
  identity: string,
  prompt: string,
  write: (text: string) => void,
) {
  using billing = new BillingTelemetry({
    identity,
    runId: crypto.randomUUID(),
  });
  const agent = createAgent(identity, billing);
  const result = await agent.stream({ prompt });

  for await (const chunk of result.textStream) {
    write(chunk);
  }
  await result.text;
}
```

Do not create `using billing` inside an HTTP handler and immediately return a still-running stream. That closes the billing scope before the final charges exist. The code consuming the stream must own the telemetry lifetime and handle any billing error after completion. Content already sent to the client cannot be recalled if the final charge fails.

An interrupted model step may not return the cost metadata needed for billing. This adapter records completed steps for which Gateway supplies a cost; it cannot reconstruct missing provider costs after an interrupted stream.

## What gets tracked [#what-gets-tracked]

After each completed model step, telemetry reads the Gateway USD cost, rounds it up to a whole nanodollar, and tracks that quantity against `inference`. Zero-cost steps do not produce an event. Missing or invalid cost metadata records a billing error; the adapter does not estimate a charge from token counts.

After each successful priced tool call, it tracks quantity `1` against the tool name. The deployed feature price determines the charge. Redeploy after changing tool prices, and do not manually track the same model or tool usage again.

| Event      | ID when `runId` is supplied |
| ---------- | --------------------------- |
| Model step | `{runId}:{stepNumber}`      |
| Tool call  | `{runId}-{toolCallId}`      |

Use a unique run ID for each new execution. Reusing a run ID for new model work can make its step IDs collide with earlier charges. If `runId` is omitted, telemetry generates UUIDs when creating events.

The adapter also works with `generateText` and `streamText` through the same AI SDK 6 `experimental_telemetry.integrations` option. Wire your checks and final `assert()` around those calls too.

Direct provider calls without Gateway cost metadata are not supported by this automatic inference adapter. Use [check and track](/checking-and-tracking) with your own measured quantities or [callback pricing](/pricing) when your application calculates the cost itself.