LLM integration

Gebweb wraps the stdlib llm module so a single import gebweb; gives applications a provider-agnostic LLM client plus DI integration, without a manual factory hop through the stdlib.

For the underlying interface (chat / embed / image analysis / image generation) see the language docs for the llm module. The Gebweb facade adds the wiring patterns below.

Wiring

Build a client with gebweb.llmClient(opts) and register it on the app with gebweb.useLlm(app, client):

import gebweb;
import sys;

let app = gebweb.app([SummaryController]);
gebweb.useLlm(app, gebweb.llmClient({
    "provider": "anthropic",
    "apiKey":   sys.getenv("ANTHROPIC_API_KEY") as string
}));
gebweb.serve(app, ":8080");

gebweb.llmClient(opts) is a thin re-export of llm.client(opts); either form works. gebweb.useLlm stores the client on the app and also registers it in the DI container under the llm.Client key.

Using the client in handlers

Two paths. For services autowired through the DI container, type the constructor parameter as llm.Client and the configured client is injected automatically:

import llm;

class Summariser {
    llm.Client client;
    func Summariser(llm.Client client) { this.client = client; }
    func run(string text): string {
        let resp = this.client.chat([
            {"role": "user", "content": "Summarise: " + text}
        ], {"model": "claude-opus-4-8", "maxTokens": 256});
        return resp["content"] as string;
    }
}

class SummaryController {
    Summariser summariser;
    func SummaryController(Summariser summariser) {
        this.summariser = summariser;
    }

    @Post("/summary")
    func summarise(SummaryRequest body): dict<string, any> {
        return {"summary": this.summariser.run(body.text)};
    }
}

useLlm registers the client under the interface name "llm.Client" in the DI container, so any constructor parameter typed with that interface resolves to the configured instance without an explicit factory. See Dependency injection for the container's autowiring rules and the registration surface.

For handler code that already has the app reference and just wants the configured client, use the gebweb.llm(app) getter:

let client = gebweb.llm(app) as llm.Client;
let resp = client.chat(messages, opts);

gebweb.llm(app) returns the registered client (or null if useLlm has not been called) without going through DI.

Picking a provider

provider in the opts dict picks the backend:

gebweb.useLlm(app, gebweb.llmClient({"provider": "openai",    "apiKey": "..."}));
gebweb.useLlm(app, gebweb.llmClient({"provider": "anthropic", "apiKey": "..."}));
gebweb.useLlm(app, gebweb.llmClient({
    "provider":  "bedrock",
    "region":    "us-east-1",
    "accessKey": gebweb.getParameter(app, "aws.accessKey") as string,
    "secretKey": gebweb.getParameter(app, "aws.secretKey") as string
}));

Same surface across all three: chat, embed, analyzeImage, generateImage. Operations that a provider does not support throw a RuntimeError naming the missing method.

Errors

Upstream API failures (rate limits, invalid keys, validation errors) surface as RuntimeError from inside the handler. Route them through Gebweb's exception handling like any other call:

import gebweb.errors as errors;

@Post("/summary")
func summarise(SummaryRequest body): dict<string, any> {
    let client = gebweb.resolve(this.app, llm.Client) as llm.Client;
    try {
        let resp = client.chat(buildMessages(body), {"model": "..."});
        return {"summary": resp["content"]};
    } catch (RuntimeError e) {
        throw errors.HttpException(503, "LLM provider error: " + e.message,
            "Service Unavailable");
    }
}

The framework's Problem Details renderer formats the thrown exception at the matching status. See Responses for the full HttpException surface and the controller-method short forms.