Skip to content

feat: add ModelRunner as a BYOK vision provider - #2

Open
hakankaan wants to merge 1 commit into
LowCarbCheck:mainfrom
hakankaan:feat/modelrunner-provider
Open

feat: add ModelRunner as a BYOK vision provider#2
hakankaan wants to merge 1 commit into
LowCarbCheck:mainfrom
hakankaan:feat/modelrunner-provider

Conversation

@hakankaan

Copy link
Copy Markdown

Upfront disclosure: I work on ModelRunner, so this PR adds my own company as a BYOK provider. Written with Claude Code; I've read and run everything in it.

On your "open an issue first" rule — you're right that this counts as non-trivial and I should have asked first. I went straight to code because the interesting part wasn't the code, it was whether it could work at all: openplate calls the provider from the page, and until yesterday ModelRunner answered no CORS headers, so a provider entry would have been dead on arrival. Fixing that was on my side, and I'd rather show you a working thing than describe an intention. Happy to convert this to an issue and close the PR if you'd prefer the order you asked for.

What this adds

ModelRunner as a sixth AiProviderType, riding the existing openai-compatible adapter with no adapter change at all.

Base URL https://queue.modelrunner.run/v1
Adapter openai-compatible
Placement advanced
Auth manual key, Authorization: Bearer
Key check GET {base}/models
Key console https://modelrunner.ai/settings/api-keys

Nine files: the enum, two wire constants, the registry entry + PROVIDER_IDS, one catalog entry, the five total maps in settings.ai.tsx, en/de copy, and the two provider manifests in tests/unit/.

The CORS part, since it is the reason this is possible

OPTIONS https://queue.modelrunner.run/v1/chat/completions now answers:

HTTP/2 204
access-control-allow-origin: *
access-control-allow-methods: GET, POST, OPTIONS
access-control-allow-headers: authorization,content-type
access-control-max-age: 86400

No access-control-allow-credentials — that surface authenticates only from the Authorization header, sets no cookie and issues none, so there is no ambient authority for a page to ride. That is the same shape your Mistral entry documents, and it is what makes baseUrl safe to put in the CSP's connect-src. Probed against production on 2026-09-09; the error path carries the headers too, so a bad key surfaces as a readable 401 rather than an opaque network failure.

Choices worth flagging

  • placement: 'advanced', not a third primary tab. Adding myself beside OpenRouter and Mistral felt presumptuous, and it would have meant touching recommendedProviderFor. The catalog is still populated, so CatalogModelSection renders the picker and the per-scan cost line exactly as it does for Anthropic — placement decides which panel it appears in, not whether it is curated. Say the word if you'd rather it were primary.
  • Exactly one model, and the honesty problem it creates. ModelRunner publishes one chat model that accepts images (alibaba/qwen3.8-max); the rest of its chat catalog is text-only and would fail a plate scan on the missing input rather than return a worse answer. Your invariant requires exactly one recommended per non-empty catalog, so it is marked recommended by arithmetic, not by evidence — the blurb says "not yet compared on real plates", mirroring your own wording for gpt-5.6-luna. Nothing here has been through the three-plate comparison your Mistral entry records, and I did not want to imply otherwise.
  • Pricing $2.00 in / $6.00 out per 1M, read off the platform's public catalog on 2026-09-08. describeApproxScanCost puts that under a cent a photo.
  • No disableReasoning. The platform documents no reasoning parameter, so there is nothing for the adapter to send — and per your own comment on that field, setting it where it can't be expressed is worse than leaving it off.
  • Empty API-key placeholder, same as Mistral: ModelRunner keys are 64 bare hex characters with no prefix, so there is no shape to teach.
  • The /models key check is a real check, and worth stating because a 401 usually proves nothing. Every other path under /v1 answers 404 without a key (/v1/nonsense, /v1/models/extra), while /v1/models answers 401 — so that 401 is the route authenticating rather than a catch-all, and a bad key genuinely cannot pass it. Not the OpenRouter /models situation your comment warns about.
  • The German is mine and has not been read by a native speaker. i18n-key-parity requires both locales, so leaving it out was not an option; please treat the two de strings as drafts.

Testing

All five pre-push gates, in your order, on this branch:

pnpm lint 0
pnpm typecheck 0
pnpm test:unit 3435 pass, 0 fail
pnpm test:integration 29 pass, 0 fail
pnpm build 0

The typecheck is the load-bearing one here: every satisfies Record<AiProviderType, …> map plus the AssertNever on PROVIDER_IDS means a missed touchpoint is a compile error, not a runtime surprise. It found all five maps in settings.ai.tsx for me.

Before the manifest updates, test:unit was 3432 pass / 1 failgetProvidersByPlacement's "keeps OpenRouter and Mistral primary and the rest behind Advanced", which is exactly the test that is supposed to fail here. I updated that expectation and added two specs to vision-catalog.test.ts pinning the id shape (namespaced owner/alias, like OpenRouter's) and the single recommended entry with its rates.

One local-environment note in case it saves you time: pnpm-workspace.yaml pins supportedArchitectures.os: [linux], so a macOS pnpm install gets no darwin rolldown/esbuild bindings and lint/typecheck die with Cannot find module './rolldown-binding.darwin-arm64.node'. I added darwin locally to run the gates and reverted it — the file is untouched in this PR, and so is pnpm-lock.yaml.


Happy to revise anything here — the placement, the blurb wording, or splitting it up. Leave a comment and I'll push a change.

🤖 Generated with Claude Code

ModelRunner serves an OpenAI-compatible chat surface with CORS on it, so it
rides the existing openai-compatible adapter unchanged: one registry entry,
one catalog list, and the total maps the compiler demands.

Behind Advanced rather than a third primary tab. The catalog is still
populated, so the picker and the per-scan cost line both work — placement
decides which panel it appears in, not whether it is curated.

Exactly one model, because ModelRunner publishes exactly one chat model that
accepts images; the rest of its chat catalog is text-only and would fail a
plate scan on the missing input rather than return a worse answer. It is
marked recommended only because the invariant requires one per non-empty
catalog, and the blurb says as much — nothing here has been compared against
real plates.

The key check is /models, which is a real one: every other path under /v1
answers 404 without a key while that one answers 401, so a bad key cannot
pass it.

Guide copy is English and German; the German is mine and has not been read
by a native speaker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant