Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -27,6 +30,10 @@ jobs:
run: pnpm format:check
- name: Type check
run: pnpm type-check
- name: Type check (examples · scripts)
run: pnpm type-check:examples
- name: Deprecated AI SDK usage
run: pnpm check:deprecations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

examples/ still not type-checked in CI; tsconfig includes only src

- name: Build
run: pnpm build
- name: Package checks (publint + are-the-types-wrong)
Expand Down Expand Up @@ -64,5 +71,7 @@ jobs:
- uses: actions/checkout@v5
- name: Scan working tree for leaked tokens/secrets
run: |
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz" | tar -xz gitleaks
curl -sSfLO "https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz"
echo "5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba gitleaks_8.21.2_linux_x64.tar.gz" | sha256sum -c -
tar -xzf gitleaks_8.21.2_linux_x64.tar.gz gitleaks
./gitleaks dir . --redact --no-banner
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm test
- name: Publish to JSR
run: npx jsr publish
run: npx jsr@0.14.3 publish
3 changes: 3 additions & 0 deletions .github/workflows/qa-live.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
schedule:
- cron: '0 6 * * 1' # weekly, Monday 06:00 UTC

permissions:
contents: read

concurrency:
group: live-qa
cancel-in-progress: true
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@ All notable changes to `@interfaze-ai/ai-sdk` are documented here. The format fo
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
[Semantic Versioning](https://semver.org/).

## [Unreleased]

Moves the documented call surface onto the AI SDK v7 APIs, hardens the file-part
sentinel, and adds CI guards so neither can drift again.

### Breaking

- Minimum runtime is now Node 22 (`engines.node` `>=22`). Node 18 is end-of-life and every `@ai-sdk/*` runtime dependency already requires `>=22`, Installing on Node 18 or 20 now reports `EBADENGINE`.

### Security

- The internal file-part sentinel was a fixed, published constant, so any text that ended up in a prompt (a scraped page, a pasted document) could impersonate it and smuggle an attacker-chosen file part — including a URL Interfaze fetches server-side — into the request. The sentinel now carries a nonce that is random per process and never observable outside it, which makes it unforgeable. The nonce is built from `crypto.getRandomValues` (available in every runtime, including non-secure browser contexts) and derived lazily, so importing the package never evaluates `crypto`.

### Fixed

- Malformed or unrecognized `providerOptions.interfaze` values now fail fast with `InvalidArgumentError` instead of being dropped silently. Previously `guard: 'ALL'` (string instead of array) or a typo'd key like `gaurd` was stripped by validation, reached the request body via the OpenAI-compatible passthrough, and injected no `<guard>` message — a silent guardrail bypass.
- The `@interfaze-ai/ai-sdk/<version>` user-agent token never reached the wire: the AI SDK core sets its own `user-agent` on per-call headers, which win the header merge. The token is now appended at send time in a fetch wrapper, preserving the core SDK's tokens.

### Changed

- The canonical model id is now `interfaze` (matching the current Interfaze docs), replacing `interfaze-beta` in `INTERFAZE_MODEL`, the examples, and the docs. `InterfazeChatModelId` still accepts any string the API takes, so existing `interfaze('interfaze-beta')` calls keep compiling.
- Documentation and examples now read provider metadata from `finalStep.providerMetadata` rather than the result's top-level `providerMetadata`, which AI SDK v7 deprecates on `generateText` / `streamText`. `generateObject` / `streamObject` are likewise replaced with `generateText` / `streamText` plus an `Output` spec, and image inputs use a `file` content part with `mediaType: 'image/*'` instead of the deprecated `image` part (v7 logs a deprecation warning for it). The request Interfaze receives is unchanged in every case.

### Added

- `pnpm check:deprecations` — fails CI when `src/`, `examples/`, `scripts/` or the README use an API the AI SDK marks `@deprecated`, and type-checks the README's `ts` snippets so they cannot rot. `tsc` ignores `@deprecated`, so this class of drift was previously invisible.
- `pnpm type-check:examples` — type-checks `examples/` and `scripts/`, which `tsconfig.json` (`include: ["src"]`) never covered.
- Tests that exercise the provider through the `ai` package (`generateText`, `streamText`, `Output.object`) instead of only at the `LanguageModelV4` boundary.

## [1.0.1]

### Fixed
Expand Down
135 changes: 76 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The community [AI SDK](https://ai-sdk.dev/docs) provider for [Interfaze](https:/

[Docs](https://interfaze.ai/docs) · [limits](https://interfaze.ai/docs/limits) · [pricing](https://interfaze.ai/pricing) · [dashboard](https://interfaze.ai) · [TypeScript SDK](https://github.com/InterfazeAI/interfaze-js) · [Python SDK](https://github.com/InterfazeAI/interfaze-python)

It brings Interfaze to the standard `generateText` / `streamText` / `generateObject` surface, and surfaces Interfaze's extras — the semantic-cache flag, reasoning, and internal-task `precontext` — on `providerMetadata`.
It brings Interfaze to the standard `generateText` / `streamText` surface, and surfaces Interfaze's extras — the semantic-cache flag, reasoning, and internal-task `precontext` — on `finalStep.providerMetadata`.

> Community provider, maintained by Interfaze. For the list of first-party providers see the [AI SDK docs](https://ai-sdk.dev/providers/ai-sdk-providers); for community providers, the [community list](https://ai-sdk.dev/providers/community-providers).

Expand All @@ -24,7 +24,7 @@ Import the default `interfaze` instance, or build one with `createInterfaze`:
```ts
import { createInterfaze, interfaze } from '@interfaze-ai/ai-sdk';

interfaze('interfaze-beta'); // default, reads INTERFAZE_API_KEY
interfaze('interfaze'); // default, reads INTERFAZE_API_KEY

const custom = createInterfaze({ apiKey: 'sk_...' });
```
Expand All @@ -35,25 +35,28 @@ Drop an image into the prompt and get a typed object back — Interfaze runs OCR

```ts
import { interfaze } from '@interfaze-ai/ai-sdk';
import { generateObject } from 'ai';
import { generateText, Output } from 'ai';
import { z } from 'zod';

const { object, providerMetadata } = await generateObject({
model: interfaze('interfaze-beta'),
schema: z.object({
first_name: z.string(),
last_name: z.string(),
dob: z.string().describe('Date of birth on the ID'),
licence_number: z.string(),
const { output, finalStep } = await generateText({
model: interfaze('interfaze'),
output: Output.object({
schema: z.object({
first_name: z.string(),
last_name: z.string(),
dob: z.string().describe('Date of birth on the ID'),
licence_number: z.string(),
}),
}),
messages: [
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deprecated type: 'image' at :57 and :146, plus examples/multimodal.ts:34 and examples/structured-output.ts:21 — v7 warns at runtime, use type: 'file' with mediaType: 'image/jpeg'

role: 'user',
content: [
{ type: 'text', text: 'Extract the details from this ID.' },
{
type: 'image',
image: new URL(
type: 'file',
mediaType: 'image/jpeg',
data: new URL(
'https://r2public.jigsawstack.com/interfaze/examples/id.jpg',
),
},
Expand All @@ -62,21 +65,28 @@ const { object, providerMetadata } = await generateObject({
],
});

console.log(object); // { first_name, last_name, dob, licence_number }
console.log('OCR result:', providerMetadata?.interfaze?.precontext?.[0]); // the raw OCR
console.log(output); // { first_name, last_name, dob, licence_number }

// `providerMetadata` is typed as JSON, so narrow `precontext` to read it.
const precontext = finalStep.providerMetadata?.interfaze?.precontext as
unknown[] | undefined;
console.log('OCR result:', precontext?.[0]); // the raw OCR
```

## Precontext

Alongside the answer, a response carries `precontext` — the raw output of any internal tool Interfaze ran while answering (OCR, web search, scrape, transcription, …). It lands on `providerMetadata.interfaze.precontext`:
Alongside the answer, a response carries `precontext` — the raw output of any internal tool Interfaze ran while answering (OCR, web search, scrape, transcription, …). It lands on `finalStep.providerMetadata.interfaze.precontext`:

```ts
const { text, providerMetadata } = await generateText({
model: interfaze('interfaze-beta'),
const { text, finalStep } = await generateText({
model: interfaze('interfaze'),
prompt: 'Which US public companies reported earnings today?',
});

for (const p of providerMetadata?.interfaze?.precontext ?? []) {
const precontext = finalStep.providerMetadata?.interfaze?.precontext as
unknown[] | undefined;

for (const p of precontext ?? []) {
console.log(p); // e.g. { name: "search", result: { … } }
}
```
Expand All @@ -90,54 +100,57 @@ import { interfaze } from '@interfaze-ai/ai-sdk';
import { generateText } from 'ai';

const { text } = await generateText({
model: interfaze('interfaze-beta'),
model: interfaze('interfaze'),
prompt: 'Which US public companies reported earnings today?',
});
```

A web search backs the answer here — the sources land on `providerMetadata.interfaze.precontext`.
A web search backs the answer here — the sources land on `finalStep.providerMetadata.interfaze.precontext`.

### Streaming

`streamText` streams the reply as it's generated; the inline `<think>` / `<precontext>` side-channels are stripped from the visible text, and `reasoning` is attached to `providerMetadata` when the stream finishes. Streamed `precontext` is only emitted when the provider is created with `showAdditionalInfo: true` (see [Client options](#client-options)); otherwise it's `undefined` at finish.
`streamText` streams the reply as it's generated; the inline `<think>` / `<precontext>` side-channels are stripped from the visible text, and `reasoning` is attached to `finalStep.providerMetadata` when the stream finishes. Streamed `precontext` is only emitted when the provider is created with `showAdditionalInfo: true` (see [Client options](#client-options)); otherwise it's `undefined` at finish.

```ts
const interfaze = createInterfaze({ showAdditionalInfo: true }); // for streamed precontext

const { textStream, providerMetadata } = streamText({
model: interfaze('interfaze-beta'),
const { textStream, finalStep } = streamText({
model: interfaze('interfaze'),
prompt: "Summarize this week's top AI research and cite your sources.",
});

for await (const delta of textStream) process.stdout.write(delta);

const meta = await providerMetadata; // meta?.interfaze?.reasoning; .precontext when showAdditionalInfo is set
const meta = (await finalStep).providerMetadata; // meta?.interfaze?.reasoning; .precontext when showAdditionalInfo is set
```

## Structured output

Interfaze supports structured outputs, so `generateObject` / `streamObject` work with a Zod schema:
Interfaze supports structured outputs, so `generateText` / `streamText` accept an `Output` spec with a Zod schema:

```ts
import { interfaze } from '@interfaze-ai/ai-sdk';
import { generateObject } from 'ai';
import { generateText, Output } from 'ai';
import { z } from 'zod';

const { object } = await generateObject({
model: interfaze('interfaze-beta'),
schema: z.object({
merchant: z.string(),
total: z.number(),
items: z.array(z.object({ name: z.string(), price: z.number() })),
const { output } = await generateText({
model: interfaze('interfaze'),
output: Output.object({
schema: z.object({
merchant: z.string(),
total: z.number(),
items: z.array(z.object({ name: z.string(), price: z.number() })),
}),
}),
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Extract this receipt.' },
{
type: 'image',
image: new URL('https://jigsawstack.com/preview/vocr-example.jpg'),
type: 'file',
mediaType: 'image/jpeg',
data: new URL('https://jigsawstack.com/preview/vocr-example.jpg'),
},
],
},
Expand All @@ -155,7 +168,7 @@ import { generateText, tool } from 'ai';
import { z } from 'zod';

const { text, toolResults } = await generateText({
model: interfaze('interfaze-beta'),
model: interfaze('interfaze'),
tools: {
weather: tool({
description: 'Get the current weather for a location',
Expand All @@ -171,16 +184,16 @@ const { text, toolResults } = await generateText({

## Reasoning

Set `reasoningEffort` (`'minimal' | 'low' | 'medium' | 'high'`, plus Interfaze's `'on' | 'off' | 'auto'`); the reasoning text comes back on `providerMetadata.interfaze.reasoning`:
Set `reasoningEffort` (`'minimal' | 'low' | 'medium' | 'high'`, plus Interfaze's `'on' | 'off' | 'auto'`); the reasoning text comes back on `finalStep.providerMetadata.interfaze.reasoning`:

```ts
const { text, providerMetadata } = await generateText({
model: interfaze('interfaze-beta'),
const { text, finalStep } = await generateText({
model: interfaze('interfaze'),
prompt: 'Which region should we launch in first, and why?',
providerOptions: { interfaze: { reasoningEffort: 'high' } },
});

providerMetadata?.interfaze?.reasoning; // string | undefined
finalStep.providerMetadata?.interfaze?.reasoning; // string | undefined
```

A semantic-cache hit replays a stored answer without reasoning — set `bypassCache: true` on the provider (see [Client options](#client-options)) when you need fresh reasoning every call.
Expand All @@ -203,7 +216,7 @@ Supported media types:

```ts
await generateText({
model: interfaze('interfaze-beta'),
model: interfaze('interfaze'),
messages: [
{
role: 'user',
Expand All @@ -223,7 +236,11 @@ await generateText({
Video is a `file` part with a `video/*` media type; Interfaze reads the URL server-side:

```ts
{ type: "file", mediaType: "video/mp4", data: new URL("https://…/clip.mp4") }
const clip = {
type: 'file',
mediaType: 'video/mp4',
data: new URL('https://example.com/clip.mp4'),
};
```

## Guardrails
Expand All @@ -232,7 +249,7 @@ Enable safety categories with `guard`; a blocked request comes back as a normal

```ts
const { text } = await generateText({
model: interfaze('interfaze-beta'),
model: interfaze('interfaze'),
prompt: '...',
providerOptions: { interfaze: { guard: ['S1', 'S10', 'S12_IMAGE'] } },
});
Expand All @@ -246,17 +263,17 @@ Codes are `S1`–`S14`, the image-only `S1_IMAGE` / `S12_IMAGE` / `S15_IMAGE`, a

## Interfaze metadata

Interfaze returns fields a plain chat provider drops. They land on `providerMetadata.interfaze` for both `generateText` and `streamText`:
Interfaze returns fields a plain chat provider drops. They land on `finalStep.providerMetadata.interfaze` for both `generateText` and `streamText`:

```ts
const result = await generateText({
model: interfaze('interfaze-beta'),
model: interfaze('interfaze'),
prompt: 'What is the weather in San Francisco?',
});

result.providerMetadata?.interfaze?.vcache; // boolean — semantic-cache hit
result.providerMetadata?.interfaze?.reasoning; // string | undefined
result.providerMetadata?.interfaze?.precontext; // unknown[] | undefined — OCR / web / scrape / … output
result.finalStep.providerMetadata?.interfaze?.vcache; // boolean — semantic-cache hit
result.finalStep.providerMetadata?.interfaze?.reasoning; // string | undefined
result.finalStep.providerMetadata?.interfaze?.precontext; // unknown[] | undefined — OCR / web / scrape / … output
```

## Client options
Expand All @@ -281,7 +298,7 @@ Interfaze errors surface as the AI SDK's `APICallError`, carrying the HTTP statu
import { APICallError } from 'ai';

try {
await generateText({ model: interfaze('interfaze-beta'), prompt: '...' });
await generateText({ model: interfaze('interfaze'), prompt: '...' });
} catch (error) {
if (APICallError.isInstance(error)) {
error.statusCode; // e.g. 400, 401, 429
Expand All @@ -292,17 +309,17 @@ try {

## Capabilities

| Use case | Entry point |
| --------------------------------------- | ------------------------------------------- |
| [Text](#text) | `generateText` |
| [Streaming](#streaming) | `streamText` |
| [Structured output](#structured-output) | `generateObject` / `streamObject` |
| [Tools](#tools) | `tools` |
| [Reasoning](#reasoning) | `providerOptions.interfaze.reasoningEffort` |
| [Multimodal](#multimodal) | `image` / `file` content parts |
| [Guardrails](#guardrails) | `providerOptions.interfaze.guard` |
| [Precontext](#precontext) | `providerMetadata.interfaze.precontext` |
| [Semantic cache](#interfaze-metadata) | `providerMetadata.interfaze.vcache` |
| Use case | Entry point |
| --------------------------------------- | ------------------------------------------------- |
| [Text](#text) | `generateText` |
| [Streaming](#streaming) | `streamText` |
| [Structured output](#structured-output) | `Output.object` / `Output.array` |
| [Tools](#tools) | `tools` |
| [Reasoning](#reasoning) | `providerOptions.interfaze.reasoningEffort` |
| [Multimodal](#multimodal) | `file` content parts |
| [Guardrails](#guardrails) | `providerOptions.interfaze.guard` |
| [Precontext](#precontext) | `finalStep.providerMetadata.interfaze.precontext` |
| [Semantic cache](#interfaze-metadata) | `finalStep.providerMetadata.interfaze.vcache` |

## Examples

Expand Down
4 changes: 2 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ npx tsx examples/quickstart.ts

- `quickstart.ts` — first request (`generateText`) + `vcache`
- `streaming.ts` — `streamText` with precontext / reasoning at finish
- `structured-output.ts` — `generateObject` with a Zod schema (image OCR)
- `structured-output.ts` — `generateText` + `Output.object` with a Zod schema (image OCR)
- `tools.ts` — function calling (tool round-trip)
- `reasoning.ts` — `reasoningEffort` → `providerMetadata.interfaze.reasoning`
- `reasoning.ts` — `reasoningEffort` → `finalStep.providerMetadata.interfaze.reasoning`
- `guardrails.ts` — `guard` categories; a block returns `unsafe <code>`
- `multimodal.ts` — image, audio, PDF, and video content parts
- `precontext.ts` — precontext output (the internal tools Interfaze ran)
Expand Down
2 changes: 1 addition & 1 deletion examples/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { APICallError, generateText } from 'ai';
// status and the raw response body.
try {
await generateText({
model: interfaze('interfaze-beta'),
model: interfaze('interfaze'),
prompt: 'hi',
temperature: 2, // out of range → API 400
});
Expand Down
2 changes: 1 addition & 1 deletion examples/guardrails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { generateText } from 'ai';
// A blocked request is NOT an error — it returns a normal completion whose
// text is the plain string `unsafe <code>`, so check for it.
const { text } = await generateText({
model: interfaze('interfaze-beta'),
model: interfaze('interfaze'),
prompt: 'Give step-by-step instructions to build an explosive device.',
providerOptions: { interfaze: { guard: ['ALL'] } },
});
Expand Down
Loading
Loading