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:
@@ -17,9 +17,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
PipeDir = `\\.\pipe\`
|
||||
PipePrefix = "lingma-"
|
||||
|
||||
MetaRequestID = "ai-coding/request-id"
|
||||
MetaMode = "ai-coding/mode"
|
||||
MetaModel = "ai-coding/model"
|
||||
|
||||
8
internal/lingmaipc/pipe_const_other.go
Normal file
8
internal/lingmaipc/pipe_const_other.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package lingmaipc
|
||||
|
||||
const (
|
||||
PipeDir = ""
|
||||
PipePrefix = ""
|
||||
)
|
||||
8
internal/lingmaipc/pipe_const_windows.go
Normal file
8
internal/lingmaipc/pipe_const_windows.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build windows
|
||||
|
||||
package lingmaipc
|
||||
|
||||
const (
|
||||
PipeDir = `\\.\pipe\`
|
||||
PipePrefix = "lingma-"
|
||||
)
|
||||
12
internal/lingmaipc/pipe_other.go
Normal file
12
internal/lingmaipc/pipe_other.go
Normal 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")
|
||||
}
|
||||
57
internal/lingmaipc/pipe_windows.go
Normal file
57
internal/lingmaipc/pipe_windows.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user