Skip to content

Commit fc2f2cd

Browse files
committed
Refactor ToolReplayCache to support store flags and improve response handling
- Enhanced ToolReplayCache to include store and storeKnown flags in toolReplayEntry and toolReplayRecord. - Updated Get and Put methods to handle new flags, allowing for better control over response storage. - Introduced RememberCompletedResponseWithStore to differentiate between stored and stateless responses. - Modified server response handling to forward store flag and manage previous response IDs appropriately. - Added comprehensive tests for new functionality, including concurrent access and edge cases for tool replay. - Improved error handling in server responses and request validation to ensure precise feedback on invalid inputs.
1 parent aa83459 commit fc2f2cd

12 files changed

Lines changed: 1171 additions & 176 deletions

‎README.md‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ flowchart LR
7878

7979
`POST /v1/responses` 默认按 OpenAI Responses API 处理:请求字段会按 Grok CLI 0.2.99 的 Responses 类型清洗,非流式响应与 SSE 事件会移除 Grok 原生扩展。只有请求包含明确的 Grok CLI 标识时才启用原生透传,包括 `X-XAI-Token-Auth: xai-grok-cli`、`x-grok-client-version`、已知的 Grok 客户端名称/标识,或 `grok-cli/`、`grok-shell/`、`grok-pager/` 等 User-Agent。任意或未知的 `x-grok-client-*` 值不会触发原生格式。
8080

81+
兼容分支未指定 `store` 时按 OpenAI 默认值使用 `store: true`。后续请求可通过 `previous_response_id` 续接,并会固定回到生成该 Response 的上游账号;同一 Response ID 可派生多个独立分支。该账号亲和与工具回放状态仅保存在当前服务进程内,重启后不会恢复。显式 `store: false` 时,普通 previous ID 会由上游返回 not-found;函数工具调用可在缓存仍有效时使用最小调用结构本地续接,服务不会额外缓存用户文本或图片。
82+
83+
兼容范围包括文本、多轮、Function/custom/namespace/tool-search、并行工具结果、Web Search、HTTPS 或 Base64 data URI 图片、`text.format.json_schema` 以及 typed SSE。错误请求返回 `400 invalid_request_error` 和准确的 `param` 路径,请求体超过 16 MiB 返回 `413`。当前不提供 `/v1/conversations`、`/v1/files`、音频、后台任务轮询、图片生成输出,以及 Web Search 之外的托管工具执行;相关参数会返回明确的 `unsupported_parameter`,不会静默忽略。
84+
8185
`POST /v1/messages` 仍以 Anthropic 格式对外,并通过上游 Responses API 执行。Anthropic `metadata.user_id` 会映射为 Responses 的 `safety_identifier`;`stop_sequences` 由兼容层在非流式与流式响应中执行。文本、图片、图片型工具结果、并行函数工具及 Web Search server-tool 结果均会转换为对应的 Anthropic 内容块;只有请求显式启用 `thinking` 时才返回 thinking/signature 块。格式错误或断裂的 `tool_use` / `tool_result` 关系会在本地返回 Anthropic `400 invalid_request_error`。
8286

8387
## 快速开始
@@ -230,6 +234,47 @@ curl http://localhost:8088/v1/responses \
230234
}'
231235
```
232236

237+
默认的多轮续接只需保存上一轮 ID:
238+
239+
```bash
240+
FIRST_ID=$(curl -s http://localhost:8088/v1/responses \
241+
-H "Content-Type: application/json" \
242+
-H "Authorization: Bearer sk-kfcvivo50" \
243+
-d '{"model":"grok-4.5","input":"Remember that my code is 7319."}' | jq -r .id)
244+
245+
curl http://localhost:8088/v1/responses \
246+
-H "Content-Type: application/json" \
247+
-H "Authorization: Bearer sk-kfcvivo50" \
248+
-d "{\"model\":\"grok-4.5\",\"previous_response_id\":\"$FIRST_ID\",\"input\":\"What is my code?\"}"
249+
```
250+
251+
图片可使用 HTTPS URL 或 Base64 data URI,并保持文本与多图的原始顺序:
252+
253+
```json
254+
{
255+
"model": "grok-4.5",
256+
"input": [{
257+
"type": "message",
258+
"role": "user",
259+
"content": [
260+
{"type": "input_text", "text": "Describe this image."},
261+
{"type": "input_image", "image_url": "https://example.com/image.png", "detail": "high"}
262+
]
263+
}]
264+
}
265+
```
266+
267+
Function 工具结果使用首轮返回的 `call_id`,并将首轮 Response ID 放入 `previous_response_id`:
268+
269+
```json
270+
{
271+
"model": "grok-4.5",
272+
"previous_response_id": "resp_...",
273+
"input": [{"type": "function_call_output", "call_id": "call_...", "output": "sunny, 26 C"}],
274+
"tools": [{"type": "function", "name": "get_weather", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}]
275+
}
276+
```
277+
233278
### Anthropic Messages
234279

235280
```bash
@@ -258,8 +303,8 @@ X-Grok-Session-ID: conversation-123
258303

259304
服务也会依次识别以下字段作为亲和标识:
260305

261-
- OpenAI `prompt_cache_key`
262306
- OpenAI `previous_response_id`
307+
- OpenAI `prompt_cache_key`
263308
- OpenAI `user`
264309
- Anthropic `metadata.user_id`
265310

@@ -352,6 +397,13 @@ curl -X DELETE http://localhost:8088/v1/admin/credentials/<credential-id> \
352397

353398
未设置 `GROK_PROXY_URL` 时,程序遵循标准的 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 与 `NO_PROXY` 环境变量。
354399

400+
例如通过本机 HTTP 代理 `7890` 访问上游:
401+
402+
```dotenv
403+
GROK_PROXY_URL=http://127.0.0.1:7890
404+
GROK_NO_PROXY=localhost,127.0.0.1
405+
```
406+
355407
命令行参数 `-host` 和 `-port` 可覆盖对应环境变量,`-version` 用于输出当前版本:
356408

357409
```bash

‎README_EN.md‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ Malformed content or broken message/tool-call relationships return an OpenAI-sha
7878

7979
`POST /v1/responses` uses the OpenAI Responses API format by default. Request fields are sanitized against the Grok CLI 0.2.99 Responses types, while Grok-native extensions are removed from non-streaming responses and SSE events. Native passthrough is enabled only for an explicit Grok CLI identity: `X-XAI-Token-Auth: xai-grok-cli`, `x-grok-client-version`, a recognized Grok client name/identifier, or a `grok-cli/`, `grok-shell/`, or `grok-pager/` User-Agent. Arbitrary or unknown `x-grok-client-*` values do not select the native format.
8080

81+
When `store` is omitted, the compatibility branch follows the OpenAI default and sends `store: true`. Later requests can continue with `previous_response_id` and are pinned to the upstream account that created the Response; one Response ID may be used for multiple independent branches. Account affinity and tool-replay state are in-memory and do not survive a service restart. With explicit `store: false`, an ordinary previous ID produces the upstream not-found error. Function-tool continuations can still use a cached minimal call shape while the cache entry is alive; user text and images are not additionally cached.
82+
83+
The supported surface includes text, multi-turn state, Function/custom/namespace/tool-search tools, parallel tool results, Web Search, HTTPS or Base64 data URI images, `text.format.json_schema`, and typed SSE. Invalid requests return `400 invalid_request_error` with an exact `param` path; bodies over 16 MiB return `413`. The proxy does not currently provide `/v1/conversations`, `/v1/files`, audio, background-task polling, image-generation output, or managed tool execution other than Web Search. Those parameters return an explicit `unsupported_parameter` instead of being silently dropped.
84+
8185
`POST /v1/messages` remains Anthropic-shaped externally and executes through the upstream Responses API. Anthropic `metadata.user_id` maps to the Responses `safety_identifier`; `stop_sequences`, which the Responses request cannot represent, is not forwarded and produces a compatibility warning.
8286

8387
## Quick Start
@@ -230,6 +234,47 @@ curl http://localhost:8088/v1/responses \
230234
}'
231235
```
232236

237+
For a default stateful conversation, retain only the previous response ID:
238+
239+
```bash
240+
FIRST_ID=$(curl -s http://localhost:8088/v1/responses \
241+
-H "Content-Type: application/json" \
242+
-H "Authorization: Bearer sk-kfcvivo50" \
243+
-d '{"model":"grok-4.5","input":"Remember that my code is 7319."}' | jq -r .id)
244+
245+
curl http://localhost:8088/v1/responses \
246+
-H "Content-Type: application/json" \
247+
-H "Authorization: Bearer sk-kfcvivo50" \
248+
-d "{\"model\":\"grok-4.5\",\"previous_response_id\":\"$FIRST_ID\",\"input\":\"What is my code?\"}"
249+
```
250+
251+
Images can use an HTTPS URL or a Base64 data URI while preserving mixed text/image order:
252+
253+
```json
254+
{
255+
"model": "grok-4.5",
256+
"input": [{
257+
"type": "message",
258+
"role": "user",
259+
"content": [
260+
{"type": "input_text", "text": "Describe this image."},
261+
{"type": "input_image", "image_url": "https://example.com/image.png", "detail": "high"}
262+
]
263+
}]
264+
}
265+
```
266+
267+
For a Function result, use the `call_id` returned by the first turn and pass that turn's Response ID as `previous_response_id`:
268+
269+
```json
270+
{
271+
"model": "grok-4.5",
272+
"previous_response_id": "resp_...",
273+
"input": [{"type": "function_call_output", "call_id": "call_...", "output": "sunny, 26 C"}],
274+
"tools": [{"type": "function", "name": "get_weather", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}]
275+
}
276+
```
277+
233278
### Anthropic Messages
234279

235280
```bash
@@ -258,8 +303,8 @@ X-Grok-Session-ID: conversation-123
258303

259304
The service also recognizes the following fields as affinity identifiers, in order:
260305

261-
- OpenAI `prompt_cache_key`
262306
- OpenAI `previous_response_id`
307+
- OpenAI `prompt_cache_key`
263308
- OpenAI `user`
264309
- Anthropic `metadata.user_id`
265310

@@ -352,6 +397,13 @@ Free-model quota cooldowns are isolated by account and model. An exhausted spend
352397

353398
When `GROK_PROXY_URL` is unset, the service honors the standard `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY` environment variables.
354399

400+
For example, to use a local HTTP proxy on port `7890`:
401+
402+
```dotenv
403+
GROK_PROXY_URL=http://127.0.0.1:7890
404+
GROK_NO_PROXY=localhost,127.0.0.1
405+
```
406+
355407
The `-host` and `-port` command-line flags override the corresponding environment variables. Use `-version` to print the current version:
356408

357409
```bash

‎internal/grok/client.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type APIError struct {
4646
RequestID string
4747
UpstreamCode string
4848
UpstreamMessage string
49+
UpstreamParam string
4950
RetryAfter time.Duration
5051
}
5152

@@ -907,6 +908,10 @@ func parseAPIError(resp *http.Response, body []byte) *APIError {
907908
if e.UpstreamMessage == "" {
908909
e.UpstreamMessage, _ = inner["message"].(string)
909910
}
911+
e.UpstreamParam, _ = inner["param"].(string)
912+
}
913+
if e.UpstreamParam == "" {
914+
e.UpstreamParam, _ = parsed["param"].(string)
910915
}
911916
if e.UpstreamMessage == "" {
912917
e.UpstreamMessage, _ = parsed["message"].(string)

‎internal/grok/client_test.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ func TestPermanentAccountDenialDetection(t *testing.T) {
7979
}
8080
}
8181

82+
func TestParseAPIErrorPreservesParameter(t *testing.T) {
83+
response := &http.Response{StatusCode: http.StatusBadRequest, Header: http.Header{}}
84+
err := parseAPIError(response, []byte(`{"error":{"type":"invalid_request_error","code":"invalid_value","message":"bad","param":"input[0]"}}`))
85+
if err.UpstreamCode != "invalid_value" || err.UpstreamMessage != "bad" || err.UpstreamParam != "input[0]" {
86+
t.Fatalf("parsed error = %#v", err)
87+
}
88+
}
89+
8290
func TestFreeModelQuotaDetection(t *testing.T) {
8391
tests := []struct {
8492
name string

0 commit comments

Comments
 (0)