mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 17:07:59 +00:00
handleImage recorded the raw request model id, so AUTO image generations showed up as model=AUTO in the request records and by-model aggregates instead of the image model actually served (e.g. Kwai-Kolors/Kolors). UnifiedResponse gains an optional Model field; Provider.Image fills it with the resolved id (AUTO resolves to the source's best image model), and handleImage prefers it when writing the audit record.
207 lines
8.2 KiB
Go
207 lines
8.2 KiB
Go
// Package types defines the unified (OpenAI-compatible) wire format that the
|
|
// gateway exposes to its clients, plus the unified internal representation.
|
|
package types
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ErrBusy is the soft "source at capacity" sentinel shared by the provider
|
|
// layer (returns it) and the scheduler layer (reacts to it): busy is not a
|
|
// failure, so no cooldown/preference penalty is recorded, and gateways map
|
|
// it to HTTP 429. Defined here so the scheduler does not depend on the
|
|
// provider package (which pulls in the Lua runtime).
|
|
var ErrBusy = errors.New("provider busy")
|
|
|
|
// ---- OpenAI wire request (gateway input) ----
|
|
|
|
type ChatRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []ChatMessage `json:"messages"`
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
MaxTokens int `json:"max_tokens,omitempty"`
|
|
Stream bool `json:"stream,omitempty"`
|
|
Tools []interface{} `json:"tools,omitempty"`
|
|
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
|
DisableThinking bool `json:"disable_thinking"`
|
|
ExtraBody map[string]interface{} `json:"-"`
|
|
}
|
|
|
|
func (r *ChatRequest) MarshalJSON() ([]byte, error) {
|
|
type Alias ChatRequest
|
|
data, err := json.Marshal((*Alias)(r))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(r.ExtraBody) == 0 {
|
|
return data, nil
|
|
}
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range r.ExtraBody {
|
|
raw[k] = v
|
|
}
|
|
return json.Marshal(raw)
|
|
}
|
|
|
|
// ChatMessage supports both plain string content and multimodal arrays
|
|
// (RawMessage preserves whatever the client sent for the adapter to process).
|
|
type ChatMessage struct {
|
|
Role string `json:"role"`
|
|
Content json.RawMessage `json:"content,omitempty"`
|
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
|
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
|
}
|
|
|
|
func StringContent(s string) json.RawMessage { b, _ := json.Marshal(s); return b }
|
|
|
|
type ToolCall struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Arguments map[string]interface{} `json:"arguments"`
|
|
}
|
|
|
|
// ---- Unified internal representation (what adapters produce) ----
|
|
|
|
type UnifiedResponse struct {
|
|
Model string `json:"model,omitempty"`
|
|
Content string `json:"content"`
|
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
|
FinishReason string `json:"finish_reason,omitempty"`
|
|
TokenUsage TokenUsage `json:"token_usage"`
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
|
// ImageData used by image-generation adapters.
|
|
ImageData []ImageData `json:"image_data,omitempty"`
|
|
}
|
|
|
|
type TokenUsage struct {
|
|
Prompt int `json:"prompt"`
|
|
Completion int `json:"completion"`
|
|
Total int `json:"total"`
|
|
// PromptTokensDetails mirrors the OpenAI v2 usage.prompt_tokens_details
|
|
// object so cache-hit counts reported by OpenAI-compatible upstreams
|
|
// (and by adapters that normalize their own cache fields into it) pass
|
|
// through to clients that read it — dsh reads cached_tokens from here.
|
|
PromptTokensDetails *PromptTokensDetails `json:"prompt_tokens_details,omitempty"`
|
|
// PromptCacheHit / PromptCacheMiss carry the DeepSeek-legacy standalone
|
|
// fields; dsh falls back to prompt_cache_hit_tokens when
|
|
// prompt_tokens_details.cached_tokens is absent.
|
|
PromptCacheHit int `json:"prompt_cache_hit_tokens,omitempty"`
|
|
PromptCacheMiss int `json:"prompt_cache_miss_tokens,omitempty"`
|
|
}
|
|
|
|
// PromptTokensDetails is the OpenAI v2 prompt_tokens_details object. Only
|
|
// CachedTokens is emitted (omitempty drops the whole object when zero).
|
|
type PromptTokensDetails struct {
|
|
// CachedTokens is always emitted (even 0) so clients can distinguish
|
|
// "upstream reports cache, this request missed" from "no cache data".
|
|
CachedTokens int `json:"cached_tokens"`
|
|
}
|
|
|
|
// MarshalJSON emits both the legacy short keys (prompt/completion/total, used
|
|
// by the internal unified representation and older clients) and the OpenAI
|
|
// standard keys (prompt_tokens/completion_tokens/total_tokens). Standard
|
|
// clients such as DSH and DevEco Code read the *_tokens fields.
|
|
func (t TokenUsage) MarshalJSON() ([]byte, error) {
|
|
// Auto-generate prompt_tokens_details from legacy DeepSeek fields when
|
|
// the upstream adapter only set the standalone hit count (openai.lua
|
|
// does this in Lua, but other adapters or the standardSSEChunk fallback
|
|
// may not). dsh reads prompt_tokens_details.cached_tokens first.
|
|
pdetails := t.PromptTokensDetails
|
|
if pdetails == nil && t.PromptCacheHit > 0 {
|
|
pdetails = &PromptTokensDetails{CachedTokens: t.PromptCacheHit}
|
|
}
|
|
return json.Marshal(struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
Prompt int `json:"prompt"`
|
|
Completion int `json:"completion"`
|
|
Total int `json:"total"`
|
|
PromptTokensDetails *PromptTokensDetails `json:"prompt_tokens_details,omitempty"`
|
|
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens,omitempty"`
|
|
PromptCacheMissTokens int `json:"prompt_cache_miss_tokens,omitempty"`
|
|
}{
|
|
PromptTokens: t.Prompt,
|
|
CompletionTokens: t.Completion,
|
|
TotalTokens: t.Total,
|
|
Prompt: t.Prompt,
|
|
Completion: t.Completion,
|
|
Total: t.Total,
|
|
PromptTokensDetails: pdetails,
|
|
PromptCacheHitTokens: t.PromptCacheHit,
|
|
PromptCacheMissTokens: t.PromptCacheMiss,
|
|
})
|
|
}
|
|
|
|
type ImageData struct {
|
|
B64JSON string `json:"b64_json,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Revised string `json:"revised_prompt,omitempty"`
|
|
}
|
|
|
|
// ---- Image generation (OpenAI /v1/images/generations wire) ----
|
|
|
|
type ImageGenRequest struct {
|
|
Model string `json:"model"`
|
|
Prompt string `json:"prompt"`
|
|
N int `json:"n,omitempty"`
|
|
Size string `json:"size,omitempty"`
|
|
ResponseFormat string `json:"response_format,omitempty"`
|
|
}
|
|
|
|
type ImageGenResponse struct {
|
|
Created int64 `json:"created"`
|
|
Data []ImageData `json:"data"`
|
|
}
|
|
|
|
// ---- Unified streaming chunk produced by adapters ----
|
|
|
|
// UnifiedChunk is one streamed delta. ToolCalls carries the raw upstream
|
|
// streaming tool_calls array (incremental fragments with an index field), which
|
|
// OpenAI-compatible clients accumulate themselves.
|
|
type UnifiedChunk struct {
|
|
Content string `json:"content"`
|
|
Done bool `json:"done"`
|
|
// FinishReason carries the upstream finish/stop reason ("tool_calls",
|
|
// "length", ...) when the adapter provides it; the gateway emits it on
|
|
// the terminating chunk instead of the default "stop".
|
|
FinishReason string `json:"finish_reason,omitempty"`
|
|
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
|
// Usage carries the upstream token usage when the stream chunk provides
|
|
// it (OpenAI-style streams attach usage to some chunks; the final one
|
|
// often has empty choices). Gateway uses it to emit exact usage in the
|
|
// final stream chunk instead of estimates.
|
|
Usage *TokenUsage `json:"usage,omitempty"`
|
|
}
|
|
|
|
// Meta passed to Lua build_headers hook
|
|
type BuildMeta struct {
|
|
URL string `json:"url"`
|
|
Method string `json:"method"`
|
|
Body string `json:"body"`
|
|
APIKey string `json:"api_key"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
Source map[string]interface{} `json:"source"`
|
|
}
|
|
|
|
func Now() int64 { return time.Now().Unix() }
|
|
|
|
// OneLine flattens an error string to a single line capped at n chars.
|
|
// Shared by the provider layer (upstream error reasons) and the gateway
|
|
// layer (client-facing AUTO-chain summaries).
|
|
func OneLine(s string, n int) string {
|
|
s = strings.Join(strings.Fields(s), " ")
|
|
if len(s) > n {
|
|
s = s[:n] + "..."
|
|
}
|
|
return s
|
|
} |