Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
306 lines
12 KiB
Go
306 lines
12 KiB
Go
package types
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
)
|
|
|
|
// PromptCacheStatus distinguishes a real cache miss from providers that do not
|
|
// expose prompt-cache accounting. Treating both as zero makes fleet-level hit
|
|
// rate dashboards misleading.
|
|
type PromptCacheStatus string
|
|
|
|
const (
|
|
PromptCacheStatusUnsupported PromptCacheStatus = "unsupported"
|
|
PromptCacheStatusUnreported PromptCacheStatus = "unreported"
|
|
PromptCacheStatusMiss PromptCacheStatus = "miss"
|
|
PromptCacheStatusHit PromptCacheStatus = "hit"
|
|
)
|
|
|
|
// TokenUsage holds token consumption statistics returned by the model API.
|
|
type TokenUsage struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
// CachedTokens is the legacy alias for CacheReadTokens. It remains on the
|
|
// wire for compatibility with existing API consumers.
|
|
CachedTokens int `json:"cached_tokens,omitempty"`
|
|
CacheReadTokens int `json:"cache_read_tokens,omitempty"`
|
|
CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
|
|
CacheMissTokens int `json:"cache_miss_tokens,omitempty"`
|
|
CacheReported bool `json:"cache_reported"`
|
|
CacheStatus PromptCacheStatus `json:"cache_status,omitempty"`
|
|
}
|
|
|
|
// SetPromptCacheUsage normalizes provider-specific cache counters into the
|
|
// shared usage model. promptTokens remains the provider's total input-token
|
|
// count; read/write/miss are descriptive subsets and must not be added to it.
|
|
func (u *TokenUsage) SetPromptCacheUsage(read, write, miss int, reported bool) {
|
|
if u == nil {
|
|
return
|
|
}
|
|
if read < 0 {
|
|
read = 0
|
|
}
|
|
if write < 0 {
|
|
write = 0
|
|
}
|
|
if miss < 0 {
|
|
miss = 0
|
|
}
|
|
u.CachedTokens = read
|
|
u.CacheReadTokens = read
|
|
u.CacheWriteTokens = write
|
|
u.CacheMissTokens = miss
|
|
u.CacheReported = reported
|
|
switch {
|
|
case !reported:
|
|
u.CacheStatus = PromptCacheStatusUnreported
|
|
case read > 0:
|
|
u.CacheStatus = PromptCacheStatusHit
|
|
default:
|
|
u.CacheStatus = PromptCacheStatusMiss
|
|
}
|
|
}
|
|
|
|
// MarkPromptCacheUnsupported marks a provider/model path that cannot report
|
|
// provider-side prompt-cache usage.
|
|
func (u *TokenUsage) MarkPromptCacheUnsupported() {
|
|
if u == nil {
|
|
return
|
|
}
|
|
u.SetPromptCacheUsage(0, 0, 0, false)
|
|
u.CacheStatus = PromptCacheStatusUnsupported
|
|
}
|
|
|
|
// Accumulate adds another call's usage into u, preserving the subset
|
|
// semantics: prompt/completion/total and every cache counter sum
|
|
// independently (cache counters stay subsets of the prompt count and are
|
|
// never folded into it). CacheReported ORs, and the cache status is
|
|
// recomputed from the combined counters so a single cache hit anywhere in
|
|
// the accumulated calls reads as a hit.
|
|
func (u *TokenUsage) Accumulate(other TokenUsage) {
|
|
if u == nil {
|
|
return
|
|
}
|
|
u.PromptTokens += other.PromptTokens
|
|
u.CompletionTokens += other.CompletionTokens
|
|
u.TotalTokens += other.TotalTokens
|
|
u.CachedTokens += other.CachedTokens
|
|
u.CacheReadTokens += other.CacheReadTokens
|
|
u.CacheWriteTokens += other.CacheWriteTokens
|
|
u.CacheMissTokens += other.CacheMissTokens
|
|
u.CacheReported = u.CacheReported || other.CacheReported
|
|
switch {
|
|
case !u.CacheReported:
|
|
u.CacheStatus = mergeUnreportedCacheStatus(u.CacheStatus, other.CacheStatus)
|
|
case u.CacheReadTokens > 0:
|
|
u.CacheStatus = PromptCacheStatusHit
|
|
default:
|
|
u.CacheStatus = PromptCacheStatusMiss
|
|
}
|
|
}
|
|
|
|
// mergeUnreportedCacheStatus folds the statuses of never-reported usage.
|
|
// "unsupported" survives only while every accumulated call was itself
|
|
// classified unsupported — the first accumulation adopts the incoming
|
|
// classification, and any later call that is not known-unsupported (an
|
|
// unreported or unclassified one) degrades the aggregate to unreported.
|
|
func mergeUnreportedCacheStatus(accumulated, incoming PromptCacheStatus) PromptCacheStatus {
|
|
if accumulated == "" {
|
|
if incoming == "" {
|
|
return PromptCacheStatusUnreported
|
|
}
|
|
return incoming
|
|
}
|
|
if accumulated == PromptCacheStatusUnsupported && incoming == PromptCacheStatusUnsupported {
|
|
return PromptCacheStatusUnsupported
|
|
}
|
|
return PromptCacheStatusUnreported
|
|
}
|
|
|
|
// PromptCacheHitRate returns cache-read tokens as a percentage of the
|
|
// provider's prompt total. Zero when the prompt is empty. A ReAct turn that
|
|
// reuses the system prefix therefore reads as a high hit rather than a miss.
|
|
func (u TokenUsage) PromptCacheHitRate() float64 {
|
|
if u.PromptTokens <= 0 {
|
|
return 0
|
|
}
|
|
return float64(u.CacheReadTokens) / float64(u.PromptTokens) * 100
|
|
}
|
|
|
|
// Value persists the usage as a jsonb column (assistant messages carry the
|
|
// turn's aggregate); nil writes SQL NULL. Mirrors the nullable-pointer
|
|
// pattern APIPrincipalConfig uses.
|
|
func (u *TokenUsage) Value() (driver.Value, error) {
|
|
if u == nil {
|
|
return nil, nil
|
|
}
|
|
return json.Marshal(u)
|
|
}
|
|
|
|
// Scan restores a jsonb usage column; NULL leaves the receiver zero-valued.
|
|
func (u *TokenUsage) Scan(value interface{}) error {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
var b []byte
|
|
switch v := value.(type) {
|
|
case []byte:
|
|
b = v
|
|
case string:
|
|
b = []byte(v)
|
|
default:
|
|
return nil
|
|
}
|
|
return json.Unmarshal(b, u)
|
|
}
|
|
|
|
// LLMToolCall represents a function/tool call from the LLM
|
|
type LLMToolCall struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"` // "function"
|
|
Function FunctionCall `json:"function"`
|
|
ProviderMetadata ToolCallMetadata `json:"provider_metadata,omitempty"`
|
|
|
|
// ModelArguments and the resolution fields are request-local observability
|
|
// state. ModelArguments preserves the exact JSON emitted by the model while
|
|
// Function.Arguments is decoded to durable application identifiers before
|
|
// tool execution. These fields must never be sent back to a provider or
|
|
// persisted in chat history.
|
|
ModelArguments string `json:"-"`
|
|
ArgumentResolution string `json:"-"`
|
|
UnresolvedHandles []string `json:"-"`
|
|
}
|
|
|
|
// ToolCallMetadata carries provider-specific tool-call state that must round-trip
|
|
// with the assistant tool call, without teaching core agent code vendor fields.
|
|
type ToolCallMetadata map[string]json.RawMessage
|
|
|
|
// FunctionCall represents the function details
|
|
type FunctionCall struct {
|
|
Name string `json:"name"`
|
|
Arguments string `json:"arguments"` // JSON string
|
|
}
|
|
|
|
// ChatResponse chat response
|
|
type ChatResponse struct {
|
|
Content string `json:"content"`
|
|
// ReasoningContent 是支持思考链的模型(DeepSeek thinking、小米 MiMo、vLLM reasoning 等)
|
|
// 在本轮输出的推理内容。需要在后续多轮请求中原样回传给那些严格校验的供应商。
|
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
|
ToolCalls []LLMToolCall `json:"tool_calls,omitempty"`
|
|
FinishReason string `json:"finish_reason,omitempty"`
|
|
Usage TokenUsage `json:"usage"`
|
|
|
|
// AnswerStreamed reports whether the user-facing answer text was already
|
|
// streamed live to the final-answer UI area during this round (i.e. the
|
|
// model answered with plain content). When true, the natural-stop branch
|
|
// must only emit the closing
|
|
// Done marker for AnswerEventID instead of re-emitting the whole answer —
|
|
// otherwise the answer would render twice and "jump" at end of stream.
|
|
// Transient, never persisted.
|
|
AnswerStreamed bool `json:"-"`
|
|
// AnswerEventID is the EventBus event ID under which the live answer
|
|
// chunks were streamed, so the natural-stop branch can close the same
|
|
// stream with a Done marker. Empty when AnswerStreamed is false.
|
|
AnswerEventID string `json:"-"`
|
|
}
|
|
|
|
// Response type
|
|
type ResponseType string
|
|
|
|
const (
|
|
// Answer response type
|
|
ResponseTypeAnswer ResponseType = "answer"
|
|
// References response type
|
|
ResponseTypeReferences ResponseType = "references"
|
|
// Thinking response type (for agent thought process)
|
|
ResponseTypeThinking ResponseType = "thinking"
|
|
// Tool call response type (for agent tool invocations)
|
|
ResponseTypeToolCall ResponseType = "tool_call"
|
|
// Tool result response type (for agent tool results)
|
|
ResponseTypeToolResult ResponseType = "tool_result"
|
|
// Error response type
|
|
ResponseTypeError ResponseType = "error"
|
|
// Reflection response type (for agent reflection)
|
|
ResponseTypeReflection ResponseType = "reflection"
|
|
// Session title response type
|
|
ResponseTypeSessionTitle ResponseType = "session_title"
|
|
// Agent query response type (query received and processing started)
|
|
ResponseTypeAgentQuery ResponseType = "agent_query"
|
|
// Complete response type (agent complete)
|
|
ResponseTypeComplete ResponseType = "complete"
|
|
// ResponseTypeArtifactsPending is sent while skill/sandbox output is being
|
|
// copied into persistent storage after the answer has already streamed.
|
|
// The UI shows a toolbar placeholder until ResponseTypeComplete carries
|
|
// the file list.
|
|
ResponseTypeArtifactsPending ResponseType = "artifacts_pending"
|
|
// ToolApprovalRequired: MCP tool marked dangerous — UI must collect user approval before execution continues
|
|
ResponseTypeToolApprovalRequired ResponseType = "tool_approval_required"
|
|
// ToolApprovalResolved: user approved/rejected (or timeout); informational for UI replay
|
|
ResponseTypeToolApprovalResolved ResponseType = "tool_approval_resolved"
|
|
// MCPOAuthRequired: an OAuth-enabled MCP service was invoked but the user
|
|
// has not authorized it — UI must surface an "Authorize" prompt and the
|
|
// agent pauses until authorization completes (or the wait times out).
|
|
ResponseTypeMCPOAuthRequired ResponseType = "mcp_oauth_required"
|
|
// MCPOAuthResolved: authorization completed / timed out / canceled;
|
|
// informational for UI replay.
|
|
ResponseTypeMCPOAuthResolved ResponseType = "mcp_oauth_resolved"
|
|
// MemoryRecalled: the long-term memories injected into this answer, so
|
|
// the UI can show and let the user delete what influenced it.
|
|
ResponseTypeMemoryRecalled ResponseType = "memory_recalled"
|
|
// ResponseTypeContextCompacted is older conversation summarized away to
|
|
// fit the context window. Surfaced because it changes what the agent
|
|
// remembers — an answer that forgets an earlier instruction is otherwise
|
|
// indistinguishable from the model ignoring it.
|
|
ResponseTypeContextCompacted ResponseType = "context_compacted"
|
|
// ResponseTypeInstallPrompt is the instruction a skill install handed to
|
|
// the installer agent. Only the skill install transcript emits this, and
|
|
// it emits it first, so replaying the log alone shows what was asked for
|
|
// — the console does not have to read the durable prompt row to caption
|
|
// the run.
|
|
ResponseTypeInstallPrompt ResponseType = "install_prompt"
|
|
)
|
|
|
|
// StreamResponse stream response
|
|
// FinishReasonIncomplete marks a stream that broke before the provider sent a
|
|
// finish reason (read error, timeout, stall). Whatever content and tool calls
|
|
// were collected are a partial response: callers must not treat them as a
|
|
// completed turn.
|
|
const FinishReasonIncomplete = "incomplete"
|
|
|
|
type StreamResponse struct {
|
|
ID string `json:"id"`
|
|
ResponseType ResponseType `json:"response_type"`
|
|
Content string `json:"content"`
|
|
Done bool `json:"done"`
|
|
KnowledgeReferences References `json:"knowledge_references,omitempty"`
|
|
SessionID string `json:"session_id,omitempty"`
|
|
AssistantMessageID string `json:"assistant_message_id,omitempty"`
|
|
ToolCalls []LLMToolCall `json:"tool_calls,omitempty"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
Usage *TokenUsage `json:"usage,omitempty"`
|
|
FinishReason string `json:"finish_reason,omitempty"`
|
|
}
|
|
|
|
// References references
|
|
type References []*SearchResult
|
|
|
|
// Value implements the driver.Valuer interface, used to convert References to database values
|
|
func (c References) Value() (driver.Value, error) {
|
|
return json.Marshal(c)
|
|
}
|
|
|
|
// Scan implements the sql.Scanner interface, used to convert database values to References
|
|
func (c *References) Scan(value interface{}) error {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
b, ok := value.([]byte)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(b, c)
|
|
}
|