@blockrun/franklin 3.21.9 → 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.
@@ -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
+ }
@@ -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,
@@ -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.7',
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 });
@@ -11,4 +11,10 @@
11
11
  * gateway ever exposes the realized payment amount on the response, that
12
12
  * should be preferred — fall back to this estimate when it's missing.
13
13
  */
14
- export declare function estimateImageCostUsd(model: string, size: string): number;
14
+ /**
15
+ * Estimate the USD cost of `n` images for a model + size. `n` defaults to 1.
16
+ * Unknown models return 0 rather than a guess — a free/custom model should not
17
+ * carry a phantom charge against the Content budget, and surprise overcharging
18
+ * from a wrong guess is worse than under-counting.
19
+ */
20
+ export declare function estimateImageCostUsd(model: string, size: string, n?: number): number;
@@ -11,22 +11,45 @@
11
11
  * gateway ever exposes the realized payment amount on the response, that
12
12
  * should be preferred — fall back to this estimate when it's missing.
13
13
  */
14
- export function estimateImageCostUsd(model, size) {
15
- const m = model.toLowerCase();
14
+ /**
15
+ * Per-image base price by model + size. Mirrors the gateway's IMAGE_MODELS.sizes
16
+ * (blockrun src/lib/models.ts). These are base prices — the realized x402 charge
17
+ * adds a small markup — but they're close enough for budget tracking. Sizes not
18
+ * listed for a model fall back to that model's 1024x1024 tier.
19
+ */
20
+ const PRICE_TABLE = {
21
+ 'openai/dall-e-3': {
22
+ base: 0.04,
23
+ sizes: { '1024x1024': 0.04, '1792x1024': 0.08, '1024x1792': 0.08 },
24
+ },
25
+ 'openai/gpt-image-1': {
26
+ base: 0.02,
27
+ sizes: { '1024x1024': 0.02, '1536x1024': 0.04, '1024x1536': 0.04 },
28
+ },
29
+ 'openai/gpt-image-2': {
30
+ base: 0.06,
31
+ sizes: { '1024x1024': 0.06, '1536x1024': 0.12, '1024x1536': 0.12 },
32
+ },
33
+ 'google/nano-banana': {
34
+ base: 0.05,
35
+ sizes: { '1024x1024': 0.05 },
36
+ },
37
+ 'google/nano-banana-pro': {
38
+ base: 0.1,
39
+ sizes: { '1024x1024': 0.1, '2048x2048': 0.1, '4096x4096': 0.15 },
40
+ },
41
+ };
42
+ /**
43
+ * Estimate the USD cost of `n` images for a model + size. `n` defaults to 1.
44
+ * Unknown models return 0 rather than a guess — a free/custom model should not
45
+ * carry a phantom charge against the Content budget, and surprise overcharging
46
+ * from a wrong guess is worse than under-counting.
47
+ */
48
+ export function estimateImageCostUsd(model, size, n = 1) {
49
+ const entry = PRICE_TABLE[model.toLowerCase()];
50
+ if (!entry)
51
+ return 0;
16
52
  const s = size.replace(/\s+/g, '');
17
- if (m === 'openai/dall-e-3') {
18
- if (s === '1792x1024' || s === '1024x1792')
19
- return 0.08;
20
- // All other sizes fall back to the standard 1024x1024 tier.
21
- return 0.04;
22
- }
23
- if (m === 'openai/gpt-image-1') {
24
- // gpt-image-1 standard tier; larger sizes would tier up but Franklin
25
- // sends 1024x1024 as default.
26
- return 0.042;
27
- }
28
- // Unknown model: return 0 rather than a guess. A free/custom model should
29
- // not have a phantom charge against the Content budget, and surprise
30
- // overcharging from a wrong guess is worse than under-counting.
31
- return 0;
53
+ const perImage = entry.sizes[s] ?? entry.base;
54
+ return perImage * Math.max(1, n);
32
55
  }
@@ -34,7 +34,7 @@ export type RecordImageDecision = {
34
34
  * estimated cost fits; `{ ok: false, reason }` when it doesn't or the
35
35
  * content doesn't exist. Non-mutating.
36
36
  */
37
- export declare function checkImageBudget(library: ContentLibrary, contentId: string, model: string, size: string): {
37
+ export declare function checkImageBudget(library: ContentLibrary, contentId: string, model: string, size: string, count?: number): {
38
38
  ok: true;
39
39
  } | {
40
40
  ok: false;
@@ -20,12 +20,12 @@ import { estimateImageCostUsd } from './image-pricing.js';
20
20
  * estimated cost fits; `{ ok: false, reason }` when it doesn't or the
21
21
  * content doesn't exist. Non-mutating.
22
22
  */
23
- export function checkImageBudget(library, contentId, model, size) {
23
+ export function checkImageBudget(library, contentId, model, size, count = 1) {
24
24
  const content = library.get(contentId);
25
25
  if (!content) {
26
26
  return { ok: false, reason: `Content ${contentId} not found` };
27
27
  }
28
- const cost = estimateImageCostUsd(model, size);
28
+ const cost = estimateImageCostUsd(model, size, count);
29
29
  if (content.spentUsd + cost > content.budgetUsd + 1e-9) {
30
30
  return {
31
31
  ok: false,
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.7'];
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.
@@ -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.7',
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',
@@ -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.7',
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.7: step-change improvement in agentic coding over 4.6 per
63
- // Anthropic. 4.6 stays in the fallback chain in case of rollout delays.
64
- primary: 'anthropic/claude-opus-4.7',
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',
@@ -20,6 +20,7 @@
20
20
  */
21
21
  const VISION_MODELS = new Set([
22
22
  // Anthropic — native vision across the line
23
+ 'anthropic/claude-opus-4.8',
23
24
  'anthropic/claude-opus-4.7',
24
25
  'anthropic/claude-opus-4.6',
25
26
  'anthropic/claude-sonnet-4.6',
@@ -237,12 +237,21 @@ export const blockrunCapability = {
237
237
  }
238
238
  catch { /* best-effort */ }
239
239
  if (!result.ok) {
240
- const detail = typeof result.body?.error === 'string'
241
- ? result.body.error
242
- : `HTTP ${result.status}`;
240
+ const b = result.body;
241
+ const detail = typeof b?.error === 'string' ? b.error : `HTTP ${result.status}`;
243
242
  const fullOutput = result.raw || JSON.stringify(result.body, null, 2);
243
+ // Surface the gateway's self-correction hints into the model-visible
244
+ // output. On a wrong path the Surf route returns `available: [...all
245
+ // valid paths]` (and often a `message`); without this the model only
246
+ // saw "Not Found" and kept guessing until the tool-failure circuit
247
+ // breaker tripped. The list comes straight from the live registry, so
248
+ // it's always complete and in sync — no drift.
249
+ const hint = typeof b?.message === 'string' ? `\n${b.message}` : '';
250
+ const avail = Array.isArray(b?.available)
251
+ ? `\nValid endpoints: ${b.available.filter((x) => typeof x === 'string').join(', ')}`
252
+ : '';
244
253
  return {
245
- output: `BlockRun ${method} ${path} failed: ${detail} (status ${result.status}). No charge if status is 4xx pre-payment.`,
254
+ output: `BlockRun ${method} ${path} failed: ${detail} (status ${result.status}). No charge if status is 4xx pre-payment.${hint}${avail}`,
246
255
  fullOutput,
247
256
  isError: true,
248
257
  };
@@ -5,11 +5,28 @@
5
5
  import type { CapabilityHandler } from '../agent/types.js';
6
6
  import type { ContentLibrary } from '../content/library.js';
7
7
  /**
8
- * Models that accept a reference image via /v1/images/image2image. Currently
9
- * limited to OpenAI's edit endpoint — Gemini Nano Banana Pro and Grok Imagine
10
- * Image Pro need gateway-side support before they can be wired in here.
8
+ * Models that accept a reference image via /v1/images/image2image. Mirrors the
9
+ * gateway's EDIT_SUPPORTED_MODELS (src/app/api/v1/images/image2image/route.ts):
10
+ * both OpenAI gpt-image-* and Google Nano Banana support image-to-image edits.
11
11
  */
12
12
  export declare const EDIT_SUPPORTED_MODELS: Set<string>;
13
+ /**
14
+ * Mask-based inpainting is OpenAI-only. Gemini (Nano Banana) does prompt-based
15
+ * edits with no mask concept. Mirrors the gateway's MASK_SUPPORTED_MODELS.
16
+ */
17
+ export declare const MASK_SUPPORTED_MODELS: Set<string>;
18
+ /**
19
+ * Output-image count ceiling. The gateway has no hard max but price scales with
20
+ * n, so cap client-side to keep a typo from draining the wallet.
21
+ */
22
+ export declare const MAX_OUTPUT_IMAGES = 4;
23
+ /**
24
+ * Valid sizes per known image model, mirroring the gateway's IMAGE_MODELS.sizes
25
+ * (src/lib/models.ts). Used to fail cheaply before paying when a caller or the
26
+ * media router picks a size the model rejects. Models absent from this table
27
+ * (custom / future gateway models) skip validation and let the gateway decide.
28
+ */
29
+ export declare const IMAGE_MODEL_SIZES: Record<string, string[]>;
13
30
  export declare const REFERENCE_IMAGE_MAX_BYTES = 4000000;
14
31
  /**
15
32
  * Normalize a reference image into a base64 data URI for the gateway. The
@@ -24,6 +41,8 @@ export interface ImageGenDeps {
24
41
  /** Invoked after successful content-linked generation; lets callers persist. */
25
42
  onContentChange?: () => void | Promise<void>;
26
43
  }
44
+ /** Insert a `-{idx}` suffix before the file extension: a.png → a-2.png. */
45
+ export declare function withIndexSuffix(p: string, idx: number): string;
27
46
  /**
28
47
  * Build the ImageGen capability. Passing `deps.library` enables the
29
48
  * contentId flow: pre-flight budget check + post-generation asset