From d21fb74f296076a2991e90b35ca9ffe8ea4cd9a8 Mon Sep 17 00:00:00 2001 From: CJACK Date: Tue, 17 Feb 2026 12:57:01 +0800 Subject: [PATCH 1/5] fix: Prevent partial tool call JSON leaks in stream processing by removing size-based buffer limits and holding incomplete blocks longer. --- api/helpers/stream-tool-sieve.js | 18 ++----- .../adapter/openai/handler_toolcall_test.go | 47 +++++++++++++++++++ internal/adapter/openai/tool_sieve.go | 18 ++----- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/api/helpers/stream-tool-sieve.js b/api/helpers/stream-tool-sieve.js index 07d8cad..0643ce5 100644 --- a/api/helpers/stream-tool-sieve.js +++ b/api/helpers/stream-tool-sieve.js @@ -129,12 +129,9 @@ function splitSafeContentForToolDetection(s) { if (suspiciousStart > 0) { return [text.slice(0, suspiciousStart), text.slice(suspiciousStart)]; } - const chars = Array.from(text); - const maxHold = 128; - if (chars.length <= maxHold) { - return ['', text]; - } - return [chars.slice(0, chars.length - maxHold).join(''), chars.slice(chars.length - maxHold).join('')]; + // If suspicious content starts at the beginning, keep holding until we can + // either parse a full tool JSON block or reach stream flush. + return ['', text]; } function findSuspiciousPrefixStart(s) { @@ -168,23 +165,14 @@ function consumeToolCapture(captured, toolNames) { const lower = captured.toLowerCase(); const keyIdx = lower.indexOf('tool_calls'); if (keyIdx < 0) { - if (Array.from(captured).length >= 256) { - return { ready: true, prefix: captured, calls: [], suffix: '' }; - } return { ready: false, prefix: '', calls: [], suffix: '' }; } const start = captured.slice(0, keyIdx).lastIndexOf('{'); if (start < 0) { - if (Array.from(captured).length >= 512) { - return { ready: true, prefix: captured, calls: [], suffix: '' }; - } return { ready: false, prefix: '', calls: [], suffix: '' }; } const obj = extractJSONObjectFrom(captured, start); if (!obj.ok) { - if (Array.from(captured).length >= 4096) { - return { ready: true, prefix: captured, calls: [], suffix: '' }; - } return { ready: false, prefix: '', calls: [], suffix: '' }; } const parsed = parseToolCalls(captured.slice(start, obj.end), toolNames); diff --git a/internal/adapter/openai/handler_toolcall_test.go b/internal/adapter/openai/handler_toolcall_test.go index e3cfc7d..9089f69 100644 --- a/internal/adapter/openai/handler_toolcall_test.go +++ b/internal/adapter/openai/handler_toolcall_test.go @@ -416,3 +416,50 @@ func TestHandleStreamToolCallMixedWithPlainTextSegments(t *testing.T) { t.Fatalf("expected finish_reason=tool_calls, body=%s", rec.Body.String()) } } + +func TestHandleStreamToolCallKeyAppearsLateStillNoPrefixLeak(t *testing.T) { + h := &Handler{} + spaces := strings.Repeat(" ", 200) + resp := makeSSEHTTPResponse( + `data: {"p":"response/content","v":"{`+spaces+`"}`, + `data: {"p":"response/content","v":"\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"go\"}}]}"}`, + `data: {"p":"response/content","v":"后置正文C。"}`, + `data: [DONE]`, + ) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil) + + h.handleStream(rec, req, resp, "cid8", "deepseek-chat", "prompt", false, false, []string{"search"}) + + frames, done := parseSSEDataFrames(t, rec.Body.String()) + if !done { + t.Fatalf("expected [DONE], body=%s", rec.Body.String()) + } + if !streamHasToolCallsDelta(frames) { + t.Fatalf("expected tool_calls delta, body=%s", rec.Body.String()) + } + if streamHasRawToolJSONContent(frames) { + t.Fatalf("raw tool_calls JSON leaked in content delta: %s", rec.Body.String()) + } + content := strings.Builder{} + for _, frame := range frames { + choices, _ := frame["choices"].([]any) + for _, item := range choices { + choice, _ := item.(map[string]any) + delta, _ := choice["delta"].(map[string]any) + if c, ok := delta["content"].(string); ok { + content.WriteString(c) + } + } + } + got := content.String() + if strings.Contains(got, "{") { + t.Fatalf("unexpected suspicious prefix leak in content: %q", got) + } + if !strings.Contains(got, "后置正文C。") { + t.Fatalf("expected stream to continue after tool json convergence, got=%q", got) + } + if streamFinishReason(frames) != "tool_calls" { + t.Fatalf("expected finish_reason=tool_calls, body=%s", rec.Body.String()) + } +} diff --git a/internal/adapter/openai/tool_sieve.go b/internal/adapter/openai/tool_sieve.go index 3fd7262..6790840 100644 --- a/internal/adapter/openai/tool_sieve.go +++ b/internal/adapter/openai/tool_sieve.go @@ -122,12 +122,9 @@ func splitSafeContentForToolDetection(s string) (safe, hold string) { if suspiciousStart > 0 { return s[:suspiciousStart], s[suspiciousStart:] } - runes := []rune(s) - const maxHold = 128 - if len(runes) <= maxHold { - return "", s - } - return string(runes[:len(runes)-maxHold]), string(runes[len(runes)-maxHold:]) + // If suspicious content starts at position 0, keep holding until we can + // parse a complete tool JSON block or reach stream flush. + return "", s } func findSuspiciousPrefixStart(s string) int { @@ -167,23 +164,14 @@ func consumeToolCapture(captured string, toolNames []string) (prefix string, cal lower := strings.ToLower(captured) keyIdx := strings.Index(lower, "tool_calls") if keyIdx < 0 { - if len([]rune(captured)) >= 256 { - return captured, nil, "", true - } return "", nil, "", false } start := strings.LastIndex(captured[:keyIdx], "{") if start < 0 { - if len([]rune(captured)) >= 512 { - return captured, nil, "", true - } return "", nil, "", false } obj, end, ok := extractJSONObjectFrom(captured, start) if !ok { - if len([]rune(captured)) >= 4096 { - return captured, nil, "", true - } return "", nil, "", false } parsed := util.ParseToolCalls(obj, toolNames) From 6697d0d227105236b68cdd4395227be20b8cd98f Mon Sep 17 00:00:00 2001 From: CJACK Date: Tue, 17 Feb 2026 13:18:52 +0800 Subject: [PATCH 2/5] feat: enhance tool call streaming and anti-leakage by suppressing invalid or incomplete tool JSON and refining detection in Node.js. --- DEPLOY.en.md | 7 +- DEPLOY.md | 7 +- README.MD | 2 +- README.en.md | 2 +- api/chat-stream.js | 20 ++--- api/helpers/stream-tool-sieve.js | 55 +++++++++++--- .../adapter/openai/handler_toolcall_test.go | 74 +++++++++++++++++++ internal/adapter/openai/tool_sieve.go | 9 +-- 8 files changed, 143 insertions(+), 33 deletions(-) diff --git a/DEPLOY.en.md b/DEPLOY.en.md index e69754a..ffdbf50 100644 --- a/DEPLOY.en.md +++ b/DEPLOY.en.md @@ -211,14 +211,15 @@ Vercel Go Runtime applies platform-level response buffering, so this project use 1. `api/chat-stream.js` receives `/v1/chat/completions` request 2. Node calls Go internal prepare endpoint (`?__stream_prepare=1`) for session ID, PoW, token 3. Go prepare creates a stream lease, locking the account -4. Node connects directly to DeepSeek upstream, relays SSE in real-time to client +4. Node connects directly to DeepSeek upstream, relays SSE in real-time to client (including OpenAI chunk framing and tools anti-leak sieve) 5. After stream ends, Node calls Go release endpoint (`?__stream_release=1`) to free the account > This adaptation is **Vercel-only**; local and Docker remain pure Go. -#### Non-Stream and Tool Call Fallback +#### Non-Stream Fallback and Tool Call Handling -- `api/chat-stream.js` automatically falls back to Go entry (`?__go=1`) for non-stream requests or requests with `tools` +- `api/chat-stream.js` falls back to Go entry (`?__go=1`) for non-stream requests only +- Streaming requests (including requests with `tools`) stay on the Node path and use Go-aligned tool-call anti-leak handling - WebUI non-stream test calls `?__go=1` directly to avoid Node hop timeout on long requests #### Function Duration diff --git a/DEPLOY.md b/DEPLOY.md index cf008cd..e3c6f80 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -211,14 +211,15 @@ api/index.go api/chat-stream.js 1. `api/chat-stream.js` 收到 `/v1/chat/completions` 请求 2. Node 调用 Go 内部 prepare 接口(`?__stream_prepare=1`),获取会话 ID、PoW、token 等 3. Go prepare 创建 stream lease,锁定账号 -4. Node 直连 DeepSeek 上游,实时流式转发 SSE 给客户端 +4. Node 直连 DeepSeek 上游,实时流式转发 SSE 给客户端(含 OpenAI chunk 封装与 tools 防泄漏筛分) 5. 流结束后 Node 调用 Go release 接口(`?__stream_release=1`),释放账号 > 该适配**仅在 Vercel 环境生效**;本地与 Docker 仍走纯 Go 链路。 -#### 非流式与 Tool Call 回退 +#### 非流式回退与 Tool Call 处理 -- `api/chat-stream.js` 对非流式请求或带 `tools` 的请求会自动回退到 Go 入口(`?__go=1`) +- `api/chat-stream.js` 仅对非流式请求回退到 Go 入口(`?__go=1`) +- 流式请求(包括带 `tools`)走 Node 路径,并执行与 Go 对齐的 tool-call 防泄漏处理 - WebUI 的"非流式测试"直接请求 `?__go=1`,避免 Node 中转造成长请求超时 #### 函数时长 diff --git a/README.MD b/README.MD index f94ee54..ebcf943 100644 --- a/README.MD +++ b/README.MD @@ -131,7 +131,7 @@ docker-compose logs -f 3. 配置环境变量(至少设置 `DS2API_ADMIN_KEY` 和 `DS2API_CONFIG_JSON`) 4. 部署 -> **流式说明**:`/v1/chat/completions` 在 Vercel 上默认走 `api/chat-stream.js`(Node Runtime)以保证实时 SSE。鉴权、账号选择、会话/PoW 准备仍由 Go 内部 prepare 接口完成,Node 端仅转发流数据。 +> **流式说明**:`/v1/chat/completions` 在 Vercel 上默认走 `api/chat-stream.js`(Node Runtime)以保证实时 SSE。鉴权、账号选择、会话/PoW 准备仍由 Go 内部 prepare 接口完成;流式响应(含 `tools`)在 Node 侧执行与 Go 对齐的输出组装与防泄漏处理。 详细部署说明请参阅 [部署指南](DEPLOY.md)。 diff --git a/README.en.md b/README.en.md index 304b117..b2c9373 100644 --- a/README.en.md +++ b/README.en.md @@ -131,7 +131,7 @@ Rebuild after updates: `docker-compose up -d --build` 3. Set environment variables (minimum: `DS2API_ADMIN_KEY` and `DS2API_CONFIG_JSON`) 4. Deploy -> **Streaming note**: `/v1/chat/completions` on Vercel is routed to `api/chat-stream.js` (Node Runtime) for real-time SSE. Auth, account selection, session/PoW preparation are still handled by the Go internal prepare endpoint; Node only relays stream data. +> **Streaming note**: `/v1/chat/completions` on Vercel is routed to `api/chat-stream.js` (Node Runtime) for real-time SSE. Auth, account selection, and session/PoW preparation are still handled by the Go internal prepare endpoint; streaming output (including `tools`) is assembled on Node with Go-aligned anti-leak handling. For detailed deployment instructions, see the [Deployment Guide](DEPLOY.en.md). diff --git a/api/chat-stream.js b/api/chat-stream.js index 852f58b..566fa57 100644 --- a/api/chat-stream.js +++ b/api/chat-stream.js @@ -5,6 +5,7 @@ const { createToolSieveState, processToolSieveChunk, flushToolSieve, + parseToolCalls, formatOpenAIStreamToolCalls, } = require('./helpers/stream-tool-sieve'); @@ -155,20 +156,19 @@ module.exports = async function handler(req, res) { return; } ended = true; - if (toolSieveEnabled) { + const detected = parseToolCalls(outputText, toolNames); + if (detected.length > 0 && !toolCallsEmitted) { + toolCallsEmitted = true; + sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(detected) }); + } else if (toolSieveEnabled) { const tailEvents = flushToolSieve(toolSieveState, toolNames); for (const evt of tailEvents) { - if (evt.type === 'tool_calls') { - toolCallsEmitted = true; - sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(evt.calls) }); - continue; - } if (evt.text) { sendDeltaFrame({ content: evt.text }); } } } - if (toolCallsEmitted) { + if (detected.length > 0 || toolCallsEmitted) { reason = 'tool_calls'; } sendFrame({ @@ -233,8 +233,10 @@ module.exports = async function handler(req, res) { continue; } if (p.type === 'thinking') { - thinkingText += p.text; - sendDeltaFrame({ reasoning_content: p.text }); + if (thinkingEnabled) { + thinkingText += p.text; + sendDeltaFrame({ reasoning_content: p.text }); + } } else { outputText += p.text; if (!toolSieveEnabled) { diff --git a/api/helpers/stream-tool-sieve.js b/api/helpers/stream-tool-sieve.js index 0643ce5..83bb265 100644 --- a/api/helpers/stream-tool-sieve.js +++ b/api/helpers/stream-tool-sieve.js @@ -1,6 +1,7 @@ 'use strict'; const crypto = require('crypto'); +const TOOL_CALL_PATTERN = /\{\s*["']tool_calls["']\s*:\s*\[(.*?)\]\s*\}/s; function extractToolNames(tools) { if (!Array.isArray(tools) || tools.length === 0) { @@ -105,7 +106,7 @@ function flushToolSieve(state, toolNames) { events.push({ type: 'text', text: consumed.suffix }); } } else if (state.capture) { - events.push({ type: 'text', text: state.capture }); + // Incomplete captured tool JSON at stream end: suppress raw capture. } state.capture = ''; state.capturing = false; @@ -177,9 +178,11 @@ function consumeToolCapture(captured, toolNames) { } const parsed = parseToolCalls(captured.slice(start, obj.end), toolNames); if (parsed.length === 0) { + // `tool_calls` key exists but strict JSON parse failed. + // Drop the captured object body to avoid leaking raw tool JSON. return { ready: true, - prefix: captured.slice(0, obj.end), + prefix: captured.slice(0, start), calls: [], suffix: captured.slice(obj.end), }; @@ -280,24 +283,53 @@ function buildToolCallCandidates(text) { candidates.push(toStringSafe(m[1])); } } - const keyIdx = trimmed.toLowerCase().indexOf('tool_calls'); - if (keyIdx >= 0) { - const start = trimmed.slice(0, keyIdx).lastIndexOf('{'); - if (start >= 0) { - const obj = extractJSONObjectFrom(trimmed, start); - if (obj.ok) { - candidates.push(toStringSafe(trimmed.slice(start, obj.end))); - } - } + for (const candidate of extractToolCallObjects(trimmed)) { + candidates.push(toStringSafe(candidate)); } const first = trimmed.indexOf('{'); const last = trimmed.lastIndexOf('}'); if (first >= 0 && last > first) { candidates.push(toStringSafe(trimmed.slice(first, last + 1))); } + const m = trimmed.match(TOOL_CALL_PATTERN); + if (m && m[1]) { + candidates.push(`{"tool_calls":[${m[1]}]}`); + } return [...new Set(candidates.filter(Boolean))]; } +function extractToolCallObjects(text) { + const raw = toStringSafe(text); + if (!raw) { + return []; + } + const lower = raw.toLowerCase(); + const out = []; + let offset = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + let idx = lower.indexOf('tool_calls', offset); + if (idx < 0) { + break; + } + let start = raw.slice(0, idx).lastIndexOf('{'); + while (start >= 0) { + const obj = extractJSONObjectFrom(raw, start); + if (obj.ok) { + out.push(raw.slice(start, obj.end).trim()); + offset = obj.end; + idx = -1; + break; + } + start = raw.slice(0, start).lastIndexOf('{'); + } + if (idx >= 0) { + offset = idx + 'tool_calls'.length; + } + } + return out; +} + function parseToolCallsPayload(payload) { let decoded; try { @@ -440,5 +472,6 @@ module.exports = { createToolSieveState, processToolSieveChunk, flushToolSieve, + parseToolCalls, formatOpenAIStreamToolCalls, }; diff --git a/internal/adapter/openai/handler_toolcall_test.go b/internal/adapter/openai/handler_toolcall_test.go index 9089f69..f9c44dd 100644 --- a/internal/adapter/openai/handler_toolcall_test.go +++ b/internal/adapter/openai/handler_toolcall_test.go @@ -463,3 +463,77 @@ func TestHandleStreamToolCallKeyAppearsLateStillNoPrefixLeak(t *testing.T) { t.Fatalf("expected finish_reason=tool_calls, body=%s", rec.Body.String()) } } + +func TestHandleStreamInvalidToolJSONDoesNotLeakRawObject(t *testing.T) { + h := &Handler{} + resp := makeSSEHTTPResponse( + `data: {"p":"response/content","v":"前置正文D。"}`, + `data: {"p":"response/content","v":"{'tool_calls':[{'name':'search','input':{'q':'go'}}]}"}`, + `data: {"p":"response/content","v":"后置正文E。"}`, + `data: [DONE]`, + ) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil) + + h.handleStream(rec, req, resp, "cid9", "deepseek-chat", "prompt", false, false, []string{"search"}) + + frames, done := parseSSEDataFrames(t, rec.Body.String()) + if !done { + t.Fatalf("expected [DONE], body=%s", rec.Body.String()) + } + if streamHasToolCallsDelta(frames) { + t.Fatalf("did not expect tool_calls delta for invalid json, body=%s", rec.Body.String()) + } + content := strings.Builder{} + for _, frame := range frames { + choices, _ := frame["choices"].([]any) + for _, item := range choices { + choice, _ := item.(map[string]any) + delta, _ := choice["delta"].(map[string]any) + if c, ok := delta["content"].(string); ok { + content.WriteString(c) + } + } + } + got := strings.ToLower(content.String()) + if strings.Contains(got, "tool_calls") { + t.Fatalf("unexpected raw tool_calls leak in content: %q", content.String()) + } + if !strings.Contains(content.String(), "前置正文D。") || !strings.Contains(content.String(), "后置正文E。") { + t.Fatalf("expected pre/post plain text to remain, got=%q", content.String()) + } +} + +func TestHandleStreamIncompleteCapturedToolJSONDoesNotLeakOnFinalize(t *testing.T) { + h := &Handler{} + resp := makeSSEHTTPResponse( + `data: {"p":"response/content","v":"{\"tool_calls\":[{\"name\":\"search\""}`, + `data: [DONE]`, + ) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil) + + h.handleStream(rec, req, resp, "cid10", "deepseek-chat", "prompt", false, false, []string{"search"}) + + frames, done := parseSSEDataFrames(t, rec.Body.String()) + if !done { + t.Fatalf("expected [DONE], body=%s", rec.Body.String()) + } + if streamHasToolCallsDelta(frames) { + t.Fatalf("did not expect tool_calls delta for incomplete json, body=%s", rec.Body.String()) + } + content := strings.Builder{} + for _, frame := range frames { + choices, _ := frame["choices"].([]any) + for _, item := range choices { + choice, _ := item.(map[string]any) + delta, _ := choice["delta"].(map[string]any) + if c, ok := delta["content"].(string); ok { + content.WriteString(c) + } + } + } + if strings.Contains(strings.ToLower(content.String()), "tool_calls") || strings.Contains(content.String(), "{") { + t.Fatalf("unexpected incomplete tool json leak in content: %q", content.String()) + } +} diff --git a/internal/adapter/openai/tool_sieve.go b/internal/adapter/openai/tool_sieve.go index 6790840..d1a9014 100644 --- a/internal/adapter/openai/tool_sieve.go +++ b/internal/adapter/openai/tool_sieve.go @@ -96,10 +96,7 @@ func flushToolSieve(state *toolStreamSieveState, toolNames []string) []toolStrea events = append(events, toolStreamEvent{Content: consumedSuffix}) } } else { - raw := state.capture.String() - if raw != "" { - events = append(events, toolStreamEvent{Content: raw}) - } + // Incomplete captured tool JSON at stream end: suppress raw capture. } state.capture.Reset() state.capturing = false @@ -176,7 +173,9 @@ func consumeToolCapture(captured string, toolNames []string) (prefix string, cal } parsed := util.ParseToolCalls(obj, toolNames) if len(parsed) == 0 { - return captured[:end], nil, captured[end:], true + // `tool_calls` key exists but strict JSON parse failed. + // Drop the captured object body to avoid leaking raw tool JSON. + return captured[:start], nil, captured[end:], true } return captured[:start], parsed, captured[end:], true } From 7dcddef91f47f958f57e6ef0b4ffbc511345f842 Mon Sep 17 00:00:00 2001 From: CJACK Date: Tue, 17 Feb 2026 13:36:19 +0800 Subject: [PATCH 3/5] feat: Update Claude model names and IDs across configuration, documentation, and tests, including the default model and thinking delta logic. --- API.en.md | 18 +++++++++--------- API.md | 18 +++++++++--------- README.MD | 6 +++--- README.en.md | 6 +++--- internal/adapter/claude/handler_stream_test.go | 10 +++++----- internal/config/models.go | 10 +++++++--- internal/testsuite/edge_cases.go | 2 +- internal/testsuite/runner.go | 6 +++--- internal/util/messages.go | 2 +- internal/util/messages_test.go | 2 +- 10 files changed, 42 insertions(+), 38 deletions(-) diff --git a/API.en.md b/API.en.md index 1c16cb3..13e39b4 100644 --- a/API.en.md +++ b/API.en.md @@ -246,9 +246,9 @@ No auth required. { "object": "list", "data": [ - {"id": "claude-sonnet-4-20250514", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, - {"id": "claude-sonnet-4-20250514-fast", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, - {"id": "claude-sonnet-4-20250514-slow", "object": "model", "created": 1715635200, "owned_by": "anthropic"} + {"id": "claude-sonnet-4-5", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, + {"id": "claude-3-5-haiku-latest", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, + {"id": "claude-opus-4-6", "object": "model", "created": 1715635200, "owned_by": "anthropic"} ] } ``` @@ -267,7 +267,7 @@ anthropic-version: 2023-06-01 | Field | Type | Required | Notes | | --- | --- | --- | --- | -| `model` | string | ✅ | `claude-sonnet-4-20250514` / `-fast` / `-slow` | +| `model` | string | ✅ | `claude-sonnet-4-5` / `claude-opus-4-6` / `claude-3-5-haiku-latest` | | `messages` | array | ✅ | Claude-style messages | | `max_tokens` | number | ❌ | Not strictly enforced by upstream bridge | | `stream` | boolean | ❌ | Default `false` | @@ -281,7 +281,7 @@ anthropic-version: 2023-06-01 "id": "msg_1738400000000000000", "type": "message", "role": "assistant", - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "content": [ {"type": "text", "text": "response"} ], @@ -325,7 +325,7 @@ data: {"type":"message_stop"} **Notes**: -- Thinking-enabled models (`-slow`) stream `thinking_delta` +- Models whose names contain `opus` / `reasoner` / `slow` stream `thinking_delta` - `signature_delta` is not emitted (DeepSeek does not provide verifiable thinking signatures) - In `tools` mode, the stream avoids leaking raw tool JSON and does not force `input_json_delta` @@ -335,7 +335,7 @@ data: {"type":"message_stop"} ```json { - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "messages": [ {"role": "user", "content": "Hello"} ] @@ -754,7 +754,7 @@ curl http://localhost:5001/anthropic/v1/messages \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}] }' @@ -768,7 +768,7 @@ curl http://localhost:5001/anthropic/v1/messages \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ - "model": "claude-sonnet-4-20250514-slow", + "model": "claude-opus-4-6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Explain relativity"}], "stream": true diff --git a/API.md b/API.md index d173031..95ecbfe 100644 --- a/API.md +++ b/API.md @@ -246,9 +246,9 @@ data: [DONE] { "object": "list", "data": [ - {"id": "claude-sonnet-4-20250514", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, - {"id": "claude-sonnet-4-20250514-fast", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, - {"id": "claude-sonnet-4-20250514-slow", "object": "model", "created": 1715635200, "owned_by": "anthropic"} + {"id": "claude-sonnet-4-5", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, + {"id": "claude-3-5-haiku-latest", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, + {"id": "claude-opus-4-6", "object": "model", "created": 1715635200, "owned_by": "anthropic"} ] } ``` @@ -267,7 +267,7 @@ anthropic-version: 2023-06-01 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | -| `model` | string | ✅ | `claude-sonnet-4-20250514` / `-fast` / `-slow` | +| `model` | string | ✅ | `claude-sonnet-4-5` / `claude-opus-4-6` / `claude-3-5-haiku-latest` | | `messages` | array | ✅ | Claude 风格消息数组 | | `max_tokens` | number | ❌ | 当前实现不会硬性截断上游输出 | | `stream` | boolean | ❌ | 默认 `false` | @@ -281,7 +281,7 @@ anthropic-version: 2023-06-01 "id": "msg_1738400000000000000", "type": "message", "role": "assistant", - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "content": [ {"type": "text", "text": "回复内容"} ], @@ -325,7 +325,7 @@ data: {"type":"message_stop"} **说明**: -- 思维模型(`-slow`)会输出 `thinking_delta` +- 名称中包含 `opus` / `reasoner` / `slow` 的模型会输出 `thinking_delta` - 不会输出 `signature_delta`(上游 DeepSeek 未提供可验证签名) - `tools` 场景优先避免泄露原始工具 JSON,不强制发送 `input_json_delta` @@ -335,7 +335,7 @@ data: {"type":"message_stop"} ```json { - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "messages": [ {"role": "user", "content": "你好"} ] @@ -754,7 +754,7 @@ curl http://localhost:5001/anthropic/v1/messages \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "你好"}] }' @@ -768,7 +768,7 @@ curl http://localhost:5001/anthropic/v1/messages \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ - "model": "claude-sonnet-4-20250514-slow", + "model": "claude-opus-4-6", "max_tokens": 1024, "messages": [{"role": "user", "content": "解释相对论"}], "stream": true diff --git a/README.MD b/README.MD index ebcf943..3182941 100644 --- a/README.MD +++ b/README.MD @@ -79,9 +79,9 @@ flowchart LR | 模型 | 默认映射 | | --- | --- | -| `claude-sonnet-4-20250514` | `deepseek-chat` | -| `claude-sonnet-4-20250514-fast` | `deepseek-chat` | -| `claude-sonnet-4-20250514-slow` | `deepseek-reasoner` | +| `claude-sonnet-4-5` | `deepseek-chat` | +| `claude-3-5-haiku-latest` | `deepseek-chat` | +| `claude-opus-4-6` | `deepseek-reasoner` | 可通过配置中的 `claude_mapping` 或 `claude_model_mapping` 覆盖映射关系。 diff --git a/README.en.md b/README.en.md index b2c9373..555859c 100644 --- a/README.en.md +++ b/README.en.md @@ -79,9 +79,9 @@ flowchart LR | Model | Default Mapping | | --- | --- | -| `claude-sonnet-4-20250514` | `deepseek-chat` | -| `claude-sonnet-4-20250514-fast` | `deepseek-chat` | -| `claude-sonnet-4-20250514-slow` | `deepseek-reasoner` | +| `claude-sonnet-4-5` | `deepseek-chat` | +| `claude-3-5-haiku-latest` | `deepseek-chat` | +| `claude-opus-4-6` | `deepseek-reasoner` | Override mapping via `claude_mapping` or `claude_model_mapping` in config. diff --git a/internal/adapter/claude/handler_stream_test.go b/internal/adapter/claude/handler_stream_test.go index 74086ae..701c8d7 100644 --- a/internal/adapter/claude/handler_stream_test.go +++ b/internal/adapter/claude/handler_stream_test.go @@ -81,7 +81,7 @@ func TestHandleClaudeStreamRealtimeTextIncrementsWithEventHeaders(t *testing.T) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", nil) - h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-20250514", []any{map[string]any{"role": "user", "content": "hi"}}, false, false, nil) + h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-5", []any{map[string]any{"role": "user", "content": "hi"}}, false, false, nil) body := rec.Body.String() if !strings.Contains(body, "event: message_start") { @@ -122,7 +122,7 @@ func TestHandleClaudeStreamRealtimeThinkingDelta(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", nil) - h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-20250514", []any{map[string]any{"role": "user", "content": "hi"}}, true, false, nil) + h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-5", []any{map[string]any{"role": "user", "content": "hi"}}, true, false, nil) frames := parseClaudeFrames(t, rec.Body.String()) foundThinkingDelta := false @@ -148,7 +148,7 @@ func TestHandleClaudeStreamRealtimeToolSafety(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", nil) - h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-20250514", []any{map[string]any{"role": "user", "content": "use tool"}}, false, false, []string{"search"}) + h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-5", []any{map[string]any{"role": "user", "content": "use tool"}}, false, false, []string{"search"}) frames := parseClaudeFrames(t, rec.Body.String()) for _, f := range findClaudeFrames(frames, "content_block_delta") { @@ -191,7 +191,7 @@ func TestHandleClaudeStreamRealtimeUpstreamErrorEvent(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", nil) - h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-20250514", []any{map[string]any{"role": "user", "content": "hi"}}, false, false, nil) + h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-5", []any{map[string]any{"role": "user", "content": "hi"}}, false, false, nil) frames := parseClaudeFrames(t, rec.Body.String()) errFrames := findClaudeFrames(frames, "error") @@ -228,7 +228,7 @@ func TestHandleClaudeStreamRealtimePingEvent(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", nil) - h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-20250514", []any{map[string]any{"role": "user", "content": "hi"}}, false, false, nil) + h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-5", []any{map[string]any{"role": "user", "content": "hi"}}, false, false, nil) frames := parseClaudeFrames(t, rec.Body.String()) if len(findClaudeFrames(frames, "ping")) == 0 { diff --git a/internal/config/models.go b/internal/config/models.go index c6cbd5f..51f6126 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -16,9 +16,13 @@ var DeepSeekModels = []ModelInfo{ } var ClaudeModels = []ModelInfo{ - {ID: "claude-sonnet-4-20250514", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, - {ID: "claude-sonnet-4-20250514-fast", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, - {ID: "claude-sonnet-4-20250514-slow", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-sonnet-4-5", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-sonnet-4-5-20250929", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-opus-4-6", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-opus-4-1", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-opus-4-1-20250805", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-7-sonnet-latest", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-5-haiku-latest", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, } func GetModelConfig(model string) (thinking bool, search bool, ok bool) { diff --git a/internal/testsuite/edge_cases.go b/internal/testsuite/edge_cases.go index 784f8ab..cba0b5a 100644 --- a/internal/testsuite/edge_cases.go +++ b/internal/testsuite/edge_cases.go @@ -275,7 +275,7 @@ func (r *Runner) caseSSEJSONIntegrity(ctx context.Context, cc *caseContext) erro "anthropic-version": "2023-06-01", }, Body: map[string]any{ - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "messages": []map[string]any{ {"role": "user", "content": "stream json integrity"}, }, diff --git a/internal/testsuite/runner.go b/internal/testsuite/runner.go index de8ea00..8af1b7d 100644 --- a/internal/testsuite/runner.go +++ b/internal/testsuite/runner.go @@ -1056,7 +1056,7 @@ func (r *Runner) caseAnthropicNonstream(ctx context.Context, cc *caseContext) er "content-type": "application/json", }, Body: map[string]any{ - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "messages": []map[string]any{ {"role": "user", "content": "hello"}, }, @@ -1084,7 +1084,7 @@ func (r *Runner) caseAnthropicStream(ctx context.Context, cc *caseContext) error "content-type": "application/json", }, Body: map[string]any{ - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "messages": []map[string]any{ {"role": "user", "content": "stream hello"}, }, @@ -1113,7 +1113,7 @@ func (r *Runner) caseAnthropicCountTokens(ctx context.Context, cc *caseContext) "content-type": "application/json", }, Body: map[string]any{ - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-5", "messages": []map[string]any{ {"role": "user", "content": "count me"}, }, diff --git a/internal/util/messages.go b/internal/util/messages.go index 69eaf87..19f2948 100644 --- a/internal/util/messages.go +++ b/internal/util/messages.go @@ -9,7 +9,7 @@ import ( var markdownImagePattern = regexp.MustCompile(`!\[(.*?)\]\((.*?)\)`) -const ClaudeDefaultModel = "claude-sonnet-4-20250514" +const ClaudeDefaultModel = "claude-sonnet-4-5" type Message struct { Role string `json:"role"` diff --git a/internal/util/messages_test.go b/internal/util/messages_test.go index b8c1304..30b8cc0 100644 --- a/internal/util/messages_test.go +++ b/internal/util/messages_test.go @@ -36,7 +36,7 @@ func TestMessagesPrepareRoles(t *testing.T) { func TestConvertClaudeToDeepSeek(t *testing.T) { store := config.LoadStore() req := map[string]any{ - "model": "claude-sonnet-4-20250514-slow", + "model": "claude-opus-4-6", "messages": []any{map[string]any{"role": "user", "content": "Hi"}}, "system": "You are helpful", "stream": true, From d0549c27c7e6e3b482e5fd12df9c564edccd0ec9 Mon Sep 17 00:00:00 2001 From: CJACK Date: Tue, 17 Feb 2026 13:44:14 +0800 Subject: [PATCH 4/5] feat: Add OpenCode CLI integration instructions to READMEs and provide an example configuration file. --- README.MD | 16 ++++++++++++++++ README.en.md | 16 ++++++++++++++++ opencode.json.example | 22 ++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 opencode.json.example diff --git a/README.MD b/README.MD index 3182941..52779da 100644 --- a/README.MD +++ b/README.MD @@ -148,6 +148,22 @@ cp config.example.json config.json ./ds2api ``` +### 方式五:OpenCode CLI 接入 + +1. 复制示例配置: + +```bash +cp opencode.json.example opencode.json +``` + +2. 编辑 `opencode.json`: +- 将 `baseURL` 改为你的 DS2API 地址(例如 `https://your-domain.com/v1`) +- 将 `apiKey` 改为你的 DS2API key(对应 `config.keys`) + +3. 在项目目录启动 OpenCode CLI(按你的安装方式运行 `opencode`)。 + +> 建议优先使用 OpenAI 兼容路径(`/v1/*`),即示例里的 `@ai-sdk/openai-compatible` provider。 + ## 配置说明 ### `config.json` 示例 diff --git a/README.en.md b/README.en.md index 555859c..189d719 100644 --- a/README.en.md +++ b/README.en.md @@ -148,6 +148,22 @@ cp config.example.json config.json ./ds2api ``` +### Option 5: OpenCode CLI + +1. Copy the example config: + +```bash +cp opencode.json.example opencode.json +``` + +2. Edit `opencode.json`: +- Set `baseURL` to your DS2API endpoint (for example, `https://your-domain.com/v1`) +- Set `apiKey` to your DS2API key (from `config.keys`) + +3. Start OpenCode CLI in the project directory (run `opencode` using your installed method). + +> Recommended: use the OpenAI-compatible path (`/v1/*`) via `@ai-sdk/openai-compatible` as shown in the example. + ## Configuration ### `config.json` Example diff --git a/opencode.json.example b/opencode.json.example new file mode 100644 index 0000000..2933e9f --- /dev/null +++ b/opencode.json.example @@ -0,0 +1,22 @@ +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "ds2api": { + "npm": "@ai-sdk/openai-compatible", + "name": "DS2API", + "options": { + "baseURL": "http://localhost:5001/v1", + "apiKey": "your-api-key" + }, + "models": { + "deepseek-chat": { + "name": "DeepSeek Chat (DS2API)" + }, + "deepseek-reasoner": { + "name": "DeepSeek Reasoner (DS2API)" + } + } + } + }, + "model": "ds2api/deepseek-chat" +} From 76ae2fed51e3f97489ea42e55cebbd29848fc29a Mon Sep 17 00:00:00 2001 From: CJACK Date: Tue, 17 Feb 2026 14:01:31 +0800 Subject: [PATCH 5/5] feat: Add comprehensive historical and current Claude model IDs for API compatibility and dynamic Docker port configuration. --- API.en.md | 6 ++++-- API.md | 6 ++++-- DEPLOY.en.md | 9 ++++++++- DEPLOY.md | 9 ++++++++- README.MD | 3 ++- README.en.md | 3 ++- docker-compose.dev.yml | 2 +- docker-compose.yml | 4 ++-- internal/config/models.go | 35 +++++++++++++++++++++++++++++++++-- 9 files changed, 64 insertions(+), 13 deletions(-) diff --git a/API.en.md b/API.en.md index 13e39b4..e570dee 100644 --- a/API.en.md +++ b/API.en.md @@ -247,12 +247,14 @@ No auth required. "object": "list", "data": [ {"id": "claude-sonnet-4-5", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, - {"id": "claude-3-5-haiku-latest", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, + {"id": "claude-haiku-4-5", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, {"id": "claude-opus-4-6", "object": "model", "created": 1715635200, "owned_by": "anthropic"} ] } ``` +> Note: the example is partial; the real response includes historical Claude 1.x/2.x/3.x/4.x IDs and common aliases. + ### `POST /anthropic/v1/messages` **Headers**: @@ -267,7 +269,7 @@ anthropic-version: 2023-06-01 | Field | Type | Required | Notes | | --- | --- | --- | --- | -| `model` | string | ✅ | `claude-sonnet-4-5` / `claude-opus-4-6` / `claude-3-5-haiku-latest` | +| `model` | string | ✅ | For example `claude-sonnet-4-5` / `claude-opus-4-6` / `claude-haiku-4-5` (compatible with `claude-3-5-haiku-latest`), plus historical Claude model IDs | | `messages` | array | ✅ | Claude-style messages | | `max_tokens` | number | ❌ | Not strictly enforced by upstream bridge | | `stream` | boolean | ❌ | Default `false` | diff --git a/API.md b/API.md index 95ecbfe..6be7f65 100644 --- a/API.md +++ b/API.md @@ -247,12 +247,14 @@ data: [DONE] "object": "list", "data": [ {"id": "claude-sonnet-4-5", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, - {"id": "claude-3-5-haiku-latest", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, + {"id": "claude-haiku-4-5", "object": "model", "created": 1715635200, "owned_by": "anthropic"}, {"id": "claude-opus-4-6", "object": "model", "created": 1715635200, "owned_by": "anthropic"} ] } ``` +> 说明:示例仅展示部分模型;实际返回包含 Claude 1.x/2.x/3.x/4.x 历史模型 ID 与常见别名。 + ### `POST /anthropic/v1/messages` **请求头**: @@ -267,7 +269,7 @@ anthropic-version: 2023-06-01 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | -| `model` | string | ✅ | `claude-sonnet-4-5` / `claude-opus-4-6` / `claude-3-5-haiku-latest` | +| `model` | string | ✅ | 例如 `claude-sonnet-4-5` / `claude-opus-4-6` / `claude-haiku-4-5`(兼容 `claude-3-5-haiku-latest`),并支持历史 Claude 模型 ID | | `messages` | array | ✅ | Claude 风格消息数组 | | `max_tokens` | number | ❌ | 当前实现不会硬性截断上游输出 | | `stream` | boolean | ❌ | 默认 `false` | diff --git a/DEPLOY.en.md b/DEPLOY.en.md index ffdbf50..b7caf8c 100644 --- a/DEPLOY.en.md +++ b/DEPLOY.en.md @@ -145,13 +145,20 @@ Docker Compose includes a built-in health check: ```yaml healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:5001/healthz"] + test: ["CMD", "wget", "-qO-", "http://localhost:${PORT:-5001}/healthz"] interval: 30s timeout: 10s retries: 3 start_period: 10s ``` +### 2.6 Docker Troubleshooting + +If container logs look normal but the admin panel is unreachable, check these first: + +1. **Port alignment**: when `PORT` is not `5001`, use the same port in your URL (for example `http://localhost:8080/admin`). +2. **WebUI assets in dev compose**: `docker-compose.dev.yml` runs `go run` in a dev image and does not auto-install Node.js inside the container; if `static/admin` is missing in your repo, `/admin` will return 404. Build once on host: `./scripts/build-webui.sh`. + --- ## 3. Vercel Deployment diff --git a/DEPLOY.md b/DEPLOY.md index e3c6f80..b7fbf9a 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -145,13 +145,20 @@ Docker Compose 已配置内置健康检查: ```yaml healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:5001/healthz"] + test: ["CMD", "wget", "-qO-", "http://localhost:${PORT:-5001}/healthz"] interval: 30s timeout: 10s retries: 3 start_period: 10s ``` +### 2.6 Docker 常见排查 + +如果容器日志正常但面板打不开,优先检查: + +1. **端口是否一致**:`PORT` 改成非 `5001` 时,访问地址也要改成对应端口(如 `http://localhost:8080/admin`)。 +2. **开发 compose 的 WebUI 静态文件**:`docker-compose.dev.yml` 使用 `go run` 开发镜像,不会在容器内自动安装 Node.js;若仓库里没有 `static/admin`,`/admin` 会返回 404。可先在宿主机构建一次:`./scripts/build-webui.sh`。 + --- ## 三、Vercel 部署 diff --git a/README.MD b/README.MD index 52779da..b438b75 100644 --- a/README.MD +++ b/README.MD @@ -80,10 +80,11 @@ flowchart LR | 模型 | 默认映射 | | --- | --- | | `claude-sonnet-4-5` | `deepseek-chat` | -| `claude-3-5-haiku-latest` | `deepseek-chat` | +| `claude-haiku-4-5`(兼容 `claude-3-5-haiku-latest`) | `deepseek-chat` | | `claude-opus-4-6` | `deepseek-reasoner` | 可通过配置中的 `claude_mapping` 或 `claude_model_mapping` 覆盖映射关系。 +另外,`/anthropic/v1/models` 现已包含 Claude 1.x/2.x/3.x/4.x 历史模型 ID 与常见别名,便于旧客户端直接兼容。 ## 快速开始 diff --git a/README.en.md b/README.en.md index 189d719..bbad73b 100644 --- a/README.en.md +++ b/README.en.md @@ -80,10 +80,11 @@ flowchart LR | Model | Default Mapping | | --- | --- | | `claude-sonnet-4-5` | `deepseek-chat` | -| `claude-3-5-haiku-latest` | `deepseek-chat` | +| `claude-haiku-4-5` (compatible with `claude-3-5-haiku-latest`) | `deepseek-chat` | | `claude-opus-4-6` | `deepseek-reasoner` | Override mapping via `claude_mapping` or `claude_model_mapping` in config. +In addition, `/anthropic/v1/models` now includes historical Claude 1.x/2.x/3.x/4.x IDs and common aliases for legacy client compatibility. ## Quick Start diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c378129..39cb6b5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -16,7 +16,7 @@ services: container_name: ds2api-dev command: ["go", "run", "./cmd/ds2api"] ports: - - "${PORT:-5001}:5001" + - "${PORT:-5001}:${PORT:-5001}" env_file: - .env environment: diff --git a/docker-compose.yml b/docker-compose.yml index d984420..a3f93f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,14 +4,14 @@ services: image: ds2api:latest container_name: ds2api ports: - - "${PORT:-5001}:5001" + - "${PORT:-5001}:${PORT:-5001}" env_file: - .env environment: - HOST=0.0.0.0 restart: unless-stopped healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:5001/healthz"] + test: ["CMD", "wget", "-qO-", "http://localhost:${PORT:-5001}/healthz"] interval: 30s timeout: 10s retries: 3 diff --git a/internal/config/models.go b/internal/config/models.go index 51f6126..13fa63d 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -16,13 +16,44 @@ var DeepSeekModels = []ModelInfo{ } var ClaudeModels = []ModelInfo{ - {ID: "claude-sonnet-4-5", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, - {ID: "claude-sonnet-4-5-20250929", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + // Current aliases {ID: "claude-opus-4-6", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-sonnet-4-5", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-haiku-4-5", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + + // Current snapshots + {ID: "claude-opus-4-5-20251101", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, {ID: "claude-opus-4-1", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, {ID: "claude-opus-4-1-20250805", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-opus-4-0", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-opus-4-20250514", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-sonnet-4-5-20250929", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-sonnet-4-0", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-sonnet-4-20250514", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-haiku-4-5-20251001", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + + // Claude 3.x (legacy/deprecated snapshots and aliases) {ID: "claude-3-7-sonnet-latest", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-7-sonnet-20250219", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-5-sonnet-latest", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-5-sonnet-20240620", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-5-sonnet-20241022", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-opus-20240229", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-sonnet-20240229", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, {ID: "claude-3-5-haiku-latest", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-5-haiku-20241022", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-3-haiku-20240307", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + + // Claude 2.x and 1.x (retired but accepted for compatibility) + {ID: "claude-2.1", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-2.0", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-1.3", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-1.2", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-1.1", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-1.0", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-instant-1.2", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-instant-1.1", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, + {ID: "claude-instant-1.0", Object: "model", Created: 1715635200, OwnedBy: "anthropic"}, } func GetModelConfig(model string) (thinking bool, search bool, ok bool) {