Skip to content

fix(responses): accept scientific created_at timestamps - #6577

Open
AmPlace wants to merge 2 commits into
QuantumNous:mainfrom
AmPlace:fix/responses-scientific-created-at
Open

fix(responses): accept scientific created_at timestamps#6577
AmPlace wants to merge 2 commits into
QuantumNous:mainfrom
AmPlace:fix/responses-scientific-created-at

Conversation

@AmPlace

@AmPlace AmPlace commented Jul 31, 2026

Copy link
Copy Markdown

📝 变更描述 / Description

原生 /v1/responses SSE handler 会先将每个 data: 解析为 ResponsesStreamResponse,再转发给客户端。部分 Grok/xAI Responses 上游在 response.completed.response.created_at 中返回合法的科学计数法 JSON number,例如 1.76848816E9。当前 DTO 使用 int,解析失败后该终止事件被跳过,Codex 最终因没有收到 response.completed 而报告流提前关闭。

本 PR 将 Responses 和 Responses Compaction 的 created_at 改为可同时接受整数及科学计数法数字的 UnixTimestamp,并同步当前 relaykit 转换层中的类型使用。输出仍序列化为整数 Unix 时间戳。

新增测试覆盖:

  • 整数和科学计数法时间戳解析;
  • 拒绝字符串时间戳;
  • 原生 Responses SSE 能完整转发带科学计数法 created_atresponse.completed

这是已关闭 PR #2946 针对当前 relaykit/dto 目录结构的重新适配。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 已根据最小 SSE 复现、当前 DTO 和 handler 调用链整理描述。
  • 非重复提交: 已搜索开放 Issues/PRs;fix: accept scientific-notation numeric created_at in responses stream #2946 已关闭且未合并,当前 main 不含等价修复。
  • Bug fix 说明: 已关联 Issue Codex使用问题 #2267
  • 变更理解: 已验证解析失败会跳过 response.completed,但 scanner 继续到 [DONE]
  • 范围聚焦: 仅修改 Responses 时间戳 DTO、必要转换点及回归测试。
  • 本地验证: 已运行原生 handler、controller、DTO 和转换层测试。
  • 安全合规: 不包含凭据或外部服务信息。

📸 运行证明 / Proof of Work

修复前的最小事件:

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_test","created_at":1.76848816E9,"status":"completed","output":[]}}

稳定产生:

cannot unmarshal number 1.76848816E9 into Go struct field OpenAIResponsesResponse.response.created_at of type int

修复后测试:

$ go test ./relay/channel/openai ./controller -count=1
ok github.com/QuantumNous/new-api/relay/channel/openai
ok github.com/QuantumNous/new-api/controller

$ cd relaykit && go test ./dto ./relayconvert/... -count=1
ok github.com/QuantumNous/new-api/relaykit/dto
ok github.com/QuantumNous/new-api/relaykit/relayconvert
ok github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_chat
ok github.com/QuantumNous/new-api/relaykit/relayconvert/internal/oai_responses

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of response timestamps, including fractional and scientific-notation values.
    • Preserved accurate creation times across standard and streaming responses.
    • Improved compatibility when converting Responses API data to chat completions.
  • Tests

    • Added regression coverage for scientific-notation timestamps in completed streaming responses.
    • Added validation for supported timestamp formats, range limits, and rejection of quoted values.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds UnixTimestamp for numeric and scientific-notation JSON timestamps. OpenAI Responses DTOs and conversion paths use the type. Streaming regression coverage validates scientific-notation created_at handling.

Changes

Timestamp normalization

Layer / File(s) Summary
UnixTimestamp contract
relaykit/dto/values.go, relaykit/dto/openai_compaction.go, relaykit/dto/openai_response.go, relaykit/dto/values_test.go
UnixTimestamp parses numeric timestamps, truncates fractional values, rejects quoted strings, and provides JSON serialization and Int64. Responses DTOs use the type. Tests cover valid and invalid inputs.
Response conversion wiring
relaykit/relayconvert/internal/oai_chat/..., relaykit/relayconvert/internal/oai_responses/...
Response conversion assigns UnixTimestamp values and uses Int64() when producing chat completion timestamps.
Streaming regression coverage
relay/channel/openai/chat_via_responses.go, relay/channel/openai/relay_responses_scientific_test.go
The buffered fallback uses UnixTimestamp. The streaming handler test validates scientific-notation created_at forwarding without stream errors.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

A rabbit checks each timestamp,
As JSON numbers pass.
Scientific notation flows,
Through every streaming path.
Int64 closes the loop.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #2267 by preserving response.completed events when created_at uses scientific notation.
Out of Scope Changes check ✅ Passed All code and test changes support timestamp parsing and complete Responses stream forwarding.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: accepting scientific-notation created_at timestamps in Responses.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/channel/openai/relay_responses_scientific_test.go`:
- Around line 18-21: Update the test setup around gin.SetMode in the affected
test to capture the existing mode with gin.Mode() before switching to
gin.TestMode, then restore that saved mode in the existing t.Cleanup callback
alongside StreamingTimeout.

In `@relaykit/dto/values_test.go`:
- Around line 9-31: Add a MarshalJSON test for UnixTimestamp using the value
1768488160, and assert the serialized output is the exact unquoted integer JSON
representation "1768488160", not scientific notation.

In `@relaykit/dto/values.go`:
- Around line 75-80: Update UnixTimestamp.UnmarshalJSON to avoid Float64
conversion: parse the numeric token using exact decimal arithmetic, truncate
only after parsing, and reject values outside the int64 range before converting
to int64. Add boundary tests covering math.MaxInt64 and the first out-of-range
value.
- Around line 67-85: Update UnixTimestamp.UnmarshalJSON and
UnixTimestamp.MarshalJSON to use the common.Unmarshal and common.Marshal
wrappers instead of the direct json functions, and adjust imports to reference
the common package while retaining json.Number if still required.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d3e6d00-da06-4381-a3be-2a804f4809d2

📥 Commits

Reviewing files that changed from the base of the PR and between df43f80 and cdc16c5.

📒 Files selected for processing (10)
  • relay/channel/openai/chat_via_responses.go
  • relay/channel/openai/relay_responses_scientific_test.go
  • relaykit/dto/openai_compaction.go
  • relaykit/dto/openai_response.go
  • relaykit/dto/values.go
  • relaykit/dto/values_test.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go

Comment thread relay/channel/openai/relay_responses_scientific_test.go Outdated
Comment thread relaykit/dto/values_test.go
Comment thread relaykit/dto/values.go
Comment on lines +67 to +85
var n json.Number
if err := json.Unmarshal(trimmed, &n); err != nil {
return err
}
if i, err := n.Int64(); err == nil {
*t = UnixTimestamp(i)
return nil
}
f, err := n.Float64()
if err != nil {
return err
}
*t = UnixTimestamp(int64(f))
return nil
}

func (t UnixTimestamp) MarshalJSON() ([]byte, error) {
return json.Marshal(int64(t))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -a 'values\.go|json\.go' . | sed 's#^\./##'

echo
echo "== relaykit values outline =="
ast-grep outline relaykit/dto/values.go --view expanded || true

echo
echo "== relaykit values lines =="
cat -n relaykit/dto/values.go | sed -n '1,140p'

echo
echo "== common json outline =="
ast-grep outline common/json.go --view expanded || true

echo
echo "== common json lines =="
cat -n common/json.go | sed -n '1,240p'

echo
echo "== imports usages direct encoding/json Marshal/Unmarshal in common and relaykit dto =="
rg -n "encoding/json|json\.(Marshal|Unmarshal)|common/json" relaykit/dto values.go common -S || true

echo
echo "== package deps references relaykit -> common =="
sed -n '1,80p' relaykit/dto/values.go.py

Repository: QuantumNous/new-api

Length of output: 9968


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search UnixTimestamp usages =="
rg -n "UnixTimestamp|Timestamp" relaykit common . --glob '!**/vendor/**' --glob '!**/.git/**' | head -200

echo
echo "== behavioral probe for JSON.Number unmarshal/Float64 edge cases using standard library semantics =="
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat > "$tmpdir/probe.go" <<'GO'
package main

import (
	"encoding/json"
	"fmt"
	"math"
)

type UnixTimestamp int64

func main() {
	inputs := []string{`4`, `4.9`, `9223372036854775807`, `9223372036854775808`, `9223372036854775807.5`, `9.223372036854776e18`, `1e18`, `-9223372036854775808`, `-9223372036854775809`, `-9.223372036854776e18`}
	for _, s := range inputs {
		var n json.Number
		err := json.Unmarshal([]byte(s), &n)
		if err != nil {
			fmt.Printf("%s -> Unmarshal err: %v\n", s, err)
			continue
		}
		i, err := n.Int64()
		fmt.Printf("%s -> Int64=%v err=%v", s, i, err)
		f, ferr := n.Float64()
		if ferr == nil {
			fmt.Printf(" Float64=%.17g cast=%d", f, int64(f))
		} else {
			fmt.Printf(" Float64 err=%v", ferr)
		}
		fmt.Println()
	}

	fmt.Println("IsNumber float literal parse behavior:")
	parsed := json.Number("9007199254740993").String()
	fmt.Println(parsed, float64Str(parsed), int64FromFloat(parsed))
}

func float64Str(s string) string {
	f, err := json.Number(s).Float64()
	if err != nil {
		return "<error>"
	}
	return fmt.Sprintf("%.17g", f)
}
func int64FromFloat(s string) string {
	f, err := json.Number(s).Float64()
	if err != nil {
		return "n/a"
	}
	return fmt.Sprintf("%d", int64(f))
}
GO
go run "$tmpdir/probe.go"

Repository: QuantumNous/new-api

Length of output: 18449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat > "$tmpdir/probe.go" <<'GO'
package main

import (
	"encoding/json"
	"fmt"
	"math"
)

func main() {
	inputs := []string{
		`4`,
		`4.9`,
		`9223372036854775807`,
		`9223372036854775808`,
		`9223372036854775807.5`,
		`9.223372036854776e18`,
		`1e18`,
		`-9223372036854775808`,
		`-9223372036854775809`,
		`-9.223372036854776e18`,
		`9007199254740993`,
	}
	for _, s := range inputs {
		var n json.Number
		err := json.Unmarshal([]byte(s), &n)
		if err != nil {
			fmt.Printf("%s -> Unmarshal err: %v\n", s, err)
			continue
		}
		i, err := n.Int64()
		fmt.Printf("%s -> Int64=%v err=%v", s, i, err)
		f, ferr := n.Float64()
		if ferr == nil {
			fSign, fMan, fExp := math.Frexp(f)
			fmt.Printf(" Float64=%.17g sign=%g man=%.17g exp=%d cast=%d", f, fSign, fMan, fExp, int64(f))
		} else {
			fmt.Printf(" Float64 err=%v", ferr)
		}
		fmt.Println()
	}

	fmt.Println("IsNumber float literal parse behavior:")
	n := json.Number("9007199254740993")
	s := n.String()
	f, _ := n.Float64()
	fmt.Printf("String=%s Float64=%.0f int64=%d\n", s, f, int64(f))
}
GO
go run "$tmpdir/probe.go"

Repository: QuantumNous/new-api

Length of output: 283


Use the JSON wrappers from common/json.go for UnixTimestamp.

UnixTimestamp.UnmarshalJSON and UnixTimestamp.MarshalJSON call json.Unmarshal and json.Marshal directly. Use the common.Unmarshal / common.Marshal wrappers for this app-level JSON path and adjust the import if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relaykit/dto/values.go` around lines 67 - 85, Update
UnixTimestamp.UnmarshalJSON and UnixTimestamp.MarshalJSON to use the
common.Unmarshal and common.Marshal wrappers instead of the direct json
functions, and adjust imports to reference the common package while retaining
json.Number if still required.

Sources: Coding guidelines, Learnings

Comment thread relaykit/dto/values.go Outdated
@AmPlace
AmPlace marked this pull request as ready for review August 1, 2026 13:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
relay/channel/openai/relay_responses_scientific_test.go (1)

43-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use non-fatal assertions for independent checks.

Keep require.Nil for the API-error guard. Use assert.Contains and assert.Zero for the independent response and stream-status checks. Add the testify/assert import.

As per coding guidelines, new or substantially rewritten Go backend tests must use testify/require for setup and fatal assertions and testify/assert for non-fatal checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/openai/relay_responses_scientific_test.go` around lines 43 -
48, Keep require.Nil as the fatal API-error guard in the test, but replace the
independent response-body checks and stream-status count check with
assert.Contains and assert.Zero. Add the testify/assert import while preserving
the existing assertions and test flow.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@relay/channel/openai/relay_responses_scientific_test.go`:
- Around line 43-48: Keep require.Nil as the fatal API-error guard in the test,
but replace the independent response-body checks and stream-status count check
with assert.Contains and assert.Zero. Add the testify/assert import while
preserving the existing assertions and test flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 27c914cc-d68e-49fc-b5d9-03a54ae3d798

📥 Commits

Reviewing files that changed from the base of the PR and between cdc16c5 and 50150bd.

📒 Files selected for processing (3)
  • relay/channel/openai/relay_responses_scientific_test.go
  • relaykit/dto/values.go
  • relaykit/dto/values_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • relaykit/dto/values_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex使用问题

1 participant