Skip to content

Client Module

A unified and powerful LLM access layer


Module Overview and Core Components

The pkg/client module implements the standardized large language model interaction interface core.Client defined in the pkg/core module. This module aims to flatten the vastly different HTTP request specifications, tool call structures, and Server-Sent Events (SSE) streaming parsing differences of various LLM vendors at the underlying level, providing you with an extremely consistent development experience.

Core Interface Contracts:

  • Chat(): Blocking request, waits for the model to finish generating and returns core.Response at once.
  • ChatStream(): Streaming request, returns core.Stream iterator, allowing character-by-character rendering and thought chain capture.

Built-in Ready-to-Use Providers: openai, anthropic, deepseek, qwen, minimax, ollama, azureopenai.


Authentication Configuration: API Key vs Auth Token

When using various vendors' SDK clients, correct authentication configuration is the first step. base.Config provides two different credential fields to handle different scenarios:

1. Using API Key (Standard Backend Service)

In most scenarios, we apply for a string of keys in sk-... format from the developer console. Use the APIKey field for configuration: Tip: If the configured string is an environment variable name (such as OPENAI_API_KEY), GoChat internally will automatically try to read the actual value from the environment variable.

config := openai.Config{
    Config: base.Config{
        APIKey: "sk-xxxxxxxxx", // Or directly write "OPENAI_API_KEY"
        Model:  "gpt-4o",
    },
}
client, _ := openai.New(config)

2. Using Auth Token (OAuth2 or Frontend Device Authentication)

For platforms like Gemini and Qwen that support or require OAuth2 Device Flow, the application obtains a time-sensitive Bearer Token (usually obtained with the pkg/provider module). In this case, use AuthToken:

// Suppose token comes from authManager.GetToken()
config := qwen.Config{
    Config: base.Config{
        AuthToken: "ya29.a0AfB_bxxxxxx", // OAuth2 access token
        Model:     "qwen-max",
    },
}
client, _ := qwen.New(config)
Distinction: When AuthToken is set, the underlying preferentially uses Bearer <AuthToken> to construct the HTTP Authorization header, skipping the default API Key logic.


Advanced Feature: Enable Deep Thinking (Reasoning)

Mainstream models (such as DeepSeek-R1, OpenAI o1/o3, Claude 3.7) introduce reinforcement learning-based "deep thinking (chain of thought)" capabilities. GoChat natively supports capturing the thinking process at the framework level.

Parameter Configuration and Usage Conditions

Enable thinking mode by passing the option core.WithThinking(budget) when calling Chat or ChatStream: - budget: Thinking token budget limit (some models like Claude strongly depend on this parameter, minimum usually 1024). - The underlying will automatically switch the model to the corresponding reasoning model for you (such as automatically switching to deepseek-reasoner when using Deepseek, or injecting reasoning_effort parameter into the OpenAI payload).

Complete Code Example for Capturing Complete Thought Stream

package main

import (
    "context"
    "fmt"
    "github.com/DotNetAge/gochat/pkg/client/base"
    "github.com/DotNetAge/gochat/pkg/client/deepseek"
    "github.com/DotNetAge/gochat/pkg/core"
)

func main() {
    client, _ := deepseek.New(deepseek.Config{
        Config: base.Config{APIKey: "DEEPSEEK_API_KEY"},
    })

    messages := []core.Message{core.NewUserMessage("Why is the sky blue? Please analyze in detail.")}

    // Enable deep thinking mode with 2048 tokens budget
    stream, err := client.ChatStream(context.Background(), messages, core.WithThinking(2048))
    if err != nil {
        panic(err)
    }
    defer stream.Close()

    fmt.Println("=== Thinking Process ===")
    for stream.Next() {
        event := stream.Event()

        // Specifically capture thought chain events
        if event.Type == core.EventThinking {
            fmt.Printf("\033[90m%s\033[0m", event.Content) // Print thinking content in gray
        } else if event.Type == core.EventContent {
            fmt.Print(event.Content) // Print formal response
        }
    }

    // Final consumption statistics
    fmt.Printf("\n\nTotal Token consumed: %d\n", stream.Usage().TotalTokens)
}

Some vendors (such as Qwen and OpenAI's specific interfaces) have built-in powerful internet search engine plugins. In GoChat, you only need one switch to activate, allowing the model to obtain real-time information.

Enabling Method and Example

Use core.WithEnableSearch(true) functional option:

messages := []core.Message{core.NewUserMessage("What was the score of last night's Champions League final?")}

resp, err := client.Chat(context.Background(), messages,
    core.WithEnableSearch(true), // Explicitly enable built-in search
)
if err != nil {
    panic(err)
}
fmt.Println(resp.Content) // Response will be synthesized based on latest search results

Multimodal Input: File and Image Attachments

Multimodal large language models (VLM) support joint reasoning through images and text. GoChat provides structured core.ContentBlock and extremely convenient core.WithAttachments option to mount media.

package main

import (
    "context"
    "os"
    "github.com/DotNetAge/gochat/pkg/client/openai"
    "github.com/DotNetAge/gochat/pkg/core"
)

func main() {
    client, _ := openai.New(openai.Config{/* ... */})

    // Read local image
    imgBytes, _ := os.ReadFile("dashboard.png")

    // Create image attachment (accurate MIME type must be specified)
    attachment := core.NewImageAttachment("dashboard.png", "image/png", imgBytes)

    messages := []core.Message{core.NewUserMessage("Please help me extract all data table metrics from this screenshot.")}

    // Mount attachment via Option
    resp, _ := client.Chat(context.Background(), messages,
        core.WithAttachments(attachment),
    )
    println(resp.Content)
}

Method 2: Manual Construction of Underlying ContentBlock

If you need precise control over the order of image-text interleaving:

messages := []core.Message{
    {
        Role: core.RoleUser,
        Content: []core.ContentBlock{
            {Type: core.ContentTypeText, Text: "This is the first image:"},
            {Type: core.ContentTypeImage, MediaType: "image/jpeg", Data: base64Image1},
            {Type: core.ContentTypeText, Text: "This is the second image, what are the differences between them?"},
            {Type: core.ContentTypeImage, MediaType: "image/jpeg", Data: base64Image2},
        },
    },
}

Extension Development Guide: How to Integrate a New LLM Vendor

Thanks to the well-designed compositional architecture, if your company or some emerging niche vendor provides a private API, encapsulating it as a Client compatible with the GoChat system is very simple.

Standard Implementation Process

  1. Compose Base Class: Define your configuration class and embed base.Config, define your Client class and embed *base.Client.
  2. Implement Interface: Implement the two methods Chat and ChatStream.
  3. Format Conversion: Write a private request builder function to convert []core.Message to the target API's JSON structure; and convert the returned JSON response to core.Response / core.StreamEvent.

Code Skeleton Example

package customllm

import (
    "context"
    "github.com/DotNetAge/gochat/pkg/client/base"
    "github.com/DotNetAge/gochat/pkg/core"
)

// 1. Define configuration and structure
type Config struct {
    base.Config
    CustomVersion string // Your proprietary parameter
}

type Client struct {
    base *base.Client
}

// 2. Constructor
func New(config Config) (*Client, error) {
    return &Client{
        base: base.New(config.Config), // Reuse underlying HTTP client with retry
    }, nil
}

// 3. Implement Chat method
func (c *Client) Chat(ctx context.Context, messages []core.Message, opts ...core.Option) (*core.Response, error) {
    // a. Apply Options
    options := core.ApplyOptions(opts...)
    messages = core.ProcessAttachments(messages, options.Attachments)

    var finalResponse *core.Response

    // b. Leverage base.Retry for automatic exponential backoff retry
    err := c.base.Retry(ctx, func() error {
        // Make your proprietary HTTP request here...
        // resp, err := doHttpRequest(messages)
        // finalResponse = mapToCoreResponse(resp)
        return nil
    })

    return finalResponse, err
}

// 4. Implement ChatStream method
func (c *Client) ChatStream(ctx context.Context, messages []core.Message, opts ...core.Option) (*core.Stream, error) {
    // Make HTTP request to get stream object...
    // Start Goroutine to listen to body, map line data to core.StreamEvent and put into channel

    ch := make(chan core.StreamEvent, 10)
    go func() {
        defer close(ch)
        // Pseudocode for SSE parsing
        for {
            // line := readLine()
            // After parsing out text:
            ch <- core.StreamEvent{Type: core.EventContent, Content: "char"}
        }
    }()

    // Return standard Stream iterator
    return core.NewStream(ch, response.Body), nil
}

By reusing base.Retry and core.NewStream, you can implement a completely enterprise-grade high-availability standard new LLM client with just a few hundred lines of code!