update: edge/node config and openapi contract changes
This commit is contained in:
parent
a47331f21d
commit
395c0b4708
12 changed files with 245 additions and 23 deletions
|
|
@ -13,6 +13,16 @@
|
|||
IOP 내부 실행은 `adapter + target` 기준이며, OpenAI-compatible 경계에서는 호환성을 위해 `model`을 사용한다.
|
||||
IOP 고유 실행 문맥은 별도 `iop` wrapper field를 만들지 않고 OpenAI request의 `metadata`에 둔다.
|
||||
|
||||
## Auth
|
||||
|
||||
Edge 설정의 `openai.bearer_token`이 비어 있지 않으면 OpenAI-compatible HTTP 표면은 다음 헤더를 요구한다.
|
||||
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
토큰이 없거나 일치하지 않으면 `401 unauthorized` OpenAI-compatible error response를 반환한다. `openai.bearer_token`이 빈 값이면 auth를 적용하지 않는다.
|
||||
|
||||
## Responses API
|
||||
|
||||
Endpoint:
|
||||
|
|
|
|||
|
|
@ -71,8 +71,11 @@ dev-runtime provider pool과 3-node 연결 상태를 점검할 때는 `agent-tes
|
|||
- provider endpoint: `http://192.168.0.59:13305/v1`
|
||||
- served model: `Qwen3.6-35B-A3B-MTP-GGUF`
|
||||
- capacity baseline: `3`
|
||||
- load baseline: backend `vulkan`, ctx size `786432`, `llamacpp_args="--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024"`, `save_options=true`
|
||||
- workspace: `C:/Users/r0bin/iop-field`
|
||||
|
||||
OneXPlayer Lemonade는 `Qwen3.6-35B-A3B-MTP-GGUF` artifact를 사용하되 runtime MTP speculative decoding은 켜지 않는다. dev long-context baseline은 `/v1/load`에 `llamacpp_backend=vulkan`, `ctx_size=786432`, `--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024`를 `save_options=true`로 저장한 상태다. llama.cpp backend의 `/slots`에서 slot 3개가 각각 `n_ctx=262144`로 보여야 한다.
|
||||
|
||||
OneXPlayer Lemonade Node는 원격 runner나 Edge host에서 다시 SSH하거나 proxy process로 띄우지 않는다. 현재 작업 호스트에서 OneXPlayer Windows host에 `ssh r0bin@192.168.0.59`로 직접 접속한 뒤 generated PowerShell bootstrap을 실행한다.
|
||||
|
||||
## 명령
|
||||
|
|
|
|||
|
|
@ -77,4 +77,11 @@ nodes:
|
|||
endpoint: http://192.168.0.59:13305/v1
|
||||
served_model: Qwen3.6-35B-A3B-MTP-GGUF
|
||||
capacity: 3
|
||||
load:
|
||||
endpoint: http://192.168.0.59:13305/v1/load
|
||||
backend: vulkan
|
||||
ctx_size: 786432
|
||||
llamacpp_args: "--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024"
|
||||
save_options: true
|
||||
mtp_runtime_policy: disabled
|
||||
windows_process_start: Win32_Process.Create
|
||||
|
|
|
|||
|
|
@ -58,8 +58,11 @@ dev-runtime의 실제 3-node 연결을 점검할 때는 원격 runner `ssh toki@
|
|||
- 접속 기준: 현재 작업 호스트에서 직접 SSH
|
||||
- provider endpoint: `http://192.168.0.59:13305/v1`
|
||||
- served model: `Qwen3.6-35B-A3B-MTP-GGUF`
|
||||
- load baseline: backend `vulkan`, ctx size `786432`, `llamacpp_args="--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024"`, `save_options=true`
|
||||
- workspace: `C:/Users/r0bin/iop-field`
|
||||
|
||||
OneXPlayer Lemonade는 `Qwen3.6-35B-A3B-MTP-GGUF` artifact를 사용하되 runtime MTP speculative decoding은 끈 상태를 dev 기준으로 삼는다. Node 검증 전 `/v1/load`의 `recipe_options`가 Vulkan, `ctx_size=786432`, `--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024`를 포함하는지 확인한다. backend `/slots`에서는 slot 3개가 각각 `n_ctx=262144`로 보여야 한다.
|
||||
|
||||
GX10은 Linux/ARM64 bootstrap, OneXPlayer는 Windows native PowerShell bootstrap을 기본으로 한다. OneXPlayer는 현재 작업 호스트에서 직접 접속해 세팅하며, 원격 runner나 Edge host에서 `node-onexplayer-lemonade.yaml`로 proxy 실행하지 않는다.
|
||||
|
||||
OneXPlayer에서 SSH 세션 안의 `Start-Process`로 `iop-node.exe`를 띄우면 SSH 세션 종료와 함께 process가 정리될 수 있다. dev 반복 배포에서는 `Win32_Process.Create` 또는 동등한 세션 독립 실행 방식으로 `C:/Users/r0bin/iop-field`에서 `iop-node.exe --config node.yaml serve`를 시작하고, WMI/process query와 `iop-node.log`의 `connected to edge` 로그로 유지 여부를 확인한다.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package openai
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -9,13 +10,27 @@ import (
|
|||
func (s *Server) routes() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", s.handleHealthz)
|
||||
mux.HandleFunc("/v1/models", s.handleModels)
|
||||
mux.HandleFunc("/v1/chat/completions", s.handleChatCompletions)
|
||||
mux.HandleFunc("/v1/responses", s.handleResponses)
|
||||
mux.HandleFunc("/api/", s.handleOllamaAPI)
|
||||
mux.HandleFunc("/v1/models", s.withAuth(s.handleModels))
|
||||
mux.HandleFunc("/v1/chat/completions", s.withAuth(s.handleChatCompletions))
|
||||
mux.HandleFunc("/v1/responses", s.withAuth(s.handleResponses))
|
||||
mux.HandleFunc("/api/", s.withAuth(s.handleOllamaAPI))
|
||||
return mux
|
||||
}
|
||||
|
||||
func (s *Server) withAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.TrimSpace(s.cfg.BearerToken) != "" {
|
||||
expected := "Bearer " + s.cfg.BearerToken
|
||||
if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte(expected)) != 1 {
|
||||
w.Header().Set("WWW-Authenticate", `Bearer realm="iop-openai"`)
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized", "unauthorized")
|
||||
return
|
||||
}
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,49 @@ type fakeRunService struct {
|
|||
events chan *iop.RunEvent
|
||||
}
|
||||
|
||||
func TestRoutesRequireBearerTokenWhenConfigured(t *testing.T) {
|
||||
srv := NewServer(config.EdgeOpenAIConf{
|
||||
BearerToken: "secret-token",
|
||||
Models: []string{"model-a"},
|
||||
}, &fakeRunService{}, nil)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
auth string
|
||||
wantStatus int
|
||||
}{
|
||||
{name: "missing", wantStatus: http.StatusUnauthorized},
|
||||
{name: "wrong", auth: "Bearer wrong", wantStatus: http.StatusUnauthorized},
|
||||
{name: "valid", auth: "Bearer secret-token", wantStatus: http.StatusOK},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
|
||||
if tc.auth != "" {
|
||||
req.Header.Set("Authorization", tc.auth)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.routes().ServeHTTP(w, req)
|
||||
|
||||
if w.Code != tc.wantStatus {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthzDoesNotRequireBearerToken(t *testing.T) {
|
||||
srv := NewServer(config.EdgeOpenAIConf{BearerToken: "secret-token"}, &fakeRunService{}, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.routes().ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fakeRunService) SubmitRun(_ context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) {
|
||||
s.req = req
|
||||
return &edgeservice.RunHandle{
|
||||
|
|
|
|||
|
|
@ -171,15 +171,30 @@ func (n *Node) OnRunRequest(ctx context.Context, sess *transport.Session, req *i
|
|||
sessionID: normalizeSessionID(spec.SessionID),
|
||||
background: spec.Background,
|
||||
}
|
||||
runSink := &terminalDeferringSink{inner: sink}
|
||||
|
||||
run := func() error {
|
||||
defer ticket.release()
|
||||
released := false
|
||||
releaseTicket := func() {
|
||||
if !released {
|
||||
ticket.release()
|
||||
released = true
|
||||
}
|
||||
}
|
||||
defer releaseTicket()
|
||||
defer cancel()
|
||||
defer n.runs.deregister(spec.RunID)
|
||||
defer close(h.done)
|
||||
|
||||
execErr := adapter.Execute(execCtx, spec, sink)
|
||||
execErr := adapter.Execute(execCtx, spec, runSink)
|
||||
releaseTicket()
|
||||
n.completeRun(spec, execErr)
|
||||
if flushErr := runSink.Flush(context.Background()); flushErr != nil {
|
||||
n.logger.Warn("session: flush terminal events", zap.String("run_id", spec.RunID), zap.Error(flushErr))
|
||||
if execErr == nil {
|
||||
return flushErr
|
||||
}
|
||||
}
|
||||
return execErr
|
||||
}
|
||||
|
||||
|
|
@ -691,6 +706,48 @@ type noopSender struct{}
|
|||
|
||||
func (noopSender) Send(proto.Message) error { return nil }
|
||||
|
||||
// terminalDeferringSink holds terminal events until Node-local admission has
|
||||
// released its slot. Edge uses terminal run events to advance queued work, so
|
||||
// emitting them before the local slot is free can over-dispatch back into Node.
|
||||
type terminalDeferringSink struct {
|
||||
inner runtime.EventSink
|
||||
|
||||
mu sync.Mutex
|
||||
deferring bool
|
||||
deferred []runtime.RuntimeEvent
|
||||
}
|
||||
|
||||
func (s *terminalDeferringSink) Emit(ctx context.Context, event runtime.RuntimeEvent) error {
|
||||
s.mu.Lock()
|
||||
if s.deferring || isTerminalRuntimeEvent(event.Type) {
|
||||
s.deferring = true
|
||||
s.deferred = append(s.deferred, event)
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return s.inner.Emit(ctx, event)
|
||||
}
|
||||
|
||||
func (s *terminalDeferringSink) Flush(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
events := append([]runtime.RuntimeEvent(nil), s.deferred...)
|
||||
s.deferred = nil
|
||||
s.deferring = false
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, event := range events {
|
||||
if err := s.inner.Emit(ctx, event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isTerminalRuntimeEvent(t runtime.EventType) bool {
|
||||
return t == runtime.EventTypeComplete || t == runtime.EventTypeError || t == runtime.EventTypeCancelled
|
||||
}
|
||||
|
||||
// sessionSink wraps a transport.Session to implement runtime.EventSink.
|
||||
type sessionSink struct {
|
||||
sess protoSender
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@ import (
|
|||
|
||||
// mockSender is a minimal mock for protoSender.
|
||||
type mockSender struct {
|
||||
sent *iop.RunEvent
|
||||
sent *iop.RunEvent
|
||||
sentEvents []*iop.RunEvent
|
||||
}
|
||||
|
||||
func (m *mockSender) Send(msg proto.Message) error {
|
||||
if re, ok := msg.(*iop.RunEvent); ok {
|
||||
m.sent = re
|
||||
m.sentEvents = append(m.sentEvents, re)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -124,3 +126,65 @@ func TestSessionSinkPrintEventStreamsDeltaImmediately(t *testing.T) {
|
|||
t.Fatalf("expected node-message prefix once, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalDeferringSinkFlushesTerminalEvents(t *testing.T) {
|
||||
ms := &mockSender{}
|
||||
var out bytes.Buffer
|
||||
inner := &sessionSink{
|
||||
sess: ms,
|
||||
out: &out,
|
||||
nodeID: "test-node-123",
|
||||
sessionID: "session-abc",
|
||||
}
|
||||
sink := &terminalDeferringSink{inner: inner}
|
||||
|
||||
if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: "run-terminal",
|
||||
Type: runtime.EventTypeStart,
|
||||
Timestamp: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatalf("Emit start failed: %v", err)
|
||||
}
|
||||
if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: "run-terminal",
|
||||
Type: runtime.EventTypeComplete,
|
||||
Message: "done",
|
||||
Timestamp: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatalf("Emit complete failed: %v", err)
|
||||
}
|
||||
if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: "run-terminal",
|
||||
Type: runtime.EventTypeDelta,
|
||||
Delta: "late",
|
||||
Timestamp: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatalf("Emit late delta failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ms.sentEvents) != 1 {
|
||||
t.Fatalf("expected only start to be sent before flush, got %d events", len(ms.sentEvents))
|
||||
}
|
||||
if got := ms.sentEvents[0].GetType(); got != string(runtime.EventTypeStart) {
|
||||
t.Fatalf("first event type = %q, want start", got)
|
||||
}
|
||||
if strings.Contains(out.String(), "complete run_id=run-terminal") {
|
||||
t.Fatalf("complete was printed before flush: %q", out.String())
|
||||
}
|
||||
|
||||
if err := sink.Flush(context.Background()); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
if len(ms.sentEvents) != 3 {
|
||||
t.Fatalf("expected start, complete, late delta after flush, got %d events", len(ms.sentEvents))
|
||||
}
|
||||
if got := ms.sentEvents[1].GetType(); got != string(runtime.EventTypeComplete) {
|
||||
t.Fatalf("second event type = %q, want complete", got)
|
||||
}
|
||||
if got := ms.sentEvents[2].GetType(); got != string(runtime.EventTypeDelta) {
|
||||
t.Fatalf("third event type = %q, want delta", got)
|
||||
}
|
||||
if !strings.Contains(out.String(), "[node-event] complete run_id=run-terminal detail=\"done\"") {
|
||||
t.Fatalf("complete was not printed after flush: %q", out.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ a2a:
|
|||
openai:
|
||||
enabled: false
|
||||
listen: "0.0.0.0:18081"
|
||||
bearer_token: ""
|
||||
node: ""
|
||||
adapter: "ollama"
|
||||
target: ""
|
||||
|
|
|
|||
|
|
@ -125,6 +125,8 @@ GOOS=windows GOARCH=amd64 go build -trimpath -o build/dev-runtime/bin/iop-node-w
|
|||
- Edge OpenAI-compatible base URL: `http://toki-labs.com:18083/v1`
|
||||
- Edge-Node TCP transport: `toki-labs.com:18084`
|
||||
|
||||
외부로 노출되는 OpenAI-compatible API는 `openai.bearer_token`으로 보호한다. tracked 문서에는 실제 token 원문을 기록하지 않고, Cline 같은 외부 client에는 운영자가 전달한 API key를 `Authorization: Bearer ...` 형태로 설정한다.
|
||||
|
||||
기준 provider pool:
|
||||
|
||||
- model alias: `qwen3.6:35b`
|
||||
|
|
@ -134,6 +136,14 @@ GOOS=windows GOARCH=amd64 go build -trimpath -o build/dev-runtime/bin/iop-node-w
|
|||
|
||||
OneXPlayer Lemonade Node는 원격 runner나 Edge host에서 다시 SSH하거나 proxy process로 띄우지 않는다. 현재 작업 호스트에서 Windows host에 `ssh r0bin@192.168.0.59`로 직접 접속한 뒤, 그 host 안에서 generated PowerShell bootstrap 명령을 실행한다. 이때 Node 작업 경로는 `$HOME\iop-field`이며, Lemonade provider endpoint는 Windows host 로컬에서 접근 가능한 값을 기준으로 검증한다.
|
||||
|
||||
OneXPlayer Lemonade는 `Qwen3.6-35B-A3B-MTP-GGUF` artifact를 사용하되 runtime MTP speculative decoding은 끈다. dev long-context 기준 load 설정은 Vulkan backend, `ctx_size=786432`, `llamacpp_args="--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024"`로 고정한다. llama.cpp server 기준 `ctx_size`는 전체 KV/context 예산이고 `-np`는 server slot 수이므로, 이 설정은 slot 3개가 각각 `n_ctx=262144`인 구조를 만든다. 2026-06-24 dev throughput 측정의 약 `95.9 tok/s` 관측은 `ctx_size=4096`, `-np 4` 기준이므로 이 long-context 설정과 직접 비교하지 않는다. 재시작 뒤에도 유지되도록 `/v1/load`는 `save_options=true`로 적용한다.
|
||||
|
||||
```bash
|
||||
curl -fsS http://192.168.0.59:13305/v1/load \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"model_name":"Qwen3.6-35B-A3B-MTP-GGUF","ctx_size":786432,"llamacpp_backend":"vulkan","llamacpp_args":"--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024","save_options":true}'
|
||||
```
|
||||
|
||||
SSH 세션 안의 `Start-Process`는 세션 종료와 함께 `iop-node.exe`가 정리될 수 있다. 반복 배포에서는 Windows host에서 `Win32_Process.Create` 방식으로 세션 독립 실행한다.
|
||||
|
||||
```powershell
|
||||
|
|
@ -161,7 +171,7 @@ Node host OS 재부팅은 필요하지 않다. Edge process 재시작이나 일
|
|||
2. Edge host에서 `toki-labs.com:18084`의 established node TCP connection 수를 확인한다.
|
||||
3. Edge process를 재시작하고 bootstrap 재실행 없이 3개 Node 연결이 회복되는지 확인한다.
|
||||
4. `http://toki-labs.com:18083/healthz`와 `/v1/models`를 확인한다.
|
||||
5. capacity를 `gx10-vllm=4`, `onexplayer-lemonade=3`로 맞춘 후보 config를 refresh apply하고 Edge process가 유지되는지 확인한다.
|
||||
5. capacity를 `gx10-vllm=4`, `onexplayer-lemonade=3`로 맞춘 후보 config를 refresh apply하고 Edge process가 유지되는지 확인한다. Node adapter capacity 변경이 `restart_required`로 반환되면 Edge process를 새 config로 재시작한 뒤 Node reconnect 상태를 확인한다.
|
||||
6. `qwen3.6:35b`로 `/v1/responses`와 `/v1/chat/completions` 각각에 provider capacity 총합 + 1개 동시 요청을 보낸다.
|
||||
7. capacity `gx10-vllm=4`, `onexplayer-lemonade=3` 기준이면 endpoint별 8개 동시 요청에서 Control Plane status의 provider snapshot이 총 `in_flight=7`, `queued>=1`을 한 번 이상 보여야 한다.
|
||||
8. 요청 완료 후 provider snapshot이 `in_flight=0`, `queued=0`으로 회복되는지 확인한다.
|
||||
|
|
|
|||
|
|
@ -81,21 +81,21 @@ type EdgeInfo struct {
|
|||
|
||||
// NodeDefinition is the edge-side record for a pre-registered node.
|
||||
type NodeDefinition struct {
|
||||
ID string `mapstructure:"id" yaml:"id"` // stable node identity; if empty, a UUID v4 is auto-assigned (dev fallback only)
|
||||
Alias string `mapstructure:"alias" yaml:"alias"`
|
||||
Token string `mapstructure:"token" yaml:"token"`
|
||||
AgentKind string `mapstructure:"agent_kind" yaml:"agent_kind"` // generic-node (default)
|
||||
Adapters AdaptersConf `mapstructure:"adapters" yaml:"adapters"`
|
||||
ID string `mapstructure:"id" yaml:"id"` // stable node identity; if empty, a UUID v4 is auto-assigned (dev fallback only)
|
||||
Alias string `mapstructure:"alias" yaml:"alias"`
|
||||
Token string `mapstructure:"token" yaml:"token"`
|
||||
AgentKind string `mapstructure:"agent_kind" yaml:"agent_kind"` // generic-node (default)
|
||||
Adapters AdaptersConf `mapstructure:"adapters" yaml:"adapters"`
|
||||
Providers []NodeProviderConf `mapstructure:"providers" yaml:"providers,omitempty"`
|
||||
Runtime RuntimeConf `mapstructure:"runtime" yaml:"runtime"`
|
||||
Runtime RuntimeConf `mapstructure:"runtime" yaml:"runtime"`
|
||||
}
|
||||
|
||||
// Category represents the provider category.
|
||||
type Category string
|
||||
|
||||
const (
|
||||
CategoryAPI Category = "api"
|
||||
CategoryCLI Category = "cli"
|
||||
CategoryAPI Category = "api"
|
||||
CategoryCLI Category = "cli"
|
||||
CategoryLocalInference Category = "local_inference"
|
||||
)
|
||||
|
||||
|
|
@ -266,6 +266,7 @@ func (e ModelCatalogEntry) Validate(resolvedProviderIDs map[string]struct{}, ser
|
|||
type EdgeOpenAIConf struct {
|
||||
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
|
||||
Listen string `mapstructure:"listen" yaml:"listen"`
|
||||
BearerToken string `mapstructure:"bearer_token" yaml:"bearer_token"`
|
||||
NodeRef string `mapstructure:"node" yaml:"node"`
|
||||
Adapter string `mapstructure:"adapter" yaml:"adapter"`
|
||||
Target string `mapstructure:"target" yaml:"target"`
|
||||
|
|
@ -771,6 +772,7 @@ func setEdgeDefaults(v *viper.Viper) {
|
|||
v.SetDefault("bootstrap.artifact_dir", "artifacts")
|
||||
v.SetDefault("openai.enabled", false)
|
||||
v.SetDefault("openai.listen", "0.0.0.0:18081")
|
||||
v.SetDefault("openai.bearer_token", "")
|
||||
v.SetDefault("openai.adapter", "ollama")
|
||||
v.SetDefault("openai.session_id", "openai")
|
||||
v.SetDefault("openai.timeout_sec", 120)
|
||||
|
|
|
|||
|
|
@ -119,6 +119,9 @@ func TestLoadEdge_OpenAIDefaults(t *testing.T) {
|
|||
if cfg.OpenAI.Listen != "0.0.0.0:18081" {
|
||||
t.Fatalf("expected openai.listen default, got %q", cfg.OpenAI.Listen)
|
||||
}
|
||||
if cfg.OpenAI.BearerToken != "" {
|
||||
t.Fatalf("expected openai.bearer_token empty by default, got %q", cfg.OpenAI.BearerToken)
|
||||
}
|
||||
if cfg.OpenAI.Adapter != "ollama" {
|
||||
t.Fatalf("expected openai.adapter=%q, got %q", "ollama", cfg.OpenAI.Adapter)
|
||||
}
|
||||
|
|
@ -148,6 +151,7 @@ server:
|
|||
openai:
|
||||
enabled: true
|
||||
listen: "127.0.0.1:8088"
|
||||
bearer_token: "secret-token"
|
||||
node: "node0"
|
||||
adapter: "ollama"
|
||||
target: "llama-test"
|
||||
|
|
@ -172,6 +176,9 @@ openai:
|
|||
if cfg.OpenAI.Listen != "127.0.0.1:8088" || cfg.OpenAI.NodeRef != "node0" {
|
||||
t.Fatalf("unexpected openai routing config: %+v", cfg.OpenAI)
|
||||
}
|
||||
if cfg.OpenAI.BearerToken != "secret-token" {
|
||||
t.Fatalf("expected openai.bearer_token=%q, got %q", "secret-token", cfg.OpenAI.BearerToken)
|
||||
}
|
||||
if cfg.OpenAI.Target != "llama-test" || len(cfg.OpenAI.Models) != 1 || cfg.OpenAI.Models[0] != "llama-test" {
|
||||
t.Fatalf("unexpected openai model config: %+v", cfg.OpenAI)
|
||||
}
|
||||
|
|
@ -2267,10 +2274,10 @@ func TestNodeProviderConf_Validate(t *testing.T) {
|
|||
|
||||
// Negative max_queue
|
||||
p7 := config.NodeProviderConf{
|
||||
ID: "p1",
|
||||
Type: "vllm",
|
||||
Category: config.CategoryAPI,
|
||||
MaxQueue: -1,
|
||||
ID: "p1",
|
||||
Type: "vllm",
|
||||
Category: config.CategoryAPI,
|
||||
MaxQueue: -1,
|
||||
}
|
||||
if err := p7.Validate(); err == nil {
|
||||
t.Fatal("expected error for negative max_queue")
|
||||
|
|
@ -2278,9 +2285,9 @@ func TestNodeProviderConf_Validate(t *testing.T) {
|
|||
|
||||
// Negative queue_timeout_ms
|
||||
p8 := config.NodeProviderConf{
|
||||
ID: "p1",
|
||||
Type: "vllm",
|
||||
Category: config.CategoryAPI,
|
||||
ID: "p1",
|
||||
Type: "vllm",
|
||||
Category: config.CategoryAPI,
|
||||
QueueTimeoutMS: -1,
|
||||
}
|
||||
if err := p8.Validate(); err == nil {
|
||||
|
|
|
|||
Loading…
Reference in a new issue