Skip to main content

AI Model Integration

DEVELOPER Advanced

This page summarizes how EZ-Console wires AI models, toolsets, skills, streaming, and client tools. For skills storage, APIs, and tool-binding rules, see AI Agent Skills. For browser-side registration of page tools and prompts (useAI, registerPageAI), see React hooks & context → useAI.

Overview​

  • Multiple providers: OpenAI-compatible APIs and custom providers registered on the backend.
  • Service layer: AIChatService resolves the org model, merges RBAC-authorized toolsets, optional skill loader, and runs ClassicChatClient (tool loops, summarization, retries).
  • Low-level AIClient: Single round-trip Chat / ChatStream with toolset.ToolSets; the classic client adds iteration, auto-summarization, and client-tool handoff.
  • Tool namespaces: ToolSets is map[string]ToolSet; the map key is a prefix (e.g. tool query under key mcp → mcp_query).
  • Streaming: Chat HTTP uses SSE with typed events (content, tool_call, client_tool_pending, error).
  • Org scope: Models and tool permissions are organization-scoped; see Multi-tenancy.

Built-in OpenAI-compatible provider​

Typical config fields (also used for auto-summarization thresholds):

FieldNotes
api_keyRequired; stored encrypted
model_idRequired model name
base_urlOptional; default OpenAI API base
organization_idOptional OpenAI org
max_tokensOptional; context window hint for summarization

Create models from System → AI Models or POST /api/ai/models. Type definitions for dynamic forms come from GET /api/ai/models/types.

service.Service embeds *AIChatService. Prefer these entry points so toolsets, skills, and permissions stay consistent.

Non-streaming without toolsets

responses, err := svc.CreateChatCompletionWithoutToolSets(ctx, organizationID, modelID, messages, opts...)

Non-streaming with authorized toolsets (pass nil skill loader if skills are not used)

responses, err := svc.CreateChatCompletion(ctx, organizationID, modelID, messages, skillLoader, opts...)

Streaming

stream, err := svc.CreateChatCompletionStream(ctx, organizationID, modelID, messages, skillLoader, opts...)
defer stream.Close()
for {
event, err := stream.Recv(ctx)
if err != nil {
if err == io.EOF {
break
}
return err
}
switch event.EventType {
case ai.EventTypeContent:
// event.Content
case ai.EventTypeToolCall:
// event.ToolCalls
case ai.EventTypeClientToolPending:
// hand off to browser; see usages / AI chat controller
case ai.EventTypeError:
// event.Content as error text
}
}

Common functional options include ai.WithChatMaxIterations, ai.WithChatMaxTokens, ai.WithChatAutoSummarization, ai.WithChatToolSets, and hooks such as ai.WithChatOnMessageAdded. See pkg/clients/ai WithChatOptions.

Low-level: factory and ClassicChatClient​

Resolve the default (or specific) model, then use the registry. ai.GetFactory returns a ClassicChatClientFactory wrapping the registered provider factory.

aiModel, err := svc.GetDefaultAIModel(ctx, organizationID)
if err != nil {
return err
}
factory, ok := ai.GetFactory(aiModel.Provider)
if !ok {
return fmt.Errorf("unsupported provider: %s", aiModel.Provider)
}
client, err := factory.CreateClient(ctx, organizationID, aiModel.Config)
if err != nil {
return err
}

messages := []ai.ChatMessage{{Role: model.AIChatMessageRoleUser, Content: "Hello"}}

responses, err := client.Exchange(ctx, messages,
ai.WithChatMaxIterations(10),
ai.WithChatAutoSummarization(true),
)

ClassicChatClient implements Exchange / ExchangeStream for full tool-call loops on top of your AIClient.

Implementing a custom provider​

AIClient​

Implement Chat and ChatStream only (single provider round-trip). Tool iteration is handled by the classic client.

func (c *CustomAIClient) Chat(ctx context.Context, messages []ai.ChatMessage, toolSets toolset.ToolSets) (*ai.ChatMessage, error) {
// Build provider request including tools from toolSets, return assistant message
return nil, nil
}

func (c *CustomAIClient) ChatStream(ctx context.Context, messages []ai.ChatMessage, toolSets toolset.ToolSets) (ai.ChatStream, error) {
return nil, nil
}

AIClientFactory​

Implement GetName, GetDescription, GetConfigFields, and CreateClient. Register in init():

const AIModelProviderCustom model.AIModelProvider = "custom"

func init() {
if err := ai.RegisterFactory(AIModelProviderCustom, &CustomAIClientFactory{}); err != nil {
panic(err)
}
}

Optionally implement AIClientFactoryV2 (GetConfigSchema) so the admin UI gets JSON Schema + UI schema instead of legacy ConfigField conversion (pkg/clients/ai/client.go).

Toolsets​

Built-in examples include utils (time, sleep, random string) and MCP. Toolsets are configured under system toolsets and exposed to the model according to role AI tool permissions.

To add a custom toolset, implement toolset.ToolSet and toolset.ToolSetFactory, call toolset.RegisterToolSet, and import your package from main so init() runs. Optional ToolSetFactoryV2 supplies JSON Schema like AI providers (pkg/toolset).

Do not use ad-hoc client-side maps for tools; the product stack expects pkg/toolset and service-layer authorization.

Skills and progressive loading​

When domains / skill_ids are present, SkillService.CreateSkillLoader returns an ai.SkillLoader that injects metadata and the prefixed tool skill_loader_get_skill_content. Binding between skills and org tools is controlled by system_enable_skill_tool_binding. Details: AI Agent Skills.

Client tools (ui_*)​

Browser-executed tools must match ^ui_[a-zA-Z0-9_]+$. The frontend registers them via registerPageAI in AIContext; the stream emits client_tool_pending until results are posted back. Server validation and proxy behavior live in pkg/api/ai.

Chat sessions and SSE​

Session CRUD and streaming send live under /api/ai/chat/sessions (see Swag in pkg/api/ai). SSE event: message carries JSON with event_type as in the overview above.

Best practices​

  1. Prefer AIChatService methods so RBAC, toolsets, and skills stay aligned with the rest of the product.
  2. Store secrets with password field types; keys are encrypted at rest.
  3. Set max_tokens on models used for long chats so summarization thresholds are meaningful.
  4. Cap tool iterations (WithChatMaxIterations) and use streaming for interactive UIs.
  5. Chat permission is typically ai:chat:create (see RBAC defaults in the main repo).

Need help? Ask in GitHub Discussions.