Closes #16.
Summary
The review model was pinned to anthropic/claude-sonnet-4.6 in lib/agent.ts, which DurableAgent resolves through the Vercel AI Gateway. This adds two optional environment variables so a self-hosted deployment can change the model, or point at a gateway it already uses. With neither set, behaviour is unchanged.
| Variable | Effect |
|---|---|
MODEL | Model id on either path. Defaults to the current anthropic/claude-sonnet-4.6. |
MODEL_BASE_URL | Routes calls through an OpenAI-compatible /chat/completions endpoint instead. |
MODEL_API_KEY | Optional key for that endpoint. |
MODEL_API_KEY is optional so an unauthenticated endpoint — a local vLLM or Ollama server — works by setting MODEL_BASE_URL alone.
Routing lives in a new lib/model.ts; lib/agent.ts changes by one line (model: resolveModel()). Nothing in the workflow or the agent's tools is touched.
Two details worth a look in review
The provider factory has to be a workflow step. DurableAgent accepts string | (() => Promise<CompatibleLanguageModel>) and passes it to doStreamStep as a step argument, which the runtime cannot serialize as a plain closure. Marking it "use step" makes the compiler register it — confirmed in the build output as registerStepFunction("step//./lib/model//customGatewayModel", ...), and the step count goes 25 → 26. The provider instance itself is built inside the streaming step and is never serialized. This mirrors how @workflow/ai's own gateway() / openai() providers are written.
includeUsage: true is required, not cosmetic. Without stream_options.include_usage an OpenAI-compatible endpoint returns no usage on streamed responses, and the 200K token budget in runAgent would silently never trigger.
One unavoidable cast is commented in place: @workflow/ai types its model boundary against the AI SDK v2 language model while @ai-sdk/openai-compatible returns a v3 one. They are structurally compatible at runtime — doStreamStep calls doStream with v2-shaped options, which v3 models accept, as @workflow/ai's own CompatibleLanguageModel type documents — but the tool union does not line up ("provider-defined" vs "provider").
Validation
tsc --noEmitclean.next buildsucceeds; manifest goes from 25 to 26 steps with the new provider step registered.oxlintandoxfmtclean on every file touched. (The pre-existing findings inlib/utils.tsare untouched.)- Exercised end to end against a live OpenAI-compatible gateway through the AI SDK: streaming, tool calling across a 2-step agent loop, and usage reporting all confirmed working.
@ai-sdk/openai-compatible is added as a direct dependency. It was already in the lockfile transitively via @ai-sdk/xai, so the install graph does not grow.
Disclosure
I run LLMTR, one gateway this would make reachable, so I have an interest here and would rather say so up front than have it noticed later. That is why this is written as a seam rather than an integration — MODEL_BASE_URL takes any OpenAI-compatible endpoint, the README example lists LLMTR next to OpenRouter, Groq, vLLM and Ollama, and nothing in the code names a provider.
I also know unsolicited vendor-adjacent PRs have landed here before (#13, #14). If this direction is not wanted, or you would rather it were shaped differently, just say so and I will close it.
@knowhycodata is attempting to deploy a commit to the Vercel Labs Team on Vercel.
A member of the Team first needs to authorize it.