Release v1.4.7 with unlimited default timeout
This commit is contained in:
@@ -2,10 +2,16 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## v1.4.7 - 2026-05-06
|
||||
|
||||
- Renamed user-facing product, desktop app, release assets, and documentation from Lingma IPC Proxy to Lingma Proxy.
|
||||
- Clarified that Remote API mode is the recommended default and that only IPC plugin mode is based on the `coolxll/lingma-ipc-proxy` protocol discovery.
|
||||
- Added `lingma-proxy.json` and `~/.config/lingma-proxy/config.json` config lookup/write paths while keeping legacy `lingma-ipc-proxy` config fallback.
|
||||
- Added a desktop top-bar force quit button that stops the proxy and exits the app on macOS and Windows.
|
||||
- Added Anthropic `/v1/messages/count_tokens` compatibility for Claude Code v2.1.129+.
|
||||
- Reduced prompt-emulated tool loops by allowing final answers after tool results and dropping tool calls with missing required arguments.
|
||||
- Prevented hosted Anthropic `web_search` from being short-circuited again after a `tool_result` follow-up.
|
||||
- Changed the default proxy request timeout to `0`, meaning no proxy-level per-request deadline. Positive timeout values still enable timeout-triggered remote fallback.
|
||||
|
||||
## v1.4.6 - 2026-05-06
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ The proxy now supports two backend modes:
|
||||
|
||||
## Current Version
|
||||
|
||||
The current desktop line is `v1.4.6`.
|
||||
The current desktop line is `v1.4.7`.
|
||||
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for release history.
|
||||
|
||||
@@ -327,7 +327,7 @@ The proxy only reports models actually exposed by your Lingma plugin. The table
|
||||
|
||||
Default model when the client omits `model`: `kmodel` (`Kimi-K2.6` in the remote model list).
|
||||
|
||||
Remote mode enables timeout fallback by default. On timeout, upstream 5xx/429, or network interruption, the proxy only switches models if no streaming bytes have been sent to the client yet. Fallback candidates are filtered against the actual `/v1/models` response, so unavailable models are skipped. Default order:
|
||||
Remote mode enables fallback by default. The default proxy request timeout is `0`, which means Lingma Proxy does not set its own per-request deadline and is suitable for long agent workflows. If you set `"timeout"` to a positive number of seconds, timeout errors can also trigger fallback. Upstream 5xx/429 or network interruption can trigger fallback regardless of the timeout setting, but the proxy only switches models if no streaming bytes have been sent to the client yet. Fallback candidates are filtered against the actual `/v1/models` response, so unavailable models are skipped. Default order:
|
||||
|
||||
`Kimi-K2.6 -> MiniMax-M2.7 -> Qwen3-Coder -> Qwen3.6-Plus -> Qwen3-Max -> Qwen3-Thinking`
|
||||
|
||||
@@ -354,7 +354,7 @@ Example:
|
||||
"mode": "agent",
|
||||
"shell_type": "zsh",
|
||||
"session_mode": "auto",
|
||||
"timeout": 300,
|
||||
"timeout": 0,
|
||||
"remote_fallback_enabled": true,
|
||||
"remote_fallback_models": [
|
||||
"kmodel",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
## 当前版本
|
||||
|
||||
当前桌面端版本线:`v1.4.6`
|
||||
当前桌面端版本线:`v1.4.7`
|
||||
|
||||
版本更新记录见 [CHANGELOG.md](./CHANGELOG.md)。
|
||||
|
||||
@@ -409,7 +409,7 @@ export ANTHROPIC_API_KEY="any"
|
||||
|
||||
当客户端请求没有携带 `model` 字段时,代理默认使用:`kmodel`(远端模型列表里的 Kimi-K2.6)。
|
||||
|
||||
远端模式默认开启超时兜底。遇到请求超时、上游 5xx/429 或网络中断时,代理只会在尚未向客户端输出任何流式内容的情况下切换模型。兜底候选会先和实际 `/v1/models` 返回结果求交集,不存在或当前账号不可用的模型会自动跳过。默认顺序:
|
||||
远端模式默认开启兜底。代理默认请求超时为 `0`,表示 Lingma Proxy 不设置自己的单次请求 deadline,适合长流程 Agent 任务。如果你把 `"timeout"` 设置为正数秒,超时错误也会触发兜底。上游 5xx/429 或网络中断不受超时设置影响,仍可触发兜底;但代理只会在尚未向客户端输出任何流式内容的情况下切换模型。兜底候选会先和实际 `/v1/models` 返回结果求交集,不存在或当前账号不可用的模型会自动跳过。默认顺序:
|
||||
|
||||
`Kimi-K2.6 -> MiniMax-M2.7 -> Qwen3-Coder -> Qwen3.6-Plus -> Qwen3-Max -> Qwen3-Thinking`
|
||||
|
||||
@@ -436,7 +436,7 @@ export ANTHROPIC_API_KEY="any"
|
||||
"mode": "agent",
|
||||
"shell_type": "zsh",
|
||||
"session_mode": "auto",
|
||||
"timeout": 300,
|
||||
"timeout": 0,
|
||||
"remote_fallback_enabled": true,
|
||||
"remote_fallback_models": [
|
||||
"kmodel",
|
||||
|
||||
@@ -100,7 +100,7 @@ func loadConfig() (service.Config, string) {
|
||||
Model: "kmodel",
|
||||
ShellType: defaultShellType(),
|
||||
SessionMode: service.SessionModeAuto,
|
||||
Timeout: 300 * time.Second,
|
||||
Timeout: 0,
|
||||
RemoteFallbackEnabled: true,
|
||||
RemoteFallbackModels: service.DefaultRemoteFallbackModels(),
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func loadConfig() (service.Config, string) {
|
||||
mode := flag.String("mode", cfg.Mode, "Lingma ACP mode value")
|
||||
model := flag.String("model", cfg.Model, "Default Lingma model when API request omits model")
|
||||
shellType := flag.String("shell-type", cfg.ShellType, "Shell type sent through ACP meta")
|
||||
timeoutSeconds := flag.Int("timeout", int(cfg.Timeout/time.Second), "Per-request timeout in seconds")
|
||||
timeoutSeconds := flag.Int("timeout", int(cfg.Timeout/time.Second), "Per-request timeout in seconds; 0 disables the proxy deadline")
|
||||
remoteFallbackEnabled := flag.Bool("remote-fallback", cfg.RemoteFallbackEnabled, "Enable remote timeout/5xx fallback to the next available model")
|
||||
remoteFallbackModels := flag.String("remote-fallback-models", strings.Join(cfg.RemoteFallbackModels, ","), "Comma-separated remote fallback model IDs")
|
||||
sessionMode := flag.String("session-mode", string(cfg.SessionMode), "Session mode: auto, fresh, reuse")
|
||||
@@ -243,7 +243,7 @@ func overlayFileConfig(dst *service.Config, src fileConfig) {
|
||||
if strings.TrimSpace(src.SessionMode) != "" {
|
||||
dst.SessionMode = parseSessionMode(src.SessionMode)
|
||||
}
|
||||
if src.TimeoutSeconds > 0 {
|
||||
if src.TimeoutSeconds >= 0 {
|
||||
dst.Timeout = time.Duration(src.TimeoutSeconds) * time.Second
|
||||
}
|
||||
if src.RemoteFallbackEnabled != nil {
|
||||
@@ -300,7 +300,7 @@ func overlayEnvConfig(dst *service.Config) {
|
||||
if value := strings.TrimSpace(os.Getenv("LINGMA_PROXY_SESSION_MODE")); value != "" {
|
||||
dst.SessionMode = parseSessionMode(value)
|
||||
}
|
||||
if value := envInt("LINGMA_PROXY_TIMEOUT_SECONDS", 0); value > 0 {
|
||||
if value := envInt("LINGMA_PROXY_TIMEOUT_SECONDS", -1); value >= 0 {
|
||||
dst.Timeout = time.Duration(value) * time.Second
|
||||
}
|
||||
if value, ok := envBool("LINGMA_REMOTE_FALLBACK_ENABLED"); ok {
|
||||
|
||||
@@ -922,7 +922,7 @@ func defaultConfig() service.Config {
|
||||
Model: "kmodel",
|
||||
ShellType: defaultShellType(),
|
||||
SessionMode: service.SessionModeAuto,
|
||||
Timeout: 300 * time.Second,
|
||||
Timeout: 0,
|
||||
RemoteFallbackEnabled: true,
|
||||
RemoteFallbackModels: service.DefaultRemoteFallbackModels(),
|
||||
}
|
||||
@@ -1000,7 +1000,7 @@ func defaultConfig() service.Config {
|
||||
if fileCfg.SessionMode != "" {
|
||||
cfg.SessionMode = service.SessionMode(fileCfg.SessionMode)
|
||||
}
|
||||
if fileCfg.TimeoutSeconds > 0 {
|
||||
if fileCfg.TimeoutSeconds >= 0 {
|
||||
cfg.Timeout = time.Duration(fileCfg.TimeoutSeconds) * time.Second
|
||||
}
|
||||
if fileCfg.RemoteFallbackEnabled != nil {
|
||||
|
||||
@@ -252,7 +252,7 @@ onUnmounted(() => {
|
||||
<span class="status-dot" :class="{ running: status.running }"></span>
|
||||
<div>
|
||||
<strong>{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }}</strong>
|
||||
<small>v1.4.6</small>
|
||||
<small>v1.4.7</small>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -318,7 +318,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="config-summary-item">
|
||||
<label>超时</label>
|
||||
<strong>{{ config.Timeout || 120 }} 秒</strong>
|
||||
<strong>{{ config.Timeout > 0 ? `${config.Timeout} 秒` : '不限制' }}</strong>
|
||||
</div>
|
||||
<div class="config-summary-item span-2">
|
||||
<label>工作目录</label>
|
||||
|
||||
@@ -163,12 +163,13 @@ async function save() {
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>超时秒数</label>
|
||||
<input v-model.number="config.Timeout" type="number" min="1" />
|
||||
<input v-model.number="config.Timeout" type="number" min="0" />
|
||||
<small>0 表示不设置代理层单次请求超时,适合长流程任务。</small>
|
||||
</div>
|
||||
<div class="field span-2 switch-field">
|
||||
<div>
|
||||
<label>远端超时兜底</label>
|
||||
<p>远端 API 超时、限流或 5xx 且尚未流式输出时,自动切换到下一个可用模型。</p>
|
||||
<p>设置正数超时后,远端 API 超时、限流或 5xx 且尚未流式输出时,自动切换到下一个可用模型。</p>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input v-model="config.RemoteFallbackEnabled" type="checkbox" />
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"email": "lutc5@asiainfo.com"
|
||||
},
|
||||
"info": {
|
||||
"productVersion": "1.4.6"
|
||||
"productVersion": "1.4.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +75,6 @@ func New(cfg Config) *Client {
|
||||
if cfg.CosyVersion == "" {
|
||||
cfg.CosyVersion = "2.11.2"
|
||||
}
|
||||
if cfg.Timeout <= 0 {
|
||||
cfg.Timeout = 300 * time.Second
|
||||
}
|
||||
cfg.BaseURL = strings.TrimRight(cfg.BaseURL, "/")
|
||||
return &Client{cfg: cfg, client: &http.Client{Timeout: cfg.Timeout}}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,23 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewKeepsZeroTimeoutUnlimited(t *testing.T) {
|
||||
client := New(Config{Timeout: 0})
|
||||
if client.client.Timeout != 0 {
|
||||
t.Fatalf("timeout = %v, want 0", client.client.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewKeepsPositiveTimeout(t *testing.T) {
|
||||
client := New(Config{Timeout: 7 * time.Second})
|
||||
if client.client.Timeout != 7*time.Second {
|
||||
t.Fatalf("timeout = %v, want 7s", client.client.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractBaseURLFromEndpointLog(t *testing.T) {
|
||||
got := extractBaseURLFromText(`2026-04-10 INFO Update endpoint success. endpoint config: https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com`)
|
||||
want := "https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com"
|
||||
|
||||
@@ -167,9 +167,6 @@ func New(cfg Config) *Service {
|
||||
if strings.TrimSpace(cfg.ShellType) == "" {
|
||||
cfg.ShellType = lingmaipc.DefaultShellType()
|
||||
}
|
||||
if cfg.Timeout <= 0 {
|
||||
cfg.Timeout = 300 * time.Second
|
||||
}
|
||||
if cfg.Transport == "" {
|
||||
cfg.Transport = lingmaipc.TransportAuto
|
||||
}
|
||||
@@ -225,6 +222,13 @@ func (s *Service) Close() error {
|
||||
return s.closeClientLocked()
|
||||
}
|
||||
|
||||
func contextWithOptionalTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
if timeout <= 0 {
|
||||
return context.WithCancel(parent)
|
||||
}
|
||||
return context.WithTimeout(parent, timeout)
|
||||
}
|
||||
|
||||
func (s *Service) State() State {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -365,7 +369,7 @@ func (s *Service) generateRemote(
|
||||
client := s.remoteClientLocked()
|
||||
var lastErr error
|
||||
for i, model := range models {
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, s.cfg.Timeout)
|
||||
attemptCtx, cancel := contextWithOptionalTimeout(ctx, s.cfg.Timeout)
|
||||
result, emitted, err := s.generateRemoteWithModel(attemptCtx, client, req, prompt, model, onDelta)
|
||||
cancel()
|
||||
if err == nil {
|
||||
@@ -513,7 +517,7 @@ func (s *Service) generateLocked(
|
||||
req ChatRequest,
|
||||
onDelta func(string),
|
||||
) (result *ChatResult, err error) {
|
||||
requestCtx, cancel := context.WithTimeout(ctx, s.cfg.Timeout)
|
||||
requestCtx, cancel := contextWithOptionalTimeout(ctx, s.cfg.Timeout)
|
||||
defer cancel()
|
||||
|
||||
ipcClient, err := s.ensureConnected(requestCtx)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIsRecoverableIPCError(t *testing.T) {
|
||||
@@ -23,3 +25,26 @@ func TestIsRecoverableIPCErrorIgnoresModelErrors(t *testing.T) {
|
||||
t.Fatal("timeout should not be treated as an immediate reconnect retry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewKeepsZeroTimeoutUnlimited(t *testing.T) {
|
||||
svc := New(Config{Timeout: 0})
|
||||
if svc.cfg.Timeout != 0 {
|
||||
t.Fatalf("timeout = %v, want 0", svc.cfg.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextWithOptionalTimeoutZeroDoesNotSetDeadline(t *testing.T) {
|
||||
ctx, cancel := contextWithOptionalTimeout(context.Background(), 0)
|
||||
defer cancel()
|
||||
if _, ok := ctx.Deadline(); ok {
|
||||
t.Fatal("zero timeout should not set a deadline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextWithOptionalTimeoutPositiveSetsDeadline(t *testing.T) {
|
||||
ctx, cancel := contextWithOptionalTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if _, ok := ctx.Deadline(); !ok {
|
||||
t.Fatal("positive timeout should set a deadline")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user