> ## Documentation Index
> Fetch the complete documentation index at: https://telluvian.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SDKs

> Using the official OpenAI Python and TypeScript SDKs against Telluvian.

Source: https://telluvian.ai/docs/sdks

The API implements both the OpenAI **Chat Completions** and **Responses**
interfaces, so the **official OpenAI SDKs work unmodified**. Point `base_url` at
Telluvian and use your Telluvian key. There is no Telluvian SDK to install and
nothing to wrap.

This page covers `client.chat.completions`. For `client.responses` and
server-side conversation state, see [Responses API](https://telluvian.ai/docs/responses.md).

Probe fields are extensions to the standard SDK types. The examples below
explicitly enable probing wherever they read scores.

## Install

```bash
pip install openai
```

```bash
npm install openai
```

## Client setup

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.telluvian.ai/v1",
    api_key=os.environ["TELLUVIAN_API_KEY"],
)
```

```typescript
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://api.telluvian.ai/v1',
  apiKey: process.env.TELLUVIAN_API_KEY,
});
```

> **Note the /v1**
>
> The base URL includes `/v1`. The SDKs append paths like `/chat/completions`
> to whatever you give them, so omitting it produces a 404 that looks like a
> routing bug.

## Reading scores off a response

`scores` and `tokens` are extensions to the OpenAI schema. The SDKs keep
unrecognised response fields, so they are readable as ordinary attributes.

```python
response = client.chat.completions.create(
    model="google/gemma-4-31b-it",
    extra_body={"include_scores": True},
    messages=[{"role": "user", "content": "When was the Eiffel Tower built?"}],
)

print(response.choices[0].message.content)
# The Eiffel Tower opened in 1889.

# Extension fields. The SDK keeps them in `model_extra`, and attribute
# access works. Your type checker will not know about them.
print(response.tokens)   # ['The', ' Eiffel', ' Tower', ' opened', ' in', ' 1889', '.']
print(response.scores)   # {'hallucination': [0.01, 0.02, 0.02, 0.05, 0.03, 0.61, 0.01]}
```

```typescript
const response = await client.chat.completions.create({
  model: 'google/gemma-4-31b-it',
  messages: [{ role: 'user', content: 'When was the Eiffel Tower built?' }],

  include_scores: true,
} as OpenAI.ChatCompletionCreateParamsNonStreaming & { include_scores: boolean });

console.log(response.choices[0].message.content);
// The Eiffel Tower opened in 1889.

// Extension fields. Cast, because they are not in the OpenAI types.
const { tokens, scores } = response as typeof response & {
  tokens: string[];
  scores: Record<string, (number | null)[]>;
};
console.log(tokens);
console.log(scores.hallucination);
```

## Finding the flagged span

`tokens[i]` and `scores["hallucination"][i]` are aligned and the same length,
which is what makes the scores usable. You can point at the exact claim rather
than at the whole answer.

```python
THRESHOLD = 0.5   # pick your own; see Reading the scores

flagged = [
    tok for tok, score
    in zip(response.tokens, response.scores["hallucination"])
    if score is not None and score > THRESHOLD
]
print("".join(flagged))   # ' 1889'
```

```typescript
const THRESHOLD = 0.5;

const flagged = tokens.filter((_, i) => scores.hallucination[i] != null && scores.hallucination[i]! > THRESHOLD);
console.log(flagged.join(''));   // ' 1889'
```

> **Reassembling the text**
>
> `"".join(tokens)` equals `choices[0].message.content` exactly. You never need
> to re-tokenise to line the scores up with the text.

## Streaming

With probing enabled, content chunks carry available scores for that token.
A missing reading, role chunk, or finish chunk may have no score.

```python
stream = client.chat.completions.create(
    model="google/gemma-4-31b-it",
    extra_body={"include_scores": True},
    messages=[{"role": "user", "content": "Tell me about Ada Lovelace."}],
    stream=True,
)

usage = None
for chunk in stream:
    # The final chunk carries usage and no content; role-only chunks carry
    # neither. Guard both rather than assuming every chunk has a delta.
    if chunk.usage is not None:
        usage = chunk.usage

    delta = chunk.choices[0].delta.content
    if not delta:
        continue

    scores = getattr(chunk, "scores", None)
    score = scores.get("hallucination") if scores else None
    print(delta, score)

print(usage)   # CompletionUsage(prompt_tokens=9, completion_tokens=7, ...)
```

```typescript
const stream = await client.chat.completions.create({
  model: 'google/gemma-4-31b-it',
  messages: [{ role: 'user', content: 'Tell me about Ada Lovelace.' }],
  stream: true,

  include_scores: true,
} as OpenAI.ChatCompletionCreateParamsStreaming & { include_scores: boolean });

let usage = null;
for await (const chunk of stream) {
  if (chunk.usage) usage = chunk.usage;

  const delta = chunk.choices[0]?.delta?.content;
  if (!delta) continue;

  const { scores } = chunk as typeof chunk & {
    scores?: Record<string, number>;
  };
  console.log(delta, scores?.hallucination);
}

console.log(usage);
```

> **Usage arrives without asking**
>
> Token counts are attached to the final chunk of every stream. You do not need
> `stream_options: {include_usage: true}`. It is always there, and it is what
> your request is billed from.

## Turning verification off

Probing is off by default. You can also disable it explicitly; Python passes
the extension through `extra_body`, while TypeScript sends it inline.

```python
# Python's SDK validates arguments against the OpenAI schema, so a probe
# extension must go through extra_body or it is rejected before sending.
response = client.chat.completions.create(
    model="google/gemma-4-31b-it",
    messages=[{"role": "user", "content": "Hello"}],
    extra_body={"include_scores": False},
)

print(response.scores)   # None
```

```typescript
// The JS SDK passes unknown top-level keys straight through, so the field
// goes inline. The cast is only to satisfy the compiler.
const response = await client.chat.completions.create({
  model: 'google/gemma-4-31b-it',
  messages: [{ role: 'user', content: 'Hello' }],
  include_scores: false,
} as OpenAI.ChatCompletionCreateParamsNonStreaming & { include_scores: boolean });
```

See [`include_scores`](https://telluvian.ai/docs/include-scores.md) for what this changes, and
[Pricing](https://telluvian.ai/docs/pricing.md) for what it saves.

## Listing models

```python
for model in client.models.list().data:
    extra = model.model_extra or {}
    print(model.id, extra.get("pricing"), extra.get("probe"))
```

```typescript
const { data } = await client.models.list();
for (const model of data) {
  console.log(model.id, (model as never as { pricing: unknown }).pricing);
}
```

## Other OpenAI-compatible tooling

Anything that speaks Chat Completions and lets you set a base URL should work,
LangChain, LlamaIndex, Vercel AI SDK, and so on. The caveat is always the same:
tools that re-serialise responses into their own types will **drop `scores` and
`tokens`**, because those are not part of the OpenAI schema. If a framework is
losing your scores, call the endpoint directly for the scored path.
