summaryrefslogtreecommitdiffhomepage
path: root/packages/sdk/src/internal
diff options
context:
space:
mode:
authoradamdotdevin <[email protected]>2025-07-22 11:50:51 -0500
committeradamdotdevin <[email protected]>2025-07-22 11:50:51 -0500
commit10c8b495907069461f5dc464f6285321290c8b14 (patch)
tree9fed07910dd7d99688c0261a364d810a500e21d3 /packages/sdk/src/internal
parent500cea5ce7fa635a924cd9abea63aaf672f7645d (diff)
downloadopencode-10c8b495907069461f5dc464f6285321290c8b14.tar.gz
opencode-10c8b495907069461f5dc464f6285321290c8b14.zip
chore: generate sdk into packages/sdk
Diffstat (limited to 'packages/sdk/src/internal')
-rw-r--r--packages/sdk/src/internal/README.md3
-rw-r--r--packages/sdk/src/internal/builtin-types.ts93
-rw-r--r--packages/sdk/src/internal/decoders/line.ts135
-rw-r--r--packages/sdk/src/internal/detect-platform.ts196
-rw-r--r--packages/sdk/src/internal/errors.ts33
-rw-r--r--packages/sdk/src/internal/headers.ts97
-rw-r--r--packages/sdk/src/internal/parse.ts64
-rw-r--r--packages/sdk/src/internal/request-options.ts93
-rw-r--r--packages/sdk/src/internal/shim-types.ts26
-rw-r--r--packages/sdk/src/internal/shims.ts107
-rw-r--r--packages/sdk/src/internal/to-file.ts154
-rw-r--r--packages/sdk/src/internal/types.ts95
-rw-r--r--packages/sdk/src/internal/uploads.ts187
-rw-r--r--packages/sdk/src/internal/utils.ts8
-rw-r--r--packages/sdk/src/internal/utils/base64.ts40
-rw-r--r--packages/sdk/src/internal/utils/bytes.ts32
-rw-r--r--packages/sdk/src/internal/utils/env.ts18
-rw-r--r--packages/sdk/src/internal/utils/log.ts126
-rw-r--r--packages/sdk/src/internal/utils/path.ts88
-rw-r--r--packages/sdk/src/internal/utils/sleep.ts3
-rw-r--r--packages/sdk/src/internal/utils/uuid.ts17
-rw-r--r--packages/sdk/src/internal/utils/values.ts105
22 files changed, 1720 insertions, 0 deletions
diff --git a/packages/sdk/src/internal/README.md b/packages/sdk/src/internal/README.md
new file mode 100644
index 000000000..3ef5a25ba
--- /dev/null
+++ b/packages/sdk/src/internal/README.md
@@ -0,0 +1,3 @@
+# `internal`
+
+The modules in this directory are not importable outside this package and will change between releases.
diff --git a/packages/sdk/src/internal/builtin-types.ts b/packages/sdk/src/internal/builtin-types.ts
new file mode 100644
index 000000000..c23d3bded
--- /dev/null
+++ b/packages/sdk/src/internal/builtin-types.ts
@@ -0,0 +1,93 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
+
+/**
+ * An alias to the builtin `RequestInit` type so we can
+ * easily alias it in import statements if there are name clashes.
+ *
+ * https://developer.mozilla.org/docs/Web/API/RequestInit
+ */
+type _RequestInit = RequestInit;
+
+/**
+ * An alias to the builtin `Response` type so we can
+ * easily alias it in import statements if there are name clashes.
+ *
+ * https://developer.mozilla.org/docs/Web/API/Response
+ */
+type _Response = Response;
+
+/**
+ * The type for the first argument to `fetch`.
+ *
+ * https://developer.mozilla.org/docs/Web/API/Window/fetch#resource
+ */
+type _RequestInfo = Request | URL | string;
+
+/**
+ * The type for constructing `RequestInit` Headers.
+ *
+ * https://developer.mozilla.org/docs/Web/API/RequestInit#setting_headers
+ */
+type _HeadersInit = RequestInit['headers'];
+
+/**
+ * The type for constructing `RequestInit` body.
+ *
+ * https://developer.mozilla.org/docs/Web/API/RequestInit#body
+ */
+type _BodyInit = RequestInit['body'];
+
+/**
+ * An alias to the builtin `Array<T>` type so we can
+ * easily alias it in import statements if there are name clashes.
+ */
+type _Array<T> = Array<T>;
+
+/**
+ * An alias to the builtin `Record<K, T>` type so we can
+ * easily alias it in import statements if there are name clashes.
+ */
+type _Record<K extends keyof any, T> = Record<K, T>;
+
+export type {
+ _Array as Array,
+ _BodyInit as BodyInit,
+ _HeadersInit as HeadersInit,
+ _Record as Record,
+ _RequestInfo as RequestInfo,
+ _RequestInit as RequestInit,
+ _Response as Response,
+};
+
+/**
+ * A copy of the builtin `EndingType` type as it isn't fully supported in certain
+ * environments and attempting to reference the global version will error.
+ *
+ * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L27941
+ */
+type EndingType = 'native' | 'transparent';
+
+/**
+ * A copy of the builtin `BlobPropertyBag` type as it isn't fully supported in certain
+ * environments and attempting to reference the global version will error.
+ *
+ * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L154
+ * https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#options
+ */
+export interface BlobPropertyBag {
+ endings?: EndingType;
+ type?: string;
+}
+
+/**
+ * A copy of the builtin `FilePropertyBag` type as it isn't fully supported in certain
+ * environments and attempting to reference the global version will error.
+ *
+ * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L503
+ * https://developer.mozilla.org/en-US/docs/Web/API/File/File#options
+ */
+export interface FilePropertyBag extends BlobPropertyBag {
+ lastModified?: number;
+}
diff --git a/packages/sdk/src/internal/decoders/line.ts b/packages/sdk/src/internal/decoders/line.ts
new file mode 100644
index 000000000..b3bfa97cd
--- /dev/null
+++ b/packages/sdk/src/internal/decoders/line.ts
@@ -0,0 +1,135 @@
+import { concatBytes, decodeUTF8, encodeUTF8 } from '../utils/bytes';
+
+export type Bytes = string | ArrayBuffer | Uint8Array | null | undefined;
+
+/**
+ * A re-implementation of httpx's `LineDecoder` in Python that handles incrementally
+ * reading lines from text.
+ *
+ * https://github.com/encode/httpx/blob/920333ea98118e9cf617f246905d7b202510941c/httpx/_decoders.py#L258
+ */
+export class LineDecoder {
+ // prettier-ignore
+ static NEWLINE_CHARS = new Set(['\n', '\r']);
+ static NEWLINE_REGEXP = /\r\n|[\n\r]/g;
+
+ #buffer: Uint8Array;
+ #carriageReturnIndex: number | null;
+
+ constructor() {
+ this.#buffer = new Uint8Array();
+ this.#carriageReturnIndex = null;
+ }
+
+ decode(chunk: Bytes): string[] {
+ if (chunk == null) {
+ return [];
+ }
+
+ const binaryChunk =
+ chunk instanceof ArrayBuffer ? new Uint8Array(chunk)
+ : typeof chunk === 'string' ? encodeUTF8(chunk)
+ : chunk;
+
+ this.#buffer = concatBytes([this.#buffer, binaryChunk]);
+
+ const lines: string[] = [];
+ let patternIndex;
+ while ((patternIndex = findNewlineIndex(this.#buffer, this.#carriageReturnIndex)) != null) {
+ if (patternIndex.carriage && this.#carriageReturnIndex == null) {
+ // skip until we either get a corresponding `\n`, a new `\r` or nothing
+ this.#carriageReturnIndex = patternIndex.index;
+ continue;
+ }
+
+ // we got double \r or \rtext\n
+ if (
+ this.#carriageReturnIndex != null &&
+ (patternIndex.index !== this.#carriageReturnIndex + 1 || patternIndex.carriage)
+ ) {
+ lines.push(decodeUTF8(this.#buffer.subarray(0, this.#carriageReturnIndex - 1)));
+ this.#buffer = this.#buffer.subarray(this.#carriageReturnIndex);
+ this.#carriageReturnIndex = null;
+ continue;
+ }
+
+ const endIndex =
+ this.#carriageReturnIndex !== null ? patternIndex.preceding - 1 : patternIndex.preceding;
+
+ const line = decodeUTF8(this.#buffer.subarray(0, endIndex));
+ lines.push(line);
+
+ this.#buffer = this.#buffer.subarray(patternIndex.index);
+ this.#carriageReturnIndex = null;
+ }
+
+ return lines;
+ }
+
+ flush(): string[] {
+ if (!this.#buffer.length) {
+ return [];
+ }
+ return this.decode('\n');
+ }
+}
+
+/**
+ * This function searches the buffer for the end patterns, (\r or \n)
+ * and returns an object with the index preceding the matched newline and the
+ * index after the newline char. `null` is returned if no new line is found.
+ *
+ * ```ts
+ * findNewLineIndex('abc\ndef') -> { preceding: 2, index: 3 }
+ * ```
+ */
+function findNewlineIndex(
+ buffer: Uint8Array,
+ startIndex: number | null,
+): { preceding: number; index: number; carriage: boolean } | null {
+ const newline = 0x0a; // \n
+ const carriage = 0x0d; // \r
+
+ for (let i = startIndex ?? 0; i < buffer.length; i++) {
+ if (buffer[i] === newline) {
+ return { preceding: i, index: i + 1, carriage: false };
+ }
+
+ if (buffer[i] === carriage) {
+ return { preceding: i, index: i + 1, carriage: true };
+ }
+ }
+
+ return null;
+}
+
+export function findDoubleNewlineIndex(buffer: Uint8Array): number {
+ // This function searches the buffer for the end patterns (\r\r, \n\n, \r\n\r\n)
+ // and returns the index right after the first occurrence of any pattern,
+ // or -1 if none of the patterns are found.
+ const newline = 0x0a; // \n
+ const carriage = 0x0d; // \r
+
+ for (let i = 0; i < buffer.length - 1; i++) {
+ if (buffer[i] === newline && buffer[i + 1] === newline) {
+ // \n\n
+ return i + 2;
+ }
+ if (buffer[i] === carriage && buffer[i + 1] === carriage) {
+ // \r\r
+ return i + 2;
+ }
+ if (
+ buffer[i] === carriage &&
+ buffer[i + 1] === newline &&
+ i + 3 < buffer.length &&
+ buffer[i + 2] === carriage &&
+ buffer[i + 3] === newline
+ ) {
+ // \r\n\r\n
+ return i + 4;
+ }
+ }
+
+ return -1;
+}
diff --git a/packages/sdk/src/internal/detect-platform.ts b/packages/sdk/src/internal/detect-platform.ts
new file mode 100644
index 000000000..e82d95c92
--- /dev/null
+++ b/packages/sdk/src/internal/detect-platform.ts
@@ -0,0 +1,196 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { VERSION } from '../version';
+
+export const isRunningInBrowser = () => {
+ return (
+ // @ts-ignore
+ typeof window !== 'undefined' &&
+ // @ts-ignore
+ typeof window.document !== 'undefined' &&
+ // @ts-ignore
+ typeof navigator !== 'undefined'
+ );
+};
+
+type DetectedPlatform = 'deno' | 'node' | 'edge' | 'unknown';
+
+/**
+ * Note this does not detect 'browser'; for that, use getBrowserInfo().
+ */
+function getDetectedPlatform(): DetectedPlatform {
+ if (typeof Deno !== 'undefined' && Deno.build != null) {
+ return 'deno';
+ }
+ if (typeof EdgeRuntime !== 'undefined') {
+ return 'edge';
+ }
+ if (
+ Object.prototype.toString.call(
+ typeof (globalThis as any).process !== 'undefined' ? (globalThis as any).process : 0,
+ ) === '[object process]'
+ ) {
+ return 'node';
+ }
+ return 'unknown';
+}
+
+declare const Deno: any;
+declare const EdgeRuntime: any;
+type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | `other:${string}` | 'unknown';
+type PlatformName =
+ | 'MacOS'
+ | 'Linux'
+ | 'Windows'
+ | 'FreeBSD'
+ | 'OpenBSD'
+ | 'iOS'
+ | 'Android'
+ | `Other:${string}`
+ | 'Unknown';
+type Browser = 'ie' | 'edge' | 'chrome' | 'firefox' | 'safari';
+type PlatformProperties = {
+ 'X-Stainless-Lang': 'js';
+ 'X-Stainless-Package-Version': string;
+ 'X-Stainless-OS': PlatformName;
+ 'X-Stainless-Arch': Arch;
+ 'X-Stainless-Runtime': 'node' | 'deno' | 'edge' | `browser:${Browser}` | 'unknown';
+ 'X-Stainless-Runtime-Version': string;
+};
+const getPlatformProperties = (): PlatformProperties => {
+ const detectedPlatform = getDetectedPlatform();
+ if (detectedPlatform === 'deno') {
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': normalizePlatform(Deno.build.os),
+ 'X-Stainless-Arch': normalizeArch(Deno.build.arch),
+ 'X-Stainless-Runtime': 'deno',
+ 'X-Stainless-Runtime-Version':
+ typeof Deno.version === 'string' ? Deno.version : Deno.version?.deno ?? 'unknown',
+ };
+ }
+ if (typeof EdgeRuntime !== 'undefined') {
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': 'Unknown',
+ 'X-Stainless-Arch': `other:${EdgeRuntime}`,
+ 'X-Stainless-Runtime': 'edge',
+ 'X-Stainless-Runtime-Version': (globalThis as any).process.version,
+ };
+ }
+ // Check if Node.js
+ if (detectedPlatform === 'node') {
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': normalizePlatform((globalThis as any).process.platform ?? 'unknown'),
+ 'X-Stainless-Arch': normalizeArch((globalThis as any).process.arch ?? 'unknown'),
+ 'X-Stainless-Runtime': 'node',
+ 'X-Stainless-Runtime-Version': (globalThis as any).process.version ?? 'unknown',
+ };
+ }
+
+ const browserInfo = getBrowserInfo();
+ if (browserInfo) {
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': 'Unknown',
+ 'X-Stainless-Arch': 'unknown',
+ 'X-Stainless-Runtime': `browser:${browserInfo.browser}`,
+ 'X-Stainless-Runtime-Version': browserInfo.version,
+ };
+ }
+
+ // TODO add support for Cloudflare workers, etc.
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': 'Unknown',
+ 'X-Stainless-Arch': 'unknown',
+ 'X-Stainless-Runtime': 'unknown',
+ 'X-Stainless-Runtime-Version': 'unknown',
+ };
+};
+
+type BrowserInfo = {
+ browser: Browser;
+ version: string;
+};
+
+declare const navigator: { userAgent: string } | undefined;
+
+// Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts
+function getBrowserInfo(): BrowserInfo | null {
+ if (typeof navigator === 'undefined' || !navigator) {
+ return null;
+ }
+
+ // NOTE: The order matters here!
+ const browserPatterns = [
+ { key: 'edge' as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'ie' as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'ie' as const, pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'chrome' as const, pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'firefox' as const, pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'safari' as const, pattern: /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/ },
+ ];
+
+ // Find the FIRST matching browser
+ for (const { key, pattern } of browserPatterns) {
+ const match = pattern.exec(navigator.userAgent);
+ if (match) {
+ const major = match[1] || 0;
+ const minor = match[2] || 0;
+ const patch = match[3] || 0;
+
+ return { browser: key, version: `${major}.${minor}.${patch}` };
+ }
+ }
+
+ return null;
+}
+
+const normalizeArch = (arch: string): Arch => {
+ // Node docs:
+ // - https://nodejs.org/api/process.html#processarch
+ // Deno docs:
+ // - https://doc.deno.land/deno/stable/~/Deno.build
+ if (arch === 'x32') return 'x32';
+ if (arch === 'x86_64' || arch === 'x64') return 'x64';
+ if (arch === 'arm') return 'arm';
+ if (arch === 'aarch64' || arch === 'arm64') return 'arm64';
+ if (arch) return `other:${arch}`;
+ return 'unknown';
+};
+
+const normalizePlatform = (platform: string): PlatformName => {
+ // Node platforms:
+ // - https://nodejs.org/api/process.html#processplatform
+ // Deno platforms:
+ // - https://doc.deno.land/deno/stable/~/Deno.build
+ // - https://github.com/denoland/deno/issues/14799
+
+ platform = platform.toLowerCase();
+
+ // NOTE: this iOS check is untested and may not work
+ // Node does not work natively on IOS, there is a fork at
+ // https://github.com/nodejs-mobile/nodejs-mobile
+ // however it is unknown at the time of writing how to detect if it is running
+ if (platform.includes('ios')) return 'iOS';
+ if (platform === 'android') return 'Android';
+ if (platform === 'darwin') return 'MacOS';
+ if (platform === 'win32') return 'Windows';
+ if (platform === 'freebsd') return 'FreeBSD';
+ if (platform === 'openbsd') return 'OpenBSD';
+ if (platform === 'linux') return 'Linux';
+ if (platform) return `Other:${platform}`;
+ return 'Unknown';
+};
+
+let _platformHeaders: PlatformProperties;
+export const getPlatformHeaders = () => {
+ return (_platformHeaders ??= getPlatformProperties());
+};
diff --git a/packages/sdk/src/internal/errors.ts b/packages/sdk/src/internal/errors.ts
new file mode 100644
index 000000000..82c7b14d5
--- /dev/null
+++ b/packages/sdk/src/internal/errors.ts
@@ -0,0 +1,33 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export function isAbortError(err: unknown) {
+ return (
+ typeof err === 'object' &&
+ err !== null &&
+ // Spec-compliant fetch implementations
+ (('name' in err && (err as any).name === 'AbortError') ||
+ // Expo fetch
+ ('message' in err && String((err as any).message).includes('FetchRequestCanceledException')))
+ );
+}
+
+export const castToError = (err: any): Error => {
+ if (err instanceof Error) return err;
+ if (typeof err === 'object' && err !== null) {
+ try {
+ if (Object.prototype.toString.call(err) === '[object Error]') {
+ // @ts-ignore - not all envs have native support for cause yet
+ const error = new Error(err.message, err.cause ? { cause: err.cause } : {});
+ if (err.stack) error.stack = err.stack;
+ // @ts-ignore - not all envs have native support for cause yet
+ if (err.cause && !error.cause) error.cause = err.cause;
+ if (err.name) error.name = err.name;
+ return error;
+ }
+ } catch {}
+ try {
+ return new Error(JSON.stringify(err));
+ } catch {}
+ }
+ return new Error(err);
+};
diff --git a/packages/sdk/src/internal/headers.ts b/packages/sdk/src/internal/headers.ts
new file mode 100644
index 000000000..c724a9d22
--- /dev/null
+++ b/packages/sdk/src/internal/headers.ts
@@ -0,0 +1,97 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { isReadonlyArray } from './utils/values';
+
+type HeaderValue = string | undefined | null;
+export type HeadersLike =
+ | Headers
+ | readonly HeaderValue[][]
+ | Record<string, HeaderValue | readonly HeaderValue[]>
+ | undefined
+ | null
+ | NullableHeaders;
+
+const brand_privateNullableHeaders = /* @__PURE__ */ Symbol('brand.privateNullableHeaders');
+
+/**
+ * @internal
+ * Users can pass explicit nulls to unset default headers. When we parse them
+ * into a standard headers type we need to preserve that information.
+ */
+export type NullableHeaders = {
+ /** Brand check, prevent users from creating a NullableHeaders. */
+ [brand_privateNullableHeaders]: true;
+ /** Parsed headers. */
+ values: Headers;
+ /** Set of lowercase header names explicitly set to null. */
+ nulls: Set<string>;
+};
+
+function* iterateHeaders(headers: HeadersLike): IterableIterator<readonly [string, string | null]> {
+ if (!headers) return;
+
+ if (brand_privateNullableHeaders in headers) {
+ const { values, nulls } = headers;
+ yield* values.entries();
+ for (const name of nulls) {
+ yield [name, null];
+ }
+ return;
+ }
+
+ let shouldClear = false;
+ let iter: Iterable<readonly (HeaderValue | readonly HeaderValue[])[]>;
+ if (headers instanceof Headers) {
+ iter = headers.entries();
+ } else if (isReadonlyArray(headers)) {
+ iter = headers;
+ } else {
+ shouldClear = true;
+ iter = Object.entries(headers ?? {});
+ }
+ for (let row of iter) {
+ const name = row[0];
+ if (typeof name !== 'string') throw new TypeError('expected header name to be a string');
+ const values = isReadonlyArray(row[1]) ? row[1] : [row[1]];
+ let didClear = false;
+ for (const value of values) {
+ if (value === undefined) continue;
+
+ // Objects keys always overwrite older headers, they never append.
+ // Yield a null to clear the header before adding the new values.
+ if (shouldClear && !didClear) {
+ didClear = true;
+ yield [name, null];
+ }
+ yield [name, value];
+ }
+ }
+}
+
+export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => {
+ const targetHeaders = new Headers();
+ const nullHeaders = new Set<string>();
+ for (const headers of newHeaders) {
+ const seenHeaders = new Set<string>();
+ for (const [name, value] of iterateHeaders(headers)) {
+ const lowerName = name.toLowerCase();
+ if (!seenHeaders.has(lowerName)) {
+ targetHeaders.delete(name);
+ seenHeaders.add(lowerName);
+ }
+ if (value === null) {
+ targetHeaders.delete(name);
+ nullHeaders.add(lowerName);
+ } else {
+ targetHeaders.append(name, value);
+ nullHeaders.delete(lowerName);
+ }
+ }
+ }
+ return { [brand_privateNullableHeaders]: true, values: targetHeaders, nulls: nullHeaders };
+};
+
+export const isEmptyHeaders = (headers: HeadersLike) => {
+ for (const _ of iterateHeaders(headers)) return false;
+ return true;
+};
diff --git a/packages/sdk/src/internal/parse.ts b/packages/sdk/src/internal/parse.ts
new file mode 100644
index 000000000..dcb4026e3
--- /dev/null
+++ b/packages/sdk/src/internal/parse.ts
@@ -0,0 +1,64 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import type { FinalRequestOptions } from './request-options';
+import { Stream } from '../core/streaming';
+import { type Opencode } from '../client';
+import { formatRequestDetails, loggerFor } from './utils/log';
+
+export type APIResponseProps = {
+ response: Response;
+ options: FinalRequestOptions;
+ controller: AbortController;
+ requestLogID: string;
+ retryOfRequestLogID: string | undefined;
+ startTime: number;
+};
+
+export async function defaultParseResponse<T>(client: Opencode, props: APIResponseProps): Promise<T> {
+ const { response, requestLogID, retryOfRequestLogID, startTime } = props;
+ const body = await (async () => {
+ if (props.options.stream) {
+ loggerFor(client).debug('response', response.status, response.url, response.headers, response.body);
+
+ // Note: there is an invariant here that isn't represented in the type system
+ // that if you set `stream: true` the response type must also be `Stream<T>`
+
+ if (props.options.__streamClass) {
+ return props.options.__streamClass.fromSSEResponse(response, props.controller, client) as any;
+ }
+
+ return Stream.fromSSEResponse(response, props.controller, client) as any;
+ }
+
+ // fetch refuses to read the body when the status code is 204.
+ if (response.status === 204) {
+ return null as T;
+ }
+
+ if (props.options.__binaryResponse) {
+ return response as unknown as T;
+ }
+
+ const contentType = response.headers.get('content-type');
+ const mediaType = contentType?.split(';')[0]?.trim();
+ const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json');
+ if (isJSON) {
+ const json = await response.json();
+ return json as T;
+ }
+
+ const text = await response.text();
+ return text as unknown as T;
+ })();
+ loggerFor(client).debug(
+ `[${requestLogID}] response parsed`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url: response.url,
+ status: response.status,
+ body,
+ durationMs: Date.now() - startTime,
+ }),
+ );
+ return body;
+}
diff --git a/packages/sdk/src/internal/request-options.ts b/packages/sdk/src/internal/request-options.ts
new file mode 100644
index 000000000..56765e5aa
--- /dev/null
+++ b/packages/sdk/src/internal/request-options.ts
@@ -0,0 +1,93 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { NullableHeaders } from './headers';
+
+import type { BodyInit } from './builtin-types';
+import { Stream } from '../core/streaming';
+import type { HTTPMethod, MergedRequestInit } from './types';
+import { type HeadersLike } from './headers';
+
+export type FinalRequestOptions = RequestOptions & { method: HTTPMethod; path: string };
+
+export type RequestOptions = {
+ /**
+ * The HTTP method for the request (e.g., 'get', 'post', 'put', 'delete').
+ */
+ method?: HTTPMethod;
+
+ /**
+ * The URL path for the request.
+ *
+ * @example "/v1/foo"
+ */
+ path?: string;
+
+ /**
+ * Query parameters to include in the request URL.
+ */
+ query?: object | undefined | null;
+
+ /**
+ * The request body. Can be a string, JSON object, FormData, or other supported types.
+ */
+ body?: unknown;
+
+ /**
+ * HTTP headers to include with the request. Can be a Headers object, plain object, or array of tuples.
+ */
+ headers?: HeadersLike;
+
+ /**
+ * The maximum number of times that the client will retry a request in case of a
+ * temporary failure, like a network error or a 5XX error from the server.
+ *
+ * @default 2
+ */
+ maxRetries?: number;
+
+ stream?: boolean | undefined;
+
+ /**
+ * The maximum amount of time (in milliseconds) that the client should wait for a response
+ * from the server before timing out a single request.
+ *
+ * @unit milliseconds
+ */
+ timeout?: number;
+
+ /**
+ * Additional `RequestInit` options to be passed to the underlying `fetch` call.
+ * These options will be merged with the client's default fetch options.
+ */
+ fetchOptions?: MergedRequestInit;
+
+ /**
+ * An AbortSignal that can be used to cancel the request.
+ */
+ signal?: AbortSignal | undefined | null;
+
+ /**
+ * A unique key for this request to enable idempotency.
+ */
+ idempotencyKey?: string;
+
+ /**
+ * Override the default base URL for this specific request.
+ */
+ defaultBaseURL?: string | undefined;
+
+ __binaryResponse?: boolean | undefined;
+ __streamClass?: typeof Stream;
+};
+
+export type EncodedContent = { bodyHeaders: HeadersLike; body: BodyInit };
+export type RequestEncoder = (request: { headers: NullableHeaders; body: unknown }) => EncodedContent;
+
+export const FallbackEncoder: RequestEncoder = ({ headers, body }) => {
+ return {
+ bodyHeaders: {
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ };
+};
diff --git a/packages/sdk/src/internal/shim-types.ts b/packages/sdk/src/internal/shim-types.ts
new file mode 100644
index 000000000..8ddf7b0ad
--- /dev/null
+++ b/packages/sdk/src/internal/shim-types.ts
@@ -0,0 +1,26 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+/**
+ * Shims for types that we can't always rely on being available globally.
+ *
+ * Note: these only exist at the type-level, there is no corresponding runtime
+ * version for any of these symbols.
+ */
+
+type NeverToAny<T> = T extends never ? any : T;
+
+/** @ts-ignore */
+type _DOMReadableStream<R = any> = globalThis.ReadableStream<R>;
+
+/** @ts-ignore */
+type _NodeReadableStream<R = any> = import('stream/web').ReadableStream<R>;
+
+type _ConditionalNodeReadableStream<R = any> =
+ typeof globalThis extends { ReadableStream: any } ? never : _NodeReadableStream<R>;
+
+type _ReadableStream<R = any> = NeverToAny<
+ | ([0] extends [1 & _DOMReadableStream<R>] ? never : _DOMReadableStream<R>)
+ | ([0] extends [1 & _ConditionalNodeReadableStream<R>] ? never : _ConditionalNodeReadableStream<R>)
+>;
+
+export type { _ReadableStream as ReadableStream };
diff --git a/packages/sdk/src/internal/shims.ts b/packages/sdk/src/internal/shims.ts
new file mode 100644
index 000000000..17a7967aa
--- /dev/null
+++ b/packages/sdk/src/internal/shims.ts
@@ -0,0 +1,107 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+/**
+ * This module provides internal shims and utility functions for environments where certain Node.js or global types may not be available.
+ *
+ * These are used to ensure we can provide a consistent behaviour between different JavaScript environments and good error
+ * messages in cases where an environment isn't fully supported.
+ */
+
+import type { Fetch } from './builtin-types';
+import type { ReadableStream } from './shim-types';
+
+export function getDefaultFetch(): Fetch {
+ if (typeof fetch !== 'undefined') {
+ return fetch as any;
+ }
+
+ throw new Error(
+ '`fetch` is not defined as a global; Either pass `fetch` to the client, `new Opencode({ fetch })` or polyfill the global, `globalThis.fetch = fetch`',
+ );
+}
+
+type ReadableStreamArgs = ConstructorParameters<typeof ReadableStream>;
+
+export function makeReadableStream(...args: ReadableStreamArgs): ReadableStream {
+ const ReadableStream = (globalThis as any).ReadableStream;
+ if (typeof ReadableStream === 'undefined') {
+ // Note: All of the platforms / runtimes we officially support already define
+ // `ReadableStream` as a global, so this should only ever be hit on unsupported runtimes.
+ throw new Error(
+ '`ReadableStream` is not defined as a global; You will need to polyfill it, `globalThis.ReadableStream = ReadableStream`',
+ );
+ }
+
+ return new ReadableStream(...args);
+}
+
+export function ReadableStreamFrom<T>(iterable: Iterable<T> | AsyncIterable<T>): ReadableStream<T> {
+ let iter: AsyncIterator<T> | Iterator<T> =
+ Symbol.asyncIterator in iterable ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator]();
+
+ return makeReadableStream({
+ start() {},
+ async pull(controller: any) {
+ const { done, value } = await iter.next();
+ if (done) {
+ controller.close();
+ } else {
+ controller.enqueue(value);
+ }
+ },
+ async cancel() {
+ await iter.return?.();
+ },
+ });
+}
+
+/**
+ * Most browsers don't yet have async iterable support for ReadableStream,
+ * and Node has a very different way of reading bytes from its "ReadableStream".
+ *
+ * This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
+ */
+export function ReadableStreamToAsyncIterable<T>(stream: any): AsyncIterableIterator<T> {
+ if (stream[Symbol.asyncIterator]) return stream;
+
+ const reader = stream.getReader();
+ return {
+ async next() {
+ try {
+ const result = await reader.read();
+ if (result?.done) reader.releaseLock(); // release lock when stream becomes closed
+ return result;
+ } catch (e) {
+ reader.releaseLock(); // release lock when stream becomes errored
+ throw e;
+ }
+ },
+ async return() {
+ const cancelPromise = reader.cancel();
+ reader.releaseLock();
+ await cancelPromise;
+ return { done: true, value: undefined };
+ },
+ [Symbol.asyncIterator]() {
+ return this;
+ },
+ };
+}
+
+/**
+ * Cancels a ReadableStream we don't need to consume.
+ * See https://undici.nodejs.org/#/?id=garbage-collection
+ */
+export async function CancelReadableStream(stream: any): Promise<void> {
+ if (stream === null || typeof stream !== 'object') return;
+
+ if (stream[Symbol.asyncIterator]) {
+ await stream[Symbol.asyncIterator]().return?.();
+ return;
+ }
+
+ const reader = stream.getReader();
+ const cancelPromise = reader.cancel();
+ reader.releaseLock();
+ await cancelPromise;
+}
diff --git a/packages/sdk/src/internal/to-file.ts b/packages/sdk/src/internal/to-file.ts
new file mode 100644
index 000000000..245e84933
--- /dev/null
+++ b/packages/sdk/src/internal/to-file.ts
@@ -0,0 +1,154 @@
+import { BlobPart, getName, makeFile, isAsyncIterable } from './uploads';
+import type { FilePropertyBag } from './builtin-types';
+import { checkFileSupport } from './uploads';
+
+type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | DataView;
+
+/**
+ * Intended to match DOM Blob, node-fetch Blob, node:buffer Blob, etc.
+ * Don't add arrayBuffer here, node-fetch doesn't have it
+ */
+interface BlobLike {
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
+ readonly size: number;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
+ readonly type: string;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
+ text(): Promise<string>;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
+ slice(start?: number, end?: number): BlobLike;
+}
+
+/**
+ * This check adds the arrayBuffer() method type because it is available and used at runtime
+ */
+const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.size === 'number' &&
+ typeof value.type === 'string' &&
+ typeof value.text === 'function' &&
+ typeof value.slice === 'function' &&
+ typeof value.arrayBuffer === 'function';
+
+/**
+ * Intended to match DOM File, node:buffer File, undici File, etc.
+ */
+interface FileLike extends BlobLike {
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
+ readonly lastModified: number;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
+ readonly name?: string | undefined;
+}
+
+/**
+ * This check adds the arrayBuffer() method type because it is available and used at runtime
+ */
+const isFileLike = (value: any): value is FileLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.name === 'string' &&
+ typeof value.lastModified === 'number' &&
+ isBlobLike(value);
+
+/**
+ * Intended to match DOM Response, node-fetch Response, undici Response, etc.
+ */
+export interface ResponseLike {
+ url: string;
+ blob(): Promise<BlobLike>;
+}
+
+const isResponseLike = (value: any): value is ResponseLike =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.url === 'string' &&
+ typeof value.blob === 'function';
+
+export type ToFileInput =
+ | FileLike
+ | ResponseLike
+ | Exclude<BlobLikePart, string>
+ | AsyncIterable<BlobLikePart>;
+
+/**
+ * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
+ * @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
+ * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
+ * @param {Object=} options additional properties
+ * @param {string=} options.type the MIME type of the content
+ * @param {number=} options.lastModified the last modified timestamp
+ * @returns a {@link File} with the given properties
+ */
+export async function toFile(
+ value: ToFileInput | PromiseLike<ToFileInput>,
+ name?: string | null | undefined,
+ options?: FilePropertyBag | undefined,
+): Promise<File> {
+ checkFileSupport();
+
+ // If it's a promise, resolve it.
+ value = await value;
+
+ // If we've been given a `File` we don't need to do anything
+ if (isFileLike(value)) {
+ if (value instanceof File) {
+ return value;
+ }
+ return makeFile([await value.arrayBuffer()], value.name);
+ }
+
+ if (isResponseLike(value)) {
+ const blob = await value.blob();
+ name ||= new URL(value.url).pathname.split(/[\\/]/).pop();
+
+ return makeFile(await getBytes(blob), name, options);
+ }
+
+ const parts = await getBytes(value);
+
+ name ||= getName(value);
+
+ if (!options?.type) {
+ const type = parts.find((part) => typeof part === 'object' && 'type' in part && part.type);
+ if (typeof type === 'string') {
+ options = { ...options, type };
+ }
+ }
+
+ return makeFile(parts, name, options);
+}
+
+async function getBytes(value: BlobLikePart | AsyncIterable<BlobLikePart>): Promise<Array<BlobPart>> {
+ let parts: Array<BlobPart> = [];
+ if (
+ typeof value === 'string' ||
+ ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
+ value instanceof ArrayBuffer
+ ) {
+ parts.push(value);
+ } else if (isBlobLike(value)) {
+ parts.push(value instanceof Blob ? value : await value.arrayBuffer());
+ } else if (
+ isAsyncIterable(value) // includes Readable, ReadableStream, etc.
+ ) {
+ for await (const chunk of value) {
+ parts.push(...(await getBytes(chunk as BlobLikePart))); // TODO, consider validating?
+ }
+ } else {
+ const constructor = value?.constructor?.name;
+ throw new Error(
+ `Unexpected data type: ${typeof value}${
+ constructor ? `; constructor: ${constructor}` : ''
+ }${propsForError(value)}`,
+ );
+ }
+
+ return parts;
+}
+
+function propsForError(value: unknown): string {
+ if (typeof value !== 'object' || value === null) return '';
+ const props = Object.getOwnPropertyNames(value);
+ return `; props: [${props.map((p) => `"${p}"`).join(', ')}]`;
+}
diff --git a/packages/sdk/src/internal/types.ts b/packages/sdk/src/internal/types.ts
new file mode 100644
index 000000000..b668dfc0f
--- /dev/null
+++ b/packages/sdk/src/internal/types.ts
@@ -0,0 +1,95 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export type PromiseOrValue<T> = T | Promise<T>;
+export type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
+
+export type KeysEnum<T> = { [P in keyof Required<T>]: true };
+
+export type FinalizedRequestInit = RequestInit & { headers: Headers };
+
+type NotAny<T> = [0] extends [1 & T] ? never : T;
+
+/**
+ * Some environments overload the global fetch function, and Parameters<T> only gets the last signature.
+ */
+type OverloadedParameters<T> =
+ T extends (
+ {
+ (...args: infer A): unknown;
+ (...args: infer B): unknown;
+ (...args: infer C): unknown;
+ (...args: infer D): unknown;
+ }
+ ) ?
+ A | B | C | D
+ : T extends (
+ {
+ (...args: infer A): unknown;
+ (...args: infer B): unknown;
+ (...args: infer C): unknown;
+ }
+ ) ?
+ A | B | C
+ : T extends (
+ {
+ (...args: infer A): unknown;
+ (...args: infer B): unknown;
+ }
+ ) ?
+ A | B
+ : T extends (...args: infer A) => unknown ? A
+ : never;
+
+/* eslint-disable */
+/**
+ * These imports attempt to get types from a parent package's dependencies.
+ * Unresolved bare specifiers can trigger [automatic type acquisition][1] in some projects, which
+ * would cause typescript to show types not present at runtime. To avoid this, we import
+ * directly from parent node_modules folders.
+ *
+ * We need to check multiple levels because we don't know what directory structure we'll be in.
+ * For example, pnpm generates directories like this:
+ * ```
+ * node_modules
+ * ├── .pnpm
+ * │ └── [email protected]
+ * │ └── node_modules
+ * │ └── pkg
+ * │ └── internal
+ * │ └── types.d.ts
+ * ├── pkg -> .pnpm/[email protected]/node_modules/pkg
+ * └── undici
+ * ```
+ *
+ * [1]: https://www.typescriptlang.org/tsconfig/#typeAcquisition
+ */
+/** @ts-ignore For users with \@types/node */
+type UndiciTypesRequestInit = NotAny<import('../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit>;
+/** @ts-ignore For users with undici */
+type UndiciRequestInit = NotAny<import('../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/undici/index.d.ts').RequestInit>;
+/** @ts-ignore For users with \@types/bun */
+type BunRequestInit = globalThis.FetchRequestInit;
+/** @ts-ignore For users with node-fetch@2 */
+type NodeFetch2RequestInit = NotAny<import('../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit>;
+/** @ts-ignore For users with node-fetch@3, doesn't need file extension because types are at ./@types/index.d.ts */
+type NodeFetch3RequestInit = NotAny<import('../node_modules/node-fetch').RequestInit> | NotAny<import('../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/node-fetch').RequestInit>;
+/** @ts-ignore For users who use Deno */
+type FetchRequestInit = NonNullable<OverloadedParameters<typeof fetch>[1]>;
+/* eslint-enable */
+
+type RequestInits =
+ | NotAny<UndiciTypesRequestInit>
+ | NotAny<UndiciRequestInit>
+ | NotAny<BunRequestInit>
+ | NotAny<NodeFetch2RequestInit>
+ | NotAny<NodeFetch3RequestInit>
+ | NotAny<RequestInit>
+ | NotAny<FetchRequestInit>;
+
+/**
+ * This type contains `RequestInit` options that may be available on the current runtime,
+ * including per-platform extensions like `dispatcher`, `agent`, `client`, etc.
+ */
+export type MergedRequestInit = RequestInits &
+ /** We don't include these in the types as they'll be overridden for every request. */
+ Partial<Record<'body' | 'headers' | 'method' | 'signal', never>>;
diff --git a/packages/sdk/src/internal/uploads.ts b/packages/sdk/src/internal/uploads.ts
new file mode 100644
index 000000000..eb55f834e
--- /dev/null
+++ b/packages/sdk/src/internal/uploads.ts
@@ -0,0 +1,187 @@
+import { type RequestOptions } from './request-options';
+import type { FilePropertyBag, Fetch } from './builtin-types';
+import type { Opencode } from '../client';
+import { ReadableStreamFrom } from './shims';
+
+export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView;
+type FsReadStream = AsyncIterable<Uint8Array> & { path: string | { toString(): string } };
+
+// https://github.com/oven-sh/bun/issues/5980
+interface BunFile extends Blob {
+ readonly name?: string | undefined;
+}
+
+export const checkFileSupport = () => {
+ if (typeof File === 'undefined') {
+ const { process } = globalThis as any;
+ const isOldNode =
+ typeof process?.versions?.node === 'string' && parseInt(process.versions.node.split('.')) < 20;
+ throw new Error(
+ '`File` is not defined as a global, which is required for file uploads.' +
+ (isOldNode ?
+ " Update to Node 20 LTS or newer, or set `globalThis.File` to `import('node:buffer').File`."
+ : ''),
+ );
+ }
+};
+
+/**
+ * Typically, this is a native "File" class.
+ *
+ * We provide the {@link toFile} utility to convert a variety of objects
+ * into the File class.
+ *
+ * For convenience, you can also pass a fetch Response, or in Node,
+ * the result of fs.createReadStream().
+ */
+export type Uploadable = File | Response | FsReadStream | BunFile;
+
+/**
+ * Construct a `File` instance. This is used to ensure a helpful error is thrown
+ * for environments that don't define a global `File` yet.
+ */
+export function makeFile(
+ fileBits: BlobPart[],
+ fileName: string | undefined,
+ options?: FilePropertyBag,
+): File {
+ checkFileSupport();
+ return new File(fileBits as any, fileName ?? 'unknown_file', options);
+}
+
+export function getName(value: any): string | undefined {
+ return (
+ (
+ (typeof value === 'object' &&
+ value !== null &&
+ (('name' in value && value.name && String(value.name)) ||
+ ('url' in value && value.url && String(value.url)) ||
+ ('filename' in value && value.filename && String(value.filename)) ||
+ ('path' in value && value.path && String(value.path)))) ||
+ ''
+ )
+ .split(/[\\/]/)
+ .pop() || undefined
+ );
+}
+
+export const isAsyncIterable = (value: any): value is AsyncIterable<any> =>
+ value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function';
+
+/**
+ * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value.
+ * Otherwise returns the request as is.
+ */
+export const maybeMultipartFormRequestOptions = async (
+ opts: RequestOptions,
+ fetch: Opencode | Fetch,
+): Promise<RequestOptions> => {
+ if (!hasUploadableValue(opts.body)) return opts;
+
+ return { ...opts, body: await createForm(opts.body, fetch) };
+};
+
+type MultipartFormRequestOptions = Omit<RequestOptions, 'body'> & { body: unknown };
+
+export const multipartFormRequestOptions = async (
+ opts: MultipartFormRequestOptions,
+ fetch: Opencode | Fetch,
+): Promise<RequestOptions> => {
+ return { ...opts, body: await createForm(opts.body, fetch) };
+};
+
+const supportsFormDataMap = /* @__PURE__ */ new WeakMap<Fetch, Promise<boolean>>();
+
+/**
+ * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending
+ * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]".
+ * This function detects if the fetch function provided supports the global FormData object to avoid
+ * confusing error messages later on.
+ */
+function supportsFormData(fetchObject: Opencode | Fetch): Promise<boolean> {
+ const fetch: Fetch = typeof fetchObject === 'function' ? fetchObject : (fetchObject as any).fetch;
+ const cached = supportsFormDataMap.get(fetch);
+ if (cached) return cached;
+ const promise = (async () => {
+ try {
+ const FetchResponse = (
+ 'Response' in fetch ?
+ fetch.Response
+ : (await fetch('data:,')).constructor) as typeof Response;
+ const data = new FormData();
+ if (data.toString() === (await new FetchResponse(data).text())) {
+ return false;
+ }
+ return true;
+ } catch {
+ // avoid false negatives
+ return true;
+ }
+ })();
+ supportsFormDataMap.set(fetch, promise);
+ return promise;
+}
+
+export const createForm = async <T = Record<string, unknown>>(
+ body: T | undefined,
+ fetch: Opencode | Fetch,
+): Promise<FormData> => {
+ if (!(await supportsFormData(fetch))) {
+ throw new TypeError(
+ 'The provided fetch function does not support file uploads with the current global FormData class.',
+ );
+ }
+ const form = new FormData();
+ await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value)));
+ return form;
+};
+
+// We check for Blob not File because Bun.File doesn't inherit from File,
+// but they both inherit from Blob and have a `name` property at runtime.
+const isNamedBlob = (value: unknown) => value instanceof Blob && 'name' in value;
+
+const isUploadable = (value: unknown) =>
+ typeof value === 'object' &&
+ value !== null &&
+ (value instanceof Response || isAsyncIterable(value) || isNamedBlob(value));
+
+const hasUploadableValue = (value: unknown): boolean => {
+ if (isUploadable(value)) return true;
+ if (Array.isArray(value)) return value.some(hasUploadableValue);
+ if (value && typeof value === 'object') {
+ for (const k in value) {
+ if (hasUploadableValue((value as any)[k])) return true;
+ }
+ }
+ return false;
+};
+
+const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => {
+ if (value === undefined) return;
+ if (value == null) {
+ throw new TypeError(
+ `Received null for "${key}"; to pass null in FormData, you must use the string 'null'`,
+ );
+ }
+
+ // TODO: make nested formats configurable
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ form.append(key, String(value));
+ } else if (value instanceof Response) {
+ form.append(key, makeFile([await value.blob()], getName(value)));
+ } else if (isAsyncIterable(value)) {
+ form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value)));
+ } else if (isNamedBlob(value)) {
+ form.append(key, value, getName(value));
+ } else if (Array.isArray(value)) {
+ await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry)));
+ } else if (typeof value === 'object') {
+ await Promise.all(
+ Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)),
+ );
+ } else {
+ throw new TypeError(
+ `Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`,
+ );
+ }
+};
diff --git a/packages/sdk/src/internal/utils.ts b/packages/sdk/src/internal/utils.ts
new file mode 100644
index 000000000..3cbfacce2
--- /dev/null
+++ b/packages/sdk/src/internal/utils.ts
@@ -0,0 +1,8 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export * from './utils/values';
+export * from './utils/base64';
+export * from './utils/env';
+export * from './utils/log';
+export * from './utils/uuid';
+export * from './utils/sleep';
diff --git a/packages/sdk/src/internal/utils/base64.ts b/packages/sdk/src/internal/utils/base64.ts
new file mode 100644
index 000000000..05d7bd9b4
--- /dev/null
+++ b/packages/sdk/src/internal/utils/base64.ts
@@ -0,0 +1,40 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { OpencodeError } from '../../core/error';
+import { encodeUTF8 } from './bytes';
+
+export const toBase64 = (data: string | Uint8Array | null | undefined): string => {
+ if (!data) return '';
+
+ if (typeof (globalThis as any).Buffer !== 'undefined') {
+ return (globalThis as any).Buffer.from(data).toString('base64');
+ }
+
+ if (typeof data === 'string') {
+ data = encodeUTF8(data);
+ }
+
+ if (typeof btoa !== 'undefined') {
+ return btoa(String.fromCharCode.apply(null, data as any));
+ }
+
+ throw new OpencodeError('Cannot generate base64 string; Expected `Buffer` or `btoa` to be defined');
+};
+
+export const fromBase64 = (str: string): Uint8Array => {
+ if (typeof (globalThis as any).Buffer !== 'undefined') {
+ const buf = (globalThis as any).Buffer.from(str, 'base64');
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
+ }
+
+ if (typeof atob !== 'undefined') {
+ const bstr = atob(str);
+ const buf = new Uint8Array(bstr.length);
+ for (let i = 0; i < bstr.length; i++) {
+ buf[i] = bstr.charCodeAt(i);
+ }
+ return buf;
+ }
+
+ throw new OpencodeError('Cannot decode base64 string; Expected `Buffer` or `atob` to be defined');
+};
diff --git a/packages/sdk/src/internal/utils/bytes.ts b/packages/sdk/src/internal/utils/bytes.ts
new file mode 100644
index 000000000..8da627abe
--- /dev/null
+++ b/packages/sdk/src/internal/utils/bytes.ts
@@ -0,0 +1,32 @@
+export function concatBytes(buffers: Uint8Array[]): Uint8Array {
+ let length = 0;
+ for (const buffer of buffers) {
+ length += buffer.length;
+ }
+ const output = new Uint8Array(length);
+ let index = 0;
+ for (const buffer of buffers) {
+ output.set(buffer, index);
+ index += buffer.length;
+ }
+
+ return output;
+}
+
+let encodeUTF8_: (str: string) => Uint8Array;
+export function encodeUTF8(str: string) {
+ let encoder;
+ return (
+ encodeUTF8_ ??
+ ((encoder = new (globalThis as any).TextEncoder()), (encodeUTF8_ = encoder.encode.bind(encoder)))
+ )(str);
+}
+
+let decodeUTF8_: (bytes: Uint8Array) => string;
+export function decodeUTF8(bytes: Uint8Array) {
+ let decoder;
+ return (
+ decodeUTF8_ ??
+ ((decoder = new (globalThis as any).TextDecoder()), (decodeUTF8_ = decoder.decode.bind(decoder)))
+ )(bytes);
+}
diff --git a/packages/sdk/src/internal/utils/env.ts b/packages/sdk/src/internal/utils/env.ts
new file mode 100644
index 000000000..2d8480077
--- /dev/null
+++ b/packages/sdk/src/internal/utils/env.ts
@@ -0,0 +1,18 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+/**
+ * Read an environment variable.
+ *
+ * Trims beginning and trailing whitespace.
+ *
+ * Will return undefined if the environment variable doesn't exist or cannot be accessed.
+ */
+export const readEnv = (env: string): string | undefined => {
+ if (typeof (globalThis as any).process !== 'undefined') {
+ return (globalThis as any).process.env?.[env]?.trim() ?? undefined;
+ }
+ if (typeof (globalThis as any).Deno !== 'undefined') {
+ return (globalThis as any).Deno.env?.get?.(env)?.trim();
+ }
+ return undefined;
+};
diff --git a/packages/sdk/src/internal/utils/log.ts b/packages/sdk/src/internal/utils/log.ts
new file mode 100644
index 000000000..44ac16a02
--- /dev/null
+++ b/packages/sdk/src/internal/utils/log.ts
@@ -0,0 +1,126 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { hasOwn } from './values';
+import { type Opencode } from '../../client';
+import { RequestOptions } from '../request-options';
+
+type LogFn = (message: string, ...rest: unknown[]) => void;
+export type Logger = {
+ error: LogFn;
+ warn: LogFn;
+ info: LogFn;
+ debug: LogFn;
+};
+export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
+
+const levelNumbers = {
+ off: 0,
+ error: 200,
+ warn: 300,
+ info: 400,
+ debug: 500,
+};
+
+export const parseLogLevel = (
+ maybeLevel: string | undefined,
+ sourceName: string,
+ client: Opencode,
+): LogLevel | undefined => {
+ if (!maybeLevel) {
+ return undefined;
+ }
+ if (hasOwn(levelNumbers, maybeLevel)) {
+ return maybeLevel;
+ }
+ loggerFor(client).warn(
+ `${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(
+ Object.keys(levelNumbers),
+ )}`,
+ );
+ return undefined;
+};
+
+function noop() {}
+
+function makeLogFn(fnLevel: keyof Logger, logger: Logger | undefined, logLevel: LogLevel) {
+ if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
+ return noop;
+ } else {
+ // Don't wrap logger functions, we want the stacktrace intact!
+ return logger[fnLevel].bind(logger);
+ }
+}
+
+const noopLogger = {
+ error: noop,
+ warn: noop,
+ info: noop,
+ debug: noop,
+};
+
+let cachedLoggers = /* @__PURE__ */ new WeakMap<Logger, [LogLevel, Logger]>();
+
+export function loggerFor(client: Opencode): Logger {
+ const logger = client.logger;
+ const logLevel = client.logLevel ?? 'off';
+ if (!logger) {
+ return noopLogger;
+ }
+
+ const cachedLogger = cachedLoggers.get(logger);
+ if (cachedLogger && cachedLogger[0] === logLevel) {
+ return cachedLogger[1];
+ }
+
+ const levelLogger = {
+ error: makeLogFn('error', logger, logLevel),
+ warn: makeLogFn('warn', logger, logLevel),
+ info: makeLogFn('info', logger, logLevel),
+ debug: makeLogFn('debug', logger, logLevel),
+ };
+
+ cachedLoggers.set(logger, [logLevel, levelLogger]);
+
+ return levelLogger;
+}
+
+export const formatRequestDetails = (details: {
+ options?: RequestOptions | undefined;
+ headers?: Headers | Record<string, string> | undefined;
+ retryOfRequestLogID?: string | undefined;
+ retryOf?: string | undefined;
+ url?: string | undefined;
+ status?: number | undefined;
+ method?: string | undefined;
+ durationMs?: number | undefined;
+ message?: unknown;
+ body?: unknown;
+}) => {
+ if (details.options) {
+ details.options = { ...details.options };
+ delete details.options['headers']; // redundant + leaks internals
+ }
+ if (details.headers) {
+ details.headers = Object.fromEntries(
+ (details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(
+ ([name, value]) => [
+ name,
+ (
+ name.toLowerCase() === 'authorization' ||
+ name.toLowerCase() === 'cookie' ||
+ name.toLowerCase() === 'set-cookie'
+ ) ?
+ '***'
+ : value,
+ ],
+ ),
+ );
+ }
+ if ('retryOfRequestLogID' in details) {
+ if (details.retryOfRequestLogID) {
+ details.retryOf = details.retryOfRequestLogID;
+ }
+ delete details.retryOfRequestLogID;
+ }
+ return details;
+};
diff --git a/packages/sdk/src/internal/utils/path.ts b/packages/sdk/src/internal/utils/path.ts
new file mode 100644
index 000000000..6ca96e0dd
--- /dev/null
+++ b/packages/sdk/src/internal/utils/path.ts
@@ -0,0 +1,88 @@
+import { OpencodeError } from '../../core/error';
+
+/**
+ * Percent-encode everything that isn't safe to have in a path without encoding safe chars.
+ *
+ * Taken from https://datatracker.ietf.org/doc/html/rfc3986#section-3.3:
+ * > unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ * > sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+ * > pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+ */
+export function encodeURIPath(str: string) {
+ return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
+}
+
+const EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
+
+export const createPathTagFunction = (pathEncoder = encodeURIPath) =>
+ function path(statics: readonly string[], ...params: readonly unknown[]): string {
+ // If there are no params, no processing is needed.
+ if (statics.length === 1) return statics[0]!;
+
+ let postPath = false;
+ const invalidSegments = [];
+ const path = statics.reduce((previousValue, currentValue, index) => {
+ if (/[?#]/.test(currentValue)) {
+ postPath = true;
+ }
+ const value = params[index];
+ let encoded = (postPath ? encodeURIComponent : pathEncoder)('' + value);
+ if (
+ index !== params.length &&
+ (value == null ||
+ (typeof value === 'object' &&
+ // handle values from other realms
+ value.toString ===
+ Object.getPrototypeOf(Object.getPrototypeOf((value as any).hasOwnProperty ?? EMPTY) ?? EMPTY)
+ ?.toString))
+ ) {
+ encoded = value + '';
+ invalidSegments.push({
+ start: previousValue.length + currentValue.length,
+ length: encoded.length,
+ error: `Value of type ${Object.prototype.toString
+ .call(value)
+ .slice(8, -1)} is not a valid path parameter`,
+ });
+ }
+ return previousValue + currentValue + (index === params.length ? '' : encoded);
+ }, '');
+
+ const pathOnly = path.split(/[?#]/, 1)[0]!;
+ const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
+ let match;
+
+ // Find all invalid segments
+ while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
+ invalidSegments.push({
+ start: match.index,
+ length: match[0].length,
+ error: `Value "${match[0]}" can\'t be safely passed as a path parameter`,
+ });
+ }
+
+ invalidSegments.sort((a, b) => a.start - b.start);
+
+ if (invalidSegments.length > 0) {
+ let lastEnd = 0;
+ const underline = invalidSegments.reduce((acc, segment) => {
+ const spaces = ' '.repeat(segment.start - lastEnd);
+ const arrows = '^'.repeat(segment.length);
+ lastEnd = segment.start + segment.length;
+ return acc + spaces + arrows;
+ }, '');
+
+ throw new OpencodeError(
+ `Path parameters result in path with invalid segments:\n${invalidSegments
+ .map((e) => e.error)
+ .join('\n')}\n${path}\n${underline}`,
+ );
+ }
+
+ return path;
+ };
+
+/**
+ * URI-encodes path params and ensures no unsafe /./ or /../ path segments are introduced.
+ */
+export const path = /* @__PURE__ */ createPathTagFunction(encodeURIPath);
diff --git a/packages/sdk/src/internal/utils/sleep.ts b/packages/sdk/src/internal/utils/sleep.ts
new file mode 100644
index 000000000..65e52962b
--- /dev/null
+++ b/packages/sdk/src/internal/utils/sleep.ts
@@ -0,0 +1,3 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
diff --git a/packages/sdk/src/internal/utils/uuid.ts b/packages/sdk/src/internal/utils/uuid.ts
new file mode 100644
index 000000000..b0e53aaf7
--- /dev/null
+++ b/packages/sdk/src/internal/utils/uuid.ts
@@ -0,0 +1,17 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+/**
+ * https://stackoverflow.com/a/2117523
+ */
+export let uuid4 = function () {
+ const { crypto } = globalThis as any;
+ if (crypto?.randomUUID) {
+ uuid4 = crypto.randomUUID.bind(crypto);
+ return crypto.randomUUID();
+ }
+ const u8 = new Uint8Array(1);
+ const randomByte = crypto ? () => crypto.getRandomValues(u8)[0]! : () => (Math.random() * 0xff) & 0xff;
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>
+ (+c ^ (randomByte() & (15 >> (+c / 4)))).toString(16),
+ );
+};
diff --git a/packages/sdk/src/internal/utils/values.ts b/packages/sdk/src/internal/utils/values.ts
new file mode 100644
index 000000000..b2421fd88
--- /dev/null
+++ b/packages/sdk/src/internal/utils/values.ts
@@ -0,0 +1,105 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { OpencodeError } from '../../core/error';
+
+// https://url.spec.whatwg.org/#url-scheme-string
+const startsWithSchemeRegexp = /^[a-z][a-z0-9+.-]*:/i;
+
+export const isAbsoluteURL = (url: string): boolean => {
+ return startsWithSchemeRegexp.test(url);
+};
+
+export let isArray = (val: unknown): val is unknown[] => ((isArray = Array.isArray), isArray(val));
+export let isReadonlyArray = isArray as (val: unknown) => val is readonly unknown[];
+
+/** Returns an object if the given value isn't an object, otherwise returns as-is */
+export function maybeObj(x: unknown): object {
+ if (typeof x !== 'object') {
+ return {};
+ }
+
+ return x ?? {};
+}
+
+// https://stackoverflow.com/a/34491287
+export function isEmptyObj(obj: Object | null | undefined): boolean {
+ if (!obj) return true;
+ for (const _k in obj) return false;
+ return true;
+}
+
+// https://eslint.org/docs/latest/rules/no-prototype-builtins
+export function hasOwn<T extends object = object>(obj: T, key: PropertyKey): key is keyof T {
+ return Object.prototype.hasOwnProperty.call(obj, key);
+}
+
+export function isObj(obj: unknown): obj is Record<string, unknown> {
+ return obj != null && typeof obj === 'object' && !Array.isArray(obj);
+}
+
+export const ensurePresent = <T>(value: T | null | undefined): T => {
+ if (value == null) {
+ throw new OpencodeError(`Expected a value to be given but received ${value} instead.`);
+ }
+
+ return value;
+};
+
+export const validatePositiveInteger = (name: string, n: unknown): number => {
+ if (typeof n !== 'number' || !Number.isInteger(n)) {
+ throw new OpencodeError(`${name} must be an integer`);
+ }
+ if (n < 0) {
+ throw new OpencodeError(`${name} must be a positive integer`);
+ }
+ return n;
+};
+
+export const coerceInteger = (value: unknown): number => {
+ if (typeof value === 'number') return Math.round(value);
+ if (typeof value === 'string') return parseInt(value, 10);
+
+ throw new OpencodeError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
+};
+
+export const coerceFloat = (value: unknown): number => {
+ if (typeof value === 'number') return value;
+ if (typeof value === 'string') return parseFloat(value);
+
+ throw new OpencodeError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
+};
+
+export const coerceBoolean = (value: unknown): boolean => {
+ if (typeof value === 'boolean') return value;
+ if (typeof value === 'string') return value === 'true';
+ return Boolean(value);
+};
+
+export const maybeCoerceInteger = (value: unknown): number | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ return coerceInteger(value);
+};
+
+export const maybeCoerceFloat = (value: unknown): number | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ return coerceFloat(value);
+};
+
+export const maybeCoerceBoolean = (value: unknown): boolean | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ return coerceBoolean(value);
+};
+
+export const safeJSON = (text: string) => {
+ try {
+ return JSON.parse(text);
+ } catch (err) {
+ return undefined;
+ }
+};