跳转至

自定义协议

内置适配器覆盖了 OpenAI 兼容和 Anthropic 兼容的端点。要支持另一种通信协议,实现 ProtocolAdapter 并将它注册到客户端上即可。

一个适配器实现两个方法:Protocol 返回它的注册表键,Stream 将提供方响应翻译成包事件。StreamWriter 提供与内置适配器相同的生命周期机制:一个 EventStart、非终止事件上的 Partial 快照、恰好一个终止事件,以及报告为 StopReasonAborted 的取消。

1. 声明适配器及其注册表键。 Protocol 返回客户端用来把模型路由到本适配器的键。

type myAdapter struct{ http *http.Client }

func (myAdapter) Protocol() llm.Protocol { return "my-protocol" }

2. 准备响应消息和一个 StreamWriter Stream 会立即返回通道,实际工作在 goroutine 中进行。NewStreamWriter 会发出开头的 EventStart 并跟踪 Partial 快照。

events := make(chan llm.Event)
go func() {
    defer close(events)

    message := llm.AssistantMessage{
        Protocol: model.Protocol,
        Provider: model.Provider,
        Model:    model.ID,
    }
    writer := llm.NewStreamWriter(ctx, events, &message)

3. 调用端点;失败时通过 writer 上报。 writer.Fail 会发出唯一的终止事件 EventError,使失败的请求仍能正确关闭流。

    reply, usage, err := callMyEndpoint(ctx, a.http, model, input, options)
    if err != nil {
        writer.Fail(err)
        return
    }

4. 发出内容块的生命周期事件。 把内容块追加进 message.Content,再依次发出 start 事件、每个分片的 delta,以及携带最终文本的 end 事件。

    text := &llm.TextContent{}
    message.Content = append(message.Content, text)
    writer.Emit(llm.Event{Type: llm.EventTextStart, ContentIndex: 0})
    for chunk := range reply {
        text.Text += chunk
        writer.Emit(llm.Event{
            Type: llm.EventTextDelta, ContentIndex: 0, Delta: chunk,
        })
    }
    writer.Emit(llm.Event{
        Type: llm.EventTextEnd, ContentIndex: 0, Content: text.Text,
    })

5. 记录用量与停止原因,然后收尾。 writer.Done 发出唯一的终止事件 EventDone,携带组装好的消息。

    message.Usage = usage
    message.StopReason = llm.StopReasonStop
    writer.Done()
}()
return events, nil
完整适配器
type myAdapter struct{ http *http.Client }

func (myAdapter) Protocol() llm.Protocol { return "my-protocol" }

func (a myAdapter) Stream(
    ctx context.Context,
    model llm.Model,
    input llm.Context,
    options llm.StreamOptions,
) (<-chan llm.Event, error) {
    events := make(chan llm.Event)
    go func() {
        defer close(events)

        message := llm.AssistantMessage{
            Protocol: model.Protocol,
            Provider: model.Provider,
            Model:    model.ID,
        }
        writer := llm.NewStreamWriter(ctx, events, &message)

        reply, usage, err := callMyEndpoint(ctx, a.http, model, input, options)
        if err != nil {
            writer.Fail(err)
            return
        }

        text := &llm.TextContent{}
        message.Content = append(message.Content, text)
        writer.Emit(llm.Event{Type: llm.EventTextStart, ContentIndex: 0})
        for chunk := range reply {
            text.Text += chunk
            writer.Emit(llm.Event{
                Type: llm.EventTextDelta, ContentIndex: 0, Delta: chunk,
            })
        }
        writer.Emit(llm.Event{
            Type: llm.EventTextEnd, ContentIndex: 0, Content: text.Text,
        })

        message.Usage = usage
        message.StopReason = llm.StopReasonStop
        writer.Done()
    }()
    return events, nil
}

注册它并构建 client:

registry := llm.NewAdapterRegistry()
if err := registry.Register(myAdapter{http: http.DefaultClient}); err != nil {
    log.Fatal(err)
}
client := llm.NewClient(registry)

model := llm.Model{
    ID: "x", Provider: "me", Protocol: "my-protocol", MaxTokens: 1024,
}
message, err := client.Complete(ctx, model, input, llm.StreamOptions{})

若想让同一个 client 也支持内置协议,把 openai.NewAdapter(nil)anthropic.NewAdapter(nil)(来自 github.com/ktsoator/or/llm/openaigithub.com/ktsoator/or/llm/anthropic)一并注册进注册表。

适配器负责双向翻译:构建底层请求、切分响应、更新用量和停止原因,以及发出增量。 CloneToolCall 为事件深拷贝工具调用。ParseToolArgumentsMode 提供与内置适配器相同的不完整 JSON 恢复能力。

自定义协议选项

具有协议特定语义的设置,可以使用这个共享扩展点,而无需改动 StreamOptions

type myProtocolOptions struct {
    SafetyMode string
}

func (*myProtocolOptions) Protocol() llm.Protocol { return "my-protocol" }

func (options *myProtocolOptions) Validate(_ []llm.ToolDefinition) error {
    if options.SafetyMode == "" {
        return errors.New("safety mode is required")
    }
    return nil
}

options := llm.StreamOptions{
    ProtocolOptions: &myProtocolOptions{SafetyMode: "strict"},
}

Client.Stream 会先校验 ProtocolOptions.Protocol() 与目标模型匹配,再在调用适配器之前调用 Validate