opengradient-ai-provider
v0.1.1
Published
Vercel AI SDK provider for OpenGradient TEE LLM (verifiable inference)
Maintainers
Readme
opengradient-ai-provider
A Vercel AI SDK community provider for the OpenGradient TEE LLM: verifiable inference inside a Trusted Execution Environment, paid for on-chain via x402.
Implements LanguageModelV3: generateText, streamText, and tool calling, with
TEE attestation surfaced through providerMetadata.
Install
npm install opengradient-ai-provider aiai (the Vercel AI SDK) is a peer dependency.
Quick start
import { createOpenGradient } from 'opengradient-ai-provider';
import { generateText } from 'ai';
const opengradient = createOpenGradient({
// server-only; falls back to OPENGRADIENT_PRIVATE_KEY when omitted
privateKey: process.env.OPENGRADIENT_PRIVATE_KEY,
// see "TEE endpoints" below, currently required while the SDK registry is stale
llmServerUrl: process.env.OPENGRADIENT_LLM_SERVER_URL?.split(','),
});
const { text, providerMetadata } = await generateText({
model: opengradient('anthropic/claude-haiku-4-5'),
prompt: 'In one sentence, what is a TEE?',
});
console.log(text);
console.log('TEE signature:', providerMetadata?.opengradient?.teeSignature);TEE endpoints (llmServerUrl): currently required
The published OpenGradient SDK ships a default on-chain TEE registry that
currently returns no active TEEs, so the normal discovery path fails. As an
interim workaround, pass one or more TEE endpoints explicitly via llmServerUrl
(a string or an array). The provider tries them in order and fails over to the
next on a connection failure, surfacing a warning when it does.
const opengradient = createOpenGradient({
llmServerUrl: ['https://13.59.207.188', 'https://3.15.214.21'],
});Caveats: passing llmServerUrl bypasses on-chain TLS pinning, and the endpoint
IPs rotate over time. This is temporary until the SDK's registry discovery is
fixed (after which no llmServerUrl is needed). You can also set
OPENGRADIENT_LLM_SERVER_URL (comma-separated for a failover list).
Streaming
import { streamText } from 'ai';
const result = streamText({
model: opengradient('anthropic/claude-haiku-4-5'),
prompt: 'Explain verifiable inference in two sentences.',
});
for await (const delta of result.textStream) process.stdout.write(delta);
console.log('\n', await result.providerMetadata);Tool calling
import { generateText, tool, jsonSchema } from 'ai';
const { toolCalls, finishReason } = await generateText({
model: opengradient('anthropic/claude-haiku-4-5'),
prompt: 'What is the weather in Paris? Use the tool.',
tools: {
get_weather: tool({
description: 'Get the current weather for a city.',
inputSchema: jsonSchema<{ city: string }>({
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
}),
}),
},
});streamText with tools works too, but is degraded upstream: the SDK falls back
to non-streaming and returns the tool call in a single final chunk (arguments are
not token-streamed). The provider synthesizes the proper V3 tool-call stream parts
from that chunk.
TEE attestation
Every response exposes attestation and payment data under
providerMetadata.opengradient:
| Field | Description |
| ------------------------------------------------------- | ------------------------------------------------------------ |
| teeSignature | RSA-PSS signature over the response (verifiable). |
| teeId | On-chain registry id of the enclave that served the request. |
| teeTimestamp | ISO-8601 signing time. |
| teeEndpoint | Endpoint URL of the serving TEE. |
| teePaymentAddress | Payment address registered for the TEE. |
| paymentHash | x402 payment hash (non-streaming only). |
| dataSettlementTransactionHash, dataSettlementBlobId | Data-settlement details, when available. |
Configuration
createOpenGradient(settings): all fields optional, each has an env fallback.
| Setting | Type | Env fallback | Notes |
| -------------------- | -------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------- |
| privateKey | string | OPENGRADIENT_PRIVATE_KEY | EVM key that pays for inference. Server-only. |
| rpcUrl | string | OPENGRADIENT_RPC_URL | RPC for the on-chain TEE registry (see "Networks & RPC"). |
| llmServerUrl | string \| string[] | OPENGRADIENT_LLM_SERVER_URL (comma-separated) | Explicit TEE endpoint(s) with failover (see above). |
| maxPaymentValue | bigint | OPENGRADIENT_MAX_PAYMENT_VALUE | Passed to the SDK. Not enforced as a spend cap upstream, so do not rely on it as one. |
| teeRegistryAddress | string | OPENGRADIENT_TEE_REGISTRY_ADDRESS | Override the TEERegistry contract. |
Per-call options
await generateText({
model: opengradient('anthropic/claude-haiku-4-5'),
prompt: '...',
providerOptions: {
opengradient: {
// x402 settlement mode: 'private' | 'batch' | 'individual'
settlementMode: 'individual',
},
},
});Networks & RPC
Two separate networks are involved, plus the TEE servers that run the inference:
| Network | Role | Custom RPC |
| -------------------- | ------------------------------------------------------- | -------------------------------------------------------------------- |
| Base mainnet | Payment: the OPG token, x402, and Permit2 all live here | BASE_MAINNET_RPC env var (read by the SDK) |
| OpenGradient EVM | The on-chain TEE registry used to discover enclaves | rpcUrl setting / OPENGRADIENT_RPC_URL, plus teeRegistryAddress |
| TEE servers (HTTP) | The inference itself | llmServerUrl (see above) |
A few things worth knowing:
- The registry RPC is fully configurable through
rpcUrl/teeRegistryAddress. When you use thellmServerUrlworkaround, the registry network is skipped entirely. - The Base RPC can only be changed through the
BASE_MAINNET_RPCenvironment variable. The SDK reads it directly and does not accept it through provider settings, so there is no provider-levelbaseRpcUrloption.checkOpenGradientSetuptakes its ownbaseRpcUrlfor the Base reads it performs. - Mainnet only, by OpenGradient's design. The OPG token and the x402 payment
rails are deployed on Base mainnet and hardcoded in the SDK (
BASE_OPG_ADDRESSis a fixed Base address). There is no testnet OPG or testnet payment path, so this provider cannot run on a testnet until OpenGradient ships one. The provider is a thin wrapper over the SDK and cannot move the payment chain.
Security: server-only
This provider takes an EVM private key that controls real funds. Run it
server-side only (route handler, server action, backend). Never bundle it into
client-side code, never hard-code or commit the key, and load it from
OPENGRADIENT_PRIVATE_KEY.
Prerequisite: OPG / Permit2 approval
The paying wallet must hold OPG on Base mainnet (and a little ETH for the one-time approval gas), and grant Permit2 approval once before any inference call. The provider intentionally never does this, since it sends on-chain transactions, so you run it yourself:
import { ensureOpgApproval } from 'opengradient-sdk';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(
process.env.OPENGRADIENT_PRIVATE_KEY as `0x${string}`,
);
// run once: approve up to 100 OPG for Permit2 (sends a tx; needs ETH for gas)
await ensureOpgApproval(account, 5, 100);Preflight: check your setup (read-only)
checkOpenGradientSetup inspects the wallet's OPG balance, ETH-for-gas, and Permit2
allowance on Base so you can fix funding/approval before paying. It sends no
transactions:
import { checkOpenGradientSetup } from 'opengradient-ai-provider';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(
process.env.OPENGRADIENT_PRIVATE_KEY as `0x${string}`,
);
const report = await checkOpenGradientSetup(account);
if (!report.ready) {
console.error(report.issues.join('\n'));
// e.g. "Permit2 allowance for OPG is 0 OPG. Run ensureOpgApproval(account, 5, 100)..."
}It accepts a viem Account or a plain address, plus optional { baseRpcUrl } for
a custom Base RPC and { minAllowance } in raw atomic units. Inference errors
are likewise diagnostic: a 402 tells you to check OPG funds and allowance, not
just that payment failed.
Limitations
- No multimodal: file / image / audio parts are dropped with a warning (text only).
- Streaming + tools is degraded: the tool call arrives in one synthesized final
chunk, not token-streamed, and the upstream SDK omits token
usagefor this path, so thefinishpart reports no usage. toolChoice: { type: 'tool' }(force a specific tool) is unsupported; it falls back to'auto'with a warning.- Ignored sampling params:
topP,topK,presencePenalty,frequencyPenalty,seed,abortSignal(mid-flight), andheadersare not supported and warn. - JSON without a schema:
responseFormat: { type: 'json' }without a schema maps tojson_object, which Anthropic models reject, so provide a schema. - OPG / Permit2 approval is a prerequisite (see above).
Examples
Runnable scripts live in examples/ on GitHub
(generate-text.ts, stream-text.ts, tool-call.ts).
License
MIT © Gilberts Ahumada
