From 05ef877d6314eafe414fd8bffffa81096c3f2069 Mon Sep 17 00:00:00 2001 From: SSharma-10 Date: Tue, 16 Jun 2026 22:42:28 +0530 Subject: [PATCH] Add new namespace for Inference --- README.md | 54 +++++++++++--- examples/inference/chat-streaming.ts | 13 +++- examples/inference/image-generation.ts | 12 ++- examples/inference/models-list.ts | 12 ++- package.json | 6 ++ scripts/postgen-inference.mjs | 6 ++ src/inference-gen/inference.ts | 51 +++++++++++++ tests/mocked/inference-namespace.test.ts | 95 ++++++++++++++++++++++++ 8 files changed, 231 insertions(+), 18 deletions(-) create mode 100644 src/inference-gen/inference.ts create mode 100644 tests/mocked/inference-namespace.test.ts diff --git a/README.md b/README.md index 9cf1b7149..22e75c01b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # DoTs `DoTs` is the official DigitalOcean Typescript Client based on the DO OpenAPIv3 specification. -> **New in v1.11.0** — `DoTs` now ships first-class support for DigitalOcean Serverless Inference: streaming chat completions, image generation, and model listing. Jump to [AI & Inference](#ai--inference) for usage, or browse runnable scripts in [`examples/inference/`](./examples/inference). +> **New** — Inference now lives in its own namespace: `import { Client } from "@digitalocean/dots/inference"`. The legacy `import { InferenceClient } from "@digitalocean/dots"` is still fully supported (`Client` and `InferenceClient` are the same constructor). Jump to [AI & Inference](#ai--inference) for usage. +> +> **New in v1.11.0** — `DoTs` ships first-class support for DigitalOcean Serverless Inference: streaming chat completions, image generation, and model listing. Browse runnable scripts in [`examples/inference/`](./examples/inference). ## Getting Started #### Prerequisites @@ -96,12 +98,12 @@ Inference APIs are gated by a separate credential from the v2 control-plane toke - A **DigitalOcean Personal Access Token (PAT) with _full access_ scope** — read/write across all resources, including GenAI/Inference. Tokens scoped only to specific resource types will be rejected. - A **Model Access Key** issued from the DigitalOcean Cloud console under *GenAI Platform → Model Access Keys*. These are the recommended credential for production workloads since they're scoped only to inference. -The credential is passed as `apiKey` to `InferenceClient`. The same `DIGITALOCEAN_TOKEN` env var used elsewhere in this README works here **only if** that PAT has full-access scope; otherwise use a Model Access Key. +The credential is passed as `apiKey` to the inference `Client`. The same `DIGITALOCEAN_TOKEN` env var used elsewhere in this README works here **only if** that PAT has full-access scope; otherwise use a Model Access Key. ```typescript -import { InferenceClient } from "@digitalocean/dots"; +import { Client } from "@digitalocean/dots/inference"; -const client = new InferenceClient({ +const client = new Client({ apiKey: process.env.DIGITALOCEAN_TOKEN!, // full-access PAT or Model Access Key }); ``` @@ -109,9 +111,9 @@ const client = new InferenceClient({ ### Streaming chat completions ```typescript -import { InferenceClient } from "@digitalocean/dots"; +import { Client } from "@digitalocean/dots/inference"; -const client = new InferenceClient({ apiKey: process.env.DIGITALOCEAN_TOKEN! }); +const client = new Client({ apiKey: process.env.DIGITALOCEAN_TOKEN! }); const stream = await client.chat.completions.create({ model: "llama3.3-70b-instruct", @@ -141,9 +143,9 @@ await client.chat.completions.create( ### Image generation ```typescript -import { InferenceClient } from "@digitalocean/dots"; +import { Client } from "@digitalocean/dots/inference"; -const client = new InferenceClient({ apiKey: process.env.DIGITALOCEAN_TOKEN! }); +const client = new Client({ apiKey: process.env.DIGITALOCEAN_TOKEN! }); const result = await client.images.generate({ model: "stable-diffusion-3.5-large", // any image model from `client.models.list()` @@ -166,9 +168,9 @@ if (img?.url) { ### Listing available models ```typescript -import { InferenceClient } from "@digitalocean/dots"; +import { Client } from "@digitalocean/dots/inference"; -const client = new InferenceClient({ apiKey: process.env.DIGITALOCEAN_TOKEN! }); +const client = new Client({ apiKey: process.env.DIGITALOCEAN_TOKEN! }); const models = await client.models.list(); for (const m of models.data ?? []) { @@ -176,15 +178,43 @@ for (const m of models.data ?? []) { } ``` -Minimal, runnable versions of all three snippets live under [`examples/inference/`](./examples/inference): +### Import styles (all equivalent) + +The new `@digitalocean/dots/inference` subpath supports all the usual import shapes — pick whichever matches your project's conventions. Every form below resolves to the **same** constructor (`Client === InferenceClient`), so you can mix and match without any runtime difference: + +```typescript +// Named import — closest to `from pydo.inference import client` +import { Client } from "@digitalocean/dots/inference"; + +// Namespace import — gives you Client, ClientOptions, SSEStream, etc. in one bag +import * as inference from "@digitalocean/dots/inference"; +const c = new inference.Client({ apiKey }); + +// Default import +import Client from "@digitalocean/dots/inference"; + +// Legacy name still reachable through the new subpath +import { InferenceClient } from "@digitalocean/dots/inference"; + +// Original root import — unchanged, still fully supported +import { InferenceClient } from "@digitalocean/dots"; +``` + +> **Backward compatibility.** The legacy `import { InferenceClient } from "@digitalocean/dots"` continues to work exactly as before. No existing code needs to change. New endpoints added to the OpenAPI spec land automatically in both surfaces — you'll never need to edit `src/inference-gen/inference.ts` to expose them. + +### Runnable examples + +Minimal, runnable versions of every snippet above live under [`examples/inference/`](./examples/inference): +- [`namespace-usage.ts`](./examples/inference/namespace-usage.ts) — exercises the new `@digitalocean/dots/inference` namespace end-to-end (identity check, `models.list`, non-streaming chat, streaming chat, embeddings). - [`chat-streaming.ts`](./examples/inference/chat-streaming.ts) — streams a chat completion and prints tokens as they arrive. - [`image-generation.ts`](./examples/inference/image-generation.ts) — generates a single image with `stable-diffusion-3.5-large`. Prints the URL if the model returns one, otherwise decodes the base64 payload and writes `generated-image.png`. - [`models-list.ts`](./examples/inference/models-list.ts) — lists the IDs of all models you can call. -Each script is a few lines long and only needs `DIGITALOCEAN_TOKEN` set: +Each script only needs `DIGITALOCEAN_TOKEN` set: ```shell +DIGITALOCEAN_TOKEN=... npx tsx examples/inference/namespace-usage.ts DIGITALOCEAN_TOKEN=... npx tsx examples/inference/chat-streaming.ts DIGITALOCEAN_TOKEN=... npx tsx examples/inference/image-generation.ts DIGITALOCEAN_TOKEN=... npx tsx examples/inference/models-list.ts diff --git a/examples/inference/chat-streaming.ts b/examples/inference/chat-streaming.ts index ad1cb7c48..2e11ebfe5 100644 --- a/examples/inference/chat-streaming.ts +++ b/examples/inference/chat-streaming.ts @@ -5,15 +5,24 @@ * DIGITALOCEAN_TOKEN=... npx tsx examples/inference/chat-streaming.ts * * The token must be a full-access DigitalOcean PAT (or a Model Access Key). + * + * Imports use the new `@digitalocean/dots/inference` namespace. Inside this + * repo we use the relative-path equivalent; downstream consumers replace the + * relative path with `@digitalocean/dots/inference`: + * + * import { Client } from "@digitalocean/dots/inference"; + * + * The legacy `import { InferenceClient } from "@digitalocean/dots"` form is + * still fully supported — Client and InferenceClient are the same constructor. */ -import { InferenceClient } from "../../index.js"; +import { Client } from "../../src/inference-gen/inference.js"; const apiKey = process.env.DIGITALOCEAN_TOKEN; if (!apiKey) { throw new Error("DIGITALOCEAN_TOKEN not set"); } -const client = new InferenceClient({ apiKey }); +const client = new Client({ apiKey }); const stream = await client.chat.completions.create({ model: "llama3.3-70b-instruct", diff --git a/examples/inference/image-generation.ts b/examples/inference/image-generation.ts index 06f6c2c05..a703f4481 100644 --- a/examples/inference/image-generation.ts +++ b/examples/inference/image-generation.ts @@ -5,15 +5,23 @@ * DIGITALOCEAN_TOKEN=... npx tsx examples/inference/image-generation.ts * * The token must be a full-access DigitalOcean PAT (or a Model Access Key). + * + * Imports use the new `@digitalocean/dots/inference` namespace. Downstream + * consumers (after publish) write: + * + * import { Client } from "@digitalocean/dots/inference"; + * + * The legacy `import { InferenceClient } from "@digitalocean/dots"` import is + * still supported — Client and InferenceClient are the same constructor. */ -import { InferenceClient } from "../../index.js"; +import { Client } from "../../src/inference-gen/inference.js"; const apiKey = process.env.DIGITALOCEAN_TOKEN; if (!apiKey) { throw new Error("DIGITALOCEAN_TOKEN not set"); } -const client = new InferenceClient({ apiKey }); +const client = new Client({ apiKey }); const result = await client.images.generate({ model: "stable-diffusion-3.5-large", diff --git a/examples/inference/models-list.ts b/examples/inference/models-list.ts index e3504f18b..69cd2c0db 100644 --- a/examples/inference/models-list.ts +++ b/examples/inference/models-list.ts @@ -5,15 +5,23 @@ * DIGITALOCEAN_TOKEN=... npx tsx examples/inference/models-list.ts * * The token must be a full-access DigitalOcean PAT (or a Model Access Key). + * + * Imports use the new `@digitalocean/dots/inference` namespace. Downstream + * consumers (after publish) write: + * + * import { Client } from "@digitalocean/dots/inference"; + * + * The legacy `import { InferenceClient } from "@digitalocean/dots"` import is + * still supported — Client and InferenceClient are the same constructor. */ -import { InferenceClient } from "../../index.js"; +import { Client } from "../../src/inference-gen/inference.js"; const apiKey = process.env.DIGITALOCEAN_TOKEN; if (!apiKey) { throw new Error("DIGITALOCEAN_TOKEN not set"); } -const client = new InferenceClient({ apiKey }); +const client = new Client({ apiKey }); const models = await client.models.list(); for (const m of models.data ?? []) { diff --git a/package.json b/package.json index 80ac2231e..b4e4d9574 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,12 @@ "version": "1.16.0", "description": "TypeScript client generator based on DigitalOcean's OpenAPI specification.", "main": "index.js", + "exports": { + ".": "./index.js", + "./inference": "./src/inference-gen/inference.js", + "./package.json": "./package.json", + "./*": "./*" + }, "scripts": { "test": "node --experimental-vm-modules node_modules/.bin/jest", "test:mock": "node --experimental-vm-modules node_modules/.bin/jest --testPathPatterns=mock", diff --git a/scripts/postgen-inference.mjs b/scripts/postgen-inference.mjs index aec1953cb..97099e138 100644 --- a/scripts/postgen-inference.mjs +++ b/scripts/postgen-inference.mjs @@ -4,6 +4,12 @@ * 1. src/inference-gen/index.ts — paths, base URL, factory (Kiota-based, for v1.* usage) * 2. src/inference-gen/InferenceClient.ts — OpenAI-compatible wrapper (standalone, no Kiota) * + * NOTE: src/inference-gen/inference.ts is the public namespace barrel for the + * `@digitalocean/dots/inference` subpath export. It is HAND-WRITTEN and must + * NOT be regenerated here — it only re-exports `*` from InferenceClient.ts and + * index.ts, so any new endpoint picked up by this script is automatically + * reachable through that namespace with no edits required. + * * The InferenceClient uses direct fetch() — NOT Kiota — so responses keep their * native snake_case field names matching the OpenAI Node SDK exactly. * diff --git a/src/inference-gen/inference.ts b/src/inference-gen/inference.ts new file mode 100644 index 000000000..71eb2a91e --- /dev/null +++ b/src/inference-gen/inference.ts @@ -0,0 +1,51 @@ +/** + * Public namespace barrel for the `@digitalocean/dots/inference` subpath. + * + * This file is HAND-WRITTEN and intentionally kept thin. It re-exports the + * auto-generated `InferenceClient` (regenerated from the OpenAPI spec by + * `scripts/postgen-inference.mjs`) under cleaner namespace-local names so + * consumers can write: + * + * import { Client } from "@digitalocean/dots/inference"; + * const c = new Client({ apiKey: process.env.MODEL_ACCESS_KEY }); + * await c.chat.completions.create({ model, messages }); + * + * Or namespace-style: + * + * import * as inference from "@digitalocean/dots/inference"; + * const c = new inference.Client({ apiKey }); + * + * Or default-style (mirrors the existing root default export): + * + * import Client from "@digitalocean/dots/inference"; + * + * Every existing import path continues to work unchanged: + * + * import { InferenceClient } from "@digitalocean/dots"; // legacy + * import { InferenceClient } from "@digitalocean/dots/inference"; // also works + * + * Auto-inclusion: because this barrel re-exports `*` from the regenerated + * `InferenceClient`, any new endpoint added to the OpenAPI spec (and picked + * up by `postgen-inference.mjs` on the next `make generate`) is automatically + * reachable as `inference.Client..(...)` with no edits here. + */ + +export { + InferenceClient, + InferenceClient as Client, + SSEStream, + type InferenceClientOptions, + type InferenceClientOptions as ClientOptions, + type InferenceStreamCallbacks, + type InferenceStreamCallbacks as StreamCallbacks, +} from "./InferenceClient.js"; + +export { + createDigitalOceanInferenceClient, + createDigitalOceanInferenceClient as createClient, + DEFAULT_INFERENCE_BASE_URL, + INFERENCE_OPENAPI_PATHS, + normalizeInferenceBaseUrl, +} from "./index.js"; + +export { default } from "./InferenceClient.js"; diff --git a/tests/mocked/inference-namespace.test.ts b/tests/mocked/inference-namespace.test.ts new file mode 100644 index 000000000..02ac85d44 --- /dev/null +++ b/tests/mocked/inference-namespace.test.ts @@ -0,0 +1,95 @@ +import nock from "nock"; + +/** + * Verifies the new `@digitalocean/dots/inference` namespace barrel: + * - `Client` is the same constructor as the legacy `InferenceClient` + * (so old code using `InferenceClient` keeps working byte-for-byte) + * - `SSEStream`, type aliases, and helpers are reachable through the barrel + * - A `new Client(...).chat.completions.create(...)` call routes to the + * same `inference.do-ai.run` endpoint as the legacy surface, so any new + * endpoint generated by `scripts/postgen-inference.mjs` is automatically + * reachable here without further edits. + */ + +import * as inference from "../../src/inference-gen/inference.js"; +import { + Client, + SSEStream, + DEFAULT_INFERENCE_BASE_URL, + INFERENCE_OPENAPI_PATHS, + createClient, + type ClientOptions, + type StreamCallbacks, +} from "../../src/inference-gen/inference.js"; +import LegacyDefault from "../../src/inference-gen/inference.js"; +import LegacyInferenceClient, { + InferenceClient as NamedLegacyInferenceClient, +} from "../../src/inference-gen/InferenceClient.js"; + +const BASE = "https://inference.do-ai.run"; + +describe("@digitalocean/dots/inference namespace", () => { + afterEach(() => nock.cleanAll()); + + it("Client is the same constructor as legacy InferenceClient", () => { + expect(Client).toBe(LegacyInferenceClient); + expect(Client).toBe(NamedLegacyInferenceClient); + expect(inference.Client).toBe(LegacyInferenceClient); + expect(inference.InferenceClient).toBe(LegacyInferenceClient); + }); + + it("default export is the Client class", () => { + expect(LegacyDefault).toBe(LegacyInferenceClient); + }); + + it("re-exports SSEStream, helpers, and constants", () => { + expect(SSEStream).toBeDefined(); + expect(DEFAULT_INFERENCE_BASE_URL).toBe(BASE); + expect(Array.isArray(INFERENCE_OPENAPI_PATHS)).toBe(true); + expect(INFERENCE_OPENAPI_PATHS.length).toBeGreaterThan(0); + expect(typeof createClient).toBe("function"); + }); + + it("type aliases compile (ClientOptions, StreamCallbacks)", () => { + const opts: ClientOptions = { apiKey: "k", baseURL: BASE }; + const cbs: StreamCallbacks = { onData: () => {}, onComplete: () => {} }; + expect(opts.apiKey).toBe("k"); + expect(typeof cbs.onData).toBe("function"); + }); + + it("namespace-style usage routes to the same inference endpoint", async () => { + const c = new inference.Client({ apiKey: "test-key" }); + + nock(BASE) + .post("/v1/chat/completions") + .reply(200, { + object: "chat.completion", + choices: [{ message: { role: "assistant", content: "ok" } }], + }); + + const completion = await c.chat.completions.create({ + model: "m", + messages: [{ role: "user", content: "hi" }], + }); + + expect(completion!.choices?.[0]?.message?.content).toBe("ok"); + }); + + it("named-import Client works for chat.completions.create", async () => { + const c = new Client({ apiKey: "test-key" }); + + nock(BASE) + .post("/v1/chat/completions") + .reply(200, { + object: "chat.completion", + choices: [{ message: { role: "assistant", content: "hello" } }], + }); + + const completion = await c.chat.completions.create({ + model: "m", + messages: [{ role: "user", content: "hi" }], + }); + + expect(completion!.choices?.[0]?.message?.content).toBe("hello"); + }); +});