@@ -142,6 +142,100 @@ const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds
142142const FORCED_CONTEXT_REDUCTION_PERCENT = 75 // Keep 75% of context (remove 25%) on context window errors
143143const MAX_CONTEXT_WINDOW_RETRIES = 3 // Maximum retries for context window errors
144144
145+ // ── Usage Stats: endpoint domain extraction ──────────────────────────────────
146+
147+ /**
148+ * Default base URLs per provider. Only providers with a user-configurable
149+ * base URL field are listed. When the user's configured URL matches the
150+ * default, `endpoint` is left undefined to keep events clean.
151+ */
152+ const PROVIDER_DEFAULT_BASE_URLS : Partial < Record < string , string > > = {
153+ openai : "https://api.openai.com/v1" ,
154+ "openai-native" : "https://api.openai.com" ,
155+ openrouter : "https://openrouter.ai/api/v1" ,
156+ deepseek : "https://api.deepseek.com" ,
157+ litellm : "http://localhost:4000" ,
158+ ollama : "http://127.0.0.1:11434" ,
159+ lmstudio : "http://localhost:1234/v1" ,
160+ requesty : "https://router.requesty.ai/v1" ,
161+ }
162+
163+ /**
164+ * Maps a provider name to the corresponding base URL field on ProviderSettings.
165+ * Returns the raw configured value (may be undefined if the user hasn't
166+ * customized it). Providers not in this map have no user-configurable base URL.
167+ */
168+ function getProviderBaseUrlField ( provider : string , config : ProviderSettings ) : string | undefined {
169+ switch ( provider ) {
170+ case "anthropic" :
171+ return config . anthropicBaseUrl
172+ case "openai" :
173+ return config . openAiBaseUrl
174+ case "openai-native" :
175+ return config . openAiNativeBaseUrl
176+ case "openrouter" :
177+ return config . openRouterBaseUrl
178+ case "deepseek" :
179+ return config . deepSeekBaseUrl
180+ case "litellm" :
181+ return config . litellmBaseUrl
182+ case "ollama" :
183+ return config . ollamaBaseUrl
184+ case "lmstudio" :
185+ return config . lmStudioBaseUrl
186+ case "requesty" :
187+ return config . requestyBaseUrl
188+ case "zoo-gateway" :
189+ return config . zooGatewayBaseUrl
190+ default :
191+ return undefined
192+ }
193+ }
194+
195+ /**
196+ * Extracts a display-friendly endpoint domain from the provider's base URL.
197+ *
198+ * Only returns a value when the user has configured a CUSTOM base URL that
199+ * differs from the provider's default. For localhost / 127.0.0.1 hosts the
200+ * port is included (e.g. "localhost:1234") so distinct local servers can be
201+ * distinguished. Returns undefined for default endpoints, providers without
202+ * a base URL field, or malformed URLs.
203+ */
204+ function resolveEndpoint ( config : ProviderSettings ) : string | undefined {
205+ const provider = config . apiProvider
206+ if ( ! provider ) return undefined
207+
208+ const configuredUrl = getProviderBaseUrlField ( provider , config )
209+ if ( ! configuredUrl ) return undefined
210+
211+ // Only record endpoint when the user customized the base URL.
212+ const defaultUrl = PROVIDER_DEFAULT_BASE_URLS [ provider ]
213+ if ( configuredUrl === defaultUrl ) return undefined
214+
215+ // zoo-gateway default is dynamic — skip when it matches the derived default.
216+ if ( provider === "zoo-gateway" ) {
217+ // The dynamic default is `${getZooCodeBaseUrl()}/api/gateway/v1`.
218+ // We can't import getZooCodeBaseUrl here without a circular dependency,
219+ // so we compare against the known suffix pattern. If the configured URL
220+ // ends with /api/gateway/v1 and starts with a zoocode host, treat as default.
221+ if ( / ^ h t t p s ? : \/ \/ [ ^ / ] * z o o c o d e \. d e v \/ a p i \/ g a t e w a y \/ v 1 \/ ? $ / . test ( configuredUrl ) ) {
222+ return undefined
223+ }
224+ }
225+
226+ try {
227+ const url = new URL ( configuredUrl )
228+ const hostname = url . hostname
229+ // Include port for localhost / 127.0.0.1 so distinct local servers differ.
230+ if ( ( hostname === "localhost" || hostname === "127.0.0.1" ) && url . port ) {
231+ return `${ hostname } :${ url . port } `
232+ }
233+ return hostname
234+ } catch {
235+ return undefined
236+ }
237+ }
238+
145239export interface TaskOptions extends CreateTaskOptions {
146240 provider : ClineProvider
147241 apiConfiguration : ProviderSettings
@@ -3168,6 +3262,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
31683262 reasoningInOutput : "unknown" ,
31693263 costSource : "provider" ,
31703264 tokenSource : "provider" ,
3265+ endpoint : resolveEndpoint ( this . apiConfiguration ) ,
31713266 }
31723267 // Fire-and-forget: store error must not block task
31733268 this . usageRecorder
@@ -3314,6 +3409,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
33143409 reasoningInOutput : "unknown" ,
33153410 costSource : "provider" ,
33163411 tokenSource : "provider" ,
3412+ endpoint : resolveEndpoint ( this . apiConfiguration ) ,
33173413 }
33183414 // Fire-and-forget: store error must not block task
33193415 this . usageRecorder
0 commit comments