Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.git
.github
docs
*.md
tmp/
data/
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ jobs:
- name: Create release
uses: softprops/action-gh-release@v2
with:
name: "Strait ${{ github.ref_name }}"
files: build/*
generate_release_notes: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# 环境变量
.env
.env.local
k8s/secret.yaml

# 依赖缓存
vendor/
Expand Down
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 构建阶段
FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o strait ./cmd/strait/

# 运行阶段
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/strait .
COPY --from=builder /app/configs ./configs

EXPOSE 8080
CMD ["./strait"]
15 changes: 15 additions & 0 deletions api/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ type ChatAdapter interface {
SendChatStream(ctx context.Context, payload *ChatRequest, route *RouteDecision) (<-chan *StreamChunk, error)
}

type Guard interface {
Plugin
Guard(pctx *PipelineContext) error
}

type PreProcessor interface {
Plugin
PreProcess(pctx *PipelineContext) error
}

type PostProcessor interface {
Plugin
PostProcess(pctx *PipelineContext) error
}

// Constructor 插件构造函数
type Constructor func() Plugin

Expand Down
18 changes: 15 additions & 3 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,28 @@ type ToolCallFunction struct {

// RouteDecision 路由决策
type RouteDecision struct {
Protocol string `json:"protocol"` // 调用协议:openAI / anthropic / deepseek / ollama
BaseURL string `json:"base_url"` // 调用地址
APIKey string `json:"api_key"` // 认证密钥
Protocol string `json:"protocol"` // 调用协议:openAI / anthropic / deepseek / ollama
BaseURL string `json:"base_url"` // 调用地址
APIKey string `json:"api_key"` // 认证密钥
Model string `json:"model,omitempty"` // 目标模型名称
}

// Subject 认证后的调用方信息
type Subject struct {
ID string `json:"id"` // 调用方唯一标识
}

// ─ 管线 ─

// PipelineContext 管线上下文,贯穿 guard → preprocess → route → adapter → postprocess 全流程
type PipelineContext struct {
Context context.Context // 请求上下文
Request any // 请求体:*ChatRequest / *EmbeddingRequest / ...
Response any // 响应体:*ChatResponse / *EmbeddingResponse / ...
Route *RouteDecision // 路由决策
Meta map[string]any // 插件间传递的元数据
}

// ─ Context ─

type ctxKey string
Expand Down
69 changes: 60 additions & 9 deletions cmd/strait/main.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,59 @@
// Strait — AI 代理网关入口。
// main Strait — AI 代理网关入口。
package main

import (
"context"
"log"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"strait/internal/app"
"strait/internal/config"

"strait/internal/hotreload"

"strait/internal/app"
"strait/internal/metrics"
"strait/internal/plugin"
"strait/internal/router"
_ "strait/plugins/adapter-ollama"
_ "strait/plugins/adapter-openai"
_ "strait/plugins/auth-static-token"
_ "strait/plugins/prompt-injector"
)

const banner = `
███████╗████████╗██████╗ █████╗ ██╗████████╗
██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║╚══██╔══╝
███████╗ ██║ ██████╔╝███████║██║ ██║
╚════██║ ██║ ██╔══██╗██╔══██║██║ ██║
███████║ ██║ ██║ ██║██║ ██║██║ ██║
╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ v0.2
`

func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})))

loader := plugin.NewLoader(config.PluginsPath)
m, err := loader.Build()
if err != nil {
log.Fatal(err)
slog.Error("startup failed", "error", err)
os.Exit(1)
}

if os.Getenv("STRAIT_BANNER") != "false" {
fmt.Print(banner)
fmt.Println(" Plugins:")
fmt.Print(m.Summary())
fmt.Println()
}

scheduler := plugin.NewScheduler(m)
met := metrics.NewMetrics()

reload := func(filename string) error {
if filename == "plugins.yaml" {
Expand All @@ -49,7 +77,30 @@ func main() {
_ = watcher.Start(context.Background())
}()

server := app.NewServer(scheduler)
log.Println("strait listening on :8080")
log.Fatal(http.ListenAndServe(":8080", server.Handler()))
// 启动 HTTP 服务
server := app.NewServer(scheduler, met)
srv := &http.Server{
Addr: ":8080",
Handler: server.Handler(),
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

go func() {
slog.Info("strait listening on :8080")
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server crashed", "error", err)
os.Exit(1)
}
}()

<-ctx.Done()
slog.Info("shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("shutdown failed", "error", err)
}
slog.Info("strait stopped")
}
6 changes: 5 additions & 1 deletion configs/plugins.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ plugins:
token: sk-admin-init
subject: admin
- id: adapter-ollama
type: adapter
type: adapter
- id: prompt-injector
type: preprocessor
config:
system_prompt: "You are a helpful assistant."
21 changes: 17 additions & 4 deletions configs/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ routes:
- id: deepseek-chat
match:
model: deepseek-chat
strategy: weight
target:
provider: deepseek-main
model: deepseek-chat
model: deepseek-reasoner
targets:
- provider: deepseek-main
model: deepseek-chat
priority: 1
weight: 3
- provider: ollama-local
model: qwen2.5:0.5b
priority: 1
weight: 1

- id: deepseek-reasoner
match:
Expand All @@ -16,6 +26,9 @@ routes:
- id: ollama-qwen
match:
model: qwen2.5:0.5b
target:
provider: ollama-local
model: qwen2.5:0.5b
strategy: priority
targets:
- provider: ollama-local
model: qwen2.5:0.5b
priority: 1
weight: 1
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
strait:
build: .
ports:
- "8080:8080"
volumes:
- ./configs:/app/configs
restart: unless-stopped
87 changes: 82 additions & 5 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,97 @@
# 部署指南

## MySQL
## Docker

### 构建镜像

```bash
docker-compose up -d
docker build -t strait .
```

### 运行

```bash
docker run -d \
-p 8080:8080 \
-e DEEPSEEK_API_KEY=your-key \
-v $(pwd)/configs:/app/configs \
strait
```

## Docker Compose
### Docker Compose

```bash
# 设置环境变量
export DEEPSEEK_API_KEY=your-key

# 启动
docker-compose up -d
```

`docker-compose.yml` 会自动挂载 `configs/` 目录并注入环境变量。

---

## Kubernetes

### 1. 创建 Secret(存放 API Key)

参考 `k8s/secret.example.yaml`:

```bash
kubectl create secret generic strait-secret \
--from-literal=DEEPSEEK_API_KEY=your-key
```

### 2. 创建 ConfigMap(挂载配置文件)

```bash
kubectl create configmap strait-config \
--from-file=configs/
```

### 3. 部署

```bash
helm repo add strait https://yourorg.github.io/helm-charts
helm install strait strait/strait
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/deployment.yaml
```

### 4. 验证

```bash
# 检查 Pod 状态
kubectl get pods -l app=strait

# 健康检查
kubectl port-forward svc/strait 8080:8080
curl http://localhost:8080/health
```

### 探针说明

| 探针 | 路径 | 说明 |
|------|------|------|
| liveness | `/health` | 存活检测,失败则重启容器 |
| readiness | `/ready` | 就绪检测,失败则从 Service 摘除 |

---

## 监控

### Prometheus Metrics

服务暴露 `/metrics` 端点,返回 Prometheus 格式的请求计数:

```bash
curl http://localhost:8080/metrics
```

---

## 配置热重载

修改 `configs/` 目录下的 YAML 文件后自动重载,无需重启:

- `providers.yaml` / `routes.yaml` → 路由重载
- `plugins.yaml` → 插件重载
Loading
Loading