assistant-cloud
Version:
Cloud integration for assistant-ui
153 lines (141 loc) • 4.38 kB
text/typescript
import { buildCloudHeaders, type AssistantCloudAPI } from "./AssistantCloudAPI";
import type { AssistantCloudRunReportToolCall } from "./runTelemetry";
import { AssistantStream, PlainTextDecoder } from "assistant-stream";
import {
CloudResponseError,
readCloudRecord,
readCloudString,
} from "./cloudResponse";
type AssistantCloudRunsStreamBody = {
thread_id: string;
assistant_id: "system/thread_title";
messages: readonly unknown[]; // TODO type
};
// NOTE: Keep this payload shape aligned with the runtime validator in
// assistant-cloud: apps/api/src/endpoints/runs/create.ts
// (createRunSchema). New telemetry fields must be accepted there first.
export type AssistantCloudRunReport = {
thread_id: string;
status: "completed" | "incomplete" | "error";
outcome_type?:
| "rate_limited"
| "validation_failed"
| "provider_error"
| "server_error"
| "budget_denied"
| "persistence_error"
| "aborted"
| "timeout"
| "disconnected"
| "length"
| "content_filter";
message_id?: string;
user_message_id?: string;
first_token_ms?: number;
release?: string;
environment?: string;
tags?: string[];
provider?: string;
trace_id?: string;
root_span_id?: string;
error_code?: string;
error?: string;
total_steps?: number;
tool_calls?: AssistantCloudRunReportToolCall[];
steps?: {
input_tokens?: number;
output_tokens?: number;
reasoning_tokens?: number;
cached_input_tokens?: number;
tool_calls?: AssistantCloudRunReportToolCall[];
start_ms?: number;
end_ms?: number;
finish_reason?: string;
input?: string;
}[];
input_tokens?: number;
output_tokens?: number;
reasoning_tokens?: number;
cached_input_tokens?: number;
cost_usd?: number;
cost_details?: {
input?: number;
input_cached_tokens?: number;
output?: number;
total?: number;
};
model_id?: string;
provider_type?: string;
duration_ms?: number;
output_text?: string;
attributes?: Record<string, unknown>;
metadata?: Record<string, unknown>;
};
export class AssistantCloudRuns {
private cloud: AssistantCloudAPI;
constructor(cloud: AssistantCloudAPI) {
this.cloud = cloud;
}
public __internal_getAssistantOptions(assistantId: string) {
return {
api: `${this.cloud._baseUrl}/v1/runs/stream`,
protocol: "ui-message-stream" as const,
headers: (): Promise<{ Accept: string; "Aui-Sdk": string }> =>
buildCloudHeaders(this.cloud, { Accept: "text/plain" }),
body: async (options?: { threadId?: string }) => {
const threadId = options?.threadId;
if (threadId === undefined) {
throw new Error(
"Assistant Cloud runs need a thread; the thread list adapter has not assigned a remote id to this thread yet.",
);
}
return {
assistant_id: assistantId,
response_format: "vercel-ai-data-stream/v1",
thread_id: threadId,
};
},
};
}
public async stream(
body: AssistantCloudRunsStreamBody,
): Promise<AssistantStream> {
const response = await this.cloud.makeRawRequest("/runs/stream", {
method: "POST",
headers: {
Accept: "text/plain",
},
body,
});
if (!response.body) {
throw new CloudResponseError(
'Invalid Assistant Cloud response for "run stream": expected a response body',
);
}
const receivedContentType = response.headers.get("content-type");
const contentType = receivedContentType
?.split(";", 1)[0]
?.trim()
.toLowerCase();
if (contentType !== "text/plain") {
await response.body.cancel().catch(() => undefined);
throw new CloudResponseError(
`Invalid Assistant Cloud response for "run stream": expected a "text/plain" content type, received ${
receivedContentType
? `"${receivedContentType}"`
: "no Content-Type header"
}`,
);
}
return AssistantStream.fromResponse(response, new PlainTextDecoder());
}
public async report(
body: AssistantCloudRunReport,
): Promise<{ run_id: string }> {
const response = readCloudRecord(
await this.cloud.makeRequest("/runs", { method: "POST", body }),
"run report response",
);
return { run_id: readCloudString(response.run_id, "run_id") };
}
}