@modelworks/quack 0.2.8 → 0.2.9
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/dist/bin.js +20 -1
- package/dist/build-info.json +5 -5
- package/dist/circuit.d.ts +2 -1
- package/dist/circuit.js +48 -13
- package/dist/install.js +4 -0
- package/dist/proxy.js +54 -3
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -247,7 +247,7 @@ async function cmdCircuitAdd(url) {
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
async function cmdCircuitRemove(ref) {
|
|
250
|
-
const result = await removeCircuit(ref);
|
|
250
|
+
const result = await removeCircuit(ref, effectiveApiBase());
|
|
251
251
|
console.log(`Removed circuit '${result.name}' (${result.circuit_uid}).`);
|
|
252
252
|
// Clear default_circuit if it pointed to the removed circuit.
|
|
253
253
|
const config = readConfig();
|
|
@@ -404,6 +404,19 @@ async function cmdAgentDescribe(name) {
|
|
|
404
404
|
console.log(`Default: yes`);
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
|
+
async function cmdAgentSet(name) {
|
|
408
|
+
const agent = getAgent(name);
|
|
409
|
+
if (!agent) {
|
|
410
|
+
console.error(`Unknown agent: ${name}`);
|
|
411
|
+
console.error("Run `quack agent list` to see available agents.");
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
requireQuackProject();
|
|
415
|
+
const config = readConfig();
|
|
416
|
+
config.default_agent = name;
|
|
417
|
+
writeConfig(config);
|
|
418
|
+
console.log(`Default agent set to ${GREEN}${name}${RESET}.`);
|
|
419
|
+
}
|
|
407
420
|
// ---------------------------------------------------------------------------
|
|
408
421
|
// Interactive picker
|
|
409
422
|
// ---------------------------------------------------------------------------
|
|
@@ -713,6 +726,12 @@ function buildCli() {
|
|
|
713
726
|
.action(async (name) => {
|
|
714
727
|
await cmdAgentDescribe(name);
|
|
715
728
|
});
|
|
729
|
+
agent
|
|
730
|
+
.command("set <name>")
|
|
731
|
+
.description("Set the default agent (written to .quack/config.json)")
|
|
732
|
+
.action(async (name) => {
|
|
733
|
+
await cmdAgentSet(name);
|
|
734
|
+
});
|
|
716
735
|
// auth group
|
|
717
736
|
const auth = program
|
|
718
737
|
.command("auth")
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.2.
|
|
3
|
-
"build": "
|
|
4
|
-
"commit": "
|
|
5
|
-
"commitLong": "
|
|
2
|
+
"version": "0.2.9",
|
|
3
|
+
"build": "20261027101714",
|
|
4
|
+
"commit": "a1daf81",
|
|
5
|
+
"commitLong": "a1daf819f41569ed710ee0697ec56db7ab370339",
|
|
6
6
|
"dirty": false,
|
|
7
|
-
"builtAt": "2026-10-
|
|
7
|
+
"builtAt": "2026-10-27T10:17:14.504Z"
|
|
8
8
|
}
|
package/dist/circuit.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare function fromApiAdapterType(apiType: string): string;
|
|
|
25
25
|
* Accepts:
|
|
26
26
|
* - `https://modelworks.ai/circuits/<circuit_uid>`
|
|
27
27
|
* - `https://modelworks.ai/circuits/<circuit_uid>/versions/<version_uid>`
|
|
28
|
+
* - `https://modelworks.ai/<username>/<circuitName>` (slug-based URL)
|
|
28
29
|
* - `<circuit_uid>` or `c-xxx`
|
|
29
30
|
* - `<version_uid>` or `cv-xxx`
|
|
30
31
|
*/
|
|
@@ -99,7 +100,7 @@ export declare function addCircuit(input: string, apiBase: string): Promise<AddR
|
|
|
99
100
|
* Public adaptors are NOT deleted — they belong to the circuit (system-created),
|
|
100
101
|
* not to the quack user. Other users of the same circuit rely on them.
|
|
101
102
|
*/
|
|
102
|
-
export declare function removeCircuit(input: string): Promise<{
|
|
103
|
+
export declare function removeCircuit(input: string, apiBase: string): Promise<{
|
|
103
104
|
circuit_uid: string;
|
|
104
105
|
name: string;
|
|
105
106
|
}>;
|
package/dist/circuit.js
CHANGED
|
@@ -59,16 +59,17 @@ export function fromApiAdapterType(apiType) {
|
|
|
59
59
|
* Accepts:
|
|
60
60
|
* - `https://modelworks.ai/circuits/<circuit_uid>`
|
|
61
61
|
* - `https://modelworks.ai/circuits/<circuit_uid>/versions/<version_uid>`
|
|
62
|
+
* - `https://modelworks.ai/<username>/<circuitName>` (slug-based URL)
|
|
62
63
|
* - `<circuit_uid>` or `c-xxx`
|
|
63
64
|
* - `<version_uid>` or `cv-xxx`
|
|
64
65
|
*/
|
|
65
66
|
export function parseCircuitRef(input) {
|
|
66
67
|
// Bare UIDs first.
|
|
67
68
|
if (input.startsWith("cv-")) {
|
|
68
|
-
return { circuit_uid: "", version_uid: input };
|
|
69
|
+
return { circuit_uid: "", version_uid: input, slug: null };
|
|
69
70
|
}
|
|
70
71
|
if (input.startsWith("c-")) {
|
|
71
|
-
return { circuit_uid: input, version_uid: null };
|
|
72
|
+
return { circuit_uid: input, version_uid: null, slug: null };
|
|
72
73
|
}
|
|
73
74
|
// URLs.
|
|
74
75
|
try {
|
|
@@ -76,16 +77,21 @@ export function parseCircuitRef(input) {
|
|
|
76
77
|
const parts = url.pathname.split("/").filter(Boolean);
|
|
77
78
|
// Expected: ["circuits", "<circuit_uid>"] or ["circuits", "<circuit_uid>", "versions", "<version_uid>"]
|
|
78
79
|
const circuitsIdx = parts.indexOf("circuits");
|
|
79
|
-
if (circuitsIdx
|
|
80
|
-
|
|
80
|
+
if (circuitsIdx !== -1 && circuitsIdx + 1 < parts.length) {
|
|
81
|
+
const circuit_uid = parts[circuitsIdx + 1];
|
|
82
|
+
let version_uid = null;
|
|
83
|
+
const versionsIdx = parts.indexOf("versions", circuitsIdx);
|
|
84
|
+
if (versionsIdx !== -1 && versionsIdx + 1 < parts.length) {
|
|
85
|
+
version_uid = parts[versionsIdx + 1];
|
|
86
|
+
}
|
|
87
|
+
return { circuit_uid, version_uid, slug: null };
|
|
81
88
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
version_uid = parts[versionsIdx + 1];
|
|
89
|
+
// Slug-based URL: ["<username>", "<circuitName>"]
|
|
90
|
+
if (parts.length === 2) {
|
|
91
|
+
const slug = `${parts[0]}/${parts[1]}`;
|
|
92
|
+
return { circuit_uid: "", version_uid: null, slug };
|
|
87
93
|
}
|
|
88
|
-
|
|
94
|
+
throw new Error(`Could not find circuit UID in URL: ${input}`);
|
|
89
95
|
}
|
|
90
96
|
catch (err) {
|
|
91
97
|
throw new Error(`Invalid circuit reference: ${input}. Use a circuit URL, c-xxx, or cv-xxx. (${err.message})`);
|
|
@@ -156,6 +162,16 @@ export async function getUsername(apiBase, userUid) {
|
|
|
156
162
|
const payload = await parseJson(res);
|
|
157
163
|
return unwrap(payload).username ?? "unknown";
|
|
158
164
|
}
|
|
165
|
+
/** Resolve a slug (`"username/circuitName"`) to a circuit UID via the API. */
|
|
166
|
+
async function resolveSlug(apiBase, slug) {
|
|
167
|
+
const res = await authedFetch(apiBase, `/api/slug/${slug}`);
|
|
168
|
+
const data = unwrap(await parseJson(res));
|
|
169
|
+
if (data.entity_type !== "circuit") {
|
|
170
|
+
throw new Error(`'${slug}' resolved to a ${data.entity_type}, not a circuit. ` +
|
|
171
|
+
`Use a circuit URL, c-xxx, or cv-xxx.`);
|
|
172
|
+
}
|
|
173
|
+
return data.entity_uid;
|
|
174
|
+
}
|
|
159
175
|
/**
|
|
160
176
|
* Resolve a circuit reference to a RUNNING version.
|
|
161
177
|
*
|
|
@@ -165,6 +181,11 @@ export async function getUsername(apiBase, userUid) {
|
|
|
165
181
|
*/
|
|
166
182
|
export async function resolveRunningVersion(apiBase, ref) {
|
|
167
183
|
let circuitUid = ref.circuit_uid;
|
|
184
|
+
// Slug-based URL (e.g. https://modelworks.ai/openscale/deepseek-v41-flash):
|
|
185
|
+
// resolve the slug to a circuit UID via the API before proceeding.
|
|
186
|
+
if (!circuitUid && ref.slug) {
|
|
187
|
+
circuitUid = await resolveSlug(apiBase, ref.slug);
|
|
188
|
+
}
|
|
168
189
|
// If only a version UID was given, fetch the version first to discover
|
|
169
190
|
// its parent circuit UID.
|
|
170
191
|
if (!circuitUid && ref.version_uid) {
|
|
@@ -327,11 +348,25 @@ export async function addCircuit(input, apiBase) {
|
|
|
327
348
|
* Public adaptors are NOT deleted — they belong to the circuit (system-created),
|
|
328
349
|
* not to the quack user. Other users of the same circuit rely on them.
|
|
329
350
|
*/
|
|
330
|
-
export async function removeCircuit(input) {
|
|
351
|
+
export async function removeCircuit(input, apiBase) {
|
|
331
352
|
const ref = parseCircuitRef(input);
|
|
332
|
-
|
|
333
|
-
// Find the circuit name from config.
|
|
353
|
+
let circuitUid = ref.circuit_uid;
|
|
334
354
|
const config = readConfig();
|
|
355
|
+
// For slug-based URLs, try matching by the stored `url` first (no API call).
|
|
356
|
+
// If that fails, resolve the slug via the API to get the circuit UID.
|
|
357
|
+
if (ref.slug && !circuitUid) {
|
|
358
|
+
const byUrl = config.circuits.find((c) => c.url === input);
|
|
359
|
+
if (byUrl) {
|
|
360
|
+
circuitUid = byUrl.circuit_uid;
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
circuitUid = await resolveSlug(apiBase, ref.slug);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!circuitUid) {
|
|
367
|
+
throw new Error("Could not resolve circuit UID from the given reference.");
|
|
368
|
+
}
|
|
369
|
+
// Find the circuit name from config.
|
|
335
370
|
const entry = config.circuits.find((c) => c.circuit_uid === circuitUid);
|
|
336
371
|
if (!entry) {
|
|
337
372
|
throw new Error(`Circuit ${circuitUid} is not in this project.`);
|
package/dist/install.js
CHANGED
|
@@ -266,6 +266,10 @@ async function main() {
|
|
|
266
266
|
fs.mkdirSync(binDir, { recursive: true });
|
|
267
267
|
fs.rmSync(distDir, { recursive: true, force: true });
|
|
268
268
|
copyDir(sourceDist, distDir);
|
|
269
|
+
// Write a minimal package.json so Node treats dist/ as ESM.
|
|
270
|
+
// (The source package.json with "type": "module" is at the tarball
|
|
271
|
+
// root, not inside dist/, so it doesn't get copied.)
|
|
272
|
+
fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify({ type: "module" }, null, 2) + "\n");
|
|
269
273
|
spinner.succeed(`Installed to ${installDir}`);
|
|
270
274
|
}
|
|
271
275
|
catch (err) {
|
package/dist/proxy.js
CHANGED
|
@@ -25,9 +25,9 @@ export function startProxy(opts) {
|
|
|
25
25
|
opts.onDebug(line);
|
|
26
26
|
};
|
|
27
27
|
// TCP-level connection log — fires for EVERY request, even before
|
|
28
|
-
// HTTP parsing.
|
|
28
|
+
// HTTP parsing.
|
|
29
29
|
if (opts.onDebug) {
|
|
30
|
-
opts.onDebug(`[tcp]
|
|
30
|
+
opts.onDebug(`[tcp] ${new Date().toISOString()} ${req.method} ${req.url} from ${req.socket.remoteAddress}:${req.socket.remotePort}`);
|
|
31
31
|
}
|
|
32
32
|
try {
|
|
33
33
|
const targetUrl = new URL(req.url ?? "/", opts.apiBase);
|
|
@@ -41,8 +41,45 @@ export function startProxy(opts) {
|
|
|
41
41
|
const chunks = [];
|
|
42
42
|
for await (const chunk of req)
|
|
43
43
|
chunks.push(chunk);
|
|
44
|
-
|
|
44
|
+
let body = chunks.length > 0 ? Buffer.concat(chunks) : null;
|
|
45
45
|
debug(`body read: ${body ? body.length + " bytes" : "(empty)"}`);
|
|
46
|
+
// Strip output token limits from circuit adaptor query requests.
|
|
47
|
+
//
|
|
48
|
+
// OpenCode's openai-compatible provider sends a low default output
|
|
49
|
+
// token limit (~100-128). Reasoning models like DeepSeek V4.1 Flash
|
|
50
|
+
// spend this budget on reasoning before producing content, resulting
|
|
51
|
+
// in empty responses. Stripping lets the circuit's own default_value
|
|
52
|
+
// (e.g. 128000) apply instead.
|
|
53
|
+
//
|
|
54
|
+
// Different SDKs use different field names for the same concept:
|
|
55
|
+
// - OpenAI Chat Completions API: max_tokens / max_completion_tokens
|
|
56
|
+
// - AI SDK: maxOutputTokens
|
|
57
|
+
const isAdaptorQuery = req.method === "POST" &&
|
|
58
|
+
/\/api\/circuit-adaptors\/[^/]+\/query/.test(req.url ?? "");
|
|
59
|
+
if (isAdaptorQuery && body) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
62
|
+
let stripped = false;
|
|
63
|
+
for (const key of [
|
|
64
|
+
"max_tokens",
|
|
65
|
+
"max_completion_tokens",
|
|
66
|
+
"max_output_tokens",
|
|
67
|
+
"maxOutputTokens",
|
|
68
|
+
]) {
|
|
69
|
+
if (key in parsed) {
|
|
70
|
+
delete parsed[key];
|
|
71
|
+
stripped = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (stripped) {
|
|
75
|
+
body = Buffer.from(JSON.stringify(parsed), "utf8");
|
|
76
|
+
debug(`stripped output token limit from request body`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Not JSON or malformed — forward as-is.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
46
83
|
// Build forwarded headers — strip client auth + host, inject our own.
|
|
47
84
|
const headers = {};
|
|
48
85
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
@@ -53,6 +90,9 @@ export function startProxy(opts) {
|
|
|
53
90
|
}
|
|
54
91
|
headers.authorization = `Bearer ${opts.getAccessToken()}`;
|
|
55
92
|
headers.host = targetUrl.host;
|
|
93
|
+
// Update content-length if we modified the body.
|
|
94
|
+
if (body !== null)
|
|
95
|
+
headers["content-length"] = String(body.length);
|
|
56
96
|
if (opts.onDebug) {
|
|
57
97
|
debug(`--- request headers (incoming) ---`);
|
|
58
98
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
@@ -88,6 +128,17 @@ export function startProxy(opts) {
|
|
|
88
128
|
// Forward response status + headers.
|
|
89
129
|
res.statusCode = upstream.status;
|
|
90
130
|
for (const [key, value] of upstream.headers) {
|
|
131
|
+
// Rewrite content-type for circuit adaptor SSE responses.
|
|
132
|
+
// The API returns `application/stream+json` but OpenCode's
|
|
133
|
+
// openai-compatible provider expects `text/event-stream` to
|
|
134
|
+
// parse SSE chunks. Without this, the provider doesn't
|
|
135
|
+
// recognize the stream and the response appears empty.
|
|
136
|
+
if (key.toLowerCase() === "content-type" &&
|
|
137
|
+
isAdaptorQuery &&
|
|
138
|
+
value.includes("application/stream+json")) {
|
|
139
|
+
res.setHeader(key, "text/event-stream; charset=utf-8");
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
91
142
|
res.setHeader(key, value);
|
|
92
143
|
}
|
|
93
144
|
// Capture the circuit run UID for history saving.
|
package/dist/types.d.ts
CHANGED
|
@@ -114,10 +114,16 @@ export interface QuackState {
|
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* Parsed circuit URL or bare UID.
|
|
117
|
+
*
|
|
118
|
+
* For slug-based URLs (`/{username}/{circuitName}`), `slug` is set and
|
|
119
|
+
* `circuit_uid` is empty — the caller must resolve the slug to a circuit
|
|
120
|
+
* UID via the API (`GET /api/slug/{slug}`).
|
|
117
121
|
*/
|
|
118
122
|
export interface ParsedCircuitRef {
|
|
119
123
|
circuit_uid: string;
|
|
120
124
|
version_uid: string | null;
|
|
125
|
+
/** `"username/circuitName"` for slug-based URLs, null for UID-based refs. */
|
|
126
|
+
slug: string | null;
|
|
121
127
|
}
|
|
122
128
|
/**
|
|
123
129
|
* A circuit version record as returned by the ModelWorks API
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelworks/quack",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Launch AI coding agents (OpenCode, and more) with ModelWorks circuits as model providers. OAuth login, local proxy with auth injection, run history, and git-like project config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|