Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/GuaiZai233/FrostAgent/llms.txt

Use this file to discover all available pages before exploring further.

FrostAgent keeps its LLM communication behind the core.LLMProvider interface, so the engine’s reasoning loop never depends on a specific API vendor. Swapping providers — or adding support for a new one — means implementing a single method, wiring the new struct into main.go, and letting the engine continue without modification.

The LLMProvider interface

The interface is defined in internal/core/interfaces.go:
// LLMProvider defines the interface for LLM API calls.
type LLMProvider interface {
    Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
}
Any struct that implements Chat can be used as a drop-in provider. The engine calls this method at every iteration of its reasoning loop, passing the full conversation history and the list of available tools.

ChatRequest and ChatResponse types

Both types are defined in internal/core/types.go and represent FrostAgent’s provider-neutral data model. Your implementation must convert between these types and whatever format your LLM API expects.

ChatRequest

type ChatRequest struct {
    Model       string
    Messages    []ChatMessage
    Tools       []Tool
    ToolChoice  string  // "auto", "none", or a specific tool name
    MaxTokens   int
    Temperature float64
}

ChatResponse

type ChatResponse struct {
    Message ChatMessage
    Usage   *Usage
}
Usage is optional — set it to nil if your backend does not report token counts.

ChatMessage

type ChatMessage struct {
    Role       MessageRole
    Content    any         // string | []ContentPart for multimodal messages
    ToolCalls  []ToolCall
    ToolCallID string      // populated on role="tool" messages
}
MessageRole is a typed string with four defined constants:
ConstantValue
core.RoleSystem"system"
core.RoleUser"user"
core.RoleAssistant"assistant"
core.RoleTool"tool"

Tool (passed inside ChatRequest)

type Tool struct {
    Name        string
    Description string
    Parameters  map[string]any  // JSON Schema object
}

ToolCall and ToolCallFunction

type ToolCall struct {
    ID       string
    Type     string            // always "function" for function-calling tools
    Function ToolCallFunction
}

type ToolCallFunction struct {
    Name      string
    Arguments string            // JSON-encoded argument string
}
When an LLM wants to call a tool, your Chat implementation must return a ChatMessage whose ToolCalls slice is populated. When the ToolCalls slice is empty, the engine treats the response as a final answer and stops iterating.

Built-in OpenAI provider

FrostAgent ships with an OpenAI-compatible provider in internal/provider/llm/openai. Its constructor signature is:
func NewClient(baseURL, apiKey string) *openai.Client
Instantiate it with:
client := openai.NewClient(os.Getenv("UPSTREAM_ENDPOINT"), os.Getenv("UPSTREAM_API_KEY"))
The openai.Client implements core.LLMProvider and handles all serialisation to and from the OpenAI chat completions format. It works with any OpenAI-compatible API, including:
  • OpenAI (https://api.openai.com/v1)
  • Alibaba Cloud DashScope (https://dashscope.aliyuncs.com/compatible-mode/v1)
  • Local inference servers such as Ollama or vLLM that expose the /chat/completions endpoint
The built-in client sets a 120-second HTTP timeout and attaches the API key as a Bearer token in the Authorization header.

Implementing a custom provider

Create a new package under internal/provider/llm/<yourprovider>/ and implement the interface:
package myprovider

import (
    "context"
    "fmt"

    "FrostAgent/internal/core"
)

// MyProvider implements core.LLMProvider for the Acme LLM API.
type MyProvider struct {
    endpoint string
    apiKey   string
}

func NewMyProvider(endpoint, apiKey string) *MyProvider {
    return &MyProvider{
        endpoint: endpoint,
        apiKey:   apiKey,
    }
}

func (p *MyProvider) Chat(ctx context.Context, req core.ChatRequest) (*core.ChatResponse, error) {
    // 1. Convert core.ChatRequest to your API's request format.
    acmeReq := convertToAcmeRequest(req)

    // 2. Call your API (use ctx for cancellation/timeout).
    acmeResp, err := callAcmeAPI(ctx, p.endpoint, p.apiKey, acmeReq)
    if err != nil {
        return nil, fmt.Errorf("acme API call failed: %w", err)
    }

    // 3. Convert the response back to core.ChatResponse.
    coreMsg := core.ChatMessage{
        Role:    core.RoleAssistant,
        Content: acmeResp.Text,
    }

    // 4. Map any tool calls returned by the model.
    for _, tc := range acmeResp.ToolCalls {
        coreMsg.ToolCalls = append(coreMsg.ToolCalls, core.ToolCall{
            ID:   tc.ID,
            Type: "function",
            Function: core.ToolCallFunction{
                Name:      tc.FunctionName,
                Arguments: tc.FunctionArguments, // must be a JSON string
            },
        })
    }

    return &core.ChatResponse{
        Message: coreMsg,
        Usage: &core.Usage{
            PromptTokens:     acmeResp.Usage.Input,
            CompletionTokens: acmeResp.Usage.Output,
            TotalTokens:      acmeResp.Usage.Input + acmeResp.Usage.Output,
        },
    }, nil
}

Handling tool definitions

Your provider must also translate the Tools slice inside ChatRequest into whatever format your API expects. For function-calling APIs this is typically:
for _, t := range req.Tools {
    acmeReq.Functions = append(acmeReq.Functions, AcmeFunction{
        Name:        t.Name,
        Description: t.Description,
        Parameters:  t.Parameters, // already a map[string]any JSON Schema
    })
}

Wiring it in

Replace the openai.NewClient(...) call in the Engine initialisation block inside cmd/app/main.go:
// Before (built-in OpenAI provider)
GlobalEngine = &llm.Engine{
    Provider: openai.NewClient(
        os.Getenv("UPSTREAM_ENDPOINT"),
        os.Getenv("UPSTREAM_API_KEY"),
    ),
    // ...
}

// After (your custom provider)
GlobalEngine = &llm.Engine{
    Provider: myprovider.NewMyProvider(
        os.Getenv("ACME_ENDPOINT"),
        os.Getenv("ACME_API_KEY"),
    ),
    // ...
}
Add the corresponding import path and any new environment variables to .env. No other changes are needed — the engine’s run loop only calls Provider.Chat() and is unaware of the underlying transport.
Your provider must correctly populate ToolCalls in the returned ChatMessage whenever the LLM decides to invoke a tool. If ToolCalls is empty, the engine interprets the response as a final answer and exits the loop immediately, even if the model’s raw output contained a tool-call intent in a different format. Always validate your response mapping with a test that exercises a multi-turn tool call before deploying to production.

Build docs developers (and LLMs) love