Add LLM API Provider
Use when adding a new LLM provider (Anthropic, OpenAI, Google, xAI, etc.) to the inference.sh grid as a set of chat apps with a shared helper module.
Process
Research the provider API — Fetch official documentation. For each model, extract:
- Model ID (exact API string, e.g.
claude-sonnet-4-6) - Context window and max output tokens
- Pricing per million tokens (input/output)
- Capabilities: vision, tool use, reasoning/thinking, streaming format
- Auth method and header format
- Collect into
README.mdandpricing.mdat the provider root
- Model ID (exact API string, e.g.
Create provider directory and shared helper —
mkdir -p tmp/grid/api/{provider}Write
{provider}_helper.pywith:- Message conversion: Provider-specific format from
LLMInput(context messages, images, tool calls, tool results). Handle consecutive same-role message merging if required. - Tool conversion: OpenAI format → provider format
- Streaming function:
stream_completion(client, input_data, model)→ yields output dicts withresponse,reasoning,tool_calls,output_meta - Non-streaming function:
complete(client, input_data, model)→ returns output dict - Token tracking: Extract
input_tokens/output_tokensfrom API response, emit asOutputMeta(inputs=[TextMeta(tokens=N)], outputs=[TextMeta(tokens=N)]) - Error handling:
handle_api_error()that extracts provider error messages - Reasoning/thinking config: Map
ReasoningEffortEnumto provider's thinking parameter format
- Message conversion: Provider-specific format from
Scaffold each model app — Use
belt app init, never create files manually:cd tmp/grid/api/{provider} belt app init {model-name} --lang python # repeat for each modelSymlink the shared helper into each app:
for app in model-a model-b model-c; do ln -sf ../{provider}_helper.py "$app/{provider}_helper.py" doneImplement each app's inference.py — Thin wrapper (~50 lines):
AppInputextendsLLMInput+ capability mixins (ReasoningCapabilityMixin,ToolsCapabilityMixin,ImageCapabilityMixin)AppOutputextendsReasoningMixin, ToolCallsMixin, LLMOutput, BaseAppOutputApp.setup()— no parameters, init the SDK client fromos.getenv("{PROVIDER}_KEY")App.run()— delegates tostream_completion()orcomplete()based oninput_data.stream- Set
DEFAULT_MODEL,context_size, andmax_tokensper model
Update inf.yml for each app — Set:
namespace: {provider},name: {model-name}category: chatmetadata.capabilities: [reasoning, image](as appropriate)secrets: [{key: {PROVIDER}_KEY}]resources: gpu: {count: 0, type: none}, ram: 0(API-only)
Update requirements.txt for each app:
pydantic >= 2.0.0 inferencesh >= 0.6.30 {provider-sdk} >= {version}Deploy all apps:
for app in model-a model-b model-c; do cd tmp/grid/api/{provider}/$app && belt app deploy doneCloud-test every app — Not just one, ALL of them:
belt app run {provider}/{model-name} --json --input '{"text": "simple test prompt"}'Verify:
status_text: "completed", non-emptyresponse,output_metawith token counts.Run pricing agents for all apps — Get app/version IDs, then run:
belt app get {provider}/{model-name} --json # extract app.id and app.version_id belt agent run infsh/pricing-agent "<pricing instructions>" \ --context version_id=<vid> --context app_id=<aid>Group by pricing tier (e.g. all $5/$25 models together). After agents draft and evaluate, approve publish:
belt chat pending # list pending approvals belt chat approve <invocation-id> # approve each publish
Rules
NEVER create inf.yml, inference.py, init.py, or app directories manually. Always use
belt app init. The scaffolded__init__.pyand package structure are required for the validator. Reason: manual creation misses required fields, uses wrong field names, and breaks the deployment validator.setup() takes no parameters. The
metadataparameter is deprecated. Useself.context.metadataif needed.self.loggeris NOT available insetup()— useprint()for setup logging. Reason: both caused runtime errors during testing.LLMInput field is
text, notprompt. The input schema inherits fromLLMInputwhich usestextas the primary field. Reason:promptcauses a Pydantic validation error at runtime.AppOutput MUST extend BaseAppOutput (not just BaseModel) when using
output_meta. Without it,output_metais silently dropped from the response.Read files before overwriting with Write tool. The Write tool requires a prior Read of the file in the same conversation. After
belt app init, read the scaffolded file first.Use relative imports for the symlinked helper.
from .{provider}_helper import func— not absolute imports. The app runs as a package inside the deployment container.Deploy and cloud-test EVERY app, not just one. Different model IDs can have different API behaviors. A working sonnet doesn't guarantee a working haiku. Reason: we shipped with only partial testing initially.
Run pricing agents AFTER deployment, not before. The pricing agent needs the app submitted to the store, which happens during deploy. If the agent says "not submitted to store", tell it to submit first.
belt secrets are masked locally.
belt secrets getdoesn't return the raw value — you can't use it for local testing env vars. Deploy and cloud-test instead.Pricing CEL expressions need double() casts.
text_tokens()returns int; dividing by a double causes a CEL type error. Always usedouble(text_tokens(inputs)) / 1000000.0 * double(prices.var). Reason: multiple pricing agents failed on this before self-correcting.Group models by pricing tier for pricing agent efficiency. Don't run 6 identical pricing agents — group the 3 Opus models, 2 Sonnet models, etc. by their $/MTok rate.