feat(sdk): add HttpApi clients and embedded host (#33445)
This commit is contained in:
parent
c45d1db9a0
commit
cdd67cf30f
59 changed files with 4629 additions and 316 deletions
349
packages/client/src/generated/client.ts
Normal file
349
packages/client/src/generated/client.ts
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
import type {
|
||||
SessionsListInput,
|
||||
SessionsListOutput,
|
||||
SessionsCreateInput,
|
||||
SessionsCreateOutput,
|
||||
SessionsGetInput,
|
||||
SessionsGetOutput,
|
||||
SessionsSwitchAgentInput,
|
||||
SessionsSwitchAgentOutput,
|
||||
SessionsSwitchModelInput,
|
||||
SessionsSwitchModelOutput,
|
||||
SessionsPromptInput,
|
||||
SessionsPromptOutput,
|
||||
SessionsCompactInput,
|
||||
SessionsCompactOutput,
|
||||
SessionsWaitInput,
|
||||
SessionsWaitOutput,
|
||||
SessionsStageInput,
|
||||
SessionsStageOutput,
|
||||
SessionsClearInput,
|
||||
SessionsClearOutput,
|
||||
SessionsCommitInput,
|
||||
SessionsCommitOutput,
|
||||
SessionsContextInput,
|
||||
SessionsContextOutput,
|
||||
} from "./types"
|
||||
import { ClientError } from "./client-error"
|
||||
|
||||
export interface ClientOptions {
|
||||
readonly baseUrl: string
|
||||
readonly fetch?: typeof globalThis.fetch
|
||||
readonly headers?: HeadersInit
|
||||
}
|
||||
|
||||
export interface RequestOptions {
|
||||
readonly signal?: AbortSignal
|
||||
readonly headers?: HeadersInit
|
||||
}
|
||||
|
||||
interface RequestDescriptor {
|
||||
readonly method: string
|
||||
readonly path: string
|
||||
readonly query?: Record<string, unknown>
|
||||
readonly headers?: Record<string, unknown>
|
||||
readonly body?: unknown
|
||||
readonly successStatus: number
|
||||
readonly declaredStatuses: ReadonlyArray<number>
|
||||
readonly empty: boolean
|
||||
}
|
||||
|
||||
export function make(options: ClientOptions) {
|
||||
const fetch = options.fetch ?? globalThis.fetch
|
||||
|
||||
const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {
|
||||
const url = new URL(descriptor.path, options.baseUrl)
|
||||
for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value)
|
||||
const headers = new Headers(options.headers)
|
||||
for (const [key, value] of Object.entries(descriptor.headers ?? {})) {
|
||||
if (value !== undefined && value !== null) headers.set(key, String(value))
|
||||
}
|
||||
for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)
|
||||
if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")
|
||||
return {
|
||||
url,
|
||||
init: {
|
||||
method: descriptor.method,
|
||||
signal: requestOptions?.signal,
|
||||
headers,
|
||||
body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body),
|
||||
} satisfies RequestInit,
|
||||
}
|
||||
}
|
||||
|
||||
const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {
|
||||
try {
|
||||
const prepared = prepare(descriptor, requestOptions)
|
||||
return await fetch(prepared.url, prepared.init)
|
||||
} catch (cause) {
|
||||
throw new ClientError("Transport", { cause })
|
||||
}
|
||||
}
|
||||
|
||||
const responseError = async (response: Response, descriptor: RequestDescriptor): Promise<never> => {
|
||||
if (descriptor.declaredStatuses.includes(response.status)) throw await json(response)
|
||||
try {
|
||||
await response.body?.cancel()
|
||||
} catch {}
|
||||
throw new ClientError("UnexpectedStatus", { cause: { status: response.status } })
|
||||
}
|
||||
|
||||
const request = async <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise<A> => {
|
||||
const response = await execute(descriptor, requestOptions)
|
||||
if (response.status !== descriptor.successStatus) return responseError(response, descriptor)
|
||||
if (descriptor.empty) {
|
||||
try {
|
||||
await response.body?.cancel()
|
||||
} catch {}
|
||||
return undefined as A
|
||||
}
|
||||
return (await json(response)) as A
|
||||
}
|
||||
|
||||
const sse = <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable<A> => ({
|
||||
async *[Symbol.asyncIterator]() {
|
||||
const response = await execute(descriptor, requestOptions)
|
||||
if (response.status !== descriptor.successStatus) await responseError(response, descriptor)
|
||||
if (!isContentType(response, "text/event-stream")) {
|
||||
try {
|
||||
await response.body?.cancel()
|
||||
} catch {}
|
||||
throw new ClientError("UnsupportedContentType")
|
||||
}
|
||||
if (response.body === null) throw new ClientError("MalformedResponse")
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
try {
|
||||
while (true) {
|
||||
let next
|
||||
try {
|
||||
next = await reader.read()
|
||||
} catch (cause) {
|
||||
throw new ClientError("Transport", { cause })
|
||||
}
|
||||
buffer += decoder.decode(next.value, { stream: !next.done })
|
||||
if (buffer.length > 1_048_576) throw new ClientError("MalformedResponse")
|
||||
const trailingCarriageReturn = !next.done && buffer.endsWith("\r")
|
||||
if (trailingCarriageReturn) buffer = buffer.slice(0, -1)
|
||||
buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n")
|
||||
if (trailingCarriageReturn) buffer += "\r"
|
||||
if (next.done && buffer !== "") buffer += "\n\n"
|
||||
let boundary = buffer.indexOf("\n\n")
|
||||
while (boundary >= 0) {
|
||||
const block = buffer.slice(0, boundary)
|
||||
buffer = buffer.slice(boundary + 2)
|
||||
const data = block
|
||||
.split("\n")
|
||||
.flatMap((line) => (line.startsWith("data:") ? [line.slice(5).trimStart()] : []))
|
||||
.join("\n")
|
||||
if (data !== "") {
|
||||
try {
|
||||
yield JSON.parse(data) as A
|
||||
} catch (cause) {
|
||||
throw new ClientError("MalformedResponse", { cause })
|
||||
}
|
||||
}
|
||||
boundary = buffer.indexOf("\n\n")
|
||||
}
|
||||
if (next.done) return
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await reader.cancel()
|
||||
} catch {}
|
||||
reader.releaseLock()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
sessions: {
|
||||
list: (input?: SessionsListInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session`,
|
||||
query: {
|
||||
workspace: input?.workspace,
|
||||
limit: input?.limit,
|
||||
order: input?.order,
|
||||
search: input?.search,
|
||||
directory: input?.directory,
|
||||
project: input?.project,
|
||||
subpath: input?.subpath,
|
||||
cursor: input?.cursor,
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
create: (input?: SessionsCreateInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionsCreateOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session`,
|
||||
body: { id: input?.id, agent: input?.agent, model: input?.model, location: input?.location },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
get: (input: SessionsGetInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionsGetOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
switchAgent: (input: SessionsSwitchAgentInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsSwitchAgentOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
|
||||
body: { agent: input.agent },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
switchModel: (input: SessionsSwitchModelInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsSwitchModelOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
|
||||
body: { model: input.model },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
prompt: (input: SessionsPromptInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionsPromptOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`,
|
||||
body: { id: input.id, prompt: input.prompt, delivery: input.delivery, resume: input.resume },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [409, 404, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
compact: (input: SessionsCompactInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsCompactOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 503, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
wait: (input: SessionsWaitInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsWaitOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 503, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
stage: (input: SessionsStageInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionsStageOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
|
||||
body: { messageID: input.messageID, files: input.files },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 500, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
clear: (input: SessionsClearInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsClearOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 500, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
commit: (input: SessionsCommitInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionsCommitOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
context: (input: SessionsContextInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionsContextOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/context`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 500, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function appendQuery(params: URLSearchParams, key: string, value: unknown): void {
|
||||
if (value === undefined || value === null) return
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) appendQuery(params, key, item)
|
||||
return
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
for (const [child, item] of Object.entries(value)) appendQuery(params, `${key}[${child}]`, item)
|
||||
return
|
||||
}
|
||||
params.append(key, String(value))
|
||||
}
|
||||
|
||||
async function json(response: Response): Promise<unknown> {
|
||||
if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {
|
||||
try {
|
||||
await response.body?.cancel()
|
||||
} catch {}
|
||||
throw new ClientError("UnsupportedContentType")
|
||||
}
|
||||
let text: string
|
||||
try {
|
||||
text = await response.text()
|
||||
} catch (cause) {
|
||||
throw new ClientError("Transport", { cause })
|
||||
}
|
||||
if (text === "") throw new ClientError("MalformedResponse")
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (cause) {
|
||||
throw new ClientError("MalformedResponse", { cause })
|
||||
}
|
||||
}
|
||||
|
||||
function isContentType(response: Response, expected: string) {
|
||||
return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue