diff --git a/adapters/models/openai_compatible_local.go b/adapters/models/openai_compatible_local.go index 501e3d6..63d391c 100644 --- a/adapters/models/openai_compatible_local.go +++ b/adapters/models/openai_compatible_local.go @@ -96,11 +96,15 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover } type compatibleChatRequest struct { - Model string `json:"model"` - Messages []compatibleMessage `json:"messages"` - Temperature int `json:"temperature"` - Stream bool `json:"stream"` - MaxTokens int `json:"max_tokens"` + Model string `json:"model"` + Messages []compatibleMessage `json:"messages"` + Temperature int `json:"temperature"` + Stream bool `json:"stream"` + MaxTokens int `json:"max_tokens"` + ChatTemplateKwargs compatibleTemplateKwargs `json:"chat_template_kwargs"` +} +type compatibleTemplateKwargs struct { + EnableThinking bool `json:"enable_thinking"` } type compatibleMessage struct { Role string `json:"role"` @@ -121,7 +125,7 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" || len(request.Context) != 0 { return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory } - payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4} + payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}} var result compatibleChatResponse status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result) if err != nil { diff --git a/adapters/models/openai_compatible_local_test.go b/adapters/models/openai_compatible_local_test.go index 6ea7b06..5f3f1f5 100644 --- a/adapters/models/openai_compatible_local_test.go +++ b/adapters/models/openai_compatible_local_test.go @@ -2,15 +2,20 @@ package models import ( "context" + "encoding/json" "io" "net/http" "strings" "testing" "time" + + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/cognitive" ) type captureRoundTripper struct { authorization string + body []byte } func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) { @@ -31,7 +36,14 @@ func TestOpenAICompatibleLocalAdapterRejectsNonPositiveTimeout(t *testing.T) { func (t *captureRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { t.authorization = request.Header.Get("Authorization") - return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"data":[]}`)), Header: make(http.Header), Request: request}, nil + if request.Body != nil { + t.body, _ = io.ReadAll(request.Body) + } + body := `{"data":[]}` + if strings.HasSuffix(request.URL.Path, "/chat/completions") { + body = `{"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"ok"}}],"usage":{"prompt_tokens":1,"completion_tokens":1}}` + } + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header), Request: request}, nil } func TestOpenAICompatibleLocalAdapterUsesResolvedAPIKey(t *testing.T) { @@ -57,3 +69,26 @@ func TestOpenAICompatibleLocalAdapterDoesNotSendAuthorizationWithoutKey(t *testi t.Fatalf("unexpected authorization header") } } + +func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(t *testing.T) { + transport := &captureRoundTripper{} + adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport) + if err != nil { + t.Fatal(err) + } + _, err = adapter.Advise(context.Background(), cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor", PrincipalID: "principal", ScopeID: "scope", Message: "help", Locale: "ru-RU", MaxOutputBytes: 256, ProviderBindingID: "binding-1", ProviderBindingRevision: 1, ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1", ProviderModelID: capability.ProviderModelID("medium-model")}) + if err != nil { + t.Fatal(err) + } + var payload struct { + ChatTemplateKwargs struct { + EnableThinking bool `json:"enable_thinking"` + } `json:"chat_template_kwargs"` + } + if err := json.Unmarshal(transport.body, &payload); err != nil { + t.Fatal(err) + } + if payload.ChatTemplateKwargs.EnableThinking { + t.Fatal("expected bounded advisory to disable template thinking") + } +} diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index bad0ae4..d61e6b2 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000198-development-gate-live-evidence.json", - "task_id": "DEV-000198", + "task_file": "dev/tasks/DEV-000199-medium-advisory-thinking-control.json", + "task_id": "DEV-000199", "task_revision": 1 } diff --git a/dev/tasks/DEV-000199-medium-advisory-thinking-control.json b/dev/tasks/DEV-000199-medium-advisory-thinking-control.json new file mode 100644 index 0000000..571973a --- /dev/null +++ b/dev/tasks/DEV-000199-medium-advisory-thinking-control.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000199", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "c7170ee751b715b0cf6721d8491530c185870b2a", + "risk_tier": "E", + "intent": "Disable provider template reasoning for bounded OpenAI-compatible local advisory requests so a loaded MEDIUM model can return bounded advisory content instead of consuming the entire output cap with hidden reasoning.", + "requirement_ids": ["WP-200", "model fleet/failover", "bounded advisory output"], + "allowed_change_scope": {"modules": ["models adapter"], "paths": ["adapters/models/openai_compatible_local.go", "adapters/models/openai_compatible_local_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000199-medium-advisory-thinking-control.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "cmd/**", "modules/**", "contracts/**", ".gitea/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["existing OpenAI-compatible local advisory contract"], "may_change": ["provider request serialization adds typed template control"]}, + "semantic_owner": "cognitive adapter", + "canonical_state": [], + "state_transitions": ["bounded advisory request asks compatible runtimes to disable hidden template reasoning"], + "side_effects_and_privileges": ["bounded local provider inference only; no activation or promotion"], + "persistence_and_migrations": ["none"], + "failure_retry_restart": ["provider ignoring the optional request field remains an explicit qualification result"], + "assumptions": ["llama.cpp-compatible OpenAI endpoints ignore or honor the typed chat_template_kwargs field without breaking the existing contract"], + "acceptance_scenarios": ["request contains enable_thinking=false", "adapter still validates bounded completion", "provider failure remains typed and failover-compatible"], + "required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent -task dev/tasks/DEV-000199-medium-advisory-thinking-control.json", "git diff --check", "CI", "production medium bounded smoke and public health check"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"] +}