Ctrl K
ComunidadGuías…

pluginapi/completion.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package pluginapi

import (
	"encoding/json"
	"fmt"
	"strings"
)

// Usage is token accounting in provider-neutral names.
type Usage struct {
	InputTokens  int
	OutputTokens int
	TotalTokens  int
	// CachedInputTokens is the part of InputTokens served from a prompt cache.
	CachedInputTokens int
}

// Choice is one completion candidate. Chat completions may return several;
// Responses and Anthropic return one.
type Choice struct {
	Index int
	// Message uses the same Part model as the request: text, tool_call,
	// reasoning, refusal.
	Message Message
	// FinishReason is the OpenAI-style reason: "stop", "length",
	// "tool_calls", "content_filter".
	FinishReason string
}

// Completion is the unified response. Read it freely; edit it through its
// methods so the host re-encodes only what changed.
type Completion struct {
	ID      string
	Model   string
	Choices []Choice
	Usage   Usage
	// Raw is the response as received from the provider. Read-only.
	Raw json.RawMessage

	changes Changes
}

// Text returns the text parts of the given choice concatenated, or "" when
// the choice does not exist.
func (c *Completion) Text(choice int) string {
	if choice < 0 || choice >= len(c.Choices) {
		return ""
	}
	var out strings.Builder
	for _, part := range c.Choices[choice].Message.Parts {
		if part.Kind == PartText {
			out.WriteString(part.Text)
		}
	}
	return out.String()
}

// SetText replaces the text of part partIdx of the given choice. The part
// must be a text part.
func (c *Completion) SetText(choice, partIdx int, text string) error {
	return c.setPartText(choice, partIdx, PartText, text)
}

// SetReasoning replaces the text of part partIdx of the given choice. The
// part must be a reasoning part. Plugins that restore values use it so the
// client's reasoning view matches the answer.
func (c *Completion) SetReasoning(choice, partIdx int, text string) error {
	return c.setPartText(choice, partIdx, PartReasoning, text)
}

// setPartText replaces the text of part partIdx of the given choice, which
// must be of the given kind.
func (c *Completion) setPartText(choice, partIdx int, kind PartKind, text string) error {
	ch, err := c.choice(choice)
	if err != nil {
		return err
	}
	if partIdx < 0 || partIdx >= len(ch.Message.Parts) {
		return fmt.Errorf("pluginapi: choice %d has no part %d", choice, partIdx)
	}
	if ch.Message.Parts[partIdx].Kind != kind {
		return fmt.Errorf("pluginapi: part %d of choice %d is %s, not %s", partIdx, choice, ch.Message.Parts[partIdx].Kind, kind)
	}
	ch.Message.Parts[partIdx].Text = text
	c.changes.mark(choiceKey(choice), ChangeEdited)
	return nil
}

// SetToolArguments replaces the arguments of tool call callID in the given
// choice. args must be valid JSON. Plugins that anonymize or restore values
// use it so the client runs the tool with the intended arguments.
func (c *Completion) SetToolArguments(choice int, callID string, args json.RawMessage) error {
	ch, err := c.choice(choice)
	if err != nil {
		return err
	}
	if !json.Valid(args) {
		return fmt.Errorf("pluginapi: tool arguments for call %q are not valid JSON", callID)
	}
	for i := range ch.Message.Parts {
		part := &ch.Message.Parts[i]
		if part.Kind == PartToolCall && part.ToolCall != nil && part.ToolCall.ID == callID {
			call := *part.ToolCall
			call.Arguments = append(json.RawMessage(nil), args...)
			part.ToolCall = &call
			c.changes.mark(choiceKey(choice), ChangeEdited)
			return nil
		}
	}
	return fmt.Errorf("pluginapi: choice %d has no tool call %q", choice, callID)
}

// SetFinishReason sets the finish reason of the given choice.
// "content_filter" is the OpenAI-compatible way to say the response was cut.
func (c *Completion) SetFinishReason(choice int, reason string) error {
	ch, err := c.choice(choice)
	if err != nil {
		return err
	}
	ch.FinishReason = reason
	c.changes.mark(choiceKey(choice), ChangeEdited)
	return nil
}

// ReplaceText drops every text part of the choice and keeps a single text
// part with the given text, placed where the first text part was (or first).
// Tool calls and other non-text parts are kept. Used for redaction and
// synthetic answers.
func (c *Completion) ReplaceText(choice int, text string) error {
	ch, err := c.choice(choice)
	if err != nil {
		return err
	}
	parts := make([]Part, 0, len(ch.Message.Parts)+1)
	inserted := false
	for _, part := range ch.Message.Parts {
		if part.Kind == PartText {
			if !inserted {
				parts = append(parts, Part{Kind: PartText, Text: text})
				inserted = true
			}
			continue
		}
		parts = append(parts, part)
	}
	if !inserted {
		parts = append([]Part{{Kind: PartText, Text: text}}, parts...)
	}
	ch.Message.Parts = parts
	c.changes.mark(choiceKey(choice), ChangeReplaced)
	return nil
}

// Changes reports what was edited, keyed by "choice:<index>" where index is
// the position in Choices. Host-facing.
func (c *Completion) Changes() Changes {
	return c.changes.clone()
}

// Reset clears change tracking. Host-facing; plugin authors never call it.
func (c *Completion) Reset() {
	c.changes = Changes{}
}

func (c *Completion) choice(i int) (*Choice, error) {
	if i < 0 || i >= len(c.Choices) {
		return nil, fmt.Errorf("pluginapi: completion has no choice %d", i)
	}
	return &c.Choices[i], nil
}

func choiceKey(i int) string {
	return fmt.Sprintf("choice:%d", i)
}