feat: add OpenAI/Anthropic tools support with tool emulation

- Parse tools/tool_choice from OpenAI and Anthropic requests
- Inject tool definitions into system prompt via toolemulation
- Parse action blocks (```json action) from model responses
- Retry logic for forced tool_choice (any/required)
- Return proper tool_calls / tool_use in responses
- Support streaming tools via collect-and-replay pattern
- Add tool history projection (assistant tool_calls + tool results)
- Model ID normalization: use official names (Qwen3.6-Plus, etc.)
- Fix resolveSessionMode to use Fresh mode when tools present
This commit is contained in:
lutc5
2026-04-25 13:37:58 +08:00
parent c49b4b63e7
commit 74bbd8e6d2
13 changed files with 648 additions and 115 deletions

View File

@@ -17,9 +17,6 @@ import (
)
const (
PipeDir = `\\.\pipe\`
PipePrefix = "lingma-"
MetaRequestID = "ai-coding/request-id"
MetaMode = "ai-coding/mode"
MetaModel = "ai-coding/model"

View File

@@ -0,0 +1,8 @@
//go:build !windows
package lingmaipc
const (
PipeDir = ""
PipePrefix = ""
)

View File

@@ -0,0 +1,8 @@
//go:build windows
package lingmaipc
const (
PipeDir = `\\.\pipe\`
PipePrefix = "lingma-"
)

View File

@@ -0,0 +1,12 @@
//go:build !windows
package lingmaipc
import (
"context"
"errors"
)
func connectPipeTransport(ctx context.Context, pipePath string) (framedTransport, error) {
return nil, errors.New("pipe transport is only supported on Windows")
}

View File

@@ -0,0 +1,57 @@
//go:build windows
package lingmaipc
import (
"context"
"fmt"
"net"
"sync"
winio "github.com/Microsoft/go-winio"
)
type pipeTransport struct {
path string
conn net.Conn
reader *framedReader
write sync.Mutex
}
func connectPipeTransport(ctx context.Context, pipePath string) (framedTransport, error) {
conn, err := winio.DialPipeContext(ctx, pipePath)
if err != nil {
return nil, fmt.Errorf("connect Lingma IPC pipe %s: %w", pipePath, err)
}
return &pipeTransport{
path: pipePath,
conn: conn,
reader: newFramedReader(conn),
}, nil
}
func (t *pipeTransport) ReadFrame() ([]byte, error) {
return t.reader.ReadFrame()
}
func (t *pipeTransport) WriteFrame(body []byte) error {
t.write.Lock()
defer t.write.Unlock()
frame := []byte(fmt.Sprintf("Content-Length: %d\r\n\r\n", len(body)))
if _, err := t.conn.Write(frame); err != nil {
return fmt.Errorf("write frame header: %w", err)
}
if _, err := t.conn.Write(body); err != nil {
return fmt.Errorf("write frame body: %w", err)
}
return nil
}
func (t *pipeTransport) Close() error {
return t.conn.Close()
}
func (t *pipeTransport) Address() string {
return t.path
}

View File

@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"path/filepath"
@@ -19,7 +18,6 @@ import (
"sync"
"time"
winio "github.com/Microsoft/go-winio"
"github.com/gorilla/websocket"
)
@@ -74,16 +72,23 @@ func ResolveDialOptions(transport Transport, explicitPipe string, explicitWebSoc
return DialOptions{Transport: TransportWebSocket, WebSocketURL: wsURL}, nil
}
pipePath, pipeErr := ResolvePipePath(explicitPipe)
if pipeErr == nil {
return DialOptions{Transport: TransportPipe, PipePath: pipePath}, nil
if runtime.GOOS == "windows" {
pipePath, pipeErr := ResolvePipePath(explicitPipe)
if pipeErr == nil {
return DialOptions{Transport: TransportPipe, PipePath: pipePath}, nil
}
wsURL, wsErr := ResolveWebSocketURL(explicitWebSocketURL)
if wsErr == nil {
return DialOptions{Transport: TransportWebSocket, WebSocketURL: wsURL}, nil
}
return DialOptions{}, fmt.Errorf("resolve Lingma transport automatically: pipe: %w; websocket: %v", pipeErr, wsErr)
}
wsURL, wsErr := ResolveWebSocketURL(explicitWebSocketURL)
if wsErr == nil {
return DialOptions{Transport: TransportWebSocket, WebSocketURL: wsURL}, nil
}
return DialOptions{}, fmt.Errorf("resolve Lingma transport automatically: pipe: %w; websocket: %v", pipeErr, wsErr)
return DialOptions{}, fmt.Errorf("resolve Lingma transport automatically on %s: websocket: %w", runtime.GOOS, wsErr)
case TransportPipe:
pipePath, err := ResolvePipePath(explicitPipe)
if err != nil {
@@ -307,51 +312,6 @@ func connectTransport(ctx context.Context, opts DialOptions) (framedTransport, e
}
}
type pipeTransport struct {
path string
conn net.Conn
reader *framedReader
write sync.Mutex
}
func connectPipeTransport(ctx context.Context, pipePath string) (*pipeTransport, error) {
conn, err := winio.DialPipeContext(ctx, pipePath)
if err != nil {
return nil, fmt.Errorf("connect Lingma IPC pipe %s: %w", pipePath, err)
}
return &pipeTransport{
path: pipePath,
conn: conn,
reader: newFramedReader(conn),
}, nil
}
func (t *pipeTransport) ReadFrame() ([]byte, error) {
return t.reader.ReadFrame()
}
func (t *pipeTransport) WriteFrame(body []byte) error {
t.write.Lock()
defer t.write.Unlock()
frame := []byte(fmt.Sprintf("Content-Length: %d\r\n\r\n", len(body)))
if _, err := t.conn.Write(frame); err != nil {
return fmt.Errorf("write frame header: %w", err)
}
if _, err := t.conn.Write(body); err != nil {
return fmt.Errorf("write frame body: %w", err)
}
return nil
}
func (t *pipeTransport) Close() error {
return t.conn.Close()
}
func (t *pipeTransport) Address() string {
return t.path
}
type websocketTransport struct {
url string
conn *websocket.Conn