feat: add ModelRunner as a BYOK vision provider - #2
Open
hakankaan wants to merge 1 commit into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 existingopenai-compatibleadapter with no adapter change at all.https://queue.modelrunner.run/v1openai-compatibleadvancedAuthorization: BearerGET {base}/modelshttps://modelrunner.ai/settings/api-keysNine files: the enum, two wire constants, the registry entry +
PROVIDER_IDS, one catalog entry, the five total maps insettings.ai.tsx, en/de copy, and the two provider manifests intests/unit/.The CORS part, since it is the reason this is possible
OPTIONS https://queue.modelrunner.run/v1/chat/completionsnow answers:No
access-control-allow-credentials— that surface authenticates only from theAuthorizationheader, 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 makesbaseUrlsafe to put in the CSP'sconnect-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 touchingrecommendedProviderFor. The catalog is still populated, soCatalogModelSectionrenders 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.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 onerecommendedper 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 forgpt-5.6-luna. Nothing here has been through the three-plate comparison your Mistral entry records, and I did not want to imply otherwise.describeApproxScanCostputs that under a cent a photo.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./modelskey check is a real check, and worth stating because a 401 usually proves nothing. Every other path under/v1answers 404 without a key (/v1/nonsense,/v1/models/extra), while/v1/modelsanswers 401 — so that 401 is the route authenticating rather than a catch-all, and a bad key genuinely cannot pass it. Not the OpenRouter/modelssituation your comment warns about.i18n-key-parityrequires both locales, so leaving it out was not an option; please treat the twodestrings as drafts.Testing
All five pre-push gates, in your order, on this branch:
pnpm lintpnpm typecheckpnpm test:unitpnpm test:integrationpnpm buildThe typecheck is the load-bearing one here: every
satisfies Record<AiProviderType, …>map plus theAssertNeveronPROVIDER_IDSmeans a missed touchpoint is a compile error, not a runtime surprise. It found all five maps insettings.ai.tsxfor me.Before the manifest updates,
test:unitwas 3432 pass / 1 fail —getProvidersByPlacement'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 tovision-catalog.test.tspinning the id shape (namespacedowner/alias, like OpenRouter's) and the single recommended entry with its rates.One local-environment note in case it saves you time:
pnpm-workspace.yamlpinssupportedArchitectures.os: [linux], so a macOSpnpm installgets no darwin rolldown/esbuild bindings andlint/typecheckdie withCannot find module './rolldown-binding.darwin-arm64.node'. I addeddarwinlocally to run the gates and reverted it — the file is untouched in this PR, and so ispnpm-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