11"""Shared helpers for gatekeeper HTTP clients."""
22
3- import os
4-
53from typing import Any
6- from typing import Literal
7- from urllib .parse import urlparse
84
95import requests
106
11- from linux_mcp_server .config import CONFIG
12- from linux_mcp_server .config import ReasoningEffort
13-
147
158DEFAULT_TIMEOUT_SECONDS = 120
169
17- OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1"
18- OPENROUTER_DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
19-
20- ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages"
21- ANTHROPIC_API_VERSION = "2023-06-01"
22- ANTHROPIC_VERTEX_VERSION = "vertex-2023-10-16"
23- ANTHROPIC_DEFAULT_MAX_TOKENS = 4096
24-
25- GOOGLE_AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
26-
2710
2811class GatekeeperHTTPError (RuntimeError ):
2912 """Raised when an LLM provider returns an error response."""
@@ -50,136 +33,8 @@ def post_json(
5033 return response .json ()
5134
5235
53- def get_openai_base_url () -> str :
54- configured = CONFIG .gatekeeper .openai .base_url if CONFIG .gatekeeper .openai else None
55- return (configured or os .environ .get ("OPENAI_API_BASE" ) or OPENAI_DEFAULT_BASE_URL ).rstrip ("/" )
56-
57-
58- def prefers_openai_chat_completions (base_url : str ) -> bool :
59- """Hosts known to expose only Chat Completions, not the Responses API."""
60- path = urlparse (base_url ).path or ""
61- return "/endpoints/openapi" in path
62-
63-
6436def normalize_model_id (model : str ) -> str :
6537 for prefix in ("openai/" , "anthropic/" , "vertex_ai/" , "gemini/" ):
6638 if model .startswith (prefix ):
6739 return model [len (prefix ) :]
6840 return model
69-
70-
71- def normalize_openrouter_model_id (model : str ) -> str :
72- if model .startswith ("openrouter/" ):
73- return model [len ("openrouter/" ) :]
74- return model
75-
76-
77- def get_openrouter_base_url () -> str :
78- configured = CONFIG .gatekeeper .openrouter .base_url if CONFIG .gatekeeper .openrouter else None
79- return (configured or OPENROUTER_DEFAULT_BASE_URL ).rstrip ("/" )
80-
81-
82- def vertex_api_style (model : str ) -> Literal ["anthropic" , "gemini" , "openai_compatible" ]:
83- normalized = normalize_model_id (model )
84- if normalized .startswith ("claude" ):
85- return "anthropic"
86- if normalized .startswith ("gemini" ):
87- return "gemini"
88- return "openai_compatible"
89-
90-
91- def get_vertex_openapi_base_url () -> str :
92- cfg = CONFIG .gatekeeper .vertex_ai
93- if cfg and cfg .base_url :
94- return cfg .base_url .rstrip ("/" )
95- from linux_mcp_server .gatekeeper .gcp_auth import get_gcp_location
96- from linux_mcp_server .gatekeeper .gcp_auth import get_gcp_project
97-
98- project = get_gcp_project ()
99- location = get_gcp_location ()
100- return f"https://aiplatform.googleapis.com/v1/projects/{ project } /locations/{ location } /endpoints/openapi"
101-
102-
103- def vertex_auth_headers () -> dict [str , str ]:
104- from linux_mcp_server .gatekeeper .gcp_auth import get_gcp_access_token
105-
106- return {"Authorization" : f"Bearer { get_gcp_access_token ()} " }
107-
108-
109- def openai_reasoning_block (reasoning_effort : ReasoningEffort | None ) -> dict [str , Any ] | None :
110- if reasoning_effort is None or reasoning_effort == ReasoningEffort .DEFAULT :
111- return None
112- return {"effort" : reasoning_effort .value }
113-
114-
115- def openrouter_reasoning_block (reasoning_effort : ReasoningEffort | None ) -> dict [str , Any ] | None :
116- if reasoning_effort is None or reasoning_effort == ReasoningEffort .DEFAULT :
117- return None
118- if reasoning_effort == ReasoningEffort .NONE :
119- return {"enabled" : False }
120- return {"enabled" : True , "effort" : reasoning_effort .value }
121-
122-
123- def anthropic_thinking_block (reasoning_effort : ReasoningEffort | None ) -> dict [str , Any ] | None :
124- if reasoning_effort is None or reasoning_effort in {ReasoningEffort .NONE , ReasoningEffort .DEFAULT }:
125- return None
126- budget_by_effort = {
127- ReasoningEffort .MINIMAL : 1024 ,
128- ReasoningEffort .LOW : 4096 ,
129- ReasoningEffort .MEDIUM : 8192 ,
130- ReasoningEffort .HIGH : 16384 ,
131- ReasoningEffort .XHIGH : 32768 ,
132- }
133- budget = budget_by_effort .get (reasoning_effort )
134- if budget is None :
135- return None
136- return {"type" : "enabled" , "budget_tokens" : budget }
137-
138-
139- def gemini_thinking_level (reasoning_effort : ReasoningEffort | None ) -> str | None :
140- if reasoning_effort is None or reasoning_effort in {ReasoningEffort .NONE , ReasoningEffort .DEFAULT }:
141- return None
142- mapping = {
143- ReasoningEffort .MINIMAL : "MINIMAL" ,
144- ReasoningEffort .LOW : "LOW" ,
145- ReasoningEffort .MEDIUM : "MEDIUM" ,
146- ReasoningEffort .HIGH : "HIGH" ,
147- ReasoningEffort .XHIGH : "HIGH" ,
148- }
149- return mapping .get (reasoning_effort )
150-
151-
152- def get_openai_api_key () -> str :
153- api_key = os .environ .get ("OPENAI_API_KEY" )
154- if not api_key :
155- raise ValueError ("OPENAI_API_KEY is required for OpenAI gatekeeper provider." )
156- return api_key
157-
158-
159- def get_openrouter_api_key () -> str :
160- api_key = os .environ .get ("OPENROUTER_API_KEY" )
161- if not api_key :
162- raise ValueError ("OPENROUTER_API_KEY is required for OpenRouter gatekeeper provider." )
163- return api_key
164-
165-
166- def openrouter_auth_headers () -> dict [str , str ]:
167- return {"Authorization" : f"Bearer { get_openrouter_api_key ()} " }
168-
169-
170- def get_anthropic_api_key () -> str :
171- api_key = os .environ .get ("ANTHROPIC_API_KEY" )
172- if not api_key :
173- raise ValueError ("ANTHROPIC_API_KEY is required for Anthropic gatekeeper provider." )
174- return api_key
175-
176-
177- def get_google_api_key () -> str :
178- api_key = os .environ .get ("GOOGLE_API_KEY" ) or os .environ .get ("GEMINI_API_KEY" )
179- if not api_key :
180- raise ValueError ("GOOGLE_API_KEY or GEMINI_API_KEY is required for Gemini direct backend." )
181- return api_key
182-
183-
184- def openai_auth_headers () -> dict [str , str ]:
185- return {"Authorization" : f"Bearer { get_openai_api_key ()} " }
0 commit comments