mirror of
https://github.com/ollama/ollama.git
synced 2026-07-23 09:10:53 -05:00
server: add cached eval metric to response
Added cached prompt token counts to Ollama responses and compatibility usage fields. This carries local `llama-server` `cache_n` and MLX cache hits through `/api/generate`, `/api/chat`, OpenAI-compatible endpoints, and Anthropic-compatible `/v1/messages`. Cloud responses are passed through as-is, so cache counts will show up there once Cloud starts returning them.
This commit is contained in:
@@ -217,8 +217,30 @@ type MessagesResponse struct {
|
|||||||
|
|
||||||
// Usage contains token usage information
|
// Usage contains token usage information
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
InputTokens int `json:"input_tokens"`
|
InputTokens int `json:"input_tokens"`
|
||||||
OutputTokens int `json:"output_tokens"`
|
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
|
||||||
|
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
||||||
|
OutputTokens int `json:"output_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func UsageFromMetrics(metrics api.Metrics) Usage {
|
||||||
|
cached := metrics.PromptEvalCachedCount
|
||||||
|
if cached > metrics.PromptEvalCount {
|
||||||
|
cached = metrics.PromptEvalCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return Usage{
|
||||||
|
InputTokens: metrics.PromptEvalCount - cached,
|
||||||
|
CacheReadInputTokens: cached,
|
||||||
|
OutputTokens: metrics.EvalCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Usage) Add(other Usage) {
|
||||||
|
u.InputTokens += other.InputTokens
|
||||||
|
u.CacheCreationInputTokens += other.CacheCreationInputTokens
|
||||||
|
u.CacheReadInputTokens += other.CacheReadInputTokens
|
||||||
|
u.OutputTokens += other.OutputTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streaming event types
|
// Streaming event types
|
||||||
@@ -273,8 +295,10 @@ type MessageDelta struct {
|
|||||||
|
|
||||||
// DeltaUsage contains cumulative token usage
|
// DeltaUsage contains cumulative token usage
|
||||||
type DeltaUsage struct {
|
type DeltaUsage struct {
|
||||||
InputTokens int `json:"input_tokens"`
|
InputTokens int `json:"input_tokens"`
|
||||||
OutputTokens int `json:"output_tokens"`
|
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
|
||||||
|
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
||||||
|
OutputTokens int `json:"output_tokens"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageStopEvent signals the end of the message
|
// MessageStopEvent signals the end of the message
|
||||||
@@ -688,10 +712,7 @@ func ToMessagesResponse(id string, r api.ChatResponse) MessagesResponse {
|
|||||||
Model: r.Model,
|
Model: r.Model,
|
||||||
Content: content,
|
Content: content,
|
||||||
StopReason: stopReason,
|
StopReason: stopReason,
|
||||||
Usage: Usage{
|
Usage: UsageFromMetrics(r.Metrics),
|
||||||
InputTokens: r.Metrics.PromptEvalCount,
|
|
||||||
OutputTokens: r.Metrics.EvalCount,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,6 +742,8 @@ type StreamConverter struct {
|
|||||||
firstWrite bool
|
firstWrite bool
|
||||||
contentIndex int
|
contentIndex int
|
||||||
inputTokens int
|
inputTokens int
|
||||||
|
cacheCreationTokens int
|
||||||
|
cacheReadTokens int
|
||||||
outputTokens int
|
outputTokens int
|
||||||
estimatedInputTokens int // Estimated tokens from request (used when actual metrics are 0)
|
estimatedInputTokens int // Estimated tokens from request (used when actual metrics are 0)
|
||||||
thinkingStarted bool
|
thinkingStarted bool
|
||||||
@@ -753,7 +776,12 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
|||||||
c.firstWrite = false
|
c.firstWrite = false
|
||||||
// Use actual metrics if available, otherwise use estimate
|
// Use actual metrics if available, otherwise use estimate
|
||||||
c.inputTokens = r.Metrics.PromptEvalCount
|
c.inputTokens = r.Metrics.PromptEvalCount
|
||||||
if c.inputTokens == 0 && c.estimatedInputTokens > 0 {
|
c.cacheReadTokens = r.Metrics.PromptEvalCachedCount
|
||||||
|
if c.cacheReadTokens > c.inputTokens {
|
||||||
|
c.cacheReadTokens = c.inputTokens
|
||||||
|
}
|
||||||
|
c.inputTokens -= c.cacheReadTokens
|
||||||
|
if c.inputTokens == 0 && c.cacheReadTokens == 0 && c.estimatedInputTokens > 0 {
|
||||||
c.inputTokens = c.estimatedInputTokens
|
c.inputTokens = c.estimatedInputTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,8 +796,10 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
|||||||
Model: c.Model,
|
Model: c.Model,
|
||||||
Content: []ContentBlock{},
|
Content: []ContentBlock{},
|
||||||
Usage: Usage{
|
Usage: Usage{
|
||||||
InputTokens: c.inputTokens,
|
InputTokens: c.inputTokens,
|
||||||
OutputTokens: 0,
|
CacheCreationInputTokens: c.cacheCreationTokens,
|
||||||
|
CacheReadInputTokens: c.cacheReadTokens,
|
||||||
|
OutputTokens: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -939,6 +969,11 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.inputTokens = r.Metrics.PromptEvalCount
|
c.inputTokens = r.Metrics.PromptEvalCount
|
||||||
|
c.cacheReadTokens = r.Metrics.PromptEvalCachedCount
|
||||||
|
if c.cacheReadTokens > c.inputTokens {
|
||||||
|
c.cacheReadTokens = c.inputTokens
|
||||||
|
}
|
||||||
|
c.inputTokens -= c.cacheReadTokens
|
||||||
c.outputTokens = r.Metrics.EvalCount
|
c.outputTokens = r.Metrics.EvalCount
|
||||||
stopReason := mapStopReason(r.DoneReason, len(c.toolCallsSent) > 0)
|
stopReason := mapStopReason(r.DoneReason, len(c.toolCallsSent) > 0)
|
||||||
|
|
||||||
@@ -950,8 +985,10 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
|||||||
StopReason: stopReason,
|
StopReason: stopReason,
|
||||||
},
|
},
|
||||||
Usage: DeltaUsage{
|
Usage: DeltaUsage{
|
||||||
InputTokens: c.inputTokens,
|
InputTokens: c.inputTokens,
|
||||||
OutputTokens: c.outputTokens,
|
CacheCreationInputTokens: c.cacheCreationTokens,
|
||||||
|
CacheReadInputTokens: c.cacheReadTokens,
|
||||||
|
OutputTokens: c.outputTokens,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -778,6 +778,38 @@ func TestToMessagesResponse_Basic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToMessagesResponse_UsageIncludesCacheReadTokens(t *testing.T) {
|
||||||
|
resp := api.ChatResponse{
|
||||||
|
Model: "test-model",
|
||||||
|
Message: api.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "Hello there!",
|
||||||
|
},
|
||||||
|
Done: true,
|
||||||
|
DoneReason: "stop",
|
||||||
|
Metrics: api.Metrics{
|
||||||
|
PromptEvalCount: 10,
|
||||||
|
PromptEvalCachedCount: 4,
|
||||||
|
EvalCount: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ToMessagesResponse("msg_123", resp)
|
||||||
|
|
||||||
|
if result.Usage.InputTokens != 6 {
|
||||||
|
t.Errorf("expected input_tokens 6, got %d", result.Usage.InputTokens)
|
||||||
|
}
|
||||||
|
if result.Usage.CacheReadInputTokens != 4 {
|
||||||
|
t.Errorf("expected cache_read_input_tokens 4, got %d", result.Usage.CacheReadInputTokens)
|
||||||
|
}
|
||||||
|
if result.Usage.CacheCreationInputTokens != 0 {
|
||||||
|
t.Errorf("expected cache_creation_input_tokens 0, got %d", result.Usage.CacheCreationInputTokens)
|
||||||
|
}
|
||||||
|
if result.Usage.OutputTokens != 5 {
|
||||||
|
t.Errorf("expected output_tokens 5, got %d", result.Usage.OutputTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestToMessagesResponse_WithToolCalls(t *testing.T) {
|
func TestToMessagesResponse_WithToolCalls(t *testing.T) {
|
||||||
resp := api.ChatResponse{
|
resp := api.ChatResponse{
|
||||||
Model: "test-model",
|
Model: "test-model",
|
||||||
@@ -988,6 +1020,51 @@ func TestStreamConverter_Basic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStreamConverter_UsageIncludesCacheReadTokens(t *testing.T) {
|
||||||
|
conv := NewStreamConverter("msg_123", "test-model", 0)
|
||||||
|
|
||||||
|
events := conv.Process(api.ChatResponse{
|
||||||
|
Model: "test-model",
|
||||||
|
Message: api.Message{Role: "assistant"},
|
||||||
|
Done: true,
|
||||||
|
DoneReason: "stop",
|
||||||
|
Metrics: api.Metrics{
|
||||||
|
PromptEvalCount: 10,
|
||||||
|
PromptEvalCachedCount: 4,
|
||||||
|
EvalCount: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, e := range events {
|
||||||
|
if e.Event != "message_delta" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := e.Data.(MessageDeltaEvent)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected data type: %T", e.Data)
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
if data.Usage.InputTokens != 6 {
|
||||||
|
t.Errorf("expected input_tokens 6, got %d", data.Usage.InputTokens)
|
||||||
|
}
|
||||||
|
if data.Usage.CacheReadInputTokens != 4 {
|
||||||
|
t.Errorf("expected cache_read_input_tokens 4, got %d", data.Usage.CacheReadInputTokens)
|
||||||
|
}
|
||||||
|
if data.Usage.CacheCreationInputTokens != 0 {
|
||||||
|
t.Errorf("expected cache_creation_input_tokens 0, got %d", data.Usage.CacheCreationInputTokens)
|
||||||
|
}
|
||||||
|
if data.Usage.OutputTokens != 5 {
|
||||||
|
t.Errorf("expected output_tokens 5, got %d", data.Usage.OutputTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected message_delta event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStreamConverter_WithToolCalls(t *testing.T) {
|
func TestStreamConverter_WithToolCalls(t *testing.T) {
|
||||||
conv := NewStreamConverter("msg_123", "test-model", 0)
|
conv := NewStreamConverter("msg_123", "test-model", 0)
|
||||||
|
|
||||||
|
|||||||
17
api/types.go
17
api/types.go
@@ -569,12 +569,13 @@ type DebugInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
TotalDuration time.Duration `json:"total_duration,omitempty"`
|
TotalDuration time.Duration `json:"total_duration,omitempty"`
|
||||||
LoadDuration time.Duration `json:"load_duration,omitempty"`
|
LoadDuration time.Duration `json:"load_duration,omitempty"`
|
||||||
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
||||||
PromptEvalDuration time.Duration `json:"prompt_eval_duration,omitempty"`
|
PromptEvalCachedCount int `json:"prompt_eval_cached_count,omitempty"`
|
||||||
EvalCount int `json:"eval_count,omitempty"`
|
PromptEvalDuration time.Duration `json:"prompt_eval_duration,omitempty"`
|
||||||
EvalDuration time.Duration `json:"eval_duration,omitempty"`
|
EvalCount int `json:"eval_count,omitempty"`
|
||||||
|
EvalDuration time.Duration `json:"eval_duration,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options specified in [GenerateRequest]. If you add a new option here, also
|
// Options specified in [GenerateRequest]. If you add a new option here, also
|
||||||
@@ -968,6 +969,10 @@ func (m *Metrics) Summary() {
|
|||||||
fmt.Fprintf(os.Stderr, "prompt eval count: %d token(s)\n", m.PromptEvalCount)
|
fmt.Fprintf(os.Stderr, "prompt eval count: %d token(s)\n", m.PromptEvalCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.PromptEvalCachedCount > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "prompt eval cached: %d token(s)\n", m.PromptEvalCachedCount)
|
||||||
|
}
|
||||||
|
|
||||||
if m.PromptEvalDuration > 0 {
|
if m.PromptEvalDuration > 0 {
|
||||||
fmt.Fprintf(os.Stderr, "prompt eval duration: %s\n", m.PromptEvalDuration)
|
fmt.Fprintf(os.Stderr, "prompt eval duration: %s\n", m.PromptEvalDuration)
|
||||||
fmt.Fprintf(os.Stderr, "prompt eval rate: %.2f tokens/s\n", float64(m.PromptEvalCount)/m.PromptEvalDuration.Seconds())
|
fmt.Fprintf(os.Stderr, "prompt eval rate: %.2f tokens/s\n", float64(m.PromptEvalCount)/m.PromptEvalDuration.Seconds())
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ The final response in the stream also includes additional data about the generat
|
|||||||
- `total_duration`: time spent generating the response
|
- `total_duration`: time spent generating the response
|
||||||
- `load_duration`: time spent in nanoseconds loading the model
|
- `load_duration`: time spent in nanoseconds loading the model
|
||||||
- `prompt_eval_count`: number of tokens in the prompt
|
- `prompt_eval_count`: number of tokens in the prompt
|
||||||
|
- `prompt_eval_cached_count`: number of prompt tokens read from the prompt cache, when available
|
||||||
- `prompt_eval_duration`: time spent in nanoseconds evaluating the prompt
|
- `prompt_eval_duration`: time spent in nanoseconds evaluating the prompt
|
||||||
- `eval_count`: number of tokens in the response
|
- `eval_count`: number of tokens in the response
|
||||||
- `eval_duration`: time in nanoseconds spent generating the response
|
- `eval_duration`: time in nanoseconds spent generating the response
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Ollama's API responses include metrics that can be used for measuring performanc
|
|||||||
* `total_duration`: How long the response took to generate
|
* `total_duration`: How long the response took to generate
|
||||||
* `load_duration`: How long the model took to load
|
* `load_duration`: How long the model took to load
|
||||||
* `prompt_eval_count`: How many input tokens were processed
|
* `prompt_eval_count`: How many input tokens were processed
|
||||||
|
* `prompt_eval_cached_count`: How many input tokens were read from the prompt cache, when available
|
||||||
* `prompt_eval_duration`: How long it took to evaluate the prompt
|
* `prompt_eval_duration`: How long it took to evaluate the prompt
|
||||||
* `eval_count`: How many output tokens were processes
|
* `eval_count`: How many output tokens were processes
|
||||||
* `eval_duration`: How long it took to generate the output tokens
|
* `eval_duration`: How long it took to generate the output tokens
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ components:
|
|||||||
prompt_eval_count:
|
prompt_eval_count:
|
||||||
type: integer
|
type: integer
|
||||||
description: Number of input tokens in the prompt
|
description: Number of input tokens in the prompt
|
||||||
|
prompt_eval_cached_count:
|
||||||
|
type: integer
|
||||||
|
description: Number of input tokens read from the prompt cache
|
||||||
prompt_eval_duration:
|
prompt_eval_duration:
|
||||||
type: integer
|
type: integer
|
||||||
description: Time spent evaluating the prompt in nanoseconds
|
description: Time spent evaluating the prompt in nanoseconds
|
||||||
@@ -191,6 +194,9 @@ components:
|
|||||||
prompt_eval_count:
|
prompt_eval_count:
|
||||||
type: integer
|
type: integer
|
||||||
description: Number of input tokens in the prompt
|
description: Number of input tokens in the prompt
|
||||||
|
prompt_eval_cached_count:
|
||||||
|
type: integer
|
||||||
|
description: Number of input tokens read from the prompt cache
|
||||||
prompt_eval_duration:
|
prompt_eval_duration:
|
||||||
type: integer
|
type: integer
|
||||||
description: Time spent evaluating the prompt in nanoseconds
|
description: Time spent evaluating the prompt in nanoseconds
|
||||||
@@ -352,6 +358,9 @@ components:
|
|||||||
prompt_eval_count:
|
prompt_eval_count:
|
||||||
type: integer
|
type: integer
|
||||||
description: Number of tokens in the prompt
|
description: Number of tokens in the prompt
|
||||||
|
prompt_eval_cached_count:
|
||||||
|
type: integer
|
||||||
|
description: Number of prompt tokens read from the prompt cache
|
||||||
prompt_eval_duration:
|
prompt_eval_duration:
|
||||||
type: integer
|
type: integer
|
||||||
description: Time spent evaluating the prompt in nanoseconds
|
description: Time spent evaluating the prompt in nanoseconds
|
||||||
|
|||||||
@@ -1625,13 +1625,14 @@ func (s *llamaServerRunner) Completion(ctx context.Context, req CompletionReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
finalResp = CompletionResponse{
|
finalResp = CompletionResponse{
|
||||||
Content: lsResp.Content,
|
Content: lsResp.Content,
|
||||||
Done: true,
|
Done: true,
|
||||||
DoneReason: doneReason,
|
DoneReason: doneReason,
|
||||||
PromptEvalCount: lsResp.Timings.promptEvalCount(),
|
PromptEvalCount: lsResp.Timings.promptEvalCount(),
|
||||||
PromptEvalDuration: time.Duration(lsResp.Timings.PromptMS * float64(time.Millisecond)),
|
PromptEvalCachedCount: lsResp.Timings.CacheN,
|
||||||
EvalCount: lsResp.Timings.PredictN,
|
PromptEvalDuration: time.Duration(lsResp.Timings.PromptMS * float64(time.Millisecond)),
|
||||||
EvalDuration: time.Duration(lsResp.Timings.PredictMS * float64(time.Millisecond)),
|
EvalCount: lsResp.Timings.PredictN,
|
||||||
|
EvalDuration: time.Duration(lsResp.Timings.PredictMS * float64(time.Millisecond)),
|
||||||
}
|
}
|
||||||
hasFinalResp = true
|
hasFinalResp = true
|
||||||
}
|
}
|
||||||
@@ -1928,6 +1929,7 @@ func (s *llamaServerRunner) Chat(ctx context.Context, req ChatRequest, fn func(C
|
|||||||
resp.Done = true
|
resp.Done = true
|
||||||
resp.DoneReason = doneReason
|
resp.DoneReason = doneReason
|
||||||
resp.PromptEvalCount = lsResp.Timings.promptEvalCount()
|
resp.PromptEvalCount = lsResp.Timings.promptEvalCount()
|
||||||
|
resp.PromptEvalCachedCount = lsResp.Timings.CacheN
|
||||||
resp.PromptEvalDuration = time.Duration(lsResp.Timings.PromptMS * float64(time.Millisecond))
|
resp.PromptEvalDuration = time.Duration(lsResp.Timings.PromptMS * float64(time.Millisecond))
|
||||||
resp.EvalCount = lsResp.Timings.PredictN
|
resp.EvalCount = lsResp.Timings.PredictN
|
||||||
resp.EvalDuration = time.Duration(lsResp.Timings.PredictMS * float64(time.Millisecond))
|
resp.EvalDuration = time.Duration(lsResp.Timings.PredictMS * float64(time.Millisecond))
|
||||||
|
|||||||
@@ -291,6 +291,9 @@ func TestLlamaServerCompletionPromptEvalCountIncludesCache(t *testing.T) {
|
|||||||
if responses[0].PromptEvalCount != 17 {
|
if responses[0].PromptEvalCount != 17 {
|
||||||
t.Errorf("PromptEvalCount = %d, want 17", responses[0].PromptEvalCount)
|
t.Errorf("PromptEvalCount = %d, want 17", responses[0].PromptEvalCount)
|
||||||
}
|
}
|
||||||
|
if responses[0].PromptEvalCachedCount != 12 {
|
||||||
|
t.Errorf("PromptEvalCachedCount = %d, want 12", responses[0].PromptEvalCachedCount)
|
||||||
|
}
|
||||||
if responses[0].PromptEvalDuration != 10*time.Millisecond {
|
if responses[0].PromptEvalDuration != 10*time.Millisecond {
|
||||||
t.Errorf("PromptEvalDuration = %s, want 10ms", responses[0].PromptEvalDuration)
|
t.Errorf("PromptEvalDuration = %s, want 10ms", responses[0].PromptEvalDuration)
|
||||||
}
|
}
|
||||||
@@ -341,6 +344,9 @@ func TestLlamaServerChatPromptEvalCountIncludesCache(t *testing.T) {
|
|||||||
if responses[1].PromptEvalCount != 17 {
|
if responses[1].PromptEvalCount != 17 {
|
||||||
t.Errorf("PromptEvalCount = %d, want 17", responses[1].PromptEvalCount)
|
t.Errorf("PromptEvalCount = %d, want 17", responses[1].PromptEvalCount)
|
||||||
}
|
}
|
||||||
|
if responses[1].PromptEvalCachedCount != 12 {
|
||||||
|
t.Errorf("PromptEvalCachedCount = %d, want 12", responses[1].PromptEvalCachedCount)
|
||||||
|
}
|
||||||
if responses[1].PromptEvalDuration != 10*time.Millisecond {
|
if responses[1].PromptEvalDuration != 10*time.Millisecond {
|
||||||
t.Errorf("PromptEvalDuration = %s, want 10ms", responses[1].PromptEvalDuration)
|
t.Errorf("PromptEvalDuration = %s, want 10ms", responses[1].PromptEvalDuration)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,14 +242,15 @@ type ChatRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChatResponse struct {
|
type ChatResponse struct {
|
||||||
Message api.Message `json:"message"`
|
Message api.Message `json:"message"`
|
||||||
DoneReason DoneReason `json:"done_reason"`
|
DoneReason DoneReason `json:"done_reason"`
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
PromptEvalCount int `json:"prompt_eval_count"`
|
PromptEvalCount int `json:"prompt_eval_count"`
|
||||||
PromptEvalDuration time.Duration `json:"prompt_eval_duration"`
|
PromptEvalCachedCount int `json:"prompt_eval_cached_count"`
|
||||||
EvalCount int `json:"eval_count"`
|
PromptEvalDuration time.Duration `json:"prompt_eval_duration"`
|
||||||
EvalDuration time.Duration `json:"eval_duration"`
|
EvalCount int `json:"eval_count"`
|
||||||
Logprobs []Logprob `json:"logprobs,omitempty"`
|
EvalDuration time.Duration `json:"eval_duration"`
|
||||||
|
Logprobs []Logprob `json:"logprobs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoneReason represents the reason why a completion response is done
|
// DoneReason represents the reason why a completion response is done
|
||||||
@@ -285,13 +286,14 @@ type Logprob struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CompletionResponse struct {
|
type CompletionResponse struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
DoneReason DoneReason `json:"done_reason"`
|
DoneReason DoneReason `json:"done_reason"`
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
PromptEvalCount int `json:"prompt_eval_count"`
|
PromptEvalCount int `json:"prompt_eval_count"`
|
||||||
PromptEvalDuration time.Duration `json:"prompt_eval_duration"`
|
PromptEvalCachedCount int `json:"prompt_eval_cached_count"`
|
||||||
EvalCount int `json:"eval_count"`
|
PromptEvalDuration time.Duration `json:"prompt_eval_duration"`
|
||||||
EvalDuration time.Duration `json:"eval_duration"`
|
EvalCount int `json:"eval_count"`
|
||||||
|
EvalDuration time.Duration `json:"eval_duration"`
|
||||||
|
|
||||||
// Logprobs contains log probability information if requested
|
// Logprobs contains log probability information if requested
|
||||||
Logprobs []Logprob `json:"logprobs,omitempty"`
|
Logprobs []Logprob `json:"logprobs,omitempty"`
|
||||||
|
|||||||
@@ -101,13 +101,13 @@ type WebSearchAnthropicWriter struct {
|
|||||||
|
|
||||||
terminalSent bool
|
terminalSent bool
|
||||||
|
|
||||||
observedPromptEvalCount int
|
observedPromptEvalCount int
|
||||||
observedEvalCount int
|
observedPromptEvalCachedCount int
|
||||||
|
observedEvalCount int
|
||||||
|
|
||||||
loopInFlight bool
|
loopInFlight bool
|
||||||
loopBaseInputTok int
|
loopBaseUsage anthropic.Usage
|
||||||
loopBaseOutputTok int
|
loopResultCh chan webSearchLoopResult
|
||||||
loopResultCh chan webSearchLoopResult
|
|
||||||
|
|
||||||
streamMessageStarted bool
|
streamMessageStarted bool
|
||||||
streamHasOpenBlock bool
|
streamHasOpenBlock bool
|
||||||
@@ -201,10 +201,7 @@ func (w *WebSearchAnthropicWriter) Write(data []byte) (int, error) {
|
|||||||
loopCtx, cancel := w.startLoopContext()
|
loopCtx, cancel := w.startLoopContext()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
initialUsage := anthropic.Usage{
|
initialUsage := w.usageWithObservedMetrics(chatResponse.Metrics)
|
||||||
InputTokens: max(w.observedPromptEvalCount, chatResponse.Metrics.PromptEvalCount),
|
|
||||||
OutputTokens: max(w.observedEvalCount, chatResponse.Metrics.EvalCount),
|
|
||||||
}
|
|
||||||
logutil.Trace("anthropic middleware: starting sync web_search loop",
|
logutil.Trace("anthropic middleware: starting sync web_search loop",
|
||||||
"tool_call", anthropic.TraceToolCall(webSearchCall),
|
"tool_call", anthropic.TraceToolCall(webSearchCall),
|
||||||
"resp", anthropic.TraceChatResponse(chatResponse),
|
"resp", anthropic.TraceChatResponse(chatResponse),
|
||||||
@@ -319,8 +316,7 @@ func (w *WebSearchAnthropicWriter) runWebSearchLoop(ctx context.Context, initial
|
|||||||
"resp", anthropic.TraceChatResponse(followUpResponse),
|
"resp", anthropic.TraceChatResponse(followUpResponse),
|
||||||
)
|
)
|
||||||
|
|
||||||
usage.InputTokens += followUpResponse.Metrics.PromptEvalCount
|
usage.Add(anthropic.UsageFromMetrics(followUpResponse.Metrics))
|
||||||
usage.OutputTokens += followUpResponse.Metrics.EvalCount
|
|
||||||
|
|
||||||
nextToolCall, hasWebSearch, hasOtherTools := findWebSearchToolCall(followUpResponse.Message.ToolCalls)
|
nextToolCall, hasWebSearch, hasOtherTools := findWebSearchToolCall(followUpResponse.Message.ToolCalls)
|
||||||
if hasWebSearch && hasOtherTools {
|
if hasWebSearch && hasOtherTools {
|
||||||
@@ -380,12 +376,8 @@ func (w *WebSearchAnthropicWriter) startLoopWorker(initialResponse api.ChatRespo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
initialUsage := anthropic.Usage{
|
initialUsage := w.usageWithObservedMetrics(initialResponse.Metrics)
|
||||||
InputTokens: max(w.observedPromptEvalCount, initialResponse.Metrics.PromptEvalCount),
|
w.loopBaseUsage = initialUsage
|
||||||
OutputTokens: max(w.observedEvalCount, initialResponse.Metrics.EvalCount),
|
|
||||||
}
|
|
||||||
w.loopBaseInputTok = initialUsage.InputTokens
|
|
||||||
w.loopBaseOutputTok = initialUsage.OutputTokens
|
|
||||||
w.loopResultCh = make(chan webSearchLoopResult, 1)
|
w.loopResultCh = make(chan webSearchLoopResult, 1)
|
||||||
w.loopInFlight = true
|
w.loopInFlight = true
|
||||||
logutil.Trace("anthropic middleware: loop worker started",
|
logutil.Trace("anthropic middleware: loop worker started",
|
||||||
@@ -438,25 +430,42 @@ func (w *WebSearchAnthropicWriter) recordObservedUsage(metrics api.Metrics) {
|
|||||||
if metrics.PromptEvalCount > w.observedPromptEvalCount {
|
if metrics.PromptEvalCount > w.observedPromptEvalCount {
|
||||||
w.observedPromptEvalCount = metrics.PromptEvalCount
|
w.observedPromptEvalCount = metrics.PromptEvalCount
|
||||||
}
|
}
|
||||||
|
if metrics.PromptEvalCachedCount > w.observedPromptEvalCachedCount {
|
||||||
|
w.observedPromptEvalCachedCount = metrics.PromptEvalCachedCount
|
||||||
|
}
|
||||||
if metrics.EvalCount > w.observedEvalCount {
|
if metrics.EvalCount > w.observedEvalCount {
|
||||||
w.observedEvalCount = metrics.EvalCount
|
w.observedEvalCount = metrics.EvalCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebSearchAnthropicWriter) applyObservedUsageDeltaToUsage(usage *anthropic.Usage) {
|
func (w *WebSearchAnthropicWriter) applyObservedUsageDeltaToUsage(usage *anthropic.Usage) {
|
||||||
if deltaIn := w.observedPromptEvalCount - w.loopBaseInputTok; deltaIn > 0 {
|
observed := w.currentObservedUsage()
|
||||||
usage.InputTokens += deltaIn
|
usage.InputTokens += observed.InputTokens - w.loopBaseUsage.InputTokens
|
||||||
}
|
usage.CacheCreationInputTokens += observed.CacheCreationInputTokens - w.loopBaseUsage.CacheCreationInputTokens
|
||||||
if deltaOut := w.observedEvalCount - w.loopBaseOutputTok; deltaOut > 0 {
|
usage.CacheReadInputTokens += observed.CacheReadInputTokens - w.loopBaseUsage.CacheReadInputTokens
|
||||||
usage.OutputTokens += deltaOut
|
usage.OutputTokens += observed.OutputTokens - w.loopBaseUsage.OutputTokens
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebSearchAnthropicWriter) currentObservedUsage() anthropic.Usage {
|
func (w *WebSearchAnthropicWriter) currentObservedUsage() anthropic.Usage {
|
||||||
return anthropic.Usage{
|
return anthropic.UsageFromMetrics(api.Metrics{
|
||||||
InputTokens: w.observedPromptEvalCount,
|
PromptEvalCount: w.observedPromptEvalCount,
|
||||||
OutputTokens: w.observedEvalCount,
|
PromptEvalCachedCount: w.observedPromptEvalCachedCount,
|
||||||
|
EvalCount: w.observedEvalCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebSearchAnthropicWriter) usageWithObservedMetrics(metrics api.Metrics) anthropic.Usage {
|
||||||
|
if w.observedPromptEvalCount > metrics.PromptEvalCount {
|
||||||
|
metrics.PromptEvalCount = w.observedPromptEvalCount
|
||||||
}
|
}
|
||||||
|
if w.observedPromptEvalCachedCount > metrics.PromptEvalCachedCount {
|
||||||
|
metrics.PromptEvalCachedCount = w.observedPromptEvalCachedCount
|
||||||
|
}
|
||||||
|
if w.observedEvalCount > metrics.EvalCount {
|
||||||
|
metrics.EvalCount = w.observedEvalCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return anthropic.UsageFromMetrics(metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebSearchAnthropicWriter) startLoopContext() (context.Context, context.CancelFunc) {
|
func (w *WebSearchAnthropicWriter) startLoopContext() (context.Context, context.CancelFunc) {
|
||||||
@@ -625,7 +634,7 @@ func (w *WebSearchAnthropicWriter) ensureStreamMessageStart(usage anthropic.Usag
|
|||||||
}
|
}
|
||||||
|
|
||||||
inputTokens := usage.InputTokens
|
inputTokens := usage.InputTokens
|
||||||
if inputTokens == 0 {
|
if inputTokens == 0 && usage.CacheCreationInputTokens == 0 && usage.CacheReadInputTokens == 0 {
|
||||||
inputTokens = w.estimatedInputTokens
|
inputTokens = w.estimatedInputTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,7 +647,9 @@ func (w *WebSearchAnthropicWriter) ensureStreamMessageStart(usage anthropic.Usag
|
|||||||
Model: w.req.Model,
|
Model: w.req.Model,
|
||||||
Content: []anthropic.ContentBlock{},
|
Content: []anthropic.ContentBlock{},
|
||||||
Usage: anthropic.Usage{
|
Usage: anthropic.Usage{
|
||||||
InputTokens: inputTokens,
|
InputTokens: inputTokens,
|
||||||
|
CacheCreationInputTokens: usage.CacheCreationInputTokens,
|
||||||
|
CacheReadInputTokens: usage.CacheReadInputTokens,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -751,8 +762,10 @@ func (w *WebSearchAnthropicWriter) writeTerminalResponse(response anthropic.Mess
|
|||||||
StopReason: response.StopReason,
|
StopReason: response.StopReason,
|
||||||
},
|
},
|
||||||
Usage: anthropic.DeltaUsage{
|
Usage: anthropic.DeltaUsage{
|
||||||
InputTokens: response.Usage.InputTokens,
|
InputTokens: response.Usage.InputTokens,
|
||||||
OutputTokens: response.Usage.OutputTokens,
|
CacheCreationInputTokens: response.Usage.CacheCreationInputTokens,
|
||||||
|
CacheReadInputTokens: response.Usage.CacheReadInputTokens,
|
||||||
|
OutputTokens: response.Usage.OutputTokens,
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2164,7 +2164,7 @@ func TestWebSearchStreamingUsageUsesObservedChunkMetrics(t *testing.T) {
|
|||||||
Message: api.Message{Role: "assistant", Content: "After search."},
|
Message: api.Message{Role: "assistant", Content: "After search."},
|
||||||
Done: true,
|
Done: true,
|
||||||
DoneReason: "stop",
|
DoneReason: "stop",
|
||||||
Metrics: api.Metrics{PromptEvalCount: 20, EvalCount: 7},
|
Metrics: api.Metrics{PromptEvalCount: 20, PromptEvalCachedCount: 5, EvalCount: 7},
|
||||||
}
|
}
|
||||||
_ = json.NewEncoder(w).Encode(resp)
|
_ = json.NewEncoder(w).Encode(resp)
|
||||||
}))
|
}))
|
||||||
@@ -2192,7 +2192,7 @@ func TestWebSearchStreamingUsageUsesObservedChunkMetrics(t *testing.T) {
|
|||||||
Model: "test-model",
|
Model: "test-model",
|
||||||
Message: api.Message{Role: "assistant", Content: "Preface "},
|
Message: api.Message{Role: "assistant", Content: "Preface "},
|
||||||
Done: false,
|
Done: false,
|
||||||
Metrics: api.Metrics{PromptEvalCount: 12, EvalCount: 4},
|
Metrics: api.Metrics{PromptEvalCount: 12, PromptEvalCachedCount: 4, EvalCount: 4},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Model: "test-model",
|
Model: "test-model",
|
||||||
@@ -2216,7 +2216,7 @@ func TestWebSearchStreamingUsageUsesObservedChunkMetrics(t *testing.T) {
|
|||||||
Message: api.Message{Role: "assistant"},
|
Message: api.Message{Role: "assistant"},
|
||||||
Done: true,
|
Done: true,
|
||||||
DoneReason: "stop",
|
DoneReason: "stop",
|
||||||
Metrics: api.Metrics{PromptEvalCount: 12, EvalCount: 4},
|
Metrics: api.Metrics{PromptEvalCount: 12, PromptEvalCachedCount: 4, EvalCount: 4},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
c.Writer.WriteHeader(http.StatusOK)
|
c.Writer.WriteHeader(http.StatusOK)
|
||||||
@@ -2259,8 +2259,11 @@ func TestWebSearchStreamingUsageUsesObservedChunkMetrics(t *testing.T) {
|
|||||||
if !found {
|
if !found {
|
||||||
t.Fatal("expected message_delta event")
|
t.Fatal("expected message_delta event")
|
||||||
}
|
}
|
||||||
if messageDelta.Usage.InputTokens != 32 {
|
if messageDelta.Usage.InputTokens != 23 {
|
||||||
t.Fatalf("expected aggregated input tokens 32 (12 passthrough + 20 followup), got %d", messageDelta.Usage.InputTokens)
|
t.Fatalf("expected aggregated fresh input tokens 23 ((12-4) passthrough + (20-5) followup), got %d", messageDelta.Usage.InputTokens)
|
||||||
|
}
|
||||||
|
if messageDelta.Usage.CacheReadInputTokens != 9 {
|
||||||
|
t.Fatalf("expected aggregated cache read tokens 9 (4 passthrough + 5 followup), got %d", messageDelta.Usage.CacheReadInputTokens)
|
||||||
}
|
}
|
||||||
if messageDelta.Usage.OutputTokens != 11 {
|
if messageDelta.Usage.OutputTokens != 11 {
|
||||||
t.Fatalf("expected aggregated output tokens 11 (4 passthrough + 7 followup), got %d", messageDelta.Usage.OutputTokens)
|
t.Fatalf("expected aggregated output tokens 11 (4 passthrough + 7 followup), got %d", messageDelta.Usage.OutputTokens)
|
||||||
|
|||||||
@@ -65,10 +65,15 @@ type CompleteChunkChoice struct {
|
|||||||
Logprobs *ChoiceLogprobs `json:"logprobs,omitempty"`
|
Logprobs *ChoiceLogprobs `json:"logprobs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PromptTokensDetails struct {
|
||||||
|
CachedTokens int `json:"cached_tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
CompletionTokens int `json:"completion_tokens"`
|
PromptTokensDetails PromptTokensDetails `json:"prompt_tokens_details"`
|
||||||
TotalTokens int `json:"total_tokens"`
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResponseFormat struct {
|
type ResponseFormat struct {
|
||||||
@@ -232,9 +237,10 @@ func NewError(code int, message string) ErrorResponse {
|
|||||||
// ToUsage converts an api.ChatResponse to Usage
|
// ToUsage converts an api.ChatResponse to Usage
|
||||||
func ToUsage(r api.ChatResponse) Usage {
|
func ToUsage(r api.ChatResponse) Usage {
|
||||||
return Usage{
|
return Usage{
|
||||||
PromptTokens: r.Metrics.PromptEvalCount,
|
PromptTokens: r.Metrics.PromptEvalCount,
|
||||||
CompletionTokens: r.Metrics.EvalCount,
|
PromptTokensDetails: PromptTokensDetails{CachedTokens: r.Metrics.PromptEvalCachedCount},
|
||||||
TotalTokens: r.Metrics.PromptEvalCount + r.Metrics.EvalCount,
|
CompletionTokens: r.Metrics.EvalCount,
|
||||||
|
TotalTokens: r.Metrics.PromptEvalCount + r.Metrics.EvalCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,9 +361,10 @@ func ToChunk(id string, r api.ChatResponse, toolCallSent bool) ChatCompletionChu
|
|||||||
// ToUsageGenerate converts an api.GenerateResponse to Usage
|
// ToUsageGenerate converts an api.GenerateResponse to Usage
|
||||||
func ToUsageGenerate(r api.GenerateResponse) Usage {
|
func ToUsageGenerate(r api.GenerateResponse) Usage {
|
||||||
return Usage{
|
return Usage{
|
||||||
PromptTokens: r.Metrics.PromptEvalCount,
|
PromptTokens: r.Metrics.PromptEvalCount,
|
||||||
CompletionTokens: r.Metrics.EvalCount,
|
PromptTokensDetails: PromptTokensDetails{CachedTokens: r.Metrics.PromptEvalCachedCount},
|
||||||
TotalTokens: r.Metrics.PromptEvalCount + r.Metrics.EvalCount,
|
CompletionTokens: r.Metrics.EvalCount,
|
||||||
|
TotalTokens: r.Metrics.PromptEvalCount + r.Metrics.EvalCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,8 +176,9 @@ func TestFromCompleteRequest_Basic(t *testing.T) {
|
|||||||
func TestToUsage(t *testing.T) {
|
func TestToUsage(t *testing.T) {
|
||||||
resp := api.ChatResponse{
|
resp := api.ChatResponse{
|
||||||
Metrics: api.Metrics{
|
Metrics: api.Metrics{
|
||||||
PromptEvalCount: 10,
|
PromptEvalCount: 10,
|
||||||
EvalCount: 20,
|
PromptEvalCachedCount: 4,
|
||||||
|
EvalCount: 20,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +188,10 @@ func TestToUsage(t *testing.T) {
|
|||||||
t.Errorf("expected PromptTokens 10, got %d", usage.PromptTokens)
|
t.Errorf("expected PromptTokens 10, got %d", usage.PromptTokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if usage.PromptTokensDetails.CachedTokens != 4 {
|
||||||
|
t.Errorf("expected CachedTokens 4, got %d", usage.PromptTokensDetails.CachedTokens)
|
||||||
|
}
|
||||||
|
|
||||||
if usage.CompletionTokens != 20 {
|
if usage.CompletionTokens != 20 {
|
||||||
t.Errorf("expected CompletionTokens 20, got %d", usage.CompletionTokens)
|
t.Errorf("expected CompletionTokens 20, got %d", usage.CompletionTokens)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -856,11 +856,10 @@ func ToResponse(model, responseID, itemID string, chatResponse api.ChatResponse,
|
|||||||
Temperature: derefFloat64(request.Temperature, 1.0),
|
Temperature: derefFloat64(request.Temperature, 1.0),
|
||||||
Reasoning: reasoning,
|
Reasoning: reasoning,
|
||||||
Usage: &ResponsesUsage{
|
Usage: &ResponsesUsage{
|
||||||
InputTokens: chatResponse.PromptEvalCount,
|
InputTokens: chatResponse.PromptEvalCount,
|
||||||
OutputTokens: chatResponse.EvalCount,
|
OutputTokens: chatResponse.EvalCount,
|
||||||
TotalTokens: chatResponse.PromptEvalCount + chatResponse.EvalCount,
|
TotalTokens: chatResponse.PromptEvalCount + chatResponse.EvalCount,
|
||||||
// TODO(drifkin): wire through the actual values
|
InputTokensDetails: ResponsesInputTokensDetails{CachedTokens: chatResponse.PromptEvalCachedCount},
|
||||||
InputTokensDetails: ResponsesInputTokensDetails{CachedTokens: 0},
|
|
||||||
// TODO(drifkin): wire through the actual values
|
// TODO(drifkin): wire through the actual values
|
||||||
OutputTokensDetails: ResponsesOutputTokensDetails{ReasoningTokens: 0},
|
OutputTokensDetails: ResponsesOutputTokensDetails{ReasoningTokens: 0},
|
||||||
},
|
},
|
||||||
@@ -1366,7 +1365,7 @@ func (c *ResponsesStreamConverter) processCompletion(r api.ChatResponse) []Respo
|
|||||||
"output_tokens": r.EvalCount,
|
"output_tokens": r.EvalCount,
|
||||||
"total_tokens": r.PromptEvalCount + r.EvalCount,
|
"total_tokens": r.PromptEvalCount + r.EvalCount,
|
||||||
"input_tokens_details": map[string]any{
|
"input_tokens_details": map[string]any{
|
||||||
"cached_tokens": 0,
|
"cached_tokens": r.PromptEvalCachedCount,
|
||||||
},
|
},
|
||||||
"output_tokens_details": map[string]any{
|
"output_tokens_details": map[string]any{
|
||||||
"reasoning_tokens": 0,
|
"reasoning_tokens": 0,
|
||||||
|
|||||||
@@ -1506,6 +1506,32 @@ func TestToResponse_WithReasoning(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToResponse_UsageIncludesCachedTokens(t *testing.T) {
|
||||||
|
response := ToResponse("gpt-oss:20b", "resp_123", "msg_456", api.ChatResponse{
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Message: api.Message{Content: "The answer is 42"},
|
||||||
|
Done: true,
|
||||||
|
Metrics: api.Metrics{
|
||||||
|
PromptEvalCount: 10,
|
||||||
|
PromptEvalCachedCount: 4,
|
||||||
|
EvalCount: 3,
|
||||||
|
},
|
||||||
|
}, ResponsesRequest{})
|
||||||
|
|
||||||
|
if response.Usage == nil {
|
||||||
|
t.Fatal("expected usage")
|
||||||
|
}
|
||||||
|
if response.Usage.InputTokens != 10 {
|
||||||
|
t.Errorf("InputTokens = %d, want 10", response.Usage.InputTokens)
|
||||||
|
}
|
||||||
|
if response.Usage.InputTokensDetails.CachedTokens != 4 {
|
||||||
|
t.Errorf("CachedTokens = %d, want 4", response.Usage.InputTokensDetails.CachedTokens)
|
||||||
|
}
|
||||||
|
if response.Usage.TotalTokens != 13 {
|
||||||
|
t.Errorf("TotalTokens = %d, want 13", response.Usage.TotalTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFromResponsesRequest_Instructions(t *testing.T) {
|
func TestFromResponsesRequest_Instructions(t *testing.T) {
|
||||||
reqJSON := `{
|
reqJSON := `{
|
||||||
"model": "gpt-oss:20b",
|
"model": "gpt-oss:20b",
|
||||||
@@ -1858,6 +1884,11 @@ func TestResponsesStreamConverter_OutputIncludesContent(t *testing.T) {
|
|||||||
events := converter.Process(api.ChatResponse{
|
events := converter.Process(api.ChatResponse{
|
||||||
Message: api.Message{},
|
Message: api.Message{},
|
||||||
Done: true,
|
Done: true,
|
||||||
|
Metrics: api.Metrics{
|
||||||
|
PromptEvalCount: 10,
|
||||||
|
PromptEvalCachedCount: 4,
|
||||||
|
EvalCount: 3,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Find the output_item.done event
|
// Find the output_item.done event
|
||||||
@@ -1906,6 +1937,11 @@ func TestResponsesStreamConverter_ResponseCompletedIncludesOutput(t *testing.T)
|
|||||||
events := converter.Process(api.ChatResponse{
|
events := converter.Process(api.ChatResponse{
|
||||||
Message: api.Message{},
|
Message: api.Message{},
|
||||||
Done: true,
|
Done: true,
|
||||||
|
Metrics: api.Metrics{
|
||||||
|
PromptEvalCount: 10,
|
||||||
|
PromptEvalCachedCount: 4,
|
||||||
|
EvalCount: 3,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Find the response.completed event
|
// Find the response.completed event
|
||||||
@@ -1935,6 +1971,12 @@ func TestResponsesStreamConverter_ResponseCompletedIncludesOutput(t *testing.T)
|
|||||||
if item["type"] != "message" {
|
if item["type"] != "message" {
|
||||||
t.Errorf("output[0].type = %q, want %q", item["type"], "message")
|
t.Errorf("output[0].type = %q, want %q", item["type"], "message")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usage := response["usage"].(map[string]any)
|
||||||
|
inputTokensDetails := usage["input_tokens_details"].(map[string]any)
|
||||||
|
if inputTokensDetails["cached_tokens"] != 4 {
|
||||||
|
t.Errorf("cached_tokens = %v, want 4", inputTokensDetails["cached_tokens"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResponsesStreamConverter_ResponseCreatedIncludesOutput(t *testing.T) {
|
func TestResponsesStreamConverter_ResponseCreatedIncludesOutput(t *testing.T) {
|
||||||
|
|||||||
@@ -681,10 +681,11 @@ func (s *Server) GenerateHandler(c *gin.Context) {
|
|||||||
Response: cr.Content,
|
Response: cr.Content,
|
||||||
Done: cr.Done,
|
Done: cr.Done,
|
||||||
Metrics: api.Metrics{
|
Metrics: api.Metrics{
|
||||||
PromptEvalCount: cr.PromptEvalCount,
|
PromptEvalCount: cr.PromptEvalCount,
|
||||||
PromptEvalDuration: cr.PromptEvalDuration,
|
PromptEvalCachedCount: cr.PromptEvalCachedCount,
|
||||||
EvalCount: cr.EvalCount,
|
PromptEvalDuration: cr.PromptEvalDuration,
|
||||||
EvalDuration: cr.EvalDuration,
|
EvalCount: cr.EvalCount,
|
||||||
|
EvalDuration: cr.EvalDuration,
|
||||||
},
|
},
|
||||||
Logprobs: toAPILogprobs(cr.Logprobs),
|
Logprobs: toAPILogprobs(cr.Logprobs),
|
||||||
}
|
}
|
||||||
@@ -2814,10 +2815,11 @@ func (s *Server) ChatHandler(c *gin.Context) {
|
|||||||
Message: api.Message{Role: "assistant", Content: r.Content},
|
Message: api.Message{Role: "assistant", Content: r.Content},
|
||||||
Done: r.Done,
|
Done: r.Done,
|
||||||
Metrics: api.Metrics{
|
Metrics: api.Metrics{
|
||||||
PromptEvalCount: r.PromptEvalCount,
|
PromptEvalCount: r.PromptEvalCount,
|
||||||
PromptEvalDuration: r.PromptEvalDuration,
|
PromptEvalCachedCount: r.PromptEvalCachedCount,
|
||||||
EvalCount: r.EvalCount,
|
PromptEvalDuration: r.PromptEvalDuration,
|
||||||
EvalDuration: r.EvalDuration,
|
EvalCount: r.EvalCount,
|
||||||
|
EvalDuration: r.EvalDuration,
|
||||||
},
|
},
|
||||||
Logprobs: toAPILogprobs(r.Logprobs),
|
Logprobs: toAPILogprobs(r.Logprobs),
|
||||||
}
|
}
|
||||||
@@ -3022,10 +3024,11 @@ func (s *Server) handleNativeChat(c *gin.Context, req api.ChatRequest, m *Model,
|
|||||||
Message: r.Message,
|
Message: r.Message,
|
||||||
Done: r.Done,
|
Done: r.Done,
|
||||||
Metrics: api.Metrics{
|
Metrics: api.Metrics{
|
||||||
PromptEvalCount: r.PromptEvalCount,
|
PromptEvalCount: r.PromptEvalCount,
|
||||||
PromptEvalDuration: r.PromptEvalDuration,
|
PromptEvalCachedCount: r.PromptEvalCachedCount,
|
||||||
EvalCount: r.EvalCount,
|
PromptEvalDuration: r.PromptEvalDuration,
|
||||||
EvalDuration: r.EvalDuration,
|
EvalCount: r.EvalCount,
|
||||||
|
EvalDuration: r.EvalDuration,
|
||||||
},
|
},
|
||||||
Logprobs: toAPILogprobs(r.Logprobs),
|
Logprobs: toAPILogprobs(r.Logprobs),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -754,12 +754,13 @@ func TestGenerateChat(t *testing.T) {
|
|||||||
|
|
||||||
mock := mockRunner{
|
mock := mockRunner{
|
||||||
CompletionResponse: llm.CompletionResponse{
|
CompletionResponse: llm.CompletionResponse{
|
||||||
Done: true,
|
Done: true,
|
||||||
DoneReason: llm.DoneReasonStop,
|
DoneReason: llm.DoneReasonStop,
|
||||||
PromptEvalCount: 1,
|
PromptEvalCount: 1,
|
||||||
PromptEvalDuration: 1,
|
PromptEvalCachedCount: 1,
|
||||||
EvalCount: 1,
|
PromptEvalDuration: 1,
|
||||||
EvalDuration: 1,
|
EvalCount: 1,
|
||||||
|
EvalDuration: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,6 +973,10 @@ func TestGenerateChat(t *testing.T) {
|
|||||||
t.Errorf("expected prompt eval count > 0, got 0")
|
t.Errorf("expected prompt eval count > 0, got 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if actual.PromptEvalCachedCount != 1 {
|
||||||
|
t.Errorf("expected prompt eval cached count 1, got %d", actual.PromptEvalCachedCount)
|
||||||
|
}
|
||||||
|
|
||||||
if actual.PromptEvalDuration == 0 {
|
if actual.PromptEvalDuration == 0 {
|
||||||
t.Errorf("expected prompt eval duration > 0, got 0")
|
t.Errorf("expected prompt eval duration > 0, got 0")
|
||||||
}
|
}
|
||||||
@@ -1471,12 +1476,13 @@ func TestGenerate(t *testing.T) {
|
|||||||
|
|
||||||
mock := mockRunner{
|
mock := mockRunner{
|
||||||
CompletionResponse: llm.CompletionResponse{
|
CompletionResponse: llm.CompletionResponse{
|
||||||
Done: true,
|
Done: true,
|
||||||
DoneReason: llm.DoneReasonStop,
|
DoneReason: llm.DoneReasonStop,
|
||||||
PromptEvalCount: 1,
|
PromptEvalCount: 1,
|
||||||
PromptEvalDuration: 1,
|
PromptEvalCachedCount: 1,
|
||||||
EvalCount: 1,
|
PromptEvalDuration: 1,
|
||||||
EvalDuration: 1,
|
EvalCount: 1,
|
||||||
|
EvalDuration: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1669,6 +1675,10 @@ func TestGenerate(t *testing.T) {
|
|||||||
t.Errorf("expected prompt eval count > 0, got 0")
|
t.Errorf("expected prompt eval count > 0, got 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if actual.PromptEvalCachedCount != 1 {
|
||||||
|
t.Errorf("expected prompt eval cached count 1, got %d", actual.PromptEvalCachedCount)
|
||||||
|
}
|
||||||
|
|
||||||
if actual.PromptEvalDuration == 0 {
|
if actual.PromptEvalDuration == 0 {
|
||||||
t.Errorf("expected prompt eval duration > 0, got 0")
|
t.Errorf("expected prompt eval duration > 0, got 0")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,10 +107,11 @@ type CompletionResponse struct {
|
|||||||
Done bool
|
Done bool
|
||||||
DoneReason int
|
DoneReason int
|
||||||
|
|
||||||
PromptEvalCount int
|
PromptEvalCount int
|
||||||
PromptEvalDuration time.Duration
|
PromptEvalCachedCount int
|
||||||
EvalCount int
|
PromptEvalDuration time.Duration
|
||||||
EvalDuration time.Duration
|
EvalCount int
|
||||||
|
EvalDuration time.Duration
|
||||||
|
|
||||||
Logprobs []llm.Logprob
|
Logprobs []llm.Logprob
|
||||||
|
|
||||||
@@ -186,14 +187,15 @@ func (c *Client) Completion(ctx context.Context, req llm.CompletionRequest, fn f
|
|||||||
}
|
}
|
||||||
|
|
||||||
cresp := llm.CompletionResponse{
|
cresp := llm.CompletionResponse{
|
||||||
Content: raw.Content,
|
Content: raw.Content,
|
||||||
Done: raw.Done,
|
Done: raw.Done,
|
||||||
DoneReason: llm.DoneReason(raw.DoneReason),
|
DoneReason: llm.DoneReason(raw.DoneReason),
|
||||||
PromptEvalCount: raw.PromptEvalCount,
|
PromptEvalCount: raw.PromptEvalCount,
|
||||||
PromptEvalDuration: raw.PromptEvalDuration,
|
PromptEvalCachedCount: raw.PromptEvalCachedCount,
|
||||||
EvalCount: raw.EvalCount,
|
PromptEvalDuration: raw.PromptEvalDuration,
|
||||||
EvalDuration: raw.EvalDuration,
|
EvalCount: raw.EvalCount,
|
||||||
Logprobs: raw.Logprobs,
|
EvalDuration: raw.EvalDuration,
|
||||||
|
Logprobs: raw.Logprobs,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn(cresp)
|
fn(cresp)
|
||||||
|
|||||||
@@ -189,7 +189,12 @@ func (r *Runner) decode(ctx context.Context, request Request, session *cacheSess
|
|||||||
wantTopLogprobs: request.SamplerOpts.TopLogprobs,
|
wantTopLogprobs: request.SamplerOpts.TopLogprobs,
|
||||||
}
|
}
|
||||||
|
|
||||||
final := CompletionResponse{Done: true, PromptEvalCount: len(request.Tokens), DoneReason: 1}
|
final := CompletionResponse{
|
||||||
|
Done: true,
|
||||||
|
PromptEvalCount: len(request.Tokens),
|
||||||
|
PromptEvalCachedCount: len(session.inputs) - len(session.remaining),
|
||||||
|
DoneReason: 1,
|
||||||
|
}
|
||||||
final.PromptEvalDuration = promptEval
|
final.PromptEvalDuration = promptEval
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user