// Package model provides a vendor-agnostic LLM adapter layer for Cyrene v2. // // It defines a normalized Message/Request/Response vocabulary, a single // Adapter interface implemented by OpenAIChatAdapter (which speaks the // OpenAI-compatible /chat/completions protocol used by OpenAI, DashScope/百炼, // local gateways and proxies), a per-role Router, an automatic fallback Chain, // multimodal (vision) detection, and per-call CallRecord accounting. // // The full tool loop (schema registration, argument binding, result re-feed) is // deferred to M2; this package only carries interface-level placeholders // (ToolSpec) so the wire contract is ready without executing any loop. package model import "time" // Role identifies who authored a message. The values are vendor-agnostic and // translated to each provider's own role vocabulary by the adapter. type Role string const ( // RoleSystem is a system/instruction message. RoleSystem Role = "system" // RoleUser is an end-user message. RoleUser Role = "user" // RoleAssistant is an assistant (model) message. RoleAssistant Role = "assistant" // RoleTool is a tool-result message (reserved for the M2 tool loop). RoleTool Role = "tool" ) // ContentType describes the kind of payload inside a Content part. type ContentType string const ( // ContentTypeText is a plain-text content part. ContentTypeText ContentType = "text" // ContentTypeImage is an image content part. ContentTypeImage ContentType = "image_url" // ContentTypeToolCall is an assistant-originated tool invocation part // (Anthropic tool_use / OpenAI tool_calls), carried on a RoleAssistant turn. ContentTypeToolCall ContentType = "tool_call" // ContentTypeToolResult is a tool execution result part, carried on a RoleTool // turn and encoded natively (Anthropic tool_result / OpenAI tool message). ContentTypeToolResult ContentType = "tool_result" ) // Content is a single, possibly multimodal, part of a message. type Content struct { // Type distinguishes a text part from an image / tool part. Type ContentType // Text is the text payload when Type == ContentTypeText, or the result body // when Type == ContentTypeToolResult. Text string // ImageURL is the image source when Type == ContentTypeImage. It may be a // data: URI or an http(s):// URL. ImageURL string // ToolCallID correlates the part to a provider tool_use/tool_call id: the call // id for ContentTypeToolCall, or the tool_use_id being answered for // ContentTypeToolResult. ToolCallID string // ToolCallName is the invoked tool name when Type == ContentTypeToolCall. ToolCallName string // ToolArgs is the structured argument object when Type == ContentTypeToolCall. // It is never a JSON-encoded string. ToolArgs map[string]any // IsError marks a ContentTypeToolResult part as an execution failure so the // provider's tool_result.is_error / error flag is set natively. IsError bool } // Message is a single conversation turn, normalized across providers. type Message struct { Role Role // Content carries one or more parts. A text-only message uses a single // ContentTypeText part. Content []Content // SenderName is the human-readable display name of the message's sender // (design/16 §4.6). It is transport metadata used by the context-assembly // layer to label third-party messages for the model (e.g. "[第三方 · 昵称]"), // never a substitute for the strongly typed sender identity carried on the // channel layer. Empty means the sender is unlabeled (the owner, or an // unknown/unresolved sender), and callers may leave it unset. SenderName string // ToolCallID correlates a RoleTool turn to the provider tool_use/tool_call id // it answers, and ToolCallName is the invoked tool name carried as a // convenience for callers that render the result. Both are set by the tool // loop when it feeds a result back, and consumed by the adapters to emit // native tool_result / tool-message blocks. ToolCallID string ToolCallName string } // TextMessage builds a text-only message. func TextMessage(role Role, text string) Message { return Message{Role: role, Content: []Content{{Type: ContentTypeText, Text: text}}} } // ImageMessage builds a message carrying optional text plus an image. imageURL // may be a data: URI or an http(s):// URL. An empty text omits the text part. func ImageMessage(role Role, text, imageURL string) Message { parts := make([]Content, 0, 2) if text != "" { parts = append(parts, Content{Type: ContentTypeText, Text: text}) } parts = append(parts, Content{Type: ContentTypeImage, ImageURL: imageURL}) return Message{Role: role, Content: parts} } // ToolResultMessage builds a RoleTool turn carrying the native result of one tool // call. toolUseID must equal the provider id of the tool_use block this result // answers; isError marks an execution failure; text is the result body. func ToolResultMessage(toolUseID, text string, isError bool) Message { return Message{Role: RoleTool, ToolCallID: toolUseID, Content: []Content{{Type: ContentTypeToolResult, Text: text, ToolCallID: toolUseID, IsError: isError}}} } // AssistantToolCallMessage builds a RoleAssistant turn advertising the tool calls // the model requested, so a subsequent RoleTool turn can be correlated to them by // the provider (OpenAI tool_call_id / Anthropic tool_use_id). func AssistantToolCallMessage(calls []ToolCall) Message { parts := make([]Content, 0, len(calls)) for _, c := range calls { parts = append(parts, Content{Type: ContentTypeToolCall, ToolCallID: c.ID, ToolCallName: c.Name, ToolArgs: c.Args}) } return Message{Role: RoleAssistant, Content: parts} } // ToolSpec is an interface-level placeholder for a tool/function schema. The // full tool loop (registration, argument binding, result re-feed) is M2; here // it only lets a request advertise tools to the provider in the wire contract. type ToolSpec struct { Name string Description string // Parameters is the JSON Schema for the tool arguments. Populated from M2. Parameters any } // ToolCall is a single assistant-originated tool invocation parsed from a // provider's tool_use content block. Args is the structured argument object // (never a serialized JSON string), so a tool loop can bind it directly without // re-parsing. type ToolCall struct { // ID is the provider tool_use/tool_call id, so a tool loop can correlate the // resulting RoleTool message to the exact call (Anthropic tool_use_id / OpenAI // tool_call_id). Empty when the provider emitted no id. ID string // Name is the tool being invoked. Name string // Args are the tool arguments, decoded from the provider's structured // tool_use.input object. It is never a JSON-encoded string. Args map[string]any } // Request is a completion request, normalized across providers. type Request struct { // Model overrides the adapter's configured default model. Empty means the // adapter default (or the chain step model) applies. Model string Messages []Message // Temperature is the sampling temperature. nil means the provider default. Temperature *float64 // MaxTokens caps the generated tokens. Zero means the provider default. MaxTokens int // Purpose labels the call for CallRecord accounting (e.g. "chat"). Purpose string // Tools is the interface-level placeholder for M2 function calling. Tools []ToolSpec // ToolChoice optionally directs how the provider selects a tool. It is an // opaque provider-agnostic directive (e.g. {"type":"auto"} or {"type":"tool", // "name":"x"}); empty means the provider default. Passed through verbatim to // protocols that support it (Anthropic's tool_choice). ToolChoice any } // Response is a normalized completion result. type Response struct { // Model is the model that produced the response. Model string Content string // Message is the normalized assistant message. Message Message // ToolCalls carries any tool_use blocks the assistant emitted. Empty when the // response is pure text. ToolCalls []ToolCall // StopReason is the provider's raw stop/finish reason (Anthropic stop_reason: // end_turn / max_tokens / tool_use; OpenAI finish_reason: stop / length / // tool_calls), so callers can distinguish a token-budget truncation from a // normal end. Empty when the provider reports none. StopReason string // PromptTokens, CompletionTokens and TotalTokens report provider usage when // available; zero when the provider does not return it. PromptTokens int CompletionTokens int TotalTokens int } // CallRecord describes a single successfully completed LLM call, for the M9 // monitor/frontend. A failed attempt emits no record. type CallRecord struct { // Model is the model that served the request. Model string // Purpose labels what the call was for. Purpose string // Tokens is the total token consumption reported by the provider. Tokens int // TookMs is the end-to-end call latency in milliseconds. TookMs int64 } // Options carries per-call knobs layered on top of the Request. type Options struct { // Timeout bounds a single HTTP call. Zero uses the adapter client timeout. Timeout time.Duration }