From 78380c8e71838082a2864509070305d3deab3076 Mon Sep 17 00:00:00 2001 From: Abhinavexist Date: Fri, 31 Jul 2026 04:45:44 +0530 Subject: [PATCH 1/3] feat: bundle openai as an internal dependency, move to internal self types --- README.md | 28 ++- package-lock.json | 9 +- package.json | 6 +- src/_compat.ts | 464 ++++++++++++++++++++++++++++++++++++++++++++++ src/chat.ts | 50 +++-- src/client.ts | 10 +- src/index.ts | 10 +- src/inputs.ts | 4 +- src/schema.ts | 2 +- src/stream.ts | 15 +- src/tasks.ts | 5 +- src/types.ts | 8 +- test/chat.test.ts | 2 +- tsup.config.ts | 7 +- 14 files changed, 555 insertions(+), 65 deletions(-) create mode 100644 src/_compat.ts diff --git a/README.md b/README.md index 593cf25..d426c0d 100644 --- a/README.md +++ b/README.md @@ -109,10 +109,10 @@ const final = await stream.finalChatCompletion(); // .precontext (the sources), ### Structured output -`responseFormat()` takes a JSON Schema - or a zod schema via `z.toJSONSchema()` - and normalizes it for Interfaze: +`parse()` sends a zod schema, validates the reply against it, and returns a typed object on `message.parsed` - no manual `JSON.parse`: ```ts -import { responseFormat, inputs } from "interfaze"; +import { inputs, zodResponseFormat } from "interfaze"; import { z } from "zod"; const Receipt = z.object({ @@ -121,22 +121,32 @@ const Receipt = z.object({ items: z.array(z.object({ name: z.string(), price: z.number() })), }); -const res = await interfaze.chat.completions.create({ +const res = await interfaze.chat.completions.parse({ 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"), + response_format: zodResponseFormat(Receipt, "receipt"), }); -const receipt = JSON.parse(res.choices[0]?.message.content ?? "{}"); // { merchant, total, items: [...] } +const receipt = res.choices[0]?.message.parsed; // typed `Receipt | null` +``` + +Prefer a raw JSON Schema (or no zod)? Use `create` with `responseFormat()` and read `message.content` yourself: + +```ts +import { responseFormat } from "interfaze"; + +const res = await interfaze.chat.completions.create({ + messages: [{ role: "user", content: "..." }], + response_format: responseFormat({ type: "object", properties: {}, required: [] }), +}); +const data = JSON.parse(res.choices[0]?.message.content ?? "{}"); ``` -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. +`responseFormat()` also accepts a zod schema via `z.toJSONSchema()`. `message.content` is a JSON string, and a non-object root is wrapped under a `result` key. ### Tools and function calling @@ -351,6 +361,8 @@ 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. +> **Import error classes, types, and `toFile` from `interfaze` itself.** It re-exports everything you need, and these are the exact classes the SDK throws, so `instanceof` matches. If you also use another SDK that exports the same class names, those are a separate copy - `instanceof` won't match across the two, so compare on `err.status` / `err.code` to bridge them. + ## Capabilities | Use case | Entry point | diff --git a/package-lock.json b/package-lock.json index 47b3b53..8b7ced8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,18 @@ { "name": "interfaze", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "interfaze", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", - "dependencies": { - "openai": "~6.47.0" - }, "devDependencies": { "@arethetypeswrong/cli": "0.18.5", "@types/node": "~22.20.1", "@vitest/coverage-v8": "^2.1.9", - "openai": "~6.47.0", + "openai": "6.47.0", "prettier": "^3.9.6", "publint": "0.3.22", "tsup": "^8.5.1", diff --git a/package.json b/package.json index 219b1b1..c4bd8d4 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,7 @@ "prepare": "tsup", "prepublishOnly": "npm run build" }, - "dependencies": { - "openai": "~6.47.0" - }, + "dependencies": {}, "peerDependencies": { "zod": "^3.23.0 || ^4.4.3" }, @@ -72,7 +70,7 @@ "@arethetypeswrong/cli": "0.18.5", "@types/node": "~22.20.1", "@vitest/coverage-v8": "^2.1.9", - "openai": "~6.47.0", + "openai": "6.47.0", "prettier": "^3.9.6", "publint": "0.3.22", "tsup": "^8.5.1", diff --git a/src/_compat.ts b/src/_compat.ts new file mode 100644 index 0000000..0bed849 --- /dev/null +++ b/src/_compat.ts @@ -0,0 +1,464 @@ +// Chat-Completions-only compatibility layer + +import _Client, { + OpenAIError as _OpenAIError, + APIError as _APIError, + APIConnectionError as _APIConnectionError, + APIConnectionTimeoutError as _APIConnectionTimeoutError, + APIUserAbortError as _APIUserAbortError, + BadRequestError as _BadRequestError, + AuthenticationError as _AuthenticationError, + PermissionDeniedError as _PermissionDeniedError, + NotFoundError as _NotFoundError, + ConflictError as _ConflictError, + UnprocessableEntityError as _UnprocessableEntityError, + RateLimitError as _RateLimitError, + InternalServerError as _InternalServerError, + toFile as _toFile, +} from "openai"; +import { zodResponseFormat as _zodResponseFormat } from "openai/helpers/zod"; + +interface Ctor { + new (...args: any[]): T; + readonly prototype: T; +} + +// Errors + +export interface OpenAIError extends Error {} +export const OpenAIError = _OpenAIError as unknown as Ctor; + +export interface APIError extends OpenAIError { + readonly status: number | undefined; + readonly headers?: Headers | undefined; + readonly error?: unknown; + readonly code: string | null | undefined; + readonly param: string | null | undefined; + readonly type: string | undefined; + readonly requestID?: string | null | undefined; +} +export const APIError = _APIError as unknown as Ctor; + +export interface APIConnectionError extends APIError {} +export const APIConnectionError = _APIConnectionError as unknown as Ctor; + +export interface APIConnectionTimeoutError extends APIConnectionError {} +export const APIConnectionTimeoutError = _APIConnectionTimeoutError as unknown as Ctor; + +export interface APIUserAbortError extends APIError {} +export const APIUserAbortError = _APIUserAbortError as unknown as Ctor; + +export interface BadRequestError extends APIError {} +export const BadRequestError = _BadRequestError as unknown as Ctor; + +export interface AuthenticationError extends APIError {} +export const AuthenticationError = _AuthenticationError as unknown as Ctor; + +export interface PermissionDeniedError extends APIError {} +export const PermissionDeniedError = _PermissionDeniedError as unknown as Ctor; + +export interface NotFoundError extends APIError {} +export const NotFoundError = _NotFoundError as unknown as Ctor; + +export interface ConflictError extends APIError {} +export const ConflictError = _ConflictError as unknown as Ctor; + +export interface UnprocessableEntityError extends APIError {} +export const UnprocessableEntityError = _UnprocessableEntityError as unknown as Ctor; + +export interface RateLimitError extends APIError {} +export const RateLimitError = _RateLimitError as unknown as Ctor; + +export interface InternalServerError extends APIError {} +export const InternalServerError = _InternalServerError as unknown as Ctor; + +export const toFile = _toFile as unknown as (value: any, name?: string, options?: { type?: string }) => Promise; + +// Shared value types + +export type FunctionParameters = Record; + +export interface FunctionDefinition { + name: string; + description?: string; + parameters?: FunctionParameters; + strict?: boolean | null; +} + +export type Metadata = Record; + +export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null; + +export interface ResponseFormatText { + type: "text"; +} + +export interface ResponseFormatJSONObject { + type: "json_object"; +} + +export interface ResponseFormatJSONSchema { + type: "json_schema"; + json_schema: { + name: string; + description?: string; + schema?: Record; + strict?: boolean | null; + }; +} + +export type ResponseFormat = ResponseFormatText | ResponseFormatJSONObject | ResponseFormatJSONSchema; + +// Content parts + +export interface ChatCompletionContentPartText { + type: "text"; + text: string; +} + +export interface ChatCompletionContentPartImage { + type: "image_url"; + image_url: { + url: string; + detail?: "auto" | "low" | "high"; + }; +} + +export interface ChatCompletionContentPartInputAudio { + type: "input_audio"; + input_audio: { + data: string; + format: "wav" | "mp3"; + }; +} + +export interface ChatCompletionContentPartFile { + type: "file"; + file: { + file_data?: string; + file_id?: string; + filename?: string; + }; +} + +export type ChatCompletionContentPart = + ChatCompletionContentPartText | ChatCompletionContentPartImage | ChatCompletionContentPartInputAudio | ChatCompletionContentPartFile; + +// Tools + +export interface ChatCompletionTool { + type: "function"; + function: FunctionDefinition; +} + +export interface ChatCompletionNamedToolChoice { + type: "function"; + function: { name: string }; +} + +export type ChatCompletionToolChoiceOption = "none" | "auto" | "required" | ChatCompletionNamedToolChoice; + +export interface ChatCompletionMessageToolCall { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +} + +// Message params (request side) + +export interface ChatCompletionSystemMessageParam { + role: "system"; + content: string | Array; + name?: string; +} + +export interface ChatCompletionUserMessageParam { + role: "user"; + content: string | Array; + name?: string; +} + +export interface ChatCompletionAssistantMessageParam { + role: "assistant"; + content?: string | Array | null; + name?: string; + refusal?: string | null; + tool_calls?: Array; +} + +export interface ChatCompletionToolMessageParam { + role: "tool"; + content: string | Array; + tool_call_id: string; +} + +export interface ChatCompletionFunctionMessageParam { + role: "function"; + content: string | null; + name: string; +} + +export type ChatCompletionMessageParam = + | ChatCompletionSystemMessageParam + | ChatCompletionUserMessageParam + | ChatCompletionAssistantMessageParam + | ChatCompletionToolMessageParam + | ChatCompletionFunctionMessageParam; + +// Message (response side) + +export interface ChatCompletionMessageAnnotation { + type: "url_citation"; + url_citation: { + end_index: number; + start_index: number; + title: string; + url: string; + }; +} + +export interface ChatCompletionMessage { + role: "assistant"; + content: string | null; + refusal?: string | null; + tool_calls?: Array; + annotations?: Array; +} + +export interface ChatCompletionTopLogprob { + token: string; + bytes: Array | null; + logprob: number; +} + +export interface ChatCompletionTokenLogprob extends ChatCompletionTopLogprob { + top_logprobs: Array; +} + +export interface ChatCompletionChoiceLogprobs { + content: Array | null; + refusal: Array | null; +} + +export interface ChatCompletionUsage { + completion_tokens: number; + prompt_tokens: number; + total_tokens: number; + completion_tokens_details?: { + accepted_prediction_tokens?: number; + audio_tokens?: number; + reasoning_tokens?: number; + rejected_prediction_tokens?: number; + }; + prompt_tokens_details?: { + audio_tokens?: number; + cache_write_tokens?: number; + cached_tokens?: number; + }; +} + +export type ChatCompletionFinishReason = "stop" | "length" | "tool_calls" | "content_filter" | "function_call"; + +export interface ChatCompletionChoice { + finish_reason: ChatCompletionFinishReason; + index: number; + logprobs: ChatCompletionChoiceLogprobs | null; + message: ChatCompletionMessage; +} + +export interface ChatCompletion { + id: string; + choices: Array; + created: number; + model: string; + object: "chat.completion"; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + system_fingerprint?: string; + usage?: ChatCompletionUsage; +} + +// Streaming chunk + +export interface ChatCompletionChunkDeltaToolCall { + index: number; + id?: string; + type?: "function"; + function?: { + name?: string; + arguments?: string; + }; +} + +export interface ChatCompletionChunkDelta { + content?: string | null; + refusal?: string | null; + role?: "developer" | "system" | "user" | "assistant" | "tool"; + tool_calls?: Array; +} + +export interface ChatCompletionChunkChoice { + delta: ChatCompletionChunkDelta; + finish_reason: ChatCompletionFinishReason | null; + index: number; + logprobs?: ChatCompletionChoiceLogprobs | null; +} + +export interface ChatCompletionChunk { + id: string; + choices: Array; + created: number; + model: string; + object: "chat.completion.chunk"; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + system_fingerprint?: string; + usage?: ChatCompletionUsage | null; +} + +// Create params + +export interface ChatCompletionStreamOptions { + include_obfuscation?: boolean; + include_usage?: boolean; +} + +interface ChatCompletionCreateParamsBase { + messages: Array; + model: string; + temperature?: number | null; + top_p?: number | null; + max_tokens?: number | null; + max_completion_tokens?: number | null; + n?: number | null; + stop?: string | null | Array; + stream?: boolean | null; + stream_options?: ChatCompletionStreamOptions | null; + presence_penalty?: number | null; + frequency_penalty?: number | null; + logit_bias?: Record | null; + logprobs?: boolean | null; + top_logprobs?: number | null; + seed?: number | null; + tools?: Array; + tool_choice?: ChatCompletionToolChoiceOption; + response_format?: ResponseFormat; + reasoning_effort?: ReasoningEffort; + user?: string; + metadata?: Metadata | null; +} + +export interface ChatCompletionCreateParamsNonStreaming extends ChatCompletionCreateParamsBase { + stream?: false | null; +} + +export interface ChatCompletionCreateParamsStreaming extends ChatCompletionCreateParamsBase { + stream: true; +} + +export type ChatCompletionCreateParams = ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; + +// Core plumbing: APIPromise / Stream / RequestOptions / ClientOptions + +export type APIPromise = Promise & { + asResponse(): Promise; + withResponse(): Promise<{ data: T; response: Response }>; + _thenUnwrap(fn: (value: T) => U): APIPromise; +}; + +export interface Stream extends AsyncIterable { + controller?: AbortController; + [Symbol.asyncIterator](): AsyncIterator; +} + +export interface RequestOptions { + headers?: Headers | Record | undefined; + signal?: AbortSignal | null | undefined; + timeout?: number; + maxRetries?: number; + query?: Record | undefined | null; + body?: unknown; + idempotencyKey?: string; +} + +export interface ClientOptions { + apiKey?: string; + baseURL?: string | null; + timeout?: number; + maxRetries?: number; + fetch?: (input: string | URL | Request, init?: RequestInit) => Promise; + defaultHeaders?: Headers | Record; + defaultQuery?: Record; + dangerouslyAllowBrowser?: boolean; +} + +// Client + +export interface Model { + id: string; + created: number; + object: "model"; + owned_by: string; +} + +export interface ModelDeleted { + id: string; + deleted: boolean; + object: string; +} + +export interface ModelsPage { + object: "list"; + data: Array; +} + +export interface Models { + list(options?: RequestOptions): APIPromise; + retrieve(model: string, options?: RequestOptions): APIPromise; + delete(model: string, options?: RequestOptions): APIPromise; +} + +// Structured-output parsing (`parse()` + `zodResponseFormat`) + +type ZodTypeLike = ({ _output: unknown } | { _zod: { output: unknown } }) & { + parse?: (data: unknown) => unknown; +}; +type InferZodType = T extends { _output: infer O } ? O : T extends { _zod: { output: infer O } } ? O : never; + +export interface AutoParseableResponseFormat extends ResponseFormatJSONSchema { + $brand: "auto-parseable-response-format"; + $parseRaw(content: string): T; +} + +export const zodResponseFormat = _zodResponseFormat as unknown as ( + schema: ZodInput, + name: string, + props?: { description?: string } +) => AutoParseableResponseFormat>; + +export interface ParsedChatCompletionMessage extends ChatCompletionMessage { + parsed: T | null; +} +export interface ParsedChoice extends Omit { + message: ParsedChatCompletionMessage; +} +export interface ParsedChatCompletion extends Omit { + choices: Array>; +} + +export interface ChatCompletions { + create(body: ChatCompletionCreateParamsNonStreaming, options?: RequestOptions): APIPromise; + create(body: ChatCompletionCreateParamsStreaming, options?: RequestOptions): APIPromise>; + parse( + body: { response_format: AutoParseableResponseFormat } & Record, + options?: RequestOptions + ): APIPromise>; +} + +export interface Client { + chat: { completions: ChatCompletions }; + models: Models; +} + +export const Client = _Client as unknown as Ctor; diff --git a/src/chat.ts b/src/chat.ts index 596c1c1..3c62e9e 100644 --- a/src/chat.ts +++ b/src/chat.ts @@ -1,7 +1,14 @@ -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 { + APIPromise, + AutoParseableResponseFormat, + ChatCompletion, + ChatCompletionChunk, + ChatCompletionMessageParam, + Client, + ParsedChatCompletion, + RequestOptions, + Stream, +} from "./_compat.js"; import { INTERFAZE_MODEL } from "./constants.js"; import { InterfazeError } from "./errors.js"; @@ -9,14 +16,13 @@ import { guardTag } from "./guard.js"; import { emptyTaskSchema } from "./schema.js"; import { InterfazeChatCompletionStream, stripJsonFence } from "./stream.js"; import type { + GuardCode, InterfazeChatCompletion, + InterfazeChatCompletionCreateParams, InterfazeChatCompletionCreateParamsNonStreaming, InterfazeChatCompletionCreateParamsStreaming, - InterfazeChatCompletionCreateParams, } from "./types.js"; -type RequestOptions = OpenAI.RequestOptions; - export function toInterfaze(raw: ChatCompletion, opts: { stripFence: boolean }): InterfazeChatCompletion { const r = raw as InterfazeChatCompletion; r.vcache = (raw as { vcache?: boolean }).vcache ?? false; @@ -79,10 +85,15 @@ function prepare(params: InterfazeChatCompletionCreateParams): { return { body, stripFence: (rf as { type?: string })?.type === "json_object" }; } +export type InterfazeChatCompletionParseParams = Omit & { + response_format: AutoParseableResponseFormat; + guard?: GuardCode[]; +}; + export class InterfazeCompletions { - #openai: OpenAI; - constructor(openai: OpenAI) { - this.#openai = openai; + #client: Client; + constructor(client: Client) { + this.#client = client; } create(params: InterfazeChatCompletionCreateParamsNonStreaming, options?: RequestOptions): APIPromise; @@ -92,23 +103,34 @@ export class InterfazeCompletions { options?: RequestOptions ): APIPromise | APIPromise> { const { body, stripFence } = prepare(params); - const raw = this.#openai.chat.completions.create(body as never, options); + const raw = this.#client.chat.completions.create(body as never, options); if (params.stream) { return raw as unknown as APIPromise>; } return (raw as unknown as APIPromise)._thenUnwrap((c) => toInterfaze(c, { stripFence })); } + /** Structured output: parses `message.content` against the schema and returns it on `message.parsed`. */ + parse(params: InterfazeChatCompletionParseParams, options?: RequestOptions): APIPromise> { + const { guard, model, messages, ...rest } = params; + const body = { + ...rest, + model: model ?? INTERFAZE_MODEL, + messages: injectTags(messages as ChatCompletionMessageParam[], undefined, guard?.length ? guardTag(guard) : undefined), + }; + return this.#client.chat.completions.parse(body as never, options); + } + /** Streaming with an Interfaze-tolerant accumulator; also surfaces ``/``. */ stream(params: Omit, options?: RequestOptions): InterfazeChatCompletionStream { const { body, stripFence } = prepare({ ...params, stream: true } as InterfazeChatCompletionCreateParamsStreaming); - return new InterfazeChatCompletionStream(this.#openai, body, options, stripFence); + return new InterfazeChatCompletionStream(this.#client, body, options, stripFence); } } export class InterfazeChat { completions: InterfazeCompletions; - constructor(openai: OpenAI) { - this.completions = new InterfazeCompletions(openai); + constructor(client: Client) { + this.completions = new InterfazeCompletions(client); } } diff --git a/src/client.ts b/src/client.ts index b7ce732..fcf84d9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,5 @@ -import OpenAI from "openai"; -import type { ClientOptions } from "openai"; +import type { ClientOptions } from "./_compat.js"; +import { Client } from "./_compat.js"; import { InterfazeChat } from "./chat.js"; import { DEFAULT_TIMEOUT_MS, HEADERS, INTERFAZE_BASE_URL } from "./constants.js"; @@ -22,9 +22,9 @@ function envKey(): string | undefined { } export class Interfaze { - readonly openai: OpenAI; + readonly openai: Client; readonly chat: InterfazeChat; - readonly models: OpenAI["models"]; + readonly models: Client["models"]; readonly tasks: Tasks; constructor(options: InterfazeOptions = {}) { @@ -41,7 +41,7 @@ export class Interfaze { if (bypassCache) headers[HEADERS.bypassCache] = "true"; if (adminKey) headers[HEADERS.adminKey] = adminKey; - this.openai = new OpenAI({ + this.openai = new Client({ ...rest, apiKey: resolvedKey, baseURL: baseURL ?? INTERFAZE_BASE_URL, diff --git a/src/index.ts b/src/index.ts index 70e59f1..8c18258 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,8 @@ export type { InterfazeOptions } from "./client.js"; export { InterfazeError } from "./errors.js"; export { InterfazeChatCompletionStream } from "./stream.js"; export { responseFormat, emptyTaskSchema } from "./schema.js"; +export { zodResponseFormat } from "./_compat.js"; +export type { InterfazeChatCompletionParseParams } from "./chat.js"; /** Content-part + input builders (`inputs.image/file/audio/video/dataUrl/fromPath/autoPart`). */ export * as inputs from "./inputs.js"; @@ -21,7 +23,7 @@ export type { export { TASK_NAMES, GUARD_CODES, GUARD_LABELS, INTERFAZE_MODEL, INTERFAZE_BASE_URL, LIMITS } from "./constants.js"; -export { toFile } from "openai"; +export { toFile } from "./_compat.js"; export { OpenAIError, APIError, @@ -36,11 +38,13 @@ export { InternalServerError, PermissionDeniedError, UnprocessableEntityError, -} from "openai"; +} from "./_compat.js"; export type { + AutoParseableResponseFormat, ChatCompletionMessageParam, ChatCompletionTool, ChatCompletionChunk, ChatCompletionMessage, ChatCompletionContentPart, -} from "openai/resources/chat/completions/completions"; + ParsedChatCompletion, +} from "./_compat.js"; diff --git a/src/inputs.ts b/src/inputs.ts index 30ef5ad..6878f41 100644 --- a/src/inputs.ts +++ b/src/inputs.ts @@ -1,4 +1,4 @@ -import type { ChatCompletionContentPart } from "openai/resources/chat/completions/completions"; +import type { ChatCompletionContentPart } from "./_compat.js"; import { BLACKLISTED_FORMATS } from "./constants.js"; import { InterfazeError } from "./errors.js"; @@ -106,7 +106,7 @@ export function audio(src: string, opts: { format?: string } = {}): ChatCompleti return { type: "input_audio", input_audio: { data: src, format } } as unknown as ChatCompletionContentPart; } -/** Video part — rides on the `file` part (the OpenAI SDK has no video part). */ +/** Video part — rides on the `file` part (there's no dedicated video content part). */ export function video(src: string, opts: { filename?: string } = {}): ChatCompletionContentPart { return file(src, opts); } diff --git a/src/schema.ts b/src/schema.ts index e0954eb..cd8fb19 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,4 +1,4 @@ -import type { ResponseFormatJSONSchema } from "openai/resources/shared"; +import type { ResponseFormatJSONSchema } from "./_compat.js"; type JSONSchema = Record; diff --git a/src/stream.ts b/src/stream.ts index 4dad507..43f096e 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -1,12 +1,9 @@ -import type OpenAI from "openai"; -import { APIUserAbortError } from "openai"; -import type { ChatCompletionChunk } from "openai/resources/chat/completions/completions"; +import type { ChatCompletionChunk, Client, RequestOptions } from "./_compat.js"; +import { APIUserAbortError } from "./_compat.js"; import { InterfazeError } from "./errors.js"; import type { InterfazeChatCompletion, Precontext } from "./types.js"; -type RequestOptions = OpenAI.RequestOptions; - interface ToolCallAcc { id: string; name: string; @@ -15,7 +12,7 @@ interface ToolCallAcc { /** Streaming helper that folds the raw `create({stream:true})` iterable itself. */ export class InterfazeChatCompletionStream implements AsyncIterable { - #openai: OpenAI; + #client: Client; #body: Record; #options: RequestOptions | undefined; #stripFence: boolean; @@ -34,8 +31,8 @@ export class InterfazeChatCompletionStream implements AsyncIterable(); - constructor(openai: OpenAI, body: Record, options?: RequestOptions, stripFence = false) { - this.#openai = openai; + constructor(client: Client, body: Record, options?: RequestOptions, stripFence = false) { + this.#client = client; this.#body = body; this.#options = options; this.#stripFence = stripFence; @@ -43,7 +40,7 @@ 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.#client.chat.completions.create({ ...this.#body, stream: true } as never, this.#options) as unknown as Promise< AsyncIterable >; } diff --git a/src/tasks.ts b/src/tasks.ts index d46935c..08ea339 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -1,11 +1,8 @@ -import type OpenAI from "openai"; -import type { ChatCompletionContentPart } from "openai/resources/chat/completions/completions"; +import type { ChatCompletionContentPart, RequestOptions } from "./_compat.js"; import type { InterfazeCompletions } from "./chat.js"; import { autoPart } from "./inputs.js"; import type { TaskName } from "./types.js"; -type RequestOptions = OpenAI.RequestOptions; - function textPart(text: string): ChatCompletionContentPart { return { type: "text", text }; } diff --git a/src/types.ts b/src/types.ts index 76c4604..d2bd71a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,10 @@ -import type { - ChatCompletion, - ChatCompletionCreateParamsNonStreaming, - ChatCompletionCreateParamsStreaming, -} from "openai/resources/chat/completions/completions"; +import type { ChatCompletion, ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming } from "./_compat.js"; import type { TASK_NAMES, GUARD_CODES } from "./constants.js"; export type TaskName = (typeof TASK_NAMES)[number]; export type GuardCode = (typeof GUARD_CODES)[number]; -/** Wider than the OpenAI enum — Interfaze also accepts `on`/`off`/`auto`. */ +/** Wider than the base enum — Interfaze also accepts `on`/`off`/`auto`. */ export type ReasoningEffort = "minimal" | "low" | "medium" | "high" | "on" | "off" | "auto"; /** One internal task's raw output, surfaced in `response.precontext`. */ diff --git a/test/chat.test.ts b/test/chat.test.ts index 6a40d81..7c40759 100644 --- a/test/chat.test.ts +++ b/test/chat.test.ts @@ -1,7 +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 "../src/_compat.js"; import { completion, fixture, jsonResponse, mockInterfaze, sseResponse, systemContent } from "./helpers.js"; function functionName(call: ChatCompletionMessageToolCall): string { diff --git a/tsup.config.ts b/tsup.config.ts index 1ee01a0..ee2fa20 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -9,6 +9,9 @@ export default defineConfig({ target: "es2022", treeshake: true, splitting: false, - // `openai` (and optional `zod`) stay external — they're deps, not bundled. - external: ["openai", "zod"], + noExternal: [/^openai(\/|$)/], + external: ["zod"], + esbuildOptions(options) { + options.sourcesContent = false; + }, }); From 527267ef9771ebd60289a3819c12d9b5a31c937e Mon Sep 17 00:00:00 2001 From: Abhinavexist Date: Fri, 31 Jul 2026 05:07:53 +0530 Subject: [PATCH 2/3] fix: lazy load responseZodformat --- src/_compat.ts | 11 +++++++++-- tsup.config.ts | 7 ++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/_compat.ts b/src/_compat.ts index 0bed849..dd89abb 100644 --- a/src/_compat.ts +++ b/src/_compat.ts @@ -16,7 +16,6 @@ import _Client, { InternalServerError as _InternalServerError, toFile as _toFile, } from "openai"; -import { zodResponseFormat as _zodResponseFormat } from "openai/helpers/zod"; interface Ctor { new (...args: any[]): T; @@ -431,12 +430,20 @@ export interface AutoParseableResponseFormat extends ResponseFormatJSONSchema $parseRaw(content: string): T; } -export const zodResponseFormat = _zodResponseFormat as unknown as ( +type ZodResponseFormatFn = ( schema: ZodInput, name: string, props?: { description?: string } ) => AutoParseableResponseFormat>; +declare const require: (id: string) => { zodResponseFormat: ZodResponseFormatFn }; +let _zodResponseFormat: ZodResponseFormatFn | undefined; + +export const zodResponseFormat: ZodResponseFormatFn = (schema, name, props) => { + _zodResponseFormat ??= require("openai/helpers/zod").zodResponseFormat; + return _zodResponseFormat(schema, name, props); +}; + export interface ParsedChatCompletionMessage extends ChatCompletionMessage { parsed: T | null; } diff --git a/tsup.config.ts b/tsup.config.ts index ee2fa20..93c8b22 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -11,7 +11,12 @@ export default defineConfig({ splitting: false, noExternal: [/^openai(\/|$)/], external: ["zod"], - esbuildOptions(options) { + esbuildOptions(options, context) { options.sourcesContent = false; + if (context.format === "esm") { + options.banner = { + js: "import { createRequire as _cr } from 'module';\nconst require = _cr(import.meta.url);", + }; + } }, }); From ad659fb404c8e1e4eec396b529c76486f330418a Mon Sep 17 00:00:00 2001 From: Abhinavexist Date: Fri, 31 Jul 2026 05:15:01 +0530 Subject: [PATCH 3/3] feat: add test for parse + zodResponseFormat --- test/parse.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/parse.test.ts diff --git a/test/parse.test.ts b/test/parse.test.ts new file mode 100644 index 0000000..b154e40 --- /dev/null +++ b/test/parse.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import { zodResponseFormat } from "../src/index.js"; +import { completion, jsonResponse, mockInterfaze } from "./helpers.js"; + +describe("structured output: parse + zodResponseFormat", () => { + it("zodResponseFormat builds an auto-parseable json_schema format", () => { + const rf = zodResponseFormat(z.object({ a: z.string() }), "x"); + expect(rf.type).toBe("json_schema"); + expect((rf as { $brand?: string }).$brand).toBe("auto-parseable-response-format"); + }); + + it("parse() validates message.content against the schema and fills message.parsed", async () => { + const { interfaze, calls } = mockInterfaze(() => jsonResponse(completion('{"city":"Tokyo","temp_c":21}'))); + const Weather = z.object({ city: z.string(), temp_c: z.number() }); + const res = await interfaze.chat.completions.parse({ + messages: [{ role: "user", content: "Weather in Tokyo?" }], + response_format: zodResponseFormat(Weather, "weather"), + }); + expect(res.choices[0]!.message.parsed).toEqual({ city: "Tokyo", temp_c: 21 }); + // defaults the model and sends the schema as the response_format + expect(calls[0]!.body!["model"]).toBe("interfaze-beta"); + expect((calls[0]!.body!["response_format"] as { type: string }).type).toBe("json_schema"); + }); + + it("parse() injects a tag when guard codes are set", async () => { + const { interfaze, calls } = mockInterfaze(() => jsonResponse(completion('{"ok":true}'))); + const res = await interfaze.chat.completions.parse({ + guard: ["S1"], + messages: [{ role: "user", content: "x" }], + response_format: zodResponseFormat(z.object({ ok: z.boolean() }), "flag"), + }); + const sys = (calls[0]!.body!["messages"] as Array<{ role: string; content: string }>).find((m) => m.role === "system"); + expect(sys?.content).toContain("S1"); + expect(res.choices[0]!.message.parsed).toEqual({ ok: true }); + }); +});