diff --git a/apple/whistlegraph/README.md b/apple/whistlegraph/README.md index 04121ad2bc..a251ff0740 100644 --- a/apple/whistlegraph/README.md +++ b/apple/whistlegraph/README.md @@ -81,7 +81,7 @@ The browser check uses Puppeteer and Chrome with mock inference; it makes no mod Every generated edit runs syntax/API checks and waits for matching-source runtime feedback before the visual review. Client errors trigger at most one repair across code and visual checks. A candidate that still fails restores the previous saved version; its failure remains visible after the restored frame paints. Missing or stale evidence cannot trigger a paid repair or save a version. There is no opt-out setting. -The initial generation uses DeepSeek V4.1 Flash with four tool rounds and one output continuation. The single repair uses DeepSeek V4 Pro (or V4.1 Flash when the request carries chalk, since V4 Pro accepts no images) with a 1,024-token thinking budget, two tool rounds and no continuation. Provider calls retain the 4,096-token output limit. Remote requests and remote edit descriptions use the same 96-grapheme, single-line limit as phone typing; oversized requests are rejected before generation. Diagnostic context and source code are separate from the short user request. Code generation/checking has a 75-second deadline; visual review has a separate 90-second deadline. These bound work, not cost. A painted frame is execution evidence, not visual acceptance. +Everyone's default is hosted Claude Sonnet 5.5 (OpenRouter, billed in braincells) with 16,384 output tokens, 4,096 thinking tokens, 12 tool rounds and two continuations; its single repair keeps the model with four rounds. Claude Opus 5.5 and DeepSeek V4.1 Flash are selectable in Brain. DeepSeek keeps a lean budget (4,096 output tokens, four rounds, thinking off); its repair uses DeepSeek V4 Pro, or V4.1 Flash when the request carries chalk, since V4 Pro accepts no images. Accounts the relay grants personal access default to Claude Opus 5 through the relay and can also pick GPT-6 Astra. Generation has no wall-clock deadline; each turn is bounded by its rounds, continuations and output tokens, and by Stop. Remote requests and remote edit descriptions use the same 96-grapheme, single-line limit as phone typing; oversized requests are rejected before generation. Diagnostic context and source code are separate from the short user request. Visual review has a 90-second deadline (300 seconds through the personal relay); Claude reviews request minimal reasoning because OpenRouter rejects disabled reasoning for Claude 5.5. These bound work, not cost. A painted frame is execution evidence, not visual acceptance. ## Pixel size diff --git a/apple/whistlegraph/Resources/Web/engine.mjs b/apple/whistlegraph/Resources/Web/engine.mjs index cc7f8e2e6d..f78cfeb856 100644 --- a/apple/whistlegraph/Resources/Web/engine.mjs +++ b/apple/whistlegraph/Resources/Web/engine.mjs @@ -463,8 +463,10 @@ async function ask(text,displayText=text,advice=null,starter=null,localText=text const viewport=frame.getBoundingClientRect(); const prompt=compileEditContract({request:text,...selectedBranch(versions.value),source})+ `\nCurrent preview: ${Math.round(viewport.width)} × ${Math.round(viewport.height)} CSS points. Compose for this shape using screen.width and screen.height; keep subjects within the canvas and remain responsive when it resizes.`; - // Jeffrey's personal turns may keep working until completion or explicit Stop. - const deadline=turnHandle==='jeffrey'?null:setTimeout(()=>{turnCancelled=true;turnError='Edit check timed out';server?.interrupt();},75000); + // No wall-clock deadline: every turn is bounded by its profile's rounds, + // continuations and output tokens, and by Stop. A fixed 75 s cut off slow + // phones and long Opus turns that were still making progress. + const deadline=null; try{ const result=await runEditExperiment({prompt,cancelled:()=>turnCancelled, onRepair:()=>{activeReceipt.value.repairs=1;activeReceipt.save();phase('Repairing…');}, diff --git a/apple/whistlegraph/Resources/Web/generation-policy.mjs b/apple/whistlegraph/Resources/Web/generation-policy.mjs index e06befcd43..23e2efb3e7 100644 --- a/apple/whistlegraph/Resources/Web/generation-policy.mjs +++ b/apple/whistlegraph/Resources/Web/generation-policy.mjs @@ -1,23 +1,30 @@ -export const DEFAULT_MODEL = 'deepseek/deepseek-v4.1-flash'; +// Everyone's default is hosted Claude Sonnet (OpenRouter, billed in braincells). +export const DEFAULT_MODEL = 'anthropic/claude-sonnet-5.5'; +export const FLASH_MODEL = 'deepseek/deepseek-v4.1-flash'; export const REPAIR_MODEL = 'deepseek/deepseek-v4-pro'; +// Served only through the personal relay, for accounts it grants. +const RELAY_MODELS = ['anthropic/claude-opus-5','openai/gpt-6-astra']; export const MODEL_LABELS = { + 'anthropic/claude-sonnet-5.5':'Claude Sonnet 5.5', + 'anthropic/claude-opus-5.5':'Claude Opus 5.5', 'deepseek/deepseek-v4.1-flash':'DeepSeek V4.1 Flash', 'deepseek/deepseek-v4-pro':'DeepSeek V4 Pro', 'anthropic/claude-opus-5':'Claude Opus 5 · personal', 'openai/gpt-6-astra':'GPT-6 Astra · personal', }; export function modelChoices(handle, {personalAccess=false}={}) { - return Object.entries(MODEL_LABELS).filter(([id])=>(handle==='jeffrey'||personalAccess)||(!id.startsWith('anthropic/')&&!id.startsWith('openai/'))).map(([id,label])=>({id,label})); + return Object.entries(MODEL_LABELS).filter(([id])=>personalAccess||!RELAY_MODELS.includes(id)).map(([id,label])=>({id,label})); } -// The handle comes from verifyAccount, never from a preference or command. +// Personal models follow the relay's access grant, never a handle or preference. +// Claude turns get the full budget for everyone; DeepSeek keeps its lean one. // DeepSeek V4 Pro is text-only on OpenRouter (404 "No endpoints found that support -// image input"), so a repair that carries chalk stays on the image-capable default. +// image input"), so a repair that carries chalk stays on the image-capable Flash. export function generationProfile(handle, {repair=false,model='',personalAccess=false,image=false}={}) { - const personal=handle==='jeffrey'||personalAccess; const selected=modelChoices(handle,{personalAccess}).some(option=>option.id===model)?model:''; - const chosen=selected||(personal?'anthropic/claude-opus-5':DEFAULT_MODEL); - const resolved=repair&&!image&&chosen===DEFAULT_MODEL?REPAIR_MODEL:chosen; - if(personal)return {personalRelay:resolved.startsWith('anthropic/')||resolved.startsWith('openai/'),model:resolved,maxTokens:16384,rounds:repair?4:12,outputContinuations:repair?1:2,reasoning:{max_tokens:4096},thinking:{type:'enabled',budget_tokens:4096}}; - return {model:resolved,maxTokens:4096,rounds:repair?2:4,outputContinuations:repair?0:1,reasoning:repair?{max_tokens:1024}:{effort:'none'},thinking:repair?{type:'enabled',budget_tokens:1024}:{type:'disabled'}}; + const chosen=selected||(personalAccess?'anthropic/claude-opus-5':DEFAULT_MODEL); + const resolved=repair&&!image&&chosen===FLASH_MODEL?REPAIR_MODEL:chosen; + const personalRelay=personalAccess&&RELAY_MODELS.includes(resolved); + if(!resolved.startsWith('deepseek/'))return {personalRelay,model:resolved,maxTokens:16384,rounds:repair?4:12,outputContinuations:repair?1:2,reasoning:{max_tokens:4096},thinking:{type:'enabled',budget_tokens:4096}}; + return {personalRelay:false,model:resolved,maxTokens:4096,rounds:repair?2:4,outputContinuations:repair?0:1,reasoning:repair?{max_tokens:1024}:{effort:'none'},thinking:repair?{type:'enabled',budget_tokens:1024}:{type:'disabled'}}; } export const GENERATION_INSTRUCTIONS = 'This surface is Whistlegraph. The person watches the piece being made. A local starter may already be visible: its source is the real current piece; refine it instead of discarding it. For an existing piece, make small revision-checked edit_piece calls instead of resending unchanged code. Use write_piece to create the initial piece or for a necessary rewrite. No prose preamble. Each ask becomes one internal version; complete edit checkpoints are preview layers within it. Make the first visible working drawing in 3–6 lines whenever possible. The UI previews complete paint statements and exact replacement checkpoints while streaming: put required declarations and a complete exported paint function first; avoid long data tables or helper code before that first drawing. Then refine in small complete write_piece checkpoints only as needed to satisfy the request. Do not add unrequested animation or features. For animation use Number(paintCount) from the paint API, or explicit state advanced in sim; there is no numeric frame API. A request for a spoken narrator needs audible speak(), not just written notes; use the speech guide. Use numeric RGB ink(r,g,b) or six-digit hex colors like "#ffffff"; three-digit CSS hex colors are unsupported. Use AC ink alpha on the 0–255 scale and size geometry relative to the screen. Every saved piece must have working helpers and a caption; never finish with placeholder or empty drawing helpers. line(x1,y1,x2,y2) draws ONE segment; its fifth argument is thickness, never another coordinate. For a path, loop over adjacent point pairs with separate four-argument line calls; never spread three or more point pairs into line. Use the native oval(x,y,radiusX,radiusY,filled) for ellipses instead of approximating them with overlapping circles. Fit the entire composition, including radii and stroke thickness, inside the screen with margins; use at most 90% of the fitting scale and center it. Do not repeat the prompt or add interaction instructions, help text, usage hints, or captions inside or outside the picture unless explicitly requested. Use the default write font for other in-piece text unless the person requests another. Use ac_preview for runtime observations; this phone has no desktop frame-capture service. Once the requested change is painted and runtime-checked, finish the generation turn. The app then captures four timed frames of the actual phone preview and reviews them against the latest request and selected branch before saving. A failed visual check can return concrete mismatches for one narrow repair; do not claim visual success yourself. Never fake progress or tests. Keep one line `export const caption = "…"` in the piece: a plain sentence under 120 characters naming the subject, its mood, what actually moves and what the person can actually do, if anything; never invent motion or interactions in the caption. Update it whenever an edit changes any of that. It is the piece\'s memory of itself and comes back to you with every follow-up. Preserve the current piece and the original intent of the selected branch on revisions. Short follow-ups modify that existing world; do not replace its subject, characters, message, interactions, or sound without a request to do so. Treat uncertain speech transcriptions conservatively in context instead of inventing a new subject from an ambiguous word. Adjectives of culture, nationality or place (Latina, Japanese, Nordic, tropical) describe how things look: palette, patterns, dress, flora, light. They never change the language of any text or speech unless the request mentions words, text, language or voice. Do not turn ordinary spoken requests into text posters, transcript timelines, waveform charts, or word-timed animation unless explicitly requested. No publishing: these are private local previews.'; diff --git a/apple/whistlegraph/Resources/Web/roblox-engine.mjs b/apple/whistlegraph/Resources/Web/roblox-engine.mjs index f73edb98c1..123be90066 100644 --- a/apple/whistlegraph/Resources/Web/roblox-engine.mjs +++ b/apple/whistlegraph/Resources/Web/roblox-engine.mjs @@ -1,7 +1,7 @@ import {AccountConnection} from './account-connection.mjs'; import {AcServer} from '/easel/src/ac-server.mjs'; import {verifyAccount} from '/easel/src/account-access.mjs'; -import {DEFAULT_MODEL} from './generation-policy.mjs'; +import {FLASH_MODEL} from './generation-policy.mjs'; import {PieceVersions} from './piece-versions.mjs'; import {wareControls,WARE_INSTRUCTIONS,requestedWare} from './wares.mjs'; import {starterRoom,validateRoom,drawPath,localRoomEdit,ROOM_SCHEMA,ROOM_LIMITS} from './room-schema.mjs'; @@ -61,7 +61,7 @@ async function ask(text){ if(local){commit(local,text);return;} if(!token){error='Sign in to ask the brain to edit this room.';post({action:'signIn'});return;} busy=true;attempt={request:text,status:'working',error:''};preview.cancel();snapshot(); - server=new AcServer({token:()=>token,model:window.__whistlegraphModel||DEFAULT_MODEL,artifacts,controls:wares,preview:false,frameCapture:false,rounds:6,reasoning:{effort:'none'},thinking:{type:'disabled'}, + server=new AcServer({token:()=>token,model:window.__whistlegraphModel||FLASH_MODEL,artifacts,controls:wares,preview:false,frameCapture:false,rounds:6,reasoning:{effort:'none'},thinking:{type:'disabled'}, developerInstructions:'This is Whistlegraph. Make the requested room edit using artifact_room. Keep changes small and use only the supported grammar. Do not claim to have run Roblox, published a game, or linked an account. No preamble. If the requested behavior is unsupported, explain that briefly instead of inventing a component. '+WARE_INSTRUCTIONS}); server.on('notification',({method,params})=>{ if(method==='item/agentMessage/delta'){output+=params.delta;snapshot();} diff --git a/apple/whistlegraph/Resources/Web/visual-review.mjs b/apple/whistlegraph/Resources/Web/visual-review.mjs index 2ff884c3d5..a9b893b2f3 100644 --- a/apple/whistlegraph/Resources/Web/visual-review.mjs +++ b/apple/whistlegraph/Resources/Web/visual-review.mjs @@ -46,7 +46,9 @@ export async function reviewVisualResult({evidence, sourceHash, renderID, source const response = await fetch('https://aesthetic.computer/api/easel-inference', { method:'POST', signal:controller.signal, headers:{'Content-Type':'application/json', Authorization:`Bearer ${token}`}, - body:JSON.stringify({model, max_tokens:1800, thinking:{type:'disabled'}, reasoning:{effort:'none'}, + // Claude 5.5 on OpenRouter rejects disabled reasoning (400 "Reasoning is + // mandatory"); ask it for the least instead. + body:JSON.stringify({model, max_tokens:1800, ...(model.startsWith('anthropic/')?{reasoning:{effort:'minimal'}}:{thinking:{type:'disabled'}, reasoning:{effort:'none'}}), system:VISUAL_REVIEW_INSTRUCTIONS, messages:[{role:'user',content:[ {type:'text',text:JSON.stringify({latestRequest:request,selectedBranch:history,sourceHash,source})}, diff --git a/apple/whistlegraph/Tests/generation-policy.test.mjs b/apple/whistlegraph/Tests/generation-policy.test.mjs index dd926b2e73..0b0cb27785 100644 --- a/apple/whistlegraph/Tests/generation-policy.test.mjs +++ b/apple/whistlegraph/Tests/generation-policy.test.mjs @@ -1,28 +1,36 @@ import assert from 'node:assert/strict'; -import {generationProfile,DEFAULT_MODEL,REPAIR_MODEL} from '../Resources/Web/generation-policy.mjs'; +import {generationProfile,DEFAULT_MODEL,FLASH_MODEL,REPAIR_MODEL} from '../Resources/Web/generation-policy.mjs'; +assert.equal(DEFAULT_MODEL,'anthropic/claude-sonnet-5.5'); for(const handle of ['', 'fixture', 'Jeffrey', 'jeffrey-other']) { + // Everyone gets hosted Sonnet with the full Claude budget. assert.equal(generationProfile(handle).model,DEFAULT_MODEL); - assert.equal(generationProfile(handle).rounds,4); - assert.equal(generationProfile(handle).maxTokens,4096); - assert.equal(generationProfile(handle,{repair:true}).model,REPAIR_MODEL); + assert.equal(generationProfile(handle).rounds,12); + assert.equal(generationProfile(handle).maxTokens,16384); + assert.equal(generationProfile(handle).personalRelay,false); + assert.equal(generationProfile(handle,{repair:true}).model,DEFAULT_MODEL); + assert.equal(generationProfile(handle,{model:'anthropic/claude-opus-5.5'}).model,'anthropic/claude-opus-5.5'); + assert.equal(generationProfile(handle,{model:'anthropic/claude-opus-5.5'}).personalRelay,false); + // DeepSeek keeps its lean budget and its text-only repair model. + assert.equal(generationProfile(handle,{model:FLASH_MODEL}).maxTokens,4096); + assert.equal(generationProfile(handle,{model:FLASH_MODEL,repair:true}).model,REPAIR_MODEL); // The text-only repair model cannot take the chalk PNG. - assert.equal(generationProfile(handle,{repair:true,image:true}).model,DEFAULT_MODEL); + assert.equal(generationProfile(handle,{model:FLASH_MODEL,repair:true,image:true}).model,FLASH_MODEL); } -assert.equal(generationProfile('jeffrey').model,'anthropic/claude-opus-5'); -assert.equal(generationProfile('jeffrey').rounds,12); -assert.equal(generationProfile('jeffrey').maxTokens,16384); -assert.equal(generationProfile('jeffrey',{repair:true}).rounds,4); -console.log('PASS personal demo profile; other handles retain existing limits.'); +assert.equal(generationProfile('jeffrey',{personalAccess:true}).model,'anthropic/claude-opus-5'); +assert.equal(generationProfile('jeffrey',{personalAccess:true}).rounds,12); +assert.equal(generationProfile('jeffrey',{personalAccess:true}).maxTokens,16384); +assert.equal(generationProfile('jeffrey',{personalAccess:true,repair:true}).rounds,4); +console.log('PASS hosted Sonnet default for everyone; relay models only with access.'); assert.equal(generationProfile('fixture',{model:'anthropic/claude-opus-5'}).model,DEFAULT_MODEL); -assert.equal(generationProfile('jeffrey',{model:DEFAULT_MODEL}).model,DEFAULT_MODEL); -assert.equal(generationProfile('jeffrey',{model:DEFAULT_MODEL,repair:true}).model,REPAIR_MODEL); -assert.equal(generationProfile('jeffrey').personalRelay,true); +assert.equal(generationProfile('jeffrey',{personalAccess:true,model:DEFAULT_MODEL}).model,DEFAULT_MODEL); +assert.equal(generationProfile('jeffrey',{personalAccess:true,model:FLASH_MODEL,repair:true}).model,REPAIR_MODEL); +assert.equal(generationProfile('jeffrey',{personalAccess:true}).personalRelay,true); assert.equal(!!generationProfile('fixture').personalRelay,false); -assert.equal(generationProfile('jeffrey',{model:DEFAULT_MODEL}).personalRelay,false); +assert.equal(generationProfile('jeffrey',{personalAccess:true,model:DEFAULT_MODEL}).personalRelay,false); -assert.equal(generationProfile('jeffrey',{model:'openai/gpt-6-astra'}).personalRelay,true); -assert.equal(generationProfile('jeffrey',{model:'openai/gpt-6-astra'}).model,'openai/gpt-6-astra'); +assert.equal(generationProfile('jeffrey',{personalAccess:true,model:'openai/gpt-6-astra'}).personalRelay,true); +assert.equal(generationProfile('jeffrey',{personalAccess:true,model:'openai/gpt-6-astra'}).model,'openai/gpt-6-astra'); for (const handle of ['', 'fixture', 'Jeffrey', 'jeffrey-other']) assert.equal(generationProfile(handle,{model:'openai/gpt-6-astra'}).model,DEFAULT_MODEL); const trial={personalAccess:true}; assert.equal(generationProfile('fifi',trial).model,'anthropic/claude-opus-5'); @@ -36,3 +44,6 @@ assert.equal(hasPersonalAccess({personal:true}),false); assert.equal(await fetchPersonalAccess('x',{fetch:async()=>({ok:false})}),null); assert.equal(await fetchPersonalAccess('x',{fetch:async()=>{throw Error('offline')}}),null); console.log('PASS verified trial capability, expiry and fail-closed discovery.'); + +// The handle alone grants nothing; access comes from the relay. +assert.equal(generationProfile('jeffrey').model,DEFAULT_MODEL); diff --git a/apple/whistlegraph/Tests/native-bridge.test.cjs b/apple/whistlegraph/Tests/native-bridge.test.cjs index 193b6ea00e..b370e17e24 100644 --- a/apple/whistlegraph/Tests/native-bridge.test.cjs +++ b/apple/whistlegraph/Tests/native-bridge.test.cjs @@ -130,8 +130,8 @@ const server = http.createServer(async (req, res) => { await page.evaluate(()=>whistlegraphAsk('Draw a clover with a bee')); const state=await page.evaluate(()=>({ledger:JSON.parse(localStorage.getItem('whistlegraph-source-versions')),messages:__nativeMessages,phase:document.getElementById('live-phase').textContent})); assert.equal(state.ledger.head,1,JSON.stringify(state)); - assert.equal(inferenceBodies[0].model,process.argv.includes('--jeffrey')?'anthropic/claude-opus-5':'deepseek/deepseek-v4.1-flash'); - assert.equal(inferenceBodies[0].max_tokens,process.argv.includes('--jeffrey')?16384:4096); + assert.equal(inferenceBodies[0].model,process.argv.includes('--jeffrey')?'anthropic/claude-opus-5':'anthropic/claude-sonnet-5.5'); + assert.equal(inferenceBodies[0].max_tokens,16384); assert.equal(requests,2,'provisional failures do not buy repair inference'); assert.equal(visualRequests,1,'final candidate is still visually checked'); const renders=state.messages.filter(m=>m.action==='render').map(m=>m.source); @@ -243,12 +243,10 @@ const server = http.createServer(async (req, res) => { assert.ok(inferenceBodies[1].messages.some(m=>JSON.stringify(m).includes('REPAIR THIS CANDIDATE ONCE'))); assert.ok(inferenceBodies.every(b=>JSON.stringify(b.messages).includes('DRAWING REFERENCE')),'both passes retain gesture intent'); assert.ok(inferenceBodies.every(b=>b.messages.some(m=>Array.isArray(m.content)&&m.content.some(c=>c.type==='image'))),'initial and repair passes retain chalk pixels'); - assert.ok(inferenceBodies.every(b=>b.max_tokens===4096)); - assert.equal(inferenceBodies[0].model,'deepseek/deepseek-v4.1-flash'); - assert.equal(inferenceBodies[0].thinking.type,'disabled'); - // V4 Pro is text-only on OpenRouter; a chalk repair stays on the image-capable model. - assert.equal(inferenceBodies[1].model,'deepseek/deepseek-v4.1-flash'); - assert.equal(inferenceBodies[1].thinking.budget_tokens,1024); + // Everyone's default is hosted Sonnet with the full Claude budget; the repair keeps it. + assert.ok(inferenceBodies.every(b=>b.max_tokens===16384)); + assert.ok(inferenceBodies.every(b=>b.model==='anthropic/claude-sonnet-5.5')); + assert.ok(inferenceBodies.every(b=>b.thinking.budget_tokens===4096)); const receipt=await page.evaluate(()=>JSON.parse(localStorage.getItem('whistlegraph-source-receipts')).at(-1).receipt); assert.equal(receipt.repairs,1);assert.equal(receipt.rounds.length,process.argv.includes('--repair-fails')?2:3);assert.equal(receipt.rounds[0].reportedModel,'fixture/reported'); assert.equal(receipt.rounds[0].providerRequestID,'provider-1');assert.equal(receipt.rounds[1].usage.costUSD,.001); diff --git a/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj b/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj index 6da5487668..5b35febe0d 100644 --- a/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj +++ b/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj @@ -622,7 +622,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 113; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -707,7 +707,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 113; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_NS_ASSERTIONS = NO; @@ -767,7 +767,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 113; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/apple/whistlegraph/project.yml b/apple/whistlegraph/project.yml index 365c73e502..4e3eff108a 100644 --- a/apple/whistlegraph/project.yml +++ b/apple/whistlegraph/project.yml @@ -16,7 +16,7 @@ settings: CODE_SIGN_STYLE: Automatic SWIFT_VERSION: "5.0" MARKETING_VERSION: "0.1.0" - CURRENT_PROJECT_VERSION: "112" + CURRENT_PROJECT_VERSION: "113" targets: Whistlegraph: type: application diff --git a/system/backend/easel-paid-credits.mjs b/system/backend/easel-paid-credits.mjs index d581fa8472..004a42bdc6 100644 --- a/system/backend/easel-paid-credits.mjs +++ b/system/backend/easel-paid-credits.mjs @@ -146,6 +146,8 @@ const OPEN_PRICES={ 'deepseek/deepseek-v4.1-flash':[0.30,1.20], 'deepseek/deepseek-v4-pro':[0.78,1.57], 'moonshotai/kimi-k3':[3.00,15.0], 'qwen/qwen3.7-plus':[0.32,1.28], 'minimax/minimax-m3':[0.30,1.20], 'z-ai/glm-5.3-flash':[0.15,0.50], + // OpenRouter 2026-10-08. Whistlegraph's default (Sonnet) and premium (Opus). + 'anthropic/claude-sonnet-5.5':[2.00,10.0], 'anthropic/claude-opus-5.5':[4.00,20.0], }; // Braincells per token, worst case: every token priced as output, marked up, // rounded up to a hundredth. Flash comes to 0.48, Kimi K3 to 6. diff --git a/system/backend/easel-policy.mjs b/system/backend/easel-policy.mjs index 0bfca1156e..95ab87bfc0 100644 --- a/system/backend/easel-policy.mjs +++ b/system/backend/easel-policy.mjs @@ -12,6 +12,8 @@ export const EASEL_MODELS = { "z-ai/glm-5.3-flash": { label: "GLM 5.3 Flash" }, "openai/gpt-5.6-luna": { label: "Luna" }, "anthropic/claude-opus-5": { label: "Opus (premium)" }, + "anthropic/claude-sonnet-5.5": { label: "Claude Sonnet 5.5" }, + "anthropic/claude-opus-5.5": { label: "Claude Opus 5.5" }, "z-ai/glm-4.6": { label: "glm" }, "qwen/qwen3-coder": { label: "qwen" }, "deepseek/deepseek-chat-v3.1": { label: "deepseek" },