@runappsai/sdk 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -2
  3. package/dist/activity-tracker.d.ts +113 -0
  4. package/dist/activity-tracker.d.ts.map +1 -0
  5. package/dist/activity-tracker.js +176 -0
  6. package/dist/activity-tracker.js.map +1 -0
  7. package/dist/audio.d.ts +129 -0
  8. package/dist/audio.d.ts.map +1 -0
  9. package/dist/audio.js +267 -0
  10. package/dist/audio.js.map +1 -0
  11. package/dist/browser-auth.d.ts +179 -0
  12. package/dist/browser-auth.d.ts.map +1 -0
  13. package/dist/browser-auth.js +1361 -0
  14. package/dist/browser-auth.js.map +1 -0
  15. package/dist/chat.d.ts +221 -0
  16. package/dist/chat.d.ts.map +1 -0
  17. package/dist/chat.js +155 -0
  18. package/dist/chat.js.map +1 -0
  19. package/dist/client.d.ts +155 -0
  20. package/dist/client.d.ts.map +1 -0
  21. package/dist/client.js +157 -0
  22. package/dist/client.js.map +1 -0
  23. package/dist/computer.d.ts +57 -0
  24. package/dist/computer.d.ts.map +1 -0
  25. package/dist/computer.js +21 -0
  26. package/dist/computer.js.map +1 -0
  27. package/dist/embeddings.d.ts +64 -0
  28. package/dist/embeddings.d.ts.map +1 -0
  29. package/dist/embeddings.js +30 -0
  30. package/dist/embeddings.js.map +1 -0
  31. package/dist/errors.d.ts +24 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +29 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/event-wrap.d.ts +59 -0
  36. package/dist/event-wrap.d.ts.map +1 -0
  37. package/dist/event-wrap.js +142 -0
  38. package/dist/event-wrap.js.map +1 -0
  39. package/dist/events.d.ts +119 -0
  40. package/dist/events.d.ts.map +1 -0
  41. package/dist/events.js +87 -0
  42. package/dist/events.js.map +1 -0
  43. package/dist/files.d.ts +218 -0
  44. package/dist/files.d.ts.map +1 -0
  45. package/dist/files.js +212 -0
  46. package/dist/files.js.map +1 -0
  47. package/dist/image.d.ts +185 -0
  48. package/dist/image.d.ts.map +1 -0
  49. package/dist/image.js +148 -0
  50. package/dist/image.js.map +1 -0
  51. package/dist/index.d.ts +30 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +26 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/media.d.ts +56 -0
  56. package/dist/media.d.ts.map +1 -0
  57. package/dist/media.js +133 -0
  58. package/dist/media.js.map +1 -0
  59. package/dist/model_options.d.ts +165 -0
  60. package/dist/model_options.d.ts.map +1 -0
  61. package/dist/model_options.js +59 -0
  62. package/dist/model_options.js.map +1 -0
  63. package/dist/models.d.ts +88 -0
  64. package/dist/models.d.ts.map +1 -0
  65. package/dist/models.js +55 -0
  66. package/dist/models.js.map +1 -0
  67. package/dist/sdk.umd.js +4 -0
  68. package/dist/sdk.umd.js.map +1 -0
  69. package/dist/transport.d.ts +120 -0
  70. package/dist/transport.d.ts.map +1 -0
  71. package/dist/transport.js +214 -0
  72. package/dist/transport.js.map +1 -0
  73. package/dist/types.d.ts +31 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/types.js +2 -0
  76. package/dist/types.js.map +1 -0
  77. package/dist/validate.d.ts +16 -0
  78. package/dist/validate.d.ts.map +1 -0
  79. package/dist/validate.js +329 -0
  80. package/dist/validate.js.map +1 -0
  81. package/dist/video.d.ts +134 -0
  82. package/dist/video.d.ts.map +1 -0
  83. package/dist/video.js +60 -0
  84. package/dist/video.js.map +1 -0
  85. package/package.json +54 -4
  86. package/src/activity-tracker.ts +251 -0
  87. package/src/audio.ts +440 -0
  88. package/src/browser-auth.ts +1529 -0
  89. package/src/chat.ts +368 -0
  90. package/src/client.ts +250 -0
  91. package/src/computer.ts +80 -0
  92. package/src/embeddings.ts +97 -0
  93. package/src/errors.ts +29 -0
  94. package/src/event-wrap.ts +168 -0
  95. package/src/events.ts +172 -0
  96. package/src/files.ts +347 -0
  97. package/src/image.ts +339 -0
  98. package/src/index.ts +151 -0
  99. package/src/media.ts +147 -0
  100. package/src/model_options.ts +220 -0
  101. package/src/models.ts +121 -0
  102. package/src/transport.ts +292 -0
  103. package/src/types.ts +31 -0
  104. package/src/validate.ts +364 -0
  105. package/src/video.ts +215 -0
@@ -0,0 +1,97 @@
1
+ import type { Transport } from "./transport.js";
2
+ import type { Usage } from "./types.js";
3
+ import type { SDKEvents } from "./events.js";
4
+ import { wrapEvents } from "./event-wrap.js";
5
+
6
+ /* ------------------------------------------------------------------ */
7
+ /* Embeddings — POST /v1/embeddings */
8
+ /* ------------------------------------------------------------------ */
9
+ /* */
10
+ /* OpenAI-compatible vector embedding endpoint. Wire shape mirrors */
11
+ /* OpenAI's `/v1/embeddings` exactly so callers porting from the */
12
+ /* official `openai` SDK can swap the import line and keep working. */
13
+ /* ------------------------------------------------------------------ */
14
+
15
+ export interface EmbeddingsParams {
16
+ /** Single string OR array of strings — gateway forwards either
17
+ * shape verbatim. Passing a string returns `data: [{...}]` (1
18
+ * element), passing an array returns one element per input. */
19
+ input: string | string[];
20
+ /** OpenAI per-end-user observability tag. */
21
+ user?: string;
22
+ /** "float" (default) or "base64". `base64` packs vectors as
23
+ * little-endian float32 + base64-encoded — saves bandwidth for
24
+ * high-D embeddings (3072-D × 1000 docs is ~3× smaller than the
25
+ * JSON float-array form). */
26
+ encoding_format?: "float" | "base64";
27
+ /** Truncate the output vector. text-embedding-3-* only — other
28
+ * models reject. */
29
+ dimensions?: number;
30
+ }
31
+
32
+ export interface Embedding {
33
+ object: "embedding";
34
+ /** Vector. When `encoding_format` was "float" (default), this is
35
+ * `number[]`. When "base64", this is the encoded `string`. */
36
+ embedding: number[] | string;
37
+ index: number;
38
+ }
39
+
40
+ /** OpenAI's usage block plus the gateway's billing extension
41
+ * (`total_cost` in USD).
42
+ *
43
+ * Shape parallels the `Usage` type used by chat / image / video so
44
+ * callers can read `.total_cost` on every response uniformly. */
45
+ export interface EmbeddingsUsage extends Usage {
46
+ prompt_tokens: number;
47
+ total_tokens: number;
48
+ }
49
+
50
+ export interface EmbeddingsResponse {
51
+ object: "list";
52
+ data: Embedding[];
53
+ model: string;
54
+ usage: EmbeddingsUsage;
55
+ }
56
+
57
+ export class EmbeddingsService {
58
+ constructor(
59
+ private readonly transport: Transport,
60
+ private readonly events: SDKEvents,
61
+ ) {}
62
+
63
+ /**
64
+ * Create one or more embeddings.
65
+ *
66
+ * ```ts
67
+ * const r = await client.embeddings.create("text-embedding-3-small", {
68
+ * input: ["alpha", "beta"],
69
+ * dimensions: 1536,
70
+ * });
71
+ * for (const e of r.data) {
72
+ * console.log(e.index, (e.embedding as number[]).slice(0, 4));
73
+ * }
74
+ * console.log(`Cost: $${r.usage.total_cost.toFixed(6)}`);
75
+ * ```
76
+ */
77
+ async create(
78
+ model: string,
79
+ params: EmbeddingsParams,
80
+ init?: { signal?: AbortSignal },
81
+ ): Promise<EmbeddingsResponse> {
82
+ return wrapEvents(
83
+ this.events,
84
+ { model, capability: "embedding" },
85
+ () =>
86
+ this.transport.postJSON<EmbeddingsResponse>(
87
+ "/v1/embeddings",
88
+ { model, ...params },
89
+ init,
90
+ ),
91
+ (r) => ({
92
+ totalTokens: r.usage?.total_tokens,
93
+ costUSD: r.usage?.total_cost,
94
+ }),
95
+ );
96
+ }
97
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * APIError represents an error returned by the RunApps gateway.
3
+ *
4
+ * Use `instanceof APIError` (or check `name === "APIError"`) to distinguish
5
+ * gateway errors from network / runtime errors:
6
+ *
7
+ * ```ts
8
+ * try {
9
+ * await client.chat.create({ model: "...", messages: [...] });
10
+ * } catch (e) {
11
+ * if (e instanceof APIError) {
12
+ * console.error(`API ${e.statusCode}: ${e.message}`);
13
+ * } else {
14
+ * throw e;
15
+ * }
16
+ * }
17
+ * ```
18
+ */
19
+ export class APIError extends Error {
20
+ public readonly statusCode: number;
21
+ public readonly type: string;
22
+
23
+ constructor(statusCode: number, type: string, message: string) {
24
+ super(`runapps: ${statusCode} ${type}: ${message}`);
25
+ this.name = "APIError";
26
+ this.statusCode = statusCode;
27
+ this.type = type;
28
+ }
29
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Tiny wrapper used by every LLM-ish service to fire
3
+ * `request:start` → `request:end` / `request:error` events around a
4
+ * single network call. Centralised here so the service files don't
5
+ * duplicate the try/catch + latency-stopwatch pattern N times.
6
+ *
7
+ * Streaming chat has its own logic (it must yield deltas mid-flight)
8
+ * — see `wrapStream` below.
9
+ */
10
+
11
+ import { newRequestId, type SDKCapability, type SDKEvents } from "./events.js";
12
+
13
+ interface CallMeta {
14
+ model: string;
15
+ capability: SDKCapability;
16
+ }
17
+
18
+ interface EndInfo {
19
+ totalTokens?: number;
20
+ costUSD?: number;
21
+ finishReason?: string;
22
+ }
23
+
24
+ /**
25
+ * Run `fn` while emitting start/end/error events around it.
26
+ * `extractEnd` plucks token / cost / finish_reason off the response
27
+ * shape so each service can map its own response type without the
28
+ * helper knowing about it.
29
+ *
30
+ * Re-throws on error so the caller still sees the rejection — the
31
+ * helper only inserts telemetry, it doesn't swallow.
32
+ */
33
+ export async function wrapEvents<T>(
34
+ events: SDKEvents,
35
+ meta: CallMeta,
36
+ fn: () => Promise<T>,
37
+ extractEnd?: (r: T) => EndInfo,
38
+ ): Promise<T> {
39
+ const id = newRequestId();
40
+ const startedAt = Date.now();
41
+ events.emit("request:start", {
42
+ id,
43
+ model: meta.model,
44
+ capability: meta.capability,
45
+ startedAt,
46
+ streaming: false,
47
+ });
48
+ try {
49
+ const result = await fn();
50
+ const info = extractEnd ? extractEnd(result) : {};
51
+ events.emit("request:end", {
52
+ id,
53
+ model: meta.model,
54
+ capability: meta.capability,
55
+ latencyMs: Date.now() - startedAt,
56
+ totalTokens: info.totalTokens ?? 0,
57
+ ...(info.costUSD !== undefined && { costUSD: info.costUSD }),
58
+ ...(info.finishReason !== undefined && { finishReason: info.finishReason }),
59
+ });
60
+ return result;
61
+ } catch (err) {
62
+ const e = err as Error & { statusCode?: number };
63
+ events.emit("request:error", {
64
+ id,
65
+ model: meta.model,
66
+ capability: meta.capability,
67
+ latencyMs: Date.now() - startedAt,
68
+ error: e,
69
+ ...(typeof e.statusCode === "number" && { statusCode: e.statusCode }),
70
+ });
71
+ throw err;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Token-count estimator for stream deltas. Stream chunks carry text,
77
+ * not tokens — getting an exact count would mean shipping a
78
+ * tokenizer in the SDK (huge), so we use the standard ~4-chars =
79
+ * 1-token English heuristic. Off by 10-30% for code or non-English,
80
+ * but the badge UI shows tokens/sec as a vibe indicator, not for
81
+ * billing, so the imprecision is fine.
82
+ */
83
+ export function estimateTokens(text: string): number {
84
+ if (!text) return 0;
85
+ return Math.max(1, Math.round(text.length / 4));
86
+ }
87
+
88
+ /**
89
+ * Stream wrapper — same start/end/error envelope as wrapEvents but
90
+ * also fires `request:streamDelta` for each yielded chunk so the
91
+ * badge's tokens/sec sliding window has samples.
92
+ *
93
+ * Generic over the chunk type because each service's stream
94
+ * (chat / video / etc.) carries its own shape. The caller passes a
95
+ * `extractDelta` that pulls text + cumulative usage off the chunk.
96
+ *
97
+ * Usage:
98
+ * yield* wrapStream(events, {model, capability}, () => transport.postJSONStream(...),
99
+ * (chunk) => ({ deltaText: chunk.choices[0]?.delta?.content, usage: chunk.usage }));
100
+ */
101
+ export async function* wrapStream<TChunk>(
102
+ events: SDKEvents,
103
+ meta: CallMeta,
104
+ source: () => AsyncIterable<TChunk>,
105
+ extractDelta: (chunk: TChunk) => {
106
+ deltaText?: string;
107
+ // When a chunk carries cumulative usage (final chunk in OpenAI
108
+ // streams), surface it so the end event can use exact totals
109
+ // instead of the heuristic running sum.
110
+ completionTokens?: number;
111
+ costUSD?: number;
112
+ finishReason?: string;
113
+ },
114
+ ): AsyncGenerator<TChunk, void, unknown> {
115
+ const id = newRequestId();
116
+ const startedAt = Date.now();
117
+ events.emit("request:start", {
118
+ id,
119
+ model: meta.model,
120
+ capability: meta.capability,
121
+ startedAt,
122
+ streaming: true,
123
+ });
124
+ let totalTokens = 0;
125
+ let lastUsageTokens: number | undefined;
126
+ let costUSD: number | undefined;
127
+ let finishReason: string | undefined;
128
+ try {
129
+ for await (const chunk of source()) {
130
+ const info = extractDelta(chunk);
131
+ if (info.deltaText) {
132
+ const delta = estimateTokens(info.deltaText);
133
+ totalTokens += delta;
134
+ events.emit("request:streamDelta", {
135
+ id,
136
+ deltaTokens: delta,
137
+ totalTokens,
138
+ });
139
+ }
140
+ if (info.completionTokens !== undefined) lastUsageTokens = info.completionTokens;
141
+ if (info.costUSD !== undefined) costUSD = info.costUSD;
142
+ if (info.finishReason !== undefined) finishReason = info.finishReason;
143
+ yield chunk;
144
+ }
145
+ events.emit("request:end", {
146
+ id,
147
+ model: meta.model,
148
+ capability: meta.capability,
149
+ latencyMs: Date.now() - startedAt,
150
+ // Prefer the exact upstream count over our heuristic when it
151
+ // arrived in the final chunk.
152
+ totalTokens: lastUsageTokens ?? totalTokens,
153
+ ...(costUSD !== undefined && { costUSD }),
154
+ ...(finishReason !== undefined && { finishReason }),
155
+ });
156
+ } catch (err) {
157
+ const e = err as Error & { statusCode?: number };
158
+ events.emit("request:error", {
159
+ id,
160
+ model: meta.model,
161
+ capability: meta.capability,
162
+ latencyMs: Date.now() - startedAt,
163
+ error: e,
164
+ ...(typeof e.statusCode === "number" && { statusCode: e.statusCode }),
165
+ });
166
+ throw err;
167
+ }
168
+ }
package/src/events.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Typed runtime event bus for SDK call telemetry.
3
+ *
4
+ * Every LLM-ish service method (chat, embeddings, image, audio, video,
5
+ * computer) emits start / streamDelta / end / error events on the
6
+ * shared `client.events` bus so UI overlays (e.g. the bottom-right
7
+ * identity badge with its activity ring) can render real-time state
8
+ * without business code having to thread anything through.
9
+ *
10
+ * Goals:
11
+ * - **Typed**: handler signatures are inferred from the event name.
12
+ * - **Zero-cost when nobody listens**: no listeners → emit is a few
13
+ * property reads. The browser `EventTarget` does the heavy lifting.
14
+ * - **Service-agnostic**: events carry just enough context (id /
15
+ * model / capability / token counts) for a generic UI to render
16
+ * them without knowing which service fired the call.
17
+ *
18
+ * Why a custom wrapper instead of `EventTarget` directly:
19
+ * - The native API is untyped (`addEventListener("foo", anyHandler)`).
20
+ * - Browser CustomEvent's `detail` payload trick is awkward to read.
21
+ * - Subscribers want an `off()` closure, not a separate
22
+ * `removeEventListener` call with the same callback reference.
23
+ */
24
+
25
+ /** Capability bucket of the calling service, mirroring /v1/models. */
26
+ export type SDKCapability =
27
+ | "text"
28
+ | "vision"
29
+ | "embedding"
30
+ | "image_generation"
31
+ | "image_edit"
32
+ | "text_to_speech"
33
+ | "speech_to_text"
34
+ | "video_generation"
35
+ | "computer_use";
36
+
37
+ /** Fired the moment a request hits the wire. */
38
+ export interface RequestStartEvent {
39
+ /** Stable id correlating start → end/error events for one call. */
40
+ id: string;
41
+ /** Model name the caller asked for (NOT the upstream id). */
42
+ model: string;
43
+ /** Capability bucket — drives badge icon / colour. */
44
+ capability: SDKCapability;
45
+ /** ms since epoch when the request left the SDK. */
46
+ startedAt: number;
47
+ /** `true` for stream:true requests so the UI can pre-pick the
48
+ * pulsing-ring animation instead of swapping mid-call. */
49
+ streaming: boolean;
50
+ }
51
+
52
+ /** Fired on every stream chunk that carried new content tokens. */
53
+ export interface RequestStreamDeltaEvent {
54
+ /** Matches the corresponding RequestStartEvent.id. */
55
+ id: string;
56
+ /** Approximate completion tokens in THIS chunk (one delta event
57
+ * per visible SSE chunk; usage roll-up arrives in end). */
58
+ deltaTokens: number;
59
+ /** Cumulative completion tokens since stream start. */
60
+ totalTokens: number;
61
+ }
62
+
63
+ /** Fired once when the call settles successfully. */
64
+ export interface RequestEndEvent {
65
+ id: string;
66
+ model: string;
67
+ capability: SDKCapability;
68
+ /** Wall-clock from start to settle. */
69
+ latencyMs: number;
70
+ /** Final completion tokens (matches the gateway's usage.completion_tokens
71
+ * when the response carries it; 0 otherwise). */
72
+ totalTokens: number;
73
+ /** USD cost as reported by the gateway's `usage.total_cost`. May be
74
+ * undefined for endpoints that don't return cost (rare). */
75
+ costUSD?: number;
76
+ /** OpenAI-style stop / length / tool_calls / content_filter, or
77
+ * undefined for non-chat endpoints. */
78
+ finishReason?: string;
79
+ }
80
+
81
+ /** Fired once when the call fails — at start, mid-stream, or anywhere. */
82
+ export interface RequestErrorEvent {
83
+ id: string;
84
+ model: string;
85
+ capability: SDKCapability;
86
+ latencyMs: number;
87
+ /** The thrown Error (typically `APIError` with statusCode / body). */
88
+ error: Error;
89
+ /** HTTP status when known (forwarded from APIError when present). */
90
+ statusCode?: number;
91
+ }
92
+
93
+ /** Compile-time map of event name → payload shape. Drives handler
94
+ * inference in `on()` / `emit()` so callers can't subscribe to a
95
+ * typo'd name or read a non-existent field. */
96
+ export interface SDKEventMap {
97
+ "request:start": RequestStartEvent;
98
+ "request:streamDelta": RequestStreamDeltaEvent;
99
+ "request:end": RequestEndEvent;
100
+ "request:error": RequestErrorEvent;
101
+ }
102
+
103
+ /** Disposer returned by `on()` — call to unsubscribe. */
104
+ export type Unsubscribe = () => void;
105
+
106
+ /**
107
+ * Lightweight typed event bus. Backed by `EventTarget` so memory
108
+ * leaks from un-disposed listeners surface in the standard DevTools
109
+ * memory profile path. In Node we fall back to a plain Map<string,
110
+ * Set<handler>> since `EventTarget` was only added in Node 15+ and we
111
+ * still support 14 in places (the SDK targets 18+ but tests sometimes
112
+ * stub globals).
113
+ */
114
+ export class SDKEvents {
115
+ private readonly handlers = new Map<keyof SDKEventMap, Set<(payload: unknown) => void>>();
116
+
117
+ /**
118
+ * Subscribe to an event. Returns a disposer; calling the disposer
119
+ * removes the subscription. Multiple subscriptions to the same
120
+ * event are supported.
121
+ */
122
+ on<K extends keyof SDKEventMap>(name: K, handler: (event: SDKEventMap[K]) => void): Unsubscribe {
123
+ let set = this.handlers.get(name);
124
+ if (!set) {
125
+ set = new Set();
126
+ this.handlers.set(name, set);
127
+ }
128
+ const wrapped = handler as (payload: unknown) => void;
129
+ set.add(wrapped);
130
+ return () => {
131
+ this.handlers.get(name)?.delete(wrapped);
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Fire an event. Handlers run synchronously in subscription order.
137
+ * Throws inside one handler don't prevent the others from running —
138
+ * the bus catches and logs (console.error) so a buggy listener can't
139
+ * take down the rest of the UI.
140
+ */
141
+ emit<K extends keyof SDKEventMap>(name: K, payload: SDKEventMap[K]): void {
142
+ const set = this.handlers.get(name);
143
+ if (!set || set.size === 0) return;
144
+ for (const h of set) {
145
+ try {
146
+ h(payload);
147
+ } catch (err) {
148
+ // Listener bug — don't propagate, but surface in the console
149
+ // so developers notice during integration.
150
+ // eslint-disable-next-line no-console
151
+ console.error("[runapps sdk] event listener for", name, "threw:", err);
152
+ }
153
+ }
154
+ }
155
+
156
+ /** Drop all subscriptions. Tests call this between cases. */
157
+ clear(): void {
158
+ this.handlers.clear();
159
+ }
160
+ }
161
+
162
+ /** Generate a short opaque ID for correlating start/end events.
163
+ * Not a UUID — sub-microsecond uniqueness within a session is
164
+ * enough, and we want to keep the wire log compact. */
165
+ export function newRequestId(): string {
166
+ return (
167
+ "req_" +
168
+ Date.now().toString(36) +
169
+ "_" +
170
+ Math.random().toString(36).slice(2, 8)
171
+ );
172
+ }