@runappsai/sdk 0.0.0-stage → 0.3.1
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.
- package/LICENSE +21 -0
- package/README.md +274 -2
- package/dist/activity-tracker.d.ts +113 -0
- package/dist/activity-tracker.d.ts.map +1 -0
- package/dist/activity-tracker.js +176 -0
- package/dist/activity-tracker.js.map +1 -0
- package/dist/audio.d.ts +129 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +267 -0
- package/dist/audio.js.map +1 -0
- package/dist/browser-auth.d.ts +179 -0
- package/dist/browser-auth.d.ts.map +1 -0
- package/dist/browser-auth.js +1361 -0
- package/dist/browser-auth.js.map +1 -0
- package/dist/chat.d.ts +221 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +155 -0
- package/dist/chat.js.map +1 -0
- package/dist/client.d.ts +155 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +157 -0
- package/dist/client.js.map +1 -0
- package/dist/computer.d.ts +57 -0
- package/dist/computer.d.ts.map +1 -0
- package/dist/computer.js +21 -0
- package/dist/computer.js.map +1 -0
- package/dist/embeddings.d.ts +64 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +30 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-wrap.d.ts +59 -0
- package/dist/event-wrap.d.ts.map +1 -0
- package/dist/event-wrap.js +142 -0
- package/dist/event-wrap.js.map +1 -0
- package/dist/events.d.ts +119 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +87 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +218 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +212 -0
- package/dist/files.js.map +1 -0
- package/dist/image.d.ts +185 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +148 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/media.d.ts +56 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +133 -0
- package/dist/media.js.map +1 -0
- package/dist/model_options.d.ts +165 -0
- package/dist/model_options.d.ts.map +1 -0
- package/dist/model_options.js +59 -0
- package/dist/model_options.js.map +1 -0
- package/dist/models.d.ts +88 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +55 -0
- package/dist/models.js.map +1 -0
- package/dist/sdk.umd.js +4 -0
- package/dist/sdk.umd.js.map +1 -0
- package/dist/transport.d.ts +120 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +214 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +16 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +329 -0
- package/dist/validate.js.map +1 -0
- package/dist/video.d.ts +134 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +60 -0
- package/dist/video.js.map +1 -0
- package/package.json +54 -4
- package/src/activity-tracker.ts +251 -0
- package/src/audio.ts +440 -0
- package/src/browser-auth.ts +1529 -0
- package/src/chat.ts +368 -0
- package/src/client.ts +250 -0
- package/src/computer.ts +80 -0
- package/src/embeddings.ts +97 -0
- package/src/errors.ts +29 -0
- package/src/event-wrap.ts +168 -0
- package/src/events.ts +172 -0
- package/src/files.ts +347 -0
- package/src/image.ts +339 -0
- package/src/index.ts +151 -0
- package/src/media.ts +147 -0
- package/src/model_options.ts +220 -0
- package/src/models.ts +121 -0
- package/src/transport.ts +292 -0
- package/src/types.ts +31 -0
- package/src/validate.ts +364 -0
- 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
|
+
}
|