@hraness/dawg 0.2.0 → 0.4.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.
- package/CHANGELOG.md +201 -0
- package/DAWG.md +762 -11
- package/README.md +79 -30
- package/core/chords.ts +1724 -0
- package/core/diff.ts +261 -0
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/key.ts +43 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/pitch.ts +60 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +1054 -14
- package/core/sdk/eval-child.ts +125 -0
- package/core/sdk/eval.ts +260 -0
- package/core/sdk/print.ts +675 -0
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +4159 -0
- package/core/slug.ts +19 -0
- package/core/synth.ts +1001 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +317 -16
- package/src/agent/brief.ts +50 -5
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +55 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +705 -14
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +35 -14
- package/src/agent/xcb.ts +233 -21
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +99 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +301 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +337 -0
- package/src/audio/engine.ts +430 -33
- package/src/audio/kits.ts +200 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +420 -0
- package/src/audio/samples.ts +1025 -0
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +550 -334
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/auth/cli.ts +146 -27
- package/src/auth/credentials.ts +167 -41
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +885 -128
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +25 -2
- package/src/auth/tui.ts +60 -43
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +192 -0
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +535 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +34 -15
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +462 -0
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +2136 -170
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +186 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +397 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/media/wavetable.ts +202 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +49 -6
- package/src/session/attach.ts +3 -4
- package/src/session/daemon.ts +25 -8
- package/src/session/delta.ts +249 -0
- package/src/session/naming.ts +6 -38
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +2125 -0
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +984 -0
- package/src/tui/sketch.ts +108 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +684 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +398 -33
- package/tui/drums.ts +44 -0
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +24 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +188 -0
package/src/agent/gateway.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { readSseData } from "./sse.ts";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* The two original model aliases. A model is either one of these or an exact
|
|
5
|
+
* `vendor/model` ID chosen from the model picker (see `models.ts`).
|
|
6
|
+
*/
|
|
4
7
|
export const GATEWAY_MODELS = Object.freeze(["opus-5.5", "sol-6.1"] as const);
|
|
5
8
|
export type GatewayModel = (typeof GATEWAY_MODELS)[number];
|
|
6
9
|
|
|
@@ -26,15 +29,20 @@ export function isGatewayModel(value: unknown): value is GatewayModel {
|
|
|
26
29
|
);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Resolve an alias (`opus-5.5`) or an exact `vendor/model` ID to the ID sent
|
|
34
|
+
* to the provider, rejecting anything else.
|
|
35
|
+
*/
|
|
30
36
|
export function resolveModelId(
|
|
31
37
|
alias: string,
|
|
32
38
|
overrides: Partial<Record<GatewayModel, string | undefined>> = {},
|
|
33
39
|
): string {
|
|
34
|
-
if (!isGatewayModel(alias))
|
|
40
|
+
if (!isGatewayModel(alias)) {
|
|
41
|
+
if (MODEL_ID_PATTERN.test(alias)) return alias;
|
|
35
42
|
throw new Error(
|
|
36
|
-
`unknown model "${alias.slice(0, 32)}"; use ${GATEWAY_MODELS.join(" or ")}`,
|
|
43
|
+
`unknown model "${alias.slice(0, 32)}"; use ${GATEWAY_MODELS.join(" or ")} or a vendor/model ID`,
|
|
37
44
|
);
|
|
45
|
+
}
|
|
38
46
|
const id = overrides[alias] ?? DEFAULT_MODEL_IDS[alias];
|
|
39
47
|
if (!MODEL_ID_PATTERN.test(id))
|
|
40
48
|
throw new Error(`model ID for ${alias} must look like provider/model`);
|
|
@@ -68,7 +76,8 @@ export type ChatTool = {
|
|
|
68
76
|
};
|
|
69
77
|
|
|
70
78
|
export type ChatStreamRequest = {
|
|
71
|
-
model
|
|
79
|
+
/** An alias (`opus-5.5`) or an exact `vendor/model` ID. */
|
|
80
|
+
model: string;
|
|
72
81
|
messages: readonly ChatMessage[];
|
|
73
82
|
tools?: readonly ChatTool[];
|
|
74
83
|
temperature?: number;
|
|
@@ -81,6 +90,8 @@ export type ChatStreamRequest = {
|
|
|
81
90
|
/** Normalized stream events; provider chunk shapes never leave this module. */
|
|
82
91
|
export type ChatStreamEvent =
|
|
83
92
|
| { type: "text"; delta: string }
|
|
93
|
+
/** Progress worth a status line, such as a retry; never model output. */
|
|
94
|
+
| { type: "activity"; message: string }
|
|
84
95
|
| {
|
|
85
96
|
type: "tool-delta";
|
|
86
97
|
index: number;
|
|
@@ -88,11 +99,25 @@ export type ChatStreamEvent =
|
|
|
88
99
|
name?: string;
|
|
89
100
|
arguments?: string;
|
|
90
101
|
}
|
|
91
|
-
| { type: "finish"; reason: string }
|
|
102
|
+
| { type: "finish"; reason: string }
|
|
103
|
+
/**
|
|
104
|
+
* Token usage for one request, from the final chunk when the request asked
|
|
105
|
+
* for `stream_options.include_usage`. `costUsd` is the provider's own
|
|
106
|
+
* charge when it reports one (OpenRouter's `usage.cost`).
|
|
107
|
+
*/
|
|
108
|
+
| {
|
|
109
|
+
type: "usage";
|
|
110
|
+
inputTokens: number;
|
|
111
|
+
outputTokens: number;
|
|
112
|
+
cachedInputTokens?: number;
|
|
113
|
+
costUsd?: number;
|
|
114
|
+
};
|
|
92
115
|
|
|
93
116
|
export type GatewayClient = {
|
|
117
|
+
/** Which service this client talks to. */
|
|
118
|
+
readonly provider?: ApiProvider;
|
|
94
119
|
/** The provider model ID that a turn will use (for diagnostics only). */
|
|
95
|
-
modelId(model:
|
|
120
|
+
modelId(model: string): string;
|
|
96
121
|
stream(
|
|
97
122
|
request: ChatStreamRequest,
|
|
98
123
|
signal?: AbortSignal,
|
|
@@ -114,21 +139,79 @@ type GatewayFetcher = (
|
|
|
114
139
|
init?: RequestInit,
|
|
115
140
|
) => Promise<Response>;
|
|
116
141
|
|
|
142
|
+
/** Retries after a failed request that has not yet produced a byte. */
|
|
143
|
+
export const GATEWAY_RETRIES = 2;
|
|
144
|
+
/** Base backoff; each retry doubles it, with +-50% jitter. */
|
|
145
|
+
const RETRY_BASE_MS = 500;
|
|
146
|
+
/** Response headers must arrive within this long, per attempt. */
|
|
147
|
+
export const GATEWAY_HEADER_TIMEOUT_MS = 15_000;
|
|
148
|
+
|
|
149
|
+
export type GatewayRetryOptions = Readonly<{
|
|
150
|
+
retries?: number;
|
|
151
|
+
baseMs?: number;
|
|
152
|
+
sleep?: (ms: number) => Promise<void>;
|
|
153
|
+
random?: () => number;
|
|
154
|
+
}>;
|
|
155
|
+
|
|
156
|
+
function retryableStatus(status: number): boolean {
|
|
157
|
+
return status === 408 || status === 429 || status >= 500;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The two OpenAI-compatible, key-based services. */
|
|
161
|
+
export type ApiProvider = "gateway" | "openrouter";
|
|
162
|
+
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
163
|
+
export const GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1";
|
|
164
|
+
/** OpenRouter's app attribution headers (https://openrouter.ai/docs/app-attribution). */
|
|
165
|
+
const OPENROUTER_HEADERS = Object.freeze({
|
|
166
|
+
"http-referer": "https://dawg.sh",
|
|
167
|
+
"x-title": "dawg",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export type ApiClientOptions = {
|
|
171
|
+
apiKey?: string;
|
|
172
|
+
baseUrl?: string;
|
|
173
|
+
modelIds?: Partial<Record<GatewayModel, string>>;
|
|
174
|
+
fetcher?: GatewayFetcher;
|
|
175
|
+
retry?: GatewayRetryOptions;
|
|
176
|
+
headerTimeoutMs?: number;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** The streaming tool-calling client for OpenRouter's OpenAI-compatible API. */
|
|
180
|
+
export function createOpenRouterClient(
|
|
181
|
+
options: ApiClientOptions = {},
|
|
182
|
+
): GatewayClient {
|
|
183
|
+
return createApiClient("openrouter", {
|
|
184
|
+
...options,
|
|
185
|
+
baseUrl:
|
|
186
|
+
options.baseUrl ?? process.env.OPENROUTER_BASE_URL ?? OPENROUTER_BASE_URL,
|
|
187
|
+
apiKey: options.apiKey ?? process.env.OPENROUTER_API_KEY,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
117
191
|
export function createGatewayClient(
|
|
118
|
-
options: {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
192
|
+
options: ApiClientOptions = {},
|
|
193
|
+
): GatewayClient {
|
|
194
|
+
return createApiClient("gateway", {
|
|
195
|
+
...options,
|
|
196
|
+
baseUrl:
|
|
197
|
+
options.baseUrl ?? process.env.AI_GATEWAY_BASE_URL ?? GATEWAY_BASE_URL,
|
|
198
|
+
apiKey: options.apiKey ?? process.env.AI_GATEWAY_API_KEY,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function createApiClient(
|
|
203
|
+
provider: ApiProvider,
|
|
204
|
+
options: ApiClientOptions,
|
|
124
205
|
): GatewayClient {
|
|
125
206
|
const fetcher = options.fetcher ?? fetch;
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
207
|
+
const name = provider === "openrouter" ? "OpenRouter" : "AI Gateway";
|
|
208
|
+
const baseUrl = (options.baseUrl ?? GATEWAY_BASE_URL).replace(/\/$/, "");
|
|
209
|
+
const apiKey = options.apiKey;
|
|
210
|
+
const retries = options.retry?.retries ?? GATEWAY_RETRIES;
|
|
211
|
+
const retryBaseMs = options.retry?.baseMs ?? RETRY_BASE_MS;
|
|
212
|
+
const sleep = options.retry?.sleep ?? ((ms: number) => Bun.sleep(ms));
|
|
213
|
+
const random = options.retry?.random ?? Math.random;
|
|
214
|
+
const headerTimeoutMs = options.headerTimeoutMs ?? GATEWAY_HEADER_TIMEOUT_MS;
|
|
132
215
|
const overrides: Partial<Record<GatewayModel, string | undefined>> = {
|
|
133
216
|
"opus-5.5": process.env.DAWG_OPUS_MODEL || undefined,
|
|
134
217
|
"sol-6.1": process.env.DAWG_SOL_MODEL || undefined,
|
|
@@ -138,6 +221,7 @@ export function createGatewayClient(
|
|
|
138
221
|
apiKey && apiKey.length > 0 ? text.split(apiKey).join("[redacted]") : text;
|
|
139
222
|
|
|
140
223
|
return {
|
|
224
|
+
provider,
|
|
141
225
|
modelId: (model) => resolveModelId(model, overrides),
|
|
142
226
|
async *stream(request, signal) {
|
|
143
227
|
const model =
|
|
@@ -146,13 +230,18 @@ export function createGatewayClient(
|
|
|
146
230
|
: resolveModelId(request.model, overrides);
|
|
147
231
|
if (!apiKey)
|
|
148
232
|
throw new GatewayError(
|
|
149
|
-
|
|
233
|
+
provider === "openrouter"
|
|
234
|
+
? "no OpenRouter key; run `dawg login openrouter` or set OPENROUTER_API_KEY"
|
|
235
|
+
: "no AI Gateway key; run `dawg login` or set AI_GATEWAY_API_KEY",
|
|
150
236
|
);
|
|
151
237
|
const body: Record<string, unknown> = {
|
|
152
238
|
model,
|
|
153
239
|
messages: request.messages,
|
|
154
240
|
stream: true,
|
|
241
|
+
// The final chunk then carries token usage (and OpenRouter's cost).
|
|
242
|
+
stream_options: { include_usage: true },
|
|
155
243
|
};
|
|
244
|
+
if (provider === "openrouter") body.usage = { include: true };
|
|
156
245
|
if (request.tools && request.tools.length > 0) {
|
|
157
246
|
body.tools = request.tools;
|
|
158
247
|
body.tool_choice = "auto";
|
|
@@ -165,26 +254,61 @@ export function createGatewayClient(
|
|
|
165
254
|
request.maxTokens > 0
|
|
166
255
|
)
|
|
167
256
|
body.max_tokens = Math.min(request.maxTokens, 4096);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
257
|
+
const encoded = JSON.stringify(body);
|
|
258
|
+
// Retry only before any byte of a response has been consumed: a
|
|
259
|
+
// network failure, a header-phase timeout, or 408/429/5xx. Once the
|
|
260
|
+
// stream is flowing, a failure surfaces as-is (replaying could repeat
|
|
261
|
+
// tool calls the model already made).
|
|
262
|
+
let response: Response | undefined;
|
|
263
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
264
|
+
if (signal?.aborted) throw abortError(signal);
|
|
265
|
+
const headerSignal = AbortSignal.any([
|
|
266
|
+
...(signal === undefined ? [] : [signal]),
|
|
267
|
+
AbortSignal.timeout(headerTimeoutMs),
|
|
268
|
+
]);
|
|
269
|
+
const init: RequestInit = {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: {
|
|
272
|
+
authorization: `Bearer ${apiKey}`,
|
|
273
|
+
"content-type": "application/json",
|
|
274
|
+
accept: "text/event-stream",
|
|
275
|
+
...(provider === "openrouter" ? OPENROUTER_HEADERS : {}),
|
|
276
|
+
},
|
|
277
|
+
body: encoded,
|
|
278
|
+
signal: headerSignal,
|
|
279
|
+
};
|
|
280
|
+
let reason: string;
|
|
281
|
+
try {
|
|
282
|
+
const candidate = await fetcher(`${baseUrl}/chat/completions`, init);
|
|
283
|
+
if (candidate.ok) {
|
|
284
|
+
response = candidate;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
const detail = redact(await boundedErrorDetail(candidate));
|
|
288
|
+
const error = new GatewayError(
|
|
289
|
+
`${name} request failed (${candidate.status})${detail ? `: ${detail}` : ""}`,
|
|
290
|
+
candidate.status,
|
|
291
|
+
);
|
|
292
|
+
if (!retryableStatus(candidate.status) || attempt >= retries)
|
|
293
|
+
throw error;
|
|
294
|
+
reason = String(candidate.status);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error instanceof GatewayError) throw error;
|
|
297
|
+
if (signal?.aborted) throw abortError(signal);
|
|
298
|
+
if (attempt >= retries)
|
|
299
|
+
throw new GatewayError(
|
|
300
|
+
headerSignal.aborted
|
|
301
|
+
? `${name} did not respond within ${headerTimeoutMs} ms`
|
|
302
|
+
: `${name} request failed: ${redact(error instanceof Error ? error.message : String(error))}`,
|
|
303
|
+
);
|
|
304
|
+
reason = headerSignal.aborted ? "timeout" : "network";
|
|
305
|
+
}
|
|
306
|
+
yield { type: "activity", message: `retrying (${reason})…` };
|
|
307
|
+
const backoff = retryBaseMs * 2 ** attempt;
|
|
308
|
+
await sleep(Math.round(backoff * (0.5 + random())));
|
|
185
309
|
}
|
|
186
310
|
if (!response.body)
|
|
187
|
-
throw new GatewayError(
|
|
311
|
+
throw new GatewayError(`${name} returned an empty stream`);
|
|
188
312
|
const streamOptions: { maxBytes: number; signal?: AbortSignal } = {
|
|
189
313
|
maxBytes: request.maxResponseBytes,
|
|
190
314
|
};
|
|
@@ -194,9 +318,9 @@ export function createGatewayClient(
|
|
|
194
318
|
try {
|
|
195
319
|
chunk = JSON.parse(data);
|
|
196
320
|
} catch {
|
|
197
|
-
throw new GatewayError(
|
|
321
|
+
throw new GatewayError(`${name} sent a malformed stream chunk`);
|
|
198
322
|
}
|
|
199
|
-
yield* normalizeChunk(chunk, redact);
|
|
323
|
+
yield* normalizeChunk(chunk, redact, name);
|
|
200
324
|
}
|
|
201
325
|
},
|
|
202
326
|
};
|
|
@@ -205,6 +329,7 @@ export function createGatewayClient(
|
|
|
205
329
|
function* normalizeChunk(
|
|
206
330
|
chunk: unknown,
|
|
207
331
|
redact: (text: string) => string,
|
|
332
|
+
name = "AI Gateway",
|
|
208
333
|
): Generator<ChatStreamEvent> {
|
|
209
334
|
if (!isRecord(chunk)) return;
|
|
210
335
|
if (isRecord(chunk.error)) {
|
|
@@ -212,8 +337,10 @@ function* normalizeChunk(
|
|
|
212
337
|
typeof chunk.error.message === "string"
|
|
213
338
|
? chunk.error.message.slice(0, 300)
|
|
214
339
|
: "unknown provider error";
|
|
215
|
-
throw new GatewayError(
|
|
340
|
+
throw new GatewayError(`${name} stream error: ${redact(message)}`);
|
|
216
341
|
}
|
|
342
|
+
const usage = parseUsage(chunk.usage);
|
|
343
|
+
if (usage) yield usage;
|
|
217
344
|
const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : undefined;
|
|
218
345
|
if (!isRecord(choice)) return;
|
|
219
346
|
const delta = isRecord(choice.delta) ? choice.delta : undefined;
|
|
@@ -242,6 +369,53 @@ function* normalizeChunk(
|
|
|
242
369
|
yield { type: "finish", reason: choice.finish_reason };
|
|
243
370
|
}
|
|
244
371
|
|
|
372
|
+
/** Bounded, non-negative token counts; anything else is ignored. */
|
|
373
|
+
function count(value: unknown): number | undefined {
|
|
374
|
+
return typeof value === "number" &&
|
|
375
|
+
Number.isFinite(value) &&
|
|
376
|
+
value >= 0 &&
|
|
377
|
+
value < 1e9
|
|
378
|
+
? Math.floor(value)
|
|
379
|
+
: undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function parseUsage(
|
|
383
|
+
value: unknown,
|
|
384
|
+
): Extract<ChatStreamEvent, { type: "usage" }> | undefined {
|
|
385
|
+
if (!isRecord(value)) return undefined;
|
|
386
|
+
const inputTokens = count(value.prompt_tokens);
|
|
387
|
+
const outputTokens = count(value.completion_tokens);
|
|
388
|
+
if (inputTokens === undefined || outputTokens === undefined) return undefined;
|
|
389
|
+
const event: Extract<ChatStreamEvent, { type: "usage" }> = {
|
|
390
|
+
type: "usage",
|
|
391
|
+
inputTokens,
|
|
392
|
+
outputTokens,
|
|
393
|
+
};
|
|
394
|
+
const details = isRecord(value.prompt_tokens_details)
|
|
395
|
+
? value.prompt_tokens_details
|
|
396
|
+
: undefined;
|
|
397
|
+
const cached = count(details?.cached_tokens);
|
|
398
|
+
if (cached !== undefined) event.cachedInputTokens = cached;
|
|
399
|
+
const rawCost =
|
|
400
|
+
typeof value.cost === "string" ? Number(value.cost) : value.cost;
|
|
401
|
+
if (
|
|
402
|
+
typeof rawCost === "number" &&
|
|
403
|
+
Number.isFinite(rawCost) &&
|
|
404
|
+
rawCost >= 0 &&
|
|
405
|
+
rawCost < 1000
|
|
406
|
+
)
|
|
407
|
+
event.costUsd = rawCost;
|
|
408
|
+
return event;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function abortError(signal: AbortSignal): Error {
|
|
412
|
+
const reason: unknown = signal.reason;
|
|
413
|
+
if (reason instanceof Error) return reason;
|
|
414
|
+
const error = new Error("agent request was aborted");
|
|
415
|
+
error.name = "AbortError";
|
|
416
|
+
return error;
|
|
417
|
+
}
|
|
418
|
+
|
|
245
419
|
async function boundedErrorDetail(response: Response): Promise<string> {
|
|
246
420
|
try {
|
|
247
421
|
const text = (await response.text()).slice(0, MAX_ERROR_BODY_BYTES);
|