@hraness/dawg 0.2.0 → 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.
- package/CHANGELOG.md +79 -0
- package/DAWG.md +196 -11
- package/README.md +79 -30
- package/core/diff.ts +249 -0
- package/core/key.ts +43 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +324 -1
- package/core/sdk/eval-child.ts +113 -0
- package/core/sdk/eval.ts +257 -0
- package/core/sdk/print.ts +393 -0
- package/core/sdk/v1.ts +954 -0
- package/core/slug.ts +19 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +280 -14
- package/src/agent/brief.ts +23 -3
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/planner.ts +38 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +398 -1
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +27 -12
- package/src/agent/xcb.ts +233 -21
- package/src/audio/click.ts +125 -0
- package/src/audio/engine.ts +407 -33
- package/src/audio/live.ts +152 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +292 -0
- package/src/audio/samples.ts +683 -0
- package/src/audio/wav.ts +290 -76
- 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/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +24 -7
- package/src/commands/sample.ts +302 -0
- package/src/main.ts +1043 -166
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +173 -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 +292 -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/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 +28 -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 +3 -37
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/menu.ts +1083 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +636 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +681 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +261 -18
- package/tui/drums.ts +44 -0
- package/tui/highway.ts +18 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +143 -0
package/src/agent/planner.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
normalizeDelay,
|
|
6
6
|
normalizeFilter,
|
|
7
7
|
normalizeReverb,
|
|
8
|
+
normalizeSampler,
|
|
8
9
|
scoreFromJSON,
|
|
9
10
|
type ScoreOperation,
|
|
10
11
|
} from "../../core/score.ts";
|
|
@@ -91,6 +92,41 @@ function parseOperation(value: unknown): ScoreOperation {
|
|
|
91
92
|
) {
|
|
92
93
|
return { type: "clearTrack", trackId: value.trackId };
|
|
93
94
|
}
|
|
95
|
+
if (
|
|
96
|
+
value.type === "removeTrack" &&
|
|
97
|
+
typeof value.trackId === "string" &&
|
|
98
|
+
value.trackId.length <= 64
|
|
99
|
+
) {
|
|
100
|
+
return { type: "removeTrack", trackId: value.trackId };
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
value.type === "moveTrack" &&
|
|
104
|
+
typeof value.trackId === "string" &&
|
|
105
|
+
value.trackId.length <= 64 &&
|
|
106
|
+
typeof value.index === "number" &&
|
|
107
|
+
Number.isInteger(value.index) &&
|
|
108
|
+
value.index >= 0 &&
|
|
109
|
+
value.index < SCORE_LIMITS.maxTracks
|
|
110
|
+
) {
|
|
111
|
+
return { type: "moveTrack", trackId: value.trackId, index: value.index };
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
value.type === "setKey" &&
|
|
115
|
+
(value.key === null ||
|
|
116
|
+
(typeof value.key === "string" &&
|
|
117
|
+
value.key.length <= SCORE_LIMITS.maxNameLength))
|
|
118
|
+
) {
|
|
119
|
+
return { type: "setKey", key: value.key };
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
value.type === "setMeter" &&
|
|
123
|
+
typeof value.beatsPerBar === "number" &&
|
|
124
|
+
Number.isInteger(value.beatsPerBar) &&
|
|
125
|
+
value.beatsPerBar >= 1 &&
|
|
126
|
+
value.beatsPerBar <= SCORE_LIMITS.maxBeatsPerBar
|
|
127
|
+
) {
|
|
128
|
+
return { type: "setMeter", beatsPerBar: value.beatsPerBar };
|
|
129
|
+
}
|
|
94
130
|
if (
|
|
95
131
|
value.type === "updateTrack" &&
|
|
96
132
|
typeof value.trackId === "string" &&
|
|
@@ -115,6 +151,8 @@ function parseOperation(value: unknown): ScoreOperation {
|
|
|
115
151
|
safe.delay = normalizeDelay(patch.delay) ?? null;
|
|
116
152
|
if (patch.reverb !== undefined)
|
|
117
153
|
safe.reverb = normalizeReverb(patch.reverb) ?? null;
|
|
154
|
+
if (patch.sampler !== undefined)
|
|
155
|
+
safe.sampler = normalizeSampler(patch.sampler) ?? null;
|
|
118
156
|
return { type: "updateTrack", trackId: value.trackId, patch: safe };
|
|
119
157
|
}
|
|
120
158
|
if (
|
package/src/agent/provider.ts
CHANGED
|
@@ -6,9 +6,12 @@ import {
|
|
|
6
6
|
PROVIDER_CHOICES,
|
|
7
7
|
readConfig,
|
|
8
8
|
resolveGatewayKey,
|
|
9
|
+
resolveOpenRouterKey,
|
|
9
10
|
type AuthEnv,
|
|
10
11
|
type CredentialSource,
|
|
12
|
+
type DawgConfig,
|
|
11
13
|
type ProviderChoice,
|
|
14
|
+
type SubscriptionSlot,
|
|
12
15
|
} from "../auth/credentials.ts";
|
|
13
16
|
import { systemRunner } from "../auth/runner.ts";
|
|
14
17
|
import {
|
|
@@ -20,45 +23,83 @@ import {
|
|
|
20
23
|
} from "./agent.ts";
|
|
21
24
|
import {
|
|
22
25
|
createGatewayClient,
|
|
26
|
+
createOpenRouterClient,
|
|
27
|
+
type ApiProvider,
|
|
23
28
|
type GatewayClient,
|
|
24
|
-
type GatewayModel,
|
|
25
29
|
} from "./gateway.ts";
|
|
30
|
+
import {
|
|
31
|
+
defaultModelId,
|
|
32
|
+
MODEL_CATALOG,
|
|
33
|
+
modelAlias,
|
|
34
|
+
resolveModelChoice,
|
|
35
|
+
} from "./models.ts";
|
|
26
36
|
import { runTextAgentTurn } from "./xcb-agent.ts";
|
|
27
37
|
import {
|
|
38
|
+
describeReason,
|
|
28
39
|
readCapabilities,
|
|
29
40
|
resolveXcbBin,
|
|
30
41
|
shortModelLabel,
|
|
31
|
-
|
|
42
|
+
type XcbAccount,
|
|
43
|
+
xcbGenerateWithRetry,
|
|
32
44
|
type XcbCapabilities,
|
|
33
45
|
} from "./xcb.ts";
|
|
34
46
|
|
|
35
47
|
/**
|
|
36
|
-
* Which model backend a turn uses. `gateway`
|
|
37
|
-
* loop on
|
|
38
|
-
* generate` with
|
|
48
|
+
* Which model backend a turn uses. `gateway` and `openrouter` run the
|
|
49
|
+
* streaming tool-calling loop on an OpenAI-compatible API; `xcb` emulates
|
|
50
|
+
* tools over `xcb --json generate` with a Codex or Claude subscription;
|
|
51
|
+
* `offline` means only direct commands.
|
|
39
52
|
*/
|
|
53
|
+
export type ApiSelection = Readonly<{
|
|
54
|
+
kind: ApiProvider;
|
|
55
|
+
choice: ProviderChoice;
|
|
56
|
+
apiKey: string;
|
|
57
|
+
source: CredentialSource;
|
|
58
|
+
/** The exact provider model ID (`anthropic/claude-opus-5.5`). */
|
|
59
|
+
modelId: string;
|
|
60
|
+
}>;
|
|
61
|
+
export type XcbSelection = Readonly<{
|
|
62
|
+
kind: "xcb";
|
|
63
|
+
choice: ProviderChoice;
|
|
64
|
+
/** `codex` or `claude` (or another xcb provider for `--xcb`). */
|
|
65
|
+
family: string;
|
|
66
|
+
bin: string;
|
|
67
|
+
account: string;
|
|
68
|
+
accountLabel: string;
|
|
69
|
+
model: string;
|
|
70
|
+
/** xcb admits this account on its first call, which takes longer. */
|
|
71
|
+
admissionPending?: boolean;
|
|
72
|
+
}>;
|
|
40
73
|
export type ProviderSelection =
|
|
74
|
+
| ApiSelection
|
|
75
|
+
| XcbSelection
|
|
41
76
|
| Readonly<{
|
|
42
|
-
kind: "
|
|
77
|
+
kind: "offline";
|
|
43
78
|
choice: ProviderChoice;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| Readonly<{ kind: "offline"; choice: ProviderChoice; reason: string }>;
|
|
79
|
+
reason: string;
|
|
80
|
+
/**
|
|
81
|
+
* Set when a saved choice stopped working (key revoked, account gone):
|
|
82
|
+
* the host says so once and offers the picker, never another provider.
|
|
83
|
+
*/
|
|
84
|
+
invalidSaved?: boolean;
|
|
85
|
+
}>;
|
|
86
|
+
|
|
87
|
+
export function isApiSelection(
|
|
88
|
+
selection: ProviderSelection,
|
|
89
|
+
): selection is ApiSelection {
|
|
90
|
+
return selection.kind === "gateway" || selection.kind === "openrouter";
|
|
91
|
+
}
|
|
58
92
|
|
|
59
93
|
export const LOGIN_HINT = "run `dawg login` to enable the agent";
|
|
60
94
|
/** The small, cheap model used for one-line helpers such as session names. */
|
|
61
95
|
export const GATEWAY_SMALL_MODEL = "anthropic/claude-haiku-4.5";
|
|
96
|
+
export const OPENROUTER_SMALL_MODEL = "anthropic/claude-haiku-4.5";
|
|
97
|
+
|
|
98
|
+
export function apiClient(selection: ApiSelection): GatewayClient {
|
|
99
|
+
return selection.kind === "openrouter"
|
|
100
|
+
? createOpenRouterClient({ apiKey: selection.apiKey })
|
|
101
|
+
: createGatewayClient({ apiKey: selection.apiKey });
|
|
102
|
+
}
|
|
62
103
|
|
|
63
104
|
export function providerChoice(
|
|
64
105
|
env: Readonly<Record<string, string | undefined>>,
|
|
@@ -70,10 +111,42 @@ export function providerChoice(
|
|
|
70
111
|
return saved ?? "auto";
|
|
71
112
|
}
|
|
72
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The model for a key-based provider: `DAWG_MODEL`, then the model saved by
|
|
116
|
+
* `/model` or `dawg login`, then Opus 5.5. Throws on an unknown `DAWG_MODEL`.
|
|
117
|
+
*/
|
|
118
|
+
export function apiModelId(
|
|
119
|
+
service: ApiProvider,
|
|
120
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
121
|
+
config: DawgConfig,
|
|
122
|
+
): string {
|
|
123
|
+
const fromEnv = env.DAWG_MODEL?.trim();
|
|
124
|
+
if (fromEnv) {
|
|
125
|
+
const id = resolveModelChoice(service, fromEnv);
|
|
126
|
+
if (!id)
|
|
127
|
+
throw new Error(
|
|
128
|
+
`unknown DAWG_MODEL "${fromEnv.slice(0, 40)}"; use one of ${MODEL_CATALOG.map((row) => row.alias).join(", ")} or a vendor/model ID`,
|
|
129
|
+
);
|
|
130
|
+
return id;
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
(service === "openrouter" ? config.openrouterModel : config.gatewayModel) ??
|
|
134
|
+
defaultModelId(service)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const FAMILY_NAMES: Readonly<Record<string, string>> = Object.freeze({
|
|
139
|
+
codex: "ChatGPT/Codex subscription",
|
|
140
|
+
claude: "Claude subscription",
|
|
141
|
+
xcb: "xcb account",
|
|
142
|
+
});
|
|
143
|
+
|
|
73
144
|
/**
|
|
74
145
|
* Resolve the provider: `DAWG_PROVIDER`, then the choice saved by `dawg
|
|
75
|
-
* login`, then `auto` (
|
|
76
|
-
*
|
|
146
|
+
* login` or `/model`, then `auto` (AI Gateway key, then OpenRouter key, then
|
|
147
|
+
* a ready Codex or Claude subscription). A saved choice that stops working
|
|
148
|
+
* resolves to `offline` with `invalidSaved`; it never falls through to a
|
|
149
|
+
* different provider.
|
|
77
150
|
*/
|
|
78
151
|
export async function selectProvider(
|
|
79
152
|
auth: AuthEnv = defaultAuthEnv(systemRunner),
|
|
@@ -81,82 +154,129 @@ export async function selectProvider(
|
|
|
81
154
|
): Promise<ProviderSelection> {
|
|
82
155
|
const config = await readConfig(auth);
|
|
83
156
|
const choice = providerChoice(auth.env, config.provider);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
157
|
+
const explicit = choice !== "auto";
|
|
158
|
+
const offline = (reason: string): ProviderSelection => ({
|
|
159
|
+
kind: "offline",
|
|
160
|
+
choice,
|
|
161
|
+
reason,
|
|
162
|
+
...(explicit && config.provider === choice && !auth.env.DAWG_PROVIDER
|
|
163
|
+
? { invalidSaved: true }
|
|
164
|
+
: {}),
|
|
165
|
+
});
|
|
166
|
+
for (const service of ["gateway", "openrouter"] as const) {
|
|
167
|
+
if (choice !== service && choice !== "auto") continue;
|
|
168
|
+
const key =
|
|
169
|
+
service === "gateway"
|
|
170
|
+
? await resolveGatewayKey(auth)
|
|
171
|
+
: await resolveOpenRouterKey(auth);
|
|
172
|
+
if (key) {
|
|
173
|
+
let modelId: string;
|
|
174
|
+
try {
|
|
175
|
+
modelId = apiModelId(service, auth.env, config);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return offline((error as Error).message);
|
|
178
|
+
}
|
|
89
179
|
return {
|
|
90
|
-
kind:
|
|
180
|
+
kind: service,
|
|
91
181
|
choice,
|
|
92
|
-
|
|
182
|
+
apiKey: key.key,
|
|
183
|
+
source: key.source,
|
|
184
|
+
modelId,
|
|
93
185
|
};
|
|
186
|
+
}
|
|
187
|
+
if (choice === service)
|
|
188
|
+
return offline(
|
|
189
|
+
`no ${service === "gateway" ? "AI Gateway" : "OpenRouter"} key any more; run \`dawg login ${service}\``,
|
|
190
|
+
);
|
|
94
191
|
}
|
|
95
192
|
const bin = resolveXcbBin(auth.env, auth.runner);
|
|
96
193
|
if (!bin)
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
? "xcb is not installed; see `dawg login --xcb`"
|
|
103
|
-
: `no model configured; ${LOGIN_HINT}`,
|
|
104
|
-
};
|
|
194
|
+
return offline(
|
|
195
|
+
explicit
|
|
196
|
+
? `xcb is not installed; run \`dawg login ${choice === "xcb" ? "--xcb" : choice}\``
|
|
197
|
+
: `no model configured; ${LOGIN_HINT}`,
|
|
198
|
+
);
|
|
105
199
|
let capabilities: XcbCapabilities;
|
|
106
200
|
try {
|
|
107
201
|
capabilities =
|
|
108
202
|
options.capabilities ?? (await readCapabilities(bin, auth.runner));
|
|
109
203
|
} catch {
|
|
110
|
-
return {
|
|
111
|
-
kind: "offline",
|
|
112
|
-
choice,
|
|
113
|
-
reason: `xcb capabilities unavailable; ${LOGIN_HINT}`,
|
|
114
|
-
};
|
|
204
|
+
return offline(`xcb capabilities unavailable; ${LOGIN_HINT}`);
|
|
115
205
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
account?.available &&
|
|
123
|
-
account.models.some((model) => model.key === saved.model)
|
|
124
|
-
)
|
|
125
|
-
return {
|
|
126
|
-
kind: "xcb",
|
|
127
|
-
choice,
|
|
128
|
-
bin,
|
|
129
|
-
account: account.id,
|
|
130
|
-
accountLabel: account.label,
|
|
131
|
-
model: saved.model,
|
|
132
|
-
...(account.admission === "pending" ? { admissionPending: true } : {}),
|
|
133
|
-
};
|
|
134
|
-
if (choice === "xcb")
|
|
135
|
-
return {
|
|
136
|
-
kind: "offline",
|
|
137
|
-
choice,
|
|
138
|
-
reason: `xcb account ${saved.account.slice(0, 12)} is ${account?.reason ?? "missing"}; run \`dawg login --xcb\``,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
const account = capabilities.accounts.find((row) => row.available);
|
|
142
|
-
if (account)
|
|
143
|
-
return {
|
|
144
|
-
kind: "xcb",
|
|
145
|
-
choice,
|
|
146
|
-
bin,
|
|
147
|
-
account: account.id,
|
|
148
|
-
accountLabel: account.label,
|
|
149
|
-
model: account.models[0]!.key,
|
|
150
|
-
...(account.admission === "pending" ? { admissionPending: true } : {}),
|
|
151
|
-
};
|
|
152
|
-
return {
|
|
153
|
-
kind: "offline",
|
|
206
|
+
const toSelection = (
|
|
207
|
+
family: string,
|
|
208
|
+
account: XcbAccount,
|
|
209
|
+
model: string,
|
|
210
|
+
): XcbSelection => ({
|
|
211
|
+
kind: "xcb",
|
|
154
212
|
choice,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
213
|
+
family,
|
|
214
|
+
bin,
|
|
215
|
+
account: account.id,
|
|
216
|
+
accountLabel: account.label,
|
|
217
|
+
model,
|
|
218
|
+
...(account.admission === "pending" ? { admissionPending: true } : {}),
|
|
219
|
+
});
|
|
220
|
+
const families: SubscriptionSlot[] =
|
|
221
|
+
choice === "auto"
|
|
222
|
+
? ["codex", "claude"]
|
|
223
|
+
: explicit
|
|
224
|
+
? [choice as SubscriptionSlot]
|
|
225
|
+
: [];
|
|
226
|
+
for (const family of families) {
|
|
227
|
+
const saved = config[family];
|
|
228
|
+
const inFamily = (account: XcbAccount) =>
|
|
229
|
+
family === "xcb" || account.provider === family;
|
|
230
|
+
if (saved) {
|
|
231
|
+
const account = capabilities.accounts.find(
|
|
232
|
+
(row) => row.id === saved.account,
|
|
233
|
+
);
|
|
234
|
+
if (
|
|
235
|
+
account?.available &&
|
|
236
|
+
account.models.some((model) => model.key === saved.model)
|
|
237
|
+
)
|
|
238
|
+
return toSelection(
|
|
239
|
+
family === "xcb" ? account.provider : family,
|
|
240
|
+
account,
|
|
241
|
+
saved.model,
|
|
242
|
+
);
|
|
243
|
+
if (choice === family)
|
|
244
|
+
return offline(
|
|
245
|
+
`your ${FAMILY_NAMES[family]} (${account?.label ?? saved.account.slice(0, 12)}) is ${
|
|
246
|
+
!account
|
|
247
|
+
? "no longer listed by xcb"
|
|
248
|
+
: account.available
|
|
249
|
+
? `missing model ${saved.model}`
|
|
250
|
+
: describeReason(account.reason).replace(
|
|
251
|
+
"<account>",
|
|
252
|
+
account.id,
|
|
253
|
+
)
|
|
254
|
+
}; run \`dawg login ${family === "xcb" ? "--xcb" : family}\``,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
// No saved pick: the first usable account, ready before admission pending.
|
|
258
|
+
const usable = capabilities.accounts
|
|
259
|
+
.filter(
|
|
260
|
+
(account) =>
|
|
261
|
+
account.available && inFamily(account) && account.models.length > 0,
|
|
262
|
+
)
|
|
263
|
+
.sort(
|
|
264
|
+
(a, b) =>
|
|
265
|
+
Number(a.admission === "pending") - Number(b.admission === "pending"),
|
|
266
|
+
);
|
|
267
|
+
const account = usable[0];
|
|
268
|
+
if (account)
|
|
269
|
+
return toSelection(
|
|
270
|
+
family === "xcb" ? account.provider : family,
|
|
271
|
+
account,
|
|
272
|
+
account.models[0]!.key,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return offline(
|
|
276
|
+
explicit
|
|
277
|
+
? `no ${FAMILY_NAMES[choice] ?? choice} is ready; run \`dawg login ${choice === "xcb" ? "--xcb" : choice}\``
|
|
278
|
+
: `no model configured; ${LOGIN_HINT}`,
|
|
279
|
+
);
|
|
160
280
|
}
|
|
161
281
|
|
|
162
282
|
/**
|
|
@@ -179,26 +299,35 @@ export function providerFingerprint(
|
|
|
179
299
|
// Presence only: a process's environment cannot change under it, and the
|
|
180
300
|
// fingerprint must never carry secret material.
|
|
181
301
|
parts.push(
|
|
182
|
-
`env:${env.DAWG_PROVIDER ?? "-"}:${env.AI_GATEWAY_API_KEY ? 1 : 0}`,
|
|
302
|
+
`env:${env.DAWG_PROVIDER ?? "-"}:${env.AI_GATEWAY_API_KEY ? 1 : 0}:${env.OPENROUTER_API_KEY ? 1 : 0}`,
|
|
183
303
|
);
|
|
184
304
|
return parts.join("|");
|
|
185
305
|
}
|
|
186
306
|
|
|
187
|
-
/** `opus-5.5 · gateway`, `
|
|
188
|
-
export function providerLabel(
|
|
189
|
-
selection
|
|
190
|
-
|
|
191
|
-
): string {
|
|
192
|
-
if (selection.kind === "gateway") return `${gatewayModel} · gateway`;
|
|
307
|
+
/** `opus-5.5 · gateway`, `sonnet · claude`, or `offline`. */
|
|
308
|
+
export function providerLabel(selection: ProviderSelection): string {
|
|
309
|
+
if (isApiSelection(selection))
|
|
310
|
+
return `${modelAlias(selection.kind, selection.modelId)} · ${selection.kind}`;
|
|
193
311
|
if (selection.kind === "xcb")
|
|
194
|
-
return `${shortModelLabel(selection.model)} ·
|
|
312
|
+
return `${shortModelLabel(selection.model)} · ${selection.family}`;
|
|
195
313
|
return "offline";
|
|
196
314
|
}
|
|
197
315
|
|
|
316
|
+
/** The model half of the label, for the header and spend line. */
|
|
317
|
+
export function selectionModel(
|
|
318
|
+
selection: ProviderSelection,
|
|
319
|
+
): string | undefined {
|
|
320
|
+
if (isApiSelection(selection))
|
|
321
|
+
return modelAlias(selection.kind, selection.modelId);
|
|
322
|
+
if (selection.kind === "xcb") return shortModelLabel(selection.model);
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
198
326
|
export type ProviderTurnOptions = Readonly<{
|
|
199
327
|
selection: ProviderSelection;
|
|
200
328
|
prompt: string;
|
|
201
|
-
model
|
|
329
|
+
/** Overrides the selection's model (tests); normally unset. */
|
|
330
|
+
model?: string;
|
|
202
331
|
host: AgentHost;
|
|
203
332
|
onEvent?: (event: AgentEvent) => void;
|
|
204
333
|
signal?: AbortSignal;
|
|
@@ -220,20 +349,18 @@ export async function runProviderTurn(
|
|
|
220
349
|
...(options.signal ? { signal: options.signal } : {}),
|
|
221
350
|
...(options.budget ? { budget: options.budget } : {}),
|
|
222
351
|
};
|
|
223
|
-
if (selection
|
|
352
|
+
if (isApiSelection(selection))
|
|
224
353
|
return runAgentTurn({
|
|
225
354
|
...common,
|
|
226
|
-
model: options.model,
|
|
227
|
-
client:
|
|
228
|
-
options.gatewayClient ??
|
|
229
|
-
createGatewayClient({ apiKey: selection.apiKey }),
|
|
355
|
+
model: options.model ?? selection.modelId,
|
|
356
|
+
client: options.gatewayClient ?? apiClient(selection),
|
|
230
357
|
});
|
|
231
358
|
if (selection.kind === "xcb") {
|
|
232
359
|
const runner = options.runner ?? systemRunner;
|
|
233
360
|
return runTextAgentTurn({
|
|
234
361
|
...common,
|
|
235
362
|
generate: (prompt, call) =>
|
|
236
|
-
|
|
363
|
+
xcbGenerateWithRetry({
|
|
237
364
|
bin: selection.bin,
|
|
238
365
|
runner,
|
|
239
366
|
account: selection.account,
|
|
@@ -289,15 +416,16 @@ export async function generateText(
|
|
|
289
416
|
? AbortSignal.any([options.signal, timeout])
|
|
290
417
|
: timeout;
|
|
291
418
|
const input = prompt.slice(0, 16_000);
|
|
292
|
-
if (selection
|
|
293
|
-
const client =
|
|
294
|
-
options.gatewayClient ??
|
|
295
|
-
createGatewayClient({ apiKey: selection.apiKey });
|
|
419
|
+
if (isApiSelection(selection)) {
|
|
420
|
+
const client = options.gatewayClient ?? apiClient(selection);
|
|
296
421
|
let text = "";
|
|
297
422
|
for await (const event of client.stream(
|
|
298
423
|
{
|
|
299
424
|
model: "opus-5.5",
|
|
300
|
-
modelId:
|
|
425
|
+
modelId:
|
|
426
|
+
selection.kind === "openrouter"
|
|
427
|
+
? OPENROUTER_SMALL_MODEL
|
|
428
|
+
: GATEWAY_SMALL_MODEL,
|
|
301
429
|
maxTokens,
|
|
302
430
|
messages: [{ role: "user", content: input }],
|
|
303
431
|
maxResponseBytes: 16 * 1024,
|
|
@@ -310,7 +438,7 @@ export async function generateText(
|
|
|
310
438
|
return text.trim().slice(0, 512);
|
|
311
439
|
}
|
|
312
440
|
if (selection.kind === "xcb") {
|
|
313
|
-
const text = await
|
|
441
|
+
const text = await xcbGenerateWithRetry({
|
|
314
442
|
bin: selection.bin,
|
|
315
443
|
runner: options.runner ?? systemRunner,
|
|
316
444
|
account: selection.account,
|
package/src/agent/sse.ts
CHANGED
|
@@ -44,12 +44,16 @@ export async function* readSseData(
|
|
|
44
44
|
total += chunk.value.byteLength;
|
|
45
45
|
if (total > options.maxBytes) throw new SseBudgetError(options.maxBytes);
|
|
46
46
|
buffer += decoder.decode(chunk.value, { stream: true });
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// Scan forward with indexOf and slice the consumed prefix once per
|
|
48
|
+
// chunk. A trailing "\r" is held back: it may be the first half of a
|
|
49
|
+
// "\r\n" split across chunks, and dispatching it early would double
|
|
50
|
+
// an empty line and end the event too soon.
|
|
51
|
+
let start = 0;
|
|
52
|
+
while (true) {
|
|
53
|
+
const next = lineBreak(buffer, start);
|
|
54
|
+
if (next === undefined) break;
|
|
55
|
+
const line = buffer.slice(start, next.index);
|
|
56
|
+
start = next.index + next.width;
|
|
53
57
|
if (line === "") {
|
|
54
58
|
if (data.length > 0) {
|
|
55
59
|
const payload = data.join("\n");
|
|
@@ -60,10 +64,11 @@ export async function* readSseData(
|
|
|
60
64
|
} else if (line.startsWith("data:")) {
|
|
61
65
|
data.push(line.slice(line[5] === " " ? 6 : 5));
|
|
62
66
|
}
|
|
63
|
-
newline = buffer.search(/\r\n|\r|\n/);
|
|
64
67
|
}
|
|
68
|
+
if (start > 0) buffer = buffer.slice(start);
|
|
65
69
|
}
|
|
66
70
|
buffer += decoder.decode();
|
|
71
|
+
if (buffer.endsWith("\r")) buffer = buffer.slice(0, -1);
|
|
67
72
|
if (buffer.startsWith("data:"))
|
|
68
73
|
data.push(buffer.slice(buffer[5] === " " ? 6 : 5));
|
|
69
74
|
if (data.length > 0) {
|
|
@@ -81,6 +86,25 @@ export async function* readSseData(
|
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The next line terminator at or after `from`: "\n", "\r\n", or a lone
|
|
91
|
+
* "\r" that is already followed by another character. A "\r" at the very
|
|
92
|
+
* end of the buffer is not a terminator yet.
|
|
93
|
+
*/
|
|
94
|
+
function lineBreak(
|
|
95
|
+
buffer: string,
|
|
96
|
+
from: number,
|
|
97
|
+
): { index: number; width: number } | undefined {
|
|
98
|
+
const newline = buffer.indexOf("\n", from);
|
|
99
|
+
const carriage = buffer.indexOf("\r", from);
|
|
100
|
+
if (carriage < 0 || (newline >= 0 && newline < carriage))
|
|
101
|
+
return newline < 0 ? undefined : { index: newline, width: 1 };
|
|
102
|
+
if (carriage === buffer.length - 1) return undefined;
|
|
103
|
+
return buffer[carriage + 1] === "\n"
|
|
104
|
+
? { index: carriage, width: 2 }
|
|
105
|
+
: { index: carriage, width: 1 };
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
function abortReason(signal: AbortSignal): Error {
|
|
85
109
|
const reason: unknown = signal.reason;
|
|
86
110
|
if (reason instanceof Error) return reason;
|