@@ -659,6 +659,13 @@ def do_POST(self) -> None: # noqa: N802
659659 elif self .path == "/v1/chat/completions" :
660660 # Kairos BYOE gateway — intercept, gate, forward.
661661 try :
662+ # Detect role from request header or infer from system prompt.
663+ req_role = self .headers .get ("X-Specsmith-Role" , "" )
664+ if not req_role :
665+ req_role = _infer_role_from_messages (
666+ body .get ("messages" ) or []
667+ )
668+
662669 result = run_chat_proxy (
663670 messages = body .get ("messages" ) or [],
664671 model = body .get ("model" , "kairos" ),
@@ -667,10 +674,15 @@ def do_POST(self) -> None: # noqa: N802
667674 import json as _j
668675
669676 raw = _j .dumps (result , ensure_ascii = False ).encode ()
677+ effective_model = result .get ("model" , body .get ("model" , "" ))
678+ effective_provider = _resolve_provider_name ()
670679 self .send_response (200 )
671680 self .send_header ("Content-Type" , "application/json" )
672681 self .send_header ("Content-Length" , str (len (raw )))
673682 self .send_header ("x-kairos-governance" , "gated" )
683+ self .send_header ("X-Specsmith-Role" , req_role or "coder" )
684+ self .send_header ("X-Specsmith-Model" , effective_model )
685+ self .send_header ("X-Specsmith-Provider" , effective_provider )
674686 self .send_header ("Access-Control-Allow-Origin" , "*" )
675687 self .end_headers ()
676688 self .wfile .write (raw )
@@ -714,6 +726,54 @@ def do_OPTIONS(self) -> None: # noqa: N802
714726# ---------------------------------------------------------------------------
715727
716728
729+ # Role keywords used by _infer_role_from_messages to detect intent from system prompts.
730+ _ROLE_KEYWORDS : dict [str , list [str ]] = {
731+ "coder" : ["write code" , "implement" , "code" , "function" , "diff" ],
732+ "architect" : ["design" , "architecture" , "system" , "trade-off" ],
733+ "reviewer" : ["review" , "feedback" , "quality" , "pr" ],
734+ "editor" : ["edit" , "format" , "refactor" , "fix" ],
735+ "researcher" : ["research" , "documentation" , "lookup" , "search" ],
736+ "tester" : ["test" , "coverage" , "assertion" , "spec" ],
737+ "classifier" : ["classify" , "categorize" , "intent" ],
738+ "strategist" : ["strategy" , "business" , "competitive" , "market" ],
739+ "drafter" : ["draft" , "specification" , "proposal" , "report" ],
740+ "ip-analyst" : ["patent" , "claims" , "prior art" , "ip" , "freedom" ],
741+ }
742+
743+
744+ def _infer_role_from_messages (messages : list [dict [str , Any ]]) -> str :
745+ """Best-effort role inference from system prompt keywords."""
746+ system_text = ""
747+ for msg in messages :
748+ if isinstance (msg , dict ) and msg .get ("role" ) == "system" :
749+ content = msg .get ("content" , "" )
750+ system_text += (content if isinstance (content , str ) else str (content )).lower ()
751+ if not system_text :
752+ return "coder"
753+ best_role = "coder"
754+ best_count = 0
755+ for role , keywords in _ROLE_KEYWORDS .items ():
756+ count = sum (1 for kw in keywords if kw in system_text )
757+ if count > best_count :
758+ best_count = count
759+ best_role = role
760+ return best_role
761+
762+
763+ def _resolve_provider_name () -> str :
764+ """Return the configured AI provider name for attribution headers."""
765+ provider = os .environ .get ("KAIROS_AI_BASE_URL" , "" )
766+ if not provider :
767+ return "specsmith-local"
768+ if "openai" in provider :
769+ return "openai"
770+ if "anthropic" in provider :
771+ return "anthropic"
772+ if "localhost" in provider or "127.0.0.1" in provider :
773+ return "local"
774+ return "byoe"
775+
776+
717777def _read_confidence_threshold (root : Path ) -> float | None :
718778 cfg = root / ".specsmith" / "config.yml"
719779 if not cfg .is_file ():
0 commit comments