mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 18:16:09 +00:00
Gemini drops tool_call_id and pairs functionCall/functionResponse by function name, so its OpenAI-compat shim requires a non-empty FunctionResponse.name. The name was only recoverable through a reverse id->name lookup over assistant tool_calls still present in the request window, which fails after pruning, truncation or tool_call collapse and produces HTTP 400 "Name cannot be empty" on any follow-up iteration. Add Message.ToolName, set it at every tool-result creation site and preserve it through context pruning, then prefer it when serializing. Fall back to the existing index for history persisted before the field, so old sessions keep working without a reset. When neither source resolves a name, drop the unlabelled tool result instead of emitting an empty one: an empty name is a guaranteed 400 and a synthetic name would match no prior functionCall. Gated behind the existing Gemini detection so other OpenAI-compat hosts, which pair by tool_call_id, are unaffected. Field is JSON-optional, so session history needs no migration and older binaries ignore it on rollback.
224 lines
10 KiB
Go
224 lines
10 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
// Options keys used in ChatRequest.Options across providers.
|
|
const (
|
|
OptMaxTokens = "max_tokens"
|
|
OptTemperature = "temperature"
|
|
OptToolChoice = "tool_choice"
|
|
OptThinkingLevel = "thinking_level"
|
|
OptReasoningEffort = "reasoning_effort"
|
|
OptEnableThinking = "enable_thinking"
|
|
OptThinkingBudget = "thinking_budget"
|
|
// OptStripThinking (bool) tells stream handlers to drop reasoning tokens
|
|
// from ChatResponse.Thinking and onChunk callbacks. Usage.ThinkingTokens
|
|
// and RawAssistantContent are preserved (billing + tool passback safety).
|
|
OptStripThinking = "strip_thinking"
|
|
|
|
// Middleware-related options (Phase 2 will use these)
|
|
OptServiceTier = "service_tier"
|
|
OptFastMode = "fast_mode"
|
|
OptPromptCacheKey = "prompt_cache_key"
|
|
OptPromptCacheRetention = "prompt_cache_retention"
|
|
)
|
|
|
|
// TokenSource provides an OAuth access token (with auto-refresh).
|
|
type TokenSource interface {
|
|
Token() (string, error)
|
|
}
|
|
|
|
type RouteEligibilityClass string
|
|
|
|
const (
|
|
RouteEligibilityHealthy RouteEligibilityClass = "healthy"
|
|
RouteEligibilityUnknown RouteEligibilityClass = "unknown"
|
|
RouteEligibilityBlocked RouteEligibilityClass = "blocked"
|
|
)
|
|
|
|
type RouteEligibility struct {
|
|
Class RouteEligibilityClass
|
|
Reason string
|
|
}
|
|
|
|
type RouteEligibilityAware interface {
|
|
RouteEligibility(ctx context.Context) RouteEligibility
|
|
}
|
|
|
|
// Provider is the interface all LLM providers must implement.
|
|
type Provider interface {
|
|
// Chat sends messages to the LLM and returns a response.
|
|
// tools defines available tool schemas; model overrides the default.
|
|
Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
|
|
|
|
// ChatStream sends messages and streams response chunks via callback.
|
|
// Returns the final complete response after streaming ends.
|
|
ChatStream(ctx context.Context, req ChatRequest, onChunk func(StreamChunk)) (*ChatResponse, error)
|
|
|
|
// DefaultModel returns the provider's default model name.
|
|
DefaultModel() string
|
|
|
|
// Name returns the provider identifier (e.g. "anthropic", "openai").
|
|
Name() string
|
|
}
|
|
|
|
// ThinkingCapable is optionally implemented by providers that support extended thinking.
|
|
// Used to gate thinking_level injection so it's not sent to providers that ignore it.
|
|
type ThinkingCapable interface {
|
|
SupportsThinking() bool
|
|
}
|
|
|
|
// ChatRequest contains the input for a Chat/ChatStream call.
|
|
type ChatRequest struct {
|
|
Messages []Message `json:"messages"`
|
|
Tools []ToolDefinition `json:"tools,omitempty"`
|
|
Model string `json:"model,omitempty"`
|
|
Options map[string]any `json:"options,omitempty"`
|
|
}
|
|
|
|
// ChatResponse is the result from an LLM call.
|
|
type ChatResponse struct {
|
|
Content string `json:"content"`
|
|
Thinking string `json:"thinking,omitempty"`
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
|
FinishReason string `json:"finish_reason"` // "stop", "tool_calls", "length"
|
|
Usage *Usage `json:"usage,omitempty"`
|
|
|
|
// Phase is Codex-specific (gpt-5.3-codex): "commentary" or "final_answer".
|
|
// Agent loop must persist this on assistant messages for Codex performance.
|
|
Phase string `json:"phase,omitempty"`
|
|
|
|
// RawAssistantContent preserves the raw content blocks array from the provider response.
|
|
// Used by Anthropic to pass thinking blocks back in tool use loops (required by API).
|
|
RawAssistantContent json.RawMessage `json:"-"`
|
|
|
|
// ThinkingSignature is the accumulated signature from streaming thinking blocks.
|
|
// Required by Anthropic API for tool use passback when thinking is enabled.
|
|
ThinkingSignature string `json:"-"`
|
|
|
|
// Images holds generated images returned by image_generation_call tools (Codex).
|
|
// Not persisted to DB; populated at runtime from provider response.
|
|
Images []ImageContent `json:"-"`
|
|
}
|
|
|
|
// StreamChunk is a piece of a streaming response.
|
|
type StreamChunk struct {
|
|
Content string `json:"content,omitempty"`
|
|
Thinking string `json:"thinking,omitempty"`
|
|
Done bool `json:"done,omitempty"`
|
|
Images []ImageContent `json:"images,omitempty"` // image generation frames (Codex)
|
|
}
|
|
|
|
// ImageContent represents an image (either base64-encoded or a direct URL) for vision-capable models.
|
|
type ImageContent struct {
|
|
MimeType string `json:"mime_type"` // e.g. "image/jpeg"
|
|
Data string `json:"data"` // base64-encoded image bytes
|
|
URL string `json:"url,omitempty"` // URL of the image
|
|
Partial bool `json:"partial,omitempty"` // true for intermediate frames (Codex image_generation_call)
|
|
}
|
|
|
|
// VideoContent represents a video (either base64-encoded or a direct URL) for video-capable models.
|
|
type VideoContent struct {
|
|
MimeType string `json:"mime_type"` // e.g. "video/mp4"
|
|
Data string `json:"data"` // base64-encoded video bytes
|
|
URL string `json:"url,omitempty"` // URL of the video
|
|
Partial bool `json:"partial,omitempty"` // true for intermediate frames
|
|
}
|
|
|
|
// MediaRef is a lightweight reference to a persistently stored media file.
|
|
// Stored in session JSONB (~60 bytes each) instead of megabytes for base64.
|
|
// On reload, MediaRefs are resolved to file paths and loaded into Images (for images).
|
|
type MediaRef struct {
|
|
ID string `json:"id"` // unique media ID (uuid)
|
|
MimeType string `json:"mime_type"` // e.g. "image/jpeg", "application/pdf"
|
|
Kind string `json:"kind"` // "image", "video", "audio", "document"
|
|
Path string `json:"path,omitempty"` // absolute workspace path (persisted for /v1/files/ serving)
|
|
Prompt string `json:"prompt,omitempty"` // prompt that generated this asset, if known
|
|
}
|
|
|
|
// Message represents a conversation message.
|
|
type Message struct {
|
|
Role string `json:"role"` // "system", "user", "assistant", "tool"
|
|
Content string `json:"content"`
|
|
Thinking string `json:"thinking,omitempty"` // reasoning_content for thinking models (Kimi, DeepSeek, etc.)
|
|
Images []ImageContent `json:"-"` // vision: base64 images (runtime only, never persisted to DB)
|
|
Videos []VideoContent `json:"-"` // vision: base64 videos (runtime only, never persisted to DB)
|
|
MediaRefs []MediaRef `json:"media_refs,omitempty"` // persistent media file references
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
|
ToolCallID string `json:"tool_call_id,omitempty"` // for role="tool" responses
|
|
IsError bool `json:"is_error,omitempty"` // for role="tool" responses
|
|
|
|
// ToolName is the originating tool's name for role="tool" messages.
|
|
// Google Gemini drops tool_call_id entirely and pairs functionCall↔functionResponse
|
|
// by function name, so its OpenAI-compat shim requires a non-empty
|
|
// FunctionResponse.name. Carrying the name on the message itself keeps it
|
|
// recoverable after pruning, truncation or tool_call collapse — all of which
|
|
// can remove the assistant tool_call that a reverse id→name lookup depends on.
|
|
// Must match the assistant ToolCall.Name that was sent on the wire (raw name,
|
|
// not the resolved registry name). Empty on history persisted before this field.
|
|
ToolName string `json:"tool_name,omitempty"`
|
|
|
|
// Phase is a Codex-specific field (gpt-5.3-codex) indicating message purpose.
|
|
// Values: "commentary" (intermediate), "final_answer" (closeout), or "" (unset).
|
|
// Must be persisted and passed back in subsequent requests for Codex performance.
|
|
// Other providers ignore this field.
|
|
Phase string `json:"phase,omitempty"`
|
|
|
|
// RawAssistantContent carries raw provider content blocks through tool loop iterations.
|
|
// Anthropic requires thinking blocks to be passed back exactly as received.
|
|
RawAssistantContent json.RawMessage `json:"-"`
|
|
|
|
// CreatedAt records when this message was added to the session.
|
|
// Pointer type so that older messages (stored before this field existed) deserialize as nil,
|
|
// allowing the frontend to fall back to synthetic timestamps.
|
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
|
|
|
// Transient messages are runtime-only context for the next provider call.
|
|
// They must not be persisted to session history or serialized to providers.
|
|
Transient bool `json:"-"`
|
|
}
|
|
|
|
// ToolCall represents a tool invocation requested by the LLM.
|
|
type ToolCall struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Arguments map[string]any `json:"arguments"`
|
|
Metadata map[string]string `json:"metadata,omitempty"` // provider-specific (e.g. Gemini thought_signature)
|
|
ParseError string `json:"parse_error,omitempty"` // set when arguments JSON was malformed/truncated
|
|
}
|
|
|
|
// ToolDefinition describes a tool available to the LLM.
|
|
// Type is "function" for standard function tools, or a native provider tool type
|
|
// (e.g. "image_generation") for first-class provider-native tools.
|
|
// Function is nil when Type is not "function".
|
|
type ToolDefinition struct {
|
|
Type string `json:"type"` // "function" | "image_generation" | ...
|
|
Function *ToolFunctionSchema `json:"function,omitempty"` // nil when Type != "function"
|
|
}
|
|
|
|
// ToolFunctionSchema is the schema for a function tool.
|
|
type ToolFunctionSchema struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Parameters map[string]any `json:"parameters"`
|
|
Strict *bool `json:"strict,omitempty"` // OpenAI strict mode — constrained decoding
|
|
}
|
|
|
|
// Usage tracks token consumption.
|
|
type Usage struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
CacheCreationTokens int `json:"cache_creation_input_tokens,omitempty"`
|
|
CacheReadTokens int `json:"cache_read_input_tokens,omitempty"`
|
|
PromptTokensIncludeCachedSegments bool `json:"prompt_tokens_include_cached_segments,omitempty"`
|
|
ThinkingTokens int `json:"thinking_tokens,omitempty"`
|
|
RequestCount int `json:"request_count,omitempty"`
|
|
ImageCount int `json:"image_count,omitempty"`
|
|
WebSearchCount int `json:"web_search_count,omitempty"`
|
|
}
|