dyadic 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # dyadic
2
+
3
+ ```sh
4
+ npm install dyadic
5
+ ```
6
+
7
+ The official TypeScript and JavaScript SDK for the [Dyadic API](https://dyadic.app/developers/): Iris with streaming, function calling, images and typed errors. No dependencies; Node 18+, Deno, Bun and browsers.
8
+
9
+ ```ts
10
+ import { Dyadic } from "dyadic";
11
+
12
+ const client = new Dyadic({ apiKey: process.env.DYADIC_API_KEY });
13
+ console.log(await client.ask("Hello!", { model: "iris" }));
14
+ ```
15
+
16
+ Full guide: https://dyadic.app/developers/
@@ -0,0 +1,540 @@
1
+ export declare const VERSION = "1.0.0";
2
+ export declare const DEFAULT_BASE_URL = "https://dyadic.app/api/openai/v1";
3
+ export type Model = "iris" | "iris-think" | (string & {});
4
+ export type Role = "system" | "developer" | "user" | "assistant" | "tool";
5
+ export type ContentPart = {
6
+ type: "text";
7
+ text: string;
8
+ } | {
9
+ type: "image_url";
10
+ image_url: {
11
+ url: string;
12
+ detail?: "auto" | "low" | "high";
13
+ };
14
+ } | {
15
+ type: "file";
16
+ file: {
17
+ file_id?: string;
18
+ filename?: string;
19
+ file_data?: string;
20
+ };
21
+ } | {
22
+ type: "input_audio";
23
+ input_audio: {
24
+ data: string;
25
+ format: "wav" | "mp3";
26
+ };
27
+ };
28
+ export interface ToolCall {
29
+ id: string;
30
+ type: "function";
31
+ function: {
32
+ name: string;
33
+ arguments: string;
34
+ };
35
+ }
36
+ export interface ChatMessage {
37
+ role: Role;
38
+ content: string | ContentPart[] | null;
39
+ name?: string;
40
+ tool_calls?: ToolCall[];
41
+ tool_call_id?: string;
42
+ }
43
+ export interface Tool {
44
+ type: "function";
45
+ function: {
46
+ name: string;
47
+ description?: string;
48
+ parameters?: Record<string, unknown>;
49
+ strict?: boolean;
50
+ };
51
+ }
52
+ export interface GenerationParams {
53
+ temperature?: number;
54
+ top_p?: number;
55
+ top_k?: number;
56
+ min_p?: number;
57
+ top_a?: number;
58
+ presence_penalty?: number;
59
+ frequency_penalty?: number;
60
+ repetition_penalty?: number;
61
+ seed?: number;
62
+ logit_bias?: Record<string, number>;
63
+ max_tokens?: number;
64
+ max_completion_tokens?: number;
65
+ stop?: string | string[];
66
+ reasoning_effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
67
+ logprobs?: boolean;
68
+ top_logprobs?: number;
69
+ user?: string;
70
+ stream_options?: {
71
+ include_usage?: boolean;
72
+ };
73
+ }
74
+ export interface ChatCompletionParams extends GenerationParams {
75
+ model: Model;
76
+ messages: ChatMessage[];
77
+ tools?: Tool[];
78
+ tool_choice?: "auto" | "none" | "required" | {
79
+ type: "function";
80
+ function: {
81
+ name: string;
82
+ };
83
+ };
84
+ parallel_tool_calls?: boolean;
85
+ response_format?: {
86
+ type: "text" | "json_object" | "json_schema";
87
+ json_schema?: Record<string, unknown>;
88
+ };
89
+ stream?: boolean;
90
+ }
91
+ export interface CompletionParams extends GenerationParams {
92
+ model: Model;
93
+ prompt: string;
94
+ stream?: boolean;
95
+ }
96
+ export interface BillingInfo {
97
+ credits?: number;
98
+ credits_final?: boolean;
99
+ request_id?: string;
100
+ log_id?: string;
101
+ }
102
+ export interface Usage {
103
+ prompt_tokens: number;
104
+ completion_tokens: number;
105
+ total_tokens: number;
106
+ dyadic?: BillingInfo;
107
+ }
108
+ export interface ChatCompletion {
109
+ id: string;
110
+ object: "chat.completion";
111
+ created: number;
112
+ model: string;
113
+ choices: {
114
+ index: number;
115
+ finish_reason: string;
116
+ logprobs: unknown;
117
+ message: {
118
+ role: "assistant";
119
+ content: string | null;
120
+ reasoning_content?: string;
121
+ tool_calls?: ToolCall[];
122
+ };
123
+ }[];
124
+ usage: Usage;
125
+ dyadic?: BillingInfo;
126
+ }
127
+ export interface ChatCompletionChunk {
128
+ id: string;
129
+ object: "chat.completion.chunk";
130
+ created: number;
131
+ model: string;
132
+ usage?: Usage;
133
+ choices: {
134
+ index: number;
135
+ finish_reason: string | null;
136
+ logprobs?: unknown;
137
+ delta: {
138
+ role?: string;
139
+ content?: string;
140
+ reasoning_content?: string;
141
+ tool_calls?: {
142
+ index: number;
143
+ id?: string;
144
+ type?: "function";
145
+ function?: {
146
+ name?: string;
147
+ arguments?: string;
148
+ };
149
+ }[];
150
+ };
151
+ }[];
152
+ }
153
+ export interface TextCompletion {
154
+ id: string;
155
+ object: "text_completion";
156
+ created: number;
157
+ model: string;
158
+ choices: {
159
+ text: string;
160
+ index: number;
161
+ finish_reason: string | null;
162
+ }[];
163
+ usage?: Usage;
164
+ dyadic?: BillingInfo;
165
+ }
166
+ export interface ModelInfo {
167
+ id: string;
168
+ object: "model";
169
+ created: number;
170
+ owned_by: string;
171
+ name?: string;
172
+ description?: string;
173
+ }
174
+ export interface ImageParams {
175
+ prompt: string;
176
+ model?: Model;
177
+ size?: string;
178
+ quality?: "standard" | "hd" | "high" | "max";
179
+ n?: 1;
180
+ }
181
+ export interface ImageResponse {
182
+ created: number;
183
+ data: {
184
+ b64_json?: string;
185
+ url?: string;
186
+ revised_prompt?: string;
187
+ }[];
188
+ }
189
+ export interface ResponseParams {
190
+ model: Model;
191
+ input: string | Record<string, unknown>[];
192
+ instructions?: string;
193
+ previous_response_id?: string;
194
+ store?: boolean;
195
+ tools?: Record<string, unknown>[];
196
+ tool_choice?: unknown;
197
+ parallel_tool_calls?: boolean;
198
+ text?: {
199
+ format?: Record<string, unknown>;
200
+ };
201
+ reasoning?: {
202
+ effort?: "minimal" | "low" | "medium" | "high";
203
+ summary?: string;
204
+ };
205
+ max_output_tokens?: number;
206
+ temperature?: number;
207
+ top_p?: number;
208
+ top_logprobs?: number;
209
+ metadata?: Record<string, string>;
210
+ user?: string;
211
+ safety_identifier?: string;
212
+ stream?: boolean;
213
+ }
214
+ export interface ResponseObject {
215
+ id: string;
216
+ object: "response";
217
+ created_at: number;
218
+ status: "completed" | "incomplete" | "failed" | "in_progress";
219
+ model: string;
220
+ output: {
221
+ type: string;
222
+ id?: string;
223
+ role?: string;
224
+ content?: {
225
+ type: string;
226
+ text?: string;
227
+ }[];
228
+ call_id?: string;
229
+ name?: string;
230
+ arguments?: string;
231
+ }[];
232
+ output_text?: string;
233
+ previous_response_id: string | null;
234
+ error: {
235
+ code: string;
236
+ message: string;
237
+ } | null;
238
+ usage: {
239
+ input_tokens: number;
240
+ output_tokens: number;
241
+ total_tokens: number;
242
+ } | null;
243
+ metadata: Record<string, string>;
244
+ }
245
+ export interface ResponseEvent {
246
+ type: string;
247
+ sequence_number: number;
248
+ delta?: string;
249
+ response?: ResponseObject;
250
+ [key: string]: unknown;
251
+ }
252
+ export interface EmbeddingParams {
253
+ model: "text-embedding-3-small" | "text-embedding-3-large" | "text-embedding-ada-002" | (string & {});
254
+ input: string | string[];
255
+ dimensions?: number;
256
+ encoding_format?: "float" | "base64";
257
+ user?: string;
258
+ }
259
+ export interface EmbeddingList {
260
+ object: "list";
261
+ model: string;
262
+ data: {
263
+ object: "embedding";
264
+ index: number;
265
+ embedding: number[] | string;
266
+ }[];
267
+ usage: {
268
+ prompt_tokens: number;
269
+ total_tokens: number;
270
+ };
271
+ }
272
+ export interface SpeechParams {
273
+ model: "tts-1" | "tts-1-hd" | "gpt-4o-mini-tts" | (string & {});
274
+ voice: string;
275
+ input: string;
276
+ response_format?: "mp3" | "wav" | "pcm";
277
+ speed?: number;
278
+ instructions?: string;
279
+ }
280
+ /** A file to upload: a Blob or File, bytes, or { name, data, type }. In Node, `await fs.openAsBlob(path)` reads one from disk. */
281
+ export type Uploadable = Blob | Uint8Array | ArrayBuffer | {
282
+ name: string;
283
+ data: Blob | Uint8Array | ArrayBuffer | string;
284
+ type?: string;
285
+ };
286
+ export interface TranscriptionParams {
287
+ file: Uploadable;
288
+ model: "whisper-1" | "gpt-4o-transcribe" | "gpt-4o-mini-transcribe" | (string & {});
289
+ language?: string;
290
+ prompt?: string;
291
+ response_format?: "json" | "text" | "srt" | "verbose_json" | "vtt";
292
+ temperature?: number;
293
+ timestamp_granularities?: ("word" | "segment")[];
294
+ }
295
+ export interface Transcription {
296
+ text: string;
297
+ language?: string;
298
+ duration?: number;
299
+ segments?: unknown[];
300
+ words?: unknown[];
301
+ usage?: Record<string, unknown>;
302
+ }
303
+ export interface Moderation {
304
+ id: string;
305
+ model: string;
306
+ results: {
307
+ flagged: boolean;
308
+ categories: Record<string, boolean>;
309
+ category_scores: Record<string, number>;
310
+ category_applied_input_types: Record<string, string[]>;
311
+ }[];
312
+ }
313
+ export interface FileObject {
314
+ id: string;
315
+ object: "file";
316
+ bytes: number;
317
+ created_at: number;
318
+ filename: string;
319
+ purpose: string;
320
+ status: string;
321
+ expires_at: number | null;
322
+ }
323
+ export interface Page<T> {
324
+ object: "list";
325
+ data: T[];
326
+ first_id: string | null;
327
+ last_id: string | null;
328
+ has_more: boolean;
329
+ }
330
+ export interface Batch {
331
+ id: string;
332
+ object: "batch";
333
+ endpoint: string;
334
+ status: "validating" | "failed" | "in_progress" | "finalizing" | "completed" | "expired" | "cancelling" | "cancelled";
335
+ input_file_id: string;
336
+ output_file_id: string | null;
337
+ error_file_id: string | null;
338
+ errors: unknown;
339
+ completion_window: string;
340
+ request_counts: {
341
+ total: number;
342
+ completed: number;
343
+ failed: number;
344
+ };
345
+ created_at: number;
346
+ completed_at: number | null;
347
+ expires_at: number | null;
348
+ metadata: Record<string, string>;
349
+ }
350
+ export interface RequestOptions {
351
+ form?: FormData;
352
+ query?: Record<string, string | number | undefined>;
353
+ raw?: boolean;
354
+ }
355
+ /** Bytes from the API (speech audio, file downloads). */
356
+ export declare class Binary {
357
+ readonly data: ArrayBuffer;
358
+ readonly contentType: string;
359
+ readonly requestId: string;
360
+ constructor(data: ArrayBuffer, contentType: string, requestId?: string);
361
+ bytes(): Uint8Array;
362
+ text(): string;
363
+ blob(): Blob;
364
+ get size(): number;
365
+ }
366
+ export interface ClientOptions {
367
+ apiKey?: string;
368
+ baseURL?: string;
369
+ timeoutMs?: number;
370
+ maxRetries?: number;
371
+ appName?: string;
372
+ fetch?: typeof fetch;
373
+ }
374
+ /** An error from the API, in OpenAI's shape (status 0 for a network failure). */
375
+ export declare class DyadicError extends Error {
376
+ status: number;
377
+ type: string;
378
+ code: string | null;
379
+ param: string | null;
380
+ requestId: string;
381
+ body: unknown;
382
+ constructor(message: string, init?: {
383
+ status?: number;
384
+ type?: string;
385
+ code?: string | null;
386
+ param?: string | null;
387
+ requestId?: string;
388
+ body?: unknown;
389
+ });
390
+ }
391
+ /** A streamed completion: `for await (const chunk of stream)`. `text` and `usage` fill in as it goes. */
392
+ export declare class Stream<T> implements AsyncIterable<T> {
393
+ private http;
394
+ readonly controller: AbortController;
395
+ private onDone;
396
+ text: string;
397
+ usage: Usage | undefined;
398
+ /** Responses streams: the final Response once response.completed arrives. */
399
+ response: ResponseObject | undefined;
400
+ readonly requestId: string;
401
+ constructor(http: Response, controller: AbortController, onDone?: () => void);
402
+ [Symbol.asyncIterator](): AsyncIterator<T>;
403
+ /** Read to the end and return the whole text. */
404
+ finalText(): Promise<string>;
405
+ abort(): void;
406
+ }
407
+ export declare class Dyadic {
408
+ readonly apiKey: string;
409
+ readonly baseURL: string;
410
+ readonly timeoutMs: number;
411
+ readonly maxRetries: number;
412
+ readonly appName?: string;
413
+ private fetcher;
414
+ constructor(options?: ClientOptions);
415
+ /** Low-level request: JSON (or multipart `options.form`) in; JSON, text, Binary (`options.raw`) or a Stream out,
416
+ * with retries for busy or failing servers. */
417
+ request<T>(method: string, path: string, body?: unknown, stream?: boolean, options?: RequestOptions): Promise<T>;
418
+ readonly models: {
419
+ list: () => Promise<{
420
+ object: "list";
421
+ data: ModelInfo[];
422
+ }>;
423
+ retrieve: (model: Model) => Promise<ModelInfo>;
424
+ };
425
+ readonly chat: {
426
+ completions: {
427
+ create: {
428
+ (params: ChatCompletionParams & {
429
+ stream: true;
430
+ }): Promise<Stream<ChatCompletionChunk>>;
431
+ (params: ChatCompletionParams & {
432
+ stream?: false;
433
+ }): Promise<ChatCompletion>;
434
+ (params: ChatCompletionParams): Promise<ChatCompletion | Stream<ChatCompletionChunk>>;
435
+ };
436
+ };
437
+ };
438
+ readonly completions: {
439
+ create: {
440
+ (params: CompletionParams & {
441
+ stream: true;
442
+ }): Promise<Stream<TextCompletion>>;
443
+ (params: CompletionParams & {
444
+ stream?: false;
445
+ }): Promise<TextCompletion>;
446
+ };
447
+ };
448
+ readonly responses: {
449
+ create: {
450
+ (params: ResponseParams & {
451
+ stream: true;
452
+ }): Promise<Stream<ResponseEvent>>;
453
+ (params: ResponseParams & {
454
+ stream?: false;
455
+ }): Promise<ResponseObject>;
456
+ };
457
+ retrieve: (id: string) => Promise<ResponseObject>;
458
+ delete: (id: string) => Promise<{
459
+ id: string;
460
+ object: "response.deleted";
461
+ deleted: boolean;
462
+ }>;
463
+ inputItems: (id: string, query?: {
464
+ limit?: number;
465
+ after?: string;
466
+ order?: "asc" | "desc";
467
+ }) => Promise<Page<Record<string, unknown>>>;
468
+ cancel: (id: string) => Promise<ResponseObject>;
469
+ };
470
+ readonly embeddings: {
471
+ create: (params: EmbeddingParams) => Promise<EmbeddingList>;
472
+ };
473
+ readonly audio: {
474
+ speech: {
475
+ create: (params: SpeechParams) => Promise<Binary>;
476
+ };
477
+ transcriptions: {
478
+ create: ({ file, ...params }: TranscriptionParams) => Promise<string | Transcription>;
479
+ };
480
+ translations: {
481
+ create: ({ file, ...params }: Omit<TranscriptionParams, "language" | "timestamp_granularities">) => Promise<string | Transcription>;
482
+ };
483
+ };
484
+ readonly moderations: {
485
+ create: (params: {
486
+ input: string | string[] | ContentPart[];
487
+ model?: string;
488
+ }) => Promise<Moderation>;
489
+ };
490
+ readonly images: {
491
+ generate: (params: ImageParams) => Promise<ImageResponse>;
492
+ edit: ({ image, ...params }: Omit<ImageParams, "model"> & {
493
+ image: Uploadable;
494
+ }) => Promise<ImageResponse>;
495
+ createVariation: ({ image, ...params }: Omit<ImageParams, "prompt" | "model"> & {
496
+ image: Uploadable;
497
+ }) => Promise<ImageResponse>;
498
+ };
499
+ readonly files: {
500
+ create: (params: {
501
+ file: Uploadable;
502
+ purpose: "batch" | "user_data" | "assistants" | "vision" | (string & {});
503
+ }) => Promise<FileObject>;
504
+ list: (query?: {
505
+ purpose?: string;
506
+ limit?: number;
507
+ after?: string;
508
+ order?: "asc" | "desc";
509
+ }) => Promise<Page<FileObject>>;
510
+ retrieve: (id: string) => Promise<FileObject>;
511
+ delete: (id: string) => Promise<{
512
+ id: string;
513
+ object: "file";
514
+ deleted: boolean;
515
+ }>;
516
+ content: (id: string) => Promise<Binary>;
517
+ };
518
+ readonly batches: {
519
+ create: (params: {
520
+ input_file_id: string;
521
+ endpoint: string;
522
+ completion_window?: "24h";
523
+ metadata?: Record<string, string>;
524
+ }) => Promise<Batch>;
525
+ list: (query?: {
526
+ limit?: number;
527
+ after?: string;
528
+ }) => Promise<Page<Batch>>;
529
+ retrieve: (id: string) => Promise<Batch>;
530
+ cancel: (id: string) => Promise<Batch>;
531
+ };
532
+ /** One question, one answer: the reply's text. */
533
+ ask(prompt: string, options?: Omit<ChatCompletionParams, "messages" | "model" | "stream"> & {
534
+ model?: Model;
535
+ system?: string;
536
+ }): Promise<string>;
537
+ /** The API's OpenAPI description. */
538
+ openapi(): Promise<Record<string, unknown>>;
539
+ }
540
+ export default Dyadic;
package/dist/index.js ADDED
@@ -0,0 +1,246 @@
1
+ // The official TypeScript SDK for the Dyadic API: Iris
2
+ // through the OpenAI-compatible endpoints at https://dyadic.app/api/openai/v1.
3
+ export const VERSION = "1.0.0";
4
+ export const DEFAULT_BASE_URL = "https://dyadic.app/api/openai/v1";
5
+ /** Bytes from the API (speech audio, file downloads). */
6
+ export class Binary {
7
+ constructor(data, contentType, requestId = "") {
8
+ this.data = data;
9
+ this.contentType = contentType;
10
+ this.requestId = requestId;
11
+ }
12
+ bytes() { return new Uint8Array(this.data); }
13
+ text() { return new TextDecoder().decode(this.data); }
14
+ blob() { return new Blob([this.data], { type: this.contentType }); }
15
+ get size() { return this.data.byteLength; }
16
+ }
17
+ function toBlob(file, fallback) {
18
+ if (typeof Blob !== "undefined" && file instanceof Blob)
19
+ return [file, file.name || fallback];
20
+ if (file instanceof Uint8Array || file instanceof ArrayBuffer)
21
+ return [new Blob([file]), fallback];
22
+ const named = file;
23
+ const data = named.data instanceof Blob ? named.data : new Blob([named.data], named.type ? { type: named.type } : {});
24
+ return [data, named.name || fallback];
25
+ }
26
+ function form(fields, files) {
27
+ const out = new FormData();
28
+ for (const [name, [file, fallback]] of Object.entries(files)) {
29
+ const [blob, filename] = toBlob(file, fallback);
30
+ out.append(name, blob, filename);
31
+ }
32
+ for (const [name, value] of Object.entries(fields)) {
33
+ if (value === undefined || value === null)
34
+ continue;
35
+ if (Array.isArray(value))
36
+ for (const item of value)
37
+ out.append(name.endsWith("[]") ? name : `${name}[]`, String(item));
38
+ else
39
+ out.append(name, String(value));
40
+ }
41
+ return out;
42
+ }
43
+ /** An error from the API, in OpenAI's shape (status 0 for a network failure). */
44
+ export class DyadicError extends Error {
45
+ constructor(message, init = {}) {
46
+ super(message);
47
+ this.name = "DyadicError";
48
+ this.status = init.status ?? 0;
49
+ this.type = init.type ?? "";
50
+ this.code = init.code ?? null;
51
+ this.param = init.param ?? null;
52
+ this.requestId = init.requestId ?? "";
53
+ this.body = init.body;
54
+ }
55
+ }
56
+ /** A streamed completion: `for await (const chunk of stream)`. `text` and `usage` fill in as it goes. */
57
+ export class Stream {
58
+ constructor(http, controller, onDone = () => { }) {
59
+ this.http = http;
60
+ this.controller = controller;
61
+ this.onDone = onDone;
62
+ this.text = "";
63
+ this.requestId = http.headers.get("x-request-id") ?? "";
64
+ }
65
+ async *[Symbol.asyncIterator]() {
66
+ const reader = this.http.body.getReader();
67
+ const decoder = new TextDecoder();
68
+ let buffer = "";
69
+ try {
70
+ for (;;) {
71
+ const { value, done } = await reader.read();
72
+ if (done)
73
+ return;
74
+ buffer += decoder.decode(value, { stream: true });
75
+ let index;
76
+ while ((index = buffer.indexOf("\n\n")) >= 0) {
77
+ const frame = buffer.slice(0, index);
78
+ buffer = buffer.slice(index + 2);
79
+ const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
80
+ if (!data)
81
+ continue;
82
+ if (data === "[DONE]")
83
+ return;
84
+ const chunk = JSON.parse(data);
85
+ if (chunk.error)
86
+ throw new DyadicError(chunk.error.message, { status: 200, type: chunk.error.type, code: chunk.error.code, body: chunk });
87
+ if (chunk.type === "response.failed") {
88
+ const failure = chunk.response?.error ?? { message: "The response failed." };
89
+ throw new DyadicError(failure.message, { status: 200, code: failure.code ?? null, body: chunk });
90
+ }
91
+ if (chunk.usage)
92
+ this.usage = chunk.usage;
93
+ if ((chunk.type === "response.completed" || chunk.type === "response.incomplete") && chunk.response) {
94
+ this.response = chunk.response;
95
+ this.usage = chunk.response.usage ?? undefined;
96
+ }
97
+ const choice = chunk.choices?.[0];
98
+ this.text += chunk.type === "response.output_text.delta" ? chunk.delta ?? "" : choice?.delta?.content ?? choice?.text ?? "";
99
+ yield chunk;
100
+ }
101
+ }
102
+ }
103
+ finally {
104
+ reader.releaseLock();
105
+ this.onDone();
106
+ }
107
+ }
108
+ /** Read to the end and return the whole text. */
109
+ async finalText() { for await (const _ of this) { /* drain */ } return this.text; }
110
+ abort() { this.controller.abort(); }
111
+ }
112
+ const RETRY = new Set([408, 409, 429, 500, 502, 503, 504]);
113
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
114
+ export class Dyadic {
115
+ constructor(options = {}) {
116
+ this.models = {
117
+ list: () => this.request("GET", "/models"),
118
+ retrieve: (model) => this.request("GET", `/models/${model}`),
119
+ };
120
+ this.chat = {
121
+ completions: {
122
+ create: ((params) => {
123
+ const body = params.stream ? { stream_options: { include_usage: true }, ...params } : params;
124
+ return this.request("POST", "/chat/completions", body, Boolean(params.stream));
125
+ }),
126
+ },
127
+ };
128
+ this.completions = {
129
+ create: ((params) => {
130
+ const body = params.stream ? { stream_options: { include_usage: true }, ...params } : params;
131
+ return this.request("POST", "/completions", body, Boolean(params.stream));
132
+ }),
133
+ };
134
+ this.responses = {
135
+ create: ((params) => this.request("POST", "/responses", params, Boolean(params.stream))),
136
+ retrieve: (id) => this.request("GET", `/responses/${id}`),
137
+ delete: (id) => this.request("DELETE", `/responses/${id}`),
138
+ inputItems: (id, query = {}) => this.request("GET", `/responses/${id}/input_items`, undefined, false, { query }),
139
+ cancel: (id) => this.request("POST", `/responses/${id}/cancel`),
140
+ };
141
+ this.embeddings = {
142
+ create: (params) => this.request("POST", "/embeddings", params),
143
+ };
144
+ this.audio = {
145
+ speech: { create: (params) => this.request("POST", "/audio/speech", params, false, { raw: true }) },
146
+ transcriptions: {
147
+ create: ({ file, ...params }) => this.request("POST", "/audio/transcriptions", undefined, false, { form: form(params, { file: [file, "audio"] }) }),
148
+ },
149
+ translations: {
150
+ create: ({ file, ...params }) => this.request("POST", "/audio/translations", undefined, false, { form: form(params, { file: [file, "audio"] }) }),
151
+ },
152
+ };
153
+ this.moderations = {
154
+ create: (params) => this.request("POST", "/moderations", params),
155
+ };
156
+ this.images = {
157
+ generate: (params) => this.request("POST", "/images/generations", params),
158
+ edit: ({ image, ...params }) => this.request("POST", "/images/edits", undefined, false, { form: form(params, { image: [image, "image.png"] }) }),
159
+ createVariation: ({ image, ...params }) => this.request("POST", "/images/variations", undefined, false, { form: form(params, { image: [image, "image.png"] }) }),
160
+ };
161
+ this.files = {
162
+ create: (params) => this.request("POST", "/files", undefined, false, { form: form({ purpose: params.purpose }, { file: [params.file, "file"] }) }),
163
+ list: (query = {}) => this.request("GET", "/files", undefined, false, { query }),
164
+ retrieve: (id) => this.request("GET", `/files/${id}`),
165
+ delete: (id) => this.request("DELETE", `/files/${id}`),
166
+ content: (id) => this.request("GET", `/files/${id}/content`, undefined, false, { raw: true }),
167
+ };
168
+ this.batches = {
169
+ create: (params) => this.request("POST", "/batches", { completion_window: "24h", ...params }),
170
+ list: (query = {}) => this.request("GET", "/batches", undefined, false, { query }),
171
+ retrieve: (id) => this.request("GET", `/batches/${id}`),
172
+ cancel: (id) => this.request("POST", `/batches/${id}/cancel`),
173
+ };
174
+ // process.env where it exists (Node, Bun, Deno with compat); browsers pass apiKey.
175
+ const env = globalThis.process?.env ?? {};
176
+ this.apiKey = options.apiKey ?? env.DYADIC_API_KEY ?? "";
177
+ if (!this.apiKey)
178
+ throw new DyadicError("No API key. Pass apiKey or set DYADIC_API_KEY. Create one at https://dyadic.app/workspace/?view=api.");
179
+ this.baseURL = (options.baseURL ?? env.DYADIC_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
180
+ this.timeoutMs = options.timeoutMs ?? 600000;
181
+ this.maxRetries = options.maxRetries ?? 2;
182
+ this.appName = options.appName;
183
+ this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
184
+ }
185
+ /** Low-level request: JSON (or multipart `options.form`) in; JSON, text, Binary (`options.raw`) or a Stream out,
186
+ * with retries for busy or failing servers. */
187
+ async request(method, path, body, stream = false, options = {}) {
188
+ const headers = { Authorization: `Bearer ${this.apiKey}`, Accept: stream ? "text/event-stream" : options.raw ? "*/*" : "application/json" };
189
+ if (body !== undefined && !options.form)
190
+ headers["Content-Type"] = "application/json";
191
+ const query = Object.entries(options.query ?? {}).filter(([, v]) => v !== undefined).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
192
+ const url = `${this.baseURL}${path}${query ? `?${query}` : ""}`;
193
+ const payload = options.form ?? (body === undefined ? undefined : JSON.stringify(body));
194
+ if (this.appName)
195
+ headers["X-Title"] = this.appName;
196
+ for (let attempt = 0;; attempt++) {
197
+ const controller = new AbortController();
198
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
199
+ timer?.unref?.();
200
+ let response;
201
+ try {
202
+ response = await this.fetcher(url, { method, headers, body: payload, signal: controller.signal });
203
+ }
204
+ catch (error) {
205
+ clearTimeout(timer);
206
+ if (attempt < this.maxRetries) {
207
+ await sleep(500 * 2 ** attempt);
208
+ continue;
209
+ }
210
+ throw new DyadicError(`Could not reach the Dyadic API: ${error.message}`);
211
+ }
212
+ if (!stream)
213
+ clearTimeout(timer);
214
+ if (response.ok) {
215
+ if (stream)
216
+ return new Stream(response, controller, () => clearTimeout(timer));
217
+ const kind = (response.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
218
+ if (!options.raw && (kind === "application/json" || kind.endsWith("+json")))
219
+ return await response.json();
220
+ if (!options.raw && kind.startsWith("text/"))
221
+ return await response.text();
222
+ return new Binary(await response.arrayBuffer(), kind, response.headers.get("x-request-id") ?? "");
223
+ }
224
+ clearTimeout(timer);
225
+ const data = await response.json().catch(() => ({ error: { message: `HTTP ${response.status}` } }));
226
+ const error = data?.error ?? {};
227
+ if (RETRY.has(response.status) && attempt < this.maxRetries && error.type !== "insufficient_quota") {
228
+ const after = Number(response.headers.get("retry-after"));
229
+ await sleep(Number.isFinite(after) && after > 0 ? Math.min(after, 30) * 1000 : 500 * 2 ** attempt);
230
+ continue;
231
+ }
232
+ throw new DyadicError(error.message ?? `HTTP ${response.status}`, { status: response.status, type: error.type, code: error.code, param: error.param,
233
+ requestId: response.headers.get("x-request-id") ?? "", body: data });
234
+ }
235
+ }
236
+ /** One question, one answer: the reply's text. */
237
+ async ask(prompt, options = {}) {
238
+ const { model = "iris", system, ...params } = options;
239
+ const messages = [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }];
240
+ const reply = await this.chat.completions.create({ ...params, model, messages, stream: false });
241
+ return reply.choices[0]?.message.content ?? "";
242
+ }
243
+ /** The API's OpenAPI description. */
244
+ openapi() { return this.request("GET", "/openapi.json"); }
245
+ }
246
+ export default Dyadic;
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "dyadic",
3
+ "version": "1.0.0",
4
+ "description": "The official TypeScript and JavaScript SDK for the Dyadic API (Iris, OpenAI-compatible).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
9
+ "files": ["dist", "README.md"],
10
+ "engines": { "node": ">=18" },
11
+ "license": "MIT",
12
+ "homepage": "https://dyadic.app/developers/",
13
+ "scripts": { "build": "tsc", "test": "node --test test/client.test.js" },
14
+ "devDependencies": { "typescript": "5.6.3" }
15
+ }