mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 20:17:20 +00:00
fix(providers): parse Codex cached token usage (#1309)
This commit is contained in:
1 parent
8963ec3177
commit
b8726ff4f4
5 files changed
+94
-38
No files matched your search
@@ -128,6 +128,33 @@ func (a *CodexAdapter) FromStreamChunk(data []byte) (*StreamChunk, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func usageFromCodexUsage(u *codexUsage) *Usage {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
usage := &Usage{
|
||||
PromptTokens: u.InputTokens,
|
||||
CompletionTokens: u.OutputTokens,
|
||||
TotalTokens: u.TotalTokens,
|
||||
}
|
||||
if u.InputTokensDetails != nil {
|
||||
usage.CacheReadTokens = u.InputTokensDetails.CachedTokens
|
||||
usage.PromptTokensIncludeCachedSegments = true
|
||||
}
|
||||
if u.PromptTokensDetails != nil {
|
||||
// Some Responses API payloads use the older Chat Completions naming.
|
||||
// Prefer the larger value if both aliases are present.
|
||||
if u.PromptTokensDetails.CachedTokens > usage.CacheReadTokens {
|
||||
usage.CacheReadTokens = u.PromptTokensDetails.CachedTokens
|
||||
}
|
||||
usage.PromptTokensIncludeCachedSegments = true
|
||||
}
|
||||
if u.OutputTokensDetails != nil {
|
||||
usage.ThinkingTokens = u.OutputTokensDetails.ReasoningTokens
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
// parseCodexAPIResponse converts a Codex API response to internal ChatResponse.
|
||||
// Simple concatenation — does not handle phase-aware ordering like the streaming
|
||||
// path (codex_stream_state.go). Sufficient for Pipeline non-streaming use.
|
||||
@@ -169,16 +196,7 @@ func parseCodexAPIResponse(resp *codexAPIResponse) *ChatResponse {
|
||||
result.FinishReason = "length"
|
||||
}
|
||||
|
||||
if resp.Usage != nil {
|
||||
result.Usage = &Usage{
|
||||
PromptTokens: resp.Usage.InputTokens,
|
||||
CompletionTokens: resp.Usage.OutputTokens,
|
||||
TotalTokens: resp.Usage.TotalTokens,
|
||||
}
|
||||
if resp.Usage.OutputTokensDetails != nil {
|
||||
result.Usage.ThinkingTokens = resp.Usage.OutputTokensDetails.ReasoningTokens
|
||||
}
|
||||
}
|
||||
result.Usage = usageFromCodexUsage(resp.Usage)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -151,6 +151,7 @@ func TestCodexAdapter_FromResponse_TextAndUsage(t *testing.T) {
|
||||
"input_tokens":10,
|
||||
"output_tokens":3,
|
||||
"total_tokens":13,
|
||||
"input_tokens_details":{"cached_tokens":7},
|
||||
"output_tokens_details":{"reasoning_tokens":2}
|
||||
}
|
||||
}`)
|
||||
@@ -170,6 +171,12 @@ func TestCodexAdapter_FromResponse_TextAndUsage(t *testing.T) {
|
||||
if resp.Usage == nil || resp.Usage.ThinkingTokens != 2 {
|
||||
t.Errorf("Usage ThinkingTokens = %+v, want 2", resp.Usage)
|
||||
}
|
||||
if resp.Usage == nil || resp.Usage.CacheReadTokens != 7 {
|
||||
t.Errorf("Usage CacheReadTokens = %+v, want 7", resp.Usage)
|
||||
}
|
||||
if resp.Usage == nil || !resp.Usage.PromptTokensIncludeCachedSegments {
|
||||
t.Errorf("Usage PromptTokensIncludeCachedSegments = %+v, want true", resp.Usage)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCodexAdapter_FromResponse_FunctionCallAndIncomplete covers two branches:
|
||||
@@ -351,3 +358,31 @@ func TestCodexAdapter_ToRequest_StreamOption(t *testing.T) {
|
||||
|
||||
// ensure the adapter satisfies the interface at compile time.
|
||||
var _ ProviderAdapter = (*CodexAdapter)(nil)
|
||||
|
||||
func TestCodexAdapter_FromResponse_PromptTokensDetailsAlias(t *testing.T) {
|
||||
a, _ := NewCodexAdapter(ProviderConfig{})
|
||||
raw := []byte(`{
|
||||
"id":"resp_cache_alias",
|
||||
"object":"response",
|
||||
"status":"completed",
|
||||
"output":[
|
||||
{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}
|
||||
],
|
||||
"usage":{
|
||||
"input_tokens":100,
|
||||
"output_tokens":5,
|
||||
"total_tokens":105,
|
||||
"prompt_tokens_details":{"cached_tokens":80}
|
||||
}
|
||||
}`)
|
||||
resp, err := a.FromResponse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("FromResponse: %v", err)
|
||||
}
|
||||
if resp.Usage == nil || resp.Usage.CacheReadTokens != 80 {
|
||||
t.Fatalf("CacheReadTokens = %+v, want 80", resp.Usage)
|
||||
}
|
||||
if !resp.Usage.PromptTokensIncludeCachedSegments {
|
||||
t.Fatal("PromptTokensIncludeCachedSegments = false, want true")
|
||||
}
|
||||
}
|
||||
@@ -313,15 +313,7 @@ func (p *CodexProvider) processSSEEvent(event *codexSSEEvent, result *ChatRespon
|
||||
}
|
||||
}
|
||||
if event.Response.Usage != nil {
|
||||
u := event.Response.Usage
|
||||
result.Usage = &Usage{
|
||||
PromptTokens: u.InputTokens,
|
||||
CompletionTokens: u.OutputTokens,
|
||||
TotalTokens: u.TotalTokens,
|
||||
}
|
||||
if u.OutputTokensDetails != nil {
|
||||
result.Usage.ThinkingTokens = u.OutputTokensDetails.ReasoningTokens
|
||||
}
|
||||
result.Usage = usageFromCodexUsage(event.Response.Usage)
|
||||
}
|
||||
if event.Response.Status == "incomplete" {
|
||||
result.FinishReason = "length"
|
||||
|
||||
@@ -305,7 +305,7 @@ func TestCodexProviderChatStream(t *testing.T) {
|
||||
events := []string{
|
||||
`{"type":"response.output_text.delta","delta":"Hello"}`,
|
||||
`{"type":"response.output_text.delta","delta":" world"}`,
|
||||
`{"type":"response.completed","response":{"usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}`,
|
||||
`{"type":"response.completed","response":{"usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7,"input_tokens_details":{"cached_tokens":4}}}}`,
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
@@ -344,6 +344,12 @@ func TestCodexProviderChatStream(t *testing.T) {
|
||||
if result.Usage.TotalTokens != 7 {
|
||||
t.Errorf("TotalTokens = %d, want 7", result.Usage.TotalTokens)
|
||||
}
|
||||
if result.Usage.CacheReadTokens != 4 {
|
||||
t.Errorf("CacheReadTokens = %d, want 4", result.Usage.CacheReadTokens)
|
||||
}
|
||||
if !result.Usage.PromptTokensIncludeCachedSegments {
|
||||
t.Error("PromptTokensIncludeCachedSegments = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexProviderChatFallsBackToOutputTextDone(t *testing.T) {
|
||||
@@ -1122,4 +1128,3 @@ func TestCodexBuildRequestBody_NilFunction_HandlesGracefully(t *testing.T) {
|
||||
t.Errorf("expected function tool 'web_search', got: %v", tool)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,31 +42,37 @@ type codexSummary struct {
|
||||
}
|
||||
|
||||
type codexUsage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
OutputTokensDetails *codexTokensDetails `json:"output_tokens_details,omitempty"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
InputTokensDetails *codexInputTokensDetails `json:"input_tokens_details,omitempty"`
|
||||
PromptTokensDetails *codexInputTokensDetails `json:"prompt_tokens_details,omitempty"`
|
||||
OutputTokensDetails *codexOutputTokensDetails `json:"output_tokens_details,omitempty"`
|
||||
}
|
||||
|
||||
type codexTokensDetails struct {
|
||||
type codexInputTokensDetails struct {
|
||||
CachedTokens int `json:"cached_tokens"`
|
||||
}
|
||||
|
||||
type codexOutputTokensDetails struct {
|
||||
ReasoningTokens int `json:"reasoning_tokens"`
|
||||
}
|
||||
|
||||
// SSE streaming types
|
||||
|
||||
type codexSSEEvent struct {
|
||||
Type string `json:"type"`
|
||||
Delta string `json:"delta,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ItemID string `json:"item_id,omitempty"`
|
||||
OutputIndex int `json:"output_index,omitempty"`
|
||||
ContentIndex int `json:"content_index,omitempty"`
|
||||
Item *codexItem `json:"item,omitempty"`
|
||||
Part *codexContentPart `json:"part,omitempty"`
|
||||
Response *codexAPIResponse `json:"response,omitempty"`
|
||||
OutputFormat string `json:"output_format,omitempty"` // response.image_generation_call.partial_image
|
||||
PartialImageB64 string `json:"partial_image_b64,omitempty"` // response.image_generation_call.partial_image
|
||||
PartialImageIndex int `json:"partial_image_index,omitempty"` // response.image_generation_call.partial_image
|
||||
Type string `json:"type"`
|
||||
Delta string `json:"delta,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ItemID string `json:"item_id,omitempty"`
|
||||
OutputIndex int `json:"output_index,omitempty"`
|
||||
ContentIndex int `json:"content_index,omitempty"`
|
||||
Item *codexItem `json:"item,omitempty"`
|
||||
Part *codexContentPart `json:"part,omitempty"`
|
||||
Response *codexAPIResponse `json:"response,omitempty"`
|
||||
OutputFormat string `json:"output_format,omitempty"` // response.image_generation_call.partial_image
|
||||
PartialImageB64 string `json:"partial_image_b64,omitempty"` // response.image_generation_call.partial_image
|
||||
PartialImageIndex int `json:"partial_image_index,omitempty"` // response.image_generation_call.partial_image
|
||||
}
|
||||
|
||||
type codexToolCallAcc struct {
|
||||
|
||||
Reference in new issue
Block a user