@blockrun/franklin 3.25.2 → 3.25.3
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/agent/llm.js +84 -18
- package/dist/commands/start.js +6 -0
- package/package.json +1 -1
package/dist/agent/llm.js
CHANGED
|
@@ -102,6 +102,58 @@ function linkAbortSignal(parent, child) {
|
|
|
102
102
|
function createModelTimeoutError(stage, model, timeoutMs) {
|
|
103
103
|
return new Error(`Model ${stage} timed out after ${timeoutMs}ms on ${model}`);
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Walk a tool-schema object and drop any `enum` whose entries are strings
|
|
107
|
+
* containing "/". Grok's request validator rejects such enums outright (see
|
|
108
|
+
* the call site for the verbatim upstream error). The model still sees the
|
|
109
|
+
* intended values via the tool's description text, so dropping the schema-
|
|
110
|
+
* level constraint is purely a compatibility shim — no behavioral loss.
|
|
111
|
+
*/
|
|
112
|
+
function stripSlashEnumsForGrok(node) {
|
|
113
|
+
if (Array.isArray(node))
|
|
114
|
+
return node.map(stripSlashEnumsForGrok);
|
|
115
|
+
if (!node || typeof node !== 'object')
|
|
116
|
+
return node;
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const [k, v] of Object.entries(node)) {
|
|
119
|
+
if (k === 'enum' &&
|
|
120
|
+
Array.isArray(v) &&
|
|
121
|
+
v.some((x) => typeof x === 'string' && x.includes('/'))) {
|
|
122
|
+
continue; // drop the constraint entirely
|
|
123
|
+
}
|
|
124
|
+
out[k] = stripSlashEnumsForGrok(v);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Wrap `fetch()` so that undici's opaque `TypeError: fetch failed` is
|
|
130
|
+
* replaced with the underlying network reason (ECONNRESET, UND_ERR_*,
|
|
131
|
+
* certificate, DNS, etc.). Without this, every transient connection blip
|
|
132
|
+
* surfaces to the user as "Network: fetch failed" with no way to tell
|
|
133
|
+
* whether it's their network, the gateway, or the upstream provider.
|
|
134
|
+
*
|
|
135
|
+
* Verified 2026-06-03: stress-testing claude-sonnet-4.6 reproduces
|
|
136
|
+
* intermittent "fetch failed" (cheetah's report on 3.25.0). With this
|
|
137
|
+
* helper the message becomes e.g. "fetch failed (UND_ERR_SOCKET: other
|
|
138
|
+
* side closed)" which is actionable.
|
|
139
|
+
*/
|
|
140
|
+
async function fetchWithUnwrappedCause(url, init) {
|
|
141
|
+
try {
|
|
142
|
+
return await fetch(url, init);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err instanceof Error && err.message === 'fetch failed' && err.cause) {
|
|
146
|
+
const cause = err.cause;
|
|
147
|
+
const detail = cause.code || cause.errno || cause.message;
|
|
148
|
+
if (detail) {
|
|
149
|
+
const enriched = new Error(`fetch failed (${detail})`);
|
|
150
|
+
enriched.cause = err.cause;
|
|
151
|
+
throw enriched;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
105
157
|
async function withAbortableTimeout(work, controller, timeoutError, timeoutMs) {
|
|
106
158
|
if (timeoutMs <= 0)
|
|
107
159
|
return work();
|
|
@@ -416,6 +468,20 @@ export class ModelClient {
|
|
|
416
468
|
if (requestPayload['tool_choice'] !== undefined && modelDoesNotSupportToolChoice(request.model)) {
|
|
417
469
|
delete requestPayload['tool_choice'];
|
|
418
470
|
}
|
|
471
|
+
// ── Grok: strip enum constraints containing "/" from tool schemas ────────
|
|
472
|
+
// Verified 2026-06-03 via Franklin repro: xAI's request validator hard-
|
|
473
|
+
// rejects any tool-schema enum string containing "/", e.g.
|
|
474
|
+
// "[engine_imposed] /properties/endpoint/enum/0: '/' in 'enum' string
|
|
475
|
+
// value is currently not supported"
|
|
476
|
+
// The Surf tools (and a few others) use endpoint paths like
|
|
477
|
+
// "market/ranking" as enum values to constrain the model's choice. The
|
|
478
|
+
// path list is also enumerated in each tool's description text, so the
|
|
479
|
+
// model still sees the legal values — only the schema-level constraint
|
|
480
|
+
// gets dropped. Other providers keep the enum unchanged.
|
|
481
|
+
if (request.model.startsWith('xai/') && Array.isArray(requestPayload['tools'])) {
|
|
482
|
+
const tools = requestPayload['tools'];
|
|
483
|
+
requestPayload['tools'] = tools.map((tool) => stripSlashEnumsForGrok(tool));
|
|
484
|
+
}
|
|
419
485
|
// ── GLM-specific optimizations ───────────────────────────────────────────
|
|
420
486
|
// GLM models work best with temperature=0.8 per official zai spec.
|
|
421
487
|
// Enable thinking mode only for explicit reasoning variants (-thinking-).
|
|
@@ -481,20 +547,20 @@ export class ModelClient {
|
|
|
481
547
|
// session reached ≥3 messages (system + tool + 3 = 5). See issue #73.
|
|
482
548
|
requestPayload = applyAnthropicPromptCaching(requestPayload, request);
|
|
483
549
|
}
|
|
484
|
-
// ──
|
|
485
|
-
//
|
|
486
|
-
// "developer"
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
550
|
+
// ── No client-side system → developer role rewrite for GPT-5/Codex ─────
|
|
551
|
+
// We used to move the top-level `system` field into `messages[0]` with
|
|
552
|
+
// role "developer" for GPT-5/Codex (OpenAI docs say that role gets
|
|
553
|
+
// stronger instruction-following weight). But the BlockRun gateway
|
|
554
|
+
// speaks Anthropic Messages, which only accepts user|assistant in
|
|
555
|
+
// messages[] — the developer-role payload returns HTTP 400 from the
|
|
556
|
+
// gateway's protocol validator BEFORE it ever reaches OpenAI:
|
|
557
|
+
// {"error":{"message":"messages.0.role: Invalid option: expected
|
|
558
|
+
// one of \"user\"|\"assistant\""}}
|
|
559
|
+
// Verified 2026-06-03 via direct curl + Franklin repro: all GPT-5
|
|
560
|
+
// family models (mini/nano/5.4/5.5) were silently failing under
|
|
561
|
+
// headless -p mode. Keep `system` as a top-level field and let the
|
|
562
|
+
// gateway translate to whatever the upstream needs (it already knows
|
|
563
|
+
// gpt-5 expects developer role internally).
|
|
498
564
|
const body = JSON.stringify(requestPayload);
|
|
499
565
|
const endpoint = `${this.apiUrl}/v1/messages`;
|
|
500
566
|
const headers = {
|
|
@@ -515,7 +581,7 @@ export class ModelClient {
|
|
|
515
581
|
const requestController = new AbortController();
|
|
516
582
|
const unlinkAbort = linkAbortSignal(signal, requestController);
|
|
517
583
|
try {
|
|
518
|
-
let response = await withAbortableTimeout(() =>
|
|
584
|
+
let response = await withAbortableTimeout(() => fetchWithUnwrappedCause(endpoint, {
|
|
519
585
|
method: 'POST',
|
|
520
586
|
headers,
|
|
521
587
|
body,
|
|
@@ -530,7 +596,7 @@ export class ModelClient {
|
|
|
530
596
|
yield { kind: 'error', payload: { message: 'Payment signing failed' } };
|
|
531
597
|
return;
|
|
532
598
|
}
|
|
533
|
-
response = await withAbortableTimeout(() =>
|
|
599
|
+
response = await withAbortableTimeout(() => fetchWithUnwrappedCause(endpoint, {
|
|
534
600
|
method: 'POST',
|
|
535
601
|
headers: { ...headers, ...paymentHeader },
|
|
536
602
|
body,
|
|
@@ -580,7 +646,7 @@ export class ModelClient {
|
|
|
580
646
|
if (this.debug) {
|
|
581
647
|
console.error(`[franklin] tool_choice rejected by upstream; retrying without it (model=${request.model})`);
|
|
582
648
|
}
|
|
583
|
-
response = await withAbortableTimeout(() =>
|
|
649
|
+
response = await withAbortableTimeout(() => fetchWithUnwrappedCause(endpoint, {
|
|
584
650
|
method: 'POST',
|
|
585
651
|
headers,
|
|
586
652
|
body: retryBody,
|
|
@@ -592,7 +658,7 @@ export class ModelClient {
|
|
|
592
658
|
yield { kind: 'error', payload: { message: 'Payment signing failed' } };
|
|
593
659
|
return;
|
|
594
660
|
}
|
|
595
|
-
response = await withAbortableTimeout(() =>
|
|
661
|
+
response = await withAbortableTimeout(() => fetchWithUnwrappedCause(endpoint, {
|
|
596
662
|
method: 'POST',
|
|
597
663
|
headers: { ...headers, ...paymentHeader },
|
|
598
664
|
body: retryBody,
|
package/dist/commands/start.js
CHANGED
|
@@ -420,6 +420,12 @@ async function runOneShot(agentConfig, prompt) {
|
|
|
420
420
|
}
|
|
421
421
|
else if (event.kind === 'turn_done') {
|
|
422
422
|
exitCode = oneShotExitCodeForTurnReason(event.reason);
|
|
423
|
+
// Without this, headless callers see exit 1 + zero stderr — impossible
|
|
424
|
+
// to triage. Verified 2026-06-03: GPT-5 family failing with HTTP 400
|
|
425
|
+
// from the gateway looked identical to a network timeout in `-p` mode.
|
|
426
|
+
if (event.reason !== 'completed' && event.error) {
|
|
427
|
+
process.stderr.write(`\n${event.error}\n`);
|
|
428
|
+
}
|
|
423
429
|
process.stdout.write('\n');
|
|
424
430
|
}
|
|
425
431
|
});
|
package/package.json
CHANGED