The Coline App SDK: apps, tools, file types and surfaces that run inside Coline workspaces, and public websites with Coline Sites. TypeScript, with an in-memory test workspace. MIT.
| 5 | prompt: string; |
| 6 | /** Reusing a key with a different request is rejected. */ |
| 7 | idempotencyKey: string; |
| 8 | driveId: string; |
| 9 | referenceFileIds?: string[]; |
| 10 | maskFileId?: string; |
| 11 | count?: number; |
| 12 | size?: `${number}x${number}`; |
| 13 | aspectRatio?: `${number}:${number}`; |
| 14 | seed?: number; |
| 15 | quality?: "auto" | "low" | "medium" | "high"; |
| 16 | outputFormat?: "png" | "jpeg" | "webp"; |
| 17 | background?: "auto" | "opaque" | "transparent"; |
| 18 | /** Maximum workspace charge approved by the caller, in millionths of USD. */ |
| 19 | maxCostMicrousd: number; |
| 20 | providerOptions?: Record<string, Record<string, ColineAiJson>>; |
| 21 | partialImages?: 0 | 1 | 2 | 3; |
| 22 | } |
| 23 | export interface ColineAiModel { |
| 24 | id: string; |
| 25 | name: string; |
| 26 | kind: "image"; |
| 27 | supports: { |
| 28 | edits: boolean; |
| 29 | masks: boolean; |
| 30 | partialImages: boolean; |
| 31 | transparentBackground: boolean; |
| 32 | }; |
| 33 | /** Conservative charge per image. Configuration is controlled by Coline. */ |
| 34 | priceMicrousd: number; |
| 35 | } |
| 36 | export interface ColineAiImageAsset { |
| 37 | fileId: string; |
| 38 | mimeType: string; |
| 39 | } |
| 40 | export interface ColineAiJob { |
| 41 | id: string; |
| 42 | kind: ColineAiKind; |
| 43 | status: "queued" | "running" | "succeeded" | "failed" | "cancelled"; |
| 44 | model: string; |
| 45 | output?: ColineAiJson; |
| 46 | images: ColineAiImageAsset[]; |
| 47 | previews: ColineAiImageAsset[]; |
| 48 | error: { code: string; message: string } | null; |
| 49 | chargeMicrousd: number; |
| 50 | createdAt: string; |
| 51 | updatedAt: string; |
| 52 | } |
| 53 | export interface ColineAi { |
| 54 | models(): Promise<ColineAiModelInfo[]>; |
| 55 | run(request: ColineAiRequest): Promise<ColineAiJob>; |
| 56 | images: { generate(input: ColineImageOptions): Promise<ColineAiJob> }; |
| 57 | jobs: { |
| 58 | events(id: string, afterSequence?: number): Promise<ColineAiEvent[]>; |
| 59 | get(id: string): Promise<ColineAiJob>; |
| 60 | /** Summaries omit output; use get(id) for the completed result. */ |
| 61 | list(input?: { limit?: number }): Promise<ColineAiJob[]>; |
| 62 | /** Cancellation stops pending work; a submitted provider request may still incur cost. */ |
| 63 | cancel(id: string): Promise<ColineAiJob>; |
| 64 | }; |
| 65 | } |
| 66 |
| 67 | export type ColineAiKind = |
| 68 | | "language" |
| 69 | | "image" |
| 70 | | "embedding" |
| 71 | | "speech" |
| 72 | | "transcription" |
| 73 | | "reranking" |
| 74 | | "video"; |
| 75 | export type ColineAiJson = |
| 76 | | null |
| 77 | | boolean |
| 78 | | number |
| 79 | | string |
| 80 | | ColineAiJson[] |
| 81 | | { [key: string]: ColineAiJson }; |
| 82 | export interface ColineAiRequestBase { |
| 83 | model: string; |
| 84 | idempotencyKey: string; |
| 85 | maxCostMicrousd: number; |
| 86 | providerOptions?: Record<string, Record<string, ColineAiJson>>; |
| 87 | } |
| 88 | export interface ColineTextRequest extends ColineAiRequestBase { |
| 89 | prompt?: string; |
| 90 | system?: string; |
| 91 | messages?: Array<{ role: "system" | "user" | "assistant" | "tool"; content: ColineAiJson }>; |
| 92 | maxOutputTokens?: number; |
| 93 | temperature?: number; |
| 94 | topP?: number; |
| 95 | topK?: number; |
| 96 | presencePenalty?: number; |
| 97 | frequencyPenalty?: number; |
| 98 | stopSequences?: string[]; |
| 99 | seed?: number; |
| 100 | /** JSON Schema for structured output. */ |
| 101 | outputSchema?: Record<string, ColineAiJson>; |
| 102 | output?: |
| 103 | | { type: "json" } |
| 104 | | { type: "object"; schema: Record<string, ColineAiJson> } |
| 105 | | { type: "array"; element: Record<string, ColineAiJson> } |
| 106 | | { type: "choice"; options: string[] }; |
| 107 | toolChoice?: "auto" | "none" | "required" | { type: "tool"; toolName: string }; |
| 108 | webSearch?: boolean; |
| 109 | /** Destination for files emitted by a language model. Requires files.write. */ |
| 110 | outputDriveId?: string; |
| 111 | /** Declared app tool names. Coline executes them through the normal app guard. */ |
| 112 | tools?: string[]; |
| 113 | maxSteps?: number; |
| 114 | } |
| 115 | export type ColineAiRequest = |
| 116 | | { kind: "language"; input: ColineTextRequest } |
| 117 | | { kind: "embedding"; input: ColineAiRequestBase & { values: string[]; dimensions?: number } } |
| 118 | | { |
| 119 | kind: "speech"; |
| 120 | input: ColineAiRequestBase & { |
| 121 | text: string; |
| 122 | voice: string; |
| 123 | driveId: string; |
| 124 | outputFormat?: string; |
| 125 | instructions?: string; |
| 126 | speed?: number; |
| 127 | language?: string; |
| 128 | }; |
| 129 | } |
| 130 | | { kind: "transcription"; input: ColineAiRequestBase & { fileId: string } } |
| 131 | | { |
| 132 | kind: "reranking"; |
| 133 | input: ColineAiRequestBase & { query: string; documents: string[]; topN?: number }; |
| 134 | } |
| 135 | | { |
| 136 | kind: "video"; |
| 137 | input: ColineAiRequestBase & { |
| 138 | prompt: string; |
| 139 | driveId: string; |
| 140 | duration?: number; |
| 141 | fps?: number; |
| 142 | generateAudio?: boolean; |
| 143 | referenceFileId?: string; |
| 144 | frameImages?: Array<{ fileId: string; frameType: "first_frame" | "last_frame" }>; |
| 145 | inputReferenceFileIds?: string[]; |
| 146 | aspectRatio?: `${number}:${number}`; |
| 147 | resolution?: `${number}x${number}`; |
| 148 | seed?: number; |
| 149 | count?: number; |
| 150 | }; |
| 151 | }; |
| 152 | export interface ColineAiEvent { |
| 153 | sequence: number; |
| 154 | type: string; |
| 155 | data: ColineAiJson; |
| 156 | } |
| 157 | export interface ColineAiModelInfo { |
| 158 | id: string; |
| 159 | name: string; |
| 160 | kind: ColineAiKind; |
| 161 | priceMicrousd: number; |
| 162 | provider?: string; |
| 163 | supports: Record<string, boolean>; |
| 164 | } |
| 165 |
| 166 | /** Resume from a persisted sequence after reconnecting; aborting observation does not cancel the job. */ |
| 167 | export async function* watchAiJob( |
| 168 | ai: ColineAi, |
| 169 | id: string, |
| 170 | options: { signal?: AbortSignal; afterSequence?: number; intervalMs?: number } = {}, |
| 171 | ) { |
| 172 | let sequence = options.afterSequence ?? 0; |
| 173 | while (!options.signal?.aborted) { |
| 174 | const events = await ai.jobs.events(id, sequence); |
| 175 | for (const event of events) { |
| 176 | sequence = event.sequence; |
| 177 | if (options.signal?.aborted) return; |
| 178 | yield { type: "event" as const, event }; |
| 179 | } |
| 180 | const job = await ai.jobs.get(id); |
| 181 | if (options.signal?.aborted) return; |
| 182 | yield { type: "job" as const, job }; |
| 183 | if (["succeeded", "failed", "cancelled"].includes(job.status)) { |
| 184 | // Drain events written between the previous event read and terminal status. |
| 185 | let remaining = await ai.jobs.events(id, sequence); |
| 186 | while (remaining.length) { |
| 187 | for (const event of remaining) { |
| 188 | sequence = event.sequence; |
| 189 | if (options.signal?.aborted) return; |
| 190 | yield { type: "event" as const, event }; |
| 191 | } |
| 192 | remaining = await ai.jobs.events(id, sequence); |
| 193 | } |
| 194 | return; |
| 195 | } |
| 196 | await new Promise<void>((resolve) => { |
| 197 | const done = () => { |
| 198 | clearTimeout(timer); |
| 199 | options.signal?.removeEventListener("abort", done); |
| 200 | resolve(); |
| 201 | }; |
| 202 | const timer = setTimeout(done, Math.max(100, options.intervalMs ?? 500)); |
| 203 | options.signal?.addEventListener("abort", done, { once: true }); |
| 204 | if (options.signal?.aborted) done(); |
| 205 | }); |
| 206 | } |
| 207 | } |
| 208 |
| 209 | /** Additional capabilities required by a job, shared by the host and test gate. */ |
| 210 | export function requiredAiOperations(request: { |
| 211 | kind: ColineAiKind; |
| 212 | input: { |
| 213 | model?: string | undefined; |
| 214 | referenceFileIds?: readonly string[] | undefined; |
| 215 | maskFileId?: string | undefined; |
| 216 | referenceFileId?: string | undefined; |
| 217 | frameImages?: readonly { fileId: string }[] | undefined; |
| 218 | inputReferenceFileIds?: readonly string[] | undefined; |
| 219 | tools?: readonly string[] | undefined; |
| 220 | outputDriveId?: string | undefined; |
| 221 | messages?: readonly { content: ColineAiJson }[] | undefined; |
| 222 | }; |
| 223 | }): Array<"files.read" | "files.write" | "tools.invoke"> { |
| 224 | const operations = new Set<"files.read" | "files.write" | "tools.invoke">(); |
| 225 | if (request.kind === "image" || request.kind === "speech" || request.kind === "video") |
| 226 | operations.add("files.write"); |
| 227 | if ( |
| 228 | request.kind === "image" && |
| 229 | (request.input.referenceFileIds?.length || request.input.maskFileId) |
| 230 | ) |
| 231 | operations.add("files.read"); |
| 232 | if (request.kind === "transcription") operations.add("files.read"); |
| 233 | if ( |
| 234 | request.kind === "video" && |
| 235 | (request.input.referenceFileId || |
| 236 | request.input.frameImages?.length || |
| 237 | request.input.inputReferenceFileIds?.length) |
| 238 | ) |
| 239 | operations.add("files.read"); |
| 240 | if (request.kind === "language") { |
| 241 | if (request.input.tools?.length) operations.add("tools.invoke"); |
| 242 | if (request.input.outputDriveId) operations.add("files.write"); |
| 243 | for (const message of request.input.messages ?? []) |
| 244 | if (Array.isArray(message.content)) |
| 245 | for (const part of message.content) |
| 246 | if ( |
| 247 | part && |
| 248 | typeof part === "object" && |
| 249 | !Array.isArray(part) && |
| 250 | typeof part.fileId === "string" |
| 251 | ) |
| 252 | operations.add("files.read"); |
| 253 | } |
| 254 | return [...operations]; |
| 255 | } |
| 256 |