@blockrun/franklin 3.22.0 → 3.23.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/dist/agent/context.js +1 -1
- package/dist/agent/llm.js +20 -7
- package/dist/agent/loop.js +29 -0
- package/dist/agent/optimize.js +3 -2
- package/dist/agent/repair/flatten.d.ts +32 -0
- package/dist/agent/repair/flatten.js +77 -0
- package/dist/agent/repair/index.d.ts +66 -0
- package/dist/agent/repair/index.js +77 -0
- package/dist/agent/repair/scavenge.d.ts +12 -0
- package/dist/agent/repair/scavenge.js +193 -0
- package/dist/agent/repair/truncation.d.ts +17 -0
- package/dist/agent/repair/truncation.js +94 -0
- package/dist/agent/tokens.js +2 -1
- package/dist/commands/init.js +1 -1
- package/dist/pricing.js +2 -1
- package/dist/proxy/server.js +2 -1
- package/dist/router/index.js +6 -5
- package/dist/router/vision.js +1 -0
- package/dist/ui/model-picker.js +3 -2
- package/package.json +2 -2
package/dist/agent/context.js
CHANGED
|
@@ -242,7 +242,7 @@ You run on the BlockRun AI Gateway. When the user asks you to "test the BlockRun
|
|
|
242
242
|
- \`GET /.well-known/x402\` — x402 resource list with prices
|
|
243
243
|
|
|
244
244
|
**LLM (POST, x402-paid)**
|
|
245
|
-
- \`POST /v1/chat/completions\` — OpenAI-compatible. Body: \`{ model, messages, stream?, tools?, max_tokens?, temperature? }\`. \`model\` MUST come from \`GET /v1/models\` (real frontier examples on the gateway as of 2026-05: \`anthropic/claude-sonnet-4.6\`, \`anthropic/claude-opus-4.
|
|
245
|
+
- \`POST /v1/chat/completions\` — OpenAI-compatible. Body: \`{ model, messages, stream?, tools?, max_tokens?, temperature? }\`. \`model\` MUST come from \`GET /v1/models\` (real frontier examples on the gateway as of 2026-05: \`anthropic/claude-sonnet-4.6\`, \`anthropic/claude-opus-4.8\`, \`deepseek/deepseek-v4-pro\`, \`zai/glm-5.1\`, \`nvidia/qwen3-coder-480b\`, \`openai/gpt-5-nano\`). Do NOT invent versions like \`openai/gpt-5.1\` or \`xai/grok-5\` — those don't exist; the gateway 400s with the valid list in the error body, so when in doubt fetch \`GET /v1/models\` first.
|
|
246
246
|
- \`POST /v1/messages\` — Anthropic-compatible. Body: \`{ model, messages, max_tokens, system?, tools? }\`.
|
|
247
247
|
|
|
248
248
|
**Media (POST, x402-paid; GET to poll async jobs)**
|
package/dist/agent/llm.js
CHANGED
|
@@ -9,6 +9,7 @@ import { appendSettlementRow } from '../stats/cost-log.js';
|
|
|
9
9
|
import { routeRequest, parseRoutingProfile } from '../router/index.js';
|
|
10
10
|
import { ThinkTagStripper } from './think-tag-stripper.js';
|
|
11
11
|
import { isNemotronProseModel, stripNemotronProse } from './nemotron-prose-stripper.js';
|
|
12
|
+
import { repairAndParseArgs } from './repair/index.js';
|
|
12
13
|
// Reasoning-tier models the gateway routes to that reject `tool_choice`
|
|
13
14
|
// outright. Pattern: OpenAI o1/o3 family + DeepSeek's reasoner variant.
|
|
14
15
|
// Add new entries as their 400 errors appear in real sessions; this is
|
|
@@ -195,6 +196,8 @@ export function modelHasExtendedThinking(model) {
|
|
|
195
196
|
const m = model.toLowerCase();
|
|
196
197
|
// Excluded: Opus 4.7+ uses adaptive thinking; sending `thinking: enabled`
|
|
197
198
|
// causes the API to 400.
|
|
199
|
+
if (m.includes('opus-4.8') || m.includes('opus-4-8'))
|
|
200
|
+
return false;
|
|
198
201
|
if (m.includes('opus-4.7') || m.includes('opus-4-7'))
|
|
199
202
|
return false;
|
|
200
203
|
return (m.includes('opus-4.6') || m.includes('opus-4-6') ||
|
|
@@ -774,17 +777,27 @@ export class ModelClient {
|
|
|
774
777
|
if (currentToolId) {
|
|
775
778
|
let parsedInput = {};
|
|
776
779
|
let inputParseError = false;
|
|
780
|
+
// First try strict parse; on failure, fall back to the
|
|
781
|
+
// truncation-repair pipeline (closes unbalanced braces,
|
|
782
|
+
// trims trailing commas, fills dangling keys with null).
|
|
783
|
+
// Saves a turn whenever max_tokens cut a tool_use mid-emit.
|
|
777
784
|
try {
|
|
778
785
|
parsedInput = JSON.parse(currentToolInput || '{}');
|
|
779
786
|
}
|
|
780
787
|
catch (parseErr) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
+
const repaired = repairAndParseArgs(currentToolInput || '{}');
|
|
789
|
+
if (repaired) {
|
|
790
|
+
parsedInput = repaired.input;
|
|
791
|
+
if (this.debug && repaired.repaired) {
|
|
792
|
+
console.error(`[franklin] repaired truncated tool_use JSON for ${currentToolName}: ${repaired.notes.join('; ')}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
inputParseError = true;
|
|
797
|
+
if (this.debug) {
|
|
798
|
+
console.error(`[franklin] Malformed tool input JSON for ${currentToolName}: ${parseErr.message}`);
|
|
799
|
+
console.error(`[franklin] Raw input was: ${currentToolInput.slice(0, 200)}`);
|
|
800
|
+
}
|
|
788
801
|
}
|
|
789
802
|
}
|
|
790
803
|
if (inputParseError) {
|
package/dist/agent/loop.js
CHANGED
|
@@ -14,6 +14,7 @@ import { StreamingExecutor } from './streaming-executor.js';
|
|
|
14
14
|
import { optimizeHistory, CAPPED_MAX_TOKENS, ESCALATED_MAX_TOKENS, getMaxOutputTokens } from './optimize.js';
|
|
15
15
|
import { classifyAgentError } from './error-classifier.js';
|
|
16
16
|
import { SessionToolGuard } from './tool-guard.js';
|
|
17
|
+
import { ToolCallRepair } from './repair/index.js';
|
|
17
18
|
import { resetToolSessionState } from '../tools/index.js';
|
|
18
19
|
import { CORE_TOOL_NAMES, dynamicToolsEnabled } from '../tools/tool-categories.js';
|
|
19
20
|
import { createActivateToolCapability } from '../tools/activate.js';
|
|
@@ -608,6 +609,11 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
608
609
|
// outputs, or paths. Fed into opt-in telemetry at session end.
|
|
609
610
|
const sessionToolCounts = new Map();
|
|
610
611
|
const toolGuard = new SessionToolGuard();
|
|
612
|
+
// Recovers tool calls that the model leaked into the text or thinking
|
|
613
|
+
// channels instead of the structured tool_use channel. See
|
|
614
|
+
// src/agent/repair/scavenge.ts for the failure modes — most common on
|
|
615
|
+
// DeepSeek R1 and small Qwen/Llama variants behind the BlockRun gateway.
|
|
616
|
+
const callRepair = new ToolCallRepair({ allowedToolNames: activeTools });
|
|
611
617
|
const persistSessionMeta = () => {
|
|
612
618
|
updateSessionMeta(sessionId, {
|
|
613
619
|
model: config.model,
|
|
@@ -1302,6 +1308,29 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1302
1308
|
responseParts = result.content;
|
|
1303
1309
|
usage = result.usage;
|
|
1304
1310
|
stopReason = result.stopReason;
|
|
1311
|
+
// ── Tool-call scavenge ──
|
|
1312
|
+
// Recover tool calls the model emitted as text/thinking instead of
|
|
1313
|
+
// structured tool_use blocks. Common on DeepSeek R1 (leaks JSON
|
|
1314
|
+
// into reasoning_content) and small Qwen/Llama variants. If the
|
|
1315
|
+
// scavenger finds anything, splice it into responseParts so the
|
|
1316
|
+
// empty-response and stalled-intent checks below see tools.
|
|
1317
|
+
{
|
|
1318
|
+
const declaredCalls = responseParts.filter((p) => p.type === 'tool_use');
|
|
1319
|
+
const reasoningText = responseParts
|
|
1320
|
+
.filter((p) => p.type === 'thinking')
|
|
1321
|
+
.map(p => p.thinking)
|
|
1322
|
+
.join('\n');
|
|
1323
|
+
const contentText = responseParts
|
|
1324
|
+
.filter((p) => p.type === 'text')
|
|
1325
|
+
.map(p => p.text)
|
|
1326
|
+
.join('\n');
|
|
1327
|
+
const repaired = callRepair.process(declaredCalls, reasoningText || null, contentText || null);
|
|
1328
|
+
if (repaired.report.scavenged > 0) {
|
|
1329
|
+
const novelCalls = repaired.calls.slice(declaredCalls.length);
|
|
1330
|
+
responseParts = [...responseParts, ...novelCalls];
|
|
1331
|
+
logger.warn(`[franklin] scavenged ${repaired.report.scavenged} leaked tool call(s) from ${config.model}: ${repaired.report.notes.join('; ')}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1305
1334
|
// ── Empty response recovery ──
|
|
1306
1335
|
// If the model returns nothing, DON'T just retry the same model with the same input.
|
|
1307
1336
|
// That's deterministic waste. Instead: switch to a different model — then give up and tell the user.
|
package/dist/agent/optimize.js
CHANGED
|
@@ -21,10 +21,11 @@ export const CAPPED_MAX_TOKENS = 16_384;
|
|
|
21
21
|
export const ESCALATED_MAX_TOKENS = 65_536;
|
|
22
22
|
/** Per-model max output tokens — prevents requesting more than the model supports */
|
|
23
23
|
const MODEL_MAX_OUTPUT = {
|
|
24
|
-
// Opus 4.7
|
|
25
|
-
// (anthropic/claude-opus-4.
|
|
24
|
+
// Opus 4.8 / 4.7 support 128k output per the BlockRun gateway model entry
|
|
25
|
+
// (anthropic/claude-opus-4.8 maxOutput: 128000). Bumping from 32k to
|
|
26
26
|
// 128k unlocks the full headroom — runaway generations are gated
|
|
27
27
|
// separately by CAPPED_MAX_TOKENS / ESCALATED_MAX_TOKENS budgets.
|
|
28
|
+
'anthropic/claude-opus-4.8': 128_000,
|
|
28
29
|
'anthropic/claude-opus-4.7': 128_000,
|
|
29
30
|
'anthropic/claude-opus-4.6': 32_000,
|
|
30
31
|
'anthropic/claude-sonnet-4.6': 64_000,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema flatten — ported from reasonix (MIT) and adapted to Franklin's
|
|
3
|
+
* CapabilitySchema shape. Deep / wide schemas get dropped or hallucinated
|
|
4
|
+
* by some models (DeepSeek R1, smaller Llamas, some Qwen variants); present
|
|
5
|
+
* them as dot-paths and re-nest before dispatch.
|
|
6
|
+
*
|
|
7
|
+
* Pure functions; no side effects. Wire into a tool registry via
|
|
8
|
+
* analyzeSchema(spec.input_schema) at registration time, then call
|
|
9
|
+
* flattenSchema() on the spec sent to the model and nestArguments() on
|
|
10
|
+
* the parsed call arguments before invoking the handler.
|
|
11
|
+
*/
|
|
12
|
+
import type { CapabilitySchema } from '../types.js';
|
|
13
|
+
/** Loose recursive schema — properties of a CapabilitySchema are typed
|
|
14
|
+
* `unknown`, but in practice they are JSON-Schema-like objects. */
|
|
15
|
+
export interface SchemaNode {
|
|
16
|
+
type?: string | string[];
|
|
17
|
+
properties?: Record<string, SchemaNode>;
|
|
18
|
+
required?: string[];
|
|
19
|
+
items?: SchemaNode;
|
|
20
|
+
[k: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface FlattenDecision {
|
|
23
|
+
shouldFlatten: boolean;
|
|
24
|
+
leafCount: number;
|
|
25
|
+
maxDepth: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function analyzeSchema(schema: SchemaNode | CapabilitySchema | undefined, opts?: {
|
|
28
|
+
leafLimit?: number;
|
|
29
|
+
depthLimit?: number;
|
|
30
|
+
}): FlattenDecision;
|
|
31
|
+
export declare function flattenSchema(schema: SchemaNode | CapabilitySchema): CapabilitySchema;
|
|
32
|
+
export declare function nestArguments(flatArgs: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Caller defines the trigger thresholds; reasonix's defaults are 10/2. */
|
|
2
|
+
const DEFAULT_LEAF_LIMIT = 10;
|
|
3
|
+
const DEFAULT_DEPTH_LIMIT = 2;
|
|
4
|
+
export function analyzeSchema(schema, opts = {}) {
|
|
5
|
+
if (!schema)
|
|
6
|
+
return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
|
|
7
|
+
const leafLimit = opts.leafLimit ?? DEFAULT_LEAF_LIMIT;
|
|
8
|
+
const depthLimit = opts.depthLimit ?? DEFAULT_DEPTH_LIMIT;
|
|
9
|
+
let leafCount = 0;
|
|
10
|
+
let maxDepth = 0;
|
|
11
|
+
walk(schema, 0, (depth, isLeaf) => {
|
|
12
|
+
if (isLeaf)
|
|
13
|
+
leafCount++;
|
|
14
|
+
if (depth > maxDepth)
|
|
15
|
+
maxDepth = depth;
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
shouldFlatten: leafCount > leafLimit || maxDepth > depthLimit,
|
|
19
|
+
leafCount,
|
|
20
|
+
maxDepth,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function flattenSchema(schema) {
|
|
24
|
+
const flatProps = {};
|
|
25
|
+
const required = [];
|
|
26
|
+
collect('', schema, flatProps, required, true);
|
|
27
|
+
return {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: flatProps,
|
|
30
|
+
required,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function nestArguments(flatArgs) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [key, value] of Object.entries(flatArgs)) {
|
|
36
|
+
setByPath(out, key.split('.'), value);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function walk(schema, depth, visit) {
|
|
41
|
+
if (schema.type === 'object' && schema.properties) {
|
|
42
|
+
for (const child of Object.values(schema.properties)) {
|
|
43
|
+
walk(child, depth + 1, visit);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (schema.type === 'array' && schema.items) {
|
|
48
|
+
walk(schema.items, depth + 1, visit);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
visit(depth, true);
|
|
52
|
+
}
|
|
53
|
+
function collect(prefix, schema, out, required, isRootRequired) {
|
|
54
|
+
if (schema.type === 'object' && schema.properties) {
|
|
55
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
56
|
+
for (const [key, child] of Object.entries(schema.properties)) {
|
|
57
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
58
|
+
const childRequired = isRootRequired && requiredSet.has(key);
|
|
59
|
+
collect(nextPrefix, child, out, required, childRequired);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
out[prefix] = schema;
|
|
64
|
+
if (isRootRequired)
|
|
65
|
+
required.push(prefix);
|
|
66
|
+
}
|
|
67
|
+
function setByPath(target, path, value) {
|
|
68
|
+
let cur = target;
|
|
69
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
70
|
+
const key = path[i];
|
|
71
|
+
const next = cur[key];
|
|
72
|
+
if (typeof next !== 'object' || next === null)
|
|
73
|
+
cur[key] = {};
|
|
74
|
+
cur = cur[key];
|
|
75
|
+
}
|
|
76
|
+
cur[path[path.length - 1]] = value;
|
|
77
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-call repair pipeline — ported and adapted from reasonix (MIT).
|
|
3
|
+
*
|
|
4
|
+
* The pipeline has three layers, each used at a different boundary:
|
|
5
|
+
*
|
|
6
|
+
* 1. `repairTruncatedJson(rawArgs)` — call at the LLM-client boundary,
|
|
7
|
+
* right before JSON.parse on the streamed tool_use input. Catches
|
|
8
|
+
* max_tokens-cut-mid-structure and rebalances the braces.
|
|
9
|
+
* 2. `ToolCallRepair.process(calls, reasoning, content)` — call once
|
|
10
|
+
* the assistant turn has finished but before dispatch. Scavenges
|
|
11
|
+
* tool calls the model leaked into text/reasoning channels and
|
|
12
|
+
* merges them in (deduped).
|
|
13
|
+
* 3. `analyzeSchema` + `flattenSchema` + `nestArguments` — apply at
|
|
14
|
+
* tool-registration time for tools whose schemas are deep or wide
|
|
15
|
+
* enough that smaller models drop required params.
|
|
16
|
+
*
|
|
17
|
+
* Storm suppression is intentionally not part of this pipeline.
|
|
18
|
+
* Franklin's `SessionToolGuard` (src/agent/tool-guard.ts) already does
|
|
19
|
+
* per-tool repeat suppression with richer logic — Jaccard search
|
|
20
|
+
* families, mtime-aware Read cache, per-tool circuit breakers.
|
|
21
|
+
*/
|
|
22
|
+
import type { CapabilityInvocation } from '../types.js';
|
|
23
|
+
export { analyzeSchema, flattenSchema, nestArguments } from './flatten.js';
|
|
24
|
+
export type { FlattenDecision, SchemaNode } from './flatten.js';
|
|
25
|
+
export { repairTruncatedJson } from './truncation.js';
|
|
26
|
+
export type { TruncationRepairResult } from './truncation.js';
|
|
27
|
+
export { scavengeToolCalls } from './scavenge.js';
|
|
28
|
+
export type { ScavengeOptions, ScavengeResult } from './scavenge.js';
|
|
29
|
+
export interface RepairReport {
|
|
30
|
+
scavenged: number;
|
|
31
|
+
duplicatesDropped: number;
|
|
32
|
+
notes: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface ToolCallRepairOptions {
|
|
35
|
+
allowedToolNames: ReadonlySet<string>;
|
|
36
|
+
maxScavenge?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Boundary-level helper: parse tool-use argument JSON with truncation
|
|
39
|
+
* recovery. Returns `null` if every attempt fails and the caller should
|
|
40
|
+
* reject the call (better than dispatching with `{}`).
|
|
41
|
+
*
|
|
42
|
+
* Usage at the streaming-client boundary:
|
|
43
|
+
* const args = repairAndParseArgs(jsonAccumulator);
|
|
44
|
+
* if (args == null) return reject("invalid JSON in tool_use");
|
|
45
|
+
*/
|
|
46
|
+
export declare function repairAndParseArgs(raw: string): {
|
|
47
|
+
input: Record<string, unknown>;
|
|
48
|
+
repaired: boolean;
|
|
49
|
+
notes: string[];
|
|
50
|
+
} | null;
|
|
51
|
+
export declare class ToolCallRepair {
|
|
52
|
+
private readonly opts;
|
|
53
|
+
constructor(opts: ToolCallRepairOptions);
|
|
54
|
+
/**
|
|
55
|
+
* Scavenge leaked tool calls from text/reasoning channels and merge
|
|
56
|
+
* into the declared list, deduped.
|
|
57
|
+
*
|
|
58
|
+
* @param declaredCalls Tool calls the model emitted structurally.
|
|
59
|
+
* @param reasoningText Optional reasoning_content / thinking text.
|
|
60
|
+
* @param contentText Optional plain text-channel content.
|
|
61
|
+
*/
|
|
62
|
+
process(declaredCalls: CapabilityInvocation[], reasoningText: string | null, contentText?: string | null): {
|
|
63
|
+
calls: CapabilityInvocation[];
|
|
64
|
+
report: RepairReport;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { scavengeToolCalls } from './scavenge.js';
|
|
2
|
+
import { repairTruncatedJson } from './truncation.js';
|
|
3
|
+
export { analyzeSchema, flattenSchema, nestArguments } from './flatten.js';
|
|
4
|
+
export { repairTruncatedJson } from './truncation.js';
|
|
5
|
+
export { scavengeToolCalls } from './scavenge.js';
|
|
6
|
+
/** Boundary-level helper: parse tool-use argument JSON with truncation
|
|
7
|
+
* recovery. Returns `null` if every attempt fails and the caller should
|
|
8
|
+
* reject the call (better than dispatching with `{}`).
|
|
9
|
+
*
|
|
10
|
+
* Usage at the streaming-client boundary:
|
|
11
|
+
* const args = repairAndParseArgs(jsonAccumulator);
|
|
12
|
+
* if (args == null) return reject("invalid JSON in tool_use");
|
|
13
|
+
*/
|
|
14
|
+
export function repairAndParseArgs(raw) {
|
|
15
|
+
const r = repairTruncatedJson(raw);
|
|
16
|
+
if (r.fallback)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(r.repaired);
|
|
20
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
21
|
+
return { input: parsed, repaired: r.changed, notes: r.notes };
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class ToolCallRepair {
|
|
30
|
+
opts;
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
this.opts = opts;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Scavenge leaked tool calls from text/reasoning channels and merge
|
|
36
|
+
* into the declared list, deduped.
|
|
37
|
+
*
|
|
38
|
+
* @param declaredCalls Tool calls the model emitted structurally.
|
|
39
|
+
* @param reasoningText Optional reasoning_content / thinking text.
|
|
40
|
+
* @param contentText Optional plain text-channel content.
|
|
41
|
+
*/
|
|
42
|
+
process(declaredCalls, reasoningText, contentText = null) {
|
|
43
|
+
const report = { scavenged: 0, duplicatesDropped: 0, notes: [] };
|
|
44
|
+
const combined = [reasoningText ?? '', contentText ?? ''].filter(Boolean).join('\n');
|
|
45
|
+
const scavenged = scavengeToolCalls(combined || null, {
|
|
46
|
+
allowedNames: this.opts.allowedToolNames,
|
|
47
|
+
maxCalls: this.opts.maxScavenge ?? 4,
|
|
48
|
+
});
|
|
49
|
+
const seenSignatures = new Set(declaredCalls.map(signature));
|
|
50
|
+
const merged = [...declaredCalls];
|
|
51
|
+
for (const sc of scavenged.calls) {
|
|
52
|
+
const sig = signature(sc);
|
|
53
|
+
if (seenSignatures.has(sig)) {
|
|
54
|
+
report.duplicatesDropped++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
merged.push(sc);
|
|
58
|
+
report.scavenged++;
|
|
59
|
+
seenSignatures.add(sig);
|
|
60
|
+
}
|
|
61
|
+
report.notes.push(...scavenged.notes);
|
|
62
|
+
return { calls: merged, report };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function signature(call) {
|
|
66
|
+
return `${call.name}::${stableStringify(call.input)}`;
|
|
67
|
+
}
|
|
68
|
+
function stableStringify(value) {
|
|
69
|
+
if (value === null || typeof value !== 'object')
|
|
70
|
+
return JSON.stringify(value);
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
73
|
+
}
|
|
74
|
+
const obj = value;
|
|
75
|
+
const keys = Object.keys(obj).sort();
|
|
76
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CapabilityInvocation } from '../types.js';
|
|
2
|
+
export interface ScavengeOptions {
|
|
3
|
+
/** Allowlist of tool names the model is permitted to call. */
|
|
4
|
+
allowedNames: ReadonlySet<string>;
|
|
5
|
+
/** Cap on scavenged calls per pass — defence against runaway. */
|
|
6
|
+
maxCalls?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ScavengeResult {
|
|
9
|
+
calls: CapabilityInvocation[];
|
|
10
|
+
notes: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function scavengeToolCalls(text: string | null | undefined, opts: ScavengeOptions): ScavengeResult;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scavenge tool calls that the model emitted as text instead of via the
|
|
3
|
+
* structured tool_use channel. Ported from reasonix (MIT) and adapted to
|
|
4
|
+
* Franklin's Anthropic-shape `CapabilityInvocation` (parsed `input` object
|
|
5
|
+
* + synthetic id).
|
|
6
|
+
*
|
|
7
|
+
* Triggers we've actually seen:
|
|
8
|
+
* - DeepSeek R1 leaks tool-call JSON into `reasoning_content` and forgets
|
|
9
|
+
* to populate `tool_calls`. The text channel ends up with raw JSON like
|
|
10
|
+
* `{"name":"Read","arguments":{...}}`.
|
|
11
|
+
* - DeepSeek V3.1 sometimes emits its chat-template DSML markup
|
|
12
|
+
* (`<|DSML|invoke …>`) in the content channel.
|
|
13
|
+
* - Smaller OpenAI-compatible models (some Qwen / Llama variants behind
|
|
14
|
+
* the BlockRun gateway) leak the OpenAI tool-call shape inline.
|
|
15
|
+
*
|
|
16
|
+
* All three are recoverable. This module turns the leaked text back into
|
|
17
|
+
* a `CapabilityInvocation` so the agent loop doesn't waste a turn telling
|
|
18
|
+
* the model "you forgot to call a tool".
|
|
19
|
+
*/
|
|
20
|
+
import { randomBytes } from 'node:crypto';
|
|
21
|
+
/** Bounds regex input — DSML matchers are O(n²) on adversarial input. */
|
|
22
|
+
const MAX_SCAVENGE_INPUT = 100 * 1024;
|
|
23
|
+
export function scavengeToolCalls(text, opts) {
|
|
24
|
+
if (!text)
|
|
25
|
+
return { calls: [], notes: [] };
|
|
26
|
+
if (text.length > MAX_SCAVENGE_INPUT) {
|
|
27
|
+
return {
|
|
28
|
+
calls: [],
|
|
29
|
+
notes: [`scavenge skipped: input too large (${text.length} chars)`],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const max = opts.maxCalls ?? 4;
|
|
33
|
+
const notes = [];
|
|
34
|
+
const out = [];
|
|
35
|
+
// Pattern A — DSML invoke blocks (DeepSeek chat-template markup leaked
|
|
36
|
+
// into the content channel).
|
|
37
|
+
for (const invoke of iterateDsmlInvokes(text)) {
|
|
38
|
+
if (out.length >= max)
|
|
39
|
+
break;
|
|
40
|
+
if (!opts.allowedNames.has(invoke.name))
|
|
41
|
+
continue;
|
|
42
|
+
out.push(makeInvocation(invoke.name, invoke.args));
|
|
43
|
+
notes.push(`scavenged DSML call: ${invoke.name}`);
|
|
44
|
+
}
|
|
45
|
+
// Pattern B — raw JSON objects in the three canonical shapes. Strip
|
|
46
|
+
// DSML blocks first so their parameter payloads don't get re-scavenged
|
|
47
|
+
// as standalone JSON calls.
|
|
48
|
+
const nonDsml = stripDsmlBlocks(text);
|
|
49
|
+
for (const candidate of iterateJsonObjects(nonDsml)) {
|
|
50
|
+
if (out.length >= max)
|
|
51
|
+
break;
|
|
52
|
+
const call = coerceToInvocation(candidate, opts.allowedNames);
|
|
53
|
+
if (call) {
|
|
54
|
+
out.push(call);
|
|
55
|
+
notes.push(`scavenged call: ${call.name}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { calls: out, notes };
|
|
59
|
+
}
|
|
60
|
+
function stripDsmlBlocks(text) {
|
|
61
|
+
let out = text;
|
|
62
|
+
out = out.replace(/<[||]DSML[||]function_calls>[\s\S]*?<\/?[||]DSML[||]function_calls>/g, '');
|
|
63
|
+
out = out.replace(/<[||]DSML[||]invoke\s+[^>]*>[\s\S]*?<\/[||]DSML[||]invoke>/g, '');
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function* iterateDsmlInvokes(text) {
|
|
67
|
+
// `|` (U+FF5C) in practice; `|` (ASCII) as a fallback variant.
|
|
68
|
+
const INVOKE_RE = /<[||]DSML[||]invoke\s+name="([^"]+)">([\s\S]*?)<\/[||]DSML[||]invoke>/g;
|
|
69
|
+
for (const match of text.matchAll(INVOKE_RE)) {
|
|
70
|
+
const name = match[1];
|
|
71
|
+
const body = match[2];
|
|
72
|
+
if (!name || body === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
yield { name, args: parseDsmlParameters(body) };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function parseDsmlParameters(body) {
|
|
78
|
+
const PARAM_RE = /<[||]DSML[||]parameter\s+name="([^"]+)"(?:\s+string="(true|false)")?\s*>([\s\S]*?)<\/[||]DSML[||]parameter>/g;
|
|
79
|
+
const args = {};
|
|
80
|
+
for (const m of body.matchAll(PARAM_RE)) {
|
|
81
|
+
const key = m[1];
|
|
82
|
+
const stringFlag = m[2];
|
|
83
|
+
const raw = (m[3] ?? '').trim();
|
|
84
|
+
if (!key)
|
|
85
|
+
continue;
|
|
86
|
+
if (stringFlag === 'false') {
|
|
87
|
+
try {
|
|
88
|
+
args[key] = JSON.parse(raw);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Fall through — preserve literal so info isn't lost.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
args[key] = raw;
|
|
96
|
+
}
|
|
97
|
+
return args;
|
|
98
|
+
}
|
|
99
|
+
function* iterateJsonObjects(text) {
|
|
100
|
+
for (let i = 0; i < text.length; i++) {
|
|
101
|
+
if (text[i] !== '{')
|
|
102
|
+
continue;
|
|
103
|
+
let depth = 0;
|
|
104
|
+
let inString = false;
|
|
105
|
+
let escaped = false;
|
|
106
|
+
for (let j = i; j < text.length; j++) {
|
|
107
|
+
const c = text[j];
|
|
108
|
+
if (escaped) {
|
|
109
|
+
escaped = false;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (inString) {
|
|
113
|
+
if (c === '\\') {
|
|
114
|
+
escaped = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (c === '"')
|
|
118
|
+
inString = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (c === '"')
|
|
122
|
+
inString = true;
|
|
123
|
+
else if (c === '{')
|
|
124
|
+
depth++;
|
|
125
|
+
else if (c === '}') {
|
|
126
|
+
depth--;
|
|
127
|
+
if (depth === 0) {
|
|
128
|
+
yield text.slice(i, j + 1);
|
|
129
|
+
i = j;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function coerceToInvocation(candidateJson, allowedNames) {
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(candidateJson);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (!parsed || typeof parsed !== 'object')
|
|
145
|
+
return null;
|
|
146
|
+
const obj = parsed;
|
|
147
|
+
// Pattern 1 — { name, arguments } (Anthropic-ish flat form).
|
|
148
|
+
if (typeof obj.name === 'string' && allowedNames.has(obj.name)) {
|
|
149
|
+
return makeInvocation(obj.name, normalizeArgs(obj.arguments));
|
|
150
|
+
}
|
|
151
|
+
// Pattern 2 — OpenAI-style { type: "function", function: { name, arguments } }.
|
|
152
|
+
if (obj.type === 'function' &&
|
|
153
|
+
obj.function &&
|
|
154
|
+
typeof obj.function === 'object') {
|
|
155
|
+
const fn = obj.function;
|
|
156
|
+
if (typeof fn.name === 'string' && allowedNames.has(fn.name)) {
|
|
157
|
+
return makeInvocation(fn.name, normalizeArgs(fn.arguments));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Pattern 3 — { tool_name, tool_args } (R1 free-form variant).
|
|
161
|
+
if (typeof obj.tool_name === 'string' && allowedNames.has(obj.tool_name)) {
|
|
162
|
+
return makeInvocation(obj.tool_name, normalizeArgs(obj.tool_args));
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function normalizeArgs(raw) {
|
|
167
|
+
if (raw == null)
|
|
168
|
+
return {};
|
|
169
|
+
if (typeof raw === 'string') {
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(raw);
|
|
172
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
173
|
+
return parsed;
|
|
174
|
+
}
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
|
182
|
+
return raw;
|
|
183
|
+
}
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
function makeInvocation(name, input) {
|
|
187
|
+
return {
|
|
188
|
+
type: 'tool_use',
|
|
189
|
+
id: `toolu_repair_${randomBytes(6).toString('hex')}`,
|
|
190
|
+
name,
|
|
191
|
+
input,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncated-JSON repair — ported from reasonix (MIT). Format-agnostic:
|
|
3
|
+
* works on any raw JSON argument string. Common trigger: model hits
|
|
4
|
+
* max_tokens mid-structure; the last useful argument is half-emitted.
|
|
5
|
+
*
|
|
6
|
+
* Local-only — never makes a continuation call. The agent loop owns
|
|
7
|
+
* budgets; this just patches what we have so the dispatcher can either
|
|
8
|
+
* parse it or report a clean fallback.
|
|
9
|
+
*/
|
|
10
|
+
export interface TruncationRepairResult {
|
|
11
|
+
repaired: string;
|
|
12
|
+
changed: boolean;
|
|
13
|
+
notes: string[];
|
|
14
|
+
/** True when all repair attempts failed and the result fell back to "{}". */
|
|
15
|
+
fallback: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function repairTruncatedJson(input: string): TruncationRepairResult;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncated-JSON repair — ported from reasonix (MIT). Format-agnostic:
|
|
3
|
+
* works on any raw JSON argument string. Common trigger: model hits
|
|
4
|
+
* max_tokens mid-structure; the last useful argument is half-emitted.
|
|
5
|
+
*
|
|
6
|
+
* Local-only — never makes a continuation call. The agent loop owns
|
|
7
|
+
* budgets; this just patches what we have so the dispatcher can either
|
|
8
|
+
* parse it or report a clean fallback.
|
|
9
|
+
*/
|
|
10
|
+
export function repairTruncatedJson(input) {
|
|
11
|
+
const notes = [];
|
|
12
|
+
if (!input || !input.trim()) {
|
|
13
|
+
return {
|
|
14
|
+
repaired: '{}',
|
|
15
|
+
changed: input !== '{}',
|
|
16
|
+
notes: ['empty input → {}'],
|
|
17
|
+
fallback: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Fast path: already valid JSON.
|
|
21
|
+
try {
|
|
22
|
+
JSON.parse(input);
|
|
23
|
+
return { repaired: input, changed: false, notes: [], fallback: false };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* fall through to repair */
|
|
27
|
+
}
|
|
28
|
+
const stack = [];
|
|
29
|
+
let escaped = false;
|
|
30
|
+
let inString = false;
|
|
31
|
+
let lastSignificant = -1;
|
|
32
|
+
for (let i = 0; i < input.length; i++) {
|
|
33
|
+
const c = input[i];
|
|
34
|
+
if (!/\s/.test(c))
|
|
35
|
+
lastSignificant = i;
|
|
36
|
+
if (escaped) {
|
|
37
|
+
escaped = false;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (inString) {
|
|
41
|
+
if (c === '\\') {
|
|
42
|
+
escaped = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (c === '"') {
|
|
46
|
+
inString = false;
|
|
47
|
+
stack.pop();
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === '"') {
|
|
52
|
+
inString = true;
|
|
53
|
+
stack.push('"');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (c === '{' || c === '[')
|
|
57
|
+
stack.push(c);
|
|
58
|
+
else if (c === '}' || c === ']')
|
|
59
|
+
stack.pop();
|
|
60
|
+
}
|
|
61
|
+
let s = input.slice(0, lastSignificant + 1);
|
|
62
|
+
if (/,$/.test(s)) {
|
|
63
|
+
s = s.replace(/,$/, '');
|
|
64
|
+
notes.push('trimmed trailing comma');
|
|
65
|
+
}
|
|
66
|
+
if (/"\s*:\s*$/.test(s)) {
|
|
67
|
+
s += ' null';
|
|
68
|
+
notes.push('filled dangling key with null');
|
|
69
|
+
}
|
|
70
|
+
if (inString) {
|
|
71
|
+
s += '"';
|
|
72
|
+
stack.pop();
|
|
73
|
+
notes.push('closed unterminated string');
|
|
74
|
+
}
|
|
75
|
+
while (stack.length > 0) {
|
|
76
|
+
const top = stack.pop();
|
|
77
|
+
if (top === '{')
|
|
78
|
+
s += '}';
|
|
79
|
+
else if (top === '[')
|
|
80
|
+
s += ']';
|
|
81
|
+
else if (top === '"')
|
|
82
|
+
s += '"';
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
JSON.parse(s);
|
|
86
|
+
return { repaired: s, changed: s !== input, notes, fallback: false };
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const preview = input.length <= 500 ? input : `${input.slice(0, 500)} …[+${input.length - 500} chars]`;
|
|
90
|
+
notes.push(`fallback to {}: ${err.message}`);
|
|
91
|
+
notes.push(`unrecoverable truncation — original args preview: ${preview}`);
|
|
92
|
+
return { repaired: '{}', changed: true, notes, fallback: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/agent/tokens.js
CHANGED
|
@@ -191,10 +191,11 @@ export function estimateHistoryTokens(history) {
|
|
|
191
191
|
*/
|
|
192
192
|
const MODEL_CONTEXT_WINDOWS = {
|
|
193
193
|
// Anthropic. The BlockRun gateway model entry advertises 1M context for
|
|
194
|
-
// Opus 4.7, but the 1M beta header may not be enabled at the gateway
|
|
194
|
+
// Opus 4.8 / 4.7, but the 1M beta header may not be enabled at the gateway
|
|
195
195
|
// edge yet — sending more than 200k without it 413s. Keep 200k as the
|
|
196
196
|
// safe Franklin baseline; bump to 1_000_000 in a separate commit once
|
|
197
197
|
// a real >200k call has been verified end-to-end.
|
|
198
|
+
'anthropic/claude-opus-4.8': 200_000,
|
|
198
199
|
'anthropic/claude-opus-4.7': 200_000,
|
|
199
200
|
'anthropic/claude-opus-4.6': 200_000,
|
|
200
201
|
'anthropic/claude-sonnet-4.6': 200_000,
|
package/dist/commands/init.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function initCommand(options) {
|
|
|
29
29
|
ANTHROPIC_AUTH_TOKEN: 'x402-proxy-handles-auth',
|
|
30
30
|
ANTHROPIC_MODEL: 'blockrun/auto',
|
|
31
31
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'anthropic/claude-sonnet-4.6',
|
|
32
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: 'anthropic/claude-opus-4.
|
|
32
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: 'anthropic/claude-opus-4.8',
|
|
33
33
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'anthropic/claude-haiku-4.5-20251001',
|
|
34
34
|
};
|
|
35
35
|
fs.mkdirSync(path.dirname(CLAUDE_SETTINGS_FILE), { recursive: true });
|
package/dist/pricing.js
CHANGED
|
@@ -27,6 +27,7 @@ export const MODEL_PRICING = {
|
|
|
27
27
|
'nvidia/mistral-large-3-675b': { input: 0, output: 0 },
|
|
28
28
|
// Anthropic
|
|
29
29
|
'anthropic/claude-sonnet-4.6': { input: 3.0, output: 15.0 },
|
|
30
|
+
'anthropic/claude-opus-4.8': { input: 5.0, output: 25.0 },
|
|
30
31
|
'anthropic/claude-opus-4.7': { input: 5.0, output: 25.0 },
|
|
31
32
|
'anthropic/claude-opus-4.6': { input: 5.0, output: 25.0 },
|
|
32
33
|
'anthropic/claude-haiku-4.5': { input: 1.0, output: 5.0 },
|
|
@@ -90,7 +91,7 @@ export const MODEL_PRICING = {
|
|
|
90
91
|
'zai/glm-5.1-turbo': { input: 0, output: 0, perCall: 0.001 }, // client alias for zai/glm-5-turbo
|
|
91
92
|
};
|
|
92
93
|
/** Opus pricing for savings calculations — tracks the current flagship. */
|
|
93
|
-
export const OPUS_PRICING = MODEL_PRICING['anthropic/claude-opus-4.
|
|
94
|
+
export const OPUS_PRICING = MODEL_PRICING['anthropic/claude-opus-4.8'];
|
|
94
95
|
/**
|
|
95
96
|
* Estimate cost in USD for a request.
|
|
96
97
|
* Falls back to $2/$10 per 1M for unknown models.
|
package/dist/proxy/server.js
CHANGED
|
@@ -100,7 +100,8 @@ const MODEL_SHORTCUTS = {
|
|
|
100
100
|
sonnet: 'anthropic/claude-sonnet-4.6',
|
|
101
101
|
claude: 'anthropic/claude-sonnet-4.6',
|
|
102
102
|
'sonnet-4.6': 'anthropic/claude-sonnet-4.6',
|
|
103
|
-
opus: 'anthropic/claude-opus-4.
|
|
103
|
+
opus: 'anthropic/claude-opus-4.8',
|
|
104
|
+
'opus-4.8': 'anthropic/claude-opus-4.8',
|
|
104
105
|
'opus-4.7': 'anthropic/claude-opus-4.7',
|
|
105
106
|
'opus-4.6': 'anthropic/claude-opus-4.6',
|
|
106
107
|
haiku: 'anthropic/claude-haiku-4.5-20251001',
|
package/dist/router/index.js
CHANGED
|
@@ -55,14 +55,15 @@ const AUTO_TIERS = {
|
|
|
55
55
|
// Hard tasks — multi-file refactors, ambiguous specs, dense reasoning
|
|
56
56
|
// chains — still go to Opus. V4 Pro is great but not a Sonnet/Opus
|
|
57
57
|
// replacement at the high end of difficulty per recent agent-bench runs.
|
|
58
|
-
primary: 'anthropic/claude-opus-4.
|
|
59
|
-
fallback: ['openai/gpt-5.5', 'anthropic/claude-sonnet-4.6', 'deepseek/deepseek-v4-pro'],
|
|
58
|
+
primary: 'anthropic/claude-opus-4.8',
|
|
59
|
+
fallback: ['anthropic/claude-opus-4.7', 'openai/gpt-5.5', 'anthropic/claude-sonnet-4.6', 'deepseek/deepseek-v4-pro'],
|
|
60
60
|
},
|
|
61
61
|
REASONING: {
|
|
62
|
-
// Opus 4.
|
|
63
|
-
//
|
|
64
|
-
primary: 'anthropic/claude-opus-4.
|
|
62
|
+
// Opus 4.8: latest flagship, most capable for agentic coding. 4.7 and 4.6
|
|
63
|
+
// stay in the fallback chain in case of rollout delays.
|
|
64
|
+
primary: 'anthropic/claude-opus-4.8',
|
|
65
65
|
fallback: [
|
|
66
|
+
'anthropic/claude-opus-4.7',
|
|
66
67
|
'anthropic/claude-opus-4.6',
|
|
67
68
|
'openai/o3',
|
|
68
69
|
'deepseek/deepseek-v4-pro',
|
package/dist/router/vision.js
CHANGED
package/dist/ui/model-picker.js
CHANGED
|
@@ -19,7 +19,8 @@ export const MODEL_SHORTCUTS = {
|
|
|
19
19
|
sonnet: 'anthropic/claude-sonnet-4.6',
|
|
20
20
|
claude: 'anthropic/claude-sonnet-4.6',
|
|
21
21
|
'sonnet-4.6': 'anthropic/claude-sonnet-4.6',
|
|
22
|
-
opus: 'anthropic/claude-opus-4.
|
|
22
|
+
opus: 'anthropic/claude-opus-4.8',
|
|
23
|
+
'opus-4.8': 'anthropic/claude-opus-4.8',
|
|
23
24
|
'opus-4.7': 'anthropic/claude-opus-4.7',
|
|
24
25
|
'opus-4.6': 'anthropic/claude-opus-4.6',
|
|
25
26
|
haiku: 'anthropic/claude-haiku-4.5-20251001',
|
|
@@ -149,7 +150,7 @@ export const PICKER_CATEGORIES = [
|
|
|
149
150
|
// free-tier entries and v3.9.2 used to retire Kimi K2.5.
|
|
150
151
|
category: '✨ Premium frontier',
|
|
151
152
|
models: [
|
|
152
|
-
{ id: 'anthropic/claude-opus-4.
|
|
153
|
+
{ id: 'anthropic/claude-opus-4.8', shortcut: 'opus', label: 'Claude Opus 4.8', price: '$5/$25', highlight: true },
|
|
153
154
|
{ id: 'anthropic/claude-sonnet-4.6', shortcut: 'sonnet', label: 'Claude Sonnet 4.6', price: '$3/$15' },
|
|
154
155
|
{ id: 'openai/gpt-5.5', shortcut: 'gpt', label: 'GPT-5.5', price: '$5/$30', highlight: true },
|
|
155
156
|
{ id: 'google/gemini-3.1-pro', shortcut: 'gemini-3', label: 'Gemini 3.1 Pro', price: '$2/$12' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/franklin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.0",
|
|
4
4
|
"description": "Franklin Agent — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"build": "tsc && node scripts/copy-plugin-assets.mjs",
|
|
32
32
|
"dev": "tsc --watch",
|
|
33
33
|
"start": "node dist/index.js",
|
|
34
|
-
"test": "npm run build && node --test --test-reporter=spec test/local.mjs test/skills.local.mjs",
|
|
34
|
+
"test": "npm run build && node --test --test-reporter=spec test/local.mjs test/skills.local.mjs test/repair.mjs",
|
|
35
35
|
"test:e2e": "npm run build && node --test --test-reporter=spec test/e2e.mjs",
|
|
36
36
|
"test:free-models": "npm run build && node --test --test-reporter=spec test/free-model-matrix.mjs",
|
|
37
37
|
"test:all": "npm run test && npm run test:e2e",
|