feat(cli): add vlmrun gateway / gw CLI for the OpenAI-compatible model gateway - #203
Conversation
…models Add a gateway resource and CLI subcommand group that talk to the OpenAI-compatible model gateway (https://gateway.vlm.run/v1) using the same VLMRUN_API_KEY. Mirrors the existing Agent.completions pattern. - vlmrun/client/gateway.py: Gateway resource (completions / async_completions, models(), health()), pointed at {gateway_url}/openai. Configurable via VLMRUN_GATEWAY_URL. Wired into VLMRun as client.gateway. - vlmrun/cli/_cli/gateway.py: `gw health`, `gw models` (pricing table + --json), `gw chat FILES... -m MODEL` with base64 data-URL file inlining, optional -p prompt, -e key=value extras, streaming, and --json output. Requires >=1 input file since most gateway (OCR) models do not accept text-only input. - constants: DEFAULT_GATEWAY_URL. - tests/test_gateway.py: 24 tests (resource resolution/health, CLI helpers, command flows) using concrete mocks per repo conventions. - docs: gateway section + VLMRUN_GATEWAY_URL in CLI README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XUFL12cRkGcTo4S7HcTbJ
…inputs Send documents (.pdf/.doc/.docx) as `document_url` content parts and other files (e.g. images) as `file_url` parts, instead of `image_url`, matching the gateway chat-completions contract. Values remain base64 `data:` URLs. - gateway CLI: add `_content_part_type()`; `_encode_file_part` selects document_url vs file_url by extension. - update client docstring example, README, and module docstring. - tests: cover content-part selection, mixed-file ordering, and an end-to-end capture asserting the wired `gw chat` sends document_url/file_url (27 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XUFL12cRkGcTo4S7HcTbJ
…dling Verified end-to-end against the live gateway; every fix below was found by running the CLI on real files rather than by reading the code. Fixes: - Images were sent as `file_url`, which the gateway routes through its document/PDF path. That 400s outright on some images and returns less content when it works. Images now use `image_url`. - MIME type came from the filename extension, so a WebP named `.jpg` produced a `data:image/jpeg` URL that lied. The gateway trusts the declared type and misroutes the file. MIME is now sniffed from magic bytes, falling back to the extension. - The gateway only streams document requests; image- and text-only requests return a plain chat.completion body still labelled text/event-stream, which an SSE reader drains to empty. `chat` now streams only when a document is present. This made image OCR and all VQA silently return nothing. - `--extra` sent gateway-only fields (`method`, `document_dpi`, ...) as top-level create() kwargs, which the OpenAI SDK rejects with a TypeError. Non-OpenAI keys now route through `extra_body`, split by introspecting the installed SDK's signature. - OCR output is wrapped in <document>/<page> tags, which Rich's Markdown renderer treats as HTML and drops, blanking the panel. Tag/JSON payloads now render as plain text. - Corrected model ids in help: `paddle-ocrv6` and `qwen3.6-0.8b` do not exist. Features: - `vlmrun gateway` works alongside `vlmrun gw`. - `chat --method/--method-params` for model methods (ocr, detect, markdown, parse_layout, ...). - `gw models <model>` details one model's methods, params and runnable example commands, derived from the live catalog. Replaces the two pricing columns, which were always empty because the API returns no pricing fields. - `gw embed` for text/image/video embeddings. Multimodal `input` nests content parts one level deeper than the docs show; more than one image per item 500s, so joining is explicit via `--join`. - `gw transcribe` for audio, or a video's audio track. - SDK: `client.gateway.embeddings` and `client.gateway.transcriptions`. Tests: 75 gateway tests (was 27). Five existing tests asserted the broken `file_url` behaviour and were corrected. Fakes now mirror real API payloads: the previous FakeModel invented a `pricing` field the gateway never returns, and FakeCompletions accepted any kwarg, which is why the TypeError above was never caught. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces an OpenAI-compatible model gateway resource, exposing third-party OCR and vision-language models through both the SDK (client.gateway) and the CLI (vlmrun gw). It includes commands for health checks, model listing, chat/OCR completions, embeddings, and audio transcriptions, along with comprehensive test coverage. The feedback highlights opportunities to improve error handling by raising a user-friendly DependencyError when the openai package is missing, validate file types in the embedding endpoint to prevent gateway failures, respect user-configured custom timeouts instead of silently overriding them, and align model IDs in the README with the CLI help strings.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| key = "video_url" if mime.startswith("video/") else "image_url" | ||
| return {"type": key, key: {"url": f"data:{mime};base64,{b64}"}} |
There was a problem hiding this comment.
If a user passes a non-image/non-video file (such as a PDF or text document) to the embed command, it will fall back to treating it as an image_url with an incorrect MIME type, which will fail on the gateway. Consider validating that the file is indeed an image or video and raising a clean error message.
if mime.startswith("video/"):
key = "video_url"
elif mime.startswith("image/"):
key = "image_url"
else:
console.print(
f"[red]Error:[/] Unsupported file type '{mime}' for {path.name}. "
"Embedding models only support images and videos."
)
raise typer.Exit(1)
return {"type": key, key: {"url": f"data:{mime};base64,{b64}"}}There was a problem hiding this comment.
Resolved in 1fa3bce. gw embed now rejects any non-image/non-video file client-side with a clear message ("images and video only; use --text for text") instead of sending a mislabelled image_url the gateway 400s on. Verified live: a PDF is rejected before any request. Covered by test_embed_rejects_non_image_file.
| The gateway (`https://gateway.vlm.run/v1`) exposes third-party OCR and | ||
| vision-language models (e.g. `glm-ocr`, `paddle-ocrv6`, `qwen3.6-0.8b`) through | ||
| an OpenAI-compatible API, authenticated with the same `VLMRUN_API_KEY`. | ||
|
|
||
| Unlike `vlmrun chat` (which calls the Orion agent), the gateway is a raw | ||
| passthrough to the underlying models: input files are sent inline as base64 | ||
| `data:` URLs — documents as `document_url` content parts and other files (e.g. | ||
| images) as `file_url` parts. **Most models — especially OCR models — do not | ||
| accept text-only input**, so at least one file is required. | ||
|
|
||
| ```bash | ||
| # Health check | ||
| vlmrun gw health | ||
|
|
||
| # List gateway models with pricing ($ per 1M tokens) | ||
| vlmrun gw models | ||
| vlmrun gw models --json | ||
|
|
||
| # Parse a document (PDF -> text/markdown) | ||
| vlmrun gw chat document.pdf -m glm-ocr | ||
|
|
||
| # Multiple documents | ||
| vlmrun gw chat doc1.pdf doc2.pdf -m paddle-ocrv6 | ||
|
|
||
| # OCR an image | ||
| vlmrun gw chat scan.jpg -m paddle-ocrv6 | ||
|
|
||
| # Prompt a model that supports text input | ||
| vlmrun gw chat image.jpg -p "describe this image" -m qwen3.6-0.8b | ||
|
|
||
| # Forward extra completion kwargs as key=value (JSON-parsed) | ||
| vlmrun gw chat document.pdf -m glm-ocr -e temperature=0 -e max_tokens=4096 | ||
| ``` |
There was a problem hiding this comment.
Fixed in 1fa3bce. Rewrote the whole README gateway section: corrected the model ids (paddleocr/pp-ocrv6, qwen/qwen3.5-0.8b), and it now uses full <org>/<name> ids throughout.
| vlmrun gw chat doc1.pdf doc2.pdf -m paddle-ocrv6 | ||
|
|
||
| # OCR an image | ||
| vlmrun gw chat scan.jpg -m paddle-ocrv6 | ||
|
|
||
| # Prompt a model that supports text input | ||
| vlmrun gw chat image.jpg -p "describe this image" -m qwen3.6-0.8b |
There was a problem hiding this comment.
🟡 Documentation examples use model names that don't exist, so copy-pasted commands fail
The gateway examples reference the models paddle-ocrv6 and qwen3.6-0.8b (README vlmrun/cli/README.md:246-252), which are the exact identifiers this change states do not exist (the real ones are pp-ocrv6 and qwen/qwen3.5-0.8b), so anyone copy-pasting these commands hits a "model not found" error.
Impact: Users following the documentation run commands that fail immediately against the gateway.
Stale model identifiers in newly added docs
The PR description explicitly lists these as wrong ("paddle-ocrv6 and qwen3.6-0.8b don't exist (pp-ocrv6, qwen/qwen3.5-0.8b)") and the CLI help strings in vlmrun/cli/_cli/gateway.py:52-56 use the corrected ids, but this README section (added in the same change) and the prose at vlmrun/cli/README.md:225 were not updated. AGENTS.md requires docs to be kept in sync with the implementation.
| vlmrun gw chat doc1.pdf doc2.pdf -m paddle-ocrv6 | |
| # OCR an image | |
| vlmrun gw chat scan.jpg -m paddle-ocrv6 | |
| # Prompt a model that supports text input | |
| vlmrun gw chat image.jpg -p "describe this image" -m qwen3.6-0.8b | |
| vlmrun gw chat doc1.pdf doc2.pdf -m pp-ocrv6 | |
| # OCR an image | |
| vlmrun gw chat scan.jpg -m pp-ocrv6 | |
| # Prompt a model that supports text input | |
| vlmrun gw chat image.jpg -p "describe this image" -m qwen/qwen3.5-0.8b |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 1fa3bce — the stale paddle-ocrv6/qwen3.6-0.8b ids and the prose at line 225 are corrected; the section now uses full <org>/<name> ids.
| # Health check | ||
| vlmrun gw health | ||
|
|
||
| # List gateway models with pricing ($ per 1M tokens) |
There was a problem hiding this comment.
🟡 Documentation claims the model listing shows pricing that the tool no longer displays
The docs describe the models listing as showing pricing per 1M tokens (vlmrun/cli/README.md:238 and vlmrun/cli/README.md:261), but the implementation deliberately removed the pricing columns and now shows only model/task/methods, so the described output does not match what users see.
Impact: Readers are told to expect pricing information from a command that never prints it.
Docs describe removed pricing columns and old file_url routing
The models command in vlmrun/cli/_cli/gateway.py:446-475 builds a table with MODEL/TASK/METHODS columns and no pricing. The PR description confirms pricing columns were removed because "the API returns no pricing fields at all." Additionally vlmrun/cli/README.md:230-231 still says images are sent as file_url parts, whereas the code now routes images through image_url (vlmrun/cli/_cli/gateway.py:123-138). AGENTS.md requires documentation to be kept in sync with the implementation.
Prompt for agents
The README gateway section is out of sync with the implementation in vlmrun/cli/_cli/gateway.py. Two issues: (1) Lines 238 and 261 describe the `vlmrun gw models` command as listing pricing ("pricing ($ per 1M tokens)" and "input/output pricing"), but the models command no longer prints pricing columns — it shows MODEL/TASK/METHODS and supports detailing a single model. Update these to describe methods/task listing and the per-model detail view instead. (2) Lines 230-231 state images are sent as `file_url` content parts, but the code now sends images as `image_url` (documents as `document_url`, `file_url` only as fallback). Update the prose to match.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 1fa3bce. Dropped the pricing claim (the API returns no pricing; gw models shows task + methods and details one model), and corrected the file_url prose to image_url for images with file_url as fallback. Also documented embed/transcribe/the gateway alias.
Minor rather than patch: this branch adds the `gw`/`gateway` command group (health, models, chat, embed, transcribe) and the `client.gateway.embeddings` / `client.gateway.transcriptions` SDK resources. Follows the precedent of #180, which bumped 0.5.12 -> 0.6.0 for a new CLI command group. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vlmrun gateway / gw CLI for the OpenAI-compatible model gateway
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…ut, docs Resolves review feedback on PR #203: - _openai_create_params now routes a missing `openai` install through the SDK's DependencyError (install hints) instead of a raw ImportError. - `gw embed` rejects non-image/non-video files client-side with a clear message, rather than sending a mislabelled image_url the gateway 400s on. - Gateway timeout floor (600s) now applies only when the client is at its 120s default; an explicit shorter/longer timeout is respected. - README: corrected stale model ids (paddle-ocrv6 -> paddleocr/pp-ocrv6, qwen3.6-0.8b -> qwen/qwen3.5-0.8b), dropped the removed pricing claim, fixed the file_url->image_url description, and documented models detail / methods / embed / transcribe / the `gateway` alias. Also switched help/examples to full `<org>/<name>` model ids (aliases still work); `gw models` already lists full ids. Verified end-to-end against the live gateway with full model names: chat (all methods), embed (text/image + PDF-rejection), transcribe (json/srt). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Improvements found by running the gateway CLI end-to-end:
- Broaden the `gw` group help to cover embed/transcribe and name
VLMRUN_API_KEY; point users at `gw models` for discovery.
- Fix stale `gw embed --text` help (a file + text are separate vectors unless
--join; it claimed joint).
- Surface `{"error": ...}` response bodies (e.g. an unknown --method) as a real
error with non-zero exit, instead of a success-styled Response panel. Detects
only a lone `error` key, so normal `{"text": ...}` / <document> output is
unaffected.
- Align the `gw models --json` example column.
Adds `.claude/commands/vlmrun-gateway.md`, an objective-driven playbook that
codifies exploring and improving the gateway CLI against the live gateway
(auth check, exercise every surface, known failure modes, prove-the-test,
keep docs in sync).
Tests: 85 gateway tests (+4). Full suite green, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous /vlmrun-gateway was a developer playbook for debugging/improving the CLI. Replace it with a user-facing natural-language interface: given a request like "embed this image ~/data/image.jpg" it parses the intent + inputs, maps to the right `vlmrun gw` subcommand/model/method, runs it, and reports. Covers image understanding (VQA), document parsing / OCR / markdown / layout, embedding (text/image/video, --join, --dimensions), and transcription (audio + video audio track, formats, --url). Model defaults are verified against the live catalog and every example mapping was run end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gateway returns a per-request `usage.cost` (USD) that the CLI was discarding. `gw chat` now shows it in the panel footer (e.g. `… · $0.001508 · 1.35s`), and `--json` already carried it under `usage.cost`. Sub-cent costs print without scientific notation and never collapse a real cost to "$0". Embeddings/transcriptions responses carry no cost, so their output is unchanged. Also notes cost reporting in the /vlmrun-gateway-cli command so the natural-language interface surfaces (and can total) it. Tests: +4 (format + panel + json). Full suite green, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an OpenAI-style `response_format` to `gw chat`: 'text', 'json_object' (JSON mode), or a full JSON object (e.g. json_schema). Sent to the gateway as the top-level `response_format` field. The gateway does not honor it yet — it is accepted and currently ignored server-side — so this wires it up now to test once support lands. Distinct from `--json`, which controls the CLI's own output format; the help calls this out to avoid confusion. Tests: +5 (parser shorthands/object/invalid, wired-through, invalid-value exit). Full suite green, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds
vlmrun gateway(aliasedvlmrun gw), a CLI for the OpenAI-compatible VLM Run gateway — OCR, VQA, embeddings and transcription against third-party models, authenticating with the sameVLMRUN_API_KEYas the rest of the SDK.Also adds
client.gateway.completions,.embeddingsand.transcriptionsto the Python SDK.Verified against the live gateway
Every model and method in the catalog, run end-to-end through the CLI on real files:
paddleocr/pp-ocrv6gw chat --method ocr <image>.jpg*paddleocr/pp-ocrv6gw chat --method detect <image>.jpgpaddleocr/pp-ocrv6gw chat --method markdown <image>.jpgpaddleocr/pp-ocrv6gw chat --method ocr --method-params '{"lang":"en","score_threshold":0.5}' <image>.jpgzai-org/glm-ocrgw chat --method ocr <image>.jpg*zai-org/glm-ocrgw chat --method markdown <image>.jpgzai-org/glm-ocrgw chat --method markdown <document>.pdfrednote-hilab/dots.mocrgw chat --method parse_layout <image>.jpg*rednote-hilab/dots.mocrgw chat --method parse_layout_only <image>.jpgrednote-hilab/dots.mocrgw chat --method ocr <image>.jpgrednote-hilab/dots.mocrgw chat --method markdown <image>.jpgqwen/qwen3.5-0.8bgw chat -p "what car is this?" <image>.jpg*qwen/qwen3-vl-embedding-2bgw embed -t "a blue parrot"qwen/qwen3-vl-embedding-2bgw embed <image>.jpgqwen/qwen3-vl-embedding-2bgw embed <image>.jpg <image2>.jpg -t "caption"qwen/qwen3-vl-embedding-2bgw embed --join -t "caption" <image>.jpgqwen/qwen3-vl-embedding-2bgw embed --dimensions 64 -t "hi"qwen/qwen3-vl-embedding-2bgw embed <video>.mp4nvidia/parakeet-tdt-0.6b-v3gw transcribe -f json/text/verbose_json <audio>.mp3nvidia/parakeet-tdt-0.6b-v3gw transcribe -f srt <audio>.mp3/-f vttnvidia/parakeet-tdt-0.6b-v3gw transcribe <video>.mp4nvidia/parakeet-tdt-0.6b-v3gw transcribe --language en --prompt "..." <audio>.wav*= model's default method. Every row green except the video-embedding caveat, which is server-side.Cross-modal retrieval works as expected:
cos(parrot image, "a colorful parrot bird")= 0.708 vscos(pcb image, same text)= 0.146.Implementation notes
Each of these looks arbitrary in the diff but was forced by observed gateway behaviour. They're the parts most worth reviewing.
Images are sent as
image_url, notfile_url.file_urlis routed through the gateway's document/PDF path: it 400s outright on some images and returns less content when it does work (2350 vs 645 chars on the same file). Confirmed it's the content-part type, not the model — withfile_urlthe same image 400s on all three OCR models; withimage_urlall three succeed.file_urlremains the fallback for anything unidentifiable.MIME is sniffed from magic bytes, not the extension. Extensions lie — one of our demo files is a WebP named
.jpg. Trusting it emitteddata:image/jpeg;base64,<webp bytes>; the gateway believes the declared type and misroutes the file. Falls back to the extension when the content is unrecognized.chatstreams only when a document is present. The gateway streams document requests one SSE chunk per page. Image- and text-only requests return a plainchat.completionbody still labelledContent-Type: text/event-stream, so an SSE reader finds no events and yields empty content — a silent blank response. Verified directly against the API with no proxy in the path.Gateway-only fields ride in
extra_body.method,method_params,document_dpi,image_resolutionare not OpenAI parameters, andcreate()has an explicit signature that raisesTypeErroron unknown kwargs.--extrasplits keys by introspecting the installed SDK's signature, so it tracks openai upgrades instead of hardcoding a list.OCR output renders as plain text, not Markdown. Responses are wrapped in
<document>/<page>tags; Rich's Markdown renderer treats them as HTML and drops the body entirely, blanking the panel. Payloads starting with markup or JSON render viaText.gw embed --joinis explicit. Models embed at most one image per vector —[[img, img]]returns a 500. Every file and-tis its own vector by default;--joincombines them and rejects 2+ files client-side rather than sending a request that 500s.gw modelsshows methods, not pricing. The catalog returns no pricing fields at all, so those columns could only ever render-. It now surfacestaskandmethods(default marked*), andgw models <model>details params and copy-pasteable example commands parsed from the liveextra_body_help.gw chatsurfacesusage.cost. The gateway returns a per-request USD cost that was being discarded; it now shows in the panel footer (… · $0.001508 · 1.35s) and underusage.costin--json. Sub-cent costs print without scientific notation and never collapse a real cost to$0.Errors dressed as success now fail loudly. An
{"error": ...}response body (e.g. an unknown--method, which the gateway returns on a 200) is detected and surfaced as a real error with a non-zero exit, instead of a success-styled panel. Detection is limited to a loneerrorkey, so normal{"text": ...}/<document>output is unaffected.gw chat --response-format(JSON mode). Acceptstext,json_object, or a full JSON object (e.g.json_schema) and sends it as the top-levelresponse_formatfield. The gateway does not honor it yet — accepted and currently ignored server-side — so it's wired now to test once support lands. Distinct from--json, which controls the CLI's own output.Notes for the gateway / docs team
Found while testing; all server- or docs-side, none fixable from the client:
input: [{"type":"image_url",...}], which 422s. The union islist[union[str, list[EmbeddingContentPart]]]— parts nest one level deeper:input: [[{...}]].[[img, img]]→ Internal server error;[[img, text]]is fine).--methodreturns an{"error": ...}body on a 200, not a 4xx. The CLI now detects and surfaces this (see above), but a proper status code would be cleaner.gw chatinlines the file as base64 in the request body, so a PDF over ~20MB hits413 Request Entity Too Large(a raw HTML error). Batch-OCRing a 26/39/58MB drawing set required splitting each into page-range chunks client-side. A large-file upload path (Files API) or server-side size handling would remove that workaround.Response format & CLI output
--response-format(the model's output constraint) and--json(the CLI's own stdout format) are deliberately separate flags.usage.costis surfaced in both the panel and--json.Tests
93 gateway tests. Full suite 437 passed / 26 skipped, ruff clean. Fakes mirror real API payloads —
FakeModelmatches an actual/modelsresponse (no invented pricing field), and the completions fake asserts onextra_body/response_formatrather than accepting any kwarg. Every new test was checked to fail against the pre-change source.Version bumped 0.6.5 → 0.7.0: minor rather than patch, since this adds a new CLI command group and two SDK resources, following #180 (0.5.12 → 0.6.0 for the execute/executions commands).
🤖 Generated with Claude Code