summaryrefslogtreecommitdiffhomepage
path: root/packages/sdk/src/client.ts
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/client.ts
parent500cea5ce7fa635a924cd9abea63aaf672f7645d (diff)
downloadopencode-10c8b495907069461f5dc464f6285321290c8b14.tar.gz
opencode-10c8b495907069461f5dc464f6285321290c8b14.zip
chore: generate sdk into packages/sdk
Diffstat (limited to 'packages/sdk/src/client.ts')
-rw-r--r--packages/sdk/src/client.ts862
1 files changed, 862 insertions, 0 deletions
diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts
new file mode 100644
index 000000000..c514ca877
--- /dev/null
+++ b/packages/sdk/src/client.ts
@@ -0,0 +1,862 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import type { RequestInit, RequestInfo, BodyInit } from './internal/builtin-types';
+import type { HTTPMethod, PromiseOrValue, MergedRequestInit, FinalizedRequestInit } from './internal/types';
+import { uuid4 } from './internal/utils/uuid';
+import { validatePositiveInteger, isAbsoluteURL, safeJSON } from './internal/utils/values';
+import { sleep } from './internal/utils/sleep';
+export type { Logger, LogLevel } from './internal/utils/log';
+import { castToError, isAbortError } from './internal/errors';
+import type { APIResponseProps } from './internal/parse';
+import { getPlatformHeaders } from './internal/detect-platform';
+import * as Shims from './internal/shims';
+import * as Opts from './internal/request-options';
+import { VERSION } from './version';
+import * as Errors from './core/error';
+import * as Uploads from './core/uploads';
+import * as API from './resources/index';
+import { APIPromise } from './core/api-promise';
+import {
+ App,
+ AppInitResponse,
+ AppLogParams,
+ AppLogResponse,
+ AppModesResponse,
+ AppProvidersResponse,
+ AppResource,
+ Mode,
+ Model,
+ Provider,
+} from './resources/app';
+import {
+ Config,
+ ConfigResource,
+ KeybindsConfig,
+ McpLocalConfig,
+ McpRemoteConfig,
+ ModeConfig,
+} from './resources/config';
+import { Event, EventListResponse } from './resources/event';
+import { File, FileReadParams, FileReadResponse, FileResource, FileStatusResponse } from './resources/file';
+import {
+ Find,
+ FindFilesParams,
+ FindFilesResponse,
+ FindSymbolsParams,
+ FindSymbolsResponse,
+ FindTextParams,
+ FindTextResponse,
+ Match,
+ Symbol,
+} from './resources/find';
+import {
+ AssistantMessage,
+ FilePart,
+ FilePartInput,
+ FilePartSource,
+ FilePartSourceText,
+ FileSource,
+ Message,
+ Part,
+ Session,
+ SessionAbortResponse,
+ SessionChatParams,
+ SessionDeleteResponse,
+ SessionInitParams,
+ SessionInitResponse,
+ SessionListResponse,
+ SessionMessagesResponse,
+ SessionResource,
+ SessionSummarizeParams,
+ SessionSummarizeResponse,
+ SnapshotPart,
+ StepFinishPart,
+ StepStartPart,
+ SymbolSource,
+ TextPart,
+ TextPartInput,
+ ToolPart,
+ ToolStateCompleted,
+ ToolStateError,
+ ToolStatePending,
+ ToolStateRunning,
+ UserMessage,
+} from './resources/session';
+import { Tui, TuiAppendPromptParams, TuiAppendPromptResponse, TuiOpenHelpResponse } from './resources/tui';
+import { type Fetch } from './internal/builtin-types';
+import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
+import { FinalRequestOptions, RequestOptions } from './internal/request-options';
+import { readEnv } from './internal/utils/env';
+import {
+ type LogLevel,
+ type Logger,
+ formatRequestDetails,
+ loggerFor,
+ parseLogLevel,
+} from './internal/utils/log';
+import { isEmptyObj } from './internal/utils/values';
+
+export interface ClientOptions {
+ /**
+ * Override the default base URL for the API, e.g., "https://api.example.com/v2/"
+ *
+ * Defaults to process.env['OPENCODE_BASE_URL'].
+ */
+ baseURL?: string | null | 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.
+ *
+ * Note that request timeouts are retried by default, so in a worst-case scenario you may wait
+ * much longer than this timeout before the promise succeeds or fails.
+ *
+ * @unit milliseconds
+ */
+ timeout?: number | undefined;
+ /**
+ * Additional `RequestInit` options to be passed to `fetch` calls.
+ * Properties will be overridden by per-request `fetchOptions`.
+ */
+ fetchOptions?: MergedRequestInit | undefined;
+
+ /**
+ * Specify a custom `fetch` function implementation.
+ *
+ * If not provided, we expect that `fetch` is defined globally.
+ */
+ fetch?: Fetch | undefined;
+
+ /**
+ * 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 | undefined;
+
+ /**
+ * Default headers to include with every request to the API.
+ *
+ * These can be removed in individual requests by explicitly setting the
+ * header to `null` in request options.
+ */
+ defaultHeaders?: HeadersLike | undefined;
+
+ /**
+ * Default query parameters to include with every request to the API.
+ *
+ * These can be removed in individual requests by explicitly setting the
+ * param to `undefined` in request options.
+ */
+ defaultQuery?: Record<string, string | undefined> | undefined;
+
+ /**
+ * Set the log level.
+ *
+ * Defaults to process.env['OPENCODE_LOG'] or 'warn' if it isn't set.
+ */
+ logLevel?: LogLevel | undefined;
+
+ /**
+ * Set the logger.
+ *
+ * Defaults to globalThis.console.
+ */
+ logger?: Logger | undefined;
+}
+
+/**
+ * API Client for interfacing with the Opencode API.
+ */
+export class Opencode {
+ baseURL: string;
+ maxRetries: number;
+ timeout: number;
+ logger: Logger | undefined;
+ logLevel: LogLevel | undefined;
+ fetchOptions: MergedRequestInit | undefined;
+
+ private fetch: Fetch;
+ #encoder: Opts.RequestEncoder;
+ protected idempotencyHeader?: string;
+ private _options: ClientOptions;
+
+ /**
+ * API Client for interfacing with the Opencode API.
+ *
+ * @param {string} [opts.baseURL=process.env['OPENCODE_BASE_URL'] ?? http://localhost:54321] - Override the default base URL for the API.
+ * @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
+ * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
+ * @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation.
+ * @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request.
+ * @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API.
+ * @param {Record<string, string | undefined>} opts.defaultQuery - Default query parameters to include with every request to the API.
+ */
+ constructor({ baseURL = readEnv('OPENCODE_BASE_URL'), ...opts }: ClientOptions = {}) {
+ const options: ClientOptions = {
+ ...opts,
+ baseURL: baseURL || `http://localhost:54321`,
+ };
+
+ this.baseURL = options.baseURL!;
+ this.timeout = options.timeout ?? Opencode.DEFAULT_TIMEOUT /* 1 minute */;
+ this.logger = options.logger ?? console;
+ const defaultLogLevel = 'warn';
+ // Set default logLevel early so that we can log a warning in parseLogLevel.
+ this.logLevel = defaultLogLevel;
+ this.logLevel =
+ parseLogLevel(options.logLevel, 'ClientOptions.logLevel', this) ??
+ parseLogLevel(readEnv('OPENCODE_LOG'), "process.env['OPENCODE_LOG']", this) ??
+ defaultLogLevel;
+ this.fetchOptions = options.fetchOptions;
+ this.maxRetries = options.maxRetries ?? 2;
+ this.fetch = options.fetch ?? Shims.getDefaultFetch();
+ this.#encoder = Opts.FallbackEncoder;
+
+ this._options = options;
+ }
+
+ /**
+ * Create a new client instance re-using the same options given to the current client with optional overriding.
+ */
+ withOptions(options: Partial<ClientOptions>): this {
+ const client = new (this.constructor as any as new (props: ClientOptions) => typeof this)({
+ ...this._options,
+ baseURL: this.baseURL,
+ maxRetries: this.maxRetries,
+ timeout: this.timeout,
+ logger: this.logger,
+ logLevel: this.logLevel,
+ fetch: this.fetch,
+ fetchOptions: this.fetchOptions,
+ ...options,
+ });
+ return client;
+ }
+
+ /**
+ * Check whether the base URL is set to its default.
+ */
+ #baseURLOverridden(): boolean {
+ return this.baseURL !== 'http://localhost:54321';
+ }
+
+ protected defaultQuery(): Record<string, string | undefined> | undefined {
+ return this._options.defaultQuery;
+ }
+
+ protected validateHeaders({ values, nulls }: NullableHeaders) {
+ return;
+ }
+
+ /**
+ * Basic re-implementation of `qs.stringify` for primitive types.
+ */
+ protected stringifyQuery(query: Record<string, unknown>): string {
+ return Object.entries(query)
+ .filter(([_, value]) => typeof value !== 'undefined')
+ .map(([key, value]) => {
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
+ }
+ if (value === null) {
+ return `${encodeURIComponent(key)}=`;
+ }
+ throw new Errors.OpencodeError(
+ `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`,
+ );
+ })
+ .join('&');
+ }
+
+ private getUserAgent(): string {
+ return `${this.constructor.name}/JS ${VERSION}`;
+ }
+
+ protected defaultIdempotencyKey(): string {
+ return `stainless-node-retry-${uuid4()}`;
+ }
+
+ protected makeStatusError(
+ status: number,
+ error: Object,
+ message: string | undefined,
+ headers: Headers,
+ ): Errors.APIError {
+ return Errors.APIError.generate(status, error, message, headers);
+ }
+
+ buildURL(
+ path: string,
+ query: Record<string, unknown> | null | undefined,
+ defaultBaseURL?: string | undefined,
+ ): string {
+ const baseURL = (!this.#baseURLOverridden() && defaultBaseURL) || this.baseURL;
+ const url =
+ isAbsoluteURL(path) ?
+ new URL(path)
+ : new URL(baseURL + (baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path));
+
+ const defaultQuery = this.defaultQuery();
+ if (!isEmptyObj(defaultQuery)) {
+ query = { ...defaultQuery, ...query };
+ }
+
+ if (typeof query === 'object' && query && !Array.isArray(query)) {
+ url.search = this.stringifyQuery(query as Record<string, unknown>);
+ }
+
+ return url.toString();
+ }
+
+ /**
+ * Used as a callback for mutating the given `FinalRequestOptions` object.
+ */
+ protected async prepareOptions(options: FinalRequestOptions): Promise<void> {}
+
+ /**
+ * Used as a callback for mutating the given `RequestInit` object.
+ *
+ * This is useful for cases where you want to add certain headers based off of
+ * the request properties, e.g. `method` or `url`.
+ */
+ protected async prepareRequest(
+ request: RequestInit,
+ { url, options }: { url: string; options: FinalRequestOptions },
+ ): Promise<void> {}
+
+ get<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('get', path, opts);
+ }
+
+ post<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('post', path, opts);
+ }
+
+ patch<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('patch', path, opts);
+ }
+
+ put<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('put', path, opts);
+ }
+
+ delete<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('delete', path, opts);
+ }
+
+ private methodRequest<Rsp>(
+ method: HTTPMethod,
+ path: string,
+ opts?: PromiseOrValue<RequestOptions>,
+ ): APIPromise<Rsp> {
+ return this.request(
+ Promise.resolve(opts).then((opts) => {
+ return { method, path, ...opts };
+ }),
+ );
+ }
+
+ request<Rsp>(
+ options: PromiseOrValue<FinalRequestOptions>,
+ remainingRetries: number | null = null,
+ ): APIPromise<Rsp> {
+ return new APIPromise(this, this.makeRequest(options, remainingRetries, undefined));
+ }
+
+ private async makeRequest(
+ optionsInput: PromiseOrValue<FinalRequestOptions>,
+ retriesRemaining: number | null,
+ retryOfRequestLogID: string | undefined,
+ ): Promise<APIResponseProps> {
+ const options = await optionsInput;
+ const maxRetries = options.maxRetries ?? this.maxRetries;
+ if (retriesRemaining == null) {
+ retriesRemaining = maxRetries;
+ }
+
+ await this.prepareOptions(options);
+
+ const { req, url, timeout } = await this.buildRequest(options, {
+ retryCount: maxRetries - retriesRemaining,
+ });
+
+ await this.prepareRequest(req, { url, options });
+
+ /** Not an API request ID, just for correlating local log entries. */
+ const requestLogID = 'log_' + ((Math.random() * (1 << 24)) | 0).toString(16).padStart(6, '0');
+ const retryLogStr = retryOfRequestLogID === undefined ? '' : `, retryOf: ${retryOfRequestLogID}`;
+ const startTime = Date.now();
+
+ loggerFor(this).debug(
+ `[${requestLogID}] sending request`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ method: options.method,
+ url,
+ options,
+ headers: req.headers,
+ }),
+ );
+
+ if (options.signal?.aborted) {
+ throw new Errors.APIUserAbortError();
+ }
+
+ const controller = new AbortController();
+ const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError);
+ const headersTime = Date.now();
+
+ if (response instanceof Error) {
+ const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
+ if (options.signal?.aborted) {
+ throw new Errors.APIUserAbortError();
+ }
+ // detect native connection timeout errors
+ // deno throws "TypeError: error sending request for url (https://example/): client error (Connect): tcp connect error: Operation timed out (os error 60): Operation timed out (os error 60)"
+ // undici throws "TypeError: fetch failed" with cause "ConnectTimeoutError: Connect Timeout Error (attempted address: example:443, timeout: 1ms)"
+ // others do not provide enough information to distinguish timeouts from other connection errors
+ const isTimeout =
+ isAbortError(response) ||
+ /timed? ?out/i.test(String(response) + ('cause' in response ? String(response.cause) : ''));
+ if (retriesRemaining) {
+ loggerFor(this).info(
+ `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - ${retryMessage}`,
+ );
+ loggerFor(this).debug(
+ `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (${retryMessage})`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url,
+ durationMs: headersTime - startTime,
+ message: response.message,
+ }),
+ );
+ return this.retryRequest(options, retriesRemaining, retryOfRequestLogID ?? requestLogID);
+ }
+ loggerFor(this).info(
+ `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - error; no more retries left`,
+ );
+ loggerFor(this).debug(
+ `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (error; no more retries left)`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url,
+ durationMs: headersTime - startTime,
+ message: response.message,
+ }),
+ );
+ if (isTimeout) {
+ throw new Errors.APIConnectionTimeoutError();
+ }
+ throw new Errors.APIConnectionError({ cause: response });
+ }
+
+ const responseInfo = `[${requestLogID}${retryLogStr}] ${req.method} ${url} ${
+ response.ok ? 'succeeded' : 'failed'
+ } with status ${response.status} in ${headersTime - startTime}ms`;
+
+ if (!response.ok) {
+ const shouldRetry = await this.shouldRetry(response);
+ if (retriesRemaining && shouldRetry) {
+ const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
+
+ // We don't need the body of this response.
+ await Shims.CancelReadableStream(response.body);
+ loggerFor(this).info(`${responseInfo} - ${retryMessage}`);
+ loggerFor(this).debug(
+ `[${requestLogID}] response error (${retryMessage})`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url: response.url,
+ status: response.status,
+ headers: response.headers,
+ durationMs: headersTime - startTime,
+ }),
+ );
+ return this.retryRequest(
+ options,
+ retriesRemaining,
+ retryOfRequestLogID ?? requestLogID,
+ response.headers,
+ );
+ }
+
+ const retryMessage = shouldRetry ? `error; no more retries left` : `error; not retryable`;
+
+ loggerFor(this).info(`${responseInfo} - ${retryMessage}`);
+
+ const errText = await response.text().catch((err: any) => castToError(err).message);
+ const errJSON = safeJSON(errText);
+ const errMessage = errJSON ? undefined : errText;
+
+ loggerFor(this).debug(
+ `[${requestLogID}] response error (${retryMessage})`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url: response.url,
+ status: response.status,
+ headers: response.headers,
+ message: errMessage,
+ durationMs: Date.now() - startTime,
+ }),
+ );
+
+ const err = this.makeStatusError(response.status, errJSON, errMessage, response.headers);
+ throw err;
+ }
+
+ loggerFor(this).info(responseInfo);
+ loggerFor(this).debug(
+ `[${requestLogID}] response start`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url: response.url,
+ status: response.status,
+ headers: response.headers,
+ durationMs: headersTime - startTime,
+ }),
+ );
+
+ return { response, options, controller, requestLogID, retryOfRequestLogID, startTime };
+ }
+
+ async fetchWithTimeout(
+ url: RequestInfo,
+ init: RequestInit | undefined,
+ ms: number,
+ controller: AbortController,
+ ): Promise<Response> {
+ const { signal, method, ...options } = init || {};
+ if (signal) signal.addEventListener('abort', () => controller.abort());
+
+ const timeout = setTimeout(() => controller.abort(), ms);
+
+ const isReadableBody =
+ ((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) ||
+ (typeof options.body === 'object' && options.body !== null && Symbol.asyncIterator in options.body);
+
+ const fetchOptions: RequestInit = {
+ signal: controller.signal as any,
+ ...(isReadableBody ? { duplex: 'half' } : {}),
+ method: 'GET',
+ ...options,
+ };
+ if (method) {
+ // Custom methods like 'patch' need to be uppercased
+ // See https://github.com/nodejs/undici/issues/2294
+ fetchOptions.method = method.toUpperCase();
+ }
+
+ try {
+ // use undefined this binding; fetch errors if bound to something else in browser/cloudflare
+ return await this.fetch.call(undefined, url, fetchOptions);
+ } finally {
+ clearTimeout(timeout);
+ }
+ }
+
+ private async shouldRetry(response: Response): Promise<boolean> {
+ // Note this is not a standard header.
+ const shouldRetryHeader = response.headers.get('x-should-retry');
+
+ // If the server explicitly says whether or not to retry, obey.
+ if (shouldRetryHeader === 'true') return true;
+ if (shouldRetryHeader === 'false') return false;
+
+ // Retry on request timeouts.
+ if (response.status === 408) return true;
+
+ // Retry on lock timeouts.
+ if (response.status === 409) return true;
+
+ // Retry on rate limits.
+ if (response.status === 429) return true;
+
+ // Retry internal errors.
+ if (response.status >= 500) return true;
+
+ return false;
+ }
+
+ private async retryRequest(
+ options: FinalRequestOptions,
+ retriesRemaining: number,
+ requestLogID: string,
+ responseHeaders?: Headers | undefined,
+ ): Promise<APIResponseProps> {
+ let timeoutMillis: number | undefined;
+
+ // Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it.
+ const retryAfterMillisHeader = responseHeaders?.get('retry-after-ms');
+ if (retryAfterMillisHeader) {
+ const timeoutMs = parseFloat(retryAfterMillisHeader);
+ if (!Number.isNaN(timeoutMs)) {
+ timeoutMillis = timeoutMs;
+ }
+ }
+
+ // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
+ const retryAfterHeader = responseHeaders?.get('retry-after');
+ if (retryAfterHeader && !timeoutMillis) {
+ const timeoutSeconds = parseFloat(retryAfterHeader);
+ if (!Number.isNaN(timeoutSeconds)) {
+ timeoutMillis = timeoutSeconds * 1000;
+ } else {
+ timeoutMillis = Date.parse(retryAfterHeader) - Date.now();
+ }
+ }
+
+ // If the API asks us to wait a certain amount of time (and it's a reasonable amount),
+ // just do what it says, but otherwise calculate a default
+ if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) {
+ const maxRetries = options.maxRetries ?? this.maxRetries;
+ timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries);
+ }
+ await sleep(timeoutMillis);
+
+ return this.makeRequest(options, retriesRemaining - 1, requestLogID);
+ }
+
+ private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number {
+ const initialRetryDelay = 0.5;
+ const maxRetryDelay = 8.0;
+
+ const numRetries = maxRetries - retriesRemaining;
+
+ // Apply exponential backoff, but not more than the max.
+ const sleepSeconds = Math.min(initialRetryDelay * Math.pow(2, numRetries), maxRetryDelay);
+
+ // Apply some jitter, take up to at most 25 percent of the retry time.
+ const jitter = 1 - Math.random() * 0.25;
+
+ return sleepSeconds * jitter * 1000;
+ }
+
+ async buildRequest(
+ inputOptions: FinalRequestOptions,
+ { retryCount = 0 }: { retryCount?: number } = {},
+ ): Promise<{ req: FinalizedRequestInit; url: string; timeout: number }> {
+ const options = { ...inputOptions };
+ const { method, path, query, defaultBaseURL } = options;
+
+ const url = this.buildURL(path!, query as Record<string, unknown>, defaultBaseURL);
+ if ('timeout' in options) validatePositiveInteger('timeout', options.timeout);
+ options.timeout = options.timeout ?? this.timeout;
+ const { bodyHeaders, body } = this.buildBody({ options });
+ const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount });
+
+ const req: FinalizedRequestInit = {
+ method,
+ headers: reqHeaders,
+ ...(options.signal && { signal: options.signal }),
+ ...((globalThis as any).ReadableStream &&
+ body instanceof (globalThis as any).ReadableStream && { duplex: 'half' }),
+ ...(body && { body }),
+ ...((this.fetchOptions as any) ?? {}),
+ ...((options.fetchOptions as any) ?? {}),
+ };
+
+ return { req, url, timeout: options.timeout };
+ }
+
+ private async buildHeaders({
+ options,
+ method,
+ bodyHeaders,
+ retryCount,
+ }: {
+ options: FinalRequestOptions;
+ method: HTTPMethod;
+ bodyHeaders: HeadersLike;
+ retryCount: number;
+ }): Promise<Headers> {
+ let idempotencyHeaders: HeadersLike = {};
+ if (this.idempotencyHeader && method !== 'get') {
+ if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey();
+ idempotencyHeaders[this.idempotencyHeader] = options.idempotencyKey;
+ }
+
+ const headers = buildHeaders([
+ idempotencyHeaders,
+ {
+ Accept: 'application/json',
+ 'User-Agent': this.getUserAgent(),
+ 'X-Stainless-Retry-Count': String(retryCount),
+ ...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
+ ...getPlatformHeaders(),
+ },
+ this._options.defaultHeaders,
+ bodyHeaders,
+ options.headers,
+ ]);
+
+ this.validateHeaders(headers);
+
+ return headers.values;
+ }
+
+ private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }): {
+ bodyHeaders: HeadersLike;
+ body: BodyInit | undefined;
+ } {
+ if (!body) {
+ return { bodyHeaders: undefined, body: undefined };
+ }
+ const headers = buildHeaders([rawHeaders]);
+ if (
+ // Pass raw type verbatim
+ ArrayBuffer.isView(body) ||
+ body instanceof ArrayBuffer ||
+ body instanceof DataView ||
+ (typeof body === 'string' &&
+ // Preserve legacy string encoding behavior for now
+ headers.values.has('content-type')) ||
+ // `Blob` is superset of `File`
+ body instanceof Blob ||
+ // `FormData` -> `multipart/form-data`
+ body instanceof FormData ||
+ // `URLSearchParams` -> `application/x-www-form-urlencoded`
+ body instanceof URLSearchParams ||
+ // Send chunked stream (each chunk has own `length`)
+ ((globalThis as any).ReadableStream && body instanceof (globalThis as any).ReadableStream)
+ ) {
+ return { bodyHeaders: undefined, body: body as BodyInit };
+ } else if (
+ typeof body === 'object' &&
+ (Symbol.asyncIterator in body ||
+ (Symbol.iterator in body && 'next' in body && typeof body.next === 'function'))
+ ) {
+ return { bodyHeaders: undefined, body: Shims.ReadableStreamFrom(body as AsyncIterable<Uint8Array>) };
+ } else {
+ return this.#encoder({ body, headers });
+ }
+ }
+
+ static Opencode = this;
+ static DEFAULT_TIMEOUT = 60000; // 1 minute
+
+ static OpencodeError = Errors.OpencodeError;
+ static APIError = Errors.APIError;
+ static APIConnectionError = Errors.APIConnectionError;
+ static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError;
+ static APIUserAbortError = Errors.APIUserAbortError;
+ static NotFoundError = Errors.NotFoundError;
+ static ConflictError = Errors.ConflictError;
+ static RateLimitError = Errors.RateLimitError;
+ static BadRequestError = Errors.BadRequestError;
+ static AuthenticationError = Errors.AuthenticationError;
+ static InternalServerError = Errors.InternalServerError;
+ static PermissionDeniedError = Errors.PermissionDeniedError;
+ static UnprocessableEntityError = Errors.UnprocessableEntityError;
+
+ static toFile = Uploads.toFile;
+
+ event: API.Event = new API.Event(this);
+ app: API.AppResource = new API.AppResource(this);
+ find: API.Find = new API.Find(this);
+ file: API.FileResource = new API.FileResource(this);
+ config: API.ConfigResource = new API.ConfigResource(this);
+ session: API.SessionResource = new API.SessionResource(this);
+ tui: API.Tui = new API.Tui(this);
+}
+Opencode.Event = Event;
+Opencode.AppResource = AppResource;
+Opencode.Find = Find;
+Opencode.FileResource = FileResource;
+Opencode.ConfigResource = ConfigResource;
+Opencode.SessionResource = SessionResource;
+Opencode.Tui = Tui;
+export declare namespace Opencode {
+ export type RequestOptions = Opts.RequestOptions;
+
+ export { Event as Event, type EventListResponse as EventListResponse };
+
+ export {
+ AppResource as AppResource,
+ type App as App,
+ type Mode as Mode,
+ type Model as Model,
+ type Provider as Provider,
+ type AppInitResponse as AppInitResponse,
+ type AppLogResponse as AppLogResponse,
+ type AppModesResponse as AppModesResponse,
+ type AppProvidersResponse as AppProvidersResponse,
+ type AppLogParams as AppLogParams,
+ };
+
+ export {
+ Find as Find,
+ type Match as Match,
+ type Symbol as Symbol,
+ type FindFilesResponse as FindFilesResponse,
+ type FindSymbolsResponse as FindSymbolsResponse,
+ type FindTextResponse as FindTextResponse,
+ type FindFilesParams as FindFilesParams,
+ type FindSymbolsParams as FindSymbolsParams,
+ type FindTextParams as FindTextParams,
+ };
+
+ export {
+ FileResource as FileResource,
+ type File as File,
+ type FileReadResponse as FileReadResponse,
+ type FileStatusResponse as FileStatusResponse,
+ type FileReadParams as FileReadParams,
+ };
+
+ export {
+ ConfigResource as ConfigResource,
+ type Config as Config,
+ type KeybindsConfig as KeybindsConfig,
+ type McpLocalConfig as McpLocalConfig,
+ type McpRemoteConfig as McpRemoteConfig,
+ type ModeConfig as ModeConfig,
+ };
+
+ export {
+ SessionResource as SessionResource,
+ type AssistantMessage as AssistantMessage,
+ type FilePart as FilePart,
+ type FilePartInput as FilePartInput,
+ type FilePartSource as FilePartSource,
+ type FilePartSourceText as FilePartSourceText,
+ type FileSource as FileSource,
+ type Message as Message,
+ type Part as Part,
+ type Session as Session,
+ type SnapshotPart as SnapshotPart,
+ type StepFinishPart as StepFinishPart,
+ type StepStartPart as StepStartPart,
+ type SymbolSource as SymbolSource,
+ type TextPart as TextPart,
+ type TextPartInput as TextPartInput,
+ type ToolPart as ToolPart,
+ type ToolStateCompleted as ToolStateCompleted,
+ type ToolStateError as ToolStateError,
+ type ToolStatePending as ToolStatePending,
+ type ToolStateRunning as ToolStateRunning,
+ type UserMessage as UserMessage,
+ type SessionListResponse as SessionListResponse,
+ type SessionDeleteResponse as SessionDeleteResponse,
+ type SessionAbortResponse as SessionAbortResponse,
+ type SessionInitResponse as SessionInitResponse,
+ type SessionMessagesResponse as SessionMessagesResponse,
+ type SessionSummarizeResponse as SessionSummarizeResponse,
+ type SessionChatParams as SessionChatParams,
+ type SessionInitParams as SessionInitParams,
+ type SessionSummarizeParams as SessionSummarizeParams,
+ };
+
+ export {
+ Tui as Tui,
+ type TuiAppendPromptResponse as TuiAppendPromptResponse,
+ type TuiOpenHelpResponse as TuiOpenHelpResponse,
+ type TuiAppendPromptParams as TuiAppendPromptParams,
+ };
+
+ export type MessageAbortedError = API.MessageAbortedError;
+ export type ProviderAuthError = API.ProviderAuthError;
+ export type UnknownError = API.UnknownError;
+}