diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c297801..c554fb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ concurrency: jobs: static: - name: static (lint · types · build · package) + name: static (format · types · build · package) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -20,8 +20,8 @@ jobs: node-version: 22 cache: npm - run: npm ci - - name: Lint + format check (biome) - run: npm run lint + - name: Format check (prettier) + run: npm run format:check - name: Type check run: npm run typecheck - name: Build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9eb4595 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +coverage +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e68cb8f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 150, + "tabWidth": 2, + "singleQuote": false, + "bracketSameLine": false, + "trailingComma": "es5" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index f6537c8..93b67a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to `interfaze` are documented here. The format follows Initial release — a typed wrapper over the OpenAI SDK for the Interfaze API. ### Added + - `Interfaze` client (composition over `openai@6`) exposing `chat.completions`, `models`, and `tasks.*`. - `chat.completions.create()` returning `InterfazeChatCompletion` with typed `precontext`, `reasoning`, and `vcache`. - `chat.completions.stream()` — an Interfaze-tolerant streaming helper (handles role-less deltas; surfaces ``/``). diff --git a/README.md b/README.md index 37531e8..593cf25 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,8 @@ -# interfaze-js +# Interfaze Typescript and Javascript SDK The official [Interfaze](https://interfaze.ai) SDK for TypeScript/JavaScript -- **Familiar chat surface** - `chat.completions`, streaming, tools, and structured output. -- **Typed Interfaze extras** - `precontext` (internal tool output), `reasoning`, and `vcache` (semantic-cache hit) on every response. -- **One-line task helpers** - OCR, web search, scraping, speech-to-text, translation, object/GUI detection, forecasting. -- **Multimodal inputs** - images, PDFs, audio, video, and CSV, by URL or base64. -- **Universal** - Node 18+, browsers, and edge/workers; ESM + CommonJS; fully typed. - -## Learn more - -- [interfaze.ai](https://interfaze.ai) - dashboard and API keys. -- [Python SDK](https://github.com/InterfazeAI/interfaze-python). - -## Capabilities - -| Category | Capabilities | -| ---------------- | ----------------------------------------------------------- | -| **Chat & text** | Chat completions, structured output, tools, reasoning | -| **Vision & OCR** | `tasks.ocr` - text and structured data from images and PDFs | -| **Web** | `tasks.webSearch`, `tasks.scrape` | -| **Audio** | `tasks.transcribe` - speech-to-text | -| **Detection** | `tasks.objectDetection`, `tasks.guiDetection` | -| **Translation** | `tasks.translate` | -| **Forecasting** | `tasks.forecast` - time-series prediction | +[Docs](https://interfaze.ai/docs) · [limits](https://interfaze.ai/docs/limits) · [pricing](https://interfaze.ai/pricing) · [dashboard](https://interfaze.ai) · [Python SDK](https://github.com/InterfazeAI/interfaze-python) ## Install @@ -34,96 +13,366 @@ npm install interfaze ## Setup -Get an API key from the [Interfaze dashboard](https://interfaze.ai), then: - ```ts -import { Interfaze } from "interfaze"; // or: import Interfaze from "interfaze" +import { Interfaze } from "interfaze"; const interfaze = new Interfaze({ apiKey: "sk_..." }); // or set INTERFAZE_API_KEY and call new Interfaze() ``` -CommonJS: `const { Interfaze } = require("interfaze");`. `model` defaults to `interfaze-beta`. +## Your first request + +This guide will get you started with your first request to Interfaze, which follows the Chat Completions API standard. + +```ts +import { Interfaze, responseFormat } from "interfaze"; +import { z } from "zod"; + +const interfaze = new Interfaze(); + +const IdCard = 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 res = await interfaze.chat.completions.create({ + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Extract the details from this ID." }, + { + type: "image_url", + image_url: { + url: "https://r2public.jigsawstack.com/interfaze/examples/id.jpg", + }, + }, + ], + }, + ], + response_format: responseFormat(z.toJSONSchema(IdCard), "id_card"), +}); + +const idCard = JSON.parse(res.choices[0]?.message.content ?? "{}"); +console.log(idCard); // your IdCard schema +console.log("OCR result:", res.precontext?.[0]?.result); // the raw OCR that produced it +``` + +## Precontext -## Usage +Alongside the answer, a response carries `precontext` - the raw metadata Interfaze produced while answering: -Chat completion: +```ts +for (const p of res.precontext ?? []) { + console.log(p.name, p.result); // e.g. "ocr" -> { text, boxes, confidence, … } +} +``` + +## Chat ```ts const res = await interfaze.chat.completions.create({ - messages: [{ role: "user", content: "Write a haiku about deterministic AI." }], + messages: [ + { + role: "user", + content: "Which US public companies reported earnings today?", + }, + ], }); -console.log(res.choices[0].message.content); -console.log("cache hit:", res.vcache); // typed Interfaze extra + +res.choices[0]?.message.content; ``` -Task helpers - each returns the extracted result directly: +The result is a standard `ChatCompletion` with `precontext`, and `reasoning` added (a web search backs the answer here). + +### Streaming + +Stream the reply as it's generated; the final completion still carries `precontext` and `reasoning`. ```ts -await interfaze.tasks.ocr("https://example.com/receipt.jpg"); -await interfaze.tasks.webSearch("latest AI agent news"); -await interfaze.tasks.transcribe("https://example.com/audio.wav"); -await interfaze.tasks.scrape("https://example.com/product"); -await interfaze.tasks.translate("Hello", { to: "French" }); -await interfaze.tasks.objectDetection("https://example.com/photo.jpg"); -await interfaze.tasks.guiDetection("https://example.com/screenshot.png"); -await interfaze.tasks.forecast("https://example.com/timeseries.csv", { periods: 30 }); +const stream = interfaze.chat.completions.stream({ + messages: [ + { + role: "user", + content: "Summarize this week's top AI research and cite your sources.", + }, + ], +}); + +for await (const text of stream.textDeltas()) process.stdout.write(text); + +const final = await stream.finalChatCompletion(); // .precontext (the sources), .reasoning ``` -Structured output: +`textDeltas()` yields display-ready text - the inline ``/`` side-channels are stripped and returned structured on `finalChatCompletion()`. For the raw chunk iterator, use `create({ stream: true })`. + +### Structured output + +`responseFormat()` takes a JSON Schema - or a zod schema via `z.toJSONSchema()` - and normalizes it for Interfaze: ```ts -import { responseFormat } from "interfaze"; +import { responseFormat, inputs } from "interfaze"; +import { z } from "zod"; + +const Receipt = z.object({ + merchant: z.string(), + total: z.number(), + items: z.array(z.object({ name: z.string(), price: z.number() })), +}); const res = await interfaze.chat.completions.create({ - messages: [{ role: "user", content: "Weather in Tokyo?" }], - response_format: responseFormat({ - type: "object", - properties: { city: { type: "string" }, temp_c: { type: "number" } }, - required: ["city", "temp_c"], - }), + messages: [ + { + role: "user", + content: [{ type: "text", text: "Extract this receipt." }, inputs.image("https://jigsawstack.com/preview/vocr-example.jpg")], + }, + ], + response_format: responseFormat(z.toJSONSchema(Receipt), "receipt"), }); -const data = JSON.parse(res.choices[0].message.content!); + +const receipt = JSON.parse(res.choices[0]?.message.content ?? "{}"); // { merchant, total, items: [...] } ``` -`responseFormat()` normalizes the schema (object-root wrap, `.optional()` → `.nullable()`); with zod v4 pass `responseFormat(z.toJSONSchema(schema))`. +Prefer a plain schema? +Pass one directly: +`responseFormat({ type: "object", properties: { … }, required: [ … ] })`. `message.content` comes back as a JSON string, so parse it - and keep the root an `object`, since a non-object root is wrapped under a `result` key. + +### Tools and function calling -Streaming - `.stream()` yields typed events, with the inline ``/`` side-channels stripped: +Interfaze supports tools - define `tools`, read `message.tool_calls`, run them, then pass the results back for the final answer. ```ts -const stream = interfaze.chat.completions.stream({ - messages: [{ role: "user", content: "Tell me a story." }], +import type { ChatCompletionMessageParam, ChatCompletionTool } from "interfaze"; + +const tools: ChatCompletionTool[] = [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather for a city.", + parameters: { + type: "object", + properties: { city: { type: "string", description: "e.g. Tokyo" } }, + required: ["city"], + }, + }, + }, +]; + +const messages: ChatCompletionMessageParam[] = [{ role: "user", content: "What's the weather in Tokyo?" }]; + +const res = await interfaze.chat.completions.create({ + messages, + tools, + tool_choice: "auto", }); -for await (const text of stream.textDeltas()) { - process.stdout.write(text); + +const message = res.choices[0]?.message; +if (message) messages.push(message); // the assistant turn, carrying any tool_calls + +for (const call of message?.tool_calls ?? []) { + if (call.type !== "function") continue; + const { city } = JSON.parse(call.function.arguments); + messages.push({ + role: "tool", + tool_call_id: call.id, + content: await getWeather(city), + }); } -const final = await stream.finalChatCompletion(); + +// send the tool results back for the final answer +const final = await interfaze.chat.completions.create({ + messages, + tools, + tool_choice: "auto", +}); ``` -> `stream.textDeltas()` yields clean visible text; iterating the stream directly gives the raw -> events, and `create({ stream: true })` gives the raw chunk iterator. +## Reasoning -## Inputs +Ask for reasoning with `reasoning_effort`; the text comes back on `res.reasoning`. + +```ts +const res = await interfaze.chat.completions.create({ + reasoning_effort: "high", // also accepts Interfaze's "on" / "off" / "auto" + messages: [ + { + role: "user", + content: "Which region should we launch in first, and why?", + }, + ], +}); + +res.reasoning; // reasoning text - present with reasoning_effort and no schema +``` + +## Multimodal Inputs + +Interfaze handles images, PDFs, audio, video, and CSV. The simplest way is to drop a public URL into the prompt - Interfaze fetches and reads it: + +```ts +await interfaze.chat.completions.create({ + messages: [ + { + role: "user", + content: "Summarize this document: https://arxiv.org/pdf/1706.03762", + }, + ], +}); +``` + +Or attach it as a content part - a `file` part for documents, audio, and video; `image_url` for images: + +```ts +await interfaze.chat.completions.create({ + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Summarize this document." }, + { + type: "file", + file: { + filename: "paper.pdf", + file_data: "https://arxiv.org/pdf/1706.03762", + }, + }, + ], + }, + ], +}); +``` + +`file_data` also takes a base64 data URI: + +```ts +{ type: "file", file: { filename: "report.pdf", file_data: `data:application/pdf;base64,${base64}` } } +``` + +`inputs.*` is a typed shortcut that builds these parts - and turns raw bytes or a local file into a data URI for you: ```ts import { inputs } from "interfaze"; -inputs.image("https://…/a.png"); // image_url part -inputs.file("https://…/doc.pdf"); // file part (pdf/csv/xml/json/txt/video…) -inputs.audio("https://…/a.wav"); // input_audio part -await inputs.dataUrl(bytes, "image/png"); // base64 data URI (Uint8Array/ArrayBuffer/Blob) -await inputs.fromPath("./doc.pdf"); // Node-only: read a local file +inputs.image("https://…/photo.png"); // image_url part +inputs.file("https://…/report.pdf"); // file part +inputs.audio("https://…/call.wav"); // input_audio part +inputs.file(await inputs.dataUrl(pdfBytes, "application/pdf"), { + filename: "report.pdf", +}); // bytes / Blob +inputs.image(await inputs.fromPath("./photo.png")); // Node: read a local file +``` + +Interfaze rejects `image/gif` and `image/avif` client-side, and `inputs.fromPath` is Node-only. + +## Tasks + +A task ([run_task](https://interfaze.ai/docs/run-tasks)) runs one built-in tool instead of the full model - faster and cheaper, but limited to that tool and its fixed output structure. So `task` can't be combined with a custom `response_format`; reach for a full completion (like [your first request](#your-first-request)) when you need the whole model or your own schema. + +The `tasks.*` helpers are the shortest way - each takes a source and returns the raw result (typed `unknown`, so validate before use): + +```ts +await interfaze.tasks.ocr(url); +await interfaze.tasks.objectDetection(url); +await interfaze.tasks.guiDetection(url); +await interfaze.tasks.webSearch(query); +await interfaze.tasks.scrape(url); +await interfaze.tasks.transcribe(url); +await interfaze.tasks.translate(text, { to: "Spanish" }); +await interfaze.tasks.forecast(csvUrl, { periods: 30, unit: "days" }); ``` -URLs and base64 both work; `image/gif` and `image/avif` are rejected client-side. +For a multi-part message (text plus an input), set `task` on a normal `create` call - the result comes back on `message.content`: + +```ts +const res = await interfaze.chat.completions.create({ + task: "ocr", + messages: [ + { + role: "user", + content: [{ type: "text", text: "Extract the total." }, inputs.image(url)], + }, + ], +}); +const { result } = JSON.parse(res.choices[0]?.message.content ?? "{}"); // same output as tasks.ocr() +``` + +Or a `` system message plus an empty response schema: + +```ts +import { emptyTaskSchema } from "interfaze"; -## Interfaze extras +const res = await interfaze.chat.completions.create({ + messages: [ + { role: "system", content: "ocr" }, + { + role: "user", + content: [{ type: "text", text: "Extract the total." }, inputs.image(url)], + }, + ], + response_format: emptyTaskSchema(), // an empty JSON schema +}); +const { result } = JSON.parse(res.choices[0]?.message.content ?? "{}"); +``` + +## Guardrails + +Enable safety categories with `guard`; a blocked request comes back as a normal completion, not an exception. + +```ts +const res = await interfaze.chat.completions.create({ + guard: ["S1", "S10", "S12_IMAGE"], + messages: [{ role: "user", content: "..." }], +}); +``` + +A match returns the plain string `unsafe S1` as `message.content` - so check for it. See the exported `GUARD_CODES` / `GUARD_LABELS`. + +## Client options + +Set router, cache, and streaming behavior once on the client: + +```ts +const interfaze = new Interfaze({ + showAdditionalInfo: true, // stream deltas as they're produced + bypassMoe: true, // skip the mixture-of-experts router + bypassCache: true, // skip the semantic cache +}); +``` + +## Errors + +Interfaze re-exports typed error classes to catch and narrow on: + +```ts +import { BadRequestError, InterfazeError, RateLimitError } from "interfaze"; +``` + +`InterfazeError` is client-side (missing key, invalid guard code, stream misuse). Everything else is an `APIError` subclass carrying `status` and `code` - `BadRequestError` (400), `AuthenticationError` (401), `RateLimitError` (429), and so on. + +## Capabilities -- `res.precontext` - raw outputs of any internal tools that ran (OCR/web/scrape/STT/forecast/…). -- `res.reasoning` - reasoning text (with `reasoning_effort: "high"` and no schema). -- `res.vcache` - whether the semantic cache was hit. -- `reasoning_effort` also accepts `"on" | "off" | "auto"`. -- Guardrails: `create({ guard: ["S1", "S12_IMAGE"], … })`. -- Control options: `new Interfaze({ showAdditionalInfo, bypassMoe, bypassCache, adminKey })`. +| Use case | Entry point | +| --------------------------------------- | --------------------------------------------- | +| [Chat](#chat) | `chat.completions.create` | +| [Streaming](#streaming) | `chat.completions.stream` | +| [Structured output](#structured-output) | `responseFormat()` | +| [Reasoning](#reasoning) | `reasoning_effort` | +| [Tools](#tools-and-function-calling) | `tools` | +| [Multimodal inputs](#inputs) | `inputs.*` | +| [OCR](#tasks) | `tasks.ocr` | +| [Object and GUI detection](#tasks) | `tasks.objectDetection`, `tasks.guiDetection` | +| [Web search and scraping](#tasks) | `tasks.webSearch`, `tasks.scrape` | +| [Speech to text](#tasks) | `tasks.transcribe` | +| [Translation](#tasks) | `tasks.translate` | +| [Forecasting](#tasks) | `tasks.forecast` | +| [Guardrails](#guardrails) | `guard` | +| [Precontext](#precontext) | `res.precontext` | + +## Examples + +Runnable snippets in [`examples/`](./examples). ## License diff --git a/biome.json b/biome.json deleted file mode 100644 index 070ea65..0000000 --- a/biome.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.5.5/schema.json", - "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, - "files": { "includes": ["src/**", "test/**", "scripts/**", "examples/**"] }, - "assist": { "enabled": false }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 120 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "style": { "noNonNullAssertion": "off" }, - "complexity": { "useLiteralKeys": "off" } - } - }, - "javascript": { - "formatter": { "quoteStyle": "double", "semicolons": "always" } - } -} diff --git a/examples/file-inputs.ts b/examples/file-inputs.ts index bc97965..8898961 100644 --- a/examples/file-inputs.ts +++ b/examples/file-inputs.ts @@ -7,10 +7,7 @@ const a = await interfaze.chat.completions.create({ messages: [ { role: "user", - content: [ - { type: "text", text: "Summarize this PDF." }, - inputs.file("https://arxiv.org/pdf/1706.03762", { filename: "paper.pdf" }), - ], + content: [{ type: "text", text: "Summarize this PDF." }, inputs.file("https://arxiv.org/pdf/1706.03762", { filename: "paper.pdf" })], }, ], }); diff --git a/package-lock.json b/package-lock.json index af507ff..47b3b53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "interfaze-js", + "name": "interfaze", "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "interfaze-js", + "name": "interfaze", "version": "1.0.1", "license": "MIT", "dependencies": { @@ -13,9 +13,10 @@ }, "devDependencies": { "@arethetypeswrong/cli": "0.18.5", - "@biomejs/biome": "2.5.5", "@types/node": "~22.20.1", "@vitest/coverage-v8": "^2.1.9", + "openai": "~6.47.0", + "prettier": "^3.9.6", "publint": "0.3.22", "tsup": "^8.5.1", "tsx": "^4.23.1", @@ -188,169 +189,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@biomejs/biome": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.5.tgz", - "integrity": "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.5", - "@biomejs/cli-darwin-x64": "2.5.5", - "@biomejs/cli-linux-arm64": "2.5.5", - "@biomejs/cli-linux-arm64-musl": "2.5.5", - "@biomejs/cli-linux-x64": "2.5.5", - "@biomejs/cli-linux-x64-musl": "2.5.5", - "@biomejs/cli-win32-arm64": "2.5.5", - "@biomejs/cli-win32-x64": "2.5.5" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.5.tgz", - "integrity": "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.5.tgz", - "integrity": "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.5.tgz", - "integrity": "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.5.tgz", - "integrity": "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.5.tgz", - "integrity": "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.5.tgz", - "integrity": "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.5.tgz", - "integrity": "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.5.tgz", - "integrity": "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, "node_modules/@braidai/lang": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", @@ -2561,6 +2399,7 @@ "version": "6.47.0", "resolved": "https://registry.npmjs.org/openai/-/openai-6.47.0.tgz", "integrity": "sha512-xYr+R9woSzWxVxeiqkkNbHhv89tZDEI6eBMbrdPnv3poh+mijHvbhS35a+3o6xHa411/ns8j5ENY3So9DCXWYw==", + "dev": true, "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", @@ -2783,6 +2622,22 @@ } } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/publint": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.22.tgz", @@ -4689,7 +4544,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index dd7f42d..1fad271 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,8 @@ "scripts": { "build": "tsup", "typecheck": "tsc --noEmit", - "lint": "biome check .", - "format": "biome format --write .", - "format:check": "biome format --check .", + "format": "prettier --write .", + "format:check": "prettier --check .", "check:pkg": "publint --strict && attw --pack", "test": "vitest run", "test:watch": "vitest", @@ -71,11 +70,11 @@ }, "devDependencies": { "@arethetypeswrong/cli": "0.18.5", - "@biomejs/biome": "2.5.5", "@types/node": "~22.20.1", "@vitest/coverage-v8": "^2.1.9", - "publint": "0.3.22", "openai": "~6.47.0", + "prettier": "^3.9.6", + "publint": "0.3.22", "tsup": "^8.5.1", "tsx": "^4.23.1", "typescript": "~5.9.3", diff --git a/scripts/capture-fixtures.ts b/scripts/capture-fixtures.ts index e8cc211..4148af7 100644 --- a/scripts/capture-fixtures.ts +++ b/scripts/capture-fixtures.ts @@ -30,7 +30,7 @@ save( model: "interfaze-beta", messages: [{ role: "user", content: "Say hi in one short sentence." }], max_tokens: 60, - }), + }) ); save( @@ -39,7 +39,7 @@ save( model: "interfaze-beta", messages: [{ role: "user", content: "Return a JSON object with keys city and temp_c for Tokyo." }], response_format: { type: "json_object" }, - }), + }) ); save( @@ -51,17 +51,15 @@ save( { role: "user", content: [{ type: "text", text: "Extract total price" }, receipt as never] }, ], response_format: { type: "json_schema", json_schema: { name: "empty_schema", schema: {} } } as never, - }), + }) ); save( "precontext.json", await client.chat.completions.create({ model: "interfaze-beta", - messages: [ - { role: "user", content: [{ type: "text", text: "Extract total price from this receipt" }, receipt as never] }, - ], - }), + messages: [{ role: "user", content: [{ type: "text", text: "Extract total price from this receipt" }, receipt as never] }], + }) ); { diff --git a/scripts/qa-live.ts b/scripts/qa-live.ts index ec60141..39593a5 100644 --- a/scripts/qa-live.ts +++ b/scripts/qa-live.ts @@ -54,7 +54,7 @@ await check("structured output (responseFormat)", async () => { properties: { greeting: { type: "string" }, count: { type: "number" } }, required: ["greeting", "count"], }, - "greeting", + "greeting" ), }); const p = JSON.parse(r.choices[0]!.message.content!); @@ -219,10 +219,7 @@ await check("input: base64 image (data URI)", async () => { messages: [ { role: "user", - content: [ - { type: "text", text: "What is in this image? One sentence." }, - inputs.image(await inputs.dataUrl(bytes, "image/jpeg")), - ], + content: [{ type: "text", text: "What is in this image? One sentence." }, inputs.image(await inputs.dataUrl(bytes, "image/jpeg"))], }, ], }); diff --git a/src/chat.ts b/src/chat.ts index 1894387..596c1c1 100644 --- a/src/chat.ts +++ b/src/chat.ts @@ -1,11 +1,7 @@ import type OpenAI from "openai"; import type { APIPromise } from "openai"; import type { Stream } from "openai/streaming"; -import type { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionMessageParam, -} from "openai/resources/chat/completions/completions"; +import type { ChatCompletion, ChatCompletionChunk, ChatCompletionMessageParam } from "openai/resources/chat/completions/completions"; import { INTERFAZE_MODEL } from "./constants.js"; import { InterfazeError } from "./errors.js"; @@ -31,11 +27,7 @@ export function toInterfaze(raw: ChatCompletion, opts: { stripFence: boolean }): return r; } -function injectTags( - messages: readonly ChatCompletionMessageParam[], - task?: string, - guard?: string, -): ChatCompletionMessageParam[] { +function injectTags(messages: readonly ChatCompletionMessageParam[], task?: string, guard?: string): ChatCompletionMessageParam[] { const tags = [task, guard].filter(Boolean).join(" "); if (!tags) return messages.slice(); const out = messages.slice(); @@ -68,9 +60,7 @@ function prepare(params: InterfazeChatCompletionCreateParams): { let rf = response_format; if (task) { if (rf && isNonEmptySchema(rf)) { - throw new InterfazeError( - "A non-empty `response_format` cannot be combined with `task` (Interfaze runs tasks with raw output).", - ); + throw new InterfazeError("A non-empty `response_format` cannot be combined with `task` (Interfaze runs tasks with raw output)."); } rf = emptyTaskSchema(); } @@ -81,7 +71,7 @@ function prepare(params: InterfazeChatCompletionCreateParams): { messages: injectTags( messages as ChatCompletionMessageParam[], task ? `${task}` : undefined, - guard?.length ? guardTag(guard) : undefined, + guard?.length ? guardTag(guard) : undefined ), }; if (rf !== undefined) body["response_format"] = rf; @@ -95,17 +85,11 @@ export class InterfazeCompletions { this.#openai = openai; } - create( - params: InterfazeChatCompletionCreateParamsNonStreaming, - options?: RequestOptions, - ): APIPromise; - create( - params: InterfazeChatCompletionCreateParamsStreaming, - options?: RequestOptions, - ): APIPromise>; + create(params: InterfazeChatCompletionCreateParamsNonStreaming, options?: RequestOptions): APIPromise; + create(params: InterfazeChatCompletionCreateParamsStreaming, options?: RequestOptions): APIPromise>; create( params: InterfazeChatCompletionCreateParams, - options?: RequestOptions, + options?: RequestOptions ): APIPromise | APIPromise> { const { body, stripFence } = prepare(params); const raw = this.#openai.chat.completions.create(body as never, options); @@ -116,10 +100,7 @@ export class InterfazeCompletions { } /** Streaming with an Interfaze-tolerant accumulator; also surfaces ``/``. */ - stream( - params: Omit, - options?: RequestOptions, - ): InterfazeChatCompletionStream { + stream(params: Omit, options?: RequestOptions): InterfazeChatCompletionStream { const { body, stripFence } = prepare({ ...params, stream: true } as InterfazeChatCompletionCreateParamsStreaming); return new InterfazeChatCompletionStream(this.#openai, body, options, stripFence); } diff --git a/src/client.ts b/src/client.ts index 5be1161..b7ce732 100644 --- a/src/client.ts +++ b/src/client.ts @@ -32,9 +32,7 @@ export class Interfaze { const resolvedKey = apiKey ?? envKey(); if (!resolvedKey) { - throw new InterfazeError( - "Missing API key. Pass `new Interfaze({ apiKey })` or set the INTERFAZE_API_KEY environment variable.", - ); + throw new InterfazeError("Missing API key. Pass `new Interfaze({ apiKey })` or set the INTERFAZE_API_KEY environment variable."); } const headers: Record = { ...(defaultHeaders as Record | undefined) }; diff --git a/src/constants.ts b/src/constants.ts index a12b04c..41ec737 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,16 +3,7 @@ export const INTERFAZE_MODEL = "interfaze-beta"; export const DEFAULT_TIMEOUT_MS = 900_000; /** Task names accepted in a `` tag. */ -export const TASK_NAMES = [ - "ocr", - "object_detection", - "gui_detection", - "web_search", - "scraper", - "translate", - "speech_to_text", - "forecast", -] as const; +export const TASK_NAMES = ["ocr", "object_detection", "gui_detection", "web_search", "scraper", "translate", "speech_to_text", "forecast"] as const; /** Guardrail categories (`ALL` enables everything). */ export const GUARD_CODES = [ diff --git a/src/index.ts b/src/index.ts index 9810847..70e59f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,14 +19,7 @@ export type { InterfazeChatCompletionCreateParamsStreaming, } from "./types.js"; -export { - TASK_NAMES, - GUARD_CODES, - GUARD_LABELS, - INTERFAZE_MODEL, - INTERFAZE_BASE_URL, - LIMITS, -} from "./constants.js"; +export { TASK_NAMES, GUARD_CODES, GUARD_LABELS, INTERFAZE_MODEL, INTERFAZE_BASE_URL, LIMITS } from "./constants.js"; export { toFile } from "openai"; export { diff --git a/src/stream.ts b/src/stream.ts index f4ab718..4dad507 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -43,10 +43,9 @@ export class InterfazeChatCompletionStream implements AsyncIterable> { if (!this.#raw) { - this.#raw = this.#openai.chat.completions.create( - { ...this.#body, stream: true } as never, - this.#options, - ) as unknown as Promise>; + this.#raw = this.#openai.chat.completions.create({ ...this.#body, stream: true } as never, this.#options) as unknown as Promise< + AsyncIterable + >; } return this.#raw; } diff --git a/src/tasks.ts b/src/tasks.ts index 15716d2..d46935c 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -21,11 +21,7 @@ export class Tasks { this.#c = completions; } - async #run( - task: TaskName, - content: string | ChatCompletionContentPart[], - options?: RequestOptions, - ): Promise { + async #run(task: TaskName, content: string | ChatCompletionContentPart[], options?: RequestOptions): Promise { const res = await this.#c.create({ task, messages: [{ role: "user", content }] }, options); const raw = res.choices[0]?.message.content; if (!raw) return undefined; @@ -73,17 +69,10 @@ export class Tasks { } /** Forecast a time series from a CSV (MoE-selected, so prompt-driven; reads the `forecast` precontext). */ - async forecast( - csvSource: string, - opts: { periods?: number; unit?: string } = {}, - options?: RequestOptions, - ): Promise { + async forecast(csvSource: string, opts: { periods?: number; unit?: string } = {}, options?: RequestOptions): Promise { const n = opts.periods ?? 10; const unit = opts.unit ?? "days"; - const res = await this.#c.create( - { messages: [{ role: "user", content: `Forecast the next ${n} ${unit} of this: ${csvSource}` }] }, - options, - ); + const res = await this.#c.create({ messages: [{ role: "user", content: `Forecast the next ${n} ${unit} of this: ${csvSource}` }] }, options); const pc = res.precontext?.find((p) => p.name === "forecast"); return pc?.result ?? res.choices[0]?.message.content ?? undefined; } diff --git a/src/types.ts b/src/types.ts index 795adb3..76c4604 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,18 +39,10 @@ interface InterfazeExtraParams { guard?: GuardCode[]; } -export type InterfazeChatCompletionCreateParamsNonStreaming = Omit< - ChatCompletionCreateParamsNonStreaming, - "reasoning_effort" | "model" -> & +export type InterfazeChatCompletionCreateParamsNonStreaming = Omit & InterfazeExtraParams & { stream?: false | null }; -export type InterfazeChatCompletionCreateParamsStreaming = Omit< - ChatCompletionCreateParamsStreaming, - "reasoning_effort" | "model" -> & +export type InterfazeChatCompletionCreateParamsStreaming = Omit & InterfazeExtraParams & { stream: true }; -export type InterfazeChatCompletionCreateParams = - | InterfazeChatCompletionCreateParamsNonStreaming - | InterfazeChatCompletionCreateParamsStreaming; +export type InterfazeChatCompletionCreateParams = InterfazeChatCompletionCreateParamsNonStreaming | InterfazeChatCompletionCreateParamsStreaming; diff --git a/test/chat.test.ts b/test/chat.test.ts index 012a53e..6a40d81 100644 --- a/test/chat.test.ts +++ b/test/chat.test.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from "vitest"; import { toInterfaze } from "../src/chat.js"; import { HEADERS } from "../src/constants.js"; -import type { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionMessageToolCall, -} from "openai/resources/chat/completions/completions"; +import type { ChatCompletion, ChatCompletionChunk, ChatCompletionMessageToolCall } from "openai/resources/chat/completions/completions"; import { completion, fixture, jsonResponse, mockInterfaze, sseResponse, systemContent } from "./helpers.js"; function functionName(call: ChatCompletionMessageToolCall): string { @@ -91,7 +87,7 @@ describe("request serialization", () => { type: "json_schema", json_schema: { name: "s", schema: { type: "object", properties: { a: { type: "string" } } } }, }, - }), + }) ).toThrow(/non-empty `response_format` cannot be combined with `task`/); }); @@ -171,7 +167,7 @@ describe("request serialization", () => { const { interfaze, calls } = mockInterfaze(() => jsonResponse(basic), { adminKey: "client-default" }); await interfaze.chat.completions.create( { messages: [{ role: "user", content: "x" }] }, - { headers: { [HEADERS.adminKey]: "per-request-override" } }, + { headers: { [HEADERS.adminKey]: "per-request-override" } } ); expect(calls[0]!.headers.get(HEADERS.adminKey)).toBe("per-request-override"); }); @@ -224,9 +220,7 @@ describe("response mapping", () => { it("preserves the raw HTTP response via .withResponse() (guards the _thenUnwrap mapping)", async () => { const { interfaze } = mockInterfaze(() => jsonResponse(basic)); - const { data, response } = await interfaze.chat.completions - .create({ messages: [{ role: "user", content: "hi" }] }) - .withResponse(); + const { data, response } = await interfaze.chat.completions.create({ messages: [{ role: "user", content: "hi" }] }).withResponse(); expect(response.status).toBe(200); expect(typeof data.vcache).toBe("boolean"); expect(data.choices[0]!.message.content).toBeDefined(); @@ -260,9 +254,7 @@ describe("response mapping", () => { it("leaves tool-call responses with content: null untouched", async () => { const toolCall = completion(null, { finishReason: "tool_calls", - toolCalls: [ - { id: "call_1", type: "function", function: { name: "get_weather", arguments: '{"city": "Paris"}' } }, - ], + toolCalls: [{ id: "call_1", type: "function", function: { name: "get_weather", arguments: '{"city": "Paris"}' } }], }); const { interfaze } = mockInterfaze(() => jsonResponse(toolCall)); const r = await interfaze.chat.completions.create({ diff --git a/test/errors.test.ts b/test/errors.test.ts index fb11f1a..c5359f5 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -18,11 +18,7 @@ const STATUS_MAP = [ { status: 400, ExcType: BadRequestError, - body: errorBody( - "Field 'temperature': Too big: expected number to be <=1", - "invalid_request_error", - "invalid_request", - ), + body: errorBody("Field 'temperature': Too big: expected number to be <=1", "invalid_request_error", "invalid_request"), }, { status: 401, @@ -88,7 +84,7 @@ describe("task + response_format conflict", () => { type: "json_schema", json_schema: { name: "s", schema: { type: "object", properties: { a: { type: "string" } } } }, }, - }), + }) ).toThrow(InterfazeError); expect(calls).toHaveLength(0); }); @@ -101,7 +97,7 @@ describe("invalid guard code", () => { interfaze.chat.completions.create({ guard: ["NOT_A_CODE" as never], messages: [{ role: "user", content: "x" }], - }), + }) ).toThrow(InterfazeError); expect(calls).toHaveLength(0); }); diff --git a/test/guard.test.ts b/test/guard.test.ts index 25fddfa..aee5e87 100644 --- a/test/guard.test.ts +++ b/test/guard.test.ts @@ -44,7 +44,7 @@ describe("create() + guard integration", () => { interfaze.chat.completions.create({ guard: ["NOT_A_CODE" as never], messages: [{ role: "user", content: "x" }], - }), + }) ).toThrow(/Invalid guard code/); expect(calls).toHaveLength(0); }); diff --git a/test/helpers.ts b/test/helpers.ts index db11ba0..2b0f23d 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -18,7 +18,7 @@ export function fixture(name: string): T { /** Build an Interfaze client whose transport is a mock `fetch`, capturing every request. */ export function mockInterfaze( responder: (req: CapturedRequest) => Response | Promise, - options: Partial = {}, + options: Partial = {} ): { interfaze: Interfaze; calls: CapturedRequest[] } { const calls: CapturedRequest[] = []; const fetchImpl = async (input: unknown, init: RequestInit = {}): Promise => { @@ -78,7 +78,7 @@ export function systemContent(body: Record | undefined): string /** Build a synthetic chat.completion body, for shapes not worth a fixture file. */ export function completion( content: unknown = "Hi!", - options: { finishReason?: string; toolCalls?: unknown[] } & Record = {}, + options: { finishReason?: string; toolCalls?: unknown[] } & Record = {} ): Record { const { finishReason = "stop", toolCalls, ...extra } = options; const message: Record = { role: "assistant", content, refusal: null }; diff --git a/test/models.test.ts b/test/models.test.ts index b8dafde..b1b3589 100644 --- a/test/models.test.ts +++ b/test/models.test.ts @@ -26,7 +26,7 @@ describe("models endpoints (interfaze#218 shapes)", () => { message: "The model 'nope' does not exist", type: "invalid_request_error", code: "model_not_found", - }), + }) ); await expect(interfaze.models.retrieve("nope")).rejects.toThrow(); }); diff --git a/test/stream.test.ts b/test/stream.test.ts index d01d8ac..56347d3 100644 --- a/test/stream.test.ts +++ b/test/stream.test.ts @@ -22,12 +22,7 @@ const mkChunk = (delta: object, finish: string | null = null) => ({ // A plain stream with NO side-channels (neither nor ) — the accumulator must not hang. const plainChunks = [mkChunk({ content: "Hello " }), mkChunk({ content: "world" }), mkChunk({}, "stop")]; -const fencedJson = [ - mkChunk({ content: "```json\n" }), - mkChunk({ content: '{"city": "Tokyo"}' }), - mkChunk({ content: "\n```" }), - mkChunk({}, "stop"), -]; +const fencedJson = [mkChunk({ content: "```json\n" }), mkChunk({ content: '{"city": "Tokyo"}' }), mkChunk({ content: "\n```" }), mkChunk({}, "stop")]; const usageChunks = [ mkChunk({ content: "Hi" }), @@ -93,9 +88,7 @@ describe("streaming accumulator", () => { it("finalChatCompletion works without iterating first", async () => { const { interfaze } = mockInterfaze(() => sseResponse(plainChunks)); - const final = await interfaze.chat.completions - .stream({ messages: [{ role: "user", content: "hi" }] }) - .finalChatCompletion(); + const final = await interfaze.chat.completions.stream({ messages: [{ role: "user", content: "hi" }] }).finalChatCompletion(); expect(final.choices[0]!.message.content).toBe("Hello world"); }); @@ -190,14 +183,11 @@ describe("streaming accumulator", () => { it("surfaces an aborted signal as APIUserAbortError instead of a silent end", async () => { const controller = new AbortController(); const { interfaze } = mockInterfaze(() => sseResponse(plainChunks)); - const s = interfaze.chat.completions.stream( - { messages: [{ role: "user", content: "x" }] }, - { signal: controller.signal }, - ); + const s = interfaze.chat.completions.stream({ messages: [{ role: "user", content: "x" }] }, { signal: controller.signal }); await expect( (async () => { for await (const _chunk of s) controller.abort(); - })(), + })() ).rejects.toBeInstanceOf(APIUserAbortError); }); }); diff --git a/test/tasks.test.ts b/test/tasks.test.ts index e961b12..32b4d2c 100644 --- a/test/tasks.test.ts +++ b/test/tasks.test.ts @@ -13,9 +13,7 @@ interface Part { describe("tasks.ocr", () => { it("builds the ocr request and returns the raw result", async () => { - const { interfaze, calls } = mockInterfaze(() => - taskResult("ocr", { extracted_text: "See back of receipt", width: 800 }), - ); + const { interfaze, calls } = mockInterfaze(() => taskResult("ocr", { extracted_text: "See back of receipt", width: 800 })); const result = await interfaze.tasks.ocr(ASSETS.image); const body = calls[0]!.body!; expect(systemContent(body)).toContain("ocr"); @@ -30,9 +28,7 @@ describe("tasks.ocr", () => { describe("tasks.objectDetection", () => { it("builds the object_detection request and returns the raw result", async () => { - const { interfaze, calls } = mockInterfaze(() => - taskResult("object_detection", { objects: [{ label: "bus", box: [0, 0, 10, 10] }] }), - ); + const { interfaze, calls } = mockInterfaze(() => taskResult("object_detection", { objects: [{ label: "bus", box: [0, 0, 10, 10] }] })); const result = await interfaze.tasks.objectDetection(ASSETS.scene); const body = calls[0]!.body!; expect(systemContent(body)).toContain("object_detection"); @@ -47,9 +43,7 @@ describe("tasks.objectDetection", () => { describe("tasks.guiDetection", () => { it("falls through to a generic file part (no extension to sniff) and returns the raw result", async () => { - const { interfaze, calls } = mockInterfaze(() => - taskResult("gui_detection", { elements: [{ label: "button", box: [1, 2, 3, 4] }] }), - ); + const { interfaze, calls } = mockInterfaze(() => taskResult("gui_detection", { elements: [{ label: "button", box: [1, 2, 3, 4] }] })); const result = await interfaze.tasks.guiDetection(ASSETS.gui); const body = calls[0]!.body!; expect(systemContent(body)).toContain("gui_detection"); @@ -81,9 +75,7 @@ describe("tasks.transcribe", () => { describe("tasks.webSearch", () => { it("sends the query as plain string content under web_search", async () => { - const { interfaze, calls } = mockInterfaze(() => - taskResult("web_search", { results: [{ title: "AI agents", url: ASSETS.scrape }] }), - ); + const { interfaze, calls } = mockInterfaze(() => taskResult("web_search", { results: [{ title: "AI agents", url: ASSETS.scrape }] })); const result = await interfaze.tasks.webSearch("latest AI agent news"); const body = calls[0]!.body!; expect(systemContent(body)).toContain("web_search"); diff --git a/tsup.config.ts b/tsup.config.ts index 22b17f4..1ee01a0 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -12,4 +12,3 @@ export default defineConfig({ // `openai` (and optional `zod`) stay external — they're deps, not bundled. external: ["openai", "zod"], }); -