add sse streaming to sdk

This commit is contained in:
Dax Raad
2025-08-22 18:30:25 -04:00
parent 6e626afdcb
commit 0f1697b2ab
18 changed files with 497 additions and 172 deletions

View File

@@ -26,7 +26,7 @@
}, },
"cloud/core": { "cloud/core": {
"name": "@opencode/cloud-core", "name": "@opencode/cloud-core",
"version": "0.5.13", "version": "0.5.15",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"drizzle-orm": "0.41.0", "drizzle-orm": "0.41.0",
@@ -40,7 +40,7 @@
}, },
"cloud/function": { "cloud/function": {
"name": "@opencode/cloud-function", "name": "@opencode/cloud-function",
"version": "0.5.13", "version": "0.5.15",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@@ -60,7 +60,7 @@
}, },
"cloud/web": { "cloud/web": {
"name": "@opencode/cloud-web", "name": "@opencode/cloud-web",
"version": "0.5.13", "version": "0.5.15",
"dependencies": { "dependencies": {
"@kobalte/core": "0.13.9", "@kobalte/core": "0.13.9",
"@openauthjs/solid": "0.0.0-20250322224806", "@openauthjs/solid": "0.0.0-20250322224806",
@@ -79,7 +79,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode/function", "name": "@opencode/function",
"version": "0.5.13", "version": "0.5.15",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
@@ -94,7 +94,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "0.5.13", "version": "0.5.15",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -144,7 +144,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "0.5.13", "version": "0.5.15",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
}, },
@@ -156,16 +156,25 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "0.5.13", "version": "0.5.15",
"dependencies": {
"@hey-api/openapi-ts": "0.80.1",
},
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.80.1", "@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
}, },
}, },
"packages/tmp": {
"name": "@opencode-ai/tmp",
"dependencies": {
"@opencode-ai/sdk": "workspace:",
},
},
"packages/web": { "packages/web": {
"name": "@opencode/web", "name": "@opencode/web",
"version": "0.5.13", "version": "0.5.15",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -678,6 +687,8 @@
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"], "@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
"@opencode-ai/tmp": ["@opencode-ai/tmp@workspace:packages/tmp"],
"@opencode/cloud-app": ["@opencode/cloud-app@workspace:cloud/app"], "@opencode/cloud-app": ["@opencode/cloud-app@workspace:cloud/app"],
"@opencode/cloud-core": ["@opencode/cloud-core@workspace:cloud/core"], "@opencode/cloud-core": ["@opencode/cloud-core@workspace:cloud/core"],

View File

@@ -1,4 +1,4 @@
configured_endpoints: 41 configured_endpoints: 41
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d5200eaa145f567a58daa78941ab1141dd63f5f0cfe1596d5c9ecf12d34fea35.yml openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-6d8e9dfd438cac63fc7d689ea29adfff81ff8880c2d8e1e10fc36f375a721594.yml
openapi_spec_hash: abeb66291dc158f2cdc90bf9945e283e openapi_spec_hash: 7ac6028dd5957c67a98d91e790863c80
config_hash: fb625e876313a9f8f31532348fa91f59 config_hash: fb625e876313a9f8f31532348fa91f59

View File

@@ -41,6 +41,7 @@ func (r *EventService) ListStreaming(ctx context.Context, opts ...option.Request
err error err error
) )
opts = append(r.Options[:], opts...) opts = append(r.Options[:], opts...)
opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...)
path := "event" path := "event"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...)
return ssestream.NewStream[EventListResponse](ssestream.NewDecoder(raw), err) return ssestream.NewStream[EventListResponse](ssestream.NewDecoder(raw), err)

View File

@@ -27,5 +27,8 @@
"typescript": "catalog:", "typescript": "catalog:",
"@hey-api/openapi-ts": "0.80.1", "@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:" "@tsconfig/node22": "catalog:"
},
"dependencies": {
"@hey-api/openapi-ts": "0.80.1"
} }
} }

View File

@@ -1,8 +1,8 @@
export * from "./gen/types.gen.js" export * from "./gen/types.gen.js"
export { type Config as OpencodeClientConfig, OpencodeClient } export { type Config as OpencodeClientConfig, OpencodeClient }
import { createClient } from "./gen/client/client.js" import { createClient } from "./gen/client/client.gen.js"
import { type Config } from "./gen/client/types.js" import { type Config } from "./gen/client/types.gen.js"
import { OpencodeClient } from "./gen/sdk.gen.js" import { OpencodeClient } from "./gen/sdk.gen.js"
export function createOpencodeClient(config?: Config) { export function createOpencodeClient(config?: Config) {

View File

@@ -1,4 +1,7 @@
import type { Client, Config, RequestOptions } from "./types.js" // This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from "../core/serverSentEvents.gen.js"
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js"
import { import {
buildUrl, buildUrl,
createConfig, createConfig,
@@ -7,7 +10,7 @@ import {
mergeConfigs, mergeConfigs,
mergeHeaders, mergeHeaders,
setAuthParams, setAuthParams,
} from "./utils.js" } from "./utils.gen.js"
type ReqInit = Omit<RequestInit, "body" | "headers"> & { type ReqInit = Omit<RequestInit, "body" | "headers"> & {
body?: any body?: any
@@ -24,14 +27,15 @@ export const createClient = (config: Config = {}): Client => {
return getConfig() return getConfig()
} }
const interceptors = createInterceptors<Request, Response, unknown, RequestOptions>() const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>()
const request: Client["request"] = async (options) => { const beforeRequest = async (options: RequestOptions) => {
const opts = { const opts = {
..._config, ..._config,
...options, ...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers), headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined,
} }
if (opts.security) { if (opts.security) {
@@ -46,18 +50,26 @@ export const createClient = (config: Config = {}): Client => {
} }
if (opts.body && opts.bodySerializer) { if (opts.body && opts.bodySerializer) {
opts.body = opts.bodySerializer(opts.body) opts.serializedBody = opts.bodySerializer(opts.body)
} }
// remove Content-Type header if body is empty to avoid sending invalid requests // remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.body === "") { if (opts.serializedBody === undefined || opts.serializedBody === "") {
opts.headers.delete("Content-Type") opts.headers.delete("Content-Type")
} }
const url = buildUrl(opts) const url = buildUrl(opts)
return { opts, url }
}
const request: Client["request"] = async (options) => {
// @ts-expect-error
const { opts, url } = await beforeRequest(options)
const requestInit: ReqInit = { const requestInit: ReqInit = {
redirect: "follow", redirect: "follow",
...opts, ...opts,
body: opts.serializedBody,
} }
let request = new Request(url, requestInit) let request = new Request(url, requestInit)
@@ -166,20 +178,35 @@ export const createClient = (config: Config = {}): Client => {
} }
} }
const makeMethod = (method: Required<Config>["method"]) => {
const fn = (options: RequestOptions) => request({ ...options, method })
fn.sse = async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options)
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record<string, string>,
method,
url,
})
}
return fn
}
return { return {
buildUrl, buildUrl,
connect: (options) => request({ ...options, method: "CONNECT" }), connect: makeMethod("CONNECT"),
delete: (options) => request({ ...options, method: "DELETE" }), delete: makeMethod("DELETE"),
get: (options) => request({ ...options, method: "GET" }), get: makeMethod("GET"),
getConfig, getConfig,
head: (options) => request({ ...options, method: "HEAD" }), head: makeMethod("HEAD"),
interceptors, interceptors,
options: (options) => request({ ...options, method: "OPTIONS" }), options: makeMethod("OPTIONS"),
patch: (options) => request({ ...options, method: "PATCH" }), patch: makeMethod("PATCH"),
post: (options) => request({ ...options, method: "POST" }), post: makeMethod("POST"),
put: (options) => request({ ...options, method: "PUT" }), put: makeMethod("PUT"),
request, request,
setConfig, setConfig,
trace: (options) => request({ ...options, method: "TRACE" }), trace: makeMethod("TRACE"),
} } as Client
} }

View File

@@ -1,8 +1,14 @@
export type { Auth } from "../core/auth.js" // This file is auto-generated by @hey-api/openapi-ts
export type { QuerySerializerOptions } from "../core/bodySerializer.js"
export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer } from "../core/bodySerializer.js" export type { Auth } from "../core/auth.gen.js"
export { buildClientParams } from "../core/params.js" export type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
export { createClient } from "./client.js" export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from "../core/bodySerializer.gen.js"
export { buildClientParams } from "../core/params.gen.js"
export { createClient } from "./client.gen.js"
export type { export type {
Client, Client,
ClientOptions, ClientOptions,
@@ -12,7 +18,8 @@ export type {
OptionsLegacyParser, OptionsLegacyParser,
RequestOptions, RequestOptions,
RequestResult, RequestResult,
ResolvedRequestOptions,
ResponseStyle, ResponseStyle,
TDataShape, TDataShape,
} from "./types.js" } from "./types.gen.js"
export { createConfig, mergeHeaders } from "./utils.js" export { createConfig, mergeHeaders } from "./utils.gen.js"

View File

@@ -1,6 +1,9 @@
import type { Auth } from "../core/auth.js" // This file is auto-generated by @hey-api/openapi-ts
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.js"
import type { Middleware } from "./utils.js" import type { Auth } from "../core/auth.gen.js"
import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen.js"
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen.js"
import type { Middleware } from "./utils.gen.js"
export type ResponseStyle = "data" | "fields" export type ResponseStyle = "data" | "fields"
@@ -49,13 +52,18 @@ export interface Config<T extends ClientOptions = ClientOptions>
} }
export interface RequestOptions< export interface RequestOptions<
TData = unknown,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
Url extends string = string, Url extends string = string,
> extends Config<{ > extends Config<{
responseStyle: TResponseStyle responseStyle: TResponseStyle
throwOnError: ThrowOnError throwOnError: ThrowOnError
}> { }>,
Pick<
ServerSentEventsOptions<TData>,
"onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay"
> {
/** /**
* Any body that you want to add to your request. * Any body that you want to add to your request.
* *
@@ -71,6 +79,14 @@ export interface RequestOptions<
url: Url url: Url
} }
export interface ResolvedRequestOptions<
TResponseStyle extends ResponseStyle = "fields",
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
serializedBody?: string
}
export type RequestResult< export type RequestResult<
TData = unknown, TData = unknown,
TError = unknown, TError = unknown,
@@ -112,23 +128,36 @@ export interface ClientOptions {
throwOnError?: boolean throwOnError?: boolean
} }
type MethodFn = < type MethodFnBase = <
TData = unknown, TData = unknown,
TError = unknown, TError = unknown,
ThrowOnError extends boolean = false, ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
>( >(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method">, options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle> ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
type MethodFnServerSentEvents = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
) => Promise<ServerSentEventsResult<TData, TError>>
type MethodFn = MethodFnBase & {
sse: MethodFnServerSentEvents
}
type RequestFn = < type RequestFn = <
TData = unknown, TData = unknown,
TError = unknown, TError = unknown,
ThrowOnError extends boolean = false, ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
>( >(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method"> & options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, "method">, Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle> ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
type BuildUrlFn = < type BuildUrlFn = <
@@ -143,7 +172,7 @@ type BuildUrlFn = <
) => string ) => string
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
interceptors: Middleware<Request, Response, unknown, RequestOptions> interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>
} }
/** /**
@@ -171,8 +200,10 @@ type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>
export type Options< export type Options<
TData extends TDataShape = TDataShape, TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
TResponse = unknown,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
> = OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> & Omit<TData, "url"> > = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
Omit<TData, "url">
export type OptionsLegacyParser< export type OptionsLegacyParser<
TData = unknown, TData = unknown,
@@ -180,12 +211,12 @@ export type OptionsLegacyParser<
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
> = TData extends { body?: any } > = TData extends { body?: any }
? TData extends { headers?: any } ? TData extends { headers?: any }
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData ? OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> & : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "url"> &
TData & TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers"> Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "headers">
: TData extends { headers?: any } : TData extends { headers?: any }
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "headers" | "url"> & ? OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "headers" | "url"> &
TData & TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body"> Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body">
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "url"> & TData

View File

@@ -1,84 +1,11 @@
import { getAuthToken } from "../core/auth.js" // This file is auto-generated by @hey-api/openapi-ts
import type { QuerySerializer, QuerySerializerOptions } from "../core/bodySerializer.js"
import { jsonBodySerializer } from "../core/bodySerializer.js"
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.js"
import type { Client, ClientOptions, Config, RequestOptions } from "./types.js"
interface PathSerializer { import { getAuthToken } from "../core/auth.gen.js"
path: Record<string, unknown> import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
url: string import { jsonBodySerializer } from "../core/bodySerializer.gen.js"
} import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen.js"
import { getUrl } from "../core/utils.gen.js"
const PATH_PARAM_RE = /\{[^{}]+\}/g import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js"
type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"
type MatrixStyle = "label" | "matrix" | "simple"
type ArraySeparatorStyle = ArrayStyle | MatrixStyle
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url
const matches = _url.match(PATH_PARAM_RE)
if (matches) {
for (const match of matches) {
let explode = false
let name = match.substring(1, match.length - 1)
let style: ArraySeparatorStyle = "simple"
if (name.endsWith("*")) {
explode = true
name = name.substring(0, name.length - 1)
}
if (name.startsWith(".")) {
name = name.substring(1)
style = "label"
} else if (name.startsWith(";")) {
name = name.substring(1)
style = "matrix"
}
const value = path[name]
if (value === undefined || value === null) {
continue
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }))
continue
}
if (typeof value === "object") {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
)
continue
}
if (style === "matrix") {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
)
continue
}
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
url = url.replace(match, replaceValue)
}
}
return url
}
export const createQuerySerializer = <T = unknown>({ allowReserved, array, object }: QuerySerializerOptions = {}) => { export const createQuerySerializer = <T = unknown>({ allowReserved, array, object }: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => { const querySerializer = (queryParams: T) => {
@@ -161,6 +88,21 @@ export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"
return return
} }
const checkForExistence = (
options: Pick<RequestOptions, "auth" | "query"> & {
headers: Headers
},
name?: string,
): boolean => {
if (!name) {
return false
}
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) {
return true
}
return false
}
export const setAuthParams = async ({ export const setAuthParams = async ({
security, security,
...options ...options
@@ -169,6 +111,10 @@ export const setAuthParams = async ({
headers: Headers headers: Headers
}) => { }) => {
for (const auth of security) { for (const auth of security) {
if (checkForExistence(options, auth.name)) {
continue
}
const token = await getAuthToken(auth, options.auth) const token = await getAuthToken(auth, options.auth)
if (!token) { if (!token) {
@@ -192,13 +138,11 @@ export const setAuthParams = async ({
options.headers.set(name, token) options.headers.set(name, token)
break break
} }
return
} }
} }
export const buildUrl: Client["buildUrl"] = (options) => { export const buildUrl: Client["buildUrl"] = (options) =>
const url = getUrl({ getUrl({
baseUrl: options.baseUrl as string, baseUrl: options.baseUrl as string,
path: options.path, path: options.path,
query: options.query, query: options.query,
@@ -208,36 +152,6 @@ export const buildUrl: Client["buildUrl"] = (options) => {
: createQuerySerializer(options.querySerializer), : createQuerySerializer(options.querySerializer),
url: options.url, url: options.url,
}) })
return url
}
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string
path?: Record<string, unknown>
query?: Record<string, unknown>
querySerializer: QuerySerializer
url: string
}) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
let url = (baseUrl ?? "") + pathUrl
if (path) {
url = defaultPathSerializer({ path, url })
}
let search = query ? querySerializer(query) : ""
if (search.startsWith("?")) {
search = search.substring(1)
}
if (search) {
url += `?${search}`
}
return url
}
export const mergeConfigs = (a: Config, b: Config): Config => { export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b } const config = { ...a, ...b }

View File

@@ -1,3 +1,5 @@
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined export type AuthToken = string | undefined
export interface Auth { export interface Auth {

View File

@@ -1,4 +1,6 @@
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.js" // This file is auto-generated by @hey-api/openapi-ts
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen.js"
export type QuerySerializer = (query: Record<string, unknown>) => string export type QuerySerializer = (query: Record<string, unknown>) => string
@@ -13,6 +15,8 @@ export interface QuerySerializerOptions {
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
if (typeof value === "string" || value instanceof Blob) { if (typeof value === "string" || value instanceof Blob) {
data.append(key, value) data.append(key, value)
} else if (value instanceof Date) {
data.append(key, value.toISOString())
} else { } else {
data.append(key, JSON.stringify(value)) data.append(key, JSON.stringify(value))
} }

View File

@@ -1,3 +1,5 @@
// This file is auto-generated by @hey-api/openapi-ts
type Slot = "body" | "headers" | "path" | "query" type Slot = "body" | "headers" | "path" | "query"
export type Field = export type Field =

View File

@@ -1,3 +1,5 @@
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {} interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
interface SerializePrimitiveOptions { interface SerializePrimitiveOptions {

View File

@@ -0,0 +1,210 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from "./types.gen.js"
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param error The error that occurred.
*/
onSseError?: (error: unknown) => void
/**
* Callback invoked when an event is streamed from the server.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param event Event streamed from the server.
* @returns Nothing (void).
*/
onSseEvent?: (event: StreamEvent<TData>) => void
/**
* Default retry delay in milliseconds.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 3000
*/
sseDefaultRetryDelay?: number
/**
* Maximum number of retry attempts before giving up.
*/
sseMaxRetryAttempts?: number
/**
* Maximum retry delay in milliseconds.
*
* Applies only when exponential backoff is used.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 30000
*/
sseMaxRetryDelay?: number
/**
* Optional sleep function for retry backoff.
*
* Defaults to using `setTimeout`.
*/
sseSleepFn?: (ms: number) => Promise<void>
url: string
}
export interface StreamEvent<TData = unknown> {
data: TData
event?: string
id?: string
retry?: number
}
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>
}
export const createSseClient = <TData = unknown>({
onSseError,
onSseEvent,
responseTransformer,
responseValidator,
sseDefaultRetryDelay,
sseMaxRetryAttempts,
sseMaxRetryDelay,
sseSleepFn,
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
let lastEventId: string | undefined
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)))
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000
let attempt = 0
const signal = options.signal ?? new AbortController().signal
while (true) {
if (signal.aborted) break
attempt++
const headers =
options.headers instanceof Headers
? options.headers
: new Headers(options.headers as Record<string, string> | undefined)
if (lastEventId !== undefined) {
headers.set("Last-Event-ID", lastEventId)
}
try {
const response = await fetch(url, { ...options, headers, signal })
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`)
if (!response.body) throw new Error("No body in SSE response")
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
let buffer = ""
const abortHandler = () => {
try {
reader.cancel()
} catch {
// noop
}
}
signal.addEventListener("abort", abortHandler)
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += value
const chunks = buffer.split("\n\n")
buffer = chunks.pop() ?? ""
for (const chunk of chunks) {
const lines = chunk.split("\n")
const dataLines: Array<string> = []
let eventName: string | undefined
for (const line of lines) {
if (line.startsWith("data:")) {
dataLines.push(line.replace(/^data:\s*/, ""))
} else if (line.startsWith("event:")) {
eventName = line.replace(/^event:\s*/, "")
} else if (line.startsWith("id:")) {
lastEventId = line.replace(/^id:\s*/, "")
} else if (line.startsWith("retry:")) {
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10)
if (!Number.isNaN(parsed)) {
retryDelay = parsed
}
}
}
let data: unknown
let parsedJson = false
if (dataLines.length) {
const rawData = dataLines.join("\n")
try {
data = JSON.parse(rawData)
parsedJson = true
} catch {
data = rawData
}
}
if (parsedJson) {
if (responseValidator) {
await responseValidator(data)
}
if (responseTransformer) {
data = await responseTransformer(data)
}
}
onSseEvent?.({
data,
event: eventName,
id: lastEventId,
retry: retryDelay,
})
if (dataLines.length) {
yield data as any
}
}
}
} finally {
signal.removeEventListener("abort", abortHandler)
reader.releaseLock()
}
break // exit loop on normal completion
} catch (error) {
// connection failed or aborted; retry after delay
onSseError?.(error)
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
break // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000)
await sleep(backoff)
}
}
}
const stream = createStream()
return { stream }
}

View File

@@ -1,5 +1,7 @@
import type { Auth, AuthToken } from "./auth.js" // This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.js"
import type { Auth, AuthToken } from "./auth.gen.js"
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js"
export interface Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> { export interface Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> {
/** /**

View File

@@ -0,0 +1,109 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { QuerySerializer } from "./bodySerializer.gen.js"
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from "./pathSerializer.gen.js"
export interface PathSerializer {
path: Record<string, unknown>
url: string
}
export const PATH_PARAM_RE = /\{[^{}]+\}/g
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url
const matches = _url.match(PATH_PARAM_RE)
if (matches) {
for (const match of matches) {
let explode = false
let name = match.substring(1, match.length - 1)
let style: ArraySeparatorStyle = "simple"
if (name.endsWith("*")) {
explode = true
name = name.substring(0, name.length - 1)
}
if (name.startsWith(".")) {
name = name.substring(1)
style = "label"
} else if (name.startsWith(";")) {
name = name.substring(1)
style = "matrix"
}
const value = path[name]
if (value === undefined || value === null) {
continue
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }))
continue
}
if (typeof value === "object") {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
)
continue
}
if (style === "matrix") {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
)
continue
}
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
url = url.replace(match, replaceValue)
}
}
return url
}
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string
path?: Record<string, unknown>
query?: Record<string, unknown>
querySerializer: QuerySerializer
url: string
}) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
let url = (baseUrl ?? "") + pathUrl
if (path) {
url = defaultPathSerializer({ path, url })
}
let search = query ? querySerializer(query) : ""
if (search.startsWith("?")) {
search = search.substring(1)
}
if (search) {
url += `?${search}`
}
return url
}

View File

@@ -123,7 +123,7 @@ class Event extends _HeyApiClient {
* Get events * Get events
*/ */
public subscribe<ThrowOnError extends boolean = false>(options?: Options<EventSubscribeData, ThrowOnError>) { public subscribe<ThrowOnError extends boolean = false>(options?: Options<EventSubscribeData, ThrowOnError>) {
return (options?.client ?? this._client).get<EventSubscribeResponses, unknown, ThrowOnError>({ return (options?.client ?? this._client).get.sse<EventSubscribeResponses, unknown, ThrowOnError>({
url: "/event", url: "/event",
...options, ...options,
}) })

View File

@@ -17,7 +17,7 @@ export async function createOpencodeServer(config?: ServerConfig) {
config ?? {}, config ?? {},
) )
const proc = spawn(`opencode`, [`servel`, `--hostname=${config.hostname}`, `--port=${config.port}`], { const proc = spawn(`opencode`, [`serve`, `--hostname=${config.hostname}`, `--port=${config.port}`], {
signal: config.signal, signal: config.signal,
}) })