Simon Willison’s Weblog

Newsletter
Atom feed

Releases

Filters: Sorted by date

I released ttok 0.4, ran uv tool upgrade ttok, piped a file into the new version... and realized that it was defaulting to the GPT-4 tokenizer when it should very clearly default to GPT-5/GPT-6 instead!

I figured switching the default was a reasonable excuse to finally ship a 1.0.

OpenAI haven't actually confirmed that GPT-6 uses the same tokenizer as the GPT-5 family yet - there's an angry issue about it - but I found this commit by William Liu which reports on an experiment he ran confirming that the tokenizers are likely the same:

All seven GPT models (5.5, 5.6 Sol/Terra/Luna, 6 Astra/Sol/Luna) report 44,794 tokens and match each other on every one of the 31 fixtures. GPT-6 introduces no input-count change on this corpus.

ttok is my CLI tool for counting tokens, using OpenAI's open source tiktoken library.

It hasn't been in updated in a couple of years, but I finally fixed a Click warning, updated CI, and added a --list-models command to list available models.

It works with uvx, so you can count tokens in anything like this:

cat file.txt | uvx ttok

OpenAI released their new Jev-style Decisions API, as previously announced at last week's DevDay.

Since I already have an llm-typesafe plugin for talking to Jev, I had GPT-6 Astra read the new OpenAI API documentation and build an llm-openai-decisions plugin inspired by llm-typesafe.

Unlike Jev, the new gpt-6-luna decision model supports image input in addition to text. Both models charge for input it and not for output: OpenAI's is 10 cents per million input tokens, Jev's is 4.2 cents per million.

Otherwise the API shape is very similar to Jev, at least conceptually. Jev supports three question types for yes/no, choices, or scores. OpenAI Decisions supports the same three types.

Install the plugin like this:

llm install llm-openai-decisions

Here's an example query against an image attachment:

llm -m openai-decisions/gpt-6-luna \
  -a https://static.simonwillison.net/static/2025/two-pelicans.jpg \
  -s 'Does this image contain any mammals?'

And example output:

{"type": "predicate", "name": "evaluation", "probability": 0.0}

Consult the README for full details of how to run the other types of questions.

Adds support for reasoning models, such as the newly released Mistral Large 4.

A minor fix for compatibility with the latest Datasette alphas. This meant we could upgrade the datasette.io site to Datasette 1.0a41.

pwasm is one of my folly projects - an entirely vibe-coded pure Python WebAssembly engine that I built in January during my first bout of AI mania.

I hadn't touched it since January, so I decided to let Claude Opus 5.5 loose on it and see if it could make any significant improvements:

Evaluate current state of pwasm - then consider what it would take to get the MicroPython and micro JavaScript experiments from the research repo working under it - and what it would take to speed it up

42 commits later (with minimal follow-up prompting) it now handles almost all of the WASM specification, and the wheel from PyPI bundles working WASM builds of MicroPython, QuickJS and Micro QuickJS.

I wouldn't trust this thing at all - hence the alpha version tag - but it's interesting seeing how today's models can improve on the work of models from 10 months ago.

In addition to Claude Sonnet 5.5, this release adds the ability to run llm anthropic refresh to refresh the list of Anthropic models directly from their API - which means I don't need to push a new release just to add support for a newly released model.

I also added an llm anthropic count command which can use their free token counting API to return a count of tokens that will be used by any prompt, before you send that prompt.

Support for branches other than the default branch. Use uvx commit-rewriter --branch other to run against another branch. #3

Alec Garcia added support for OpenTelemetry to Datasette in this release.

I've also refactored all of Datasette's modal dialogs to a single Web Component, which is now documented for other plugins to use.

  • New OpenAI models: gpt-6-sol for GPT-6 Sol and gpt-6-luna for GPT-6 Luna. #1702
  • Model plugins can now declare supports_conversation = False for models that only accept single-turn prompts. LLM raises llm.ConversationNotSupported when these models receive assistant or tool history, and llm chat rejects them before starting a session. See Models that do not support conversations. The first plugin to use this is llm-typesafe. #1692
  • Reasoning traces in the Markdown output of llm logs are now wrapped in <details><summary> tags. #1701

Plus bug fixes from five new contributors.

Adds support for Claude Opus 5.5:

llm -m claude-opus-5.5 "prompt goes here"

I built this new plugin for LLM to add support for TypeSafe AI's new Jev model. Install it like this:

llm install llm-typesafe

Then set an API key (get one here, the waitlist seems to move pretty fast):

llm keys set typesafe
# Paste key

And now you can ask yes/no "noul" questions like this:

llm -m jev 'Please refund my last payment.' \
  -s 'Does this message explicitly request a refund?'

Output:

{"type": "noul", "noul": 0.99}

Or choice questions like this:

cat message.txt | llm -m jev \
  -s 'Which team should handle this message? If billing and technical issues both occur, choose billing.' \
  -o answer_type choice \
  -o criteria '{
    "billing":"Charges, invoices, payments, or refunds",
    "technical":"Problems installing or using the product",
    "other":"Neither category fits"
  }'

Or scoring questions like this:

cat report.txt | llm -m jev \
  -s 'How reproducible is the problem described in this report?' \
  -o answer_type score \
  -o criteria '[
    "No reproduction instructions",
    "Some instructions, but important steps are missing",
    "Complete steps with expected and actual results"
  ]'

See the README for more details.

This plugin solves a very specific problem.

I've started using Codex Remote to run coding agents on various machines while controlling them from my phone.

Sometimes I use those machines to hack on LLM projects, and occasionally that means I need to configure an API key.

I don't like pasting API keys into agent sessions, so I wanted a way to get those keys onto a machine without pasting them into the ChatGPT app directly.

With this plugin, I can tell Codex to run:

uvx --with llm-keys-ui llm keys-ui --all

Then have it tell me the URL - including local network or Tailscale device IPs - for an interface to save additional API keys.

Then later it can use a command like llm keys get anthropic as part of a shell command when it needs to use a key.

Chat conversation requesting uvx --with llm-keys-ui llm keys-ui --all, with a response listing four server URLs on port 8010 and confirming the server is still running. LLM keys web interface listing anthropic, openai, openrouter, and qwen-dummy as stored keys, with a form containing Key name and New value fields and a Save key button. Existing key values are never displayed.
  • Explain plans now work on read-only stored-query pages.

I upgraded datasette.simonwillison.net to Datasette 1.0a40, which inspired me to ship a new version of this explain plugin.

I run this GitHub login plugin on the agent.datasette.io demo site and I noticed that my authenticated sessions weren't lasting very long. It turned out that the plugin was setting cookies without a Max-Age parameter, so they were expiring at the end of a browser session (which in Mobile Safari seems to happen pretty often, independently of how you are using the app.)

I fixed that in #80 and, since this plugin has been around for quite a while and is tested against both Datasette 0.65.x and Datasette 1.0ax, I decided to bump it up to a 1.0 release. I'm trying to get better at promoting stable plugins to 1.0.

Same security fix as 0.65.5, plus some neat new features and bug fixes:

  • Plugins can now launch and manage background tasks using the new datasette.add_background_task() method. Thanks, Alex Garcia.
  • I've migrated Datasette to httpx2 for features like the internal datasette.client.get() method.
  • A whole lot of bug fixes, many of them stemming from a recent effort to triage issues for a 1.0 stable release.

Security fix for an issue where a trailing newline in a requested table name could bypass table permissions and expose private rows, reported by dpfkdlemtp in GHSA-h547-rmjf-5m2m.

I built this little web app the other day to help edit the commit messages for the Datasette security releases. The initial commits were full of coding agent cruft and references to issue IDs from our private repository, so they weren't fit for publication.

If you want to edit the commit messages for a repository you can run it like this:

uvx commit-rewriter path/to/repo

Omit the path if you are already in the directory for that repo.

Screenshot of the commit-rewriter web interface. A heading reads commit-rewriter above the repository path and current branch and commit hash, with a short description of the tool. A toolbar shows a pending edits count with Discard drafts and Rewrite commit messages buttons, followed by a search box for message, author, or hash and an Edited only checkbox. A left sidebar titled Navigate commits lists recent commit messages with their short hashes. The main panel shows a card for each commit with its hash, author and timestamp, an editable text area containing the commit message, and a View full formatted diff toggle.

When you submit your edits the tool creates a timestamped branch of your current repo state - to allow you to revert if you need to - and then rewrites every commit from the first one you edited to the most recent.

I've added WebP support to my shot-scraper screenshot automation tool. You can now take a WebP screenshot of a web page like this:

shot-scraper https://simonwillison.net -o screenshot.webp --quality 80

The --quality option sets the quality - without that option the WebP file will be lossless.

In my experience WebP screenshots are almost always significantly smaller in file size than their JPEG or PNG equivalents. See the PR for some examples.

I shipped this feature so I could use it to generate the screenshot for my new commit-rewriter tool.

  • Sets force_https=true in fly.toml. #31
  • Fix for Volume could not be found bug. #32
  • Compatible with app-scoped deploy tokens. #34
  • Performance fix for loading OpenRouter models. Thanks, waveplate. #59

One new feature:

  • llm logs --usage Markdown output now includes the response duration in milliseconds and as a human-readable duration. llm logs --short includes a new duration_ms field. #1653

Plus several contributed bug fixes, and a significant performance improvement to llm logs thanks to waveplate on GitHub, see also llm-openrouter 0.7.1.

Claude Fable 5.1, reasoning traces are now displayed by default for models that support them, plus a new llm_anthropic.ClaudeRefusal exception for when Claude throws a refusal.

Google released Gemini 3.8 Flash (and 3.8 Flash Cyber, but that's available to "trusted defenders" only) today.

Here are the pelicans for high, medium, and low. This is high:

Description by Gemini 3.8 Flash: Digital illustration of a cartoon pelican wearing a red and white polka-dot scarf riding a teal cruiser bicycle along a wooden boardwalk by the beach, with a small blue fish in the front basket and a glowing sun over the ocean.

For comparison, here are the same pelicans generated using Gemini 3.7 Flash.

Something I appreciate about Gemini Flash is that it's fast, cheap, and competent at things like HTML and JavaScript. I was messing around with it and prompted "make me a cool thing in html" and it built this, which is certainly a cool thing in HTML! Took 13 seconds, cost 1.8 cents.

If you click through to the demo you'll see one more thing I built with Gemini 3.8 Flash.

My markdown-svg-renderer tool lets me feed in the URL to a Gist with Markdown in and renders that markdown with fenced code blocks for SVG correctly rendered.

I used Gemini 3.8 Flash (with my very basic llm-coding-agent coding agent plugin) to add support for HTML as well, so now any HTML blocks in the Markdown are rendered using a sandboxed iframe. Here's the transcript.

  • "rows" from execute_sql is now an array of objects. Previously it was an array of arrays. This should help weaker models avoid losing track of which positional array element maps to which column. #1
  • Now depends on mcp>=2.1.1.

This is the first non-alpha release of the plugin. I'm confident it's ready as I've been using it quite a bit myself.

This release of the Anthropic plugin for LLM mainly provides compatibility with the recently released anthropic v1.0.0 Python library, which switches from httpx to httpx2. OpenAI made the same change in their v3.0.0 release two weeks ago.

Anthropic provide this migration guide for upgrading to 1.0, so I prompted Fable 5 in Claude Code with:

Upgrade to anthropic>=1 - read https://raw.githubusercontent.com/anthropics/anthropic-sdk-python/refs/heads/main/MIGRATION.md and get the tests passing

Here's the resulting PR.